第一个线上Bug:OOM内存泄漏的惊魂夜

记录第一次处理线上OOM内存泄漏问题的完整过程和解决方案

-- 次阅读

第一个线上Bug:OOM内存泄漏的惊魂夜

问题背景

2016年11月初,我入职Android开发岗位已经两个月了。在完成第一个注册功能开发后不久,突然接到紧急通知:线上用户反馈App频繁崩溃,崩溃日志显示大量OOM(Out of Memory)异常。

作为新人,这是我第一次面对线上Bug,内心既紧张又忐忑。当时我们使用的Android SDK版本是23(6.0),项目采用了MVP架构,使用Retrofit 2.0进行网络请求。

问题分析

1. 初步排查

首先查看崩溃日志,发现主要的异常信息:

java.lang.OutOfMemoryError: Failed to allocate a 8388608 byte allocation with 4194304 free bytes and 12MB until OOM
    at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
    at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
    at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:609)
    at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:444)

2. 使用LeakCanary检测

在mentor张哥的指导下,我们决定使用LeakCanary来检测内存泄漏。首先在build.gradle中添加依赖:

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

然后在Application中初始化:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            LeakCanary.install(this);
        }
    }
}

3. MAT内存分析

除了LeakCanary,我们还使用MAT(Memory Analyzer Tool)进行更深入的分析。通过Android Studio的Profiler功能导出hprof文件,然后在MAT中分析内存使用情况。

问题定位

经过分析,我们发现问题出现在注册页面的RegisterActivity中。具体代码如下:

public class RegisterActivity extends AppCompatActivity implements RegisterContract.View {
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case REGISTER_SUCCESS:
                    // 处理注册成功逻辑
                    break;
                case REGISTER_FAILED:
                    // 处理注册失败逻辑
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 启动定时任务
        handler.sendEmptyMessageDelayed(REGISTER_CHECK, 1000);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 忘记移除消息,导致内存泄漏
    }
}

问题分析

  1. Handler持有Activity的隐式引用(this)
  2. 当Activity被销毁时,Handler中的消息还未处理完
  3. Message持有Handler引用,Handler持有Activity引用
  4. 导致Activity无法被GC回收,造成内存泄漏

解决方案

1. 使用WeakReference修复

修改后的代码:

public class RegisterActivity extends AppCompatActivity implements RegisterContract.View {
    private MyHandler mHandler = new MyHandler(this);

    private static class MyHandler extends Handler {
        private WeakReference<RegisterActivity> mActivity;

        public MyHandler(RegisterActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            RegisterActivity activity = mActivity.get();
            if (activity != null) {
                switch (msg.what) {
                    case REGISTER_SUCCESS:
                        // 安全的引用
                        break;
                    case REGISTER_FAILED:
                        // 安全的引用
                        break;
                }
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.sendEmptyMessageDelayed(REGISTER_CHECK, 1000);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除所有消息,避免内存泄漏
        mHandler.removeCallbacksAndMessages(null);
    }
}

2. 使用静态内部类

另一种解决方案是使用静态内部类:

public class RegisterActivity extends AppCompatActivity {
    private static class SafeHandler extends Handler {
        private final WeakReference<RegisterActivity> mActivity;

        public SafeHandler(RegisterActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            RegisterActivity activity = mActivity.get();
            if (activity != null) {
                // 处理消息
            }
        }
    }

    private SafeHandler mHandler = new SafeHandler(this);
}

验证修复效果

1. LeakCanary验证

修复后,LeakCanary不再报告内存泄漏问题。当我们退出RegisterActivity时,LeakCanary的Notification显示"没有检测到内存泄漏"。

2. MAT验证

使用MAT重新分析内存,发现Activity的引用链被正确释放,内存使用量显著下降。

3. 线上监控

部署修复版本后,线上OOM异常率从原来的5.2%下降到0.3%,问题得到根本解决。

经验总结

1. Handler内存泄漏的常见场景

  • 匿名内部类Handler:持有外部类引用
  • 静态Handler:持有Activity的静态引用
  • 未清理的消息队列:Activity销毁后消息仍在队列中

2. 预防措施

// 正确的Handler使用方式
public class BaseActivity extends AppCompatActivity {
    private SafeHandler mHandler = new SafeHandler(this);

    private static class SafeHandler extends Handler {
        private final WeakReference<BaseActivity> mActivity;

        public SafeHandler(BaseActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            BaseActivity activity = mActivity.get();
            if (activity != null) {
                activity.handleMessage(msg);
            }
        }
    }

    protected void handleMessage(Message msg) {
        // 子类重写此方法处理消息
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

3. 最佳实践

  1. 使用静态内部类:避免隐式持有外部类引用
  2. 使用WeakReference:确保不会阻止GC
  3. 及时清理:在onDestroy中移除所有消息和回调
  4. 工具检测:开发阶段使用LeakCanary检测
  5. 代码审查:重点关注Handler、Thread、Timer等异步操作

4. 技术栈清单(2016年11月)

Android SDK 23 (6.0)
LeakCanary 1.5
MAT (Memory Analyzer Tool)
MVP Architecture
Retrofit 2.0
OkHttp 3.0

后续思考

这次OOM事件让我深刻认识到:

  1. 代码质量的重要性:不仅仅是功能实现,更要考虑内存管理
  2. 工具的价值:LeakCanary等工具能帮助我们快速定位问题
  3. 学习的必要性:Android内存管理机制需要深入学习
  4. 团队协作:mentor的指导对新人成长至关重要

通过这次事件,我从一个只关注功能实现的新人,开始重视代码质量和性能优化,这是技术成长的重要一步。

-- 次访问
Powered by Hugo & Stack Theme
使用 Hugo 构建
主题 StackJimmy 设计