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ò)onPageStarted
和onPageFinished
進(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é)果示例
{
"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中。
- 1.用戶點(diǎn)擊webview中的dom正什,通過(guò)webview的JSBridge啥纸,將事件傳遞給native
以上流程节吮,第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);