Android 内存优化实践

这篇文章主要想记录一下在排查线上 OOM 问题时的分析思路和解决方案,同时思考了如何优化监控 Android 应用的内存。

线上 OOM 问题案例

目前维护的应用主要存在两类 OOM 问题

1.创建线程时抛出 OOM 异常(只出现在华为手机上,华为手机对应用可创建的最大线程数做了约束)

1
2
3
4
5
6
7
8
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:753)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:970)
at java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1038)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1180)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:784)

2.Fresco 加载图片申请内存时虚拟机内存不足

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java.lang.OutOfMemoryError: Failed to allocate a 1821612 byte allocation with 1218096 free bytes and 1189KB until OOM
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at android.graphics.Bitmap.nativeCreate(Native Method)
at android.graphics.Bitmap.createBitmap(Bitmap.java:825)
at android.graphics.Bitmap.createBitmap(Bitmap.java:802)
at android.graphics.Bitmap.createBitmap(Bitmap.java:769)
at com.facebook.imagepipeline.memory.e.i(BitmapPool.java:51)
at com.facebook.imagepipeline.memory.e.b(BitmapPool.java:26)
at com.facebook.imagepipeline.memory.a.a(BasePool.java:259)
at com.facebook.imagepipeline.b.a.a(ArtBitmapFactory.java:48)
at com.facebook.imagepipeline.b.f.a(PlatformBitmapFactory.java:74)
at com.facebook.imagepipeline.b.f.b(PlatformBitmapFactory.java:41)
at com.facebook.fresco.animation.b.b.c$a.a(DefaultBitmapFramePreparer.java:143)
at com.facebook.fresco.animation.b.b.c$a.a(DefaultBitmapFramePreparer.java:167)
at com.facebook.fresco.animation.b.b.c$a.run(DefaultBitmapFramePreparer.java:110)
at com.facebook.common.b.b$a.run(ConstrainedExecutorService.java:175)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at com.facebook.imagepipeline.e.p.run(PriorityThreadFactory.java:51)
at java.lang.Thread.run(Thread.java:818)

问题排查解决

第一个创建线程过多导致 OOM 只存在于华为手机上,网上已经有人碰到,参考这篇文章:不可思议的OOM。可以通过Android Studio 的 Profiler 中 CPU 状况来查看应用当前所有的线程列表,或者使用命令adb shell ps -T -p pid 查看对应进程的所有线程信息。发现项目中存在大量的常驻线程,如 RxJava 内的线程池,Fresco 内部的线程池,合作方 sdk 内部的线程池,各种单线程池以及应用本身定义的线程池,所以得想办法把这些线程池复用为应用自身定义的线程池来减少线程数,同时需要考虑更改应用自己定义的线程池的策略来兼容 RxJava 的线程池,以及满足合作方 sdk 内线程池的需求。RxJava中存在一些延时和间隔执行任务的操作,这些操作是放到计算线程池执行的,它的线程池实现是ScheduledThreadPoolExecutor,所以原先应用内的计算线程池需要继承自可定时执行任务的线程池。合作方sdk内部的线程池则需要推动他们提供接口设置线程池。还有部分代码是通过直接 new Thread的方式来创建线程,这些代码也需要修改优化,一些业务模块中则是直接自己用单线程池,猜测当时可能是为了方便开发,不用考虑线程安全问题以及可以直接终止线程池来使所有任务中断退出,但这也导致了应用多出了很多活跃度不高的线程,同时应用线程池也没得到充分的利用。在优化完一般情况下的线程数之后,还需要监控在短期内是否存在代码频繁创建线程的逻辑,可以使用 PLTHook 技术在native层获取线程创建的堆栈,具体实现代码可以参考这个仓库

第二个问题是加载图片时没有足够的内存可供分配,这个分析起来比较复杂,这类问题主要出现在 5.X 系统上,而且出现问题的机型大部分是是内存较小但分辨率较高的手机,在较高的系统版本如 Android 8.0 之后图片的 Bitmap 引用在 Java 层,实际字节数组在 native 层减少了 Java 层的内存压力。出现这个问题有可能是其他大对象占用内存过多,有可能是内存中存在内存占用较大的 Bitmap ,可以用 MAT 分析应用 Java 层内存占用情况,找出可疑的大对象进行优化。

dump 出内存进行分析后发现 sdk 内部有几个对象的内存占用较大,而且当时 sdk 并没有被使用,却一直在后台占用内存,这几个对象内部有很大的集合对象和字符串对象,需要跟对应的 sdk 开发团队沟通优化。还可以分析图片 byte 数组的占用内存情况,毕竟图片是内存消耗大户。分析发现某些高分辨率图片在 dump 内存时处于隐藏状态,而且也不频繁显示,或者是上一个页面的图片,这些图片有些可以通过查看 byte 数组被哪个对象引用来找出对应的位置,有些是通过 fresco 加载,这时候可以把 byte 数组保存为.rgba文件,用 ImageMagick 工具转换为 png 格式的文件来查看具体是哪张图片。对于这种不经常显示的图片可以更改渲染策略:只在图片可见时设置对应 View 的图片资源。还有图片是在放置资源文件时放置错了,1080p 分辨率下的图片放到了 drawable 目录下面,这将会导致图片渲染出来占用的内存是放到正确目录下的 9 倍!具体图片内存占用大小计算可以看这篇文章

在优化完占用内存过大的对象和图片后还需要通过版本迭代来验证优化是否有效,同时我们也想知道在发生 OOM 时的内存占用情况,如果在外发版本发生 OOM 崩溃时去 dump 应用的内存可能会导致二次 OOM ,让应用一直处于黑屏状态,影响用户体验,但是可以在线下测试的时候 dump 应用内存来分析 OOM 时的内存占用情况。同时可以监控应用内部的大图加载,在发生 OOM 时把内存占用大于 1M 的图片上报上来,方便后期针对性地优化。

编码建议

在分析完上述 OOM 问题后想总结下平时编码时减少内存占用的一些建议
1.应用内部提供统一的线程池管理,配置 sdk 的线程池为自己应用内部的线程池减少线程数,所有其他需要在子线程执行的任务都交由线程池执行,在编码过程中保证自己写的类线程安全。
2.针对高分辨率的图片考虑在使用不频繁且它不可见时及时释放掉或者移除对应的 View。
3.异步任务执行在 Activity 或 Fragment 被销毁时需及时中断执行, RxJava 在创建 Observable 时内部的匿名内部类在页面退出时解除订阅只能取消订阅者内部对外部类的引用,如果 Observable 内部任务没执行完还是会继续持有外部类的引用导致内存泄漏,需要用静态类或外部类包装或者使用静态方法来创建 Observable ,对于需要在页面退出还能执行的任务用静态类或外部类包装。

参考