安卓插件化實例

安卓插件化實例。首先聲明下,實現的例子是基於安卓5.1的,而且實現的功能僅僅是能啟動插件的Activity,當然瞭原理弄懂瞭,別的也好說,那麼下面正式開始。

實現插件化大概有三個難點

1:使我們插件中的代碼可以被宿主程序調用
2:Activity等四大組件可以有正常的生命周期
3:插件可以正常使用資源文件,就是正常的調用R什麼的

我們在自定義的Application中解決以上三個問題

使我們插件中的代碼可以被宿主程序調用

apk中的代碼都是通過ClassLoader來加載,而ClassLoader中的dexElements就是指的dex文件,我們通過反射將插件中的dex添加進dexElements中,這樣宿主程序就能執行插件中的代碼瞭

//將插件apk中的代碼導入
String cachePath = getCacheDir().getAbsolutePath();
DexClassLoader mClassLoader = new DexClassLoader(MyApplication.PLUGIN_PATH, cachePath, null, getClassLoader());
DexHookHelper.inject(mClassLoader);

public class DexHookHelper {

    /**
     * 加載插件
     * @param loader
     */
    public static void inject(DexClassLoader loader){
        //拿到本應用的ClassLoader
        PathClassLoader pathLoader = (PathClassLoader) MyApplication.getContext().getClassLoader();
        try {
            //獲取宿主pathList
            Object suZhuPathList = getPathList(pathLoader);
            Object chaJianPathList = getPathList(loader);
            Object dexElements = combineArray(
                    //獲取本應用ClassLoader中的dex數組
                    getDexElements(suZhuPathList),
                    //獲取插件CassLoader中的dex數組
                    getDexElements(chaJianPathList));
            //將合並的pathList設置到本應用的ClassLoader
            setField(suZhuPathList, suZhuPathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取pathList字段
     * @param baseDexClassLoader 需要獲取pathList字段的ClassLoader
     * @return 返回pathList字段
     */
    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        //通過這個ClassLoader獲取pathList字段
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 反射需要獲取的字段
     */
    private static Object getField(Object obj, Class cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        //反射需要獲取的字段
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 獲取DexElements
     */
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }

    /**
     * 反射需要設置字段的類並設置新字段
     */
    private static void setField(Object obj, Class cl, String field,
                                 Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 合成dex數組
     */
    private static Object combineArray(Object suzhu, Object chajian) {
        //獲取原數組類型
        Class localClass = suzhu.getClass().getComponentType();
        //獲取原數組長度
        int i = Array.getLength(suzhu);
        //插件數組加上原數組的長度
        int j = i + Array.getLength(chajian);
        //創建一個新的數組用來存儲
        Object result = Array.newInstance(localClass, j);
        //一個個的將dex文件設置到新數組中
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(suzhu, k));
            } else {
                Array.set(result, k, Array.get(chajian, k - i));
            }
        }
        return result;
    }
}

需要說明的是熱更新基本上也是基於上面的方法

Activity等四大組件可以有正常的生命周期

首先我們先瞭解下Activity的啟動流程,當在應用中我們需要啟動Activity後,最終會調用到AMS在本地的一個代理類上,然後通過IPC通信告知AMS,在AMS中如果檢驗正常後,通過IPC通知我們的ActivityThread裡的一個binder類,然後利用Handler的方式轉到主線程中去啟動指定的Activity
但是在AMS中並不會持有我們的Activity對象,AMS在通知啟動Activity的時候會傳遞過來一個Binder對象,而在Activity中會有一個Map對象,鍵就是傳遞過來的binder,而值可以認為是我們的Activity,這樣的話如果AMS需要調用某個Activity的時候隻需要傳進來binder對象以及操作信息,我們的ActivityThread就可以知道要對哪個Activity回調哪個生命周期瞭。
也就是說如果告知AMS我們要啟動AActivity,然後在AMS校驗成功後,AMS通知我們的進程可以啟動AActivity時,我們啟動BActivity也是完全可以的,系統在回調生命周期的時候也是完全正常的,所以我們首先在manifest中定義一個Activity用於占位



public class XWActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

}

欺騙AMS分為兩部分,首先是在向AMS發送啟動請求的時候,將請求信息修改下,將真正的意圖隱藏,換成啟動我們的占位Activity,主要是通過反射獲取AMS的代碼對象,之後我們再創建一個代理對象來攔截並修改信息

/**
     * Hook AMS
     * 主要完成的操作是  "把真正要啟動的Activity臨時替換為在AndroidManifest.xml中聲明的替身Activity"
     * 進而騙過AMS
     */
    private static void hookActivityManagerNative() throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException {
        //獲取ActivityManagerNative的類
        Class activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        //拿到gDefault字段
        Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
        gDefaultField.setAccessible(true);
        //從gDefault字段中取出這個對象的值
        Object gDefault = gDefaultField.get(null);
        // gDefault是一個 android.util.Singleton對象; 我們取出這個單例裡面的字段
        Class singleton = Class.forName("android.util.Singleton");
        //這個gDefault是一個Singleton類型的,我們需要從Singleton中再取出這個單例的AMS代理
        Field mInstanceField = singleton.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        //ams的代理對象
        Object rawIActivityManager = mInstanceField.get(gDefault);
        // 創建一個這個對象的代理對象, 然後替換這個字段, 讓我們的代理對象幫忙幹活,這裡我們使用動態代理
        Class iActivityManagerInterface = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{iActivityManagerInterface}, new IActivityManagerHandler(rawIActivityManager));
        mInstanceField.set(gDefault, proxy);
    }

class IActivityManagerHandler implements InvocationHandler {

    private static final String TAG = "IActivityManagerHandler";
    Object mBase;

    public IActivityManagerHandler(Object base) {
        mBase = base;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if ("startActivity".equals(method.getName())) {
            Log.e("Main","startActivity方法攔截瞭");
            // 找到參數裡面的第一個Intent 對象
            Intent raw;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            raw = (Intent) args[index];
            //創建一個要被掉包的Intent
            Intent newIntent = new Intent();
            // 替身Activity的包名, 也就是我們自己的"包名"
            String stubPackage = MyApplication.getContext().getPackageName();
            // 這裡我們把啟動的Activity臨時替換為 ZhanKengActivitiy
            ComponentName componentName = new ComponentName(stubPackage, XWActivity.class.getName());
            newIntent.setComponent(componentName);

            // 把我們原始要啟動的TargetActivity先存起來
            newIntent.putExtra(ActivityHookHelper.EXTRA_TARGET_INTENT, raw);

            // 替換掉Intent, 達到欺騙AMS的目的
            args[index] = newIntent;
            Log.e("xw","startActivity方法 hook 成功");
            Log.e("xw","args[index] hook = " + args[index]);
            return method.invoke(mBase, args);
        }

        return method.invoke(mBase, args);
    }
}

AMS順利檢驗完成後,通知我們啟動Activity的時候,我們再將信息修改回來,同樣是通過反射來實現的

 /**
     * 由於之前我們用替身欺騙瞭AMS; 現在我們要換回我們真正需要啟動的Activity
     * 不然就真的啟動替身瞭, 貍貓換太子...
     * 到最終要啟動Activity的時候,會交給ActivityThread 的一個內部類叫做 H 來完成
     * H 會完成這個消息轉發; 最終調用它的callback
     */
    private static void hookActivityThreadHandler() throws Exception {

//         先獲取到當前的ActivityThread對象
        Class activityThreadClass = Class.forName("android.app.ActivityThread");
        //他有一個方法返回瞭自己
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //執行方法得到ActivityThread對象
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

        // 由於ActivityThread一個進程隻有一個,我們獲取這個對象的mH
        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        //得到H這個Handler
        Handler mH = (Handler) mHField.get(currentActivityThread);

        Field mCallBackField = Handler.class.getDeclaredField("mCallback");
        mCallBackField.setAccessible(true);
        //設置我們自己的CallBackField
        mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));

    }

public class ActivityThreadHandlerCallback implements Handler.Callback {

    Handler mBase;

    public ActivityThreadHandlerCallback(Handler base) {
        mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {
        Log.e("Main", "handleMessage what = " + msg.what);
        switch (msg.what) {
            // ActivityThread裡面 "LAUNCH_ACTIVITY" 這個字段的值是100
            case 100:
                handleLaunchActivity(msg);
                break;
        }

        mBase.handleMessage(msg);
        return true;
    }

    private void handleLaunchActivity(Message msg) {
        // 這裡簡單起見,直接取出TargetActivity;
        Log.e("Main", "handleLaunchActivity方法 攔截");
        Object obj = msg.obj;
        try {
            // 把替身恢復成真身
            Field intent = obj.getClass().getDeclaredField("intent");
            intent.setAccessible(true);
            Intent raw = (Intent) intent.get(obj);

            Intent target = raw.getParcelableExtra(ActivityHookHelper.EXTRA_TARGET_INTENT);
            if (target != null) {
                raw.setComponent(target.getComponent());
            }
            Log.e("xw", "target = " + target);

        } catch (Exception e) {
            throw new RuntimeException("hook launch activity failed", e);
        }
    }

}

插件可以正常使用資源文件

插件中的Activity創建,回調等都是在宿主程序中執行的,那麼插件中想要獲取資源的時候也會去宿主程序的資源管理器中獲取,這顯然是獲取不到的,我們需要給插件創建他自己的資源管理器,並提供方法使其能夠獲取到

private AssetManager assetManager;
private Resources newResource;
private Resources.Theme mTheme;

private void creatPluginResources() {
        try {
            assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);

            addAssetPathMethod.invoke(assetManager, PLUGIN_PATH);

            Resources supResource = getResources();
            newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());

            mTheme = newResource.newTheme();
            mTheme.setTo(super.getTheme());
        } catch (Exception e) {
            Log.e("xw", "創建插件的配置資源失敗" + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public AssetManager getAssets() {
        return assetManager == null ? super.getAssets() : assetManager;
    }

    @Override
    public Resources getResources() {
        return newResource == null ? super.getResources() : newResource;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

在宿主程序的MyApplication中我們為插件定義資源管理器,如果插件想使用資源文件的話,需要復寫幾個方法

@Override
    public Resources getResources() {
        if(getApplication() != null && getApplication().getResources() != null){
            return getApplication().getResources();
        }
        return super.getResources();
    }

    @Override
    public AssetManager getAssets() {
        if(getApplication() != null && getApplication().getAssets() != null){
            return getApplication().getAssets();
        }
        return super.getAssets();
    }

    @Override
    public Resources.Theme getTheme() {
        if(getApplication() != null && getApplication().getTheme() != null){
            return getApplication().getTheme();
        }
        return super.getTheme();
    }

如果需要順利運行demo的話,需要將chajiandemo生成的apk命名為chajiandemo.apk然後放到手機存儲的根目錄下,再次強調下demo是基於安卓5.1的。

You May Also Like