Android Tencent Shadow 插件接入指南

Android Tencent Shadow 插件接入指南

  • 插件化简述
    • 一、clone 仓库
    • 二、编译运行官方demo
    • 三、发布Shadow到我们本地仓库
      • 3.1、安装Nexus 3.x版本
      • 3.2、修改发布配置
      • 3.3、发布仓库
      • 3.4、引用仓库包
      • 四、编写我们自己的代码
        • 4.1、新建项目导入maven等共同配置
          • 4.1.1、导入buildScript
          • 4.1.2、修改gradle版本和插件包版本
          • 4.1.3、添加maven依赖
          • 4.2、实现宿主模块
            • 4.2.1、添加依赖
            • 4.2.2、撸码
            • 4.3、静态参数 constant 的module编写
            • 4.4、plugin-loader模块实现
            • 4.5、plugin-manager的实现
            • 4.6、plugin-runtime实现
            • 4.7、插件项目的实现
            • 4.8、宿主调用插件Activity
            • 4.9、启动项目
            • 4.10、运行看看新效果吧
            • 五、踩坑
              • 5.1、插件项目的清单文件中一定要添加package
              • 5.2、每个插件项目的build.gradle中都要引用插件
              • 5.3、每个插件都要添加依赖
              • 5.4、注意gradle版本官方推荐
              • 5.5、注意修改每个build.gradle中的版本号
              • 5.6、注意修改我文中没有提到的包名
              • 5.7、官方建议
              • 5.8、几个插件就要新建一个service并且进行不能再统一进程
              • 5.9、注意包名不要错和,尽量partKey和module名保持一致,这样可以减少容错率
              • 5.10、注意看文中的每一个注释代码
              • 六、写在最后

                插件化简述

                关于插件化的概念就不提了大家可以自行百度!

                插件化难点痛点在于AndroidManifest.xml清单文件,如果不是清单文件注册其实就不需要什么高深技术了,可以通过反射等等方式实现很简单。因此市场上很多插件化的框架都是通过占位去实现的,但是由于Android 9.0后私有API权限的问题,基本市场上的框架都停止了对其的维护,大家可以搜一搜基本都是只支持到9.0。那么问题来了仅仅只支持到9.0怎么够呢?Android已经13了,马上就14了!所以今天给大家介绍下shadow的接入。他的介绍啊简述啊我就不重复复制了,给大家甩几个网址:

                Shadow作者解析:https://juejin.cn/user/536217405890903/posts

                别人录制的B站视频:https://www.bilibili.com/video/BV1u14y1f7v8/?spm_id_from=333.337.search-card.all.click&vd_source=0f8f0025ace2f1265a86bba19aa4778d

                别人写的博客:

                https://www.jianshu.com/p/f00dc837227f

                本文demo:https://github.com/fzkf9225/shadow-plugin-master

                一、clone 仓库

                首先我们克隆官方仓库代码:

                https://github.com/Tencent/Shadow

                二、编译运行官方demo

                在 Shadow 目录下使用命令, 确保在 Java8 环境:

                ./gradlew build
                

                或者直接点击他们项目根目录下的脚本文件

                gradlew.bat
                

                三、发布Shadow到我们本地仓库

                3.1、安装Nexus 3.x版本

                大家可以下载一个nexus,具体操作可以自行百度搞定它,3.x Nexus很无脑的,记得配置https证书和端口:https://help.sonatype.com/repomanager3/product-information/download

                3.2、修改发布配置

                修改Shadow框架中的maven.gradle和common.gradle配置,在根目录下的buildScript中。其中有github相关配置,可改可不改,不重要,主要是修改如下部分因为源码都有所以直接截图了:

                可以先在 buildScripts/gradle/common.gradle 路径下,修改发布版本或其他信息:

                ext.ARTIFACT_VERSION = ext.VERSION_NAME + ext.VERSION_SUFFIX
                

                直接修改为你想要的版本号:

                ext.ARTIFACT_VERSION = 1.0
                

                3.3、发布仓库

                使用命令或者自行百度如何发布到nexus的maven仓库

                ./gradlew publish
                

                3.4、引用仓库包

                发布成功后即可在Shadow框架目录下找到pom文件如图

                怕有人不知道怎么用我多说一句:

                在需要引用的build.gradle中加入:

                ##对应上图
                implementation "groupId:artifactId:version"
                

                示例:

                implementation "com.tencent.shadow.core:common:1.0"
                

                为了防止回头升级导致的大量修改版本号,我们可以新增一个全局配置shadow_version,注意系统导包的时候是单引号,我们引用参数需要改为双引号

                #错误
                implementation 'com.tencent.shadow.core:common:$shadow_version'
                #正确
                implementation "com.tencent.shadow.core:common:$shadow_version"
                

                四、编写我们自己的代码

                4.1、新建项目导入maven等共同配置

                项目结构如图(仅供参考):

                • app包名:推荐:com.域名.项目名,示例:com.casic.titan.shadow
                • common:没啥好解释的,基础封装module而已
                • constant:静态参数module,其实你可以不用,但是好几个module都需要各自写魔法值,容易错,不如加个module
                • plugin-app包名:推荐:com.域名.项目名,示例:com.casic.titan.shadow,为了演示官方推荐和素质包名一致
                • pliugin-loader:推荐:com.域名.loader,示例:com.casic.titan.loader
                • plugin-manager:推荐:com.域名.manager,示例:com.casic.titan.manager
                • plugin-runtime:推荐:com.域名.runtime,示例:com.casic.titan.runtime
                • plugin-user:推荐:com.域名.随意,示例:com.casic.titan.user,记得再gradle中加上脚本配置(后面会提到)
                  4.1.1、导入buildScript

                  复制shadow项目下的buildScript目录中的common.gradle和version.gradle文件,其实也可以不复制但是添加公共的模块会节省很多事情,复制后我们稍作修改

                  common.gradle如下:

                  def gitShortRev() { def gitCommit = ""
                      def proc = "git rev-parse --short HEAD".execute()
                      proc.in.eachLine { line -> gitCommit = line }
                      proc.err.eachLine { line -> println line }
                      proc.waitFor()
                      return gitCommit
                  }
                  allprojects { ext.COMPILE_SDK_VERSION = 31
                      ext.MIN_SDK_VERSION = 24
                      ext.TARGET_SDK_VERSION = 31
                      ext.VERSION_CODE = 1
                      if ("${System.env.CI}".equalsIgnoreCase("true")) { ext.VERSION_NAME = System.getenv("GITHUB_REF_SLUG")
                      } else { ext.VERSION_NAME = "local"
                      }
                  	//System.env是获取window环境变量,不用管他
                      if ("${System.env.PUBLISH_RELEASE}".equalsIgnoreCase("true")) { ext.VERSION_SUFFIX = ""
                      } else if ("${System.env.CI}".equalsIgnoreCase("true")) { ext.VERSION_SUFFIX = "-${System.env.GITHUB_SHA_SHORT}-SNAPSHOT"
                      } else { ext.VERSION_SUFFIX = "-${gitShortRev()}-SNAPSHOT"
                      }
                      ext.ARTIFACT_VERSION = ext.VERSION_NAME + ext.VERSION_SUFFIX
                      ext.TEST_HOST_APP_APPLICATION_ID = 'com.casic.titan.test.shadow'//你测试的applicationId
                      ext.SAMPLE_HOST_APP_APPLICATION_ID = 'com.casic.titan.shadow'//你项目的applicationId
                  }
                  

                  version.gradle如下

                  constraintlayoutVersion=2.1.3
                  materialVersion=1.5.0
                  appcompatVersion=1.4.1
                  espressoCoreVersion=3.4.0
                  shadow_version=1.0
                  compileSdk=31
                  minSdk=24
                  targetSdk=31
                  javassist_version=3.28.0-GA
                  
                  4.1.2、修改gradle版本和插件包版本

                  现在新版Android Studio新建的项目默认都是8.0版本了但是Shadow不支持我们需要修改一下

                  a)修改项目根目录下的builde.gradle

                  plugins { id 'com.android.application' version '7.0.3' apply false
                  }
                  

                  b) gradle-wrapper.properties文件

                  distributionBase=GRADLE_USER_HOME
                  distributionPath=wrapper/dists
                  distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
                  zipStoreBase=GRADLE_USER_HOME
                  zipStorePath=wrapper/dists
                  
                  4.1.3、添加maven依赖

                  打开setting.gradle文件

                  pluginManagement { repositories { maven {setUrl("https://localhost:9224/repository/casic_group/")}
                          maven { setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
                          google()
                          mavenLocal()
                          mavenCentral()
                          gradlePluginPortal()
                      }
                  }
                  dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
                      repositories { maven {setUrl("https://localhost:9224/repository/casic_group/")}
                          maven { setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
                          google()
                          mavenLocal()
                          mavenCentral()
                      }
                  }
                  

                  尤其注意上述的几个:一定要添加

                  mavenLocal()
                  

                  其中https://localhost:9224/repository/casic_group/是我本机的地址,你们用不了,改为自己的,一定是https端口

                  4.2、实现宿主模块

                  4.2.1、添加依赖

                  在build.gradle下android 目录节点下添加

                   sourceSets { debug { assets.srcDir('build/generated/assets/plugin-manager/debug/')
                              assets.srcDir('build/generated/assets/plugin-zip/debug/')
                          }
                          release { assets.srcDir('build/generated/assets/plugin-manager/release/')
                              assets.srcDir('build/generated/assets/plugin-zip/release/')
                          }
                      }
                      dataBinding{ enabled = true
                      }
                      lintOptions { checkReleaseBuilds false
                          abortOnError false
                      }
                  

                  添加依赖包

                   implementation project(":constant")
                      implementation "com.tencent.shadow.core:common:$shadow_version"//AndroidLogLoggerFactory
                      implementation 'commons-io:commons-io:2.9.0'//sample-host从assets中复制插件用的
                      implementation "com.tencent.shadow.dynamic:host:$shadow_version"//腾讯插件框架shadow
                      implementation "com.tencent.shadow.dynamic:host-multi-loader-ext:$shadow_version"//腾讯插件框架shadow
                  

                  记得将compileSdk、minSdk等等修改一些统一版本号,此处可忽略

                   compileSdk project.COMPILE_SDK_VERSION
                      defaultConfig { applicationId "你的applicationId"
                          minSdk project.MIN_SDK_VERSION
                          targetSdk project.TARGET_SDK_VERSION
                          versionCode 1
                          versionName "1.0"
                          multiDexEnabled = true
                          testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
                      }
                  

                  依赖库的版本也记得修改一下,注意单双引号的问题,此处也可忽略根据需求

                   implementation "androidx.appcompat:appcompat:$appcompatVersion"
                      implementation "com.google.android.material:material:$materialVersion"
                      testImplementation 'junit:junit:4.13.2'
                      androidTestImplementation 'androidx.test.ext:junit:1.1.5'
                      androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion"
                  
                  4.2.2、撸码

                  新建Application,记得配置到清单文件中去

                  public class MyApplication extends Application { private static MyApplication sApp;
                      private PluginManager mPluginManager;
                      @Override
                      public void onCreate() { super.onCreate();
                          sApp = this;
                          detectNonSdkApiUsageOnAndroidP();
                          setWebViewDataDirectorySuffix();
                          LoggerFactory.setILoggerFactory(new AndroidLoggerFactory());
                          if (isProcess(this, ":plugin")) { //在全动态架构中,Activity组件没有打包在宿主而是位于被动态加载的runtime,
                              //为了防止插件crash后,系统自动恢复crash前的Activity组件,此时由于没有加载runtime而发生classNotFound异常,导致二次crash
                              //因此这里恢复加载上一次的runtime
                              DynamicRuntime.recoveryRuntime(this);
                          }
                          if (isProcess(this, getPackageName())) { PluginHelper.getInstance().init(this);
                          }
                  //        HostUiLayerProvider.init(this);
                      }
                      private static void setWebViewDataDirectorySuffix() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { return;
                          }
                          WebView.setDataDirectorySuffix(Application.getProcessName());
                      }
                      private static void detectNonSdkApiUsageOnAndroidP() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { return;
                          }
                          StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
                          builder.detectNonSdkApiUsage();
                          StrictMode.setVmPolicy(builder.build());
                      }
                      public static MyApplication getApp() { return sApp;
                      }
                      public void loadPluginManager(File apk) { if (mPluginManager == null) { mPluginManager = Shadow.getPluginManager(apk);
                          }
                      }
                      public PluginManager getPluginManager() { return mPluginManager;
                      }
                      private static boolean isProcess(Context context, String processName) { String currentProcName = "";
                          ActivityManager manager =
                                  (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
                          for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) { if (processInfo.pid == myPid()) { currentProcName = processInfo.processName;
                                  break;
                              }
                          }
                          return currentProcName.endsWith(processName);
                      }
                  }
                  

                  新建插件帮助类PluginHelper

                  public class PluginHelper { private static final Logger mLogger = LoggerFactory.getLogger(PluginHelper.class);
                      public final static String sPluginManagerName = "plugin-manager.apk";//动态加载的插件管理apk
                      /**
                       * 动态加载的插件包,里面包含以下几个部分,插件apk,插件框架apk(loader apk和runtime apk), apk信息配置关系json文件
                       */
                      public final static String sPluginZip = DEBUG ? "plugin-debug.zip" : "plugin-release.zip";
                      public File pluginManagerFile;
                      public File pluginZipFile;
                      public ExecutorService singlePool = Executors.newSingleThreadExecutor();
                      private Context mContext;
                      private static PluginHelper sInstance = new PluginHelper();
                      public static PluginHelper getInstance() { return sInstance;
                      }
                      private PluginHelper() { }
                      public void init(Context context) { pluginManagerFile = new File(context.getFilesDir(), sPluginManagerName);
                          pluginZipFile = new File(context.getFilesDir(), sPluginZip);
                          mLogger.debug("pluginManagerFile:"+pluginManagerFile);
                          mLogger.debug("pluginZipFile:"+pluginZipFile);
                          mContext = context.getApplicationContext();
                          singlePool.execute(() -> preparePlugin());
                      }
                      private void preparePlugin() { try { InputStream is = mContext.getAssets().open(sPluginManagerName);
                              FileUtils.copyInputStreamToFile(is, pluginManagerFile);
                              InputStream zip = mContext.getAssets().open(sPluginZip);
                              FileUtils.copyInputStreamToFile(zip, pluginZipFile);
                          } catch (IOException e) { throw new RuntimeException("从assets中复制apk出错", e);
                          }
                      }
                  }
                  

                  新建日志类AndroidLoggerFactory这玩意没啥修改的,直接copy源码里的吧,太长了

                  新建shadow类

                  public class Shadow { public static PluginManager getPluginManager(File apk){ final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
                          File tempPm = fixedPathPmUpdater.getLatest();
                          if (tempPm != null) { return new DynamicPluginManager(fixedPathPmUpdater);
                          }
                          return null;
                      }
                  }
                  

                  新建FixedPathPmUpdater

                  import com.tencent.shadow.dynamic.host.PluginManagerUpdater;
                  import java.io.File;
                  import java.util.concurrent.Future;
                  public class FixedPathPmUpdater implements PluginManagerUpdater { final private File apk;
                      public FixedPathPmUpdater(File apk) { this.apk = apk;
                      }
                      /**
                       * @return true表示之前更新过程中意外中断了
                       */
                      @Override
                      public boolean wasUpdating() { return false;
                      }
                      /**
                       * 更新
                       *
                       * @return 当前最新的PluginManager,可能是之前已经返回过的文件,但它是最新的了。
                       */
                      @Override
                      public Future update() { return null;
                      }
                      /**
                       * 获取本地最新可用的
                       *
                       * @return null表示本地没有可用的
                       */
                      @Override
                      public File getLatest() { return apk;
                      }
                      /**
                       * 查询是否可用
                       *
                       * @param file PluginManagerUpdater返回的file
                       * @return true表示可用,false表示不可用
                       */
                      @Override
                      public Future isAvailable(final File file) { return null;
                      }
                  }
                  

                  新建service服务,一个service代表一个插件,你几个插件就新建几个,因为我写了两个插件因此我写了两个service

                  import com.tencent.shadow.dynamic.host.PluginProcessService;
                  /**
                   * 一个PluginProcessService(简称PPS)代表一个插件进程。插件进程由PPS启动触发启动。
                   * 新建PPS子类允许一个宿主中有多个互不影响的插件进程。
                   */
                  public class MainPluginProcessService extends PluginProcessService {}
                  
                  import com.tencent.shadow.dynamic.host.PluginProcessService;
                  /**
                   * 一个PluginProcessService(简称PPS)代表一个插件进程。插件进程由PPS启动触发启动。
                   * 新建PPS子类允许一个宿主中有多个互不影响的插件进程。
                   */
                  public class UserPluginProcessService extends PluginProcessService {}
                  

                  接下来就是配置清单文件了

                                   

                  主题PluginContainerActivity

                   

                  4.3、静态参数 constant 的module编写

                  直接新建一个类

                  final public class Constant { public static final String KEY_PLUGIN_ZIP_PATH = "pluginZipPath";
                      public static final String KEY_ACTIVITY_CLASSNAME = "KEY_ACTIVITY_CLASSNAME";
                      public static final String KEY_EXTRAS = "KEY_EXTRAS";
                      public static final String KEY_PLUGIN_NAME = "key_plugin_name";
                      public static final String KEY_PLUGIN_PART_KEY = "KEY_PLUGIN_PART_KEY";
                      public static final String PLUGIN_APP_NAME = "plugin-app";//建议者两个参数一致也和module名保持一致,建议但非必须
                      public static final String PART_KEY_PLUGIN_BASE = "plugin-app";  //part-key  和 plugin-app  build.gradle中一致
                      public static final String PLUGIN_USER_NAME = "plugin-user";//建议者两个参数一致也和module名保持一致,建议但非必须
                      public static final String PART_KEY_PLUGIN_USER = "plugin-user";  //part-key  和 plugin-app  build.gradle中一致
                      public static final int FROM_ID_NOOP = 1000;
                      public static final long FROM_ID_START_ACTIVITY = 1002;//标识启动的是Activity
                      public static final int FROM_ID_CALL_SERVICE = 1001;//标识启动的是Service
                      public static final int FROM_ID_CLOSE = 1003;
                      public static final int FROM_ID_LOAD_VIEW_TO_HOST = 1004;
                  }
                  

                  build.grale配置

                  plugins {//修改为library
                      id 'com.android.library'
                  }
                  

                  4.4、plugin-loader模块实现

                  在build.gradle中导入依赖库

                  //自带的可以删除只保留这几个,不是一定要删,随你,但是你删除库会导致资源文件引用不到res下也要删,还有清单文件
                      implementation "com.tencent.shadow.dynamic:loader-impl:$shadow_version"
                      compileOnly "com.tencent.shadow.core:activity-container:$shadow_version"
                      compileOnly "com.tencent.shadow.core:common:$shadow_version"
                      compileOnly "com.tencent.shadow.dynamic:host:$shadow_version"//下面这行依赖是为了防止在proguard的时候找不到LoaderFactory接口
                  

                  新建SampleComponentManager,记得修改三个activity的路径包名

                  import android.content.ComponentName;
                  import android.content.Context;
                  import com.tencent.shadow.core.loader.infos.ContainerProviderInfo;
                  import com.tencent.shadow.core.loader.managers.ComponentManager;
                  public class SampleComponentManager extends ComponentManager { /**
                       * runtime 模块中定义的壳子Activity, 路径类名保持一致,需要在宿主AndroidManifest.xml注册
                       */
                      private static final String DEFAULT_ACTIVITY = "com.casic.titan.plugin_runtime.PluginDefaultProxyActivity";
                      private static final String SINGLE_INSTANCE_ACTIVITY = "com.casic.titan.plugin_runtime.PluginSingleInstance1ProxyActivity";
                      private static final String SINGLE_TASK_ACTIVITY = "com.casic.titan.plugin_runtime.PluginSingleTask1ProxyActivity";
                      private Context context;
                      public SampleComponentManager(Context context) { this.context = context;
                      }
                      /**
                       * 配置插件Activity 到 壳子Activity的对应关系
                       *
                       * @param pluginActivity 插件Activity
                       * @return 壳子Activity
                       */
                      @Override
                      public ComponentName onBindContainerActivity(ComponentName pluginActivity) { switch (pluginActivity.getClassName()) { /**
                               * 这里配置对应的对应关系, 启动不同启动模式的Acitvity
                               */
                          }
                          return new ComponentName(context, DEFAULT_ACTIVITY);
                      }
                      /**
                       * 配置对应宿主中预注册的壳子contentProvider的信息
                       */
                      @Override
                      public ContainerProviderInfo onBindContainerContentProvider(ComponentName pluginContentProvider) { return new ContainerProviderInfo(
                                  "com.tencent.shadow.core.runtime.container.PluginContainerContentProvider",
                                  context.getPackageName() + ".contentprovider.authority.dynamic");
                      }
                  

                  新建SamplePluginLoader

                  public class SamplePluginLoader extends ShadowPluginLoader { private final static String TAG = "shadow";
                      private ComponentManager componentManager;
                      public SamplePluginLoader(Context hostAppContext) { super(hostAppContext);
                          componentManager = new SampleComponentManager(hostAppContext);
                      }
                      @Override
                      public ComponentManager getComponentManager() { return componentManager;
                      }
                  }
                  

                  新建目录:这个目录一定要是这个,并且不能错,不能改

                  com.tencent.shadow.dynamic.loader.impl
                  

                  新建类CoreLoaderFactoryImpl,类名不可改

                  import android.content.Context;
                  import com.casic.titan.plugin_loader.SamplePluginLoader;
                  import com.tencent.shadow.core.loader.ShadowPluginLoader;
                  import org.jetbrains.annotations.NotNull;
                  /**
                   * 这个类的包名类名是固定的。
                   * 

                  * 见com.tencent.shadow.dynamic.loader.impl.DynamicPluginLoader#CORE_LOADER_FACTORY_IMPL_NAME */ public class CoreLoaderFactoryImpl implements CoreLoaderFactory { @NotNull @Override public ShadowPluginLoader build(@NotNull Context context) { return new SamplePluginLoader(context); } }

                  官方demo中有WhiteList,你可以不用,需要用的时候再说

                  4.5、plugin-manager的实现

                  修改build.gradle依赖

                   implementation project(path: ":constant")
                      implementation "com.tencent.shadow.dynamic:manager:$shadow_version"
                      compileOnly "com.tencent.shadow.core:common:$shadow_version"
                      compileOnly "com.tencent.shadow.dynamic:host:$shadow_version"
                  

                  新建FastPluginManager

                  import android.content.Context;
                  import android.os.RemoteException;
                  import android.util.Pair;
                  import com.tencent.shadow.core.common.Logger;
                  import com.tencent.shadow.core.common.LoggerFactory;
                  import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
                  import com.tencent.shadow.core.manager.installplugin.InstalledType;
                  import com.tencent.shadow.core.manager.installplugin.PluginConfig;
                  import com.tencent.shadow.dynamic.host.FailedException;
                  import com.tencent.shadow.dynamic.manager.PluginManagerThatUseDynamicLoader;
                  import org.json.JSONException;
                  import java.io.File;
                  import java.io.IOException;
                  import java.util.HashMap;
                  import java.util.LinkedList;
                  import java.util.List;
                  import java.util.Map;
                  import java.util.concurrent.Callable;
                  import java.util.concurrent.ExecutionException;
                  import java.util.concurrent.ExecutorService;
                  import java.util.concurrent.Executors;
                  import java.util.concurrent.Future;
                  import java.util.concurrent.TimeUnit;
                  import java.util.concurrent.TimeoutException;
                  public abstract class FastPluginManager extends PluginManagerThatUseDynamicLoader { private static final Logger mLogger = LoggerFactory.getLogger(FastPluginManager.class);
                      private ExecutorService mFixedPool = Executors.newFixedThreadPool(4);
                      public FastPluginManager(Context context) { super(context);
                      }
                      public InstalledPlugin installPlugin(String zip, String hash, boolean odex) throws IOException, JSONException, InterruptedException, ExecutionException { final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
                          final String uuid = pluginConfig.UUID;
                          List futures = new LinkedList<>();
                          List>> extractSoFutures = new LinkedList<>();
                          if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) { Future odexRuntime = mFixedPool.submit(new Callable() { @Override
                                  public Object call() throws Exception { oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME,
                                              pluginConfig.runTime.file);
                                      return null;
                                  }
                              });
                              futures.add(odexRuntime);
                              Future odexLoader = mFixedPool.submit(new Callable() { @Override
                                  public Object call() throws Exception { oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER,
                                              pluginConfig.pluginLoader.file);
                                      return null;
                                  }
                              });
                              futures.add(odexLoader);
                          }
                          for (Map.Entry plugin : pluginConfig.plugins.entrySet()) { final String partKey = plugin.getKey();
                              final File apkFile = plugin.getValue().file;
                              Future> extractSo = mFixedPool.submit(() -> extractSo(uuid, partKey, apkFile));
                              futures.add(extractSo);
                              extractSoFutures.add(extractSo);
                              if (odex) { Future odexPlugin = mFixedPool.submit(new Callable() { @Override
                                      public Object call() throws Exception { oDexPlugin(uuid, partKey, apkFile);
                                          return null;
                                      }
                                  });
                                  futures.add(odexPlugin);
                              }
                          }
                          for (Future future : futures) { future.get();
                          }
                          Map soDirMap = new HashMap<>();
                          for (Future> future : extractSoFutures) { Pair pair = future.get();
                              soDirMap.put(pair.first, pair.second);
                          }
                          onInstallCompleted(pluginConfig, soDirMap);
                          return getInstalledPlugins(1).get(0);
                      }
                      protected void callApplicationOnCreate(String partKey) throws RemoteException { Map map = mPluginLoader.getLoadedPlugin();
                          Boolean isCall = (Boolean) map.get(partKey);
                          if (isCall == null || !isCall) { mPluginLoader.callApplicationOnCreate(partKey);
                          }
                      }
                      private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException { if (mPpsController == null) { bindPluginProcessService(getPluginProcessServiceName(partKey));
                              waitServiceConnected(10, TimeUnit.SECONDS);
                          }
                          loadRunTime(uuid);
                          loadPluginLoader(uuid);
                      }
                      protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException { loadPluginLoaderAndRuntime(uuid, partKey);
                          Map map = mPluginLoader.getLoadedPlugin();
                          if (!map.containsKey(partKey)) { mPluginLoader.loadPlugin(partKey);
                          }
                      }
                      protected abstract String getPluginProcessServiceName(String partKey);
                  }
                  

                  新建类SamplePluginManager

                  public class SamplePluginManager extends FastPluginManager { private static final Logger logger = LoggerFactory.getLogger(SamplePluginManager.class);
                      private ExecutorService executorService = Executors.newSingleThreadExecutor();
                      private Context mCurrentContext;
                      public SamplePluginManager(Context context) { super(context);
                          mCurrentContext = context;
                      }
                      /**
                       * @return PluginManager实现的别名,用于区分不同PluginManager实现的数据存储路径
                       */
                      @Override
                      protected String getName() { return "test-dynamic-manager";
                      }
                      /**
                       * @return 宿主中注册的PluginProcessService实现的类名
                       */
                      @Override
                      protected String getPluginProcessServiceName(String partKey) { if (PART_KEY_PLUGIN_USER.equals(partKey)) { return "com.casic.titan.shadow.plugin_manager.UserPluginProcessService";
                          } else if (PART_KEY_PLUGIN_BASE.equals(partKey)) { return "com.casic.titan.shadow.plugin_manager.MainPluginProcessService";
                          }
                          throw new RuntimeException("partKey is unknown");
                      }
                      @Override
                      public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) { if (fromId == Constant.FROM_ID_NOOP) { //do nothing.
                          } else if (fromId == Constant.FROM_ID_START_ACTIVITY) { onStartActivity(context, bundle, callback);
                          } else if (fromId == Constant.FROM_ID_CLOSE) { close();
                          } else if (fromId == Constant.FROM_ID_LOAD_VIEW_TO_HOST) { loadViewToHost(context, bundle);
                          } else { throw new IllegalArgumentException("不认识的fromId==" + fromId);
                          }
                      }
                      private void loadViewToHost(final Context context, Bundle bundle) { Intent pluginIntent = new Intent();
                          pluginIntent.setClassName(
                                  context.getPackageName(),
                                  "com.tencent.shadow.sample.plugin.app.lib.usecases.service.HostAddPluginViewService"
                          );
                          pluginIntent.putExtras(bundle);
                          try { mPluginLoader.startPluginService(pluginIntent);
                          } catch (RemoteException e) { throw new RuntimeException(e);
                          }
                      }
                      private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) { final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
                          final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
                          final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
                          logger.debug("pluginZipPath:"+pluginZipPath);
                          logger.debug("className:"+className);
                          if (className == null) { throw new NullPointerException("className == null");
                          }
                          final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
                          if (callback != null) { final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
                              callback.onShowLoadingView(view);
                          }
                          executorService.execute(() -> { try { InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
                                  loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                                  loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_USER);
                                  callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
                                  callApplicationOnCreate(PART_KEY_PLUGIN_USER);
                                  Intent pluginIntent = new Intent();
                                  pluginIntent.setClassName(
                                          context.getPackageName(),
                                          className
                                  );
                                  if (extras != null) { pluginIntent.replaceExtras(extras);
                                  }
                                  Intent intent = mPluginLoader.convertActivityIntent(pluginIntent);
                                  intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                  mPluginLoader.startActivityInPluginProcess(intent);
                              } catch (Exception e) { throw new RuntimeException(e);
                              }
                              if (callback != null) { callback.onCloseLoadingView();
                              }
                          });
                      }
                  }
                  

                  修改上述SamplePluginManager 类代码:

                   /**
                       * @return 宿主中注册的PluginProcessService实现的类名
                       */
                      @Override
                      protected String getPluginProcessServiceName(String partKey) { if (PART_KEY_PLUGIN_USER.equals(partKey)) {//与你partKey和Service对应起来,修改为自己的报名和静态参数
                              return "com.casic.titan.shadow.plugin_manager.UserPluginProcessService";
                          } else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {//与你partKey和Service对应起来,修改为自己的报名和静态参数
                              return "com.casic.titan.shadow.plugin_manager.MainPluginProcessService";
                          }
                          throw new RuntimeException("partKey is unknown");
                      }
                  

                  修改SamplePluginManager 类方法onStartActivity

                  //两个插件这样写
                                  loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                                  loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_USER);
                                  callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
                                  callApplicationOnCreate(PART_KEY_PLUGIN_USER);
                    //一个插件就删掉一个,这样写
                                    loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                                  callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
                  

                  新建包名:不可修改不能错

                  com.tencent.shadow.dynamic.impl
                  

                  路径下新建:ManagerFactoryImpl,类型不可修改

                  import android.content.Context;
                  import com.casic.titan.plugin_manager.SamplePluginManager;
                  import com.tencent.shadow.dynamic.host.ManagerFactory;
                  import com.tencent.shadow.dynamic.host.PluginManagerImpl;
                  /**
                   * 此类包名及类名固定
                   */
                  public final class ManagerFactoryImpl implements ManagerFactory { @Override
                      public PluginManagerImpl buildManager(Context context) { return new SamplePluginManager(context);
                      }
                  }
                  

                  4.6、plugin-runtime实现

                  添加依赖包

                   implementation "com.tencent.shadow.core:activity-container:$shadow_version"
                  	//其他的包可以删除,当然你删除后也一定要删除同步的res和清单文件不然会报错,也可以不删
                  

                  新建三个Activity,不用再清单文件注册,这就是宿主清单文件中注册的Activity,其实这里是用来加载你插件中的Activity,因为shadow几乎重写了Activity代码

                  import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
                  public class PluginDefaultProxyActivity extends PluginContainerActivity {}
                  
                  import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
                  public class PluginSingleInstance1ProxyActivity extends PluginContainerActivity {}
                  
                  import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
                  public class PluginSingleTask1ProxyActivity extends PluginContainerActivity {}
                  

                  在清单文件上添加上package,因为Android Studio新建项目和module时不会添加这个package,所以你需要手动添加

                  package="com.casic.titan.plugin_runtime"
                  

                  完整的清单文件

                  4.7、插件项目的实现

                  这里我就写一个了,另外一个没区别只是代码不一样而已

                  添加依赖:

                   compileOnly "com.tencent.shadow.core:runtime:$shadow_version"
                  

                  在android节点下添加

                   buildTypes { debug { minifyEnabled false
                              proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                          }
                          release { minifyEnabled false
                              proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                              signingConfig signingConfigs.create("release")
                              signingConfig.initWith(buildTypes.debug.signingConfig)
                          }
                      }
                      compileOptions { sourceCompatibility JavaVersion.VERSION_1_8
                          targetCompatibility JavaVersion.VERSION_1_8
                      }
                      lintOptions { abortOnError false
                      }
                      // 将插件applicationId设置为和宿主相同
                      productFlavors { plugin { applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
                          }
                      }
                      // 将插件的资源ID分区改为和宿主0x7F不同的值
                      aaptOptions { additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
                      }
                  

                  在build.gradle顶部添加,一定要写在最顶部

                  buildscript {//这个模块要放在plugins {} 之前
                      repositories { maven { setUrl("https://localhost:9224/repository/casic_group/") }
                          maven { setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
                          google()
                          mavenLocal()
                          mavenCentral()
                      }
                      dependencies { classpath "com.tencent.shadow.core:runtime:$shadow_version"
                          classpath "com.tencent.shadow.core:activity-container:$shadow_version"
                          classpath "com.tencent.shadow.core:gradle-plugin:$shadow_version"
                          classpath "org.javassist:javassist:$javassist_version"
                      }
                  }
                  apply plugin: 'com.android.application'
                  apply plugin: 'com.tencent.shadow.plugin'
                  

                  在build.gradle底部,最底部添加代码,只用一个插件添加就可以了,不用每个插件项目都添加,因为这里代码仅用于测试。上面的依赖配置是每个插件项目都要加,只有这里不用,因为他是只是一个打包的脚本而已,我看了下源码他有kotlin编写的gradle脚本,你们引用一下就行,跟开发接入都无关的

                  shadow { transform {//   useHostContext = ['abc']
                      }
                      packagePlugin { pluginTypes { debug { loaderApkConfig = new Tuple2('plugin-loader-debug.apk', ':plugin-loader:assembleDebug')//其中plugin-loader是moduel的名字plugin-loader
                                  runtimeApkConfig = new Tuple2('plugin-runtime-debug.apk', ':plugin-runtime:assembleDebug')
                                  pluginApks { pluginApp { businessName = 'plugin-app'//与下面保持一致最好,也可以为空,具体解释可以看官方的
                                          partKey = 'plugin-app'//静态参数constant 的module中的partKey,我建议和项目名保持一致
                                          buildTask = ':plugin-app:assemblePluginDebug'//这里的plugin-app是module名字,因为需要调用脚本assemblePluginDebug,这句话是意思是加载plugin-app下的assemblePluginDebug任务
                                          apkPath = 'plugin-app/build/outputs/apk/plugin/debug/plugin-app-plugin-debug.apk'//编译后apk的输出位置
                                      }
                                      pluginUser {//第二个插件
                                          businessName = 'plugin-user'//与下面保持一致最好,也可以为空,具体解释可以看官方的
                                          partKey = 'plugin-user'//静态参数constant 的module中的partKey
                                          buildTask = ':plugin-user:assemblePluginDebug'//这里的plugin-app是module名字,因为需要调用脚本assemblePluginDebug,这句话是意思是加载plugin-app下的assemblePluginDebug任务
                                          apkPath = 'plugin-user/build/outputs/apk/plugin/debug/plugin-user-plugin-debug.apk'//编译后apk的输出位置
                                      }
                                  }
                              }
                              release {//这个就不解释了,是版本的区别
                                  loaderApkConfig = new Tuple2('plugin-loader-release.apk', ':plugin-loader:assembleRelease')
                                  runtimeApkConfig = new Tuple2('plugin-runtime-release.apk', ':plugin-runtime:assembleRelease')
                                  pluginApks { pluginApp { businessName = 'plugin-app'
                                          partKey = 'plugin-app'
                                          buildTask = ':plugin-app:assemblePluginRelease'
                                          apkPath = 'plugin-app/build/outputs/apk/release/plugin-app-release.apk'
                                      }
                                      pluginUser { businessName = 'plugin-user'
                                          partKey = 'plugin-user'
                                          buildTask = ':plugin-user:assemblePluginRelease'
                                          apkPath = 'plugin-user/build/outputs/apk/plugin/debug/plugin-user-plugin-debug.apk'
                                      }
                                  }
                              }
                          }
                          loaderApkProjectPath = 'plugin-loader'
                          runtimeApkProjectPath = 'plugin-runtime'
                          archiveSuffix = System.getenv("PluginSuffix") ?: ""
                          archivePrefix = 'plugin'
                          destinationDir = "${getRootProject().getBuildDir()}"
                          version = 1//暂时不知道干嘛的,等我阅读下源码再说
                          compactVersion = [1, 2, 3]
                          uuidNickName = "1.0"
                      }
                  }
                  

                  记得在清单文件中添加package,一定要添加

                  package="com.casic.titan.shadow"
                  

                  其他的没啥说的了,编写一个Activity用于演示:

                  public class MainActivity extends AppCompatActivity { @Override
                      protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
                          setContentView(R.layout.activity_main);
                      }
                  }
                  
                   

                  插件就编写好了,当然写项目时不可能就这么点业务逻辑,根据自己的实现吧

                  4.8、宿主调用插件Activity

                  在宿主项目中新建一个布局文件,我开启了databinding,所以布局不一样,不用我解释把

                      

                  新建一个MainActivity并设置为启动页,点击事件代码:

                  public void start_plugin_app(View view) { PluginHelper.getInstance().singlePool.execute(()-> { MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);
                              /**
                               * @param context context
                               * @param formId  标识本次请求的来源位置,用于区分入口
                               * @param bundle  参数列表, 建议在参数列表加入自己的验证
                               * @param callback 用于从PluginManager实现中返回View
                               */
                              Bundle bundle = new Bundle();//插件 zip,这几个参数也都可以不传,直接在 PluginManager 中硬编码
                              bundle.putString(
                                      Constant.KEY_PLUGIN_ZIP_PATH,
                                      PluginHelper.getInstance().pluginZipFile.getAbsolutePath()
                              );
                              bundle.putString(
                                      Constant.KEY_PLUGIN_NAME,
                                      Constant.PLUGIN_APP_NAME
                              ); // partKey 每个插件都有自己的 partKey 用来区分多个插件,如何配置在下面讲到
                              bundle.putString(
                                      Constant.KEY_ACTIVITY_CLASSNAME,
                                      "com.casic.titan.shadow.MainActivity"
                              ); //要启动的插件的Activity页面
                              bundle.putBundle(Constant.KEY_EXTRAS, new Bundle()) ;// 要传入到插件里的参数
                              MyApplication.getApp().getPluginManager().enter(
                                      this,
                                      Constant.FROM_ID_START_ACTIVITY,
                                      bundle,
                                      new EnterCallback() { @Override
                                          public void onShowLoadingView(View view) { }
                                          @Override
                                          public void onCloseLoadingView() { }
                                          @Override
                                          public void onEnterComplete() { }
                                      });
                          });
                      }
                  

                  启动plugin-user插件点击事件代码

                  public void start_plugin_user(View view) { PluginHelper.getInstance().singlePool.execute(()-> { MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);
                              /**
                               * @param context context
                               * @param formId  标识本次请求的来源位置,用于区分入口
                               * @param bundle  参数列表, 建议在参数列表加入自己的验证
                               * @param callback 用于从PluginManager实现中返回View
                               */
                              Bundle bundle = new Bundle();//插件 zip,这几个参数也都可以不传,直接在 PluginManager 中硬编码
                              bundle.putString(
                                      Constant.KEY_PLUGIN_ZIP_PATH,
                                      PluginHelper.getInstance().pluginZipFile.getAbsolutePath()
                              );
                              bundle.putString(
                                      Constant.KEY_PLUGIN_NAME,
                                      Constant.PLUGIN_USER_NAME
                              ); // partKey 每个插件都有自己的 partKey 用来区分多个插件,如何配置在下面讲到
                              bundle.putString(
                                      Constant.KEY_ACTIVITY_CLASSNAME,
                                      "com.casic.titan.user.LoginActivity"//插件中的Activity全路径
                              ); //要启动的插件的Activity页面
                              bundle.putBundle(Constant.KEY_EXTRAS, new Bundle()) ;// 要传入到插件里的参数
                              MyApplication.getApp().getPluginManager().enter(
                                      this,
                                      Constant.FROM_ID_START_ACTIVITY,
                                      bundle,
                                      new EnterCallback() { @Override
                                          public void onShowLoadingView(View view) { }
                                          @Override
                                          public void onCloseLoadingView() { }
                                          @Override
                                          public void onEnterComplete() { }
                                      });
                          });
                      }
                  

                  4.9、启动项目

                  在启动项目前需要对插件进行打包,在宿主module下的build.gradle最底部添加代码

                  def createCopyTask(projectName, buildType, name, apkName, inputFile, taskName) { def outputFile = file("${getBuildDir()}/generated/assets/${name}/${buildType}/${apkName}")
                      outputFile.getParentFile().mkdirs()
                      return tasks.create("copy${buildType.capitalize()}${name.capitalize()}Task", Copy) { group = 'build'
                          description = "复制${name}到assets中."
                          from(inputFile.getParent()) { include(inputFile.name)
                              rename { outputFile.name }
                          }
                          into(outputFile.getParent())
                      }.dependsOn("${projectName}:${taskName}")
                  }
                  def generateAssets(generateAssetsTask, buildType) { def moduleName = 'plugin-manager'
                      def pluginManagerApkFile = file(
                              "${project(":plugin-manager").getBuildDir()}" +
                                      "/outputs/apk/${buildType}/" +
                                      "${moduleName}-${buildType}.apk"
                      )
                      generateAssetsTask.dependsOn createCopyTask(
                              ':plugin-manager',//项目名
                              buildType,
                              moduleName,
                              'plugin-manager.apk',//plugin-manager   module打包后的apk名字,我建议是module名保持一致,此出名字要和宿主类PluginHelper中的sPluginManagerName参数保持一致
                              pluginManagerApkFile,
                              "assemble${buildType.capitalize()}"
                      )
                      def pluginZip = file("${getRootProject().getBuildDir()}/plugin-${buildType}.zip")
                      generateAssetsTask.dependsOn createCopyTask(
                              ':plugin-app',//这里至于为什么写plugin-app具体暂时还没摸明白,反正这么写可以等我再研究下源码
                              buildType,
                              'plugin-zip',
                              "plugin-${buildType}.zip",
                              pluginZip,
                              "package${buildType.capitalize()}Plugin"
                      )
                  }
                  tasks.whenTaskAdded { task -> if (task.name == "generateDebugAssets") { generateAssets(task, 'debug')
                      }
                      if (task.name == "generateReleaseAssets") { generateAssets(task, 'release')
                      }
                  }
                  

                  4.10、运行看看新效果吧

                  五、踩坑

                  5.1、插件项目的清单文件中一定要添加package

                  5.2、每个插件项目的build.gradle中都要引用插件

                  apply plugin: 'com.tencent.shadow.plugin'
                  

                  并且添加classpath,在最上面,当然你如果是单独app打包的话,这些配置应该在项目下的build.gradle中,这得看插件项目时单独开发还是在插件宿主项目下开发了。

                  buildscript {//这个模块要放在plugins {} 
                      repositories { //换成你自己的module
                          maven { setUrl("https://localhost:9224/repository/casic_group/") }
                          maven { setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
                          google()
                          mavenLocal()
                          mavenCentral()
                      }
                      dependencies { classpath "com.tencent.shadow.core:runtime:$shadow_version"
                          classpath "com.tencent.shadow.core:activity-container:$shadow_version"
                          classpath "com.tencent.shadow.core:gradle-plugin:$shadow_version"
                          classpath "org.javassist:javassist:$javassist_version"
                      }
                  }
                  

                  5.3、每个插件都要添加依赖

                  compileOnly "com.tencent.shadow.core:runtime:$shadow_version"
                  

                  5.4、注意gradle版本官方推荐

                  plugins { id 'com.android.application' version '7.0.3' apply false
                  }
                  
                  #Thu Aug 03 17:23:27 CST 2023
                  distributionBase=GRADLE_USER_HOME
                  distributionPath=wrapper/dists
                  distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
                  zipStoreBase=GRADLE_USER_HOME
                  zipStorePath=wrapper/dists
                  

                  5.5、注意修改每个build.gradle中的版本号

                  例如依赖库的版本,统一一下,防止某一个版本过高导致报错,你半天都找不到问题

                   implementation "androidx.appcompat:appcompat:$appcompatVersion"
                      implementation "com.google.android.material:material:$materialVersion"
                      testImplementation 'junit:junit:4.13.2'
                      androidTestImplementation 'androidx.test.ext:junit:1.1.5'
                      androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion"
                  
                  minSdk project.MIN_SDK_VERSION
                          targetSdk project.TARGET_SDK_VERSION
                  

                  5.6、注意修改我文中没有提到的包名

                  com.casic.titan.shadow  //这是我宿主的包名
                  

                  5.7、官方建议

                  官方建议插件包名和宿主包名一致,但是大家都知道这不实际,所以我写了两个插件一个是和宿主一样一个是不一样,官方提供方法解决问题,在插件build.gradle中android节点下添加配置

                   // 将插件applicationId设置为和宿主相同
                      productFlavors { plugin { applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
                          }
                      }
                  

                  5.8、几个插件就要新建一个service并且进行不能再统一进程

                    

                  5.9、注意包名不要错和,尽量partKey和module名保持一致,这样可以减少容错率

                  5.10、注意看文中的每一个注释代码

                  六、写在最后

                  有问题欢迎博客留言、也欢迎在github上提issue,我会尽快回复