Router: 教你如何進行任意插件化環境下的路由適配

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

在上一篇文章中,咱們介紹了Router在單品與組件化環境下的使用配置,這篇文章就將專門的對Router在插件化環境下的使用配置,做詳細說明!android

插件化的使用很複雜。這也是我要把插件化的配置單獨拿出來說的主要緣由!git

照規矩,先上開源庫地址:github.com/JumeiRdGrou…github

爲何要作適配任意插件化的路由

插件化因爲其實現方式各不相同,因此一直以來也沒有個統一的路由框架提供使用。api

對於大型項目來講,不少都有接入使用各自的Router框架,Router框架已經作好了上層跳轉的解耦。可是若是接入插件化的話,因爲啓動方式與啓動流程都發生了變化,因此路由自己的跳轉邏輯就都不能使用了。因此這也在無形中,對插件化方案的選擇形成了極大影響:安全

  1. 選擇的插件化方案必需要支持當前使用的路由框架才行。
  2. 插件化方案一旦選擇後很難更換,切換成本過高。

因此在這個時候,咱們須要的路由框架就是:無論個人開發環境如何變化。我都能經過必定的手段,對當前的運行環境進行適配兼容,而不用修改上層路由跳轉的代碼邏輯。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的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");
    }
}
複製代碼

結語

以上便是插件化兼容的具體核心所在,對於別的插件化框架,按照以上思路進行對應適配便可。

相關文章
相關標籤/搜索