美文网首页
插件化总结

插件化总结

作者: 卡卡的箱子 | 来源:发表于2020-01-03 10:18 被阅读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);
    
    • 主要API调用
     //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);
          }
      }
    
    • 主要API调用
     //安装插件、升级插件
      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);
    
    • 主要API调用
     //安装插件
      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通信
    

    相关文章

      网友评论

          本文标题:插件化总结

          本文链接:https://www.haomeiwen.com/subject/dyevuctx.html