2.20 模块化&插件化

2 minute read

2.13.1 VirtualAPK

  • 框架架构图(来自官网) 框架架构图,来自官网

  • 使用

  • loadApk, 加载插件


  // PluginManager.java
  public void loadPlugin(File apk) throws Exception {
        ...

        LoadedPlugin plugin = createLoadedPlugin(apk);

        ...

        // ConcurrentHashMap保存plugin
        this.mPlugins.put(plugin.getPackageName(), plugin);
        ...
  }

  protected LoadedPlugin createLoadedPlugin(File apk) throws Exception {
        return new LoadedPlugin(this, this.mContext, apk);
  }

  public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
        ...
        // 初始化插件的PackageManager, Resource, ClassLoader(Dex)
        this.mPackageManager = createPluginPackageManager();
        this.mPluginContext = createPluginContext(null);
        ...
        // 把插件的native so拷贝宿主的nativedir下
        tryToCopyNativeLib(apk);
        ...
    }

  • Activity, 通过hookInstrumentation, 且在CoreLibrary下的AndroidManifest增加占位
  protected PluginManager(Context context) {
      // 初始化PluginManager时hook掉Instrumentation, IActivityManager和DataBindingUtil
      ...
      hookCurrentProcess();
  }

  protected void hookCurrentProcess() {
        hookInstrumentationAndHandler();
        hookSystemServices();
        hookDataBindingUtil();
  }

  // 1. hook Instrumentation, 由于Context#startActivity()实际调.
  // 用Instrumentation#execStartActivity, 因此这里可以把目标替换成占位Activity
  // 从而绕过PackageManager检查宿主Manifest是否注册了Activity, 另外当
  // 启动Activity, 即可ActivityThread#performLaunchActivity时, 会调
  // 用Instrumentation#newActivity, 在newActivity替换成目标插件实
  // 例, 从而完成对插件Activity的加载
  protected void hookInstrumentationAndHandler() {
    // 获取ActivityThread
    ActivityThread activityThread = ActivityThread.currentActivityThread();
    // 获取原来的Instrumentation
    Instrumentation baseInstrumentation = activityThread.getInstrumentation();
    final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
    // 通过反射, 把自定义VAInstrumentation赋值给ActivityThread
    Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
    Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
    Reflector.with(mainHandler).field("mCallback").set(instrumentation);
    this.mInstrumentation = instrumentation;
    ...
  }

  // 2. 启动Activity, 即Instrumentation#execStartActivity
  public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
        injectIntent(intent);
        return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
  }

  protected void injectIntent(Intent intent) {
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        ...
        // resolve intent with Stub Activity if needed
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
  }

  public void markIntentIfNeeded(Intent intent) {
        // search map and return specific launchmode stub activity
        ...
        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);
        ...
        int launchMode = info.launchMode;
        Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
        themeObj.applyStyle(info.theme, true);
        // 根据targetClassName, launchMode等信息, 找到占位Activity
        String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
        ...
        intent.setClassName(mContext, stubActivity);
    }

    // 3. 回到真正实例化Activity的方法, 即Instrumentation#newActivity
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            cl.loadClass(className);
            Log.i(TAG, String.format("newActivity[%s]", className));

        } catch (ClassNotFoundException e) {
            ComponentName component = PluginUtil.getComponent(intent);

            ...

            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);

            ...
            // 通过插件的DexClassLoader把插件Activity实例化
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

            ...

            return newActivity(activity);
        }

        return newActivity(mBase.newActivity(cl, className, intent));
    }

  • Activity插件实现原理总结:
    1. hook Instrumentation
    2. 在宿主的Manifest埋下占位Activity, 在Instrumentation#execStartActivity中替换成占位Activity, 绕过AMS的检查
    3. Instrumentation#newActivity中再实例化插件Activity并返回
  • Resource, 资源加载
protected Resources createResources(Context context, String packageName, File apk) throws Exception {
      if (Constants.COMBINE_RESOURCES) {
          // 插件资源合并到宿主中, 插件可以访问宿主的资源
          return ResourcesManager.createResources(context, packageName, apk);
      } else {
          // 生成新的AssetManager, 并把插件的资源加载...
          Resources hostResources = context.getResources();
          AssetManager assetManager = createAssetManager(context, apk);
          return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
      }
  }

  public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
        ...

        Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
        ResourcesManager.hookResources(hostContext, resources);
        return resources;
    }

    private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
       Resources hostResources = hostContext.getResources();
       Resources newResources = null;
       AssetManager assetManager;
       Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
       ...
       // 加载宿主资源
       assetManager = hostResources.getAssets();
       reflector.bind(assetManager);

       ...
       // 加载插件资源
       List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
       for (LoadedPlugin plugin : pluginList) {
           final int cookie3 = reflector.call(plugin.getLocation());
           if (cookie3 == 0) {
               throw new RuntimeException("createResources failed, can't addAssetPath for " + plugin.getLocation());
           }
       }
       ...
        newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
       // lastly, sync all LoadedPlugin to newResources
       for (LoadedPlugin plugin : pluginList) {
           plugin.updateResources(newResources);
       }
       // 返回加载好宿舍和插件资源的Resource对象
       return newResources;
   }
  • 总结
    • 资源的加载包含两种模式, 模式一即参考Instant Run, 创建AssetManager, 再通过反射调用addAssetPath加载插件的资源, 模式二即合并插件和宿主资源, 也是类似, 创建AssetManager, 再通过反射调用addAssetPath加载把宿主和插件的资源均加载进来

参考

  • dynamic-load-apk
  • DYNAMIC-LOAD-APK插件原理整理
  • VirtualAPK 资源篇
  • [深度 滴滴插件化方案 VirtualApk 源码解析](https://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ==&mid=2650823488&idx=1&sn=2976c8ddc0c206149b14c527260f7766&chksm=80b78fdeb7c006c8a9585db794c51e799049ec50d23c4d738c78d77454f6b0291227a00e2def&mpshare=1&scene=1&srcid=0712oTUswGWi172UK0Azpg4i&key=8652b956ca1971a47b1e263b435230c7469d30646ddbe6ce2fb781033d6eba5215c9fd7e5eaf0bd73dd5da279b32dd901261d5e55bf32997bc333ad8a059e095e2193a5baa805447fc49cd315fca4404&ascene=0&uin=MTI0NjM4NTEyMA%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.11.1+build(15B42)&version=12010110&nettype=WIFI&fontScale=100&pass_ticket=mswE9bS3QeCxTOoepaUWh9VXHxeYMosdkHkAydyR09JHQkVe%2BAJHCCnPQrRpBfQN)