前言
內(nèi)存問題很常見 而且經(jīng)常會(huì)因?yàn)閮?nèi)存問題引起卡頓問題 在接下來的卡頓分析中 內(nèi)存也是一個(gè)很重要的方向
內(nèi)存抖動(dòng)
內(nèi)存抖動(dòng)是由頻繁gc導(dǎo)致產(chǎn)生 由于內(nèi)存空間的不足 回導(dǎo)致頻繁gc 在Profile中查看是鋸齒狀
內(nèi)存抖動(dòng)實(shí)戰(zhàn)
我們可以通過Profile來分析memory Profile的優(yōu)勢(shì)大概就是圖表非常直觀 我們一般可以配合使用mat來解決內(nèi)存泄漏的問題
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
for (int i = 0; i < 100; i++) {
String args[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
我們通過頻繁申請(qǐng)大對(duì)象來模擬內(nèi)存抖動(dòng) 我們來觀察Profile

emmmm 現(xiàn)在的手機(jī)還是很厲害啊 沒有鋸齒狀 但是可以看到gc非常頻繁 這時(shí)候我們就可以dump heap
然后看一下內(nèi)存的主要消耗

我們主要看Shallow Size 和Retained Siz
Jvm內(nèi)存分配
JVM內(nèi)存分配主要分為以下幾個(gè)部分
- 方法區(qū) (常量 靜態(tài)變量 編譯之后代碼)
- 程序計(jì)數(shù)器 (計(jì)算當(dāng)前線程的當(dāng)前方法執(zhí)行到多少行)
- 虛擬機(jī)棧 (java對(duì)象引用)
- 本地方法棧 (native對(duì)象引用)
- 堆 (生成的對(duì)象)
工具選擇
Profile
Profile是Android Studio自帶的工具 我們可以用來查看Cpu Memory Network Energy的消耗
我們可以使用Profile來定位內(nèi)存抖動(dòng)問題 因?yàn)榭梢院苊黠@的看到鋸齒狀或者頻繁gc的情況
也可以用Cpu Profile 或者 Memory Profile來查看內(nèi)存泄漏問題
Profile提供了Fragment/Activity的監(jiān)測(cè) 還可以通過包名等方式 來查看內(nèi)存中的泄漏問題
可以定位查看 內(nèi)存中是否存在不合理的對(duì)象
MAT
MAT全稱Memory Anlyzer Tools ,是一款可以分析Hprof文件的可視化工具 我們可以使用Mat工具
查找定位我們預(yù)設(shè)的懷疑點(diǎn) 通過exclude weak soft等引用 獲取到gc到對(duì)象的引用路徑 幫助我們解決問題
LeakCanary源碼解析
在2.0之前的版本 需要我們手動(dòng)調(diào)用Install方法 在2.0之后 LeakCanary注冊(cè)了ContentProvider 不需要手動(dòng)的調(diào)用Install
LeakCanary分為兩部分 監(jiān)控和分析
監(jiān)控
查看LeakCanary源碼 發(fā)現(xiàn)AppWatcherInstaller類,繼承ContentProvider
在Oncreate方法中調(diào)用了AppWatcher.manualInstall(application)
然后AppWatcher中調(diào)用了InternalAppWatcher.install(application)
查看InternalAppWatcher.install方法
fun install(application: Application) {
checkMainThread()
if (this::application.isInitialized) {
return
}
SharkLog.logger = DefaultCanaryLog()
InternalAppWatcher.application = application
val configProvider = { AppWatcher.config }
//監(jiān)控Activity 這里傳遞了ObjectWatcher用來監(jiān)控Object對(duì)象
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
//監(jiān)控Fragment
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)
}
我們已經(jīng)察覺到了 關(guān)鍵代碼就在ActivityDestroyWatcher.install里面了 讓我們跟上
internal class ActivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
//使用我們上面提到的objectWatcher來觀察activity
objectWatcher.watch(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
}
companion object {
fun install(
application: Application,
objectWatcher: ObjectWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
//注冊(cè)生命周期
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
我們可能發(fā)現(xiàn)了LeakCanary的原理 就是監(jiān)聽Activity的生命周期回調(diào) 在OnDestroy之后 使用objectWatcher去觀察Activity是否有被回收 如果沒有回收 就表示泄漏了
Follow Me,勝利就在前面了!!!
接著查看objectWatcher的watch方法
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
//移除一些弱引用對(duì)象
removeWeaklyReachableObjects()
//這里使用了隨機(jī)數(shù)生成key
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
//將觀察對(duì)象用WeakRefrence引用 并且使用RefrenceQueue來接收銷毀的對(duì)象
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}
watchedObjects[key] = reference
checkRetainedExecutor.execute {
//接著判斷
moveToRetained(key)
}
}
@Synchronized private fun moveToRetained(key: String) {
//刪除不可達(dá)的對(duì)象
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
//分析內(nèi)存泄漏
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
private fun removeWeaklyReachableObjects() {
// 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.
var ref: KeyedWeakReference?
//queue中存在的對(duì)象是已經(jīng)被回收的對(duì)象
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
上面的方法也很簡(jiǎn)單 就是將我們要觀察的對(duì)象 用WeakRefrence和RefrenceQueue對(duì)象來進(jìn)行包裝
如果gc之后 對(duì)象被回收 那么會(huì)將回收的對(duì)象放入RefrenceQueue中
如果retainedRef不為null 那么開始分析HProf
監(jiān)控總結(jié)
我們發(fā)現(xiàn) LeakCanary的監(jiān)控原理其實(shí)也比較簡(jiǎn)單 就是在OnDestroy之后用WeakRefrence來檢查Activity/Fragment是否泄漏
分析
接著分析泄漏
剛最后調(diào)用了onObjectRetainedListeners.forEach { it.onObjectRetained() }
我們發(fā)現(xiàn)AppWatcher繼承了onObjectRetainedListener
忽略一些簡(jiǎn)單方法 最后會(huì)調(diào)用到
private fun checkRetainedObjects(reason: String) {
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
return
}
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
//再gc一次
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//檢查保留數(shù)量
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
onRetainInstanceListener.onEvent(DebuggerIsAttached)
//顯示通知彈窗
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_debugger_attached
)
)
scheduleRetainedObjectCheck(
reason = "debugger is attached",
rescheduling = true,
delayMillis = WAIT_FOR_DEBUG_MILLIS
)
return
}
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
onRetainInstanceListener.onEvent(DumpHappenedRecently)
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
)
scheduleRetainedObjectCheck(
reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
rescheduling = true,
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
//取消通知彈窗
dismissRetainedCountNotification()
//Dump Heap 堆轉(zhuǎn)儲(chǔ)
dumpHeap(retainedReferenceCount, retry = true)
}
在dumpHeap方法中 我們先看一下如何生成HProf文件(省略了一些代碼)
override fun dumpHeap(): File? {
val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null
......
return try {
//生成HProf文件
Debug.dumpHprofData(heapDumpFile.absolutePath)
if (heapDumpFile.length() == 0L) {
SharkLog.d { "Dumped heap file is 0 byte length" }
null
} else {
heapDumpFile
}
} catch (e: Exception) {
SharkLog.d(e) { "Could not dump heap" }
// Abort heap dump
null
} finally {
cancelToast(toast)
notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
}
}
然后調(diào)用HeapAnalyzerService.runAnalysis(application, heapDumpFile)開啟分AnalyzerService
HeapAnalyzerService這部分我們?cè)诰€上LeakCanary里 其實(shí)可以閹割掉 只需要想辦法把Hprof刪除保留有效數(shù)據(jù) 并傳回服務(wù)端就好了
AnalyzerService會(huì)調(diào)用analyzerHeap來分析內(nèi)存泄漏并生成圖表
private fun analyzeHeap(
heapDumpFile: File,
config: Config
): HeapAnalysis {
val heapAnalyzer = HeapAnalyzer(this)
val proguardMappingReader = try {
ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
} catch (e: IOException) {
null
}
return heapAnalyzer.analyze(
heapDumpFile = heapDumpFile,
leakingObjectFinder = config.leakingObjectFinder,
referenceMatchers = config.referenceMatchers,
computeRetainedHeapSize = config.computeRetainedHeapSize,
objectInspectors = config.objectInspectors,
metadataExtractor = config.metadataExtractor,
proguardMapping = proguardMappingReader?.readProguardMapping()
)
}
最后會(huì)調(diào)用
Hprof.open(heapDumpFile)
.use { hprof ->
val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
val helpers =
FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
helpers.analyzeGraph(
metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
)
}
生成圖表
ARTHook監(jiān)控不合理圖片
內(nèi)存優(yōu)化的過程中 Bitmap優(yōu)化肯定是其中之一 我們可能需要監(jiān)測(cè)大圖 或者監(jiān)測(cè)重復(fù)圖 現(xiàn)在一張圖的內(nèi)存可能就占用1M 解決一張重復(fù)的 就可以省下1M內(nèi)存
優(yōu)化方法
-
使用統(tǒng)一接口
我們可以使用統(tǒng)一接口來設(shè)置圖片 在接口層 我們可以監(jiān)控大圖或者重復(fù)圖片
弊端:程序員可能會(huì)忘記使用統(tǒng)一接口導(dǎo)致監(jiān)控遺漏
-
使用ART Hook
我們可以使用Epic框架,Epic 是一個(gè)在虛擬機(jī)層面、以 Java Method 為粒度的 運(yùn)行時(shí) AOP Hook 框架。簡(jiǎn)單來說,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以攔截本進(jìn)程內(nèi)部幾乎任意的 Java 方法調(diào)用,可用于實(shí)現(xiàn) AOP 編程、運(yùn)行時(shí)插樁、性能分析、安全審計(jì)等。
但是在使用Epic的過程中 也遇到很多奇葩無(wú)解問題 等待作者解決
Epic使用方法
我們這邊使用Epic來Hook所有setImageBitmap來監(jiān)控Bitmap是否過大
public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView, ((ImageView) param.thisObject).getDrawable());
}
private void checkBitmap(final ImageView imageView, Drawable drawable) {
if (imageView != null && drawable != null) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
int height = imageView.getLayoutParams().height;
int width = imageView.getLayoutParams().width;
if (height > 0 && width > 0) {
if (bitmap.getHeight() >= height << 1
&& bitmap.getWidth() >= width << 1) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
final Throwable stackTrace = new RuntimeException();
//還咩有初始化完成
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = imageView.getWidth();
int h = imageView.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
.append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
.append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
.toString();
LogUtils.i(warnInfo);
}
}
代碼很簡(jiǎn)單 就是獲取Bitmap和ImageView的width 和height 然后對(duì)比是否超過兩倍大
內(nèi)存必解Bitmap
其他內(nèi)存優(yōu)化點(diǎn)
-
設(shè)備分級(jí)
我們可以對(duì)設(shè)備性能進(jìn)行分級(jí) 4G內(nèi)存的手機(jī)和1G內(nèi)存的手機(jī)運(yùn)行肯定是不一樣的
比如一些動(dòng)畫我們可以在13年之后的手機(jī)開啟 10年之前的手機(jī)不開啟任何動(dòng)畫
可以參考device-year-class庫(kù) -
緩存管理
我們需要一套統(tǒng)一的緩存管理機(jī)制 當(dāng)遇到LMK時(shí) 果斷釋放占有內(nèi)存 減小被殺幾率 我們可以使用
OnTrimMemory回調(diào) 根據(jù)不同的狀態(tài)決定釋放不同的內(nèi)存 -
進(jìn)程模型
減少應(yīng)用啟動(dòng)的進(jìn)程數(shù),常駐的進(jìn)程數(shù) 有節(jié)操的保活 對(duì)低端機(jī)優(yōu)化很有效
-
安裝包大小
安裝包的代碼 資源 圖片以及so都跟占有內(nèi)存有很大的關(guān)系 所以我們可以針對(duì)低端機(jī)型退出Lite版本
-
統(tǒng)一圖片庫(kù)
收攏圖片庫(kù)的使用 統(tǒng)一使用自研庫(kù)或者Glide,Fresco等 低端機(jī)使用565,更嚴(yán)格的縮放策略
而且可以進(jìn)一步的將Bitmap.createBitmap,BitmapFactory相關(guān)接口收攏 方便監(jiān)控 -
統(tǒng)一監(jiān)控
可以采用接口的方式 也可以采用ARTHook的方式 不過ART Hook在實(shí)驗(yàn)室環(huán)境沒什么關(guān)系 在實(shí)驗(yàn)室環(huán)境,遇到內(nèi)存不合理或者圖片合理 可以立即彈窗提醒開發(fā)人員解決 但是在線上環(huán)境 我們要更多的考慮穩(wěn)定性和容錯(cuò)性
線上監(jiān)控方案
-
java內(nèi)存泄漏
我們可以簡(jiǎn)歷類似LeakCanary的線上方案 裁剪大部分圖片對(duì)應(yīng)的byte數(shù)組 再使用壓縮 進(jìn)一步提高文件上傳的成功率
-
native內(nèi)存泄漏
native內(nèi)存泄漏往往很難采集 可以參考 《微信Android終端內(nèi)存優(yōu)化實(shí)踐》
-
采集方式
用戶在前臺(tái)時(shí) 我們可以每五分鐘采集一次PSS,JAVA堆,圖片總內(nèi)存,建議按照用戶抽樣 而不是按照次抽樣
容災(zāi)方案參考
我們可以在一些特殊的時(shí)間點(diǎn) 重啟應(yīng)用 釋放一些已經(jīng)泄漏的內(nèi)存 可以更好的提高用戶體驗(yàn) 下面參考自微信:
微信是否在主界面退到后臺(tái) 且 位于后臺(tái)的時(shí)間超過 30 分鐘
當(dāng)前時(shí)間為凌晨 2~5 點(diǎn)
不存在前臺(tái)服務(wù)(存在通知欄,音樂播放欄等情況)
java heap 必須大于當(dāng)前進(jìn)程最大可分配的 85% || native 內(nèi)存大于 800M || vmsize 超過了 4G(微信 32bit)的 85%
非大量的流量消耗(每分鐘不超過 1M) && 進(jìn)程無(wú)大量 CPU 調(diào)度情況