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

Haoge發表於2018-02-07

承接上一篇文章: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);
複製程式碼
  • 外掛化按需載入模型適配

很多外掛化都有提供外接外掛,或者又可以稱為遠端外掛。這些外掛由於不在本地。所以就需要在啟動的時候進行動態適配:

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

所以可以看到。相比於普通的單品、元件化模式。外掛化中因為有其各自的按需載入模型。所以也會需要做好對應的路由<-->外掛匹配。做到更好的進行外掛化相容。

外掛化框架分類及適配說明

外掛化框架各種各樣。這裡我也對外掛化框架進行了個簡單的劃分:隔離型外掛非隔離型外掛

隔離型外掛: 此類外掛是指:每個外掛都是相對獨立的個體。而且都執行在各自不同的沙盒中,比如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為外掛的唯一標識,用於過濾已註冊的外掛。避免同一外掛多次重複進行新增。

共享路由表原理說明圖

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

新增遠端路由表校驗

由於使用的是共享遠端路由的方式。所以此時我們的遠端路由表可以說是完全對外的,別的應用也完全可以通過我們的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 {}
複製程式碼

當然。這是建議的用法,但是現實開發中,很難提供這樣統一的路由表。所以這個時候你根據自己的具體需要來定製此轉換器即可。

  • 啟動或者下載外掛

解決了路由-外掛名對映表問題。我們就可以繼續往下走了。現在的流程就成了下面這個樣子:

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

所以最終的銜接實現程式碼應該是如下所示:

@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");
    }
}
複製程式碼

結語

以上即是外掛化相容的具體核心所在,對於別的外掛化框架,按照以上思路進行對應適配即可。

相關文章