最近發現咱們產品在打開廣告連接(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 數據傳輸等各個階段的耗時。由此看來,網頁加載慢的罪魁禍首並不是廣告主網頁的質量問題,而是網絡的不穩定問題。雖然最終也沒有獲得有效的解決方案,但至少明確了問題的發生緣由,給出了使人信服的解釋。