深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
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()
实现原理上injec只是对get进行了Lazy封装而已,时机也是调用get方法
3.代码缩减
3.0 编码规范优化
对共用能力使用表格统一管理,便于查询防止不同的开发新增相同能力的重复代码
3.1 PB降级为JSON
由于PB的特性,导致很简单的一段代码也会生成巨量格式化代码,而且这巨量的代码还必须不能混淆。
一方面PB是为了减少传输量进而降低网络负载而存在,但其实有些是非常低频的没必要一直占着客户端稀缺的内存,这里也有一些优化空间。
另一方面则是查看是否有可以做减法的PB对象,比如服务端定义的通用接口有10个字段,但是我们只需要用5个,则可以只定义这5个。
比如这个一个字段也用的pb,导致很简单的一段代码也会生成巨量格式化代码
3.2 通过扫描工具sonar检查重复代码进行优化
使用工具对重复代码进行检查并人工优化
4.代码混淆
4.1 增加混淆
混淆通过缩短应用的类、方法和字段的名称来缩减应用的大小
混淆可以检测并删除未使用的类,字段,方法和属性
4.2 根据包名进行混淆分组
Dex文件中数据基本是按类名的字母顺序进行排列的,这样同样包名的类会排在一起。但在实际程序执行中,同一个package下的类并不会全部一起调用,而是和很多其他package下的类进行交互,但mmap加载了整个页面,可能会有很多无用数据。为了减少这样的情况,我们在生成文件时要尽量将使用到的数据内容排布在一起。在APK的编译流程中,Proguard混淆工具正好是能够对类名进行修改的,可以根据程序运行的逻辑,将那些会互相调用的类改为同一个package名,这样就可以使它们的数据排布在一起,通过混淆整理dex文件顺序,减少由于4k限制导致打开的文件页大小
4.3 减少混淆粒度
·例如只豁免需要被xml文件引用的自定义View,而不是豁免全部的自定义View
·例如只豁免Parceable实现类中的CREATOR成员变量,而不是整个类
5.类库裁剪
5.1 集成使用lite库等相同功能但内存占用更小的库
5.2 只保留重复功能的其中一个库
例如视频播放器优化为只采用一个,图片库只集成一个
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
]( )**
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
[外链图片转存中…(img-YPuvNXzv-1715220007317)]
[外链图片转存中…(img-z89n6Btr-1715220007317)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新