從零開始的Android插件化 宿主app加載sd卡apk

Blog - 從零開始的Android插件化

關(guān)于插件化有很多知識點可講蛉鹿,市面上也有很多成熟的第三方庫缸匪,這篇 Blog 不是講解這些第三方庫的使用担汤,而是探索如何從零開始使用"反射"老虫、"Hook"等知識點框都,在不安裝插件 apk 的情況下更鲁,實現(xiàn)自己的宿主 App 加載放在 sd 卡的插件 apk霎箍,從而一窺 Android 插件化的原理。

背景

Android 開發(fā)中經(jīng)常有這樣的情況:模塊化不清晰澡为、方法數(shù)超限漂坏、想在不重新安裝 App 的情況下增加新的模塊,基于這樣的需求媒至,結(jié)合 Android DexClassLoader 可以加載 dex 文件以及包含 dex 的壓縮文件(apk 和 jar)的特點顶别,催生出了 Android 插件化技術(shù)。

原理

1.DexClassLoader 可以加載外部 dex 文件以及包含 dex 的壓縮文件(apk 和 jar)拒啰。
2.熟知 Activity 的啟動流程驯绎,利用 Hook 技術(shù)啟動外部 dex 文件中的 Activity。

實現(xiàn)步驟

實現(xiàn)流程圖

流程概覽.png

具體步驟

我們知道 Activity 必須在 AndroidManifest 中配置才能正常啟動谋旦,否則會報 ActivityNotFound 異常剩失,而外部插件 apk 中的 Activity 肯定是無法在宿主 App 中配置的,這樣因為找不到相關(guān)配置 startActivity 就會 crash册着。為了方便理解拴孤,我們先去實現(xiàn)如何在 AndroidManifest 沒有配置 TestActivity 的情況下,啟動宿主 App 的 TestActivity(注意 TestActivity 是宿主 App 而不是外部 apk 或 dex 文件的)甲捏。

步驟1:繞過 AndroidManifest 檢測

Activity 啟動過程中是應(yīng)用程序進程與 AMS 頻繁交互的過程演熟。AMS 處于系統(tǒng) SystemServer 進程中,我們無法修改司顿,所以只能 Hook 應(yīng)用程序進程部分芒粹,以實現(xiàn)需求兄纺。

這里采用占位策略,原理是提前在 AndroidManifest 中配置一個占位頁面<activity android:name=".SubActivity" />化漆,在應(yīng)用程序進程將 targetIntent 傳給 AMS 之前估脆,替換 targetIntent 為該占位 intent,之后在 AMS 傳回應(yīng)用程序進程之后获三、應(yīng)用程序進程調(diào)用 intent 啟動 Activity 之前旁蔼,將占位 intent 再次替換為 targetIntent 即可锨苏。下面具體實現(xiàn):

首先實現(xiàn)工具類 FieldUtil疙教,方便后續(xù)反射操作:

<1>

public class FieldUtil {

    /**
     * 獲取Field對應(yīng)的值
     *
     * @param clazz
     * @param target
     * @param name
     * @return
     * @throws Exception
     */
    public static Object getField(Class clazz, Object target, String name) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field.get(target);
    }

    /**
     * 獲取Field
     *
     * @param clazz
     * @param name
     * @return
     * @throws Exception
     */
    public static Field getField(Class clazz, String name) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field;
    }

    /**
     * 給Field賦值
     *
     * @param clazz
     * @param target
     * @param name
     * @param value
     * @throws Exception
     */
    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);
    }
}

那么應(yīng)用程序進程是什么時候?qū)?intent 傳給 AMS 的呢?通過層層查找 startActivity 源碼伞租,最終定位在下面的源碼上:

android.app.Instrumentation

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        ...
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);//1.
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

IActivityManager 是 AMS 在客戶端的代理類贞谓,通過它與 AMS 跨進城通信,看下注釋1處 ActivityManagerService 源碼:

    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);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };

這里用到 Singleton 實現(xiàn)單例葵诈,接著研究 Singleton 的源碼:

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;
        }
    }
}

于是 Hook 點找到了:利用反射裸弦,替換 Singleton<IActivityManager> 中的 IActivityManager 為自己的代理類,即可插入替換 targetIntent 的代碼作喘。下面是具體實現(xiàn):

首先創(chuàng)建自己的代理類理疙,用于替換 targetIntent 為占位 intent 以繞過檢測,這里使用動態(tài)代理:

<2>

public class IActivityManagerProxy implements InvocationHandler {

    private Object mActivityManager;
    private static final String TAG = "IActivityManagerProxy";

    public IActivityManagerProxy(Object activityManager) {
        this.mActivityManager = activityManager;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //執(zhí)行原方法之前泞坦,先執(zhí)行代理方法
        if ("startActivity".equals(method.getName())) {
            Intent intent = null;
            int index = 0;
            //找到intent參數(shù)
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            intent = (Intent) args[index];
            Intent subIntent = new Intent();
            String packageName = "com.app.dixon.studyplug";//1.替換 TargetIntent 的參數(shù)
            subIntent.setClassName(packageName, packageName + ".SubActivity");//替換 TargetIntent 的參數(shù)
            subIntent.putExtra(HookHelper.TARGET_INTENT, intent);//2.
            args[index] = subIntent;
        }
        return method.invoke(mActivityManager, args);
    }
}

注釋1處窖贤,創(chuàng)建占位 intent 用于替換傳給 AMS 的 targetIntent;
注釋2處贰锁,將 targetIntent 存儲起來赃梧,方便后續(xù)拿出啟動。

創(chuàng)建完代理類豌熄,就可以 Hook 替換 IActivityManager 了授嘀,由于 Singleton<IActivityManager> 是靜態(tài)的,所以替換整個進程生效锣险。

創(chuàng)建 HookHelper 類蹄皱,Hook Singleton<IActivityManager>.mInstance:

<3>

public class HookHelper {

    public static final String TARGET_INTENT = "target_intent";
    public static final String TARGET_INTENT_NAME = "target_intent_name";
    public static final String TAG = "HOOK";

    /**
     * Hook IActivityManager 由于IActivityManagerSingleton是靜態(tài)成員變量 所以是全局Hook
     *
     * @throws Exception
     */
    public static void hookAMS() throws Exception {
        Object defaultSingleton = null;
        if (Build.VERSION.SDK_INT >= 26) {//版本號 > 8.0
            Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
            //獲取Singleton<IActivityManager>
            defaultSingleton = FieldUtil.getField(activityManagerClazz, null, "IActivityManagerSingleton");
        } else {
            Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
            //獲取ActivityManagerNative中的gDefault字段
            defaultSingleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");
        }
        //替換Singleton中的值
        //1.獲取class,找到其屬性
        Class<?> singletonClazz = Class.forName("android.util.Singleton");
        Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance");
        //2.獲取IActivityManager
        Object iActivityManager = mInstanceField.get(defaultSingleton);
        //3.獲取IActivityManager的Proxy代理類
        Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager"); //IActivityManager的全路徑
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{iActivityManagerClazz},
                new IActivityManagerProxy(iActivityManager));
        //4.替換
        mInstanceField.set(defaultSingleton, proxy);
        Log.e(TAG, "Hook Finish");
    }
}

大致步驟是:獲取 android.app.ActivityManager 中的靜態(tài)成員變量 Singleton<IActivityManager>芯肤,獲取其 mInstance 屬性夯接,創(chuàng)建動態(tài)代理類 IActivityManagerProxy,替換 mInstance纷妆。詳情已經(jīng)在上述注釋中標(biāo)明盔几。

之后在 Application 中調(diào)用:

<4>

public class MyApplication extends Application {

    public static Application application;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        application = this;
        try {
            HookHelper.hookAMS();
        } catch (Exception e) {
            Log.e(HookHelper.TAG, "The reason for the error is " + e.toString());
            e.printStackTrace();
        }
}

通過上述操作,AMS 遍歷 AndroidManifest 時檢測的是占位 intent 而不是 targetIntent掩幢,故不會拋出 ActivityNotFound 的異常逊拍。

步驟2:還原 targetIntent

為了繞過檢測我們將 targetIntent 臨時替換為了占位 intent上鞠,相應(yīng)的,在 AMS 允許應(yīng)用程序進程啟動 Activity 時芯丧,我們應(yīng)當(dāng)將占位 intent 還原為 targetIntent芍阎。

那么什么時間點還原合適呢?

我們知道 ActivityThread 作為應(yīng)用程序進程的主線程缨恒,在很多方面起了關(guān)鍵的作用谴咸,其中包括 Activity 的啟動。其中 handleLaunchActivity 中有一行源碼如下:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        ...
        Activity a = performLaunchActivity(r, customIntent);
        ...
}        

這里 r 為 ActivityClientRecord 類型骗露,它有個 Intent 類型的成員變量名為 intent岭佳,這個 intent 就是上面 AMS 傳回給應(yīng)用程序進程的 intent。

回到該方法的上一步萧锉,看下它的源碼:

public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;

ActivityThread 通過特殊的 Handler :H 來分發(fā) AMS 發(fā)來的各種事件珊随,其中 Handler 的 dispatchMessage 源碼如下:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

結(jié)合上述源碼,我們知道了 msg.obj 中包含了之前了占位 intent柿隙,所以我們只需要將 H 的 mCallback 賦值為自定義的 Callback叶洞,并在 Callback.handleMessage 中做替換 intent 的操作,之后再重新手動調(diào)用 H.handleMessage(msg); 即可禀崖。具體實現(xiàn)如下:

首先實現(xiàn)自定義的 HCallback 類衩辟,在其中做替換 intent 操作:

<5>

public class HCallback implements Handler.Callback {

    public static final int LAUNCH_ACTIVITY = 100;

    Handler mHandler;

    public HCallback(Handler handler) {
        mHandler = handler;
    }

    @Override
    public boolean handleMessage(Message msg) {
        //執(zhí)行原h(huán)andleMessage方法之前執(zhí)行Hook的HandleMessage
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //獲取之前消息中的真實Intent
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                if (target != null) {
                    //替換
                    FieldUtil.setField(r.getClass(), r, "intent", target);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //手動重新調(diào)用handleMessage
        mHandler.handleMessage(msg);
        return true;
    }
}

接下來需要把我們上述自定義的 HCallback 賦值給 ActivityThread.H.mCallback,在 HookHelper 類中添加如下方法:

<6>

    /**
     * 目標(biāo)是對 ActivityThread 的 mH.callback 進行替換波附,而 ActivityThread 單進程只有一個艺晴,所以是全局替換
     *
     * @throws Exception
     */
    public static void hookHandler() throws Exception {
        //獲取ActivityThread.mH
        Class activityThreadClazz = Class.forName("android.app.ActivityThread");
        Object currentActivityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");
        Field mHField = FieldUtil.getField(activityThreadClazz, "mH");
        Handler mH = (Handler) mHField.get(currentActivityThread);
        //替換H.mCallback
        FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback(mH));
    }

ActivityThread 單進程只有一個,可以通過它的靜態(tài)成員變量 sCurrentActivityThread 獲得叶雹,獲取到之后將 HCallback 賦值給 mH.mCallback 即可财饥。

最后記得在上述 Application 中調(diào)用:

<4>改

public class MyApplication extends Application {

    public static Application application;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        application = this;
        try {
            HookHelper.hookAMS();
            HookHelper.hookHandler();
        } catch (Exception e) {
            Log.e(HookHelper.TAG, "The reason for the error is " + e.toString());
            e.printStackTrace();
        }
    }
}

通過上述倆大步驟,我們在項目中創(chuàng)建任意 Activity折晦,然后刪除掉它在 AndroidManifest 中的配置钥星,然后通過下面代碼也可以正常啟動,如示例:

startActivity(new Intent(MainActivity.this, TestActivity.class));
AndroidManifest截圖.png

那么有一個問題:這樣啟動的 Activity 可以正常遵從 AMS 對生命周期的管理嗎满着?
答案是肯定谦炒,AMS 通知應(yīng)用程序進程創(chuàng)建 Activity 之后是通過 Token 進行后續(xù)生命周期通信的,而 Token 依賴于真實創(chuàng)建的 TargetActivity风喇,所以 TargetActivity 是有生命周期的宁改。有興趣的可以單獨研究源碼,這里不再深入探討魂莫。

步驟3:加載插件 dex

步驟1还蹲、2實現(xiàn)了不配置 AndroidManifest 也能正常啟動 Activity,但我們的終極目標(biāo)是啟動外部 apk,首先就需要把外部 apk 加載進來谜喊。

使用 Android 提供的 DexClassLoader 可以加載外部 dex 文件或加載包含 dex 的文件潭兽,如 apk、jar 等斗遏。這里我創(chuàng)建了 AppClassLoaderHelper 類山卦,用于獲取加載了外部 apk 的 ClassLoader。源碼如下:

<7>

public class AppClassLoaderHelper {

    private static final Map<String, ClassLoader> classLoaderCache = new HashMap<>();
    private static final Map<String, Resources> resourceCache = new HashMap<>();

    private static final String TAG = "AppClassLoaderHelper";

    /**
     * @param appPath
     * @return 得到對應(yīng)插件的ClassLoader對象
     */
    public static ClassLoader getDexClassLoader(Context context, String appPath) {
        if (classLoaderCache.containsKey(appPath)) {
            return classLoaderCache.get(appPath);
        }
        Log.e(TAG, "path is " + appPath);
        String dexOutFilePath = context.getCacheDir().getAbsolutePath();
        Log.e(TAG, "dexOutFilePath is " + dexOutFilePath);
        DexClassLoader classLoader = new DexClassLoader(appPath, dexOutFilePath, null, context.getClassLoader());
        classLoaderCache.put(appPath, classLoader);
        return classLoader;
    }

利用 DexClassLoader 將外部的 apk 加載了進來诵次,他的構(gòu)造函數(shù)如下:

DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

四個參數(shù)分別是:
1.dexPath 的全路徑账蓉。
2.optimizedDirectory:加載的 dex 存放的目錄。
3.librarySearchPath:library 庫路徑逾一。
4.parent:父類 ClassLoader铸本,雙親委托不是本文重點,有興趣可以 Google 了解嬉荆。

這里我新建了一個項目归敬,用于生成插件 apk酷含。項目很簡單鄙早,只有一個空頁面:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

生成 apk 之后將該文件存放到手機 sd 卡根目錄下:/storage/emulated/0/app-debug.apk

因為我放的位置特殊椅亚,所以需要在 AndroidManifest 中配置讀取 sd 卡的權(quán)限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Android 6.0 及以上還需要動態(tài)獲取限番,所以假如報 ClassNotFound 異常,檢查你的 App 是否真的有 sd 卡讀寫權(quán)限呀舔。

之后就可以嘗試在宿主 App 中啟動我們的目標(biāo)頁面了:

<8>

    public void startOtherApp(View view) {
        try {
            ClassLoader loader = AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk");
            Class<?> targetClass = loader.loadClass("com.app.dixon.plugin.MainActivity");
            Intent intent = new Intent(MainActivity.this, targetClass);
            intent.putExtra(HookHelper.TARGET_INTENT_NAME, intent.getComponent().getClassName());
            startActivity(intent);
        } catch (ClassNotFoundException e) {
            //classNotFound 注意有可能是權(quán)限問題
            e.printStackTrace();
        }
    }

這里我通過加載了外部 apk 文件的 ClassLoader 去獲取目標(biāo)頁面 com.app.dixon.plugin.MainActivity 的 Class弥虐,之后創(chuàng)建 intent,并賦值 HookHelper.TARGET_INTENT_NAME 用于標(biāo)記這是一個插件 apk 的頁面霜瘪,便于后續(xù)識別。最后通過 startActivity(intent) 啟動惧磺。

運行,果然 Crash 了,報錯如下:

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.app.dixon.studyplug/com.app.dixon.plugin.MainActivity}: java.lang.ClassNotFoundException: Didn't find class "com.app.dixon.plugin.MainActivity" on path: DexPathList[[zip file "/data/app/com.app.dixon.studyplug-RqW1yxJOWJ4JvMTrVZogWA==/base.apk"],nativeLibraryDirectories=[/data/app/com.app.dixon.studyplug-RqW1yxJOWJ4JvMTrVZogWA==/lib/arm64, /system/lib64, /vendor/lib64]]

這是為什么呢设预?仔細分析魄梯,可以看出:

我們使用 DexClassLoader 獲取到的插件 Activity 的 Class 只是用于創(chuàng)建 Intent缠局,在上述步驟2中狭园、真正 new Activity 時绎谦,使用的 ClassLoader 仍然是宿主 App 的 ClassLoader冤留,宿主 App 的 ClassLoader 從來沒有加載過外部插件 apk,當(dāng)然會報 ClassNotFoundException州既。

步驟4:替換插件 Activity ClassLoader

經(jīng)過上述分析实束,我們需要在加載插件 apk 時侮叮,將宿主 App 的 ClassLoader 替換為自定義的 DexClassLoader。

替換時機一定和 new Activity 的時機有關(guān),上面我們說到 Activity 示例是 performLaunchActivity 方法返回的静浴,看下它的源碼:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        ...
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
        ...

經(jīng)常做單元測試的同學(xué)可能對 Instrumentation 這個類不會陌生掷邦,它是 ActivityThread 的成員變量之一,用于轉(zhuǎn)交執(zhí)行 Activity 的一些關(guān)鍵方法挟鸠。這里可以看到 Activity 就是它創(chuàng)建的佳遂,它的 newActivity 源碼如下:

public Activity newActivity(ClassLoader cl, String className,
            Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {
        return (Activity)cl.loadClass(className).newInstance();
    }

第一個參數(shù) cl 就是 Activity 對應(yīng)的 ClassLoader,所以這兒的做法是宅楞,替換 ActivityThread.mInstrumentation,重寫它的 newActivity 方法,使其在啟動插件 apk 時瓷们,加載自定義的 DexClassLoader歹茶。下面是具體實現(xiàn):

<9>

public class InstrumentationProxy extends Instrumentation {
    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
        if (!TextUtils.isEmpty(intentName)) {//1.
            //通過自定義的classLoader加載目標(biāo)類
            Activity activity = super.newActivity(AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk"), intentName, intent);
            return activity;
        }
        return super.newActivity(cl, className, intent);
    }
}

注釋1處,前面我們給 intent 賦值了 HookHelper.TARGET_INTENT_NAME饿序,這里我們利用該標(biāo)識判斷:如果是插件 apk,就使用自定義 DexClassLoader往弓,否則還是走宿主 ClassLoader啤握。

TARGET_INTENT_NAME意味著插件 apk 也需要傳此標(biāo)識晶框,這對插件的獨立開發(fā)是不友好的恨统。
實際上有更好的實現(xiàn)方式,不需要傳遞標(biāo)識也能識別是否是插件 apk 中的 class三妈,詳情參考 Github 源碼畜埋。

接下來就是想辦法把 ActivityThread.mInstrumention 替換為上述 InstrumentationProxy,在 HookHelper 中增加方法:

<10>

    /**
     * Hook newActivity 使其不加載系統(tǒng)畴蒲、而加載自定義ClassLoader中的Activity
     *
     * @param context
     * @throws Exception
     */
    public static void hookInstrumentation(Context context) throws Exception {
        Class activityThreadClazz = Class.forName("android.app.ActivityThread");
        Object currentActivityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");
        FieldUtil.setField(activityThreadClazz, currentActivityThread, "mInstrumentation", new InstrumentationProxy());
    }

記得在 Application 中調(diào)用:

<4>再改

public class MyApplication extends Application {

    public static Application application;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        application = this;
        try {
            HookHelper.hookAMS();
            HookHelper.hookHandler();
            HookHelper.hookInstrumentation();
        } catch (Exception e) {
            Log.e(HookHelper.TAG, "The reason for the error is " + e.toString());
            e.printStackTrace();
        }
    }
}

然后運行悠鞍,不再報 ClassNotFound 的異常了,說明插件 Activity 的 Class 正常找到模燥,且該頁面能正常創(chuàng)建了咖祭。

但是,仍然發(fā)生了 Crash蔫骂,這次的異常是資源找不到么翰。

仔細想想,我們在加載外部插件 apk 的時候辽旋,從頭到尾都只加載了 Class浩嫌,沒有加載其資源,插件 Activity 使用的 mResources 是宿主 App 的补胚,資源當(dāng)然會找不到码耐!

步驟5:替換插件 Activity mResources

在 AppClassLoaderHelper 中增加如下方法加載插件資源并獲取其 Resources,關(guān)于資源加載的原理這里不再深入探討溶其,直接看下面源碼:

<11>

    /**
     * @param appPath
     * @return 得到對應(yīng)插件的Resource對象
     */
    public static Resources getPluginResources(Context context, String appPath) {
        if (resourceCache.containsKey(appPath)) {
            return resourceCache.get(appPath);
        }
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            //反射調(diào)用方法addAssetPath(String path)
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            //將未安裝的Apk文件的添加進AssetManager中,第二個參數(shù)是apk的路徑
            addAssetPath.invoke(assetManager, appPath);
            Resources superRes = context.getResources();
            Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
            resourceCache.put(appPath, mResources);
            return mResources;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

mResourcesContextThemeWrapper 的成員變量之一骚腥,而 Activity 繼承自 ContextThemeWrapper,所以只需要將插件 Activity 的 mResources 重新賦值為上述 getPluginResources 返回的 resources 即可瓶逃。還記得 InstrumentationProxy嗎束铭?我們剛才在那里 new Activity廓块,所以只需要緊隨其后更換 mResources 即可。

<9>改

public class InstrumentationProxy extends Instrumentation {
    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
        if (!TextUtils.isEmpty(intentName)) {
            //通過自定義的classLoader加載目標(biāo)類
            Activity activity = super.newActivity(AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk"), intentName, intent);
            //新增:
            //通過自定義的resources加載目標(biāo)資源
            //這樣TargetActivity使用的就是其Apk本身的資源
            try {
                FieldUtil.setField(ContextThemeWrapper.class, activity, "mResources", AppClassLoaderHelper.getPluginResources(MyApplication.application, "/storage/emulated/0/app-debug.apk"));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return activity;
        }
        return super.newActivity(cl, className, intent);
    }
}

到這里似乎沒什么問題了契沫,然后點擊啟動插件 apk带猴,boom~crash

ClassLoader 正常加載了,資源也映射正確了埠褪,為什么還是 crash 了呢浓利?

查找原因,crash 說資源仍然找不到钞速,資源號是 0x7xxxxx贷掖。通過查找該資源,發(fā)現(xiàn)是宿主 App 的 R.mipmap.ic_launcher渴语,就是 Android app 默認的圖標(biāo)苹威。插件 Activity 使用的資源是自己 apk 的,當(dāng)它使用宿主 app 的 id 去查找資源當(dāng)然會找不到了驾凶。

分析到這里牙甫,我明白當(dāng)前的錯誤也許和插件 app 使用的 AppTheme 有關(guān)系,也許是 TopBar 不完全由 Activity 掌控调违,導(dǎo)致 TopBar 的資源 id 仍然使用宿主 App 的窟哺。為了演示方便(其實是懶),我這里暫時將插件 apk 的目標(biāo)頁面設(shè)置成了沒有 TopBar 的主題:

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="android:windowNoTitle">true</item>
    </style>

該問題后續(xù)已修復(fù)技肩。

果然且轨,再次啟動插件 apk,終于成功了虚婿!

步驟6:后續(xù)實踐

在插件 apk 頁面里旋奢,我首先放了張圖,資源能正確加載然痊,如圖:

效果圖.png set-w300

隨后我在插件 apk 里又新建了一個 OtherActivity至朗,然后插件 MainActivity 通過下面代碼啟動 OtherActivity:

    public void start(View view) {
        Intent intent = new Intent(MainActivity.this,OtherActivity.class);
        intent.putExtra("target_intent_name", intent.getComponent().getClassName());
        startActivity(intent);
    }

這里 target_intent_name 就是我們之前識別插件頁面的標(biāo)識,有了這個標(biāo)識剧浸,才會給當(dāng)前 Activity 加載正確的 ClassLoader 和 Resources锹引。這里測試插件 A 頁面啟動插件 B 頁面沒有問題。

總結(jié)

插件化涉及到的內(nèi)容很多辛蚊,本文只是對插件化實現(xiàn)的一個從零開始的探索粤蝎,原理基于此,相信今后擴展袋马、完善、源碼探索也就有據(jù)可循秸应。

Github 源碼地址虑凛,后續(xù)會不斷完善碑宴。

本人能力有限,步驟1桑谍、2部分參考了 Android 進階揭秘一書延柠,錯誤之處還請指出。

[TOC]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锣披,一起剝皮案震驚了整個濱河市贞间,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雹仿,老刑警劉巖增热,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異胧辽,居然都是意外死亡峻仇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門邑商,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摄咆,“玉大人蔬胯,你說我怎么就攤上這事童芹。” “怎么了胞此?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵恶迈,是天一觀的道長涩金。 經(jīng)常有香客問我,道長蝉绷,這世上最難降的妖魔是什么鸭廷? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮熔吗,結(jié)果婚禮上辆床,老公的妹妹穿的比我還像新娘。我一直安慰自己桅狠,他們只是感情好讼载,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著中跌,像睡著了一般咨堤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上漩符,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天一喘,我揣著相機與錄音,去河邊找鬼。 笑死凸克,一個胖子當(dāng)著我的面吹牛议蟆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萎战,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼咐容,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚂维?” 一聲冷哼從身側(cè)響起戳粒,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虫啥,沒想到半個月后蔚约,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡孝鹊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年炊琉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片又活。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡苔咪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柳骄,到底是詐尸還是另有隱情团赏,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布耐薯,位于F島的核電站舔清,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏曲初。R本人自食惡果不足惜体谒,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臼婆。 院中可真熱鬧抒痒,春花似錦、人聲如沸颁褂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颁独。三九已至彩届,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間誓酒,已是汗流浹背樟蠕。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坯墨。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓寂汇,卻偏偏與公主長得像病往,于是被迫代替她去往敵國和親捣染。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容