拆轮子之Tinker热修复

538 阅读2分钟

前言

最近公司打算自己在Tinker的基础上做二次开发,因为TinkerPatch是要收费的,而且价格不菲,说白了就是想白嫖!

顺便研究了下的Tineker类修复的原理,本文只涉及类的热修复,至于资源以及so库的热修复有时间再研究吧!

正文

在进入主题之前我们先来了解下Android的 ClassLoder,android 的 ClassLoader 主要又以下几种:PathClassLoader、DexClassLoader等,我们在Application中通过getClassLoader()得知ClassLode是 PathClassLoader ,Tinker就是通过处理PathClassLoader来实现类的热修复。先来看看 PathClassLoader 源码,PathClassLoader是继承BaseDexClassLoader重要的操作都在里面,ClassLoader 重要的方法就是findClass,如下:

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

可以看到,最终的逻辑在DexPathList的findClass()里面,如下:

 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

通过遍历dexElements执行 Element 的 findClass() ,只要找到就返回。Element可以理解为一个dex文件的表示,Tinker就是通过发射修改 dexElements 这个 Element 数组,将补丁里面的dex对应的 Element 插入到 dexElements 前面来实现类的热修复;比如 patch.dex 里面有 A.class ,原来的旧的apk里面也有 A.class ,由于 patch.dex 对应的 Element 在前面,每次调用 A.class 的方法时都是走的 patch.dex 里面的逻辑。以下是 Tinker 里扒下来的关键代码,做了部分删减,这个是 android6.0 及其以上的设备:

public class SystemClassLoaderAdder {
    private static final String TAG = "SystemClassLoaderAdder";

    public static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                               File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.BaseDexClassLoader. We modify its
         * dalvik.system.DexPathList pathList field to append additional DEX
         * file entries.
         */
        Log.d(TAG, "ClassLoader =" + loader.getClass().getCanonicalName());
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makePathElement", e);
                throw e;
            }

        }
    }

    /**
     * A wrapper around
     * {@code private static final dalvik.system.DexPathList#makePathElements}.
     */
    private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        Method makePathElements = null;
        try {
            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");

            }
        }

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

在 HotFixApplication 的 attachBaseContext() 中增加如下代码:

 @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        List<File> fileList = new ArrayList<>(1);
        File patchFile = new File(getCacheDir().getAbsolutePath() + "/classes.dex");
        if (patchFile.exists()) {
            fileList.add(patchFile);
        }
        patchFile = new File(getCacheDir().getAbsolutePath() + "/classes2.dex");
        if (patchFile.exists()) {
            fileList.add(patchFile);
        }
        if (fileList.size() > 0) {
            try {
                SystemClassLoaderAdder.install(getClassLoader(), fileList, new File(getDataDir().getAbsolutePath() + "/odex"));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

在classes.dex、classes2.dex是修改之后的apk解压之后出来,这里为了图方便,直接把补丁的 dex 放在 cache 目录下,杀掉进程,重启就会生效。