5、内存优化(下):LeakCanary原理与使用、MAT工具分析堆转储、内存抖动优化、Bitmap内存管理

好,咱们接着聊内存优化。上一章我们把基础概念和工具链理清了,这一章直接上硬菜——内存泄漏、堆转储分析、内存抖动,还有那个让人又爱又恨的Bitmap。这几个问题,说白了就是Android性能优化的“四大天王”,你绕不开,也躲不掉。

我个人习惯是,先讲原理,再给工具,最后说实战。这样你心里有谱,手上也有招。

5.1 LeakCanary:内存泄漏的“照妖镜”

先说说LeakCanary。这玩意儿,我愿称之为Android开发者的“救命稻草”。为什么?因为内存泄漏这东西,你肉眼根本看不出来。代码跑着跑着,内存就涨上去了,你甚至不知道是哪行代码惹的祸。

LeakCanary的原理其实不复杂。它会在你的App运行时,默默监控那些本该被回收的对象。比如一个Activity销毁了,它就会去检查这个Activity的实例是不是还被某些对象持有。如果发现没被回收,它就立刻dump一份堆内存快照,然后分析出泄漏路径,直接告诉你:“嘿,哥们儿,你的xxx对象泄漏了,是yyy类在搞鬼。”

怎么用?简单到令人发指。在build.gradle里加一行依赖就行:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'

嗯,就这一行。然后你正常跑App,一旦发生泄漏,通知栏就会弹出提示。点进去,你能看到完整的引用链。我曾经在一个老项目里加了这个依赖,结果一跑,好家伙,一口气报了七八个泄漏。当时我冷汗都下来了——这要是上线了,用户手机不卡死才怪。

核心要点:LeakCanary只适合在debug版本使用。release版本千万别加,否则你的APK会变大,而且还会影响性能。

避坑指南:我曾经遇到过一个情况——LeakCanary报了泄漏,但我死活找不到原因。后来发现是第三方SDK里有个单例持有了Activity的引用。这种问题最坑,因为代码不在你手里。我的建议是:遇到第三方SDK的泄漏,先升级版本,不行就换SDK。

5.2 MAT:堆转储分析的“手术刀”

LeakCanary能帮你发现泄漏,但有时候你需要更深入的分析。比如,你想知道某个对象到底占了多少内存,或者想看看哪些对象是“垃圾”但没被回收。这时候,MAT(Memory Analyzer Tool)就派上用场了。

MAT是Eclipse基金会出的一个工具,专门用来分析Java堆转储文件(.hprof)。你可以在Android Studio里dump一份堆快照,然后用MAT打开。它能生成一份详细的报告,告诉你哪些对象占用了大量内存,哪些对象有泄漏嫌疑。

我个人最常用的功能是“Histogram”和“Dominator Tree”。Histogram能列出所有类的实例数量和占用内存大小,Dominator Tree则能告诉你哪些对象是“根”,哪些对象是“叶子”。

举个例子。有一次我怀疑某个图片加载库有内存泄漏,就用MAT分析了一下。结果发现,有个叫“BitmapPool”的类,里面存了上千个Bitmap对象,而且都没被释放。我顺着引用链一查,原来是缓存策略设置得太激进了。调整了一下参数,内存直接降了30%。

注意:MAT分析的是Java堆,不是Native堆。如果你遇到的是Native内存泄漏(比如通过JNI分配的内存),MAT是看不到的。这时候你得用其他工具,比如AddressSanitizer或者Valgrind。

5.3 内存抖动:看不见的“性能杀手”

内存抖动,说白了就是短时间内频繁地创建和销毁对象。这会导致GC(垃圾回收)频繁触发,而GC一跑,你的App就会卡顿。你想想看,用户正刷着朋友圈,突然卡了一下,是不是很烦?

为什么会发生内存抖动?最常见的原因就是循环里创建对象。比如:

for (int i = 0; i < 1000; i++) {
    String temp = new String("hello " + i);
    // 做一些操作
}

这段代码每次循环都会创建一个新的String对象。1000次循环,就是1000个对象。如果这个循环在UI线程里跑,GC就会频繁触发,卡顿就来了。

怎么优化?我的建议是:

  • 复用对象:能用StringBuilder就别用String拼接。
  • 避免在循环里创建对象:把对象创建提到循环外面。
  • 使用对象池:比如Android的Message.obtain(),就是典型的对象池用法。

实战经验:我曾经优化过一个列表滑动卡顿的问题。用Profile工具一看,发现每次滑动都会创建大量的Rect对象。后来我把Rect对象池化,卡顿直接消失了。嗯,就是这么立竿见影。

5.4 Bitmap内存管理:图片优化的“重灾区”

说到Bitmap,我估计每个Android开发者都有一把辛酸泪。Bitmap是内存消耗的大户,一张1920x1080的图片,如果按ARGB_8888格式加载,占用的内存是1920 * 1080 * 4 = 8MB。你想想看,一个列表里加载十几张这样的图片,内存直接爆了。

Bitmap优化的核心思路就四个字:按需加载。什么意思?就是你的ImageView需要多大,你就加载多大的图片。别一张原图直接怼进去,那是暴殄天物。

具体怎么做?我一般用BitmapFactory.Options的inSampleSize参数。比如:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;

// 计算采样率
int inSampleSize = 1;
if (imageHeight > reqHeight || imageWidth > reqWidth) {
    final int halfHeight = imageHeight / 2;
    final int halfWidth = imageWidth / 2;
    while ((halfHeight / inSampleSize) >= reqHeight
            && (halfWidth / inSampleSize) >= reqWidth) {
        inSampleSize *= 2;
    }
}

options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

这段代码的逻辑是:先读取图片的宽高,然后根据目标尺寸计算采样率,最后按采样率加载图片。这样加载出来的Bitmap,内存占用会小很多。

避坑指南:我曾经遇到过一个坑——inSampleSize必须是2的幂次方。如果你设置成3,系统会向下取整到2。所以,别想着用3、5、7这种奇数,老老实实用2的幂次方。

另外,还有几个小技巧:

  • 使用RGB_565格式:如果图片不需要透明度,用RGB_565代替ARGB_8888,内存直接减半。
  • 及时回收:在Android 2.3之前,Bitmap的内存是分配在Native堆的,需要手动调用recycle()。虽然现在不用了,但如果你用完了Bitmap,最好还是把引用置为null,方便GC回收。
  • 使用图片加载库:Glide、Picasso、Fresco这些库,内部已经做了大量的优化。我个人习惯用Glide,因为它支持多种图片格式,还能自动处理缓存。

知识体系总览

下面这张图,是我自己总结的内存优化知识体系。你可以把它当成一张“地图”,随时回来看看。

内存优化 LeakCanary 原理:监控对象回收 使用:debug依赖一行搞定 注意:release版本禁用 MAT工具 分析堆转储文件 Histogram & Dominator Tree 只分析Java堆 内存抖动 频繁GC导致卡顿 优化:复用对象、对象池 Bitmap管理 按需加载、inSampleSize RGB_565、及时回收

好了,这一章的内容就到这里。内存优化是个细活,急不得。你只要把LeakCanary、MAT、内存抖动和Bitmap这几个点吃透了,大部分内存问题都能搞定。剩下的,就是多动手、多踩坑、多总结。