嘿!让我们来手动编译安卓项目吧

本文有点像是在呼应我的文章放弃现代的ide拥抱命令行  。不过这次我是认真的。

对那些所谓魔法般的功能,我已经怕了。

神秘的后台进程执行着我不知道也不晓得原理的任务。IDE就像知道我头脑里的想法似的悄悄生成代码。“这里,试试这个东西”,它们对我说。然后我真的会去试。得了吧,其实我也挺喜欢这点的。但是当所有这些神秘的功能都在一起的时候,我又觉得挺难消化的。

其中一个这样的功能就是安卓的编译过程。即使在IDE之外, Gradle把Java和XML变成APK这个功能也足以让我感到不舒服了。

一旦有可能,我更希望剥离这些魔法而自己手动做这些事情。即使只是作为一种练习。

本文就是这样的练习。

目的

手动编译和部署安卓app,只使用SDK命令行工具,

规则

  • 不能用IDE
  • 不能用Gradle

And for extra credit:

The project

对于这个任务,我创建了一个只有一个activity的应用。运行结果如下:

并不是一个最佳典范,但是足够演绎编译的 主要过程了:

为了便于演示,我采用了一个简单的,平铺的工程目录结构:

handbuilt-android-project/
├── AndroidManifest.xml
├── gen/
├── lib/
│   └── picasso-2.5.2.jar
├── out/
├── res/
│   ├── drawable-xhdpi/
│   │   └── icon.png
│   └── layout/
│       └── activity_main.xml
└── src/
    └── pl/
        └── czak/
            └── handbuilt/
                └── MainActivity.java

当完成之后:

  • gen/ 将包含生成的R.java class。
  • out/ 将包含编译生成的产品。

完整的源码在(目录结构稍微多点,以及简单的依赖管理)GitHub上。

我们需要事先做一些设置。

Prerequisites

下载最新的SDK工具包(写本文的时候是24.4.1),解压并把跟目录设置到 $ANDROID_HOME。然后使用下面的,命令启动SDK Manager:

$ $ANDROID_HOME/tools/android

我们需要下载一些额外的组建才能完全使用SDK:

  • Tools
  • Platform tools
  • Build tools
  • Android N Preview SDK
  • Android N System Image (如果你想在模拟器上测试)

我是走在前沿的人;) ,所以全是用的预览版:

一旦所有东西都安装完毕,确保$PATH中存在以下命令:

  • $JAVA_HOME/bin/java
  • $JAVA_HOME/bin/jarsigner
  • $ANDROID_HOME/platform-tools/adb
  • $ANDROID_HOME/tools/build-tools/24.0.0-preview/aapt
  • $ANDROID_HOME/tools/build-tools/24.0.0-preview/zipalign

Jack compiler放在编译工具的$ANDROID_HOME/build-tools/24.0.0-preview/jack.jar中。为了方便,我使用下面的别名来触发它:

$ alias jack='java -jar "$ANDROID_HOME/build-tools/24.0.0-preview/jack.jar"'  

编译

完整的编译包含7个不同的步骤,下面分小节描述:

  1. Generate R.java
  2. Compile
  3. Package
  4. Sign
  5. Zipalign
  6. Upload
  7. Run

步骤 1. 生成 R.java

在Android Studio中,这个步骤在后台不断的发生着,也许是整个过程最神奇的部分。你添加一个XML布局,然后大概1秒过后你就能在代码中使用R.layout.activity_main来引用它了。这是因为每当资源文件改变的时候,IDE就会生成R.java源码文件。

在我们的练习中,我们需要手动做这件事情,这是aapt的第一个工作:

$ aapt package -f 
               -M AndroidManifest.xml 
               -I "$ANDROID_HOME/platforms/android-N/android.jar" 
               -S res/ 
               -J gen/ 
               -m

这个命令使用了AndroidManifest.xml以及res/并生成了下面的源文件:

gen/pl/czak/handbuilt/R.java

/* AUTO-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */
package pl.czak.handbuilt;
public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int icon=0x7f020000;
    }
    public static final class id {
        public static final int image=0x7f040000;
    }
    public static final class layout {
        public static final int activity_main=0x7f030000;
    }
}

注:

解释几个关键参数。

-f 如果编译出来的文件已经存在,强制覆盖。

-m 使生成的包的目录放在-J参数指定的目录。

-J 指定生成的R.java的输出目录

-S res文件夹路径

-A assert文件夹的路径

-M AndroidManifest.xml的路径

-I 某个版本平台的android.jar的路径

-F 具体指定apk文件的输出

步骤 2. 编译

现在完整的代码包含了MainActivity.java和最新生成的R.java.可以使用Jack来编译了:

$ jack --classpath "$ANDROID_HOME/platforms/android-N/android.jar" 
       --import lib/picasso-2.5.2.jar 
       --output-dex out/ 
       src/ gen/

我们传递了N的android.jar到classpath并在import中包含了Picasso的JAR。我们还传入了两个包含了java文件的文件夹(src/ 和 gen/)。

最终,我们得到了被用来打包的out/classes.dex。

步骤 3. Package

现在我们可以开始编译APK了。我把打包过程分成两步。首先,我们用manifest和资源文件创建初始包(initial APK package):

$ aapt package -f 
               -M AndroidManifest.xml 
               -I "$ANDROID_HOME/platforms/android-N/android.jar" 
               -S res/ 
               -F out/handbuilt.apk

我们又一次使用了Android Asset Packaging Tool (aapt),不过这次我使用的是-F,它告诉aapt去创建一个apk,而上次我们使用的是-J。

现在,我们加入已经编译好的classes.dex。注意aapt是对相对路径敏感的,classes.dex需要在package的根目录,因此,这一步我们在out/目录执行:

$ cd out/
$ aapt add handbuilt.apk classes.dex

我们提供了APK需要的所有内容。在安装它之前,还需要签名。

步骤 4. 签名

每一个APK在安装到设备或者模拟器之前都需要被签名。不管你只是在开发中尝试debug版本还是准备公开发布最终版,签名都是必须的。而使用IDE的时候这个过程是不可见的,除非你想把你的app发布到Google Play,不然不会出现任何关于key的提示。

为了方便开发,SDK提供了一个标准的debug key,在~/.android/debug.keystore中。这个key可以这样使用:

  • Keystore password: android
  • Key password: android
  • Key alias: androiddebugkey

知道这点之后,我们使用JDK的jarsigner来执行这个任务:

$ jarsigner -verbose 
            -keystore ~/.android/debug.keystore 
            -storepass android 
            -keypass android 
            out/handbuilt.apk 
            androiddebugkey

APK可以上传了,但是让我们先做一个重要的优化。

步骤 5. Zipalign

Zipalign对apk文件中未压缩的数据在4个字节边界上对齐,当资源文件通过内存映射对齐到4字节边界时,访问资源文件的代码才是有效率的。4字节对齐后,android系统就可以通过调用mmap函数读取文件,进程可以像读写内存一样对普通文件的操作,系统共享内存IPC,以在读取资源上获得较高的性能。 如果资源本身没有进行对齐处理,它就必须显式地读取它们——这个过程将会比较缓慢且会花费额外的内存。  这是一个很重要的优化。幸运的是,它非常简单:

$ zipalign -f 4 out/handbuilt.apk out/handbuilt-aligned.apk

我们的最终APK是handbuilt-aligned.apk。

注:Zipalign的参考文章:改善android性能工具篇【zipalign】

步骤 6. 上传

现在启动一个模拟器(出于练习,这一步也是用的命令行):

$ adb install -r out/handbuilt-aligned.apk

可以启动app了,当然,我们可以点击drawer上的图标,但是那样就没有乐趣了。  

步骤 7. Run

启动app:

$ adb shell am start pl.czak.handbuilt/.MainActivity

完工。

Notes

  • Jack工具大大简化了编译过程。以前需要多个步骤(javac,dx, ProGuard),现在都被这一个工具处理好了。
  • Android Studio在底层会比我们多生成一个calss-BuildConfig.java。出于练习,我把它省略了。
  • Gradle编译系统使用了不同命名方式-zipaligning之前的package叫做:handbuilt-unaligned.apk,而最终的APK叫handbuilt.apk。这只是一种惯例,我这里打破了它。你咬我啊!
  • A typical build process would also incorporate Jill – Jack’s helper tool for converting libraries into Jack’s expected jayce format. Again, I’ve omitted it in the examples here - as of version 1.2-rc2 Jack seems to accept android.jar and picasso-2.5.2.jar just fine. To speed up consecutive builds, you would want to “pre-jill” both of these just like you would “pre-dex” libraries with the old toolchain.
  • 现在可以使用某些Java 8的特性。如果你想使用,需要在Jack指定-D jack.java.source.version=8。

总结

我又一次觉得安卓编译过程的背后并不那么糟糕。我会因此而用make取代gradle吗?不太可能!我甚至开始怀恋Android Studio了。

原文:Jack, Jill & building Android apps by hand

另附其它参考文章:

  1. https://spin.atomicobject.com/2011/08/22/building-android-application-bundles-apks-by-hand/
  2. https://www.douban.com/note/205203144/