Hybrid开发之WebView使用方法及注意事项

工作这么长时间,细细想来有很长时间都在与WebView打交道。在Hybrid App的开发中,积累了一定的经验,在此做一个简单的工作总结。

Hybrid开发中最常用的组件就是WebView了。WebView不仅用来展示Web页面,更是Web页面和安卓手机Native之间沟通的桥梁。但由于历史原因,android不同版本之间WebView不同,存在一些兼容性问题。

注:鉴于在市场上Android 4.0及以后的系统占90%之上,较多的开源工程的minSdkVersion为14。本人参与开发的app也只关注Android 4.0及以上系统的兼容性。

WebView

在Android 4.4系统之前,WebView一直采用WebKit内核;而在Android 4.4及以后google采用了chromium内核。二者的API变化不大,但是在某些场景下的表现有些微差异。整体来说chromium内核更为高效,支持V8引擎解析Javascript。更重要的是Chromium支持远程调试。

开发中web页面可使用console.log打印控制台日志。此时可通过Android Studio的Logcat查看到打印信息。在4.4系统之前,WebView相关日志tag是webkit;而在4.4及之后,tag是chromium。日志中包括Javascript运行中打印的日志,错误信息等,有助于分析Hybrid开发中遇到的问题。

I/chromium: [INFO:CONSOLE(1)] "The key "target-densitydpi" is not supported.", source: file:///data/user/0/com.tfzq.gcs.dev/files/www/m_tf/trade/indexTota.js?v=0.4672763997119571 (1)

创建WebView包括两种方法,一是在Xml中配置;二是直接使用new动态创建。推荐使用第二种方式进行开发。WebView可以使用loadUrl加载本地或线上的Web页面,执行javascript语句;也可以使用loadData直接加载html数据。另一方法loadDataWithBaseUrl在加载页面中有本地图片时可以使用。

使用WebView之前需要通过WebSettings进行一定的配置。

WebSettings settings = getSettings();
 //默认是false 设置true允许和js交互
 settings.setJavaScriptEnabled(true);
 //  WebSettings.LOAD_DEFAULT 如果本地缓存可用且没有过期则使用本地缓存,否加载网络数据 默认值
 //  WebSettings.LOAD_CACHE_ELSE_NETWORK 优先加载本地缓存数据,无论缓存是否过期
 //  WebSettings.LOAD_NO_CACHE  只加载网络数据,不加载本地缓存
 //  WebSettings.LOAD_CACHE_ONLY 只加载缓存数据,不加载网络数据
 //Tips:有网络可以使用LOAD_DEFAULT 没有网时用LOAD_CACHE_ELSE_NETWORK
 settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
 //开启 DOM storage API 功能 较大存储空间,使用简单
 settings.setDomStorageEnabled(true);
 //设置数据库缓存路径 存储管理复杂数据 方便对数据进行增加、删除、修改、查询 不推荐使用
 settings.setDatabaseEnabled(true);
 final String dbPath = context.getApplicationContext().getDir("db", Context.MODE_PRIVATE).getPath();
 settings.setDatabasePath(dbPath);
 //开启 Application Caches 功能 方便构建离线APP 不推荐使用
 settings.setAppCacheEnabled(true);
 final String cachePath = context.getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath();
 settings.setAppCachePath(cachePath);
 settings.setAppCacheMaxSize(5 * 1024 * 1024);

WebChromeClient

WebChromeClient常用以下几个回调方法

  • WebChromeClient. onProgressChanged
    页面加载进度回调,progress从0-100。可用来实现自定义加载进度条。

  • WebChromeClient. onReceivedTitle
    可用来接收当前页面的标题,实现本地标题栏的变化。

  • WebChromeClient.onJsPrompt
    可用来实现自定义的弹窗,也可用来实现安全的JSBridge。phonegap混合开发框架即使用onJsPrompt实现在Android 4.2以下系统的安全性。类似的方法还有onJsConfirm,onJsAlert。

在开发中遇到过界面相关问题,恍惚记得WebView使用chromium内核之后,onJsPrompt方法运行线程不在是主线程导致。年代久远,有兴趣的读者可自行验证。

WebViewClient

WebViewClient常用以下几个回调方法。

  • WebViewClient.shouldOverrideUrlLoading
    在WebView加载Url前调用,app可拦截该方法来自己处理本次url的加载。该方法返回true代表app自己处理url;返回false代表WebView处理url。该方法可配合自定义的协议头来区分url是事件或普通的web连接。JSBridge框架主要依赖此方法实现JS事件的回调。

  • WebViewClient.shouldInterceptRequest
    可拦截WebView对页面中资源的加载,使用本地资源代替。曾经在一项目中用该方法实现本地缓存。

  • WebViewClient.onPageStarted
    该方法表示页面开始加载,理论上是只调用一次。但是在WebKit内核开发时,遇到过调用次数超过一次的情况。

  • WebViewClient.onReceivedError
    页面加载出现错误时调用该方法。可用来定制错误页面。

  • WebViewClient.onPageFinished
    该方法一般用来处理页面加载完成时的一些操作。比如注入JSBridge框架代码建立JS-Native通信通道。但是在我早期的开发经验中,4.*的系统上低概率出现onPageFinished方法未回调的问题。在开发测试中需要关注。Web页面中很容易有一些自动跳转逻辑,这时onPageFinished会被调用多次,当然url的参数不同。可参考How to listen for a Webview finishing loading a URL in Android?

  • WebViewClient

Web与Native通信

关于Web与Native之间的通信,可参考我的文章。介绍了基本的通信方法,及开源库JsBridge的使用。
JSbridge系列解析(一):JS-Native调用方法
JSbridge系列解析(二):lzyzsd/JsBridge使用方法
JSbridge系列解析(三):lzyzsd/JsBridge源码解析
JSbridge系列解析(四):Web端发消息给Native代码流程具体分析

WebView内存泄漏

网上搜索Webview,可以说最多的就是内存泄漏相关的介绍,如说某篇文章中出现的下段。文章指出不要在xml中直接使用webview,否则可能出现webview所在activity的内存泄漏。

webview的创建也是有技巧的,最好不要在layout.xml中使用webview,可以通过一个viewgroup容器,使用代码动态往容器里addview(webview),这样可以在onDestory()里销毁掉webview及时清理内存,另外需要注意创建webview需要使用applicationContext而不是activity的context,销毁时不再占有activity对象,这个大家应该都知道了,最后离开的时候需要及时销毁webview,onDestory()中应该先从viewgroup中remove掉webview,再调用webview.removeAllViews();webview.destory();

最初做hybrid相关开发时,4.4及以上系统的手机较少,确实存在webview.destroy()后无法释放内存的问题。但现在6.0系统已经成了主流,为了进一步验证,我用红米Note 4,Android 6.0系统进行了测试。

代码如下,webview直接定义在xml文件中,且onDestroy中没有针对webview的destroy操作。通过模拟器验证Android 5.0表现与6.0一致,back退出后内存顺利回收;但4.4系统就会出现内存泄漏。

public class WebViewTestActivity extends Activity {
    private WebView webview;

    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_webview_test);

        webview = findViewById(R.id.webview);
        webview.loadUrl("https://www.baidu.com");

        //部分页面,如百度主页,如果不设置setJavaScriptEnabled为true,则显示白屏
        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
    }
}

多次进入主界面后按Back键退出,通过执行adb shell dumpsys meminfo 包名命令,监控发现WebView及对应Activity的资源释放了。这也说明google官方做了一定的修复。但为了兼容低版本,仍建议通过ViewGroup容器动态添加WebView,使用完成后进行清理操作。

public class WebViewTestActivity extends Activity {
    private WebView webview;
    private FrameLayout webviewContainer;

    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_webview_test);

        webviewContainer = findViewById(R.id.webview_container);

        webview = new WebView(this);
        webviewContainer.addView(webview);
        webview.loadUrl("https://www.baidu.com");

        //部分页面,如百度主页,如果不设置setJavaScriptEnabled为true,则显示白屏
        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();

        if(webview != null){
            ViewGroup parent = (ViewGroup)webview.getParent();
            if (parent != null){
                parent.removeView(webview);
            }
            webview.removeAllViews();
            webview.destroy();
        }
    }
}

4.4的模拟器使用上面改进的代码,但仍出现内存泄漏,不过是InputMethodManager泄漏引起。后面有时间再研究这个。

网上另一种解决内存泄漏的方法,是在创建WebView时使用Application的Context。但是如果Web页面中出现Video时,会出现如果崩溃。Web页面弹框的场景未测试。

W/System.err: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
W/System.err:     at android.app.ContextImpl.startActivity(ContextImpl.java:1034)
W/System.err:     at android.app.ContextImpl.startActivity(ContextImpl.java:1021)
W/System.err:     at android.content.ContextWrapper.startActivity(ContextWrapper.java:311)
W/System.err:     at com.android.webview.chromium.WebViewContentsClientAdapter$NullWebViewClient.shouldOverrideUrlLoading(WebViewContentsClientAdapter.java:196)
W/System.err:     at com.android.webview.chromium.WebViewContentsClientAdapter.shouldOverrideUrlLoading(WebViewContentsClientAdapter.java:293)
W/System.err:     at com.android.org.chromium.android_webview.AwContentsClientBridge.shouldOverrideUrlLoading(AwContentsClientBridge.java:96)
W/System.err:     at com.android.org.chromium.base.SystemMessageHandler.nativeDoRunLoopOnce(Native Method)
W/System.err:     at com.android.org.chromium.base.SystemMessageHandler.handleMessage(SystemMessageHandler.java:27)
W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:102)
W/System.err:     at android.os.Looper.loop(Looper.java:136)
W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:5017)
W/System.err:     at java.lang.reflect.Method.invokeNative(Native Method)
W/System.err:     at java.lang.reflect.Method.invoke(Method.java:515)
W/System.err:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
W/System.err:     at dalvik.system.NativeStart.main(Native Method)
A/libc: Fatal signal 6 (SIGABRT) at 0x0000084d (code=-6), thread 2125 (n.myapplication)

WebView如果执行destory操作,则后续不能再进行loadUrl的操作,否则会出现白屏。这一点在webView的复用时需要考虑。同时WebView复用时,

实际测试WebView复用的场景,将WebView置为static变量,下列代码仅有示范的作用,不具实际的意义。目前我仍未想到WebView复用的意义所在。注意在界面销毁时需要将WebView从父布局中remove,避免持有父布局引用导致当前界面的内存泄漏。但是仍无办法解决WebView创建时持有的Activity的内存泄漏。最初我怀疑用复用的WebVIew播放视频会出现问题,毕竟其持有的Context已不可见,但实际测试运行视频OK。

public class WebViewTestActivity extends Activity {
    private static WebView webview;
    private FrameLayout webviewContainer;

    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_webview_test);

        webviewContainer = findViewById(R.id.webview_container);

        if (webview == null) {
            webview = new WebView(this);
        }

        webviewContainer.addView(webview);
        webview.loadUrl("https://www.baidu.com");

        //部分页面,如百度主页,如果不设置setJavaScriptEnabled为true,则显示白屏
        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();

        if(webview != null){
            ViewGroup parent = (ViewGroup)webview.getParent();
            if (parent != null){
                parent.removeView(webview);
            }
            webview.removeAllViews();
//            webview.destroy();
        }
    }
}

关于内存泄漏的另一种解决方案,即将显示WebView的activity运行在另外的进程。这样在WebView界面关闭时将该进程直接kill,避免对主程序的影响。但是该方法对多WebView的程序不太适合,毕竟跨进程通信成本较大,容易出现各种问题。该方案只适应一定场景,了解即可。

参考:
Android Webview的一些使用总结和遇到过得坑
Android:你不知道的 WebView 使用漏洞
Android 各个版本WebView
安卓webview的一些坑
H5 缓存机制浅析 移动端 Web 加载性能优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容