Android打包相关概念
由于只有半调子的客户端开发经验,导致遇见一些环境问题时都需要使用搜索大法挨个试,本文决定整理一下Android 打包过程中的一些概念和流程,方便后续定位问题。
Java
编译型语言和解释型语言
使用高级编程语言编写的编码,主要是给程序员读的,计算机只认识0和1组成的二进制指令。
源码想要被执行,就需要先转换成二进制指令,对于这个步骤,不同的编程语言有不同的实现
- 有的编程语言要求必须提前将所有源代码一次性转换成二进制指令,也就是生成一个可执行程序(比如大家都熟悉的
.exe
文件),这种语言称为编译型语言(常见的有C
、GoLang
等),将源码转成可执行程序的工具称为编译器 - 有的编程语言可以一边执行一边转换,需要哪些源代码就转换哪些源代码,不会生成可执行程序,这类语言被称为解释型语言(常见的有
JavaScript
、Python
、PHP
等),使用的工具称为解释器
从运行的过程可以看出,由于可执行程序在运行时无需再翻译源码,相当于“一次编译、终生运行”,相较于解释型语言每次运行都要解析而言,编译型语言的运行效率理论上应该会更高,但由于不同平台对应的CPU可识别的二进制指令存在差异,需要为不同的平台生成不同的可执行文件。
Java
、C#
等语言采用了折中的方案:先将源代码转成字节码文件,再将该文件放在可跨平台的虚拟机中平行,各个平台只需要安装对应的虚拟机就行。
解释型语言为什么不存在跨平台的问题?因为解释型语言无法脱离解释器单独开发和运行,因此安装对应语言的开发环境时会安装对应的解释器,而解释器会由语言官方为不同的平台编写进行兼容。因此解释型语言的可移植性就非常强。
从这个角度看,Java虚拟机也可以看做是平台特定的解释器。
Java SE、 Java EE 、Java ME是什么?
- Java SE(Java Platform,Standard Edition),应该先说这个,因为这个是标准版本。
- Java EE (Java Platform,Enterprise Edition),java 的企业版本
- Java ME(Java Platform,Micro Edition),java的微型版本。
Java、JVM、JDK、JRE分别是什么?
Java是编程语言
JVM(Java Virtual Machine,Java虚拟机),是整个Java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行Java字节码文件的虚拟计算机
JRE(Java Runtime Environment, Java运行环境),顾名思义是java运行时环境,也就是Java程序运行时所依赖的环境,包含了JVM,java基础类库,主要是给想运行Java程序的人使用的
JDK(Java Development Kit,Java开发工具包),顾名思义是java开发工具包,使用Java开发Java程序所需的工具包,主要是给程序员使用的,JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具
Java8和Java1.8 命名是什么?
是一样的,最开始用1.x
命名,在2004年JavaOne会议后版本数提升为5.0
,之后沿用x.0
命名。
而JDK则在 Java1.0 到 Java9 对应每一个版本号 :JDK1.0、JDK1.2 ... JDK1.8、JDK1.9,在Java10以后JDK对应名称为:JDk10、JDK11、JDK12。
JDK是个非常核心的基础设施,除了安全漏洞,基本上是不会再去动生产环境JDK了,Java8是目前最主流的版本。
Java运行流程
下面是一段Java代码从源码到运行时经历的步骤
Java源文件 *.java
--编译--> Java字节码文件 *.class
--加载--> JVM --解释--> 机器指令执行
- 编译:通过语法分析、语义分析、注解处理 最后才生成会class文件
- 加载:这个环节又分为了三个部分
- 装载:将class文件装载到JVM中
- 连接:校验class信息、分配内存空间及赋默认值
- 初始化: 变量初始化
- 解释:把字节码转换成操作系统可识别的执行指令
- 执行:调用系统的硬件执行最终的程序指令
编译
编译就是源码文件*.java
到字节码文件*.class
的过程,将代码编译成字节码,JVM虚拟机才能识别
源代码
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
执行javac Main.java
文件,得到Main.class
文件,即编译后的文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.company;
public class Main {
public Main() {
}
public static void main(String[] var0) {
System.out.println("Hello World!");
}
}
打包
在字节码文件中,JVM如果要加载一个类,它需要知道从哪儿去找到这个类,这是通过classpath
来实现的。
classpath
跟系统环境变量的作用非常相似,本质上就是一组目录的集合,比如classpath
是
.;/workspace/bin;/usr/bin/;
当JVM加载com.company.Main
这个类的时候,会依次查找,当找到目标文件后,JVM就会停止查询,不再往后搜索
当前目录/com/company/Main.class
/workspace/bin/com/company/Main.class
/usr/bin/com/company/Main.class
一个项目可能会有多个class文件,分散在各个目录下,将这堆文件全丢给JVM显然不太合适,因此需要归档。
把所有文件合并在一起,首先想到的是zip
压缩文件,在Java中对应的压缩文件被称为jar
(JavaTM Archive file)。
jar就是将一个META-INF/MANIFEST.MF
清单文件和一堆编译后的class文件合并起来,压缩在一起形成的产物。
下面简单梳理一下通过IDEA打jar包的流程
路径 File -> Project Structure -> Artifacts进行配置
配置完成之后,点build
然后在项目 out/artifacts目录下就可以看看见对应的jar包了
一个jar文件可以用于
- 用于发布和使用类库
- 作为应用程序和扩展的构建单元
- 作为组件、applet 或者插件程序的部署单位
- 用于打包与组件相关联的辅助资源
外部依赖
大部分项目都或多或少会依赖一些外部模块,怎么在项目中使用外部jar包呢?
第一种方式是直接下载已经构建好的jar包,然后将这个jar包的位置添加进classpath
。参考:向IntelliJ IDEA创建的项目导入Jar包的两种方式。
这种方式跟Web前端开发中,下载一份js
文件,然后通过script标签引入如出一辙,比较原始,手动管理外部依赖也比较麻烦。
第二种就是使用maven
,maven是专门为Java项目打造的管理和构建工具
maven
不管是javac
编译成字节码,还是jar
打包成jar包,对于项目而言,这些都是比较机械和重复的工作,因此需要构建工具来解决这些工作。
linux上面一个叫make的工具,可以通过Makefile
来执行工程的构建。java由于是跨平台的,就写了一个叫ant
的工具,用来定义构建任务,最后通过一个命令执行各个任务,完成项目构建
ant的缺点在于无法管理项目的第三方依赖,打包时需要开发者自己手动去把正确的版本拷到lib下面。因此就出现了maven
。
maven提出了仓库的概念,所有依赖包都放在仓库里面,项目里面只需要声明依赖什么版本的包,在构建时就会自动将外部包打进来。
一个maven项目的标准结构如下
a-maven-project
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ ├── java
│ └── resources
└── target
pom.xml
是项目描述文件,类似于前端项目中的package.json
<project ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.copmpany</groupId>
<artifactId>javademo</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
...
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.21</version>
</dependency>
</dependencies>
</project>
需要理解的是maven下载和加载依赖的一些流程
maven通过jar包坐标替代jar包本身,这样就不用将所有外部依赖一起打包了,极大减少了项目构建的jar包的体积
项目初始化时,声明依赖的第三方的jar包都会下载到本地仓库
下面是maven几个仓库的概念,加载顺序为
- 本地缓存:当运行项目的时候,maven会自动根据配置文件查找本地仓库,再从本地仓库中调用jar包使用。
- 远程仓库:当本地仓库中没有项目所需要的jar包时,那么maven会继续查找远程仓库,一般远程仓库指的是公司搭建的私有服务器,也叫私服;
- 中央仓库:就是maven官方仓库,地址传送门。
除了管理依赖,maven还负责项目的编译、打包等构建流程,非常方便
Android
Android、Android应用程序、Android SDK、Android NDK
- Android是一个基于Linux的操作系统
- Android应用程序是用Java语言编写的程序,编译后的字节码、其他数据和资源文件等,通过appt工具绑定在一起组成的
apk
文件,也称为Android包 - Android SDK (Android 开发工具包) 是专门为 Android 开发的基于 Java 的程序库,当然现在也有kotlin版本的Android SDK,此外还包括构建、调试、虚拟机等
- Android NDK(Native Development Kit),是一套实现能够在 Android 应用中使用 C 和 C++ 代码的工具,深度操作Android 系统,在应用层面一般无需用到
Android开发环境需要JDK、Android SDK、Android Studio,而Android Studio构建系统基于Gradle。
Android studio中project和module
在IntelliJ IDEA中Project是最顶级的结构单元,一个Project是由一个或者多个Module组成。一些主流大型项目结构基本上都是由多个Module的结构组成。
Android studio中,
- 一个Project代表一个完整的APP,
- Module表示APP中的一些依赖库或独立开发的模块。
使用Android Studio初始化的Android项目中,默认包含一个app
的module。
module又可以分为下面几种类型,主要的区别是生成内容不同
app module
生成 apk 程序文件java module
生成 jar 文件library module
生成 aar 文件,aar 除了能携带编译好的程序以外,还能携带资源文件
minSdk、compileSdk和targetSdk
在Android配置文件中可以看到的几个关于Android SDK版本
- minSdkVersion 代表着最低版本,在编译的时候兼容到该参数指定最低版本api。
- compileSdkVersion代表着编译的时候,会采用该api的规范进行代码检查和警告,但是并不会编译进apk中。
- targetSdkVersion代表着目标版本,在编译的时候会将该版本的api编译进apk中,通过调节该版本可以让App适应更广泛的手机系统版本,
各版本号的大小关系就是:compileSdkVersion>=targetSdkVersion>=minSdkVersion
Android打包流程
参考:
- 配置build,先看看官方文档
- Android Apk 编译打包流程,了解一下~
下面是文档里面给出的打包流程
字节码文件可以在JVM中运行,但如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dex 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作,得到dex
Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码。
整个打包还有很多细节,下面是另外一张流传比较广的apk打包流程图
gradle
参考
maven已经足够优秀了,但由于xml语法、以及约定大于配置导致的不灵活性,导致无法实现某些定制功能,因此就出现了gradle。
gradle继承了maven仓库、依赖等优秀思想,通过groovy语法和task自定义任务,能够更灵活地完成构建任务。
Android Studio使用gradle来自动执行和管理构建流程。
gradle中的一些概念
- gradle,提供核心构建流程,但不提供具体构建逻辑
- gradle plugin,是运行在gradle机制上的一些具体构建逻辑,比如Android构建流程就是通过
Android Gradle Plugin
实现的 - gradle Daemon,用于提升速度的后台进程
- gradle wrapper,对gradle的包装,增加了自动下载安装 Gradle 环境的能力
gradle
:使用系统环境变量定义的 Gradle 环境进行构建;gradlew
:使用 Gradle Wrapper 执行构建。
为什么要提供一个gradle wrapper
自动安装gradle环境呢?因为gradle的迭代频率比较高,向后兼容性也比较差,因此,提供一个自动安装项目所需的gradle环境是很有必要的。
gradle构建流程,主要分为下面三个步骤
- 初始化阶段
- 执行 Init 脚本,有多种设置初始化脚本的方式,如
gradle —init-script <file>
、USER_HOME/.gradle/init.gradle
等,可以用来设置全局属性、日志打印等 - 实例化settings接口,主要是解析并执行
settings.gradle
- 实例化 Project 接口实例,解析
include
声明的模块,并为每个模块build.gradle
文件实例化 Project 接口实例
- 执行 Init 脚本,有多种设置初始化脚本的方式,如
- 配置阶段,执行
build.gradle
中的构建逻辑,完成project的配置- 下载插件和依赖
- 执行脚本代码
- 构造Task DAG,根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task
- 执行阶段,按照Task DAG定义的依赖关系执行Task
签名与验签
参考:
Android APK 签名是指对 APK 文件进行数字签名,以确保其来源可信、未被篡改。在 Android 系统中,每个应用程序都需要通过签名验证才能被安装和运行。
需要签名的原因是:
- 身份验证: APK 签名用于验证应用的身份,确保应用是由合法的开发者或发布者创建的。这有助于防止恶意方篡改应用并将其重新分发。
- 完整性验证: APK 签名也用于验证应用的完整性。签名信息包括应用的数字摘要,如果应用在发布后被修改,其签名将失效,系统会拒绝安装或运行已被篡改的应用。
- 信任体系: Android 系统通过信任体系来确保应用的安全性。通过数字签名,Android 系统能够验证应用是否来自经过验证的开发者,并且该应用在发布过程中未被篡改。
- 系统权限: 一些 Android 系统功能和 API 需要应用提供数字签名,以便系统可以确认应用是否有权访问这些敏感功能。例如,通过 Google Play 服务的某些 API 需要应用的数字签名与开发者账户关联。
- 应用更新: APK 签名还用于验证应用的更新。当应用的新版本发布时,系统可以通过比较新版本的签名与旧版本的签名来验证应用是否是同一个开发者发布的。
签名的过程一般包括以下几个步骤:
- 生成密钥对:使用 Keytool 工具生成公钥和私钥对,Keystore 是一个加密的容器,用于存储和管理密钥,包括公钥和私钥。它通常用于 Java 应用程序开发中,其中开发者可以使用 Keytool 工具来生成和管理证书和密钥对。Keystore 可以用于实现自签名证书,也可以用于从 CA 获取签名证书。
- 对应用程序进行签名:使用开发者的私钥对 APK 文件进行签名,生成数字签名证书(包含开发者的公钥)。
- 将证书添加到应用程序中:将数字签名证书添加到应用程序中,以便在安装和运行时进行验证。
- 在安装apk的时候,Android系统会读取签名信息,通过Android系统内置的根证书验证应用证书的合法性
- 证书合法性验证通过后,使用公钥验证APK的数字签名,验证应用的摘要是否与数字签名匹配。
- 验证通过,安装应用
与HTTPS的区别
可以看出,Android应用签名和验签的过程与HTTPS有很大区别:数字证书是使用开发者自己的私钥进行签名的,而不是CA机构私钥签名。
这是因为,Apk的签名目的主要是开发者身份和应用完整性验证这两点,Android 生态系统中有大量的开发者,使用开发者自己的私钥签署应用使得开发者能够更加灵活地控制和管理他们的应用。
那么,一个与HTTPS相似的问题,如何保证开发者(在HTTPS中对应网站)公钥的可靠性呢?
应用的数字证书与内置的根证书之间可能存在一系列中间证书,这被称为证书链。系统使用内置的根证书验证整个证书链的有效性,确保应用的数字证书是由被系统信任的根证书签发的。
根据系统根证书验证开发者的公钥的合法性是通过建立数字证书链和使用公钥基础设施(PKI)的原理来实现的。
可以看出,Android系统只验证了应用数字签名的合法性,无法保证数字签名一定是真正原始开发者的。
那么假设有开发者A和开发者B,他们都向CA申请了自己的公钥和私钥,那么,对于开发者A签名的Apk,开发者B可以将里面的数字签名删除,然后用开发者B自己的私钥生成新的签名吗?
答案是:可以的!开发者B可以通过删除应用的原始签名,然后使用自己的私钥生成新的签名来重新签名APK,只不过重新签名后的应用与原来将完全失去关联,视为全新的应用,需要重新发布和上架。
在验签阶段,数字签名是可以信任的(正常申请的秘钥);开发者B的公钥也可以验证数字签名。那么,这个APK就可以正常安装。
换言之,从技术层面上,在不知道源码的情况下,可以删除一个Apk的数字签名然后重新签名。
- 解压 APK 文件: APK 文件实际上是一个压缩文件,可以使用压缩工具(如zip工具)将其解压缩。
- 删除原始签名: 在解压后的文件夹中,通常会有 META-INF 目录,其中包含应用的数字签名文件。删除这些签名文件,尤其是 CERT.RSA、CERT.SF、MANIFEST.MF 等文件。
- 重新签名: 使用新的私钥对 APK 文件进行签名。这可以通过工具如 jarsigner 或 apksigner 进行,它们允许你使用指定的私钥和证书对 APK 进行签名。
作为应用的开发者A,有一些技术手段可以避免这些情况发生。
首先,可以在应用启动时进行应用完整性检查,通过验证数字签名和检查 APK 文件的完整性来实现:
- 使用自己的公钥验证应用的数字签名,如果无法通过,就说明不是自己的私钥进行的签名,这样就可以拒绝启动。
- APK 文件在签名后通常会生成一个摘要或哈希值,如果重新签名,则摘要应该会发生变化,这样也可以拒绝启动
其次,还可以使用第三方提供的应用加固,通过对应用程序进行安全性增强来提高应用的安全性的技术。
加固措施旨在增加应用的抗逆向工程、反编译和修改的难度,从而防止攻击者对应用进行不正当操作。
常见的加固手段有
- 代码混淆,使反编译后的代码难以理解。这增加了逆向工程的难度
- 加密关键代码和资源,以确保在运行时解密,并在内存中使用
- 反调试和反监视(Anti-Debugging and Anti-Emulation): 防止应用在调试器中运行,以防止攻击者进行动态分析。此外,加固技术还可能检测和抵御在模拟器或虚拟环境中运行的尝试
应用加固并不是绝对安全的,但它可以有效地提高应用的安全性,阻碍一些常见的逆向工程和攻击尝试
小结
本文整理了
- Java中的一些概念如Java版本、JRE、JVM等,然后介绍了Java运行流程和打包原理
- Android中的一些概念如Android SDK、NDK,然后介绍了Android构建流程,以及使用的gradle构建工具
至此,对于Java打包和Android打包都有了一些了解,当然本文还有很多细节待完善,如果文章内容存在错误,欢迎指正。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。