博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
携程dynamicAPK框架研究(一)
阅读量:6993 次
发布时间:2019-06-27

本文共 8543 字,大约阅读时间需要 28 分钟。

  hot3.png

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文件压缩包

113631_Aehe_2270118.png

最后执行 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文件夹下

114003_WGhP_2270118.png

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在运行时阶段是如何加载插件的。

转载于:https://my.oschina.net/gef/blog/1408193

你可能感兴趣的文章
界面 切换
查看>>
linux输入及简单的for循环使用
查看>>
Java自带的性能监测工具之jstat
查看>>
我的友情链接
查看>>
通过rsync清除目录的shell脚本
查看>>
我的友情链接
查看>>
vim基本操作
查看>>
快速划分子网的方法
查看>>
Servlet的Web访问名称与Servlet家族简介
查看>>
第一次来
查看>>
Linux命令记录神器history命令基础6
查看>>
TurboMail邮件系统再次通过涉密软件认证
查看>>
JAVA之JNI从头完整实例
查看>>
win10安装解压缩版mysql5.7.25(图解)
查看>>
python redis 分页
查看>>
nmap教程之nmap命令使用示例(nmap使用方法) 服务器***利器
查看>>
[转载] 七龙珠第一部——第118话 天津饭的决心
查看>>
ORACLE DATAGUARD 11G R2 RAC TO RAC
查看>>
AIX slibclean
查看>>
用fopen,fseek,ftell,fread读取文件
查看>>