WebView 性能和用户体验优化
回顾系统 WebView 进化史
- 从Android4.4系统开始,Chromium内核取代了Webkit内核。
- 从Android5.0系统开始,WebView移植成了一个独立的apk,可以不依赖系统而独立存在和更新。
- 从Android7.0 系统开始,如果用户手机里安装了 Chrome , 系统优先选择 Chrome 为应用提供 WebView 渲染。
- 从Android8.0系统开始,默认开启WebView多进程模式,即WebView运行在独立的沙盒进程中。
随着技术的发展 , Google 推出了 PWA Web 形态App ,微信推出小程序 ,Facebook 推出 React , 前端变得越来越广泛(复杂的前端环境) , 所以移动端的 Web 性能变得越来越重要 , 虽然随着 Google 不断的对 WebView 内核升级 , 性能也跟上了脚步 ,但是在移动端还是有很多方面值得我们去优化 。
内核初始化
第一次打开 Web 页面 , 使用 WebView 加载页面的时候特别慢 ,第二次打开就能明显的感觉到速度有提升 ,为什么 ? 是因为在你第一次加载页面的时候 WebView 内核并没有初始化 , 所以在第一次加载页面的时候需要耗时去初始化 WebView 内核 。提前初始化 WebView 内核 ,例如如下把它放到了 Application 里面去初始化 , 在页面里可以直接使用该 WebView
public class App extends Application {
private WebView mWebView ;
@Override
public void onCreate() {
super.onCreate();
mWebView = new WebView(new MutableContextWrapper(this));
}
}
复用 WebView
复用思想在移动端是一种很重要的思想 , 像 ListView ,RecyclerView 复用子View 一样 , 大大提高了性能和节俭内存 , 如果你大量使用 WebView 那么我建议你可以考虑一下复用 WebView , 如果你的应用只是在某些页面使用了 WebView 那么我建议你放弃复用 WebView , 因为复用 WebView 并不会给你带来多大的性能提升而且会带来一些问题 ,而且在内存吃紧移动端 ,内存显得特别珍贵 , 下面给出一些测试代码和数据。
验证复用 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();
//测试环境 Android 7.0 三星S7
testWebViewFirstInit use time:182
testWebViewFirstInit use time:4
上面是测试 WebView 初始耗时的一些代码 , 可以看出第一次提前初始化还是很有必要的 , 第二初始化只耗时 4 毫秒 , 也就是说一般情况创建一个 WebView 只需要4毫秒 ,如果单纯几个页面是复用 WebView 这种优化意义不大 , 因为稍微处理不妥当就会出现泄漏 。
下面给出复用 WebView 的一些关键代码
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 进入 WebPools 之前 , 需要重置 WebView ,包括清空注入 WebView 的注入对象 , 否则非常容易泄露。
WebView 独立进程 , 进程预加载 。
因为 WebView 内存泄露 , 以及多进程内存拓展 , 相信有一部分开发人员会把 WebView 放在一个独立的进程里面 , 那么第一次加载 WebView 页面 ,加上系统需要时间 Fork 出新进程 , 那么加载变得更慢了 , 因为进程的创建也是一件耗时的事情 , 所谓的预加载进程 , 就是提前把进程创建出来 , 提升加载速度 ,大致的做法如下
<service
android:name=".PreWebService"
android:process=":web"/>
<activity
android:name=".WebActivity"
android:process=":web"
/>
其实不一定要 Service , 启动「web」 进程 Broadcast 广播也是可以的 , 提前在进入 WebView 页面之前 , 先启动 PreWebService 把 「web」 进程创建了 ,当系统在启动 WebActivity
的时候 , 系统发现了 「web」 进程已经创建存在了 , 系统就不需要耗费时间 Fork 出新的「web」进程了。
提前显示进度条
提前显示进度条不是提升性能 , 但是对用户体验来说也是很重要的一点 , WebView.loadUrl("url")
不会立马就回调 onPageStarted
或者 onProgressChanged
因为在这一时间段 , WebView 有可能在初始化内核 , 也有可能在与服务器建立连接 , 这个时间段容易出现白屏 , 白屏用户体验是很糟糕的 , 所以我建议
private void go(String url) {
this.mWebView.loadUrl(url);
this.mIndicator.show() //显示进度条
}
在loadUrl
之后立马就把进度条显示出来 , 给用户一个明显视觉 。
开启软硬件加速
开启软硬件加速这个性能提升还是很明显的,但是会耗费更大的内存 。
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);
}
总结
上面提出这些性能优化都不是那么完美无缺的,基本都会带来一部分系统资源的消耗 , 比如在 Application 里面提前初始化WebView , 虽然提升了 WebView 页面的启动速度, 但是缺拖慢了 App 的冷启动速度 ,独立进程和开启软硬件加速也都会带来内存更大的开销 ,所以凡事都是存在利和弊,至于在项目中利与弊怎么权衡,都是需要根据用户需求和各种因素来量度的。
最后
留下一个基于 WebView 的强大库的传送门 GitHub 。