Android插件開發框架、源碼、原理及重點介紹

[url]https://github.com/limpoxe/Android-Plugin-Framework[/url]

#Android-Plugin-Framework
此項目是Android插件開發框架完整源碼及示例。用來通過動態加載的方式在宿主程序中運行插件APK。

已支持的功能:
1、插件apk無需安裝,由宿主程序動態加載運行。

2、支持fragment、activity、service、receiver、contentprovider、jni so、application、組件。

3、支持插件自定義控件、宿主自定控件。

4、開發插件apk和開發普通apk時代碼編寫方式無區別。對插件apk和宿主程序來說,插件框架完全透明,開發插件apk時無約定、無規範約束。

5、插件中的組件擁有真正生命週期,完全交由系統管理、非反射無代理

6、支持插件引用宿主程序的依賴庫、插件資源、宿主資源。

7、支持插件使用宿主程序主題(部分系統暫不支持,如MX5)、系統主題、插件自身主題以及style(插件主題不支持透明)

8、支持非獨立插件和獨立插件(非獨立插件指自己編譯的需要依賴宿主中的公共類和資源的插件,不可獨立安裝運行。獨立插件又分爲兩種: 一種是自己編譯的不需要依賴宿主中的類和資源的插件,可獨立安裝運行;一種是第三方發佈的apk,如從應用市場下載的apk,可獨立安裝運行,這種只做了簡單支持。)

9、支持插件Activity的4個LaunchMode

10、支持插件資源文件中直接通過@xxx方式引用共享依賴庫中的資源

11、支持插件發送notification時在RemoteViews使用插件自定義的佈局資源(只支持5.x及以上)

暫不支持的功能:
1、插件Activity切換動畫不支持使用插件自己的資源。

2、不支持插件申請權限,權限必須預埋到宿主中。

3、不支持第三方app試圖喚起插件中的組件時直接使用插件app的Intent。即插件app不能認爲自己是一個正常安裝的app。第三方app要喚起插件中的靜態組件時必須由宿主程序進行橋接。

開發注意事項
1、插件開發必須要解決插件資源id和宿主資源id重複產生的衝突問題。

解決衝突的方式有如下兩種:

a)通過在宿主中添加一個public.xml文件來解決資源id衝突(master分支採用的方案)

b)通過定製過的aapt在編譯插件時指定id範圍來解決衝突(For-gradle-with-aapt分支採用的方案)
此方案需要替換sdk原生的aapt,且要區分多平臺,buildTools版本更新後需同步升級aapt。
定製的aapt由 openAtlasExtention@github 項目提供,目前的版本是基於22.0.1,將項目中的BuildTools替換到本地Android Sdk中相應版本的BuildTools中,
並指定gradle的buildTools version爲對應版本即可。

2、非獨立插件中的class不能同時存在於宿主和插件程序中,因此其引用的公共庫僅參與編譯,不參與打包,參看demo中的gradle腳本。
目錄結構說明:

1、PluginCore工程是插件庫核心工程,用於提供對插件功能的支持。

2、PluginMain是用來測試的宿主程序Demo工程。

3、PluginShareLib是用來測試非獨立插件的公共依賴庫Demo工程。

4、PluginTest是用來測試的非獨立插件Demo工程。

5、PluginHelloWorld是用來測試的獨立插件Demo工程。

demo安裝說明:

1、宿主程序demo工程的assets目錄下已包含了編譯好的獨立插件demo apk和非獨立插件demo apk。

2、宿主程序demo工程根目錄下已包含一個已經編譯好的宿主demo,可直接安裝運行。

3、宿主程序demo工程源碼可直接編譯安裝運行。

4、插件demo工程:

1、若使用master分支:
直接編譯即可,無特別要求。

2、若使用For-gradle-with-aapt分支:
將openAtlasExtention@github項目提供的BuildTools替換自己的Sdk中相應版本的BuildTools。剩下的步驟照常即可。

3、若使用For-eclipse-ide分支:
需要使用ant編譯,關注PluginTest工程的ant.properties文件和project.properties文件以及custom_rules.xml,若編譯失敗,請升級androidSDK。


待插件編譯完成後,插件的編譯腳本會自動將插件demo的apk複製到PlugiMain/assets目錄下(參看插件工程的build.gradle),然後重新打包安裝PluginMain。
或者也可將插件複製到sdcard,然後在宿主程序中調用PluginLoader.installPlugin("插件apk絕對路徑")進行安裝。
實現原理簡介:
1、插件apk的class

通過構造插件apk的Dexclassloader來加載插件apk中的類。
DexClassLoader的parent設置爲宿主程序的classloader,即可將主程序和插件程序的class貫通。
若是獨立插件,將parent設置爲宿主程序的classloader的parent,可隔離宿主class和插件class,此時宿主和插件可包含同名的class。
2、插件apk的Resource

直接構造插件apk的AssetManager和Resouce對象即可,需要注意的是,
通過addAssetsPath方法添加資源的時候,需要同時添加插件程序的資源文件和宿主程序的資源,
以及其依賴的資源。這樣可以將Resource合併到一個Context裏面去,解決資源訪問時需要切換上下文的問題。
3、插件apk中的資源id衝突

完成上述第二點以後,宿主程序資源id和插件程序id可能有重複而參數衝突。
我們知道,資源id是在編譯時生成的,其生成的規則是0xPPTTNNNN
PP段,是用來標記apk的,默認情況下系統資源PP是01,應用程序的PP是07
TT段,是用來標記資源類型的,比如圖標、佈局等,相同的類型TT值相同,但是同一個TT值
不代表同一種資源,例如這次編譯的時候可能使用03作爲layout的TT,那下次編譯的時候可能
會使用06作爲TT的值,具體使用那個值,實際上和當前APP使用的資源類型的個數是相關聯的。
NNNN則是某種資源類型的資源id,默認從1開始,依次累加。

那麼我們要解決資源id問題,就可從TT的值開始入手,只要將每次編譯時的TT值固定,即可是資
源id達到分組的效果,從而避免重複。例如將宿主程序的layout資源的TT固定爲33,將插件程序
資源的layout的TT值固定爲03(也可不對插件程序的資源id做任何處理,使其使用編譯出來的原生的值), 即可解決資源id重複的問題了。

固定資源id的TT值的辦法也非常簡單,提供一份public.xml,在public.xml中指定什麼資源類型以
什麼TT值開頭即可。具體public.xml如何編寫,可參考PluginMain/public.xml,是用來固定宿主程序資源id範圍的。


還有一個方法是通過定製過的aapt在編譯時指定插件的PP段的值來實現分組:
參考openAtlasExtention@github項目提供的重寫過的aapt指定PP段來實現id分組,代碼見For-gradle-with-aapt分支
4、插件apk的Context和LayoutInfalter

構造一個Context對象即可,具體的Context實現請參考PluginCore/src/com/plugin/core/PluginContextTheme.java
關鍵是要重寫幾個獲取資源、主題的方法,以及重寫getClassLoader方法,再從構造粗來的context中獲取LayoutInfalter
6、插件代碼無約定無規範約束。

要做到這一點,主要有幾點:
1、上訴第4步驟,
2、在classloader樹中插入自己的Classloader,在loadclass時進行映射
3、替換ActivityThread的的Instrumentation對象和Handle CallBack對象,用來攔截組件的創建過程。
4、利用反射修改成員變量,注入Context。利用反射調用隱藏方法。
7、插件中Activity等不在宿主manifest中註冊即擁有完整生命週期的方法。

由於Activity等是系統組件,必須在manifest中註冊才能被系統喚起並擁有完整生命週期。
通過反射代理方式實現的實際是僞生命週期,並非完整生命週期。要實現插件組件免註冊有2個方法。

前提:宿主中預註冊幾個組件。預註冊的組件可實際存在也可不存在。

a、替換classloader。適用於所有組件。
App安裝時,系統會掃描app的Manifest並緩存到一個xml中,activity啓動時,系統會現在查找緩存的xml,
如果查到了,再通過classLoad去load這個class,並構造一個activity實例。那麼我們只需要將classload
加載這個class的時候做一個簡單的映射,讓系統以爲加載的是A class,而實際上加載的是B class,達到掛羊頭買狗肉的效果,
即可將預註冊的A組件替換爲未註冊的插件中的B組件,從而實現插件中的組件
完全被系統接管,而擁有完整生命週期。其他組件同理。


b、替換Instrumention。
這種方式僅適用於Activity。通過修改Instrumentation進行攔截,可以利用Intent傳遞參數。
如果是Receiver和Service,利用Handler Callback進行攔截,再配合Classloader在loadclass時進行映射
8、通過activity代理方式實現加載插件中的activity是如何實現的

要實現這一點,同樣是基於上述第4點,構造出插件的Context後,通過attachBaseContext的方式,
替換代理Activiyt的context即可。
另外還需要在獲得插件Activity對象後,通過反射給Activity的attach()方法中attach的成員變量賦值。

更新:activity代理方式已放棄,不再支持,要了解實現可以查看歷史版本
9、插件編譯問題。

如果插件和宿主共享依賴庫,常見的如supportv4,那麼編譯插件的時候不可將共享庫編譯到插件當中,
包括共享庫的代碼以及R文件,只需在編譯時添加到classpath中,且插件中如果要使用共享依賴庫中的資源,
需要使用共享庫的R文件來進行引用。這幾點在PluginTest示例工程中有體現。

更新:已接入gradle,通過provided方式即可,具體可參考PluginShareLib和PluginTest的build.gradle文件
10、插件Fragment 插件UI可通過fragment或者activity來實現

如果是fragment實現的插件,又分爲3種:
1、fragment運行在宿主中的普通Activity中
2、fragment運行在宿主中的特定Activity中
3、fragment運行在插件中的Activity中

對第2種和第3種,fragmet的開發方式和正常開發方式沒有任何區別

對第1種,fragmeng中凡是要使用context的地方,都需要使用通過PluginLoader.getPluginContext()獲取的context,
那麼這種fragment對其運行容器沒有特殊要求

第1種Activity和第2種Activity,兩者在代碼上沒有任何區別。主要是插件框架在運行時需要區分注入的Context的類型。
11、插件主題 重要實現原理仍然基於上述第2、3點。

12、插件Activity的LaunchMode 要實現插件Activity的LaunchMode,需要在宿主程序中預埋若干個相應launchMode的Activity(預註冊的組件可實際存在也可不存在),在運行時進行動態映射選擇

13、對多Service的支持 Service的啓動模式類似於Activity的singleInstance,因此爲了支持插件多service,採用了和上述第12像類似的做法。

需要注意的問題
1、項目插件化後,特別需要注意的是宿主程序混淆問題。公共庫混淆後,可能會導致非獨立插件程序運行時出現classnotfound,原因很好理解。 所以公共庫一定要排除混淆。

2、android sdk中的build tools版本較低時也無法編譯public.xml文件,因此如果採用public.xml的方式,應使用較新版本的buildtools。

項目已遷移至android studio,eclispe的分支不再更新

更新紀錄:
2015-12-05: 1、修復插件so在多cpu平臺下模式選擇錯誤的問題
2、添加對基於主題style和自定義屬性的換膚功能

2015-11-22: 1、gradle插件1.3.0以上版本不支持public.xml文件也無法識別public-padding節點的文件的問題已解決,因此master分支切回到利用public.xml分組的實現
2、支持插件資源文件直接通過@package:type/name方式引用宿主資源
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章