记录一次内存调优实战
测试使用adb monkey频繁测试App的拍照功能,发现内存持续升高,GC后仍缓慢持续升高;
业务场景描述
拍照界面大致如下:
和传图相机拍照稍许不同,此拍照业务是:点击拍照按钮 —> 系统把拍好的图片放到特定目录 —> 从特定目录copy照片到app目录 —> 解码、裁剪图片未缩略图 —> 更新到拍照界面
profile工具看现象
问题1
首先理论分析整个拍照链路业务,这步配合profile抓取dump java heap分析,查看每个类、内部类得创建实例数量是否合理,如下图:
上面某个内联inline的内部类多达9个,明显数量不合理,而且Depth显示-,实质它就是一个GC Root的根节点,查看源代码:
fun takePicture(){
....
Timer().schedule(1000) {
CameraLog.e("延迟", "1750")
copyFile("source img", "target img")
}
....
}
就是拍照里面一个定时器,延时1s后执行拷贝任务,这是一个局部匿名内部类,并且持有外部类的引用,Timer执行schedule方法后就会创建一个线程去执行任务,所以这里如果多次调用takePicture方法就会创建多个线程;
解决:
Timer定时器执行完后,需要执行cancel结束,这样线程就会结束掉;或者换一种延时方法如handler来处理;处理后在看看profile的dump结果,如下图:
问题2
七分钟后,App内存增大了2.6M,主要来源于others项部分,others简而言之就是系统不知道如何分类的,其它的java、native等内存是什么意思,可参考内存分析器profile,那怎么来分析这块增长的内存others来自于代码的哪一块呢?
我没有想到比较好的办法,只能像类似于加日志的方法来逐步判断,就是注释所有的业务代码,一行行代码放开,同时profile观察others内存增长情况,如果有,就说明和放开的代码有关;最终,找到了疑似的代码块:
private val soundPool = SoundPool(1, AudioManager.STREAM_MUSIC, 1)
fun takePicture(){
.....
soundPool.load(this, R.raw.music, 1)
soundPool.setOnLoadCompleteListener {
soundPool, i, i2 ->
soundPool.play(
1, 1f, 1f, //右声道
1, //优先级
0, 1f
) //播放比率,0.5~2,一般为1
}
.....
}
读者从上代码有看出问题吗?
从现象上来看,每次拍照都调用load方法,others就会不停的增加,反之把load移除到外面去,拍照时只播放这块音频就不会增加;看来就是load加载音频资源导致的,但是为什么加载音频资源就会导致others不停的增加呢?
答案:
查看系统源码,每次load加载音频资源时最终会调用到这里framework/base/media/jni/soundpool/SoundPool.cpp的doLoad方法,代码如下:
status_t Sample::doLoad()
{
......
mHeap = new MemoryHeapBase(kDefaultHeapSize);
ALOGV("Start decode");
status = decode(mFd, mOffset, mLength, &sampleRate, &numChannels, &format,
&channelMask, mHeap, &mSize);
......
mData = new MemoryBase(mHeap, 0, mSize);
......
return NO_ERROR;
}
首先,从java层到上面这个方法都是在App进程内,没有跨进程调用,所以在此期间的内存分配都属于app的;
其次,上面代码使用到MemoryHeapBase和MemoryBase是一套binder机制,去申请、管理共享内存的接口,而每次都load的话都会去重新申请一份共享内存,使用完后保存在SoundPool.cpp的一个Sample集合中,就导致申请的共享内存越来越多,系统并且将此块内存花销分类到others去了,这样就导致app的总内存一直增加,垃圾GC也无法处理掉,这就是原因;
解决方案有两种,load放到外面一次加载多次使用,或者将SoundPool.java放到函数内部,每次都重新创建一个SoundPool并load音频资源,这样函数内部是一个局部变量,每次执行完后都会去销毁!
好,本篇内存分析完结,欢迎讨论!