背景
- 下载转化率
- 推广成本
- Google Play限制:APK Expansion Files
- 安装之间时间
APK是什么
Android应用程序包(Android application package,APK)是Android操作系统使用的一种应用程序包文件格式,用于分发和安装移动应用及组件。一个Android应用程序的代码想要在Android设备上运行,必须先进行编译,然后被打包成为一个被Android系统所能识别的文件才可以被运行,而这种能被Android系统识别并运行的文件格式便是“APK”。 一个APK文件内包含被编译的代码文件(dex 文件),文件资源(resources), assets资源(assets),证书(certificates),和清单文件(manifest file)。
Android Studio打包过程
- 转化资源。
第一类资源是assets类资源,开发时会将其置于根目录的assets子文件夹下。这个文件夹下可以放任何类型的文件,并且这些文件不会经过任何处理的,最终跟随着打包,被原封不地放在APK文件里。
第二类资源是放在工程根目录res文件夹下的res类资源,主要是APP的图标、背景图片、界面布局文件、字符串和颜色信息等。res类资源绝大部分都会在APK构建过程中被转换成二进制化文件,并且在R类中生成ID,便于在代码中直接通过ID来访问res类的资源。 - 转化adil文件
aidl是指Android接口定义语言,主要用户跨进程代码接口的定义。 adil工具会将aidl后缀的文件转化生成Java代码,用于跨进程通信。 - 将源代码文件编译为class文件
将生成的R文件、Kotlin或者Java源代码以及通过aidl转化生成的Java代码经过编译器转换成.class文件。 - 将class文件转化为dex文件
dx工具会通过生成常量池,消除冗余数据等手段,将上一步转化得到的class文件转化为dex文件(Dalvik字节码文件),此时文件大小会减少50%左右。 - 打包
获得经过处理后的资源文件和一个dex文件,然后将其打包成一个类似于APK的文件。 - 通过签名工具对其进行签名
安装Android应用的时候,系统会对APK中的签名文件进行验证,一旦检测到该文件丢失或者签名与APK不符合,系统会立即终止这个APK的安装。因此,应用程序在对外发布之前一定要进行签名,并保存好key和密码,防止被伪造的篡改。
dex是啥?
在明白什么是 Dex 文件之前,要先了解一下 JVM,Dalvik 和 ART。JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。Dalvik 是 Google 设计的用于 Android平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在Android 4.4推出。ART 比 Dalvik 的性能更好。Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式。所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。
虚拟机
JIT意思是Just In Time Compiler,就是即时编译技术,与Dalvik虚拟机相关。
JIT是干嘛的
JIT在Android2.2到Android4.4版本(7.0版本也有,后文会叙述),JIT的目的是为了提高Android的运行效率。
Dalvik虚拟机可以看做是一个Java虚拟机。在 Android系统初期,每次运行程序的时候,Dalvik负责将dex翻译为机器码交由系统调用。这样有一个缺陷:每次执行代码,都需要Dalvik将操作码代码翻译为机器对应的微处理器指令,然后交给底层系统处理,运行效率很低。
为了提升效率Android在2.2版本中添加了JIT编译器,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行即时编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。JIT 编译器可以对执行次数频繁的 dex/odex 代码进行编译与优化,将 dex/odex 中的 Dalvik Code(Smali 指令集)翻译成相当精简的 Native Code 去执行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。
JIT缺陷
每次启动应用都需要重新编译(没有缓存)
运行时比较耗电,耗电量大
ART和AOT
AOT是指"Ahead Of Time",与"Just In Time"不同,从字面来看是说提前编译。
AOT是干嘛的
JIT是运行时编译,是动态编译,可以对执行次数频繁的dex代码进行编译和优化,减少以后使用时的翻译时间,虽然可以加快Dalvik运行速度,但是有一个很大的问题:将dex翻译为本地机器码也要占用时间。 所以Google在4.4推出了全新的虚拟机运行环境ART(Android RunTime),用来替换Dalvik(4.4上ART和Dalvik共存,用户可以手动选择,5.0 后Dalvik被替换)。
AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex预编译成 ELF 文件,每次运行程序的时候不用重新编译。 ART 对 Garbage Collection(GC)过程的也进行了改进:
只有一次 GC 暂停(Dalvik 需要两次)
在 GC 保持暂停状态期间并行处理
在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
压缩 GC 以减少后台内存使用和碎片
AOT的缺陷
应用安装和系统升级之后的应用优化比较耗时(重新编译,把程序代码转换成机器语言)
优化后的文件会占用额外的存储空间(缓存转换结果)
JIT和AOT共存
Android 7.0上,JIT 编译器被再次使用,采用AOT/JIT 混合编译的策略,特点是:
应用在安装的时候dex不会再被编译
App运行时,dex文件先通过解析器被直接执行,热点函数会被识别并被JIT编译后存储在 jit code cache 中并生成profile文件以记录热点函数的信息。
手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。
Gradle是 Android 官方的编译工具
Dex.groovy//----------------------生成Dex文件
AidlCompile.groovy//--------------编译Aidl库
GenerateBuildConfig.groovy//------分析编译配置
Lint.groovy//---------------------进行Lint扫描
MergeAsserts.groovy//-------------整合Asserts目录下的资源
MergeResources.groovy//-----------整合资源文件
NdkCompile.groovy//---------------编译NDK库
PackageApplication.groovy//-------打包App
PreDex.groovy//-------------------生成Dex的准备工作
ProcessAndroidResources.groovy//--处理资源文件
ProcessAppManifest.groovy//-------处理Manifest文件
ZipAlign.groovy//-----------------压缩并对其操作
Buck
Google 也做了大量的其他优化,例如使用AAPT2替代了...
Android APK安装的过程
在启动的过程中,Android底层会在内核空间启动一个叫做应用程序管理(Package Manager Service)的服务。这个服务通过扫描和解析系统中特定目录下的APK文件,得到应用程序的相关信息,完成应用程序的安装。
简单来说,应用程序管理服务安装应用程序就是在解析Manifest.xml文件,通过对Manifest.xml的读取,可以获取到应用程序的图标、名字以及四大组件等信息。
APK安装步骤:
- 拷贝apk文件到指定目录
APK文件是会被系统静默保存的,安装的APK的时候首先会被复制一份到 /data目录下的/app文件夹内。用户对这个文件夹有权限访问的,在APK安装时,系统会使用这个文件夹来存放安装文件。 - 解压apk,拷贝文件,创建应用的数据目录
为了缩短app的冷启动时间,apk在安装的时候,会首先将apk内的dex文件复制到 /data目录下面的/dalvik-cache文件夹里缓存起来。然后在/data/data/目录下创建应用程序包名为文件夹名的目录,存放应用的相关数据,如sqlite文件、xml文件、缓存文件、二进制的so动态库等等。 - 解析apk的Manifinest.xml文件
Android系统通过/data/system/packages.xml文件记录当前手机上所有安装的应用的基本信息,每次系统安装或者卸载了任何APK文件,都会去更新这个文件。
安装APK时,系统会解析APK中的Manifinest.xml文件,提取出这个APK的权限、应用包名、APK的安装位置、版本、userID等等重要信息写入到packages.xml文件中。 - 显示图标
最后,系统通过一个Launcher程序来展示安装的APK。Launcher程序从PackageManagerService服务中获取到这些安装好的应用程序信息。
包大小优化
资源文件文件检测
- 重复文件处理
- 图片压缩
so库
- 动态加载
- abiFilter
R文件
随着项目中资源的增加,会发现生成的dex文件里面R.class文件越来越大。我们知道真正使用资源的地方都是以R.xxx.xxx这种方式访问的,而R.xxx.xx是对应于.arsc文件里面的一个常量值。
Dex 分包
defines 19272 methods
references 40229 methods
“define classes and methods" 是指真正在这个Dex中定义的类以及它们的方法。而“reference methods”指的是define methods以及define methods弓|用到的方法。
简单来说,如下图所示如果将Class A与Class B分别编译到不同的Dex中,由于method a
调用了method b,所以在classes2.dex 中也需要加上method b的id。
因为跨Dex调用造成的这些冗余信息,它对我们Dex的大小会造成哪些影响呢?
- method id爆表。我们都知道每个Dex的method id需要小于65536,因为method id的
大量冗余导致每个Dex真正可以放的Class变少,这是造成最终编译的Dex数量增多。 - 信息冗余。因为我们需要记录跨Dex调用的方法的详细信息,所以在classes2.dex我们还需
要记录Class B以及method b的定义,造成string_ ids、type ids、 proto_ ids 这几部分信
息的冗余。
Dex 压缩
参考FaceBook APP
资源混淆
R.string.name -> R.string.a
res/drawable/icon -> res/s/a
- resources.arsc。 因为资源索引文件resources.arsc需要记录资源文件的名称与路径,使用混,
淆后的短路径res/s/a,可以减少整个文件的大小。 - metadata签名文件。签名文件MF与SF都需要记录所有文件的路径以及它们的哈希值,使
用短路径可以减少这两个文件的大小。
Android APP Bundle
优化
ProGuard
从Android2.3开始,Google在SDK中加入了一款叫ProGuard的代码混淆工具,ProGuard会删除这些调试信息,并用无意义的字符序列来替换类名、方法名等,使得使用反编译出来的代码难以阅读,提升逆向难度。
ProGuard它可以混淆Android项目里面的java代码,
注意:仅仅是java代码。它是无法混淆Native代码,资源文件drawable、xml等,资源压缩通过 Android Plugin for Gradle 提供,该插件会移除封装应用中未使用的资源,包括代码库中未使用的资源,两者协同使用,使得在移除未使用的代码后,任何不再被引用的资源也能安全地移除。
ProGuard的三大作用:
- 压缩(Shrinking):移除未被使用的类、属性、方法等,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员。
- 优化(Optimization): 优化字节码,并删除未使用的结构。(内联、修饰符、合并类)
- 混淆(Obfuscation):将类名、属性名、方法名混淆为难以读懂的字母,比如a,b,c等,增大反编译难度。
D*
dx工具是一种用来转换Java class成为DEX格式的工具。多个类被包含在一个dex文件之中。各个类中重复的字符串和其他常数只在DEX中存放一次,以节省空间。Java字节码(bytecode)被转换成Dalvik虚拟机所使用的替代指令集。一个未压缩dex文件通常稍小于一个已经压缩的.jar档
d8
Google 一直在致力于提升 Dex 文件的编译和运行优化工作,并开发出称之为下一代 dex 编译器:D8。其实早在 AS 3.0 Beta 版本中,Google 已经引入 D8 的测试使用。直到当前 3.1 新版本的发布,才正式将其作为默认 Dex 编译器。
FaceBook Redex
Redex是2016年Facebook开源的对字节码进行优化以减小Apk 大小,同时提高 App 启动速度的工具。
Dex 优化相比基于源码或者 Java 字节码的好处是:可以最大限度的从全局以及类间进行优化;可以类似于 C 在编译最后的 Linking 阶段做优化一样进行优化。
Redex 设计时将优化过程分为不同阶段,每个阶段可当做优化插件进行插拔(取舍),并可以跟在其他不相关的阶段后面,这样的好处有:
- 方便多个优化阶段并行开发;
- 方便后续添加新的优化阶段以及开源接受更多的优化;
- 用户可以通过配置方便的决定用哪些优化。
Redex 主要的优化在于减小 APK 大小以及提高启动速度,分为 6 大点:
- 基于反馈(启动加载顺序测试)的 Class 字节码布局
类字节码在单个 dex 中的布局是根据编译顺序而不是运行时行为决定,即便 Multidex 会将 App 定义的组件以及部分启动需要的类放在 Main Dex 中,但依然不是根据运行时顺序布局,这会导致程序启动时需要查找随机分布在 dex 中的类。Redex 会将 APK 在 Lab 中试运行,并跟踪启动时哪些类需要加载,然后将这些类字节码放到 dex 前部,减少启动时从闪存中寻找类的时间,从而提高 App 启动速度。 - 混淆和压缩
跟 JS 的压缩以及 Android 的 Proguard 类似。跟 Java 层代码使用 Proguard 混淆后需要生成 mapping.txt 文件类似,字节码的混淆也需要生成对应的映射文件,以便当 APP 运行时出现问题需要定位的时候,可以将混淆后的日志信息通过映射文件还原成可读的字符串信息 - 内联函数
将一些函数直接展开到调用它的函数中,减少函数调用切换的时间消耗。
内联函数是在编译期间将函数体直接嵌入该函数的调用处,也就是在编译时不具备函数的性质,不存在执行函数调用产生的开销,从而得到提高代码运行性能的目的,同时,如果正确的应用它,还可以减小编译后生成的文件大小。软件工程的最佳实践鼓励开发者要具备封装的思想,要明确类的职责,这样往往会导致需要对一个类按功能和职责进行拆分等操作。在实际开发中,这种思想是很重要的,但同时在最终生成的字节码中也留有进一步优化的空间。 - 删除无用的interfaces
删除只有一个实现的接口,用实现直接代替。一定程度加快函数调用时间,并减少内存空间以及函数引用。 - 删除无用代码
通过类似早期内存回收的标记-删除策略,删除无用代码。 - 删除metadata
metadata 的一些数据在运行中并不需要,用 Dex 中已有的字符串代替源文件引用以及删除无用的注解来减小 Dex 大小。
Main Dex启动优化
- 减少主Dex大小
- 异步加载dex
1 条评论
新盘 上车集合 留下 我要发发 立马进裙