Android打包相关概念

由于只有半调子的客户端开发经验,导致遇见一些环境问题时都需要使用搜索大法挨个试,本文决定整理一下Android 打包过程中的一些概念和流程,方便后续定位问题。

<!--more-->

1. Java

1.1. 编译型语言和解释型语言

参考:编译型语言和解释型语言的区别 使用高级编程语言编写的编码,主要是给程序员读的,计算机只认识0和1组成的二进制指令。

源码想要被执行,就需要先转换成二进制指令,对于这个步骤,不同的编程语言有不同的实现

  • 有的编程语言要求必须提前将所有源代码一次性转换成二进制指令,也就是生成一个可执行程序(比如大家都熟悉的.exe文件),这种语言称为编译型语言(常见的有CGoLang等),将源码转成可执行程序的工具称为编译器
  • 有的编程语言可以一边执行一边转换,需要哪些源代码就转换哪些源代码,不会生成可执行程序,这类语言被称为解释型语言(常见的有JavaScriptPythonPHP等),使用的工具称为解释器

从运行的过程可以看出,由于可执行程序在运行时无需再翻译源码,相当于“一次编译、终生运行”,相较于解释型语言每次运行都要解析而言,编译型语言的运行效率理论上应该会更高,但由于不同平台对应的CPU可识别的二进制指令存在差异,需要为不同的平台生成不同的可执行文件。

JavaC#等语言采用了折中的方案:先将源代码转成字节码文件,再将该文件放在可跨平台的虚拟机中平行,各个平台只需要安装对应的虚拟机就行。

解释型语言为什么不存在跨平台的问题?因为解释型语言无法脱离解释器单独开发和运行,因此安装对应语言的开发环境时会安装对应的解释器,而解释器会由语言官方为不同的平台编写进行兼容。因此解释型语言的可移植性就非常强。

从这个角度看,Java虚拟机也可以看做是平台特定的解释器。

1.2. 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的微型版本。

1.3. 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程序调试和分析的工具

1.4. Java8和Java1.8 命名是什么?

参考:JAVA版本号的问题 Java版本号与JDK版本

是一样的,最开始用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是目前最主流的版本。

1.5. Java运行流程

下面是一段Java代码从源码到运行时经历的步骤

Java源文件 *.java --编译--> Java字节码文件 *.class --加载--> JVM --解释--> 机器指令执行

  • 编译:通过语法分析、语义分析、注解处理 最后才生成会class文件
  • 加载:这个环节又分为了三个部分
    • 装载:将class文件装载到JVM中
    • 连接:校验class信息、分配内存空间及赋默认值
    • 初始化: 变量初始化
  • 解释:把字节码转换成操作系统可识别的执行指令
  • 执行:调用系统的硬件执行最终的程序指令

1.6. 编译

编译就是源码文件*.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!");
    }
}

1.7. 打包

在字节码文件中,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 或者插件程序的部署单位
  • 用于打包与组件相关联的辅助资源

1.8. 外部依赖

大部分项目都或多或少会依赖一些外部模块,怎么在项目中使用外部jar包呢?

第一种方式是直接下载已经构建好的jar包,然后将这个jar包的位置添加进classpath,参考:向IntelliJ IDEA创建的项目导入Jar包的两种方式

这种方式跟Web前端开发中,下载一份js文件,然后通过script标签引入如出一辙,比较原始,手动管理外部依赖也比较麻烦。

第二种就是使用maven,maven是专门为Java项目打造的管理和构建工具

1.9. maven

参考:构建工具的进化:ant, maven, gradle

不管是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还负责项目的编译、打包等构建流程,非常方便

2. Android

2.1. 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。

2.2. 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 除了能携带编译好的程序以外,还能携带资源文件

2.3. minSdk、compileSdk和targetSdk

在Android配置文件中可以看到的几个关于Android SDK版本

  • minSdkVersion 代表着最低版本,在编译的时候兼容到该参数指定最低版本api。
  • compileSdkVersion代表着编译的时候,会采用该api的规范进行代码检查和警告,但是并不会编译进apk中。
  • targetSdkVersion代表着目标版本,在编译的时候会将该版本的api编译进apk中,通过调节该版本可以让App适应更广泛的手机系统版本,

各版本号的大小关系就是:compileSdkVersion>=targetSdkVersion>=minSdkVersion

2.4. Android打包流程

参考:

下面是文档里面给出的打包流程

字节码文件可以在JVM中运行,但如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dex 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作,得到dexDalvik 可执行文件,其中包括在 Android 设备上运行的字节码。

整个打包还有很多细节,下面是另外一张流传比较广的apk打包流程图

2.5. 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 接口实例
  • 配置阶段,执行 build.gradle 中的构建逻辑,完成project的配置
    • 下载插件和依赖
    • 执行脚本代码
    • 构造Task DAG,根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task
  • 执行阶段,按照Task DAG定义的依赖关系执行Task

2.6. 签名

参考:为应用签名

Android系统要求每一个应用都必须经过数字签名才能被安装,这里暂时先不扩展

3. 小结

本文整理了

  • Java中的一些概念如Java版本、JRE、JVM等,然后介绍了Java运行流程和打包原理
  • Android中的一些概念如Android SDK、NDK,然后介绍了Android构建流程,以及使用的gradle构建工具

至此,对于Java打包和Android打包都有了一些了解,当然本文还有很多细节待完善,如果文章内容存在错误,欢迎指正。