本文有点像是在呼应我的文章放弃现代的ide, 拥抱命令行 。不过这次我是认真的。
对那些所谓魔法般的功能,我已经怕了。
神秘的后台进程执行着我不知道也不晓得原理的任务。IDE就像知道我头脑里的想法似的悄悄生成代码。“这里,试试这个东西”,它们对我说。然后我真的会去试。得了吧,其实我也挺喜欢这点的。但是当所有这些神秘的功能都在一起的时候,我又觉得挺难消化的。
其中一个这样的功能就是安卓的编译过程。即使在IDE之外, Gradle把Java和XML变成APK这个功能也足以让我感到不舒服了。
一旦有可能,我更希望剥离这些魔法而自己手动做这些事情。即使只是作为一种练习。
本文就是这样的练习。
目的
手动编译和部署安卓app,只使用SDK命令行工具,
规则
不能用IDE
不能用Gradle
And for extra credit:
Let’s use the Jack toolchain
Let’s build for Android N
The project
对于这个任务,我创建了一个只有一个activity的应用。运行结果如下:
并不是一个最佳典范,但是足够演绎编译的 主要过程了:
一个 activity
一个app图标的 drawable 资源
一个包含一个InageView的 layout 资源
一个外部依赖 - Picasso
为了便于演示,我采用了一个简单的,平铺的工程目录结构:
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
Java JDK
Android SDK tools
下载最新的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
步骤 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
另附其它参考文章:
https://spin.atomicobject.com/2011/08/22/building-android-application-bundles-apks-by-hand/
https://www.douban.com/note/205203144/