前言
阅读本文的时候,配合demo进行演示,效果更佳哦~
项目地址:apk-build
现在绝大部分人应该是使用Android Studio进行app的开发,通常我们运行一个app,直接点击 run
按钮或者使用gradle 命令 ./gradlew assembleDebug
等就可以构建出一个apk文件,那么这个apk文件到底是怎么生成的呢?
本文通过命令行工具构建一个完整的app,来演示app的打包过程,借此来了解一下app的构建流程。由于网上已经有很多相关的文章,本文不会对基本的打包流程做过多详细的分析,有兴趣的读者可以查看文末的相关文章。
基本的打包流程
首先来一张Android官网比较经典的打包流程图,这张图比较早了,但是依然具有指导意义。
从这张图上看,构建一个基本的app,主要需要经历7个过程
- java文件生成过程,
- 通过aapt工具生成R.java文件,输入文件是res目录下的文件和AndroidManifest.xml文件
- 通过aidl工具把.aidl文件生成java文件
- 其实还有apt的方式生成java文件
使用android gradle plugin
打包,在build/generated目录下存放的就是这些生成的java文件。
aapt工具存放在/android-sdk/build-tools/$version/ 目录下。
-
通过aapt工具来生成生成资源索引文件,一般来说生成的文件名是resources.ap_,使用
android gradle plugin
打包,这个文件命名一般是resources_${buildVariant}.ap_
,例如
使用javac命令编译java文件
就是使用jdk中的javac工具,做java开发的应该都知道怎么使用。通过dx工具生成dex文件,dx工具与aapt存放目录一致。
-
通过apkbuilder打包apk,可以在/android-sdk/tools/lib目录下找到sdklib.jar,执行其 com.android.sdklib.build.ApkBuilderMain的main方法
签名,可以使用jarsigner工具签名和apksigner工具签名。jarsigner是Java本生自带的一个工具,他可以对jar进行签名的。而apksigner是后面专门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不一样。jarsigner工具签名时使用的是keystore文件,apksigner工具签名时使用的是pk8,x509.pem文件。
想要了解更多内容可以查看一位大神的文章Android签名机制之---签名过程详解zipaligin,它位于/android-sdk/build-tools/$build-tools-version 目录,是一个zip文件整理工具用来优化apk文件。它的主要工作是将apk包进行对齐处理,使apk包中的所有资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时速度会更快。
注意:关于apk签名和zipaligin这块,如果使用不同的工具签名,zipaligin和签名的顺序是不一致的。例如,如果使用apksigner,那么zipaligin就必须是在签名之前进行。如果使用jarsigner,zipaligin就必须是在签名之后进行。具体可查看官网介绍:https://developer.android.google.cn/studio/command-line/apksigner
demo演示
在apk-build中,分别通过shell脚本和gradle打包的方式来演示构建apk的过程。同时,为了增加一些知识点,demo中演示了通过类加载机制实现代码热修复的一个基本过程。
例如,有如下代码,被除数为0,对于一个已经安装的apk,执行divide()方法肯定会crash。
//修复前
public class SimpleMathUtils {
public static String divide(){
int a=10/0;
return "the divide result is "+a;
}
}
//修复后
public class SimpleMathUtils {
public static String divide(){
int a=10/1;
return "the divide result is "+a;
}
}
修改代码后,把被除数改为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
简单起见,我把修复好的dex文件存放在一个目录中了
打包的时候把assets目录中的内容复制到apk文件中对应的assets目录下。
整个构建前后的文件目录是这样的,bin目录下是构建过程中的产物,gen目录下是生成的R.java文件。
把打包后的apk文件拖入到AS中,可以看到assets目录中的内容已经复制到apk中了。
运行效果图,如下:
直接点击getResult按钮会crash,因为 10/0
,运行期肯定会报错。点击fix 按钮之后通过热修复的方式把代码做了更改,把代码中的 10/0
改成了 10/1
,然后再点击 getResult 按钮的时候就没问题了。
相关代码:
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[]数组,对应的字段名是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[]数组插入到PathClassLoader对象对应的DexPathList的Element数组中
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;
}
热修复就是基于类加载机制,把修复好的dex插入到app的PathClassLoader关联的dex数组的前部,这样的话根据类加载机制,就会先找到修复好的class,进而实现了修复的目的。
关于类加载机制,可以阅读相关文章:
gradle打包处理添加assets过程
如果我们使用android gradle plugin打包,为了要把外部assets目录下的文件打包到apk的assets目录中,需要hook gradle的打包流程。给出相关代码,也可以亲自研究一下项目apk-build。
在build.gradle中创建一个类实现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学习。
关于gradle打包apk的过程,这里就不多做介绍了,感兴趣的可以自行了解。如果想要深入研究,推荐研究一下fastdex,是我司的大神写的,一定会让你深受启发。
也可以阅读他写的相关文章:加快apk的构建速度,如何把编译时间从130秒降到17秒
apk打包shell脚本
为了方便起见,写了一个构建apk文件的脚本 build.sh,
如果要再自己本机上运行一下脚本,需要更改配置的android sdk目录。
注意:使用命令行打包,注意需要配置好环境变量,确保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的最大方法数目是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的内容加进去
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 打印方法数
dexdump -f bin/app-debug.apk | grep method_ids_size
相关文章
- 自己动手生成Android Apk
- android Apk打包过程概述_android是如何打包apk的
- dx使用出现的错误总结
- Android动态加载入门 简单加载模式
- 如何使用Ant脚本编译出Jar和Apk包
- dex分包变形记
- multidex分包续:将指定的类打包到主dex中
- 彻底掌握Android多分包技术MultiDex-用Ant和Gradle分别构建(一)
- Android 打包系列-基本打包流程
- Android签名机制之---签名过程详解
- https://developer.android.google.cn/studio/command-line/zipalign
- 加快apk的构建速度,如何把编译时间从130秒降到17秒