Android—WebView加載速度優(yōu)化工程實(shí)踐

一少梁、混合開發(fā)的優(yōu)勢與缺陷

在混合開發(fā)大行其道的今天,很多頁面和功能都轉(zhuǎn)由前端實(shí)現(xiàn)考杉,客戶端只要在APP中嵌入一個(gè)WebView即可,同時(shí)前端開發(fā)的頁面對于Android和iOS端的效果是統(tǒng)一的舰始,省去了適配的困擾崇棠。

適合前端開發(fā)的界面主要有以下兩種:
1、新聞咨詢類頁面丸卷,這類頁面布局比較復(fù)雜枕稀,通過前端實(shí)現(xiàn)相對原生更為簡單。
2谜嫉、運(yùn)營活動類界面萎坷,這類頁面更新較為頻繁,前端迭代后可以直接上線沐兰,跳過了客戶端的發(fā)版流程哆档。

前端開發(fā)的優(yōu)勢顯而易見:開發(fā)敏捷、發(fā)版靈活住闯。但是它的缺點(diǎn)同樣明顯瓜浸,我認(rèn)為主要有以下3點(diǎn)。
第一個(gè)問題就是性能問題比原,冷啟APP后第一次新建WebView的耗時(shí)會讓用戶感受到明顯的卡頓插佛,而WebView.loadUrl(String url)的耗時(shí)更為嚴(yán)重,很容易出現(xiàn)白屏的現(xiàn)象春寿。
第二個(gè)問題是浪費(fèi)流量朗涩,雖然WebView內(nèi)核有緩存機(jī)制(例如兩次打開相同的頁面,第二次會快很多)绑改,但是對于新聞咨詢頁面來說谢床,很多css、js甚至圖片資源都是重復(fù)的厘线,但是內(nèi)核的緩存機(jī)制不一定能準(zhǔn)確地復(fù)用這些資源识腿。
第三個(gè)問題就是安全問題,部分前端頁面需要通過JSBridge調(diào)用原生API造壮,如果JavaScriptInterface不對調(diào)用方的域名進(jìn)行限制渡讼,某些惡意網(wǎng)址就可能調(diào)用原生的方法進(jìn)行入侵。

安全問題是依賴業(yè)務(wù)去解決的耳璧,如果沒有暴露相應(yīng)的方法成箫,惡意網(wǎng)址也就無從下手。而性能問題與流量問題則會影響WebView的加載速度旨枯,比較影響用戶體驗(yàn)蹬昌,下面從WebView的加載流程來看我們應(yīng)該如何解決這兩個(gè)問題來提高加載速度。

二攀隔、WebView加載頁面的流程

假設(shè)WebView嵌入在Activity中皂贩,從Activity啟動到顯示出前端頁面的第一幀栖榨,大致要經(jīng)歷以下階段。
① Activity啟動
② WebView新建與初始化
③ WebView.loadUrl(String url)
而當(dāng)調(diào)用WebView.loadUrl(String url)之后明刷,所有的資源請求都會經(jīng)過WebViewClient的shouldInterceptRequest(...)方法婴栽,這些資源請求包括但不局限于主html內(nèi)容、css辈末、js和圖片資源等愚争,應(yīng)用可以在該方法攔截資源請求并加載別的內(nèi)容。

可以看到加載頁面的流程是串行的本冲,主要分為兩個(gè)部分准脂,一是WebView的新建與初始化,二是WebView.loadUrl(String url)檬洞,那么只要我們縮短其中任一階段的耗時(shí)狸膏,就可以達(dá)到縮短耗時(shí)的目的。

三添怔、WebView加載速度優(yōu)化方案

這里以新聞資訊類頁面為例湾戳,目前這類頁面加載最快的就是頭條系A(chǔ)PP,例如“懂車帝”广料,在信息流內(nèi)點(diǎn)擊咨詢跳轉(zhuǎn)后直接展示咨詢詳情砾脑,不會像其他APP那樣出現(xiàn)過渡界面或白屏。

那么懂車帝究竟做了什么才能讓W(xué)ebView加載地如此之快艾杏?
首先對于新聞咨詢頁面來說韧衣,不同的頁面之間有很多重復(fù)的資源,這些資源可以直接保存在本地復(fù)用购桑。反編譯懂車帝APK之后會發(fā)現(xiàn)Assets中有很多的css畅铭、js和圖片文件,應(yīng)用可以攔截這部分的請求轉(zhuǎn)而加載這些文件勃蜘。
其次硕噩,WebView的新建與初始化也是比較耗時(shí)的,因此可以使用WebView緩存池進(jìn)行復(fù)用缭贡。
當(dāng)然還有最重要的一點(diǎn)就是預(yù)加載炉擅,在懂車帝信息流內(nèi),即使你斷網(wǎng)后點(diǎn)開一條咨詢阳惹,你會發(fā)現(xiàn)文字內(nèi)容還是正常加載谍失。也就是說,在信息流內(nèi)時(shí)莹汤,url的主html內(nèi)容已經(jīng)被下載到了內(nèi)存中快鱼,可以直接通過WebView.loadDataWithBaseUrl(...)加載。

接下來介紹這3種方案的具體實(shí)現(xiàn)以及對加載速度的影響,我在懂車帝中挑選了7篇咨詢攒巍,編寫Demo并統(tǒng)計(jì)其加載時(shí)間(這里統(tǒng)計(jì)的是點(diǎn)擊item到WebView加載進(jìn)度為100所需的時(shí)間),在沒有使用任何優(yōu)化的情況下荒勇,平均加載時(shí)間為1203ms柒莉。

3.1 資源緩存

上面提到,WebView 請求任何的資源時(shí)都會回調(diào)shouldInterceptRequest()方法沽翔,此時(shí)可以將 WebView 需要請求的資源替換為本地資源以提升加載速度兢孝。
本地資源可以保存在Assets或文件系統(tǒng)中,如果保存在Assets中仅偎,文件的安全性會得到保證跨蟹,沒有出錯(cuò)或者被修改的風(fēng)險(xiǎn),但是無法實(shí)時(shí)更新橘沥,只能依賴客戶端發(fā)版更新文件窗轩;如果保存在文件系統(tǒng)中,可以做到實(shí)時(shí)更新座咆,但是下載文件時(shí)可能出錯(cuò)痢艺,使用時(shí)需要對文件進(jìn)行校驗(yàn)。

這里以保存在Assets中為例介陶,當(dāng)WebView回調(diào)shouldInterceptRequest(...)時(shí)堤舒,如果發(fā)現(xiàn)當(dāng)前的文件可以使用Assets中的緩存,即可將其包裝成WebResourceResponse哺呜,如下所示舌缤。

mWebView.setWebViewClient(new WebViewClient() {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        // 根據(jù)url得到文件名
        String fileName = getFileNameByUrl(request.getUrl().toString());
        if (!fileInCache(fileName)) {
            // 如果當(dāng)前文件不在緩存列表中, 不使用緩存
            return super.shouldInterceptRequest(view, request);
        } else {
            // 當(dāng)前文件可以使用緩存, 根據(jù)后綴判斷 mimetype
            InputStream inputStream = null;
            String mimeType = null;
            if (fileName.endsWith("css")) {
                mimeType = "text/css";
            } else if (fileName.endsWith("js")) {
                mimeType = "text/javascript";
            } else if (fileName.endsWith("png")) {
                mimeType = "image/png";
            }
            if (mimeType == null) {
                return null;
            }
            try {
                inputStream = App.getContext().getAssets().open(fileName);
            } catch (IOException e) {
                Log.e(LOG_TAG, "read file IOException: " + e.getMessage());
            }
            if (inputStream != null) {
                WebResourceResponse response = new WebResourceResponse(
                            mimeType, "utf-8", inputStream);
                // 解決css、js的跨域問題
                Map<String, String> headers = new HashMap<>();
                headers.put("Access-Control-Allow-Origin", "*");
                headers.put("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                headers.put("Access-Control-Max-Age", "3600");
                headers.put("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization");
                response.setResponseHeaders(headers);
                return response;
            }
            return null;
        }
    }
});

使用資源緩存后某残,平均的加載時(shí)間為1001ms国撵,相較于初始時(shí)提升了200ms左右。

3.2 WebView緩存池

平時(shí)使用WebView的時(shí)候我們都是動態(tài)新建并添加到ViewGroup中驾锰,新建時(shí)就傳入了Context卸留,那如果使用緩存池,Context該怎么傳呢椭豫?

一種方案是直接用ApplicationContext新建WebView耻瑟;另一種方案是使用MutableContextWrapper,如果在某個(gè)Activity中被使用了就改為該Activity的Context赏酥,回收時(shí)改為ApplicationContext喳整。一個(gè)簡易版的WebView緩存池實(shí)現(xiàn)如下。

注意在APP啟動時(shí)應(yīng)該就要調(diào)用WebViewPool的初始化方法新建一個(gè)WebView裸扶,不然啟動第一個(gè)包含WebView的頁面時(shí)耗時(shí)也會很久框都。

public class WebViewPool {

    private List<DetailWebView> mIdleWebViewList;
    private List<DetailWebView> mUsingWebViewList;

    private static class Holder {
        private static WebViewPool sInstance = new WebViewPool();
    }

    public static WebViewPool getInstance() {
        return Holder.sInstance;
    }

    private WebViewPool() {
    }

    /**
     * 在 APP 啟動時(shí)調(diào)用, 直接新建一個(gè)備用的WebView
     */
    public void init() {
        mIdleWebViewList = new CopyOnWriteArrayList<>();
        mUsingWebViewList = new ArrayList<>();
        MutableContextWrapper contextWrapper = new MutableContextWrapper(App.getContext());
        DetailWebView detailWebView = new DetailWebView(contextWrapper);
        mIdleWebViewList.add(detailWebView);
    }

    public DetailWebView acquireWebView(Context context) {
        if (mIdleWebViewList != null && mIdleWebViewList.size() > 0) {
            DetailWebView webView = mIdleWebViewList.remove(0);
            MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
            contextWrapper.setBaseContext(context);
            mUsingWebViewList.add(webView);
            return webView;
        } else {
            MutableContextWrapper contextWrapper = new MutableContextWrapper(context);
            DetailWebView webView = new DetailWebView(contextWrapper);
            mUsingWebViewList.add(webView);
            return webView;
        }
    }

    public void recycleWebView(DetailWebView webView) {
        if (webView == null) {
            return;
        }
        ViewGroup viewParent = (ViewGroup) webView.getParent();
        if (viewParent != null) {
            viewParent.removeView(webView);
        }
        webView.loadUrl("about:blank");
        if (mUsingWebViewList != null && mUsingWebViewList.contains(webView)) {
            mUsingWebViewList.remove(webView);
            MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
            contextWrapper.setBaseContext(App.getContext());
            webView.setWebViewClient(null);
            webView.setWebChromeClient(null);
            mIdleWebViewList.add(webView);
        } else {
            webView.clearHistory();
            webView.destroy();
        }
    }
}

在使用資源緩存的基礎(chǔ)上再使用WebView緩存池,平均的加載時(shí)間為918ms呵晨。

3.3 預(yù)加載

預(yù)加載主體的實(shí)現(xiàn)比較簡單魏保,在信息流的RecyclerView進(jìn)入IDLE狀態(tài)時(shí)熬尺,判斷當(dāng)前有多少個(gè)條目曝光,然后啟動對應(yīng)數(shù)量的子線程開始下載數(shù)據(jù)為String谓罗,用戶進(jìn)入某個(gè)條目時(shí)粱哼,如果數(shù)據(jù)下載完畢就可以直接通過WebView.loadDataWithBaseURL(mUrl, data, "text/html", "utf-8", null)展示數(shù)據(jù)。
應(yīng)用對預(yù)加載下載的數(shù)據(jù)是需要統(tǒng)一管理的檩咱,可以考慮使用LRU緩存揭措。不過由于是多線程下載數(shù)據(jù),需要對在對LRU緩存讀取時(shí)加鎖保證線程安全刻蚯,這里的鎖可以選擇讀寫鎖绊含。

通過實(shí)驗(yàn),使用預(yù)加載之后的平均加載時(shí)間為698ms炊汹。

3.4 總結(jié)

這里的Demo對于加載時(shí)間的統(tǒng)計(jì)其實(shí)是存在誤差的躬充,首先選取的咨詢較少;其次前端頁面的加載時(shí)間受網(wǎng)速影響是最大的讨便。所以這個(gè)統(tǒng)計(jì)僅供參考麻裳,但是也能看出來每個(gè)優(yōu)化方法都是有作用的,在實(shí)際項(xiàng)目中可以根據(jù)項(xiàng)目實(shí)際情況使用器钟。

四津坑、其余優(yōu)化方案

騰訊有一個(gè)WebView加載優(yōu)化的方案VasSonic,將WebView初始化和WebView加載數(shù)據(jù)這兩個(gè)操作由原本的串行改為并行傲霸,縮短整體的加載時(shí)間疆瑰。

VasSonic大概的流程為:WebView開始初始化時(shí)啟動一個(gè)子線程去下載html數(shù)據(jù),當(dāng)WebView初始化完的時(shí)候通知子線程已經(jīng)初始化完畢昙啄,此時(shí)數(shù)據(jù)下載有3種情況:1穆役、子線程還沒開始下載數(shù)據(jù);2梳凛、子線程下載了一部分?jǐn)?shù)據(jù)耿币;3、子線程已經(jīng)下載完了數(shù)據(jù)韧拒。收到WebView初始化完的消息后淹接,子線程將已經(jīng)下載的數(shù)據(jù)和沒有下載的數(shù)據(jù)拼接為橋接流返回給內(nèi)核渲染。

當(dāng)然這個(gè)框架還有很多功能叛溢,例如將html內(nèi)容分為模板和數(shù)據(jù)塑悼,并且為模板和數(shù)據(jù)分別提供更新的功能,適合用于更新頻繁的運(yùn)營類界面楷掉,具體可見參考3厢蒜。

參考

  1. 常用文件的mime和mimetype的對應(yīng)關(guān)系
  2. 跨域詳解
  3. 騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市斑鸦,隨后出現(xiàn)的幾起案子愕贡,更是在濱河造成了極大的恐慌,老刑警劉巖巷屿,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颂鸿,死亡現(xiàn)場離奇詭異,居然都是意外死亡攒庵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門败晴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浓冒,“玉大人,你說我怎么就攤上這事尖坤∥壤粒” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵慢味,是天一觀的道長场梆。 經(jīng)常有香客問我,道長纯路,這世上最難降的妖魔是什么或油? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮驰唬,結(jié)果婚禮上顶岸,老公的妹妹穿的比我還像新娘。我一直安慰自己叫编,他們只是感情好辖佣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搓逾,像睡著了一般卷谈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上霞篡,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天世蔗,我揣著相機(jī)與錄音,去河邊找鬼朗兵。 笑死凸郑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的矛市。 我是一名探鬼主播芙沥,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了而昨?” 一聲冷哼從身側(cè)響起救氯,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎歌憨,沒想到半個(gè)月后着憨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡务嫡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年甲抖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片心铃。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡准谚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出去扣,到底是詐尸還是另有隱情柱衔,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布愉棱,位于F島的核電站唆铐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奔滑。R本人自食惡果不足惜艾岂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望朋其。 院中可真熱鬧澳盐,春花似錦、人聲如沸令宿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粒没。三九已至筛婉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間癞松,已是汗流浹背爽撒。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留响蓉,地道東北人硕勿。 一個(gè)月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像枫甲,于是被迫代替她去往敵國和親源武。 傳聞我的和親對象是個(gè)殘疾皇子扼褪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356