Router: 教你如何進行任意外掛化環境下的路由適配
外掛化的使用很複雜。這也是我要把外掛化的配置單獨拿出來講的主要原因!
為什麼要做適配任意外掛化的路由
外掛化由於其實現方式各不相同,所以一直以來也沒有個統一的路由框架提供使用。
對於大型專案來說,很多都有接入使用各自的Router框架,Router框架已經做好了上層跳轉的解耦。但是如果接入外掛化的話,由於啟動方式與啟動流程都發生了變化,所以路由本身的跳轉邏輯就都不能使用了。所以這也在無形中,對外掛化方案的選擇造成了極大影響:
- 選擇的外掛化方案必須要支援當前使用的路由框架才行。
- 外掛化方案一旦選擇後很難更換,切換成本太高。
所以在這個時候,我們需要的路由框架就是:不管我的開發環境如何變化。我都能通過一定的手段,對當前的執行環境進行適配相容,而不用修改上層路由跳轉的程式碼邏輯。
這就是Router的適配邏輯:你需要做的就是根據不同的外掛化方案,定製出對應的路由啟動相容流程即可!
PS :有興趣的加入Android工程師交流QQ群:752016839 主要針對Android開發人員提升自己,突破瓶頸,相信你來學習,會有提升和收穫。
外掛化適配方案簡述
外掛化環境的適配難點,包括以下幾個方面:
- 外掛路由表註冊:
外掛的路由表註冊方式與具體的外掛化方案有關。具體實現方案可以參考後面具體的案例。
- 啟動方式適配:
基本所有的外掛化框架。都有提供自身的不同的啟動方式。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.
針對此兩種型別的外掛化,分別提供兩種形式的適配方案:
1. 非隔離型外掛
對於非隔離型外掛。相對來說需要適配的點比較少。
非隔離型外掛的路由配置,這裡舉例使用的外掛化框架是Small外掛化框架。
Small外掛化路由配置
- 外掛路由表註冊
因為是非隔離型元件,都是執行在同一程序環境下。所以與元件化的邏輯類似。也需要分別對不同的外掛。指定不同的路由表生成類包名。
而對於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連結。此處主要使用此進行舉例:如何針對隔離型外掛進行對應的路由適配。如果有朋友已經使用了此框架。建議仔細看一遍。便於以後遇到問題進行修復
外掛路由表註冊
隔離型外掛中。各個外掛都是執行在一個自己獨立的沙盒之中,所以不能單純只使用上面非隔離型外掛的做法,而是需要按照下面的流程進行路由表註冊:
首先。還是不管是宿主還是外掛。都先各自注冊自身的路由表類,使外掛被載入執行後可以自動註冊自身的路由表進行使用:
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");
}
}
複製程式碼
結語
以上即是外掛化相容的具體核心所在,對於別的外掛化框架,按照以上思路進行對應適配即可。