16、Android性能优化之内存优化:内存泄漏常见场景与LeakCanary工具使用
内存泄漏,说白了就是本该被回收的对象,因为被某个地方一直引用着,导致GC拿它没办法。时间一长,堆内存越占越多,最后就是OOM,应用直接闪退。
我刚开始做Android开发那会儿,对内存泄漏其实没什么概念。觉得反正Java有垃圾回收,我管那么多干嘛?结果有一次,我负责的一个新闻资讯App,用户反馈说刷着刷着就卡死了,再打开就闪退。我一看日志,好家伙,OOM了。从那以后,我才真正开始重视内存优化这件事。
这一章,我们就来聊聊内存泄漏最常见的几个场景,以及怎么用LeakCanary这个工具快速定位问题。
16.1 内存泄漏的本质
先搞清楚一个概念:内存泄漏不是内存不够用,而是内存被“占着茅坑不拉屎”。
Java的垃圾回收机制,是通过判断对象是否还有引用链可达来决定的。如果一个对象明明已经没用了,但还有一条引用链连着它,那GC就不会回收它。这就是泄漏。
你想想看,一个Activity被销毁了,按理说它占用的内存应该被回收。但如果有个静态变量还持有它的引用,那这个Activity就永远无法被回收。一次两次还好,次数多了,内存就炸了。
核心观点:内存泄漏 = 长生命周期对象持有短生命周期对象的引用,导致短生命周期对象无法被回收。
16.2 常见内存泄漏场景
16.2.1 Handler 引起的内存泄漏
这个场景我估计很多人都遇到过。Handler在Android里太常用了,但用不好就是个大坑。
看下面这段代码:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
updateUI();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 发送一个延迟消息
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 做一些耗时操作
}
}, 10 * 60 * 1000); // 10分钟后执行
}
}
问题在哪?Handler是匿名内部类,它隐式持有外部类MainActivity的引用。如果Activity被销毁了,但Handler的消息队列里还有未处理的消息,那这个Activity就泄漏了。
我个人习惯的做法是:使用静态内部类 + 弱引用。
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<MainActivity> mActivityRef;
public MyHandler(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivityRef.get();
if (activity != null) {
activity.updateUI();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 移除所有消息和回调
mHandler.removeCallbacksAndMessages(null);
}
}
避坑指南:我曾经在项目里遇到过一个问题,明明用了静态内部类,但泄漏还是发生了。后来发现是忘了在onDestroy里调用removeCallbacksAndMessages。记住,弱引用只是辅助,主动清理才是根本。
16.2.2 单例模式引起的内存泄漏
单例模式本身没问题,问题在于单例的生命周期是整个应用,如果你不小心把Activity的Context传进去了,那就麻烦了。
public class AppManager {
private static AppManager sInstance;
private Context mContext;
private AppManager(Context context) {
// 这里如果传入的是Activity的Context,就会泄漏
this.mContext = context;
}
public static AppManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppManager(context);
}
return sInstance;
}
}
解决办法很简单:用Application的Context。
public class AppManager {
private static AppManager sInstance;
private Context mContext;
private AppManager(Context context) {
// 使用Application的Context
this.mContext = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppManager(context.getApplicationContext());
}
return sInstance;
}
}
注意:不是所有地方都能用Application Context。比如启动Activity、显示Dialog这些操作,必须用Activity的Context。所以设计单例时,要明确你到底需要哪种Context。
16.2.3 静态变量引起的内存泄漏
静态变量的生命周期和类一样长,如果它持有了Activity或Fragment的引用,那泄漏就不可避免了。
我记得有一次排查一个图片加载的OOM问题,发现是某个工具类里用静态变量缓存了Bitmap,而Bitmap又引用了Activity的View。结果Activity销毁了,Bitmap还被静态变量拽着不放。
public class ImageCache {
private static List<Bitmap> sCache = new ArrayList<>();
public static void addBitmap(Bitmap bitmap) {
sCache.add(bitmap);
}
public static void clearCache() {
sCache.clear();
}
}
静态缓存本身不是问题,问题在于没有清理机制。如果你非要用静态缓存,一定要在合适的时机清理,比如在Activity的onDestroy里调用clearCache。
16.2.4 其他常见场景
- 内部类/匿名类:非静态内部类会持有外部类的引用。能写成静态的就写成静态的。
- 集合类:往集合里添加了对象,用完不清理。特别是Map、List这种。
- 资源未关闭:Cursor、FileInputStream、BroadcastReceiver这些,用完一定要close或unregister。
- WebView:WebView的内存泄漏是老生常谈了。建议单独开一个进程加载WebView,或者手动调用destroy。
16.3 LeakCanary 工具使用
手动排查内存泄漏太累了,而且有些泄漏藏得很深。这时候就需要LeakCanary出马了。
LeakCanary是Square公司开源的一个内存泄漏检测工具。它会在你的应用运行时自动检测内存泄漏,一旦发现,就会生成一条通知,点进去就能看到泄漏的引用链。
16.3.1 集成LeakCanary
在build.gradle里添加依赖:
dependencies {
// debug版本使用
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
// release版本不需要
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}
然后就没有然后了。LeakCanary 2.x版本会自动注册,不需要你写任何初始化代码。你只需要在debug包下运行应用,它就会默默工作。
16.3.2 使用LeakCanary
集成完成后,正常使用你的应用。当发生内存泄漏时,LeakCanary会发送一条系统通知。点开通知,就能看到泄漏详情。
泄漏详情里最重要的就是引用链。它会告诉你:
- 泄漏的对象是什么(比如MainActivity)
- 它被谁引用了(比如Handler的匿名内部类)
- 引用链的完整路径
我一般会先看泄漏对象的类型,然后顺着引用链往上找,看哪个引用是不应该存在的。比如看到Activity被一个静态变量引用了,那问题就很明显了。
小技巧:LeakCanary的引用链有时候会很长,别被吓到。你只需要关注最上面和最下面的几行。最下面是泄漏对象,最上面是GC Root,中间是引用路径。找到那个“不该存在的引用”,问题就解决了。
16.3.3 LeakCanary的配置
LeakCanary也支持一些自定义配置。比如你可以设置只检测特定类型的对象,或者设置检测的间隔时间。
public class ExampleApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// 这个进程是LeakCanary的分析进程,不要在这里做初始化
return;
}
// 自定义配置
LeakCanary.config = LeakCanary.config.copy(
// 只检测Activity的泄漏
retainedVisibleThreshold = 1,
// 自定义排除某些类
excludedRefs = AndroidExcludedRefs.createAppDefaults().build()
);
}
}
16.4 知识体系总览
下面这张图,我把本章的核心知识点梳理了一下。你可以把它当作一个快速索引,遇到内存泄漏问题时,对照着排查。
16.5 总结
内存泄漏这个问题,说白了就是引用管理不当。你只要记住一句话:长生命周期对象不要持有短生命周期对象的引用。做到这一点,大部分泄漏问题都能避免。
LeakCanary是个好工具,但它只是帮你发现问题。真正解决问题的,还是你对代码的理解和设计。我建议你在项目初期就集成LeakCanary,养成随时检测的习惯。不要等到OOM了再回头排查,那时候就晚了。
嗯,这一章的内容就到这里。内存优化是个大话题,后面我们还会聊到内存抖动、Bitmap优化、对象池这些内容。一步步来,先把泄漏的问题搞定,后面的路就好走了。