Router: 教你如何进行任意插件化环境下的路由适配

4,713 阅读13分钟

承接上一篇文章:Router:一款单品、组件化、插件化全支持的路由框架

在上一篇文章中,我们介绍了Router在单品与组件化环境下的使用配置,这篇文章就将专门的对Router在插件化环境下的使用配置,作详细说明!

插件化的使用很复杂。这也是我要把插件化的配置单独拿出来讲的主要原因!

照规矩,先上开源库地址:github.com/JumeiRdGrou…

为什么要做适配任意插件化的路由

插件化由于其实现方式各不相同,所以一直以来也没有个统一的路由框架提供使用。

对于大型项目来说,很多都有接入使用各自的Router框架,Router框架已经做好了上层跳转的解耦。但是如果接入插件化的话,由于启动方式与启动流程都发生了变化,所以路由本身的跳转逻辑就都不能使用了。所以这也在无形中,对插件化方案的选择造成了极大影响:

  1. 选择的插件化方案必须要支持当前使用的路由框架才行。
  2. 插件化方案一旦选择后很难更换,切换成本太高。

所以在这个时候,我们需要的路由框架就是:不管我的开发环境如何变化。我都能通过一定的手段,对当前的运行环境进行适配兼容,而不用修改上层路由跳转的代码逻辑。

这就是Router的适配逻辑:你需要做的就是根据不同的插件化方案,定制出对应的路由启动兼容流程即可!

插件化适配方案简述

插件化环境的适配难点,包括以下几个方面:

  • 插件路由表注册

插件的路由表注册方式与具体的插件化方案有关。具体实现方案可以参考后面具体的案例。

  • 启动方式适配:

基本所有的插件化框架。都有提供自身的不同的启动方式。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的RePluginDroidPlugin。各个插件及宿主之间。不能像同一个应用一样直接共享数据。DroidPlugin是不同插件有分别运行在不同的插件进程。RePlugin是每个插件都是使用的一个独立的classLoader来类加载器。都实现了代码级别的隔离,这两种都是隔离型插件。

非隔离型插件: 这种是对业务逻辑存在耦合的环境下,开发app最友好的插件化方案。这种插件框架,所有的插件都是运行在同一个进程中且未做隔离。宿主与插件、插件与插件之间可以直接共享数据。比如SmallVirtualAPK.

针对此两种类型的插件化,分别提供两种形式的适配方案:

1. 非隔离型插件

对于非隔离型插件。相对来说需要适配的点比较少。

非隔离型插件的路由配置,这里举例使用的插件化框架是Small插件化框架

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插件化框架,本身也完全支持使用原生的方式进行插件间页面跳转,所以此处谨作为说明。不需要再进行其他的额外适配

而对于其他的插件化方案。不能直接支持原生方式跳转的。可以参考下方隔离型插件配置中的对应适配方案。

2. 隔离型插件

隔离型插件的路由配置,这里举例使用的插件化框架是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必须是宿主的包名。此包名将用于启动绑定远程路由服务进程。
  • 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已被调用。进行相应的初始化操作,即代表当前插件的路由表已被注册,并添加到远程路由服务中。

    这个时候需要进行插件启动方式适配,兼容插件指定的跳转方式。

插件启动流程适配(install状态)

因为插件的路由规则尚未注册。所以当此时你使用插件中的路由链接进行启动时。肯定是会路由匹配失败的。回调到路由回调接口的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");
    }
}

结语

以上即是插件化兼容的具体核心所在,对于别的插件化框架,按照以上思路进行对应适配即可。