前言
現在許多app都嵌入了H5頁面, 然而WebView加載速度慢這個問題卻一直影響着用戶的體驗, 所以本文就如何提高H5頁面的加載速度展開討論。
問題原因
首先我們需要知道爲什麼WebView的加載速度那麼慢。H5頁面的渲染速度其實主要取決於兩個
- js解析效率
如果js文件較多、解析比較複雜, 就會導致渲染速度較慢。或者手機的硬件性能比較差的話, 也會導致渲染速度比較慢。 - 頁面資源的下載
一般加載一個H5頁面, 都會產生較多的網絡請求, 如圖片、js文件、css文件等, 需要將這些資源都下載完成之後才能完成渲染, 這樣也會導致頁面渲染速度變慢
對於上面的第一點, 其實主要是由前端代碼和手機硬件決定的, 因爲我們這裏討論的是對於app的性能優化, 暫時不考慮, 所以我們可以從第二點做文章, 主要思路就是一些資源文件都使用App本地資源, 而不需要從網絡下載, 從而提高頁面的打開速度。
方案實現
騰訊出品的一個輕量級的高性能的Hybrid框架,專注於提升頁面首屏加載速度,完美支持靜態直出頁面和動態直出頁面,兼容離線包等方案。優點是性能好, 速度快, 大廠出品, 缺點是配置複雜, 同時需要前後端接入。
首先在build.gradle導入
implementation 'com.tencent.sonic:sdk:3.1.0'
代碼準備:
public class SonicRuntimeImpl extends SonicRuntime {
public SonicRuntimeImpl(Context context) {
super(context);
}
/**
* 獲取用戶UA信息
* @return
*/
@Override
public String getUserAgent() {
return "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Mobile Safari/537.36";
}
/**
* 獲取用戶ID信息
* @return
*/
@Override
public String getCurrentUserAccount() {
return "sonic-demo-master";
}
@Override
public String getCookie(String url) {
CookieManager cookieManager = CookieManager.getInstance();
return cookieManager.getCookie(url);
}
@Override
public void log(String tag, int level, String message) {
switch (level) {
case Log.ERROR:
Log.e(tag, message);
break;
case Log.INFO:
Log.i(tag, message);
break;
default:
Log.d(tag, message);
}
}
@Override
public Object createWebResourceResponse(String mimeType, String encoding, InputStream data, Map<String, String> headers) {
WebResourceResponse resourceResponse = new WebResourceResponse(mimeType, encoding, data);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
resourceResponse.setResponseHeaders(headers);
}
return resourceResponse;
}
@Override
public void showToast(CharSequence text, int duration) {
}
@Override
public void notifyError(SonicSessionClient client, String url, int errorCode) {
}
@Override
public boolean isSonicUrl(String url) {
return true;
}
@Override
public boolean setCookie(String url, List<String> cookies) {
if (!TextUtils.isEmpty(url) && cookies != null && cookies.size() > 0) {
CookieManager cookieManager = CookieManager.getInstance();
for (String cookie : cookies) {
cookieManager.setCookie(url, cookie);
}
return true;
}
return false;
}
@Override
public boolean isNetworkValid() {
return true;
}
@Override
public void postTaskToThread(Runnable task, long delayMillis) {
Thread thread = new Thread(task, "SonicThread");
thread.start();
}
@Override
public File getSonicCacheDir() {
if (BuildConfig.DEBUG) {
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "sonic/";
File file = new File(path.trim());
if(!file.exists()){
file.mkdir();
}
return file;
}
return super.getSonicCacheDir();
}
@Override
public String getHostDirectAddress(String url) {
return null;
}
}
public class SonicJavaScriptInterface {
private final SonicSessionClientImpl sessionClient;
private final Intent intent;
public static final String PARAM_CLICK_TIME = "clickTime";
public static final String PARAM_LOAD_URL_TIME = "loadUrlTime";
public SonicJavaScriptInterface(SonicSessionClientImpl sessionClient, Intent intent) {
this.sessionClient = sessionClient;
this.intent = intent;
}
@JavascriptInterface
public void getDiffData() {
// the callback function of demo page is hardcode as 'getDiffDataCallback'
getDiffData2("getDiffDataCallback");
}
@JavascriptInterface
public void getDiffData2(final String jsCallbackFunc) {
if (null != sessionClient) {
sessionClient.getDiffData(new SonicDiffDataCallback() {
@Override
public void callback(final String resultData) {
Runnable callbackRunnable = new Runnable() {
@Override
public void run() {
String jsCode = "javascript:" + jsCallbackFunc + "('"+ toJsString(resultData) + "')";
sessionClient.getWebView().loadUrl(jsCode);
}
};
if (Looper.getMainLooper() == Looper.myLooper()) {
callbackRunnable.run();
} else {
new Handler(Looper.getMainLooper()).post(callbackRunnable);
}
}
});
}
}
@JavascriptInterface
public String getPerformance() {
long clickTime = intent.getLongExtra(PARAM_CLICK_TIME, -1);
long loadUrlTime = intent.getLongExtra(PARAM_LOAD_URL_TIME, -1);
try {
JSONObject result = new JSONObject();
result.put(PARAM_CLICK_TIME, clickTime);
result.put(PARAM_LOAD_URL_TIME, loadUrlTime);
return result.toString();
} catch (Exception e) {
}
return "";
}
/*
* * From RFC 4627, "All Unicode characters may be placed within the quotation marks except
* for the characters that must be escaped: quotation mark,
* reverse solidus, and the control characters (U+0000 through U+001F)."
*/
private static String toJsString(String value) {
if (value == null) {
return "null";
}
StringBuilder out = new StringBuilder(1024);
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
switch (c) {
case '"':
case '\\':
case '/':
out.append('\\').append(c);
break;
case '\t':
out.append("\\t");
break;
case '\b':
out.append("\\b");
break;
case '\n':
out.append("\\n");
break;
case '\r':
out.append("\\r");
break;
case '\f':
out.append("\\f");
break;
default:
if (c <= 0x1F) {
out.append(String.format("\\u%04x", (int) c));
} else {
out.append(c);
}
break;
}
}
return out.toString();
}
}
最後在activity裏調用:
public void initWebViewInfo() {
Intent intent = getIntent();
String url = intent.getStringExtra(PARAM_URL);
showLoadding = intent.getBooleanExtra("showLoadding", showLoadding);
Log.d(TAG, "showLoadding=" + showLoadding);
int mode = intent.getIntExtra(PARAM_MODE, -1);
if (TextUtils.isEmpty(url) || -1 == mode) {
finish();
return;
}
// init sonic engine if necessary, or maybe u can do this when application created
if (!SonicEngine.isGetInstanceAllowed()) {
SonicEngine.createInstance(new SonicRuntimeImpl(getApplication()), new SonicConfig.Builder().build());
}
SonicSessionClientImpl sonicSessionClient = null;
// if it's sonic mode , startup sonic session at first time
if (MainActivity.MODE_DEFAULT != mode) { // sonic mode
SonicSessionConfig.Builder sessionConfigBuilder = new SonicSessionConfig.Builder();
sessionConfigBuilder.setSupportLocalServer(true);
// if it's offline pkg mode, we need to intercept the session connection
if (MainActivity.MODE_SONIC_WITH_OFFLINE_CACHE == mode) {
sessionConfigBuilder.setCacheInterceptor(new SonicCacheInterceptor(null) {
@Override
public String getCacheData(SonicSession session) {
return null; // offline pkg does not need cache
}
});
sessionConfigBuilder.setConnectionInterceptor(new SonicSessionConnectionInterceptor() {
@Override
public SonicSessionConnection getConnection(SonicSession session, Intent intent) {
return new OfflinePkgSessionConnection(WebBrowserActivity.this, session, intent);
}
});
}
// create sonic session and run sonic flow
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
if (null != sonicSession) {
sonicSession.bindClient(sonicSessionClient = new SonicSessionClientImpl());
} else {
Toast.makeText(this, "create sonic session fail!", Toast.LENGTH_LONG).show();
}
}
webview.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (sonicSession != null) {
sonicSession.getSessionClient().pageFinish(url);
}
}
@TargetApi(21)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (sonicSession != null) {
return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
}
return null;
}
});
WebSettings webSettings = webview.getSettings();
// 設置與Js交互的權限
webSettings.setJavaScriptEnabled(true);
webview.removeJavascriptInterface("searchBoxJavaBridge_");
intent.putExtra(SonicJavaScriptInterface.PARAM_LOAD_URL_TIME, System.currentTimeMillis());
webview.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
// init webview settings
webSettings.setAllowContentAccess(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
// 設置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webview.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
if (url.contains("baidu")) {
finish();
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public void onPageStarted(WebView webView, String s, Bitmap bitmap) {
super.onPageStarted(webView, s, bitmap);
if (showLoadding) {
llLoadding.setVisibility(View.VISIBLE);
}
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 不要使用super,否則有些手機訪問不了,因爲包含了一條 handler.cancel()
// super.onReceivedSslError(view, handler, error);
// 接受所有網站的證書,忽略SSL錯誤,執行訪問網頁
handler.proceed();
}
@Override
public void onReceivedError(WebView webView, WebResourceRequest webResourceRequest, WebResourceError webResourceError) {
super.onReceivedError(webView, webResourceRequest, webResourceError);
}
@Override
public void onPageFinished(WebView webView, String s) {
super.onPageFinished(webView, s);
if (showLoadding) {
llLoadding.setVisibility(View.GONE);
}
}
});
// webview is ready now, just tell session client to bind
if (sonicSessionClient != null) {
sonicSessionClient.bindWebView(webview);
sonicSessionClient.clientReady();
} else { // default mode
webview.loadUrl(url);
}
}
佈局xml裏還是用原生webview接入就行了!