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'])
我們一點點分析:
- reload task:把兩個so文件放在宿主APK的assets文件夾下,合成一個demo-release-reloaded.apk
- repack task:重新壓縮 生成demo-release-repacked.apk
- resign task:對demo-release-repacked.apk重新簽名,生成demo-release-resigned.apk
- realigin task:重新對jar包對齊操作,生成demo-release-final.apk.
到此就生成了我們最終想要的APK。
當然這只是編譯期的整個流程,主要說了插件的代碼編譯和資源編譯,最終生成了一個插件化APK。
下一篇博客會講解APK在運行時階段是如何加載插件的。