背景
在回家的地鐵上使用自家應用H5相關功能時,可能由於網絡原因導致體驗較差,在使用微信、今日頭條App時,感覺很流暢,基本做到了秒開,然后就想了解下業內H5秒開方案。
問題原因
-
文件下載耗時:包括html、css、js、圖片等
-
頁面渲染耗時:頁面渲染,解析js、css文件等
-
WebView創建耗時:首次創建WebView耗時大約需要500ms左右,第二次創建耗時大約需要20ms左右
常見解決方案
WebView緩存相關
-
瀏覽器緩存機制,通過請求頭控制緩存
-
Dom Storgage(Web Storage)存儲機制
-
Web SQL Database 存儲機制
-
Application Cache(AppCache)機制
-
Indexed Database (IndexedDB)
可通過以下代碼實現:
WebSettings webSettings = myWebView.getSettings();
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setDomStorageEnabled(true); webSettings.setDatabaseEnabled(true); final String dbPath = getApplicationContext().getDir("db", Context.MODE_PRIVATE).getPath(); webSettings.setDatabasePath(dbPath); webSettings.setAppCacheEnabled(true); final String cachePath = getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath(); webSettings.setAppCachePath(cachePath); webSettings.setAppCacheMaxSize(5*1024*1024); webSettings.setJavaScriptEnabled(true);
開源方案
-
CacheWebView: 通過攔截shouldInterceptRequest方法使用okhttp的緩存功能實現,使用簡單可配置。
-
VasSonic:騰訊出品的一個輕量級的高性能的Hybrid框架,專注於提升頁面首屏加載速度,完美支持靜態直出頁面和動態直出頁面,支持預加載兼容離線包等方案。優點是性能好,速度快,大廠出品,缺點是配置復雜, 同時需要前后端接入。
今日頭條方案
先來看下今日頭條的效果,第二次斷網打開頁面做到了秒開的效果:

今日頭條針對自己平台的文章詳情頁做了很多優化,具體包括以下幾點:
-
內置文章詳情頁所需的css、js等文件,並可以控制版本
-
WebView預創建
-
預加載包含文章詳情頁所需的css、js的空html
-
在列表頁預加載文章詳情所需的內容使用LRU內存緩存並保存到本地數據庫
-
在文章詳情頁獲取預創建的WebView(預加載了html),直接調用js設置頁面內容
-
通過js控制圖片的顯示,圖片懶加載(當圖片在可見區域或即將可見才會加載圖片),點擊加載圖片等
-
Html中的圖片通過ContentProvider獲取使用Fresco下載的圖片
內置所需文件
<img0.5847255369928401" data-src="http://img01.store.sogou.com/net/a/04/link?appid=100520029&url=http://img01.store.sogou.com/net/a/04/link?appid=100520029&url=https://mmbiz.qpic.cn/mmbiz/cmOLumrNib1eOO0yAWeZFdc5DGtcsYlJf81Tho9FnxDO7jYNtuIw7S3FmYibYiceptkRCMGu7puDPDUYw7j9awKWg/640?wx_fmt=other" data-type="other" data-w="419" title="" _width="419px" src="https://www.itcodemonkey.com/data/upload/portal/20190625/1561446899710621.jpg" data-fail="0">
WebView預創建,資源預加載
首次創建WebView要比第二次創建耗時慢很多,原因估計是WebView首次創建需要初始化一些靜態資源,第二次創建時不需要初始化,所以第二次創建耗時要少很多。
使用Context包裝類MutableContextWrapper傳入Application預創建WebView對象,然后預加載一個使用java代碼拼接的html,提前對js、css資源進行解析。等獲取預創建的WebView時再替換為Activity的context。
public class PreloadWebView { private PreloadWebView(){} private static final int CACHED_WEBVIEW_MAX_NUM = 2; private static final Stack<WebView> mCachedWebViewStack = new Stack<>(); public static PreloadWebView getInstance(){ return Holder.INSTANCE; } private static class Holder{ private static final PreloadWebView INSTANCE = new PreloadWebView(); } /** * 創建WebView實例 * 用了applicationContext */ public void preload() { L.d("webview preload"); Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { if (mCachedWebViewStack.size() < CACHED_WEBVIEW_MAX_NUM) { mCachedWebViewStack.push(createWebView()); } return false; } }); } private WebView createWebView() { WebView webview = new WebView(new MutableContextWrapper(App.getApp())); webview.getSettings().setJavaScriptEnabled(true); webview.loadDataWithBaseURL("file:///android_asset/article/?item_id=0&token=0",getHtml(),"text/html","utf-8","file:///android_asset/article/?item_id=0&token=0"); return webview; } private static String getHtml() { StringBuilder builder = new StringBuilder(); builder.append("<!DOCTYPE html>\n"); builder.append("<html>\n"); builder.append("<head>\n"); builder.append("<meta charset=\"utf-8\">\n"); builder.append("<meta name=\"viewport\" content=\"initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n"); builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\""); builder.append("file:///android_asset/article/css/android.css"); builder.append("\">\n</head>\n"); builder.append("<body class=\"font_m\"><header></header><article></article><footer></footer>"); builder.append("<script type=\"text/javascript\" src=\""); builder.append("file:///android_asset/article/js/lib.js"); builder.append("\"></script>"); builder.append("<script type=\"text/javascript\" src=\""); builder.append("file:///android_asset/article/js/android.js"); builder.append("\" ></script>\n"); builder.append("</body>\n"); builder.append("</html>\n"); return builder.toString(); } /** * 從緩存池中獲取合適的WebView * * @param context activity context * @return WebView */ public WebView getWebView(Context context) { // 為空,直接返回新實例 if (mCachedWebViewStack == null || mCachedWebViewStack.isEmpty()) { WebView web = createWebView(); MutableContextWrapper contextWrapper = (MutableContextWrapper) web.getContext(); contextWrapper.setBaseContext(context); return web; } WebView webView = mCachedWebViewStack.pop(); // webView不為空,則開始使用預創建的WebView,並且替換Context MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext(); contextWrapper.setBaseContext(context); return webView; } }
本地數據庫緩存
使用數據庫進行持久化。
<img0.516" data-src="http://img01.store.sogou.com/net/a/04/link?appid=100520029&url=http://img01.store.sogou.com/net/a/04/link?appid=100520029&url=https://mmbiz.qpic.cn/mmbiz/cmOLumrNib1eOO0yAWeZFdc5DGtcsYlJfhT8kDXaSWwicwQuUp89j5bTiaNC7HyWedPhu5jKkRVM0KdTjDZFyBGwg/640?wx_fmt=other" data-type="other" data-w="1000" title="" _width="677px" src="https://www.itcodemonkey.com/data/upload/portal/20190625/1561446900162107.jpg" data-fail="0">
廣州VI設計公司https://www.houdianzi.com
圖片資源的顯示
使用ContentProvider獲取圖片資源:
content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3
上面的ContentProvider的uri會調用對應ContentProvider的openFile方法,別忘了在清單文件中注冊。
public class ImageProvider extends ContentProvider { ... public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { File file = getFile(uri); return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) ; } ... }
中間字符串使用zip壓縮,使用下面的代碼解壓zip數據的代碼:
static final byte[] buffer = new byte[4096]; public static final String unzip(String str) { try { Inflater inflater = new Inflater(); inflater.setInput(Base64.decode(str, 8)); int size = inflater.inflate(buffer); inflater.end(); String temp = new String(buffer, 0, size, "UTF-8"); return temp; } catch (Exception e) { e.printStackTrace(); } return ""; }
解壓后的數據如下:
{
"origin": { "uri": "large/pgc-image/8e72c19ce0f2456880947531d5bbb230", "urls": ["http://p1-tt.byteimg.com/large/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p1-tt.byteimg.com/large/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p3-tt.byteimg.com/large/pgc-image/8e72c19ce0f2456880947531d5bbb230"] }, "webp_origin": { "uri": "details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "urls": ["http://p99.pstatp.com/details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p6-tt.byteimg.com/details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p1-tt.byteimg.com/details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp"] }, "thumb": { "uri": "thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230", "urls": ["http://p9-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p3-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p1-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230"] }, "webp_thumb": { "uri": "thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "urls": ["http://p1-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p3-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p6-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp"] } }
uri的最后兩個片段表示文章id及圖片索引,用於通過js通知頁面圖片加載完成。通過解析content的uri中的數據獲取Fresco下載的緩存文件,返回一個ParcelFileDescriptor對象即可。
