tinker热更新原理

什么是 Tinker?

Tinker 是一个开源项目(Github链接),它是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。

热更新方案比较

当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。
tinker热更新原理_第1张图片

1、AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;

2、Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;

3、Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。

特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?

Tinker的已知问题

由于原理与系统限制,Tinker有以下已知问题:

1、Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity)

2、由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
在Android N上,补丁对应用启动时间有轻微的影响;

3、不支持部分三星android-21机型,加载补丁时会主动出"TinkerRuntimeException:checkDexInstall failed";

4、对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

Android类动态加载机制

要了解tinker热更新原理就要先了解Android的类加载流程Android中虚拟机类加载流程图如下:

tinker热更新原理_第2张图片
DexClassLoader 和 PathClassLoader

在Android中,ClassLoader是一个抽象类,实际开发过程中,一般是使用其具体的子类DexClassLoader、PathClassLoader这些类加载器来加载类的,不同之处是:

1、PathClassLoader:支持加载DEX或者已经安装的APK(因为存在缓存的DEX)

2、DexClassLoader:支持加载APK、DEX和JAR,也可以从SD卡进行加载。

这2个类都继承于BaseDexClassLoader, BaseDexClassLoader继承于ClassLoader。

DexClassLoader的构造方法:

public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

PathClassLoader的构造方法:

public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }

BaseDexClassLoader的构造方法:

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

DexPathList的loadDexFile方法

private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }

private static String optimizedPathFor(File path,File optimizedDirectory) {
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

由上可知:
1、optimizedDirectory是用来缓存需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile对象。

2、optimizedDirectory必须是一个内部存储路径,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory。

3、PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。

Tinker热更新原理

首先给出tinker官方的热更新原理图:
tinker热更新原理_第3张图片
由上图可以看出tinker的主要流程是:

1、通过生成的fix.dex,也就是修复包的dex文件与base.dex也就是已经发布出去的需要修复的dex文件进行一个对比,生成patch.dex文件。

2、然后通过patch.dex文件与classes.dex文件合并生成新的fix_classes.dex文件代替掉原来的classes.dex文件。

3、将合成后的全量dex 插入到dex elements前面,完成修复

tinker热更新流程图
tinker热更新原理_第4张图片
下面看下tinker加载的源码

  public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
        Intent resultIntent = new Intent();
        long begin = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent); //在tryLoadPatchFilesInternal中首先要进行环境校验,完成校验流程后再加载补丁,校验的详细内容不展开讨论
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

tinker中的类加载器TinkerDexLoader

 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {

        PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
        String dexPath = directory + "/" + DEX_PATH + "/";
        File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);
        ArrayList<File> legalFiles = new ArrayList<>();
        final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
        // 获取合法的文件列表
        for (ShareDexDiffPatchInfo info : dexList) {
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }
            String path = dexPath + info.realName;
            File file = new File(path);
            }
            legalFiles.add(file);
        }
//isSystemOTA判断,如果用户是ART环境并且做了OTA升级,加载dex补丁的时候首先将最近一次的补丁全部DexFile.loadDex一遍.之所以这样做是因为有些场景做了OTA后,OTA的规则可能发生变化,在这种情况下去加载上个系统版本oat过的dex就会出现问题.
        if (isSystemOTA) {
            parallelOTAResult = true;
            parallelOTAThrowable = null;
            Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");

            TinkerParallelDexOptimizer.optimizeAll(
                legalFiles, optimizeDir,
                new TinkerParallelDexOptimizer.ResultCallback() {
                    long start;

                    @Override
                    public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
                        // Do nothing.
                        Log.i(TAG, "success to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
                    }
                    @Override
                    public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                        parallelOTAResult = false;
                        parallelOTAThrowable = thr;
                        Log.i(TAG, "fail to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
                    }
                }
            );          
               intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, parallelOTAThrowable);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_PARALLEL_DEX_OPT_EXCEPTION);
                return false;
            }
        }
        try {
            //接下来就是调用SystemClassLoaderAdder的installDexes方法
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
        } catch (Throwable e) {
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
            return false;
        }
        return true;
    }

加载patch.dex过程,在tinker中针对不同的版本有不同的加载代码下面是版本23的加载代码:

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader); //通过反射拿到classloader的patchlist变量
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // 通过反射获取pathList的dexElements参数,把经过合并后的DexElements设置为pathList的dexElements。
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));
        }


        private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

            Method makePathElements;
            try {
            //反射pathList的makeDexElements方法,传入插件补丁dexList路径与优化过的opt目录,通过这个方法生成一个新的DexElements,这个DexElements为插件的DexElements。
                makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
                    List.class);
            } catch (NoSuchMethodException e) {
                Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
                try {
                    makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
                } catch (NoSuchMethodException e1) {
                    Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
                    try {
                        Log.e(TAG, "NoSuchMethodException: try use v19 instead");
                        return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
                    } catch (NoSuchMethodException e2) {
                        Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
                        throw e2;
                    }
                }
            }

            return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
        }
    }

上面代码主要是:

通过反射获取到patchlist的dexElements字段,然后将合成补丁后的dex文件插入到dexelements数组的前面,这样打了补丁后的dex就可以先记载到从而完成修复工作。

参考文献

1、http://www.tinkerpatch.com/Docs/intro
2、https://github.com/Tencent/tinker
3、https://www.jianshu.com/p/2216554d3291

你可能感兴趣的:(Android)