[TOC]
最近在做一个车机的项目,在使用淘宝SDK登录的过程中,使用loadUrl加载指定的地址之后,WebViewClient没有收到任何Callback调用,而且一旦出现了这种现象之后,再次调用WebView的loadUrl也无效,只有把整个进程杀掉之后才能恢复。
经过思考之后,定位这个问题不是应用导致的,而是4.2系统的WebView的代码有bug引起。
既然已经确认是系统bug,那就开始了系统平台的代码分析。
基本流程梳理
先从WebView这个类的源码看起,我们跟踪跟踪一下loadUrl这个主要方法的执行流程
先看看WebView的构造函数,创建一个WebViewProvider,然后调用init方法初始化WebViewProvider
init初始化很重要,因为忽视了分析初始化方法走了很多弯路
protected WebView(Context context, AttributeSet attrs, int defStyle,
Map javaScriptInterfaces, boolean privateBrowsing) {
super(context, attrs, defStyle);
if (context == null) {
throw new IllegalArgumentException("Invalid context argument");
}
checkThread();
ensureProviderCreated();
mProvider.init(javaScriptInterfaces, privateBrowsing);
}
frameworks/base/core/java/android/webkit/WebVewFactory.java
获取WebViewFactoryProvider抽象工厂,从这个WebVewFactory这个简单工厂,可以看出这块谷歌是由替换内核的能力,通过返回不同WebViewFactoryProvider抽象工厂,实现生产不同内核的产品。
static WebViewFactoryProvider getProvider() {
synchronized (sProviderLock) {
// For now the main purpose of this function (and the factory abstraction) is to keep
// us honest and minimize usage of WebViewClassic internals when binding the proxy.
if (sProviderInstance != null) return sProviderInstance;
// For debug builds, we allow a system property to specify that we should use the
// Chromium powered WebView. This enables us to switch between implementations
// at runtime. For user (release) builds, don't allow this.
if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean("webview.use_chromium", false)) {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
sProviderInstance = loadChromiumProvider();
if (DEBUG) Log.v(LOGTAG, "Loaded Chromium provider: " + sProviderInstance);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
if (sProviderInstance == null) {
if (DEBUG) Log.v(LOGTAG, "Falling back to default provider: "
+ DEFAULT_WEBVIEW_FACTORY);
sProviderInstance = getFactoryByName(DEFAULT_WEBVIEW_FACTORY,
WebViewFactory.class.getClassLoader());
if (sProviderInstance == null) {
if (DEBUG) Log.v(LOGTAG, "Falling back to explicit linkage");
sProviderInstance = new WebViewClassic.Factory();
}
}
return sProviderInstance;
}
}
通过跟踪代码,梳理出相关的类,弄清楚了WebView的初始化流程,和浏览器内核交互的核心就是在WebViewClassic类。
现在进入到loadUrl执行流程分析,发现最终执行到BrowserFrame的nativeLoadUrl中,整个流程很清晰,没发现有什么特殊的控制逻辑,会导致异常。
@Override
public void loadUrl(String url, Map additionalHttpHeaders) {
loadUrlImpl(url, additionalHttpHeaders);
}
private void loadUrlImpl(String url, Map extraHeaders) {
switchOutDrawHistory();
WebViewCore.GetUrlData arg = new WebViewCore.GetUrlData();
arg.mUrl = url;
arg.mExtraHeaders = extraHeaders;
mWebViewCore.sendMessage(EventHub.LOAD_URL, arg);
clearHelpers();
}
如果loadUrl不会出现异常,这个时候有点陷入了是否是内核本身有异常,难道是WebKit内核本身有问题?
这个时候经验告诉我,不要轻易去怀疑开源库的问题,而且如果是WebKit的问题,我也无能无力?
是否还有遗漏的点没有考虑到。
之前说是回调没有执行,那么先看看回调的执行流程是什么样子?
发现里面涉及到了一个CallbackProxy的类,操作只是在CallbackProxy保存了一个变量
/**
* See {@link WebView#setWebViewClient(WebViewClient)}
*/
@Override
public void setWebViewClient(WebViewClient client) {
mCallbackProxy.setWebViewClient(client);
}
frameworks/base/core/java/android/webkit/CallbackProxy.java
/**
* Set the WebViewClient.
*
* @param client An implementation of WebViewClient.
*/
public void setWebViewClient(WebViewClient client) {
mWebViewClient = client;
}
CallbackProxy继承于Handler,也就意味着它具有处理消息的能力,那消息是怎么发送给CallbackProxy的呢?
通过在webkit包下搜索onPageStarted,是通过BrowserFrame回调上来的
在这个流程里依然没有发现明显的异常,我们发现了BrowserFrame作为和C层的通信的中间层,但是这个关系是怎么建立的呢?
带着这个问题,我们发现我们忽视了什么?
最开始我们分析,我们发现WebViewClassic是核心类,WebViewCore是向浏览器内核转发请求的核心类
那我们重新再看看WebViewClassic里面的init到底做了什么 ?
@Override
public void init(Map javaScriptInterfaces, boolean privateBrowsing) {
Context context = mContext;
// Used by the chrome stack to find application paths
JniUtil.setContext(context);
mCallbackProxy = new CallbackProxy(context, this);
mViewManager = new ViewManager(this);
L10nUtils.setApplicationContext(context.getApplicationContext());
mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javaScriptInterfaces);
mDatabase = WebViewDatabaseClassic.getInstance(context);
mScroller = new OverScroller(context, null, 0, 0, false); //TODO Use OverScroller's flywheel
mZoomManager = new ZoomManager(this, mCallbackProxy);
/* The init method must follow the creation of certain member variables,
* such as the mZoomManager.
*/
init();
setupPackageListener(context);
setupProxyListener(context);
setupTrustStorageListener(context);
updateMultiTouchSupport(context);
if (privateBrowsing) {
startPrivateBrowsing();
}
mAutoFillData = new WebViewCore.AutoFillData();
mEditTextScroller = new Scroller(context);
}
从WebViewClassic的初始化方法发现最开始梳理初始化流程的图,WebViewClassic其实少关联了一个核心的CallbackProxy类
再单独细化一下WebViewClassic这个核心的对象
从这个WebViewClassic的对象间的关系,我们会发现一些线索
CallbackProxy的生命周期在WebViewClassic里面和WebViewCore里面是不一致的
WebViewThread和WebViewCore生命周期是不一致的
BrowserFrame同时持有WebViewCore对象和CallbackProxy,并且把WebViewCore对象和BrowserFrame与Native进行关联,提供Native回调的接口执行CallbackProxy切换到android sdk开发者的回调接口
通过代码跟踪,我们发现
- EventHub负责接收UI线程WebView接口发送的请求,同步到WebCoreThread线程
-
CallbackProxy负责把WebCoreThread同步到UI线程
整体架构
运行视图
整个浏览器内核涉及到3个线程
通过上面的分析,我们清晰的了解了UI线程和浏览器内核线程之间的请求关系,查看EventHub的sendMessage方法
/**
* Send a message internally to the queue or to the handler
*/
private synchronized void sendMessage(Message msg) {
if (mBlockMessages) {
return;
}
if (mMessages != null) {
mMessages.add(msg);
} else {
mHandler.sendMessage(msg);
}
}
查看CallbackProxy的回调方法发现有返回值的回调,都会锁住WebViewCoreThread
private synchronized void sendMessageToUiThreadSync(Message msg) {
sendMessage(msg);
WebCoreThreadWatchdog.pause();
try {
wait();
} catch (InterruptedException e) {
Log.e(LOGTAG, "Caught exception waiting for synchronous UI message to be processed");
Log.e(LOGTAG, Log.getStackTraceString(e));
}
WebCoreThreadWatchdog.resume();
}
现在我们通过上面的代码发现有个很关键的控制语句,就是如果mBlockMessages为true的时候,就不执行UI线程发送的请求了。
这个字段是通过WebViewCore的destroy赋值的,而这个方法是直接从UI层调用下来
/**
* Sends a DESTROY message to WebCore.
* Called from UI thread.
*/
void destroy() {
synchronized (mEventHub) {
// send DESTROY to front of queue
// PAUSE/RESUME timers will still be processed even if they get handled later
mEventHub.mDestroying = true;
mEventHub.sendMessageAtFrontOfQueue(
Message.obtain(null, EventHub.DESTROY));
mEventHub.blockMessages();
WebCoreThreadWatchdog.unregisterWebView(mWebViewClassic);
}
}
从上面的分析来看,存在两种可能性:
1. EventHub的标记没有恢复,导致EventHub没有办法进行初始化
2. WebViewCoreThread阻塞,UI层没有返回导致WebViewCoreThread一直等待,产生了死锁
通过之前的代码分析,排除了情况1的可能性,因为 EventHub 是个成员变量,生命周期是和WebViewClassic一致的,而WebViewCoreThread是单例的,生命周期和WebViewClassic不一致,这个更符合需要重启进程才能恢复的现象。
通过添加日志分析,的确是情况2导致的,那是什么原因导致的呢?
下面是扫码登录的基本流程
把一个对应下面用例的场景把一个比较完整的的涉及到各个类的时序图画出来
通过上面的时序图分析,会发现WebViewCore线程会存在两种情况被一直阻塞
- messageBlocked
- mWebChromClient等于null的时候(这种情况也就是说可能在多线程的情况下如果把mWebChromClient置成null,在消息处理的是可以造成整个webview死锁,这是一个风险点)
所以对应的修改有方式有三种
- WebView销毁的时候把mWebChromeClient提前置null(如果销毁后就设置null,这样就杜绝了异步线程的消息进入到主线程的消息队列中,从而浏览器内核线程不会wait掉)
- 框架层修改在messageBlocked为true时,onPrompt执行返回false
- 在handleMessage的时候messageBlocked为true,同时执行一下notifyCallbackProxy
关于为什么native调用onPrompt是切换到了WebViewCoreThread是因为定时器是在WebViewCoreThread线程创建的,而onPrompt是通过定时器触发回调的
V webcore : WebCoreThread isAlive=true isInterrupted=false getState=WAITING
V webcore : at java.lang.Object.wait(Native Method)
V webcore : at java.lang.Object.wait(Object.java:364)
V webcore : at android.webkit.CallbackProxy.sendMessageToUiThreadSync(CallbackProxy.java:1612)
V webcore : at android.webkit.CallbackProxy.onJsPrompt(CallbackProxy.java:1366)
V webcore : at android.webkit.WebViewCore.jsPrompt(WebViewCore.java:574)
V webcore : at android.webkit.JWebCoreJavaBridge.sharedTimerFired(Native Method)
V webcore : at android.webkit.JWebCoreJavaBridge.fireSharedTimer(JWebCoreJavaBridge.java:92)
V webcore : at android.webkit.JWebCoreJavaBridge.handleMessage(JWebCoreJavaBridge.java:108)
V webcore : at android.os.Handler.dispatchMessage(Handler.java:99)
V webcore : at android.os.Looper.loop(Looper.java:138)
V webcore : at android.webkit.WebViewCore$WebCoreThread.run(WebViewCore.java:877)
V webcore : at java.lang.Thread.run(Thread.java:856)
V webcore : WebViewCore Construtor android.webkit.WebViewCore@42764f08 sWebCoreHandler2=Handler (android.webkit.WebViewCore$WebCoreThread$1) {41e58398}
系统调试接口
Android4.2 调试开关可以打开监听线程是否阻塞了,这样会弹框提示线程阻塞。
WebViewClassic.setShouldMonitorWebCoreThread();
总结
- 在解决一个问题前,需要对功能的内部流程进行比较深入的分析,通过分析自身功能内部执行了什么动作,为后面的源码提供有针对性的分析
- 熟悉源码的基本结构,特别是
- 初始化操作
- 线程关系
- 特殊的控制标记
- 对于复杂的问题,使用UML图一步一步分析,可以把逻辑梳理的更清晰,如果没有相应的存档信息,很容易被复杂的问题弄的逻辑混乱。
- 对于存在阻塞的操作,需要确保后面的操作能有对应的释放操作,并且最好阻塞操作的时候,和释放操作都用同一个标记来控制
- 设置给系统接口的回调,最好能在销毁之前置空,这样系统内部的检查逻辑可以规避一些BUG