记录一次内存调优实战

记录一次内存调优实战

测试使用adb monkey频繁测试App的拍照功能,发现内存持续升高,GC后仍缓慢持续升高;

业务场景描述

拍照界面大致如下:
在这里插入图片描述
和传图相机拍照稍许不同,此拍照业务是:点击拍照按钮 —> 系统把拍好的图片放到特定目录 —> 从特定目录copy照片到app目录 —> 解码、裁剪图片未缩略图 —> 更新到拍照界面

profile工具看现象

问题1

首先理论分析整个拍照链路业务,这步配合profile抓取dump java heap分析,查看每个类、内部类得创建实例数量是否合理,如下图:

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音频资源,这样函数内部是一个局部变量,每次执行完后都会去销毁!

好,本篇内存分析完结,欢迎讨论!

猜你喜欢

转载自blog.csdn.net/jackzhouyu/article/details/128837003