AndroidAndroid开发Android开发经验谈

40 编译插桩技术-APK 的编译和打包

2021-01-28  本文已影响0人  凤邪摩羯

众所周知,编译 主要分为 词法分析、语法分析 、语义检查和代码优化 等步骤。额。。等等,别慌,这篇文章并不是要讲编译原理,对于绝大多数的 Android 开发来说,我们能将 App 的编译和打包流程理解清楚就 OK 了。因此,我们这篇文章主要讲的是 App 的编译过程,本篇包含的主要内容如下所示:

本篇文章可能是从现在到今年年底 最简单的一篇文章 了。放轻松,好好享受,后面可能就。。。

好了,下面,我们就先回顾下 App 的编译和打包过程。

一、App 的编译和打包流程

1、APK 的组成

我们都知道,APK 其实是一个 zip 类型的压缩包,而一个典型的 APK 通常都会包含了以下七部分的内容

接下来,我们来看看 App 的编译和打包过程。

2、APK 的编译打包流程

早在 深入探索 Android 包体积优化 一文中我们就探讨过打包的部分流程,这里我们需要更加全面地了解下。Android 官方的编译打包流程图如下所示:

image.png

为了 了解更多打包过程中的细节,我们需要查看更加详细的旧版 APK 打包流程图 ,如下图所示:

image.png

打包流程可简述为如下 八个步骤

至此,我们已经了解了整个 APK 编译和打包的流程。

那么,为什么 XML 资源文件要从文本格式编译成二进制格式?

主要基于以下 两点原因

而 Android 资源管理框架又是如何快速定位到最匹配资源的?

主要基于两个文件,如下所示:

除此之外,APK 的签名也是至关重要的,那么,其签名算法的实现原理是怎样的呢?下面我们就来了解下 APK 签名算法的实现原理。

3、签名算法的原理

什么是签名?

在 Apk 中写入一个 “指纹”。指纹写入以后,Apk 中有任何修改,都会导致这个指纹无效,Android 系统在安装 Apk 进行签名校验时就会不通过,从而保证了安全性

那么,为什么要签名?

主要有 两点原因,如下所示:

在了解 APK 签名的实现之前,我们还必须知道什么是数字摘要。

数字摘要

对一个任意长度的数据,通过一个 Hash 算法计算后,都可以得到一个固定长度的二进制数据,这个数据就称为 “摘要”

在签名和校验的流程之中,应用了许多密码学的知识,这里我们需要先大致了解一下。

Hash(散列算法)的基础原理

Hash 算法就是 将数据(如一段文字)运算变为另一固定长度值。它的特点主要有如下 三点

而常用的 Hash 算法有如下 三种

签名和校验的主要过程

签名就是 在摘要的基础上再进行一次加密,对摘要加密后的数据就可以当作数字签名。

签名过程:

签名过程可以细分为 三步,如下所示:

校验过程:

校验过程同样也可以分为 三步,如下:

那么,我们该如何保证公钥的可靠性呢?答案是 数字证书

数字证书

数字证书是 身份认证机构(Certificate Authority)颁发的,主要包含了以下 六类信息

接收方收到消息后,需要先向 CA 验证证书的合法性,再进行签名校验

需要注意的是,Apk 的证书通常是自签名的,也就是由开发者自己制作,没有向 CA 机构申请。Android 在安装 Apk 时并没有校验证书本身的合法性,只是从证书中提取公钥和加密算法,这也正是对第三方 Apk 重新签名后,还能够继续在没有安装这个 Apk 的系统中继续安装的原因。

keystore 和证书格式

keystore 文件中包含了 私钥、公钥和数字证书。根据编码不同,keystore 文件分为很多种,Android 使用的是 Java 标准 keystore 格式 JKS(Java Key Storage),所以通过 Android Studio 导出的 keystore 文件是以 .jks 结尾的。

keystore 使用的 证书标准是 X.509,X.509 标准也有多种 编码格式,常用的有两种:pem(Privacy Enhanced Mail)和 der(Distinguished Encoding Rules)jks 使用的是 der 格式,但是,Android 也支持直接使用 pem 格式的证书进行签名

下面,我们了解下两种证书编码格式的区别,如下所示:

jarsigner 和 apksigner 的区别

Android 提供了 两种对 Apk 的签名方式,一种是基于 JAR 的签名方式,另一种是基于 Apk 的签名方式,它们的 主要区别在于使用的签名文件不一样:jarsigner 使用 keystore 文件进行签名;而 apksigner 除了支持使用 keystore 文件进行签名外,还支持直接指定 pem 证书文件和私钥进行签名

在我们签名时,除了要指定 keystore 文件和密码外,也要指定 alias 和 key 的密码,这是为什么呢?

keystore 是一个密钥库,也就是说它可以存储多对密钥和证书,keystore 的密码是用于保护 keystore 本身的,每一对密钥和证书是通过 alias 来区分的。所以 jarsigner 是支持使用多个证书对 Apk 进行签名的,apksigner 也同样支持。

Android Apk V1 验证签名的原理

Android Apk V1 验证签名的过程主要可以分为如下 四步

在整个 App 的编译打包过程中,Gradle 自动化构建工具发挥出了重要作用,而编译速度可是需要我们迫切解决的一大痛点。下面,我们就来看看如何对编译进行提速。

二、编译提速

1、了解 Android Studio 3.0 依赖类型的变化

Android Studio 3.0 之前 共有 六种 依赖方式,如下所示:

而在 Android Studio 3.0 之后,新增了两种方式:api 和 implementation。其中 api 完全等同于 compile

api

等同于 compile, 用 api 指令编译,表示 三方库的依赖对 module 是可见的,即等同 app Module 可以使用此三方库依赖。

implementation

特点是 将该依赖隐藏在内部,而不对外部公开。比如在组件化项目中,有一个 app module 和一个 base module,app moudle 引入了 base module。其中 base module 使用 implementation 依赖了 Glide 库,因为 implementation 是内部依赖,所以是无法调用到 Glide 库的功能的。因此 implementation 可 以 对外隐藏不必要的接口,并且,使用它可以有效地 提高编译速度。比如,在组件化项目中一般含有多个 Moudle 模块,如 Module A => Module B => Moudle C, 比如 改动 Moudle C 接口的相关代码,如果使用的是 implementation,这时候编译只需要单独编译 Module B 模块就行,但是如果使用 api 或者旧版本的 compile,由于 Module A 也可以访问到 Moudle C,所以 Module A 部分也需要重新编译。所以,在使用无错的情况下,可以优先使用 implementation

2、现有编译方案

Gradle 的官方方案 Instant Run在 Android Plugin 2.3 之前,它使用了 Multidex 实现。在 Android Plugin 2.3 之后,它使用了 Android 5.0 新增的 Split APK 机制。如下图所示:

image.png

但是,如果你的应用较大,会有如下四个问题:

阿里的 FreeLine 在大部分情况比 Instant Run 更快,但是,它 牺牲了正确性。因为,为了追求更快的速度,它直接忽略了 Annotation 和常量改变可能带来错误的编译产物。而 Instant Run 作为官方方案,它优先保证了 100% 的正确性

但是,在 Android Studio 3.5 之后,Android 8.0 以后的设备将会使用新的方案 Apply Changes 去代替 Instant Run。而 ApplyChange 采用了跟 InstantRun 不一样的原理来加快 AndroidStudio 部署安装 APK 的流程。下面,我们就来了解下他们之间的区别。

InstantRun

InstantRun 主要解决以下两个问题:

为了实现这两个目标,InstantRun 通过重写 apk 的构建流程往每个类里去注入 Hook(钩子) 来达到类的热替换。关于 InstantRun 详细的实现原理可以看看我之前写的深入探索Android启动速度优化一文。

对于小型的应用,InstantRun 确实很好用,能够节省构建和部署的时间,并且不会出错。但是,对于大型的复杂应用,它会导致更长的构建时间,同时由于 InstantRun 构建过程和正常的 app 构建存在冲突,常常出现让开发者意想不到的错误。AS 开发团队在连续几个大版本中都尝试去解决这些问题,但是效果不理想。

所以基于此,AS 开发者团队 重新设计了底层的架构,推出了 ApplyChangs。和 InstantRun 不同的是,它不会在构建过程中去修改 apk。取而代之,它使用了 Android 8.0(Oreo)上支持的 Runtime Instrumentation 以及更新的设备和模拟器在运行时重定义类

ApplyChanges

对于 运行在 Android 8.0 或者更新版本上的设备和虚拟机Android Studio 现在有 三个按钮 来控制应用程序重启的程度:

通常只有方法体内部的代码更改才会对 Apply Changes 具有兼容性。而 ApplyChanges 的 实现原理 就是 找出 AndroidStudio 构建出来的 apk 和已经安装到手机设备 apk 的差异。找出差异后,然后将差异发送到手机上执行差异合并。ApplyChanges 的 总体架构 如下图所示:

image.png

那么,理想的编译方案是怎么样的呢?

3、理想的编译方案

我们可以把安装的 Base APK 作为一个壳 APK,而真正的业务代码都放到 Assets 的 ClassesN.dex 中。该方案需要包含以下 三个优化点

4、编译速度优化

除了将电脑更换为 Mac Pro 顶配版之外,还有以下方式可以提升编译速度:

那么,Flutter 的 Hot Reload 的实现原理是什么呢?

在回答这个问题之前,我们必须先了解 Flutter 的编译模式

Flutter 的编译模式

编译模式大体可以分为 两种,如下所示:

而 Flutter 使用了与众不同的编译模式,在开发阶段下,使用了 Kernel Snapshot 模式(对应 JIT 编译),将 dart 代码生成了标记化的源代码,而在运行时编译使用的是解释执行。在 release 阶段,iOS 使用 AOT 编译,编译器将 dart 代码生成汇编代码,最终生成 app.framwork,而 android 使用了 Core JIT 编译,将 dart 转化为二进制模式,并在 VM 启动前载入

因此,在开发阶段的 Kernel Snapshot 编译模式下,Hot Reload 会通过扫描项目文件,将有改动的 dart 文件转化为标记化源代码 kernel files,并发送到正在运行的 DartVM,等待 DartVM 替换资源,然后通知 Flutter Framework 重建、重新布局、重新绘制 WidgetsTree,即可看到改动效果

那么,flutter 又是如何触发 WidgetsTree 的重建呢?

Flutter framework 中 BindingBase 注册了名为 reassemble的Dart VM 服务,用于外部与正在运行的 Dart VM 通信,这样,便能够触发根节点树实现重建操作。当 Hot Reload 导致需重建 WidgetsTree时,reassemble 的 Dart VM 服务就会被触发,触发后,就会由根节点开始一步步实现widgets树重建,其重建流程如下所示:

    ext.flutter.reassemble => BindingBase.reassembleApplication => 
    WidgetsBinding.performReassemble => BuildOwner.reassemble => Element.reassemble
复制代码

三、广义的编译-CI

CI 即 持续集成,在大型开发团队中,CI 的建设是重中之重,CI 主要包括 打包构建、Code Review、代码工程管理、代码扫描 等一系列流程。它的 整套运转体系 可以简化为下图:

image.png

1、持续集成的原因

构建 CI 的目的主要是为了解决以下四个问题。

1)、项目依赖复杂

随着业务的发展,基础组件库的数量会持续上涨,这个时候组件间的关系就会变得错综复杂,这将会导致如下 两个问题

2)、琐碎的研发流程

在日常的功能开发中,我们一般都会经 代码开发、组件发版、组件集成、打包、测试这五个步骤。如果测试发现 Bug 需要进行修复,然后会再次经历代码修改、组件发版、组件集成、打包、测试,直到测试通过交付产品。传统的研发流程如下图所示:

image.png

可以看到,开发同学在整个开发流程中需要手动提交 MR、升级组件、触发打包以及去实时监控流程的状态,这样肯定会严重影响开发的专注度,降低研发的生产力。

3)、与 App 性能监控体系的融合

随着 App从 项目初期 => 成长期 => 成熟期,对性能的要求会越来越高,为了保障性能的足够稳定,我们需要制造出许多性能监控的工具,以实时监控我们应用的性能。而 App 性能监控体系必须和 CI 结合起来,以实现流程的自动化和平台化。

4)、项目的编译构建速度缓慢

随着 App 的体积变大,依赖变多,项目的编译构建速度会越来越慢,缓慢的编译速度会严重拖垮开发同学的研发效率。因此,提升 App 的编译构建速度刻不容缓。

2、持续集成的主要步骤

持续集成涉及的流程非常多,但是有 两个主要的步骤是非常重要 的,具体如下所示:

1)、代码检查

为了防止不符合规范的代码提交到远程仓库中,我们需要 自定义一套符合自身项目的编码规范,并使用专门的插件来检测。自定义代码检测可以通过完全自己实现或者扩展 Findbugs 插件,例如美团就利用 Findbugs 实现了 Android 漏洞扫描工具 Code Arbiter,其中 FindBugs 是一个静态分析工具,它一般用来检查类或者 JAR 文件,将字节码与一组缺陷模式进行对比来发现可能存在的问题,它可以以独立的 JAR 包形式运行,也可以作为集成开发工具的插件形式而存在。而 FindBugs 插件具有着极强的可扩展性,只需要将扩展的 JAR 包导入 FindBugs 插件,重启 AS,即可完成相关功能的扩展。

在 FindBugs 有一款专门对安全问题进行检测的扩展插件 Find Security Bugs,该插件主要用于对 Web 安全问题进行检测,也有极少对Android相关安全问题的检测规则。我们只需要 定制化自己的 Find Security Bugs,通过增加检测项来检测尽可能多的安全问题,通过优化检测规则来减少检测的误报 即可,这里我们可以直接使用 Android_Code_Arbiter 这个插件,它 去除了其中跟 Android 漏洞无关的漏洞,保留了与 Android 相关的,并增加了其它的一些检测项,以此形成了针对与于 Android 的源码审计工具

此外,我们也可以使用 第三方的代码检查工具,例如收费的 Coverity,以及 Facebook 开源的 Infer

然而,尽管将问题代码扫描出来了,可是还是会有不少开发同学不知道如何修改,对于这种情况,我们可以给在自定义代码扫描工具的时候,对于每一个问题检查项都给出对应的修改方针

最后,我们可以据此建立一个解决项目异常的流程:建立一个服务专门每天跑项目的 Lint 检查,跑完将警告汇总分配到对应的负责人身上,并邮件告知他,直到上线。

2)、Code Review

Code Review 非常重要,在每一次提交代码时,我们都需要自己进行一次 Code Review,然后再让别人去 Review,以建立自身良好的技术品牌。

有些同学可能会认为 CI 并不重要,它好像跟具体的技术并无关联。但是,我们需要知道,学会不仅仅是钻在开发角度看问题,跳脱出来,站在用户角度,站在产品角度,或许会有意外的收获

四、总结

到这里,关于 Android 编译相关的知识就介绍完了。下面,总结一下本篇文章涉及的 三大主题

在本篇文章,我们即涉及到了 Android 编译的深度方面:App 的编译和打包流程、签名算法的原理,也涉及到了 Android 编译的广度方面:持续集成。因此,在我们学习的过程中,技术就像是一棵树,在顶部叶子上各个领域看似毫不相干,但是在一个领域越往下深入,各个领域相互交错到的知识或者设计方式就越多,所以技术深度和广度并不是对立面,对技术深度的探索不仅有利于你在特定领域有更深理解,更加可以帮助你轻松切换到另一个领域,特别是像前端的各细分领域的工作,很多领域的知识背后都殊途同归,而技术的广度也不是有的人说的那样不堪,在有技术深度的基础上,去拓展自己的技术广度,其实会让你对原有技术的理解变得更加地深入。

五 参考

链接:https://juejin.cn/post/6844904106545414157
来源:掘金

上一篇下一篇

猜你喜欢

热点阅读