Android 插件技术实战总结
前言
安卓應用開發的大量難題,其實最后都需要插件技術去解決。
現今插件技術的使用非常普遍,比如微信、QQ、淘寶、天貓、空間、攜程、大眾點評、手機管家等等這些大家在熟悉不過的應用都在使用。
插件技術可以給項目開發帶來巨大的好處,比如:并行高效開發、模塊解耦、解除單個dex函數不能超過65535的限制、動態更新升級、按需加載等等。
本文的目的是從一個典型的復雜項目中總結出較為全面與完整的安卓插件技術。
掌握好插件技術,需要如下的安卓基礎和相關知識,例如:
Android應用程序安裝,加載過程
Android應用運行機制,生命周期調用原理
Android應用資源編譯打包原理
Android應用讀取資源原理
Android系統AMS、PMS、NMS等系統服務的運作原理
增量更新
HOOK等技術
插件技術知識領域如圖:
這些技術中每一個點都需要大篇幅內容才能完全講清楚。不過,好在Android是開源的,每一個插件技術涉及到的技術點都可以翻閱源碼進行進一步的研究。下面我從當前所負責的一個插件化項目(PACEWEAR手表助手)經歷,來梳理一下插件技術的應用及核心內容。
項目的困惑
PACEWEAR手表助手原自騰訊TOS的智能穿戴項目。
因為目前大部分智能手表和手環還不能獨立聯網通訊,須通過藍牙連接手機,借助手機的網絡來完成一系列業務功能。PACEWEAR手表助手就是這么一個手機軟件,幫助智能穿戴設備使用手機網絡,并通過藍牙連接的方式完成對智能穿戴設備的各種配置和管理。
PACEWEAR手表助手項目開始初期,業務并沒有大面積鋪開,三四個工程師還算跑的比較順利,隨著項目的進展,主工程框架、登錄、配對、設置、ota、市場、天氣、地圖、運動、音樂、健康管理、支付、應用管理、表盤管理等功能不斷加入,參與的人也慢慢變多,問題也就多了起來,維護越來越困難,總結有如下幾點:
針對以上問題雖然我們考慮過動態加載jar、Html5等措施來緩解,但最終還是沒能徹底從根本上解決這些問題,一直在苦惱著整個項目團隊...
尋找適合項目的插件框架
這種情況下我們很快意識到需要引入插件化的開發模式,才能一勞永逸地這解決這一系列問題。
引入Dynamic-load-apk插件框架
團隊在2015年中開始引入了Dynamic-load-apk(后面簡稱DyLA)框架,這套框架是從App應用層解決加載插件的問題:創建一個繼承自Activity的ProxyActivity類,然后讓插件中的所有Activity都繼承自ProxyActivity,并重寫Activity所有的方法。然而在功能上,僅支持Activity組件,這個是這套框架最大的短板;另外基于這套框架進行的插件應用開發,依賴條件復雜[需要內置jar包,組件必須實現ProxyActivity的所有接口]、調試困難等各種問題。重重約束是的項目插件化業務進展及其緩慢,比如支付模塊兩個同事開發了兩個月最后發現很多需求沒法實現,最終不得不放棄插件化;健康模塊開發不到兩周的同事開始抓狂,被各種問題不斷折騰著(為啥不能聯調、為什么這個要特殊處理、為什么這里資源找不到等等)。最后僅有健康、Yiya語音極少數幾個模塊勉強插件化。隨著項目的進展,業務模塊的不斷增多,當初的問題不但沒有得到解決,反而增加了對DyLA模塊的維護,這個狀態一直持續到了2016下半年9月。
預研適合項目的插件框架
PACEWEAR手表助手項目團隊在9月份初對比了一些開源插件框架的能力:
同時評估了他們的優缺點,最后確定基于APF進行開發一套適合PACEWEAR手表助手的插件框架。
然而,僅是支持application和四大組件還遠遠不能滿足PACEWEAR手表助手項目的要求,PACEWEAR手表助手有二十多個業務模塊,第一批需要進行插件化的就有十五個,由不同的同事進行開發負責,而且有些業務還需要和第三方進行交互對接...因此,團隊要能高效的將PACEWEAR手表助手項目完成插件化并且讓所有插件業務都符合產品需求穩定的運行,對插件框架要求首先就需要做到基于框架開發的插件應用功能對齊原生,這樣框架就需要:
支持application、四大組件(activity4個LaunchMode)、so、fragment、notification、toast等基礎能力
支持聯調插件應用
支持加載本地網頁等
支持插件自定義控件和樣式
組件進程配置等原生應用程序的能力;
同時需要這套框架支持將宿主的基礎能力:設備賬號信息、和手表通訊、統計上報、文件傳輸、網絡、ota、控件庫及宿主的資源共享給插件應用;
另外需要將插件運行時間及在宿主中的顯示與宿主完全解耦。不然插件的調整必然要影響到宿主的代碼調整,這可不是一個明智的落地方案。
綜合上面的要求及項目進行過程中的調整,經過進一個月的努力,這套框架終于預研成功,正式應用到PACEWEAR手表助手項目上。
這套框架就叫TwsPluginFramework框架(后面簡稱TPF框架,已經開源:https://github.com/rickdynasty/TwsPluginFramework?)。
這套框架相比業界其他插件框架能力對比如下:
另外Hook系統服務的安全隱患是不可預知的,因此TwsPluginFramework框架盡可能少的對系統服務等進行hook處理。
TPF框架原理
插件技術的實現原理是源于Android系統(Android系統本身就是一套插件框架,運行在這個系統之上的應用就是一個個的”插件應用”)對應用的管理機制:安裝(Install)、運行(Running)、卸載(Uninstall)。
運行在TPF框架之上的插件應用和android應用程序又有所不同,不同點主要有下面幾點:
應用程序的安裝有android系統負責完成,而插件應用的安裝流程由插件框架負責完成;
插件應用沒有走系統的安裝流程,組件等信息沒有被注冊到系統里面,要使插件應用能正常的運行,插件框架需要將這些插件應用內部的組件全部“合法化”;
插件應用的卸載也不走系統的卸載流程,而是由宿主負責完成的。
上面三個流程中安裝、卸載基本和系統的處理方式是一樣的。而運行就一樣,插件應用程序的運行需要經過“插件框架”這個中間層進行合法化后才能運行在系統里面,這個合法化過程就需要做很多事,下面會重點講解,先來看一下插件控件的這幾個流程和系統的差別:
系統應用管理機制示例圖
TPF框架插件應用管理機制示意圖
插件框架是插件化項目的核心,它運行在宿主應用里面。宿主程序在啟動過程中的第一件事就是將插件框架加載好,以便接下來可以運行插件應用里面的業務。
插件框架是插件應用的承載體,負責了插件應用的安裝、運行、卸載管理。因插件應用并不是直接安裝在系統里面,因此插件框架就必須承載android系統的這一系列能力:
必須自己去識別插件應用并完成拷貝解析工作
必須給插件應用組件賦予android系統正常的生命才能讓插件應用正常運行。
必須自己去清理將要卸載的應用數據和正在運行的功能及組件。
剖析TPF框架
下面我就從加載TPF插件框架、安裝插件應用程序、運行插件應用程序、卸載插件應用程序四個環節詳細講述一下TPF框架內幕。
加載TPF插件框架
宿主程序在啟動過程中的首要事情就是將插件框架加載好,以便接下來可以將插件應用正常的運作起來。插件框架在整個項目工程中扮演的是一個極其核心的角色:除了負責所有插件應用的安裝卸載,還需要賦予插件應用組件一個合法的身份。
在android系統中,應用程序運行的背后有很多服務在維持這些組件的運作,比如ActivityManagerService、PackageManagerService、WindowManagerService、NotificationManagerService等以及應用程序背后的ActivityThread等等,這些都是TPF框架需要Hook的范圍內容。
具體的流程如下:
為了讓插件應用內部的組件合法化,插件框架需要對應用程序做一些HOOK處理,以便讓插件的組件能正常運行。
安裝插件應用程序
插件應用程序要能夠運行在宿主里面,首先得經過安裝這個過程讓宿主知道當前這個插件應用的信息,然后插件框架就會將當前插件解壓拷貝到指定目錄以便后面的運行需要。
在TwsPluginFramework框架中,插件包就是一個應用程序apk。對插件信息的收集方式和系統一樣,通過解析AndroidManifest.xml來收集應用信息,包括版本、sdk、application、四大組件等等。
具體的流程如下:
這個過程基本和應用程序的安裝過程無異,只是插件應用程序的顯示圖標等內容直接由插件框架在解析的過程中獲取并拷貝到私有目錄下面。
運行插件應用程序
運行插件內部的任何組件之前,首先得加載好插件的代碼和資源,然后就在構建插件的上下文以及Application等信息,TwsPluginFramework框架啟動插件的流程圖如下:
類加載
在TwsPluginFramework框架中,通過DexClassLoader來加載插件應用的代碼, DexClassLoade的使用示意圖如下:
TwsPluginFramework框架在構建插件應用的ClassLoader的時候會指定其父ClassLoader為宿主的。這樣插件內部就可以直接訪問宿主的代碼內容。
資源加載
在TwsPluginFramework框架中資源的加載和系統一樣,也是通過AssetManager的addAssetPath/addAssetPaths方法進行處理的,只是這兩個方法是隱藏的,得用通過反射來調用。
在TwsPluginFramework框架里,在構建插件應用上下文Resource的時候,將宿主的資源與插件的資源合并在一起了。這樣做的好處就是插件應用可以共享宿主的資源數據。
對于插件框架來說,如何處理插件資源和宿主資源是一個非常糾結的選擇:
然而,資源合并方案就得處理資源ID沖突問題,在TwsPluginFramework框架里面是通過修改AAPT來指定插件應用資源的package id,從而達到區分宿主和插件的資源id的目的。
生命周期
插件應用程序是運行在插件框架這個中間層上面的,而非直接運行在android系統里的。也正因為如此,插件框架就需得自己去完成應用程序包的內容加載以及組件的生命賦予工作。
在Android的世界里面,應用的組件是有“生命”的,比如:activity、service、BroadcastReceive、application等,這種“生命”是由Android系統所賦予的。
對于應用程序來說,只要在AndroidManifest.xml里面注冊便可以輕易獲得這種生命,因為應用的I(安裝)R(運行)U(卸載)是由安裝系統來承載的。而對于插件應用的I(安裝)R(運行)U(卸載)是由運行在宿主里面的插件框架來承載的。僅因這一點的差別,使得插件應用內部的組件如果不做一些特殊處理,系統是不會給予它們“生命”的。
在TwsPluginFramework框架里面,插件的組件是擁有真正生命周期,完全交由系統管理、非反射代理。插件應用并沒有經過系統安裝,內部的組件并沒有注冊到系統里面。那TPF是怎么做到讓插件里面的組件也能讓系統給沒被注冊的插件應用組件擁有完整生命周期的?
答案就在TPF框架里面的兩個計策: 偷梁換柱、瞞天過海。
瞞天過海:在宿主中提前申明好多個組件,在向系統請求啟動的過程中用這些預先申明號的組件去做請求,等系統的校驗流程結束后換回成目標的插件組件,從而達到瞞過系統。
瞞天過海環節需要在宿主中申明好用來做替身的receiver、service(多個)[獨立進程的單獨配置多個]、activity(多個) [不同single模式的單獨配置多個]。
偷梁換柱:為了讓系統能夠按著我們的意愿在組件啟時將目標插件組件替換成宿主中預先申明號的對應組件,等系統校驗環節過了在換回成目標插件組件,我們就需要替換掉應用程序空間一些重要的處理對象,比如:ActivityThread里面負責應用程序與系統交互的Instrumentation對象以及組件處理流程的回調Handler.Callback等。
下面就以基本組件的啟動流程來描述一下這兩個計策:
Activity
Activity生命周期大家在熟悉不過了,可是在onCreate之前系統做很多你所不知道的事。
從點擊桌面圖標(或者出發啟動一個activity)到這個應用activity組件進入onCreate()。這個環節是解決插件組件activity完整生命周期的關鍵。這個環節在TwsPluginFramework框架內部的處理流程:
從開始執行execStartActivity到最終將Activity對象new出來這個過程,系統層會去校驗需要啟動的activity的合法性[是否有在應用的AndroidManifest.xml里面注冊]以及按啟動要求創建activity對象。了解了這點就可以很好的繞過系統的約束,達到需要的目的。
Service
stopService、bindService以及sendBroadcast的流程和startService是一樣的,這里就不贅述了。
卸載插件應用程序
當前插件應用要下架或者需要更新到新版本的時候,就需要將當前的插件應用給卸載掉。這個過程和Android系統卸載應用程序是一樣的。
和插件應用安裝過程相反,這個過程就是清理記錄在宿主插件框架里面的信息、刪除代碼和資源同時停止所有該插件正在運行的組件及服務。
流程如下:
顯示協議框架
TPF框架將插件在宿主中的調用時機及顯示入口完全與宿主解耦,也就是說插件應用的調整不需調整宿主程序的任何代碼。這些都歸功于TPF提供了一套顯示協議框架,插件應用只需要知道顯示協議的使用就可以,顯示協議(可以根據項目需求自定義,下面是輸出給PACEWEAR手表助手插件應用項目的規范) 的概要如下:
顯示位置pos: 1 Hotseat; 2 MyWatchFragment; 3 ActionBarMenu; 4 其他 分隔符: # 分割DisplayConfig; @ 分割DisplayConfig的屬性; = 屬性賦值; / 分割屬性值 圖標資源icon:統一使用 模塊名_[hotseat or watch_fragment or menu]_描述信息.png 配置在AndroidManifest.xml不需要帶后綴。 【normal/focus/press/...】 標題title:中文/英文 也可以只配置一個 顯示內容content:如果是fragment 直接配置name,其他的配置類名信息內容類型ctype:1 fragment; 2 activity; 3 service; 4 application; 5 view 插件啟動時機: 1 手動觸發 2 隨DM啟動 3 配對成功后插件依賴: 1 已安裝的app 2 已安裝的插件ActionBar 配置只在顯示位置是Hotseat的前提下可用 ActionBar標題ab-title:actionbar標題 中文/英文 也可以只配置一個 暫不支持subTitle ActionBar右側按鈕顯示內容ab-rbtncontent:actionbar右側按鈕點擊觸發顯示內容 ActionBar右側按鈕顯示內容類型ab-rbtnctype: ?觸發顯示內容 的類型 1 fragment; 2 activity; 3 service; 4 application; 5 view(當前只支持activity,如果是activity可以不配置) ActionBar右側按鈕內容ab-rbtnres: 顯示在按鈕上的內容根據類型不同而不一樣(類型1 文本;類型2 圖標 ActionBar右側按鈕內容ab-rbtnrestype:1、文本按鈕(res配置中英文String) 2、ImageButton(res配置圖標)更多詳細的內容請移步到https://github.com/rickdynasty/TwsPluginFramework。
TPF框架給項目團隊帶來的好處
當前PACEWEAR手表助手項目除宿主應用外還有15個(業務)插件應用,PACEWEAR手表助手僅僅是一個包含基礎功能和插件框架的調度平臺。后續所有新增加的業務都會議插件應用的方式集成進來,宿主基本不用care到底有哪些業務會集成進來。而且當前PACEWEAR手表助手項目計劃將其他兩個產品項目合并進來成一個平臺產品。這一切的改善很大部分是TPF帶來的,下面總結了一下TPF框架的好處:
業務模塊完全解耦,不再有調整一個模塊而影響到另一個甚至多個模塊的情況。
各個業務的插件應用開發、編譯各自進行,開發效率大幅度提升,從而縮短開發周期。
業務插件可單獨動態更新升級,不需要重啟PACEWEAR手表助手便可生效。
對于宿主 — PACEWEAR手表助手來說,可以按需求加載需要的插件應用,這樣本來多個相似的產品線就可以合成一個,大幅度降低人力成本。
不再被65535困擾。
團隊協作更和諧。
...
TPF框架一路走過的經典Bug
Theme/Style異常
Log截圖:
這類問題主要出現在第一套區分資源ID方案(通過public.xml的public-padding特性來處理)上,這類問題的根本原因是:android系統處理應用資源,在底層處理ResourceTable的bag資源的出現了異常。
Android資源管理機制是一個非常復雜的課題(包括:資源打包、資源加載、資源尋找,每一塊又分java層和C層),有興趣興趣的可以去翻一下源碼,在線地址:http://androidxref.com?。簡單來說這個問題:“就是style不同于其他資源,style本身是不創建資源的,它僅僅是一個資源的應用集合,而系統訪問資源是通過偏移量的方式去獲取資源。這種方式在同一個packageID的段來說,只要style是連續的就ok。但是如果不符合這個要求,那上面的問題就會出現。”
在TPF的第一套區分資源ID方案中,通過public.xml的public-padding特性來區分資源id,不難做到讓style連續,但要做到多個插件工程并發的情況下做到連續卻是基本不可能。這也是為什么TPF放棄了這套方案的原因。
明白了其中的原因,要解決這類問題也就簡單了。
解決方案:盡可能的符合系統規則,在同一個packageID段內讓相同type的資源ID連續就行。當前通過修改aapt來指定資源的packageID是一個很好的方式。
ClassNotFound
嚴格來說這個不是TPF框架的問題,TPF框架在處理加載代碼上完全是按著系統的規格要求。把這類問題拿出來放這里,只是因為在項目開發過程中插件工程反饋之類問題不較多。
出現ClassNotFound,無非兩種情況:1、類被混淆了 2、類不在當前ClassLoader的可視范圍內。
解決方案:
混淆的很容有處理,找出來不做混淆就行。
不在ClassLoader可視范圍內這個就需要注意一下,插件的ClassLoader父類是宿主的ClassLoader,這個自然就不存在插件內部范文不了的情況。在TPF里面多次出現這個問題的主要原因在共享庫的更新上:TPF提供了一套共享庫,這套庫里面包括了一套控件、宿主基礎能力、和手表通訊、網絡、文件傳輸等等一系列共性的內容,在開發階段難免會對內容進行變更處理,而有些插件工程如果長時間沒有更新,那就有可能出現ClassNotFound的問題。這樣就需要在調整的時候做兼容,同事插件開發同事及時更新sdk。
Resources$NotFoundException
在TPF里面,插件是可以直接訪問宿主提供的共享資源,然而這僅僅只能滿足插件內部的邏輯流程。
但在極少特定機型(比如:vivo)里面會比較奇葩的存在這類問題。
解決方案:插件的上下文以及Resources對象(PluginResourceWrapper)都是由TPF構造的。在插件的PluginResourceWrapper內部進行重定向到宿主就可以了。
但對于Notification等這些系統的通用服務也是會出這類問題。這些服務內部通過id獲取資源,最終是會落到宿主的上下文上面。而對于宿主來說,插件的資源是不可見,自然就沒法通過插件的resID來獲取插件的資源。
解決方案:像Notification這類的系統服務,如果需要傳遞資源id到系統里面進行處理獲取資源,一律使用宿主的資源id。
備注:情況②沒法用情況①的方式進行處理的原因這里簡單描述一下:應用程序在啟動的過程中,在application被關聯之前Resources就創建好了,而且這個Resources對象在ContextImpl里面還是final類型,這樣再java層就沒法實施偷梁換柱的方式進行替換處理。
項目進展過程中更多的bug記錄請移步:https://github.com/rickdynasty/TwsPluginFramework_Doc
TwsPluginFramework(TPF)框架現已經開源:
https://github.com/rickdynasty/TwsPluginFramework
原文地址: https://mp.weixin.qq.com/s/1p5Y0f5XdVXN2EZYT0AM_A
總結
以上是生活随笔為你收集整理的Android 插件技术实战总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PWN2OWN 2017 Linux 内
- 下一篇: android sina oauth2.