攜程dynamicAPK框架研究(一) 頂 原

2014年攜程android APK實現了動態插分技術,經過這兩三年的實踐,dynamicAPK這套框架已經相當成熟,雖然github上已經停止維護,但是攜程這套框架還在不斷的優化,只是沒有在Github上再次更新。

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

首先分析一下這個插件化APK是如何生成的,下一篇博客會講解生成的插件化APK是如何運行的。

 

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文件壓縮包

最後執行 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文件夾下

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在運行時階段是如何加載插件的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章