作者:zzy的学习笔记
记录一次开机内存分析的全过程,尽量详尽的介绍常用内存分析工具和命令行的使用,结合具体问题探讨开机内存分析的实践经验。通过这篇文章我会介绍开机内存的常用测试分析工具的基本使用方法,以及如何通过抓取出来的内存数据得出下一步的分析思路,其中包括BootRemainMemory、MTN、Profiler、Mat、Meminfo、Smaps、Showmap。
文章概览:
问题背景:
测试同事反馈开机内存有较大的新增,同时通过对比打包和去除业务X模块的版本发现问题可能出在业务X,因此我们需要分析具体是什么原因。
说明:
分析的这个问题只是一个引子,目的是想通过分析这个问题,学习掌握内存分析的常用工具和思路,所以内容会显得有些冗长,如果有不准确的地方欢迎探讨。
一、发现问题
1.使用BootRemainMemory测试开机内存
1.1 测试方法
按照测试手法进行多组测试。
使用BootRemainMemory需要注意几个点:
-
手动覆盖安装应用和直接内置系统的版本的开机内存是有差异的,这里据同事说是因为两种方式对内存分区有不同的影响,因此我们如果没有条件每次都刷内置版本只能覆盖安装,那着重关注对比增量部分即可。
-
如果设置间隔时间过短也会导致测试数据波动过大,因此这一步最好按照默认时间测试,这也导致比较耗时。
1.2 测试结果
带业务X均值46.5
不带业务X均值43.4
结果:开机内存确实有3MB左右的增长
这里我有个经验就是在跟测试对任何性能问题之前,必须首先确认这几项内容避免浪费时间:测试工具+测试手法+测试包+测试包的运行环境
2.排查是否引入带自启动功能的相关组件
我首先怀疑是否由于新增了自启动功能的组件导致内存新增,因此这里先选择排查内容提供器和静态广播。
2.1 ContentProvider
通过此前文章分析ContentProvider的启动特性+此前的Provider优化经验,我们知道Provider的启动时机是跟随应用自启动,而且Provider的onCreate方法执行早于Application的onCreate,这就很容易带来开机内存的增长。
因此首先怀疑是业务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)来执行,所以该部分内存是程序运行必不可少的。
这里产生一个疑问,我们通过前面的分析可以确定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移除然后使用手动延迟初始化的用法进行替代
<providerandroid:name="androidx.startup.InitializationProvider"android:authorities="${applicationId}.androidx-startup"tools:node="remove" />
初始化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
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap