【騰訊Bugly乾貨分享】Android 插件技術實戰總結

本文來自於騰訊Bugly公衆號(weixinBugly),未經做者贊成,請勿轉載,原文地址:https://mp.weixin.qq.com/s/1p5Y0f5XdVXN2EZYT0AM_Ajava

前言

安卓應用開發的大量難題,其實最後都須要插件技術去解決。android

現今插件技術的使用很是廣泛,好比微信、QQ、淘寶、天貓、空間、攜程、大衆點評、手機管家等等這些你們在熟悉不過的應用都在使用。git

插件技術能夠給項目開發帶來巨大的好處,好比:並行高效開發、模塊解耦、解除單個dex函數不能超過65535的限制、動態更新升級、按需加載等等。github

本文的目的是從一個典型的複雜項目中總結出較爲全面與完整的安卓插件技術。安全

掌握好插件技術,須要以下的安卓基礎和相關知識,例如:微信

  1. Android應用程序安裝,加載過程
  2. Android應用運行機制,生命週期調用原理
  3. Android應用資源編譯打包原理
  4. Android應用讀取資源原理
  5. Android系統AMS、PMS、NMS等系統服務的運做原理
  6. 增量更新
  7. HOOK等技術

插件技術知識領域如圖:網絡

這些技術中每個點都須要大篇幅內容才能徹底講清楚。不過,好在Android是開源的,每個插件技術涉及到的技術點均可以翻閱源碼進行進一步的研究。下面我從當前所負責的一個插件化項目(PACEWEAR手錶助手)經歷,來梳理一下插件技術的應用及核心內容。架構

項目的困惑

PACEWEAR手錶助手原自騰訊TOS的智能穿戴項目。併發

由於目前大部分智能手錶和手環還不能獨立聯網通信,須經過藍牙鏈接手機,藉助手機的網絡來完成一系列業務功能。PACEWEAR手錶助手就是這麼一個手機軟件,幫助智能穿戴設備使用手機網絡,並經過藍牙鏈接的方式完成對智能穿戴設備的各類配置和管理。app

PACEWEAR手錶助手項目開始初期,業務並無大面積鋪開,三四個工程師還算跑的比較順利,隨着項目的進展,主工程框架、登陸、配對、設置、ota、市場、天氣、地圖、運動、音樂、健康管理、支付、應用管理、錶盤管理等功能不斷加入,參與的人也慢慢變多,問題也就多了起來,維護愈來愈困難,總結有以下幾點:

  1. 工程頻繁報方法數超65535
  2. 多個模塊在同一個app中開發代碼耦合,架構冗餘,牽一髮而動全身
  3. 人員效率低下,時間每每花費在溝通,構建問題處理上
  4. 分工不明確,灰色地帶重複邏輯比較多
  5. 業務與業務之間互相調用,不夠獨立
  6. 問題跟進原來越繁瑣,牽扯人數衆多
  7. 功能愈來愈多,目前這種開發方式不可持續
  8. 鏈接的手錶和手環設備種類愈來愈多

針對以上問題雖然咱們考慮過動態加載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手錶助手項目完成插件化而且讓全部插件業務都符合產品需求穩定的運行,對插件框架要求首先就須要作到基於框架開發的插件應用功能對齊原生,這樣框架就須要:

  1. 支持application、四大組件(activity4個LaunchMode)、so、fragment、notification、toast等基礎能力
  2. 支持聯調插件應用
  3. 支持加載本地網頁等
  4. 支持插件自定義控件和樣式
  5. 組件進程配置等原生應用程序的能力;

同時須要這套框架支持將宿主的基礎能力:設備帳號信息、和手錶通信、統計上報、文件傳輸、網絡、ota、控件庫及宿主的資源共享給插件應用;

另外須要將插件運行時間及在宿主中的顯示與宿主徹底解耦。否則插件的調整必然要影響到宿主的代碼調整,這可不是一個明智的落地方案。

綜合上面的要求及項目進行過程當中的調整,通過進一個月的努力,這套框架終於預研成功,正式應用到PACEWEAR手錶助手項目上。

這套框架就叫TwsPluginFramework框架(後面簡稱TPF框架,已經開源:https://github.com/rickdynasty/TwsPluginFramework )。

這套框架相比業界其餘插件框架能力對好比下:

另外Hook系統服務的安全隱患是不可預知的,所以TwsPluginFramework框架儘量少的對系統服務等進行hook處理。

TPF框架原理

插件技術的實現原理是源於Android系統(Android系統自己就是一套插件框架,運行在這個系統之上的應用就是一個個的」插件應用」)對應用的管理機制:安裝(Install)、運行(Running)、卸載(Uninstall)。

運行在TPF框架之上的插件應用和android應用程序又有所不一樣,不一樣點主要有下面幾點:

  1. 應用程序的安裝有android系統負責完成,而插件應用的安裝流程由插件框架負責完成;
  2. 插件應用沒有走系統的安裝流程,組件等信息沒有被註冊到系統裏面,要使插件應用能正常的運行,插件框架須要將這些插件應用內部的組件所有「合法化」;
  3. 插件應用的卸載也不走系統的卸載流程,而是由宿主負責完成的。

上面三個流程中安裝、卸載基本和系統的處理方式是同樣的。而運行就同樣,插件應用程序的運行須要通過「插件框架」這個中間層進行合法化後才能運行在系統裏面,這個合法化過程就須要作不少事,下面會重點講解,先來看一下插件控件的這幾個流程和系統的差異:


系統應用管理機制示例圖


TPF框架插件應用管理機制示意圖

插件框架是插件化項目的核心,它運行在宿主應用裏面。宿主程序在啓動過程當中的第一件事就是將插件框架加載好,以便接下來能夠運行插件應用裏面的業務。

插件框架是插件應用的承載體,負責了插件應用的安裝、運行、卸載管理。因插件應用並非直接安裝在系統裏面,所以插件框架就必須承載android系統的這一系列能力:

  1. 必須本身去識別插件應用並完成拷貝解析工做
  2. 必須給插件應用組件賦予android系統正常的生命才能讓插件應用正常運行。
  3. 必須本身去清理將要卸載的應用數據和正在運行的功能及組件。

剖析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:一、文本按鈕(res配置中英文String) 二、ImageButton(res配置圖標)

更多詳細的內容請移步到https://github.com/rickdynasty/TwsPluginFramework。

TPF框架給項目團隊帶來的好處

當前PACEWEAR手錶助手項目除宿主應用外還有15個(業務)插件應用,PACEWEAR手錶助手僅僅是一個包含基礎功能和插件框架的調度平臺。後續全部新增長的業務都會議插件應用的方式集成進來,宿主基本不用care到底有哪些業務會集成進來。並且當前PACEWEAR手錶助手項目計劃將其餘兩個產品項目合併進來成一個平臺產品。這一切的改善很大部分是TPF帶來的,下面總結了一下TPF框架的好處:

  1. 業務模塊徹底解耦,再也不有調整一個模塊而影響到另外一個甚至多個模塊的狀況。
  2. 各個業務的插件應用開發、編譯各自進行,開發效率大幅度提高,從而縮短開發週期。
  3. 業務插件可單獨動態更新升級,不須要重啓PACEWEAR手錶助手即可生效。
  4. 對於宿主 — PACEWEAR手錶助手來講,能夠按需求加載須要的插件應用,這樣原本多個類似的產品線就能夠合成一個,大幅度下降人力成本。
  5. 再也不被65535困擾。
  6. 團隊協做更和諧。
  7. ...

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,無非兩種狀況:一、類被混淆了 二、類不在當前ClassLoader的可視範圍內。

解決方案:

  1. 混淆的很容有處理,找出來不作混淆就行。
  2. 不在ClassLoader可視範圍內這個就須要注意一下,插件的ClassLoader父類是宿主的ClassLoader,這個天然就不存在插件內部範文不了的狀況。在TPF裏面屢次出現這個問題的主要緣由在共享庫的更新上:TPF提供了一套共享庫,這套庫裏面包括了一套控件、宿主基礎能力、和手錶通信、網絡、文件傳輸等等一系列共性的內容,在開發階段不免會對內容進行變動處理,而有些插件工程若是長時間沒有更新,那就有可能出現ClassNotFound的問題。這樣就須要在調整的時候作兼容,同事插件開發同事及時更新sdk。

Resources$NotFoundException

在TPF裏面,插件是能夠直接訪問宿主提供的共享資源,然而這僅僅只能知足插件內部的邏輯流程。

  1. 但在極少特定機型(好比:vivo)裏面會比較奇葩的存在這類問題。
    解決方案:插件的上下文以及Resources對象(PluginResourceWrapper)都是由TPF構造的。在插件的PluginResourceWrapper內部進行重定向到宿主就能夠了。

  2. 但對於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


更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索