前言
閱讀本文的時候,配合demo進(jìn)行演示,效果更佳哦~
項目地址:apk-build
現(xiàn)在絕大部分人應(yīng)該是使用Android Studio進(jìn)行app的開發(fā),通常我們運行一個app,直接點擊 run 按鈕或者使用gradle 命令 ./gradlew assembleDebug 等就可以構(gòu)建出一個apk文件,那么這個apk文件到底是怎么生成的呢?
本文通過命令行工具構(gòu)建一個完整的app,來演示app的打包過程,借此來了解一下app的構(gòu)建流程。由于網(wǎng)上已經(jīng)有很多相關(guān)的文章,本文不會對基本的打包流程做過多詳細(xì)的分析,有興趣的讀者可以查看文末的相關(guān)文章。
基本的打包流程
首先來一張Android官網(wǎng)比較經(jīng)典的打包流程圖,這張圖比較早了,但是依然具有指導(dǎo)意義。

從這張圖上看,構(gòu)建一個基本的app,主要需要經(jīng)歷7個過程
- java文件生成過程,
- 通過aapt工具生成R.java文件,輸入文件是res目錄下的文件和AndroidManifest.xml文件
- 通過aidl工具把.aidl文件生成java文件
- 其實還有apt的方式生成java文件
使用android gradle plugin打包,在build/generated目錄下存放的就是這些生成的java文件。
java generated.png
aapt工具存放在/android-sdk/build-tools/$version/ 目錄下。

-
通過aapt工具來生成生成資源索引文件,一般來說生成的文件名是resources.ap_,使用
android gradle plugin打包,這個文件命名一般是resources_${buildVariant}.ap_,例如
資源索引文件.png 使用javac命令編譯java文件
就是使用jdk中的javac工具,做java開發(fā)的應(yīng)該都知道怎么使用。通過dx工具生成dex文件,dx工具與aapt存放目錄一致。
-
通過apkbuilder打包apk,可以在/android-sdk/tools/lib目錄下找到sdklib.jar,執(zhí)行其 com.android.sdklib.build.ApkBuilderMain的main方法
sdklib.jar.png 簽名,可以使用jarsigner工具簽名和apksigner工具簽名。jarsigner是Java本生自帶的一個工具,他可以對jar進(jìn)行簽名的。而apksigner是后面專門為了Android應(yīng)用程序apk進(jìn)行簽名的工具,他們兩的簽名算法沒什么區(qū)別,主要是簽名時使用的文件不一樣。jarsigner工具簽名時使用的是keystore文件,apksigner工具簽名時使用的是pk8,x509.pem文件。
想要了解更多內(nèi)容可以查看一位大神的文章Android簽名機制之---簽名過程詳解zipaligin,它位于/android-sdk/build-tools/$build-tools-version 目錄,是一個zip文件整理工具用來優(yōu)化apk文件。它的主要工作是將apk包進(jìn)行對齊處理,使apk包中的所有資源文件距離文件起始偏移為4字節(jié)整數(shù)倍,這樣通過內(nèi)存映射訪問apk文件時速度會更快。
注意:關(guān)于apk簽名和zipaligin這塊,如果使用不同的工具簽名,zipaligin和簽名的順序是不一致的。例如,如果使用apksigner,那么zipaligin就必須是在簽名之前進(jìn)行。如果使用jarsigner,zipaligin就必須是在簽名之后進(jìn)行。具體可查看官網(wǎng)介紹:https://developer.android.google.cn/studio/command-line/apksigner
demo演示
在apk-build中,分別通過shell腳本和gradle打包的方式來演示構(gòu)建apk的過程。同時,為了增加一些知識點,demo中演示了通過類加載機制實現(xiàn)代碼熱修復(fù)的一個基本過程。
例如,有如下代碼,被除數(shù)為0,對于一個已經(jīng)安裝的apk,執(zhí)行divide()方法肯定會crash。
//修復(fù)前
public class SimpleMathUtils {
public static String divide(){
int a=10/0;
return "the divide result is "+a;
}
}
//修復(fù)后
public class SimpleMathUtils {
public static String divide(){
int a=10/1;
return "the divide result is "+a;
}
}
修改代碼后,把被除數(shù)改為1,通過javac命令生成class文件,然后再通過dex命令把class文件生成為dex文件,名稱為fixed.dex。
javac -encoding UTF-8 -g -target 1.7 -source 1.7 -d bin src/main/java/com/sososeen09/multidexbuild/SimpleMathUtils.java
cd bin
dx --dex --output=fixed.dex com/sososeen09/multidexbuild/SimpleMathUtils.class
簡單起見,我把修復(fù)好的dex文件存放在一個目錄中了

打包的時候把assets目錄中的內(nèi)容復(fù)制到apk文件中對應(yīng)的assets目錄下。
整個構(gòu)建前后的文件目錄是這樣的,bin目錄下是構(gòu)建過程中的產(chǎn)物,gen目錄下是生成的R.java文件。

把打包后的apk文件拖入到AS中,可以看到assets目錄中的內(nèi)容已經(jīng)復(fù)制到apk中了。

運行效果圖,如下:

直接點擊getResult按鈕會crash,因為 10/0,運行期肯定會報錯。點擊fix 按鈕之后通過熱修復(fù)的方式把代碼做了更改,把代碼中的 10/0 改成了 10/1,然后再點擊 getResult 按鈕的時候就沒問題了。
相關(guān)代碼:
public void fix(View view) {
tvFix.setText("fixing...");
File originDex = null;
try {
originDex = copyFileFromAssets("fixed.dex", getCacheDir().getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
return;
}
if (originDex != null) {
File dexOptimizeDir = getDir("dex", Context.MODE_PRIVATE);
String dexOutputPath = dexOptimizeDir.getAbsolutePath();
PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(originDex.getAbsolutePath(), dexOutputPath, null,
pathClassLoader);
try {
// 獲取DexClassLoader對象的pathList對象,DexPathList
Object dexPathListWithDexClassLoader = ReflectUtils.findField(dexClassLoader, "pathList").get(dexClassLoader);
// 獲取DexPathList對象Element[]數(shù)組,對應(yīng)的字段名是dexElements
Field dexElements = ReflectUtils.findField(dexPathListWithDexClassLoader, "dexElements");
Object[] elements = (Object[]) dexElements.get(dexPathListWithDexClassLoader);
// 獲取PathClassLoader對象的pathList對象,DexPathList
Object dexPathListWithPathClassLoader = ReflectUtils.findField(pathClassLoader, "pathList").get(pathClassLoader);
//把之前獲取的Element[]數(shù)組插入到PathClassLoader對象對應(yīng)的DexPathList的Element數(shù)組中
ReflectUtils.insertFieldArray(dexPathListWithPathClassLoader, "dexElements", elements);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
tvFix.setText("done!");
}
private File copyFileFromAssets(String assetName, String dexOutputDir) throws IOException {
File originDex = null;
AssetManager assets = getAssets();
InputStream open = assets.open(assetName);
originDex = new File(dexOutputDir, assetName);
FileOutputStream fileOutputStream = new FileOutputStream(originDex);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = open.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
fileOutputStream.close();
open.close();
return originDex;
}
熱修復(fù)就是基于類加載機制,把修復(fù)好的dex插入到app的PathClassLoader關(guān)聯(lián)的dex數(shù)組的前部,這樣的話根據(jù)類加載機制,就會先找到修復(fù)好的class,進(jìn)而實現(xiàn)了修復(fù)的目的。
關(guān)于類加載機制,可以閱讀相關(guān)文章:
gradle打包處理添加assets過程
如果我們使用android gradle plugin打包,為了要把外部assets目錄下的文件打包到apk的assets目錄中,需要hook gradle的打包流程。給出相關(guān)代碼,也可以親自研究一下項目apk-build。
在build.gradle中創(chuàng)建一個類實現(xiàn)Plugin接口:
class AssetPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.afterEvaluate {
project.android.applicationVariants.each { ApplicationVariant variant ->
String variantName = variant.name.capitalize()
def packageTask = project.tasks.getByName("package${variantName}")
project.logger.quiet("packageTask: " + packageTask.class)
project.logger.quiet("packageTask assets path: " + packageTask.assets.asPath)
packageTask.doFirst {
project.copy { param ->
from "../multidexbuild/assets"
//很坑的一點是,2.3.3下是packageTask.assets,到3.0就變成packageTask.assets.asPath
into "${packageTask.assets.asPath}"
}
}
}
}
}
}
然后在build.gradle中引入插件:
apply plugin: AssetPlugin
想要了解gradle可以查看我之前寫過的一個gradle系列文章Gradle學(xué)習(xí)。
關(guān)于gradle打包apk的過程,這里就不多做介紹了,感興趣的可以自行了解。如果想要深入研究,推薦研究一下fastdex,是我司的大神寫的,一定會讓你深受啟發(fā)。
也可以閱讀他寫的相關(guān)文章:加快apk的構(gòu)建速度,如何把編譯時間從130秒降到17秒
apk打包shell腳本
為了方便起見,寫了一個構(gòu)建apk文件的腳本 build.sh,
如果要再自己本機上運行一下腳本,需要更改配置的android sdk目錄。
注意:使用命令行打包,注意需要配置好環(huán)境變量,確保adb、dx、aapt等命令都可以正常使用
echo 'init...'
project_dir=$(pwd)
echo "project_dir: ${project_dir}"
# 需要更改為自己的android sdk存放的目錄
sdk_folder=/works/android/android-sdk-macosx
platform_folder=${sdk_folder}/platforms/android-26
android_jar=${platform_folder}/android.jar
# 使用通配符,因為有的命名是sdklib-26.0.0-dev.jar這樣的形式
sdklib_jar=${sdk_folder}/tools/lib/sdklib*.jar
src=${project_dir}/src/main
bin=${project_dir}/bin
libs=${project_dir}/libs
java_source_folder=${src}/java
if [ -d gen ];then
rm -rf gen
fi
if [ -d bin ];then
rm -rf bin
fi
mkdir gen
mkdir bin
#1.生成R文件
echo 'generate R.java file'
aapt package -f -m -J ./gen -S ${src}/res -M ${src}/AndroidManifest.xml -I ${android_jar}
#2.生成資源索引文件
echo 'generate resourses index file'
aapt package -f -M ${src}/AndroidManifest.xml -S ${src}/res -I ${android_jar} -F bin/resources.ap_
#3.編譯java文件
echo 'compile java file'
javac -encoding UTF-8 -g -target 1.7 -source 1.7 -cp ${android_jar} -d bin ${java_source_folder}/com/sososeen09/multidexbuild/*.java ${java_source_folder}/com/sososeen09/multidexbuild/utils/*.java gen/com/sososeen09/multidexbuild/*.java
# javac -encoding UTF-8 -g -target 1.7 -source 1.7 -cp $android_jar -d bin src/ gen/
#4.生成dex文件,這里我們把MainActivity打包到主dex中,utils打包到secondaryDex中
# --minimal-main-dex 表示只把maindexlist.txt中指定的類打包到主dex中
# --set-max-idx-number=2000 表示指定沒個dex的最大方法數(shù)目是2001,最大65535
echo 'generate dex file'
dx --dex --output=bin/ --multi-dex --main-dex-list=maindexlist.txt --minimal-main-dex bin/
#5.打包apk
echo 'generate apk file'
java -cp ${sdklib_jar} com.android.sdklib.build.ApkBuilderMain bin/app-debug-unsigned.apk -v -u -z bin/resources.ap_ -f bin/classes.dex -rf src
#6.通過aapt工具把secondarydex copy到apk中
echo 'aapt add dex into apk'
cd bin
aapt add -f app-debug-unsigned.apk classes2.dex
cd ..
#7.把assets的內(nèi)容加進(jìn)去
echo 'put some file into apk assets'
aapt add -f bin/app-debug-unsigned.apk assets/ic_launcher-web.png assets/fixed.dex
#8 簽名
echo 'sign apk'
java -jar auto-sign/signapk.jar auto-sign/testkey.x509.pem auto-sign/testkey.pk8 ./bin/app-debug-unsigned.apk ./bin/app-debug.apk
#9 打印方法數(shù)
dexdump -f bin/app-debug.apk | grep method_ids_size
相關(guān)文章
- 自己動手生成Android Apk
- android Apk打包過程概述_android是如何打包apk的
- dx使用出現(xiàn)的錯誤總結(jié)
- Android動態(tài)加載入門 簡單加載模式
- 如何使用Ant腳本編譯出Jar和Apk包
- dex分包變形記
- multidex分包續(xù):將指定的類打包到主dex中
- 徹底掌握Android多分包技術(shù)MultiDex-用Ant和Gradle分別構(gòu)建(一)
- Android 打包系列-基本打包流程
- Android簽名機制之---簽名過程詳解
- https://developer.android.google.cn/studio/command-line/zipalign
- 加快apk的構(gòu)建速度,如何把編譯時間從130秒降到17秒


