1. 程式人生 > >Android WebView: 效能優化不得不說的事

Android WebView: 效能優化不得不說的事

Mo說:大家通過前兩篇文章想必都能順利的 get 到 WebView 與 JavaScript 互動的技能了。現在 App 嵌入 H5 頁面已經是稀鬆平常的事情了,開發者要面對 WebView 也越來越多的爆發出來,比如頁面載入慢,記憶體洩露,不同 Android 系統版本採用了不同核心的相容問題等等。 所以當我們使用了 WebView 這個元件的時候,效能優化的事情就不能不提上議程了。這篇文章我們就針對上述問題來總結下 Android WebView 效能優化的常見方法。

頁面載入速度優化

影響頁面載入速度的因素有非常多,我們在對 WebView 載入一個網頁的過程進行除錯發現,每次載入的過程中都會有較多的網路請求,除了 web 頁面自身的 URL 請求,還會有 web 頁面外部引用的JS、CSS、字型、圖片等等都是個獨立的 http 請求。這些請求都是序列的,這些請求加上瀏覽器的解析、渲染時間就會導致 WebView 整體載入時間變長,消耗的流量也對應的真多。接下來我們就來說說幾種優化方案來是怎麼解決這個問題的。

選擇合適的 WebView 快取

WebView 快取看似就是開啟幾個開關的問題,但是要弄懂這幾種快取機制還是很有深度。下圖是騰訊某工程師總結六種 H5 常用的快取機制的優勢及適用場景。


圖片來自Bugly
瀏覽器快取機制:

主要前端負責,Android 端不需要進行特別的配置。

Dom Storage(Web Storage)儲存機制:

配合前端使用,使用時需要開啟 DomStorage 開關。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDomStorageEnabled(true
);
Web SQL Database 儲存機制:

雖然已經不推薦使用了,但是為了相容性,還是提供下 Android 端使用的方法

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDatabaseEnabled(true);
final String dbPath = getApplicationContext().getDir("db",Context.MODE_PRIVATE).getPath();
webSettings.setDatabasePath(dbPath)
Application Cache 儲存機制

Application Cache(簡稱 AppCache)似乎是為支援 Web App 離線使用而開發的快取機制。它的快取機制類似於瀏覽器的快取(Cache-Control 和 Last-Modified)機制,都是以檔案為單位進行快取,且檔案有一定更新機制。但 AppCache 是對瀏覽器快取機制的補充,不是替代。

不過根據官方文件,AppCache 已經不推薦使用了,標準也不會再支援。現在主流的瀏覽器都是還支援 AppCache的,以後就不太確定了。同樣給出 Android 端啟用 AppCache 的程式碼。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setAppCacheEnabled(true);
final String cachePath = getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(5*1024*1024);
Indexed Database 儲存機制

IndexedDB 也是一種資料庫的儲存機制,但不同於已經不再支援的 Web SQL Database。IndexedDB 不是傳統的關係資料庫,可歸為 NoSQL 資料庫。IndexedDB 又類似於 Dom Storage 的 key-value 的儲存方式,但功能更強大,且儲存空間更大。

Android 在4.4開始加入對 IndexedDB 的支援,只需開啟允許 JS 執行的開關就好了。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
File System API

File System API 是 H5 新加入的儲存機制。它為 Web App 提供了一個虛擬的檔案系統,就像 Native App 訪問本地檔案系統一樣。由於安全性的考慮,這個虛擬檔案系統有一定的限制。Web App 在虛擬的檔案系統中,可以進行檔案(夾)的建立、讀、寫、刪除、遍歷等操作。很可惜到目前,Android 系統的 WebView 還不支援 File System API。

簡單的介紹完了上面六種 H5 常用的快取模式,想必大家能對 Android WebView 所支援的快取模式有個粗略的瞭解。如果想和前端更好的配合使用 Android WebView 所支援的快取,建議看下這篇文章《H5 快取機制淺析 移動端 Web 載入效能優化》

常用資源預載入

上面介紹的快取技術,能優化二次啟動 WebView 的載入速度,那首次載入 H5 頁面的速度該怎麼優化呢?上面分析了一次載入過程會有許多外部依賴的 JS、CSS、圖片等資源需要下載,那我們能不能提前將這些資源下載好,等H5 載入時直接替換呢?

好在從 API 11(Android 3.0)開始,WebView 引入了 shouldInterceptRequest 函式,這個函式有兩種過載。

  • public WebResourceResponse shouldInterceptRequest(WebView webView, String url) 從 API 11 引入,API 21 廢棄
  • public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) 從 API 21 開始引入

考慮到目前大多數 App 還要支援 API 14,所以還是使用 shouldInterceptRequest (WebView view, String url) 為例。

WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView webView, final String url) {
                WebResourceResponse response = null;
                  // 檢查該資源是否已經提前下載完成。我採用的策略是在應用啟動時,使用者在 wifi 的網路環境下                // 提前下載 H5 頁面需要的資源。
                boolean resDown = JSHelper.isURLDownValid(url);
                if (resDown) {
                    jsStr = JsjjJSHelper.getResInputStream(url);
                    if (url.endsWith(".png")) {
                        response = getWebResourceResponse(url, "image/png", ".png");
                    } else if (url.endsWith(".gif")) {
                        response = getWebResourceResponse(url, "image/gif", ".gif");
                    } else if (url.endsWith(".jpg")) {
                        response = getWebResourceResponse(url, "image/jepg", ".jpg");
                    } else if (url.endsWith(".jepg")) {
                        response = getWebResourceResponse(url, "image/jepg", ".jepg");
                    } else if (url.endsWith(".js") && jsStr != null) {
                        response = getWebResourceResponse("text/javascript", "UTF-8", ".js");
                    } else if (url.endsWith(".css") && jsStr != null) {
                        response = getWebResourceResponse("text/css", "UTF-8", ".css");
                    } else if (url.endsWith(".html") && jsStr != null) {
                        response = getWebResourceResponse("text/html", "UTF-8", ".html");
                    }
                }
                // 若 response 返回為 null , WebView 會自行請求網路載入資源。 
                return response;
            }
        });

private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
        WebResourceResponse response = null;
        try {
            response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(getJSPath() + TPMD5.md5String(url) + style)));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return response;
    }

public String getJsjjJSPath() {
        String splashTargetPath = JarEnv.sApplicationContext.getFilesDir().getPath() + "/JS";
        if (!TPFileSysUtil.isDirFileExist(splashTargetPath)) {
            TPFileSysUtil.createDir(splashTargetPath);
        }
        return splashTargetPath + "/";
    }

常用 JS 本地化及延遲載入

比預載入更粗暴的優化方法是直接將常用的 JS 指令碼本地化,直接打包放入 apk 中。比如 H5 頁面獲取使用者資訊,設定標題等通用方法,就可以直接寫入一個 JS 檔案,放入 asserts 資料夾,在 WebView 呼叫了onPageFinished() 方法後進行載入。需要注意的是,在該 JS 檔案中需要寫入一個 JS 檔案載入完畢的事件,這樣前端才能接受都愛 JS 檔案已經種植完畢,可以呼叫 JS 中的方法了。 附上一段本地化的 JS 程式碼。

javascript: ;
(function() {
  try{
    window.JSBridge = {
      'invoke': function(name) {
        var args = [].slice.call(arguments, 1),
          callback = args.pop(),
          params, obj = this[name];
        if (typeof callback !== 'function') {
          params = callback;
          callback = function() {}
        } else {
          params = args[0]
        } if (typeof obj !== 'object' || typeof obj.func !== 'function') {
          callback({
            'err_msg': 'system:function_not_exist'
          });
          return
        }
        obj.callback = callback;
        obj.params = params;
        obj.func(params)
      },
      'on': function(event, callback) {
        var obj = this['on' + event];
        if (typeof obj !== 'object') {
          callback({
            'err_msg': 'system:function_not_exist'
          });
          retrun
        }
        if (typeof callback !== 'undefined') obj.callback = callback
      },
      'login': {
        'func': function(params) {
          prompt("login", JSON.stringify(params))
        },
        'params': {},
        'callback': function(res) {}
      },
      'settitle': {
        'func': function(params) {
          prompt("settitle",JSON.stringify(params))
        },
        'params': {},
        'callback': function(res) {}
      },
    }catch(e){
    alert('demo.js error:'+e);
  }
  var readyEvent = document.createEvent('Events');
  readyEvent.initEvent('JSBridgeReady', true, true);
  document.dispatchEvent(readyEvent)
})();

關於 JS 延遲載入

Android 的 OnPageFinished 事件會在 Javascript 指令碼執行完成之後才會觸發。如果在頁面中使 用JQuery,會在處理完 DOM 物件,執行完 $(document).ready(function() {}); 事件自會後才會渲染並顯示頁面。而同樣的頁面在 iPhone 上卻是載入相當的快,因為 iPhone 是顯示完頁面才會觸發指令碼的執行。所以我們這邊的解決方案延遲 JS 指令碼的載入,這個方面的問題是需要Web前端工程師幫忙優化的。

使用第三方 WebView 核心

WebView 的相容性一直也是困擾我們 Android 開發者的一個大問題,不說 Android 4.4 版本 Google 使用了Chromium 替代 Webkit 作為 WebView 核心,就看看國內眾多的第三方 ROM 都有可能會對原生的 WebView 做出修改,這時候如果出現相容問題,是非常難定位到問題和解決的。

在一次使用微信瀏覽訂閱公眾號文章的過程中,發現微信的 H5 頁面有一行 『QQ 瀏覽器 X5 核心提供技術支援』。順著這個線索我就找到了騰訊瀏覽服務。發現騰訊已經把這個功能開放了,而且整合的 SDK 很小隻有212 KB。這是很驚人的,通過介紹才發現這個 SDK 是可以共享微信和手機 QQ 的 X5 核心。這就很方便了,作為國內市場最不可或缺的兩個 App,我們能只需要整合一個很小的 SDK 就可以共享使用 X5 核心了,不得不說騰訊還是很有想法的。

簡單摘錄些功能亮點,想必能讓大家高潮一番。詳細內容大家可以直接到騰訊瀏覽服務看看,我相信不會讓你們失望的。

網頁瀏覽能力

Web頁面crash率降低75%

頁面開啟速度提升35%

流量節省60%

閱讀模式

去除網頁中廣告等雜質

優化文章的閱讀體驗

檔案開啟能力

包括會話頁的互傳檔案及郵件中附件

支援doc、ppt、xls、pdf等辦公格式

支援jpg、gif、png、bmp等圖片格式

支援zip、rar等壓縮檔案

支援mp3、mp4、RMVB等音視訊格式

視訊選單能力

支援螢幕調節等常規視訊選單功能

靈活切換全屏&小窗功能

WebView 導致的記憶體洩露

Android 中的 WebView 存在很大的相容性問題,不僅僅是 Android 系統版本的不同對 WebView 產生很大的差異,另外不同的廠商出貨的 ROM 裡面 WebView 也存在著很大的差異。更嚴重的是標準的 WebView 存在記憶體洩露的問題,看這裡WebView causes memory leak - leaks the parent Activity。所以通常根治這個問題的辦法是為 WebView 開啟另外一個程序,通過 AIDL 與主程序進行通訊,WebView 所在的程序可以根據業務的需要選擇合適的時機進行銷燬,從而達到記憶體的完整釋放。

這段話來自胡凱翻譯的 Google Android 記憶體優化之 OOM 。這裡提到的讓 WebView 獨立執行在一個程序裡,用完 WebView 後直接銷燬這個程序,即使記憶體洩露了,也不會影響到主程序。微信,手 Q 等 App 也採用了這個方案。但是這就涉及到了跨程序通訊,處理起來就比較麻煩。

另外個解決方案,就是使用自己封裝的 WebView,比如上面提到的 X5 核心,且使用 WebView 的時候,不在 XML 裡面宣告,而是在程式碼中直接 new 出來,傳入 application context 來防止 activity 引用被濫用。

WebView webView =  new WebView(getContext().getApplicationContext());
webFrameLayout.addView(webView, 0);

在使用了這個方式後,基本上 90% 的 WebView 記憶體洩漏的問題便得以解決。

上面兩個方案,大家可以結合自己的專案情況選擇。另外對 WebView 記憶體洩露原因感興趣的可以看看這篇文章。

參考文章

相關閱讀

到這裡,Android WebView 系列文章就告一段落了,大家還有什麼想了解的或者對文中什麼知識點有疑惑的,歡迎留言探討。