众所周知,在Android中,四大组件是需要在AndroidManifest.xml文件中注册之后才能调用的,但是插件APP可能并没有安装,只是放在终端的某个存储路径上,故系统会找不到插件里面用到的四大组件;那么如果宿主APP需要调用插件APP的四大组件呢,比如,宿主APP要启动插件APP中的某个Activity,需要怎么操作呢?

本篇文章只讲述两个问题:

  1. 宿主如何启动插件中的Activity;
  2. 插件APP不安装的情况下,插件APP中的资源并没有解压到系统data目录,需要怎么去调用插件中的资源(String,drawable等);

用到的技术:

  1. Hook Activity;
  2. Java动态代理;
  3. Java 反射;
  4. 熟悉AMS启动Activity相关流程,可参考;
  5. 熟悉AssetManager加载app资源相关流程;

不熟悉相关技术的,可以先行做个了解,明白基础的用法即可。

宿主APP中启动插件APP中的Activity

要从宿主中启动插件的Activity,但是插件并没有安装,如果直接启动,Android系统会抛出没有在AndroidManifest.xml中注册该Activity的错误,要解决这个问题,这里就需要用到Hook技术,绕过AMS对Activity的检查。

HOOK简介

在Java中,正常情况下,对象A调用对象B,对象B处理完数据之后,会将结果返回给A,如下图:

android hook interface参数 android hook原理_java

加入HOOK技术后,就会变成如下形式:

android hook interface参数 android hook原理_java_02

HOOK可以是方法,也可以是对象,它像一个钩子一样挂在对象B上,当A调用B的时候,会先经过HOOK,那么HOOK就可以做一些小动作,起到“欺上瞒下”的作用。对象B就是我们常说的HOOK点,为了保证HOOK的稳定性,HOOK点一般选择容易找到,并且不易变化的对象,如静态变量和单例。

那么按照HOOK的思路,首先我们在宿主APP中创建一个代理ProxyActivity继承自Activity,并且在宿主的清单文件(AndroidManifest.xml)中注册,当启动插件Activity的时候,在AMS检测之前,先找到一个HOOK点,将目标Activity替换为ProxyActivity(也就是假装启动ProxyActivity),等检测完之后,再找一个HOOK点,将目标Activity替换回来,这样就成功绕过了系统的检测。

要寻找到合适的HOOK点,需要熟悉AMS启动Activity的流程:

android hook interface参数 android hook原理_java_03

通过上图,我们可以确定HOOK的大概位置:

  1. 在进入AMS之前,找HOOK点,将目标Activity(插件中的Activity)替换为ProxyActivity;
  2. 在从AMS出来之后,找HOOK点,将ProxyActivity替换为目标Activity;

启动Activity的过程,在这里不做详述,这里只说明具体的HOOK点(注意此HOOK点不是唯一的,可以根据自己对Android源码的理解,自己选择认为合适的HOOK点),这里也可以参照滴滴的VirtualAPK中的PluginManager中的操作,选择HOOK点:

//android/app/Instrumentation.java
public ActivityResult execStartActivity(
    Context who, IBinder contextThread, IBinder token, String target,
    Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                ActivityResult result = null;
                if (am.ignoreMatchingSpecificIntents()) {
                    result = am.onStartActivity(intent);
                }
                if (result != null) {
                    am.mHits++;
                    return result;
                } else if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        // 这儿就是我们的 Hook 点,替换传入 startActivity 方法中的 intent 参数
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target, requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

找到了HOOK点,具体怎么操作呢?

这儿就需要用到前面提到的动态代理,需要替换的就是ActivityManager.getService()这个对象;简单的说,就是在此处代码执行之前,将ActivityManager.getService()这个对象替换我们自己代码中提供的代理对象,这样在执行startActivity方法的时候,其实执行的就是在我们自己在代码中做过一些小动作的startActivity方法,把startActivity方法中的第三个参数intent,原本用于启动目标Activity的Intent替换为启动ProxyActivity的Intent。

这儿这么做的目的就是因为ProxyActivity是在宿主APP中的,并且是在AndroidManifest.xml中声明的,进入AMS之后,就不会抛出Activity没有声明的错误。

接着上面的思路,要替换ActivityManager.getService()这个对象,先看下它返回的是啥?

//android/app/ActivityManager.java
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;
            }
        };

ActivityManager.getService()返回的是一个IActivityManager对象,并且可以看出这儿的IActivityManager是从Singleton的get()方法中获取的,看一下Singleton的get方法:

//android/util/Singleton.java
package android.util;

/**
 * Singleton helper class for lazily initialization.
 *
 * Modeled after frameworks/base/include/utils/Singleton.h
 *
 * @hide
 */
public abstract class Singleton<T> {
    private T mInstance;

    protected abstract T create();

    //可以看到get方法其实返回的就是mInstance
    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}

屡清楚上面这个流程之后,需要做以下两点去执行HOOK的动作:

  1. 通过动态代理替换掉startActivity方法中的intent参数;
  2. 通过反射替换到调用startActivity方法的IActivityManager对象;

Talk is easy,show me the code:

public static final String TARGET_INTENT = "target_intent";
public static void hookAMS(){

    try {
        Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
        Class<?> activityManagerCls = Class.forName("android.app.ActivityManager");
        Field singletonField = activityManagerCls.getDeclaredField("IActivityManagerSingleton");
        if(!singletonField.isAccessible()){
            singletonField.setAccessible(true);
        }
        //IActivityManagerSingleton在ActivityManager中是静态的,可通过反射直接获取
        Object singleObj = singletonField.get(null);

        //Singletone中的get()方法返回值就是IActivityManager类型的对象
        Class<?> singleClz = Class.forName("android.util.Singleton");
        Field instanceField = singleClz.getDeclaredField("mInstance");
        if(!instanceField.isAccessible())
            instanceField.setAccessible(true);
        final Object mInstance = instanceField.get(singleObj);
        //也可通过Singleton类的get()获取,get方法返回的是mInstance,mInstance就是IActivityManager对象
        //Method getMethod = singleClz.getDeclaredMethod("get");
        //final Object mInstance = getMethod.invoke(singleObj);

        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{iActivityManagerClass}, new InvocationHandler() {
                    Intent targetIntent = null;
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if(method.getName().equals("startActivity")){
                            int index = 0;
                            for(int i=0;i<args.length;i++){
                                //if(args[i].getClass().getName().contains("intent")){
                                if(args[i] instanceof Intent){
                                    index = i;
                                    break;
                                }
                            }

                            //目标intent,也就是启动插件Activity的Intent
                            targetIntent = (Intent) args[index];
                            Intent proxyIntent = new Intent();
                            //保存目标intent,绕过AMS之后,再次hook的时候需要用到
                            proxyIntent.putExtra(TARGET_INTENT,targetIntent);
                            Log.d("Rayman", "invoke: extra intent = "+targetIntent);

                            //此处Intnet应该是用于启动代理Activity
                            proxyIntent.setClassName("com.enjoy.myplugin",
                                    "com.enjoy.myplugin.ProxyActivity");
                            ComponentName cpn =  targetIntent.getComponent();
                            if(cpn != null){
                                String clsName = cpn.getClassName();
                                //此处做判断,因为可能会启动很多个不同的Activity,需要对不同的Activity做区分,有可能是启动宿主本身的Activity,就不需要执行替换的动作
                                if(!TextUtils.isEmpty(clsName) && clsName.equals("com.enjoy.plugin.MainActivity")){
                                    //替换目标intent
                                    args[index] = proxyIntent;
                                }
                            }
                            //到此处,启动插件Activity的intent就被我们替换了
                        }
                        //此处要反射调用IActivityManager的startActivity方法,需要一个IActivityManager对象
                        return method.invoke(mInstance,args);
                    }
                });
        //利用反射,替换调用startActivity的IActivityManager对象
        instanceField.set(singleObj,proxy);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

那么到这儿,进入AMS之前的HOOK就结束了,接下来要做的就是从AMS出来之后,再次HOOK,把启动目标Activity的intent再替换回来,用以启动目标Activity。

根据Activity启动流程,参考前面的流程图可知,在从AMS出来之后,进入ActivityThread,会调用Hander的handleMessage,我们看相关源码:(这儿查找源码目的就是再找出一个启动Activity之前的Intent,将之前已经被替换的Intent再替换回来)

//android/app/ActivityThread.java
public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
        switch (msg.what) {
            ...
            case EXECUTE_TRANSACTION:
                final ClientTransaction transaction = (ClientTransaction) msg.obj;
                mTransactionExecutor.execute(transaction);
                if (isSystem()) {
                     // Client transactions inside system process are recycled on the client side
                     // instead of ClientLifecycleManager to avoid being cleared before this                 // message is handled.
                    transaction.recycle();
                 }
                // TODO(lifecycler): Recycle locally scheduled transactions.
                break;
            ...
        }
}

EXECUTE_TRANSACTION的值是159。这里的ClientTransaction,也就是msg.obj,来看一下TransactionExecutor的execute方法。

//android/app/servertransaction/TransactionExecutor.java
public void execute(ClientTransaction transaction) {
    ......
    executeCallbacks(transaction);
    ......
}
/** Cycle through all states requested by callbacks and execute them at proper times. */
@VisibleForTesting
public void executeCallbacks(ClientTransaction transaction) {
    //获取transaction的Callbacks
    final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
    ......
    final int size = callbacks.size();
    for (int i = 0; i < size; ++i) {
       final ClientTransactionItem item = callbacks.get(i);
        if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
        final int postExecutionState = item.getPostExecutionState();
        final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
                item.getPostExecutionState());
        if (closestPreExecutionState != UNDEFINED) {
            cycleToPath(r, closestPreExecutionState, transaction);
        }
        //此处会调用ClientTransactionItem的execute
        item.execute(mTransactionHandler, token, mPendingActions);
        item.postExecute(mTransactionHandler, token, mPendingActions);
        ......   
    }
}

上面ClientTransaction的getCallbacks()方法如下:

//android/app/servertransaction/ClientTransaction.java

/** A list of individual callbacks to a client. */
@UnsupportedAppUsage
private List<ClientTransactionItem> mActivityCallbacks;

/**
 * Add a message to the end of the sequence of callbacks.
 * @param activityCallback A single message that can contain a lifecycle request/callback.
 */
public void addCallback(ClientTransactionItem activityCallback) {
    if (mActivityCallbacks == null) {
        mActivityCallbacks = new ArrayList<>();
    }
    mActivityCallbacks.add(activityCallback);
}

/** Get the list of callbacks. */
@Nullable
@UnsupportedAppUsage
List<ClientTransactionItem> getCallbacks() {
    return mActivityCallbacks;
}

getCallbacks()方法返回的是mActivityCallbacks,而mActivityCallbacks是通过addCallback方法赋值的,addCallback方法的掉用是在下面这ActivityStackSupervisor.java中的realStartActivityLocked方法中的:

//android/server/ActivityStackSupervisor.java
boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
            boolean andResume, boolean checkConfig) throws RemoteException {
    ......
    // Create activity launch transaction.
    final ClientTransaction clientTransaction = ClientTransaction.obtain(
                proc.getThread(), r.appToken);

        final DisplayContent dc = r.getDisplay().mDisplayContent;
        //此处调用addCallbacks,LauncherActivityItem是ClientTransactionItem的子类
        clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
                System.identityHashCode(r), r.info,
                // TODO: Have this take the merged configuration instead of separate global
                // and override configs.
                mergedConfiguration.getGlobalConfiguration(),
                mergedConfiguration.getOverrideConfiguration(), r.compat,
                r.launchedFromPackage, task.voiceInteractor, proc.getReportedProcState(),
                r.icicle, r.persistentState, results, newIntents,
                dc.isNextTransitionForward(), proc.createProfilerInfoIfNeeded(),
                r.assistToken));

        // Set desired final state.
        final ActivityLifecycleItem lifecycleItem;
        if (andResume) {
            lifecycleItem = ResumeActivityItem.obtain(dc.isNextTransitionForward());
        } else {
            lifecycleItem = PauseActivityItem.obtain();
        }
        clientTransaction.setLifecycleStateRequest(lifecycleItem);

        // Schedule transaction.
        mService.getLifecycleManager().scheduleTransaction(clientTransaction);
        ......

}
看下LaunchActivityItem源码:
//android/app/servertransaction/LaunchActivityItem.java
public class LaunchActivityItem extends ClientTransactionItem {

    //这里我们发现LaunchActivityItem中正好有个Intent
    @UnsupportedAppUsage
    private Intent mIntent;
    private int mIdent;
    @UnsupportedAppUsage
    private ActivityInfo mInfo;
    private Configuration mCurConfig;
    private Configuration mOverrideConfig;
    ......
}
这里LaunchActivityItem,中就有一个Intent可用于HOOK,这个流程scheduleTransaction方法最终会调用到上面提到的ActivityThread中handleMessage方法中,msg.obj对象就是ClientTransaction,ClientTransaction中的mActivityCallbacks中可获取LaunchActivityItem,再从LaunchActivityItem中获取需要的Intent。

找到Intent之后,接下来就是通过反射将此处的Intent替换掉,替换Intent,需要操作一下前面提到的ActivityThread中的Handler,也就是ActivityThread类的成员mH,那么看下Handler的源码:

//android/os/Handler.java
/**
 * Handle system messages here.
 */
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

当 mCallback != null 时,首先会执行 mCallback.handleMessage(msg),再执行 handleMessage(msg),所以我
们可以将 mCallback 作为 Hook 点,创建它。代码如下:

public static void hookHandler(){
    try {
        final Class<?> activtyThreadCls = Class.forName("android.app.ActivityThread");
        Field activitThreadField = activtyThreadCls.getDeclaredField("sCurrentActivityThread");
        activitThreadField.setAccessible(true);
        final Object activityTheadObj = activitThreadField.get(null);
        Field mHField = activtyThreadCls.getDeclaredField("mH");
        if(!mHField.isAccessible()){
            mHField.setAccessible(true);
        }
        Object mHObj = mHField.get(activityTheadObj);

        Class<?> handlerCls = Class.forName("android.os.Handler");
        Field mCallbackField = handlerCls.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);
        mCallbackField.set(mHObj,new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                switch(msg.what){
                    case 159:
                        try {
                            //Class<?> transExecutorCls = Class.forName("android.app.servertransaction.TransactionExecutor");
                            Object clientTransObj = msg.obj;
                            /*Method getActivityTokenMethod = clientTransObj.getClass().getDeclaredMethod("getActivityToken");
                            getActivityTokenMethod.setAccessible(true);
                            Object tokenObj = getActivityTokenMethod.invoke(clientTransObj);
                            此方法会报错,(greylist-max-o, reflection, denied),在Android Q上已经禁止反射调用了
                            Method getActivityClientMethod = activtyThreadCls.getDeclaredMethod("getActivityClient", IBinder.class);
                            getActivityClientMethod.setAccessible(true);
                            Object activityClientRecordObj = getActivityClientMethod.invoke(activityTheadObj,tokenObj);
                            Field intentField = activityClientRecordObj.getClass().getDeclaredField("intent");
                            intentField.setAccessible(true);
                            Intent proxyIntent = (Intent)intentField.get(activityClientRecordObj);*/

                            Field mActivityCallbacksField = clientTransObj.getClass().getDeclaredField("mActivityCallbacks");
                            mActivityCallbacksField.setAccessible(true);
                            List<Object> mActivityCallbacksObj = (List<Object>) mActivityCallbacksField.get(clientTransObj);
                            Object launchActivityItem = null;
                            for(Object obj:mActivityCallbacksObj){
                                Log.d("Rayman", "name: "+obj.getClass().getName());
                                    if(obj.getClass().getName().contains("android.app.servertransaction.LaunchActivityItem")){
                                    launchActivityItem = obj;
                                    break;
                                }
                            }
                            if(launchActivityItem == null) return false;
                            Log.d("Rayman", "launchActivityItem = "+launchActivityItem+",clsname = "+launchActivityItem.getClass().getName());
                            Field mIntentField = launchActivityItem.getClass().getDeclaredField("mIntent");
                            mIntentField.setAccessible(true);
                            Intent proxyIntent = (Intent)mIntentField.get(launchActivityItem);

                            //设置目标Intent
                            Intent targetIntent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                            Log.d("Rayman","targetIntent = "+targetIntent+","+proxyIntent.getComponent().getClassName());
                            //判断是否是之前我们hook时候,启动的代理Activity
                                if("com.enjoy.myplugin.ProxyActivity".equals(proxyIntent.getComponent().getClassName())){
                                /*if(targetIntent == null){
                                    targetIntent = new Intent();
                                        targetIntent.setClassName("com.enjoy.plugin","com.enjoy.plugin.MainActivity");
                                }*/
                                if(targetIntent != null)
                                        mIntentField.set(launchActivityItem,targetIntent);
                            }

                        } catch (Exception e) {
                            e.printStackTrace();
                        }

                        break;
                }
                return false;
            }
        });

       /*Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{mHObj.getClass()}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        return null;
                    }
                });*/
    } catch (Exception e) {
        e.printStackTrace();
    }
}

到这里,HOOK操作就完成了,只需要再宿主APP的Application的onCreate方法中,分别调用hookAMS和hookHandler方法就可以了。

接下来看下,怎么操作资源的加载,由于插件app没有安装,即便是在插件app中,也是没法调用本身的资源的,运行时会报找不到资源的错误。

一般在Android中通过,getResources获取Resources,然后通过Resources调用app相应资源。需要通过反射替换到原本系统中的Resources,或者说通过反射,自己写代码加载插件APP中的资源,这里当然需要跟踪Android系统是怎样加载APP资源的。

比如通常用如下方式获取调用app资源:

String appName = getResources().getString(R.string.app_name);
InputStream is = getAssets().open("icon_1.png");

getResources()方法的实现是在ContextImpl.java中,看ContextImpl源码:

//android/app/ContextImpl.java
@Override
public Resources getResources() {
    return mResources;
}

void setResources(Resources r) {
    if (r instanceof CompatResources) {
        ((CompatResources) r).setContext(this);
    }
    mResources = r;
}

@Override
public Context createApplicationContext(ApplicationInfo application, int flags)
        throws NameNotFoundException {
    LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),
            flags | CONTEXT_REGISTER_PACKAGE);
    if (pi != null) {
        ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken,
                new UserHandle(UserHandle.getUserId(application.uid)), flags, null, null);

        final int displayId = getDisplayId();
        //此处会调用createResources方法创建新的Resources
        c.setResources(createResources(mActivityToken, pi, null, displayId, null,
                getDisplayAdjustments(displayId).getCompatibilityInfo()));
        if (c.mResources != null) {
            return c;
        }
    }

    throw new PackageManager.NameNotFoundException(
            "Application package " + application.packageName + " not found");
}


private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
    int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
    final String[] splitResDirs;
    final ClassLoader classLoader;
    try {
        splitResDirs = pi.getSplitPaths(splitName);
        classLoader = pi.getSplitClassLoader(splitName);
    } catch (NameNotFoundException e) {
        throw new RuntimeException(e);
    }
    return ResourcesManager.getInstance().getResources(activityToken,
            pi.getResDir(),
            splitResDirs,
            pi.getOverlayDirs(),
            pi.getApplicationInfo().sharedLibraryFiles,
            displayId,
            overrideConfig,
            compatInfo,
            classLoader);
}

看ResourcesManager的,getResources方法:

//android/app/ResourcesManager.java
public @Nullable Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,//这个resDir其实就是需要加载资源的app路径
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    ......
    // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
    ResourcesImpl resourcesImpl = createResourcesImpl(key);
    if (resourcesImpl == null) {
        return null;
    }
    ......
}

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

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

protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        final AssetManager.Builder builder = new AssetManager.Builder();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
            try {
                //我们需要跟踪AssetManager是怎么加载app资源的,也就是addApkAssets方法
                builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
                        false /*overlay*/));
            } catch (IOException e) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }
    ......
}

AssetManager

//android/content/res/AssetManager.java
public static class Builder {
        private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>();

        //从这里继续跟踪,需要看mUserApkAssets在哪儿调用
        public Builder addApkAssets(ApkAssets apkAssets) {
            mUserApkAssets.add(apkAssets);
            return this;
        }

        public AssetManager build() {
            // Retrieving the system ApkAssets forces their creation as well.
            final ApkAssets[] systemApkAssets = getSystem().getApkAssets();

            final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
            final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];

            System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);

            final int userApkAssetCount = mUserApkAssets.size();
            for (int i = 0; i < userApkAssetCount; i++) {
                //mUserApkAssets列表最中会加入apkAssets变量中
                apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
            }

            // Calling this constructor prevents creation of system ApkAssets, which we took care
            // of in this Builder.
            final AssetManager assetManager = new AssetManager(false /*sentinel*/);
            //apkAssets变量又被赋值给assetManager.mApkAssets
            assetManager.mApkAssets = apkAssets;
            //调用native方法添加app资源文件
            AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                    false /*invalidateCaches*/);
            return assetManager;
        }
    }

//在此利用反射调用Builder的addApkAssets方法可以添加插件app的资源,又或者在9.0之前,我们可以看另一个方法如下:
/**
 * @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}
 * @hide
 */
@Deprecated
@UnsupportedAppUsage
public int addAssetPath(String path) {
    return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
}

private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
    ......
    //此处有没有很熟悉,跟前面Builder中最终调用的native方法是同一个,而且参数都是mApkAssets,在builder中有assetManager.mApkAssets = apkAssets;赋值语句,殊途同归。。。
    nativeSetApkAssets(mObject, mApkAssets, true);
    ......
}

用反射调用addAssetPath方法比较简单,因为需要的参数较少,这儿就用addAssetpath方法做反射处理,代码如下:

public static Resources loadResources(Context context){
        Class<?> assetManagerCls = AssetManager.class;
        try {
            Object assetManagerObj = assetManagerCls.newInstance();
            Method addAssetPathMethod = assetManagerCls.getDeclaredMethod("addAssetPath",String.class);
            addAssetPathMethod.setAccessible(true);

            //此处第一个参数请根据自己运行环境(手机或者虚拟机),申请权限,并且能够访问的存储路径
            addAssetPathMethod.invoke(assetManagerObj,"/storage/emulated/0/plugin-debug.apk");

            //创建需要替换掉的Resource
            Resources resources = context.getResources();
            return new Resources((AssetManager) assetManagerObj,resources.getDisplayMetrics(),resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

紧接着在插件APP中创建BaseActivity如下:

public class BaseActivity extends AppCompatActivity {

    protected Context mContext;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Resources resources = LoadResourcesUtils.loadResources(getApplicationContext());

        // 替换这个 context 的resource 为我们自己通过反射写入的 resource
        mContext = new ContextThemeWrapper(getBaseContext(),0);

        Class<?> contextCls = mContext.getClass();
        try {
            /*
            //ContextThemeWrapper.java
            @Override
            public Resources getResources() {
                return getResourcesInternal();
            }

            private Resources getResourcesInternal() {
                if (mResources == null) {
                    if (mOverrideConfiguration == null) {
                        mResources = super.getResources();
                    } else {
                        final Context resContext = createConfigurationContext(mOverrideConfiguration);
                        mResources = resContext.getResources();
                    }
                }
                return mResources;
            }*/

            Field mResourcesField = contextCls.getDeclaredField("mResources");
            mResourcesField.setAccessible(true);
            mResourcesField.set(mContext,resources);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*@Override
    public Resources getResources() {
        Resources resources = LoadUtils.loadResources(getApplicationContext());
        return resources != null ? resources:super.getResources();
    }*/
}

插件中的所有Activity都要继承自BaseActivity:

package com.enjoy.plugin;

import android.os.Bundle;
import android.util.Log;

public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("Rayman", "onCreate: this is plugin Activity...");

        String res = mContext.getResources().getString(R.string.test_plugin_res);
        Log.d("Rayman","onCreate plugin Activity res = "+res);
        //下面这种写法是不可用的,因为插件APP没有安装,在调用getResource的时候AssetManager无法获取资源路径,会报找不到资源的错误
        //Log.d("Rayman", "onCreate: xxx = "+getResources().getString(R.string.test_plugin_xxx));

    }
}

按照MainActivity中的方法调用资源就可以了,插件中其他的Activity调用资源也是相同的,到这儿通过反射,HOOK等技术调用插件App的Activity和资源就结束了。

总结一下,主要的操作步骤:

  1. 利用ClassLoader加载没有安装的插件APP,这个是前提条件;
  2. 利用HOOK、动态代理、反射等技术绕过AMS的检测,实现启动未注册的Activity;
  3. 利用反射技术,通过AssetManager加载插件APP的资源,实现对安装的APP资源的调用;

以上就是动态加载的基本内容了,实际操作可以参考github上滴滴的VirtualAPK的源码,参考实现,其实也没有必要重复造轮子,只是通过撸源码了解其中原理,做到知其然,知其所以然。