Android外掛化初體驗
最近把Activity啟動流程整體看了一遍,估摸著弄個啥來鞏固下,發現外掛化正好是這塊技術的實踐,而說道外掛化其實有好幾種實現方式,這裡我用的是hook的方式實現,主要目的呢是為了對activity啟動流程有個整體的認識,當然了本篇的外掛化也只是一個demo版本並沒有任何相容適配,重在流程和原理的理解。
概述
外掛化顧名思義,就是將一個APK拆成多個,當需要的時候下載對應外掛APK載入的技術。本文demo中除了下載是通過adb命令,其他都是模擬真實環境的,這裡先理下流程。
- 將外掛工程打包為APK,然後通過adb push命令傳送到宿主APK目錄(模擬下載流程)。
- 利用ClassLoader載入外掛APK中的類檔案。
- hook Activity啟動流程中部分類,利用佔坑Activity幫助PluginActivity繞過AMS驗證,在真正啟動的時候又替換回PluginActivity。
- 建立外掛Apk的Resources物件,完成外掛資源的載入。
對整體流程有個大概認識後,下面將結合原始碼和Demo來詳細講解,本文貼出的原始碼基於API27。
初始化外掛APK類檔案
既然外掛APK是通過網路下載下來的,那麼APK中的類檔案就需要我們自己載入了,這裡我們要用到DexClassLoader去載入外掛APK中的類檔案,然後將DexClassLoader中的Element陣列和宿主應用的PathClassLoader的Element數組合並再設定回PathClassLoader,完成外掛APK中類的載入。對ClassLoader不太熟悉的可以看下我另篇
public class InjectUtil {
private static final String TAG = "InjectUtil";
private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
private static final String FIELD_PATH_LIST = "pathList";
private static final String FIELD_DEX_ELEMENTS = "dexElements";
public static void inject(Context context, ClassLoader origin) throws Exception {
File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/Android/data/$packageName/files/plugin
if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0) {
Log.i(TAG, "外掛檔案不存在");
return;
}
pluginFile = pluginFile.listFiles()[0];//獲取外掛apk檔案
File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
if (!optimizeFile.exists()) {
optimizeFile.mkdirs();
}
DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到外掛Elements
Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements
Object array = combineArray(originElements, pluginElements);//合併陣列
FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//設定回PathClassLoader
Log.i(TAG, "外掛檔案載入成功");
}
private static Object combineArray(Object pathElements, Object dexElements) {//合併陣列
Class<?> componentType = pathElements.getClass().getComponentType();
int i = Array.getLength(pathElements);
int j = Array.getLength(dexElements);
int k = i + j;
Object result = Array.newInstance(componentType, k);
System.arraycopy(dexElements, 0, result, 0, j);
System.arraycopy(pathElements, 0, result, j, i);
return result;
}
}
這裡我們約定將外掛APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目錄,然後為了儘早載入所以在Application中執行載入邏輯。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//載入外掛Apk的類檔案
} catch (Exception e) {
e.printStackTrace();
}
}
}
Hook啟動流程
在說之前我們得先了解下Activity的啟動流程。
上圖抽象的給出了Acticity的啟動過程。在應用程式程序中的Activity向AMS請求建立Activity(步驟1),AMS會對這個Activty的生命週期棧進行管理,校驗Activity等等。如果Activity滿足AMS的校驗,AMS就會請求應用程式程序中的ActivityThread去建立並啟動Activity。
那麼在上一步我們已經將外掛Apk的類檔案載入進來了,但是我們並不能通過startActivity的方式去啟動PluginActivity,因為PluginActivity並沒有在AndroidManifest中註冊過不了AMS的驗證,既然這樣我們換一個思路。
- 在宿主專案中提前弄一個SubActivity佔坑,在啟動PluginActivity的時候替換為啟動這個SubActivity繞過驗證。
- 在AMS處理完相應驗證通知我們ActivityThread建立Activty的時候在替換為PluginActivity。
佔坑SubActivity非常簡單
public class SubActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
然後在AndroidManifest註冊好即可
<activity android:name=".SubActivity"/>
對於startActivity()最終都會調到ActivityManagerService的startActivity()方法。
ActivityManager.getService()//獲取AMS
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
那麼我們可以通過動態代理hook ActivityManagerService,然後在startActivity()的時候將PluginActivity替換為SubActivity,不過對於ActivityManagerService的獲取不同版本方式有所不同。
在Android7.0以下會呼叫ActivityManagerNative的getDefault方法獲取,如下所示。
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");//獲取ams
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);//拿到ams代理物件
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
getDefault()返回的是IActivityManager,而gDefault是一個單例物件Singleton並且是靜態的是非常容易用反射獲取。
Android8.0會呼叫ActivityManager的getService方法獲取,如下所示。
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);//拿到ams
final IActivityManager am = IActivityManager.Stub.asInterface(b);//拿到ams代理物件
return am;
}
};
返回一個IActivityManager,而IActivityManagerSingleton是一個單例物件Singleton並且是靜態非常容易獲取。
在看下上面提到的Singleton等會hook會用到
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
到這裡會發現其實返回的都是AMS的介面IActivityManager,那麼我們只要能通過反射拿到,然後通過動態代理去Hook這個介面在啟動的時候把PluginActivity替換為SubActivity即可繞過AMS的驗證。
public class IActivityManagerProxy implements InvocationHandler {//動態代理
private final Object am;
public IActivityManagerProxy(Object am) {//傳入代理的AMS物件
this.am = am;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {//startActivity方法
Intent oldIntent = null;
int i = 0;
for (; i < args.length - 1; i++) {//獲取startActivity Intent引數
if (args[i] instanceof Intent) {
oldIntent = (Intent) args[i];
break;
}
}
Intent newIntent = new Intent();//建立新的Intent
newIntent.setClassName("rocketly.demo", "rocketly.demo.SubActivity");//啟動目標SubActivity
newIntent.putExtra(HookHelper.TRANSFER_INTENT, oldIntent);//保留原始intent
args[i] = newIntent;//把外掛Intent替換為佔坑Intent
}
return method.invoke(am, args);
}
}
動態代理寫好後,我們還需要通過反射去hook住原始AMS。因為會用到反射弄了一個簡單的工具類
public class FieldUtil {
public static Object getField(Class clazz, Object target, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
public static Field getField(Class clazz, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
}
public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
}
接下來是hook程式碼
public class HookHelper {
public static final String TRANSFER_INTENT = "transfer_intent";
public static void hookAMS() throws Exception {
Object singleton = null;
if (Build.VERSION.SDK_INT >= 26) {//大於等於8.0
Class<?> clazz = Class.forName("android.app.ActivityManager");
singleton = FieldUtil.getField(clazz, null, "IActivityManagerSingleton");//拿到靜態欄位
} else {//8.0以下
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
singleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");//拿到靜態欄位
}
Class<?> singleClazz = Class.forName("android.util.Singleton");
Method getMethod = singleClazz.getMethod("get");
Object iActivityManager = getMethod.invoke(singleton);//拿到AMS
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));//生成動態代理
FieldUtil.setField(singleClazz, singleton, "mInstance", proxy);//將代理後的物件設定回去
}
}
接下來我們需要在Application去執行hook
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//載入外掛Apk的類檔案
HookHelper.hookAMS();//hookAMS
} catch (Exception e) {
e.printStackTrace();
}
}
}
那麼這裡我們已經實現了第一步
在宿主專案中提前弄一個SubActivity佔坑,在啟動PluginActivity的時候替換為啟動這個SubActivity繞過驗證。
接下來我們在看如何在收到AMS建立Activity的通知時替換回PluginActivity。
AMS建立Activity的通知會先發送到ApplicationThread,然後ApplicationThread會通過Handler去執行對應邏輯。
private class ApplicationThread extends IApplicationThread.Stub {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {//收到AMS啟動Activity事件
ActivityClientRecord r = new ActivityClientRecord();
r.intent = intent;//給r賦上要啟動的intent
...//省略很多r屬性初始化
sendMessage(H.LAUNCH_ACTIVITY, r);//傳送r到Handler
}
private void sendMessage(int what, Object obj) {
sendMessage(what, obj, 0, 0, false);
}
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);//傳送到mH
}
}
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//執行啟動activity
} break;
}
}
}
既然是通過sendMessage()方式通知Handler去執行對應的方法,那麼在呼叫handleMessage()之前會通過dispatchMessage()分發事件。
public class Handler {
final Callback mCallback;
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
public interface Callback {
public boolean handleMessage(Message msg);
}
}
可以發現一個很好的hook點就是mCallback這個介面,可以讓我們在handleMessage方法之前將ActivityClientRecord中的SubActivity Intent替換回PluginActivity Intent。
public class HCallback implements Handler.Callback {//實現Callback介面
public static final int LAUNCH_ACTIVITY = 100;
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY://啟動事件
Object obj = msg.obj;
try {
Intent intent = (Intent) FieldUtil.getField(obj.