tinker热修复——资源补丁加载过程

通过上一篇文章《tinker热修复——dex补丁加载过程》,基本上我们已经熟悉了tinker加载dex的过程,tinker除了能够新增类,还可以新增资源和so库等,那么tinker是如何做到热更资源的呢?资源补丁的加载过程又是怎么样的呢?

我们都知道,tinker打补丁包的时候,只会打diff的补丁包,也就说补丁包中包括资源的diff,而生成资源的diff的时候,会把变化的索引写在补丁包的assets/res_meta.txt中,当补丁下发到app后,会将所有的资源整合起来生成一个resources.apk(该资源包,包含新增的资源和原来的资源),在加载资源补丁时候就是加载这个包含所有资源的包,然后将LoadApk中的资源路径替换成补丁的路径,并将Resources容器中的属性都替换成新的加载补丁的AssertManager。那么接下来我们看看tinker是如何一步步加载资源补丁的。

《tinker热修复——dex补丁加载过程》文章中知道,最终调用的加载过程都是在TinkerLoader的tryLoadPatchFilesInternal方法中的。那么我们重点关注该方法,我们注意到经过对补丁包的一层层的安全校验,检查资源补丁的时候,会调用TinkerResourceLoader的checkComplete方法。

 /**
     * resource file exist?
     * fast check, only check whether exist
     *
     * @param directory
     * @return boolean
     */
    public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
        //拿到补丁包中res_meta.txt的信息
        String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
        //not found resource
        if (meta == null) {
            return true;
        }
        /**
         * only parse first line for faster
         * 将meta中第一行的数据读取到resPatchInfo中用来快速校验.
         */
        ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);

        if (resPatchInfo.resArscMd5 == null) {
            return true;
        }
        //checkResPatchInfo:检查asrc文件的MD5本身是否为空或者长度是否合法
        if (!ShareResPatchInfo.checkResPatchInfo(resPatchInfo)) {
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
            return false;
        }
        /**
         * 校验合成资源补丁的路径和文件是否存在
         */

        //合成资源补丁的路径
        String resourcePath = directory + "/" + RESOURCE_PATH + "/";

        File resourceDir = new File(resourcePath);

        if (!resourceDir.exists() || !resourceDir.isDirectory()) {
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_DIRECTORY_NOT_EXIST);
            return false;
        }
        //合成资源补丁的文件
        File resourceFile = new File(resourcePath + RESOURCE_FILE);
        if (!SharePatchFileUtil.isLegalFile(resourceFile)) {
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_FILE_NOT_EXIST);
            return false;
        }
        try {
            TinkerResourcePatcher.isResourceCanPatch(context);
        } catch (Throwable e) {
            Log.e(TAG, "resource hook check failed.", e);
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
            return false;
        }
        return true;
    }
复制代码

该方法首先会读取res_meta.txt的信息,并把meta中的第一行的数据读取到resPatchInfo中,做一个快速检验,然后检查文件的md5是否有效,以及检查资源补丁的文件是否有效,最后调用TinkerResourcePatcher的isResourceCanPatch方法。

/**
     * 获取资源更新时需要的method和Field
     * 并且保存起来,方便补丁加载时使用
     * @param context
     * @throws Throwable
     */
    public static void isResourceCanPatch(Context context) throws Throwable {
        //   - Replace mResDir to point to the external resource file instead of the .apk. This is
        //     used as the asset path for new Resources objects.
        //   - Set Application#mLoadedApk to the found LoadedApk instance

        /**
         *
         * Find the ActivityThread instance for the current thread
         * 获取当前线程的ActivityThread对象
         */
        Class<?> activityThread = Class.forName("android.app.ActivityThread");
        currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread);

        /**
         *
         * API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
         * LoadedApk是从Android 2.3开始才有的
         * 该类是对之前系统ActivityThread的内部类PackageInfo重新封装而成的.所以这里要分开处理.
         */
        Class<?> loadedApkClass;
        try {
            loadedApkClass = Class.forName("android.app.LoadedApk");
        } catch (ClassNotFoundException e) {
            loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
        }

        //获取loadedApkClass的mResDir属性,设置成可访问
        resDir = loadedApkClass.getDeclaredField("mResDir");
        resDir.setAccessible(true);
        //获取activityThread的mPackages属性,设置成可访问
        packagesFiled = activityThread.getDeclaredField("mPackages");
        packagesFiled.setAccessible(true);
        //获取activityThread的mResourcePackages属性,设置可访问
        resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages");
        resourcePackagesFiled.setAccessible(true);

        /**
         *
         * Create a new AssetManager instance and point it to the resources
         * 创建一个AssetManager实例
         * 一些ROM修改了类名,如Baidu的ROM改成了android.content.res.BaiduAssetManager
         * 所以要做兼容
         */
        AssetManager assets = context.getAssets();
        // Baidu os
        if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) {
            Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager");
            newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance();
        } else {
            newAssetManager = AssetManager.class.getConstructor().newInstance();
        }

        //获取AssetManager的addAssetPath方法,设置成可访问
        addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        addAssetPathMethod.setAccessible(true);

        /**
         *
         * Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
         * in L, so we do it unconditionally.
         * 在Kitkat需要调用AssetManager的ensureStringBlocks方法
         * 在Lollipop则不需要调用
         * 但是不用区分系统,照常调用,不会造成任何问题
         */
        ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        ensureStringBlocksMethod.setAccessible(true);
        /**
         * 加载补丁要替换的Resources对象在KITKAT以下是以HashMap的类型作为ActivityThread类的属性.
         * 其余的系统版本都是以ArrayMap被ResourcesManager持有的.
         * 需要做系统区分
         */
        // Iterate over all known Resources objects
        //获取所有的Resources对象的引用
        if (SDK_INT >= KITKAT) {
            //pre-N
            // Find the singleton instance of ResourcesManager
            Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
            mGetInstance.setAccessible(true);
            //调用getInstance方法,拿到ResourcesManager对象
            Object resourcesManager = mGetInstance.invoke(null);
            try {
                //获取ResourcesManager的mActiveResources对象,设置可访问
                Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                //获取得到所有Resources对象的引用
                ArrayMap<?, WeakReference<Resources>> activeResources19 =
                    (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
                references = activeResources19.values();
            } catch (NoSuchFieldException ignore) {
                // N moved the resources to mResourceReferences
                Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                mResourceReferences.setAccessible(true);
                references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
            }
        } else {
            Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
            fMActiveResources.setAccessible(true);
            HashMap<?, WeakReference<Resources>> activeResources7 =
                (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread);
            references = activeResources7.values();
        }
        // check resource,校验是否为null
        if (references == null) {
            throw new IllegalStateException("resource references is null");
        }
        // fix jianGuo pro has private field 'mAssets' with Resource
        // try use mResourcesImpl first
        //将Resources对象中持有AssetManager对象引用的属性mAssets,hook出来
        //mAssets是加载补丁时进行替换的
        if (SDK_INT >= 24) {
            //Android N的路径变成了Resources -> ResourcesImpl -> AssetManager
            try {
                // N moved the mAssets inside an mResourcesImpl field
                resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl");
                resourcesImplFiled.setAccessible(true);
            } catch (Throwable ignore) {
                // for safety
                assetsFiled = Resources.class.getDeclaredField("mAssets");
                assetsFiled.setAccessible(true);
            }
        } else {
            assetsFiled = Resources.class.getDeclaredField("mAssets");
            assetsFiled.setAccessible(true);
        }
//        final Resources resources = context.getResources();
//        isMiuiSystem = resources != null && MIUI_RESOURCE_CLASSNAME.equals(resources.getClass().getName());

        try {
            publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir");
        } catch (NoSuchFieldException ignore) {
        }
    }
复制代码

该方法就是为资源补丁的替换做准备的工作,并且把这些准备的工作保存起来,等到替换的时候用。首先获取ActivityThread和LoadedApk,通过反射获取LoadedApk的mResDir属性,该属性需要设置补丁包的路径。通过ActivityThread获取mPackages和mResourcePackages属性,然后创建新的AssetManager同时通过反射获取AssetManager的addAssetPath方法(通过addAssetPath可以将补丁加载到新的AssetManager中)。

Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
            mGetInstance.setAccessible(true);
            //调用getInstance方法,拿到ResourcesManager对象
            Object resourcesManager = mGetInstance.invoke(null);
复制代码

通过反射调用getInstance方法,拿到ResourcesManager对象,因为该对象持有Resources容器对象,因此通过反射就可以拿到所有的Resources对象,拿到Resources对象就可以将其属性替换为新创建的AssetManager。

这里就是通过反射拿到Resources对象中持有AssetManager对象引用的属性mAssets,而mAssets是加载补丁时需要进行替换的。

publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir");
复制代码

最后获取ApplicationInfo的属性publicSourceDir是了解决Android的webview引起的问题,在Android N上,如果一个活动包含一个webview,当屏幕旋转时,资源补丁可能会失去效果。

当全部校验通过,并且为资源补丁更新做好准备之后,调用TinkerResourceLoader的loadTinkerResources方法进行资源补丁更新。

loadTinkerResources:

/**
     * Load tinker resources
     */
    public static boolean loadTinkerResources(TinkerApplication application, 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 (application.isTinkerLoadVerifyFlag()) {
            //校验文件的md5
            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(application, 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(application.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;
    }
复制代码

该方法首先检查资源文件是否有更新,然后找到资源文件路径,并检查md5是否校验通过,再进一步调用TinkerResourcePatcher的monkeyPatchExistingResources方法来加载资源。

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

        for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
            //获取当前线程的ActivityThread对象
            Object value = field.get(currentActivityThread);

            for (Map.Entry<String, WeakReference<?>> entry
                : ((Map<String, WeakReference<?>>) value).entrySet()) {
                //获取LoadedApk容器的对象
                Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                if (externalResourceFile != null) {
                    //将LoadedApk对象的mResDir属性的值替换成资源补丁包的路径
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }
        /**
         *
         * Create a new AssetManager instance and point it to the resources installed under
         * 通过addAssetPath方法,将补丁加载到新的AssetManager中
         */
        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);

        //遍历ResourcesManager持有的Resources容器对象
        for (WeakReference<Resources> wr : references) {
            Resources resources = wr.get();
            //pre-N
            if (resources != null) {
                // Set the AssetManager of the Resources instance to our brand new one
                try {
                    //替换Resources的属性assetsFiled为新的AssetManager
                    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对象的配置.
                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.
        // for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
        if (Build.VERSION.SDK_INT >= 24) {
            try {
                if (publicSourceDirField != null) {
                    publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
                }
            } catch (Throwable ignore) {
            }
        }

        if (!checkResUpdate(context)) {
            throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
        }
    }
复制代码

1、获取当前线程的ActivityThread对象和该对象持有的LoadedApk容器;

2、遍历容器中的LoadedApk对象,替换LoadedApk的mResDir属性为补丁物理路径;

3、通过addAssetPath方法,将补丁加载到新创建的AssetManager中;

4、遍历ResourcesManager持有的Resources容器对象;

5、替换Resources的属性assetsFiled为新的AssetManager;

6、根据原属性重新更新Resources对象的配置;

7、调用checkResUpdate方法,补丁生效校验及卸载;

8、更加详细说明请看文章的代码的注释。

以上就是tinker的资源补丁加载的全过程,本人也是刚刚涉及热修复的,如果哪里讲的不够明白,也可以参考《Android 热修复方案Tinker(四) 资源补丁加载》,该文章讲得更加详细,思路也非常清晰。

参考文章:《Android 热修复方案Tinker(四) 资源补丁加载》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值