4、内存优化基础:Java与Android内存模型、内存泄漏的常见场景(Handler、静态变量、单例)、使用LeakCanary定位泄漏
各位同学,咱们今天聊点实在的。内存优化,说白了就是让你的App在低端机上也能流畅跑起来,不卡顿、不闪退。我做了这么多年性能优化,发现很多问题其实都出在内存上。今天咱们就从最基础的内存模型讲起,再到那些让人头疼的内存泄漏,最后教你怎么用LeakCanary这个神器抓出泄漏点。
4.1 Java与Android内存模型:你得知道内存是怎么分的
先说说Java的内存模型。嗯,这里我尽量不拽术语,咱们用大白话讲。
Java把内存分成了几个区域:
- 堆(Heap):所有对象都在这儿。你new出来的东西,全在堆里。GC(垃圾回收)主要就是打扫这块地方。
- 栈(Stack):每个线程都有自己的栈,存的是局部变量、方法调用这些。栈里的东西用完就扔,不用GC操心。
- 方法区(Method Area):存类信息、常量、静态变量。这块以前叫永久代,现在叫元空间。
- 本地方法栈(Native Method Stack):跑native代码用的,咱们平时接触不多。
Android这边呢,其实大同小异。但有个关键点——Android对堆内存有硬性限制。低端机可能只有64MB甚至更少。你想想看,一个App要跑界面、加载图片、处理数据,这点内存真不够挥霍的。
重点来了:Android系统会给每个App分配一个堆内存上限,超过就会OOM(Out Of Memory)。低端机这个值特别小,所以内存优化在低端机上尤为重要。
我在项目中遇到过一台512MB内存的老手机,跑我们App,加载三张高清图就直接崩了。后来一查,堆内存上限才48MB。从那以后,我养成了一个习惯——先看目标设备的堆内存上限,再决定怎么优化。
4.2 内存泄漏的常见场景:这些坑你踩过几个?
内存泄漏,说白了就是对象用完了,但GC回收不掉。它一直占着内存,越积越多,最后OOM。我见过太多App因为泄漏导致卡顿、闪退,用户直接卸载。
下面这几个场景,是内存泄漏的重灾区。我一个个说。
4.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.sendMessageDelayed(Message.obtain(), 60000);
}
}
问题在哪?Handler内部类持有外部Activity的引用。如果Activity销毁了,但Handler的消息还没处理完(比如延迟60秒),那Activity就泄漏了。GC一看,Handler还活着,Handler又引着Activity,得,Activity你也别走了。
避坑指南:我曾经在一个项目里,就因为Handler泄漏,导致Activity泄漏了上百个实例。用户切几次横竖屏,内存直接爆掉。后来我强制要求:所有Handler必须用静态内部类 + 弱引用。
正确的写法是这样的:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<MainActivity> mActivityRef;
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。不然就算用了弱引用,消息还在队列里,Handler对象本身还是泄漏的。
4.2.2 静态变量泄漏:全局的“钉子户”
静态变量的生命周期和App一样长。如果你把一个Activity或Context赋值给静态变量,那这个Activity就别想被回收了。
我见过最离谱的代码:
public class GlobalData {
public static Activity sCurrentActivity;
}
你想想看,这个sCurrentActivity一旦被赋值,除非App被杀,否则永远活着。用户切到别的页面,原来的Activity本该被回收,结果因为静态变量还指着它,它就赖在内存里不走。
我的建议:能用局部变量就别用静态变量。实在要用,记得在不用的时候置为null。比如在onPause或onDestroy里清掉。
4.2.3 单例模式泄漏:看似优雅,实则危险
单例模式本身没问题,但如果你在单例里持有了Activity或Context的引用,那就出事了。
public class AppManager {
private static AppManager sInstance;
private Context mContext;
private AppManager(Context context) {
// 这里持有了传入的Context
this.mContext = context;
}
public static AppManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppManager(context);
}
return sInstance;
}
}
问题在哪?如果你传入的是Activity的Context,那这个Activity就被单例给“绑架”了。单例不销毁,Activity也别想走。
正确的做法是:单例里只持有Application的Context。因为Application的Context和App同生共死,不存在泄漏问题。
public class AppManager {
private static AppManager sInstance;
private Context mContext;
private AppManager() {
// 使用Application的Context
this.mContext = MyApplication.getInstance();
}
public static AppManager getInstance() {
if (sInstance == null) {
sInstance = new AppManager();
}
return sInstance;
}
}
4.3 使用LeakCanary定位泄漏:抓泄漏的神器
前面讲了那么多泄漏场景,但实际开发中,泄漏往往藏得很深。你不可能靠肉眼一行行代码去查。这时候就需要LeakCanary了。
LeakCanary是Square公司开源的一个内存泄漏检测库。它的原理很简单:监听Activity和Fragment的生命周期,在它们销毁后,检查是否还被引用着。如果被引用了,就生成一个泄漏分析报告。
怎么用?三步走:
- 添加依赖:在build.gradle里加上LeakCanary的依赖。
- 初始化:在Application的onCreate里初始化。
- 跑起来:正常使用App,LeakCanary会在后台默默监控。一旦发现泄漏,会弹出通知。
代码示例:
// 在Application的onCreate中初始化
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// 这个进程是专门用来分析泄漏的,不要在这里做初始化
return;
}
LeakCanary.install(this);
}
}
安装好之后,你正常操作App。比如打开一个页面,再返回。如果这个页面泄漏了,LeakCanary会弹出一个通知。点开通知,就能看到详细的泄漏路径。
实战经验:我记得有一次,LeakCanary报了一个泄漏,路径显示是一个匿名内部类持有了Activity。我顺着路径一查,发现是一个回调没解注册。这种问题,光靠代码审查很难发现,但LeakCanary一秒就揪出来了。
LeakCanary的泄漏报告长这样:
┬───
│ GC Root: Local variable in native code
│
├─ android.os.Handler instance
│ Leaking: NO (Handler is not leaking)
│ Handler.mClass = class com.example.MainActivity
│ ↓ Handler.mCallback
│ ~~~~~~~~~~
├─ com.example.MainActivity$1 instance
│ Leaking: YES (RefWatcher was watching this)
│ Anonymous class implementing Runnable
│ ↓ MainActivity$1.this$0
│ ~~~~~~
╰→ com.example.MainActivity instance
Leaking: YES (RefWatcher was watching this)
你看,这个报告清晰地告诉你:泄漏的根是Handler,中间是一个匿名Runnable,最终指向了MainActivity。你一看就知道,是Handler里的Runnable持有了Activity。
小技巧:LeakCanary默认只监控Activity。如果你想监控Fragment、ViewModel或者其他自定义对象,可以手动调用RefWatcher的watch方法。我个人习惯在Fragment的onDestroyView里调用一下,这样Fragment的泄漏也能抓到。
总结一下
今天咱们聊了内存优化的基础。说白了就是三件事:
- 理解内存模型:知道内存怎么分,Android的堆内存限制是多少。
- 识别泄漏场景:Handler、静态变量、单例,这三个是重灾区。
- 用LeakCanary抓泄漏:别靠猜,让工具帮你找。
嗯,最后说一句。内存优化不是一蹴而就的事。我刚开始做优化时,也踩过不少坑。但只要你养成了好习惯——写代码时多想一步“这个对象会不会泄漏”,再配合LeakCanary这样的工具,内存问题基本都能搞定。
下一章,咱们聊聊图片内存优化。低端机上,图片往往是内存消耗的大头。到时候我会分享一些实战技巧,比如怎么压缩图片、怎么用Glide的缓存策略。咱们下期见。