承接上一篇文章: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"); } } 複製代碼
以上便是插件化兼容的具體核心所在,對於別的插件化框架,按照以上思路進行對應適配便可。