OkHttp是如何監測連線洩漏的
1. 前言
最近在找工作,所以複習了很多常用的基礎知識,在看OkHttp原始碼的時候,有這麼一段程式碼引起了我的好奇心。如果你的團隊正在招人,可以內推我喲。通過微訊號bytestation或者掃以下二維碼加我微信。希望你:
-
備註來意,備明公司名稱
-
團隊有良好的技術氛圍
-
Android團隊人員最好超過10人
-
辦公地點是在北京

意思就是,如果忘記關閉ResponseBody,這將會導致Http連線洩漏。顯然這是,OkHttp庫內部做的洩漏監測機制。條件反射的冒出幾個疑問:
1.為什麼不關閉ResponseBody會導致Http連線洩漏?
2.OkHttp監測連線洩漏的原理是什麼?
第一反應就是想知道監測連線洩漏的原理。說到監測洩漏,自然而然地就想到了LeakCanary記憶體洩漏原理。它兩是一樣的原理嗎?
在通讀了一遍原始碼後,發現OkHttp和LeakCanary的洩漏監測機制大不相同,這引發了的好奇心,下定決心要把這個問題搞懂
2. 簡述LeakCanary監測洩漏
大家對LeakCanary是如何檢測記憶體洩漏的原理是信手拈來,這可能要歸功於其他Android同仁早已將它的原理分析地透透的了。俺也一樣,隨便search一下,就能找到很多精彩的文章。所以這裡就簡單概述下它的原理,主要是為了突出它和OkHttp的差異。
以監測Activity記憶體洩漏為例。LeakCanary的做法是,在Activity的onDestroy方法中,用WeakReference指向該Activity,然後在一段時間後,主動觸發一次gc,如果發現WeakReference.get() != null。那麼就可以判斷該Activity因為有其他強引用持有導致無法被回收掉,就認為該Activity發生了記憶體洩漏。
❝
注意,上述是LeakCanary監測的大概原理,它的實際實現比這更復雜些,簡而言之,最核心的就是如果GC後,弱引用還持有該物件,那就說明被監測的物件發生了洩漏。而OkHttp恰恰相反,它是發現弱引用持有的物件已經被回收了,才判斷髮生了連線洩漏。這究竟是怎麼回事呢?一起來看看吧
❞
3. OkHttp連線洩漏
OkHttp的連線洩漏監測是在RealConnectionPool的pruneAndGetAllocationCount的方法中發生的,我們可以看到reference.get()返回null才會被認為發生洩漏了。
那麼問題來了
-
為什麼是reference.get()返回null時發生連線洩漏?
-
呼叫了ResponseBody的close方法後,為什麼就不會洩漏呢?
先來探尋下第一個問題的答案,我們首先要找到conection.calls的定義。它定義在RealConnection.kt中
val calls = mutableListOf<Reference<RealCall>>()
以上看了個寂寞,Reference到底是個啥?是強引用?軟引用?還是弱引用呢?
//RealCall.kt 的acquireConnectionNoEvents方法中找到如下程式碼
connection.calls.add(CallReference(this, callStackTrace))
internal class CallReference(
referent: RealCall,
/**
* Captures the stack trace at the time the Call is executed or enqueued. This is helpful for
* identifying the origin of connection leaks.
*/
val callStackTrace: Any?
) : WeakReference<RealCall>(referent)
原來calls是CallReference的集合,而CallReference是一個弱引用。
而我們知道,RealCall是真正發起網路請求的物件,當它發起請求時,會獲取到一條Connection,並把Recall封裝成一個弱引用,儲存到Connection的calls列表中。
那這裡有個很重要的概念,calls如果是空的,那麼對應的Connection則認為是空閒的。清理執行緒會根據Connection是否空閒來清理多餘的空閒連線
我們知道RealCall在一個網路請求中應該是一次性用品,當網路請求返回後,它就應該處於可被GC的狀態。既然RealCall之後再也不會被使用,那麼它也應該從Connection的calls列表中移除掉才對,否則OkHttp將會錯誤的認為該Connection不是空閒狀態,導致連線無法被清理掉,就造成連線洩漏了。
所以,當RealCall的真身在網路請求成功,並讀取完資料後,Connection中calls儲存RealCall的弱引用也應該相應地被移除掉。否則就會出現GC後,reference.get()返回null的情況。而這種情況就表明,完成了網路請求的RealCall沒有正確的被移除掉,導致本應該空閒Connection無法被及時清理掉
第一個問題就解答完畢了。
第二個問題,為什麼呼叫了ResponseBody的close方法就不會發生洩漏呢?
原因是因為ResponseBody.close()方法最終會呼叫到RealCall.kt的releaseConnectNoEvents()方法,該方法會將RealCall對應的弱引用物件從Connection的calls列表中移除。這樣就不會造成連線洩漏。
好了原理就介紹到此。
4. 重現該bug
因為我沒有讀取Response裡面的資料,所以不會呼叫相關的close方法,列印結果如下
6月 21, 2022 3:24:14 下午 okhttp3.internal.platform.Platform log
警告: A connection to https://www.zhihu.com/ was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
6月 21, 2022 3:24:14 下午 okhttp3.internal.platform.Platform log
警告: A connection to https://www.zhihu.com/ was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
5. 學以致用
OkHttp連線洩漏,原理就是建立Closeable物件時,儲存它的弱引用到一個集合中,當close呼叫時,則將弱引用從集合中移除。後臺開啟一個清理執行緒,當清理執行緒發現弱引用 reference.get() 返回null時,則可以斷定Closeable物件沒有被正確close。基於這個原理,應該是可以開發一套監控Closeable是否正確關閉的系統。
- 值得擁有~
- 協程是如何實現執行緒切換的
- 協程官方引入currentCoroutineContext方法的原因大揭祕。
- OkHttp是如何監測連線洩漏的
- 聊聊Glide中的LRU BitmapPool
- Android啟動優化之精確測量啟動各個階段的耗時
- LinkedHashMap實現的LRU快取有什麼侷限性?業界有更好的實現方式嗎?
- 不容錯過的一款神器
- Android如何優雅地解決重複Drawable資源
- 一個減少冗餘Drawable資源的解決方案
- 讓這些優秀的Android開源專案大幅提升我們的工作效率
- APP有安全漏洞?這個工具測一下就知道了
- Framework保姆級學習路線分享,看完漲薪10k不是夢
- 揭祕kotlin協程中的CoroutineContext
- 還有哪些值得關注的Android公眾號?
- 協程異常處理機制竟然和事件分發機制一母同胞?
- 新年就搞新技術!Jetpack Compose 完全開發手冊,從入門到精通!
- 深入理解協程suspend工作原理(初學者也能看得懂)
- 你知道過去的一年有多少活躍的Kotlin開發者嗎?讓Jetbrains告訴你
- Kotlin協程是如何建立結構化併發的?