Small 插件化单一 Resources 方案

Small 插件化单一 Resources 方案

27 February 2018 Android

single resources

前言


「所有插件的资源都加载到一个 AssetManager,全局可用」。

单一 ResourcesAssetManager」的方案,主要问题在于资源 ID 冲突,解决的方案大体上分三种:

    1. 修改 AAPT 的源码
    1. 修改 AAPT 的生成产物「 R.java,resource.arsc,各类 xml 包括 layout
    1. 使用 public.xml 手动设置 padding


小结


    1. 1 出现的较早,原理也比较简单,修改的部分不多,比如:携程的 DynamicApk
    1. 2 比较多的项目用,比如: Small
    1. 3 不涉及到打包流程改动



资源的打包过程


    1. 解析 AndroidManifest.xml
    1. 添加被引用资源包
    1. 收集资源文件
    1. 将收集到的资源增加到资源表
    1. 编译 values 类资源
    1. 给 Bag 资源分配 ID
    1. 编译 xml 资源文件
    1. 生成资源符号
    1. 生成资源索引表
    1. 编译 AndroidManifest.xml 文件
    1. 生成 R.java 文件
    1. 打包 APK 文件

:app:preBuild UP-TO-DATE

:app:preDebugBuild UP-TO-DATE

:app:checkDebugManifest

:app:preReleaseBuild UP-TO-DATE

...

:app:prepareDebugDependencies

:app:compileDebugAidl UP-TO-DATE

:app:compileDebugRenderscript UP-TO-DATE

:app:generateDebugBuildConfig UP-TO-DATE

:app:generateDebugResValues UP-TO-DATE

:app:generateDebugResources UP-TO-DATE

:app:mergeDebugResources UP-TO-DATE

:app:processDebugManifest UP-TO-DATE

:app:processDebugResources UP-TO-DATE

:app:generateDebugSources UP-TO-DATE

:app:incrementalDebugJavaCompilationSafeguard UP-TO-DATE

:app:compileDebugJavaWithJavac Incremental compilation of 2 classes completed in 0.737 secs.

:app:compileDebugNdk UP-TO-DATE

...

:app:transformResourcesWithMergeJavaResForDebug UP-TO-DATE

:app:validateSigningDebug

:app:packageDebug

:app:assembleDebug



SingleResourcesPlugin


Hook 第 11 到第 12 之间

/**
 * CaMnter
 * */

class SingleResourcesPlugin implements Plugin<Project> {

    private static final String TAG = SingleResourcesPlugin.class.simpleName

    private static final int UNSET_TYPEID = 99
    private static final int UNSET_ENTRYID = -1
    protected static def sPackageIds = [:] as LinkedHashMap<String, Integer>

    protected Project project

    def idMaps = [:]
    def idStrMaps = [:]
    def retainedTypes = []
    def retainedStyleables = []
    def allTypes = []
    def allStyleables = []
    // 0x77
    def packageId = 119

    def packagePath = "com/camnter/single/resources/plugin"
    def packageName = "com.camnter.single.resources.plugin"

    @Override
    void apply(Project project) {
        this.project = project

        project.afterEvaluate {
            def processDebugResources = project.tasks['processDebugResources']
            // 防止 Up-To-Data 跳过该任务
            processDebugResources.outputs.upToDateWhen { false }
            processDebugResources.doLast {
                println "[${TAG}]   prepare hook aapt"
                if (it instanceof ProcessAndroidResources) {
                    println "[${TAG}]  start hook aapt"
                    hookAapt(it)
                }
            }
        }
    }

    /*********************
     * Small source code *
     *********************/


    /**
     * Hook aapt task to slice asset package and resolve library resource ids
     * */
    private def hookAapt(ProcessAndroidResources aaptTask) {

        // Unpack resources.ap_
        File apFile = aaptTask.packageOutputFile
        FileTree apFiles = project.zipTree(apFile)
        File unzipApDir = new File(apFile.parentFile, 'ap_unzip')
        unzipApDir.delete()
        project.copy {
            from apFiles
            into unzipApDir

            include 'AndroidManifest.xml'
            include 'resources.arsc'
            include 'res/**/*'
        }

        // Modify assets
        File symbolFile = new File(aaptTask.textSymbolOutputDir, 'R.txt')
        prepareSplit(symbolFile)
        File sourceOutputDir = aaptTask.sourceOutputDir
        File rJavaFile = new File(sourceOutputDir, "${packagePath}/R.java")
        def rev = project.android.buildToolsRevision
        int noResourcesFlag = 0
        def filteredResources = new HashSet()
        def updatedResources = new HashSet()


        Aapt aapt = new Aapt(unzipApDir, rJavaFile, symbolFile, rev)
        if (this.retainedTypes != null && this.retainedTypes.size() > 0) {
            aapt.filterResources(this.retainedTypes, filteredResources)
            println "[${TAG}]   [${project.name}] split library res files..."

            aapt.filterPackage(this.retainedTypes, this.packageId, this.idMaps, null,
                    this.retainedStyleables, updatedResources)

            println "[${TAG}]   [${project.name}] slice asset package and reset package id..."

            String pkg = packageName
            // Overwrite the aapt-generated R.java with full edition
            rJavaFile.delete()
            aapt.generateRJava(rJavaFile, pkg, this.allTypes, this.allStyleables)


            println "[${TAG}]   [${project.name}] split library R.java files..."
        } else {
            println 'No Resource To Modify'
        }


        String aaptExe = aaptTask.buildTools.getPath(BuildToolInfo.PathId.AAPT)

        // Delete filtered entries.
        // Cause there is no `aapt update' command supported, so for the updated resources
        // we also delete first and run `aapt add' later.
        filteredResources.addAll(updatedResources)
        ZipUtils.with(apFile).deleteAll(filteredResources)

        // Re-add updated entries.
        // $ aapt add resources.ap_ file1 file2 ...
        project.exec {
            executable aaptExe
            workingDir unzipApDir
            args 'add', apFile.path
            args updatedResources

            // store the output instead of printing to the console
            // standardOutput = new ByteArrayOutputStream()
        }
    }

    /**
     * Prepare retained resource types and resource id maps for package slicing
     * */
    protected void prepareSplit(File symbolFile) {
        def idsFile = symbolFile
        if (!idsFile.exists()) return



        def publicEntries = SymbolParser.getResourceEntries(new File(""))
        def bundleEntries = SymbolParser.getResourceEntries(idsFile)
        def staticIdMaps = [:]
        def staticIdStrMaps = [:]
        def retainedEntries = []
        def retainedPublicEntries = []
        def retainedStyleables = []

        bundleEntries.each { k, Map be ->
            be._typeId = UNSET_TYPEID // for sort
            be._entryId = UNSET_ENTRYID

            Map le = publicEntries.get(k)
            if (le != null) {
                // Use last built id
                be._typeId = le.typeId
                be._entryId = le.entryId
                retainedPublicEntries.add(be)
                publicEntries.remove(k)
                return
            }


            be.isStyleable ? retainedStyleables.add(be) : retainedEntries.add(be)
        }

        if (publicEntries.size() > 0) {
            throw new RuntimeException(
                    "No support deleting resources on lib.* now!\n" + "  - ${publicEntries.keySet().join(", ")}\n" +
                            "see https://github.com/wequick/Small/issues/53 for more information.")
        }
        if (retainedEntries.size() == 0 && retainedPublicEntries.size() == 0) {
            this.retainedTypes = [] // Doesn't have any resources
            return
        }

        // Prepare public types
        def publicTypes = [:]
        def maxPublicTypeId = 0
        def unusedTypeIds = [] as Queue
        if (retainedPublicEntries.size() > 0) {
            retainedPublicEntries.each { e ->
                def typeId = e._typeId
                def entryId = e._entryId
                def type = publicTypes[e.type]
                if (type == null) {
                    publicTypes[e.type] = [id      : typeId, maxEntryId: entryId,
                                           entryIds: [entryId], unusedEntryIds: [] as Queue]
                    maxPublicTypeId = Math.max(typeId, maxPublicTypeId)
                } else {
                    type.maxEntryId = Math.max(entryId, type.maxEntryId)
                    type.entryIds.add(entryId)
                }
            }
            if (maxPublicTypeId != publicTypes.size()) {
                for (int i = 1; i < maxPublicTypeId; i++) {
                    if (publicTypes.find { k, t -> t.id == i } == null) unusedTypeIds.add(i)
                }
            }
            publicTypes.each { k, t ->
                if (t.maxEntryId != t.entryIds.size()) {
                    for (int i = 0; i < t.maxEntryId; i++) {
                        if (!t.entryIds.contains(i)) t.unusedEntryIds.add(i)
                    }
                }
            }
        }

        // First sort with origin(full) resources order
        retainedEntries.sort { a, b -> a.typeId <=> b.typeId ?: a.entryId <=> b.entryId
        }

        // Reassign resource type id (_typeId) and entry id (_entryId)
        def lastEntryIds = [:]
        if (retainedEntries.size() > 0) {
            if (retainedEntries[0].type != 'attr') {
                // reserved for `attr'
                if (maxPublicTypeId == 0) maxPublicTypeId = 1
                if (unusedTypeIds.size() > 0) unusedTypeIds.poll()
            }
            def selfTypes = [:]
            retainedEntries.each { e ->
                // Check if the type has been declared in public.txt
                def type = publicTypes[e.type]
                if (type != null) {
                    e._typeId = type.id
                    if (type.unusedEntryIds.size() > 0) {
                        e._entryId = type.unusedEntryIds.poll()
                    } else {
                        e._entryId = ++type.maxEntryId
                    }
                    return
                }
                // Assign new type with unused type id
                type = selfTypes[e.type]
                if (type != null) {
                    e._typeId = type.id
                } else {
                    if (unusedTypeIds.size() > 0) {
                        e._typeId = unusedTypeIds.poll()
                    } else {
                        e._typeId = ++maxPublicTypeId
                    }
                    selfTypes[e.type] = [id: e._typeId]
                }
                // Simply increase the entry id
                def entryId = lastEntryIds[e.type]
                if (entryId == null) {
                    entryId = 0
                } else {
                    entryId++
                }
                e._entryId = lastEntryIds[e.type] = entryId
            }

            retainedEntries += retainedPublicEntries
        } else {
            retainedEntries = retainedPublicEntries
        }

        // Resort with reassigned resources order
        retainedEntries.sort { a, b -> a._typeId <=> b._typeId ?: a._entryId <=> b._entryId
        }

        // Resort retained resources
        def retainedTypes = []
        def pid = (this.packageId << 24)
        def currType = null
        retainedEntries.each { e ->
            // Prepare entry id maps for resolving resources.arsc and binary xml files
            if (currType == null || currType.name != e.type) {
                // New type
                currType = [type: e.vtype, name: e.type, id: e.typeId, _id: e._typeId, entries: []]
                retainedTypes.add(currType)
            }
            def newResId = pid | (e._typeId << 16) | e._entryId
            def newResIdStr = "0x${Integer.toHexString(newResId)}"
            staticIdMaps.put(e.id, newResId)
            staticIdStrMaps.put(e.idStr, newResIdStr)

            // Prepare styleable id maps for resolving R.java
            if (retainedStyleables.size() > 0 && e.typeId == 1) {
                retainedStyleables.findAll { it.idStrs != null }.each {
                    // Replace `e.idStr' with `newResIdStr'
                    def index = it.idStrs.indexOf(e.idStr)
                    if (index >= 0) {
                        it.idStrs[index] = newResIdStr
                        it.mapped = true
                    }
                }
            }

            def entry = [name: e.key, id: e.entryId, _id: e._entryId, v: e.id, _v: newResId,
                         vs  : e.idStr, _vs: newResIdStr]
            currType.entries.add(entry)
        }

        // Update the id array for styleables
        retainedStyleables.findAll { it.mapped != null }.each {
            it.idStr = "{ ${it.idStrs.join(', ')} }"
            it.idStrs = null
        }

        // Collect all the resources for generating a temporary full edition R.java
        // which required in javac.
        // TODO: Do this only for the modules who's code really use R.xx of lib.*
        def allTypes = []
        def allStyleables = []
        def addedTypes = [:]

        retainedTypes.each { t ->
            def at = addedTypes[t.name]
            if (at != null) {
                at.entries.addAll(t.entries)
            } else {
                allTypes.add(t)
            }
        }
        allStyleables.addAll(retainedStyleables)

        this.idMaps = staticIdMaps
        this.idStrMaps = staticIdStrMaps
        this.retainedTypes = retainedTypes
        this.retainedStyleables = retainedStyleables

        this.allTypes = allTypes
        this.allStyleables = allStyleables
    }
}



generate local plugin


gradle :plugin-life:single-resources:single-resources-gradle-plugin:uploadArchives 



module build.gradle


apply plugin: 'com.camnter.gradle.plugin.single.resources'

buildscript {
    repositories {
        jcenter()
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
        maven { url "https://plugins.gradle.org/m2/" }
        google()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
        // local repository
        maven { url uri('../../single-resources-gradle-plugin/repository') }
    }
    dependencies {
        // local repository
        classpath 'com.camnter.gradle.plugin:single-resources-gradle-plugin:1.0.2'
    }
}

SingleResourcesPluginActivity

/**
 * @author CaMnter
 */

@SuppressWarnings("DanglingJavadoc")
public class SingleResourcesPluginActivity extends AppCompatActivity {

    private static final String HOST_PACKAGE_NAME = "com.camnter.single.resources.host";

    private Resources singleResources;


    /**
     * ---------------------------------------------------------------------------------------------
     *
     * // 加载 插件资源
     *
     * this.setContentView(R.layout.single_resources_plugin);
     *
     * ---------------------------------------------------------------------------------------------
     *
     * // 加载 宿主资源
     *
     * final TextView hostText = (TextView) this.findViewById(R.id.host_text);
     * final Resources resources = this.getResources();
     * final String hostInfo = resources.getString(resources.getIdentifier("host_info", "string",
     * HOST_PACKAGE_NAME));
     * final int hostInfoColor = resources.getColor(resources.getIdentifier("host_info_color",
     * "color", HOST_PACKAGE_NAME));
     * hostText.setText(hostInfo);
     * hostText.setTextColor(hostInfoColor);
     *
     * ---------------------------------------------------------------------------------------------
     *
     * @param savedInstanceState Bundle
     */
    @SuppressLint("SetTextI18n")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 加载 插件资源
        this.setContentView(R.layout.single_resources_plugin);

        // 加载 宿主资源
        final TextView hostText = (TextView) this.findViewById(R.id.host_text);
        final Resources resources = this.getResources();
        final String hostInfo = resources.getString(
            resources.getIdentifier("host_info", "string", HOST_PACKAGE_NAME));
        final int hostInfoColor = resources.getColor(
            resources.getIdentifier("host_info_color", "color", HOST_PACKAGE_NAME));
        hostText.setText(hostInfo);
        hostText.setTextColor(hostInfoColor);
    }


    @Override
    protected void attachBaseContext(Context newBase) {
        this.hookResources(newBase);
        super.attachBaseContext(newBase);
    }


    private void hookResources(@NonNull final Context newBase) {
        try {
            final Field field = newBase.getClass().getDeclaredField("mResources");
            field.setAccessible(true);
            if (this.singleResources == null) {
                this.singleResources = this.getPluginResources(newBase);
            }
            field.set(newBase, this.singleResources);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 获取 插件 Resources
     *
     * @param newBase Context
     * @return Resources
     */
    private Resources getPluginResources(final Context newBase) {
        try {
            String dir = null;
            final File cacheDir = newBase.getExternalCacheDir();
            final File filesDir = newBase.getExternalFilesDir("");
            if (cacheDir != null) {
                dir = cacheDir.getAbsolutePath();
            } else {
                if (filesDir != null) {
                    dir = filesDir.getAbsolutePath();
                }
            }
            if (TextUtils.isEmpty(dir)) return null;

            // assets 的 single-resources-plugin.apk 拷贝到 /storage/sdcard0/Android/data/[package name]/cache
            // 或者  /storage/sdcard0/Android/data/[package name]/files
            final File dexPath = new File(dir + File.separator + "single-resources-plugin.apk");

            if (!dexPath.exists()) {
                AssetsUtils.copyAssets(newBase, "single-resources-plugin.apk",
                    dexPath.getAbsolutePath());
            }

            final AssetManager assetManager = AssetManager.class.newInstance();
            @SuppressWarnings("RedundantArrayCreation") final Method addAssetPath
                = assetManager.getClass()
                .getMethod("addAssetPaths", new Class[] { String[].class });

            final String[] paths = new String[2];
            // 插件 Asset
            paths[0] = dexPath.getAbsolutePath();
            // 宿主 Asset
            paths[1] = newBase.getPackageResourcePath();
            addAssetPath.invoke(assetManager, new Object[] { paths });

            final Resources originalResources = newBase.getResources();
            return new Resources(
                assetManager,
                originalResources.getDisplayMetrics(),
                originalResources.getConfiguration()
            );
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}



参考资料