资源分区框架

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wangzihan91/article/details/79412183

现状:宿主工程和插件工程依赖同一个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中做的

猜你喜欢

转载自blog.csdn.net/wangzihan91/article/details/79412183