从零开始的Android插件化 宿主app加载sd卡apk

Blog - 从零开始的Android插件化

关于插件化有很多知识点可讲,市面上也有很多成熟的第三方库,这篇 Blog 不是讲解这些第三方库的使用,而是探索如何从零开始使用"反射"、"Hook"等知识点,在不安装插件 apk 的情况下,实现自己的宿主 App 加载放在 sd 卡的插件 apk,从而一窥 Android 插件化的原理。

背景

Android 开发中经常有这样的情况:模块化不清晰、方法数超限、想在不重新安装 App 的情况下增加新的模块,基于这样的需求,结合 Android DexClassLoader 可以加载 dex 文件以及包含 dex 的压缩文件(apk 和 jar)的特点,催生出了 Android 插件化技术。

原理

1.DexClassLoader 可以加载外部 dex 文件以及包含 dex 的压缩文件(apk 和 jar)。
2.熟知 Activity 的启动流程,利用 Hook 技术启动外部 dex 文件中的 Activity。

实现步骤

实现流程图

流程概览.png

具体步骤

我们知道 Activity 必须在 AndroidManifest 中配置才能正常启动,否则会报 ActivityNotFound 异常,而外部插件 apk 中的 Activity 肯定是无法在宿主 App 中配置的,这样因为找不到相关配置 startActivity 就会 crash。为了方便理解,我们先去实现如何在 AndroidManifest 没有配置 TestActivity 的情况下,启动宿主 App 的 TestActivity(注意 TestActivity 是宿主 App 而不是外部 apk 或 dex 文件的)。

步骤1:绕过 AndroidManifest 检测

Activity 启动过程中是应用程序进程与 AMS 频繁交互的过程。AMS 处于系统 SystemServer 进程中,我们无法修改,所以只能 Hook 应用程序进程部分,以实现需求。

这里采用占位策略,原理是提前在 AndroidManifest 中配置一个占位页面<activity android:name=".SubActivity" />,在应用程序进程将 targetIntent 传给 AMS 之前,替换 targetIntent 为该占位 intent,之后在 AMS 传回应用程序进程之后、应用程序进程调用 intent 启动 Activity 之前,将占位 intent 再次替换为 targetIntent 即可。下面具体实现:

首先实现工具类 FieldUtil,方便后续反射操作:

<1>

public class FieldUtil {

    /**
     * 获取Field对应的值
     *
     * @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);
    }
}

那么应用程序进程是什么时候将 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 实现单例,接着研究 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 的代码。下面是具体实现:

首先创建自己的代理类,用于替换 targetIntent 为占位 intent 以绕过检测,这里使用动态代理:

<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 {
        //执行原方法之前,先执行代理方法
        if ("startActivity".equals(method.getName())) {
            Intent intent = null;
            int index = 0;
            //找到intent参数
            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 的参数
            subIntent.setClassName(packageName, packageName + ".SubActivity");//替换 TargetIntent 的参数
            subIntent.putExtra(HookHelper.TARGET_INTENT, intent);//2.
            args[index] = subIntent;
        }
        return method.invoke(mActivityManager, args);
    }
}

注释1处,创建占位 intent 用于替换传给 AMS 的 targetIntent;
注释2处,将 targetIntent 存储起来,方便后续拿出启动。

创建完代理类,就可以 Hook 替换 IActivityManager 了,由于 Singleton<IActivityManager> 是静态的,所以替换整个进程生效。

创建 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是静态成员变量 所以是全局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 中的静态成员变量 Singleton<IActivityManager>,获取其 mInstance 属性,创建动态代理类 IActivityManagerProxy,替换 mInstance。详情已经在上述注释中标明。

之后在 Application 中调用:

<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,相应的,在 AMS 允许应用程序进程启动 Activity 时,我们应当将占位 intent 还原为 targetIntent。

那么什么时间点还原合适呢?

我们知道 ActivityThread 作为应用程序进程的主线程,在很多方面起了关键的作用,其中包括 Activity 的启动。其中 handleLaunchActivity 中有一行源码如下:

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

这里 r 为 ActivityClientRecord 类型,它有个 Intent 类型的成员变量名为 intent,这个 intent 就是上面 AMS 传回给应用程序进程的 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 来分发 AMS 发来的各种事件,其中 Handler 的 dispatchMessage 源码如下:

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

结合上述源码,我们知道了 msg.obj 中包含了之前了占位 intent,所以我们只需要将 H 的 mCallback 赋值为自定义的 Callback,并在 Callback.handleMessage 中做替换 intent 的操作,之后再重新手动调用 H.handleMessage(msg); 即可。具体实现如下:

首先实现自定义的 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) {
        //执行原handleMessage方法之前执行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();
            }
        }
        //手动重新调用handleMessage
        mHandler.handleMessage(msg);
        return true;
    }
}

接下来需要把我们上述自定义的 HCallback 赋值给 ActivityThread.H.mCallback,在 HookHelper 类中添加如下方法:

<6>

    /**
     * 目标是对 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 单进程只有一个,可以通过它的静态成员变量 sCurrentActivityThread 获得,获取到之后将 HCallback 赋值给 mH.mCallback 即可。

最后记得在上述 Application 中调用:

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

通过上述俩大步骤,我们在项目中创建任意 Activity,然后删除掉它在 AndroidManifest 中的配置,然后通过下面代码也可以正常启动,如示例:

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

那么有一个问题:这样启动的 Activity 可以正常遵从 AMS 对生命周期的管理吗?
答案是肯定,AMS 通知应用程序进程创建 Activity 之后是通过 Token 进行后续生命周期通信的,而 Token 依赖于真实创建的 TargetActivity,所以 TargetActivity 是有生命周期的。有兴趣的可以单独研究源码,这里不再深入探讨。

步骤3:加载插件 dex

步骤1、2实现了不配置 AndroidManifest 也能正常启动 Activity,但我们的终极目标是启动外部 apk,首先就需要把外部 apk 加载进来。

使用 Android 提供的 DexClassLoader 可以加载外部 dex 文件或加载包含 dex 的文件,如 apk、jar 等。这里我创建了 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 得到对应插件的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 加载了进来,他的构造函数如下:

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

四个参数分别是:
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 卡的权限:

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

Android 6.0 及以上还需要动态获取,所以假如报 ClassNotFound 异常,检查你的 App 是否真的有 sd 卡读写权限。

之后就可以尝试在宿主 App 中启动我们的目标页面了:

<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 注意有可能是权限问题
            e.printStackTrace();
        }
    }

这里我通过加载了外部 apk 文件的 ClassLoader 去获取目标页面 com.app.dixon.plugin.MainActivity 的 Class,之后创建 intent,并赋值 HookHelper.TARGET_INTENT_NAME 用于标记这是一个插件 apk 的页面,便于后续识别。最后通过 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 只是用于创建 Intent,在上述步骤2中、真正 new Activity 时,使用的 ClassLoader 仍然是宿主 App 的 ClassLoader,宿主 App 的 ClassLoader 从来没有加载过外部插件 apk,当然会报 ClassNotFoundException。

步骤4:替换插件 Activity ClassLoader

经过上述分析,我们需要在加载插件 apk 时,将宿主 App 的 ClassLoader 替换为自定义的 DexClassLoader。

替换时机一定和 new Activity 的时机有关,上面我们说到 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);
            }
        }
        ...

经常做单元测试的同学可能对 Instrumentation 这个类不会陌生,它是 ActivityThread 的成员变量之一,用于转交执行 Activity 的一些关键方法。这里可以看到 Activity 就是它创建的,它的 newActivity 源码如下:

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

第一个参数 cl 就是 Activity 对应的 ClassLoader,所以这儿的做法是,替换 ActivityThread.mInstrumentation,重写它的 newActivity 方法,使其在启动插件 apk 时,加载自定义的 DexClassLoader。下面是具体实现:

<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加载目标类
            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,这里我们利用该标识判断:如果是插件 apk,就使用自定义 DexClassLoader,否则还是走宿主 ClassLoader。

TARGET_INTENT_NAME意味着插件 apk 也需要传此标识,这对插件的独立开发是不友好的。
实际上有更好的实现方式,不需要传递标识也能识别是否是插件 apk 中的 class,详情参考 Github 源码。

接下来就是想办法把 ActivityThread.mInstrumention 替换为上述 InstrumentationProxy,在 HookHelper 中增加方法:

<10>

    /**
     * Hook newActivity 使其不加载系统、而加载自定义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 中调用:

<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 正常找到,且该页面能正常创建了。

但是,仍然发生了 Crash,这次的异常是资源找不到。

仔细想想,我们在加载外部插件 apk 的时候,从头到尾都只加载了 Class,没有加载其资源,插件 Activity 使用的 mResources 是宿主 App 的,资源当然会找不到!

步骤5:替换插件 Activity mResources

在 AppClassLoaderHelper 中增加如下方法加载插件资源并获取其 Resources,关于资源加载的原理这里不再深入探讨,直接看下面源码:

<11>

    /**
     * @param appPath
     * @return 得到对应插件的Resource对象
     */
    public static Resources getPluginResources(Context context, String appPath) {
        if (resourceCache.containsKey(appPath)) {
            return resourceCache.get(appPath);
        }
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            //反射调用方法addAssetPath(String path)
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            //将未安装的Apk文件的添加进AssetManager中,第二个参数是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加载目标类
            Activity activity = super.newActivity(AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk"), intentName, intent);
            //新增:
            //通过自定义的resources加载目标资源
            //这样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。通过查找该资源,发现是宿主 App 的 R.mipmap.ic_launcher,就是 Android app 默认的图标。插件 Activity 使用的资源是自己 apk 的,当它使用宿主 app 的 id 去查找资源当然会找不到了。

分析到这里,我明白当前的错误也许和插件 app 使用的 AppTheme 有关系,也许是 TopBar 不完全由 Activity 掌控,导致 TopBar 的资源 id 仍然使用宿主 App 的。为了演示方便(其实是懒),我这里暂时将插件 apk 的目标页面设置成了没有 TopBar 的主题:

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

该问题后续已修复。

果然,再次启动插件 apk,终于成功了!

步骤6:后续实践

在插件 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 就是我们之前识别插件页面的标识,有了这个标识,才会给当前 Activity 加载正确的 ClassLoader 和 Resources。这里测试插件 A 页面启动插件 B 页面没有问题。

总结

插件化涉及到的内容很多,本文只是对插件化实现的一个从零开始的探索,原理基于此,相信今后扩展、完善、源码探索也就有据可循。

Github 源码地址,后续会不断完善。

本人能力有限,步骤1、2部分参考了 Android 进阶揭秘一书,错误之处还请指出。

[TOC]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容