Android 性能優(yōu)化之內(nèi)存優(yōu)化與泄漏分析工具LeakCanary
一、背景
在Android應(yīng)用中,除了正常的業(yè)務(wù)開(kāi)發(fā),我們也要關(guān)注性能問(wèn)題。卡頓、內(nèi)存溢出、內(nèi)存泄漏等問(wèn)題,直接的表現(xiàn)會(huì)反饋到用戶(hù)體驗(yàn)上,用戶(hù)體驗(yàn)不好導(dǎo)致應(yīng)用被卸載或者換到其他平臺(tái)。
在性能優(yōu)化,各大公司都會(huì)付出一些代價(jià),或者安排專(zhuān)人負(fù)責(zé)。有些新手也想做,但是無(wú)從下手,對(duì)專(zhuān)業(yè)工具和專(zhuān)業(yè)代碼使用以及分析比較吃力,排查起來(lái)也比較費(fèi)勁。如果有專(zhuān)業(yè)的工具能夠只管的把這些記錄并標(biāo)記好。這樣新手也可以通過(guò)詳情的問(wèn)題去排查,那么LeaksCanary就是這款工具了。
二、性能的簡(jiǎn)單介紹(在面試的時(shí)候,也經(jīng)常會(huì)被問(wèn)起)
1、卡頓:卡頓的主要涉及到線(xiàn)程的使用,在LeaksCanary中,會(huì)給出當(dāng)前線(xiàn)程的使用信息
2、內(nèi)存溢出:是程序在申請(qǐng)內(nèi)存時(shí),沒(méi)有足夠的內(nèi)存空間供其使用
3、內(nèi)存泄漏:是程序在申請(qǐng)內(nèi)存后,無(wú)法釋放已申請(qǐng)的內(nèi)存空間
三、LeaksCanary
1、介紹
LeakCanary是Square公司為Android開(kāi)發(fā)者提供的一個(gè)自動(dòng)檢測(cè)內(nèi)存泄漏的工具,
LeakCanary本質(zhì)上是一個(gè)基于MAT進(jìn)行Android應(yīng)用程序內(nèi)存泄漏自動(dòng)化檢測(cè)的的開(kāi)源工具,我們可以通過(guò)集成LeakCanary提供的jar包到自己的工程中,一旦檢測(cè)到內(nèi)存泄漏,LeakCanary就會(huì)dump Memory信息,并通過(guò)另一個(gè)進(jìn)程分析內(nèi)存泄漏的信息并展示出來(lái),隨時(shí)發(fā)現(xiàn)和定位內(nèi)存泄漏問(wèn)題,而不用每次在開(kāi)發(fā)流程中都抽出專(zhuān)人來(lái)進(jìn)行內(nèi)存泄漏問(wèn)題檢測(cè),極大地方便了Android應(yīng)用程序的開(kāi)發(fā)。
2、github地址:
GitHub - square/leakcanary: A memory leak detection library for Android.https://www.baidu.com/link?url=bcGyW7ySy9cW2azMXz5h-je6EtkwbMggSIlaEiFfWbtIqf3Yx3rak5qcn3Om3WJp&wd=&eqid=87caa544001114160000000463e1f198

3、接入
3.1依賴(lài)庫(kù):
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.9.1'
3.2、代碼引入
2.9.1的庫(kù)已kotlin開(kāi)發(fā)語(yǔ)言為核心,網(wǎng)絡(luò)上大多數(shù)還是1.6.1。這就導(dǎo)致在接入的時(shí)候無(wú)法方式不同。
1.6.1版本,直接在application中,通過(guò)LeakCanary.install(application),來(lái)完成。
2.9.1接入:
在androidManifest中引入
leak_canary_watcher_auto_install:
是否自動(dòng)安裝,如果你不想自動(dòng)安裝,可以自行社會(huì)一個(gè)變量引入,默認(rèn)不自動(dòng)安裝

介紹:MainProcessAppWatcherInstaller
internal class MainProcessAppWatcherInstaller : ContentProvider() {
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
MainProcessAppWatcherInstaller安裝是ContentProvider的一個(gè)派生類(lèi)。安裝也是通過(guò)
AppWatcher.manualInstall(application)


安裝方法manualInstall會(huì)做安裝前的檢查
如果不想通過(guò)自動(dòng)安裝,可以通過(guò)
Java:AppWatcher.INSTANCE.manualInstall(application);
kotlin:AppWatcher.manualInstall(application);
如果不想自己控制,可以在xml資源文件,設(shè)置為true。
注意:
如果手動(dòng)安裝,最好判斷一下是否已安裝AppWatcher.isInstalled()否則會(huì)報(bào)錯(cuò)
四、卡頓問(wèn)題優(yōu)化:
LeakCanary 造成卡頓的原因就是在主進(jìn)程中 dump hprof 文件,文件就會(huì)涉及到IO操作,在讀寫(xiě)時(shí),占用大量線(xiàn)程,導(dǎo)致頁(yè)面會(huì)出現(xiàn)卡頓情況。針對(duì)這個(gè)問(wèn)題,可以通過(guò)引入多進(jìn)程方案。避免在IO時(shí),影響主進(jìn)程
leakcanary-android-process
依賴(lài)庫(kù):
debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:2.9.1'
該庫(kù)的核心是RemoteLeakCanaryWorkerService,這個(gè)服務(wù)就是多進(jìn)程的核心。我們使用的時(shí)候,需要提前注冊(cè)這個(gè)service。
在該service中:
override fun onCreate() {
// Ideally we wouldn't need to install AppWatcher at all here, however
// the installation triggers InternalsLeakCanary to store the application instance
// which is then used by the event listeners that respond to analysis progress.
if (!AppWatcher.isInstalled) {
val application = super.getApplicationContext() as Application
AppWatcher.manualInstall(
application,
// Nothing to watch in the :leakcanary process.
watchersToInstall = emptyList()
)
}
super.onCreate()
}
在oncreate中,執(zhí)行安裝。
其實(shí),在核心庫(kù)中,已加入了該service。我們只需要通過(guò)

HeapDump:核心

RemoteWorkManagerHeapAnalyzer
WorkManger也是一款開(kāi)源工具,用于后臺(tái)工作的架構(gòu)組件,需要兼顧機(jī)會(huì)和有保證的執(zhí)行。機(jī)會(huì)性執(zhí)行意味著WorkManager將盡快完成您的后臺(tái)工作。
依賴(lài)庫(kù):
def versions_work = "2.3.3"implementation "androidx.work:work-runtime:$versions_work"
五、線(xiàn)上接入:
目前很多調(diào)試都是在debug下進(jìn)行,通過(guò)專(zhuān)業(yè)工具去處理。但是我們一旦打包以后會(huì)移除debug這些模塊。如果線(xiàn)上發(fā)生了這些,如何獲取?
很多開(kāi)發(fā)人員通過(guò)獲取句柄文件來(lái)分析,但是如果我們沒(méi)有問(wèn)題設(shè)備,就無(wú)法獲取到dump的句柄文件。這時(shí),如果能把句柄文件傳回到日志服務(wù)器,這樣就方便開(kāi)發(fā)人員定位信息。
線(xiàn)上使用 LeakCanary,首要要確定以下問(wèn)題:
- 如何獲取 LeakCanary 分析的結(jié)果?
- 結(jié)果以何種形式上報(bào)到質(zhì)量平臺(tái)上?
- 如何確定合理的監(jiān)控采集時(shí)機(jī),盡可能小的影響用戶(hù)體驗(yàn)?
定義自己的監(jiān)聽(tīng):
上面我們介紹了workmanager這個(gè)工具,他就是通過(guò)定義一個(gè)監(jiān)聽(tīng),來(lái)完成后臺(tái)的處理,減少主進(jìn)程的線(xiàn)程操作,降低卡頓。同樣我們也可以定義自己的EventListener。
目前LeakCanary的方法中已提供了一些監(jiān)聽(tīng):
val eventListeners: List = listOf(
LogcatEventListener,
ToastEventListener,
LazyForwardingEventListener {
if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
},
when {
RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
RemoteWorkManagerHeapAnalyzer
WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
else -> BackgroundThreadHeapAnalyzer
}
),
小試牛刀:
import leakcanary.EventListener
class MyLeakCanaryEventListener:EventListener {
override fun onEvent(event: EventListener.Event) {
}
}
Event:
目前已提供了如下Event
1、class DumpingHeap(uniqueId: String) : Event(uniqueId)
從“LeakCanary堆轉(zhuǎn)儲(chǔ)”HandlerThread發(fā)送
2、class HeapDump( uniqueId: String, val file: File, val durationMillis: Long, val reason: String) : Event(uniqueId)
從“LeakCanary堆轉(zhuǎn)儲(chǔ)”HandlerThread發(fā)送
3、class HeapDumpFailed( uniqueId: String, val exception: Throwable, val willRetryLater: Boolean) : Event(uniqueId)
從“LeakCanary堆轉(zhuǎn)儲(chǔ)”HandlerThread發(fā)送。失敗
4、class HeapAnalysisProgress( uniqueId: String, val step: Step, val progressPercent: Double) : Event(uniqueId)
從執(zhí)行分析的線(xiàn)程發(fā)送。
5、sealed class HeapAnalysisDone ( uniqueId: String, val heapAnalysis: T, showIntent: Intent) : Event(uniqueId)
5.1、class HeapAnalysisSucceeded( uniqueId: String, heapAnalysis: HeapAnalysisSuccess, val unreadLeakSignatures: Set , showIntent: Intent)
分析成功,
5.2、class HeapAnalysisFailed( uniqueId: String, heapAnalysis: HeapAnalysisFailure, showIntent: Intent)
分析失敗
這些類(lèi)都是繼承了class Event( val uniqueId: String),且都是內(nèi)部類(lèi)。
分析結(jié)果:
if (event is EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded) {
//分析成功
val successEvent = event as EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded
val list = successEvent.heapAnalysis.allLeaks
list?.let {
for (leak in it) {
leak.leakTraces
}
}
}
這樣我們就可以得到了分析結(jié)果。
六、注冊(cè)監(jiān)聽(tīng):
注冊(cè)監(jiān)聽(tīng)涉及到LeakCanary的配置,這邊我們先講解一下LeakCanary.Config配置參數(shù)
data class Config(
/**
* Whether LeakCanary should dump the heap when enough retained instances are found. This needs
* to be true for LeakCanary to work, but sometimes you may want to temporarily disable
* LeakCanary (e.g. for a product demo).
*
* Defaults to true.
*/
val dumpHeap: Boolean = true,
/**
* If [dumpHeapWhenDebugging] is false then LeakCanary will not dump the heap
* when the debugger is attached. The debugger can create temporary memory leaks (for instance
* if a thread is blocked on a breakpoint).
*
* Defaults to false.
*/
val dumpHeapWhenDebugging: Boolean = false,
/**
* When the app is visible, LeakCanary will wait for at least
* [retainedVisibleThreshold] retained instances before dumping the heap. Dumping the heap
* freezes the UI and can be frustrating for developers who are trying to work. This is
* especially frustrating as the Android Framework has a number of leaks that cannot easily
* be fixed.
*
* When the app becomes invisible, LeakCanary dumps the heap after
* [AppWatcher.retainedDelayMillis] ms.
*
* The app is considered visible if it has at least one activity in started state.
*
* A higher threshold means LeakCanary will dump the heap less often, therefore it won't be
* bothering developers as much but it could miss some leaks.
*
* Defaults to 5.
*/
val retainedVisibleThreshold: Int = 5,
/**
* Known patterns of references in the heap, added here either to ignore them
* ([IgnoredReferenceMatcher]) or to mark them as library leaks ([LibraryLeakReferenceMatcher]).
*
* When adding your own custom [LibraryLeakReferenceMatcher] instances, you'll most
* likely want to set [LibraryLeakReferenceMatcher.patternApplies] with a filter that checks
* for the Android OS version and manufacturer. The build information can be obtained by calling
* [shark.AndroidBuildMirror.fromHeapGraph].
*
* Defaults to [AndroidReferenceMatchers.appDefaults]
*/
val referenceMatchers: List = AndroidReferenceMatchers.appDefaults,
/**
* List of [ObjectInspector] that provide LeakCanary with insights about objects found in the
* heap. You can create your own [ObjectInspector] implementations, and also add
* a [shark.AppSingletonInspector] instance created with the list of internal singletons.
*
* Defaults to [AndroidObjectInspectors.appDefaults]
*/
val objectInspectors: List = AndroidObjectInspectors.appDefaults,
/**
* Deprecated, add to LeakCanary.config.eventListeners instead.
* Called on a background thread when the heap analysis is complete.
* If you want leaks to be added to the activity that lists leaks, make sure to delegate
* calls to a [DefaultOnHeapAnalyzedListener].
*
* Defaults to [DefaultOnHeapAnalyzedListener]
*/
@Deprecated(message = "Add to LeakCanary.config.eventListeners instead")
val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),
/**
* Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
* Called on a background thread during heap analysis.
*
* Defaults to [AndroidMetadataExtractor]
*/
val metadataExtractor: MetadataExtractor = AndroidMetadataExtractor,
/**
* Whether to compute the retained heap size, which is the total number of bytes in memory that
* would be reclaimed if the detected leaks didn't happen. This includes native memory
* associated to Java objects (e.g. Android bitmaps).
*
* Computing the retained heap size can slow down the analysis because it requires navigating
* from GC roots through the entire object graph, whereas [shark.HeapAnalyzer] would otherwise
* stop as soon as all leaking instances are found.
*
* Defaults to true.
*/
val computeRetainedHeapSize: Boolean = true,
/**
* How many heap dumps are kept on the Android device for this app package. When this threshold
* is reached LeakCanary deletes the older heap dumps. As several heap dumps may be enqueued
* you should avoid going down to 1 or 2.
*
* Defaults to 7.
*/
val maxStoredHeapDumps: Int = 7,
/**
* LeakCanary always attempts to store heap dumps on the external storage if the
* WRITE_EXTERNAL_STORAGE is already granted, and otherwise uses the app storage.
* If the WRITE_EXTERNAL_STORAGE permission is not granted and
* [requestWriteExternalStoragePermission] is true, then LeakCanary will display a notification
* to ask for that permission.
*
* Defaults to false because that permission notification can be annoying.
*/
val requestWriteExternalStoragePermission: Boolean = false,
/**
* Finds the objects that are leaking, for which LeakCanary will compute leak traces.
*
* Defaults to [KeyedWeakReferenceFinder] which finds all objects tracked by a
* [KeyedWeakReference], ie all objects that were passed to
* [ObjectWatcher.expectWeaklyReachable].
*
* You could instead replace it with a [FilteringLeakingObjectFinder], which scans all objects
* in the heap dump and delegates the decision to a list of
* [FilteringLeakingObjectFinder.LeakingObjectFilter]. This can lead to finding more leaks
* than the default and shorter leak traces. This also means that every analysis during a
* given process life will bring up the same leaking objects over and over again, unlike
* when using [KeyedWeakReferenceFinder] (because [KeyedWeakReference] instances are cleared
* after each heap dump).
*
* The list of filters can be built from [AndroidObjectInspectors]:
*
* ```kotlin
* LeakCanary.config = LeakCanary.config.copy(
* leakingObjectFinder = FilteringLeakingObjectFinder(
* AndroidObjectInspectors.appLeakingObjectFilters
* )
* )
* ```
*/
val leakingObjectFinder: LeakingObjectFinder = KeyedWeakReferenceFinder,
/**
* Dumps the Java heap. You may replace this with your own implementation if you wish to
* change the core heap dumping implementation.
*/
val heapDumper: HeapDumper = AndroidDebugHeapDumper,
/**
* Listeners for LeakCanary events. See [EventListener.Event] for the list of events and
* which thread they're sent from. You most likely want to keep this list and add to it, or
* remove a few entries but not all entries. Each listener is independent and provides
* additional behavior which you can disable by not excluding it:
*
* ```kotlin
* // No cute canary toast (very sad!)
* LeakCanary.config = LeakCanary.config.run {
* copy(
* eventListeners = eventListeners.filter {
* it !is ToastEventListener
* }
* )
* }
* ```
*/
val eventListeners: List = listOf(
LogcatEventListener,
ToastEventListener,
LazyForwardingEventListener {
if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
},
when {
RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
RemoteWorkManagerHeapAnalyzer
WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
else -> BackgroundThreadHeapAnalyzer
}
),
/**
* Deprecated: This is a no-op, set a custom [leakingObjectFinder] instead.
*/
@Deprecated("This is a no-op, set a custom leakingObjectFinder instead")
val useExperimentalLeakFinders: Boolean = false
)
默認(rèn)都是有值,對(duì)應(yīng)參數(shù)如下:
private var dumpHeap = config.dumpHeap
private var dumpHeapWhenDebugging = config.dumpHeapWhenDebugging
private var retainedVisibleThreshold = config.retainedVisibleThreshold
private var referenceMatchers = config.referenceMatchers
private var objectInspectors = config.objectInspectors
private var onHeapAnalyzedListener = config.onHeapAnalyzedListener
private var metadataExtractor = config.metadataExtractor
private var computeRetainedHeapSize = config.computeRetainedHeapSize
private var maxStoredHeapDumps = config.maxStoredHeapDumps
private var requestWriteExternalStoragePermission =
config.requestWriteExternalStoragePermission
private var leakingObjectFinder = config.leakingObjectFinder
private var heapDumper = config.heapDumper
private var eventListeners = config.eventListeners
private var useExperimentalLeakFinders = config.useExperimentalLeakFinders
新增我們的監(jiān)聽(tīng):
val eventListeners = LeakCanary.config.eventListeners.toMutableList().apply {
add(MyLeakCanaryEventListener())
}
LeakCanary.config=LeakCanary.config.copy(
eventListeners=eventListeners
)
這樣整個(gè)線(xiàn)上監(jiān)聽(tīng)基本完成。
我們只要對(duì)LeakTrace進(jìn)行分析,把結(jié)果存儲(chǔ)后,在一個(gè)合適的時(shí)機(jī)上報(bào)給服務(wù)器即可
七、AppWatcher的看守配置:
知道如何去分析這些問(wèn)題,LeakCanary還提供了看門(mén)機(jī)制,監(jiān)聽(tīng)哪些也是可以通過(guò)配置的。
在AppWatcher中,也提供了Config
data class Config(
@Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
val watchActivities: Boolean = true,
@Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
val watchFragments: Boolean = true,
@Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
val watchFragmentViews: Boolean = true,
@Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
val watchViewModels: Boolean = true,
@Deprecated("Call AppWatcher.manualInstall() with a custom retainedDelayMillis value")
val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5),
@Deprecated("Call AppWatcher.appDefaultWatchers() with a custom ReachabilityWatcher")
val enabled: Boolean = true
)
可以看守的有以下,默認(rèn)都是開(kāi)啟,如果你想關(guān)閉哪個(gè),默認(rèn)設(shè)為false即可
AppWatcher.config= AppWatcher.config.copy(watchActivities = false, enabled = false)
這樣,我們就可以很好的看守我們想要的結(jié)果。
八、ANR日志收集
ANR或者crash的創(chuàng)建,也意味著ActivityThread的線(xiàn)程已經(jīng)掛了,這時(shí)候我們?nèi)魏蔚淖龇ǘ疾荒芤蕾?lài)主線(xiàn)程來(lái)完成,這樣會(huì)導(dǎo)致抓取不到日志。
我們知道,虛擬機(jī)android的跨線(xiàn)程通訊都是binder,雖然ActivityThread掛了,但是AMS沒(méi)有掛,AMS還存活在SystemManager中,又因?yàn)槊總€(gè)進(jìn)程在fork出來(lái)的時(shí)候都有自己的binder。所以這個(gè)時(shí)候我們可以通過(guò)AMS來(lái)獲取。
日志收集核心:獲取、存儲(chǔ)、上報(bào)。獲取到存儲(chǔ)是核心流程,如何將獲取的日志進(jìn)行存儲(chǔ),IO機(jī)制顯然無(wú)法滿(mǎn)足這么短時(shí)間的操作。這個(gè)時(shí)候我們可以參考內(nèi)存映射mmp機(jī)制。目前包括一些開(kāi)源的都是采用mmap,常見(jiàn)的框架如微信的mmkv,mmkv是采用了mmap,也都是c在處理,利用內(nèi)存映射來(lái)獲取日志文檔,這樣就很容易獲取到日志信息。
九、總結(jié)
通過(guò)接入、卡頓優(yōu)化、攔截、配置等,能夠很好的滿(mǎn)足我們從線(xiàn)上到線(xiàn)下的各種把控??梢院芎玫耐晟茟?yīng)用的監(jiān)控與優(yōu)化機(jī)制。
Debug模式,直接在LeaksApp中可以很好的看到問(wèn)題列表。其他更多的可以參考github的開(kāi)源信息,或者自己接入調(diào)試。
本文僅代表作者觀點(diǎn),版權(quán)歸原創(chuàng)者所有,如需轉(zhuǎn)載請(qǐng)?jiān)谖闹凶⒚鱽?lái)源及作者名字。
免責(zé)聲明:本文系轉(zhuǎn)載編輯文章,僅作分享之用。如分享內(nèi)容、圖片侵犯到您的版權(quán)或非授權(quán)發(fā)布,請(qǐng)及時(shí)與我們聯(lián)系進(jìn)行審核處理或刪除,您可以發(fā)送材料至郵箱:service@tojoy.com






