这一系列将从源码角度,分析WebView加载页面的全过程。在摸透缓存机制的基础上,实现自己的WebView缓存控制项目Vindow。
从0到1很难,但是克服了之后,从1到100就容易了许多。
WebView.java
这里采用android-23
的源码示例。
WebView的类说明注释非常长,建议耐心地读完每一行,随后,你就会对WebView的主要功能有一个轮廓上的认识。
1 | /** |
这段是说,WebView主要用来展示网页信息,它使用WebKit内核(这里很重要,后续分析大部分源码都是来自WebKit的),包含了控制网页前进后退的导航功能、缩放功能、文本搜索功能以及其他。
1 | /** |
这里强调了,WebView应当仅仅提供展示,默认情况下是禁用JavaScript,并且隐藏网页错误信息的。换句话说,Google本意是不提倡在WebView中引导用户进行过多的操作,如果有这种需求,就通过intent打开浏览器页面进行操作。然而当下这条规则在很多应用场景下是被无视的,想想微信在H5页面里,可以做多少事。
1 | /** |
默认WebView的很多功能是关闭的,需要我们手动打开。这里列出了如何处理JS Alert与错误、如何开启JS、如何使native代码与页面JS进行交互。
1 | // Implementation notes. |
上面这段说明了WebView主要的实现机理————WebView本身只是一个代理(delegate),提供了公共的API供客户端调用,而这些API的实现,都是代理到了一个叫WebViewProvider的对象上面。既然这样,我们就大致浏览下WebView有哪些公有API,把更多的精力保留下来,集中分析WebViewProvider的实现。
WebView API
这部分不罗列了,只做一个简单的归类,API文档见 https://developer.android.com/reference/android/webkit/WebView.html
加载页面
这是最主要的功能,全部由代理Provider完成
- loadUrl
- postUrl
- loadData
- loadDataWithBaseURL
- getUrl,getOriginalUrl
- getFavicon,getTouchIconUrl
- 保存页面:saveWebArchive
- 控制加载:stopLoading,reload,getProgress
- 页面尺寸:getContentHeight,getContentWidth
计时器
- JS中的计时器,在onPause时可以暂停,onResume时恢复:pauseTimers,resumeTimers
导航
- canGoBack,canGoForward,canGoBackOrForward
- goBack,goForward,goBackOrForward
- pageUp,pageDown
- copyBackForwardList
JavaScript
- evaluateJavascript
- addJavascriptInterface,removeJavascriptInterface
WebViewClient/WebChromeClient
- setWebViewClient:WebView本身是控制页面整体框架的前进、后退、缩放、加载等功能,而具体页面内容的变化,则要交给WebViewClient来管理。
- setWebChromeClient:WebChromeClient主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等。如果页面只是简单地展示HTML,并没有JS操作,那么用WebViewClient就足够了。
下载
- 完成下载监听器:setDownloadListener
查找
- 通过FindListener回调接口实现:setFindListener,findNext,findAll……
缩放
- 展示缩放控件:invokeZoomPicker
- 控制缩放:zoomIn,zoomOut……
安全认证
- 证书操作:getCertificate,setCertificate(deprecated),
- 用户名密码:setHttpAuthUsernamePassword,getHttpAuthUsernamePassword
- 私密模式:isPrivateBrowsingEnabled
设置网络
- 网络可用性:setNetworkAvailable
保存状态
- 代理给Provider进行:saveState,restoreState
生命周期与回调
- postVisualStateCallback
页面内容标签
- HitTestResult系列:getHitTestResult
Cache与访问历史
本系列文章重点了解的内容,代理给Provider实现
- clearCache
- clearFormData
- clearHistory
- clearSslPreferences
被废弃的方法
- 设置滚动条样式:setHorizontalScrollbarOverlay,setVerticalScrollbarOverlay,overlayHorizontalScrollbar,overlayVerticalScrollbar……
- 获取Title高度:getVisibleTitleHeight
- 平台通知:enablePlatformNotifications,disablePlatformNotifications
- 图片操作:savePicture,restorePicture……
- 内存:freeMemory
- Plugins:getPluginList,refreshPlugins……
WebViewProvider
前面说了,WebView的功能几乎全部都是代理给WebViewProvider来实现的,android.webkit.WebViewProvider
是一个接口,其实现要追溯源码,据版本不同有所区分。
- Android4.4之前的版本,由WebViewClassic实现。
- Android4.4以及之后的版本,由WebViewChromium实现。
随SDK下载的源码里不包含这部分代码,在这个页面查看:WebViewChromium.java
WebViewChromium类实现了WebView中被代理的全部方法,篇幅所限,我们不逐一进行分析,只追踪我们关注的加载页面/cache相关,也就是loadUrl
和clearCache
两个方法。
loadUrl
1 |
|
我们通常所用的loadUrl("http://www.foo.com")
,会走到loadUrl(String url, Map<String, String> additionalHttpHeaders)
这个方法,可以看到首先把url拼装成了一个LoadUrlParams
,那么这个LoadUrlParams
是用来做什么的呢?
LoadUrlParams.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/**
* Holds parameters for ContentViewCore.LoadUrl. Parameters should match
* counterparts in NavigationController::LoadURLParams, including default
* values.
*/
"content") (
public class LoadUrlParams {
// Should match NavigationController::LoadUrlType exactly. See comments
// there for proper usage. Values are initialized in initializeConstants.
public static int LOAD_TYPE_DEFAULT;
public static int LOAD_TYPE_BROWSER_INITIATED_HTTP_POST;
public static int LOAD_TYPE_DATA;
// Should match NavigationController::UserAgentOverrideOption exactly.
// See comments there for proper usage. Values are initialized in
// initializeConstants.
public static int UA_OVERRIDE_INHERIT;
public static int UA_OVERRIDE_FALSE;
public static int UA_OVERRIDE_TRUE;
// Fields with counterparts in NavigationController::LoadURLParams.
// Package private so that ContentViewCore.loadUrl can pass them down to
// native code. Should not be accessed directly anywhere else outside of
// this class.
final String mUrl;
int mLoadUrlType;
int mTransitionType;
int mUaOverrideOption;
private Map<String, String> mExtraHeaders;
byte[] mPostData;
String mBaseUrlForDataUrl;
String mVirtualUrlForDataUrl;
boolean mCanLoadLocalResources;
public LoadUrlParams(String url) {
// Check initializeConstants was called.
assert LOAD_TYPE_DEFAULT != LOAD_TYPE_BROWSER_INITIATED_HTTP_POST;
mUrl = url;
mLoadUrlType = LOAD_TYPE_DEFAULT;
mTransitionType = PageTransitionTypes.PAGE_TRANSITION_LINK;
mUaOverrideOption = UA_OVERRIDE_INHERIT;
mPostData = null;
mBaseUrlForDataUrl = null;
mVirtualUrlForDataUrl = null;
}
// 以下略
}
一个URL竟然可以解析出这么多东西来,逐个看看这些变量的含义:(参考navigation_controller.h)
- mUrl:最原始的URL
- mLoadUrlType:加载类型,有如下三种
- LOAD_TYPE_DEFAULT:默认类型,以下两种以外的任意类型。
- LOAD_TYPE_BROWSER_INITIATED_HTTP_POST:POST请求需要设置此类型。
- LOAD_TYPE_DATA:使用Base64编码的图片类型,通过Base64编码图片能够减少一次网络资源加载。(可以使用以下这两个工具,查看如何在图片与Base64编码之间进行转换:http://dataurl.net/#dataurlmaker http://codebeautify.org/base64-to-image-converter)
- mTransitionType:页面变化的类型(实在找不出合适的词语来描述),默认为,详见page_transition_types.h,举例说明:
- PAGE_TRANSITION_LINK:默认值,点击link
- PAGE_TRANSITION_TYPED:在地址栏输入link
- PAGE_TRANSITION_RELOAD:刷新页面
- 略
- mUaOverrideOption:控制http中的UserAgent,有三个可选值
- UA_OVERRIDE_INHERIT:默认值,Use the override value from the previous NavigationEntry in the NavigationController.
- UA_OVERRIDE_FALSE:Use the default user agent
- UA_OVERRIDE_TRUE:Use the user agent override, if it’s available.
- mPostData:POST请求的data
- mBaseUrlForDataUrl:仅对
LOAD_TYPE_DATA
有效,用于URL的相对路径以及JavaScript跨域检验。 - mVirtualUrlForDataUrl:仅对
LOAD_TYPE_DATA
有效,显示在外给用户看的地址。
分析完了LoadUrlParams,继续追溯WebViewChromium中的loadUrl方法。
1 | if (additionalHttpHeaders != null) params.setExtraHeaders(additionalHttpHeaders); |
这里设置的ExtraHeaders
是一个Map,具体参见http协议的header部分。
1 | mAwContents.loadUrl(params); |
最终交给AwContent
对象处理,完整源码见AwContents.java。”Aw”是”Android WebView”的缩写。
1 | /** |
上述注释中最重要的是This is the primary entry point for the WebViewProvider implementation; it holds a 1:1 object relationship with application WebView instances.
这一句,说明每一个WebView示例,都有一个AwContent对象和它对应。同样,我们只关注loadUrl
方法,这里通过我们上一步分析的LoadUrlParams
参数进行加载。
1 | /** |
设置加载本地文件
1 | if (params.getLoadUrlType() == LoadUrlParams.LOAD_TYPE_DATA && |
重设transitionType
1 | // If we are reloading the same url, then set transition type as reload. |
设置UA为override
1 | // For WebView, always use the user agent override, which is set |
把ExtraHeaders中的referer属性提取出来单独设置,并将其从ExtraHeaders中删除,referer属性用于声明当前页面是从哪个页面跳转来的。
1 | // We don't pass extra headers to the content layer, as WebViewClassic |
然后对于剩下的属性,单独设置,最终清楚ExtraHeaders
1 | if (mNativeAwContents != 0) { |
对params进行过上述二次加工后,调用mContentViewCore的loadUrl方法
1 | mContentViewCore.loadUrl(params); |
继续追溯至ContentViewCore.java,位于org.chromium.content.browser
包中。
1 | /** |
这里调用了nativeLoadUrl方法,传入的参数我们之前都已经分析过其含义,除了第一个参数mNativeContentViewCore
,它是一个指向ContentViewCoreImpl
的指针地址。
1 | // Native pointer to C++ ContentViewCoreImpl object which will be set by nativeInit(). |
接下来就要深入到cpp文件中了,content_view_core_impl.cc。注意!可能是版本的原因,这里的LoadUrl方法多出了第一个参数JNIEnv* env
,通过对比可以发现,其它的参数是完全一致的。
1 | void ContentViewCoreImpl::LoadUrl( |
GURL是一个宏,虽然不了解它具体做了什么,但是根据上下文可以猜测这里生成了一个NavigationController::LoadURLParams
对象
1 | NavigationController::LoadURLParams params( |
随后对几个参数进行类型转换,把它们拼入params中,最后调用LoadUrl(params)
方法。
1 | void ContentViewCoreImpl::LoadUrl( |
虽然自己对C语言不是很熟悉,但在这里也可以发现,是把上一步拼装成的params参数集合传递给了某个Controller对象。那么到底是哪一个Controller呢?跟着代码走~
1 | WebContents* ContentViewCoreImpl::GetWebContents() const { |
这里返回成员变量web_contents_
,而Controller就在它内部
1 | void ContentViewCoreImpl::InitWebContents() { |
找到了,是NavigationController类型,源码见navigation_controller_impl.cc。
1 | void NavigationControllerImpl::LoadURLWithParams(const LoadURLParams& params) { |
这里做的事情与前面类似,取出参数再次拼装成NavigationEntryImpl* entry
,然后调用LoadEntry。LoadEntry这里的注释讲解的很清楚,当我们进入新页面时,我们并不清楚是不是要终止上一个页面,因为新页面有可能只是一个下载或者邮件。由于我们的url等信息都保存在entry中,继续追溯 SetPendingEntry(entry) 方法。
1 | void NavigationControllerImpl::LoadEntry(NavigationEntryImpl* entry) { |
1 | void NavigationControllerImpl::SetPendingEntry(NavigationEntryImpl* entry) { |
上述代码中,先是终止了尚未提交处理的Entry,然后将欲访问的entry保存在pending_entry_
变量,最后通过NotificationService::current()->Notify
,把Entry插入一个消息队列,可以在notification_service_impl.cc的源码中看到,收到这个消息后,会通知所有的Observer
1 | void NotificationServiceImpl::Notify(int type, |
我们必须找到是哪个Observer处理了加载Entry的消息,可是到这里,线索似乎断了,怎么才能找到对应的Observer呢?
内事不决问百度,外事不决问谷歌。
在Google的帮助下,找到了这篇文档Getting Around the Chromium Source Code Directory Structure。这里介绍了整个Chromium的架构,重点关注“Navigating from the URL bar”一节。
Navigating from the URL bar
- When the user types into or accepts an entry in the URL bar, the autocomplete edit box determines the final target URL and passes that to
AutocompleteEdit::OpenURL
. (This may not be exactly what the user typed - for example, an URL is generated in the case of a search query.) - The navigation controller is instructed to navigate to the URL in
NavigationController::LoadURL
. - The
NavigationController
callsTabContents::Navigate
with theNavigationEntry
it created to represent this particular page transition. It will create a newRenderViewHost
if necessary, which will cause creation of a RenderView in the renderer process. ARenderView
won’t exist if this is the first navigation, or if the renderer has crashed, so this will also recover from crashes. Navigate
forwards toRenderViewHost::NavigateToEntry
. TheNavigationController
stores this navigation entry, but it is marked as “pending” because it doesn’t know for sure if the transition will take place (maybe the host can not be resolved).RenderViewHost::NavigateToEntry
sends aViewMsg_Navigate
to the newRenderView
in the renderer process.- When told to navigate,
RenderView
may navigate, it may fail, or it may navigate somewhere else instead (for example, if the user clicks a link).RenderViewHost
waits for aViewHostMsg_FrameNavigate
from theRenderView
. - When the load is “committed” by WebKit (the server responded and is sending us data), the
RenderView
sends this message, which is handled inRenderViewHost::OnMsgNavigate
. - The
NavigationEntry
is updated with the information on the load. In the case of a link click, the browser has never seen this URL before. If the navigation was browser-initiated, as in the startup case, there may have been redirects that have changed the URL. - The
NavigationController
updates its list of navigations to account for this new information.
在步骤3
中看到,NavigationController调用了TabContents::Navigate来处理Entry,随后就进入了渲染(Render)过程。而我们想要追踪的缓存文件管理的疑问,还是要深入到渲染阶段才能有个答案。
更多分析,将在后续文章中一一道出。