滴滴插件化解析(一)

我们这一节来分析一下滴滴插件化是如何启动插件的 Activity 的。

一、使用

  • 1.配置宿主工程的 Module#build.gradle
image
  • 2.配置插件工程的 Module#build.gradle
image
  • 3.将插件工程使用 assemblePlugin 任务打包出一个 apk 文件,放进宿主工程的 assets 目录中

  • 4.在宿主的 Application 中进行滴滴插件化工具的初始化

image
  • 5.在需要用到插件 apk 的资源的地方进行插件加载
image
  • 6.正常调用插件 apk 的 Activity、Service 等资源
image

二、插件化加载 Activity 的过程解析

  • 1.插件初始化

PluginManager.getInstance(context).init();

我们仔细看看这里做了什么工作:

public static PluginManager getInstance(Context base) {
        //宿主的 context
        if (sInstance == null) {
            synchronized (PluginManager.class) {
                if (sInstance == null)
                    sInstance = new PluginManager(base);
            }
        }

        return sInstance;
    }

    private PluginManager(Context context) {
        Context app = context.getApplicationContext();
        if (app == null) {
            this.mContext = context;
        } else {
            this.mContext = ((Application)app).getBaseContext();
        }
        prepare();
    }

    private void prepare() {
        Systems.sHostContext = getHostContext();
        //关键操作
        this.hookInstrumentationAndHandler();
        //关键操作
        this.hookSystemServices();
    }

    public void init() {
        mComponentsHandler = new ComponentsHandler(this);
        RunUtil.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                doInWorkThread();
            }
        });
    }
    
    private void hookInstrumentationAndHandler() {
        try {
            Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
            if (baseInstrumentation.getClass().getName().contains("lbe")) {
                // reject executing in paralell space, for example, lbe.
                System.exit(0);
            }

            //hook Instrumentation,set 进 ActivityThread 中
            final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
            Object activityThread = ReflectUtil.getActivityThread(this.mContext);
            ReflectUtil.setInstrumentation(activityThread, instrumentation);
            ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
            this.mInstrumentation = instrumentation;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

可以看到,PluginManager 初始化的时候回保存宿主的 Application context,之后会反射获取 ActivityThread 的 Instrumentation 对象,并且生成一个 VAInstrumentation 对象,再反射设置进 ActivityThread 中。

这一步很重要,因为 Instrumentation 涉及到加载 Activity 的过程,插件化能对 Android 系统进行欺上瞒下以及调用宿主的 Activity 就是因为 hook 了 Instrumentation。

  • 2.加载插件 apk
PluginManager.getInstance(mContext).loadPlugin(apk);
/**
     * load a plugin into memory, then invoke it's Application.
     * @param apk the file of plugin, should end with .apk
     * @throws Exception
     */
    public void loadPlugin(File apk) throws Exception {
        if (null == apk) {
            throw new IllegalArgumentException("error : apk is null.");
        }

        if (!apk.exists()) {
            throw new FileNotFoundException(apk.getAbsolutePath());
        }

        //调用 PackageParser.parsePackage 解析 apk,得到的信息封装进 loadedPlugin 对象
        LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
        if (null != plugin) {
            //加载过的插件缓存起来
            this.mPlugins.put(plugin.getPackageName(), plugin);
            synchronized (mCallbacks) {
                for (int i = 0; i < mCallbacks.size(); i++) {
                    //hookDataBindUtil 的时候会用到
                    mCallbacks.get(i).onAddedLoadedPlugin(plugin);
                }
            }
            //构造插件的 Application,并调用插件 Application 的 onCreate()
            plugin.invokeApplication();
        } else {
            throw  new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
        }
    }

可以看到,这里创建了一个 LoadedPlugin 对象,这个 LoadedPlugin 对象很重要,用来解析插件 apk 的资源。

/**
 * 资源加载
 * Created by renyugang on 16/8/9.
 */
public final class LoadedPlugin {

    public static LoadedPlugin create(PluginManager pluginManager, Context host, File apk) throws Exception {
        //需要注意 context 是宿主的 Context
        //apk 指的是插件的路径
        return new LoadedPlugin(pluginManager, host, apk);
    }
    
    ......
    
    LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
        this.mPluginManager = pluginManager;
        this.mHostContext = context;
        this.mLocation = apk.getAbsolutePath();
        this.mPackage = PackageParserCompat.parsePackage(context, apk, /*PackageParser.PARSE_MUST_BE_APK*/0);
        this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
        this.mPackageInfo = new PackageInfo();
        this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
        this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();

        if (Build.VERSION.SDK_INT >= 28
                || (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // Android P Preview
            try {
                this.mPackageInfo.signatures = this.mPackage.mSigningDetails.signatures;
            } catch (Throwable e) {
                PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
                this.mPackageInfo.signatures = info.signatures;
            }
        } else {
            this.mPackageInfo.signatures = this.mPackage.mSignatures;
        }
        this.mPackageInfo.packageName = this.mPackage.packageName;
        if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
            throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
        }
        this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
        this.mPackageInfo.versionName = this.mPackage.mVersionName;
        this.mPackageInfo.permissions = new PermissionInfo[0];
        this.mPackageManager = new PluginPackageManager();
        this.mPluginContext = new PluginContext(this);
        this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);

        //通过 COMBINE_RESOURCES 决定是否将插件资源加载到宿主中
        this.mResources = createResources(context, apk);
        //创建 ClassLoader
        this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());

        tryToCopyNativeLib(apk);

        // Cache instrumentations
        Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
        for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
            instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
        }
        this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
        this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);

        // Cache activities
        Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity activity : this.mPackage.activities) {
            activityInfos.put(activity.getComponentName(), activity.info);
        }
        this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
        this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);

        // Cache services
        Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
        for (PackageParser.Service service : this.mPackage.services) {
            serviceInfos.put(service.getComponentName(), service.info);
        }
        this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
        this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);

        // Cache providers
        Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
        Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
        for (PackageParser.Provider provider : this.mPackage.providers) {
            providers.put(provider.info.authority, provider.info);
            providerInfos.put(provider.getComponentName(), provider.info);
        }
        this.mProviders = Collections.unmodifiableMap(providers);
        this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
        this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);

        // Register broadcast receivers dynamically
        Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity receiver : this.mPackage.receivers) {
            receivers.put(receiver.getComponentName(), receiver.info);

            BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
            for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
                //静态广播转动态注册
                this.mHostContext.registerReceiver(br, aii);
            }
        }
        this.mReceiverInfos = Collections.unmodifiableMap(receivers);
        this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
    }

    private static ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) {
        File dexOutputDir = context.getDir(Constants.OPTIMIZE_DIR, Context.MODE_PRIVATE);
        String dexOutputPath = dexOutputDir.getAbsolutePath();
        //双亲委托机制加载,parent 为宿主的 ClassLoader,这样可以让插件模块调起宿主工程的 Activity
        //DexClassLoader: 可以从包含classes.dex的jar或者apk中,加载类,一般用于执行动态加载
        DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

        if (Constants.COMBINE_CLASSLOADER) {
            try {
                DexUtil.insertDex(loader);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return loader;
    }

    private static AssetManager createAssetManager(Context context, File apk) {
        try {
            AssetManager am = AssetManager.class.newInstance();
            ReflectUtil.invoke(AssetManager.class, am, "addAssetPath", apk.getAbsolutePath());
            return am;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static Resources createResources(Context context, File apk) {
        if (Constants.COMBINE_RESOURCES) {
            //将插件 Assetmanager 的路径传进去,适用于插件资源合并到宿主里面去的情况,
            // 最后插件通过宿主的 Resources 对象去访问宿主的资源
            Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
            //这里是宿主的 context
            ResourcesManager.hookResources(context, resources);
            return resources;
        } else {
            Resources hostResources = context.getResources();
            //获取插件 apk 的 AssetManager
            AssetManager assetManager = createAssetManager(context, apk);
            //适用于资源独立,返回插件独立的 Resources 对象,不与宿主有关系,无法访问到宿主的资源
            return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
            //return context.getResources();
        }
    }

    private static ResolveInfo chooseBestActivity(Intent intent, String s, int flags, List<ResolveInfo> query) {
        return query.get(0);
    }
    

可以看到,LoadedPlugin 负责生成 ClassLoader、Resources 对象来加载插件 apk 的Activity 和资源。

  • 3.正常调用插件的 Activity

这个是真正难的地方,这里需要结合 Android 启动一个 Activity 的过程来看。

Intent intent = new Intent();
intent.setClassName("com.example.moduleone", "com.example.moduleone.ModuleOneMainActivity");
startActivity(intent);

当调用 startActivity(intent); 时,其实是调用 startActivityForResult(intent, -1);然后就走到了:

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            
            //转到了 Instrumentation 启动 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) {
            
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
        
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

可以看到我们之前的重点关注对象 Instrumentation 出现了!!!

一起看看 Instrument 如何启动一个 Activity。

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        //当 intent.getComponent() 为空时,根据 intent 的 action,data,category 等
        // 去已加载的 plugin 中匹配到确定的 Activity
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        // null component is an implicitly intent
        if (intent.getComponent() != null) {
            //这句 Log 信息可以判断 Loadplugin 对象是否已经加载成功
            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                    intent.getComponent().getClassName()));
            //欺上瞒下,根据需要是否替换,替换的话就根据 launchode 替换插件的 Activity 为插件 Manifest.xml 占坑的 Activity
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }

        ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                intent, requestCode, options);

        return result;

    }
    
    private ActivityResult realExecStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        ActivityResult result = null;
        try {
            Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
                    int.class, Bundle.class};
            //调用宿主的 Instrument 对象使用 ActivityManagerProxy 和 AMS 进行跨进程通信
            result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
                    "execStartActivity", parameterTypes,
                    who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            if (e.getCause() instanceof ActivityNotFoundException) {
                throw (ActivityNotFoundException) e.getCause();
            }
            e.printStackTrace();
        }

        return result;
    }

重点看一下 this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);

public void markIntentIfNeeded(Intent intent) {
        if (intent.getComponent() == null) {
            return;
        }

        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        // 判断如果启动的是插件中类,则将启动的包名和Activity类名存到了intent中,可以看到这里存储明显是为了后面恢复用的
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            dispatchStubActivity(intent);
        }
    }

    private void dispatchStubActivity(Intent intent) {
        ComponentName component = intent.getComponent();
        String targetClassName = intent.getComponent().getClassName();
        LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
        ActivityInfo info = loadedPlugin.getActivityInfo(component);
        if (info == null) {
            throw new RuntimeException("can not find " + component);
        }
        int launchMode = info.launchMode;
        Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
        themeObj.applyStyle(info.theme, true);
        String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
        Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
        //intent通过setClassName替换启动的Activity为占坑Activity
        intent.setClassName(mContext, stubActivity);
    }

//Manifest.xml 写好的占坑 Activity
public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

public String getStubActivity(String className, int launchMode, Theme theme) {
        String stubActivity= mCachedStubActivity.get(className);
        if (stubActivity != null) {
            return stubActivity;
        }

        TypedArray array = theme.obtainStyledAttributes(new int[]{
                android.R.attr.windowIsTranslucent,
                android.R.attr.windowBackground
        });
        boolean windowIsTranslucent = array.getBoolean(0, false);
        array.recycle();
        if (Constants.DEBUG) {
            Log.d("LogUtils_StubActivity", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
        }
        //这里只是为了骗过系统,Activity 已在 Manifest.xml 中注册,所以这里的包名以及类的全名必须
        //和 Manifest.xml 占坑的相同
        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
        switch (launchMode) {
            case ActivityInfo.LAUNCH_MULTIPLE: {
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) {
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                }
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TOP: {
                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TASK: {
                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                break;
            }

            default:break;
        }

        mCachedStubActivity.put(className, stubActivity);
        return stubActivity;
    }
image

可以看到,本来启动未在 Manifest.xml 中注册的插件 Activity 应该会报错的,但是经过 VAInstrumentation 这么一个欺上瞒下,就做到了欺骗系统加载插件 Activity。

但是这里只是做到了一半,因为换了要启动的 Activity,欺骗过了 AMS 之后,最终要启动的不可能是占坑 Activity,还应该是我们的启动的目标 Activity 呀。

接下来的就要看 Activity 的启动过程了。

Instrumentation.execStartActivity() 会调用ActivityManagerService.startActivity() 来启动 Activity (IPC 过程)。这个过程 AMS 会对要启动的 Activity 进行 Activity 栈还有其他的处理。

APP 进程客户端:ActivityManagerProxy =====> Binder驱动 =====> ActivityManagerService:AMS 服务端

在 AMS 处理完启动 Activity 后,会调用:app.thread.scheduleLaunchActivity() (同样是 IPC 过程),这里的 thread 对应为 server 端,其实就是我们 APP 进程的 ActivityThread 中的 ApplicationThread 对象,而此时的 AMS 的 IApplicationThread 作为客户端,所以是 AMS 客户端调用 APP 进程服务端的 ApplicationThread.scheduleLaunchActivity() 方法,这样启动 Activity 的操作又回到了 APP 进程。

APP 进程服务端:ApplicationThread <===== Binder驱动 <===== ApplicationThreadProxy:AMS 客户端

这时会在 ApplicationThread 内部会调用 mH 类(类 H 是 ActivityThread 的内部类,并继承了 Handler)的 sendMessage() 方法,传递的标识为 H.LAUNCH_ACTIVITY,进入调用到 ActivityThread 的 handleLaunchActivity() 方法 :

ActivityThread.handleLaunchActivity() -> ActivityThread.performLaunchActivity() -> mInstrumentation.newActivity() (通过 ClassLoader 实例化 Activity 对象)-> Instrumentation.callActivityOnCreate() 调用 Activity 的 onCreate()。

至此,我们就知道了 Activity 的启动流程,那么应用到滴滴的插件化技术,我们可以知道,在 AMS 处理完毕后,会回调到 VAInstrumentation.newActivity()。我们重点看看这个方法:

@Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            //这个是宿主的 ClassLoader,而我们替换之后的Activity是插件 Manifest.xml 中声明的 Activity,
            // 实际上并没有这样的类,所以如果要加载的是插件的 占坑 Activity 一定会抛异常,加载宿主的就不会
            cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            //取出目标启动 Activity 的包名和类名
            ComponentName component = PluginUtil.getComponent(intent);
            //根据包名获取加载插件的 ClassLoader
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
            //取出要启动的插件目标 Activity 类名
            String targetClassName = component.getClassName();

            //这里的 className 就是插件占坑的 Activity 名字
            Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));

            if (plugin != null) {
                //这里传入的是构造的插件的 ClassLoader,所以能加载插件的 Activity
                Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
                activity.setIntent(intent);

                try {
                    // for 4.1+
                    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
                } catch (Exception ignored) {
                    // ignored.
                }

                return activity;
            }
        }

        //如果是启动宿主的 Activity,直接就跳到了这一步,中间的过程不会进入,intent 的内容也不会被替换
        return mBase.newActivity(cl, className, intent);
    }

可以看到,滴滴插件化也是通过在 Instrumentation 里面进行还原目标 Activity 的加载。这个做法侵入性比较小,所以兼容性好一点,感觉也比较优雅。

接下来我们看最后一步:Instrumentation.callActivityOnCreate()。这一步直接关联到了传说中的 Activity.onCreate()。

@Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        final Intent intent = activity.getIntent();
        //在「欺上瞒下」的时候进行标记为 true,否则就是启动 宿主的Activity,跳过资源替换的步骤
        if (PluginUtil.isIntentFromPlugin(intent)) {
            //这个 base 是宿主的 Application
            Context base = activity.getBaseContext();
            try {
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                //由于 ContextWrapper 和 Resources 资源加载相关,所以必须替换为插件的 Context 以及插件的 Resources
                //详细见 LoadedPlugin#createResources()
                ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
                //mBase 就是 pluginContext 对象,里面包装有宿主的 Application
                ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
                //之前在 LoadPlugin 对象的时候,就已经创建了插件的 Application,进行替换
                ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
                ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());

                // set screenOrientation
                ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
                if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                    activity.setRequestedOrientation(activityInfo.screenOrientation);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            int theme = PluginUtil.getTheme(mPluginManager.getHostContext(), intent);
            if (theme != 0) {
                activity.setTheme(theme);
            }
        }

        mBase.callActivityOnCreate(activity, icicle);
    }

//LoadPlugin#createResources()
private static Resources createResources(Context context, File apk) {
        if (Constants.COMBINE_RESOURCES) {
            //将插件 Assetmanager 的路径传进去,适用于插件资源合并到宿主里面去的情况,
            // 最后插件通过宿主的 Resources 对象去访问宿主的资源
            Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
            //这里是宿主的 context
            ResourcesManager.hookResources(context, resources);
            return resources;
        } else {
            Resources hostResources = context.getResources();
            //获取插件 apk 的 AssetManager
            AssetManager assetManager = createAssetManager(context, apk);
            //适用于资源独立,返回插件独立的 Resources 对象,不与宿主有关系,无法访问到宿主的资源
            return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
            //return context.getResources();
        }
    }

可以看到,对于要启动的插件 Activity,这里修改了插件的 mResources、mBase(Context)、mApplication 对象,以及设置一些可动态设置的属性,这里仅设置了屏幕方向。

注意啦,这里将 mBase 替换为 PluginContext,可以修改 Resources、AssetManager 以及拦截相当多的操作。

原本 Activity 的部分 get 操作

# ContextWrapper
@Override
public AssetManager getAssets() {
    return mBase.getAssets();
}

@Override
public Resources getResources()
{
    return mBase.getResources();
}

@Override
public PackageManager getPackageManager() {
    return mBase.getPackageManager();
}

@Override
public ContentResolver getContentResolver() {
    return mBase.getContentResolver();
}

替换之后:

# PluginContext

@Override
public Resources getResources() {
    return this.mPlugin.getResources();
}

@Override
public AssetManager getAssets() {
    return this.mPlugin.getAssets();
}

@Override
public ContentResolver getContentResolver() {
    return new PluginContentResolver(getHostContext());
}

这样可以很巧妙地对资源的加载进行拦截,甚至拦截和替换启动的插件 Service 等。

到这里,我们的插件 Activity 就可以正常启动啦。

三、优秀的参考资料

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