关于Android WebView的那些事

[TOC]

概述

Webkit是一个开源浏览器项目,其中,对Android开发者来说,或多或少的都有些接触。 在应用层来看,最经常使用无非这么几个类:WebView(Android中最为复杂,也是最为简单的一个View,继承自AbsoluteLayout),WebViewClient、WebChromeClient(作为回调控制类)、WebSettings(进行设置项的配置)等;Webkit内部包含了网络请求、页面渲染、Js引擎等等。在Android4.4之前的版本中,系统使用的是Webkit内核,其后,切换到Google的Chromium内核。本文主要介绍的是在Android中,如何使用Webkit进行H5页面的展现,以及常见问题的分析手段。

内核简介

下面的内容抄自百度百科 & 乱七八糟的地方,简单了解一下。

Webkit内核

WebKit 是一个开源的浏览器引擎,与之相对应的引擎有Gecko(Mozilla Firefox 等使用)和Trident(也称MSHTML,IE 使用)。
同时WebKit 也是苹果Mac OS X 系统引擎框架版本的名称,主要用于Safari,Dashboard,Mail 和其他一些Mac OS X 程序。WebKit 前身是 KDE 小组的 KHTML,WebKit 所包含的 WebCore 排版引擎和 JSCore 引擎来自于 KDE 的 KHTML 和 KJS,当年苹果比较了 Gecko 和 KHTML 后,仍然选择了后者,就因为它拥有清晰的源码结构、极快的渲染速度。Apple将 KHTML 发扬光大,推出了装备 KHTML 改进型 WebKit 引擎的浏览器 Safari。
WebKit 所包含的 WebCore排版引擎和 JSCore 引擎,均是从KDE的KHTML及KJS引擎衍生而来。它们都是自由软件,在GPL条约下授权,同时支持BSD系统的开发。所以Webkit也是自由软件,同时开放源代码。
WebKit的优势在于高效稳定,兼容性好,且源码结构清晰,易于维护。

Chrominum内核

Chromium 是 Google 的chrome浏览器背后的引擎,其目的是为了创建一个安全、稳定和快速的通用浏览器。
Chromium是一个由Google主导开发的网页浏览器。以BSD许可证等多重自由版权发行并开放源代码。Chromium的开发可能早自2006年即开始,设计思想基于简单、高速、稳定、安全等理念,在架构上使用了Apple发展出来的WebKit排版引擎、Safari的部份源代码与Firefox的成果,并采用Google独家开发出的V8引擎以提升解译JavaScript的效率,而且设计了“沙盒”、“黑名单”、“无痕浏览”等功能来实现稳定与安全的网页浏览环境。Chromium是Google为发展自家的浏览器Google Chrome(以下简称Chrome)而开启的计划,所以Chromium相当于Chrome的工程版或称实验版(尽管Chrome自身也有β版阶段),新功能会率先在Chromium上实现,待验证后才会应用在Chrome上,故Chrome的功能会相对落后但较稳定。Chromium的更新速度很快,每隔数小时即有新的开发版本发布,而且可以免安装,下载zip封装版后解压缩即可使用(Windows下也有安装版)。Chrome虽然理论上也可以免安装,但Google仅提供安装版。
Chromium和Chrome所使用的webkit内核是目前公认的最快的网页浏览方式。
使用Chromium开源代码(基于webkit内核)的浏览器有360极速浏览器、枫树浏览器、太阳花浏览器、世界之窗极速版、傲游浏览器和UC浏览器电脑版等。搜狗高速浏览器和qq浏览器官网未提及Chromium,只是说采用webkit内核,经网友测试这两款浏览器极有可能也是使用的Chromium,只是官方不承认而已。

Blink内核

Blink 是由 WebKit 内核衍生出来的,是由 Chromium 开发维护的新项目。在官方的博客上有详细的说明:Blink: A rendering engine for the Chromium project。关于这个浏览器的更加详细的特性和目标,可以看这里:http://www.chromium.org/blink。大体就是一大堆的优化,提高速度,各种支持。
之前的 WebKit 内核是由 谷歌 chrome 和 苹果 safari 共同开发,谷歌不干了,从 WebKit 中衍生分离出 Blink 不知道是何居心,不过浏览器渲染内核的多样性是肯定有了,以谷歌的技术,这种多样性应该会促进 Web 的发展。
对于国内来说,Blink 是开源引擎,X60 、X狗 之类的浏览器,又可以再增加一个核了,因为 Blink 对速度做了优化,应该会比 WebKit 还要快,所以可能会被命名为超急速三核切换功能。总之,妈妈再也不用担心我的浏览器核心不够用了。

<b><i>前面都是吹牛逼的信息,如何使用Webkit来更好的搬砖? 且听如下分解</i></b>

如何使用

最基本的使用

XML布局中丢一个<WebView>标签,然后再Activity或者FragmentfindViewById,进而loadUrl,一般也没人这么简单的用,除非写Demo。很简单,它就是一个Layout,提供了一个调用加载页面的接口,不写范例了,能看到这篇文章的都看过Google的API说明。

对WebView的行为进行控制

对WebView进行设置

主要涉及到WebView和WebSettings两个类。

视觉方面

例如:

WebView.setHorizontalScrollBarEnabled(false);
WebView.setBackgroundColor(resId);

其实就是WebView的父类ViewGroup和View的方法,不多说了。不过需要注意的是,不是所有的View或ViewGroup的方法对WebView都生效。

常用属性设置

列举几类常用的,几乎所有App的WebView都会设置的属性:

//设置Js开启(不开启,你玩个毛线。实际场景中一般用于定位问题)
WebView.getSettings().setJavaScriptEnabled(true);
//缓存相关
WebView.getSettings().setAppCacheEnabled(true);
WebView.getSettings().setDatabaseEnabled(true);
WebView.getSettings().setDomStorageEnabled(true);
// 设置Client实现类,对于一个追求上进的App来说,自己实现一下是非常有必要的,因为不是所有的Rom都做了默认行为的实现(例如Google大爷),并且默认实现不一定满足业务需求。
WebView.setWebChromeClient(new WebChromeClient());
WebView.setWebViewClient(new WebViewClient());
// 设置下载监听,注意,这里是跟随WebView实现的,一般情况下,都会尝试打开此链接,出现一个空白加载页,然后Webkit才会判断出此链接是一个下载链接,触发DownloadListener回调。
WebView.setDownloadListener(new DownloadListener()); 
//User-agent设置,标示由谁请求
String ua = WebView.getSettings().getUserAgentString()
WebView.getSettings().setUserAgentString(ua);

其他设置项:

//Api >=19 时,支持Web内容调试,FE同学会比较依赖于此:
WebView.setWebContentsDebuggingEnabled(true);

页面显示:

//概览模式进行网页浏览 
WebSettings.setLoadWithOverviewMode(boolean overview);  
WebSettings.setUseWideViewPort(boolean use)

</br>

处理页面&数据交互

如何处理页面跳转以及特殊Scheme

public boolean shouldOverrideUrlLoading(WebView view, String url)

这个回调可以说是最容易出问题的一个回调,表示什么? 字面意思,让你重写这个URL 的loading,比如点击html打电话的一个<a href=“tel:110”>标签,作为一个有节操、有责任心的浏览器,你需要处理 H5常用的几个Scheme :

  • sms 发送短信
  • mailto 发送邮件
  • geo 查看定位信息
  • tel 拨打电话

除此之外,还有各个应用自定义的scheme ,举个例子,支付宝的支付Scheme :alipay:。 这里的返回值,就代表你有没有能力处理这个url,没有的话Webkit就默认处理了。
需要注意的是,这个回调的触发的绝大多数情况是点击页面的<a href="xxxx"> a标签,在Android中loadUrl("http://www.baidu.com"),是不会回调的,为什么不会回调,各位自行理解吧。

超链接<a>标签怎么写:点我
特别说下窗口常见的两种打开方式:

  1. <a href="http://www.meituan.com" target="_self">
    <i>不写target时,默认为self,当前窗口打开连接 </i>
  2. <a href="http://www.meituan.com" target="_blank">

针对单页模式的WebView框架(所有的html窗口均使用同一个WebView实例),不需要关注target的。
如果作为一个成熟的浏览器框架的话,是需要支持Html、JavaScript使用新窗口打开页面,需要实现如下回调:

boolean onCreateWindow(WebView view, boolean isDialog,  boolean isUserGesture, Message resultMsg)

还有一个相关设置项:WebSettings.setJavaScriptCanOpenWindowsAutomatically
此时,系统将不会再回调shouldOverrideUrlLoading。新窗口逻辑的具体实现机制,可以参考系统browser实现逻辑。

<b> 这里有个坑 </b>
Android 4.4版本 ,如果实现了onCreateWindow,也就是说页面<a>标签是这么写的:<a href="http://www.baidu.com" target="_blank">,点击此链接打开的新WebView窗口,此窗口中的url点击,是不会触发shouldOverrideUrlLoading。 这是刚替换成Chrominum内核出的一个bug。本人并没在新版本上验证是否已经修复。

另外,根据不同的Rom,底层实现是不一样的,有的ROM会帮你处理各种调起scheme,也就是startActivity,有的ROM点一个url,就会抛一个intent出来,让用户选择系统浏览器进行加载。

Js 与 Native进行通讯

系统默认,提供了一个接口:

public void addJavascriptInterface(Object object, String name)

有什么安全隐患呢?
戳这里

Js三种窗口

如果不知道Js怎么写,请戳我

  • Alert
public boolean onJsAlert(WebView view, String url, String message, final JsResult result)

  • Confirm
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result)

  • Prompt
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result)

用PC的截图意思一下,看出区别了吧。 这里确定、取消点击以后就得调用 JsResult、JsPromptResult 的 confirm或者cancel。

因为安全问题,大一些的App Native与Js通信都不再用WebView.addJavascriptInterface(Object) 了,都改用JsPrompt,因为JsPrompt中有message、有JsPromptResult可以返回给Js一些信息,所以桥选中了JsPrompt,另一个备选方案是JsConsole。

客户端与Web进行数据传输

大体有这么几种方式进行传递

  1. User-agent
    适用场景,非常通用的数据可以通过设置Ua进行传递,类似于标示客户端平台类型、版本等,一般,应用内的浏览框架的Ua是统一的。
  2. Header
    适用场景:特定的页面,传递少量key-value数据,出现在Request的Header中。 对于WebView来说,就是通过
    public void loadUrl(String url, Map<String, String> additionalHttpHeaders)
    有没有人想过,对于Http Request和Response ,Header有什么区别? 反正我是知道Response中,Header的Key是可以重复的,比如 “Set-Cookie”,这里用的是Map,Request的Header的Key是不是永远不会重复?
  3. Url parameters
    适用场景:Web页一般为GET请求,Url的query部分添加参数,比如这么一个Uri : https://www.baidu.com/s?wd=小猪佩奇 ,适用于少量key-value数据,单个页面。
  4. Cookie
    适用场景:针对域的数据存储与传输,并且,客户端的Cookie是App全局的,各个界面中的WebView均可以读取,并且所有的请求会自动带上请求域的Cookie数据。
    从客户端的角度来说,Cookie又分为Session Cookie和全局Cookie,默认情况下(不设置超时时间),为Session Cookie,生命周期为App启动-结束。 一般,应用启动时,会进行一次CookieManager.removeSessionCookie操作。
    对于FE来说,Session和Cookie是不同的概念,这点需要注意。
  5. JsObjectInterface & 桥
    适用场景:更偏向于业务的一种方式,并且,执行时机取决于桥的实现机制,且一般为异步操作,数据方面更偏向于需要客户端进行界面操作或逻辑处理,而前几种方式,客户端在加载Web页面前已可以准确获知数据。

具体方案实现时,多方面考虑使用何种方式。

页面加载相关(历史&前进&后退)

// 需要特殊说明的是,这个方法不仅可以load网络uri,也可以load本地静态html文件
loadUrl(String url)
// 需要添加自定义Http Header时使用
loadUrl(String url, Map<String, String> additionalHttpHeaders)
// 刷新
reload()
// 停止加载(异步)
stopLoading()
 // Post请求
postUrl(String url, byte[] postData) 
// load本地静态html代码时使用,注意是html代码。data = "<html><body></body></html>
loadData(String data, String mimeType, String encoding) "
// baseUrl可以指定基准url,所以这个方法可以load本地与网络混合html,最常用解决的问题是html中的css、js资源的相对路径问题
loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)
// 前进
goForward()
canGoForward()
// 后退
goBack()
canGoBack()

还有一个比较牛逼的

//一次前进后退多个页面
canGoBackOrForward(int steps)
goBackOrForward(int steps)

系统源码中均有方法注释,怎么用自己看吧。
那么问题来了

WebView中的历史记录有哪些操作呢,又怎样调试?

查了下,只有这两个相关的:
WebBackForwardList copyBackForwardList()
void clearHistory()
系统提供的关于历史记录的操作并不多,因为,不支持单条删除啊,啊啊啊!
WebViewClient中,还有一个相关callback,当系统更新历史记录时回调:
void doUpdateVisitedHistory(WebView view, String url, boolean isReload)

<b>相关问题分析法:历史栈回退错误的定位</b>

绝大多数回退错误是由于接口调用、回调中逻辑执行时序错误。
定位方法:利用copyBackForwardListdoUpdateVisitedHistory两个接口在loadUrl、onPageStart、onPageFinish以及逻辑相关的地方调用,打log,查看历史栈,这里注意下由于loadurl是异步的,需要考虑是否加延迟等等保证调用时机的准确。
本人曾经遇到一个问题:在WebChromeClient中的 JsPrompt回调中,直接进行WebView.goBack操作,结果发现WebView确实回退到上一个页面,但是BackFowardList当前页面的index未更新的问题,具体见另一个篇blog。

销毁

网上有很多关于WebView内存泄露的讨论,据传,老版本的WebView在展示大量图片的时候,即使WebView.destory() WebView=null,也不会销毁。
在新版本上,实际测试结果:compileSDKVersion 23 不会泄露。
一般,我们如何销毁WebView比较保险?

    @Override
    protected void onDestroy() {
        final WebView tempWebView = mWebView;
        mWebView = null;
        if (tempWebView.getParent() != null 
            && tempWebView.getParent() instanceof ViewGroup) {
            ((ViewGroup) tempWebview.getParent()).removeView(mWebView);
        }
        tempWebView.destroy();

    }

缓存

这个问题好大。。。
暂时不介绍,另起blog进行说明。

安全相关

  1. 源生JSInterface漏洞
    Js安全漏洞的说明:http://blog.csdn.net/leehong2005/article/details/11808557

  2. Android WebView File域攻击
    漏洞说明:http://drops.wooyun.org/mobile/11263
    解决方法:

  • 将不必要导出的组件设置为不导出 android:exported=false;
  • 如果需要导出组件,禁止使用File域 websettings.setAllowFileAccess(false);
  • 如果需要使用File协议,禁止File协议调用JavaScript: WebSettings.setJavaScriptEnabled(false);
  1. http401认证:
    实现WebViewClient.onReceivedHttpAuthRequest回调,如何实现,参考系统browser源码。
  2. SSLError
    当网站https证书出现问题时,所有的浏览器有义务提示用户该网站访问有风险,
    比如我们的铁老大的网站
12306 SSLError

解决方案:
实现回调void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)

需要注意的几个问题

首先,提几个需要注意的点:

  1. WebView所有的调用,都需要在UI线程
    请看源码,随便找个方法。 基本上,每个方法,都会首先调用checkThread();
/**
     * Loads the given URL.
     *
     * @param url the URL of the resource to load
     */
    public void loadUrl(String url) {
        checkThread();
        mProvider.loadUrl(url);
    }
  1. 不要阻塞Js调用和返回
    比如,Js在调用Prompt时,客户端没有给返回值(JsPromptResult.confirm或cancel)就进行WebView goBack或者其他操作。 会怎样? boom!
    再比如,页面中的一段Js跑了一个死循环,会怎样? 不杀进程,整个应用休想再使用WebView展示Web页。

  2. HTTPS与HTTP混合页面时
    Android L开始,WebView默认情况下,不支持HTTPS HTTP混合型内容的展示,会对渲染进行阻塞。
    https://datatheorem.github.io/android/2014/12/20/webviews-andorid-lollipop/

    解决方法:

if (Build.VERSION.SDK_INT >= 21) {
    WebSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

问题排查方法论

个人归纳总结几点:

  1. 别乱设置属性,使用WebViewActivity基类时,了解WebView的Settings设置情况。
  2. Web页面是否有非常规的Js或者html属性调用。
  3. 查Log,主要的Log涉及几方面:
  • Webkit、Chrominue的Java层抛warning 或 exception
    <i> 没什么好说的,基本上就是代码调用有问题。</i>
  • 内核的C层抛出Native crash
    <i>可能是Web页的适配(html & Js)适配有问题,或者是客户端调用有问题,这个如果是客户端问题,比较难查,靠使用经验居多。</i>
  • console Web页面抛出的信息。 (特别注意,查log时,不要限定Application Filter)
    <i> 找FE了解相关情况吧,或者Google。 基本是web上的一些元素错误,比如:Js对象找不着,跟FE沟通吧</i>
  1. 笨方法,也是最有效的方法:对比测试,利用Demo、测试Html页面<b>单一变量法</b>进行验证。

奇巧淫技

  1. 假如总是有PM问你,怎么知道某个App的某一个界面是Native的,还是H5的,你可以把这一段截图丢他/她脸上。

step1 进入开发者模式,勾选“显示布局边界”;
step 2,回到你想查看的界面; step 3 假如内容区只有一层基本就是H5 WebView的,多个层级,就是Native。

开启布局边界设置
查看界面情况

看到左右图的差异了吧。
还有另一种方法,RD屌丝们看这里,特别说明,这种方法不太适合浏览器。 (自有内核,可能会不准确)

查看界面层级

好了,就介绍到这里,零零散散的几年前写的文章,第一篇简书blog,如有不对的地方,还恳请大家指正。

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

推荐阅读更多精彩内容