在 webview 中加載 vue 項(xiàng)目,加載時(shí)間將近5~6秒,嚴(yán)重影響用戶體驗(yàn),為此,本文將探索 webview 性能優(yōu)化相關(guān)技術(shù)細(xì)節(jié),將用戶體驗(yàn)優(yōu)化到極致,做到原生app效果一般。
1、webview 優(yōu)化
1.1 創(chuàng)建 BaseWebView.java
import android.content.Context;
import android.os.Build;
import android.util.Log;
import android.view.MotionEvent;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.annotation.NonNull;
public class BaseWebView extends WebView {
private static final String TAG = "BaseWebView:";
public WChromeClient wChromeClient;
public BaseWebView(@NonNull Context context, String url) {
super(context);
wChromeClient = new WChromeClient(context);
initWebView(context, url);
}
// 初始化
private void initWebView(Context context, String url) {
this.setBackgroundColor(0); // 設(shè)置背景
this.setDrawingCacheEnabled(true); // 啟用或禁用圖形緩存
this.setWebViewClient(new WViewClient(context)); // 處理各種通知、請(qǐng)求事件
this.setWebChromeClient(wChromeClient); // 處理解析,渲染網(wǎng)頁(yè)
this.addJavascriptInterface(new JSInterface(context, this),"jsWebView"); // 設(shè)置 js 調(diào)用接口
WebSettings settings = this.getSettings(); // webView 配置項(xiàng)
settings.setUseWideViewPort(true); // 是否啟用對(duì)視口元標(biāo)記的支持
settings.setJavaScriptEnabled(true); // 是否啟用 JavaScript
settings.setDomStorageEnabled(true); // 是否啟用本地存儲(chǔ)(允許使用 localStorage 等)
settings.setAllowFileAccess(true); // 是否啟用文件訪問(wèn)
settings.setAppCacheEnabled(true); // 是否應(yīng)啟用應(yīng)用程序緩存
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setAppCacheMaxSize(1024*1024*8); // 設(shè)置應(yīng)用程序緩存內(nèi)容的最大大小
String appCachePath = context.getApplicationContext().getCacheDir().getAbsolutePath(); // 緩存地址
settings.setAppCachePath(appCachePath); // 設(shè)置緩存地址
settings.setAllowContentAccess(true); // 是否啟用內(nèi)容 URL 訪問(wèn)
settings.setJavaScriptCanOpenWindowsAutomatically(true); // 是否允許 JS 彈窗
settings.setMediaPlaybackRequiresUserGesture(false); // 是否需要用戶手勢(shì)來(lái)播放媒體
settings.setLoadWithOverviewMode(true); // 是否以概覽模式加載頁(yè)面,即按寬度縮小內(nèi)容以適應(yīng)屏幕
settings.setBuiltInZoomControls(true); // 是否應(yīng)使用其內(nèi)置的縮放機(jī)制
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
// Hide the zoom controls for HONEYCOMB+
settings.setDisplayZoomControls(false); // 是否應(yīng)顯示屏幕縮放控件
}
settings.setAllowFileAccessFromFileURLs(true); // 是否應(yīng)允許在文件方案 URL 上下文中運(yùn)行的 JavaScript 訪問(wèn)來(lái)自其他文件方案 URL 的內(nèi)容
settings.setAllowUniversalAccessFromFileURLs(true); // 是否應(yīng)允許在文件方案URL上下文中運(yùn)行的 JavaScript 訪問(wèn)任何來(lái)源的內(nèi)容
this.loadUrl(url); // 設(shè)置訪問(wèn)地址
}
// 注入 js 腳本
public void injection(String js) {
this.post(() -> this.loadUrl("javascript:" + js + ";",null));
}
// 執(zhí)行 js 腳本
public void executeMethod(String method, String data) {
this.post(() -> this.loadUrl("javascript:" + method + "('" + data + "');",null));
}
}
1.2 創(chuàng)建 WChromeClient.java
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.ConsoleMessage;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.PermissionRequest;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
public class WChromeClient extends WebChromeClient {
private static final String TAG = "WChromeClient:";
private Context _c;
public WChromeClient(Context context) {
super();
_c = context;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
Log.d(TAG,"當(dāng)前加載進(jìn)度:" + newProgress);
}
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
Log.d(TAG,"網(wǎng)站標(biāo)題:"+ title);
}
// 響應(yīng) js 的 alert() 函數(shù)
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(_c);
b.setTitle("");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
b.setCancelable(false);
b.create().show();
return true;
}
// 響應(yīng) js 的 confirm() 函數(shù)
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(_c);
b.setTitle("");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
b.setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel());
b.create().show();
return true;
}
// 響應(yīng) js 的 prompt() 函數(shù)
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
final JsPromptResult result) {
result.confirm();
return super.onJsPrompt(view, url, message, message, result);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onPermissionRequest(PermissionRequest request) {
request.grant(request.getResources());
}
// 獲取 js 的 console 消息
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Log.w(TAG,consoleMessage.message());
return true;
}
}
1.3 創(chuàng)建 WViewClient.java
優(yōu)化重點(diǎn):通過(guò)攔截常用資源從本地直接返回,減少網(wǎng)絡(luò)請(qǐng)求下載的時(shí)間。
該方式需要 vue 項(xiàng)目使用第三方 js 庫(kù)分離打包來(lái)實(shí)現(xiàn)
import android.content.Context;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.util.Log;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.io.IOException;
import java.io.InputStream;
public class WViewClient extends WebViewClient {
private static final String TAG = "WViewClient:";
private Context _c;
public WViewClient(Context context) {
super();
_c = context;
}
// ssl 證書(shū)錯(cuò)誤
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (handler != null) {
handler.proceed(); // 忽略證書(shū)的錯(cuò)誤繼續(xù)加載頁(yè)面內(nèi)容,不會(huì)變成空白頁(yè)面
}
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
Log.i(TAG, description);
}
@Override
public void onPageFinished(WebView view, String url) {
// 開(kāi)始
Log.e(TAG,"開(kāi)始");
super.onPageFinished(view, url);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// 結(jié)束
Log.e(TAG,"結(jié)束");
super.onPageStarted(view, url, favicon);
}
// 請(qǐng)求攔截
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
// 判斷攔截資源的條件
InterceptRes ir = interceptResources(url);
if (ir != null) {
try {
// 獲得需要替換的資源(存放在assets文件夾中,如何創(chuàng)建 assets 文件夾請(qǐng)看下文)
InputStream inputStream = _c.getApplicationContext().getAssets().open(ir.assetsUrl);
// 替換資源
return new WebResourceResponse(ir.mimeType, "utf-8", inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, url);
}
private InterceptRes interceptResources(String url) {
InterceptRes interceptRes = null;
switch (url){
case "https://unpkg.com/element-ui/lib/theme-chalk/index.css":
interceptRes = new InterceptRes("css/element-ui-index.css","text/css");
break;
case "https://unpkg.com/vue@2":
interceptRes = new InterceptRes("js/vue.min.js","application/x-javascript");
break;
case "https://unpkg.com/vue-router@3":
interceptRes = new InterceptRes("js/vue-router.js","application/x-javascript");
break;
case "https://unpkg.com/vuex@3":
interceptRes = new InterceptRes("js/vuex.js","application/x-javascript");
break;
case "https://unpkg.com/element-ui/lib/index.js":
interceptRes = new InterceptRes("js/element-ui-index.js","application/x-javascript");
break;
case "https://unpkg.com/axios/dist/axios.min.js":
interceptRes = new InterceptRes("js/axios.min.js","application/x-javascript");
break;
default:
break;
}
return interceptRes;
}
private class InterceptRes {
String assetsUrl;
String mimeType;
InterceptRes(String assetsUrl,String mimeType) {
this.assetsUrl = assetsUrl;
this.mimeType = mimeType;
}
}
}
1.4 創(chuàng)建 assets
assets 用于存放本地資源,訪問(wèn)路徑為 file:///android_asset/index.html
在 main 目錄下右鍵 new -> Directory -> 選擇 assets
在 assets 目錄下 創(chuàng)建 css 和 js 目錄,然后將資源文件復(fù)制到此目錄即可。
1.5 創(chuàng)建 JSInterface.java
該類用于向 js 網(wǎng)頁(yè)提供調(diào)用 Android 方法
import android.content.Context;
import android.webkit.JavascriptInterface;
public class JSInterface {
private Context _c;
private MainActivity _m;
private BaseWebView _w;
public JSInterface(Context context, BaseWebView view) {
_c = context;
_m = (MainActivity) context;
_w = view;
}
// 注入js
@JavascriptInterface
public void testInject() {
String js = "alert();";
_w.injection(js);
}
// 執(zhí)行操作
@JavascriptInterface
public void testExecute() {
// TODO:在這里可以執(zhí)行Android程序方法和操作
_w.executeMethod("cbExecute", "test"); // 回調(diào)執(zhí)行 js 方法
}
}
1.5 實(shí)例化 BaseWebView
package com.hlzh.meeting;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private BaseWebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
webView = new BaseWebView(this);
setContentView(webView);
// 去除狀態(tài)欄
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
// 程序退出銷(xiāo)毀
@Override
protected void onDestroy() {
if (this.webView != null) {
webView.removeAllViews();
webView.destroy();
}
super.onDestroy();
}
long exitTime = 0;
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();//返回上一頁(yè)面
return;
} else {
if (System.currentTimeMillis() - exitTime > 2000) {
Toast.makeText(getApplicationContext(), "再按一次退出程序", Toast.LENGTH_SHORT).show();
exitTime = System.currentTimeMillis();
} else {
moveTaskToBack(true); // 返回主頁(yè)面,也可以完全退出程序
// finish();
// System.exit(0);
// android.os.Process.killProcess(android.os.Process.myPid());
}
}
}
}