Android外掛化快速入門與例項解析(VirtualApk)

戀貓de小郭發表於2019-03-04

  整合一個第三方相簿功能,只需整合一個外掛APK到專案中,無需整合額外程式碼,並且支援隨時更新相簿功能,無需釋出版本更新,無需AndroidManifest中宣告四大元件,這就是外掛化。

  外掛化可利用性很廣,但事實上大多數開發者,因為未知而放棄使用,所以本篇將深入淺出帶你瞭解外掛化原理,從基礎到實現,外掛化不再是你陌生的領域。

本篇主要涉及到:

  • 一、Activity/Service的啟動原理和流程。
  • 二、外掛化實現原理。
  • 三、DiDi開源VirtualApk原始碼解析(Activity/Service)。
  • VirtualApk優化反射帶來損耗的小技巧。

ps:如果你對此(一、二)已經十分了解,請自行略過。

一、 Activity/Service啟動流程

  Activity和Service的啟動流程十分複雜,一個startActivity的背後是無數的邏輯實現,這裡不深入討論,但需要理解這個流程,因為外掛化是在流程上動手腳,以達到繞過系統限制的目的。

  下方圖片是Activity啟動的簡化流程,可以看到,從Instrumentation開始,到ActivityManagerServiceActivityThread結束,啟動一個Activity,流程並不簡單。

  在InstrumentationexecStartActivity開始啟動,到通過checkStartActivityResult校驗Activity是否在Manifest中宣告,從圖中可以看出,流程還是相當繁瑣的。
  
Activity啟動流程詳見圖片

Activity簡化啟動流程圖
Activity簡化啟動流程圖

  下方圖片是Service啟動的簡化流程,同樣可以看到,ActivityManagerServiceActivityThread同樣起到了關鍵性的作用。外掛化的關鍵,就在於InstrumentationActivityManagerServiceActivityThread

Service啟動流程詳見圖片

Service簡化啟動流程圖
Service簡化啟動流程圖

  為了更好理解外掛化,如下圖,是幾個關鍵類的對應關係與實際作用,有點S/C的味道。它們的通訊是通過IBinder,實現程式通訊的,可以看出,啟動Activity和Service,ActivityThreadActivityManagerService是關鍵,並且上面我們知道,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住InstrumentationActivityThread等。通過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接管。

Hook Instrumentation 圖1
Hook Instrumentation 圖1

  這裡是如何拿到Instrumentation的?

  因為在ActivityThread內部有一個sCurrentActivityThread靜態變數。如下圖,通過反射sCurrentActivityThread我們可以獲取當前ActivityThread,而ActivityThread的公開方法getInstrumentation即可拿到Instrumentation物件。

Hook Instrumentation 圖2
Hook Instrumentation 圖2

  另外,上方圖1還有設定HandlerCallback的流程,其實就是攔截了ActivityThread中的mH這個Handler的Callback,從【 一、 Activity/Service啟動流程】流程圖可以看到,mH的handleMessage處理很多Activity的啟動狀態。

  如下圖, 是Hook Service的流程,如圖中註釋所示,通過ActivityManagerNativegetDefault,拿到AndroidManagerService(詳見啟動流程圖),而VirtualApk通過自定義ActivityManagerProxy,重新生成了一個IActivityManager,然後注入回AndroidManagerService中,這樣接管了系統啟動、管理service等操作。

Hook Service 圖1
Hook Service 圖1

2、載入外掛Apk

  載入外掛APK是通過PluginManagerloadPlugin方法,如下圖所示,此處就是將apk拆開,解析,讀取,載入,組裝為LoadedPlugin並儲存,以方便後面管理與使用。

載入Apk外掛
載入Apk外掛

  此處對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,因為在ActivityTreadperformLaunchActivity中,會呼叫InstrumentationnewActivity

啟動Activity
啟動Activity

  在newActivity中,如下圖,類沒有找到時(坑位類肯定找不到啦),那麼就去獲取原本儲存在Intent的目標Activity,然後呼叫建立VAInstrumentation時儲存的Instrumentation(mBase)去建立Activity。

建立Activity
建立Activity

  Activity雖然建立好了,但是它對應的資源和context都還不對,所以我們需要在Activity的OnCreate之前完成好Resource的注入。前面載入Apk時,這些資源都儲存在Plugin中。所以我們攔截callActivityOnCreate方法,如下圖,將Activity的Context、Application、Reource,都替換成Plugin中對應的物件。一個Activity就這樣繞過AndroidManifest啟動起來了。

螢幕快照 2017-07-15 上午12.36.21.png
螢幕快照 2017-07-15 上午12.36.21.png

4、啟動外掛Service

  startService啟動Service時,還記得上面我們通過ActivityManagerProxy生成IActivityManager嗎?它主要攔截了Service相關啟動和停止等方法,然後將其都轉化為對應的startService方法,指向代理Service。因為startService方法的特性,他們最終都會在代理Service的onStartCommand中被統一處理。

  如下圖,是ActivityManagerProxy,其中invoke攔截了所有相關的服務請求,並做了轉化處理,下面以startService為例。

ActivityManagerProxy
ActivityManagerProxy

  startService這裡,主要便是提取原本目標service資訊,然後轉化為代理Service,傳送到代理Service,下方圖片啟動流程轉化流程

啟動流程
啟動流程

轉化流程
轉化流程

  如下圖,在代理service中,根據請求型別,代理service會通過classLoader載入來建立service,並操作其attach、onCreate、onStartCommand等,讓service工作起來。

啟動真正的目標service
啟動真正的目標service

自此Activity和Service都成功啟動了,是不是對外掛化有了不一樣的瞭解?

5、AndroidStub

容許這裡插入這一塊,安利下Virtual中的AndroidStub模組,如下圖

AndroidStub
AndroidStub

  因為都用反射很浪費效能,所以有了AndroidStub,它是用來欺騙編譯器的。正常情況下你想操作ActivityThread就會出現如下圖情況,因為它是一個@hide類,這時候除了反射得到ActivityThread,你還需要再反射需要執著方法才能執行,這在一定程度會損耗一些效能。

  但是如下圖,VirtualApk通過AndroidStub,模擬原始碼建立瞭如ActivityThread類,這裡你就可以如圖正常使用ActivityThread了,而AndroidStub中的ActivityThread,其實只是定義了和原碼中一摸一樣的方法,並沒有其他實現。

  因為CoreLibrary依賴AndroidStub使用的是provided ,因為provided依賴是不打包依賴包,而是執行時提供,所以成功欺騙了編輯器,以此提高了效能。很神奇吧?

ActivityThread
ActivityThread

CoreLibrary依賴AndroidStub
CoreLibrary依賴AndroidStub

  終於結束了,如果你看到了這裡,相信你是一個很有耐心的同志!當然外掛化還是其他實現方式,如Replugin,只Hook住了ClassLoader,流程更加複雜,如有什麼建議和疑問,歡迎留言討論。

VirtualApk:github.com/didi/Virtua…

個人github:github.com/CarGuo

終於結束了,點個贊不?
終於結束了,點個贊不?

相關文章