拆輪子之Tinker熱修復

前言

最近公司打算本身在Tinker的基礎上作二次開發,由於TinkerPatch是要收費的,並且價格不菲,說白了就是想白嫖!android

順便研究了下的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()裏面,以下:bash

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 及其以上的設備:app

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() 中增長以下代碼:ide

@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 目錄下,殺掉進程,重啓就會生效。ui

相關文章
相關標籤/搜索