1. 程式人生 > >Android外掛化初體驗

Android外掛化初體驗

最近把Activity啟動流程整體看了一遍,估摸著弄個啥來鞏固下,發現外掛化正好是這塊技術的實踐,而說道外掛化其實有好幾種實現方式,這裡我用的是hook的方式實現,主要目的呢是為了對activity啟動流程有個整體的認識,當然了本篇的外掛化也只是一個demo版本並沒有任何相容適配,重在流程和原理的理解。

概述

外掛化顧名思義,就是將一個APK拆成多個,當需要的時候下載對應外掛APK載入的技術。本文demo中除了下載是通過adb命令,其他都是模擬真實環境的,這裡先理下流程。

  1. 將外掛工程打包為APK,然後通過adb push命令傳送到宿主APK目錄(模擬下載流程)。
  2. 利用ClassLoader載入外掛APK中的類檔案。
  3. hook Activity啟動流程中部分類,利用佔坑Activity幫助PluginActivity繞過AMS驗證,在真正啟動的時候又替換回PluginActivity。
  4. 建立外掛Apk的Resources物件,完成外掛資源的載入。

對整體流程有個大概認識後,下面將結合原始碼和Demo來詳細講解,本文貼出的原始碼基於API27。

初始化外掛APK類檔案

既然外掛APK是通過網路下載下來的,那麼APK中的類檔案就需要我們自己載入了,這裡我們要用到DexClassLoader去載入外掛APK中的類檔案,然後將DexClassLoader中的Element陣列和宿主應用的PathClassLoader的Element數組合並再設定回PathClassLoader,完成外掛APK中類的載入。對ClassLoader不太熟悉的可以看下我另篇

Android 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的驗證,既然這樣我們換一個思路。

  1. 在宿主專案中提前弄一個SubActivity佔坑,在啟動PluginActivity的時候替換為啟動這個SubActivity繞過驗證。
  2. 在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.