承接上一篇文章:Router:一款单品、组件化、插件化全支持的路由框架java
在上一篇文章中,咱们介绍了Router在单品与组件化环境下的使用配置,这篇文章就将专门的对Router在插件化环境下的使用配置,做详细说明!android
插件化的使用很复杂。这也是我要把插件化的配置单独拿出来说的主要缘由!git
照规矩,先上开源库地址:github.com/JumeiRdGrou…github
插件化因为其实现方式各不相同,因此一直以来也没有个统一的路由框架提供使用。api
对于大型项目来讲,不少都有接入使用各自的Router框架,Router框架已经作好了上层跳转的解耦。可是若是接入插件化的话,因为启动方式与启动流程都发生了变化,因此路由自己的跳转逻辑就都不能使用了。因此这也在无形中,对插件化方案的选择形成了极大影响:安全
因此在这个时候,咱们须要的路由框架就是:无论个人开发环境如何变化。我都能经过必定的手段,对当前的运行环境进行适配兼容,而不用修改上层路由跳转的代码逻辑。bash
这就是Router的适配逻辑:你须要作的就是根据不一样的插件化方案,定制出对应的路由启动兼容流程便可!app
插件化环境的适配难点,包括如下几个方面:框架
插件的路由表注册方式与具体的插件化方案有关。具体实现方案能够参考后面具体的案例。ide
基本全部的插件化框架。都有提供自身的不一样的启动方式。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");
}
}
复制代码
以上便是插件化兼容的具体核心所在,对于别的插件化框架,按照以上思路进行对应适配便可。