Android 進(jìn)階實(shí)現(xiàn)性能優(yōu)化之OOM與Leakcanary詳解原理
本文主要探討以下幾個(gè)問題:
- Android內(nèi)存泄漏常見場(chǎng)景以及解決方案
- Leakcanary 使用及原理
Android內(nèi)存泄漏常見場(chǎng)景以及解決方案
資源性對(duì)象未關(guān)閉
對(duì)于資源性對(duì)象不再使用時(shí),應(yīng)該立即調(diào)用它的close()函數(shù),將其關(guān)閉,然后再置為null。例如Bitmap等資源未關(guān)閉會(huì)造成內(nèi)存泄漏,此時(shí)我們應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉。
注冊(cè)對(duì)象未注銷
例如BraodcastReceiver、EventBus未注銷造成的內(nèi)存泄漏,我們應(yīng)該在Activity銷毀時(shí)及時(shí)注銷。
類的靜態(tài)變量持有大數(shù)據(jù)
對(duì)象盡量避免使用靜態(tài)變量存儲(chǔ)數(shù)據(jù),特別是大數(shù)據(jù)對(duì)象,建議使用數(shù)據(jù)庫(kù)存儲(chǔ)。
單例造成的內(nèi)存泄漏
優(yōu)先使用Application的Context,如需使用Activity的Context,可以在傳入Context時(shí)使用弱引用進(jìn)行封裝,然后,在使用到的地方從弱引用中獲取Context,如果獲取不到,則直接return即可。
非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例
該實(shí)例的生命周期和應(yīng)用一樣長(zhǎng),這就導(dǎo)致該靜態(tài)實(shí)例一直持有該Activity的引用,Activity的內(nèi)存資源不能正?;厥铡4藭r(shí),我們可以將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例,如果需要使用Context,盡量使用Application Context,如果需要使用Activity Context,就記得用完后置空讓GC可以回收,否則還是會(huì)內(nèi)存泄漏。
Handler臨時(shí)性內(nèi)存泄漏
Message發(fā)出之后存儲(chǔ)在MessageQueue中,在Message中存在一個(gè)target,它是Handler的一個(gè)引用,Message在Queue中存在的時(shí)間過長(zhǎng),就會(huì)導(dǎo)致Handler無法被回收。如果Handler是非靜態(tài)的,則會(huì)導(dǎo)致Activity或者Service不會(huì)被回收。并且消息隊(duì)列是在一個(gè)Looper線程中不斷地輪詢處理消息,當(dāng)這個(gè)Activity退出時(shí),消息隊(duì)列中還有未處理的消息或者正在處理的消息,并且消息隊(duì)列中的Message持有Handler實(shí)例的引用,Handler又持有Activity的引用,所以導(dǎo)致該Activity的內(nèi)存資源無法及時(shí)回收,引發(fā)內(nèi)存泄漏。解決方案如下所示:
1. 使用一個(gè)靜態(tài)Handler內(nèi)部類,然后對(duì)Handler持有的對(duì)象(一般是Activity)使用弱引用,這樣在回收時(shí),也可以回收Handler持有的對(duì)象。
2. 在Activity的Destroy或者Stop時(shí),應(yīng)該移除消息隊(duì)列中的消息,避免Looper線程的消息隊(duì)列中有待處理的消息需要處理。需要注意的是,AsyncTask內(nèi)部也是Handler機(jī)制,同樣存在內(nèi)存泄漏風(fēng)險(xiǎn),但其一般是臨時(shí)性的。對(duì)于類似AsyncTask或是線程造成的內(nèi)存泄漏,我們也可以將AsyncTask和Runnable類獨(dú)立出來或者使用靜態(tài)內(nèi)部類。
容器中的對(duì)象沒清理造成的內(nèi)存泄漏
在退出程序之前,將集合里的東西clear,然后置為null,再退出程序
WebView
WebView都存在內(nèi)存泄漏的問題,在應(yīng)用中只要使用一次WebView,內(nèi)存就不會(huì)被釋放掉。我們可以為WebView開啟一個(gè)獨(dú)立的進(jìn)程,使用AIDL與應(yīng)用的主進(jìn)程進(jìn)行通信,WebView所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(jī)進(jìn)行銷毀,達(dá)到正常釋放內(nèi)存的目的。
使用ListView時(shí)造成的內(nèi)存泄漏
在構(gòu)造Adapter時(shí),使用緩存的convertView。
Leakcanary
leakcanary 導(dǎo)入
// leakcanary 添加支持庫(kù)即可,只在debug下使用 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
leakcanary 是如何安裝的
leakcanary 不需要初始化,用的是 ContentProvider!
ContentProvider.onCreate 方法比 Application.onCreate 更早執(zhí)行。LeakCanary 源碼的 Manifest.xml 里有聲明ContentProvider,apk打包流程中會(huì)把所有的Manifest合并到app 的 Manifest 里,即APP就有了ContentProvider。
// package="com.squareup.leakcanary.leaksentry"
<application>
<provider
android:name="leakcanary.internal.LeakSentryInstaller"
android:authorities="${applicationId}.leak-sentry-installer"
android:exported="false"/>
</application>
下面是初始化的代碼
internal class LeakSentryInstaller : ContentProvider() {
override fun onCreate(): Boolean {
CanaryLog.logger = DefaultCanaryLog()
val application = context!!.applicationContext as Application
// 進(jìn)行初始化工作,核心
InternalLeakSentry.install(application)
return true
}
監(jiān)聽實(shí)現(xiàn)
fun install(application: Application) {
CanaryLog.d("Installing LeakSentry")
// 只能在主線程調(diào)用,否則會(huì)拋出異常
checkMainThread()
if (this::application.isInitialized) {
return
}
InternalLeakSentry.application = application
val configProvider = { LeakSentry.config }
// 監(jiān)聽 Activity.onDestroy()
ActivityDestroyWatcher.install(
application, refWatcher, configProvider
)
// 監(jiān)聽 Fragment.onDestroy()
FragmentDestroyWatcher.install(
application, refWatcher, configProvider
)
// Sentry 哨兵
listener.onLeakSentryInstalled(application)
}
leakcanary 如何監(jiān)聽Activity、Fragment銷毀
在了解監(jiān)聽過程前有必要了解下 ActivityLifecycleCallbacks 與 FragmentLifeCycleCallbacks
// ActivityLifecycleCallbacks 接口
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity var1, Bundle var2);
void onActivityStarted(Activity var1);
void onActivityResumed(Activity var1);
void onActivityPaused(Activity var1);
void onActivityStopped(Activity var1);
void onActivitySaveInstanceState(Activity var1, Bundle var2);
void onActivityDestroyed(Activity var1);
}
// FragmentLifecycleCallbacks 接口
public abstract static class FragmentLifecycleCallbacks {
public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}
public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}
public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}
// 省略其他的生命周期 ...
}
Application 類提供了 registerActivityLifecycleCallbacks 和 unregisterActivityLifecycleCallbacks 方法用于注冊(cè)和反注冊(cè) Activity 的生命周期監(jiān)聽類,這樣我們就能在 Application 中對(duì)所有的 Activity 生命周期回調(diào)中做一些統(tǒng)一處理。同理,F(xiàn)ragmentManager 類提供了 registerFragmentLifecycleCallbacks 和 unregisterFragmentLifecycleCallbacks 方法用戶注冊(cè)和反注冊(cè) Fragment 的生命周期監(jiān)聽類,這樣我們對(duì)每一個(gè) Activity 進(jìn)行注冊(cè),就能獲取所有的 Fragment 生命周期回調(diào)。
下面是 ActivityDestroyWatcher 的實(shí)現(xiàn),refWatcher 監(jiān)聽 activity 的 onActivityDestroyed
internal class ActivityDestroyWatcher private constructor(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
// 監(jiān)聽到 onDestroy() 之后,通過 refWatcher 監(jiān)測(cè) Activity
refWatcher.watch(activity)
}
}
}
companion object {
fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(refWatcher, configProvider)
// 注冊(cè) Activity 生命周期監(jiān)聽
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
如此一來Activity、Fragment在調(diào)用onDestroy時(shí)我們都能知道。講道理,如果在調(diào)用onDestroy時(shí)被GC是正常的,如果沒有被回收則是發(fā)生了內(nèi)存泄漏,這是我們要處理的。那 refWatcher.watch(activity) 監(jiān)聽到銷毀后怎么處理?
RefWatcher 核心原理
在讀這塊代碼前舉個(gè)栗子比較好理解:比如我們?nèi)タ萍贾行拿嬖?/p>
- 進(jìn)去的時(shí)候會(huì)登記個(gè)人信息在觀察列表,并標(biāo)明停留時(shí)間30分鐘
- 30分鐘過后查看是否有登出
- 如果未登出將信息由觀察列表轉(zhuǎn)移至懷疑列表
- 懷疑列表名單超過5個(gè)時(shí),找公安人員確定是否是恐怖分子
- 確定是恐怖分子,警察抓人
RefWatcher 的實(shí)現(xiàn)原理跟上面的栗子神似:
- Activity調(diào)用onDestroy后,以UUID生成key,被KeyedWeakReference包裝,并與ReferenceQueue關(guān)聯(lián),并把<key,KeyedWeakReference>存入 watchedReferences 中(watchedReferences 對(duì)應(yīng)觀察隊(duì)列)
- 等待5s時(shí)間
- 調(diào)用 moveToRetained 方法,先判斷是否已經(jīng)釋放,如果未釋放由 watchedReferences (觀察隊(duì)列) 轉(zhuǎn)入 retainedReferences(懷疑隊(duì)列)
- 當(dāng) retainedReferences 隊(duì)列的長(zhǎng)度大于5時(shí),先調(diào)用一次GC,用HAHA這個(gè)開源庫(kù)去分析dump之后的heap內(nèi)存
- 確定內(nèi)存泄漏對(duì)象
咱們先看下 refWatcher.watch(activity) 的實(shí)現(xiàn)
@Synchronized fun watch(
watchedReference: Any,
referenceName: String
) {
if (!isEnabled()) {
return
}
// 移除隊(duì)列中將要被 GC 的引用
removeWeaklyReachableReferences()
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
// 構(gòu)建當(dāng)前引用的弱引用對(duì)象,并關(guān)聯(lián)引用隊(duì)列 queue
val reference = KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
if (referenceName != "") {
CanaryLog.d(
"Watching instance of %s named %s with key %s", reference.className,
referenceName, key
)
} else {
CanaryLog.d(
"Watching instance of %s with key %s", reference.className, key
)
}
// 將引用存入 watchedReferences
watchedReferences[key] = reference
checkRetainedExecutor.execute {
// 如果當(dāng)前引用未被移除,仍在 watchedReferences 隊(duì)列中,
// 說明仍未被 GC,移入 retainedReferences 隊(duì)列中,暫時(shí)標(biāo)記為泄露
moveToRetained(key)
}
}
分析上面這段代碼都做了什么:
- 移除隊(duì)列中將要被 GC 的引用,這里的隊(duì)列包括 watchedReferences 和 retainedReferences
- 使用UUID生成唯一key,構(gòu)建 WeakReference 包裝 activity 并與 ReferenceQueue 關(guān)聯(lián)
- 將 reference 放入觀察隊(duì)列 watchedReferences 中
- 線程池調(diào)用 moveToRetained 函數(shù),此函數(shù)先走一遍gc,依舊沒回收的對(duì)象會(huì)進(jìn)入 retainedReferences 懷疑隊(duì)列,當(dāng)隊(duì)列大于5時(shí)調(diào)用HAHA庫(kù)走可達(dá)性分析確定是否是內(nèi)存泄漏
下面是細(xì)節(jié)分析 —》removeWeaklyReachableReferences() 邏輯
private fun removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
// 弱引用一旦變得弱可達(dá),就會(huì)立即入隊(duì)。這將在 finalization 或者 GC 之前發(fā)生。
var ref: KeyedWeakReference?
do {
// 隊(duì)列 queue 中的對(duì)象都是會(huì)被 GC 的
ref = queue.poll() as KeyedWeakReference?
//說明被釋放了
if (ref != null) {
val removedRef = watchedReferences.remove(ref.key)//獲取被釋放的引用的key
if (removedRef == null) {
retainedReferences.remove(ref.key)
}
// 移除 watchedReferences 隊(duì)列中的會(huì)被 GC 的 ref 對(duì)象,剩下的就是可能泄露的對(duì)象
}
} while (ref != null)
}
removeWeaklyReachableReferences 函數(shù)會(huì)根據(jù) ReferenceQueue 出來的 KeyedWeakReference 的 key 移除 watchedReferences(觀察隊(duì)列)和 retainedReferences(懷疑隊(duì)列)中的引用,即把已經(jīng)釋放的移出,剩下的是內(nèi)存泄漏的
moveToRetained(key) 邏輯實(shí)現(xiàn)
@Synchronized private fun moveToRetained(key: String) {
// 再次調(diào)用,防止遺漏
removeWeaklyReachableReferences()
val retainedRef = watchedReferences.remove(key)
//說明可能存在內(nèi)存泄漏
if (retainedRef != null) {
retainedReferences[key] = retainedRef
onReferenceRetained()
}
}
此函數(shù)的作用:
- 走一遍 removeWeaklyReachableReferences 方法,將已經(jīng)回收的清除
- 將 watchedReferences(觀察隊(duì)列)中未被回收的引用移到 retainedReferences(懷疑隊(duì)列)中
- onReferenceRetained() 則是在工作線程中檢測(cè)內(nèi)存泄漏,最后會(huì)調(diào)用 checkRetainedInstances 函數(shù)
下面是 checkRetainedInstances 的具體實(shí)現(xiàn)
private fun checkRetainedInstances(reason: String) {
CanaryLog.d("Checking retained instances because %s", reason)
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
return
}
var retainedKeys = refWatcher.retainedKeys
// 當(dāng)前泄露實(shí)例個(gè)數(shù)小于 5 個(gè),不進(jìn)行 heap dump
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
showRetainedCountWithDebuggerAttached(retainedKeys.size)
scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
CanaryLog.d(
"Not checking for leaks while the debugger is attached, will retry in %d ms",
WAIT_FOR_DEBUG_MILLIS
)
return
}
// 可能存在被觀察的引用將要變得弱可達(dá),但是還未入隊(duì)引用隊(duì)列。
// 這時(shí)候應(yīng)該主動(dòng)調(diào)用一次 GC,可能可以避免一次 heap dump
gcTrigger.runGc()
retainedKeys = refWatcher.retainedKeys
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)
CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
dismissNotification()
val heapDumpFile = heapDumper.dumpHeap() // AndroidHeapDumper
if (heapDumpFile == null) {
CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
showRetainedCountWithHeapDumpFailed(retainedKeys.size)
return
}
refWatcher.removeRetainedKeys(retainedKeys) // 移除已經(jīng) heap dump 的 retainedKeys
HeapAnalyzerService.runAnalysis(application, heapDumpFile) // 分析 heap dump 文件
}
流程圖



到此這篇關(guān)于Android 進(jìn)階實(shí)現(xiàn)性能優(yōu)化之OOM與Leakcanary詳解原理的文章就介紹到這了,更多相關(guān)Android 性能優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android消息推送:手把手教你集成小米推送(附demo)
本篇文章主要介紹了Android消息推送:手把手教你集成小米推送,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2016-12-12
詳解Android跨進(jìn)程IPC通信AIDL機(jī)制原理
本篇文章主要介紹了詳解Android跨進(jìn)程IPC通信AIDL機(jī)制原理,詳細(xì)的介紹了AIDL的概念和使用,具有一定的參考價(jià)值,有興趣的可以了解一下2018-01-01
Android開發(fā)仿bilibili刷新按鈕的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 仿bilibili刷新按鈕的實(shí)現(xiàn)代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-10-10
Android應(yīng)用開發(fā)中WebView的常用方法筆記整理
WebView即是在安卓本地應(yīng)用中打開網(wǎng)頁(yè)視圖功能,其中對(duì)于JavaScript加載的各項(xiàng)操作是重點(diǎn)和難點(diǎn),本文就為大家送上Android應(yīng)用開發(fā)中WebView的常用方法筆記整理2016-05-05
Android Canvas方法總結(jié)最全面詳解API(小結(jié))
本篇文章主要介紹了Android Canvas方法總結(jié)最全面詳解API(小結(jié)),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11
android中ViewPager結(jié)合Fragment進(jìn)行無限滑動(dòng)
本篇文章中主要介紹了android中ViewPager結(jié)合Fragment進(jìn)行無限滑動(dòng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03
Android仿Keep運(yùn)動(dòng)休息倒計(jì)時(shí)圓形控件
這篇文章主要為大家詳細(xì)介紹了Android仿Keep運(yùn)動(dòng)休息倒計(jì)時(shí)圓形控件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
Android實(shí)現(xiàn)語(yǔ)音識(shí)別代碼
語(yǔ)音識(shí)別在android上使用起來很方便也很簡(jiǎn)單.但是有個(gè)前提條件,就是android機(jī)器上必須預(yù)先安裝google的語(yǔ)音搜索工具,今天我們就來詳細(xì)探討下2015-06-06

