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、mappingtask 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文件:
然后执行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在运行时阶段是如何加载插件的。