深入理解Android插件化

插件化的APP可以根据上线后用户所需,下载所需要的组件。组件化是全部打包。需要理解:组件化是一个整体,无论哪个模块有问题,或者需要更新,都必须全部升级,插件化则不同,只需重新下载对应的组件就行了。

如何加载插件的普通类

首先我们需要知道在Java中一个类加载的流程:

插件化中,我们主要需要处理的就是类的加载,插件化需要用到反射,下图就是反射需要的一些基础知识:

反射比较消耗性能,主要有以下的几点导致的:

(1)反射会产生大量临时对象,导致GC;

(2)检查可见性,(private或者public);

(3)会生成字节码,但是这些字节码没有优化;

(4)自动装箱拆箱的操作。

(5)反射次数不是太多,没有超过1000的量级,无太大的影响。

类的加载都是由classloader来实现的,安卓中主要有三个类加载器:BootClassLoader,DexClassLoader, PathClassLoader,第一个用来加载Framework中的类,后两个用来加载自己写的类和谷歌的拓展库,例如AppCompatActivity等,需要注意的是,在Android8之后,DexClassLoader, PathClassLoader已经没有区分了,我们在加载类的时候,用的还是PathClassLoader,之所以还保留DexClassLoader,谷歌可能是考虑的拓展原因。 各个类的继承关系:

BootClassLoader是ClassLoader的一个内部类。

typescript 复制代码
// optimizedDirectory是odex的路径
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super((String)null, (File)null, (String)null, (ClassLoader)null);
    throw new RuntimeException("Stub!");
}

dex文件的生成命令(dx在build-tools目录下):

dx --dex --output = output.dex input.class

output.dex 指输出的dex文件的绝对路径

input.class 表述输入的class文件的绝对路径

比如,我们想通过dexClassLoader来加载一个自己生成的类,就可以通过上边的命令生成dex文件之后,使用以下代码:

java 复制代码
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/text.dex", MainActivity.this.getCacheDir().getAbsolutePath(), null, MainActivity.this.getClassLoader());
try {
    dexClassLoader.loadClass("com.enjoy.plugin.Test");
} catch (ClassNotFoundException e) {
    throw new RuntimeException(e);
}

这里的dexClassLoader只能加载自己添加的类(这里换成PathClassLoader也可以)。没法加载其他的类。加载完成后,就可以用反射来实现对其中方法的调用了:

ini 复制代码
try {
    Class<?> clazz = Class.forName("com.enjoy.plugin.Test");
    Method print = clazz.getMethod("print");
    print.invoke(null);
} catch (Exception e) {
    e.printStackTrace();
}

安卓中的类加载机制

需要注意,这里的DexClassLoader因为是我们自己使用,所以可以传入PathClassloader为父类,如果没有传入,就是空,所以这个图的使用需要注意这点。

class加载流程时序图

对应的源码流程就是:

dalvik.system.BaseDexClassLoader#findClass:

java 复制代码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // First, check whether the class is present in our shared libraries.
    if (sharedLibraryLoaders != null) {
        for (ClassLoader loader : sharedLibraryLoaders) {
            try {
                return loader.loadClass(name);
            } catch (ClassNotFoundException ignored) {
            }
        }
    }
    // Check whether the class in question is present in the dexPath that
    // this classloader operates on.
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c != null) {
        return c;
    }
    // Now, check whether the class is present in the "after" shared libraries.
    if (sharedLibraryLoadersAfter != null) {
        for (ClassLoader loader : sharedLibraryLoadersAfter) {
            try {
                return loader.loadClass(name);
            } catch (ClassNotFoundException ignored) {
            }
        }
    }
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException(
                "Didn't find class "" + name + "" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

之后会进入java.lang.ClassLoader#loadClass(java.lang.String, boolean)

scss 复制代码
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

然后进入java.lang.ClassLoader#findLoadedClass:

ini 复制代码
protected final Class<?> findLoadedClass(String name) {
    ClassLoader loader;
    if (this == BootClassLoader.getInstance())
        loader = null;
    else
        loader = this;
    return VMClassLoader.findLoadedClass(loader, name);
}

最终通过这个java.lang.VMClassLoader#findLoadedClass来实现类的加载。

一个app的所有文件都在dexElement中。我们加载自己写的代码时,会从前向后遍历这个数组,将插件的类与dexElement合并成一个,然后加载这个合并后的,就是插件化的基本思想。

实现插件化的步骤:

(1)获取数组的dexElements;

(2)获取插件的dexElements;

(3)合并两个dexElements;

(4)将新的dexElement复制到宿主的dexElements;

如何合并的代码:

ini 复制代码
public class LoadUtil {

    private final static String apkPath = "/sdcard/plugin-debug.apk";

    public static void loadClass(Context context) {

        /**
         * 宿主dexElements = 宿主dexElements + 插件dexElements
         *
         * 1.获取宿主dexElements
         * 2.获取插件dexElements
         * 3.合并两个dexElements
         * 4.将新的dexElements 赋值到 宿主dexElements
         *
         * 目标:dexElements  -- DexPathList类的对象 -- BaseDexClassLoader的对象,类加载器
         *
         * 获取的是宿主的类加载器  --- 反射 dexElements  宿主
         *
         * 获取的是插件的类加载器  --- 反射 dexElements  插件
         */
        try {
            // 获取宿主apk包中的pathList
            Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
            @SuppressLint("DiscouragedPrivateApi") Field pathListField = clazz.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            // 宿主的 类加载器
            ClassLoader pathClassLoader = context.getClassLoader();
            // DexPathList类的对象
            Object hostPathList = pathListField.get(pathClassLoader);
            // 宿主的 dexElements
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
            // 插件的 类加载器
            ClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(), null, pathClassLoader);
            // DexPathList类的对象
            Object pluginPathList = pathListField.get(dexClassLoader);
            // 获取插件的 dexElements
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
            // 宿主dexElements = 宿主dexElements + 插件dexElements
            // Object[] obj = new Object[]; // 不行,为什么呢?因为我们要通过反射把结果放到pathListField,而这个这个对象是Element,不是Object,所以不行。
            // 创建一个新数组
            Object[] newDexElements = (Object[]) 
            // 反射创建数组
            Array.newInstance(hostDexElements.getClass().getComponentType(), hostDexElements.length + pluginDexElements.length);
            // 将两个数组的数据拷贝到新的对象中。
            System.arraycopy(hostDexElements, 0, newDexElements, 0, hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, newDexElements, hostDexElements.length, pluginDexElements.length);
            // 赋值
            // hostDexElements = newDexElements
            // 替换宿主的dexPathList.
            dexElementsField.set(hostPathList, newDexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里边加载自己的pathElements的时候,用PathClassLoader也是可以的。因为之前提过,PathClassLoader和DexClassloadery在android8以后已经没有区别了。

如何启动插件的四大组件

上一节知识讲了普通类,而Activity这种是需要在Manifest中注册的,否则会报错,本节会讲解如何加载Activity。

Activity的启动流程

通过上边可以知道我们可以确定 Hook 点的大致位置。

  1. 在进入 AMS 之前,找到一个 Hook 点,用来将插件 Activity 替换为 ProxyActivity。
  2. 从 AMS 出来后,再找一个 Hook 点,用来将 ProxyActivity 替换为插件 Activity。

Hook的思路

我们需要解决插件的Activity没有在宿主的manifest注册的问题, 因为AMS会验证Activity是否在清单中注册了。在AMS验证的过程中,需要用一个宿主中一个占位的Activity来跳过验证,完成验证后,又需要将占位的Activity替换成我们真正需要启动的Activity。因此总共需要两步。

通过动态代理和反射实现hook。

(1)Hook尽量使用静态变量或者单例对象;

(2)尽量hook public的方法。

一般的,我们启动一个插件中的Activity会用这种方式,因为他和宿主不在同一个包下边:

ini 复制代码
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.enjoy.plugin", "com.enjoy.plugin.MainActivity"));
startActivity(intent);

最终会走到这里:

less 复制代码
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
        @Nullable Bundle options) {
    if (mParent == null) {
        options = transferSpringboardActivityOptions(options);
        // 这里是第一个hook点。因为intent中有我们的需要启动的Activity的信息。
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                ar.getResultData());
        }
        if (requestCode >= 0) {
            // If this start is requesting a result, we can avoid making
            // the activity visible until the result is received.  Setting
            // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
            // activity hidden during this time, to avoid flickering.
            // This can only be done when a result is requested because
            // that guarantees we will get information back when the
            // activity is finished, no matter what happens to it.
            mStartedActivity = true;
        }

        cancelInputsAndStartExitTransition(options);
        // TODO Consider clearing/flushing other event sources and events for child windows.
    } else {
        if (options != null) {
            mParent.startActivityFromChild(this, intent, requestCode, options);
        } else {
            // Note we want to go through this method for compatibility with
            // existing applications that may have overridden it.
            mParent.startActivityFromChild(this, intent, requestCode);
        }
    }
}

接着进入了这里:

scss 复制代码
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
        // 省略无关源码。
    try {
        intent.migrateExtraStreamToClipData(who);
        intent.prepareToLeaveProcess(who);
        // 我们可以通过动态代理来对此处做出修改,替换掉ActivityTaskManager.getService(),用我们自己实现的来hook他,完成Activity插件化第一步。
        int result = ActivityTaskManager.getService().startActivity(whoThread,
                who.getOpPackageName(), who.getAttributionTag(), intent,
                intent.resolveTypeIfNeeded(who.getContentResolver()), token,
                target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
        notifyStartActivityResult(result, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

通过动态代理来实现切面变成很适合现在的场景,因为:动态代理的核心在于它允许你在运行时创建一个实现了一组给定接口的新对象(代理对象)。 这个代理对象可以拦截到对任何接口方法的调用,然后将这些调用转发到一个InvocationHandler实例 动态代理的核心在于它允许你在运行时创建一个实现了一组给定接口的新对象(代理对象)。 这个代理对象可以拦截到对任何接口方法的调用,然后将这些调用转发到一个InvocationHandler实例。 这意味着,当通过代理对象调用任何方法时,实际上是在调用InvocationHandler的invoke方法。 在这个invoke方法里,你有机会在调用实际方法之前执行自定义逻辑。 使用动态代理拦截startActivity方法的调用时,实际上首先执行的是通过动态代理设置的InvocationHandler中的invoke方法逻辑。 invoke方法中的逻辑会在任何实际的startActivity逻辑执行之前运行。

分为2步完成第一步的hook:

(1)使用反射将系统中的 IActivityManager 对象替换为我们的代理对象 mInstanceProxy。那如何替换呢?

(2)实现动态代理。

完整源码:

ini 复制代码
public static void hookAMS() {
    try {
        // 获取 singleton 对象
        Field singletonField;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // 小于8.0
            Class<?> clazz = Class.forName("android.app.ActivityManagerNative");
            singletonField = clazz.getDeclaredField("gDefault");
        } else {
            Class<?> clazz = Class.forName("android.app.ActivityManager");
            singletonField = clazz.getDeclaredField("IActivityManagerSingleton");
        }
        singletonField.setAccessible(true);
        // 拿到系统的IActivityManagerSingleton单例对象,并在后边替换他,用来执行我们自己的流程。
        Object singleton = singletonField.get(null);
        // 获取 系统的 IActivityManager 对象
        Class<?> singletonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        // 获取mInstance,这个时候还没有替换,这个是系统原有的mInstance, 我们就是不想改变原有执行流程。这个值必须保存下来。
        final Object mInstance = mInstanceField.get(singleton);
        // 获取动态代理的接口,我们代理的就是mInstance对象。
        Class<?> iActivityManager = Class.forName("android.app.IActivityManager");

        // 创建动态代理对象,用来替换系统的IActivityManagerSingleton,用动态代理才能实现我们修改intent参数的效果。
        Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                // 这里为什么是一个数组?因为一个接口可能有多个实现类。
                new Class[]{iActivityManager}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args/*通过他来获取intent*/) throws Throwable {
                        // do something
                        // Intent的修改 -- 过滤
                        /**
                         * IActivityManager类的方法
                         * startActivity(whoThread, who.getBasePackageName(), intent,
                         *                         intent.resolveTypeIfNeeded(who.getContentResolver()),
                         *                         token, target != null ? target.mEmbeddedID : null,
                         *                         requestCode, 0, null, options)
                         *                         method表示的是方法对应的对象。
                         */
                        // 过滤
                        if ("startActivity".equals(method.getName())) {
                            int index = -1;

                            for (int i = 0; i < args.length; i++) {
                                if (args[i] instanceof Intent) {
                                    index = i;
                                    break;
                                }
                            }
                            // 启动插件的
                            Intent intent = (Intent) args[index];
                            Intent proxyIntent = new Intent();
                            proxyIntent.setClassName("com.enjoy.leo_plugin",
                                    "com.enjoy.leo_plugin.ProxyActivity");
                            proxyIntent.putExtra(TARGET_INTENT, intent);
                            args[index] = proxyIntent;
                        }

                        // args  method需要的参数  --- 不改变原有的执行流程
                        // mInstance 系统的 IActivityManager 对象,这样就不会影响原有的系统流程了,此时的intent内部的activity参数已经被修改了。这里会走startactivity的真实流程。
                        return method.invoke(mInstance, args);
                    }
                });

        // ActivityManager.getService() 替换成 proxyInstance,这样就能不影响系统的执行流程了。这样当系统走到
        // int result = ActivityTaskManager.getService().startActivity(whoThread,
        //                    who.getOpPackageName(), who.getAttributionTag(), intent,
        //                    intent.resolveTypeIfNeeded(who.getContentResolver()), token,
        //                    target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
        //                    ActivityTaskManager.getService()实际上使用的是我们替换后的单例了。调用startActivity走的是我们的invoke方法。
        // ActivityManager.getService() 替换成 proxyInstance,只有通过代理对象,
        // 这样我们才能真正的执行proxyInstance.startActivity(),所以我们需要进行替换。否则上边的逻辑根本不会触发。
        mInstanceField.set(singleton, proxyInstance);
        // 我们不能这样调用,因为我们需要通过startActivity(intent);一步一步的走到hook点,
        // 也就是ActivityTaskManager.getService().startActivity,之后才去进行intent的替换,因此一定不能自己执行
        // proxyInstance.startActivity();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

第一步已经完成了,如何完成hook的第二步呢?在出来的时候,会调用 Handler 的 handleMessage,看下 Handler 的源码。

typescript 复制代码
public void handleMessage(Message msg) { }
 
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            // 这里hook之后,一定要让他返回false,不然会影响后边的handleMessage方法
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

当 mCallback != null 时,首先会执行 mCallback.handleMessage(msg),再执行 handleMessage(msg),所以我 们可以将 mCallback 作为 Hook 点,创建它。ok,现在问题就只剩一个了,就是找到含有 intent 的对象。

java 复制代码
// android/app/ActivityThread.java public void handleMessage(Message msg) {
    switch (msg.what) {
        case LAUNCH_ACTIVITY: {
            final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
        } break;
    }
}
 
static final class ActivityClientRecord {
    Intent intent;
}

可以看到,在ActivityClientRecord 类中,刚好就有个 intent,而且这个类的对象,我们也可以获取到,就是msg.obj

完整源码:

ini 复制代码
/**
 * 第二阶段,将proxyactivity替换回来为我们的目标Activity。
 */
public static void hookHandler() {
    try {
        // 获取 ActivityThread 类的 Class 对象
        Class<?> clazz = Class.forName("android.app.ActivityThread");

        // 获取 ActivityThread 对象
        Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
        activityThreadField.setAccessible(true);
        Object activityThread = activityThreadField.get(null);

        // 获取 mH 对象
        Field mHField = clazz.getDeclaredField("mH");
        mHField.setAccessible(true);
        final Handler mH = (Handler) mHField.get(activityThread);

        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);

        // 创建的 callback
        Handler.Callback callback = new Handler.Callback() {

            @Override
            public boolean handleMessage(@NonNull Message msg) {
                // 通过msg  可以拿到 Intent,可以换回执行插件的Intent

                // 找到 Intent的方便替换的地方  --- 在这个类里面 ActivityClientRecord --- Intent intent 非静态
                // msg.obj == ActivityClientRecord
                switch (msg.what) {
                    case 100:
                        try {
                            Field intentField = msg.obj.getClass().getDeclaredField("intent");
                            intentField.setAccessible(true);
                            // 启动代理Intent
                            Intent proxyIntent = (Intent) intentField.get(msg.obj);
                            // 启动插件的 Intent
                            Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                            if (intent != null) {
                                intentField.set(msg.obj, intent);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        break;
                    case 159:
                        try {
                            // 获取 mActivityCallbacks 对象
                            Field mActivityCallbacksField = msg.obj.getClass()
                                    .getDeclaredField("mActivityCallbacks");

                            mActivityCallbacksField.setAccessible(true);
                            List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);

                            for (int i = 0; i < mActivityCallbacks.size(); i++) {
                                if (mActivityCallbacks.get(i).getClass().getName()
                                        .equals("android.app.servertransaction.LaunchActivityItem")) {
                                    Object launchActivityItem = mActivityCallbacks.get(i);

                                    // 获取启动代理的 Intent
                                    Field mIntentField = launchActivityItem.getClass()
                                            .getDeclaredField("mIntent");
                                    mIntentField.setAccessible(true);
                                    Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);

                                    // 目标 intent 替换 proxyIntent
                                    Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                                    if (intent != null) {
                                        mIntentField.set(launchActivityItem, intent);
                                    }
                                }
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        break;
                }
                // 必须 return false
                return false;
            }
        };

        // 替换系统的 callBack
        mCallbackField.set(mH, callback);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

如何加载插件的资源

首先我们需要弄明白资源是怎么加载到Activity中去的。 资源加载的时序图:

对应的源码流程:

android.app.ActivityThread#performLaunchActivity

ini 复制代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ActivityInfo aInfo = r.activityInfo;
    if (r.packageInfo == null) {
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, mCompatibilityInfo,
                Context.CONTEXT_INCLUDE_CODE);
    }

    ComponentName component = r.intent.getComponent();
    if (component == null) {
        component = r.intent.resolveActivity(
            mInitialApplication.getPackageManager());
        r.intent.setComponent(component);
    }

    if (r.activityInfo.targetActivity != null) {
        component = new ComponentName(r.activityInfo.packageName,
                r.activityInfo.targetActivity);
    }
    // 创建Activity的contenxt,同时会创建资源文件
    ContextImpl appContext = createBaseContextForActivity(r);
    
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
    final int displayId = ActivityClient.getInstance().getDisplayId(r.token);
    // 然后会进入这里
    ContextImpl appContext = ContextImpl.createActivityContext(
            this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);

    final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
    // For debugging purposes, if the activity's package name contains the value of
    // the "debug.use-second-display" system property as a substring, then show
    // its content on a secondary display if there is one.
    String pkgName = SystemProperties.get("debug.second-display.pkg");
    if (pkgName != null && !pkgName.isEmpty()
            && r.packageInfo.mPackageName.contains(pkgName)) {
        for (int id : dm.getDisplayIds()) {
            if (id != DEFAULT_DISPLAY) {
                Display display =
                        dm.getCompatibleDisplay(id, appContext.getResources());
                appContext = (ContextImpl) appContext.createDisplayContext(display);
                break;
            }
        }
    }
    return appContext;
}

之后会进入ContextImpl :

less 复制代码
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
static ContextImpl createActivityContext(ActivityThread mainThread,
        LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
        Configuration overrideConfiguration) {
    //  ......
    // will be rebased upon. 这里边会将activity与资源绑定
    context.setResources(resourcesManager.createBaseTokenResources(activityToken,
            packageInfo.getResDir(),
            splitDirs,
            packageInfo.getOverlayDirs(),
            packageInfo.getOverlayPaths(),
            packageInfo.getApplicationInfo().sharedLibraryFiles,
            displayId,
            overrideConfiguration,
            compatInfo,
            classLoader,
            packageInfo.getApplication() == null ? null
                    : packageInfo.getApplication().getResources().getLoaders()));
    context.setDisplay(resourcesManager.getAdjustedDisplay(
            displayId, context.getResources()));
    return context;
}

之后进入ResourcesManager:

less 复制代码
public @Nullable Resources createBaseTokenResources(@NonNull IBinder token,
        @Nullable String resDir,
        @Nullable String[] splitResDirs,
        @Nullable String[] legacyOverlayDirs,
        @Nullable String[] overlayPaths,
        @Nullable String[] libDirs,
        int displayId,
        @Nullable Configuration overrideConfig,
        @NonNull CompatibilityInfo compatInfo,
        @Nullable ClassLoader classLoader,
        @Nullable List<ResourcesLoader> loaders) {
    try {
        // 省略无关代码。。。。。。
        // Now request an actual Resources object.
        // 这里实现绑定。
        return createResourcesForActivity(token, key,
                /* initialOverrideConfig */ Configuration.EMPTY, /* overrideDisplayId */ null,
                classLoader, /* apkSupplier */ null);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    }
}

private Resources createResourcesForActivity(@NonNull IBinder activityToken,
        @NonNull ResourcesKey key, @NonNull Configuration initialOverrideConfig,
        @Nullable Integer overrideDisplayId, @NonNull ClassLoader classLoader,
        @Nullable ApkAssetsSupplier apkSupplier) {
        // 这里完成了资源与Activity的绑定。
        ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier);
        if (resourcesImpl == null) {
            return null;
        }

        return createResourcesForActivityLocked(activityToken, initialOverrideConfig,
                overrideDisplayId, classLoader, resourcesImpl, key.mCompatInfo);
    }
}

private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
        @NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) {
    ResourcesImpl impl = findResourcesImplForKeyLocked(key);
    if (impl == null) {
        impl = createResourcesImpl(key, apkSupplier);
        if (impl != null) {
            mResourceImpls.put(key, new WeakReference<>(impl));
        }
    }
    return impl;
}

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
        @Nullable ApkAssetsSupplier apkSupplier) {
    // 这一行代码就实现了资源管理的创建。
    final AssetManager assets = createAssetManager(key, apkSupplier);
    if (assets == null) {
        return null;
    }

    final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
    daj.setCompatibilityInfo(key.mCompatInfo);

    final Configuration config = generateConfig(key);
    final DisplayMetrics displayMetrics = getDisplayMetrics(generateDisplayId(key), daj);
    final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj);

    if (DEBUG) {
        Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
    }
    return impl;
}

private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
        @Nullable ApkAssetsSupplier apkSupplier) {
    final AssetManager.Builder builder = new AssetManager.Builder();

    final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
    // 所有的资源都会被保存起来,以便及时调用。
    for (int i = 0, n = apkKeys.size(); i < n; i++) {
        final ApkKey apkKey = apkKeys.get(i);
        try {
            builder.addApkAssets(
                    (apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey));
        } catch (IOException e) {
            if (apkKey.overlay) {
                Log.w(TAG, String.format("failed to add overlay path '%s'", apkKey.path), e);
            } else if (apkKey.sharedLib) {
                Log.w(TAG, String.format(
                        "asset path '%s' does not exist or contains no resources",
                        apkKey.path), e);
            } else {
                Log.e(TAG, String.format("failed to add asset path '%s'", apkKey.path), e);
                return null;
            }
        }
    }

根据之前插件化启动类的方式,我们是通过hook宿主的代码dexElements。而assets.addAssetPath(插件的资源)把宿主的资源 添加到集合中使用资源--直接使用到插件的资源了。

因此,针对资源的插件化,就有以下两种实现方式: 1.插件的资源 和 宿主的资源 直接合并 2.专内创建一个Resource或者AssetManger(其实ResourceManager最终也是调用AssetManger) 加载插件的资源. 第一种方式处理一个问题:资源冲突,怎么解决呢?资源编译完成后,会变成这种类似的编号:0x7f0a000a 这个编号中: 7f:apk包的id;0a:资源类型的 id;000a:资源在其类型中的唯一标识符

7f:一般我们修改为7e~ff之间的数字(利用aapt修改),

0a:资源类型的 id,可以修改为01++。

000可以修改为:0000++。

为了实现这样的效果,我们需要先了解apk打包完整流程:

资源ID赋值的流程:

我们需要修改aapt工具,修改它里边的源码,就能够实现自定义资源的ID了。

第二种方式就是通过反射,让assets.addAssetPath去加载我们插件的资源就可以解决了。

arduino 复制代码
public static Resources loadResource(Context context) {
    // assets.addAssetPath(key.mResDir)
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        // 让 这个 AssetManager对象 加载的 资源为插件的
        Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
        // 填写的是资源加载的路径。
        addAssetPathMethod.invoke(assetManager, apkPath);
        Resources resources = context.getResources();
        // 加载插件的资源的 resources
        return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

那么我们怎么让宿主获取插件的这个Resources呢?

scala 复制代码
public class BaseActivity extends AppCompatActivity {

    protected Context mContext;

    @Override
    public Resources getResources() {
//        if (getApplication() != null && getApplication().getResources() != null) {
//            return getApplication().getResources();
//        }
//        return super.getResources();
//
        Resources resources = LoadUtil.getResources(getApplication()/*这里传入Activity会导致栈溢出崩溃*/);
        // 如果插件作为一个单独的app,返回 super.getResources()
        return resources == null ? super.getResources() : resources;
    }

不过该方式还是有资源冲突的问题,后边会提出解决方案。

我们只需要在插件里边的BaseActivity去做这个操作就行了。因为Application无论是宿主还是插件,是唯一的。

无论是aapt还是通过自定义Resource,都可能存在这样的问题:

// 宿主的 0x7f07004e decor_content_parent // 插件的 0x7f07004d decor_content_parent, 两者的id值不一样。

decor_content_parent,这个id有在androidx.appcompat.app.AppCompatDelegateImpl#createSubDecor使用,如果这个id在宿主和插件中不相同,就有可能会出现资源找不到的错误,因为无论是宿主还是插件,用到的AppCompatDelegateImpl是同一个,这个是有双亲委派机制以及PathClassLoader中的Element数组决定的。插件正常应该是使用0x7f07004d,但实际拿到的是0x7f07004e。如果我们新创建一个插件的上下文,同时将这个上下文与资源绑定,就能解决这个问题了。

插件的基类的onCreate:

less 复制代码
    // 该方式可以完全解决插件影响宿主的问题。
    // decor_content_parent这个id有在androidx.appcompat.app.AppCompatDelegateImpl#createSubDecor使用,
    // 如果这个id在宿主和插件中不相同,就有可能会出现资源找不到的错误,因为无论是宿主还是插件,
    // 用到的AppCompatDelegateImpl是同一个,这个是有双亲委派机制以及PathClassLoader中的Element数组决定的。
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Resources resources = LoadUtil.getResources(getApplication());
        // 使用新的context就是为解决注释中的问题。
        mContext = new ContextThemeWrapper(getBaseContext(), 0);

        Class<? extends Context> clazz = mContext.getClass();
        try {
            Field mResourcesField = clazz.getDeclaredField("mResources");
            mResourcesField.setAccessible(true);
            mResourcesField.set(mContext, resources);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用时,需要这样使用:

scala 复制代码
public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 暂时注释,没涉及到资源
//        setContentView(R.layout.activity_main);
        Log.e("leo", "onCreate: 启动插件的Activity");
        // 通过该方式实现效果。
        View view = LayoutInflater.from(mContext).inflate(R.layout.activity_main, null);
        setContentView(view);
    }
}
相关推荐
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端
永远不打烊3 小时前
librtmp 原生API做直播推流
前端