一、内存抖动
1.内存抖动的危害
由于垃圾回收机制老年代里面的标记清理算法,大有大量对象创建并快速销毁后,会在内存里面留下大量的内存碎片,这时如果有大对象需要申请内存时,就会产生OOM。
2.如何查看程序是否有内存抖动现象
可以利用Android studio 的profiler工具
当内存出现大量小幅度升降时,即可判断为内存抖动
3.比较常见造成内存抖动的场景
造成内存抖动的原因是在短时间内创建了大量的对象,所以我们特别应该避免在频繁调用的方法里面创建对象。
(1)字符串拼接
String str = "一"+ "二"+"三"+"四";
在JVM将java代码翻译成字节码的时候,String对象的每次拼接都会创建StringBuilder对象。 优化方案:我们可以自己StringBuilder对象
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("一");
stringBuilder.append("二");
stringBuilder.append("三");
stringBuilder.append("四");
(2)在循环里面创建对象
如果循环次数过多,直接在循环体里面创建对象可能需要慎重考虑,能在循环外面创建对象的尽量在外面创建。
(3)在onDraw 里面创建对象
onDraw也是调用比较频繁的函数,所以创建对象也需要慎重考虑,比如Paint、Path的创建可以在View初始化的时候就创建,如果要在onDraw里面对其改变,直接重置即可。
二、内存泄漏
内存泄漏是指程序中已分配的内存,由于某种原因,没有被释放或者是无法被释放,造成内存浪费的现象。主要是短生命周期对象强引用了长生命周期,导致短生命周期对象该被回收的时候无法被回收。
1.内存泄漏的危害
大量的内存泄漏会直接导致程序浪费大量的内存,从而导致系统OOM
2.如何查看程序中是否有内存泄漏
项目中主要的内存泄漏是Activity的内存泄漏,如果我们怀疑SecondActivity 存在内存泄漏,可以用 Android studio 的profiler工具和MAT工具查看。
我们在App里面频繁进出SecondActivity ,如果内存状态如下图所示持续增加,则说明可能存在内存泄漏,需要继续查找
这时需要点上门的下载按钮,将此时的内存状态dump下来。dump下来后,profiler会自动显示如下Heap Dump数据,我们以Arrange by package 排序,去对应包名下找我们检测的SecondActivity 对象是否存在,如下图所示,这里存在6个SecondActivity 对象以及6个SecondActivity 内部类对象。
看到这里我们只能知道内存中存在SecondActivity 对象,但并不代表就存在内存泄漏,因为内存泄漏是需要被其他对象强引用。所以我们需要借助另一个工具MAT。 我们先需要将当前看到的内存文件以hprof的格式导出来,直接在profiler窗口的右边对应的session 右键即可导出。由于Android studio 导出来的hprof文件并非标准hprof文件,所以需要先用借助Android sdk 安装目录下platform-tools目录下的hprof-conv.exe工具转换下。
hprof-conv -z D:\workspace\09.05\memory-20200905T142908.hprof D:\workspace\09.05\1.hprof
备注:hprof-conv命令需要配置环境变量 用MAT 以open heap file 的方式打开转换后的1.hprof文件
如上图所示,点1处可以列出内存中所有对象,点2处可以输入想要查找的对象名称
查到对应的对象后,右键按右键选Merge Shortest Paths to GC Roots ——> exclude all phantom/weak/soft etc.references 去除掉虚引用、弱引用、和软引用。 这时如果还存在对象,就说明SecondActivity 确实存在内存泄漏 再点开过滤虚引用、弱引用、和软引用后还存在的对象,如下图所示
可以看出是系统InputMethodManager引起的内存泄漏。规避方案网上很多,这里就不详述了。
3.内存泄漏的常见场景
(1)将Activity 的context对象传入单例类
由于单例类的的对象经常是static的,而static对象可作为垃圾回收机制的GCRoots对象,也就是单例类的对象的生命周期远远高于Activity的生命周期,所以会造成内存泄漏 优化方案:先看看是否是必须传context,一般Activity 的context只在UI层传递即可。如果必须要传context对象,可以考虑传Application的context。
(2)匿名内部类的使用
举例
public class SecondActivity extends Activity {public static final String TAG = "SecondActivity";private Handler handler ;private void handleMsg(){handler.postDelayed(new Runnable() {@Overridepublic void run() {}},2000);}
...
}
由于匿名内部类会含有外部类的引用对象,上面代码Runnable对象中就会引用SecondActivity 对象,所以会造成内存泄漏。 优化方案1: 在SecondActivity 的onDestroy方法中调用handler.removeCallbacksAndMessages(null)可从Handler 队列中移除所有的Runnable对象,从而打断引用链。
@Override
protected void onDestroy() {super.onDestroy();handler.removeCallbacksAndMessages(null);
}
优化方案2: 将Runnable写成静态内部类,静态内部类不会引用外部类的对象。如果静态内部类中需要用到外部类的对象,可以用弱引用传进去。对应代码如下:
public class SecondActivity extends Activity {public static final String TAG = "SecondActivity";private Handler handler ;private MyRunnable myRunnable = new MyRunnable(this);static class MyRunnable implements Runnable{WeakReference<SecondActivity> reference ;public MyRunnable(SecondActivity activity){reference = new WeakReference<SecondActivity>(activity);}@Overridepublic void run() {SecondActivity secondActivity = reference.get();if(null != secondActivity ){//to do something}}}private void handleMsg(){handler.postDelayed(myRunnable,2000);}...
}
(3)集合的使用
入股集合中的某个元素对象 后期不用了,需要把这个元素remove掉。
(4)监听器的使用
设置的监听器,在监听页面销毁的时候,记得把监听器清除掉。
(5)未释放资源
比如文件流未关闭,这里一定要保证在finally中把每个流单独try catch全都关掉,以防在前面关流过程中就发生了异常,后面的留就无法关闭了。
(6)系统bug
如WebView 、InputMethodManager
内存抖动方案解决
1.集合类
集合类如果仅仅有添加元素的机制,而没有相应删除元素机制,这样就会造成内存被占用,如果这个类是全局性变量(比如类中有静态属性,全局性的map等即有静态引用或final一直指向它)。那么没有相应删除机制,很可能导致集合所占内存只增不减。 解决办法:在使用集合类时,增加删除元素机制,并适当调用减少集合所占内存。
2.单例模式
不正确使用单例模式,也会引起内存泄漏单例对象在初始化后将在JVM的整个生命周期存在(以静态变量方式),如果单例对象持有外部对象的引用,那么这个外部对象就会一直占用着内存,可能导致内存泄漏(取决于这外部对象是否一致有用)。 解决办法:单例对象中避免含有不是一直都有用的外部对象引用。
3.Android组件或特殊集合对象的使用
BraodcastReceiver ,ContentObserver,fileObserver,Cursor,Callback等在Activity onDestory或者某类生命周期结束之后一定要unregistere或者close掉,否则这个Activity类会被system强引用,不会被回收。不要直接对Activity进行直接引用作为成员变量,如果不得不这么做,调用private WeakPeferense mActivity 来做,相同的,对与Service等其他有自己生命周期的对象来说,直接引用都需要考虑是否会存在内存泄露的可能。
4.Handler
要知道,只要Handler 发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有。由于Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的。因此这种实现方式一般很难保证跟view或者Activity的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler使用要特别小心,否则很可能内存泄漏。 解决办法:在view 或者Activity生命周期结束前,确保Handler已没有未处理的消息(特别是延时消息)。
5.Thread 内存泄漏
线程也是造成内存泄露的一个重要源头,线程产生内存泄露的主要原因在于线程生命周期不可控,比如线程是Activity的内部类,则线程对象中保存了Activity的一个引用,当线程的run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的Activity就出现了内存泄漏问题。解决办法:1.简化线程run函数执行的任务,使他在Activity生命周期结束前,任务运行完。2.为Thread增加撤销机制,当Activity生命周期结束时,将Thread的耗时任务撤销(笔者推荐这种)。
6.一些不良代码造成的内存压力
有些代码并不造成内存泄漏,但是他们是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
(1) Bitmap 没调用recycle()
Bitmap 对象在不使用时,我们应该先调用recycle()释放内存,然后才置空,因为加载bitmap对象的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过jni调用的,Android的Bitmap底层是使用skia图形库实现,skia是用c实现的)。这个recycle()函数就是针对c部分的内存释放。
(2)构造Adapter时,没有使用缓存的convertView。 解决办法:使用静态holdview的方式构造Adapter。
这样到这里内存抖动和内存泄漏的发现,定位以及解决方法以说明完毕。
内存抖动一直是Android性能优化的重要环节,Android的性能优化除了内存抖动导致的卡顿外,还有布局优化、卡顿优化、启动优化等等。Android性能优化也是大厂面试的必备技术,面试也是被常常问及到的问题。
更多有关Android性能优化的技术进阶学习,可以参考《Android性能优化手册》这个文档。里面包含了在Android性能优化这块的技术点的解析。点击可以查看详细类目。