Android

android4.2 webview加载无响应问题

2019-12-13  本文已影响0人  jxcyly1985

[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<String, Object> 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类。

1575880004037.png

现在进入到loadUrl执行流程分析,发现最终执行到BrowserFrame的nativeLoadUrl中,整个流程很清晰,没发现有什么特殊的控制逻辑,会导致异常。


    @Override
    public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
        loadUrlImpl(url, additionalHttpHeaders);
    }

    private void loadUrlImpl(String url, Map<String, String> extraHeaders) {
        switchOutDrawHistory();
        WebViewCore.GetUrlData arg = new WebViewCore.GetUrlData();
        arg.mUrl = url;
        arg.mExtraHeaders = extraHeaders;
        mWebViewCore.sendMessage(EventHub.LOAD_URL, arg);
        clearHelpers();
    }


1575879983731.png

如果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;
}
1575879953290.png

CallbackProxy继承于Handler,也就意味着它具有处理消息的能力,那消息是怎么发送给CallbackProxy的呢?

通过在webkit包下搜索onPageStarted,是通过BrowserFrame回调上来的

1575879928304.png

在这个流程里依然没有发现明显的异常,我们发现了BrowserFrame作为和C层的通信的中间层,但是这个关系是怎么建立的呢?

带着这个问题,我们发现我们忽视了什么?

最开始我们分析,我们发现WebViewClassic是核心类,WebViewCore是向浏览器内核转发请求的核心类

那我们重新再看看WebViewClassic里面的init到底做了什么 ?

   @Override
    public void init(Map<String, Object> 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类

1575882589471.png

再单独细化一下WebViewClassic这个核心的对象


1576133131237.png

从这个WebViewClassic的对象间的关系,我们会发现一些线索

通过代码跟踪,我们发现

整体架构

1576224289804.png

运行视图

整个浏览器内核涉及到3个线程


1576132974317.png
1575896417569.png

通过上面的分析,我们清晰的了解了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导致的,那是什么原因导致的呢?

下面是扫码登录的基本流程

1576136483875.png

把一个对应下面用例的场景把一个比较完整的的涉及到各个类的时序图画出来


webview destroy Diagram (1).png webview destroy Diagram.png

通过上面的时序图分析,会发现WebViewCore线程会存在两种情况被一直阻塞

所以对应的修改有方式有三种

关于为什么native调用onPrompt是切换到了WebViewCoreThread是因为定时器是在WebViewCoreThread线程创建的,而onPrompt是通过定时器触发回调的

1576221772312.png
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();

总结

上一篇下一篇

猜你喜欢

热点阅读