酷狗 Android App 插件化實施過程

什麼是插件化框架

    插件化框架可以在主程序不重新安裝的情況下,針對單個業務模塊進行加載達到模塊更新的目的,整個加載更新過程,對用戶來說也是無感知的。

    正式因爲這樣,新需求比起傳統更新方式覆蓋率和覆蓋速度都會更高和更快,對於大型開發團隊,各個業務模塊開發小組組也不需要再等所有組的需求開發完統一發布版本,發版本可以單獨針對小組內單個功能發佈了,有了這些優點才使得這1年來插件化框架如此流行的重要原因。

目前網上流行的主流的插件化技術核心主要分兩類:

    一類是360公司開源的DroidPlugin,特點在宿主程序上打造一個純粹的環境,可以讓一個個普通apk(插件)不安裝就可以正常使用,並且插件之前資源和代碼都是相互獨立互相不干擾也不能訪問,爲了達到這樣的目的,框架hook了大量的系統api。

    另一類是從dynamic-load-apk 開始,通過反射少量api,達到插件代碼和資源與宿主合併,達到相互調用的結果,目前大部分都是框架從底層代碼合併和資源合併用的手法都差不多,只是各個框架在這基礎上對插件化的理解不一樣,爲各自的項目做了不少的業務封裝。

常用的類加載方法是:

常見的資源加載方式是:

    目前看來怎麼加載代碼,怎麼加載資源、怎麼動態聲明和啓動android組件。網上大部分開源的開源框架都很好的解決了這些怎麼實現的問題,很完美的做到了從0到1。但是對於一個有幾十個開發人員千萬級別的大型的app來說,單解決了這些問題還是不夠的。插件框架的穩定性、從原有代碼到插件化的遷移成本、後期維護成本等等方面都需要考慮到。
所以兼容性、遷移成本、後期維護成本是我們在插件化選型時最基本的考慮因素。

    首先兼容性、後期維護成本,大家都瞭解到由於android系統的碎片化,android官方提供的api也存在着不少的兼容性問題,況且針對創新能力如此強大的國內手機廠商,國產手機也額外的多了不少兼容性問題。

    具體例子:常用到的資源加載方式,放在vivo的部分手機就不能使用原因是其ROM把系統的Resources封裝成爲了VivoResources直接導致了反射失敗插件資源無法加載,同樣的Nubia的部分手機也是。所以基於這樣在做插件化的時候hook系統的api就應該儘量的少,因爲hook的api不確定性太多了,而且在這部分的開發過程肯定不會有任何文檔提供參考的,遇到問題就幹擼代碼吧。

    處理兼容性的工作量越大其實後期的維護成本就越高,至少如果android一個新版本出來了首先要看的是之前hook的官方api有沒有被改掉。如果有問題還要再針對新的版本尋求新的實現,這部分工作量是非常大的。這也是我們不選擇DroidPlugin的重要原因,從網上的能找到的所有資料並沒有看到DroidPlugin的兼容性能達到多少能適配多少臺手機。但是預判一下DroidPlugin hook了大量的api比起其他框架hook兩個,這部分後續維護成本也是足夠喝一壺的。

    遷移成本,其實很多大型的項目實現插件化,在這個調整的過程中對代碼結構,調用邏輯等等的修改肯定是有的。怎麼保證這個改動是最少的,也是我們的考慮之一畢竟有改動就會產生bug,比較幸運的是,我們從打包腳本上下手在保證傳統的項目結構和邏輯調用不改變的情況下實現模塊插件化。讓插件化先跑起來,在實現之後再讓各個業務小組針對插件化的建議慢慢的完善和封裝插件和宿主之間的協議和約定。

插件化遷移過程:

    首先,看看我們酷狗原有的基礎項目結構:

    項目底層是一個公用library 提供大部分的公共的基礎模塊,酷狗作爲application作爲主程序,其他聽看唱其他業務模塊也作爲一個個library,各個業務組關聯公共模塊和酷狗主程序,在各自的業務模塊下開發、調試。當發版本的時候就統一在打包平臺上讓酷狗關聯所有業務模塊 然後統一打包,這是最常見的項目組成架構業務模塊有項目級別的代碼分離而且業務項目依賴公共基礎庫。

    項目優化目標

    優化後,業務組之前的開發方式完全不變,項目結構對比優化前完整保留,打包之後每個業務模塊是一個個插件可以單獨加載運行,每個插件都是隻包含插件自己的資源和代碼(不包含公共庫),插件可以正常訪問宿主的資源和代碼,只要宿主保留了插件所需的資源和代碼,無論宿主怎麼改變都可以啓動插件。

在這個過程中主要需要解決的問題有:

  1. 打包插件只保留插件本身的代碼,打包後插件不改變任何調用邏輯能順利調用回宿主邏輯。
  2. 決插件和宿主資源衝突問題,插件只保留本身資源,插件能訪問到宿主資源。
  3. 重新編譯之後怎麼保證舊的宿主能支持新的插件。
    首先怎麼把原來跟底層項目依賴的業務模塊 單獨打成一個插件包 只保留業務模塊的代碼

    我們拿聽模塊做個例子先編譯宿主程序也就是酷狗項目和底層基礎庫,一直編譯完javac這時候主項目資源R.java和映射表都可以得到,然後把編譯出來的class打包成common.jar把common項目資源複製到一個空殼項目commonres。
接着修改聽項目的屬性把它從一個library變成一個application,關聯讓它不直接關聯基礎庫,而是讓它關聯 commonn.jar 和 commonres,其中common.jar做提供編譯。
按照這樣編譯下去,聽項目編譯出來的apk就只包含自己的代碼了。

    接下來解決資源問題,正常的資源查找方式

    應用層獲取資源 是用資源id直接去獲取,Resources先根據我們的id去資源映射表去查找這個資源的名稱是,拿到資源名稱不對應文件的資源只需要執行從資源ID到資源名稱的轉換即可,而對應有文件的資源還需要根據資源名稱來打開對應的文件。經過反射 resources 裏面包含了多個映射表的目錄,查找的時候會按照順序先查宿主再查各個插件的映射表。

    資源衝突問題因爲上面項目結構調整之後,插件和宿主都是application編譯時候就會出資源id相同,插件做資源id查找的時候就會有可能查找到宿主的資源,所以只要修改了resources.arsc和代碼層用到的R.java的id就可以解決衝突了常見的修改方式是修改插件的id的pp段。

    程序編譯到這裏,修改關聯後的插件項目還保留了一份commonres資源,跟宿主的程序上的是一摸一樣的,能不能修改資源id來解決呢,答案是肯定的,因爲插件和宿主查找資源的邏輯是一樣的,只要插件代碼調用中相同的資源id即R.java裏面的id,修改爲宿主資源id,資源查找的時候就會順利的到宿主的resources.arsc去查找資源了。

最後,刪除插件resources.arsc多餘的資源id和插件多餘的資源文件。這樣下來 最終得出的插件包 就是隻含有插件代碼和插件資源的 而且還能隨意訪問宿主資源和代碼。

最後我們看看整體的編譯流程。

    與微信資源混淆工具的兼容性問題

    插件化工具主要在編譯時修改ID和去除其他多餘資源,資源混淆工具主要是把名稱和路徑改短不修改ID,所以並不衝突。
只要保證讀寫操作都是嚴格按照 resources.arsc 的格式去寫就可以了。

接下來最後一個問題,重新編譯之後怎麼保證舊的宿主能支持新的插件,簡單說就是多程序怎麼一起 做代碼混淆,怎麼保持宿主的資源ID。

    多項目一起混淆:

    我們選擇的是統一做混淆,爲什麼不能先混淆整體混淆一個項目然後再混淆第二個項目的時候保持用上個項目的mapping 繼續混淆,一直這樣編譯下去?

    主要因爲插件和宿主公共庫之間並沒有固定接口,單獨混淆原來直接關聯調用的方法就會被混淆移除掉。
我們還記得插件模塊和common基礎模塊本來就是直接關聯、直接調用的,後面我們改變項目結構讓插件獨立出來了,但是這部分調用還是存在的。
一旦單獨混淆他們之間關聯的代碼就會被移除掉,宿主公共庫的final靜態變量混淆後也會消失,插件也沒法調用得到。

    其實正常來說,宿主和插件之間的調用本來就是需要先有固定的接口做好解耦 規範好所有的調用,宿主提供一套完整的api給插件使用,然後混淆的時候 keep好各自邊界 。 這樣對於後續插件版本更新和管理纔是最正確的。
爲什麼這個問題到現在才聊呢,因爲讓各個業務模塊組爲了插件化然後去封裝接口,等他們解耦封裝好纔來做的話時間太長了,所以我們先用這種方式讓他們不需要做任何封裝和解耦就能用,後續再要求他們慢慢的規範好這部分的接口。

    怎麼keep資源問題

    我們知道資源id的生成是按照資源名稱隨機生成的,一旦添加或者修改了某個資源名稱所有的資源id都有可能改變。 如果不能固定資源id 每次編譯都id都變的話插件也無法下發給用戶使用。

    解決方案是在編譯的時候根據宿主R.java的生成ids.xml和public.xml下次編譯把ids.xml和public.xml放到宿主的/res/value目錄下編譯可保持id不變,這樣即使下次宿主的其他資源改變了,只要插件用到的所有資源沒有改變,新打出來的插件 一樣是可以給舊的宿主使用的。

    到這裏一個完成的插件包已經出來了,剩下的就是 按照基本的加載方式,把這個插件加載進去就順利完成了。

    最後,本文主要是我們在插件化過程中遇到一些問題的解決方案,其實每個解決方案都會有各自的取捨,也無誰優誰劣,如有更好的方案歡迎下面留言交流。


轉載用於學習交流,如冒犯,請聯繫刪除。


原文


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章