apk文件是什么(apk文件里面的内容介绍)

apk是什么

全称:Android application package,Android应用程序包,是一个标准的 ZIP 文件,狭义上说,他不是可执行文件,linux 上可执行文件是 ELF 文件,但是 APK 不是 ELF 文件。因此 aaa.apk == aaa.zip

apk由什么组成

APK 的组成有 Dex 文件,资源,资源表和签名摘要信息等四部分组成,这四部分是不可或缺的,不然任何一个 OS 都无法正常的运行你带 Activity 的 Android 应用。

apk文件是什么(apk文件里面的内容介绍)

一个APK文件通常包含以下文件:

META-INF文件夹:用于保存 App 的签名和校验信息,以保证程序的完整性。当生成 APK 包时,系统会对包中的所有内容做一次校验,然后将结果保存在这里。而手机在安装这一 App 时还会对内容再做一次校验,并和 META-INF 中的值进行比较,以避免 APK 被恶意篡改。其中包含如下 三个文件,如下所示:

1)、MANIFEST.MF:其中每一个资源文件都有一个对应的 SHA-256-Digest(SHA1) 签名,MANIFEST.MF 文件的 SHA256(SHA1) 经过 base64 编码的结果即为 CERT.SF 中的 SHA256(SHA1)-Digest-Manifest 值。

2)、CERT.SF:除了开头处定义的 SHA256(SHA1)-Digest-Manifest 值,后面几项的值是对 MANIFEST.MF 文件中的每项再次 SHA256(SHA1) 经过 base64 编码后的值。

3)、CERT.RSA:其中包含了公钥、加密算法等信息。首先,对前一步生成的 CERT.SF 使用了 SHA256(SHA1)生成了数字摘要并使用了 RSA 加密,接着,利用了开发者私钥进行签名。然后,在安装时使用公钥解密。最后,将其与未加密的摘要信息(MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被修改。

res: APK所需要的资源文件夹。

AndroidManifest.xml: 一个传统的Android清单文件,用于描述该应用程序的名字、版本号、所需权限、注册的服务、链接的其他应用程序。

classes.dex: classes文件通过DEX编译后的文件格式,用于在Dalvik虚拟机上运行的主要代码部分。

resources.arsc: 资源索引表文件,通过该文件对资源进行定位。也可以用ApkTool等工具反编译后再开始进行软件修改

Dex文件是什么

先了解一下 JVM,Dalvik 和 ART。

JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。

Dalvik 是 Google 设计的用于 Android 平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。

ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在 Android 4.4 推出。ART 比 Dalvik 的性能更好。

Dalvik 虚拟机不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx/d8/r8(这是一个工具,位置为$ANDROID_HOME/build-tools/(不同版本号)/dx) 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。

Dex 文件格式是专为 Dalvik 设计的一种压缩格式。

那么可以知道,

JVM的输入是java文件,输出是class文件

Dalvik的输入时class文件,输出是dex文件,Dex 文件是很多 .class 文件处理后的产物

java文件是如何变成dex文件的

首先在java中,java文件通过javac命令编译生成.class文件,dex文件的流程也是类似。

假设我们编写了一个HelloWorld程序,并且只有一个main函数,main函数里只有打印一个HelloWorld,通过javac生成.class文件后,再通过 dx 工具的如下命令:

$ANDROID_HOME/build-tools/28.0.3/dx –dex –output=classes.dex HelloWorld.class

可以输出一个classes.dex文件,拿到dex文件后,我们需要把他放到一个可以运行它的OS,就是Android系统,我们直接push到手机上,下一步通过如下命令:

dalvikvm -cp HelloWorld.dex HelloWorld

可以直接运行HelloWorld程序,并输出HelloWorld,其中 cp 指定的是 classpath,后面指定的类名,毕竟 dex 文件一旦有多个类存在 main 函数的话,就不知道选哪个类去运行了。

Dalvik虚拟机 除了能接受一个裸露的 dex 文件以外,还能接受一个 zip 格式的文件,只要求里面的 dex 文件名必须是 classes.dex 就行或者是一个zip文件解压出来有dex文件。比如我们传一个 zip/apk/jar 都能接受,毕竟他们的本质都是 zip。

热修复

现在对于 java 代码的热修复主要从 DexClassLoader 里面的 dexPathList 入手,这里应用的原理就是 classloader 双亲委派里对于加载后的类的缓存机制。

如果一个类在一个类加载器中加载过,就不会从其他类加载器中装载了。

Android 提供的 DexClassloader 是按提供的 dex 顺序找的,因此对于 java 代码的热修复变得很简单 —— 只要把想要被修复的 Dex 放到最前面,加载相关的类就好了,Tinker 和 DexPatch 当然还做了更多的事情,比如对 dex 进行 merge 之类的工作。

我们可以通过 cp命令来对两个dex文件处理,如下:

dalvikvm -cp new.dex:classes.dex HelloWorld

这样就可以让new.dex比classes.dex 先运行,如果new中有一个和classes一模一样的类路径类名,那么classes就不会再去加载这个类

那项目中的资源文件怎么处理的

上一步的产物是将项目中的java文件进行统一处理,那项目剩下的还有资源部分。

资源文件指的就是res下除了raw的文件和 AndroidManifest.xml ,通过 aapt/aapt2(文件位于SDK下的build-tools中) 一起编译和链接出相应的二进制版本

AAPT2(Android 资源打包工具)是一个构建工具,Android Studio 和 Android Gradle Plugin 使用它来编译和打包应用的资源。AAPT2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。

从 Android Gradle Plugin 3.0.0 开始,AAPT2 默认开启,相对于 AAPT,资源打包流程由原来的单一编译过程拆分为「编译」和「链接」两个阶段。

编译

Android 所有类型的资源的编译都是通过 AAPT2 来完成,资源的编译使用 compile 子命令,编译成功后,会生成一个扩展名为 .flat 的中间二进制文件,正常情况下,每一个输入的资源文件对应输出一个 .flat 文件,然后在后续的链接阶段使用。

语法

使用 compile 的一般语法如下:

aapt2 compile path-to-input-files [options] -o output-directory/

对于资源文件,输入文件的路径必须符合以下结构:path/resource-type[-config]/file

编译单个资源 aapt2 compile -o build ./app/src/main/res/mipmap-xxxhdpi/ic_launcher.png编译多个资源 aapt2 compile -o build \ ./app/src/main/res/mipmap-xxxhdpi/ic_launcher.png \ ./app/src/main/res/layout/activity_main.xml \ ./app/src/main/res/values/strings.xml编译整个目录aapt2 compile -o build/resources.ap_ –dir ./app/src/main/res/

编译出的文件解压后,都是.flat文件

编译选项

选项说明-opath指定已编译资源的输出路径。

这是一个必需的标记,因为您必须指定 AAPT2 可将已编译的资源输出并存储到其中的目录的路径。

–dirdirectory指定要在其中搜索资源的目录。

虽然您可以使用此标记通过一个命令编译多个资源文件,但这样就无法获得增量编译的优势,因此不建议对大型项目使用。

链接阶段

在链接阶段,AAPT2 会合并在编译阶段生成的所有中间文件(.flat 文件与AndroidManifest.xml),并将它们打包成 ZIP 包(最终 APK 的原型,由于不包括 DEX 文件且未签名,所以无法正常安装)

链接语法aapt2 link path-to-input-files [options] -ooutputdirectory/outputfilename.apk –manifest AndroidManifest.xml

链接资源使用 link 子命令,如下所示:

aapt2 link -o resources.ap_ -I $ANDROID_HOME/platforms/android-29/android.jar –manifest build/intermediates/manifests/full/debug/AndroidManifest.xml build/layout_activity_main.xml.flat build/values_styles.arsc.flat build/values_colors.arsc.flat build/values_strings.arsc.flat build/mipmap-xxxhdpi_ic_launcher.png.flat build/mipmap-xxxhdpi_ic_launcher_round.png.flat

链接选项

选项说明-opath指定链接的资源 APK 的输出路径。

这是一个必需的标记,因为您必须指定可以存放链接资源的输出 APK 的路径。

–manifestfile指定要构建的 Android 清单文件的路径。

这是一个必需的标记,因为清单文件中包含有关您应用的基本信息(如软件包名称和应用 ID)。

-I提供平台的 android.jar 或其他 APK(如 framework-res.apk)的路径,这在构建功能时可能很有用。如果您要在资源文件中使用带有 android 命名空间(例如 android:id)的属性,则必须使用此标记。–javadirectory指定要在其中生成 R.java 的目录。

具体语法可查看 https://developer.android.com/studio/command-line/aapt2#link_options

链接结束后,就会生成一个apk文件,如果你将这个apk文件拖入AS中查看,可以看到他只有资源文件,没有dex文件。

拿一个APK作为例子,可以查看他的资源索引文件 resources.arsc

红色部分的16进制ID,对应的就是R文件中的值,像这样

AssetManager 就是这么定位资源的

转储语法

除了把apk拖入as中查看外,还可以使用dump命令

aapt2 dump sub-command filename.apk [options]

子命令 sub-command

子命令说明apc输出在编译期间生成的 AAPT2 容器(APC)的内容。badging输出从 APK 的清单中提取的信息。configurations输出 APK 中的资源使用的每项配置。packagename输出 APK 的软件包名称。permissions输出从 APK 的清单提取的权限。strings输出 APK 的资源表字符串池的内容。styleparents输出 APK 中使用的样式的父项。resources输出 APK 的资源表的内容。xmlstrings输出 APK 的已编译 xml 中的字符串。xmltree输出 APK 的已编译 xml 树。

选项说明–no-values禁止在显示资源时输出值。–filefile将文件指定为要从 APK 转储的参数。-v提高输出的详细程度。

flat(aapt2的产出物) 与 AAPT 产物的关系

在 Android Gradle Plugin 3.0 以前的版本中,AAPT 的产物主要有 3 类:

已编译的二进制 XML,例如:布局 XML 文件;

字符串池(String Pool),内嵌于 Resource Table 中,一般不会独立存在;

资源表(Resource Table),例如:ARSC 文件;

AAPT2 的大部分数据结构都采用 protobuf 重新进行编码,但还有一小部分数据结构仍然复用了AAPT 的格式,例如:String Pool ,我们从 AAPT2 的 proto 定义便可以看出来:

message StringPool { bytes data = 1;}message ResourceTable { // The string pool containing source paths referenced throughout the resource table. This does // not end up in the final binary ARSC file. StringPool source_pool = 1; // Resource definitions corresponding to an Android package. repeated Package package = 2;}AAPT2 为什么要将中间产物编码成 flat 格式

主要原因在于 AAPT2 将资源打包过程拆分成了两个阶段:「编译阶段」和「链接阶段」,为了在链接阶段得到资源更详细的信息,例如:资源名称、配置信息(Configuration) 等,因此,直接将资源的元信息连同资源本身一同编码进 AAPT2 容器文件中,这样,资源链接的过程可以完全与编译过程解耦了,而且,对于增量构建来说,这样大大提升了资源打包的性能。

以上步骤完成就可以使用apk了吗

首先,一个apk的结构大致如下,

classes.dex

资源文件

resources.arsc

签名摘要

可选的 assets 等

前三个在前面已经单独编译过,现在需要整合他们。也就是通过appt2得到的文件重命名为 apk后 ,通过如下命令整合dex文件

zip -ur app-debug.apk classes.dex

这样就得到了一个未签名的apk,app-unsigned.apk,下一步就是对apk签名,可以通过 apksigner 工具,使用 android debug key进行签名

签名成功后,就可以安装到手机上了。

d8? R8?d8

Android Studio 3.0 推出了d8,并在 3.1 正式成为默认工具。它的作用是将“.class”文件编译为 Dex 文件,取代之前的 dx 工具。

d8 除了更快的编译速度之外,还有一个优化是减少生成的 Dex 大小。根据 Google 的测试结果,大约会有 3%~5% 的优化。

R8

R8 在 Android Studio 3.1 中引入,它的志向更加高远,它的目标是取代 ProGuard 和 d8。我们可以直接使用 R8 把“.class”文件变成 Dex。现在默认为使用gradle插件3.4.0及更高版本的应用程序和Android库项目启用。

同时,R8 还支持 ProGuard 中混淆、裁剪、优化这三大功能,R8 的最终目的跟 d8 一样,一个是加快编译速度,一个是更强大的代码优化。

总结

现在再来谈谈apk的整个构建流程,会显得更加清晰,下面是官方的图:

通过上面的分析,再做一个流程的总结

使用aapt工具,编译res/文件,生成编译后的二进制资源文件(.ap_文件)、R.java文件。(目前新版使用aapt2工具,R.java也替换成了R.jar)

使用aidl工具,根据aidl文件生成对应的Java接口文件

使用Java Compiler工具,Java Compiler(俗称javac)将R.java、项目中的代码、Aidl接口文件编译成.class文件。

使用dex工使用apkbuilder工具,将编译后的资源(.ap_文件)、dex文件及其他资源文件(例如:so文件),压缩成一个.apk文件。

使用apkbuilder工具,将编译后的资源(.ap_文件)、dex文件及其他资源文件(例如:so文件),压缩成一个.apk文件。

使用Jarsigner工具,读取签名文件,对上一步中产生的apk文件进行签名,生成一个已签名的apk文件。

使用zipalign工具,对已签名的apk文件进行体积优化(只有v1签名才有这一步,v2签名的apk会在zipalign后签名被破坏)。

工具

把下面的代码放进app/build.gradle里把时间花费超过50ms的任务时间打印出来

public class BuildTimeListener implements TaskExecutionListener, BuildListener { private Clock clock private times = [] @Override void beforeExecute(Task task) { clock = new org.gradle.util.Clock() } @Override void afterExecute(Task task, TaskState taskState) { def ms = clock.timeInMs times.add([ms, task.path]) //task.project.logger.warn “${task.path} spend ${ms}ms” } @Override void buildFinished(BuildResult result) { println “Task spend time:” for (time in times) { if (time[0] >= 50) { printf “%7sms %s\n”, time } } } ……}project.gradle.addListener(new BuildTimeListener())

执行./gradlew assembleDebug

写入如下代码,输出每个Task对应的类,然后查看Task的具体工作:

//build.gradlegradle.taskGraph.whenReady { it.allTasks.each { task -> println(“Task Name : ${task.name}”) task.dependsOn.each{ t-> println “—–${t.class}” } //def outputFileStr = task.outputs.files.getAsPath(); //def inputFileStr = task.inputs.files.getAsPath() }}dependencies { … testImplementation “com.android.tools.build:gradle:4.0.0” …}

发表评论

登录后才能评论