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