背景
笔者优化音乐App内存泄漏时候,遇到了3个典型内存泄漏,泄漏的内存为39kb,一次39KB看上去不多,积少成多很有可能导致OOM,值得重视。
 PS:文末有优化方案,优化后内存减少至原先的150分之一。
 操作步骤为进入歌单列表页面,点击播放按钮,我们按照此步骤抓取一份内存日志,来分析下:
 
 ①点+导入hprof文件
 ②选择app 堆存储
 ③根据类名排序
 ④仅展示Activity、Fragment内存泄漏
 ⑤过滤关键字,如app进程的包名
 ⑥点击Leaks,下方Class Name区域会展示出内存泄漏的类名
 ⑦在Class Name区域点击选择要观察的类名
 ⑧在Instance List区域点击选择该类的实例,这一步我们选择了第三个实例
 ⑨在Instace Details区域点击References 选项卡
 ⑩选中 GC root only,一层一层展开引用链
 也可以在第9步时候观察Activity的生命周期来判断内存泄漏
PlayUtil导致内存泄漏
首先点击Fields,查看Activity的生命周期,可以看到Instacen Details - Fields -Instance视图中Activity#mDestroyed = true ,表明此页面已经处于销毁状态,但Activity的内存空间仍然未释放
 
其次点击References,点一层层展开引用链,可以看到Activity使用了PlayUtil,PlayUtil初始化的时候传入了此Activity,而PlayUtil是一个单例类,该单例类全局持有了此Activity的实例,导致Activity一直被PlayUtil持有着,在Activity生命周期结束后,Activity占据的堆内存,无法被垃圾回收器回收,出现了泄漏的情况。
 单例类持有Activity,导致Activity占据的内存无法被释放,遇到这种问题,解决起来也很容易:
- 替换context,使用ApplicationContext
 - 使用一个更轻量无任何业务的Activity来初始化PlayUtil
 - 如无必要不要使用context
 

PlayUtil问题,笔者采用了方法3:
修改前
    private PlayUtil(Context context) {
        m_MediaPlayerIml = MediaPlayerIml.getInstance();
       this.m_context = context;
   }
 
修改后
    private PlayUtil(Context context) {
        m_MediaPlayerIml = MediaPlayerIml.getInstance();
       // 兼容老代码,保留入参context,但不使用,避免内存泄漏
//        this.m_context = context;
   }
 
这样可以达到PlayUtil不持有Activity的目的,在Activity#onDestroyed时候,Activity占据的内存将被垃圾回收器回收。
LoadProgress导致内存泄漏
接着我们继续看该Activity的第二个实例,查看该实例的生命周期可知该Activity已经处于Destroyed状态,但内存未被回收,这是为什么呢?

 我们继续查看References,可以看到Activity被LoadProgress持有了,且无法被释放,笔者分析可能是Activity#showLoading期间,Activity转入后台或其他因素,并未来得及调用dissLoading导致LoadProgress工具类持续持有该Activity的引用,产生了内存泄漏。
 笔者分析可能是Activity#showLoading期间,Activity转入后台或其他因素,并未来得及调用dissLoading导致LoadProgress工具类持续持有该Activity的引用,产生了内存泄漏。我们来看看问题代码
 问题代码
 Activity#initLoading,传入了当前Activity
  private void initLoading() {
        if (this.viewModel != null) {
            this.viewModel.showLoading.observe(this, new Observer<Boolean>() {
                public void onChanged(Boolean aBoolean) {
                    if (aBoolean) {
                        if (!LoadProgress.get().getDialog(BaseActivity.this).isShowing()) {
                            LoadProgress.get().getDialog(BaseActivity.this).show();
                        }
                    } else {
                        LoadProgress.get().dismissDialog(BaseActivity.this);
                    }
                }
            });
        }
    }
 
LoadProgress内使用HashMap缓存了传入的Activity
 this.dialogs.put(context, lp);
 
解决方法
 遇到这种问题也很好处理——在适当的时机清除HashMap缓存的Activity引用。
 按照这种思路,我们看到:LoadProgress工具类里提供了HashMap的清空方法LoadProgress#cleanUpTrash,那么我们在合适的地方,清空`Activity缓存即可
 在Activity#onDestroyed调用
 @Override protected void onDestroy() {
  super.onDestroy(); // 清空缓存
   LoadProgress.get().cleanUpTrash(); 
  }
 
这样,当Activity生命周期处于onDestroyed的时候,HashMap会清空持有的Activity,避免Activity内存泄漏
 
MediaplayerListener导致内存泄漏
接着我们看Activity的第三个实例,有前两个泄漏点的分析经验,可得知Activity此时已经处于onDestroyed状态,被某个类引用了,导致Activity的内存无法回收,熟能生巧,我们按图索骥可看到是MediaplayerIml通过mMediaPlayListenerCacheList持有了该类,该类是用于存储播放回调的,用于AIDL服务端通知Client的回调,更新播放界面。
 问题代码:
 Activity#onCreate时候调用了注册回调
   mediaPlayerIml.registerListener(playListener);
    private List<MediaPlayListener> mMediaPlayListenerCacheList = new ArrayList<>();
 public synchronized void registerListener(MediaPlayListener listener) {
        if (!mMediaPlayListenerCacheList.contains(listener)) {
            mMediaPlayListenerCacheList.add(listener);
        }
    }
 
解决办法也很简单,页面销毁时,调用解绑注册
@Override
protected void onDestroy() {
    super.onDestroy();
    if(mediaPlayerIml!=null){
        mediaPlayerIml.unregisterListener(playListener);
    }
    // 清空缓存
    LoadProgress.get().cleanUpTrash();
}
 

总结
总结上述3个内存泄漏点
- PlayUtil构造函数持有Activity未释放
 - LoadProgress成员变量HashMap持有Activity未释放
 - AIDL远程服务端持有Listener,Listener持有Activity未释放
 
优化效果
优化前,多次进入该页面,点击播放按钮,产生4个实例,有3个实例无法被垃圾回收,出现了内存泄漏的情况
 
优化后,多次进入该页面,点击播放按钮,只产生一个实例

根据Retained Szie来看,优化前后节省内存,效果达到了 49937/332 = 150 倍。由此可见,Activity及时回收,极大的节省了内存占用。






![[图解]企业应用架构模式2024新译本讲解01-事务脚本](https://img-blog.csdnimg.cn/direct/57300217c7594ec984f76eaec7ba632a.png)












