前言:
术语
开机内存: 手机连接Wifi热点、插入注册网络的SIM卡,重启后、静置5分钟,采集的进程内存值;
常驻内存: 业务进程工作任务结束退至后台、静置5分钟,采集的进程内存值;
动态内存: 业务进程在后台运行工作任务时,采集的进程内存峰值;
场景内存: 用户使用典型业务场景时,相关依赖服务进程在前台、后台运行的内存总和;
驻留比率: 用于标识进程在后台的常驻概率,驻留比率=“B Serveices优先级及以上采样命中数” /“总样本数”。
VSS: Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
VSS表示一个进程可访问的全部内存地址空间的大小。这个大小包括了进程已经申请但尚未使用的内存空间。在实际中很少用这种方式来表示进程占用内存的情况,用它来表示单个进程的内存使用情况是不准确的。此大小还包括可能不驻留在RAM中的内存,如已分配但未写入的malloc。 VSS对于确定进程的实际内存使用非常少用。
RSS: Resident Set Size 实际使用物理内存(包含共享库占用的内存)
表示一个进程在RAM中实际使用的空间地址大小,包括了全部共享库占用的内存,这种表示进程占用内存的情况也是不准确的。RSS可能会产生误导,因为它报告进程使用的所有共享库的总数,即使共享库只加载到内存中一次,无论有多少进程使用它。 RSS不是单个进程的内存使用的准确表示。
PSS: Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
表示一个进程在RAM中实际使用的空间地址大小,它按比例包含了共享库占用的内存。假如有3个进程使用同一个共享库,那么每个进程的PSS就包括了1/3大小的共享库内存。这种方式表示进程的内存使用情况较准确,但当只有一个进程使用共享库时,其情况和RSS一模一样。
PSS可能有点误导,因为当进程被杀死时,PSS不能准确地表示返回到整个系统的内存。
USS: Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)表示一个进程本身占用的内存空间大小,不包含其它任何成分,这是表示进程内存大小的最好方式!USS是一个非常有用的数字,因为它表示运行特定进程的真正增量成本。当进程被终止时,USS是实际返回到系统的总内存。 USS是判断进程中的内存泄漏时最值得注意的数字。
一般来说占用大小有如下规律:VSS >= RSS >= PSS >= USS
常用分析工具
Android Studio Profile
Android Studio 3.0 及更高版本中的 Android Profiler 取代了 Android Monitor 工具。Android Profiler 工具可提供实时数据,帮助您了解应用的 CPU、内存、网络和电池资源使用情况。该工具大家用得比较多,这里就不过多赘述。
传送门:developer.android.google.cn/studio/prof…
Memory Analyzer Tool
MAT 是一个快速,功能丰富的 Java Heap 分析工具,通过分析 Java 进程的内存快照 HPROF 分析,从众多的对象中分析,快速计算出在内存中对象占用的大小,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象。
注意: adb或Android studio Profile抓取的heap文件,需要使用(AndroidSdk\platform-tools\hprof-conv.exe)转换之后才能打开
命令:hprof-conv [-z] <infile> <outfile>
传送门:www.eclipse.org/mat/
Perfetto
可以分析内存产生过程的方法栈,区别于hprof文件,heap文件时某个时刻的内存。而perfetto作用的是过程内存。一般用于分析可以复现内存问题的场景,这里不过多赘述。
传送门:ui.perfetto.dev/#!/record?p…
Jadx-gui
jadx是个人首选的反编译利器,同时支持命令行和图形界面,能以最简便的方式完成apk的反编译操作。
下载地址
常用adb命令
adb shell dumpsys meminfo
查看进程内存分布信息,如:adb shell dumpsys meminfo com.demo
注意该方式会产生一次gc,加上--local可以避免触发gc
adb shell "dumpsys meminfo | grep pcakgename"
查看某个进程内存总值,该方式不会触发gc
adb shell showmap <pid>
可以确定进程中引用的库占用的内存大小,包括so库,分析code的内存部分,多进程的情况,特别有帮助
adb pull proc/<pid>/smaps
code,system的内存分布,可以用于分析代码量内存分布,包括系统的类
adb shell am dumpheap
dump javaHeap部分的对象实例,可以借助Android Profile,MAT打开hprof文件分析
Android Profile会显示所有的对象实例,包括可以待gc回收的对象,而且方便清除知道对象的变量,方便定义是什么业务产生的
MAT只会显示不会被gc回收的对象,可以查看GC链,但是可以对比两个hprof文件差异性
adb shell am dumpheap -n
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工程解析也可以
传送门:
native_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等
通过java Heap文件可知,
-
其中一个内存大块为数据库相关,应用由5个db数据库,
只能做到延迟加载,使用到对应的数据库之后才进行加载,并且减少数据库执行语句的缓存数,
SQLiteDatabase.setMaxSqlCacheSize()
另外,我也尝试过,写一个有效期的Map,长时间不使用主动关闭数据库,并且释放缓存。发现没法完全释放掉,会残留一些用于同步的ThreadLocal对象,如果频繁的创建,连接、关闭数据库,就会累计很多无用的对象,导致内存泄漏,所有我放弃了该方式 -
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);
如果没有及时关闭closeable对象,日志里会打印相关的堆栈打印
如: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、如果有发现不妥的地方,欢迎来扰