當(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)存小的用戶會感激你為他們所做的工作忱详。