聊聊 APK —— 直接运行 Dex

in 开发 with 0 comment

因为近期的工作接触了许多 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

> adb shell
> dalvikvm -version

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

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

ART version 2.1.0 arm64

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

> java -version

那么输出的结果是

 ~/Desktop/ java -version
java 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 文件:

public class HelloWorld {
    public static void main(String[] args) {
         System.out.println("Hello World!");
    }
}

这四行 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 . HelloWorld
Hello 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 HelloWorld
Hello World!

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

> /sdcard $ dalvikvm -cp HelloWorld.dex HelloWorl
Unable 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 运行。