想针对启动耗时&卡顿优化做一个专题,一来是复习二来是构建完整知识结构。分析应用的启动势必需要先了解整个启动的流程,那么在分析前,需要先简单总结下整个启动流程的梳理大纲,基于android 9.0版本:
那么我梳理的场景整体流程是:安装一个应用,完成安装之后点击Launcher桌面某App图标,应用启动到主界面显示完成整个过程。过程不涉及代码,具体分析可以去看之前对应的文章,里面有详细的源码分析。
一、应用安装流程
应用安装过程简单说就两步:
1.1 复制Apk到指定目录
主Apk复制路径如下:
system/app 系统应用存放目录
data/app 三方应用存放目录
~:/system/app/Bluetooth # ls -al
total 4048
drwxr-xr-x 4 root root 4096 2009-01-01 08:00 .
drwxr-xr-x 103 root root 4096 2009-01-01 08:00 ..
-rw-r--r-- 1 root root 4124698 2009-01-01 08:00 Bluetooth.apk
drwxr-xr-x 3 root root 4096 2009-01-01 08:00 lib
drwxr-xr-x 3 root root 4096 2009-01-01 08:00 oat
~:/data/app/com.ss.android.article.news-kk9ZSokxPMvccIA7c41aJA== # ls -al
total 26108
drwxrwxr-x 4 system system 3488 2019-10-31 23:57 .
drwxrwx--x 41 system system 3488 2019-10-31 23:57 ..
-rw-r--r-- 1 system system 26690526 2019-10-31 23:57 base.apk
drwxr-xr-x 3 system system 3488 2019-10-31 23:57 lib
drwxrwx--x 3 system install 3488 2019-10-31 23:57 oat
内容与权限一目了然,这里就不赘述了。另外再关注一个目录,data/data,这是各三方应用的私有目录,自然是用户权限的,那么是不是root权限的进程就可以随意访问了呢?其实也不然,在锁屏解锁前,data/data目录内的文件都是按FBE加密的,但是在解锁之后会解密。这属于文件系统的东西了,没有深入研究。
但是我总结的结论是:/data目录下,凡是_ce结尾的是一直加密的,_de结尾是不加密的。其他的没有调研,但是data/data是在锁屏解锁前加密,解锁后解密。感兴趣的可以自行研究Android的文件系统加密。
[FBE加密官方介绍]https://source.android.google.cn/security/encryption/file-based
~:/data/data/com.ss.android.article.news # ls -al
total 73
drwx------ 13 u0_a214 u0_a214 3488 2019-11-01 00:08 .
drwxrwx--x 322 system system 24576 2019-10-31 23:57 ..
drwxrwx--x 2 u0_a214 u0_a214 3488 2019-11-01 00:08 app_textures
drwx------ 3 u0_a214 u0_a214 3488 2019-11-01 00:08 app_webview
drwx------ 2 u0_a214 u0_a214 3488 2019-11-01 00:08 app_webview_2930
drwx------ 2 u0_a214 u0_a214 3488 2019-11-01 00:08 app_webview_3017
drwx------ 4 u0_a214 u0_a214 3488 2019-11-01 00:08 app_webview_3298
drwxrws--x 10 u0_a214 u0_a214_cache 3488 2019-11-01 00:08 cache
drwxrws--x 2 u0_a214 u0_a214_cache 3488 2019-10-31 23:57 code_cache
drwxrwx--x 2 u0_a214 u0_a214 3488 2019-11-01 00:08 databases
drwxrwx--x 26 u0_a214 u0_a214 3488 2019-11-01 00:08 files
lrwxrwxrwx 1 root root 74 2019-10-31 23:57 lib -> /data/app/com.ss.android.article.news-kk9ZSokxPMvccIA7c41aJA==/lib/arm
drwx------ 2 u0_a214 u0_a214 3488 2019-11-01 00:08 lib-main
drwxrwx--x 2 u0_a214 u0_a214 20480 2019-11-01 00:09 shared_prefs
1.2 安装APK:
安装过程简单说就是AndroidManifest.xml的解析,包括名字、版本、权限、四大组件等信息。然后分别做了内存存储和文件持久化。Launcher会获取<action android:name="android.intent.action.MAIN" />和<category android:name="android.intent.category.LAUNCHER" />的Activity,并绑定一个应用图标,点击Launcher对应的应用图标,实际上就是做了一次Activity的隐式启动。
当然应用安装过程是很复杂的,包括权限的检查、文件夹的创建、安装逻辑判断、数据结构的封装等等内容。我这只是挑了几点与启动相关的总结一下。
1.3 其他APK相关知识点
1)APK组成
AndroidManifest.xml
应用整体配置信息。META-INF
签名信息。lib
应用依赖的native .so库,也包括一些插件。根据手机CPU的架构,lib库大体上可以分为4种:ARM、ARM-V7、MIPS和X86,分别对应着4种CPU架构。实际上,市面上的手机几乎全都是ARM架构的,所以大多数情况下我们只需要有armeabi和armeabi-v7a两种类型的库就足够了。res
系统资源目录,与工程对应目录内容一致。assets
跟res目录有点相似,但实际上二者还是有区别的。res目录中的文件会映射到R文件中,每个资源文件都有自己的ID,而assets中的文件则直接通过AssetManager类进行访问,而且assets目录你可以添加任意深度的子目录,这一点会比较方便管理和归类文件。相比较之下,res目录目前不能支持更深级的子目录。附带了解assets与raw的区别:assets不做任何处理被打包,资源通过AssetManager访问,目录可以分层。raw通过资源ID访问,不参与编译,目录不可分层。classes.dex
.java文件通过javac编译生成.class,多个.class通过dex生成.dex文件。.dex是面向Android虚拟机的标准字节码。resources.arsc
编译后的二进制资源文件索引,记录了资源文件(即res目录中的文件)和资源文件ID的映射关系,这样程序运行的时候就可以根据资源的ID获取到相应的资源。
2)编译
App在android上运行,首先需要dex加载到内存,dex执行方式主要有两种:解释器执行 和 执行编译后的机器码 两种执行方式。解释执行就是及时编译,运行到哪解释到哪,这样效率不高,所以google在2.2版本引入了JIT,在解释执行过程中针对热点代码编译为机器码并且优化,然后缓存在内存中,下次执行到相同代码直接调用缓存的机器码,提升效率。但是app进程消亡下次重新冷启就没了。google4.4.推出art虚拟机替换Dalvik,推出AOT编译,即生成的机器码缓存为文件,作为持久化优化,每次编译会更新文件,但是更新频率并不高。优点是机器码持久化保证下次使用也能直接跑机器码,提升执行效率,缺点是编译耗时且CPU抢占严重。为了不影响用户体验,编译时机非常关键。
Android推出4种编译filter:
- verify:只运行 DEX 代码验证。
- quicken:运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解释器性能。
- speed-profile:运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。
- speed:运行 DEX 代码验证,并对所有方法进行 AOT 编译。
执行效率上:
verify < quicken < speed-profile < speed
编译速度上:
verify > quicken > speed-profile > speed
编译触发的几个时机:
路径 | 描述 | 编译方式 | 编译内容 |
---|---|---|---|
Install | 应用安装通过installd触发的编译 | speed-profile | 主apk |
OTA升级 | 系统升级通过installd触发的编译 | verify | 主apk |
load dexFile | 动态加载插件直接通过虚拟机触发的编译 | quicken | 插件 |
postboot | 开机1分钟后,针对全部安装的7天内未使用且过期的应用通过installd触发的编译 | verify | 主apk |
idle | 同时满足充电、idle状态且24小时内只触发一次,主apk通过installd触发编译,插件通过虚拟机触发编译 | speed-profile | 主apk 和 插件 |
另外,编译的内存除了主apk之外还包含应用的插件,主apk间距通过PMS交由installd来触发dex2oat编译,插件则通过动态加载直接由虚拟机触发dex2oat编译。
3)动态加载插件
核心流程在于loadDexFile,这个过程核心逻辑是:先判断是否有.odex文件,然后判断是否过期,如果既有.odex又没过期,那么直接加载,如果不满足则先执行dex2oat编译,编译完之后将.odex加载到内存才开始执行。另外.dex编译过程是持锁的,个人感觉目的可能是防止多处同时触发loadDexFile,避免重复编译。到了Android Q之后,动态加载插件去掉了编译逻辑,也就是如果没有.odex或者有但是过期,直接跳出去加载.dex文件走解释模式,不再进行编译了。 好处是减少第一次加载等锁造成的卡顿,坏处就是解释模式执行效率不高。
这里其实我多加了很多内容,不光是关于应用安装流程的,好了这部分先到这。
上面牵涉到的部分知识点详细细节可以参看之前写过的文章:
Android PMS(一)-启动流程
Android PMS(二)-Apk安装流程
Android PMS(三)-Installd执行dexopt流程
Android PMS(四)-安装微信
启动耗时分析(三)-ART编译分析
Android 9.0 ART编译分析(一)-编译通路梳理
Android 9.0 ART编译分析(二)-Installd触发dex2oat编译流程
Android 9.0 ART编译分析(三)-虚拟机触发dex2oat编译流程