承接上一篇文章:Router:一款单品、组件化、插件化全支持的路由框架java
在上一篇文章中,咱们介绍了Router在单品与组件化环境下的使用配置,这篇文章就将专门的对Router在插件化环境下的使用配置,做详细说明!android
插件化的使用很复杂。这也是我要把插件化的配置单独拿出来说的主要缘由!git
照规矩,先上开源库地址:github.com/JumeiRdGrou…github
插件化因为其实现方式各不相同,因此一直以来也没有个统一的路由框架提供使用。api
对于大型项目来讲,不少都有接入使用各自的Router框架,Router框架已经作好了上层跳转的解耦。可是若是接入插件化的话,因为启动方式与启动流程都发生了变化,因此路由自己的跳转逻辑就都不能使用了。因此这也在无形中,对插件化方案的选择形成了极大影响:安全
因此在这个时候,咱们须要的路由框架就是:无论个人开发环境如何变化。我都能经过必定的手段,对当前的运行环境进行适配兼容,而不用修改上层路由跳转的代码逻辑。bash
这就是Router的适配逻辑:你须要作的就是根据不一样的插件化方案,定制出对应的路由启动兼容流程便可!markdown
插件化环境的适配难点,包括如下几个方面:app
插件的路由表注册方式与具体的插件化方案有关。具体实现方案能够参考后面具体的案例。框架
基本全部的插件化框架。都有提供自身的不一样的启动方式。Router提供了启动器接口。能够针对不一样的插件化方案,针对性的适配各自的路由启动器:
public abstract class ActivityLauncher extends Launcher<ActivityRouteRule>{ // 建立启动intent public abstract Intent createIntent(Context context); // 使用android.app.Fragment进行启动 public abstract void open(Fragment fragment) throws Exception; // 使用v4包的fragment进行启动 public void open(android.support.v4.app.Fragment fragment) throws Exception; // 使用Context进行启动 public abstract void open(Context context) throws Exception; } 复制代码
插件化环境下。定制好对应的Activity启动器以后。便可经过下方的api进行默认启动器适配了:
RouterConfiguration.get().setActivityLauncher(DefaultActivityLauncher.class);
复制代码
不少插件化都有提供外置插件,或者又能够称为远程插件。这些插件因为不在本地。因此就须要在启动的时候进行动态适配:
因此能够看到。相比于普通的单品、组件化模式。插件化中由于有其各自的按需加载模型。因此也会须要作好对应的路由<-->插件匹配。作到更好的进行插件化兼容。
插件化框架各类各样。这里我也对插件化框架进行了个简单的划分:隔离型插件与非隔离型插件。
隔离型插件: 此类插件是指:每一个插件都是相对独立的个体。并且都运行在各自不一样的沙盒中,好比360的RePlugin与DroidPlugin。各个插件及宿主之间。不能像同一个应用同样直接共享数据。DroidPlugin是不一样插件有分别运行在不一样的插件进程。RePlugin是每一个插件都是使用的一个独立的classLoader来类加载器。都实现了代码级别的隔离,这两种都是隔离型插件。
非隔离型插件: 这种是对业务逻辑存在耦合的环境下,开发app最友好的插件化方案。这种插件框架,全部的插件都是运行在同一个进程中且未作隔离。宿主与插件、插件与插件之间能够直接共享数据。好比Small和VirtualAPK.
针对此两种类型的插件化,分别提供两种形式的适配方案:
对于非隔离型插件。相对来讲须要适配的点比较少。
非隔离型插件的路由配置,这里举例使用的插件化框架是Small插件化框架。
此处也有提供Small插件化配置环境下的demo,能够做为具体的代码参考
由于是非隔离型组件,都是运行在同一进程环境下。因此与组件化的逻辑相似。也须要分别对不一样的插件。指定不一样的路由表生成类包名。
而对于Small框架的插件路由表注册方式。推荐的方式是直接在各自的插件中的Application中。各自注册自身的路由表便可, 使得插件被加载后能够自动注册自身的路由表:
// 指定插件的包名 @RouteConfig(pack="com.small.router.plugin") public class PluginApp extends Application { ... @Override public void onCreate() { // 注册自身module生成的路由表类。 RouterConfiguration.get().addRouteCreator(new com.small.router.plugin.RouterRuleCreator()); } } 复制代码
不一样插件化方案。都有提供自身的不一样的启动api。可是Small插件化框架,自己也彻底支持使用原生的方式进行插件间页面跳转,因此此处谨做为说明。不须要再进行其余的额外适配
而对于其余的插件化方案。不能直接支持原生方式跳转的。能够参考下方隔离型插件配置中的对应适配方案。
隔离型插件的路由配置,这里举例使用的插件化框架是RePlugin插件化框架.
RePlugin的路由适配方案因为比较复杂。因此这里我已经专门封装了针对于RePlugin的Router适配框架:
Router-RePlugin:github.com/yjfnypeu/Ro…
关于Router-RePlugin的具体使用配置方式。请参考上方的Github连接。此处主要使用此进行举例:如何针对隔离型插件进行对应的路由适配。若是有朋友已经使用了此框架。建议仔细看一遍。便于之后遇到问题进行修复
隔离型插件中。各个插件都是运行在一个本身独立的沙盒之中,因此不能单纯只使用上面非隔离型插件的作法,而是须要按照下面的流程进行路由表注册:
首先。仍是不论是宿主仍是插件。都先各自注册自身的路由表类,使插件被加载运行后能够自动注册自身的路由表进行使用:
RouterConfiguration.get().addRouteCreator(new RouterRuleCreator());
复制代码
Router提供了router-host依赖: 经过AIDL的方式提供一个远程路由服务进程,能够打破隔离,达到让全部插件共享路由表的目的!因此须要在宿主模块中。添加host依赖进行使用。
// 在宿主中添加此依赖 compile "com.github.yjfnypeu.Router:router-host:2.6.0" 复制代码
此远程路由进程名为 applicationId:routerHostService
添加host依赖以后, 便可在宿主与插件中,启动并绑定到此远程服务中,将自身的路由注册添加到远程服务中去进行共享:
在宿主中调用:
RouterConfiguration.get().startHostService(hostPackage, context);
复制代码
在插件中调用:
RouterConfiguration.get().startHostService(hostPackage, context, pluginname);
复制代码
请注意。此启动方法中的hostPackage与pluginname:
共享路由表原理说明图
因为使用的是共享远程路由的方式。因此此时咱们的远程路由表能够说是彻底对外的,别的应用也彻底能够经过咱们的hostPackage来连接到咱们本身的应用中来。这样的话,是很是不安全的。因此框架也提供的对应的安全校验接口。
public class RePluginVerification implements RemoteVerify{ @Override public boolean verify(Context context) throws Exception { // 在此进行安全验证,只有符合条件的才能运行成功链接上远程路由服务。 // 这里因为是RePlugin框架。经测试此框架中全部插件均处于同一进程中。 // 因此此处只运行同一uid的进行通讯便可 return Process.myUid() == Binder.getCallingUid(); } } // 在宿主中添加远程路由服务链接时的安全校验接口 RouterHostService.setVerify(new RePluginVerification()); 复制代码
配置以后。每次有插件想进行链接的时候。都会触发此校验接口进行检查。避免其余应用非法攻击链接。
RePlugin的插件,根据其插件的状态的不一样,须要走不一样的流程。
这里主要看这两个状态:install和running.
install
表明当前插件已安装。可是还没有被加载运行。此处还没有触发插件的application进行插件初始化,即表明当前插件的路由表还没有注册
这个时候须要进行插件启动流程适配,对首次插件启动任务作衔接。
running
表明当前插件已载入,正在运行。此时插件的application已被调用。进行相应的初始化操做,即表明当前插件的路由表已被注册,并添加到远程路由服务中。
这个时候须要进行插件启动方式适配,兼容插件指定的跳转方式。
由于插件的路由规则还没有注册。因此当此时你使用插件中的路由连接进行启动时。确定是会路由匹配失败的。回调到路由回调接口的notFound回调中去。那么此时就应该以此做为衔接点:
public class RePluginRouteCallback implements RouteCallback { ... @Override public void notFound(Uri uri, NotFoundException e) { // 在此进行跨插件路由衔接 } } 复制代码
回调到notFound以后。这里须要作插件路由的衔接工做。而此时对应插件多是install状态,也多是未安装状态(多是远程插件)。可是不论是哪一个状态。都须要首先知道当前的路由url启动连接,所对应的是哪一个插件中的页面,这个时候,就须要创建一个路由-插件名的映射表进行使用了:
public interface IUriConverter { String transform(Uri uri); /** * 默认的插件路由规则转换器。此转换器的规则为:使用路由uri的scheme做为各自插件的别名。 */ IUriConverter internal = new IUriConverter() { @Override public String transform(Uri uri) { return uri.getScheme(); } }; } public class RePluginRouteCallback implements RouteCallback { ... IUriConverter converter; @Override public void notFound(Uri uri, NotFoundException e) { // 转换获取对应的插件名 String pluginName = converter.transform(uri); } } 复制代码
Router-RePlugin的适配方案中。创建了此转换器。在此用来对路由连接进行解析转换。获取到正确的插件名。
这里建议使用上面所提供的默认规则转换器进行使用。由于此种匹配方式。只要你给每一个不一样的插件配置上不一样的scheme便可无缝接入使用。好比当前插件为plugina。那么就能够以下进行配置:
@RouteConfig(baseUrl="plugina://") public class PluginAApplication extends Application {} 复制代码
固然。这是建议的用法,可是现实开发中,很难提供这样统一的路由表。因此这个时候你根据本身的具体须要来定制此转换器便可。
解决了路由-插件名映射表问题。咱们就能够继续往下走了。如今的流程就成了下面这个样子:
因此最终的衔接实现代码应该是以下所示:
@Override public void notFound(Uri uri, NotFoundException e) { String pluginName = converter.transform(uri); if (TextUtils.isEmpty(pluginName)) { // 表示此uri非法。不处理 return; } // 用于判断此别名所表明的插件路由 if (RouterConfiguration.get().isRegister(pluginName)) { // 当插件已被注册过。表示此路由的确是没有能够匹配到的路由地址。 return; } /* 请求加载插件并启动中间桥接页面.便于加载插件成功后恢复路由。 * * RePlugin的触发加载逻辑为: * 当须要启动插件中的某一页面时,触发插件加载或者判断此插件是否须要远程下载 * 因此这里提供了一个中转页面RouterBridgeActivity进行流程衔接 */ RouterBridgeActivity.start(context, pluginName, uri, extras); } 复制代码
public class RouterBridgeActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 启动成功,表明插件加载成功,能够进行路由恢复 Uri uri = getIntent().getParcelableExtra("uri"); RouteBundleExtras extras = getIntent().getParcelableExtra("extras"); // 恢复路由启动并销毁当前页面 Router.resume(uri, extras).open(this); finish(); } public static void start(Context context, String alias, Uri uri, RouteBundleExtras extras) { // 请求加载插件并启动中间桥接页面.便于加载插件成功后恢复路由。 Intent intent = RePlugin.createIntent(alias, RouterBridgeActivity.class.getCanonicalName()); intent.putExtra("uri", uri); intent.putExtra("extras", extras); if (!(context instanceof Activity)) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } RePlugin.startActivity(context, intent); } } 复制代码
通过上面的流程后,就应该是对插件为running状态进行兼容了:
由于此时插件是running的状态,代码对应的插件的路由表已经被注册。能够直接匹配到对应的路由地址,如今剩下的就是进行对应的跳转了:
通常来讲,有指定使用特殊api进行跳转的插件化框架。都有须要一些额外的数据,好比RePlugin在进行跨插件跳转时,须要指定对应的插件别名(或者插件的包名)才行:
Intent intent = RePlugin.createIntent(alias, activityclass);
RePlugin.startActivity(context, intent);
复制代码
因此针对这种状况,Router框架提供了IRemoteFactory接口,用于灵活的添加这种跨插件时须要使用到的额外数据:
class PluginRemoteFactory implements IRemoteFactory { String alias;// 插件别名 public PluginRemoteFactory(String alias) { this.alias = alias; } @Override public Bundle createRemote(Context application, RouteRule rule) { Bundle bundle = new Bundle(); bundle.putString("alias", alias); return bundle; } } // 提供远程数据建立工厂 RouterConfiguration.get().setRemoteFactory(new PluginRemoteFactory(alias)); 复制代码
而后针对性的建立出对应的启动器便可
public class HostActivityLauncher extends DefaultActivityLauncher { @Override public Intent createIntent(Context context) { String alias = alias(); if (TextUtils.isEmpty(alias)) { return super.createIntent(context); } else { Intent intent = RePlugin.createIntent(alias, rule.getRuleClz()); intent.putExtras(bundle); intent.putExtras(extras.getExtras()); intent.addFlags(extras.getFlags()); return intent; } } @Override public void open(Context context) throws Exception { // 根据是否含有alias判断是否须要使用RePlugin进行跳转 String alias = alias(); if (TextUtils.isEmpty(alias)) { super.open(context); } else { RouterBridgeActivity.start(context, alias, uri, extras); } } /* ActivityLauncher基类提供remote变量供上层使用, * 此remote即为IRemoteFactory所建立的额外数据 * * 当alias不存在时,表明这次跳转为插件内跳转。直接走原生api跳转便可 * 当alias存在时,表明是跨插件跳转。须要走RePlugin指定api进行跳转 */ private String alias() { if (remote == null || !remote.containsKey("alias")) { return null; } return remote.getString("alias"); } } 复制代码
以上便是插件化兼容的具体核心所在,对于别的插件化框架,按照以上思路进行对应适配便可。