Android 性能优化案例(3)内存抖动和泄漏的优化
正文
jvm 内存管理常识
LMK (LowMemoryKill)机制 android 底层会在系统内存告急的时候,按照一定规则杀死一些进程来满足其他进程的内存需要。其中 消耗内存的高低就是其中一项指标,所以,优化 app 的内存占用,能够有效降低 app 被系统杀死的概率。
GC STW 机制 GC,垃圾回收进程,在 GC 线程执行任务的时候,会存在一个 STW (stop the world) 机制,他就会把其他所有线程都挂起。如果 GC 非常频繁地调用,那就会导致主线程不流畅,给用户的感觉就是
卡顿。
内存抖动频繁引起 OOM 内存抖动太频繁,导致大量对象频繁创建和销毁,会产生大量不连续的内存空间,如果此时有一个大对象需要申请内存,就有可能申请失败,导致
OOM内存溢出。
一句话解释 内存泄漏
长生命周期的对象持有短生命周期对象的强引用,在短生命周期对象需要回收的时候发现不能被回收,视为泄漏
GC 回收 可达性分析
GC 线程判定 一个对象是不是可以回收,是根据可达性分析算法,计算
GcRoot,从GcRoot向下搜索,把GcRoot没有直接关联的对象全部作为垃圾来回收。
强软弱虚四大引用强和虚自不必说。强 最常见,没有特殊处理的都是强引用(包括,匿名内部类会持有外部类的强引用)。虚引用没什么用,不予讨论。软引用,用来定义一些还有用,但是不是必须的对象,使用
SoftRefrence<T>修饰,在内存紧张的时候,GC 回收之后,使用SoftRefrence<T>修饰,如果系统还有足够的内存可用,那么软引用关联的对象就不会被回收。如果不足,则回收软引用关联的对象。弱引用(WeakRefrence<T>),比软引用更弱一些,只要 GC 触发,弱引用关联的对象就会被回收。注意,使用软和弱引用,要判定关联对象是否为空。
检测以及处理内存抖动
我们使用 s 开发,平时我们运行 app,一般会点 RunApp,但是还有另一个选择, 那就是 profileApp, 运行 app 起来之后,会在 as 下方看到 profile 窗口
![]()
点击之后,as 下方会出现 profile,图中会显示网络,内存和 cpu 使用情况
![]()
如果内存的图中抖动得非常明显,比如像这样的心电图一样:
![]()
那就说明非常明显存在内存抖动,急需处理:
点击内存图形区域之后,就能看到详细的内存变化情况,以及内存分配情况:
![]()
这里有个坑:
如果你从图形中观察到,内存走势平稳,并没有出现上满模拟抖动的图中那么夸张,是不是就不存在内存抖动呢?并不是。因为我们的 gc,是在内存不可用的情况下才会去回收内存,如果 app 占用内存一直比较少,没有触及 gc 的临界值,那么就不会出现
断崖式下跌. 那么这样就观察不出内存抖动了,怎么办呢?
解决方法在 8.0 以下的安卓手机上,在下方的位置上会出现一个 Record 按钮(如果是 8.0 以上,你可以直接用拖拽的方式来截取一段内存 record):
![]()
点击它,一段时间之后,再点一下:你就能在下方发现一张表格:
![]()
这张表格代表的是,你 Record 这段时间之内创建的对象,点击一下第二列Allocations,对创建的数量进行排序,找出创建次数最多的对象:
![]()
然后,点击排行第一的 String 之后,会在右方看到:
![]()
然后点击其中的一个,又会看到一个新的窗口:
![]()
到此为止,就找到了
创建对象的元凶,以这个为线索,找到你们自己包名下的类和方法,确定是我们自己的代码在不合理地创建对象.
再往后,就是根据各自的业务代码去做优化了,记住一个宗旨:不要让代码干多余的事。如果是我们调用了系统的 api 导致了不合理地大量对象的创建,那么就要考虑这个系统 API 为什么会这样创建对象,有没有其他方法避免吗,从业务代码层来合理使用这个 api,实在不行再考虑自定义 api 或者换个系统 api。
在我们做了一次优化之后,再 profile 运行一次 app,再重复上面的过程。以此类推,直到内存抖动达到理想状态。
总结
优化内存抖动,核心就是防止频繁创建对象。常见的反面教材就是:循环中创建对象,大量调用的 api 中创建对象。而优化的主要手段,就是对象复用,常见的手段是:对象池,像是 Handler 的 Message 单链表池,Glide 的 bitmap 池等。
检测以及处理内存泄漏
经典案例:处理
handler异步任务导致的内存泄漏方法
在 Activity 的 onDestroy 中移除所有的任务
@Overrideprotected void onDestroy() {super.onDestroy();handler.removeCallbacksAndMessages(null);//移除所有任务}
使用静态内部类 + Activity 弱引用的方式
MyHandler handler = new MyHandler(this);private static class MyHandler extends Handler {WeakReference<Activity> activityWeakReference;
MyHandler(Activity activity) {activityWeakReference = new WeakReference<>(activity);}
@Overridepublic void handleMessage(Message msg) {//在执行任务的时候,判断弱引用所关联的对象是否为空,能在对象已经被回收的情况下,不去执行不必要的任务 switch (msg.what) {case 1:if (activityWeakReference.get() != null) {//TODO}}}}
工具的使用
依然是profileApp,先用profile看出内存的变化情况。
问:如何判断内存泄漏?答:内存泄漏是精细功夫,不能全盘观察,只能凭借 profile 的内存变化来推测。比如,打开 app 之后内存一路飙升,直到超出 app 能够使用的最大内存,app 崩溃,,这是最明显的。又比如,你反复打开关闭某一个界面,发现内存的稳定线(
内存稳定之后,内存占用值)随着每一次的打开关闭,都在提高,这说明,这一个界面上存在泄漏,有对象无法被回收。
上一章节使用profile 最多是了解到 哪些对象的创建和回收引起了内存抖动,但是,涉及到泄漏,只通过 profile 尚且不能知道是哪个类持有了希望被回收的对象的强引用.这里就要借助另外一款工具,他的名字叫做 Eclipse Mat (自行百度)
先回到刚才的profile,
![]()
点一下,然后再点一下,界面会自动跳转:
![]()
![]()
点击上面的保存按钮,将文件存到本地;
然后:
![]()
但是这个文件是无法直接在 mat 打开的。
找到 SDK 目录下的要hprof-conv.exe:
![]()
使用 cmd 命令,对文件进行转换,命令为:
hprof-conv [源文件名] [目标文件名]如hprof-conv 1.hprof 2.hprof回车
将得到的2.hprof利用刚才下载的 Mat 工具打开:
这里有很多指标,但是检查内存泄漏,我们只需要关注这个直方图按钮即可:
这个图中会列出你 dump 的这一段内存中的所有对象,包括 framework 层的,也包括我们自己代码创建的对象。
![]()
案例模拟
我模拟了一个经典案例,也就是前面提到的
Handler延时任务导致Activity不能被释放,核心代码如下:![]()
我就用一个非常普通的方式创建了一个
handler对象,并且用它来执行一段延时任务,只不过,延时任务的延时时间是Integer的最大值,也就是说,任务要很久以后才会执行。之后,我反复进出这一
个Activity,然后按照上面的方式dump了一段 hprof,经过 hprof-conv 转化,然后用Mat打开:
结果如下:
![]()
我填写过滤信息:
SecondActivity回车:![]()
在我们最终退出
SecondActivity之后,内存中依然保留了18个无用的对象。
那么是不是我们这 18 个都是泄漏的呢?
不一定。
![]()
前文讲过,只有不合理的强引用,才会导致内存泄漏,所以我们要按照上面的方式排除软弱虚引用。之后我们能看到下面的界面,把能展开的信息尽数展开:
![]()
了解
Handler源码的同志们应该一眼就看明白了,handler引起了内存泄漏,是因为存在不合理地强引用链,上图中可以看出,最终是 callback 对象持有了SecondActivity对象。
callback 任务的延时时间太长了,还没有执行完,所以强引用不会给你释放掉,而callback持有了Activity,导致Activity不能被释放。
如何优化内存泄漏
我们刚才已经看到了 Handler 的不合理使用导致了内存泄漏,那么如果在
onDestroy中移除所有的任务:![]()
执行同样的任务,dump 下来的 hprof 在 mat:
![]()
触发了 GC 之后,
SecondActivity数量变为了 0,内存泄漏解决。
当然还有另一种做法,静态内部类+弱引用。











评论