ClassLoader和熱修復(fù)

Android源碼來自28.0.2

ClassLoader

參考Android工程師進(jìn)階 34講
1.每個(gè)ClassLoader加載的Class路徑不同,
2.ClassLoader加載class主要是通過loadClass方法

ClassLoader

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            //首先,先判斷自己是否曾經(jīng)加載過這個(gè)類,
            //如果曾經(jīng)加載過,直接返回之前加載的Class
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                //如果沒有加載過,就開始加載
                try {
                    if (parent != null) {
                        //如果有parent(ClassLoader)
                        //把這個(gè)class交給parent去加載
                        c = parent.loadClass(name, false);
                    } else {
                        //如果parent為空,說明當(dāng)前classloader是bootstrap class loader
                        //執(zhí)行findBootstrapClassOrNull方法,
                        //不過ClassLoader#findBootstrapClassOrNull方法默認(rèn)返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //  如果parent為空或者parent加載不了class
                    //那就自己加載
                    c = findClass(name);
                }
            }
            return c;
    }

PathClassLoader

image.png

從log可以看出,加載MainActivity的是PathClassLoader。
而PathClassLoader繼承自BaseDexClassLoader,BaseDexClassLoader繼承自ClassLoader
, PathClassLoader和BaseDexClassLoader都沒有重寫loadClass方法。PathClassLoader僅僅是重寫了兩個(gè)構(gòu)造方法。
所以PathClassLoader執(zhí)行l(wèi)oadClass的邏輯是:
1.PathClassLoader自己是否曾經(jīng)加載過目標(biāo)class,如果加載過,就直接返回。如果沒加載過,執(zhí)行步驟2
2.執(zhí)行BootClassLoader#loadClass. PathClassLoader的parent是BootClassLoader,不為空,所以交給執(zhí)行BootClassLoader#loadClass(步驟3)
3.在BootClassLoader#loadClass里,

    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        //同ClassLoader,也是先看自己是否曾經(jīng)親自加載過
        Class<?> clazz = findLoadedClass(className);
    
        if (clazz == null) {
            //如果沒有,就執(zhí)行Class.classForName去加載
            //而Class.classForName是native方法
            clazz = findClass(className);
        }

        return clazz;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

BootClassLoader#loadClass最后是使用java.lang.Class#classForName加載該class。平時(shí)我們調(diào)用Class.forName方法時(shí),最終也是走向了這個(gè)native方法。
到這里,ClassLoader的雙親委托就清楚了,ClassLoader雙親委托邏輯是:先交給parent(注意,這個(gè)parent并不是繼承的那個(gè)父類,而是設(shè)置進(jìn)來的另一個(gè)ClassLoader,單鏈表),如果parent不能加載class,那自己再加載,如果自己也不能加載,就返回null??梢岳斫馐菃捂湵斫M成的一串ClassLoader,每個(gè)ClassLoader里都有一個(gè)parent來指向上一個(gè)ClassLoader,如果一個(gè)ClassLoader的parent是null,那么,這個(gè)就是鏈表頭,每次ClassLoader加載class,它就先找parent,讓parent去加載,parent加載不了(返回null),自己才加載,如果自己也加載不了,就返回null。

如果這里返回null,則會(huì)執(zhí)行PathClassLoader的findClass方法,自己來加載class. PathClassLoader并沒有重寫findClass方法, 但是BaseDexClassLoader重寫了該方法,所以,PathClassLoader會(huì)執(zhí)行BaseDexClassLoader#findClass方法

BaseDexClassLoader#findClass

    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        //創(chuàng)建PathClassLoader的時(shí)候就會(huì)初始化pathList 
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //查找class的邏輯實(shí)際上交給了pathList
        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;
    }

從上面代碼可以看出,查找加載Class的邏輯實(shí)際上是由pathList完成的。

DexPathList

    private Element[] dexElements;
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        //---------------------省略
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        //---------------------省略
    }
    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;
    }

可以看到DexPathList#findClass的邏輯就是遍歷數(shù)組dexElements,而數(shù)組dexElements是在構(gòu)造方法中通過makeDexElements方法生成的。
而這個(gè)dexElements的內(nèi)容,可以打印BaseDexClassLoader#pathList內(nèi)容如下:


image.png

pathList的dexElements里只有一個(gè)元素,zip file "/data/app/com.houtrry.hotfixsample-2.apk",也就是當(dāng)前程序的apk文件。

熱修復(fù)

市場上的熱修復(fù)可以分為java層實(shí)現(xiàn)和native層實(shí)現(xiàn)。
native層:andfix sophix(不需要重啟app)
Java層:Tinker(需要重啟app)
本文討論的是Java層實(shí)現(xiàn)。

Java層實(shí)現(xiàn)主要有三種方案
1.在DexPathList的dexElements中插入dex
2.自定義ClassLoader加載dex
3.利用ClassLoader的雙親委托機(jī)制,把PathClassLoader的parent替換成自己的ClassLoader, 在這個(gè)ClassLoader中加載dex

一般來說,方案1和3比較常見

方案 在DexPathList的dexElements中插入dex

原理

DexPathList#findClass通過遍歷數(shù)組dexElements查找class,那么,是否可以把修復(fù)好的class文件放到dexElements中,并且放到dexElements中的apk前面呢?這樣,每次加載目標(biāo)class,都會(huì)先遍歷到處于前面的已經(jīng)修復(fù)了問題的dex,而有問題的dex在apk中,就沒有加載的機(jī)會(huì),從而實(shí)現(xiàn)熱修復(fù)。

  1. 而怎么把修復(fù)好的dex加到dexElements中呢?通過反射就可以實(shí)現(xiàn)。
  2. 那什么時(shí)候執(zhí)行這個(gè)邏輯呢?當(dāng)然是越早越好,因?yàn)榧虞d晚了,可能會(huì)出現(xiàn)問題class已被加載的情況,這種情況下,即使dexElements中修復(fù)好的dex位于前面也沒有機(jī)會(huì)執(zhí)行了,只能重啟app后才能生效。app中最早的應(yīng)該就是Application#attachBaseContext方法了,因此,我們?cè)谶@個(gè)方法里執(zhí)行dexElements的插入邏輯。
  3. dex的來源應(yīng)該是我們下載到本地的,下載完成后,app重啟進(jìn)入Application#attachBaseContext執(zhí)行dexElements的插入邏輯即可生效。
  4. dex的生成方法可以查看d8使用說明
    拿Utils.java舉例,生成dex主要有2步:
    ①javac Utils.java 生成Utils.class文件
    ②./d8 Utils.class 生成classes.dex文件,這個(gè)就是想要的dex文件了。
    注意:d8文件在\sdk\build-tools下,比如\sdk\build-tools\28.0.2\d8.bat;注意步驟②時(shí)d8的路徑。

實(shí)現(xiàn)

  1. 在Application#attachBaseContext中,通過反射,在dexElements中插入dex
    /**
     * 在ClassLoader中的dexElements數(shù)組中(數(shù)組0號(hào)位)插入我們自己的dex
     *
     * @param application
     */
    public static void preformHotFix(@NonNull Application application) {
        if (!hasDex(application)) {
            return;
        }
        try {
            //第一步:獲取當(dāng)前ClassLoader中的dexElements(dexElementsOld)
            ClassLoader classLoader = application.getClassLoader();
            Class<?> clsBaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = clsBaseDexClassLoader.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(classLoader);
            Class<?> clsDexPathList = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = clsDexPathList.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] dexElementsOld = (Object[]) dexElementsField.get(pathList);
            System.out.println("dexElementsOld: " + dexElementsOld);

            int sizeOfOldDexElement = dexElementsOld.length;
            List<File> dexFileList = getDexFileList(getDexDir(application));

            Method[] declaredMethods = clsDexPathList.getDeclaredMethods();
            for (Method method :
                    declaredMethods) {
                System.out.println("method: " + method);
            }

            //第二步:生成包含hot fix dex文件的dexElements(dexElementsNew)
            //當(dāng)然,也可以用另外一種方式: new 一個(gè)PathClassLoader加載dex文件夾下的dex,
            //                          然后反射獲取到這個(gè)PathClassLoader中dexElements的值,
            //                          也就是我們這里需要的,反射邏輯可以參考第一步
            //PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
            //注意:makeDexElements在不同版本中可能會(huì)有變化,注意log提示,做好兼容, 這里只是測(cè)試.
            //      具體的兼容邏輯可以參考騰訊tinker的com.tencent.tinker.loader.SystemClassLoaderAdder#installDexes
            Method makeDexElementsMethod = clsDexPathList.getDeclaredMethod("makeDexElements",
                    List.class, File.class, List.class, ClassLoader.class);
            makeDexElementsMethod.setAccessible(true);
            Object[] dexElementsNew = (Object[]) makeDexElementsMethod.invoke(null, dexFileList, null,
                    new ArrayList<IOException>(), classLoader);
            int sizeOfNewDexElement = dexElementsNew.length;
            System.out.println("sizeOfNewDexElement: " + sizeOfNewDexElement + ", sizeOfOldDexElement: " + sizeOfOldDexElement);
            if (sizeOfNewDexElement == 0) {
                return;
            }
            //第三步:合并兩個(gè)dexElements
            //注意:dexElementsNew中的元素需要放到dexElementsOld元素的前面
            //數(shù)組拷貝邏輯可以參考DexPathList#addDexPath方法
//            Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];
            //注意:這里不要像直接像上面那樣直接new Object[]數(shù)組,而是使用Array.newInstance方法(參考自tinker的com.tencent.tinker.loader.shareutil.ShareReflectUtil#expandFieldArray)
            //直接new Object[]數(shù)組的話,在執(zhí)行下面dexElementsField.set的時(shí)候會(huì)報(bào)錯(cuò)java.lang.RuntimeException: Unable to instantiate application com.houtrry.hotfixsample.HotFixApplication: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
            Object[] dexElements = (Object[]) Array.newInstance(dexElementsOld.getClass().getComponentType(), sizeOfNewDexElement + sizeOfOldDexElement);

            System.arraycopy(dexElementsNew, 0, dexElements, 0, sizeOfNewDexElement);
            System.arraycopy(dexElementsOld, 0, dexElements, sizeOfNewDexElement, sizeOfOldDexElement);
            System.out.println("dexElements: " + dexElements);
            //第四步:替換dexElements
            dexElementsField.setAccessible(true);
            dexElementsField.set(pathList, dexElements);
            System.out.println("pathList: " + pathList);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

日志中可以看到,DexPathList中有hotFixCopy.dex和base.apk,且hotFixCopy.dex在base.apk前面,也即是期望效果。

注意:

  1. 不同Android版本中makeDexElements可能會(huì)稍有不同(主要是參數(shù)不同),因此,需要考慮兼容
  2. 獲取dexElements可以不通過反射makeDexElements的方式,通過new PathClassLoader(dexPath, null),把生成dexElements的邏輯交給PathClassLoader,然后反射獲取PathClassLoader中的dexElements即可獲取
  3. 創(chuàng)建合并后DexElement數(shù)組容器時(shí),如果使用new Object[]的方式
Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];

會(huì)在執(zhí)行

dexElementsField.set(pathList, dexElements);

替換dexElementsField值的時(shí)候報(bào)錯(cuò),異常信息如下

2020-05-03 19:51:02.099 3507-3507/com.houtrry.hotfixsample E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.houtrry.hotfixsample, PID: 3507
    java.lang.RuntimeException: Unable to instantiate application com.houtrry.hotfixsample.HotFixApplication: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
        at android.app.LoadedApk.makeApplication(LoadedApk.java:802)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5377)
        at android.app.ActivityThread.-wrap2(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
     Caused by: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
        at java.lang.reflect.Field.set(Native Method)
        at com.houtrry.hotfixsample.HotFixManager.preformHotFix(HotFixManager.java:97)
        at com.houtrry.hotfixsample.HotFixApplication.attachBaseContext(HotFixApplication.java:17)
        at android.app.Application.attach(Application.java:189)
        at android.app.Instrumentation.newApplication(Instrumentation.java:1008)
        at android.app.Instrumentation.newApplication(Instrumentation.java:992)
        at android.app.LoadedApk.makeApplication(LoadedApk.java:796)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5377) 
        at android.app.ActivityThread.-wrap2(ActivityThread.java) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:154) 
        at android.app.ActivityThread.main(ActivityThread.java:6119) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 

方案 把PathClassLoader的parent替換成自己的ClassLoader

原理

利用雙親委托機(jī)制,給當(dāng)前parent換成可以加載自己dex的ClassLoader。
原本的ClassLoader路徑:PathClassLoader==>BootClassLoader
替換后的ClassLoader路徑:PathClassLoader==>自定義ClassLoader==>BootClassLoader

實(shí)現(xiàn)

    /**
     * 給ClassLoader安排一個(gè)新的parent
     * 在這個(gè)新parent中加載我們自己的dex
     * 簡單理解就是在單鏈表中間插入第三個(gè)元素
     *
     * @param application
     */
    public static void preformHotFix2(@NonNull Application application) {
        if (!hasDex(application)) {
            return;
        }
        try {
            ClassLoader classLoader = application.getClassLoader();
            //第一步:反射獲取到當(dāng)前ClassLoader的parent
            Class<?> clsBaseDexClassLoader = Class.forName("java.lang.ClassLoader");
            Field parent = clsBaseDexClassLoader.getDeclaredField("parent");
            parent.setAccessible(true);
            //第二步:創(chuàng)建新的PathClassLoader
            // 這個(gè)PathClassLoader的parent是當(dāng)前CLassLoader的parent
            //path指向我們dex文件夾下的dex文件
            ClassLoader classLoaderParent = classLoader.getParent();
            PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
            //第三步:把classLoaderParent作為當(dāng)前classLoader的parent
            //這樣,根據(jù)雙親委托機(jī)制,當(dāng)前ClassLoader加載class的時(shí)候,
            // 會(huì)將class交給它的parent(也就是我們創(chuàng)建的pathClassLoader來加載)
            //如果我們的pathClassLoader可以加載這個(gè)class(意味著該class能在dex中找到,也就是我們需要修復(fù)的class)
            //這樣系統(tǒng)的ClassLoader就沒有機(jī)會(huì)加載有問題的class,問題得到修復(fù)
            parent.set(classLoader, pathClassLoader);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

這里,我們創(chuàng)建PathClassLoader作為自定義parent。
PathClassLoader的第一個(gè)參數(shù):字符串,dex路徑。文件可以是dex/apk/zip。多個(gè)文件字符串之間用“:”分號(hào)隔開
PathClassLoader的第二個(gè)參數(shù):ClassLoader,也就是指定ClassLoader的parent。這里我們用默認(rèn)ClassLoader的parent作為自定義ClassLoader的parent。

執(zhí)行后日志如下


可以看到,當(dāng)前ClassLoader還是原來的PathClassLoader,加載的dex是base.apk。
parent是我們自定義的ClassLoader,其dex正是我們期望的hotFixCopy.dex。
parent的parent是BootClassLoader,也正是沒改之前的PathClassLoader的parent。
demo地址HotFixSample

tinker源碼分析

//TODO 待完善

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容