什么是插件化框架
插件化框架可以在主程序不重新安裝的情況下,針對單個業(yè)務(wù)模塊進行加載達到模塊更新的目的,整個加載更新過程财破,對用戶來說也是無感知的。
正式因為這樣从诲,新需求比起傳統(tǒng)更新方式覆蓋率和覆蓋速度都會更高和更快左痢,對于大型開發(fā)團隊,各個業(yè)務(wù)模塊開發(fā)小組組也不需要再等所有組的需求開發(fā)完統(tǒng)一發(fā)布版本系洛,發(fā)版本可以單獨針對小組內(nèi)單個功能發(fā)布了俊性,有了這些優(yōu)點才使得這1年來插件化框架如此流行的重要原因。
目前網(wǎng)上流行的主流的插件化技術(shù)核心主要分兩類:
一類是360公司開源的DroidPlugin描扯,特點在宿主程序上打造一個純粹的環(huán)境定页,可以讓一個個普通apk(插件)不安裝就可以正常使用,并且插件之前資源和代碼都是相互獨立互相不干擾也不能訪問绽诚,為了達到這樣的目的典徊,框架hook了大量的系統(tǒng)api。
另一類是從dynamic-load-apk 開始恩够,通過反射少量api卒落,達到插件代碼和資源與宿主合并,達到相互調(diào)用的結(jié)果蜂桶,目前大部分都是框架從底層代碼合并和資源合并用的手法都差不多儡毕,只是各個框架在這基礎(chǔ)上對插件化的理解不一樣,為各自的項目做了不少的業(yè)務(wù)封裝扑媚。
常用的類加載方法是:
常見的資源加載方式是:
目前看來怎么加載代碼腰湾,怎么加載資源雷恃、怎么動態(tài)聲明和啟動android組件。網(wǎng)上大部分開源的開源框架都很好的解決了這些怎么實現(xiàn)的問題檐盟,很完美的做到了從0到1褂萧。但是對于一個有幾十個開發(fā)人員千萬級別的大型的app來說,單解決了這些問題還是不夠的葵萎。插件框架的穩(wěn)定性导犹、從原有代碼到插件化的遷移成本、后期維護成本等等方面都需要考慮到羡忘。
所以兼容性谎痢、遷移成本、后期維護成本是我們在插件化選型時最基本的考慮因素卷雕。
首先兼容性节猿、后期維護成本,大家都了解到由于android系統(tǒng)的碎片化漫雕,android官方提供的api也存在著不少的兼容性問題滨嘱,況且針對創(chuàng)新能力如此強大的國內(nèi)手機廠商,國產(chǎn)手機也額外的多了不少兼容性問題浸间。
具體例子:常用到的資源加載方式太雨,放在vivo的部分手機就不能使用原因是其ROM把系統(tǒng)的Resources封裝成為了VivoResources直接導(dǎo)致了反射失敗插件資源無法加載,同樣的Nubia的部分手機也是魁蒜。所以基于這樣在做插件化的時候hook系統(tǒng)的api就應(yīng)該盡量的少囊扳,因為hook的api不確定性太多了,而且在這部分的開發(fā)過程肯定不會有任何文檔提供參考的兜看,遇到問題就干擼代碼吧锥咸。
處理兼容性的工作量越大其實后期的維護成本就越高,至少如果android一個新版本出來了首先要看的是之前hook的官方api有沒有被改掉细移。如果有問題還要再針對新的版本尋求新的實現(xiàn)搏予,這部分工作量是非常大的。這也是我們不選擇DroidPlugin的重要原因葫哗,從網(wǎng)上的能找到的所有資料并沒有看到DroidPlugin的兼容性能達到多少能適配多少臺手機缔刹。但是預(yù)判一下DroidPlugin hook了大量的api比起其他框架hook兩個,這部分后續(xù)維護成本也是足夠喝一壺的劣针。
遷移成本校镐,其實很多大型的項目實現(xiàn)插件化,在這個調(diào)整的過程中對代碼結(jié)構(gòu)捺典,調(diào)用邏輯等等的修改肯定是有的鸟廓。怎么保證這個改動是最少的,也是我們的考慮之一畢竟有改動就會產(chǎn)生bug,比較幸運的是,我們從打包腳本上下手在保證傳統(tǒng)的項目結(jié)構(gòu)和邏輯調(diào)用不改變的情況下實現(xiàn)模塊插件化引谜。讓插件化先跑起來牍陌,在實現(xiàn)之后再讓各個業(yè)務(wù)小組針對插件化的建議慢慢的完善和封裝插件和宿主之間的協(xié)議和約定。
插件化遷移過程:
首先员咽,看看我們酷狗原有的基礎(chǔ)項目結(jié)構(gòu):
項目底層是一個公用library 提供大部分的公共的基礎(chǔ)模塊毒涧,酷狗作為application作為主程序,其他聽看唱其他業(yè)務(wù)模塊也作為一個個library,各個業(yè)務(wù)組關(guān)聯(lián)公共模塊和酷狗主程序贝室,在各自的業(yè)務(wù)模塊下開發(fā)契讲、調(diào)試。當(dāng)發(fā)版本的時候就統(tǒng)一在打包平臺上讓酷狗關(guān)聯(lián)所有業(yè)務(wù)模塊 然后統(tǒng)一打包滑频,這是最常見的項目組成架構(gòu)業(yè)務(wù)模塊有項目級別的代碼分離而且業(yè)務(wù)項目依賴公共基礎(chǔ)庫捡偏。
項目優(yōu)化目標
優(yōu)化后,業(yè)務(wù)組之前的開發(fā)方式完全不變峡迷,項目結(jié)構(gòu)對比優(yōu)化前完整保留银伟,打包之后每個業(yè)務(wù)模塊是一個個插件可以單獨加載運行,每個插件都是只包含插件自己的資源和代碼(不包含公共庫)绘搞,插件可以正常訪問宿主的資源和代碼彤避,只要宿主保留了插件所需的資源和代碼,無論宿主怎么改變都可以啟動插件夯辖。
在這個過程中主要需要解決的問題有:
打包插件只保留插件本身的代碼忠藤,打包后插件不改變?nèi)魏握{(diào)用邏輯能順利調(diào)用回宿主邏輯。
決插件和宿主資源沖突問題楼雹,插件只保留本身資源,插件能訪問到宿主資源尖阔。
重新編譯之后怎么保證舊的宿主能支持新的插件贮缅。
首先怎么把原來跟底層項目依賴的業(yè)務(wù)模塊 單獨打成一個插件包 只保留業(yè)務(wù)模塊的代碼
我們拿聽模塊做個例子先編譯宿主程序也就是酷狗項目和底層基礎(chǔ)庫,一直編譯完javac這時候主項目資源R.java和映射表都可以得到介却,然后把編譯出來的class打包成common.jar把common項目資源復(fù)制到一個空殼項目commonres谴供。
接著修改聽項目的屬性把它從一個library變成一個application,關(guān)聯(lián)讓它不直接關(guān)聯(lián)基礎(chǔ)庫齿坷,而是讓它關(guān)聯(lián) commonn.jar 和 commonres桂肌,其中common.jar做提供編譯。
按照這樣編譯下去永淌,聽項目編譯出來的apk就只包含自己的代碼了崎场。
接下來解決資源問題,正常的資源查找方式
應(yīng)用層獲取資源 是用資源id直接去獲取,Resources先根據(jù)我們的id去資源映射表去查找這個資源的名稱是遂蛀,拿到資源名稱不對應(yīng)文件的資源只需要執(zhí)行從資源ID到資源名稱的轉(zhuǎn)換即可谭跨,而對應(yīng)有文件的資源還需要根據(jù)資源名稱來打開對應(yīng)的文件。經(jīng)過反射 resources 里面包含了多個映射表的目錄,查找的時候會按照順序先查宿主再查各個插件的映射表螃宙。
資源沖突問題因為上面項目結(jié)構(gòu)調(diào)整之后蛮瞄,插件和宿主都是application編譯時候就會出資源id相同,插件做資源id查找的時候就會有可能查找到宿主的資源谆扎,所以只要修改了resources.arsc和代碼層用到的R.java的id就可以解決沖突了常見的修改方式是修改插件的id的pp段挂捅。
程序編譯到這里,修改關(guān)聯(lián)后的插件項目還保留了一份commonres資源堂湖,跟宿主的程序上的是一摸一樣的闲先,能不能修改資源id來解決呢,答案是肯定的,因為插件和宿主查找資源的邏輯是一樣的苗缩,只要插件代碼調(diào)用中相同的資源id即R.java里面的id,修改為宿主資源id饵蒂,資源查找的時候就會順利的到宿主的resources.arsc去查找資源了。
最后酱讶,刪除插件resources.arsc多余的資源id和插件多余的資源文件退盯。這樣下來 最終得出的插件包 就是只含有插件代碼和插件資源的 而且還能隨意訪問宿主資源和代碼。
最后我們看看整體的編譯流程泻肯。
與微信資源混淆工具的兼容性問題
插件化工具主要在編譯時修改ID和去除其他多余資源渊迁,資源混淆工具主要是把名稱和路徑改短不修改ID,所以并不沖突灶挟。
只要保證讀寫操作都是嚴格按照 resources.arsc 的格式去寫就可以了琉朽。
接下來最后一個問題,重新編譯之后怎么保證舊的宿主能支持新的插件,簡單說就是多程序怎么一起 做代碼混淆,怎么保持宿主的資源ID。
** 多項目一起混淆:**
我們選擇的是統(tǒng)一做混淆稚铣,為什么不能先混淆整體混淆一個項目然后再混淆第二個項目的時候保持用上個項目的mapping 繼續(xù)混淆,一直這樣編譯下去箱叁?
主要因為插件和宿主公共庫之間并沒有固定接口,單獨混淆原來直接關(guān)聯(lián)調(diào)用的方法就會被混淆移除掉惕医。
我們還記得插件模塊和common基礎(chǔ)模塊本來就是直接關(guān)聯(lián)耕漱、直接調(diào)用的,后面我們改變項目結(jié)構(gòu)讓插件獨立出來了抬伺,但是這部分調(diào)用還是存在的螟够。
一旦單獨混淆他們之間關(guān)聯(lián)的代碼就會被移除掉,宿主公共庫的final靜態(tài)變量混淆后也會消失峡钓,插件也沒法調(diào)用得到妓笙。
其實正常來說,宿主和插件之間的調(diào)用本來就是需要先有固定的接口做好解耦 規(guī)范好所有的調(diào)用能岩,宿主提供一套完整的api給插件使用寞宫,然后混淆的時候 keep好各自邊界 。 這樣對于后續(xù)插件版本更新和管理才是最正確的拉鹃。
為什么這個問題到現(xiàn)在才聊呢淆九,因為讓各個業(yè)務(wù)模塊組為了插件化然后去封裝接口统锤,等他們解耦封裝好才來做的話時間太長了,所以我們先用這種方式讓他們不需要做任何封裝和解耦就能用炭庙,后續(xù)再要求他們慢慢的規(guī)范好這部分的接口饲窿。
** 怎么keep資源問題**
我們知道資源id的生成是按照資源名稱隨機生成的,一旦添加或者修改了某個資源名稱所有的資源id都有可能改變焕蹄。 如果不能固定資源id 每次編譯都id都變的話插件也無法下發(fā)給用戶使用逾雄。
解決方案是在編譯的時候根據(jù)宿主R.java的生成ids.xml和public.xml下次編譯把ids.xml和public.xml放到宿主的/res/value目錄下編譯可保持id不變,這樣即使下次宿主的其他資源改變了腻脏,只要插件用到的所有資源沒有改變鸦泳,新打出來的插件 一樣是可以給舊的宿主使用的。
到這里一個完成的插件包已經(jīng)出來了永品,剩下的就是 按照基本的加載方式做鹰,把這個插件加載進去就順利完成了。
最后在宿主實現(xiàn)插件管理功能鼎姐,這部分純粹就是基本的業(yè)務(wù)邏輯了钾麸。
- 下載校驗插件差異包。 (我們生成新的插件包上次到服務(wù)器炕桨,服務(wù)器就會與原始插件做差異對比饭尝,然后生成文件級別的差異文件,下發(fā)給用戶)
- 合并差異包對比插件版本號献宫。
- 加載前黑名單和白名單檢驗钥平。(某些插件版本必須強制加載,某些強制不能加載)
- 啟動時加載插件資源映射表姊途。(保證一啟動就可以查詢到所以資源涉瘾,而且這個反射效率很高,不耗時捷兰,也不耗內(nèi)存速度也很快)睡汹。
- 插件代碼選擇合適時機懶加載。 (因為加載dex的時候寂殉,需要耗時,5.0以下做opt,5.0以上做oat,而且時間還不短原在,所以需要挑合適的時機做懶加載友扰。)
最后,本文主要是我們在插件化過程中遇到一些問題的解決方案庶柿,其實每個解決方案都會有各自的取舍村怪,也無誰優(yōu)誰劣,如有更好的方案歡迎下面留言交流浮庐。