前言
内存问题很常见 而且经常会因为内存问题引起卡顿问题 在接下来的卡顿分析中 内存也是一个很重要的方向
内存抖动
内存抖动是由频繁gc导致产生 由于内存空间的不足 回导致频繁gc 在Profile中查看是锯齿状
内存抖动实战
我们可以通过Profile来分析memory Profile的优势大概就是图表非常直观 我们一般可以配合使用mat来解决内存泄漏的问题
@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);
}
};
我们通过频繁申请大对象来模拟内存抖动 我们来观察Profile
WechatIMG3.pngemmmm 现在的手机还是很厉害啊 没有锯齿状 但是可以看到gc非常频繁 这时候我们就可以dump heap
然后看一下内存的主要消耗
WechatIMG6.jpeg
我们主要看Shallow Size 和Retained Siz
Jvm内存分配
JVM内存分配主要分为以下几个部分
- 方法区 (常量 静态变量 编译之后代码)
- 程序计数器 (计算当前线程的当前方法执行到多少行)
- 虚拟机栈 (java对象引用)
- 本地方法栈 (native对象引用)
- 堆 (生成的对象)
工具选择
Profile
Profile
是Android Studio自带的工具 我们可以用来查看Cpu Memory Network Energy的消耗
我们可以使用Profile来定位内存抖动问题 因为可以很明显的看到锯齿状或者频繁gc的情况
也可以用Cpu Profile 或者 Memory Profile来查看内存泄漏问题
Profile提供了Fragment/Activity的监测 还可以通过包名等方式 来查看内存中的泄漏问题
可以定位查看 内存中是否存在不合理的对象
MAT
MAT全称Memory Anlyzer Tools
,是一款可以分析Hprof文件的可视化工具 我们可以使用Mat工具
查找定位我们预设的怀疑点 通过exclude weak soft等引用 获取到gc到对象的引用路径 帮助我们解决问题
LeakCanary源码解析
在2.0之前的版本 需要我们手动调用Install方法 在2.0之后 LeakCanary注册了ContentProvider
不需要手动的调用Install
LeakCanary分为两部分 监控和分析
监控
查看LeakCanary源码 发现AppWatcherInstaller
类,继承ContentProvider
在Oncreate方法中调用了AppWatcher.manualInstall(application)
然后AppWatcher
中调用了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 }
//监控Activity 这里传递了ObjectWatcher用来监控Object对象
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
//监控Fragment
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)
}
我们已经察觉到了 关键代码就在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)
//注册生命周期
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
我们可能发现了LeakCanary
的原理 就是监听Activity
的生命周期回调 在OnDestroy
之后 使用objectWatcher
去观察Activity
是否有被回收 如果没有回收 就表示泄漏了
Follow Me,胜利就在前面了!!!
接着查看objectWatcher
的watch
方法
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
//移除一些弱引用对象
removeWeaklyReachableObjects()
//这里使用了随机数生成key
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
//将观察对象用WeakRefrence引用 并且使用RefrenceQueue来接收销毁的对象
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) {
//删除不可达的对象
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
//分析内存泄漏
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中存在的对象是已经被回收的对象
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
上面的方法也很简单 就是将我们要观察的对象 用WeakRefrence和RefrenceQueue对象来进行包装
如果gc之后 对象被回收 那么会将回收的对象放入RefrenceQueue中
如果retainedRef不为null 那么开始分析HProf
监控总结
我们发现 LeakCanary的监控原理其实也比较简单 就是在OnDestroy之后用WeakRefrence来检查Activity/Fragment是否泄漏
分析
接着分析泄漏
刚最后调用了onObjectRetainedListeners.forEach { it.onObjectRetained() }
我们发现AppWatcher继承了onObjectRetainedListener
忽略一些简单方法 最后会调用到
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
}
//检查保留数量
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 堆转储
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)
}
}
然后调用HeapAnalyzerService.runAnalysis(application, heapDumpFile)
开启分AnalyzerService
HeapAnalyzerService这部分我们在线上LeakCanary里 其实可以阉割掉 只需要想办法把Hprof删除保留有效数据 并传回服务端就好了
AnalyzerService会调用analyzerHeap来分析内存泄漏并生成图表
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()
)
}
最后会调用
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监控不合理图片
内存优化的过程中 Bitmap优化肯定是其中之一 我们可能需要监测大图 或者监测重复图 现在一张图的内存可能就占用1M 解决一张重复的 就可以省下1M内存
优化方法
-
使用统一接口
我们可以使用统一接口来设置图片 在接口层 我们可以监控大图或者重复图片
弊端:程序员可能会忘记使用统一接口导致监控遗漏
-
使用ART Hook
我们可以使用Epic框架,Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。
但是在使用Epic的过程中 也遇到很多奇葩无解问题 等待作者解决
Epic使用方法
我们这边使用Epic来Hook所有setImageBitmap来监控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);
}
}
代码很简单 就是获取Bitmap
和ImageView
的width
和height
然后对比是否超过两倍大
内存必解Bitmap
其他内存优化点
-
设备分级
我们可以对设备性能进行分级 4G内存的手机和1G内存的手机运行肯定是不一样的
比如一些动画我们可以在13年之后的手机开启 10年之前的手机不开启任何动画
可以参考device-year-class
库 -
缓存管理
我们需要一套统一的缓存管理机制 当遇到LMK时 果断释放占有内存 减小被杀几率 我们可以使用
OnTrimMemory
回调 根据不同的状态决定释放不同的内存 -
进程模型
减少应用启动的进程数,常驻的进程数 有节操的保活 对低端机优化很有效
-
安装包大小
安装包的代码 资源 图片以及so都跟占有内存有很大的关系 所以我们可以针对低端机型退出Lite版本
-
统一图片库
收拢图片库的使用 统一使用自研库或者Glide,Fresco等 低端机使用565,更严格的缩放策略
而且可以进一步的将Bitmap.createBitmap,BitmapFactory相关接口收拢 方便监控 -
统一监控
可以采用接口的方式 也可以采用ARTHook的方式 不过ART Hook在实验室环境没什么关系 在实验室环境,遇到内存不合理或者图片合理 可以立即弹窗提醒开发人员解决 但是在线上环境 我们要更多的考虑稳定性和容错性
线上监控方案
-
java内存泄漏
我们可以简历类似LeakCanary的线上方案 裁剪大部分图片对应的byte数组 再使用压缩 进一步提高文件上传的成功率
-
native内存泄漏
native内存泄漏往往很难采集 可以参考 《微信Android终端内存优化实践》
-
采集方式
用户在前台时 我们可以每五分钟采集一次PSS,JAVA堆,图片总内存,建议按照用户抽样 而不是按照次抽样
容灾方案参考
我们可以在一些特殊的时间点 重启应用 释放一些已经泄漏的内存 可以更好的提高用户体验 下面参考自微信:
-
微信是否在主界面退到后台 且 位于后台的时间超过 30 分钟
-
当前时间为凌晨 2~5 点
-
不存在前台服务(存在通知栏,音乐播放栏等情况)
-
java heap 必须大于当前进程最大可分配的 85% || native 内存大于 800M || vmsize 超过了 4G(微信 32bit)的 85%
-
非大量的流量消耗(每分钟不超过 1M) && 进程无大量 CPU 调度情况
网友评论