六辖佣、【熱修復(fù)二】Java層類加載熱修復(fù)的簡(jiǎn)單實(shí)現(xiàn)

使用到的技術(shù)點(diǎn)

1霹抛、Java類加載機(jī)制;
2卷谈、Android加載dex文件杯拐;
3、反射;

原理:

用修復(fù)好的類替換有問(wèn)題的類端逼。在App重新啟動(dòng)后讓Classloader去加載新的類朗兵。因?yàn)樵贏pp運(yùn)行到一半的時(shí)候,所有需要發(fā)生變更的類已經(jīng)被加載過(guò)了顶滩,在Android上是無(wú)法對(duì)一個(gè)類進(jìn)行卸載的余掖。如果不重啟,原來(lái)的類還在虛擬機(jī)中礁鲁,就無(wú)法加載新類盐欺。因此,只有在下次重啟的時(shí)候仅醇,在還沒(méi)有走到業(yè)務(wù)邏輯之前搶先加載補(bǔ)丁中的新類冗美,這樣后續(xù)訪問(wèn)這個(gè)類時(shí),就會(huì)Resolve為新類析二。從而達(dá)到熱修復(fù)的目的粉洼。

關(guān)鍵問(wèn)題是:

如何替換一個(gè)類?
這里沒(méi)有直接替換一個(gè)類叶摄,而是通過(guò)類加載機(jī)制属韧,在加載問(wèn)題類之前,先加載補(bǔ)丁類的方案達(dá)到替換的目的蛤吓。具體操作的依據(jù)挫剑,首先因?yàn)橥粋€(gè)類加載器在嘗試加載一個(gè)類的時(shí)候,會(huì)先判斷這個(gè)類是否已經(jīng)加載柱衔,如果已經(jīng)加載則不會(huì)再次去加載樊破;其次,Android加載dex時(shí)唆铐,會(huì)按照先后順序依次加載的哲戚。

一、Java加載機(jī)制

ClassLoader#loadClass()方法

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

(1)艾岂、從當(dāng)前類加載器的已加載類緩存中根據(jù)類的全路徑名查詢是否存在該類顺少,如果存在則直接返回。
(2)王浴、如果當(dāng)前類存在父類加載器脆炎,則調(diào)用父類加載器的loadClass(name,false)方法對(duì)其進(jìn)行加載氓辣。
(3)秒裕、如果當(dāng)前類加載器不存在父類加載器,則直接調(diào)用啟動(dòng)類加載器對(duì)該類進(jìn)行加載钞啸。
(4)几蜻、如果當(dāng)前類的所有父類加載器都沒(méi)有成功加載class喇潘,則嘗試調(diào)用當(dāng)前類加載器的findClass方法對(duì)其進(jìn)行加載,該方法就是我們自定義加載器需要重寫的方法梭稚。
(5)颖低、最后如果類被成功加載,則做一些性能數(shù)據(jù)的統(tǒng)計(jì)弧烤。
(6)忱屑、由于loadClass指定了resolve為false,所以不會(huì)進(jìn)行連接階段的繼續(xù)執(zhí)行暇昂,這也就解釋了為什么通過(guò)類加載器加載類并不會(huì)導(dǎo)致類的初始化莺戒。

二、Android類加載的機(jī)制

Android中相關(guān)的類加載器PathClassLoaderDexClassLoader话浇。

(1)、DexClassLoader:能夠加載自定義的jar/apk/dex
(2)闹究、PathClassLoader:只能加載系統(tǒng)中已經(jīng)安裝過(guò)的apk
所以Android系統(tǒng)默認(rèn)的類加載器為PathClassLoader幔崖,而DexClassLoader可以像JVM的ClassLoader一樣提供動(dòng)態(tài)加載。

Android類加載器的繼承關(guān)系.png

PathClassLoader和DexClassLoader加載Class的相關(guān)源碼:

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    @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()

final class DexPathList {
    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

}

DexFile#loadClassBinaryName()

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

從上面的源碼可以得知渣淤,當(dāng)Android中類加載器嘗試加載類的時(shí)候赏寇,會(huì)調(diào)用DexPathListfindClass()方法,通過(guò)遍歷dexElements中的Element价认,得到DexFile嗅定,再通過(guò)DexFile調(diào)用loadClassBinaryName()方法加載類。類加載成功后用踩,就直接返回渠退。因此,可以通過(guò)將修復(fù)好的dex插入到dexElements的集合(出現(xiàn)bug的xxx.class所在的dex的前面)的位置脐彩,就可以達(dá)到間接替換bug類的目的碎乃。
最本質(zhì)的實(shí)現(xiàn)原理:類加載器去加載某個(gè)類的時(shí)候,是去dexElements里面從頭往下查找的惠奸。fixed.dex,classes1.dex,classes2.dex,classes3.dex

三梅誓、具體實(shí)現(xiàn)

Class  BaseDexClassLoader{
    private DexPathList pathList;
}

Class  DexPathList{
    private Element[] dexElements;
}

先獲取到已安裝App中原有的dexElements(appDexElements)和通過(guò)DexClassLoader加載已經(jīng)修復(fù)好的dex得到的dexElements(fixedDexElements)佛南。然后再將appDexElements和fixedDexElements合并成一個(gè)新的dexElements(newDexElements)梗掰。

類加載修復(fù)的流程.png

核心類代碼
FixDexUtils

public class FixDexUtils {
    private static HashSet<File> sLoadedDexFiles = new HashSet();

    static {
        sLoadedDexFiles.clear();
    }

    public static void loadFixedDex(Context context){
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        File[] dexFiles = fileDir.listFiles();
        for(File file:dexFiles){
            if(file.getName().startsWith("classes") && file.getName().endsWith(".dex")){
                sLoadedDexFiles.add(file);
            }
        }

        doDexInject(context, fileDir, sLoadedDexFiles);
    }

    private static void doDexInject(final Context context, File filesDir, HashSet<File> loadedDexs){
        String optimizedDir = filesDir.getAbsolutePath() + File.separator + Constants.OPT_DEX;
        File fopt = new File(optimizedDir);
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        try {
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            for(File dex:loadedDexs){
                DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, pathClassLoader);

                Object dexObj = getPathList(dexClassLoader);
                Object pathObj = getPathList(pathClassLoader);

                Object dexElementsList = getDexElements(dexObj);
                Object pathDexElementsList = getDexElements(pathObj);

                //合并dexElements
                Object dexElements = combineArray(dexElementsList, pathDexElementsList);

                //重新給PathList里面的Element[] dexElements賦值
                Object pathList = getPathList(pathClassLoader);
                setFiled(pathList, pathList.getClass(), "dexElements", dexElements);
                Object finalElementsList = getDexElements(pathList);
                System.out.println("--------"+finalElementsList);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void setFiled(Object obj, Class<?> clazz, String fieldName, Object value) throws Exception {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    private static Object getField(Object obj, Class<?> clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }

    private static Object getPathList(Object baseDexClassLoader) throws Exception {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    private static Object getDexElements(Object obj) throws Exception {
        return getField(obj, obj.getClass(), "dexElements");
    }

    private static Object combineArray(Object arrLhs, Object arrRhs){
        Class<?> clazz = arrLhs.getClass().getComponentType();
        int i = Array.getLength(arrLhs);
        int len = i + Array.getLength(arrRhs);
        Object arrResult = Array.newInstance(clazz, len);
        for(int k = 0; k < len; k++){
            if(k < i){
                Array.set(arrResult, k, Array.get(arrLhs, k));
            }else{
                Array.set(arrResult, k, Array.get(arrRhs, k - i));
            }
        }
        return arrResult;
    }

}

FMApplication

public class FMApplication extends Application {

    @Override
    public void onCreate() {
        ClassLoader classLoader = getClassLoader();
        System.out.println("--------onCreate---ClassLoader:"+getClassLoader());
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        System.out.println("--------attachBaseContext");
        MultiDex.install(base);
        FixDexUtils.loadFixedDex(base);
        super.attachBaseContext(base);

    }


}

app#build.gradle的核心配置
android {
    compileSdkVersion 27
    defaultConfig {
        ...
        multiDexEnabled true
        ...
    }

    buildTypes {
        release {
            //指定單獨(dú)打到一個(gè)dex的類
            multiDexKeepFile file('dex.keep')
            def myFile = file('dex.keep')
            println("isFileExists:"+myFile.exists())
            println "dex keep"

            minifyEnabled false
          
        }
    }

}

dependencies {
   ...
    implementation 'com.android.support:multidex:1.0.1'
  ...
}

dex.keep的配置和目錄.png

四、關(guān)于FixedClass.class打包成classes.dex

dx.bat所在目錄:

sdk\build-tools\27.0.3

dx --dex --output=D:\Users\x\Desktop\dex\classes2.dex D:\Users\x\Desktop\dex

命令解釋:
--output=D:\Users\x\Desktop\dex\classes2.dex 指定輸出路徑
D:\Users\x\Desktop\dex 最后指定去打包哪個(gè)目錄下面的class字節(jié)文件(注意要包括全路徑的文件夾嗅回,也可以有多個(gè)class)


class_2_dex.png
class_2_dex命令.png
class_2_dex結(jié)果.png

五及穗、Java反射機(jī)制可以動(dòng)態(tài)修改實(shí)例中final修飾的成員變量嗎?

回答是分兩種情況的绵载。

  1. 當(dāng)final修飾的成員變量在定義的時(shí)候就初始化了值拥坛,那么java反射機(jī)制就已經(jīng)不能動(dòng)態(tài)修改它的值了蓬蝶。
  2. 當(dāng)final修飾的成員變量在定義的時(shí)候并沒(méi)有初始化值的話,那么就還能通過(guò)java反射機(jī)制來(lái)動(dòng)態(tài)修改它的值猜惋。

【相關(guān)源碼】HotFixJava

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丸氛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子著摔,更是在濱河造成了極大的恐慌缓窜,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谍咆,死亡現(xiàn)場(chǎng)離奇詭異禾锤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)摹察,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門恩掷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人供嚎,你說(shuō)我怎么就攤上這事黄娘。” “怎么了克滴?”我有些...
    開(kāi)封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵逼争,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我劝赔,道長(zhǎng)誓焦,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任着帽,我火速辦了婚禮杂伟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仍翰。我一直安慰自己稿壁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布歉备。 她就那樣靜靜地躺著傅是,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蕾羊。 梳的紋絲不亂的頭發(fā)上喧笔,一...
    開(kāi)封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音龟再,去河邊找鬼书闸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛利凑,可吹牛的內(nèi)容都是我干的浆劲。 我是一名探鬼主播嫌术,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼牌借!你這毒婦竟也來(lái)了度气?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤膨报,失蹤者是張志新(化名)和其女友劉穎磷籍,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體现柠,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡院领,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了够吩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片比然。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖周循,靈堂內(nèi)的尸體忽然破棺而出强法,到底是詐尸還是另有隱情,我是刑警寧澤鱼鼓,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布拟烫,位于F島的核電站该编,受9級(jí)特大地震影響迄本,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜课竣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一嘉赎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧于樟,春花似錦公条、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至路捧,卻和暖如春关霸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杰扫。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工队寇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人章姓。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓佳遣,卻偏偏與公主長(zhǎng)得像识埋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子零渐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容