整合一個第三方相簿功能,只需整合一個外掛APK到專案中,無需整合額外程式碼,並且支援隨時更新相簿功能,無需釋出版本更新,無需AndroidManifest中宣告四大元件,這就是外掛化。
外掛化可利用性很廣,但事實上大多數開發者,因為未知而放棄使用,所以本篇將深入淺出帶你瞭解外掛化原理,從基礎到實現,外掛化不再是你陌生的領域。
本篇主要涉及到:
- 一、Activity/Service的啟動原理和流程。
- 二、外掛化實現原理。
- 三、DiDi開源VirtualApk原始碼解析(Activity/Service)。
- VirtualApk優化反射帶來損耗的小技巧。
ps:如果你對此(一、二)已經十分了解,請自行略過。
一、 Activity/Service啟動流程
Activity和Service的啟動流程十分複雜,一個startActivity
的背後是無數的邏輯實現,這裡不深入討論,但需要理解這個流程,因為外掛化是在流程上動手腳,以達到繞過系統限制的目的。
下方圖片是Activity啟動的簡化流程,可以看到,從Instrumentation
開始,到ActivityManagerService
和ActivityThread
結束,啟動一個Activity,流程並不簡單。
在Instrumentation
在execStartActivity
開始啟動,到通過checkStartActivityResult
校驗Activity是否在Manifest中宣告,從圖中可以看出,流程還是相當繁瑣的。
(Activity啟動流程詳見圖片)
下方圖片是Service啟動的簡化流程,同樣可以看到,ActivityManagerService
和ActivityThread
同樣起到了關鍵性的作用。外掛化的關鍵,就在於Instrumentation
、ActivityManagerService
和ActivityThread
。
(Service啟動流程詳見圖片)
為了更好理解外掛化,如下圖,是幾個關鍵類的對應關係與實際作用,有點S/C的味道。它們的通訊是通過IBinder
,實現程式通訊的,可以看出,啟動Activity和Service,ActivityThread
和ActivityManagerService
是關鍵,並且上面我們知道,Instrumentation
是Activity的啟動入口,所以實現外掛化的流程,便可以在這些關鍵類上開刀。
(下圖在外掛化實現中起到關鍵作用)
提前說明
好了,帶了一波基礎姿勢的節奏,稍安勿躁,先這裡在補充幾個概念,如果你已經習得,可以跳過:
-
Hook:攔截某個內部流程,在其中做某些修改,以實現自己的邏輯。
-
Instrumentation:每個Activity都有一個
Instrumentation
物件,它是在Activity啟動是被賦予的Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity();
這便是startActivityForResult的啟動,同時返回啟動結果。 -
佔坑:宣告一個不存在的Activity,如:
<activity android:name=".A$1" android:launchMode="standard"/>
,這樣啟動.A$1這個Activity可以欺騙系統檢測,然後再將外掛Activity注入到.A$1這個坑位中。
二、外掛化實現原理。
外掛化的實現就是在於載入、繞過系統限制、啟動和管理外掛等過程。按照VirtualApk的實現,大致流程為:
1、初始化Hook住Instrumentation
和ActivityThread
等。通過PackageParser(外掛apk包資訊)、AssetManager(資原始檔Resources)、ClassLoader等載入一個Apk外掛。
2、啟動外掛Activity:提前在主APP中佔有坑位,通過替換Intent中的targetActivity,開啟佔坑宣告的A$Activity,然後繞過AndroidManifest檢測,再攔截newActivity方法中恢復targetActivity。
3、啟動外掛Service:通過啟動一個代理Service統一管理,攔截所有Service方法,修改為startService到代理Service,在代理Service的onStartCommond
統一管理,建立/停止目標service 。
三、VirtualApk原始碼解析
1、初始化
初始化過程中,VirtualApk 建立了PluginManager ,並且hook住了Instrumentation
和SystemService,如下圖所示。
如下圖所示,VrutalApk通過Instrumentation
建立了一個VAInstrumentation
物件,VAInstrumentation
是一個繼承Instrumentation
的類。
將VAInstrumentation
反射插入到ActivityThread
中,這樣系統接下來關於Instrumentation
的操作,就會回到VAInstrumentation
中,被VrtualApk接管。
這裡是如何拿到Instrumentation
的?
因為在ActivityThread
內部有一個sCurrentActivityThread
靜態變數。如下圖,通過反射sCurrentActivityThread
我們可以獲取當前ActivityThread
,而ActivityThread
的公開方法getInstrumentation
即可拿到Instrumentation物件。
另外,上方圖1還有設定HandlerCallback
的流程,其實就是攔截了ActivityThread
中的mH
這個Handler的Callback,從【 一、 Activity/Service啟動流程】流程圖可以看到,mH的handleMessage處理很多Activity的啟動狀態。
如下圖, 是Hook Service的流程,如圖中註釋所示,通過ActivityManagerNative
的getDefault
,拿到AndroidManagerService
(詳見啟動流程圖),而VirtualApk通過自定義ActivityManagerProxy
,重新生成了一個IActivityManager
,然後注入回AndroidManagerService
中,這樣接管了系統啟動、管理service等操作。
2、載入外掛Apk
載入外掛APK是通過PluginManager
的loadPlugin
方法,如下圖所示,此處就是將apk拆開,解析,讀取,載入,組裝為LoadedPlugin並儲存,以方便後面管理與使用。
此處對Apk進行了複雜的解析、載入、合併等操作,大致流程如下:
- 解析apk的相關包資訊、判斷是否載入過apk。
- 建立一些外掛工具類。
- 通過
AssetManager
建立Resource
物件,平臺用AssetManager建立出Resource,判斷是否和宿主Apk合併資源。 - ClassLoader 根據外掛APK路徑建立loader,判斷是否合併loader中的dex,合併nativeLIbraryDirectories。
- 將so複製到mNativeLibDir路徑。
- 儲存Instrumentation、Activities、Services、Providers , 註冊Broadcast等。
- 建立出Apk的Application,並call Application onCreate。
3、啟動外掛Activity
那麼是時候啟動外掛Activity了。通過startActivity便可以啟動。從上面的流程我們知道啟動是從Instrumentation.execStartActivity();
開始的,而系統的Instrumentation
已經被VAInstrumentation
替換,其中VAInstrumentation
重寫了幾個關鍵方法:
- execStartActivity:入口。
- newActivity:建立。
- callActivityOnCreate:通知。
- handleMessage:處理。
沒錯,如下圖,在啟動Activity的入口處,VirtualApk攔截了請求,然後根據Intent的引數,去匹配plugin中的Activity坑位,之後替換Intent中的Activity,以此來達到欺騙系統的效果。
但是,因為這個Activity的物件了實際上並不存在,最終我們需要啟動的是實現了的targetActivity,所以需要攔截Instrumentation
的第二個方法newActivity
,因為在ActivityTread
的performLaunchActivity
中,會呼叫Instrumentation
的 newActivity
。
在newActivity
中,如下圖,類沒有找到時(坑位類肯定找不到啦),那麼就去獲取原本儲存在Intent的目標Activity,然後呼叫建立VAInstrumentation
時儲存的Instrumentation
(mBase)去建立Activity。
Activity雖然建立好了,但是它對應的資源和context都還不對,所以我們需要在Activity的OnCreate之前完成好Resource的注入。前面載入Apk時,這些資源都儲存在Plugin
中。所以我們攔截callActivityOnCreate
方法,如下圖,將Activity的Context、Application、Reource,都替換成Plugin中對應的物件。一個Activity就這樣繞過AndroidManifest啟動起來了。
4、啟動外掛Service
startService啟動Service時,還記得上面我們通過ActivityManagerProxy
生成IActivityManager
嗎?它主要攔截了Service相關啟動和停止等方法,然後將其都轉化為對應的startService方法,指向代理Service。因為startService方法的特性,他們最終都會在代理Service的onStartCommand
中被統一處理。
如下圖,是ActivityManagerProxy
,其中invoke攔截了所有相關的服務請求,並做了轉化處理,下面以startService為例。
startService這裡,主要便是提取原本目標service資訊,然後轉化為代理Service,傳送到代理Service,下方圖片為啟動流程和轉化流程。
如下圖,在代理service中,根據請求型別,代理service會通過classLoader載入來建立service,並操作其attach、onCreate、onStartCommand等,讓service工作起來。
自此Activity和Service都成功啟動了,是不是對外掛化有了不一樣的瞭解?
5、AndroidStub
容許這裡插入這一塊,安利下Virtual中的AndroidStub模組,如下圖
因為都用反射很浪費效能,所以有了AndroidStub
,它是用來欺騙編譯器的。正常情況下你想操作ActivityThread
就會出現如下圖情況,因為它是一個@hide
類,這時候除了反射得到ActivityThread
,你還需要再反射需要執著方法才能執行,這在一定程度會損耗一些效能。
但是如下圖,VirtualApk通過AndroidStub
,模擬原始碼建立瞭如ActivityThread
類,這裡你就可以如圖正常使用ActivityThread
了,而AndroidStub
中的ActivityThread
,其實只是定義了和原碼中一摸一樣的方法,並沒有其他實現。
因為CoreLibrary
依賴AndroidStub
使用的是provided
,因為provided
依賴是不打包依賴包,而是執行時提供,所以成功欺騙了編輯器,以此提高了效能。很神奇吧?
終於結束了,如果你看到了這裡,相信你是一個很有耐心的同志!當然外掛化還是其他實現方式,如Replugin,只Hook住了ClassLoader,流程更加複雜,如有什麼建議和疑問,歡迎留言討論。
VirtualApk:github.com/didi/Virtua…
個人github:github.com/CarGuo