[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 还要快,所以可能会被命名为超急速三核切换功能。总之,妈妈再也不用担心我的浏览器核心不够用了。
前面都是吹牛逼的信息,如何使用Webkit来更好的搬砖? 且听如下分解
如何使用
最基本的使用
XML布局中丢一个
标签,然后再Activity
或者Fragment
中findViewById
,进而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)
处理页面&数据交互
如何处理页面跳转以及特殊Scheme
public boolean shouldOverrideUrlLoading(WebView view, String url)
这个回调可以说是最容易出问题的一个回调,表示什么? 字面意思,让你重写这个URL 的loading,比如点击html打电话的一个标签,作为一个有节操、有责任心的浏览器,你需要处理 H5常用的几个Scheme :
- sms 发送短信
- mailto 发送邮件
- geo 查看定位信息
- tel 拨打电话
除此之外,还有各个应用自定义的scheme ,举个例子,支付宝的支付Scheme :alipay:
。 这里的返回值,就代表你有没有能力处理这个url,没有的话Webkit就默认处理了。
需要注意的是,这个回调的触发的绝大多数情况是点击页面的 a标签,在Android中
loadUrl("http://www.baidu.com")
,是不会回调的,为什么不会回调,各位自行理解吧。
针对单页模式的WebView框架(所有的html窗口均使用同一个WebView实例),不需要关注target的。
如果作为一个成熟的浏览器框架的话,是需要支持Html、JavaScript使用新窗口打开页面,需要实现如下回调:
boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
还有一个相关设置项:WebSettings.setJavaScriptCanOpenWindowsAutomatically
此时,系统将不会再回调shouldOverrideUrlLoading
。新窗口逻辑的具体实现机制,可以参考系统browser实现逻辑。
这里有个坑
Android 4.4版本 ,如果实现了onCreateWindow,也就是说页面标签是这么写的:
,点击此链接打开的新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进行数据传输
大体有这么几种方式进行传递
- User-agent
适用场景,非常通用的数据可以通过设置Ua进行传递,类似于标示客户端平台类型、版本等,一般,应用内的浏览框架的Ua是统一的。 - Header
适用场景:特定的页面,传递少量key-value数据,出现在Request的Header中。 对于WebView来说,就是通过
public void loadUrl(String url, MapadditionalHttpHeaders)
有没有人想过,对于Http Request和Response ,Header有什么区别? 反正我是知道Response中,Header的Key是可以重复的,比如 “Set-Cookie”,这里用的是Map,Request的Header的Key是不是永远不会重复? - Url parameters
适用场景:Web页一般为GET请求,Url的query部分添加参数,比如这么一个Uri : https://www.baidu.com/s?wd=小猪佩奇 ,适用于少量key-value数据,单个页面。 - Cookie
适用场景:针对域的数据存储与传输,并且,客户端的Cookie是App全局的,各个界面中的WebView均可以读取,并且所有的请求会自动带上请求域的Cookie数据。
从客户端的角度来说,Cookie又分为Session Cookie和全局Cookie,默认情况下(不设置超时时间),为Session Cookie,生命周期为App启动-结束。 一般,应用启动时,会进行一次CookieManager.removeSessionCookie
操作。
对于FE来说,Session和Cookie是不同的概念,这点需要注意。 - JsObjectInterface & 桥
适用场景:更偏向于业务的一种方式,并且,执行时机取决于桥的实现机制,且一般为异步操作,数据方面更偏向于需要客户端进行界面操作或逻辑处理,而前几种方式,客户端在加载Web页面前已可以准确获知数据。
具体方案实现时,多方面考虑使用何种方式。
页面加载相关(历史&前进&后退)
// 需要特殊说明的是,这个方法不仅可以load网络uri,也可以load本地静态html文件
loadUrl(String url)
// 需要添加自定义Http Header时使用
loadUrl(String url, Map additionalHttpHeaders)
// 刷新
reload()
// 停止加载(异步)
stopLoading()
// Post请求
postUrl(String url, byte[] postData)
// load本地静态html代码时使用,注意是html代码。data = "
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)
相关问题分析法:历史栈回退错误的定位
绝大多数回退错误是由于接口调用、回调中逻辑执行时序错误。
定位方法:利用copyBackForwardList
,doUpdateVisitedHistory
两个接口在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进行说明。
安全相关
源生JSInterface漏洞
Js安全漏洞的说明:http://blog.csdn.net/leehong2005/article/details/11808557Android WebView File域攻击
漏洞说明:http://drops.wooyun.org/mobile/11263
解决方法:
- 将不必要导出的组件设置为不导出 android:exported=false;
- 如果需要导出组件,禁止使用File域 websettings.setAllowFileAccess(false);
- 如果需要使用File协议,禁止File协议调用JavaScript:
WebSettings.setJavaScriptEnabled(false);
- http401认证:
实现WebViewClient.onReceivedHttpAuthRequest
回调,如何实现,参考系统browser源码。 - SSLError
当网站https证书出现问题时,所有的浏览器有义务提示用户该网站访问有风险,
比如我们的铁老大的网站
解决方案:
实现回调void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
需要注意的几个问题
首先,提几个需要注意的点:
- 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);
}
不要阻塞Js调用和返回
比如,Js在调用Prompt时,客户端没有给返回值(JsPromptResult.confirm或cancel)就进行WebView goBack或者其他操作。 会怎样? boom!
再比如,页面中的一段Js跑了一个死循环,会怎样? 不杀进程,整个应用休想再使用WebView展示Web页。-
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);
}
问题排查方法论
个人归纳总结几点:
- 别乱设置属性,使用WebViewActivity基类时,了解WebView的Settings设置情况。
- Web页面是否有非常规的Js或者html属性调用。
- 查Log,主要的Log涉及几方面:
- Webkit、Chrominue的Java层抛warning 或 exception
没什么好说的,基本上就是代码调用有问题。 - 内核的C层抛出Native crash
可能是Web页的适配(html & Js)适配有问题,或者是客户端调用有问题,这个如果是客户端问题,比较难查,靠使用经验居多。 - console Web页面抛出的信息。 (特别注意,查log时,不要限定Application Filter)
找FE了解相关情况吧,或者Google。 基本是web上的一些元素错误,比如:Js对象找不着,跟FE沟通吧
- 笨方法,也是最有效的方法:对比测试,利用Demo、测试Html页面单一变量法进行验证。
奇巧淫技
- 假如总是有PM问你,怎么知道某个App的某一个界面是Native的,还是H5的,你可以把这一段截图丢他/她脸上。
step1 进入开发者模式,勾选“显示布局边界”;
step 2,回到你想查看的界面; step 3 假如内容区只有一层基本就是H5 WebView的,多个层级,就是Native。
看到左右图的差异了吧。
还有另一种方法,RD屌丝们看这里,特别说明,这种方法不太适合浏览器。 (自有内核,可能会不准确)
好了,就介绍到这里,零零散散的几年前写的文章,第一篇blog,如有不对的地方,还恳请大家指正。