最近发现咱们产品在打开广告连接(Webview)时有必定几率会很是慢,白屏时间超过 10s,追查广告的过程当中遇到很多有意思的事情,感受很有收获。在这里分享一下,主要想聊一聊追查 bug 时的那些方法论,固然也不能太虚,仍是要带一点干货,好比 WireShark 的使用。程序员
遇到 bug 后的第一件事固然是复现。通过一番测试我发现 bug 几乎只会主要出如今 iPhone6 这种老旧机型上,而笔者的 7Plus 则基本没有问题。4G 和 Wifi 下都有必定几率出现,Wifi 彷佛更加频繁。web
其实有点经验的开发者看到这里内心应该有点谱了,这应该不是客户端的 bug,更多是因为广告主网页质量过低或者网络环境不稳定致使。但做为一个靠谱的程序员,怎么能把这种毫无根据的猜想向上级汇报呢?算法
咱们知道加载网页能够由两部分时间组成,一个是本地的处理时间,另外一个是网络加载的时间。二者的分水岭应该在 UIWebview
的 shouldStartLoadWithRequest
方法上。这个方法调用以前是本地处理耗时,调用以后是网络加载的请求。因此咱们能够把事情分红两部分来看:shell
didSelectedRowAtIndexPath
起到 UIWebview
的 shouldStartLoadWithRequest
为止。shouldStartLoadWithRequest
起到 UIWebview
的 webViewDidFinishLoad
为止。 因为 Bug 是偶现,因此不可能长时间用 Xcode 调试,因此还要注意写一个简单的工具,将每次的 Log 日志持久化存下来,保留每一步的函数调用、耗时、具体参数等。这样一旦复现出来,能够连上电脑读取手机中的日志。服务器
本地处理的耗时相对较短,但逻辑一点都不简单。在我我的看来,从展现 UITableview 处处理点击事件的流程,足以反映出一个团队的技术实力。绝不夸张的说,能把这个小业务作到完美的团队寥寥无几,其中必然涉及到 MVC/MVVM 等架构的选型设计与具体实现、网络层与持久化层的封装、项目模块化的拆分等核心知识点。我会尽快抽空专门一些篇文章来聊聊这些,这里就再也不赘述。网络
花了一番功夫整理好业务流程、作好统计之后还真有一些收获。客户端的逻辑是 pushViewController
动画执行完后才发送请求,白白浪费了大约 0.5s 的动画时间,这些时间本来能够用来加载网页。架构
借助日志我还发现,本地处理虽然浪费了时间,但这个时间相对稳定,大约在 1s 左右。更大的耗时来自于网络请求部分。通常状况下,打开网页会有短暂的白屏时间,这段时间内系统会加载 HTML 等资源并进行渲染,同时界面上有菊花在转动。并发
白屏何时消失取决于系统何时加载完网页,咱们没法控制。但菊花消失的时间是已知的,咱们的逻辑是写在 webViewDidFinishLoad
中。这么作不必定准确,由于网页重定向时也会调用 webViewDidFinishLoad
方法致使客户端误觉得已经加载完成。更加准确的作法能够参考: 如何准确判断 WebView 加载完成,固然这也也仅仅是更准确一些,就 UIWebview 而言,想准确的判断网络是否加载完成几乎是不可能的(感谢 @JackAlan 的实践)。tcp
因此说网络加载还能够细分为两部分,一个是纯白屏时间,另外一部分则是出现了网页但还在转动菊花的时间。这是由于一个 Frame(能够是 HTML 也能够是 iFrame) 所有加载完成(包括 CSS/JS 等)后才会调用 webViewDidFinishLoad
方法,因此存在网页已经渲染但还在执行 JS 请求的状况,反映在用户端,就是能看到网页但菊花还在转动。这种状况若是持续时间太久会致使用户不耐烦,但相比于纯粹的白屏时间来讲更能被接受一些。模块化
同时咱们也能够肯定,若是网页已经加载,但 JS 请求还在继续,这就是广告主的网页质量太差致使的。损失应该由他们承担,咱们无能为力。而长时间的白屏则是咱们应该重点考虑的问题。
其实分析到这里已经能够向领导汇报了。网络加载的耗时一共是三段,第一段是本地处理时间,存在性能浪费但时间比较稳定,第二段是网页白屏时间,这段时间内系统的 UIWebView
在请求资源并渲染,第三段是加载网页后的菊花转动时间,通常耗时较少,咱们也没法控制。
咱们还知道 UIWebView
提供的 API 不多,从开始请求到网页加载结束彻底是黑盒模式,几乎无从下手。但做为一名有追求,有理想,有抱负,有技术的四有程序员,怎么能轻言放弃呢?
客户端在调试网络时最经常使用的工具要数 Charles,但它只能调试 HTTP/HTTPS 请求,对 TCP 层就无能为力了。要想了解 HTTP 请求过程当中的细节,咱们必需要使用威力更大(确定也更复杂)的武器,也就是本文的主角 WireShark。
通常来讲越牛X 的工具长得就越丑,WireShark 也绝不例外的有着一副让人懵逼的外表。
不过不用太急,咱们要用到的东西很少,顶部红框里的蓝色鲨鱼标志表示开始监听网络数据,红色按钮一看也能猜出来是中止录制。与 Charles 只监听 HTTP 请求不一样的是,WireShark 能够调试到 IP 层甚至更细节,因此它的数据包也更多,几秒钟的时间就会被上千个请求淹没,因此我建议用户略微控制一下监听的时长,或者咱们能够在第二个红框中输入过滤条件来减小干扰,这个下文会详细介绍。
WireShark 能够监听本机的网卡,也能够监听手机的网络。使用 WireShark 调试真机时不用链接代理,只须要经过 USB 链接到电脑就行,不然就没法调试 4G 网络了。咱们能够用 rvictl -s 设备 UDID
命令来建立一个虚拟的网卡:
rvictl -s 902a6a449af014086dxxxxxx346490aaa0a8739复制代码
固然,看手机 UDID 仍是挺麻烦的,做为一个懒人,怎么能不用命令行来完成呢?
instruments -s | awk '{print $NF}' | sed -n 3p | awk '{print substr($0,2,length($0)-2)}' | xargs rvictl -s复制代码
这样只要连上手机,就能够直接获取到 UDID 了。
运行命令后会看到成功建立 rvi0
虚拟网卡的提示,双击 rvi0
那一行便可。
咱们主要关注两个内容,上面的大红框里面是数据流,包含了 TCP、DNS、ICMP、HTTP 等协议,颜色花花绿绿,绚丽多彩。通常来讲黑色的内容表示遇到错误,须要重点关注,其余内容则辅助理解。反复调试几回之后也就能基本记住各类颜色对应的含义了。
下面的小红框里面主要是某一个包的数据详解,会根据不一样的协议层来划分,好比我选中的 99 号包时一个 TCP 包,能够很清楚的看到它的 IP 头部、TCP 头部和 TCP Payload。这些数据必要时能够作更详细的分析,但通常也不用关注。
通常来讲一次请求的数据包会很是大,可能会有上千个,如何找到本身感兴趣的请求呢,咱们可使用以前提到的过滤功能。WireShark 的过滤使用了一套本身定义的语法,不熟悉的话须要上网查一查或者借助自动补全功能来“望文生义”。
因为是要查看 HTTP 请求的具体细节,咱们先得找到请求的网址,而后利用 ping
命令获得它对应的 IP 地址。这种作法通常没问题,但也不排除有的域名会作一些优化,好比不一样的 IP 请求 DNS 解析时返回不一样的 IP 地址来保证最佳速度。也就是说手机上 DNS 解析的结果并不老是和电脑上的解析结果一致。这种状况下咱们能够经过查看 DNS 数据包来肯定。
好比从图中能够看到 res.wx.qq.com
这个域名解析出了一大堆 IP 地址,而真正使用的仅有前两个。
解析出地址后,咱们就能够作简单的过滤了,输入ip.addr == 220.194.203.68
:
这样就只显示和 220.194.203.68
主机之间的通讯了。注意红框中的 SourcePort,这是客户端端口。咱们知道 HTTP 支持并发请求,不一样的并发请求确定是占用不一样的端口。因此在图中看到的上下两个数据包,并不是必定是请求与响应的关系,他们可能属于两个不一样的端口,彼此之间毫无关系,只是刚好在时间上最接近而已。
若是只想显示某个端口的数据,可使用:ip.addr == 220.194.203.68 and tcp.dstport == 58854
。
若是只想看 HTTP 协议的 GET 请求与响应,可使用 ip.addr == 220.194.203.68 and (http.request.method == "GET" || http.response.code == 200)
来过滤。
若是想看丢包方面的数据,能够用 ip.addr == 220.194.203.68 and (tcp.analysis.fast_retransmission || tcp.analysis.retransmission)
以上是笔者在调试过程当中用到比较多的命令,仅供参考。有兴趣的读者能够自行抓包实验,就不挨个贴图了。
通过屡次抓包后我开始分析那些长时间白屏的网页对应的数据包,果真发现很多问题,好比这里:
能够很明显的看到在一大串黑色错误信息,但若是你去调试这些数据包,那么就掉进陷阱了。DNS 是基于 UDP 的协议,不会有 TCP 重传,因此这些黑色的数据包一定是以前的丢包重传,不用关心。若是只看蓝色的 DNS 请求,就会发现连续发送了几个请求但都没有响应,直到第 12s 才获得解析后的IP 地址。
从 DNS 请求的接收方的地址以 172.24
开头能够看出,这是内网 DNS 服务器,不知道为何卡了好久。
下图是一次典型的 TCP 握手时的场景。同时也能够看到第一张图中的 SYN 握手包发出后,过了一秒钟才接受到 ACK。固然了,缘由也不清楚,只能解释为网络抖动。
随后我又在 4G 网络下抓了一次包:
此次事情就更离谱了,第二秒发出的 SYN 握手包反复丢失(也有多是服务端没有响应、或者是 ACK 丢失),总之客户端不断重传 SYN 包。
更有意思的是,观察 TSval,它表示包发出时的时间戳。咱们观察这几个值会发现,前几回的间隔时间是 1s,后来变成了 2s,4s 和 8s。这不由让我想起了 RTO 的概念。
咱们知道 RTT 表示的是网络请求从发起到接收响应的时间,它是一个随着网络环境而动态改变的值。TCP 有窗口的概念,对于窗口的第一个数据包,若是它没法发送,窗口就不能向后滑动。客户端以接收到 ACK 做为数据包成功发送的标志,那么若是 ACK 收不到呢?客户端固然不会一直等下去,它会设置一个超时时间,一旦超过这个时间就认为数据包丢失,从而重传。
这个超时时间就被称为 RTO,显然它必须略大于 RTT,不然就会误报数据包丢失。但也不能过大,不然会浪费时间。所以合理的 RTO 必须跟随 RTT 动态调整,始终保证大于 RTT 但也不至于太大。观察上面的截图能够发现,某些状况下 RTT 会很是小,小到只有几毫秒。若是 RTO 也设置为几毫秒就会显得不太合理,这会加大客户端和沿途各路由器的压力。所以 RTO 还会设置下限,不一样的操做系统可能有不一样的实现,好比 Linux 上是 200ms。同时,RTO 也会设置上限,具体的算法能够参考这篇文章 和这篇文章。
须要注意的是,RTO 随着 RTT 动态变化,但若是达到了 RTO 致使了超时重传,之后的 RTO 就再也不随着 RTT 变化了(此时的 RTT 没法计算),会指数增加。也就是上面截图中的间隔时间从 2s 变成 4s 再变成 8s 的缘由。
一样的,咱们发现了握手花费了 20s 这一现象,但没法给出准确缘由,只能解释为网络抖动。
经过 TCP 层面的抓包,咱们不只仅学习了 WireShark 的使用,也复习了 TCP 协议的相关知识,对问题的分析也更加深刻。从最初的网络问题开始细化挖掘,得出了白屏时间过长、网页加载太慢的结论,最终又具体的计算出了有多少个 HTTP 请求,DNS 解析、TCP 握手、TCP 数据传输等各个阶段的耗时。由此看来,网页加载慢的罪魁祸首并不是广告主网页的质量问题,而是网络的不稳定问题。虽然最终也没有获得有效的解决方案,但至少明确了问题的发生缘由,给出了使人信服的解释。