OkHttp是如何監測連線洩漏的

語言: CN / TW / HK

1. 前言

最近在找工作,所以複習了很多常用的基礎知識,在看OkHttp原始碼的時候,有這麼一段程式碼引起了我的好奇心。如果你的團隊正在招人,可以內推我喲。通過微訊號bytestation或者掃以下二維碼加我微信。希望你:

  1. 備註來意,備明公司名稱

  2. 團隊有良好的技術氛圍

  3. Android團隊人員最好超過10人

  4. 辦公地點是在北京

意思就是,如果忘記關閉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才會被認為發生洩漏了。

那麼問題來了

  1. 為什麼是reference.get()返回null時發生連線洩漏?

  2. 呼叫了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是否正確關閉的系統。