Native 與 H5 交互的那些事
Hybrid開發模式目前幾乎每家公司都有涉及和使用,這種開發模式兼具良好的Native用戶交互體驗的優勢與WebApp跨平臺的優勢,而這種模式,在Android中必然需要WebView作為載體來展示H5內容和進行交互,而WebView的各種安全性、兼容性的問題,我想大多數人與它友誼的小床已經翻了,特別是4.2版本之前的addjavascriptInterface接口引起的漏洞,可能導致惡意網頁通過Js方法遍歷剛剛通過addjavascriptInterface注入進來的類的所有方法從中獲取到getClass方法,然后通過反射獲取到Runtime對象,進而調用Runtime對象的exec方法執行一些操作,惡意的Js代碼如下:
function execute(cmdArgs) {
for (var obj in window) {
if ("getClass" in window[obj]) {
alert(obj);
return window[obj].getClass().forName("java.lang.Runtime")
.getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
}
}
}
為了避免這個漏洞,即需要限制Js代碼能夠調用到的Native方法,官方于是在從4.2開始的版本可以通過為可以被Js調用的方法添加 @JavascriptInterface 注解來解決,而之前的版本雖然不能通過這種方法解決,但是可以使用Js的 prompt 方法進行解決,只不過 需要和前端協商好一套公共的協議 ,除此之外,為了避免WebView加載任意url,也需要對url進行白名單檢測,由于Android碎片化太嚴重,WebView也存在兼容性問題,WebView的內核也在4.4版本進行了改變,由webkit改為chromium,此外WebView還有一個非常明顯的問題,就是內存泄露,根本原因就是Activity與WebView關聯后,WebView內部的一些操作的執行在新線程中,這些時間無法確定,而可能導致WebView一直持有Activity的引用,不能回收。下面就談談怎樣正確安全的讓Native與H5交互
1. Native與H5怎樣安全的進行交互?
要使得H5內的Js與Native之間安全的相互進行調用,我們除了可以通過添加 @JavascriptInterface 注解來解決(>=4.2),還有通過 prompt 的方式,不過如果使用官方的方式,這就需要對4.2以下做兼容了,這樣使得我們一個app中有兩套Js與Native交互的方式,這樣極其不好維護,我們應該只需要一套Js與Native交互的方式,所以,我們借助Js中的 prompt 方法來實現 一套安全的Js與Native交互的JsBridge框架
1.1 Js與Native代碼相互調用
Native Invoke Js:
我們知道如果Native需要調用Js中的方法,只需要使用 WebView:loadUrl(); 方法即可直接調用指定Js代碼,如:
mWebView.loadUrl("javascript:setUserName('zhengxiaoyong');");
這樣就直接調用了Js中的 setUserName 方法并把 zhengxiaoyong 這個名字傳到這個方法中去了,接下來就是Js自己處理了
Js Invoke Native:
而如果Js要調用Native中的Java方法呢?這就需要我們自己實現了,因為我們不采取 JavascriptInterface 的方式,而采取prompt方式
對WebView熟悉的同學們應該都知道Js中對應的 window.alert() 、 window.confirm() 、 window.prompt() 這三個方法的調用在 WebChromeClient 中都有對應的回調方法,分別為:
onJsAlert() 、 onJsConfirm() 、 onJsPrompt() ,對于它們傳入的 message ,都可以在相應的回調方法中接收到,所以,對于Js調Native方法,我們可以借助這個信道,和前端協定好一段特定規則的 message ,這個規則中應至少包含這些信息:
所調用Native方法所在類的類名
所調用Native的方法名
Js調用Native方法所傳入的參數
所以基于這些信息,很容易想到使用http協議的格式來協定規則,如下格式:
scheme://host:port/path?query
對應的我們協定 prompt 傳入 message 的格式為:
jsbridge://class:port/method?params
這樣以來,前端和app端協商好后,以后前端需要通過Js調用Native方法來獲取一些信息或功能,就只需要按照協議的格式把需要調用的類名、方法名、參數放入對應得位置即可,而我們會在 onJsPrompt 方法中接受到,所以我們根據與前端協定好的協議來進行解析,我們可以用一個 Uri 來包裝這段協議,然后通過 Uri:getHost、getPath、getQuery 方法獲取對應的類名,方法名,參數數據,最后通過反射來調用指定類中指定的方法
而此時會有人問? port 是用來干嘛的?params格式是KV還是什么格式?
當然,既然和前端協定好了協議的格式了,那么params肯定也是需要協定好的,可以用KV格式,也可以用一串Json字符串表示,為了解析方便,還是建議使用 Json格式
而 port 是用來干嘛的呢?
port 我們并不會直接操作它,它是由Js代碼自動生成的,port的作用是為了標識Js中的回調 function ,當Js調用Native方法時,我們會得到本次調用的 port 號,我們需要在Native方法執行完畢后再把該 port 、執行的后結果、是否調用成功、調用失敗的msg等信息通過調用Js的 onComplete 方法傳入,這時候Js憑什么知道你本次返回的信息是哪次調用的結果呢?就是通過 port 號,因為在Js調用Native方法時我們會把自動生成的 port 號和此次回調的 function 綁定在一起,這樣以來Native方法返回結果時把 port 也帶過來,就知道是哪次回調該用哪個 function 方法來處理
自動生成 port 和綁定 function回調 的Js代碼如下:
generatePort: function () {
return Math.floor(Math.random() * (1 << 50)) + '' + increase++;
},
//調用Native方法
callMethod: function (clazz, method, param, callback) {
var port = PrivateMethod.generatePort();
if (typeof callback !== 'function') {
callback = null;
}
//綁定對應port的function回調函數
PrivateMethod.registerCallback(port, callback);
PrivateMethod.callNativeMethod(clazz, port, method, param);
},
onComplete: function (port, result) {
//把Native返回的Json字符串轉為JSONObject
var resultJson = PrivateMethod.str2Json(result);
//獲取對應port的function回調函數
var callback = PrivateMethod.getCallback(port).callback;
PrivateMethod.unRegisterCallback(port);
if (callback) {
//執行回調
callback && callback(resultJson);
}
}
Js代碼上已經注釋的很清楚了,就不多解釋了。
經過上面介紹,那么在Native方法執行完成后,當然就需要把結果返回給Js了,那么結果的格式又是什么呢?返回給Js方法又是什么呢?沒錯,還是需要和前端進行協定,建議數據的返回格式為Json字符串,基本格式為:
resultData = {
status: {
code: 0,//0:成功,1:失敗
msg: '請求超時'//失敗時候的提示,成功可為空
},
data: {}//數據,無數據可以為空
};
其中定義了一個 status ,這樣的好處是無論在Native方法調用成功與否、Native方法是否有返回值,Js中都可以收到返回的信息,而這個Json字符串至少都會包含一個 status Json對象來描述Native方法調用的狀況
而返回給Js的方法自然是上面的 onComplete 方法:
javascript:RainbowBridge.onComplete(port,resultData);
ps:RainbowBridge是我的JsBridge框架的名字
至此Js調用Native的流程就分析完成了,一切都看起來那么美妙,因為,我們自己實現一套 Js Invoke Native 的主要目的是讓Js調用Native更加安全,同時也只維護一套 JsBridge 框架更加方便,那么這個安全性表現在哪里了?
我們知道之前原生的方式漏洞就是惡意Js代碼可能會調用Native中的其它方法,那么答案出來了,如果需要讓 Js Invoke Native 保證安全性,只需要限制我們通過反射可調用的方法,所以,在JsBridge框架中,我們需要對Js能調用的Native方法給予一定的規則,只有符合這些規則Js才能調用,而我的規則是:
1、Native方法包含public static void 這些修飾符(當然還可能有其它的,如:synchronized)
2、Native方法的參數數量和類型只能有這三個:WebView、JSONObject、JsCallback。為什么要傳入這三個參數呢?
2.1、第一個參數是為了提供一個WebView對象,以便獲取對應Context和執行WebView的一些方法
2.2、第二個參數就是Js中傳入過來的參數,這個肯定要的
2.3、第三個參數就是當Native方法執行完畢后,把執行后的結果回調給Js對應的方法中
所以符合Js調用的Native方法格式為:
public static void ***(WebView webView, JSONObject data, JsCallback callback) {
//get some info ...
JsCallback.invokeJsCallback(callback, true, result, null);
}
判斷Js調用的方法是否符合該格式的代碼為,符合則存入一個Map中供Js調用:
private void putMethod(Class<?> clazz) {
if (clazz == null)
return;
ArrayMap<String, Method> arrayMap = new ArrayMap<>();
Method method;
Method[] methods = clazz.getDeclaredMethods();
int length = methods.length;
for (int i = 0; i < length; i++) {
method = methods[i];
int methodModifiers = method.getModifiers();
if ((methodModifiers & Modifier.PUBLIC) != 0 && (methodModifiers & Modifier.STATIC) != 0 && method.getReturnType() == void.class) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes != null && parameterTypes.length == 3) {
if (WebView.class == parameterTypes[0] && JSONObject.class == parameterTypes[1] && JsCallback.class == parameterTypes[2]) {
arrayMap.put(method.getName(), method);
}
}
}
}
mArrayMap.put(clazz.getSimpleName(), arrayMap);
}
對于有返回值的方法,并不需要設置它的返回值,因為方法的結果最后我們是通過 JsCallback.invokeJsCallback 來進行對Js層的回調,比如我貼一個符合該格式的Native方法:
public static void getOsSdk(WebView webView, JSONObject data, JsCallback callback) {
JSONObject result = new JSONObject();
try {
result.put("os_sdk", Build.VERSION.SDK_INT);
} catch (JSONException e) {
e.printStackTrace();
}
JsCallback.invokeJsCallback(callback, true, result, null);
}
Js調Native代碼執行耗時操作情況處理
一般情況下,比如我們通過Js調用Native方法來獲取AppName、OsSDK版本、IMSI號、用戶信息等都不會有問題,但是,假如該Native方法需要執行一些耗時操作,如:IO、sp、Bitmap Decode、SQLite等,這時為了保護UI的流暢性,我們需要讓這些操作執行在異步線程中,待執行完畢再把結果回調給Js,而我們可以提供一個線程池來專門處理這些耗時操作,如:
public static void doAsync(WebView webView, JSONObject data, final JsCallback callback) {
AsyncTaskExecutor.runOnAsyncThread(new Runnable() {
@Override
public void run() {
//IO、sp、Bitmap Decode、SQLite
JsCallback.invokeJsCallback(callback, true, result, null);
}
});
}
【注】:對于WebView,它的方法的調用只能在主線程中調用,當設計到WebView的方法調用時,切記不可以放在異步線程中調用,否則就GG了.
Js調Native流程圖
JsBridge效果圖
RainbowBridge: github地址
1.2 白名單Check
上面我們介紹了JsBridge的基本原理,實現了Js與Native相互調用,而且還避免了惡意Js代碼調用Native方法的安全問題,通過這樣我們保證了Js調用Native方法的安全性,即Js不能隨意調用任意Native方法,不過,對于WebView容器來說,它并不關心所加載的url是Js代碼還是網頁地址,它所做的工作就是執行我們傳入的url,而WebView加載url的方式有兩種:get和post,方式如下:
mWebView.loadUrl(url);//get
mWebView.postUrl(url,data);//post
對于這兩種方式,也有不同的應用點,一般get方式用于查,也就是傳入的數據不那么重要,比如:商品列表頁、商品詳情頁等,這些傳入的數據只是一些商品類的信息。而post方式一般用于改,post傳入的數據往往是比較私密的,比如:訂單界面、購物車界面等,這些界面只有在把用戶的信息post給服務器后,服務器才能正確的返回相應的信息顯示在界面上。所以,對于post方式涉及到用戶的私密信息,我們總不能給一個url就把私密數據往這個url里面發吧,當然不可能的,這涉及到安全問題,那么就需要一個白名單機制來檢查url是否是我們自己的,是我們自己的那么即可以post數據,不是我們自己的那就不post數據,而白名單的定義通常可以以我們自己的域名來判斷,搞一個正則表達式,所以我們可以重寫WebView的 postUrl 方法:
@Override
public void postUrl(String url, byte[] postData) {
if (JsBridgeUrlCheckUtil.isTrustUrl(url)) {
super.postUrl(url, postData);
} else {
super.postUrl(url, null);
}
}
這樣就對不是我們自己的url進行了攔截,不把數據發送到不是我們自己的服務器中
至此,白名單的Check還沒有完成,因為這只是對WebView加載Url時候做的檢查,而在WebView內各中鏈接的跳轉、其中有些url還可能被運營商劫持注入了廣告,這就有可能在WebView容器內的跳轉到某些界面后,該界面的url并不是我們自己的,但是它里面有Js代碼調用Native方法來獲取一些數據,雖然說Js并不能隨便調我們的Native方法,但是有些我們指定可以被調用的Native方法可能有一些獲取設備信息、讀取文件、獲取用戶信息等方法,所以,我們也應該在Js調用Native方法時做一層白名單Check,這樣才能保證我們的信息安全
所以,白名單檢測需要在兩個地方進行檢測:
1、WebView:postUrl()前檢測url的合法性2、Js調用Native方法前檢測當前界面url的合法性
具體代碼如下:
@Override
public void postUrl(String url, byte[] postData) {
if (JsBridgeUrlCheckUtil.isTrustUrl(url)) {
super.postUrl(url, postData);
} else {
super.postUrl(url, null);
}
}
/**
* @param webView WebView
* @param message rainbow://class:port/method?params
*/
public void call(WebView webView, String message) {
if (webView == null || TextUtils.isEmpty(message))
return;
if (JsBridgeUrlCheckUtil.isTrustUrl(webView.getUrl())) {
parseMessage(message);
invokeNativeMethod(webView);
}
}
1.3 移除默認內置接口
WebView內置默認也注入了一些接口,如下:
//移除默認內置接口,防止遠程代碼執行漏洞攻擊
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mWebView.removeJavascriptInterface("searchBoxJavaBridge_");
mWebView.removeJavascriptInterface("accessibility");
mWebView.removeJavascriptInterface("accessibilityTraversal");
}
這些接口雖然不會影響用prompt方式實現的Js與Native交互,但是在使用addJavascriptInterface方式時,有可能有安全問題,最好移除
2. WebView相關
2.1 WebView的配置
下面給出WebView的通用配置:
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setSupportZoom(false);
webSettings.setBuiltInZoomControls(false);
webSettings.setAllowFileAccess(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setGeolocationEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setAppCachePath(getApplicationContext().getCacheDir().getPath());
webSettings.setDefaultTextEncodingName("UTF-8");
//屏幕自適應
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
} else {
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webSettings.setDisplayZoomControls(false);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webSettings.setLoadsImagesAutomatically(true);
} else {
webSettings.setLoadsImagesAutomatically(false);
}
mWebView.setScrollBarStyle(WDWebView.SCROLLBARS_INSIDE_OVERLAY);
mWebView.setHorizontalScrollBarEnabled(false);
mWebView.setHorizontalFadingEdgeEnabled(false);
mWebView.setVerticalFadingEdgeEnabled(false);
其中有一項配置,是在4.4以上版本時設置網頁內圖片可以自動加載,而4.4以下版本則不可自動加載,原因是4.4WebView內核的改變,使得WebView的性能更優,所以在4.4以下版本不讓圖片自動加載,而是先讓WebView加載網頁的其它靜態資源:js、css、文本等等,待網頁把這些靜態資源加載完成后,在 onPageFinished 方法中再把圖片自動加載打開讓網頁加載圖片:
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (!mWebView.getSettings().getLoadsImagesAutomatically()) {
mWebView.getSettings().setLoadsImagesAutomatically(true);
}
}
2.2 WebView的獨立進程
通常來說,WebView的使用會帶來諸多問題,內存泄露就是最常見的問題,為了避免WebView內存泄露,目前最流行的有兩種做法:
1、獨立進程,簡單暴力,不過可能涉及到進程間通信2、動態添加WebView,對傳入WebView中使用的Context使用弱引用,動態添加WebView意思在布局創建個ViewGroup用來放置WebView,Activity創建時add進來,在Activity停止時remove掉
個人推薦獨立進程,好處主要有兩點,一是在WebViewActivity使用完畢后直接干掉該進程,防止了內存泄露,二是為我們的app主進程減少了額外的內存占用量
使用獨立進程還需注意一點,這個進程中在有多個WebViewActivity,不能在Activity銷毀時就干掉進程,不然其它Activity也會蹦了,此時應該在該進程創建一個Activity的維護集合,集合為空時即可干掉進程
關于WebView的銷毀,如下:
private void destroyWebView(WebView webView) {
if (webView == null)
return;
webView.stopLoading();
ViewParent viewParent = webView.getParent();
if (viewParent != null && viewParent instanceof ViewGroup)
((ViewGroup) viewParent).removeView(webView);
webView.removeAllViews();
webView.destroy();
webView = null;
}
2.3 WebView的兼容性
2.3.1 不同版本硬件加速的問題
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 && shouldOpenHardware()) {
mWebView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
public static boolean shouldOpenHardware () {
if ("samsung".equalsIgnoreCase(Build.BRAND))
return false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
return true;
return true;
}
2.3.2 不同設備點擊WebView輸入框鍵盤的不彈起
mWebView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
try {
if (mWebView != null)
mWebView.requestFocus();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
});
2.3.3 三星手機硬件加速關閉后導致H5彈出的對話框出現不消失情況
String brand = android.os.Build.BRAND;
if ("samsung".equalsIgnoreCase(brand) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
2.3.4 不同版本shouldOverrideUrlLoading的回調時機
對于 shouldOverrideUrlLoading 的加載時機,有些同學經常與 onProgressChanged 這個方法的加載時機混淆,這兩個方法有兩點不同:
1、 shouldOverrideUrlLoading 只會走Get方式的請求,Post方式的請求將不會回調這個方法,而 onProgressChanged 對Get和Post都會走
2、 shouldOverrideUrlLoading 都知道在WebView內部點擊鏈接(Get)會觸發,它在Get請求打開界面時也會觸發, shouldOverrideUrlLoading 還有一點特殊,就是在按返回鍵返回到上一個頁面時時不會觸發的,而 onProgressChanged 在只要界面更新了都會觸發
對于 shouldOverrideUrlLoading 的返回值,返回true為剝奪WebView對該此請求的控制權,交給應用自己處理,所以WebView也不會加載該url了,返回false為WebView自己處理
對于 shouldOverrideUrlLoading 的調用時機,也會有不同,在3.0以上是會正常調用的,而在3.0以下,并不是每次都會調用,可以在 onPageStarted 方法中做處理,也沒必要了,現在應該都適配4.0以上了
2.3.5 頁面重定向導致WebView:goBack()無效的處理
像一些界面有重定向,比如:淘寶等,需要按多次(>1)才能正常返回,一般都是二次,所以可以把那些具有重定向的界面存入一個集合中,在攔截返回事件中這樣處理:
@Override
public void onBackPressed() {
if (mWebView == null)
return;
WebBackForwardList backForwardList = mWebView.copyBackForwardList();
if (backForwardList != null && backForwardList.getSize() != 0) {
int currentIndex = backForwardList.getCurrentIndex();
WebHistoryItem historyItem = backForwardList.getItemAtIndex(currentIndex - 1);
if (historyItem != null) {
String backPageUrl = historyItem.getUrl();
if (TextUtils.isEmpty(backPageUrl))
return;
int size = REDIRECT_URL.size();
for (int i = 0; i < size; i++) {
if (backPageUrl.contains(REDIRECT_URL.get(i)))
mWebView.goBack();
}
}
}
if (mWebView.canGoBack()) {
mWebView.goBack();
} else {
this.finish();
}
}
這里處理是在按返回鍵時,如果上一個界面是重定向界面,則直接調用goBack,或者也可以finish當前Activity
2.3.6 WebView無法加載不信任網頁SSL錯誤的處理
有時我們的WebView會加載一些不信任的網頁,這時候默認的處理是WebView停止加載了,而那些不信任的網頁都不是由CA機構信任的,這時候你可以選擇繼續加載或者讓手機內的瀏覽器來加載:
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
//繼續加載
handler.proceed();
//或者其它處理 ...
}
2.3.7 自定義WebView加載出錯界面
出錯的界面的顯示,可以在這個方法中控制:
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
}
你可以重新加載一段Html專門用來顯示錯誤界面,或者用布局顯示一個出錯的View,這時候需要把出錯的WebView內容清除,可以使用:
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
view.loadDataWithBaseURL(null,"","text/html","UTF-8",null);
errorView.setVisibility(View.VISIBLE);
}
2.3.8 獲取位置權限的處理
如果在WebView中有獲取地理位置的請求,那么可以直接在代碼中默認處理了,沒必要彈出一個框框讓用戶每次都確認:
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
super.onGeolocationPermissionsShowPrompt(origin, callback);
callback.invoke(origin, true, false);
}
2.4 打造一個通用的WebViewActivity界面
一個通用的WebViewActivity當然是樣式和WebView內部處理的策略都統一樣,這里只對樣式進行說明,因為WebView內部的處理各個公司都不一樣,但應該都需要包含這么幾點吧:
1、白名單檢測
2、Url的跳轉
3、出錯的處理
4、…
一個WebViewActivity界面,最主要的就是Toolbar標題欄的設計了,因為不同的app的WebViewActivity界面Toolbar上有不同的icon和操作,比如:分享按鈕、刷新按鈕、更多按鈕,都不一樣,既然需要通用,即可讓調用者傳入某個參數來動態改變這些東西吧,比如傳一個ToolbarStyle來標識此WebViewActivity的風格是什么樣的,背景色、字體顏色、圖標等,包括點擊時的動畫效果,作為通用的界面,必須是讓調用者簡單操作,不可能調用時傳入一個圖標id還是一個Drawable,所以,主要需要用到tint,來對字體、圖標的顏色動態改變,代碼如下:
public static ColorStateList createColorStateList(int normal, int pressed) {
int[] colors = new int[]{normal, pressed};
int[][] states = new int[2][];
states[0] = new int[]{-android.R.attr.state_pressed};
states[1] = new int[]{android.R.attr.state_pressed};
return new ColorStateList(states, colors);
}
public static Drawable tintDrawable(Drawable drawable, int color) {
final Drawable tintDrawable = DrawableCompat.wrap(drawable.mutate());
ColorStateList colorStateList = ColorStateList.valueOf(color);
DrawableCompat.setTintMode(tintDrawable, PorterDuff.Mode.SRC_IN);
DrawableCompat.setTintList(tintDrawable, colorStateList);
return tintDrawable;
}
3. H5與Native界面互相喚起
對于H5界面,有些操作往往是需要喚起Native界面的,比如:H5中的登錄按鈕,點擊后往往喚起Native的登錄界面來進行登錄,而不是直接在H5登錄,這樣一個app就只需要一套登錄了,而我們所做的便是攔截登錄按鈕的url:
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
parserURL(url); //解析url,如果符合跳轉native界面的url規則,則跳轉native界面
return super.shouldOverrideUrlLoading(view, url);
}
這個規則我們可以在Native的Activity的 intent-filter 中的 data 來定義,如下:
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data
android:host="native"
android:path="/login"
android:scheme="activity"/>
</intent-filter>
</activity>
解析url過程是判斷scheme、host、path的是否有完全與之匹配的,有則喚起
而Native喚H5,其實也是一個url的解析過程,只不過需要配置WebViewActivity的 intent-filter 的 data ,WebViewActivity的scheme配置為http和https
startActivity VS UrlRouter
上面說到了H5與Native互相調起,其實這個可以在app內做成一套界面跳轉的方式,摒棄startActivity,為什么原生的跳轉方式不佳?
1、因為原生的跳轉需要確定該Activity是已經存在的,否則編譯將報錯,這樣帶來的問題是不利于協同開發,如:A、B同學分別正在開發項目的兩個不同的模塊,此時B剛好需要跳A同學的某一個界面,如商品列表頁跳商品詳情頁,這時候B就必須寫個TODO,待B完成該模塊后再寫了。而通過url跳轉,只需要傳入一串url即可
2、原生的跳轉Activity與目標Activity是耦合的,跳轉Activity完全依賴于目標Activity
3、原生的跳轉方式不利于管理所傳遞來的參數,獲取參數時需要在跳轉Activity的地方確定傳遞了幾個參數、什么類型的參數,這樣以來跳轉的方式多了,就比較混亂了。當然一個原生跳轉良好的設計是在目的Activity實現一個靜態的start方法,其它界面要跳直接調用即可
4、最后一個就是在有參數傳遞的情況下,每次跳轉都要寫好多代碼啊
而UrlRouter框架的實現原理,一種實現是可以維護一套Activity與url的映射表,這種方式還是沒有擺脫不利于協同開發這個毛病,另外一種是通過一串指定規則的url與manifest中配置的data匹配,具體跳轉則是通過 intent.setData() 來設置跳轉的url,這種方式比較好,不過需要處理下匹配到多個Activity時優先選擇的問題
JsBridge地址: RainbowBridge