1. 程式人生 > >Native與H5互動的那些事

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互動

Native與H5怎樣安全的進行互動?

要使得H5內的Js與Native之間安全的相互進行呼叫,我們除了可以通過新增@JavascriptInterface註解來解決(>=4.2),還有通過prompt的方式,不過如果使用官方的方式,這就需要對4.2以下做相容了,這樣使得我們一個app中有兩套Js與Native互動的方式,這樣極其不好維護,我們應該只需要一套Js與Native互動的方式,所以,我們藉助Js中的prompt方法來實現一套安全的Js與Native互動的JsBridge框架

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字串至少都會包含一個statusJson物件來描述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流程圖

JsInvokeNative

JsBridge效果圖

RainbowBridge
RainbowBridge:github地址

白名單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);
    }
}

移除預設內建介面

WebView內建預設也注入了一些介面,如下:

//移除預設內建介面,防止遠端程式碼執行漏洞攻擊
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    mWebView.removeJavascriptInterface("searchBoxJavaBridge_");
    mWebView.removeJavascriptInterface("accessibility");
    mWebView.removeJavascriptInterface("accessibilityTraversal");
}

這些介面雖然不會影響用prompt方式實現的Js與Native互動,但是在使用addJavascriptInterface方式時,有可能有安全問題,最好移除

WebView相關

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);
    }
}

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;
}

WebView的相容性

不同版本硬體加速的問題

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;
}

不同裝置點選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;
    }
});

三星手機硬體加速關閉後導致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);
}

不同版本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以上了

頁面重定向導致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

WebView無法載入不信任網頁SSL錯誤的處理

有時我們的WebView會載入一些不信任的網頁,這時候預設的處理是WebView停止載入了,而那些不信任的網頁都不是由CA機構信任的,這時候你可以選擇繼續載入或者讓手機內的瀏覽器來載入:

@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
    //繼續載入
    handler.proceed();
    //或者其它處理 ...
}

自定義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);
}

獲取位置許可權的處理

如果在WebView中有獲取地理位置的請求,那麼可以直接在程式碼中預設處理了,沒必要彈出一個框框讓使用者每次都確認:

@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
    super.onGeolocationPermissionsShowPrompt(origin, callback);
    callback.invoke(origin, true, false);
}

打造一個通用的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;
}

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-filterdata,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

 

轉自:http://zhengxiaoyong.me/2016/04/20/Native%E4%B8%8EH5%E4%BA%A4%E4%BA%92%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B/

程式碼:https://github.com/Sunzxyong/JsBridge