精彩文章免费看

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、如果有发现不妥的地方,欢迎来扰

最后编辑于:2022-10-06 11:42