编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

13.内存治理优化实践

目录介绍

  • 01.卡顿基础介绍
    • 1.1 项目背景介绍
    • 1.2 遇到问题
    • 1.3 基础概念
    • 1.4 设计目标
    • 1.5 收益分析
  • 02.内存如何分析
    • 2.1 内存分析工具
    • 2.2 内存分析指标
    • 2.3 内存优化思路
  • 03.内存泄漏原理和根治
    • 3.1 什么是内存泄漏
    • 3.2 内存为何会泄漏
    • 3.3 常见内存泄漏案例
    • 3.4 内存捕获核心思想
    • 3.5 内存泄漏引用链
  • 04.其他内存优化治理
    • 4.1 代码层内存分析
    • 4.2 如何避免内存抖动
    • 4.3 即时销毁对象
    • 4.7 ComponentCallback优化
    • 4.8 四种引用优化
    • 4.9 关于log日志

01.卡顿基础介绍

1.1 项目背景介绍

  • 内存治理背景
    • 在所有的App中,内存泄露问题一直存在,泄露或多或少而已,对于使用时间比较小的应用来说,即便存在内存泄露也没那么危险,因为出现OOM的概率较低,但是内存泄露问题对于长时间运行的App来说是致命的,如何解决内存泄露就是是我们工作的重点方向。
  • 稳定性很重要
    • 对于工具类App来说,运行稳定很重要。

1.2 遇到问题

  • 内存衡量标准是什么?
    • App内存可以用工具查看总的消耗内存,比如java内存,native内存。但衡量app内存是否健康的标准是什么,要建立一套衡量准则……
  • 内存怎么判断泄漏?
    • 因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
  • 内存没泄漏但过大原因是什么?
  • 内存泄漏如何去分析?
  • 内存优化实践和效率分析?

1.3 基础概念

  • 泄露的原因有非常多,如果用一句话概括就是引用了不再需要的内存信息,如果详细划分可以归为一下几种类别
    • 内部类和匿名内部类导致的内存泄露,这种是最常见的,也是最容易忽略的问题,尤其在这种GUI编程中,存在大量的callback回调
    • 多线程导致的内存泄露,本质上是线程的生命周期太长了,页面销毁的时候线程有可能还在运行
    • 单例问题,本质上也是生命周期太长导致的
  • 对于内存泄露,现在有很多的工具能帮助我们定位和分析问题,你们为什么线上还是有内存泄露问题呢?
    • 线上的真实使用环境比较复杂,有很多的场景不一定在线下完全覆盖到
    • 虽然线下也有自动化工具上报问题,但是很多人都忽略了内存泄露问题,不重视和不及时是掩埋线下问题的根本原因

1.4 设计目标

  • 对于以上问题,给出我们自己的解法吧,方案设计核心思路如下
    • 建立线下内存自动化分析工具,解决过多的人力投入
    • 建立问题解决闭环机制,打通问题的发现、上报、处理、解决问题等多个链路,闭环解决问题
    • 建立内存泄露度量体现,数据和结果度量内存质量问题

1.5 收益分析

02.内存如何分析

2.1 内存分析工具

  • Android最常见的是:Leakcanary
    • leakCanary是Square开源框架,是一个Android和Java的内存泄露检测库,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知,所以可以把它理解为傻瓜式的内存泄露检测工具。通过它可以大幅度减少开发中遇到的oom问题,大大提高APP的质量。
  • java相关工具是:Memory Analyzer
    • 是一款开源的JAVA内存分析软件,查找内存泄漏,能容易找到大块内存并验证谁在一直占用它,它是基于Eclipse RCP(Rich Client Platform),可以下载RCP的独立版本或者Eclipse的插件。
    • http://www.eclipse.org/mat/

2.2 内存分析指标

2.3 内存优化思路

03.内存泄漏治理

3.1 什么是内存泄漏

  • 什么是内存泄漏
    • 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
  • 举一个内存泄漏案例
    • 比如:当Activity的onDestroy()方法被调用后,Activity以及它涉及到的View和相关的Bitmap都应该被回收掉。
    • 但是,如果有一个后台线程持有这个Activity的引用,那么该Activity所占用的内存就不能被回收,这最终将会导致内存耗尽引发OOM而让应用crash掉。
  • 它是造成应用程序OOM的主要原因之一。
    • 由于android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就可能造成App会OOM。

3.3 常见内存泄漏案例

3.3.1 错误使用单例造成的内存泄漏
  • 造成内存泄漏原因分析
    • 在平时开发中单例设计模式是我们经常使用的一种设计模式,而在开发中单例经常需要持有Context对象,如果持有的Context对象生命周期与单例生命周期更短时,或导致Context无法被释放回收,则有可能造成内存泄漏。
  • 解决办法:
    • 要保证Context和Application的生命周期一样,修改后代码如下:this.mContext = context.getApplicationContext();
    • 如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。
3.3.2 Handler使用不当造成内存泄漏
  • 造成内存泄漏原因分析
    • 通过内部类的方式创建mHandler对象,此时mHandler会隐式地持有一个外部类对象引用这里就是Activity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
  • 解决Handler内存泄露主要2点
    • 注意要在Activity销毁的时候移除Messages。或者推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。
3.3.3 Thread未关闭造成内存泄漏
  • 线程内存泄漏场景
    • 当在开启一个子线程用于执行一个耗时操作后,此时如果改变配置(例如横竖屏切换)导致了Activity重新创建,一般来说旧Activity就将交给GC进行回收。
    • 但如果创建的线程被声明为非静态内部类或者匿名类,那么线程会保持有旧Activity的隐式引用。当线程的run()方法还没有执行结束时,线程是不会被销毁的,因此导致所引用的旧的Activity也不会被销毁,并且与该Activity相关的所有资源文件也不会被回收,因此造成严重的内存泄露。
  • 因此总结来看, 线程产生内存泄露的主要原因有两点:
    • 1.线程生命周期的不可控。Activity中的Thread和AsyncTask并不会因为Activity销毁而销毁,Thread会一直等到run()执行结束才会停止,AsyncTask的doInBackground()方法同理
    • 2.非静态的内部类和匿名类会隐式地持有一个外部类的引用
  • 解决线程内存泄漏方案
    • 想要避免因为 Thread 造成内存泄漏,可以在 Activity 退出后主动停止 Thread
  • 如果想保持Thread继续运行,可以按以下步骤来:
    • 1.将线程改为静态内部类,切断Activity对于Thread的强引用;2.在线程内部采用弱引用保存Context引用,切断Thread对于Activity的强引用
3.3.4 错误使用静态变量导致引用后无法销毁
  • 在平时开发中,有时候我们创建了一个工具类。
    • 比如分享工具类,十分方便多处调用,因此使用静态方法是十分方便的。但是创建的对象,建议不要全局化,全局化的变量必须加上static。这样会引起内存泄漏!
  • 使用场景。在Activity中引用后,关闭该Activity会导致内存泄漏
    DoShareUtil.showFullScreenShareView(PNewsContentActivity.this, title, title, shareurl, logo);
  • 解决办法
    • 静态方法中,创建对象或变量,不要全局化,全局化后的变量或者对象会导致内存泄漏。
3.3.5 非静态内部类创建静态实例造成内存泄漏
  • 有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法
  • 问题代码
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //省略
        if(mResource == null){
            mResource = new TestResource();
        }
    }
    class TestResource {
         //里面代码引用上下文,Activity.this会导致内存泄漏
    }
  • 解决办法
    • 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。
  • 分析问题
    • 这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
3.3.6 不需要用的监听未移除会发生内存泄露
  • 问题代码
    //add监听,放到集合里面
    tv.getViewTreeObserver().addOnWindowFocusChangeListener(this);
  • 解决办法。关于注册监听这种,最后注册+销毁是成对的出现。
    //计算完后,一定要移除这个监听
    tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
3.3.7 资源未关闭造成的内存泄漏
  • 有哪些资源容易造成泄漏
    • BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback,Anim动画等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。
  • 注意事项和建议
    • 值得注意的是,关闭的语句必须在finally中进行关闭,否则有可能因为异常未关闭资源,致使activity泄漏。
  • 举一个具体的案例
    • 比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个广播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。
3.3.8 静态集合使用不当导致的内存泄漏
  • 具体的问题说明
    • 有时候我们需要把一些对象加入到集合容器(例如ArrayList)中,当不再需要当中某些对象时,如果不把该对象的引用从集合中清理掉,也会使得GC无法回收该对象。如果集合是static类型的话,那内存泄漏情况就会更为严重。
  • 解决办法思考
    • 因此,当不再需要某对象时,需要主动将之从集合中移除。

04.其他优化治理

4.3 即时销毁对象

  • 在组件销毁的时候即时销毁对象
    • 目前主要是指,将对象释放,也就是设置成null。将元素都置为null,中断强引用与对象之间的关系,让GC的时候能够回收这些对象的内存。

4.2 如何避免内存抖动

  • 内存抖动是由于短时间内有大量对象进出新生区导致的,它伴随着频繁的GC,gc会大量占用ui线程和cpu资源,会导致app整体卡顿。避免发生内存抖动的几点建议:
    • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
    • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
    • 当需要大量使用Bitmap的时候,试着把它们缓存在数组或容器中实现复用。
    • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

4.7 ComponentCallback优化

  • 关于ComponentCallback2,是一个细粒度的内存回收管理回调。
    • 开发者应该实现onTrimMemory(int)方法,细粒度release 内存,参数可以体现不同程度的内存可用情况
    • 响应onTrimMemory回调:开发者的app会直接受益,有利于用户体验,系统更有可能让app存活的更持久。
    • 不响应onTrimMemory回调:系统更有可能kill 进程
  • 具体看glide源码如何做到释放内存

4.8 四种引用优化

  • 引用类型有哪些种
    • 强引用:默认的引用方式,不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
    • 软引用(SoftReference):如果一个对象只被软引用指向,只有内存空间不足够时,垃圾回收器才会回收它;
    • 弱引用(WeakReference):如果一个对象只被弱引用指向,当JVM进行垃圾回收时,无论内存是否充足,都会回收该对象。
    • 虚引用(PhantomReference):虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用通常和ReferenceQueue配合使用。
  • 一般使用场景
    • 强引用,直接new出来的对象,通过引用对堆里面的对象进行操作,可能会导致内存泄漏,一般内存优化主要是针对强引用优化。可以显示地将引用赋值为null,JVM在合适的时间就会回收该对象。
    • 软引用,使用SoftReference包装对象,比如图片缓存,频率使用高且内存容易吃紧,使用软引用可以在内存紧张时释放一些对象。
    • 弱引用,使用WeakReference包装对象,比如Handler内存泄漏,主要是Activity释放后handler依然持有Activity,这个时候可以采用弱引用优化。
  • 使用软引用或者弱引用防止内存泄漏
    • 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
    • 软引用,弱引用都非常适合来保存那些可有可无的缓存数据。如果这样做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间。
  • 软引用VS弱引用选择
    • 个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
    • 还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

4.9 关于log日志

  • log 方法内存开销过大。建议这个方法 建议 Debug 进入执行 ,但是正是包不执行,因为 object 会进行字符串+拼接,产生大量内存对象。
    public static void d(Object object){
        //这个方法 建议 Debug 进入执行 ,但是正是包不执行,因为 object 会进行字符串+拼接,产生大量内存对象。
        //Log.d(TAG, object.toString());
        Log.d(TAG, " log : " + object);
    }

参考博客

  • App内存泄露测试方法总结:https://blog.csdn.net/wjky2014/article/details/119258886
  • 抖音 Android 性能优化系列: Java 内存优化篇
    • https://juejin.cn/post/6908517174667804680
  • 【万字总结】Android 内存优化知识盘点
    • https://juejin.cn/post/7359896145986289699
  • Android 内存优化的小知识
    • https://mp.weixin.qq.com/s/kbuN_I9-_XUPDGmSVc9v_A
贡献者: yangchong211
上一篇
12.内存监控优化方案
下一篇
14.FPS监测设计实践