现状:宿主工程和插件工程依赖同一个Library工程(宿主分身),插件采用完全独立的Classloader和Resources,这样的插件可以独立运行,但缺点是插件apk较大。采用资源分区方案之后,拿交易插件来说(res降低400k,arsc降低400k,r文件降低100k,classes降低200k,交易插件整体压缩后降低700k左右)
方案对比:目前资源分区主要由两种方案,第一个是修改aapt源码,工作量大、各个版本的aapt都会有区别(携程、淘宝atlas)、第二个方案是编写Gradle插件(Small)
由于Groovy编码效率比Java好很多,Java比C好很多,因此我们选择了Gradle插件的方式实现资源分区。在资源方面,之前各个插件的资源是独立的,实现资源分区之后我们会把宿主分身的资源加到插件Resource中,这样对原有的插件框架改动最小,只需要添加两行代码,之前也考虑过(淘宝Atlas、携程),他们是把所有的插件资源添加到主工程中,对现有的插件框架改动较大。代码方面,我们把主工程的Classloader设成了插件工程的父Classloader,这样插件就可以使用宿主分身的类
public APkClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent, boolean aloneMode) {
super(dexPath, optimizedDirectory, libraryPath, aloneMode ? ClassLoader.getSystemClassLoader() : parent);
this.aloneMode = aloneMode;
hostClassLoader = parent;
}
public class PluginContext extends ContextWrapper {
private Resources resources;
final public void getResource(String path, Context cnx) throws NameNotFoundException {
try {
AssetManager am = AssetManager.class.newInstance();
Method add = am.getClass().getMethod("addAssetPath", String.class);
add.setAccessible(true);
add.invoke(am, path);
if(ifShareRes) {
String apppath = cnx.getPackageResourcePath();
add.setAccessible(true);
add.invoke(am, apppath);
}
assetManager = am;
} catch (Exception e) {
throw new RuntimeException(e);
}
Resources superRes = cnx.getResources();
resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
private int getId(String resType, String resName) {
return resources.getIdentifier(resName, resType, packageName);
}
final public Drawable getDrawable(String resName) {
return resources.getDrawable(getId("drawable", resName));
}
}
实现原理:首先是编译方式的改变,先编译宿主工程,再编译插件工程
配置阶段:创建Extension(small)、创建Task(buildLib、cleanLib、buildBundle、cleanBundle)、更改子工程的依赖库版本、根据子工程名称依赖不同插件。子工程在配置阶段主要是创建Task(Lib或者Bundle)
protected void createExtension() {
project.extensions.create('small', getExtensionClass(), project)
small.type = getPluginType()
}
small {
android {
compileSdkVersion = 25
buildToolsVersion = "25.0.2"
supportVersion = "25.2.0"
}
bundles 'plugin', 'liveroom'
bundles 'plugin', 'trade'
bundles 'plugin', 'charts'
bundles 'plugin', 'circle'
bundles 'plugin', 'login'
bundles 'plugin', 'webview'
bundles 'plugin', 'feedback'
bundles 'stub', 'app+stub'
bundles 'host', 'app'
}
def base = rootExt.android
if (base != null) {
project.subprojects { p ->
p.afterEvaluate {
configVersions(p, base)
}
}
}
protected void configVersions(Project p, RootExtension.AndroidConfig base) {
if (!p.hasProperty('android')) return
com.android.build.gradle.BaseExtension android = p.android
if (base.compileSdkVersion != 0) {
android.compileSdkVersion = base.compileSdkVersion
}
if (base.buildToolsVersion != null) {
android.buildToolsVersion = base.buildToolsVersion
}
if (base.supportVersion != null) {
def sv = base.supportVersion
def cfg = p.configurations.compile
def supportDependencies = []
cfg.dependencies.each { d ->
if (d.group == 'com.android.support' && d.version != sv) {
supportDependencies.add(d)
}
}
cfg.dependencies.removeAll(supportDependencies)
supportDependencies.each { d ->
p.dependencies.add('compile', "$d.group:$d.name:$sv")
}
}
}
project.subprojects {
if(hostname.contains(it.name)) {
if (it.name == hostname) {
it.apply plugin: HostPlugin
rootExt.hostProject = it
}
} else if (appStubName == it.name) {
rootExt.hostStubProjects.add(it)
} else {
appList.each { appName ->
if (it.name == appName) {
it.apply plugin: AppPlugin
rootExt.appProjects.add(it)
}
}
}
if (it.hasProperty('buildLib')) {
it.tasks['buildLib'].doLast {
buildLib(it.project)
}
}
}
@Override
protected void configureReleaseVariant(BaseVariant variant) {
super.configureReleaseVariant(variant)
if (small.jar != null) return
def flavor = variant.flavorName
if (flavor != null) {
flavor = flavor.capitalize()
small.jar = project.tasks["jar${flavor}ReleaseClasses"]
small.aapt = project.tasks["process${flavor}ReleaseResources"]
} else {
small.jar = project.jarReleaseClasses
small.aapt = project.processReleaseResources
}
project.buildLib.dependsOn small.jar
}
编译宿主工程(buildLib任务):buildLib任务依赖于jarReleaseClasses,我们主要在jarReleaseClasses任务结束后收集Stub工程的id文件(通过读取stub工程output/aar文件中的R.txt来读取key值,通过读取主工程aapt的textSymbolOutputDir目录的R.txt文件来读取value值)
small.hostStubProjects.each {
def outputs = new File(it.buildDir,"outputs")
def aarDir = new File(outputs,"aar")
def fileName = it.getName()+"-release.aar"
def aarFile = new File(aarDir,fileName)
println "aarFile ${aarFile.getAbsolutePath()}"// app+stub/build/outputs/aar/app+stub-release.aar
File unzipDir = new File(aarFile.parentFile, 'unzip')
project.copy {
from project.zipTree(aarFile)
into unzipDir
}
File RFile = new File(unzipDir, 'R.txt')
}
def srcIdsFile = new File(aapt.textSymbolOutputDir, 'R.txt')// app/build/intermediates/symbols/normal/release/R.txt
在编译插件工程时(buildBundle),我们首先获取Stub工程的所有类文件,保存到filterFiles中
Configuration compile = project.getConfigurations().getByName("compile")
Set<DefaultProjectDependency> allLibs = compile.dependencies.withType(DefaultProjectDependency.class)
Set<File> f = new HashSet<>()
Set<File> h = new HashSet<>()
h.addAll(compile.getFiles())
allLibs.each { it ->
h.each { item ->
if (item.getAbsolutePath().contains(it.name)) {
f.add(item)
}
}
}
//h中保存了aar和jar文件,后面解压缩,提取所有的class文件
具体在剔除类文件时,分为混淆和不混淆两种:不混淆情况下,我们通过transform Api进行过滤;混淆的情况下,通过hook proguard任务,解压main.jar文件,进行过滤,然后重新打包。重点说一下为什么要区分混淆和不混淆,在混淆情况下,如果我们提前在transform api中把类过滤掉,在proguard任务中会出现错误,比如一个类A继承另一个类B,我们提前把B过滤掉,在混淆A的时候就会出错,而且混淆模式下过滤类文件需要keep住相应的文件,这就需要在混淆和过滤中间取舍,特别是宿主分身中的类,我们目前是keep住了所有
-keep class com.netease.pluginbasiclib.** {*;}
//在Transform中,忽略了directory,过滤包含filterFiles类的jar包
inputs.each {
it.directoryInputs.each {
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY);
FileUtils.copyDirectory(it.file, dest)
}
it.jarInputs.each {
//ifProguard = variant.getBuildType().buildType.minifyEnabled
if(!small.ifProguard) {
if (filterFiles.size() != 0 && small.filterFiles.contains(filterFiles.get(0))) {
return
}
}
}
}
proguard.doLast {
def minifyJar = IntermediateFolderUtils.getContentLocation(proguard.streamOutputFolder, 'main', pt.outputTypes, pt.scopes, Format.JAR)
File unzipDir = new File(minifyJar.parentFile, 'main')
project.copy {
from project.zipTree(minifyJar)
into unzipDir
}
def javac = small.javac
File pkgDir = new File(unzipDir, small.packagePath)
pkgDir.listFiles().each { f ->
if (f.name.startsWith('R$')) {
f.delete()
}
}
unzipDir.listFiles().each { f ->
filterFile(f)
}
//Re-compile the split R.java to R.class
project.ant.javac(srcdir: small.splitRJavaFile.parentFile, source: javac.sourceCompatibility, target: javac.targetCompatibility, destdir: unzipDir)
//Repack the minify jar
project.ant.zip(baseDir: unzipDir, destFile: minifyJar)
}
过滤宿主分身的资源:首先创建id映射关系(插件id和宿主分身id的对应关系、插件自身旧id和插件自身新id的对应关系、带有资源aar的id值,用于生成对应aar的R文件),然后修改arsc文件(过滤公共资源、修改资源id值)和xml文件(修改资源id值),生成修改后的全量R文件(用于通过javac编译),生成修改后的精简R文件,并在javac任务doLast中重新生成R.class
AppExtension(Project project) {
super(project)
File interDir = new File(project.buildDir, FD_INTERMEDIATES)
println "AppExtension $interDir" //charts/build/intermediates
aarDir = new File(interDir, 'exploded-aar')
}
File aarPath = new File(small.aarDir, path)
String resPath = new File(aarPath, 'res').absolutePath
File symbolFile = new File(aarPath, 'R.txt')
遇到的问题:各个插件依赖的aar版本不统一,过滤类资源出现问题,解决的方法:在各个子工程配置结束时,统一更改依赖库的版本号、编译速度慢,增加提前过滤功能
流程上的问题(版本号控制):修改公共资源以及主项目的公共接口,HostVersion要升级,所有的插件Version要升级,对于项目自带的插件,HostVersion没升级没问题,但是我们可以通过网络下发插件,且插件只有一个,如果HostVersion没升级,这回导致低版本的主项目使用现有的插件,造成NoMethodDefinedError错误;插件接口改变,abstractMethodError对应于插件Version没升级,这样主项目存在接口,但是因为用的是旧插件,没有对应的实现
资源分区框架代码流程:首先在根Project下apply该插件,然后在根Project下面配置Extension,指明编译环境的版本号和各个Module的属性(Host、App、Stub),在插件afterEvaluate函数中,我们首先统一更改compileSdkVersion、buildToolsVersion和com.android.support的各个版本,前两个通过获取project的android扩展来修改,support版本通过遍历compile类型的dependencies(remove、add)来修改,之后通过读取在Extension中配置的不同子工程,分别apply不同的插件(HostPlugin、AppPlugin)。在HostPlugin中,我们首先创建了buildLib和cleanLib任务,在afterEvaluate后,我们拿到jarReleaseClasses任务和processReleaseResources任务,并让buildLib依赖jarReleaseClasses,在buildLib的doLast中,我们首先收集所有的jar依赖,通过jarReleaseClasses.archivePath收集到主工程的classes.jar文件,通过project.fileTree收集libs文件,通过task类型为PrepareLibraryTask的输出来收集所有aar文件中的jar和libs文件。通过读取stub工程的buildDir/outputs/aar/name-release.aar文件,提取其中的R.txt文件,读取所有的id名称,然后通过processReleaseResources.textSymbolOutputDir的R.txt来更新id值。在AppPlugin的afterEvaluated中,我们首先创建package id,然后注册过滤类文件的Transform,然后我们hook了processManifest任务(用于剔除icon、label等无用的tab),hook了aapt任务,主要工作都在这里,这里主要处理三个部分,自身资源id和stub资源id的对应关系、自身资源id和新资源id的对应关系、带资源aar的资源id和新资源id的对应关系,hook了javac任务,重新生成对应的R.class文件,在gradle的whenReady回调中,我们拿到了通过getFiles拿到了stub的所有class文件,现在去除重复class是在proguard或者是transform中做的