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的生命周期,在它们销毁后,检查是否还被引用着。如果被引用了,就生成一个泄漏分析报告

怎么用?三步走:

  1. 添加依赖:在build.gradle里加上LeakCanary的依赖。
  2. 初始化:在Application的onCreate里初始化。
  3. 跑起来:正常使用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的缓存策略。咱们下期见。