Apk文件构建流程

前言

阅读本文的时候,配合demo进行演示,效果更佳哦~
项目地址:apk-build

现在绝大部分人应该是使用Android Studio进行app的开发,通常我们运行一个app,直接点击 run 按钮或者使用gradle 命令 ./gradlew assembleDebug 等就可以构建出一个apk文件,那么这个apk文件到底是怎么生成的呢?

本文通过命令行工具构建一个完整的app,来演示app的打包过程,借此来了解一下app的构建流程。由于网上已经有很多相关的文章,本文不会对基本的打包流程做过多详细的分析,有兴趣的读者可以查看文末的相关文章。

基本的打包流程

首先来一张Android官网比较经典的打包流程图,这张图比较早了,但是依然具有指导意义。

apk打包流程.png

从这张图上看,构建一个基本的app,主要需要经历7个过程

  1. 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存放目录
  1. 通过aapt工具来生成生成资源索引文件,一般来说生成的文件名是resources.ap_,使用android gradle plugin 打包,这个文件命名一般是 resources_${buildVariant}.ap_,例如

    资源索引文件.png

  2. 使用javac命令编译java文件
    就是使用jdk中的javac工具,做java开发的应该都知道怎么使用。

  3. 通过dx工具生成dex文件,dx工具与aapt存放目录一致。

  4. 通过apkbuilder打包apk,可以在/android-sdk/tools/lib目录下找到sdklib.jar,执行其 com.android.sdklib.build.ApkBuilderMain的main方法


    sdklib.jar.png
  5. 签名,可以使用jarsigner工具签名和apksigner工具签名。jarsigner是Java本生自带的一个工具,他可以对jar进行签名的。而apksigner是后面专门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不一样。jarsigner工具签名时使用的是keystore文件,apksigner工具签名时使用的是pk8,x509.pem文件。
    想要了解更多内容可以查看一位大神的文章Android签名机制之---签名过程详解

  6. 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文件存放在一个目录中了


需要被打包进apk中的文件.png

打包的时候把assets目录中的内容复制到apk文件中对应的assets目录下。

整个构建前后的文件目录是这样的,bin目录下是构建过程中的产物,gen目录下是生成的R.java文件。


构建前后的文件目录.png

把打包后的apk文件拖入到AS中,可以看到assets目录中的内容已经复制到apk中了。


打包后的apk文件.png

运行效果图,如下:


运行效果图..png

直接点击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

相关文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容