Android-類加載和熱修復(fù)

當(dāng)我們開發(fā)完一個(gè)APP,打包成了apk裝進(jìn)了手機(jī),然后啟動(dòng)和使用APP,這一個(gè)過程中,必定會(huì)使用各種的類和方法,有系統(tǒng)的有自己的,那這些類都是如何加載成功,供我們使用的呢?
例如有一個(gè)A類,我們使用的時(shí)候,一般是new A()創(chuàng)建個(gè)對(duì)象,然后使用,到底是怎么new的?其實(shí)就是Android的類加載幫我們做的。

apk的組成

懂a(chǎn)pk的打包流程或者反編譯過apk的都知道,apk里面會(huì)有一個(gè)或者多個(gè)的dex文件,這些都是我們的代碼,但是是經(jīng)過處理的代碼,把我們的代碼轉(zhuǎn)化成電腦能懂的代碼,其實(shí)就是字節(jié)碼。

安裝apk

這里需要分一下版本,不同的版本安裝機(jī)制有點(diǎn)區(qū)別。
Android N(7.0)以上的:
安裝apk的時(shí)候,不進(jìn)行任何的預(yù)編譯(提高安裝速度);
運(yùn)行的過程中解析執(zhí)行,并且對(duì)經(jīng)常使用的方法進(jìn)行優(yōu)化,就是即時(shí)編譯(JIT just in time),經(jīng)過JIT處理的代碼,都會(huì)記錄在一個(gè)profile配置文件里;
最后在手機(jī)閑的時(shí)候,有一個(gè)編譯守護(hù)進(jìn)程,會(huì)對(duì)profile里面的方法進(jìn)行預(yù)先編譯(AOT),把這些代碼轉(zhuǎn)化為本地機(jī)器碼。
Android L(5.0)- Android N:
安裝時(shí)直接使用預(yù)先編譯(AOT),就是把所有代碼一次性轉(zhuǎn)化為本地機(jī)器碼,當(dāng)需要使用時(shí)就可以直接使用了。但是缺點(diǎn)就是第一次安裝的時(shí)候十分的慢,因?yàn)橐淮涡赞D(zhuǎn)化全部代碼很耗時(shí)。
Android 2.2-4.4:這部分都是使用JIT,就是說都是在運(yùn)行的時(shí)候,需要用什么,就加載什么,好處就是安裝賊快,但缺點(diǎn)也明顯,每次運(yùn)行都需要重新編譯,浪費(fèi)資源,例如電量。

關(guān)鍵類:ClassLoader
image.png

我們先了解下類加載的所有類關(guān)系。
①BootClassLoader:用來加載系統(tǒng)framework層的class文件。
②BaseDexClassLoader:衍生出PathClassLoader和DexClassLoader。
③ PathClassLoader:Android應(yīng)用的類加載器,也就是我們寫的類,都由這個(gè)來加載。
④DexClassLoader:這個(gè)是加載一些額外的動(dòng)態(tài)類。

類加載

安裝成功了,打開APP,每當(dāng)我們使用類,創(chuàng)建對(duì)象的時(shí)候,類加載器都會(huì)幫我們從代碼里面找出我們要的那個(gè)類,然后加載給我們用。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        //交給父類加載器去加載 遞歸
                        c = parent.loadClass(name, false);
                    } else {
                        //這里是如果父類加載器是null(也就是bootstrap),那這個(gè)方法會(huì)去查找name指向的這個(gè)類是不是由bootstrap加載了,是的話就返回class對(duì)象,不是的話返回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.
                    c = findClass(name);
                }
            }
            return c;
    }

可以看到,加載一個(gè)類,會(huì)分為2步
1、Class<?> c = findLoadedClass(name);找緩存,如果已經(jīng)被加載過了,就直接return返回。
2、如果沒有緩存,就根據(jù)變量parent是否為null來判斷邏輯,如果不為null,就直接調(diào)用parent的loadClass方法;如果為null,就調(diào)用findBootStrapClassOrNull方法。

這里值得注意的是這個(gè)parent,其實(shí)這里使用了雙親委托機(jī)制(先把任務(wù)交給父類去處理,直到?jīng)]有父類或者父類處理不了,才自己去嘗試處理),我們的類需要加載的時(shí)候,是由pathClassLoader處理的,但是!這里雙親委托說的父類,指的不是BaseDexClassLoader,而是BootClassLoader。

所以這里的邏輯理解應(yīng)該是:
先判斷當(dāng)前加載器是否有父類,沒有就從Bootstrap里面找,如果沒有加載過就自己去執(zhí)行findClass方法去加載。
如果有父類,就根據(jù)雙親委托機(jī)制,遞歸加載,如果都沒有加載過,最后也是交給自己去執(zhí)行findClass。

那findClass又做了什么?

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

ClassLoader中的findClass其實(shí)是空實(shí)現(xiàn),也就是說實(shí)現(xiàn)交給了子類去實(shí)現(xiàn)。那再找BaseDexClassLoader中的。

    @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;
    }

重點(diǎn)就這一句:Class c = pathList.findClass(name, suppressedExceptions);
從變量pathList中,根據(jù)name來findClass,并且把結(jié)果return。那pathList是什么?先看定義

private final DexPathList pathList;

再看賦值

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }

可以看到,是一個(gè)DexPathList的對(duì)象,而且是在BaseDexClassLoader的構(gòu)造函數(shù)里賦值的,而構(gòu)造函數(shù)中的形參dexPath,其實(shí)就是dex文件的路徑。dex文件是什么?就是開頭講的apk里面的我們寫的代碼。繼續(xù)看DexPathList的實(shí)現(xiàn):

    public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
        this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
    }

    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
       ···省略部分代碼···

        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);

        ···省略部分代碼···
    }

通過makeDexElements,把dexPath路徑上的文件拆分,變成一個(gè)Element數(shù)組。

private Element[] dexElements;

也就是說,DexPathList類型的pathList對(duì)象里面,有一個(gè)dexElements數(shù)組,存放的就是我們dex文件里面的所有代碼的類。那再看回pathList的findClass方法:

//DexPathList類:

    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;
    }

//Element類
        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

遍歷Element數(shù)組,再根據(jù)Element對(duì)象內(nèi)的dexFile和loadClassBinaryName得到的Class,就可以用了,也就是說,我們new的對(duì)象,已經(jīng)成功了。

雙親委托的作用

第一、避免了重復(fù)加載類,交由父類先處理,可以知道是否被加載過。
第二、為了安全,因?yàn)楦鞣N各樣的對(duì)象都有類加載器來加載,有系統(tǒng)的,有我們自己的,而系統(tǒng)的必須優(yōu)先度高并且不可改,不然就會(huì)有安全性問題。所以父類加載完系統(tǒng)對(duì)象,就算我們?cè)诖a里自己寫一個(gè)去修改實(shí)現(xiàn),也是沒用。

APP熱修復(fù)

通過上面的知識(shí)點(diǎn),了解到了一個(gè)apk是怎么安裝并啟動(dòng)加載到ART虛擬機(jī)里面的了。既然類的加載,是一個(gè)Element數(shù)組的遍歷,而Element存放的又是dexFile。
那也就是說:如果APP某個(gè)類有bug,我們只需要修復(fù)這個(gè)類的bug,然后生成一個(gè)dex文件,用戶下載后,根據(jù)邏輯把這個(gè)dex文件放在Element數(shù)組的第一位,那么根據(jù)類加載的邏輯,修復(fù)后的類會(huì)先加載,而后面有bug的類,由于類名一樣,所以就不會(huì)再加載了,達(dá)到了問題被修復(fù),而不需要重新發(fā)包的目的。

簡單例子實(shí)現(xiàn)

增加入口,確保第一時(shí)間把這個(gè)新的類被加載器加載,不然由于同名的原因,如果另一個(gè)同名的類先加載了,那這個(gè)就無法修復(fù)了。

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //執(zhí)行熱修復(fù)。 插入補(bǔ)丁dex
        Hotfix.installPatch(this,new File("/sdcard/bugFix.dex"));
    }
}

根據(jù)版本,分別處理

    public static void installPatch(Application application, File patch) {
        //1、獲得classloader,PathClassLoader
        ClassLoader classLoader = application.getClassLoader();

        List<File> files = new ArrayList<>();
        if (patch.exists()) {
            files.add(patch);
        }
        File dexOptDir = application.getCacheDir();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            try {
                NewClassLoaderInjector.inject(application, classLoader, files);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } else {
            try {
                //23 6.0及以上
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    V23.install(classLoader, files, dexOptDir);
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    V19.install(classLoader, files, dexOptDir); //4.4以上
                } else {  // >= 14
                    V14.install(classLoader, files, dexOptDir);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

處理無非就是利用反射,找到BaseDexClassLoader中的pathList,然后找到pathList中的makePathElements方法,得到補(bǔ)丁創(chuàng)建的 Element[],最后合并2個(gè)Element數(shù)組并修改 classLoader中 pathList的 dexelements。

    private static final class V23 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                IOException {
            //找到 pathList
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);

            ArrayList<IOException> suppressedExceptions = new ArrayList<>();
            // 從 pathList找到 makePathElements 方法并執(zhí)行
            // 得到補(bǔ)丁創(chuàng)建的 Element[]
            Object[] patchElements = makePathElements(dexPathList,
                    new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions);

            //將原本的 dexElements 與 makePathElements生成的數(shù)組合并
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", patchElements);
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

        /**
         * 把dex轉(zhuǎn)化為Element數(shù)組
         */
        private static Object[] makePathElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            //通過閱讀android6、7、8、9源碼,都存在makePathElements方法
            Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements",
                    List.class, File.class,
                    List.class);
            return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

代碼太多,不全放了。
然后MainActivity寫點(diǎn)bug,為了方便,我新建了一個(gè)類來拋出bug。

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ExceptionBug.test();
    }
}
public class ExceptionBug {
    public static void test() {
        throw new UnsupportedOperationException("this is a exception");
    }
}

這樣就能保證出bug了。

修復(fù)
public class ExceptionBug {
    public static void test() {
        //throw new UnsupportedOperationException("this is a exception");
    }
}

注釋掉異常的拋出。重新編譯項(xiàng)目,注意只是編譯項(xiàng)目,不是重新運(yùn)行到手機(jī)。找到這個(gè)類的class文件。編譯成dex文件。位置在app-build-intermediates-javac
image.png
編譯

找到工具:/Users/chenjy/Library/Android/sdk/build-tools/29.0.3


image.png

把這個(gè)添加到配置環(huán)境里面去。然后回到ExceptionBug.class文件所在的目錄。
然后輸入命令:dx --dex --output=bugFix.dex com/cjy/hotfixdemo/ExceptionBug.class
執(zhí)行命令后就會(huì)生成這個(gè)dex文件了,然后把文件放到MyApplication指定的位置那里,也就是/sdcard/bugFix.dex,放到sdcard里。
再重新打開,就會(huì)發(fā)現(xiàn),沒拋出異常了。

值得注意的問題

1、AndroidQ(10.0)以上,熱修復(fù)的dex文件,不要放到sdcard中,因?yàn)橥獠看鎯?chǔ)的訪問權(quán)限改了,只能看到自己的,所以應(yīng)該放到私有目錄下,或者application中加入android:requestLegacyExternalStorage="true"。
2、修復(fù)的代碼一定要先于bug代碼被加載,所以這里我直接在application中就調(diào)用了,如果先啟動(dòng)的是MAinActivity,已經(jīng)加載過了ExceptionBug這個(gè)類,之后跳轉(zhuǎn)到activity2,再去熱修復(fù)ExceptionBug類就不行了。
3、這個(gè)只是個(gè)簡單的例子,實(shí)際的熱修復(fù)沒這么簡單,這里只不過是通過熱修復(fù)的例子,來解釋實(shí)現(xiàn)類加載的流程。

?著作權(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ù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

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