收藏平痰!Dropbox 是如何解決 Android App 的內(nèi)存泄漏問題的汞舱?

當(dāng)應(yīng)用程序為對象分配內(nèi)存,而對象不再被使用時卻沒有釋放宗雇,就會發(fā)生內(nèi)存泄漏昂芜。隨著時間的推移,泄漏的內(nèi)存會累積赔蒲,導(dǎo)致應(yīng)用程序性能變差泌神,甚至崩潰。泄漏可能發(fā)生在任何程序和平臺上舞虱,但由于活動生命周期的復(fù)雜性欢际,這種情況在 Android 應(yīng)用中尤其普遍。最新的 Android 模式矾兜,如 ViewModel 和 LifecycleObserver 可以幫助避免內(nèi)存泄漏损趋,但如果你遵循舊的模式或不知道要注意什么,很容易漏過錯誤椅寺。

常見例子

引用長期運行的服務(wù)

Fragment 引用了一個活動浑槽,而該活動引用一個長期運行的服務(wù)

在這種情況下,我們有一個標(biāo)準(zhǔn)設(shè)置返帕,活動持有一個長期運行的服務(wù)的引用桐玻,然后是 Fragment 及其視圖持有活動的引用。例如荆萤,假設(shè)活動以某種方式創(chuàng)建了對其子 Fragment 的引用镊靴。然后,只要活動還在观腊,F(xiàn)ragment 也會繼續(xù)存在邑闲。那么在 Fragment 的onDestroy和活動的onDestroy之間就發(fā)生了內(nèi)存泄漏。

該 Fragment 永遠(yuǎn)不會再使用梧油,但它會一直在內(nèi)存中

長期運行的服務(wù)引用了 Fragment 視圖

另一方面苫耸,如果服務(wù)獲得了 Fragment 視圖的引用呢?

首先儡陨,視圖現(xiàn)在將在服務(wù)的整個持續(xù)時間內(nèi)保持活動狀態(tài)褪子。此外,因為視圖持有對其父活動的引用骗村,所以該活動現(xiàn)在也會泄漏嫌褪。

只要服務(wù)存在,F(xiàn)ragmentView 和 Activity 都會浪費內(nèi)存

檢測內(nèi)存泄漏

現(xiàn)在胚股,我們已經(jīng)知道了內(nèi)存泄漏是如何發(fā)生的笼痛。讓我們討論下如何檢測它們。顯然,第一步是檢查你的應(yīng)用是否會因為OutOfMemoryError而崩潰缨伊。除非單個屏幕占用的內(nèi)存比手機(jī)可用內(nèi)存還多摘刑,否則肯定在某個地方存在內(nèi)存泄漏。

這種方法只告訴你存在的問題刻坊,而不是根本原因枷恕。內(nèi)存泄漏可能發(fā)生在任何地方,記錄的崩潰并不沒有指向泄漏谭胚,而是指向最終提示內(nèi)存使用超過限制的屏幕徐块。

你可以檢查所有的面包屑控件,看看它們是否有一些相似之處灾而,但很可能罪魁禍?zhǔn)撞⒉蝗菀鬃R別胡控。讓我們研究下其他選項。

LeakCanary

LeakCanary 是目前最好的工具之一绰疤,它是一個用于 Android 的內(nèi)存泄漏檢測庫铜犬。我們只需在構(gòu)建中添加一個 build.gradle 文件依賴項。下一次轻庆,我們安裝和運行我們的應(yīng)用時,LeakCanary 將與它一起運行敛劝。當(dāng)我們在應(yīng)用中導(dǎo)航時余爆,LeakCanary 會偶爾暫停以轉(zhuǎn)儲內(nèi)存,并提供檢測到的泄漏痕跡夸盟。

這個工具比我們之前的方法要好得多蛾方。但是這個過程仍然是手動的,每個開發(fā)人員只有他們個人遇到的內(nèi)存泄漏的本地副本上陕。我們可以做得更好桩砰!

LeakCanary 和 Bugsnag

LeakCanary 提供了一個非常方便的代碼配方(code recipe),用于將發(fā)現(xiàn)的泄漏上傳到 Bugsnag释簿。我們可以跟蹤內(nèi)存泄漏亚隅,就像我們在應(yīng)用程序中跟蹤任何其他警告或崩潰。我們甚至可以更進(jìn)一步庶溶,使用 Bugsnag Integration 將其連接到項目管理軟件煮纵,如 Jira,以獲得更好的可見性和問責(zé)制偏螺。

Bugsnag 連接到 Jira

LeakCanary 和集成測試

另一種提高自動化的方法是將 LeakCanary 與 CI 測試連接起來行疏。同樣,我們有一個代碼配方套像。以下內(nèi)容來自官方文件:

LeakCanary 提供了一個專門用于在 UI 測試中檢測漏洞的構(gòu)件酿联,它提供了一個運行偵聽器,后者會等待測試結(jié)束,如果測試成功贞让,它將查找留存的對象周崭,在需要時觸發(fā)堆轉(zhuǎn)儲并執(zhí)行分析。

注意震桶,LeakCanary 會降低測試速度休傍,因為它每次都會在其偵聽的測試結(jié)束后轉(zhuǎn)儲堆。在我們的例子中蹲姐,由于我們的選擇性測試和分片設(shè)置磨取,額外增加的時間可以忽略不計。

最終柴墩,就像 CI 上的任何其他構(gòu)建或測試失敗一樣忙厌,內(nèi)存泄漏也會被暴露出來,并且漏洞跟蹤信息也被記錄了下來江咳。

在 CI 上運行 LeakCanary 幫助我們學(xué)到了更好的編碼模式逢净,特別是涉及到新的庫時,在任何代碼進(jìn)入生產(chǎn)環(huán)境前歼指。例如爹土,當(dāng)我們使用 MvRx 測試時,它發(fā)現(xiàn)了這個漏洞:

<failure>Test failed because application memory leaks were detected: ==================================== HEAP ANALYSIS RESULT ==================================== 4 APPLICATION LEAKS References underlined with "~~~" are likely causes. Learn more at https://squ.re/leaks. 198449 bytes retained by leaking objects Signature: 6bf2ba80511dcb6ab9697257143e3071fca4 ┬───   
│ GC Root: System class   
│ ├─ com.airbnb.mvrx.mocking.MockableMavericks class   
│ Leaking: NO (a class is never leaking)   
│ ↓ static MockableMavericks.mockStateHolder   
│                            ~~~~~~~~~~~~~~~   
├─ com.airbnb.mvrx.mocking.MockStateHolder instance   
│ Leaking: UNKNOWN   
│ ↓ MockStateHolder.delegateInfoMap   
│                   ~~~~~~~~~~~~~~~   
├─ java.util.LinkedHashMap instance   
│ Leaking: UNKNOWN   
│ ↓ LinkedHashMap.header   
│                 ~~~~~~   
├─ java.util.LinkedHashMap$LinkedEntry instance   
│ Leaking: UNKNOWN   
│ ↓ LinkedHashMap$LinkedEntry.prv   
│                             ~~~   
├─ java.util.LinkedHashMap$LinkedEntry instance   
│ Leaking: UNKNOWN   
│ ↓ LinkedHashMap$LinkedEntry.key   
│                             ~~~   
╰→ com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment instance   
   Leaking: YES (ObjectWatcher was watching this because com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)   
   key = 391c9051-ad2c-4282-9279-d7df13d205c3   
   watchDurationMillis = 7304   
   retainedDurationMillis = 2304 198427 bytes retained by leaking objects   
   Signature: d1c9f9707034dd15604d8f2e63ff3bf3ecb61f8  

事實證明踩身,在編寫測試時胀茵,我們沒有正確地清理測試。添加幾行代碼可以避免泄漏:

   @After  
    fun teardown() {  
        scenario.close()  
        val holder = MockableMavericks.mockStateHolder  
        holder.clearAllMocks()  
    }

你可能會想:既然這種內(nèi)存泄漏只發(fā)生在測試中挟阻,那么修復(fù)它真的那么重要嗎琼娘?好吧,那就看你了附鸽!與代碼檢查一樣脱拼,泄漏檢測可以告訴你什么時候出現(xiàn)了代碼氣味或糟糕的編碼模式。

它可以幫助工程師編寫更健壯的代碼——在本例中坷备,我們知道了clearAllMocks()熄浓。泄漏的嚴(yán)重程度,以及是否必須修復(fù)击你,都是工程師可以做出的決定玉组。

對于我們不想運行泄漏檢測的測試,我們編寫了一個簡單的注解:

@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.METHOD, ElementType.TYPE})  
public @interface SkipLeakDetection {  
    /**  
     * The reason why the test should skip leak detection.  
     */  
    String value();  
}  

我們的類重寫了 LeakCanary 的FailOnLeakRunListener()

override fun skipLeakDetectionReason(description: Description): String? {  
    return when {  
        description.getAnnotation(SkipLeakDetection::class.java) != null ->  
            "is annotated with @SkipLeakDetection"  
        description.testClass.isAnnotationPresent(SkipLeakDetection::class.java) ->  
            "class is annotated with @SkipLeakDetection"  
        else -> null  
    }  
}  

單個測試或整個測試類可以使用這個注解跳過泄漏檢測丁侄。

修復(fù)內(nèi)存泄漏

現(xiàn)在惯雳,我們討論了各種查找和暴露內(nèi)存泄漏的方法。下面鸿摇,我們討論一下如何真正理解和修復(fù)它們石景。

LeakCanary 提供的泄漏跟蹤是診斷泄漏最有用的工具。本質(zhì)上講,泄漏跟蹤打印出與泄漏對象關(guān)聯(lián)的引用鏈潮孽,并解釋為什么將其視為泄漏揪荣。

關(guān)于如何閱讀和使用泄漏跟蹤,LeakCanary 有了很好的 文檔往史,這里無需重復(fù)仗颈。取而代之,讓我們回顧一下我自己經(jīng)常要處理的兩類內(nèi)存泄漏椎例。

視圖

我們經(jīng)嘲ぞ觯看到視圖被聲明為類級變量:private TextView myTextView;或者订歪,現(xiàn)在有更多的 Android 代碼正在用 Kotlin 編寫:private lateinit var myTextView: textview——非常常見脖祈,我們沒有意識到這些都可以導(dǎo)致內(nèi)存泄漏。

除非在 Fragment 的onDestroyView中消除對這些字段的引用刷晋,(對于lateinit變量不能這么做)盖高,否則對這些視圖的引用在 Fragment 的整個生命周期內(nèi)都會存在,而不是像它們應(yīng)該的那樣在 Fragment 視圖的生命周期內(nèi)存在眼虱。

導(dǎo)致內(nèi)存泄漏的一個最簡單場景是:我們在 FragmentA 上喻奥。我們導(dǎo)航到 FragmentB,現(xiàn)在 FragmentA 在棧里捏悬。FragmentA 沒有被銷毀映凳,但是 FragmentA 的視圖被銷毀了。任何綁定到 FragmentA 生命周期的視圖現(xiàn)在已經(jīng)不需要了邮破,但都還保留在內(nèi)存中。

在大多數(shù)情況下仆救,這些泄漏很小抒和,不會導(dǎo)致任何性能問題或崩潰。但是對于保存對象和數(shù)據(jù)彤蔽、圖像摧莽、視圖 / 數(shù)據(jù)綁定等的視圖,我們更有可能遇到麻煩顿痪。

所以镊辕,如果可能的話,避免在類級變量中存儲視圖蚁袭,或者確保在onDestroyView中正確地清理它們征懈。

說到視圖 / 數(shù)據(jù)綁定,Android 的視圖綁定文檔 明確地告訴我們:字段必須被清除以防止泄漏揩悄。他們提供的代碼片段建議我們做以下工作:

private var _binding: ResultProfileBinding? = null  
// This property is only valid between onCreateView and  
// onDestroyView.  
private val binding get() = _binding!!  
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?  
): View? {  
    _binding = ResultProfileBinding.inflate(inflater, container, false)  
    val view = binding.root  
    return view  
}  
override fun onDestroyView() {  
    super.onDestroyView()  
    _binding = null  
}  

每個 Fragment 中都有很多樣板代碼(另外卖哎,避免使用 !!,因為如果變量為空,這會拋出KotlinNullPointerException亏娜。使用顯式空處理來代替焕窝。)我們解決這個問題的方法是創(chuàng)建一個ViewBindingHolder(和DataBindingHolder),F(xiàn)ragment 可以實現(xiàn)為下面這樣:

interface ViewBindingHolder<B : ViewBinding> {  
    var binding: B?  
    // Only valid between onCreateView and onDestroyView.  
    fun requireBinding() = checkNotNull(binding)  
    fun requireBinding(lambda: (B) -> Unit) {  
        binding?.let {  
            lambda(it)  
        }}  
    /**  
     * Make sure to use this with Fragment.viewLifecycleOwner  
     */  
    fun registerBinding(binding: B, lifecycleOwner: LifecycleOwner) {  
        this.binding = binding  
        lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {  
            override fun onDestroy(owner: LifecycleOwner) {  
                owner.lifecycle.removeObserver(this)  
                this@ViewBindingHolder.binding = null  
            }  
        })  
    }  
}  
interface DataBindingHolder<B : ViewDataBinding> : ViewBindingHolder<B>  

這為 Fragment 提供了一種簡單而干凈的方式:

  • 確保在需要綁定時提供綁定

  • 只有在綁定可用時才執(zhí)行某些代碼

  • 自動在onDestroyView上清除綁定

暫時性泄漏

這些泄漏只會存在很短時間维贺。特別是它掂,我們遇到過一個由EditTextView異步任務(wù)引起的泄漏。異步任務(wù)持續(xù)的時間恰好比 LeakCanary 的默認(rèn)等待時間長溯泣,因此虐秋,即使內(nèi)存很快就被正確地釋放了,也會報告一個泄漏发乔。

如果你懷疑自己遇到了暫時性泄漏熟妓,一個很好的檢查方法是使用 Android Studio 的內(nèi)存分析器。一旦在分析器中啟動會話栏尚,就可以按步驟重現(xiàn)泄漏起愈,但是在轉(zhuǎn)儲堆并檢查之前要等待更長時間。經(jīng)過這段額外的時間后译仗,泄漏可能就消失了抬虽。

Android Studio 的內(nèi)存分析器顯示了清理暫時性泄漏的效果

經(jīng)常測試,盡早修復(fù)

我們希望纵菌,通過本文介紹阐污,你能在自己的應(yīng)用程序中跟蹤和解決內(nèi)存泄漏!與許多 Bug 和其他問題一樣咱圆,最好是能經(jīng)常測試笛辟,在糟糕的模式扎根代碼庫之前盡早修復(fù)。

作為一名開發(fā)人員序苏,你一定要記住手幢,雖然內(nèi)存泄漏并不總是會影響應(yīng)用性能,但低端機(jī)型和手機(jī)內(nèi)存小的用戶會感激你為他們所做的工作忱详。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末围来,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匈睁,更是在濱河造成了極大的恐慌监透,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件航唆,死亡現(xiàn)場離奇詭異胀蛮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)佛点,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門醇滥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來黎比,“玉大人,你說我怎么就攤上這事鸳玩≡某妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵不跟,是天一觀的道長颓帝。 經(jīng)常有香客問我,道長窝革,這世上最難降的妖魔是什么购城? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮虐译,結(jié)果婚禮上瘪板,老公的妹妹穿的比我還像新娘。我一直安慰自己漆诽,他們只是感情好侮攀,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厢拭,像睡著了一般兰英。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上供鸠,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天畦贸,我揣著相機(jī)與錄音,去河邊找鬼楞捂。 笑死薄坏,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寨闹。 我是一名探鬼主播颤殴,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鼻忠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杈绸,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帖蔓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瞳脓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體塑娇,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年劫侧,在試婚紗的時候發(fā)現(xiàn)自己被綠了埋酬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哨啃。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖写妥,靈堂內(nèi)的尸體忽然破棺而出拳球,到底是詐尸還是另有隱情,我是刑警寧澤珍特,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布祝峻,位于F島的核電站,受9級特大地震影響扎筒,放射性物質(zhì)發(fā)生泄漏莱找。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一嗜桌、第九天 我趴在偏房一處隱蔽的房頂上張望奥溺。 院中可真熱鬧,春花似錦骨宠、人聲如沸浮定。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽壶唤。三九已至,卻和暖如春棕所,著一層夾襖步出監(jiān)牢的瞬間闸盔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工琳省, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留迎吵,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓针贬,卻偏偏與公主長得像击费,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子桦他,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容