1.如何實作和設計一套JSBridge?
前端JS調用native的方式有很多種,或者說android有很多種方式可以攔截或者擷取到JS的行為。如下使用onConsoleMessage的方式,來設計一個簡單的JSBridge:
- 前端代碼片段
(function () {
var callbackArr = {};
window.XJSBridge = {
//JS調動native
callNative: function (func, param, callback) {
//生成調用的序列号Id
var seqId = "__bridge_id_" + Math.random() + "" + new Date().getTime();
//儲存回調
callbackArr[seqId] = callback;
//生成約定的JSBridge消息
var msgObj = {
func: func,
param: param,
msgType: "callNative",
seqId: seqId,
};
//列印約定的日志
console.log("____xbridge____:" + JSON.stringify(msgObj));
},
//native調用JS的方法
nativeInvokeJS: function (res) {
//解析native的消息
res = JSON.parse(res);
//判斷是否是callback消息
if (res && res.msgType === "jsCallback") {
//擷取之前保持的回調方法
var func = callbackArr[res.seqId];
//delete callbackArr[res.seqId];
//調用JS的回調
if ("function" === typeof func) {
setTimeout(function () {
//調用js的回調
func(res.param);
}, 1);
}
}
return true;
},
};
document.dispatchEvent(new Event("xbridge inject success..."), null);
console.log("xbridge inject success...");
})();
如上代碼就是在window對象上挂載一個XJSBridge對象,可以通過callNative函數來調用android的native方法,其原理就是JS調用console.log列印一行約定的日志(Bridge協定),android端通過WebChromeClient的onConsoleMessage方法,擷取到列印的日志,然後針對協定解析出對應的協定。
- 其中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級别的日志
if (mWebViewPage.handleMsgFromJS(consoleMessage.message())) {
return true;
}
} else if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
//ERROR級别的日志
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會調用到XJSBridgeImpl的handleLogFromJS方法,在這裡針對JSBridge進行解析,比如:
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實作
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() {
//需要在主線程調用
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如何回調給前端?
android通過mWebView.evaluateJavascript或者mWebView.loadUrl的方式調用JS,在android 4.4以上推薦使用evaluateJavascript,loadUrl會導緻頁面重新整理,鍵盤收回等問題。
- native如何向前端注入對象?
通過addJavascriptInterface添加對象,比如:
.addJavascriptInterface(new NativeLog(),"nativeLog");
其中NativeLog的實作如下:
public class NativeLog {
@JavascriptInterface
public void print(String log) {
XLog.d("nativeLog:" + log);
}
}
這裡面的方法print,需要添加注解@JavascriptInterface,JS才能通路到。
添加之後,前端可以通過如下方式調用:
.getElementById('a2').addEventListener('click', function () {
console.log('clicked....');
if (nativeLog) {
nativeLog.print('content from js');
}
});
2.如果實作url的攔截和重定向?
WebView的WebViewClient中,可以通過重寫shouldOverrideUrlLoading實作,如果需要攔截,可以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")) {
//根據前端的href進行scheme攔截,跳轉到native的頁面
view.getContext().startActivity(new Intent(view.getContext(), TestActivity.class));
return true;
}
if(url != null && url.startsWith("")){
}
return false;
}
...
}
-
攔截url
這裡可以實作很多功能,比如頁面跳轉到外部連結https://www.baidu.com時,可以攔截掉,然後讓WebView加載本地的某個頁面,或者是錯誤頁面。
- 擴充JSBridge
也可以通過shouldOverrideUrlLoading實作JSBridge,比如攔截到url是
xscheme://native_page
時,容器跳轉到某一個native頁面,達到啟動native頁面的目的,也可以擴充其他功能。前端可以通過如下方式:
<a href="xscheme://native_page?name=test_activity" class="btn read" id="a3">通過href的scheme調用native</a><br><br><br>
- 處理指定的某些url
比如這裡可以判斷url如果是以.apk結尾的時候,啟動系統的浏覽器進行下載下傳;攔截到某些scheme時,喚起對應的app等等。
3.如何實作資源的攔截?
可以對WebViewClient的shouldInterceptRequest方法進行Override,然後根據資源請求進行攔截。
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;
}
...
}
比如上述代碼,是對H5頁面的圖檔資源進行攔截,把H5頁面的圖檔資源換成本地的圖檔。如上代碼并沒有實際意義,隻是舉例說明對資源的攔截。
4.如何實作資源的離線?
上述步驟中根據攔截shouldInterceptRequest方法,建構對應的WebResourceResponse,其實H5資源離線的原理正是如此。這裡需要注意的是CORS問題,攔截之後建構的資源URI可能和目前H5頁面的域名不同,是以需要添加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頁面需要通路的線上資源,在H5頁面打開之前,提前下載下傳好或者内置到apk中,等到H5頁面加載時,通過shouldInterceptRequest攔截資源的加載,然後将已經離線的資源封裝成WebResourceResponse對象,提高H5頁面的加載速度。
5.如何擷取JS列印的日志?
之前已經講過,可以通過WebChromeClient.onConsoleMessage方法擷取。但是,如果部分機型沒有回調,可以通過addJavascriptInterface的方式,給H5注入一個Console對象,并實作Console對象的log等方法即可。
6.如何實作接口的預取?
對H5頁面的網絡請求提前預取,可以提升H5頁面的性能。其原理是在啟動H5頁面的Activity時,就時開始進行網絡請求,把請求的結果進行緩存。當頁面加載時,攔截頁面的網絡請求,然後将緩存的結果傳回給H5,完成接口預期,加速頁面的加載。
7.WebView如何和native的Cookie同步?
很多app的登入頁面是native實作的,登入成功之後,希望H5頁面能夠在加載時,共用native的Cookie,此時如何做同步呢?Android中可以使用CookieManager
= CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
List<Cookie> cookies = getCookiesFromLogin();//從業務中擷取
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頁面
webView.loadUrl(url);
當然,android也可以從CookieManager擷取WebView儲存的Cookie,比如H5頁面HTTP回應的頭資訊裡面,放置的Set-Cookie資訊,WebView會儲存在CookieManager中.
8.如何注入JSBridge?
為了友善的管控JSBridge的内容,友善後續統一更新,我們希望JSBridge的js通過攔截注入的方式加載,而不是直接寫到前端代碼中,那我們如何做呢?
- 1.将JSBridge的内容抽離成一個xbridge.js檔案,存放到assets目錄中
- 2.做一個虛拟域名,比如demo中的
https://www.baidu.com/xbridge.js
- 3.在H5的入口html中,添加script标簽,比如:
<script src="https://www.baidu.com/xbridge.js"></script>
- 4.在webview的WebViewClient中攔截虛拟url,然後将本地assets目錄中的jsbridge注入進去即可。
...
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?
大家都知道,每個app有一個scheme,比如口碑的scheme為
alipays://platformapi/startApp?appId=20000001
;具體每個app的scheme是什麼,我們可以反編譯對應點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
那我們如何喚起app呢?
- 1.前端通過
傳入scheme,比如:window.location.href
...
document.getElementById("a9").addEventListener("click", function () {
const scheme = "koubei://platformapi/startApp?appId=20000001";
console.log("scheme = " + scheme);
window.location.href = scheme;
});
...
以上是喚起口碑app的demo
- 2.在webview的WebViewClient中通過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,喚起對應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?
針對android的情況,可以有如下方式:
- 1.通過JSBridge,由native實作一個檔案下載下傳和安裝的功能,前端隻需要調用即可
- 2.通過window.location.href,用戶端對其進行攔截,同上;
- 3.使用iframe.src中添加js标本的方式,可以避免打開新的頁面;
- 4.使用form表單的action字段,通過submit也可以
11.前端根據ua判斷iOS/Android以及版本
- 1.前端直接可以通過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
如果端上想把自己的版本号等資訊,也想通過UA傳給前端,可以直接設定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;
}
}
當然,也可以直接調用webview的setUserAgentString進行重置;比如設定後的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.離線包設計
- 離線壓縮包設計
以一個
create-react-app
的H5項目為例,執行
npm build
進行編譯,我們将生成的
build
目錄下的檔案進行簡單的壓縮,壓縮成zip包。為了友善後續離線包的管理,我們在離線包中添加一個我們約定的包資訊描述檔案,比如命名為
pkg-desc.json
,當然,我們也可以複用
manifest.json
這個檔案,在json中,添加對離線包的描述,比如:
{
"launchParams": {
"indexUrl": "/index.html",
"transparentTitle": "true"
},
"vHost": "https://www.taobao.com"
}
這裡簡單列幾個參數,比如vHost,即是我們設計的
虛拟域名
,我們設計的目的,是在webview加載vHost+indexUrl時,自動加載目前離線包目錄裡的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
儲存了我們自定義的資訊。
- 離線包的解壓和加載
我們将上述設計的壓縮包,壓縮成
zip
檔案,在app啟動的時候,将離線包的内容加載到記憶體中,并以Map的形式儲存。我們以存放到assets目錄下的離線包為例,對其進行加載的部分核心代碼:
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;
//儲存離線包的内容:key為url的路徑,byte為對應的緩存内容
private Map<String, byte[]> offlinePkg = new ConcurrentHashMap<>();
//儲存離線包的位址以及啟動參數等資訊
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();
//儲存離線包内容
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();
}
//預設為/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檔案的方法實作:
//将ZIP檔案加載記憶體中,以路徑和byte[]的key/value形式存儲
public class XFileUtils {
/**
* 将壓縮包解壓到記憶體中
*
* @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);
//保持相對路徑的檔案名稱以及對應的資料
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通過loadUrl加載虛拟域名的時候,webview通過shouldInterceptRequest攔截url,查找對應的``
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 {
//嘗試擷取離線内容
byte[] contentByte = OfflinePkgManager.getInstance().getOfflineContent(url);
if (contentByte != null && contentByte.length > 0) {
InputStream inputStream = new ByteArrayInputStream(contentByte);
//構造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";
}
}
這一部分就是根據url查找離線包的内容,然後構造WebResourceResponse以及對應的MimeType;
- url參數(魔術參數)
manifest.json
裡面還可以添加一下預設的參數,比如設定titlebar的參數,這些參數可以在webview加載url之前生效。比如titlebar的背景,title等資訊,實作原理比較簡單,這裡不再贅述。
13.自定義錯誤頁面
native可以根據WebView的報錯資訊,自定義錯誤頁面,可通過WebViewClient監聽報錯回調:
onReceivedError
onReceivedHttpError
onReceivedSslError
自定義的錯誤頁面有如下幾種實作方式:
- 1.H5實作一個預設的錯誤頁,打包到apk中,直接使用webview加載。
- 2.使用native實作一個layout,然後覆寫在webview布局上方,同時可以自定義一些功能。
14.頁面加載逾時
自定義webview的頁面加載逾時時間,可以通過
onPageStarted
和
onPageFinished
進行配合
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.對前端頁面的性能統計
為了更加準确和詳細的擷取H5頁面的性能,可以通過前端的
performance
實作:
- 前端代碼:
;(function (win) {
if (!win.performance || !win.performance.timing) return {};
var time = win.performance.timing;
var timingResult = {};
timingResult["重定向時間"] = (time.redirectEnd - time.redirectStart) / 1000;
timingResult["DNS解析時間"] =
(time.domainLookupEnd - time.domainLookupStart) / 1000;
timingResult["TCP完成握手時間"] =
(time.connectEnd - time.connectStart) / 1000;
timingResult["HTTP請求響應完成時間"] =
(time.responseEnd - time.requestStart) / 1000;
timingResult["DOM開始加載前所花費時間"] =
(time.responseEnd - time.navigationStart) / 1000;
timingResult["DOM加載完成時間"] = (time.domComplete - time.domLoading) / 1000;
timingResult["DOM結構解析完成時間"] =
(time.domInteractive - time.domLoading) / 1000;
timingResult["腳本加載時間"] =
(time.domContentLoadedEventEnd - time.domContentLoadedEventStart) / 1000;
timingResult["onload事件時間"] =
(time.loadEventEnd - time.loadEventStart) / 1000;
timingResult["頁面完全加載時間"] =
timingResult["重定向時間"] +
timingResult["DNS解析時間"] +
timingResult["TCP完成握手時間"] +
timingResult["HTTP請求響應完成時間"] +
timingResult["DOM結構解析完成時間"] +
timingResult["DOM加載完成時間"];
return { result: timingResult };
})(this);
- webview在onPageFinised加載完成時執行統計代碼
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[\"重定向時間\"]=(time.redirectEnd-time.redirectStart)/1000;timingResult[\"DNS解析時間\"]=(time.domainLookupEnd-time.domainLookupStart)/1000;timingResult[\"TCP完成握手時間\"]=(time.connectEnd-time.connectStart)/1000;timingResult[\"HTTP請求響應完成時間\"]=(time.responseEnd-time.requestStart)/1000;timingResult[\"DOM開始加載前所花費時間\"]=(time.responseEnd-time.navigationStart)/1000;timingResult[\"DOM加載完成時間\"]=(time.domComplete-time.domLoading)/1000;timingResult[\"DOM結構解析完成時間\"]=(time.domInteractive-time.domLoading)/1000;timingResult[\"腳本加載時間\"]=(time.domContentLoadedEventEnd-time.domContentLoadedEventStart)/1000;timingResult[\"onload事件時間\"]=(time.loadEventEnd-time.loadEventStart)/1000;timingResult[\"頁面完全加載時間\"]=timingResult[\"重定向時間\"]+timingResult[\"DNS解析時間\"]+timingResult[\"TCP完成握手時間\"]+timingResult[\"HTTP請求響應完成時間\"]+timingResult[\"DOM結構解析完成時間\"]+timingResult[\"DOM加載完成時間\"];return{result:timingResult}})(this);";
view.evaluateJavascript(jsCode, new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
XLog.d("onPageFinished:JSResult = " + s);
//TODO 對性能資料進行上報
}
});
}
...
}
- 統計結果示例
以https://www.baidu.com為例:
{
"result":{
"DNS解析時間":0.013,
"DOM加載完成時間":1.586,
"DOM開始加載前所花費時間":0.312,
"DOM結構解析完成時間":0.142,
"HTTP請求響應完成時間":0.118,
"TCP完成握手時間":0.123,
"onload事件時間":0.001,
"腳本加載時間":0.001,
"重定向時間":0,
"頁面完全加載時間":1.982
}
}
16.H5頁面的任務棧多開
類似微信小程式或者支付寶小程式,我們可以将H5頁面在一個獨立的程序中加載.
- 多程序的優勢:
- 隔離:與主程序是程序級别的隔離,不影響主程序的Crash
- 增加app的可用記憶體
- 多程序的劣勢:
- 資料共享問題
- 不必要的初始化
對于一個H5頁面,我們需要将WebView所在的Activity設定為獨立程序。同時,為了支援多個H5頁面,我們預注冊5個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程序", R.drawable.h5_icon));
} else {
Bitmap iconBmp = BitmapFactory.decodeResource(getResources(), R.drawable.h5_icon); // 這裡應該是小程式圖示的bitmap
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setTaskDescription(new ActivityManager.TaskDescription("H5程序", 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 {
}
}
可以通過setTaskDescription設定task的名稱,比如以小程式的業務名稱命名。在AndroidManifest.xml中配置多程序以及task的屬性,以如下xml為例:主要是設定
android:process
,
launchMode
,
taskAffinity
三個屬性。
- xml的設定
<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" />
- 啟動H5Activity以及複用
最多打開5個獨立程序的H5頁面,當打開第6個時,會覆寫掉第1個打開的程序。如下是部分核心代碼:
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) {
}
}
}
- 多程序的“後遺症”
多程序下,Application會初始化多次,我們需要排除非必要的初始化,以及合理安排APP主程序和H5進行的分工,然後通過程序間通信(比如AIDL)的方式解決主程序和子程序之間的通信問題。
17.使用MessageChannel通信
- MessageChannel
MessageChannel,并不是一個新的概念,它是一個Web API。它允許我們建立一個新的MessageChannel(消息通道)然後通過這個消息通道的兩個端口(MessagePort)進行傳遞資料。比如,前端可以通過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");
運作之後,可以看到日志如下:
recv msg from port1:send to port2
recv msg from port2:send to port1
以上隻是前端方面的一個簡單Demo
- android webview與H5之間通過MessageChannel通信
我們知道,我們可以通過MessageChannel的兩個MessagePort進行通信。那native和H5的通信的思路是,native通過WebMessage把其中一個端口發送給H5,H5儲存端口的引用,然後通過端口postMessage另一個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];
//監聽從h5Port中發送過來的消息
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());
}
});
//發送webmessage,把h5Port發送給H5頁面
XLog.d(TAG, "start postWebMessage to transfer port");
WebMessage webMessage = new WebMessage("__init_port__", new WebMessagePort[]{h5Port});
webView.postWebMessage(webMessage, Uri.EMPTY);
}
/**
* 通過port,向H5發送webMessage
*
* @param msg
*/
@TargetApi(Build.VERSION_CODES.M)
private void postMessageToH5(String msg) {
nativePort.postMessage(new WebMessage(msg));
}
}
不過,MessageChannel需要在WebView的onPageFinished以後才能建立,時機相對較晚。也即是在WebViewClient的onPageFinished回調中,調用init方法。首先通過webView.createWebMessageChannel建立端口,将其中一個供native使用,并設定消息監聽回調,另外一個則通過webView.postWebMessage,将内容為
__init_port__
的WebMessage發送給前端,消息中攜帶了WebMessagePort端口資訊。其中``init_port`是我們自定義的協定,供前端判斷。
接下來,看前端部分的代碼實作,前端通過window.addEventListener,監聽所有的message消息。然後判斷messageEvent的data部分是否是
__init_port__
,如果是的話,就從messageEvent中擷取到native傳過來的port對象,将其挂載到window上,比如
window.__my_port__
,并設定onmessage消息監聽,用于監聽android發送過來的消息。另外,我們也可以通過
window.__my_port__
向native發送消息。
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對象,将native發過來的h5Port引用儲存起來
window.__my_port__ = messageEvent.ports[0];
//設定消息
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通過MessageChannel進行通信的過程。基于這個消息通道,我們可以自定義自己的通信協定。MessageChannel的優勢就是可以減少序列化和反序列化,提升消息通信的性能。stackoverflow也有關于這個問題的讨論和解答。
18.webview的安全問題
- addJavscriptInterface
addJavscriptInterface是Android 4.2之前的安全漏洞,4.2以上,可通過@JavascriptInterface注解來聲明JS可以通路的native方法,這裡不再贅述。
- file協定的跨域
手動配置setAllowFileAccessFromFileURLs或setAllowUniversalAccessFromFileURLs兩個API為false當然省事,但是很多場景需要使用到通路本地資源。此時則需要對uri進行校驗或權限控制。其他情況,參見國家資訊安全漏洞平台的公告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:對象
//params2:java方法名
//params3:js裡面寫的方法名
//params4:方法的參數類型 個數
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中調用 `console.log('test')`
v8runtime.executeScript("console.log('test');");
v8Object.close();
}
其中,AndroidConsole的源碼為:
public class AndroidConsole {
private static final String TAG = ">>>AndroidConsole<<<";
/**
* 通過反射注冊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();
}
}
/**
* 通過反射注冊Java方法
*
* @param msg
*/
public void error(String msg) {
XLog.e(TAG, msg);
}
}
這樣,在js中調用
console.log
,将會回調到AndroidConsole的log方法,然後就可以看到v8中的日志資訊了。
- JSBridge的整體設計
整體思路是,注冊一個Java回調與對應的JS方法做綁定,比如
//向v8中注入對象Java方法
v8runtime.registerJavaMethod(new XV8JsBridgeCallback(this), "__xBridge_js_func__");
當JS中調用
__xBridge_js_func__
的時候,會回調到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中傳遞過來的參數
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層的設計,參考第一部分關于WebView的設計,進行如下改造,比如JSBridge的檔案名稱為
xbridge4v8.js
,其内容如下:
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的協定内容一緻,但是最終是通過
__xBridge_js_func__
方法調用到native。
//執行自定義的JSBridge代碼
v8runtime.executeVoidScript(bridge4v8);
//擷取JSBridge對象
mXJSBridge = v8runtime.getObject("XJSBridge");
//擷取調用JS的方法
mNativeInvokeJS = (V8Function) mXJSBridge.getObject("nativeInvokeJS");
與webview不同,webview是通過
loadUrl
或者
evaluateJavascript
來調用JS,在v8中,我們是通過v8擷取對應的JSFunction的對象,來進行調用的,比如以上代碼裡,我們儲存了
mNativeInvokeJS
對象。當我們JSBridge中處理完之後,就可以通過
mNativeInvokeJS
回調給JS了。
private void sendToV8RuntimeInUiThread(String msg) {
V8Array args = new V8Array(v8runtime);
args.push(msg);
mNativeInvokeJS.call(mXJSBridge, args);
args.close();
}
- V8與WebView的通信(以點選dom觸發V8調用http請求為例,大緻的流程)
- 1.使用者點選webview中的dom,通過webview的JSBridge,将事件傳遞給native
- 2.native對事件進行分發,找到對應的JSBridge,對事件進行進行解析
- 3.然後通過v8的JSBridge,這裡的
調用V8中的JS方法mNativeInvokeJS
- 4.V8中的JS觸發對應的JS方法(需要通過反射調用)
- 5.JS方法中,調用v8的JSBridge,比如http的JSBridge,調用到native
- 6.回調到XV8JsBridgeCallback的invoke方法
- 7.通過V8BridgeImpl對消息進行分發,然後找到對應的HttpBridege
- 8.HttpBridege通過Okhttp發送請求
- 9.将http的結果回調給v8中的JS
- 10.v8中的JS再調用JSBridge,将http的結果,發送給native
- 11.native再對消息進行分發,調用PostMsgToWebViewBridge這個JSBridge
- 12.JSBridge通過loadUrl或者evaluateJavascript,或者MessageChannel,發給WebView
- 13.WebView接收到native發過來的消息,然後展示到dom中。
以上流程,第4步還未完成,是以,通過v8直接運作的
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.");
}
})();
如果通過MessageChannel進行通信,
v8_demo.html
的示範代碼如下:
<h1>H5與V8引擎測試頁面</h1>
<a href="javascript:void(0)" class="btn read" id="a1">v8測試</a
><br /><br /><br />
<body>
<div id="text"></div>
<br /><br /><br />
<div id="container"></div>
</body>
<!-- 在這裡注入JSBridge,由webview攔截這個連結,然後替換成assets目錄的xbridge.js檔案-->
<script src="https://www.baidu.com/xbridge.js"></script>
<script>.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對象,将native發過來的h5Port引用儲存起來
window.__my_port__ = messageEvent.ports[0];
//設定消息
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問題
類似console.log的方式,通過native實作
20.借助Glide圖檔加載架構對webview的圖檔進行緩存
第12部分講了基于離線包(packageApp)級别的緩存,将前端的html、js、css等進行了統一的緩存。在APP啟動的時,将離線包加載到記憶體中。當Webview加載H5頁面時,根據虛拟域名比對,加載本地的資源。但是,離線包在進行網絡請求之後,會開始渲染服務端json資料,這裡面有一些圖檔資源。我們如何對圖檔進行緩存呢?native通過圖檔加載架構(比如Glide)實作圖檔的多級緩存及複用,WebView則沒有這麼能力,本小節就是通過Glide實作圖檔資源的攔截和緩存。
- 接入Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
- 對圖檔url進行判斷,使用Glide加載圖檔
完整代碼如下,主要是通過Glide将圖檔加載為bitmap,然後建構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())) {
//不在支援的緩存範圍内
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進行壓縮轉換成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級别的緩存&預加載
第12部分講了基于離線包(packageApp)級别的緩存,20講了基于Glide對圖檔資源的緩存。那麼,如果不是離線包,是線上頁面,如何對線上頁面的文檔、js、css以及其他檔案進行緩存呢?我們可以通過OkHttp替代WebView的網絡請求,然後使用OkHttp的緩存政策,來緩存WebView中需要加載的url資源。
- 建立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();
}
}
- 對url資源進行攔截,然後使用OkHttp進行加載和緩存
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() {
//設定緩存的目錄檔案
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會使用DiskLruCache緩存
int cacheSize = 20 * 1024 * 1024; // 20 MiB
Cache cache = new Cache(httpCacheDirectory, cacheSize);
//設定緩存
mHttpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(new MyOkHttpCacheInterceptor())
.cache(cache)
.build();
}
/**
* 針對url級别的緩存,包括主文檔,圖檔,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())) {
//不在支援的緩存範圍内
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中設定
...
@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);
}
- 預加載緩存
...
//記憶體級别的預加載緩存
private static LruCache<String, byte[]> preLoadCache = new LruCache<>(100);
...
/**
* 預加載資源
*
* @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 {
...
//預加載
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;
}
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);