网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
因此首先怀疑是业务X有自启动的Provider引入了启动逻辑导致这部分内存增长,因此我们需要查找增量ContentProvider。
2.1.1 方法1
通过解析apk中合并后的AndroidManifest文件,搜索provider标签声明的组件,发现由商业化广告sdk引入一个ContentProvider
2.1.2 方法2
这里除了通过查看Manifest文件以外,还可以通过"dumpsys package +包名"的方式进行查看已声明的组件。
2.1.3 方法3
使用MAT的OQL查询语句查询所有Provider实例,此方法比Profiler更准确,因为如果单纯使用Profiler的搜索功能无法识别名称不包含Provider的实例。
SELECT * FROM INSTANCEOF android.content.ContentProvider
2.1.3 排查结论
再结合Profiler查看确实有一个ProcessProvider在开机阶段被加载。
进一步需要排查该Provider的具体作用和对开机内存的影响范围,进一步咨询广告同事,通过排查这个ContentProvider中生命周期onCreate中是否有异常内存使用情况。
确认结论为:目前初始化基本不涉及业务内存占用。
2.2 BroadcastReceiver
静态广播在开机也会立即被注册,因此还需要排查是否有静态广告在开机后被初始化带动初始化了相关逻辑。
如果我们在用户未同意申明权限的情况 下,是不应该响应接收的功能的,这样也会间接引起内存的增长。
静态广播导致的开机内存增长的例子在我们项目中此前也出现过
因此我们同样使用上面Provider的排查方法发现也没有对应的Receiver起来,排除这个原因的可能性。
3.使用Profiler定向排查怀疑的包名和类
当有确定的怀疑目标的时候,直接使用AS的Profiler进行搜索会效率会更高,因此我搜索了我能想到的业务X的类,只找到了DI注入相关的必须类,没有相关业务加载。
3.1 Profiler基础
内存分析器是 Android Profiler 中的其中一个组件,可帮助识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。
这里只介绍如何利用Profiler查看内存堆转储文件的一些技巧,使用Profiler抓取内存文件或者获取hprof文件使用Profiler打开后就是以下界面。
4.使用Mat对比Heap变化
使用Profiler分析还是有一定局限,只能对已知怀疑的相关类进行逐一排查,对未知的类引入我还不是很确定,因此我进一步抓取了两份hprof使用MAT进行对比分析,试图找出具体的增量原因。
适合使用mat分析的场景包括:需要对比某个行为前后的内存,需要对比两个不同版本的内存等。
4.1 Mat基础和分析技巧
4.1.1 Histogram
MAT中Histogram的主要作用是查看一个instance的数量,一般用来查看自己创建的类的实例的个数,通过查看Object的个数,结合代码就可以找出存在内存泄露的类,Histogram中还可以对对象进行Group,更方便查看自己Package中的对象信息
4.1.2 Dominator Tree
可以很容易的找出占用内存最多的几个对象,根据Percentage(百分比)来排序
Dominator Tree和Histogram的区别是站的角度不一样,Histogram是站在类的角度上去看,Dominator Tree是站的对象实例的角度上看,Dominator Tree可以更方便的看出其引用关系。
4.1.3 List objects -> with incoming references/with outcoming references
查看这个对象持有的外部对象引用/查看这个对象被哪些外部对象引用
4.1.4 Path To GC Roots -> exclude all phantim/weak/soft etc. references:
查看这个对象的GC Root,不包含虚、弱引用、软引用,剩下的就是强引用,通常用于查看内存泄漏的根引用关系,还有其他几种模式就不一一解释了。
4.1.5 OQL语句
SELECT * FROM com.oplus.assistantscreen.window.AssistantWindowBlurSlideView //查询某个具体类
SELECT * FROM INSTANCEOF android.content.ContentProvider // 查询所有的的Provider以及其子类的实例
SELECT * FROM INSTANCEOF java.lang.Exception exceptions// 查询所有的异常对象以及其子类的实例
SELECT * FROM java.lang.String s WHERE s.count >= 100 //查询长度超过 100的字符串
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)来执行,所以该部分内存是程序运行必不可少的。
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
a3d350b5d3dd95521b41ecf.webp?x-oss-process=image/format,png)
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
[外链图片转存中…(img-FmjwoUkb-1715530849110)]
[外链图片转存中…(img-coeDfQ0X-1715530849110)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新