Android插件化与热修复(七)-微信Tinker资源加载、gradle插件分析

tinker修复的过程包含两个过程,一方面服务端产生补丁包的过程;另一方面用户端获得补丁包之后的修复工程,简单的流程可以用如下的图描述:


Android插件化与热修复(七)-微信Tinker资源加载、gradle插件分析_第1张图片
Tinker热修复过程示意图

服务端补丁产生过程主要是tinker 定义的gradle插件来完成。我们在文章的后面会详细介绍gradle插件的细节。
我们首先来分析用户端的逻辑。一个完整的apk解压之后基本上包括dex文件,资源文件和so文件。前面的文章中已经详细介绍了dex文件的修复过程,本章主要介绍资源文件和so文件的修复。

资源文件的修复

资源文件的修复过程发生在客户端。包含两个主要的过程,首先是客户端获取到patch.apk之后,和本地的base.apk的合并过程,这个过程根据BSD算法将patch的资源文件和base的资源文件合并,最后打包到fix.apk中;另一个是资源文件的加载过程,主要的功能是引导base.apk使用fix.apk中的资源(这里有个关键点:虽然我们在用户端有了fix.apk,但是我们的fix.apk并没有安装,启动应用的还是base.apk,只是引导base.apk使用fix.apk里面的dex文件,资源文件,so文件)。我们主要介绍资源文件的加载。资源文件的合并主要是算法,我们这里不做介绍,感兴趣的读者可以在tinker源码中自行学习BSPatch.java。

 public static boolean loadTinkerResources(Context context, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult) {
        if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
            return true;
        }
        String resourceString = directory + "/" + RESOURCE_PATH +  "/" + RESOURCE_FILE;
        File resourceFile = new File(resourceString);
        long start = System.currentTimeMillis();

        if (tinkerLoadVerifyFlag) {
            if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
                Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
                return false;
            }
            Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
        }
        try {   TinkerResourcePatcher.monkeyPatchExistingResources(context, resourceString);
            Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
        } catch (Throwable e) {
            Log.e(TAG, "install resources failed");
            //remove patch dex if resource is installed failed
            try {
                SystemClassLoaderAdder.uninstallPatchDex(context.getClassLoader());
            } catch (Throwable throwable) {
                Log.e(TAG, "uninstallPatchDex failed", e);
            }
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
            return false;
        }

        return true;
    }

resourceString 局部变量代表的是fix.apk的资源文件。首先检查文件的的md5是否正确。然后再TinkerResourcePatcher中完成资源加载。

public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
        if (externalResourceFile == null) {
            return;
        }

        for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
            Object value = field.get(currentActivityThread);

            for (Map.Entry> entry
                : ((Map>) value).entrySet()) {
                Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                if (externalResourceFile != null) {
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }
        // Create a new AssetManager instance and point it to the resources installed under
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }

        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        ensureStringBlocksMethod.invoke(newAssetManager);

        for (WeakReference wr : references) {
            Resources resources = wr.get();
            //pre-N
            if (resources != null) {
                // Set the AssetManager of the Resources instance to our brand new one
                try {
                    assetsFiled.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    // N
                    Object resourceImpl = resourcesImplFiled.get(resources);
                    // for Huawei HwResourcesImpl
                    Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }

                clearPreloadTypedArrayIssue(resources);

                resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
            }
        }

        // Handle issues caused by WebView on Android N.
        // Issue: On Android N, if an activity contains a webview, when screen rotates
        // our resource patch may lost effects.
//        publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);

        if (!checkResUpdate(context)) {
            throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
        }
    }

主要的功能在两个for循环中。外层的for循环是两个Field对象packagesFiled和resourcePackagesFiled,他们分别代表ActivityThread中的成员mPackages和mResourcePackages。里层的for循环分别修改这两个Field对象中的成员resDir,也就是LoadedApk的成员mResDir。这里主要通过反射修改系统文件的属性,实现资源的加载。后面的流程主要对android系统版本兼容处理。

为什么修改了ActivityThread的属性mPackages,mResourcePackages和LoadedApk类的mResDir属性就可以实现Base.apk加载Fix.apk的资源?我们从Android源码的角度分析一下。

通常我们获取某一资源的代码如下:context.getResource().getxxxx,这里的context我们知道是ContextImpl类。ContextImpl的方法getResource()得到的是它的属性mResources,mResources代表了一个资源包,也就是说如果这里的mResources代表的是Fix.apk的资源包,我们就完成了资源的加载。mResources是如何初始化的呢?

 final void init(LoadedApk packageInfo,
                IBinder activityToken, ActivityThread mainThread,
                Resources container, String basePackageName) {
        mPackageInfo = packageInfo;
        mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;
        mResources = mPackageInfo.getResources(mainThread);

        if (mResources != null && container != null
                && container.getCompatibilityInfo().applicationScale !=
                        mResources.getCompatibilityInfo().applicationScale) {
            if (DEBUG) {
                Log.d(TAG, "loaded context has different scaling. Using container's" +
                        " compatiblity info:" + container.getDisplayMetrics());
            }
            mResources = mainThread.getTopLevelResources(
                    mPackageInfo.getResDir(), container.getCompatibilityInfo());
        }
        mMainThread = mainThread;
        mContentResolver = new ApplicationContentResolver(this, mainThread);

        setActivityToken(activityToken);
    }

在ContextImpl初始化的时候对mResources 赋值。ActivityThread的方法getTopLevelResources和LoadedApk的方法getResources都可以对mResources ,最终都是调用的

Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
        ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
        Resources r;
        synchronized (mPackages) {
            // Resources is app scale dependent.
            if (false) {
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / "
                        + compInfo.applicationScale);
            }
            WeakReference wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (false) {
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                }
                return r;
            }
        }

        //if (r != null) {
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "
        //            + r + " " + resDir);
        //}

        AssetManager assets = new AssetManager();
        if (assets.addAssetPath(resDir) == 0) {
            return null;
        }

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics metrics = getDisplayMetricsLocked(null, false);
        r = new Resources(assets, metrics, getConfiguration(), compInfo);
        if (false) {
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        }
        
        synchronized (mPackages) {
            WeakReference wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) {
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            }
            
            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference(r));
            return r;
        }
    }

这里逻辑很清晰了。利用resDir设置AssetManager的属性,并创建Resource对象,resource对象和resDir一一对应。这里resDir参数是LoadedApk的属性mResourecDir。因此整个逻辑实现就是如果我们修改LoadedApk的属性mResourceDir是Fix.apk的资源,ContextImpl的属性mResource就代表了Fix.apk的资源。如下的图说明了他们之间的关系:


Android插件化与热修复(七)-微信Tinker资源加载、gradle插件分析_第2张图片
Android系统资源文件相关类

So文件的加载

so的修复过程也是在用户端完成的。首先是根据BSD算法修复so,然后在使用so文件的地方加载。

so文件加载机制

SO文件加载的时机和Dex、资源的加载有些不一样,Dex和资源的加载都是系统在特定的时机自动去加载,而SO加载的时机则是让开发者自己控制.开发者可以通过System类对外暴露出来的两个静态方法load和loadLibarary加载SO.这两个方法都拿ClassLoader再通过Runtime实现的。

  • Sytem.loadLibrary 方法是加载app安装过之后自动从apk包中释放到/data/data/packagename/lib下对应的SO文件。如果要使用这样的方式加载fix.apk中的so文件,需要利用反射修改DexPathList的属性nativeLibraryDirectorie,具体的方法可以参考前面文章中Dex文件的加载过程。
  • System.load 方法可以根据开发者指定的路径加载SO文件,例如/data/data/packagename/tinker/patch-xxx/lib/libtest.so,这是Tinker使用的方式,开发者不用修改任何代码,只要指定路径就可以加载Fix.apk的so文件。

Gradle插件

为什么要用gradle插件

  • 根据上面的分析,用户端要先得到patch.apk,才完成后面的修复和加载的过程,patch.apk是客户端修复的关键。tinker中的Gradle插件就是完成patch.apk的。
  • 另外,dex文件的差分和合并对比的是字节码,BSD算法的差分和合并对比的是二进制文件,那么这里隐含了一条规则,服务端的fix.apk的Dex文件的混淆,以及资源文件的R文件的属性需要和base.apk的一样。以及对于代码超出65536的方法,需要分包的应用,分包方法也需要和base.apk一样。这些也是gradle插件需要处理的问题。

Tinker中gradle插件的流程

Android插件化与热修复(七)-微信Tinker资源加载、gradle插件分析_第3张图片
Tinker中gradle插件工作流程图

简单的流程如下:在fix.apk编译之前按照需要修复的base.apk的资源id映射生成map文件;按照base.apk的混淆规则映射fix.apk的混淆规则等,以及按照base.apk的multidex的分包方式编译出fix.apk,最后产生差分的补丁patch.apk。

Tinker 中gradle插件的设计

tinker定义了5个task来完成patch.apk的产生过程。如下图是tinker插件的类图,


Android插件化与热修复(七)-微信Tinker资源加载、gradle插件分析_第4张图片
图片引自别处

TinkerManifestTask 主要是在AndroidManifest.xml中插入 meta-data TINKER_ID ,它是用来在客户端合并补丁的时候验证old.apk中的TINKER_ID是否一致。

@(工作-待办)TaskAction
    def updateManifest() {
            String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId
        if (tinkerValue == null || tinkerValue.isEmpty()) {
            throw new GradleException('tinkerId is not set!!!')
        }//校验gradle配置的tinkerId的值

        tinkerValue = TINKER_ID_PREFIX + tinkerValue

        writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)//像AndriodManifest.xml插入Tinkerid
        addApplicationToLoaderPattern()
        File manifestFile = new File(manifestPath)
        if (manifestFile.exists()) {//修改后的AndriodManifest.xml拷贝到app\build\intermediates\tinker_intermediates下
            FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
            project.logger.error("tinker gen AndroidManifest.xml in ${MANIFEST_XML}")
        }

    }

TinkerManifestTask首先根据读取gradle配置的tinkerid,校验是否合法。然后往AndroidManifest.xm插入tinkerid,并拷贝到app\build\intermediates\tinker_intermediates。

TinkerResourceIdTask根据gradle配置的applyResourceMapping文件,产生public.xml和idx.xml,使得new.apk在编译资源文件的时候,按照public.xml指定的id分配给资源

 def applyResourceId() {
        String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping
        if (!FileOperation.isLegalFile(resourceMappingFile)) {
            project.logger.error("apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
            return
        }

       project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true
       
        PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)

            FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML))

            FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML))
       }
    }

TinkerMultidexConfigTask 如果开启了multiDex 会在编译中根据gradle的配置和默认配置生成出要keep在main dex中的proguard信息文件,然后copy出这个文件,方便开发者使用multiDexKeepProguard进行配置.首先打开文件并写入默认配置.文件路径也在tinker_intermediates下

 @TaskAction
    def updateTinkerProguardConfig() {
        File file = project.file(MULTIDEX_CONFIG_PATH)
        print(file.getAbsolutePath())
        project.logger.error("try update tinker multidex keep proguard file with ${file}")

        // Create the directory if it doesn't exist already
        file.getParentFile().mkdirs()

        StringBuffer lines = new StringBuffer()
        lines.append("\n")
             .append("#tinker multidex keep patterns:\n")
             .append(MULTIDEX_CONFIG_SETTINGS)
             .append("\n")
             .append("#your dex.loader patterns here\n")

        Iterable loader = project.extensions.tinkerPatch.dex.loader
        for (String pattern : loader) {
            if (pattern.endsWith("*")) {
                if (!pattern.endsWith("**")) {
                    pattern += "*"
                }
            }
            lines.append("-keep class " + pattern + " {\n" +
                    "    *;\n" +
                    "}\n")
                    .append("\n")
        }

        // Write our recommended proguard settings to this file
        FileWriter fr = new FileWriter(file.path)
        try {
            for (String line : lines) {
                fr.write(line)
            }
        } finally {
            fr.close()
        }

        File multiDexKeepProguard = null
        try {
            multiDexKeepProguard = applicationVariant.getVariantData().getScope().getManifestKeepListProguardFile()
        } catch (Throwable ignore) {
            try {
                multiDexKeepProguard = applicationVariant.getVariantData().getScope().getManifestKeepListFile()
            } catch (Throwable e) {
                project.logger.error("can't find getManifestKeepListFile method, exception:${e}")
            }
        }
        if (multiDexKeepProguard == null) {
            project.logger.error("auto add multidex keep pattern fail, you can only copy ${file} to your own multiDex keep proguard file yourself.")
            return
        }
        FileWriter manifestWriter = new FileWriter(multiDexKeepProguard, true)
        try {
            for (String line : lines) {
                manifestWriter.write(line)
            }
        } finally {
            manifestWriter.close()
        }
    }

TinkerProguardConfigTask如果开启了混淆,就会在gradle插件中构建出该任务,主要的作用是将tinker中默认的混淆信息和基准包的mapping信息加入混淆列表,这样就可以通过gradle配置自动帮开发者做一些类的混淆设置,并且可以通过applymapping的基准包的mapping文件达到在混淆上补丁包和基准包一致的目的.首先打开在编译路径下的混淆文件,为后面写入默认的keep规则做准备.文件的路径同样在tinker_intermediates下

  @TaskAction
    def updateTinkerProguardConfig() {
        def file = project.file(PROGUARD_CONFIG_PATH)
        project.logger.error("try update tinker proguard file with ${file}")

        // Create the directory if it doesnt exist already
        file.getParentFile().mkdirs()

        // Write our recommended proguard settings to this file
        FileWriter fr = new FileWriter(file.path)

        String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

        //write applymapping
        if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
            project.logger.error("try add applymapping ${applyMappingFile} to build the package")
            fr.write("-applymapping " + applyMappingFile)
            fr.write("\n")
        } else {
            project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")
        }

        fr.write(PROGUARD_CONFIG_SETTINGS)

        fr.write("#your dex.loader patterns here\n")
        //they will removed when apply
        Iterable loader = project.extensions.tinkerPatch.dex.loader
        for (String pattern : loader) {
            if (pattern.endsWith("*") && !pattern.endsWith("**")) {
                pattern += "*"
            }
            fr.write("-keep class " + pattern)
            fr.write("\n")
        }
        fr.close()
        // Add this proguard settings file to the list
        applicationVariant.getBuildType().buildType.proguardFiles(file)
        def files = applicationVariant.getBuildType().buildType.getProguardFiles()

        project.logger.error("now proguard files is ${files}")
    }

TinkerPatchSchemaTask 负责校验Extensions的参数和环境是否合法和补丁生成

 @TaskAction
    def tinkerPatch() {
        configuration.checkParameter()
        configuration.buildConfig.checkParameter()
        configuration.res.checkParameter()
        configuration.dex.checkDexMode()
        configuration.sevenZip.resolveZipFinalPath()

        InputParam.Builder builder = new InputParam.Builder()
        if (configuration.useSign) {
            if (signConfig == null) {
                throw new GradleException("can't the get signConfig for this build")
            }
            builder.setSignFile(signConfig.storeFile)
                    .setKeypass(signConfig.keyPassword)
                    .setStorealias(signConfig.keyAlias)
                    .setStorepass(signConfig.storePassword)

        }

        builder.setOldApk(configuration.oldApk)
               .setNewApk(buildApkPath)
               .setOutBuilder(outputFolder)
               .setIgnoreWarning(configuration.ignoreWarning)
               .setDexFilePattern(new ArrayList(configuration.dex.pattern))
               .setDexLoaderPattern(new ArrayList(configuration.dex.loader))
               .setDexMode(configuration.dex.dexMode)
               .setSoFilePattern(new ArrayList(configuration.lib.pattern))
               .setResourceFilePattern(new ArrayList(configuration.res.pattern))
               .setResourceIgnoreChangePattern(new ArrayList(configuration.res.ignoreChange))
               .setResourceLargeModSize(configuration.res.largeModSize)
               .setUseApplyResource(configuration.buildConfig.usingResourceMapping)
               .setConfigFields(new HashMap(configuration.packageConfig.getFields()))
               .setSevenZipPath(configuration.sevenZip.path)
               .setUseSign(configuration.useSign)

        InputParam inputParam = builder.create()
        Runner.gradleRun(inputParam);
    }

总结

  • Tinker 和其他的类加载方式的修复方案相比,利用差分算法,最大限度的减小的补丁包的大小,这一点对于移动应用来说非常重要。
  • Tinker的修复包中不能引入新的四大组件,不能修改androidManifest文件。

你可能感兴趣的:(Android插件化与热修复(七)-微信Tinker资源加载、gradle插件分析)