深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
SELECT * FROM INSTANCEOF java.util.HashMap s WHERE s.size>10//查询长度超过10的HashMap
4.2 hprof命令行抓取方法
抓取hprof有一定条件,如果整机是root的版本,那不管是release的还是debug的都能抓,如果是非root的版本,则需要是debug的进程才能抓。
hprof 文件可以在代码中进行 dump,也可以用 Android Studio进行 dump , 也可以使用其他第三方工具进行 dump。
这里我介绍另外一种使用命令行dump的方式,先查询进程号-再使用dumpheap进行抓取-最后导出文件即可。
D:\Users\80343288>adb shell
OP5267:/ # ps -ef|grep assis
u0_a237 4907 833 2 14:27:29 ? 00:00:00 com.xx.xx
u0_a237 5530 833 12 14:27:33 ? 00:00:03 com.xx.xx:xx
root 8663 7044 15 14:27:55 pts/1 00:00:00 grep assis
OP5267:/ # am dumpheap -g 4907 /data/local/tmp/heapdump_noinfo.hprof
File: /data/local/tmp/heapdump_noinfo.hprof
Waiting for dump to finish…
OP5267:/ # exit
D:\Users\80343288>adb pull /data/local/tmp/heapdump_noinfo.hprof
/data/local/tmp/heapdump_noinfo.hprof: 1 file pulled, 0 skipped. 30.5 MB/s (36076684 bytes in 1.130s)
一般使用AS或者命令行抓取的hprof文件要导入Mat之前,需要使用hprof-conv工具进行转换格式
首先进入sdk自带的hprof-conv工具目录(D:\Software\Android Studio Others\win-sdk\platform-tools目录)
执行如下命令:
hprof-conv -z memory-20200625T145636.hprof mat.hprof
4.3 使用Mat对比
使用mat分别导入需要对比的两份hprof文件,然后选择对比
特别注意要对比的话不要使用release版本的hprof文件,因为每次混淆后的类名是不一致的,会导致对比可读性很低。
4.4 排查结果
从这里的对比结果可以看到没有明显的对象分配数量异常,由于hprof只能观察Java堆内存的分配情况,因此增长应该来自于其他,需要抓取更完整的内存文件进行分析。
注意:虽然我们这个问题在这里没有得到结论,但是一般情况下,要是有内存泄漏或者是大内存的分配异常,通过这样对比两份内存文件再排序,就会很直观的暴露出来,这是很有效的办法。
5.抓取meminfo查看整体内存状况
5.1 meminfo基础和分析技巧
使用meminfo可以查看指定进程或包名的内存整体使用情况,一般查看这个文件后可以确定是哪个内存大块出问题,而且这个内存信息相对来说获取成本较低,是我们分析内存最常用的方式。
meminfo的数据来源是解析smaps等信息分类汇总而成,数据单位KB,详细原理见后面的参考文档。
5.1.1 meminfo基础解读
5.1.2 常用分析思路
分析思路一般是抓取两份meminfo进行对比查看,根据对比结果,按照以下路径进一步分析是哪一部分的问题
-
如果是 Dalvik 部分内存变大,需要去查看 hprof 文件
-
如果是 Native 部分内存变大,需要去根据 Native Debug 的文档,配合 hprof 文件进行分析,大部分 App 的 Native 内存变大都是 Java 层的调用导致的
-
如果是Graphics增大,则查看具体是 GL mtrack / EGL mtrack
-
需要查看 gfxinfo 的结果
-
需要对比两台机器的分辨率、App 的 SurfaceView 、TextureView、Webview 等使用情况
-
需要查看 App 硬件加速的使用情况
-
如果是 so / jar / apk / ttf 变大,进一步获取smaps查看 so / jar / apk / ttf 的个数,对比查看是哪部分变大,或者是因为多了哪个 so / jar / apk / ttf 导致的
-
如果是 dex/oat/art 变大,则需要对比两个 app 的运行状态、应用版本号是否一致,由于这部分与 Android 运行时的关系比较大, 需要使用 user 版本进行测试
5.2 抓取meminfo
该命令行无需手机root,无需一定要debug版本的应用
注意:一般可以多dump几次以最后一次为准,因为每次执行会默认触发一次强制GC
D:\Users\80343288\mem_tmp>adb shell dumpsys meminfo com.xx.xx> meminfo_noinfo.txt
5.2.1 dumpsys命令介绍
dumpsys这个指令很有用,除了可“dumpsys meminfo+包名”以抓取内存,还可以“dumpsys package +包名”查看包信息(此前有看到其他同事使用这个指令查看应用是否有system flag来确定是否是因为没有system标识而被冻结导致的ANR),“dumpsys gfxinfo +包名”查看显示渲染信息进而查看卡顿情况。
adb shell dumpsys [options]
■ meminfo 内存
■ cpuinfo CPU
■ gfxinfo 帧率
■ display 显示
■ power 电源
■ battery 电池
■ batterystats 电池状态
■ location 位置
■ alarm 闹钟
■ account accounts
■ activity 显示所有的activities的信息
■ window 显示键盘,窗口和它们的关系
■ wifi 显示wifi信息
5.3 排查结果
查看对比两份meminfo后结论为主要是dex mmap带来的增长。
通过meminfo能查看内存的大概情况,进一步分析则需要借助抓取smaps文件
6.抓取smaps查看内存细节
我们通过dumpsys meminfo 获取内存时, 发现某一项内存数据异常,想弄清楚数据都是有哪些文件产生, 我们就可以通过读取smaps详细排查或者进行增量对比,smaps聚合统计到的数据, 可以清晰的看到哪一个dex、so、ttf、oat所占的内存,这部分信息adb shell dumpsys meminfo是不具有的。
6.1 smaps基础
使用smaps统计出来的内存和使用adb shell dumpsys meminfo是一致的,dumpsys meminfo 命令下的 Pss、Shared Dirty、Private Dirty这三列的数据是读取smaps文件生成。
基本信息含义如图:
6.2 抓取smaps
抓取smaps文件一定需要root权限才可以, 这也是手机厂商具有的优势,抓取命令如下:
adb shell cat /proc/$pid/smaps > smaps.txt,//需要root权限,无需一定要debug版本应用
由于测试开机内存需要清除数据,因此再介绍一个方便的清除数据的命令(高版本需要root)
adb shell pm clear com.xx.xx
smaps 记录了这个进程内存映射的原始信息,不过 smap 直接看的话并不是很友好,一般是用一个脚本,让 smaps 和 meminfo 那个结果结合起来看。
6.3 解析smaps
先介绍一下如何使用smaps_parser.py脚本进行解析
解析脚本:
解析命令:
python D:\Users\80343288\Downloads\smaps_parser(1).py -f D:\Users\80343288\smaps.txt >smaps_parsed.txt
解析后的文件:
6.4 排查对比smaps结果
通过对两个版本分别抓取smaps解析对比,咱们知道dex mmap增长是由于base.vdex文件增长导致
dex mmap文件相差6MB左右,因此我再去对应目录查看了原始文件进行确认,发现两个原始vdex文件确实相差大概10MB左右,如图:
7.showmap
其实通过smaps已经定位到了问题,这里再额外介绍一下showmap
在smaps不能被解析之前是比较难分析的,因此也可以直接抓取showmap查看内存情况,showmap 就是解析了 smaps 的信息,这里可以看到进程中每个打开的文件所占用的内存,但是相比解析后的smaps,还是不如smaps直观,没有分类汇总。
抓取命令如下:
adb shell showmap –t $pid > showmap.txt //需要root权限,无需一定要debug版本应用
二、分析问题
综上,我们通过一系列工具知道开机内存增长是由于base.vdex文件增长导致,那我们接下来首先弄清楚vdex是什么,以及他和dex、odex、oat、art文件的关系是啥?
1.dex相关概念
1.1 dex
dex 文件是可被Dalvik虚拟机识别并执行的文件。
JVM执行的.class文件通过dx.bat工具就可以转换为dex ,Dalvik 会执行 .dex 文件中的 dalvik 字节码,但一般Dalvik在执行dex优化后的文件(即odex文件)。
1.2 vdex(Verified Dex)
vdex文件是 Android O (Android 8.0) 新增的格式包,其目的是为了降低dex2oat时间。
为了避免不必要的验证Dex 文件合法性的过程,例如首次安装时进行dex2oat时会校验Dex 文件各个section的合法性,这时候使用的compiler filter 为了照顾安装速度等方面,并没有采用全量编译,当app盘启动后,运行一段时间后,收集了足够多的jit 热点方法信息,Android会在后台重新进行dex2oat, 将热点方法编译成机器代码,这时候就不用再重复做验证Dex文件的过程了
1、当系统OTA后,对于安装在data分区下的app,因为它们的apk都没有任何变化,那么在首次开机时,对于这部分app如果有vdex文件存在的话,执行dexopt时就可以直接跳过verify流程,进入compile dex的流程,从而加速首次开机速度;
2、当app的jit profile信息变化时,background dexopt会在后台重新做dex2oat,因为有了vdex,这个时候也可以直接跳过
1.3 odex(Optimised Dex)
odex是OptimizedDEX的缩写,表示经过优化的dex文件,存放在/data/dalvik-cache目录下。
由于Android程序的apk文件为zip压缩包格式,Dalvik虚拟机每次加载它们时需要从apk中读取classes.dex文件,这样会耗费很多cpu时间,而采用odex方式优化的dex文件,已经包含了加载dex必须的依赖库文件列表,Dalvik虚拟机只需检测并加载所需的依赖库即可执行相应的dex文件,这大大缩短了读取dex文件所需的时间。
对于dalvik虚拟机,odex存放的是JIT后的优化后的字节码(Optimized Dalvik EXcutable file)
对于ART,odex存放的是经过AOT(Ahead Of Time)编译后的本地机器码(即:oat文件,一种私有的ELF文件格式)
在Android N之前,Dalvik虚拟机执行程序dex文件前,系统会对dex文件做优化,生成可执行文件odex,保存到data/dalvik-cache目录,最后把apk文件中的dex文件删除。
在Android O之后,odex是从vdex这个文件中 提取了部分模块生成的一个新的 可执行二进制码 文件 , odex从vdex中提取后,vdex的大小就减少了。
具体过程:
1.第一次开机就会生成在/system/app//oat/下
2.在系统运行过程中,虚拟机将其 从“/system/app”下 copy到 “/data/davilk-cache/”下
3.odex + vdex = apk的全部源码 (vdex并不是独立于odex的文件,odex + vdex才代表一个apk)
1.4 oat
ART虚拟机运行的是oat文件,oat文件是一种Android私有ELF文件格式,oat文件包含有从dex文件翻译而来的本地机器指令,还包含有原来的dex文件内容(如下图所示),因此oat文件比odex文件更大。APK在安装的过程中,会通过dex2oat工具生成一个OAT文件(文件后缀还是odex)。
对于apk来说,oat文件实际上就是对odex文件的包装,即oat=odex,而对于一些framework中的一些jar包,会生成相应的oat尾缀的文件,如system@framework@boot-telephony-common.oat
注意: Android5.0 及之后的版本,oat文件的后缀还是odex,但是已经不是android5.0 之前的文件格式,而是ELF格式封装的本地机器码。可以认为oat在dex上加了一层壳,可以从oat里提取出dex
1.5 art
目的是用于加快应用启动速度。
art文件是由虚拟机执行odex文件后,记录虚拟机执行Apk启动的常用函数地址信息后生成出来的文件(记录函数地址信息方便寻址),通常会在data/dalvik-cache/ 目录中保存常用的jar包的相关地址记录
2.dex执行流程
3.dex mmap
dex mmap在Android应用中的作用是映射classes.dex文件。dalvik虚拟机需要从dex文件中加载类信息,字符串常量等;
还需要在调用函数的时候直接从mmap内存中读取函数代码(dvm bytecode)来执行,所以该部分内存是程序运行必不可少的。
这里产生一个疑问,我们通过前面的分析可以确定hprof文件中并没有实例化导致内存增长的相关类,难道dex mmap是一股脑全部加载出来,并不是按需加载?
3.1 dex并非完全按需加载
以一个示例应用为例,我们能够在MAT中看到,应用加载了大约1500个class类型,而dex文件的class类型共有10635个。
使用dex mmap动态统计功能统计后发现,虽然只加载了1500个类,但dex内存通常却高达4-6M,差不多是dex文件大小的一半。以上数据中可以看到,很大一部分dex内存空间被浪费了,实际使用到的数据和代码并没有那么多,这是为什么呢?
这是由于dex文件在生成时是按字母顺序排列。由于4K页面加载的原因,实际运行时会加载许多相邻但不会被用到的数据。例如在代码中使用了A1类,虚拟机就需要加载包含A1类数据的页面。但由于A1的数据只有1K,那在加载的4K页面中,还会有A2A3A4类,总共占用了4K内存。假设我们的代码里在用到A1类后,还会用到B1C1D1类,那则会加载很多额外的dex文件。
3.2 dex优化
那么如果能在dex文件中将A1B1C1D1类放在一起,虚拟机就只需要加载一个4K页面,不仅减少了内存使用,还对程序的启动速度有好处。
因此,优化dex的思路就是调整Dex文件中数据的顺序,将能够用到的数据紧密排列在一起,或者是对dex直接做减法,具体的思路有多种咱们放到后面汇总当中介绍。
另外我们也得到一个经验:在优化内存时,不只有堆内存,还有其他许多类型的内存能够进行分析和优化,比如这里的dex带来的内存。
三、开机内存优化思路汇总
通过前文的分析,咱们文章开头提到的开机内存增加问题的原因很清晰:主要是由业务X静态代码和依赖库增多导致。
针对这个dex增加的问题最直接的解法肯定是对代码做减法,这里其实又和apk体积优化思路是相通的,但在日常开发中,随着需求的增加,代码量增加是必然的,有时候可能无法进一步缩减代码,咱们其实还有很多其他曲线救国的思路可以尝试,这里我做一个扩展并对收集到的方法进行分类汇总。
1.Dex布局优化
将启动期间要执行的所有代码添加到主要 classes.dex 文件中,同时将所有非启动代码从主要 classes.dex 文件中移除,使用者提供程序启动时加载类序列作为配置文件,按此顺序调整dex中类的顺序,可以有效提升冷启动速度和优化dex mmap内存。
Dex布局重排有两种方式,可以使用官方的插件进行配置或者使用facebook的redex插件。
但是一个受限于环境,一个受限于gradle插件版本要求,实现有一定难度,但如果能落地成功这个效果估计比较明显。
2.按需延迟加载
开机内存优化目的就是在在用户未同意申明权限或者是在业务未正式使用之前,占用较少的内存。所以我们的懒加载条件就是,仅有当用户同意权限,才加载需要的内容;或者是需要真正用到对应的功能才初始化对应的业务类,避免一切非必要的提前初始化。
2.1 Provider优化
–同一个应用IPC功能的Provider可以合并多个provider为一个
–避免在Provider的生命周期函数中做事情
–借助Provider手动收集业务逻辑延后初始化
–只是作为初始化入口的使用StartUp代替
–使用手动调用替代借助Provider触发初始化(例如WorkManager、StartUp、Lifecycle、LeakCanary)
2.2 检查startup是否按需加载
startup框架默认也是开机就起来的,需要将默认的Provider移除然后使用手动延迟初始化的用法进行替代
初始化WorkManager一共有三种方式:1是通过声明默认的WorkManagerInitializer初始化,缺点是没用到也会被加载 2是通过显式调用WorkManager.initialize手动初始化,缺点是没初始化就调用WorkManager.getInstance就使用就会崩溃 3是Application实现Configuration.Provider接口按需延迟初始化
特别注意是防止跟startup绑定的WorkManager开机起来就触发读取WorkDatabase恢复执行定时任务,导致开机内存增加。
可以看到我们的工程目前就使用了默认的启动方式,未对InitializationProvider配置remove,具有开机内存风险,可以优化。
2.3 避免非必要三方库提前加载
比如RxJava延迟初始化,可以使用自定义线程池替代线程分发
2.4 静态广播慎用
静态广播开机起来就会开接收外部广播,很有可能会导致一些不必要的内存占用,可以移除该静态广播考虑用动态广播代替
2.5 延迟初始化任何非必须类和属性
在用户进入同意权限后或者在进入对应页面真正要用到该类的时候再触发对应业务类的初始化
对属性使用by Lazy
2.6 Koin优化
2.6.1 按需注册
增加按需懒注册,用于在权限声明同意后再注册。
比如目前我们项目中Koin进程起来就注册了全部的类,是有优化空间的,可以新增一个注解类型用于控制注册时机,避免开机就注册,目前占用内存如下
2.6.2 懒注入
使用inject方式获取注入对象,避免使用Koin.get获取
//非懒加载,get()这里的功能是直接检索实例(非延迟)
val str : String = getKoin().get()
//懒加载,在用到的时候才注入
val str2:String by inject()
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
于控制注册时机,避免开机就注册,目前占用内存如下
2.6.2 懒注入
使用inject方式获取注入对象,避免使用Koin.get获取
//非懒加载,get()这里的功能是直接检索实例(非延迟)
val str : String = getKoin().get()
//懒加载,在用到的时候才注入
val str2:String by inject()
[外链图片转存中…(img-888R8Aad-1715750070823)]
[外链图片转存中…(img-qC5EFATG-1715750070823)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!