插件化抚芦、組件化、熱修復(fù)

一、Android插件化叉抡、組件化尔崔、熱修復(fù)的區(qū)別

插件化
插件化是一種將應(yīng)用程序按照模塊或組件進(jìn)行拆分,并以插件的方式動態(tài)加載和運(yùn)行的技術(shù)褥民。其主要原理包括以下幾個步驟:

  • 模塊劃分:應(yīng)用程序被分割成多個獨立模塊季春,每個模塊通常作為一個單獨的APK存在。
  • 動態(tài)加載:通過自定義類加載器或使用第三方框架(如DexClassLoader)來加載并實例化外部APK中的類
  • 資源隔離:由于每個APK可能有自己的資源文件消返,需要通過Hook機(jī)制來實現(xiàn)資源隔離與管理
  • 上下文運(yùn)行環(huán)境:在調(diào)用外部APK中代碼時载弄,需要正確處理上下文環(huán)境以確保功能正常運(yùn)行。

總之撵颊,Android插件化利用動態(tài)加載技術(shù)將應(yīng)用程序切割成多個獨立安裝運(yùn)行的組件侦锯,并借助合適框架管理各種資源及上下文環(huán)境等信息。

組件化
組件化是一種將應(yīng)用程序按照功能模塊拆分秦驯,并以組件的方式進(jìn)行開發(fā)尺碰、管理和復(fù)用的技術(shù)。其主要原理包括以下幾個步驟:

  • 功能模塊劃分:將應(yīng)用程序按照業(yè)務(wù)邏輯或功能劃分成多個獨立組件译隘。
  • 通信機(jī)制:使用合適的通信機(jī)制(如Intent亲桥、事件總線等)來實現(xiàn)不同組件之間的數(shù)據(jù)傳遞與交互。
  • 解耦:通過接口定義和依賴注入等方式固耘,使得各個組件之間相互解耦题篷,提高代碼重用性與維護(hù)性。

Android組件化旨在實現(xiàn)模塊化開發(fā)厅目,讓不同團(tuán)隊可以并行開發(fā)不同模塊番枚,并且能夠靈活地替換、新增或刪除某些功能损敷。

熱修復(fù)
熱修復(fù)是一種在應(yīng)用程序運(yùn)行時對已發(fā)布版本進(jìn)行動態(tài)修復(fù)bug或更新功能的技術(shù)葫笼。其主要原理包括以下幾個步驟:

  • 補(bǔ)丁生成與發(fā)布:根據(jù)需要修復(fù)或更新的問題,生成補(bǔ)丁文件并推送到客戶端設(shè)備上拗馒。

  • 補(bǔ)丁加載與應(yīng)用:當(dāng)檢測到有新補(bǔ)丁時路星,通過自定義類加載器等機(jī)制將補(bǔ)丁文件加載到應(yīng)用程序中

  • 動態(tài)修復(fù)/更新:在運(yùn)行時,通過替換诱桂、插入或修改現(xiàn)有代碼來解決問題洋丐,并確保新舊版本之間的兼容性與穩(wěn)定性。

總之挥等,Android熱修復(fù)技術(shù)利用補(bǔ)丁文件實現(xiàn)對已發(fā)布版本進(jìn)行動態(tài)修復(fù)和更新友绝。它可以快速響應(yīng)問題并部署解決方案,而無須重新發(fā)布整個應(yīng)用程序肝劲。

二迁客、Tinker熱修復(fù)的原理

熱修復(fù)的方案有很多種惭墓,其中原理也各不相同扔亥。目前開源的比較有名的有阿里的AndFix趁尼、美團(tuán)的Robust舷夺、qq的QZone以及Tinker等催蝗。

1切威、Tinker的優(yōu)點

  • 支持類替換、so替換丙号,資源替換是采用類似Instant-run的方案先朦。
  • 補(bǔ)丁包較小,自研diff方案犬缨,下發(fā)的是差量包喳魏,包括的是變更的內(nèi)容。
  • 采用全量Dex更新怀薛,不需要額外處理 CLASS_ISPREVERIFIED 問題刺彩。

2、Tinker熱修復(fù)的流程

(1)Tinker將新舊dex做了diff(差分算法)枝恋,得到patch.dex
(2)然后將patch.dex下發(fā)到客戶端创倔,客戶端將patch.dex與舊dex的classes.dex做合并,生成新的classes.dex
(3)在運(yùn)行時通過反射將合成后的全量dex插入到dex elements前面(放在Element數(shù)組的第一個元素)焚碌,完成修復(fù)畦攘。(餓了么的Amigo則是將補(bǔ)丁包中每個dex對應(yīng)的Element取出來,之后組成新的Element數(shù)組十电,在運(yùn)行時通過反射用新的Element數(shù)組替換掉現(xiàn)有的Element數(shù)組)知押。

在ClassLoader的加載過程中,其中一個環(huán)節(jié)就是調(diào)用DexPathList的findClass的方法鹃骂,如下所示台盯,libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

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

  • Element內(nèi)部封裝了DexFile,DexFile用于加載dex文件畏线,因此每個dex文件對應(yīng)一個Element爷恳。多個Element組成了有序的Element數(shù)組dexElements。
  • 當(dāng)要查找類時象踊,會在注釋1處遍歷Element數(shù)組dexElements(相當(dāng)于遍歷dex文件數(shù)組)温亲,注釋2處調(diào)用Element的findClass方法,其方法內(nèi)部會調(diào)用DexFile的loadClassBinaryName方法查找類杯矩。如果在Element中(dex文件)找到了該類就返回栈虚,如果沒有找到就接著在下一個Element中進(jìn)行查找。
    根據(jù)上面的查找流程史隆,我們將有bug的類Key.class進(jìn)行修改魂务,再將Key.class打包成包含dex的補(bǔ)丁包Patch.jar,通過反射放在Element數(shù)組dexElements的第一個元素,這樣會首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委派模式就不會被加載,這就是類的加載方案粘姜,如下圖所示:


    圖片.png

類加載方案需要重啟App后讓ClassLoader重新加載新的類鬓照,為什么要重啟呢?這是因為類是無法被卸載的孤紧,因此想要重新加載類就需要重啟App豺裆,因此采用類加載方案的熱修復(fù)框架是不能即時生效的。

Tinker熱修復(fù)的流程圖.png

3号显、65536限制與LinearAlloc限制

類的加載方案基于Dex分包方案臭猜,什么是Dex分包方案?這個得先從65536限制和LinearAlloc限制說起押蚤。

65536限制

隨著應(yīng)用功能越來越復(fù)雜蔑歌,代碼量不斷增大,引入的庫也越來越多揽碘,可能會在編譯時提示如下異常:

com.android.dex.DexIndexOverflowException:method ID not in [0,0xffff]:65536

這說明應(yīng)用中引用的方法數(shù)超過了最大數(shù)65536個次屠。產(chǎn)生這個問題的原因就是系統(tǒng)的65536限制,65536限制的主要原因是DVM Bytecode的限制雳刺。DVM指令集的方法調(diào)用指令 invoke-kind索引為16bits劫灶,最多能引用65536。

LinearAlloc限制

在安裝應(yīng)用時可能會提示INSTALL_FAILED_DEXOPT,產(chǎn)生的原因就是LinearAlloc限制煞烫,DVM中的LinearAlloc是一個固定的緩存區(qū)浑此,當(dāng)方法數(shù)超出了緩存區(qū)的大小時會報錯。

為了解決65536限制和LinearAlloc限制滞详,從而產(chǎn)生了Dex分包方案凛俱。Dex分包方案主要做的是在打包時將應(yīng)用代碼分成多個Dex,將應(yīng)用啟動時必須用到的類和這些類的直接引用類放到主Dex中料饥,其他代碼放到次Dex中蒲犬。當(dāng)應(yīng)用啟動時先加載主Dex,等到應(yīng)用啟動后再動態(tài)地加載次Dex岸啡,從而緩解了主Dex的65536限制和LinearAlloc限制原叮。

Dex分包方案主要有兩種,分別是Google官方方案巡蘸、Dex自動拆包和動態(tài)加載方案奋隶。

4、PathClassLoader和DexClassLoader

Android系統(tǒng)中又兩個應(yīng)用程序類加載器悦荒,它們分別是PathClassLoader和DexClassLoader唯欣,它們都是繼承于BaseDexClassLoader的,而BaseDexClassLoader繼承ClassLoader搬味。

BaseDexClassLoader類加載:

  • 作為ClassLoader的子類境氢,重寫了父類的findClass方法:
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //在自己的成員變量DexPathList中尋找蟀拷,找不到拋異常
        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) {
        //循環(huán)遍歷成員變量dexElements,調(diào)用DexFile.loadClassBinaryName加載class
        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;
    }

PathClassLoader和DexClassLoader的區(qū)別

  • 路徑查找方式:
    PathClassLoader使用系統(tǒng)默認(rèn)的路徑查找機(jī)制萍聊。它會根據(jù)包名去已安裝的APK文件或系統(tǒng)庫目錄下查找并加載類问芬;而DexClassLoader可以指定自定義的dex文件路徑來進(jìn)行類的查找和加載。這使得DexClassLoader能夠靈活地從外部存儲設(shè)備(如SD卡)或其他位置動態(tài)地加載已打包成dex格式的apk或jar文件寿桨。
  • 加載資源文件
    PathClassLoader除了加載類此衅,還可以通過getResource()方法來獲取APK中的資源文件;而DexClassLoader無法直接使用getResource()方法來獲取APK中的資源文件牛隅,因為它只負(fù)責(zé)加載類而不處理資源文件炕柔。但是可以通過自定義解析代碼實現(xiàn)對資源的訪問酌泰。
  • 類共享與隔離性
    所有使用相同PathClassLoader實例創(chuàng)建出來的對象都將共享同一個虛擬內(nèi)存空間媒佣,即處于同一個”命名空間“下。這意味著不同模塊之間可能存在命名沖突等問題陵刹;而每個DexClassLoader實例都會有獨立的虛擬機(jī)內(nèi)存空間默伍,”命名空間“彼此隔離,避免了命名沖突問題衰琐。

總結(jié)起來:

  • PathClassLoader是系統(tǒng)默認(rèn)的應(yīng)用程序類加載器也糊,使用系統(tǒng)默認(rèn)的路徑查找機(jī)制,在加載類和資源時比較方便羡宙。
  • DexClassLoader可以自定義dex文件路徑進(jìn)行靈活的類加載狸剃,但無法直接訪問APK中的資源文件。

4狗热、核心原理

PackageManagerService拿到apk钞馁,然后從后臺下載修復(fù)的patch.dex包。注意匿刮,這里我們可能有多個dex文件需要更新

public static void loadFixedDex(Context context) {
        if (context == null) return;
        // Dex文件目錄(私有目錄中僧凰,存在之前已經(jīng)復(fù)制過來的修復(fù)包)
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();
        // 遍歷私有目錄中所有的文件
        for (File file : listFiles) {
            // 找到修復(fù)包,加入到集合
            if (file.getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equals(file.getName())) {
                loadedDex.add(file);
            }
        }

        // 模擬類加載器
        createDexClassLoader(context, fileDir);
    }

這個方法的作用是找到dex文件存在的位置并遍歷dex文件然后保存在一個集合中熟丸。然后調(diào)用createDexClassLoader方法训措。

private static void createDexClassLoader(Context context, File fileDir) {
        // 創(chuàng)建臨時的解壓目錄(先解壓到該目錄,再加載java)
        String optimizedDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
        // 不存在就創(chuàng)建
        File fopt = new File(optimizedDir);
        if (!fopt.exists()) {
            // 創(chuàng)建多級目錄
            fopt.mkdirs();
        }
        for (File dex : loadedDex) {
            // 每遍歷一個要修復(fù)的dex文件光羞,就需要插樁一次
            DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(),
                    optimizedDir, null, context.getClassLoader());
            hotfix(classLoader, context);
        }
    }  

private static void hotfix(DexClassLoader classLoader, Context context) {
// 獲取系統(tǒng)PathClassLoader類加載器
PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
try {
        // 獲取自有的dexElements數(shù)組對象
        Object myDexElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));

        // 獲取系統(tǒng)的dexElements數(shù)組對象
        Object systemDexElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathLoader));

        // 合并成新的dexElements數(shù)組對象
        Object dexElements = ArrayUtils.combineArray(myDexElements, systemDexElements);

        // 通過反射再去獲取   系統(tǒng)的pathList對象
        Object systemPathList = ReflectUtils.getPathList(pathLoader);

        // 重新賦值給系統(tǒng)的pathList屬性  --- 修改了pathList中的dexElements數(shù)組對象
        ReflectUtils.setField(systemPathList, systemPathList.getClass(), dexElements);
    } catch (Exception e) {
        e.printStackTrace();
    }
}  
public class ReflectUtils {

    /**
     * 通過反射獲取某對象绩鸣,并設(shè)置私有可訪問
     *
     * @param obj   該屬性所屬類的對象
     * @param clazz 該屬性所屬類
     * @param field 屬性名
     * @return 該屬性對象
     */
    private static Object getField(Object obj, Class<?> clazz, String field)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        Field localField = clazz.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 給某屬性賦值,并設(shè)置私有可訪問
     *
     * @param obj   該屬性所屬類的對象
     * @param clazz 該屬性所屬類
     * @param value 值
     */
    public static void setField(Object obj, Class<?> clazz, Object value)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        Field localField = clazz.getDeclaredField("dexElements");
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 通過反射獲取BaseDexClassLoader對象中的PathList對象
     *
     * @param baseDexClassLoader BaseDexClassLoader對象
     * @return PathList對象
     */
    public static Object getPathList(Object baseDexClassLoader)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException, ClassNotFoundException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 通過反射獲取BaseDexClassLoader對象中的PathList對象纱兑,再獲取dexElements對象
     *
     * @param paramObject PathList對象
     * @return dexElements對象
     */
    public static Object  getDexElements(Object paramObject)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }
}

參考:http://www.reibang.com/p/e6c4eedd83abhttp://www.reibang.com/p/6e412b0115f1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呀闻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萍启,更是在濱河造成了極大的恐慌总珠,老刑警劉巖屏鳍,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異局服,居然都是意外死亡钓瞭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門淫奔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來山涡,“玉大人,你說我怎么就攤上這事唆迁⊙即裕” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵唐责,是天一觀的道長鳞溉。 經(jīng)常有香客問我,道長鼠哥,這世上最難降的妖魔是什么熟菲? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮朴恳,結(jié)果婚禮上抄罕,老公的妹妹穿的比我還像新娘。我一直安慰自己于颖,他們只是感情好呆贿,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著森渐,像睡著了一般做入。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上章母,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天母蛛,我揣著相機(jī)與錄音,去河邊找鬼乳怎。 笑死彩郊,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蚪缀。 我是一名探鬼主播秫逝,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼询枚!你這毒婦竟也來了违帆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤金蜀,失蹤者是張志新(化名)和其女友劉穎刷后,沒想到半個月后的畴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡尝胆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年丧裁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片含衔。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡煎娇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贪染,到底是詐尸還是另有隱情缓呛,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布杭隙,位于F島的核電站哟绊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寺渗。R本人自食惡果不足惜匿情,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一兰迫、第九天 我趴在偏房一處隱蔽的房頂上張望信殊。 院中可真熱鬧,春花似錦汁果、人聲如沸涡拘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳄乏。三九已至,卻和暖如春棘利,著一層夾襖步出監(jiān)牢的瞬間橱野,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工善玫, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留水援,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓茅郎,卻偏偏與公主長得像蜗元,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子系冗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

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