查看原文
其他

【连载】聊聊 APK——直接运行 Dex文件的黑魔法

Gemini 程序亦非猿 2021-11-05

想进大厂,就关注「 程序亦非猿 」
时不时 
8:38 推送优质文章,觉得有用,置顶加星标


Hi 大家好,接下去会有一个连载系列——聊聊 APK,共四篇文章,由我好基友 Gemini 老师提供。

导读

很多人可能不太明白 APK 有什么好聊的,我个人觉得,作为一名合格的 Android 开发,对于 APK 的简单认识一定要有。技术深度上大家的认知深度每天都在成长,如果我们每天都讨论方案却没有落地的话,对于每一个独立的人以及社区都是没有进步的,因此写了这一系列的文章,简单的介绍了下一个最简单的 APK 文件的组成,以及每个组成里面每一部分是怎么来的。

其实如果现在还把 APK 文件当作黑盒的话,我们对于编译时的产物能做的事情是非常少的,因为 Gradle 把这个繁杂的黑盒封装的特别好,你只会吐槽 Gradle 慢,卡,但是你不知道它到底做了多少事情,不过这的确不能成为它性能不好的借口。

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

《聊聊 APK —— 直接运行 Dex》以及 《聊聊 APK —— Dex 热修复与 Classpath》 这两篇文章会讲 Dex 文件的生成方式以及如何在没有 APK 文件存在的情况下使用。能帮助你理解字节码到 Dex 文件的简单转换,希望这篇文章能抛砖引玉,方便你以后加深对 Dex 文件的理解。

《聊聊 APK —— aapt 编译资源》这篇文章讲述了 aapt2 是如何编译资源文件以及产生资源表,我们在开发 Android Application 的时候,对于R.java应该是再熟悉不过了,但是大部分人并不知道R.java和资源之间的关系,甚至可能不知道resources.arsc文件的存在。

《聊聊 APK —— 脱离 AS 手工创造 APK 文件》把上面两篇文章的内容综合起来 ———— 在我们编译完 Dex、资源文件、resources.arsc 之后,是如何把这三个组合起来,产生一个 APK 的,它的本质很简单,只是 zip 一下就好了,但是它不能直接运行,需要进行签名之后才能运行。APK 的文件结构就是这么简单,我也仅仅是尝试把这个大大的黑盒进行小小的拆解。


 

第一篇——直接运行 Dex,开启。

 

因为近期的工作接触了许多 android 工具链的东西,所以我们就来介绍下 APK 这个耳熟能详的文件。首先,我们先看看如何使用 Dex 文件在手机终端上输出一个 HelloWorld

编译和运行工具

学习过 Android 的人一定知道,在 Android OS 上跑的虚拟机曾经叫 dalvik,现在叫 ART (Android Runtime),为了方便,下文不再区分两者差别,暂时统称 dalvik。

如果把 dalvik 当作一个黑盒,无视细节,我们就能拿他和 jvm 进行类比。那么,在学习 java 语言之初,使用 IDE 进行 java 开发之前,我们一定知道有两个二进制文件叫做 javac 和 java,一个是将 xxx.java 源代码编译成 xxx.class 字节码,一个是启动虚拟机加载运行字节码。

那么在 Android 中,dx 类似 javac,但是它的输入不是 java 源代码,而是 class 字节码,输出是大名鼎鼎的dex文件,今天我们不探讨dexclass文件的区别,我们只要知道,把class文件和dex文件分别指向给不同的二进制做输入,就可以执行里面的逻辑。jvm 里面运行class的是java,那么 Android 里面运行dex的二进制文件,是dalvikvm

1> adb shell

2> dalvikvm -version

一如既往令人讨厌的单横杠

我的手机是一台运行 Android 9 的手机,输出的结果是:

ART version 2.1.0 arm64

如果我们在 jvm 的环境下,运行

1> java -version

那么输出的结果是:

 ~/Desktop/ java -versionjava version "1.8.0_77"Java(TM) SE Runtime Environment (build 1.8.0_77-b03)Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode)

可以看见我的机器上运行的是 java 8,好,运行工具暂时介绍到这里,接下来我们看下如何让 jvm 和 dalvik 运行 HelloWorld 程序。

Compile HelloWorld.java

首先,我们需要写代码,写一个简单的 HelloWorld.java 文件:


1public class HelloWorld {

2    public static void main(String[] args) {

3         System.out.println("Hello World!");

4   }

5}

这四行 java 代码不能更简单了,我们应该不能更熟悉了。我们从上一个章节知道dx的输入格式是class文件,javac的输入格式是 java 源代码,输出是class文件,也就是说,不管怎么样,我们都需要生成class文件,那么,生成的方式很简单,只需要运行javac HelloWorld.java即可,在当前目录下,就会出现一个HelloWorld.class文件,jvm 上需要的文件就准备好了,接下来看看 dalvik 上需要准备的东西。

学习过 Android 的人可能会了解到,class -> dex 需要的工具是dx,它属于 Android Platform Build Tools 的一部分,会随着 SDK 的分发更新而更新,在我这使用的是 28.0.3 版本,所以它的路径就是$ANDROID_HOME/build-tools/28.0.3/dx,以下简称dx,这个二进制文件平常我们虽然天天会用,但是不会直接接触,所以对于我们来说是陌生的,知道这个二进制文件所在的路径,第一步我的习惯是使用--help命令看一下它能做什么工作(又要吐槽下垃圾 java 的单横杠),执行dx --help,我们看见如下输出(省略暂时不重要的部分)

dx --dex [--debug] [--verbose] [--positions=<style>] [--no-locals] [--no-optimize] [--statistics] [--[no-]optimize-list=<file>] [--no-strict] [--keep-classes] [--output=<file>] [--dump-to=<file>] [--dump-width=<n>] [--dump-method=<name>[*]] [--verbose-dump] [--no-files] [--core-library] [--num-threads=<n>] [--incremental] [--force-jumbo] [--no-warning] [--multi-dex [--main-dex-list=<file> [--minimal-main-dex]] [--input-list=<file>] [--min-sdk-version=<n>] [--allow-all-interface-method-invokes] [<file>.class | <file>.{zip,jar,apk} | <directory>] ... Convert a set of classfiles into a dex file, optionally embedded in a jar/zip. Output name must end with one of: .dex .jar .zip .apk or be a directory.

根据这部分的说明,我们知道 dx 可以接受一个 class 文件的集合,转成一个 dex 文件或者 jar/zip 文件,里面的内容有少许的不同,但是不管是 jar 文件还是 zip 文件,里面其实核心还是一个 dex 文件,此处为了方便,我们就直接转出 dex 文件,执行如下命令:

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

当然此处的名字不一定是 classes.dex。执行完后,我们在当前目录下也能看见刚刚产出的 dex 文件。

Run HelloWorld

我们拿到了 class 文件和 dex 文件,那么在 jvm 上,我们只要使用 java HelloWorld 就搞定了。

 ~/Desktop/ java -cp . HelloWorldHello World!

输出了 Hello World。这里我们都很熟悉,那么如何在 dalvik 上运行呢?其实也很简单。首先把需要的 dex 文件传到手机上,(以下都以 Android P 为例)

adb push classes.dex /sdcard/

然后我们 adb shell 进入到 /sdcard/ 下面。

在运行之前,我们再回忆以下,dex 文件和 class 文件不同的地方是,一个 class 文件里面通常最多只包含了一个 public 类,但是 dex 文件是 class 文件的集合,有点像 jar,但是不是 jar 文件那样简单的压缩,它是一个转换后的字节码集合文件。因此,dalvik 上面的-cp(classpath)参数和 jvm 上的-cp参数有点不同,dalvik 上指的是 dex,那么只要执行如下命令:

:/sdcard $ dalvikvm -cp HelloWorld.dex HelloWorldHello World!

就输出了我们想要的 Hello World,其中 cp 指定的是 classpath,后面指定的类名,毕竟 dex 文件一旦有多个类存在 main 函数的话,就不知道选哪个类去运行了。之前如果有的小伙伴对于 Android 上的类加载器有所耳闻的话,我们还可以在这里故意输错类名,看一下堆栈输出,比如:

> /sdcard $ dalvikvm -cp HelloWorld.dex HelloWorlUnable to locate class 'HelloWorl'java.lang.ClassNotFoundException: Didn't find class "HelloWorl" on path: DexPathList[[dex file "HelloWorld.dex"],nativeLibraryDirectories=[/system/lib64, /system/lib64]] at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134) at java.lang.ClassLoader.loadClass(ClassLoader.java:379) at java.lang.ClassLoader.loadClass(ClassLoader.java:312)Exception in thread "main" java.lang.ClassNotFoundException: Didn't find class "HelloWorl" on path: DexPathList[[dex file "HelloWorld.dex"],nativeLibraryDirectories=[/system/lib64, /system/lib64]] at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134) at java.lang.ClassLoader.loadClass(ClassLoader.java:379) at java.lang.ClassLoader.loadClass(ClassLoader.java:312)


我们可以看到,此处的类加载器是 DexClassLoader,里面存在一个 DexPathList

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

以上就是 jvm 和 dalvik 运行各自字节码的步骤和一些约定,知道了以上的情况,后续的文章我们再详细介绍下 apk 里面的东西,以及我们如何手动调用一些命令生成一个 apk 供 Android 运行。


船长注:文章的内容其实不是很多,建议大家动动手自己尝试一下,可以有更佳的体验,船长自己尝试了下,并且非常贴心的把一些产物做成了库,放在里 GitHub,方便大家尝试。

https://github.com/AlanCheen/TalkAboutApk

一定要动手试试喔!不试的小心被推下船!

下篇见,不见不散哟

听说点“在看”的都是有前途的工程师喲

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存