什么是插件化框架
插件化框架可以在主程序不重新安裝的情況下,針對(duì)單個(gè)業(yè)務(wù)模塊進(jìn)行加載達(dá)到模塊更新的目的,整個(gè)加載更新過(guò)程,對(duì)用戶來(lái)說(shuō)也是無(wú)感知的教寂。
正式因?yàn)檫@樣,新需求比起傳統(tǒng)更新方式覆蓋率和覆蓋速度都會(huì)更高和更快执庐,對(duì)于大型開(kāi)發(fā)團(tuán)隊(duì)酪耕,各個(gè)業(yè)務(wù)模塊開(kāi)發(fā)小組組也不需要再等所有組的需求開(kāi)發(fā)完統(tǒng)一發(fā)布版本,發(fā)版本可以單獨(dú)針對(duì)小組內(nèi)單個(gè)功能發(fā)布了轨淌,有了這些優(yōu)點(diǎn)才使得這1年來(lái)插件化框架如此流行的重要原因迂烁。
目前網(wǎng)上流行的主流的插件化技術(shù)核心主要分兩類:
一類是360公司開(kāi)源的DroidPlugin,特點(diǎn)在宿主程序上打造一個(gè)純粹的環(huán)境递鹉,可以讓一個(gè)個(gè)普通apk(插件)不安裝就可以正常使用盟步,并且插件之前資源和代碼都是相互獨(dú)立互相不干擾也不能訪問(wèn),為了達(dá)到這樣的目的躏结,框架hook了大量的系統(tǒng)api却盘。
另一類是從dynamic-load-apk 開(kāi)始,通過(guò)反射少量api媳拴,達(dá)到插件代碼和資源與宿主合并黄橘,達(dá)到相互調(diào)用的結(jié)果,目前大部分都是框架從底層代碼合并和資源合并用的手法都差不多屈溉,只是各個(gè)框架在這基礎(chǔ)上對(duì)插件化的理解不一樣塞关,為各自的項(xiàng)目做了不少的業(yè)務(wù)封裝。
常用的類加載方法是:
常見(jiàn)的資源加載方式是:
目前看來(lái)怎么加載代碼子巾,怎么加載資源帆赢、怎么動(dòng)態(tài)聲明和啟動(dòng)android組件。網(wǎng)上大部分開(kāi)源的開(kāi)源框架都很好的解決了這些怎么實(shí)現(xiàn)的問(wèn)題线梗,很完美的做到了從0到1椰于。但是對(duì)于一個(gè)有幾十個(gè)開(kāi)發(fā)人員千萬(wàn)級(jí)別的大型的app來(lái)說(shuō),單解決了這些問(wèn)題還是不夠的仪搔。插件框架的穩(wěn)定性瘾婿、從原有代碼到插件化的遷移成本、后期維護(hù)成本等等方面都需要考慮到僻造。
所以兼容性憋他、遷移成本孩饼、后期維護(hù)成本是我們?cè)诓寮x型時(shí)最基本的考慮因素髓削。
首先兼容性、后期維護(hù)成本镀娶,大家都了解到由于android系統(tǒng)的碎片化立膛,android官方提供的api也存在著不少的兼容性問(wèn)題,況且針對(duì)創(chuàng)新能力如此強(qiáng)大的國(guó)內(nèi)手機(jī)廠商,國(guó)產(chǎn)手機(jī)也額外的多了不少兼容性問(wèn)題宝泵。
具體例子:常用到的資源加載方式好啰,放在vivo的部分手機(jī)就不能使用原因是其ROM把系統(tǒng)的Resources封裝成為了VivoResources直接導(dǎo)致了反射失敗插件資源無(wú)法加載,同樣的Nubia的部分手機(jī)也是儿奶。所以基于這樣在做插件化的時(shí)候hook系統(tǒng)的api就應(yīng)該盡量的少框往,因?yàn)閔ook的api不確定性太多了,而且在這部分的開(kāi)發(fā)過(guò)程肯定不會(huì)有任何文檔提供參考的闯捎,遇到問(wèn)題就干擼代碼吧椰弊。
處理兼容性的工作量越大其實(shí)后期的維護(hù)成本就越高,至少如果android一個(gè)新版本出來(lái)了首先要看的是之前hook的官方api有沒(méi)有被改掉瓤鼻。如果有問(wèn)題還要再針對(duì)新的版本尋求新的實(shí)現(xiàn)秉版,這部分工作量是非常大的。這也是我們不選擇DroidPlugin的重要原因茬祷,從網(wǎng)上的能找到的所有資料并沒(méi)有看到DroidPlugin的兼容性能達(dá)到多少能適配多少臺(tái)手機(jī)清焕。但是預(yù)判一下DroidPlugin hook了大量的api比起其他框架hook兩個(gè)橄唬,這部分后續(xù)維護(hù)成本也是足夠喝一壺的诱桂。
遷移成本,其實(shí)很多大型的項(xiàng)目實(shí)現(xiàn)插件化榆芦,在這個(gè)調(diào)整的過(guò)程中對(duì)代碼結(jié)構(gòu)沃粗,調(diào)用邏輯等等的修改肯定是有的筛峭。怎么保證這個(gè)改動(dòng)是最少的,也是我們的考慮之一畢竟有改動(dòng)就會(huì)產(chǎn)生bug,比較幸運(yùn)的是陪每,我們從打包腳本上下手在保證傳統(tǒng)的項(xiàng)目結(jié)構(gòu)和邏輯調(diào)用不改變的情況下實(shí)現(xiàn)模塊插件化影晓。讓插件化先跑起來(lái),在實(shí)現(xiàn)之后再讓各個(gè)業(yè)務(wù)小組針對(duì)插件化的建議慢慢的完善和封裝插件和宿主之間的協(xié)議和約定檩禾。
插件化遷移過(guò)程:
首先挂签,看看我們酷狗原有的基礎(chǔ)項(xiàng)目結(jié)構(gòu):
項(xiàng)目底層是一個(gè)公用library 提供大部分的公共的基礎(chǔ)模塊,酷狗作為application作為主程序盼产,其他聽(tīng)看唱其他業(yè)務(wù)模塊也作為一個(gè)個(gè)library,各個(gè)業(yè)務(wù)組關(guān)聯(lián)公共模塊和酷狗主程序饵婆,在各自的業(yè)務(wù)模塊下開(kāi)發(fā)、調(diào)試戏售。當(dāng)發(fā)版本的時(shí)候就統(tǒng)一在打包平臺(tái)上讓酷狗關(guān)聯(lián)所有業(yè)務(wù)模塊 然后統(tǒng)一打包侨核,這是最常見(jiàn)的項(xiàng)目組成架構(gòu)業(yè)務(wù)模塊有項(xiàng)目級(jí)別的代碼分離而且業(yè)務(wù)項(xiàng)目依賴公共基礎(chǔ)庫(kù)。
項(xiàng)目?jī)?yōu)化目標(biāo)
優(yōu)化后灌灾,業(yè)務(wù)組之前的開(kāi)發(fā)方式完全不變搓译,項(xiàng)目結(jié)構(gòu)對(duì)比優(yōu)化前完整保留,打包之后每個(gè)業(yè)務(wù)模塊是一個(gè)個(gè)插件可以單獨(dú)加載運(yùn)行锋喜,每個(gè)插件都是只包含插件自己的資源和代碼(不包含公共庫(kù))些己,插件可以正常訪問(wèn)宿主的資源和代碼豌鸡,只要宿主保留了插件所需的資源和代碼,無(wú)論宿主怎么改變都可以啟動(dòng)插件段标。
在這個(gè)過(guò)程中主要需要解決的問(wèn)題有:
打包插件只保留插件本身的代碼涯冠,打包后插件不改變?nèi)魏握{(diào)用邏輯能順利調(diào)用回宿主邏輯。
決插件和宿主資源沖突問(wèn)題逼庞,插件只保留本身資源蛇更,插件能訪問(wèn)到宿主資源。
重新編譯之后怎么保證舊的宿主能支持新的插件赛糟。
首先怎么把原來(lái)跟底層項(xiàng)目依賴的業(yè)務(wù)模塊 單獨(dú)打成一個(gè)插件包 只保留業(yè)務(wù)模塊的代碼
我們拿聽(tīng)模塊做個(gè)例子先編譯宿主程序也就是酷狗項(xiàng)目和底層基礎(chǔ)庫(kù)械荷,一直編譯完javac這時(shí)候主項(xiàng)目資源R.java和映射表都可以得到,然后把編譯出來(lái)的class打包成common.jar把common項(xiàng)目資源復(fù)制到一個(gè)空殼項(xiàng)目commonres虑灰。
接著修改聽(tīng)項(xiàng)目的屬性把它從一個(gè)library變成一個(gè)application吨瞎,關(guān)聯(lián)讓它不直接關(guān)聯(lián)基礎(chǔ)庫(kù),而是讓它關(guān)聯(lián) commonn.jar 和 commonres穆咐,其中common.jar做提供編譯颤诀。
按照這樣編譯下去,聽(tīng)項(xiàng)目編譯出來(lái)的apk就只包含自己的代碼了对湃。
接下來(lái)解決資源問(wèn)題,正常的資源查找方式
應(yīng)用層獲取資源 是用資源id直接去獲取崖叫,Resources先根據(jù)我們的id去資源映射表去查找這個(gè)資源的名稱是,拿到資源名稱不對(duì)應(yīng)文件的資源只需要執(zhí)行從資源ID到資源名稱的轉(zhuǎn)換即可拍柒,而對(duì)應(yīng)有文件的資源還需要根據(jù)資源名稱來(lái)打開(kāi)對(duì)應(yīng)的文件心傀。經(jīng)過(guò)反射 resources 里面包含了多個(gè)映射表的目錄,查找的時(shí)候會(huì)按照順序先查宿主再查各個(gè)插件的映射表拆讯。
資源沖突問(wèn)題因?yàn)樯厦骓?xiàng)目結(jié)構(gòu)調(diào)整之后脂男,插件和宿主都是application編譯時(shí)候就會(huì)出資源id相同,插件做資源id查找的時(shí)候就會(huì)有可能查找到宿主的資源种呐,所以只要修改了resources.arsc和代碼層用到的R.java的id就可以解決沖突了常見(jiàn)的修改方式是修改插件的id的pp段宰翅。
程序編譯到這里,修改關(guān)聯(lián)后的插件項(xiàng)目還保留了一份commonres資源爽室,跟宿主的程序上的是一摸一樣的汁讼,能不能修改資源id來(lái)解決呢,答案是肯定的,因?yàn)椴寮退拗鞑檎屹Y源的邏輯是一樣的阔墩,只要插件代碼調(diào)用中相同的資源id即R.java里面的id,修改為宿主資源id嘿架,資源查找的時(shí)候就會(huì)順利的到宿主的resources.arsc去查找資源了。
最后啸箫,刪除插件resources.arsc多余的資源id和插件多余的資源文件耸彪。這樣下來(lái) 最終得出的插件包 就是只含有插件代碼和插件資源的 而且還能隨意訪問(wèn)宿主資源和代碼。
最后我們看看整體的編譯流程筐高。
與微信資源混淆工具的兼容性問(wèn)題
插件化工具主要在編譯時(shí)修改ID和去除其他多余資源搜囱,資源混淆工具主要是把名稱和路徑改短不修改ID,所以并不沖突柑土。
只要保證讀寫(xiě)操作都是嚴(yán)格按照 resources.arsc 的格式去寫(xiě)就可以了蜀肘。
接下來(lái)最后一個(gè)問(wèn)題,重新編譯之后怎么保證舊的宿主能支持新的插件,簡(jiǎn)單說(shuō)就是多程序怎么一起 做代碼混淆,怎么保持宿主的資源ID。
多項(xiàng)目一起混淆:
我們選擇的是統(tǒng)一做混淆稽屏,為什么不能先混淆整體混淆一個(gè)項(xiàng)目然后再混淆第二個(gè)項(xiàng)目的時(shí)候保持用上個(gè)項(xiàng)目的mapping 繼續(xù)混淆,一直這樣編譯下去扮宠?
主要因?yàn)椴寮退拗鞴矌?kù)之間并沒(méi)有固定接口,單獨(dú)混淆原來(lái)直接關(guān)聯(lián)調(diào)用的方法就會(huì)被混淆移除掉狐榔。
我們還記得插件模塊和common基礎(chǔ)模塊本來(lái)就是直接關(guān)聯(lián)坛增、直接調(diào)用的,后面我們改變項(xiàng)目結(jié)構(gòu)讓插件獨(dú)立出來(lái)了薄腻,但是這部分調(diào)用還是存在的收捣。
一旦單獨(dú)混淆他們之間關(guān)聯(lián)的代碼就會(huì)被移除掉,宿主公共庫(kù)的final靜態(tài)變量混淆后也會(huì)消失庵楷,插件也沒(méi)法調(diào)用得到罢艾。
其實(shí)正常來(lái)說(shuō),宿主和插件之間的調(diào)用本來(lái)就是需要先有固定的接口做好解耦 規(guī)范好所有的調(diào)用尽纽,宿主提供一套完整的api給插件使用咐蚯,然后混淆的時(shí)候 keep好各自邊界 。 這樣對(duì)于后續(xù)插件版本更新和管理才是最正確的弄贿。
為什么這個(gè)問(wèn)題到現(xiàn)在才聊呢春锋,因?yàn)樽尭鱾€(gè)業(yè)務(wù)模塊組為了插件化然后去封裝接口,等他們解耦封裝好才來(lái)做的話時(shí)間太長(zhǎng)了差凹,所以我們先用這種方式讓他們不需要做任何封裝和解耦就能用期奔,后續(xù)再要求他們慢慢的規(guī)范好這部分的接口。
怎么keep資源問(wèn)題
我們知道資源id的生成是按照資源名稱隨機(jī)生成的危尿,一旦添加或者修改了某個(gè)資源名稱所有的資源id都有可能改變能庆。 如果不能固定資源id 每次編譯都id都變的話插件也無(wú)法下發(fā)給用戶使用。
解決方案是在編譯的時(shí)候根據(jù)宿主R.java的生成ids.xml和public.xml下次編譯把ids.xml和public.xml放到宿主的/res/value目錄下編譯可保持id不變脚线,這樣即使下次宿主的其他資源改變了搁胆,只要插件用到的所有資源沒(méi)有改變,新打出來(lái)的插件 一樣是可以給舊的宿主使用的邮绿。
到這里一個(gè)完成的插件包已經(jīng)出來(lái)了渠旁,剩下的就是 按照基本的加載方式,把這個(gè)插件加載進(jìn)去就順利完成了船逮。
最后在宿主實(shí)現(xiàn)插件管理功能顾腊,這部分純粹就是基本的業(yè)務(wù)邏輯了。
下載校驗(yàn)插件差異包挖胃。 (我們生成新的插件包上次到服務(wù)器杂靶,服務(wù)器就會(huì)與原始插件做差異對(duì)比梆惯,然后生成文件級(jí)別的差異文件,下發(fā)給用戶)
合并差異包對(duì)比插件版本號(hào)吗垮。
加載前黑名單和白名單檢驗(yàn)垛吗。(某些插件版本必須強(qiáng)制加載,某些強(qiáng)制不能加載)
啟動(dòng)時(shí)加載插件資源映射表烁登。(保證一啟動(dòng)就可以查詢到所以資源怯屉,而且這個(gè)反射效率很高,不耗時(shí)饵沧,也不耗內(nèi)存速度也很快)锨络。
插件代碼選擇合適時(shí)機(jī)懶加載。 (因?yàn)榧虞ddex的時(shí)候狼牺,需要耗時(shí)羡儿,5.0以下做opt,5.0以上做oat,而且時(shí)間還不短,所以需要挑合適的時(shí)機(jī)做懶加載是钥。)
最后失受,本文主要是我們?cè)诓寮^(guò)程中遇到一些問(wèn)題的解決方案,其實(shí)每個(gè)解決方案都會(huì)有各自的取舍咏瑟,也無(wú)誰(shuí)優(yōu)誰(shuí)劣拂到,如有更好的方案歡迎下面留言交流。