2014年携程android APK实现了动态插分技术,经过这两三年的实践,dynamicAPK这套框架已经相当成熟,虽然github上已经停止维护,但是携程这套框架还在不断的优化,只是没有在Github上再次更新。

github地址:https://github.com/CtripMobile/DynamicAPK

首先分析一下这个插件化APK是如何生成的,下一篇博客会讲解生成的插件化APK是如何运行的。

 

android编译流程

android 携程 与线程_android 携程 与线程

从android源代码到生成一个APK文件,这里面的流程非常的复杂,我们只需了解一下整个主流程,那就是aapt->javac->proguard->dex。首先是把所有的资源文件通过aapt工具生成一个R.java文件和编译好的资源文件压缩包,然后所有的java文件通过JDK生成相应的class文件,接着这些class文件加上android.jar再加上一些proguard混淆文件通过proguard插件生成一个混淆好的jar包,然后再通过dex工具生成dex文件,最后dex文件加上编译好的资源文件包装成了一个APK文件。

大体知道了APK生成的整个流程我们开始分析dynamicAPK的demo。

dynamicAPK demo:

git clone下来之后代码是不能编译的,可能会出现几个错误:

首先改一下gradle的远程依赖库:

repositories {
        jcenter()
//        maven { url "http://mirrors.ibiblio.org/maven2"}
    }

然后在sample module下的build.gradle的resign任务中修改java home的路径:



task resign(type:Exec,dependsOn: 'repack'){ inputs.file "$rootDir/build-outputs/demo-release-repacked.apk" outputs.file "$rootDir/build-outputs/demo-release-resigned.apk" workingDir "$rootDir/build-outputs" executable "/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/bin/jarsigner"



这样项目应该就可以编译成功了。

最后执行gradle任务:gradle assembleRelease bundleRelease repackAll

mac下执行的其实是:

./gradlew assembleRelease bundleRelease repackAll

我们分析一下这三个task分别干了什么事情。

1.assembleRelease 任务就是生成一个宿主APK,也就是一个壳子,紧接着执行了copyReleaseOutputs task.

//打包后产出物复制到build-outputs目录。apk、manifest、mapping
task copyReleaseOutputs(type:Copy){
    from ("$buildDir/outputs/apk/sample-release.apk") {
        rename 'sample-release.apk', 'demo-base-release.apk'
    }
    from "$buildDir/intermediates/manifests/full/release/AndroidManifest.xml"
    from ("$buildDir/outputs/mapping/release/mapping.txt") {
        rename 'mapping.txt', 'demo-base-mapping.txt'
    }

    into new File(rootDir, 'build-outputs')
}

assembleRelease<<{
    copyReleaseOutputs.execute()
}

说明生成一个apk之后,又把这个apk、manifest文件和混淆文件共同copy到了build-outputs文件夹。

注意在最新的gradle版本中

assembleRelease<<{
    copyReleaseOutputs.execute()
}

这种写法是错误的,我们可以换一种思路:

task releaseOutputs(type:Copy,dependsOn: 'assembleRelease' ){
    from ("$buildDir/outputs/apk/${project.name}-release.apk") {
        rename "${project.name}-release.apk", "${project.name}-base-release.apk"
    }
    from "$buildDir/intermediates/manifests/full/release/AndroidManifest.xml"
    from ("$buildDir/outputs/mapping/release/mapping.txt") {
        rename 'mapping.txt', "${project.name}-base-mapping.txt"
    }

    into new File(rootDir, 'build-outputs')
}

这种一来,执行 ./gradlew assembleRelease 和执行 ./gradlew releaseOutputs会达到同样的目的。

2.bundleRelease 这个task的作用就是生成一个个的插件。

每一个插件都是一个压缩包,只不过这个压缩包的文件名的后缀以.so结尾,给人一种错觉感。

首先看一下bundleRelease这个任务:

task bundleRelease (type:Zip,dependsOn:['compileRelease','aaptRelease','dexRelease']){
    inputs.file "$buildDir/intermediates/dex/${project.name}_dex.zip"
    inputs.file "$buildDir/intermediates/res/resources.zip"

    outputs.file "${rootDir}/build-outputs/${apkName}.so"

    archiveName = "${apkName}.so"
    destinationDir = file("${rootDir}/build-outputs")
    duplicatesStrategy = 'fail'
    from zipTree("$buildDir/intermediates/dex/${project.name}_dex.zip")
    from zipTree("$buildDir/intermediates/res/resources.zip")
}

它又依赖三个任务,分别是compileRelease、aaptRelease和dexRelease,那这四个任务的执行顺序为:

aaptRelease->compileRelease->dexRelease->bundleRelease,我们一一分析:

aaptRelease:这个任务的主要作用是对资源的编译,生成一个R文件、一个资源压缩包和一个混淆配置文件:

task aaptRelease (type: Exec,dependsOn:'init'){


    inputs.file "$sdk.androidJar"
    inputs.file "${rootDir}/build-outputs/demo-base-release.apk"
    inputs.file "$projectDir/AndroidManifest.xml"
    inputs.dir "$projectDir/res"
    inputs.dir "$projectDir/assets"
    inputs.file "${rootDir}/sample/build/generated/source/r/release/ctrip/android/sample/R.java"

    outputs.dir "$buildDir/gen/r"
    outputs.file "$buildDir/intermediates/res/resources.zip"
    outputs.file "$buildDir/intermediates/res/aapt-rules.txt" //混淆配置文件

    workingDir buildDir
    executable sdk.aapt

    def resourceId=''
    def parseApkXml=(new XmlParser()).parse(new File(rootDir,'apk_module_config.xml'))
    parseApkXml.Module.each{ module->
        if( module.@packageName=="${packageName}") {
            resourceId=module.@resourceId
            println "find packageName: " + module.@packageName + " ,resourceId:" + resourceId
        }
    }
    def argv = []
    argv << 'package'   //打包
    argv << "-v"
    argv << '-f' //强制覆盖已有文件
    argv << "-I"
    argv << "$sdk.androidJar"        //添加一个已有的固化jar包
    argv << '-I'
    argv << "${rootDir}/build-outputs/demo-base-release.apk" //使用-I参数对宿主的apk进行引用
    argv << '-M'
    argv << "$projectDir/AndroidManifest.xml"    //指定manifest文件
    argv << '-S'
    argv << "$projectDir/res"                    //res目录
    argv << '-A'
    argv << "$projectDir/assets"                 //assets目录
    argv << '-m'        //make package directories under location specified by -J
    argv << '-J'
    argv << "$buildDir/gen/r"         //哪里输出R.java定义
    argv << '-F'
    argv << "$buildDir/intermediates/res/resources.zip"   //指定apk的输出位置
    /**
     * 资源编译中,对组件的类名、方法引用会导致运行期反射调用,所以这一类符号量是不能在代码混淆阶段被混淆或者被裁减掉的,
     * 否则等到运行时会找不到布局文件中引用到的类和方法。-G方法会导出在资源编译过程中发现的必须keep的类和接口,
     * 它将作为追加配置文件参与到后期的混淆阶段中。
     */
    argv << '-G'
    argv << "$buildDir/intermediates/res/aapt-rules.txt"
    // argv << '--debug-mode'      //manifest的application元素添加android:debuggable="true"
    argv << '--custom-package'      //指定R.java生成的package包名
    argv << "${packageName}"
    argv << '-0'    //指定哪些后缀名不会被压缩
    argv << 'apk'
    /**
     * 为aapt指明了base.R的位置,让它在编译期间把base的资源ID定义在插件的R类中完整复制一份,
     * 这样插件工程即可和之前一样,完全不用在乎资源来自于宿主或者自身,直接使用即可。
     * 当然这样做带来的副作用就是宿主和插件的资源不应有重名,这点我们通过开发规范来约束,相对比较容易理解一些。
     */
    argv << '--public-R-path'
    argv << "${rootDir}/sample/build/generated/source/r/release/ctrip/android/sample/R.java"
    argv << '--apk-module' //指定资源要去哪个插件中查找
    argv << "$resourceId"

    args = argv

}

重点代码我都做了注释,要注意resourceId是我们自己控制了,它来源于根目录下的apk_module_config文件:

<?xml version="1.0" encoding="utf-8"?>
<ApkModules>

    <Module  packageName="ctrip.android.demo1" resourceId="0x31"/>

    <Module  packageName="ctrip.android.demo2" resourceId="0x36"/>


</ApkModules>

然后执行compileRelease task:

task compileRelease(type: JavaCompile,dependsOn:'aaptRelease') {
    inputs.file "$sdk.androidJar"
    inputs.files fileTree("${projectDir}/libs").include('*.jar')
    inputs.files fileTree("$projectDir/src").include('**/*.java')
    inputs.files fileTree("$buildDir/gen/r").include('**/*.java')

    outputs.dir "$buildDir/intermediates/classes"
    sourceCompatibility = '1.6'
    targetCompatibility = '1.6'
    classpath = files(
            "${sdk.androidJar}",
            "${sdk.apacheJar}",
    	    fileTree("${projectDir}/libs").include('*.jar'),
    	    "${rootDir}/sample/build/intermediates/classes-proguard/release/classes.jar"
    	)

    inputs.file "${rootDir}/sample/build/intermediates/classes-proguard/release/classes.jar"

    destinationDir = file("$buildDir/intermediates/classes")

    dependencyCacheDir = file("${buildDir}/dependency-cache")

    source = files(fileTree("$projectDir/src").include('**/*.java'),
            fileTree("$buildDir/gen/r").include('**/*.java'))
    options.encoding = 'UTF-8'
}

这个任务是对java文件的编译,生成对应的class文件,这里需要注意的是你的宿主的build.gradle文件中必须必须配置混淆:

buildTypes {
        debug {
            signingConfig signingConfigs.demo
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.demo
        }
    }

这样一来,compileRelease中的



${rootDir}/sample/build/intermediates/classes-proguard/release/classes.jar



才会存在。

接着执行dexRelease task,这一步是把上一步的class文件通过dex工具转换成dex文件压缩包

android 携程 与线程_android_02

最后执行 bundleRelease:

task bundleRelease (type:Zip,dependsOn:['compileRelease','aaptRelease','dexRelease']){
    inputs.file "$buildDir/intermediates/dex/${project.name}_dex.zip"
    inputs.file "$buildDir/intermediates/res/resources.zip"

    outputs.file "${rootDir}/build-outputs/${apkName}.so"

    archiveName = "${apkName}.so"
    destinationDir = file("${rootDir}/build-outputs")
    duplicatesStrategy = 'fail'
    from zipTree("$buildDir/intermediates/dex/${project.name}_dex.zip")
    from zipTree("$buildDir/intermediates/res/resources.zip")
}

是把dex压缩包和资源压缩包再压缩成一个压缩文件,文件名以.so结尾,放在了build-outputs文件夹下

android 携程 与线程_java_03

3.repackAll

repackAll的任务很简单,就是把前两步生成的宿主APK和两个.so文件合成一个APK,然后再做一个压缩、对齐、优化的操作,最后生成一个完美的APK。

我们咋一看repackAll的依赖还是挺多的



task repackAll(dependsOn: ['reload','resign','repack','realign','concatMappings'])



我们一点点分析:

  1. reload task:把两个so文件放在宿主APK的assets文件夹下,合成一个demo-release-reloaded.apk
  2. repack task:重新压缩 生成demo-release-repacked.apk
  3. resign task:对demo-release-repacked.apk重新签名,生成demo-release-resigned.apk
  4. realigin task:重新对jar包对齐操作,生成demo-release-final.apk.

到此就生成了我们最终想要的APK。

当然这只是编译期的整个流程,主要说了插件的代码编译和资源编译,最终生成了一个插件化APK。

下一篇博客会讲解APK在运行时阶段是如何加载插件的。