Android H5容器整理

1.如何實(shí)現(xiàn)和設(shè)計(jì)一套JSBridge义矛?

前端JS調(diào)用native的方式有很多種途茫,或者說(shuō)android有很多種方式可以攔截或者獲取到JS的行為桌吃。如下使用onConsoleMessage的方式娜睛,來(lái)設(shè)計(jì)一個(gè)簡(jiǎn)單的JSBridge:

  • 前端代碼片段
(function () {
  var callbackArr = {};
  window.XJSBridge = {
    //JS調(diào)動(dòng)native
    callNative: function (func, param, callback) {
      //生成調(diào)用的序列號(hào)Id
      var seqId = "__bridge_id_" + Math.random() + "" + new Date().getTime();
      //保存回調(diào)
      callbackArr[seqId] = callback;
      //生成約定的JSBridge消息
      var msgObj = {
        func: func,
        param: param,
        msgType: "callNative",
        seqId: seqId,
      };
      //打印約定的日志
      console.log("____xbridge____:" + JSON.stringify(msgObj));
    },
    //native調(diào)用JS的方法
    nativeInvokeJS: function (res) {
      //解析native的消息
      res = JSON.parse(res);
      //判斷是否是callback消息
      if (res && res.msgType === "jsCallback") {
        //獲取之前保持的回調(diào)方法
        var func = callbackArr[res.seqId];
        //delete callbackArr[res.seqId];
        //調(diào)用JS的回調(diào)
        if ("function" === typeof func) {
          setTimeout(function () {
            //調(diào)用js的回調(diào)
            func(res.param);
          }, 1);
        }
      }
      return true;
    },
  };
  document.dispatchEvent(new Event("xbridge inject success..."), null);
  console.log("xbridge inject success...");
})();

如上代碼就是在window對(duì)象上掛載一個(gè)XJSBridge對(duì)象哆键,可以通過(guò)callNative函數(shù)來(lái)調(diào)用android的native方法掘托,其原理就是JS調(diào)用console.log打印一行約定的日志(Bridge協(xié)議),android端通過(guò)WebChromeClient的onConsoleMessage方法籍嘹,獲取到打印的日志闪盔,然后針對(duì)協(xié)議解析出對(duì)應(yīng)的協(xié)議。

  • 其中WebChromeClient的代碼如下

public class XWebChromeClient extends WebChromeClient {

    private WebViewPage mWebViewPage;

    public XWebChromeClient setWebViewPage(WebViewPage mWebViewPage) {
        this.mWebViewPage = mWebViewPage;
        return this;
    }

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

        if (ConsoleMessage.MessageLevel.LOG.equals(consoleMessage.messageLevel())) {
            XLog.d("onConsoleMessage:" + consoleMessage.message()
                    + ",line = " + consoleMessage.lineNumber()
                    + ",sourceId = " + consoleMessage.sourceId()
                    + ",messageLevel = " + consoleMessage.messageLevel());
            //log級(jí)別的日志
            if (mWebViewPage.handleMsgFromJS(consoleMessage.message())) {
                return true;
            }
        } else if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
            //ERROR級(jí)別的日志
            XLog.e("onConsoleMessage:" + consoleMessage.message()
                    + ",line = " + consoleMessage.lineNumber()
                    + ",sourceId = " + consoleMessage.sourceId()
                    + ",messageLevel = " + consoleMessage.messageLevel());
        } else {
            XLog.w("onConsoleMessage:" + consoleMessage.message()
                    + ",line = " + consoleMessage.lineNumber()
                    + ",sourceId = " + consoleMessage.sourceId()
                    + ",messageLevel = " + consoleMessage.messageLevel());
        }
        return super.onConsoleMessage(consoleMessage);
    }
}

這里的mWebViewPage會(huì)調(diào)用到XJSBridgeImpl的handleLogFromJS方法辱士,在這里針對(duì)JSBridge進(jìn)行解析泪掀,比如:


public class XJSBridgeImpl {

    private WebView mWebView;

    public XJSBridgeImpl(WebView view) {
        mWebView = view;
    }

    private static String XJSBRIDGE_HEADER = "____xbridge____:";

    public boolean handleLogFromJS(String log) {
        if (log != null && log.startsWith(XJSBRIDGE_HEADER)) {
            String msg = log.substring(XJSBRIDGE_HEADER.length());
            XLog.d("msg:" + msg);
            return handleMsgFromJS(msg);
        }
        return false;
    }

    private boolean dispatch(String func, final String seqId, String param) {

        if (func.equals("nativeMethod")) {
            //模擬js bridge的native實(shí)現(xiàn)
            Toast.makeText(mWebView.getContext(), "Hello XJSBridge!I am in native.", Toast.LENGTH_SHORT).show();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    XLog.d("hello, I am native method...");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    JSONObject mockRes = new JSONObject();
                    try {
                        mockRes.put("bridge", XJSBridgeImpl.class.getName());
                        mockRes.put("data", "I am from native");
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    invokeJS(seqId, mockRes);
                }
            }).start();
        }
        return false;
    }


    private void invokeJS(String seqId, JSONObject res) {
        final JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("seqId", seqId);
            jsonObject.put("msgType", "jsCallback");
            jsonObject.put("param", res);
        } catch (JSONException e) {
            XLog.e(e);
        }
        mWebView.post(new Runnable() {
            @Override
            public void run() {
                //需要在主線程調(diào)用
                mWebView.evaluateJavascript(String.format("javascript: window.XJSBridge.nativeInvokeJS('%s')", jsonObject.toString()), new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String s) {
                        XLog.d("onReceiveValue:s = " + s);
                    }
                });
            }
        });
    }


    private boolean handleMsgFromJS(String message) {
        try {
            JSONObject jsonObject = new JSONObject(message);
            String func = jsonObject.getString("func");
            String seqId = jsonObject.getString("seqId");
            String param = jsonObject.getString("param");
            dispatch(func, seqId, param);
            return true;
        } catch (JSONException e) {
            XLog.e(e);
        }
        return false;
    }

    public void changeH5Background() {
        mWebView.evaluateJavascript(String.format("javascript: changeColor('%s')", "#f00"), new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String s) {
                XLog.d("changeH5Background:onReceiveValue:s = " + s);
            }
        });
    }

    public void addObjectForJS(){
        mWebView.addJavascriptInterface(new NativeLog(),"nativeLog");
    }
}
  • native如何回調(diào)給前端?

android通過(guò)mWebView.evaluateJavascript或者mWebView.loadUrl的方式調(diào)用JS颂碘,在android 4.4以上推薦使用evaluateJavascript异赫,loadUrl會(huì)導(dǎo)致頁(yè)面刷新,鍵盤收回等問(wèn)題头岔。

  • native如何向前端注入對(duì)象塔拳?

通過(guò)addJavascriptInterface添加對(duì)象,比如:


mWebView.addJavascriptInterface(new NativeLog(),"nativeLog");

其中NativeLog的實(shí)現(xiàn)如下:


public class NativeLog {

    @JavascriptInterface
    public void print(String log) {
        XLog.d("nativeLog:" + log);
    }
}

這里面的方法print峡竣,需要添加注解@JavascriptInterface靠抑,JS才能訪問(wèn)到。

添加之后澎胡,前端可以通過(guò)如下方式調(diào)用:


document.getElementById('a2').addEventListener('click', function () {
    console.log('clicked....');
    if (nativeLog) {
        nativeLog.print('content from js');
    }
});

2.如果實(shí)現(xiàn)url的攔截和重定向孕荠?

WebView的WebViewClient中痹籍,可以通過(guò)重寫shouldOverrideUrlLoading實(shí)現(xiàn)邻奠,如果需要攔截,可以return true;比如:


public class XWebViewClient extends WebViewClient {

    public XWebViewClient() {
        super();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        XLog.d("shouldOverrideUrlLoading2...");
        if (shouldOverrideUrlLoadingInternal(view, request.getUrl().toString())) {
            return true;
        }
        return super.shouldOverrideUrlLoading(view, request);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        XLog.d("shouldOverrideUrlLoading1...");
        if (shouldOverrideUrlLoadingInternal(view, url)) {
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }

    private boolean shouldOverrideUrlLoadingInternal(WebView view, String url) {
        if (url != null && url.startsWith("https://www.baidu.com")) {
            view.loadUrl("file:///android_asset/xbridge_demo.html");
            return true;
        }
        if (url != null && url.startsWith("xscheme://native_page")) {
            //根據(jù)前端的href進(jìn)行scheme攔截苗沧,跳轉(zhuǎn)到native的頁(yè)面
            view.getContext().startActivity(new Intent(view.getContext(), TestActivity.class));
            return true;
        }
        if(url != null && url.startsWith("")){

        }
        return false;
    }
    ...
}
  • 攔截url
    這里可以實(shí)現(xiàn)很多功能戚宦,比如頁(yè)面跳轉(zhuǎn)到外部鏈接https://www.baidu.com時(shí)个曙,可以攔截掉,然后讓W(xué)ebView加載本地的某個(gè)頁(yè)面受楼,或者是錯(cuò)誤頁(yè)面垦搬。
  • 擴(kuò)展JSBridge

也可以通過(guò)shouldOverrideUrlLoading實(shí)現(xiàn)JSBridge,比如攔截到url是xscheme://native_page時(shí)艳汽,容器跳轉(zhuǎn)到某一個(gè)native頁(yè)面猴贰,達(dá)到啟動(dòng)native頁(yè)面的目的,也可以擴(kuò)展其他功能河狐。前端可以通過(guò)如下方式:


<a href="xscheme://native_page?name=test_activity" class="btn read" id="a3">通過(guò)href的scheme調(diào)用native</a><br><br><br>
  • 處理指定的某些url

比如這里可以判斷url如果是以.apk結(jié)尾的時(shí)候米绕,啟動(dòng)系統(tǒng)的瀏覽器進(jìn)行下載瑟捣;攔截到某些scheme時(shí),喚起對(duì)應(yīng)的app等等栅干。

3.如何實(shí)現(xiàn)資源的攔截迈套?

可以對(duì)WebViewClient的shouldInterceptRequest方法進(jìn)行Override,然后根據(jù)資源請(qǐng)求進(jìn)行攔截碱鳞。


public class XWebViewClient extends WebViewClient {

     ...
     
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        XLog.d("shouldInterceptRequest1:" + url + "");
        WebResourceResponse webResourceResponse = interceptRequestInternal(view, url);
        if (webResourceResponse != null) {
            return webResourceResponse;
        }
        return super.shouldInterceptRequest(view, url);
    }


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        XLog.d("shouldInterceptRequest2:" + request.getUrl() + "");
        WebResourceResponse webResourceResponse = interceptRequestInternal(view, request.getUrl().toString());
        if (webResourceResponse != null) {
            return webResourceResponse;
        }
        return super.shouldInterceptRequest(view, request);
    }

    private WebResourceResponse interceptRequestInternal(WebView view, String url) {
        if (url != null &&
                (url.endsWith("png")
                        || url.endsWith("jpg")
                        || url.endsWith("gif")
                        || url.endsWith("JPEG"))) {
            WebResourceResponse webResourceResponse = null;
            try {
                webResourceResponse = new WebResourceResponse("image/jpeg", "UTF-8", getStream());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                XLog.e(e.getMessage());
            }
            return webResourceResponse;
        }
        return null;
    }

    private InputStream getStream() throws FileNotFoundException {

        FileInputStream fileInputStream = new FileInputStream(new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/xxx.jpg"));

        return fileInputStream;
    }
    
    ...
}

比如上述代碼桑李,是對(duì)H5頁(yè)面的圖片資源進(jìn)行攔截,把H5頁(yè)面的圖片資源換成本地的圖片窿给。如上代碼并沒(méi)有實(shí)際意義贵白,只是舉例說(shuō)明對(duì)資源的攔截。

4.如何實(shí)現(xiàn)資源的離線崩泡?

上述步驟中根據(jù)攔截shouldInterceptRequest方法戒洼,構(gòu)建對(duì)應(yīng)的WebResourceResponse,其實(shí)H5資源離線的原理正是如此允华。這里需要注意的是CORS問(wèn)題,攔截之后構(gòu)建的資源URI可能和當(dāng)前H5頁(yè)面的域名不同寥掐,因此需要添加Access-Control-Allow-Origin靴寂;

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private WebResourceResponse interceptRequestInternal(WebView view, String url) {
        if (url != null &&
                (url.endsWith("png")
                        || url.endsWith("jpg")
                        || url.endsWith("gif")
                        || url.endsWith("JPEG"))) {
            WebResourceResponse webResourceResponse = null;
            try {
                webResourceResponse = new WebResourceResponse("image/jpeg", "UTF-8", getStream());
                Map<String, String> header = new HashMap<>();
                header.put("Access-Control-Allow-Origin", "**");
                webResourceResponse.setResponseHeaders(header);
            } catch (Exception e) {
                e.printStackTrace();
                XLog.e(e.getMessage());
            }
            return webResourceResponse;
        }
        return null;
    }

資源離線的原理就是在H5頁(yè)面需要訪問(wèn)的在線資源,在H5頁(yè)面打開(kāi)之前召耘,提前下載好或者內(nèi)置到apk中百炬,等到H5頁(yè)面加載時(shí),通過(guò)shouldInterceptRequest攔截資源的加載污它,然后將已經(jīng)離線的資源封裝成WebResourceResponse對(duì)象剖踊,提高H5頁(yè)面的加載速度。

5.如何獲取JS打印的日志衫贬?

之前已經(jīng)講過(guò)德澈,可以通過(guò)WebChromeClient.onConsoleMessage方法獲取。但是固惯,如果部分機(jī)型沒(méi)有回調(diào)梆造,可以通過(guò)addJavascriptInterface的方式,給H5注入一個(gè)Console對(duì)象葬毫,并實(shí)現(xiàn)Console對(duì)象的log等方法即可镇辉。

6.如何實(shí)現(xiàn)接口的預(yù)取贴捡?

對(duì)H5頁(yè)面的網(wǎng)絡(luò)請(qǐng)求提前預(yù)取忽肛,可以提升H5頁(yè)面的性能。其原理是在啟動(dòng)H5頁(yè)面的Activity時(shí)烂斋,就時(shí)開(kāi)始進(jìn)行網(wǎng)絡(luò)請(qǐng)求屹逛,把請(qǐng)求的結(jié)果進(jìn)行緩存础废。當(dāng)頁(yè)面加載時(shí),攔截頁(yè)面的網(wǎng)絡(luò)請(qǐng)求煎源,然后將緩存的結(jié)果返回給H5色迂,完成接口預(yù)期,加速頁(yè)面的加載手销。

7.WebView如何和native的Cookie同步歇僧?

很多app的登錄頁(yè)面是native實(shí)現(xiàn)的,登錄成功之后锋拖,希望H5頁(yè)面能夠在加載時(shí)诈悍,共用native的Cookie,此時(shí)如何做同步呢兽埃?Android中可以使用CookieManager

    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);

    List<Cookie> cookies = getCookiesFromLogin();//從業(yè)務(wù)中獲取

    cookieManager.removeAllCookie();

    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookie.getName().contains("session")){
                String cookieString = cookie.getName() + "=" + cookie.getValue() + "; Domain=" + cookie.getDomain();
                cookieManager.setCookie(cookie.getDomain(), cookieString);
            }
        }
    }
    ...
    //加載H5頁(yè)面
    webView.loadUrl(url);

當(dāng)然侥钳,android也可以從CookieManager獲取WebView保存的Cookie,比如H5頁(yè)面HTTP回應(yīng)的頭信息里面柄错,放置的Set-Cookie信息舷夺,WebView會(huì)保存在CookieManager中.

8.如何注入JSBridge?

為了方便的管控JSBridge的內(nèi)容售貌,方便后續(xù)統(tǒng)一升級(jí)给猾,我們希望JSBridge的js通過(guò)攔截注入的方式加載,而不是直接寫到前端代碼中颂跨,那我們?nèi)绾巫瞿兀?/p>

  • 1.將JSBridge的內(nèi)容抽離成一個(gè)xbridge.js文件敢伸,存放到assets目錄中
  • 2.做一個(gè)虛擬域名,比如demo中的https://www.baidu.com/xbridge.js
  • 3.在H5的入口html中恒削,添加script標(biāo)簽池颈,比如:
<script src="https://www.baidu.com/xbridge.js"></script>
  • 4.在webview的WebViewClient中攔截虛擬url,然后將本地assets目錄中的jsbridge注入進(jìn)去即可钓丰。
...

    private WebResourceResponse interceptRequestInternal(WebView view, String url) {
        if (url != null && url.equals("https://www.baidu.com/xbridge.js")) {
            XLog.d("start inject js bridge...");
            WebResourceResponse webResourceResponse = null;
            try {
                InputStream inputStream = view.getContext().getAssets().open("xbridge.js");
                webResourceResponse = new WebResourceResponse("text/html", "UTF-8", inputStream);
                Map<String, String> header = new HashMap<>();
                header.put("Access-Control-Allow-Origin", url);
                webResourceResponse.setResponseHeaders(header);
            } catch (Exception e) {
                e.printStackTrace();
                XLog.e(e.getMessage());
            }
            return webResourceResponse;
        }
        return null;
    }
...

9.如何喚起其他APP躯砰?

大家都知道,每個(gè)app有一個(gè)scheme携丁,比如口碑的scheme為alipays://platformapi/startApp?appId=20000001弃揽;具體每個(gè)app的scheme是什么,我們可以反編譯對(duì)應(yīng)點(diǎn)apk则北,查看AndroidManifest文件的如下代碼片段:

<activity ....>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="demoscheme{這里是scheme的值}" />
    </intent-filter>
</activity>

其中android:scheme的值即為scheme

那我們?nèi)绾螁酒餫pp呢矿微?

  • 1.前端通過(guò)window.location.href傳入scheme,比如:
...
document.getElementById("a9").addEventListener("click", function () {
    const scheme = "koubei://platformapi/startApp?appId=20000001";
    console.log("scheme = " + scheme);
    window.location.href = scheme;
});
...

以上是喚起口碑a(chǎn)pp的demo

  • 2.在webview的WebViewClient中通過(guò)shouldOverrideUrlLoading攔截scheme尚揣,然后喚起app
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        XLog.d("shouldOverrideUrlLoading1...");
        if (shouldOverrideUrlLoadingInternal(view, url)) {
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
    ...
    private boolean shouldOverrideUrlLoadingInternal(WebView view, String url) {
        //非http的scheme涌矢,喚起對(duì)應(yīng)scheme的app
        if (url != null && !url.startsWith("http")) {
            Uri uri = Uri.parse(url);
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_VIEW);
            intent.setData(uri);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            view.getContext().startActivity(intent);
            return true;
        }
        ...
        return false;
    }

10.前端如何下載apk?

針對(duì)android的情況快骗,可以有如下方式:

  • 1.通過(guò)JSBridge娜庇,由native實(shí)現(xiàn)一個(gè)文件下載和安裝的功能塔次,前端只需要調(diào)用即可
  • 2.通過(guò)window.location.href,客戶端對(duì)其進(jìn)行攔截名秀,同上励负;
  • 3.使用iframe.src中添加js標(biāo)本的方式,可以避免打開(kāi)新的頁(yè)面匕得;
  • 4.使用form表單的action字段继榆,通過(guò)submit也可以

11.前端根據(jù)ua判斷iOS/Android以及版本

  • 1.前端直接可以通過(guò)ua即可判斷,比如:

isAndroidOrIOS() {
    var u = navigator.userAgent;
    console.log("ua = " + u);
    var isAndroid = u.indexOf("Android") > -1 || u.indexOf("Adr") > -1; //android終端
    var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios終端
    if (isAndroid) {
      return "android";
    }
    if (isiOS) {
      return "ios";
    }
    return false;
 }
  • 2.自定義UA

如果端上想把自己的版本號(hào)等信息汁掠,也想通過(guò)UA傳給前端略吨,可以直接設(shè)置webview的settings,比如:


public class XWebView extends WebView {
   
    @Override
    public WebSettings getSettings() {

        WebSettings webSettings = super.getSettings();

        String ua = webSettings.getUserAgentString();

        try {
            PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
            String version = pInfo.versionName;
            String appName = getContext().getString(pInfo.applicationInfo.labelRes);
            String newUaWithVersion = ua + " AndroidApp_" + appName + "/" + version;
            webSettings.setUserAgentString(newUaWithVersion);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return webSettings;
    }
}

當(dāng)然考阱,也可以直接調(diào)用webview的setUserAgentString進(jìn)行重置翠忠;比如設(shè)置后的UA為:

Mozilla/5.0 (Linux; Android 6.0.1; MuMu Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/52.0.2743.100 Mobile Safari/537.36 AndroidApp_XWebViewDemo/1.0

12.離線包設(shè)計(jì)

  • 離線壓縮包設(shè)計(jì)

以一個(gè)create-react-app的H5項(xiàng)目為例,執(zhí)行npm build進(jìn)行編譯乞榨,我們將生成的build目錄下的文件進(jìn)行簡(jiǎn)單的壓縮秽之,壓縮成zip包。為了方便后續(xù)離線包的管理吃既,我們?cè)陔x線包中添加一個(gè)我們約定的包信息描述文件政溃,比如命名為pkg-desc.json,當(dāng)然态秧,我們也可以復(fù)用manifest.json這個(gè)文件,在json中扼鞋,添加對(duì)離線包的描述申鱼,比如:

{
  "launchParams": {
    "indexUrl": "/index.html",
    "transparentTitle": "true"
  },
  "vHost": "https://www.taobao.com"
}

這里簡(jiǎn)單列幾個(gè)參數(shù),比如vHost云头,即是我們?cè)O(shè)計(jì)的虛擬域名捐友,我們?cè)O(shè)計(jì)的目的,是在webview加載vHost+indexUrl時(shí)溃槐,自動(dòng)加載當(dāng)前離線包目錄里的index.html匣砖;壓縮成離線包之前的目錄為:

.
├── asset-manifest.json
├── favicon.ico
├── index.html
├── manifest.json
├── robots.txt
└── static
    ├── css
    │   ├── main.5b343dc0.chunk.css
    │   └── main.5b343dc0.chunk.css.map
    └── js
        ├── 2.94eb2e42.chunk.js
        ├── 2.94eb2e42.chunk.js.LICENSE.txt
        ├── 2.94eb2e42.chunk.js.map
        ├── 3.df4689f7.chunk.js
        ├── 3.df4689f7.chunk.js.map
        ├── main.83926041.chunk.js
        ├── main.83926041.chunk.js.map
        ├── runtime-main.ebe92296.js
        └── runtime-main.ebe92296.js.map

這些文件均是create-react-app編譯(npm run build)之后生成的文件,其中manifest.json保存了我們自定義的信息昏滴。

  • 離線包的解壓和加載

我們將上述設(shè)計(jì)的壓縮包猴鲫,壓縮成zip文件,在app啟動(dòng)的時(shí)候谣殊,將離線包的內(nèi)容加載到內(nèi)存中拂共,并以Map的形式保存。我們以存放到assets目錄下的離線包為例姻几,對(duì)其進(jìn)行加載的部分核心代碼:


public class OfflinePkgManager {

    private static final String PKG_DESC_JSON = "manifest.json";
    private static final String DEFAULT_INDEX_URL = "/index.html";

    private static OfflinePkgManager mOfflinePkgManager = null;
    //保存離線包的內(nèi)容:key為url的路徑宜狐,byte為對(duì)應(yīng)的緩存內(nèi)容
    private Map<String, byte[]> offlinePkg = new ConcurrentHashMap<>();

    //保存離線包的地址以及啟動(dòng)參數(shù)等信息
    private Map<String, PkgDescModel> vHostUrlInfoMap = new ConcurrentHashMap<>();

    private OfflinePkgManager() {

    }

    public synchronized static OfflinePkgManager getInstance() {
        if (mOfflinePkgManager == null) {
            mOfflinePkgManager = new OfflinePkgManager();
        }
        return mOfflinePkgManager;
    }

    /**
     * 加載assets中的離線包
     *
     * @param context
     */
    public void loadAssetsPkg(Context context) {
        try {
            InputStream inputStream = context.getAssets().open("react_zhihu_demo_offline_pkg.zip");
            Map<String, byte[]> relativePathByteMap = new HashMap<>();
            XFileUtils.loadZipFile(inputStream, relativePathByteMap);
            addPackageInfo(relativePathByteMap);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * 添加離線包信息
     *
     * @param relativePathByteMap
     */
    private void addPackageInfo(Map<String, byte[]> relativePathByteMap) {
        //獲取離線包的描述信息
        byte[] descByte = relativePathByteMap.get(PKG_DESC_JSON);
        if (descByte != null) {
            String jsonStr = new String(descByte);
            PkgDescModel pkgDesc = JSON.parseObject(jsonStr, PkgDescModel.class);
            String vHost = pkgDesc.getvHost();
            String indexUrl = getIndexUrl(pkgDesc);
            String vHostUrl = vHost + indexUrl;
            //保存離線包信息
            vHostUrlInfoMap.put(vHostUrl, pkgDesc);
            for (Map.Entry<String, byte[]> entry : relativePathByteMap.entrySet()) {
                String fullUrl = vHost + "/" + entry.getKey();
                //保存離線包內(nèi)容
                offlinePkg.put(fullUrl, entry.getValue());
            }
            XLog.d("add packageInfo success:" + vHostUrl);
        }
    }

    /**
     * 獲取入口html的url
     *
     * @param pkgDesc
     * @return
     */
    private String getIndexUrl(PkgDescModel pkgDesc) {
        if (pkgDesc != null
                && pkgDesc.getLaunchParams() != null
                && pkgDesc.getLaunchParams().getIndexUrl() != null) {
            return pkgDesc.getLaunchParams().getIndexUrl();
        }
        //默認(rèn)為/index.html
        return DEFAULT_INDEX_URL;
    }

    public byte[] getOfflineContent(String url) {
        byte[] offlineContent = offlinePkg.get(url);
        if (offlineContent != null) {
            XLog.d(String.format("url:%s  load from cache", url));
        }
        return offlineContent;
    }

}

加載zip文件的方法實(shí)現(xiàn):

//將ZIP文件加載內(nèi)存中势告,以路徑和byte[]的key/value形式存儲(chǔ)
public class XFileUtils {
    /**
     * 將壓縮包解壓到內(nèi)存中
     *
     * @param is
     * @param relativePathByteMap
     * @return
     */
    public static boolean loadZipFile(InputStream is, Map<String, byte[]> relativePathByteMap) {
        ZipInputStream zis;
        try {
            String filename;
            zis = new ZipInputStream(new BufferedInputStream(is));
            ZipEntry zipEntry;
            int count;
            byte[] buffer = new byte[1024];
            while ((zipEntry = zis.getNextEntry()) != null) {
                filename = zipEntry.getName();
                if (zipEntry.isDirectory() || TextUtils.isEmpty(filename)) {
                    continue;
                }
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                while ((count = zis.read(buffer)) != -1) {
                    byteArrayOutputStream.write(buffer, 0, count);
                }
                byte[] data = byteArrayOutputStream.toByteArray();
                String pureFilename = filename.substring(filename.indexOf('/') + 1);
                //保持相對(duì)路徑的文件名稱以及對(duì)應(yīng)的數(shù)據(jù)
                relativePathByteMap.put(pureFilename, data);
                byteArrayOutputStream.close();
                XLog.d("unzip filename = " + pureFilename);
                zis.closeEntry();
            }
            zis.close();
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

}

  • 虛擬域名與離線包的匹配

使用webview通過(guò)loadUrl加載虛擬域名的時(shí)候,webview通過(guò)shouldInterceptRequest攔截url抚恒,查找對(duì)應(yīng)的``

public class XWebViewClient extends WebViewClient {

    ...
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        WebResourceResponse webResourceResponse = interceptRequestInternal(view, url);
        if (webResourceResponse != null) {
            return webResourceResponse;
        }
        return super.shouldInterceptRequest(view, url);
    }


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        WebResourceResponse webResourceResponse = interceptRequestInternal(view, request.getUrl().toString());
        if (webResourceResponse != null) {
            return webResourceResponse;
        }
        return super.shouldInterceptRequest(view, request);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private WebResourceResponse interceptRequestInternal(WebView view, String url) {
        if (url != null) {
            WebResourceResponse webResourceResponse = null;
            try {
                //嘗試獲取離線內(nèi)容
                byte[] contentByte = OfflinePkgManager.getInstance().getOfflineContent(url);
                if (contentByte != null && contentByte.length > 0) {
                    InputStream inputStream = new ByteArrayInputStream(contentByte);
                    //構(gòu)造WebResourceResponse
                    webResourceResponse = new WebResourceResponse(getMimeType(url), "UTF-8", inputStream);
                    Map<String, String> header = new HashMap<>();
                    header.put("Access-Control-Allow-Origin", url);
                    webResourceResponse.setResponseHeaders(header);
                }
            } catch (Exception e) {
                e.printStackTrace();
                XLog.e(e.getMessage());
            }
            return webResourceResponse;
        }
        return null;
    }

    /**
     * 獲取mimeType:
     *
     * @param url
     * @return
     */
    private String getMimeType(String url) {
        try {
            String mimeType = null;
            String ext = MimeTypeMap.getFileExtensionFromUrl(url);
            if ("js".equalsIgnoreCase(ext)) {
                mimeType = "application/javascript";
            } else {
                mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
            }
            XLog.d("mimeType = " + mimeType);
            return mimeType;
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return "text/html";
    }

}

這一部分就是根據(jù)url查找離線包的內(nèi)容咱台,然后構(gòu)造WebResourceResponse以及對(duì)應(yīng)的MimeType;

  • url參數(shù)(魔術(shù)參數(shù))

manifest.json里面還可以添加一下默認(rèn)的參數(shù)俭驮,比如設(shè)置titlebar的參數(shù)回溺,這些參數(shù)可以在webview加載url之前生效。比如titlebar的背景表鳍,title等信息馅而,實(shí)現(xiàn)原理比較簡(jiǎn)單,這里不再贅述譬圣。

13.自定義錯(cuò)誤頁(yè)面

native可以根據(jù)WebView的報(bào)錯(cuò)信息瓮恭,自定義錯(cuò)誤頁(yè)面,可通過(guò)WebViewClient監(jiān)聽(tīng)報(bào)錯(cuò)回調(diào):

onReceivedError
onReceivedHttpError
onReceivedSslError

自定義的錯(cuò)誤頁(yè)面有如下幾種實(shí)現(xiàn)方式:

  • 1.H5實(shí)現(xiàn)一個(gè)默認(rèn)的錯(cuò)誤頁(yè)厘熟,打包到apk中屯蹦,直接使用webview加載。
  • 2.使用native實(shí)現(xiàn)一個(gè)layout绳姨,然后覆蓋在webview布局上方登澜,同時(shí)可以自定義一些功能。

14.頁(yè)面加載超時(shí)

自定義webview的頁(yè)面加載超時(shí)時(shí)間飘庄,可以通過(guò)onPageStartedonPageFinished進(jìn)行配合

public class XWebViewClient extends WebViewClient {

     private boolean loadTimeout;
    ...
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        startLoadTime = System.currentTimeMillis();
        XLog.d("onPageStarted:" + url);
        new Thread(new Runnable() {
            @Override
            public void run() {
                loadTimeout = true;
                try {
                    Thread.sleep(10000);
                } catch (Throwable t) {
                    t.printStackTrace();
                }
                if (loadTimeout) {
                    webViewPage.showErrorPage("TIMEOUT");
                }
            }
        }).start();
    }

    @Override
    public void onPageFinished(WebView view, String url) {
        loadTimeout = false;
        super.onPageFinished(view, url);
    }

15.對(duì)前端頁(yè)面的性能統(tǒng)計(jì)

為了更加準(zhǔn)確和詳細(xì)的獲取H5頁(yè)面的性能脑蠕,可以通過(guò)前端的performance實(shí)現(xiàn):

  • 前端代碼:
;(function (win) {
  if (!win.performance || !win.performance.timing) return {};
  var time = win.performance.timing;
  var timingResult = {};
  timingResult["重定向時(shí)間"] = (time.redirectEnd - time.redirectStart) / 1000;
  timingResult["DNS解析時(shí)間"] =
    (time.domainLookupEnd - time.domainLookupStart) / 1000;
  timingResult["TCP完成握手時(shí)間"] =
    (time.connectEnd - time.connectStart) / 1000;
  timingResult["HTTP請(qǐng)求響應(yīng)完成時(shí)間"] =
    (time.responseEnd - time.requestStart) / 1000;
  timingResult["DOM開(kāi)始加載前所花費(fèi)時(shí)間"] =
    (time.responseEnd - time.navigationStart) / 1000;
  timingResult["DOM加載完成時(shí)間"] = (time.domComplete - time.domLoading) / 1000;
  timingResult["DOM結(jié)構(gòu)解析完成時(shí)間"] =
    (time.domInteractive - time.domLoading) / 1000;
  timingResult["腳本加載時(shí)間"] =
    (time.domContentLoadedEventEnd - time.domContentLoadedEventStart) / 1000;
  timingResult["onload事件時(shí)間"] =
    (time.loadEventEnd - time.loadEventStart) / 1000;
  timingResult["頁(yè)面完全加載時(shí)間"] =
    timingResult["重定向時(shí)間"] +
    timingResult["DNS解析時(shí)間"] +
    timingResult["TCP完成握手時(shí)間"] +
    timingResult["HTTP請(qǐng)求響應(yīng)完成時(shí)間"] +
    timingResult["DOM結(jié)構(gòu)解析完成時(shí)間"] +
    timingResult["DOM加載完成時(shí)間"];
  return { result: timingResult };
})(this);

  • webview在onPageFinised加載完成時(shí)執(zhí)行統(tǒng)計(jì)代碼
public class XWebViewClient extends WebViewClient {

    ...
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        String jsCode = "(function(win){if(!win.performance||!win.performance.timing){return{}}var time=win.performance.timing;var timingResult={};timingResult[\"重定向時(shí)間\"]=(time.redirectEnd-time.redirectStart)/1000;timingResult[\"DNS解析時(shí)間\"]=(time.domainLookupEnd-time.domainLookupStart)/1000;timingResult[\"TCP完成握手時(shí)間\"]=(time.connectEnd-time.connectStart)/1000;timingResult[\"HTTP請(qǐng)求響應(yīng)完成時(shí)間\"]=(time.responseEnd-time.requestStart)/1000;timingResult[\"DOM開(kāi)始加載前所花費(fèi)時(shí)間\"]=(time.responseEnd-time.navigationStart)/1000;timingResult[\"DOM加載完成時(shí)間\"]=(time.domComplete-time.domLoading)/1000;timingResult[\"DOM結(jié)構(gòu)解析完成時(shí)間\"]=(time.domInteractive-time.domLoading)/1000;timingResult[\"腳本加載時(shí)間\"]=(time.domContentLoadedEventEnd-time.domContentLoadedEventStart)/1000;timingResult[\"onload事件時(shí)間\"]=(time.loadEventEnd-time.loadEventStart)/1000;timingResult[\"頁(yè)面完全加載時(shí)間\"]=timingResult[\"重定向時(shí)間\"]+timingResult[\"DNS解析時(shí)間\"]+timingResult[\"TCP完成握手時(shí)間\"]+timingResult[\"HTTP請(qǐng)求響應(yīng)完成時(shí)間\"]+timingResult[\"DOM結(jié)構(gòu)解析完成時(shí)間\"]+timingResult[\"DOM加載完成時(shí)間\"];return{result:timingResult}})(this);";
        view.evaluateJavascript(jsCode, new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String s) {
                XLog.d("onPageFinished:JSResult = " + s);
                //TODO 對(duì)性能數(shù)據(jù)進(jìn)行上報(bào)
            }
        });
    }
    ...
}    
  • 統(tǒng)計(jì)結(jié)果示例

https://www.baidu.com為例:


{
    "result":{
        "DNS解析時(shí)間":0.013,
        "DOM加載完成時(shí)間":1.586,
        "DOM開(kāi)始加載前所花費(fèi)時(shí)間":0.312,
        "DOM結(jié)構(gòu)解析完成時(shí)間":0.142,
        "HTTP請(qǐng)求響應(yīng)完成時(shí)間":0.118,
        "TCP完成握手時(shí)間":0.123,
        "onload事件時(shí)間":0.001,
        "腳本加載時(shí)間":0.001,
        "重定向時(shí)間":0,
        "頁(yè)面完全加載時(shí)間":1.982
    }
}

16.H5頁(yè)面的任務(wù)棧多開(kāi)

類似微信小程序或者支付寶小程序,我們可以將H5頁(yè)面在一個(gè)獨(dú)立的進(jìn)程中加載.

  • 多進(jìn)程的優(yōu)勢(shì):

    • 隔離:與主進(jìn)程是進(jìn)程級(jí)別的隔離跪削,不影響主進(jìn)程的Crash
    • 增加app的可用內(nèi)存
  • 多進(jìn)程的劣勢(shì):

    • 數(shù)據(jù)共享問(wèn)題
    • 不必要的初始化

對(duì)于一個(gè)H5頁(yè)面谴仙,我們需要將WebView所在的Activity設(shè)置為獨(dú)立進(jìn)程。同時(shí)碾盐,為了支持多個(gè)H5頁(yè)面晃跺,我們預(yù)注冊(cè)5個(gè)Activity,比如:


public class XWebViewActivity extends Activity {

    private WebViewPage mWebViewPage = null;

    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Process.setThreadPriority(-20);
        setContentView(R.layout.activity_webview);
        ...
        setTaskDesc();
    }

    private void setTaskDesc() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            setTaskDescription(new ActivityManager.TaskDescription("H5進(jìn)程", R.drawable.h5_icon));
        } else {
            Bitmap iconBmp = BitmapFactory.decodeResource(getResources(), R.drawable.h5_icon); // 這里應(yīng)該是小程序圖標(biāo)的bitmap
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                setTaskDescription(new ActivityManager.TaskDescription("H5進(jìn)程", iconBmp));
            }
        }
    }

    public static class H5Activity1 extends XWebViewActivity {

    }

    public static class H5Activity2 extends XWebViewActivity {

    }

    public static class H5Activity3 extends XWebViewActivity {

    }

    public static class H5Activity4 extends XWebViewActivity {

    }

    public static class H5Activity5 extends XWebViewActivity {

    }

}

可以通過(guò)setTaskDescription設(shè)置task的名稱毫玖,比如以小程序的業(yè)務(wù)名稱命名掀虎。在AndroidManifest.xml中配置多進(jìn)程以及task的屬性,以如下xml為例:主要是設(shè)置android:process,launchMode,taskAffinity三個(gè)屬性付枫。

  • xml的設(shè)置
<activity
    android:name=".activity.XWebViewActivity"
    android:label="@string/app_name"
    android:launchMode="singleTask"
    android:process=":h5container"
    android:taskAffinity=":lite1">
    <intent-filter>
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="xwebview" />
    </intent-filter>
</activity>

<activity
    android:name=".activity.XWebViewActivity$H5Activity1"
    android:launchMode="singleTask"
    android:process=":activity1"
    android:taskAffinity=":activity1" />

<activity
    android:name=".activity.XWebViewActivity$H5Activity2"
    android:launchMode="singleTask"
    android:process=":activity2"
    android:taskAffinity=":activity2" />

<activity
    android:name=".activity.XWebViewActivity$H5Activity3"
    android:launchMode="singleTask"
    android:process=":activity3"
    android:taskAffinity=":activity3" />

<activity
    android:name=".activity.XWebViewActivity$H5Activity4"
    android:launchMode="singleTask"
    android:process=":activity4"
    android:taskAffinity=":activity4" />

<activity
    android:name=".activity.XWebViewActivity$H5Activity5"
    android:launchMode="singleTask"
    android:process=":activity5"
    android:taskAffinity=":activity5" />
  • 啟動(dòng)H5Activity以及復(fù)用

最多打開(kāi)5個(gè)獨(dú)立進(jìn)程的H5頁(yè)面烹玉,當(dāng)打開(kāi)第6個(gè)時(shí),會(huì)覆蓋掉第1個(gè)打開(kāi)的進(jìn)程阐滩。如下是部分核心代碼:

public class RouterManager {

    private static Activity mMainActivity;
    private static int index = 0;

    private static List<String> availableActivityList = new ArrayList<>();

    private static final String[] H5_ACTIVITY_ARR = new String[]{
            XWebViewActivity.H5Activity1.class.getName(),
            XWebViewActivity.H5Activity2.class.getName(),
            XWebViewActivity.H5Activity3.class.getName(),
            XWebViewActivity.H5Activity4.class.getName(),
            XWebViewActivity.H5Activity5.class.getName(),
    };

    public static void init(Activity activity) {
        index = 0;
        mMainActivity = activity;
        availableActivityList.addAll(Arrays.asList(H5_ACTIVITY_ARR));
    }

    public static void openH5Activity(String url) {
        String activityClazz = availableActivityList.get(index % (availableActivityList.size() - 1));
        try {
            Class<?> c = Class.forName(activityClazz);
            Intent intent = new Intent(mMainActivity, c);
            intent.putExtra("url", url.trim());
            mMainActivity.startActivity(intent);
            index++;
        } catch (Exception ignored) {
        }
    }
}
  • 多進(jìn)程的“后遺癥”

多進(jìn)程下春霍,Application會(huì)初始化多次,我們需要排除非必要的初始化叶眉,以及合理安排APP主進(jìn)程和H5進(jìn)行的分工址儒,然后通過(guò)進(jìn)程間通信(比如AIDL)的方式解決主進(jìn)程和子進(jìn)程之間的通信問(wèn)題芹枷。

17.使用MessageChannel通信

MessageChannel,并不是一個(gè)新的概念莲趣,它是一個(gè)Web API鸳慈。它允許我們創(chuàng)建一個(gè)新的MessageChannel(消息通道)然后通過(guò)這個(gè)消息通道的兩個(gè)端口(MessagePort)進(jìn)行傳遞數(shù)據(jù)。比如喧伞,前端可以通過(guò)MessageChannel的

var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function (event) {
  console.log("recv msg from port2:" + event.data);
};
port2.onmessage = function (event) {
  console.log("recv msg from port1:" + event.data);
};

port1.postMessage("send to port2");
port2.postMessage("send to port1");

運(yùn)行之后走芋,可以看到日志如下:

recv msg from port1:send to port2
recv msg from port2:send to port1

以上只是前端方面的一個(gè)簡(jiǎn)單Demo

  • android webview與H5之間通過(guò)MessageChannel通信

我們知道,我們可以通過(guò)MessageChannel的兩個(gè)MessagePort進(jìn)行通信潘鲫。那native和H5的通信的思路是翁逞,native通過(guò)WebMessage把其中一個(gè)端口發(fā)送給H5,H5保存端口的引用溉仑,然后通過(guò)端口postMessage另一個(gè)port挖函;

android端代碼:

import android.annotation.TargetApi;
import android.net.Uri;
import android.os.Build;
import android.webkit.WebMessage;
import android.webkit.WebMessagePort;
import android.webkit.WebView;

import com.mochuan.github.log.XLog;

/**
 * @Author Zheng Haibo
 * @Blog github.com/nuptboyzhb
 * @Company Alibaba Group
 * @Description WebView的MessageChannel
 */
public class XMessageChannel {

    private static final String TAG = "XMessageChannel";

    private WebMessagePort nativePort = null;
    private WebMessagePort h5Port = null;

    @TargetApi(Build.VERSION_CODES.M)
    public void init(WebView webView) {
        final WebMessagePort[] channel = webView.createWebMessageChannel();

        //供native使用的port
        nativePort = channel[0];
        //供h5使用的port
        h5Port = channel[1];
        //監(jiān)聽(tīng)從h5Port中發(fā)送過(guò)來(lái)的消息
        nativePort.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
            @Override
            public void onMessage(WebMessagePort port, WebMessage message) {
                XLog.d(TAG, ":onMessage:" + message.getData());
                postMessageToH5("hello from native:" + this.getClass().getName());
            }
        });
        //發(fā)送webmessage,把h5Port發(fā)送給H5頁(yè)面
        XLog.d(TAG, "start postWebMessage to transfer port");
        WebMessage webMessage = new WebMessage("__init_port__", new WebMessagePort[]{h5Port});
        webView.postWebMessage(webMessage, Uri.EMPTY);
    }

    /**
     * 通過(guò)port浊竟,向H5發(fā)送webMessage
     *
     * @param msg
     */
    @TargetApi(Build.VERSION_CODES.M)
    private void postMessageToH5(String msg) {
        nativePort.postMessage(new WebMessage(msg));
    }
    
}

不過(guò)怨喘,MessageChannel需要在WebView的onPageFinished以后才能創(chuàng)建,時(shí)機(jī)相對(duì)較晚振定。也即是在WebViewClient的onPageFinished回調(diào)中必怜,調(diào)用init方法。首先通過(guò)webView.createWebMessageChannel創(chuàng)建端口后频,將其中一個(gè)供native使用梳庆,并設(shè)置消息監(jiān)聽(tīng)回調(diào),另外一個(gè)則通過(guò)webView.postWebMessage卑惜,將內(nèi)容為__init_port__的WebMessage發(fā)送給前端膏执,消息中攜帶了WebMessagePort端口信息。其中``init_port`是我們自定義的協(xié)議残揉,供前端判斷。

接下來(lái)芋浮,看前端部分的代碼實(shí)現(xiàn)抱环,前端通過(guò)window.addEventListener,監(jiān)聽(tīng)所有的message消息纸巷。然后判斷messageEvent的data部分是否是__init_port__镇草,如果是的話,就從messageEvent中獲取到native傳過(guò)來(lái)的port對(duì)象瘤旨,將其掛載到window上梯啤,比如window.__my_port__,并設(shè)置onmessage消息監(jiān)聽(tīng)存哲,用于監(jiān)聽(tīng)android發(fā)送過(guò)來(lái)的消息因宇。另外七婴,我們也可以通過(guò)window.__my_port__向native發(fā)送消息。

  document.getElementById("a10").addEventListener("click", function () {
    sendMessageToNative();
  });

  function sendMessageToNative() {
    if (window.__my_port__) {
      window.__my_port__.postMessage("h5 test message");
    }
  }

  window.addEventListener("message", receiverMessage, false);

  function receiverMessage(messageEvent) {
    console.log("onmessage...", JSON.stringify(messageEvent.data));
    if (messageEvent.data === "__init_port__") {
      //在window上掛載port對(duì)象察滑,將native發(fā)過(guò)來(lái)的h5Port引用保存起來(lái)
      window.__my_port__ = messageEvent.ports[0];
      //設(shè)置消息
      window.__my_port__.onmessage = function (f) {
        console.log("recv msg from native...");
        onChannelMessage(f.data);
      };
    }
  }

  function onChannelMessage(msg) {
    const content = "msg from native:" + msg;
    document.getElementById("text").innerHTML = "<span>" + content + "</span>";
  }

以上即完成native和H5通過(guò)MessageChannel進(jìn)行通信的過(guò)程打厘。基于這個(gè)消息通道贺辰,我們可以自定義自己的通信協(xié)議户盯。MessageChannel的優(yōu)勢(shì)就是可以減少序列化和反序列化,提升消息通信的性能饲化。stackoverflow也有關(guān)于這個(gè)問(wèn)題的討論和解答莽鸭。

18.webview的安全問(wèn)題

  • addJavscriptInterface

addJavscriptInterface是Android 4.2之前的安全漏洞,4.2以上吃靠,可通過(guò)@JavascriptInterface注解來(lái)聲明JS可以訪問(wèn)的native方法硫眨,這里不再贅述。

  • file協(xié)議的跨域

手動(dòng)配置setAllowFileAccessFromFileURLs或setAllowUniversalAccessFromFileURLs兩個(gè)API為false當(dāng)然省事撩笆,但是很多場(chǎng)景需要使用到訪問(wèn)本地資源捺球。此時(shí)則需要對(duì)uri進(jìn)行校驗(yàn)或權(quán)限控制。其他情況夕冲,參見(jiàn)國(guó)家信息安全漏洞平臺(tái)的公告https://www.cnvd.org.cn/webinfo/show/4365?from=timeline

  • 其他安全漏洞:TODO

19.V8引擎(J2V8)

  • V8的引入以及初始化
implementation 'com.eclipsesource.j2v8:j2v8:6.0.0@aar'
  • 初始化
v8runtime = V8.createV8Runtime();
  • 橋接console.log
    /**
     * 橋接v8中的console.log
     */
    private void registerLogMethod() {
        AndroidConsole androidConsole = new AndroidConsole();
        V8Object v8Object = new V8Object(v8runtime);
        v8runtime.add("console", v8Object);
        //params1:對(duì)象
        //params2:java方法名
        //params3:js里面寫的方法名
        //params4:方法的參數(shù)類型 個(gè)數(shù)
        v8Object.registerJavaMethod(androidConsole, "log", "log", new Class<?>[]{String.class});
        v8Object.registerJavaMethod(androidConsole, "logObj", "logObj", new Class<?>[]{V8Object.class});
        v8Object.registerJavaMethod(androidConsole, "error", "error", new Class<?>[]{String.class});
        //在js中調(diào)用 `console.log('test')`
        v8runtime.executeScript("console.log('test');");
        v8Object.close();
    }

其中际歼,AndroidConsole的源碼為:

public class AndroidConsole {

    private static final String TAG = ">>>AndroidConsole<<<";

    /**
     * 通過(guò)反射注冊(cè)Java方法
     *
     * @param msg
     */
    public void log(String msg) {
        XLog.d(TAG, msg);
    }

    public void logObj(V8Object msg) {
        try {
            JSONObject jsonObject = XV8Utils.toJSONObject(msg);
            XLog.d(TAG, jsonObject.toJSONString());
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    /**
     * 通過(guò)反射注冊(cè)Java方法
     *
     * @param msg
     */
    public void error(String msg) {
        XLog.e(TAG, msg);
    }
}

這樣蒲列,在js中調(diào)用console.log,將會(huì)回調(diào)到AndroidConsole的log方法,然后就可以看到v8中的日志信息了。

  • JSBridge的整體設(shè)計(jì)

整體思路是慨畸,注冊(cè)一個(gè)Java回調(diào)與對(duì)應(yīng)的JS方法做綁定,比如

//向v8中注入對(duì)象Java方法
v8runtime.registerJavaMethod(new XV8JsBridgeCallback(this), "__xBridge_js_func__");

當(dāng)JS中調(diào)用__xBridge_js_func__的時(shí)候骂倘,會(huì)回調(diào)到XV8JsBridgeCallback的invoke方法睡陪。

public class XV8JsBridgeCallback implements JavaVoidCallback {

    XV8Manager mXV8Manager;

    public XV8JsBridgeCallback(XV8Manager manager) {
        this.mXV8Manager = manager;
    }

    @Override
    public void invoke(V8Object receiver, V8Array params) {
        //獲取JS中傳遞過(guò)來(lái)的參數(shù)
        JSONArray jsonArray = XV8Utils.toJSONArray(params);
        if (jsonArray != null) {
            XLog.d("V8ArrayCallBack:" + jsonArray.toJSONString());
        }
        String string = (String)jsonArray.get(0);
        //獲取消息,然后處理消息
        new V8BridgeImpl(mXV8Manager).handleMsgFromJS(string);
        params.close();
        receiver.close();
    }
}

然后就是JS層的設(shè)計(jì)庭敦,參考第一部分關(guān)于WebView的設(shè)計(jì)疼进,進(jìn)行如下改造,比如JSBridge的文件名稱為xbridge4v8.js秧廉,其內(nèi)容如下:


var XJSBridge;
(function () {
  var callbackArr = {};
  XJSBridge = {
    callNative: function (func, param, callback) {
      var seqId = "__bridge_id_" + Math.random() + "" + new Date().getTime();
      callbackArr[seqId] = callback;
      var msgObj = {
        func: func,
        param: param,
        msgType: "callNative",
        seqId: seqId,
      };
      if (typeof __xBridge_js_func__ === "undefined") {
        console.log("__xBridge_js_func__ not register before...");
      } else {
        __xBridge_js_func__(JSON.stringify(msgObj));
      }
    },
    nativeInvokeJS: function (res) {
      console.log("nativeInvokeJS start...");
      try {
        var resObj = JSON.parse(res);
        if (resObj && resObj.msgType === "jsCallback") {
          var func = callbackArr[resObj.seqId];
          if ("function" === typeof func) {
            func(resObj.param);
          }
        } else {
          console.log("error...");
        }
        return true;
      } catch (error) {
        console.error(error);
        return false;
      }
    },
  };
})();

與WebView的JSBridge的協(xié)議內(nèi)容一致伞广,但是最終是通過(guò)__xBridge_js_func__方法調(diào)用到native。

//執(zhí)行自定義的JSBridge代碼
v8runtime.executeVoidScript(bridge4v8);
//獲取JSBridge對(duì)象
mXJSBridge = v8runtime.getObject("XJSBridge");
//獲取調(diào)用JS的方法
mNativeInvokeJS = (V8Function) mXJSBridge.getObject("nativeInvokeJS");

與webview不同疼电,webview是通過(guò)loadUrl或者evaluateJavascript來(lái)調(diào)用JS嚼锄,在v8中,我們是通過(guò)v8獲取對(duì)應(yīng)的JSFunction的對(duì)象蔽豺,來(lái)進(jìn)行調(diào)用的区丑,比如以上代碼里,我們保存了mNativeInvokeJS對(duì)象。當(dāng)我們JSBridge中處理完之后沧侥,就可以通過(guò)mNativeInvokeJS回調(diào)給JS了可霎。

private void sendToV8RuntimeInUiThread(String msg) {
    V8Array args = new V8Array(v8runtime);
    args.push(msg); 
    mNativeInvokeJS.call(mXJSBridge, args);
    args.close();
}
  • V8與WebView的通信(以點(diǎn)擊dom觸發(fā)V8調(diào)用http請(qǐng)求為例,大致的流程)
    • 1.用戶點(diǎn)擊webview中的dom正什,通過(guò)webview的JSBridge啥纸,將事件傳遞給native
    • 2.native對(duì)事件進(jìn)行分發(fā),找到對(duì)應(yīng)的JSBridge,對(duì)事件進(jìn)行進(jìn)行解析
    • 3.然后通過(guò)v8的JSBridge婴氮,這里的mNativeInvokeJS調(diào)用V8中的JS方法
    • 4.V8中的JS觸發(fā)對(duì)應(yīng)的JS方法(需要通過(guò)反射調(diào)用)
    • 5.JS方法中斯棒,調(diào)用v8的JSBridge,比如http的JSBridge主经,調(diào)用到native
    • 6.回調(diào)到XV8JsBridgeCallback的invoke方法
    • 7.通過(guò)V8BridgeImpl對(duì)消息進(jìn)行分發(fā)荣暮,然后找到對(duì)應(yīng)的HttpBridege
    • 8.HttpBridege通過(guò)Okhttp發(fā)送請(qǐng)求
    • 9.將http的結(jié)果回調(diào)給v8中的JS
    • 10.v8中的JS再調(diào)用JSBridge,將http的結(jié)果罩驻,發(fā)送給native
    • 11.native再對(duì)消息進(jìn)行分發(fā)穗酥,調(diào)用PostMsgToWebViewBridge這個(gè)JSBridge
    • 12.JSBridge通過(guò)loadUrl或者evaluateJavascript,或者M(jìn)essageChannel惠遏,發(fā)給WebView
    • 13.WebView接收到native發(fā)過(guò)來(lái)的消息砾跃,然后展示到dom中。

以上流程节吮,第4步還未完成抽高,因此,通過(guò)v8直接運(yùn)行的v8demo.js,其中v8demo.js的代碼如下:

(function () {
  console.log("start execute...");
  if (XJSBridge) {
    XJSBridge.callNative(
      "http",
      { url: "https://news-at.zhihu.com/api/4/news/latest", data: {} },
      (resp) => {
        if (resp) {
          console.logObj(resp);
          XJSBridge.callNative("postMsgToWebView", resp, () => {
            console.log("post end.");
          });
        }
      }
    );
  } else {
    console.log("XJSBridge is invalid.");
  }
})();

如果通過(guò)MessageChannel進(jìn)行通信透绩,v8_demo.html的演示代碼如下:

<h1>H5與V8引擎測(cè)試頁(yè)面</h1>

<a href="javascript:void(0)" class="btn read" id="a1">v8測(cè)試</a
><br /><br /><br />

<body>
  <div id="text"></div>
  <br /><br /><br />
  <div id="container"></div>
</body>
<!-- 在這里注入JSBridge翘骂,由webview攔截這個(gè)鏈接,然后替換成assets目錄的xbridge.js文件-->
<script src="https://www.baidu.com/xbridge.js"></script>
<script>
  document.getElementById("a1").addEventListener("click", function () {
    window.XJSBridge.callNative("nativeMethod", { name: "test" }, (res) => {
       //TODO
    });
  });

  window.addEventListener("message", receiverMessage, false);

  function receiverMessage(messageEvent) {
    console.log("onmessage...", JSON.stringify(messageEvent.data));
    if (messageEvent.data === "__init_port__") {
      //在window上掛載port對(duì)象帚豪,將native發(fā)過(guò)來(lái)的h5Port引用保存起來(lái)
      window.__my_port__ = messageEvent.ports[0];
      //設(shè)置消息
      window.__my_port__.onmessage = function (f) {
        console.log("recv msg from v8...");
        onChannelMessage(f.data);
      };
    }
  }

  function onChannelMessage(msg) {
    const content = "msg from native:" + msg;
    document.getElementById("text").innerHTML = "<span>" + content + "</span>";
  }
</script>

  • V8中的setTimeout,setInterval,clearTimeout,clearInterval問(wèn)題

類似console.log的方式碳竟,通過(guò)native實(shí)現(xiàn)

20.借助Glide圖片加載框架對(duì)webview的圖片進(jìn)行緩存

第12部分講了基于離線包(packageApp)級(jí)別的緩存,將前端的html狸臣、js莹桅、css等進(jìn)行了統(tǒng)一的緩存。在APP啟動(dòng)的時(shí)烛亦,將離線包加載到內(nèi)存中诈泼。當(dāng)Webview加載H5頁(yè)面時(shí),根據(jù)虛擬域名匹配此洲,加載本地的資源厂汗。但是委粉,離線包在進(jìn)行網(wǎng)絡(luò)請(qǐng)求之后呜师,會(huì)開(kāi)始渲染服務(wù)端json數(shù)據(jù),這里面有一些圖片資源贾节。我們?nèi)绾螌?duì)圖片進(jìn)行緩存呢汁汗?native通過(guò)圖片加載框架(比如Glide)實(shí)現(xiàn)圖片的多級(jí)緩存及復(fù)用衷畦,WebView則沒(méi)有這么能力,本小節(jié)就是通過(guò)Glide實(shí)現(xiàn)圖片資源的攔截和緩存知牌。

  • 接入Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
  • 對(duì)圖片url進(jìn)行判斷祈争,使用Glide加載圖片

完整代碼如下,主要是通過(guò)Glide將圖片加載為bitmap角寸,然后構(gòu)建WebResourceResponse即可菩混。

public class GlideImgCacheManager {

    private static final String TAG = "GlideCache";

    private static GlideImgCacheManager sGlideImgCacheManager = null;

    //只緩存白名單中的圖片資源
    private static final HashSet CACHE_IMG_TYPE = new HashSet() {
        {
            add("png");
            add("jpg");
            add("jpeg");
            add("bmp");
            add("webp");
        }
    };

    public synchronized static GlideImgCacheManager getInstance() {
        if (sGlideImgCacheManager == null) {
            sGlideImgCacheManager = new GlideImgCacheManager();
        }
        return sGlideImgCacheManager;
    }

    /**
     * 攔截資源
     *
     * @param url
     * @return
     */
    public WebResourceResponse interceptRequest(WebView webView, String url) {
        try {
            String extension = MimeTypeMapUtils.getFileExtensionFromUrl(url);
            if (TextUtils.isEmpty(extension) || !CACHE_IMG_TYPE.contains(extension.toLowerCase())) {
                //不在支持的緩存范圍內(nèi)
                return null;
            }
            XLog.d(TAG, String.format("start glide cache img (%s),url:%s", extension, url));
            long startTime = System.currentTimeMillis();
            //String mimeType = MimeTypeMapUtils.getMimeTypeFromUrl(url);
            InputStream inputStream = null;
            Bitmap bitmap = Glide.with(webView).asBitmap().diskCacheStrategy(DiskCacheStrategy.ALL).load(url).submit().get();
            inputStream = getBitmapInputStream(bitmap, Bitmap.CompressFormat.JPEG);
            long costTime = System.currentTimeMillis() - startTime;
            if (inputStream != null) {
                XLog.d(TAG, String.format("glide cache img(%s ms): %s", costTime, url));
                WebResourceResponse webResourceResponse = new WebResourceResponse("image/jpg", "UTF-8", inputStream);
                return webResourceResponse;
            } else {
                XLog.e(TAG, String.format("glide cache error.(%s ms): %s", costTime, url));
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return null;
    }

    /**
     * 將bitmap進(jìn)行壓縮轉(zhuǎn)換成InputStream
     *
     * @param bitmap
     * @param compressFormat
     * @return
     */
    private InputStream getBitmapInputStream(Bitmap bitmap, Bitmap.CompressFormat compressFormat) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            bitmap.compress(compressFormat, 80, byteArrayOutputStream);
            byte[] data = byteArrayOutputStream.toByteArray();
            return new ByteArrayInputStream(data);
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return null;
    }
}
  • 在webView的WebViewClient中攔截
    ...
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        WebResourceResponse webResourceResponse = GlideImgCacheManager.getInstance().interceptRequest(view, request.getUrl().toString());
        if (webResourceResponse != null) {
            return webResourceResponse;
        }
        return super.shouldInterceptRequest(view, request);
    }

21.借助OkHttp的緩存策略做url級(jí)別的緩存&預(yù)加載

第12部分講了基于離線包(packageApp)級(jí)別的緩存,20講了基于Glide對(duì)圖片資源的緩存扁藕。那么沮峡,如果不是離線包,是在線頁(yè)面亿柑,如何對(duì)在線頁(yè)面的文檔邢疙、js、css以及其他文件進(jìn)行緩存呢望薄?我們可以通過(guò)OkHttp替代WebView的網(wǎng)絡(luò)請(qǐng)求疟游,然后使用OkHttp的緩存策略,來(lái)緩存WebView中需要加載的url資源痕支。

  • 創(chuàng)建OkHttp的攔截器Interceptor
public class MyOkHttpCacheInterceptor implements Interceptor {

    private int maxAga = 365;//default
    private TimeUnit timeUnit = TimeUnit.DAYS;

    public void setMaxAge(int maxAga, TimeUnit timeUnit) {
        this.maxAga = maxAga;
        this.timeUnit = timeUnit;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());

        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(maxAga, timeUnit)
                .build();

        return response.newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                .header("Cache-Control", cacheControl.toString())
                .build();
    }
}
  • 對(duì)url資源進(jìn)行攔截颁虐,然后使用OkHttp進(jìn)行加載和緩存
public class OkHttpCacheManager {

    private static final String TAG = "CACHE";

    private static OkHttpCacheManager sOkHttpCacheManager;

    private int allCount = 0;
    private int cacheCount = 0;

    //只緩存白名單中的資源
    private static final HashSet CACHE_MIME_TYPE = new HashSet() {
        {
            add("html");
            add("htm");
            add("js");
            add("ico");
            add("css");
            add("png");
            add("jpg");
            add("jpeg");
            add("gif");
            add("bmp");
            add("ttf");
            add("woff");
            add("woff2");
            add("otf");
            add("eot");
            add("svg");
            add("xml");
            add("swf");
            add("txt");
            add("text");
            add("conf");
            add("webp");
        }
    };

    public synchronized static OkHttpCacheManager getIntance() {
        if (sOkHttpCacheManager == null) {
            sOkHttpCacheManager = new OkHttpCacheManager();
        }
        return sOkHttpCacheManager;
    }

    private OkHttpClient mHttpClient;

    private OkHttpCacheManager() {
        //設(shè)置緩存的目錄文件
        File httpCacheDirectory = new File(XApplication.getApplication().getExternalCacheDir(), "x-webview-http-cache");
        //僅作為日志使用
        if (httpCacheDirectory.exists()) {
            List<File> result = XFileUtils.listFiles(httpCacheDirectory);
            for (File file : result) {
                XLog.d(TAG, "file = " + file.getAbsolutePath());
            }
        }
        //緩存的大小,OkHttp會(huì)使用DiskLruCache緩存
        int cacheSize = 20 * 1024 * 1024; // 20 MiB
        Cache cache = new Cache(httpCacheDirectory, cacheSize);
        //設(shè)置緩存
        mHttpClient = new OkHttpClient.Builder()
                .addNetworkInterceptor(new MyOkHttpCacheInterceptor())
                .cache(cache)
                .build();
    }

    /**
     * 針對(duì)url級(jí)別的緩存采转,包括主文檔聪廉,圖片,js故慈,css等
     *
     * @param url
     * @param headers
     * @return
     */
    public WebResourceResponse interceptRequest(String url, Map<String, String> headers) {
        try {
            String extension = MimeTypeMapUtils.getFileExtensionFromUrl(url);
            if (TextUtils.isEmpty(extension) || !CACHE_MIME_TYPE.contains(extension.toLowerCase())) {
                //不在支持的緩存范圍內(nèi)
                XLog.w(TAG + "+" + url + " 's extension is " + extension + "!!not support...");
                return null;
            }
            long startTime = System.currentTimeMillis();
            Request.Builder reqBuilder = new Request.Builder()
                    .url(url);
            if (headers != null) {
                for (Map.Entry<String, String> entry : headers.entrySet()) {
                    XLog.d(TAG, String.format("header:(%s=%s)", entry.getKey(), entry.getValue()));
                    reqBuilder.addHeader(entry.getKey(), entry.getValue());
                }
            }

            Request request = reqBuilder.get().build();
            Response response = mHttpClient.newCall(request).execute();
            if (response.code() != 200) {
                XLog.e(TAG, "response code = " + response.code() + ",extension = " + extension);
                return null;
            }
            String mimeType = MimeTypeMapUtils.getMimeTypeFromUrl(url);
            XLog.d(TAG, "mimeType = " + mimeType + ",extension = " + extension + ",url = " + url);
            WebResourceResponse okHttpWebResourceResponse = new WebResourceResponse(mimeType, "", response.body().byteStream());
            Response cacheRes = response.cacheResponse();
            long endTime = System.currentTimeMillis();
            long costTime = endTime - startTime;
            allCount++;
            if (cacheRes != null) {
                cacheCount++;
                XLog.e(TAG, String.format("count rate = (%s),costTime = (%s);from cache: %s", (1.0f * cacheCount / allCount), costTime, url));
            } else {
                XLog.e(TAG, String.format("costTime = (%s);from server: %s", costTime, url));
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                String message = response.message();
                if (TextUtils.isEmpty(message)) {
                    message = "OK";
                }
                try {
                    okHttpWebResourceResponse.setStatusCodeAndReasonPhrase(response.code(), message);
                } catch (Exception e) {
                    return null;
                }
                Map<String, String> header = MimeTypeMapUtils.multimapToSingle(response.headers().toMultimap());
                okHttpWebResourceResponse.setResponseHeaders(header);
            }
            return okHttpWebResourceResponse;
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return null;
    }

}
  • 在webView的WebViewClient中設(shè)置
    ...
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        WebResourceResponse webResourceResponse = OkHttpCacheManager.getIntance().interceptRequest(request.getUrl().toString(), request.getRequestHeaders());
        if (webResourceResponse != null) {
            return webResourceResponse;
        }
        return super.shouldInterceptRequest(view, request);
    }
  • 預(yù)加載緩存

上述的邏輯是實(shí)時(shí)緩存板熊,也就是下次請(qǐng)求相同url才能使用。如果想做預(yù)加載察绷,在首次請(qǐng)求就可以使用緩存干签,則可以提前對(duì)資源的url進(jìn)行加載,然后再攔截到url時(shí)拆撼,從內(nèi)存中獲取容劳。比如:

    ...
     //內(nèi)存級(jí)別的預(yù)加載緩存
    private static LruCache<String, byte[]> preLoadCache = new LruCache<>(100);
    ...
    /**
     * 預(yù)加載資源
     *
     * @param urls
     */
    public void preLoadResource(final List<String> urls) {
        XApplication.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                for (String url : urls) {
                    try {
                        XLog.e(TAG, "start load res:" + url);
                        Request.Builder reqBuilder = new Request.Builder()
                                .url(url);
                        Request request = reqBuilder.get().build();
                        Response response = mHttpClient.newCall(request).execute();
                        if (response.code() == 200) {
                            XLog.e(TAG, "res preload success..." + url);
                            //保存下載的資源
                            preLoadCache.put(url, response.body().bytes());
                        }
                    } catch (Throwable t) {
                        t.printStackTrace();
                    }
                }
            }
        });
    }
    ...
    
    public WebResourceResponse interceptRequest(String url, Map<String, String> headers) {
        try {
            ...
            //預(yù)加載
            if (preLoadCache.get(url) != null) {
                byte[] contentByte = preLoadCache.get(url);
                InputStream inputStream = new ByteArrayInputStream(contentByte);
                WebResourceResponse webResourceResponse = new WebResourceResponse(MimeTypeMapUtils.getMimeType(url), "UTF-8", inputStream);
                XLog.e(TAG, "hit preload cache.url = " + url);
                return webResourceResponse;
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return null; 
      }

可以在APP啟動(dòng)或者頁(yè)面啟動(dòng)時(shí),預(yù)加載所需要的資源:

List<String> urls = new ArrayList<>();
urls.add("https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js");
urls.add("https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js");
urls.add("https://www.baidu.com/index.html");
OkHttpCacheManager.getIntance().preLoadResource(urls);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闸度,一起剝皮案震驚了整個(gè)濱河市竭贩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌莺禁,老刑警劉巖留量,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡楼熄,警方通過(guò)查閱死者的電腦和手機(jī)忆绰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)可岂,“玉大人错敢,你說(shuō)我怎么就攤上這事÷拼猓” “怎么了稚茅?”我有些...
    開(kāi)封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)平斩。 經(jīng)常有香客問(wèn)我峰锁,道長(zhǎng),這世上最難降的妖魔是什么双戳? 我笑而不...
    開(kāi)封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任虹蒋,我火速辦了婚禮,結(jié)果婚禮上飒货,老公的妹妹穿的比我還像新娘魄衅。我一直安慰自己,他們只是感情好塘辅,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布晃虫。 她就那樣靜靜地躺著,像睡著了一般扣墩。 火紅的嫁衣襯著肌膚如雪哲银。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天呻惕,我揣著相機(jī)與錄音荆责,去河邊找鬼。 笑死亚脆,一個(gè)胖子當(dāng)著我的面吹牛做院,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播濒持,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼键耕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了柑营?” 一聲冷哼從身側(cè)響起屈雄,我...
    開(kāi)封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎官套,沒(méi)想到半個(gè)月后酒奶,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蓖议,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年讥蟆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纺阔。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瘸彤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笛钝,到底是詐尸還是另有隱情质况,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布玻靡,位于F島的核電站结榄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏囤捻。R本人自食惡果不足惜臼朗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蝎土。 院中可真熱鬧视哑,春花似錦、人聲如沸誊涯。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)暴构。三九已至跪呈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間取逾,已是汗流浹背耗绿。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砾隅,地道東北人缭乘。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像琉用,于是被迫代替她去往敵國(guó)和親堕绩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359