前言

首先我们先搞清楚一个问题,什么是Bitmap重复,顾名思义,就是两个相同的bitmap在内存中同时存在,但这样讲并不严谨,为什么?因为bitmap的内存主要是分两部分,bitmap对象+像素数据,严格意义上说的是像素数据重复,而在虚拟机中有两个不同的bitmap对象。当遇到像素数据不断重复,导致内存被重复占用,如果不去管理,越来越多的话,随之而来的问题就是,可能会导致OOM,也可能容易导致频繁GC等,所以我们想办法去主动检测出重复的像素数据,主动将其解决掉。才是我们优化的目的

Bitmap存哪里?

搞清它存在哪里,也是我们找对检测方法的关键,那么bitmap到底存在哪里了呢?其实随着Android系统的迭代,且考虑到Bitmap是重量级的内存大户,也是不断的优化存储的位置,下面列表给你们总结一下

Android版本 存储位置 备注
3.0以前 Bitmap对象在堆中,像素数据存在Native内存中 必须手动recycle
3.0~7.0 Bitmap对象及像素数据均放堆中,让虚拟机自动管理 缺点也很明显,很容易造成内存占用过多
8.0及以后 像素数据回归Native中,但这次加了辅助回收Native
NativeAllocationRegistry.java源码请看

如何检测出重复呢?

其实也没什么好的办法,我们需使用内存 Hprof 分析工具,然后将重复图片和引用堆栈输出。可是这种方法只能分析Android 8.0以前的系统哦,因为8.0已经不在堆中缓存,那你肯定是检测不到了。那么Matrix是如何检测的呢?它有限制吗?我现在也不知道,往下看,我们一起分析。
我们先看一个简单的例子:
第一步
创建一个demo,加载两个相同的bitmap图片。
image.png
第二步
1 打开Profiler-> 2 选择bitmapdemo进程-> 3 选择MEMORY -> 4 Dump Heap -> 5 将鼠标指针停留该处,执行第二张图中操作磁盘保存按钮 Export Heap Dump
image.png
image.png
选择保存位置
image.png
第三步
下载分析jar包DuplicatedBitmapAnalyzer-1.0.jar,地址,将项目中tools目录中的jar拷贝到项目的根目录中,然后在项目的根目录执行如下命令

  1. java -jar DuplicatedBitmapAnalyzer-1.0.jar 刚才保存的文件名字.hprof

执行结果如下图:
image.png
可以看到我们找到了两个重复的图片。代码是如何实现的呢?
下面引用DuplicatedBitmapAnalyzer项目介绍:
使用 sqaure 的HAHA

  1. // 打开hprof文件
  2. final HeapSnapshot heapSnapshot = new HeapSnapshot(hprofFile);
  3. // 获得snapshot
  4. final Snapshot snapshot = heapSnapshot.getSnapshot();
  5. // 获得Bitmap Class
  6. final ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap");
  7. // 获得heap, 只需要分析app和default heap即可
  8. Heap heap = snapshot.getHeaps();
  9. // 从heap中获得所有的Bitmap实例
  10. final List<Instance> bitmapInstances = bitmapClass.getHeapInstances(heap.getId());
  11. // 从Bitmap实例中获得buffer数组
  12. ArrayInstance buffer = HahaHelper.fieldValue(((ClassInstance) bitmapInstance).getValues(), "mBuffer");

整个思路就是通过”mBuffer”的hash值判断哪些Bitmap是重复的。这种方式适用于开发阶段,想要检测的时候还是可以很容易的发现项目中的一些重复图片,可如果想在线上检查呢?是不是就需要纯代码实现呢?对的,下面就来看下Matrix是如何实现的把

Matrix Bitmap 重复检测源码分析

image.png
通过对源码的大致浏览,其实可以将其分成以上三个部分,其实最主要的就是前两个,一个主要用于分析matrix-resource-canary-analyzer 一个用户生成分析文件matrix-resource-canary-android 看下android项目的依赖,并没有发现matrix-resource-canary-analyzer,那它是如何分析出报告返回到App应用呢?随我来看下matrix-resource-canary-android的实现。
image.png

ResourcePlugin

我们知道在使用matrix 的时候,需要new一些plugin,那么这个ResourcePlugin其实就是我们要分析的主角,首先看到它的init方法。

  1. @Override
  2. public void init(Application app, PluginListener listener) {
  3. super.init(app, listener);
  4. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
  5. MatrixLog.e(TAG, "API is low Build.VERSION_CODES.ICE_CREAM_SANDWICH(14), ResourcePlugin is not supported");
  6. unSupportPlugin();
  7. return;
  8. }
  9. mWatcher = new ActivityRefWatcher(app, this);
  10. }

首先告诉我们的是,Android sdk 版本小于14的话,是不支持的。然后初始化了一个ActivityRefWatcher类,来看下它

  1. //Android生命周期监听,与我们分析相关的功能:弹出Dump内存的Dialog、Dump内存数据、读取内存数据裁剪Hprof文件、生成包含裁剪后的Hprof
  2. public class ActivityRefWatcher extends FilePublisher implements Watcher, IAppForeground
  3. //不贴代码了哈,总之这里用了SharedPreferences作为二级缓存,HashMap<String, Long>作为一级缓存,通过检索发现是在缓存mActivityName
  4. public class FilePublisher extends IssuePublisher
  5. //监听?开始结束释放,应该是监听什么东西
  6. public interface Watcher {
  7. void start();
  8. void stop();
  9. void destroy();
  10. }
  11. public interface IAppForeground {
  12. //应用前台和后台的状态
  13. void onForeground(boolean isForeground);
  14. }

我们其实是想找到dump heap对吧,打开代码目录看一番,又发现如图
image.png
看log发现 hprof file is null 检测,没跑了就是它,这下我们就可以寻找它在哪里被调用,然后去看看它到底在什么时候开始这个操作的。

  1. if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP) {
  2. final File hprofFile = mHeapDumper.dumpHeap(true);

发现mDumpHprofMode的值是 AUTO_DUMP 的时候,那么mDumpHprofMode的值是什么时候被改的呢?

  1. private ResourceConfig(IDynamicConfig dynamicConfig, DumpMode dumpHprofMode, boolean detectDebuger, Intent pendingIntent) {
  2. this.mDynamicConfig = dynamicConfig;
  3. this.mDumpHprofMode = dumpHprofMode;
  4. mDetectDebugger = detectDebuger;
  5. mContentIntent = pendingIntent;
  6. }

通过检索,我发现它是在ResourceConfig中初始化,那么就可以肯定了,这里并不是触发时机,而是一个开关控制,那我们再回到if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP)的函数中,看这个函数是谁执行的,发现它是ActivityRefWatcher的私有成员变量

  1. private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {
  2. @Override
  3. public Status execute() {
  4. // If destroyed activity list is empty, just wait to save power.
  5. if (mDestroyedActivityInfos.isEmpty()) {
  6. MatrixLog.i(TAG, "DestroyedActivityInfo isEmpty!");
  7. return Status.RETRY;
  8. }
  9. // Fake leaks will be generated when debugger is attached.
  10. if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
  11. MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
  12. return Status.RETRY;
  13. }
  14. // 该对象就是为了检测GC有没有触发,这是由于弱引用的特性,在发生GC的时候会被虚拟机回收,如果未回收,那么GC肯定没发生
  15. final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
  16. // 通过Runtime.getRuntime().gc();触发GC操作;
  17. triggerGc();
  18. // 如果未发生GC,等待下一次
  19. if (sentinelRef.get() != null) {
  20. // System ignored our gc request, we will retry later.
  21. // 系统忽略我们的gc请求,等待下一次检测。
  22. MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
  23. return Status.RETRY;
  24. }
  25. // mDestroyedActivityInfos 基于链接节点的无界线程安全队列,用于存放 被执行onActivityDestroyed的Activity对象
  26. final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
  27. //跳过,我们分析的不是它
  28. while (infoIt.hasNext()) {
  29. final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
  30. if (!mResourcePlugin.getConfig().getDetectDebugger() && isPublished(destroyedActivityInfo.mActivityName) && mDumpHprofMode != ResourceConfig.DumpMode.SILENCE_DUMP) {
  31. MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
  32. infoIt.remove();
  33. continue;
  34. }
  35. if (destroyedActivityInfo.mActivityRef.get() == null) {
  36. // The activity was recycled by a gc triggered outside.
  37. MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey);
  38. infoIt.remove();
  39. continue;
  40. }
  41. ++destroyedActivityInfo.mDetectedCount;
  42. if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
  43. && !mResourcePlugin.getConfig().getDetectDebugger()) {
  44. // Although the sentinel tell us the activity should have been recycled,
  45. // system may still ignore it, so try again until we reach max retry times.
  46. MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"
  47. + "exists in %s times, wait for next detection to confirm.",
  48. destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);
  49. continue;
  50. }
  51. MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance. mode[%s]", destroyedActivityInfo.mKey, mDumpHprofMode);
  52. //判断DumpMode,SILENCE_DUMP默认模式,AUTO_DUMP 该模式会执行Android的dumpHeap, MANUAL_DUMP 该模式是将泄漏的类名发送到通知
  53. if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP) {
  54. if (!isPublished(destroyedActivityInfo.mActivityName)) {
  55. final JSONObject resultJson = new JSONObject();
  56. try {
  57. resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
  58. } catch (JSONException e) {
  59. MatrixLog.printErrStackTrace(TAG, e, "unexpected exception.");
  60. }
  61. markPublished(destroyedActivityInfo.mActivityName);
  62. // 将泄漏的类名回调给 onDetectIssue
  63. mResourcePlugin.onDetectIssue(new Issue(resultJson));
  64. }
  65. // 将泄漏的类名回调给 onLeak
  66. if (null != activityLeakCallback) {
  67. activityLeakCallback.onLeak(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey);
  68. }
  69. } else if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP) {
  70. final File hprofFile = mHeapDumper.dumpHeap(true);
  71. if (hprofFile != null) {
  72. markPublished(destroyedActivityInfo.mActivityName);
  73. final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
  74. // 将 dump 信息进行压缩、上报
  75. mHeapDumpHandler.process(heapDump);
  76. infoIt.remove();
  77. } else {
  78. MatrixLog.i(TAG, "heap dump for further analyzing activity with key [%s] was failed, just ignore.",
  79. destroyedActivityInfo.mKey);
  80. infoIt.remove();
  81. }
  82. } else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP) {
  83. NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
  84. String dumpingHeapContent = context.getString(R.string.resource_canary_leak_tip);
  85. String dumpingHeapTitle = destroyedActivityInfo.mActivityName;
  86. mContentIntent.putExtra(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
  87. mContentIntent.putExtra(SharePluginInfo.ISSUE_REF_KEY, destroyedActivityInfo.mKey);
  88. PendingIntent pIntent = PendingIntent.getActivity(context, 0, mContentIntent,
  89. PendingIntent.FLAG_UPDATE_CURRENT);
  90. NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
  91. .setContentTitle(dumpingHeapTitle)
  92. .setContentIntent(pIntent)
  93. .setContentText(dumpingHeapContent);
  94. Notification notification = buildNotification(context, builder);
  95. notificationManager.notify(NOTIFICATION_ID, notification);
  96. infoIt.remove();
  97. markPublished(destroyedActivityInfo.mActivityName);
  98. MatrixLog.i(TAG, "show notification for notify activity leak. %s", destroyedActivityInfo.mActivityName);
  99. } else {
  100. // 否则将泄漏的类名回调给 onDetectIssue即可
  101. // Lightweight mode, just report leaked activity name.
  102. MatrixLog.i(TAG, "lightweight mode, just report leaked activity name.");
  103. markPublished(destroyedActivityInfo.mActivityName);
  104. final JSONObject resultJson = new JSONObject();
  105. try {
  106. resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
  107. } catch (JSONException e) {
  108. MatrixLog.printErrStackTrace(TAG, e, "unexpected exception.");
  109. }
  110. mResourcePlugin.onDetectIssue(new Issue(resultJson));
  111. }
  112. }
  113. return Status.RETRY;
  114. }
  115. };

好,现在找一下上面的execute方法是时间时候被执行的呢?我通过检索后发现

  1. // 递归循环调用
  2. private void postToMainThreadWithDelay(final RetryableTask task, final int failedAttempts) {
  3. mMainHandler.postDelayed(new Runnable() {
  4. @Override
  5. public void run() {
  6. RetryableTask.Status status = task.execute();
  7. if (status == RetryableTask.Status.RETRY) {
  8. postToMainThreadWithDelay(task, failedAttempts + 1);
  9. }
  10. }
  11. }, mDelayMillis);
  12. }
  13. // mDelayMillis 默认值是这个,默认一分钟一次
  14. private static final long DEFAULT_DETECT_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(1);
  15. //最终找到 mScanDestroyedActivitiesTask 任务其实是在应用进入前台的时候开始无限循环。
  16. @Override
  17. public void onForeground(boolean isForeground) {
  18. if (isForeground) {
  19. MatrixLog.i(TAG, "we are in foreground, modify scan time[%sms].", mFgScanTimes);
  20. mDetectExecutor.clearTasks();
  21. // 后台默认1分钟检测一次
  22. mDetectExecutor.setDelayMillis(mFgScanTimes);
  23. mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
  24. } else {
  25. // 后台默认20分钟检测一次
  26. MatrixLog.i(TAG, "we are in background, modify scan time[%sms].", mBgScanTimes);
  27. mDetectExecutor.setDelayMillis(mBgScanTimes);
  28. }
  29. }

从这些逻辑我们可以确定:

  1. 应用前台后,1分钟检测一次
  2. 后台后,20分钟检测一次
  3. 当检测到内存泄漏后,如果mDumpHprofMode是AUTO_DUMP模式,那么才会heap dump ,最终上传?上传哪呢?继续看 ```java

    1. new AndroidHeapDumper.HeapDumpHandler() {
    2. @Override
    3. public void process(HeapDump result) {
    4. // 最终调用的是CanaryWorkerService
    5. CanaryWorkerService.shrinkHprofAndReport(context, result);
    6. }
    7. };

    //就是一个普通的service public class CanaryWorkerService extends MatrixJobIntentService public abstract class MatrixJobIntentService extends Service // CanaryWorkerService中处理Intent消息 @Override protected void onHandleWork(Intent intent) {

    1. if (intent != null) {
    2. final String action = intent.getAction();
    3. if (ACTION_SHRINK_HPROF.equals(action)) {
    4. try {
    5. intent.setExtrasClassLoader(this.getClassLoader());
    6. final HeapDump heapDump = (HeapDump) intent.getSerializableExtra(EXTRA_PARAM_HEAPDUMP);
    7. if (heapDump != null) {
    8. //
    9. doShrinkHprofAndReport(heapDump);
    10. } else {
    11. MatrixLog.e(TAG, "failed to deserialize heap dump, give up shrinking and reporting.");
    12. }
    13. } catch (Throwable thr) {
    14. MatrixLog.printErrStackTrace(TAG, thr, "failed to deserialize heap dump, give up shrinking and reporting.");
    15. }
    16. }
    17. }

    } // 裁剪 private void doShrinkHprofAndReport(HeapDump heapDump) {

    1. final File hprofDir = heapDump.getHprofFile().getParentFile();
    2. // 生成裁剪后文件 shrinkedHProfFile
    3. final File shrinkedHProfFile = new File(hprofDir, getShrinkHprofName(heapDump.getHprofFile()));
    4. final File zipResFile = new File(hprofDir, getResultZipName("dump_result_" + android.os.Process.myPid()));
    5. final File hprofFile = heapDump.getHprofFile();
    6. ZipOutputStream zos = null;
    7. try {
    8. long startTime = System.currentTimeMillis();
    9. // 将裁剪结果写入 shrinkedHProfFile 文件中
    10. new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile);
    11. MatrixLog.i(TAG, "shrink hprof file %s, size: %dk to %s, size: %dk, use time:%d",
    12. hprofFile.getPath(), hprofFile.length() / 1024, shrinkedHProfFile.getPath(), shrinkedHProfFile.length() / 1024, (System.currentTimeMillis() - startTime));
    13. // 压缩文件
    14. zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipResFile)));
    15. final ZipEntry resultInfoEntry = new ZipEntry("result.info");
    16. final ZipEntry shrinkedHProfEntry = new ZipEntry(shrinkedHProfFile.getName());
    17. zos.putNextEntry(resultInfoEntry);
    18. final PrintWriter pw = new PrintWriter(new OutputStreamWriter(zos, Charset.forName("UTF-8")));
    19. pw.println("# Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !!");
    20. pw.println("sdkVersion=" + Build.VERSION.SDK_INT);
    21. pw.println("manufacturer=" + Build.MANUFACTURER);
    22. pw.println("hprofEntry=" + shrinkedHProfEntry.getName());
    23. pw.println("leakedActivityKey=" + heapDump.getReferenceKey());
    24. pw.flush();
    25. zos.closeEntry();
    26. zos.putNextEntry(shrinkedHProfEntry);
    27. copyFileToStream(shrinkedHProfFile, zos);
    28. zos.closeEntry();
    29. shrinkedHProfFile.delete();
    30. hprofFile.delete();
    31. MatrixLog.i(TAG, "process hprof file use total time:%d", (System.currentTimeMillis() - startTime));
    32. // 上报文件,最终调用在下面
    33. CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName());
    34. } catch (IOException e) {
    35. MatrixLog.printErrStackTrace(TAG, e, "");
    36. } finally {
    37. closeQuietly(zos);
    38. }

    } //转JSON后 回调到onDetectIssue中 private void doReportHprofResult(String resultPath, String activityName) {

    1. try {
    2. final JSONObject resultJson = new JSONObject();

    // resultJson = DeviceUtil.getDeviceInfo(resultJson, getApplication());

    1. resultJson.put(SharePluginInfo.ISSUE_RESULT_PATH, resultPath);
    2. resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, activityName);
    3. Plugin plugin = Matrix.with().getPluginByClass(ResourcePlugin.class);
    4. if (plugin != null) {
    5. plugin.onDetectIssue(new Issue(resultJson));
    6. }
    7. } catch (Throwable thr) {
    8. MatrixLog.printErrStackTrace(TAG, thr, "unexpected exception, skip reporting.");
    9. }

    }

  1. 经过这么一些列的操作,接下来就该分析hprof文件了吧,现在也基本明白,Matrixdump heap过程,比起上面的例子,其实就是多了一些自动化获取和裁剪的过程。具体分析的话肯定是通过matrix-resource-canary-analyzer它的jar来分析喽,这样整个环节是不是就清楚了?那analyzer相对于上面的**DuplicatedBitmapAnalyzer项目又有什么不同呢?请看博客:**<br />[Matrix ResourceCanary -- Activity 泄漏及Bitmap冗余检测](https://cloud.tencent.com/developer/article/1379397)<br />[检测内存中重复的Bitmap](https://lyldalek.top/2020/08/19/blog_bak/Blog/Android-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/Matrix/015-Matrix%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E6%A3%80%E6%B5%8B%E5%86%85%E5%AD%98%E4%B8%AD%E9%87%8D%E5%A4%8D%E7%9A%84Bitmap/)<br />里面写的特别详细,我就不重复写了。
  2. <a name="xjoGk"></a>
  3. ## 总结一下吧
  4. 前台一分钟检测一次泄漏,后台二十分钟检测一次泄漏,总体性能影响来说:
  5. 1. 不在线上开启dump heap,这会严重影响性能,建议开发阶段将bitmap重复的问题搞定并解决
  6. 1. 如果手机性能不好,可以将检测延迟时间拉长,目前官方是1分钟前台,可以根据手机适当放慢(为什么这么讲:因为官方测试,设定的轮询间隔为1min。以通讯录界面和朋友圈界面的帧率作为参考,在接入ResourceCanary2min内的平均帧率降低了10帧左右),所以为了降低对差手机的影响,我觉得可以适当 调为2分钟,或者甚至是5分钟。
  7. <a name="Eu9nA"></a>
  8. ## 对了还有个疑问?Matrix在8.0以上的系统中可以检测bitmap吗?
  9. 答案是不可以,请看源码:<br />是吧,26就是8.0,只有小于8.0才支持的。
  10. ```java
  11. if (sdkVersion < 26) {
  12. final ExcludedBmps excludedBmps = AndroidExcludedBmpRefs.createDefaults().build();
  13. duplicatedBmpResult = new DuplicatedBitmapAnalyzer(mMinBmpLeakSize, excludedBmps).analyze(heapSnapshot);
  14. } else {
  15. System.err.println("\n ! SDK version of target device is larger or equal to 26, "
  16. + "which is not supported by DuplicatedBitmapAnalyzer.");
  17. }