插件化总结

2020-01-03  本文已影响0人  卡卡的箱子

插件化项目总结

前言

先简单介绍一下Android插件化。很早之前已经有公司在研究这项技术,淘宝做得比较早,但淘宝的这项技术一直是保密的。直到2015年才陆续出现很多框架,Android插件化分成很多技术流派,实现的方式都不太一样。
Android大型项目中为了减小apk的体积,可以采用插件化的方法,即一些不常用的功能独立成插件,当用户需要的使用的时候再从服务器上下载回来,动态加载。这样就避免了为了满足所有用户需求而把功能全部打包到apk,导致apk体积的膨胀。所谓的插件,其实也是一个apk,但是一般都依赖正式对外发布的app,也叫宿主。

框架对比

Android插件化框架有很多,我们选取了市场上一些稳定的项目进行调研,调研项目如下:

DyLA : Dynamic-load-apk @singwhatiwanna, 百度 
DyAPK : DynamicAPK @TediWang, 携程
DPG : DroidPlugin @cmzy, 360早期项目
RPG : RePlugin @360近期项目
APG : ApkPlug @北京点豆公司项目

功能

1、DyLA
[项目地址]:(https://github.com/singwhatiwanna/dynamic-load-apk)

项目目前状态:停止维护
是否加载独立插件:是
四大组建的支持:支持部分
        1. 目前不支持service 
        2. 目前只支持动态注册广播 
        3. 插件apk必须实现DLBasePluginActivity,属于侵入式的 
通信方式:
      官方是否提供交互方案:否
      交互难度:高
      如何实现插件间通信:host与plugin共同引用的interface,然后通过interface来达到调用的效果
      交互流程:
               1 在 host中新建module plugininterface , 并添加接口类 
               2. 将plugin类反射出来 
               3. 反射出来之后,我们通过host 开始调用插件的方法 
局限性:
    1. 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。
    2. 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。
    3. activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。
    4. 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。
    5. 目前暂不支持Service、BroadcastReceiver等需要注册才能使用的组件,但广播可以采用代码动态注册。

2、DyAPK
[项目地址]:(https://github.com/CtripMobile/DynamicAPK)

项目目前状态:停止维护
是否加载独立插件:否
四大组建的支持:支持部分 
    1. 目前不支持service 
    2. 目前只支持动态注册广播 
    3. 支持Activity、ContentProvider组件 
通信方式:
    官方是否提供交互方案:否 
    交互难度:高 
    如何实现插件间通信:插件与宿主App通信方式只能使用安卓系统级别通信(跨进程通信) |
    交互流程:
        1. 约定aidl通信接口 
        2. 实现通信接口本地代理 
局限性:
    1. 项目没有详细的文档 
    2. 项目已于2年前停止维护

3、DPG
[项目地址]:(https://github.com/DroidPluginTeam/DroidPlugin)

项目目前状态:维护状态
是否加载独立插件:是
四大组建的支持:全部支持
    1. 插件的四大组件完全不需要在Host程序中注册 
    2. 支持Service、Activity、BroadcastReceiver、ContentProvider四大组件
通信方式: 
    官方是否提供交互方案:否 
    交互难度:高
    如何实现插件间通信:
    如何实现插件间通信:插件与宿主App通信方式只能使用安卓系统级别通信(跨进程通信) 
    交互流程:
        1. 约定aidl通信接口 
        2. 实现通信接口本地代理 
局限性:
    1. 无法Notification使用自定义资源发送,例如:一个通知自定义RemoteLayout,这意味着的Notification并且必须为空。contentView tickerView bigContentView headsUpContentView通过R.drawable.XXX定制的图标通知。框架会将其转换为Bitmap。
    2. 无法限定指定Intent Filter为插入应用程序的Service,Activity,BroadcastReceiver 和ContentProvider。所以插件应用程序对于外部系统和应用程序是不可见的。
    3. 缺乏Hook的Native层,从而APK(例如大多数游戏应用程序)与native代码不能被加载插件。

4、RPG
[项目地址]:(https://github.com/Qihoo360/RePlugin)

项目目前状态:近期项目
是否加载独立插件:是
四大组建的支持:全部支持
    1. 插件的四大组件完全不需要在Host程序中注册
    2. 支持Service、Activity、BroadcastReceiver、ContentProvider四大组
通信方式:
    官方是否提供交互方案:否
    交互难度:高
    如何实现插件间通信:
    如何实现插件间通信:插件与宿主App通信方式只能使用安卓系统级别通信(跨进程通信)
    交互流程:
        1. 约定aidl通信接口 
        2. 实现通信接口本地代理 
局限性:
    1. 插件权限声明无效,只认主程序的
    2. 插件声明Target SDK无效,只认主程序的
    3. 不支持Notification的Layout资源
    4. 暂不支持“宿主和插件之间直接调用类和方法”(宿主和主程序形成父子类ClassLoader)方案

5、APG
[项目地址]:(http://www.apkplug.com)

项目目前状态:维护状态
是否加载独立插件:是
四大组建的支持:全部支持 
    1. 对于广播组件,按常规使用即可,无其他要求
    2. provider组件需要在宿主配置代理标签
    3. activity和service组件有两种使用方式,代理方式和穿透方式
通信方式: 
    官方是否提供交互方案:是 
    交互难度:中 
    如何实现插件间通信:支持3种通信方式,OSGI、Dispatch和RPC通信,官方推荐使用RPC方式。 
    交互流程: 
        1. 约定通信接口 
        2. ShareProcessor实现 
        3. 注册bundlerpc服务 
局限性: 
    1. 暂不支持“宿主和插件之间直接调用类和方法”(宿主和主程序形成父子类ClassLoader)方案 
    2. 插件权限声明无效,只认主程序的

插件调用流程

1. DyLA
 //插件文件
    File plugin = new File(apkPath);
    PluginItem item = new PluginItem();
    //插件文件路径
    item.pluginPath = plugin.getAbsolutePath();
    //PackageInfo = PackageManager.getPackageArchiveInfo
    item.packageInfo = DLUtils.getPackageInfo(this, item.pluginPath);
    //launcherActivity
    if (item.packageInfo.activities != null && item.packageInfo.activities.length > 0) {
        item.launcherActivityName = item.packageInfo.activities[0].name;
    }
    //launcherService
    if (item.packageInfo.services != null && item.packageInfo.services.length > 0) {
        item.launcherServiceName = item.packageInfo.services[0].name;
    }
    //加载apk信息
    DLPluginManager.getInstance(this).loadApk(item.pluginPath);
2. DyAPK
BundleCore.getInstance().init(this);
  BundleCore.getInstance().ConfigLogger(true, 1);
  Properties properties = new Properties();
  properties.put("ctrip.android.sample.welcome", "ctrip.android.sample.WelcomeActivity"); // launch page
  sharedPreferences = getSharedPreferences("bundlecore_configs", 0);
  String lastBundleKey = sharedPreferences.getString("last_bundle_key", "");
  bundleKey = buildBundleKey();
  if (!TextUtils.equals(bundleKey, lastBundleKey)) {
      properties.put("ctrip.bundle.init", "true");
      isDexInstalled = false;
      HotPatchManager.getInstance().purge();
  }
 BundleCore.getInstance().startup(properties);
  if (isDexInstalled) {
      HotPatchManager.getInstance().run();
      BundleCore.getInstance().run();
  } 
  else {
      new Thread(new Runnable() {
          @Override
              public void run() {
                  try {
                      ZipFile zipFile = new ZipFile(getApplicationInfo().sourceDir);
                      List bundleFiles = getBundleEntryNames(zipFile, BundleCore.LIB_PATH, ".so");
                      if (bundleFiles != null && bundleFiles.size() > 0) {
                          processLibsBundles(zipFile, bundleFiles);
                          SharedPreferences.Editor edit = getSharedPreferences("bundlecore_configs", 0).edit();
                          edit.putString("last_bundle_key", bundleKey);
                          edit.commit();
                      } 
                      else {
                          Log.e("Error Bundle", "not found bundle in apk");
                      }
                      if (zipFile != null) {
                          try {
                              zipFile.close();
                          } catch (IOException e2) {
                              e2.printStackTrace();
                          }
                      }
                      BundleCore.getInstance().run();
                  } 
                  catch (IOException ex) {
                      ex.printStackTrace();
                  }
              }
          }
  ).start();}
 startActivity(new Intent(getApplicationContext(), Class.forName("xxx.xxx.xxx.MainActivity")));       
  1. DPG
 PluginHelper.getInstance().applicationAttachBaseContext(base);
  super.attachBaseContext(base);
 //1.插件安装、更新
  PluginManager.getInstance().installPackage(String filepath, int flags);
  //2.获取已安装插件:
  List<PackageInfo> installedPlugin = PluginManager.getInstance().getInstalledPackages(PackageManager.GET_ACTIVITIES);
  //3.启动插件:
  Intent intent = mPackageManager.getLaunchIntentForPackage("com.yunda.com." + info.getName() + "plugin");
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  startActivity(intent);
  1. RPG
  //继承RePlug的Application,并实现初始化
  public class SampleApplication extends RePluginApplication {
      @Override
      protected RePluginConfig createConfig() {
          //进行初始化
      return c;
      }
      @Override
      protected RePluginCallbacks createCallbacks() {
          return new HostCallbacks(this);
      }
      /**
      * 宿主针对RePlugin的自定义行为
      */
      private class HostCallbacks extends RePluginCallbacks {
          private static final String TAG = "HostCallbacks";
          private HostCallbacks(Context context) {
              super(context);
          }
          @Override
          public boolean onPluginNotExistsForActivity(Context context, String plugin, Intent intent, int process) {
              return super.onPluginNotExistsForActivity(context, plugin, intent, process);
          }
      }
      private class HostEventCallbacks extends RePluginEventCallbacks {
          private static final String TAG = "HostEventCallbacks";
          public HostEventCallbacks(Context context) {
              super(context);
          }
      @Override
      public void onInstallPluginFailed(String path, InstallResult code) {
          super.onInstallPluginFailed(path, code);
      }
      @Override
      public void onStartActivityCompleted(String plugin, String activity, boolean result) {
          // FIXME 当打开Activity成功时触发此逻辑,可在这里做一些APM、打点统计等相关工作
          super.onStartActivityCompleted(plugin, activity, result);
      }
  }
 //安装插件、升级插件
  RePlugin.install("/sdcard/exam.apk");
  //卸载插件
  RePlugin.uninstall("exam");
  //3.启动插件
  Intent intent = new Intent(v.getContext(), xxx.class);
  context.startActivity(intent);
  1. ApkPlug
  FrameworkFactory.getInstance().start(null, this).getSystemBundleContext();
  PlugManager.getInstance().init(this, bundleContext,publickey,isDebug);
 //安装插件
  PlugManager.getInstance().installPlug(Context context, PlugInfo plugInfo, OnInstallListener listener)
  //获取云端插件信息
  PlugManager.getInstance().getPlugInfo(GetPlugInfoRequest request, OnGetPlugInfoListener listener)
  //获取本地安装插件的更新版本信息
  checkAllLocalPlugVersion(Context context,final OnCheckVersionListener listener)
  //使用云端插件更新
  updataPlug(Bundle bundle,final OnUpdataListener listener)
  //插件启动与停止
  Bundle.start()
  Bundle.stop()

小结

1. DyLA
   1 宿主和插件没有任何联系,但是插件需要继承DLBasePluginActivity,这个不太友好
   2 侵入式的,对插件apk的开发限制太多,例如:必须继承DLBasePluginActivity,启动时候必须调用startPluginActivity(new DLIntent(getPackageName(),TestActivity.class))
   3 这个框架学习成本高,限制多,联调不方便,不建议使用
2. DyAPK
  1. 这个框架争议比较多,且市场上的用例很少,没有参考用例
  2. 项目于2年前停止维护,一些遗留问题没有解决,不建议使用
3. DPG
    1. 宿主和插件完全隔离,插件不依赖宿主,可以独立安装运行
    2. 低入侵设计,插件不需要继承任何类
    3. 插件apk和普通apk一样的,所以插件开发没有门槛
    4. 插件启动速度太慢,而且宿主只能调用插件的LaunchMode的Activity,不能调用其他Activity
    5. 插件间通行没有单独的API调用
4. RPG
    1. 宿主和插件完全隔离,插件不依赖宿主,可以独立安装运行
    2. 低入侵设计,插件不需要继承任何类
    3. 插件apk和普通apk一样的,所以插件开发没有门槛
    4. 插件间通行没有单独的API调用
    5. 文档尚在补充当中,有些API及使用方式还没有文档可以查阅
5. ApkPlug
    1. 宿主和插件完全隔离,插件不依赖宿主,可以独立安装运行
    2. 低入侵设计,插件不需要继承任何类
    3. 插件apk和普通apk一样的,所以插件开发没有门槛
    4. 支持3种通信方式,OSGI、Dispatch和RPC通信
上一篇下一篇

猜你喜欢

热点阅读