回顧系統(tǒng) WebView 進(jìn)化史
- 從Android4.4系統(tǒng)開始,Chromium內(nèi)核取代了Webkit內(nèi)核。
- 從Android5.0系統(tǒng)開始,WebView移植成了一個(gè)獨(dú)立的apk,可以不依賴系統(tǒng)而獨(dú)立存在和更新。
- 從Android7.0 系統(tǒng)開始,如果用戶手機(jī)里安裝了 Chrome , 系統(tǒng)優(yōu)先選擇 Chrome 為應(yīng)用提供 WebView 渲染。
- 從Android8.0系統(tǒng)開始,默認(rèn)開啟WebView多進(jìn)程模式,即WebView運(yùn)行在獨(dú)立的沙盒進(jìn)程中。
隨著技術(shù)的發(fā)展 , Google 推出了 PWA Web 形態(tài)App ,微信推出小程序 ,F(xiàn)acebook 推出 React , 前端變得越來越廣泛(復(fù)雜的前端環(huán)境) , 所以移動(dòng)端的 Web 性能變得越來越重要 , 雖然隨著 Google 不斷的對(duì) WebView 內(nèi)核升級(jí) , 性能也跟上了腳步 ,但是在移動(dòng)端還是有很多方面值得我們?nèi)?yōu)化 。
內(nèi)核初始化
第一次打開 Web 頁面 , 使用 WebView 加載頁面的時(shí)候特別慢 ,第二次打開就能明顯的感覺到速度有提升 ,為什么 ? 是因?yàn)樵谀愕谝淮渭虞d頁面的時(shí)候 WebView 內(nèi)核并沒有初始化 , 所以在第一次加載頁面的時(shí)候需要耗時(shí)去初始化 WebView 內(nèi)核 。提前初始化 WebView 內(nèi)核 ,例如如下把它放到了 Application 里面去初始化 , 在頁面里可以直接使用該 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)用只是在某些頁面使用了 WebView 那么我建議你放棄復(fù)用 WebView , 因?yàn)閺?fù)用 WebView 并不會(huì)給你帶來多大的性能提升而且會(huì)帶來一些問題 ,而且在內(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 毫秒 , 也就是說一般情況創(chuàng)建一個(gè) WebView 只需要4毫秒 ,如果單純幾個(gè)頁面是復(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)存拓展 , 相信有一部分開發(fā)人員會(huì)把 WebView 放在一個(gè)獨(dú)立的進(jìn)程里面 , 那么第一次加載 WebView 頁面 ,加上系統(tǒng)需要時(shí)間 Fork 出新進(jìn)程 , 那么加載變得更慢了 , 因?yàn)檫M(jìn)程的創(chuàng)建也是一件耗時(shí)的事情 , 所謂的預(yù)加載進(jìn)程 , 就是提前把進(jìn)程創(chuàng)建出來 , 提升加載速度 ,大致的做法如下
<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 頁面之前 , 先啟動(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)來說也是很重要的一點(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)度條顯示出來 , 給用戶一個(gè)明顯視覺 。
開啟軟硬件加速
開啟軟硬件加速這個(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)化都不是那么完美無缺的,基本都會(huì)帶來一部分系統(tǒng)資源的消耗 , 比如在 Application 里面提前初始化WebView , 雖然提升了 WebView 頁面的啟動(dòng)速度, 但是缺拖慢了 App 的冷啟動(dòng)速度 ,獨(dú)立進(jìn)程和開啟軟硬件加速也都會(huì)帶來內(nèi)存更大的開銷 ,所以凡事都是存在利和弊,至于在項(xiàng)目中利與弊怎么權(quán)衡,都是需要根據(jù)用戶需求和各種因素來量度的。
最后
留下一個(gè)基于 WebView 的強(qiáng)大庫的傳送門 GitHub 。