Android内存优化案例分析
前言:
术语
开机内存:
手机连接Wifi热点、插入注册网络的SIM卡,重启后、静置5分钟,采集的进程内存值;
常驻内存:
业务进程工作任务结束退至后台、静置5分钟,采集的进程内存值;
动态内存:
业务进程在后台运行工作任务时,采集的进程内存峰值;
场景内存:
用户使用典型业务场景时,相关依赖服务进程在前台、后台运行的内存总和;
驻留比率:
用于标识进程在后台的常驻概率,驻留比率=“B Serveices优先级及以上采样命中数” /“总样本数”。
常用分析工具
Android Studio Profile
Android Studio 3.0 及更高版本中的 Android Profiler 取代了 Android Monitor 工具。Android Profiler 工具可提供实时数据,帮助您了解应用的 CPU、内存、网络和电池资源使用情况。该工具大家用得比较多,这里就不过多赘述。
传送门: https://developer.android.google.cn/studio/profile/android-profiler
Memory Analyzer工具
MAT 是一个快速,功能丰富的 Java Heap 分析工具,通过分析 Java 进程的内存快照 HPROF 分析,从众多的对象中分析,快速计算出在内存中对象占用的大小,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象。
adb或Android studio Profile抓取的heap文件,需要使用(AndroidSdk\platform-tools\hprof-conv.exe)转换之后才能打开
传送门:
https://www.eclipse.org/mat/
Perfetto
可以分析内存产生过程的方法栈,区别于hprof文件,heap文件时某个时刻的内存。而perfetto作用的是过程内存。一般用于分析可以复现内存问题的场景,这里不过多赘述。
传送门:https://ui.perfetto.dev/#!/record?p=instructions
Jadx-gui
jadx是个人首选的反编译利器,同时支持命令行和图形界面,能以最简便的方式完成apk的反编译操作。
下载地址
常用adb命令
adb shell dumpsys meminfo <package>
查看进程内存分布信息,如:adb shell dumpsys meminfo com.demo
注意该方式会产生一次gc
adb shell "dumpsys meminfo | grep pcakgename"
查看某个进程内存总值,该方式不会触发gc
adb shell showmap <pid>
adb pull proc/<pid>/smaps
code,system的内存分布,可以用于分析代码量内存分布,包括系统的类
adb shell am dumpheap <pid>
dump javaHeap部分的对象实例,可以借助Android Profile,MAT打开hprof文件分析
Android Profile会显示所有的对象实例,包括可以待gc回收的对象,而且方便清除知道对象的变量,方便定义是什么业务产生的
MAT只会显示不会被gc回收的对象,可以查看GC链,但是可以对比两个hprof文件差异性
adb shell am dumpheap -n <pid>
dump native的对象,然后根据编译Rom的产物之一:带有符号信息so文件(默认在$ANDROID_PRODUCT_OUT/symbols目录下),如果没有可以从root的手机里获取(system/lib64,vendor/lib64),使用native_heapdump_viewer.py解析,然后前后对比,就可以知道是哪些方法导致的内存增长。
Linux命令:python native_heapdump_viewer.py --html --symbols /symbols/ heap.txt > heap_info.tx
注意需要在Linux服务器上执行,要不然无法全部解析所有的方法栈。或者使用python工程解析也可以
传送门:
n
ative_heapdump_viewer.py
案例分析:
一、常驻内存优化
根据dumpsys meminfo查看内存分布后,根据不同内存分布,做相应的优化
如下为应用优化前的内存分布:
App Summary
Pss(KB) Rss(KB)
------ ------
Java Heap: 4488 32216
Native Heap: 7016 12576
Code: 16596 62912
Stack: 2272 2288
Graphics: 0 0
Private Other: 3416
System: 1964
Unknown: 7880
TOTAL PSS: 35752 TOTAL RSS: 117872 TOTAL SWAP (KB): 0
数据分析初步结论:发现code部分占大头,native内存也偏高
Java Heap:
通过adb shell am dumpheap或profile工具抓取javaHeap文件 重点关注以下几个点(要很细心,一个个查看,不要错过任何怀疑的可能性)
1、对象个数(Allocations值)高的对象
2、对象内存占用值(Shallow Size),查看代码确认该对象是否有必要常驻
3、相关联的内存值(Retainaed Size)
4、预期结果是只有单个实例的,是否出现了多个实例
5、常见的内存大对象,比如:Thread,HandlerThread等
通过javaHeap文件可知,
1、其中一个内存大块为数据库相关,应用由5个db数据库,
只能做到延迟加载,使用到对应的数据库之后才进行加载,并且减少数据库执行语句的缓存数,
SQLiteDatabase.setMaxSqlCacheSize()
另外,我也尝试过,写一个有效期的Map,长时间不使用主动关闭数据库,并且释放缓存。发现没法完全释放掉,会残留一些用于同步的ThreadLocal对象,如果频繁的创建,连接、关闭数据库,就会累计很多无用的对象,导致内存泄漏,所有我放弃了该方式
2、Thread、HandlerThread的优化
重点:自定义线程,自定义线程池,一定要复写自己的线程名,方便定位问题,要不然dump内存时或者看日志时,都不知道该线程是由哪个业务创建的
线程的创建,也会带来Stack内存
修改措施:
1)、使用线程池,防止频繁创建线程,产生临时内存,
2)、去掉没有必要的HandlerThread,使用公共HandlerThread或者线程池替代
3)、未及时关闭的Closeable对象
可以配置
StrictMode.VmPolicy vmPolicy = new StrictMode.VmPolicy.Builder().detectActivityLeaks()
.detectLeakedClosableObjects()
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build();
StrictMode.setVmPolicy(vmPolicy);
如果没有及时关闭,日志里会打印相关的堆栈打印
如:W/System: A resource failed to call close,
Native Heap:
方式一、通过perfetto,和Android Profile的抓取,查看执行过程
主要如下几个方面:
1)、数据操作相关的,而且还发现不仅打开数据时会产生内存,执行sqlite语句,也会产生内存,目前还没分析出原因,使用的是加密的数据库,没找到对应的源码,只能暂时放弃了,知道的小伙伴欢迎留言
2)、另外一块就是网络安全请求相关,目前也没想到好的优化方法
方式二、因为找不到Rom编译的产物,带有符号信息so文件,所有放弃了查看当前native堆栈对象
Code:
1、反编译apk,去除不必要的SDK,不必要的代码
1.1、应用是由多个插件化apk的,
使用jadx工具反编译,先分析baseApk发现有引用很多界面相关的代码,
androidx.appcompat:appcompat
com.google.android.material:material
androidx.constraintlayout:constraintlayout
是后台常驻系统应用,不需要界面相关的内容,并且通过搜索反编译的代码,无界面相关的应用,可去除掉,并且引入
去掉之后,release APK由7726KB减少至3983KB
1.2、应用插件apk去掉baseApk已经引入的SDK
可以通过gradlew app:dependencies 命令可以查看SDK引用之间的依赖关系
引入某个SDK间接引入其他SDK的,可以通过在gradle文件里exclude去除如:
通过一通裁剪后,插件APK由7.7M降到3.7M
2、额外引入的SDK,深度裁剪
查看引用的SDK实现的功能,原生Java、Android接口是否有等效接口
案例:应用的某个业务使用到joda-time:joda-time SDK,用于实现某个时间戳是星期几、一年中的第几天等功能。该SDK的引用会带来应用进程1M左右的Code 内存。
□修改方案:使用等效功能的java原生接口(java.util.Calendar)替代额外引用的SDK(org.joda.time.DateTime)的功能,并打包apk时不引用该SDK
裁剪后APK 大小由3.7M降至2.4M
替换的接口,建议大家最好写个单元测试,测试一下替换前后返回的结果都是一致的
Graphics
该部分多数为view,图片等原因导致,本应用这里不涉及,不过多赘述
二、场景内存优化
主要为执行业务过程中,内存是否有优化的空间
主要从如下几个方面分析
1、内存碎片化
在执行周期性任务,或者频繁执行的任务时,避免创建新的实例对象
可能会导致产生很多临时对象,只能等到下次gc触发了才可以释放,会出现内存抖动的情况
在实现自定义view中,咱们都知道不能在onDraw()频繁创建对象
案例:
主要通过dump java heap分析,对象GC引用链为0的(Depth为-),就代表该对象待GC回收
手机静置5分钟左右,出现了大量ThreadPool$ThreadTask,Runnable,ConnectedWiifBean等对象
通过业务代码,可知,
1)、业务会周期性获取wifi连接信息,并且做一些业务处理,周期性的任务调度会产生大量(ThreadTask,Runnable)
修复方案:参考android.os.Message的缓存策略,通过链表方式缓存Message,防止频繁创建新的对象
2)、业务会保持一次队列,只需要保存最近获取到20条wifi连接信息,旧的移除掉
修复方案:复用被移除的对象,清除内部变量值,并重新赋值
2、内存泄露
估计大家都熟悉,这里就不过多赘述,
个人认为只要该内存对象以后多不需要用到了,但是无法gc回收,都属于内存泄露的范畴
主要分析方法为通过MAT查看GC引用链来定位
一般开发过程需要注意,Listener之类的要成对出现,结束后确保结束监听等
3、提前初始化
分离测试代码,正式版本使用哑类来实现
惰性加载等方式延迟初始化,防止无效的内存占用
如果使用kotlin就有现成的语法糖,by lazy,koin依赖注入框架等
三、日常开发建议
1、尽量缩小变量的应用范围,能使用局部变量,传参方式实现的,就不使用全局变量,即可以优化内存开销,也可以解决多线程带来的错误数据
2、线程一定要自定义线程名,便于定位是哪个业务使用的线程
附录:
1、如果有发现不妥的地方,欢迎来扰