回顧系統(tǒng) WebView 進(jìn)化史
- 從Android4.4系統(tǒng)開(kāi)始搞糕,Chromium內(nèi)核取代了Webkit內(nèi)核碍讨。
- 從Android5.0系統(tǒng)開(kāi)始钳踊,WebView移植成了一個(gè)獨(dú)立的apk猜拾,可以不依賴系統(tǒng)而獨(dú)立存在和更新唯绍。
- 從Android7.0 系統(tǒng)開(kāi)始,如果用戶手機(jī)里安裝了 Chrome 偿荷, 系統(tǒng)優(yōu)先選擇 Chrome 為應(yīng)用提供 WebView 渲染窘游。
- 從Android8.0系統(tǒng)開(kāi)始,默認(rèn)開(kāi)啟WebView多進(jìn)程模式跳纳,即WebView運(yùn)行在獨(dú)立的沙盒進(jìn)程中忍饰。
隨著技術(shù)的發(fā)展 , Google 推出了 PWA Web 形態(tài)App 寺庄,微信推出小程序 艾蓝,F(xiàn)acebook 推出 React , 前端變得越來(lái)越廣泛(復(fù)雜的前端環(huán)境) 斗塘, 所以移動(dòng)端的 Web 性能變得越來(lái)越重要 赢织, 雖然隨著 Google 不斷的對(duì) WebView 內(nèi)核升級(jí) , 性能也跟上了腳步 馍盟,但是在移動(dòng)端還是有很多方面值得我們?nèi)?yōu)化 于置。
內(nèi)核初始化
第一次打開(kāi) Web 頁(yè)面 , 使用 WebView 加載頁(yè)面的時(shí)候特別慢 贞岭,第二次打開(kāi)就能明顯的感覺(jué)到速度有提升 八毯,為什么 ? 是因?yàn)樵谀愕谝淮渭虞d頁(yè)面的時(shí)候 WebView 內(nèi)核并沒(méi)有初始化 瞄桨, 所以在第一次加載頁(yè)面的時(shí)候需要耗時(shí)去初始化 WebView 內(nèi)核 话速。提前初始化 WebView 內(nèi)核 ,例如如下把它放到了 Application 里面去初始化 , 在頁(yè)面里可以直接使用該 WebView
public class App extends Application {
private WebView mWebView ;
@Override
public void onCreate() {
super.onCreate();
mWebView = new WebView(new MutableContextWrapper(this));
}
}
復(fù)用 WebView
復(fù)用思想在移動(dòng)端是一種很重要的思想 芯侥, 像 ListView 尿孔,RecyclerView 復(fù)用子View 一樣 , 大大提高了性能和節(jié)儉內(nèi)存 筹麸, 如果你大量使用 WebView 那么我建議你可以考慮一下復(fù)用 WebView 活合, 如果你的應(yīng)用只是在某些頁(yè)面使用了 WebView 那么我建議你放棄復(fù)用 WebView , 因?yàn)閺?fù)用 WebView 并不會(huì)給你帶來(lái)多大的性能提升而且會(huì)帶來(lái)一些問(wèn)題 物赶,而且在內(nèi)存吃緊移動(dòng)端 白指,內(nèi)存顯得特別珍貴 , 下面給出一些測(cè)試代碼和數(shù)據(jù)酵紫。
驗(yàn)證復(fù)用 WebView 和提前初始化 WebView 必要性
private void testWebViewInitUsedTime(){
long p = System.currentTimeMillis();
WebView mWebView = new WebView(this);
long n = System.currentTimeMillis();
Log.i("Info", "testWebViewFirstInit use time:" + (n-p));
}
testWebViewInitUsedTime();
testWebViewInitUsedTime();
//測(cè)試環(huán)境 Android 7.0 三星S7
testWebViewFirstInit use time:182
testWebViewFirstInit use time:4
上面是測(cè)試 WebView 初始耗時(shí)的一些代碼 , 可以看出第一次提前初始化還是很有必要的 告嘲, 第二初始化只耗時(shí) 4 毫秒 , 也就是說(shuō)一般情況創(chuàng)建一個(gè) WebView 只需要4毫秒 错维,如果單純幾個(gè)頁(yè)面是復(fù)用 WebView 這種優(yōu)化意義不大 , 因?yàn)樯晕⑻幚聿煌桩?dāng)就會(huì)出現(xiàn)泄漏 橄唬。
下面給出復(fù)用 WebView 的一些關(guān)鍵代碼
public class WebPools {
private final Queue<WebView> mWebViews;
private Object lock = new Object();
private static WebPools mWebPools = null;
private static final AtomicReference<WebPools> mAtomicReference = new AtomicReference<>();
private static final String TAG=WebPools.class.getSimpleName();
private WebPools() {
mWebViews = new LinkedBlockingQueue<>();
}
public static WebPools getInstance() {
for (; ; ) {
if (mWebPools != null)
return mWebPools;
if (mAtomicReference.compareAndSet(null, new WebPools()))
return mWebPools=mAtomicReference.get();
}
}
public void recycle(WebView webView) {
recycleInternal(webView);
}
public WebView acquireWebView(Activity activity) {
return acquireWebViewInternal(activity);
}
private WebView acquireWebViewInternal(Activity activity) {
WebView mWebView = mWebViews.poll();
LogUtils.i(TAG,"acquireWebViewInternal webview:"+mWebView);
if (mWebView == null) {
synchronized (lock) {
return new WebView(new MutableContextWrapper(activity));
}
} else {
MutableContextWrapper mMutableContextWrapper = (MutableContextWrapper) mWebView.getContext();
mMutableContextWrapper.setBaseContext(activity);
return mWebView;
}
}
private void recycleInternal(WebView webView) {
try {
if (webView.getContext() instanceof MutableContextWrapper) {
MutableContextWrapper mContext = (MutableContextWrapper) webView.getContext();
mContext.setBaseContext(mContext.getApplicationContext());
LogUtils.i(TAG,"enqueue webview:"+webView);
mWebViews.offer(webView);
}
if(webView.getContext() instanceof Activity){
// throw new RuntimeException("leaked");
LogUtils.i(TAG,"Abandon this webview 赋焕, It will cause leak if enqueue !");
}
}catch (Exception e){
e.printStackTrace();
}
}
}
注意在 WebView 進(jìn)入 WebPools 之前 , 需要重置 WebView 仰楚,包括清空注入 WebView 的注入對(duì)象 隆判, 否則非常容易泄露。
WebView 獨(dú)立進(jìn)程 僧界, 進(jìn)程預(yù)加載 侨嘀。
因?yàn)?WebView 內(nèi)存泄露 , 以及多進(jìn)程內(nèi)存拓展 捂襟, 相信有一部分開(kāi)發(fā)人員會(huì)把 WebView 放在一個(gè)獨(dú)立的進(jìn)程里面 咬腕, 那么第一次加載 WebView 頁(yè)面 ,加上系統(tǒng)需要時(shí)間 Fork 出新進(jìn)程 葬荷, 那么加載變得更慢了 涨共, 因?yàn)檫M(jìn)程的創(chuàng)建也是一件耗時(shí)的事情 , 所謂的預(yù)加載進(jìn)程 宠漩, 就是提前把進(jìn)程創(chuàng)建出來(lái) 煞赢, 提升加載速度 ,大致的做法如下
<service
android:name=".PreWebService"
android:process=":web"/>
<activity
android:name=".WebActivity"
android:process=":web"
/>
其實(shí)不一定要 Service , 啟動(dòng)「web」 進(jìn)程 Broadcast 廣播也是可以的 哄孤, 提前在進(jìn)入 WebView 頁(yè)面之前 , 先啟動(dòng) PreWebService 把 「web」 進(jìn)程創(chuàng)建了 吹截,當(dāng)系統(tǒng)在啟動(dòng) WebActivity
的時(shí)候 瘦陈, 系統(tǒng)發(fā)現(xiàn)了 「web」 進(jìn)程已經(jīng)創(chuàng)建存在了 , 系統(tǒng)就不需要耗費(fèi)時(shí)間 Fork 出新的「web」進(jìn)程了波俄。
提前顯示進(jìn)度條
提前顯示進(jìn)度條不是提升性能 晨逝, 但是對(duì)用戶體驗(yàn)來(lái)說(shuō)也是很重要的一點(diǎn) , WebView.loadUrl("url")
不會(huì)立馬就回調(diào) onPageStarted
或者 onProgressChanged
因?yàn)樵谶@一時(shí)間段 懦铺, WebView 有可能在初始化內(nèi)核 捉貌, 也有可能在與服務(wù)器建立連接 , 這個(gè)時(shí)間段容易出現(xiàn)白屏 冬念, 白屏用戶體驗(yàn)是很糟糕的 趁窃, 所以我建議
private void go(String url) {
this.mWebView.loadUrl(url);
this.mIndicator.show() //顯示進(jìn)度條
}
在loadUrl
之后立馬就把進(jìn)度條顯示出來(lái) 呐能, 給用戶一個(gè)明顯視覺(jué) 膛虫。
開(kāi)啟軟硬件加速
開(kāi)啟軟硬件加速這個(gè)性能提升還是很明顯的,但是會(huì)耗費(fèi)更大的內(nèi)存 葫掉。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
總結(jié)
上面提出這些性能優(yōu)化都不是那么完美無(wú)缺的裆针,基本都會(huì)帶來(lái)一部分系統(tǒng)資源的消耗 刨摩, 比如在 Application 里面提前初始化WebView 寺晌, 雖然提升了 WebView 頁(yè)面的啟動(dòng)速度, 但是缺拖慢了 App 的冷啟動(dòng)速度 澡刹,獨(dú)立進(jìn)程和開(kāi)啟軟硬件加速也都會(huì)帶來(lái)內(nèi)存更大的開(kāi)銷 呻征,所以凡事都是存在利和弊,至于在項(xiàng)目中利與弊怎么權(quán)衡罢浇,都是需要根據(jù)用戶需求和各種因素來(lái)量度的陆赋。
最后
留下一個(gè)基于 WebView 的強(qiáng)大庫(kù)的傳送門(mén) GitHub 。