Android WebKit HTML主資源加載過程

Android WebKit HTML主資源加載過程

前言

在瀏覽器裡面輸入網址,最終瀏覽器會調用WebView的loadUrl(),然後就開始加載整個網頁。整個加載過程中,最重要的一步就是HTML主資源的加載。WebKit將網頁的資源分為主資源(MainResource)和子資源(SubResource)。

WebKit資源分類

主資源:HTML文件。

子資源:CSS, JS, JPG等等,除瞭HTML文件之外的所有資源都稱之為子資源

本章主要講主資源的加載過程,子資源的加載過程後期會專門詳細的分析和講解。

主資源請求

LoadUrl

主資源的請求是從WebView的loadUrl開始的。根據之前《Android WebKit消息處理》的講解,WebView的操作都會有WebViewClassic進行代理。資源加載肯定是由WebCore來處理的,所以,WebVewClassic會發消息給WebViewCore,讓WebViewCore最終將loadUrl傳遞給C++層的WebKit處理:

    /**
     * See {@link WebView#loadUrl(String, Map)}
     */
    @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();
    }

WebViewCore在接收到LOAD_URL之後,會通過BrowserFrame調用nativeLoadUrl,這個BrowserFrame與C++層的mainFrame對接。這裡順便提一下clearHeapers()的作用:如果當前網頁有對話框dialog,有輸入法之類的,clearHelpers就是用來清理這些東西的。這也是為什麼加載一個新頁面的時候,但當前頁面的輸入法以及dialog消失等等。WebViewCore收到消息之後,會直接讓BrowserFrame調用JNI: nativeLoadUrl():

// BrowserFrame.java
    public void loadUrl(String url, Map extraHeaders) {
        mLoadInitFromJava = true;
        if (URLUtil.isJavaScriptUrl(url)) {
            // strip off the scheme and evaluate the string
            stringByEvaluatingJavaScriptFromString(
                    url.substring("javascript:".length()));
        } else {
            /** M: add log */
            Xlog.d(XLOGTAG, "browser frame loadUrl: " + url);
            nativeLoadUrl(url, extraHeaders);
        }
        mLoadInitFromJava = false;
    }

由於LoadUrl()不僅可以Load一個url,還可以執行一段js。如果load的是一段js,js並沒有被繼續往下load,而是直接在這裡執行掉。stringByEvaluatingJavaScriptFromString也會通過jni調用v8的接口去在mainFrame的scriptController中執行,關於js在WebKit後期會專門寫一篇關於WebKit的js的文章進行專門分析。到目前為止,LoadUrl還隻是簡單的使用一個String傳遞字符串而已。

// WebCoreFrameBridge.cpp
static void LoadUrl(JNIEnv *env, jobject obj, jstring url, jobject headers)
{
    WebCore::Frame* pFrame = GET_NATIVE_FRAME(env, obj);
    ALOG_ASSERT(pFrame, "nativeLoadUrl must take a valid frame pointer!");

    WTF::String webcoreUrl = jstringToWtfString(env, url);
    WebCore::KURL kurl(WebCore::KURL(), webcoreUrl);
    WebCore::ResourceRequest request(kurl);
    if (headers) {
        // dalvikvm will raise exception if any of these fail
        jclass mapClass = env->FindClass("java/util/Map");
        jmethodID entrySet = env->GetMethodID(mapClass, "entrySet",
                "()Ljava/util/Set;");
        jobject set = env->CallObjectMethod(headers, entrySet);

        jclass setClass = env->FindClass("java/util/Set");
        jmethodID iterator = env->GetMethodID(setClass, "iterator",
                "()Ljava/util/Iterator;");
        jobject iter = env->CallObjectMethod(set, iterator);

        jclass iteratorClass = env->FindClass("java/util/Iterator");
        jmethodID hasNext = env->GetMethodID(iteratorClass, "hasNext", "()Z");
        jmethodID next = env->GetMethodID(iteratorClass, "next",
                "()Ljava/lang/Object;");
        jclass entryClass = env->FindClass("java/util/Map$Entry");
        jmethodID getKey = env->GetMethodID(entryClass, "getKey",
                "()Ljava/lang/Object;");
        jmethodID getValue = env->GetMethodID(entryClass, "getValue",
                "()Ljava/lang/Object;");

        while (env->CallBooleanMethod(iter, hasNext)) {
            jobject entry = env->CallObjectMethod(iter, next);
            jstring key = (jstring) env->CallObjectMethod(entry, getKey);
            jstring value = (jstring) env->CallObjectMethod(entry, getValue);
            request.setHTTPHeaderField(jstringToWtfString(env, key), jstringToWtfString(env, value));
            env->DeleteLocalRef(entry);
            env->DeleteLocalRef(key);
            env->DeleteLocalRef(value);
        }
    // ...
    pFrame->loader()->load(request, false);
}

接下來,在JNI的LoadUrl中就開始創建ResourceRequest,由於WebView的java層面可以對url的請求頭進行設定,然後通過FrameLoader進行加載。這裡的pFrame就是與Java層的BrowserFrame對應的mainFrame。HTML在WebKit的層次上看,最低層的是Frame,然後才有Document,也就意味著HTML Document也是通過Frame的FrameLoader加載的:

pFrame->loader()->load(request, false);

調用棧

最後的這句話就是讓FrameLoader去加載url的request。後面的調用棧依次是:

void FrameLoader::load(const ResourceRequest& request, bool lockHistory)
void FrameLoader::load(const ResourceRequest& request, const SubstituteData& substituteData, bool lockHistory)
void FrameLoader::load(DocumentLoader* newDocumentLoader)
void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType type, PassRefPtr prpFormState)
void FrameLoader::callContinueLoadAfterNavigationPolicy(void* argument,
    const ResourceRequest& request, PassRefPtr formState, bool shouldContinue)
void FrameLoader::continueLoadAfterNavigationPolicy(const ResourceRequest&, PassRefPtr formState, bool shouldContinue)
void FrameLoader::continueLoadAfterWillSubmitForm()

其中加載Document的DocumentLoader在load中創建的:

void FrameLoader::load(const ResourceRequest& request, const SubstituteData& substituteData, bool lockHistory)
{
    if (m_inStopAllLoaders)
        return;
        
    // FIXME: is this the right place to reset loadType? Perhaps this should be done after loading is finished or aborted.
    m_loadType = FrameLoadTypeStandard;
    RefPtr loader = m_client->createDocumentLoader(request, substituteData);
    if (lockHistory && m_documentLoader)
        loader->setClientRedirectSourceForHistory(m_documentLoader->didCreateGlobalHistoryEntry() ? m_documentLoader->urlForHistory().string() : m_documentLoader->clientRedirectSourceForHistory());
    load(loader.get());
}

m_client->createDocumentLoader(request, substituteData);中的m_client是FrameLoaderClientAndroid。後面資源下載還有跟這個m_client打交道。在void FrameLoader::continueLoadAfterWillSubmitForm()之前,還沒有真正涉及到主資源的加載,還都隻是在對當前需要加載的Url進行一些列的判斷,一方面是安全問題,SecurityOrigin會對Url進行安全檢查,例如跨域。另一方面是Scroll,因為有時候後LoadUrl加載的Url會帶有Url Fragment也就是hash。關於url的hash的內容請參考《Fragment URLS》由於URL的hash,隻會滾動到頁面的某一個位置,所以這種情況下也不需要真正的去請求mainResource. 如果這些檢查都過瞭,就需要開始去加載mainResource瞭:

// FrameLoader.cpp
void FrameLoader::continueLoadAfterWillSubmitForm()
{
    // ...
    m_provisionalDocumentLoader->timing()->navigationStart = currentTime();

    // ...
    if (!m_provisionalDocumentLoader->startLoadingMainResource(identifier))
        m_provisionalDocumentLoader->updateLoading();
}

startLoadingMainResource這就開始load主資源也就是前面說的html文件。

三種DocumentLoader

這裡需要對m_provisionalDocumentLoader進行講解下:

    RefPtr m_documentLoader;
    RefPtr m_provisionalDocumentLoader;
    RefPtr m_policyDocumentLoader;
    void setDocumentLoader(DocumentLoader*);
    void setPolicyDocumentLoader(DocumentLoader*);
    void setProvisionalDocumentLoader(DocumentLoader*);

我們可以看到在FrameLoader.h中定義瞭三個DocumentLoader,WebKit其實是按角色劃分這幾個DocumentLoader的。其中:m_documentLoader是上一次已經加載過的DocumentLoader的指針,m_policyDocumentLoader就是用來做一些策略性的工作的,例如延遲加載等等。m_provisionalDocumentLoade是用來做實際的加載工作的。當一個DocumentLoader的工作完成之後,會通過setXXXXDocumentLoader來傳遞指針。按照URL加載的主流程:PolicyChcek——>Load MainResouce。也就是先進行策略檢查,最後才開始加載主資源。那麼這個三個DocumentLoader的順序應該是先createDocumentLoader後的指針傳遞給m_pollicyDocumentLoader,在策略檢查完之後,將指針傳遞給m_provisionalDocumentLoader,在Document加載完畢之後,將指針傳遞給m_documentLoader。

// FrameLoader.cpp
void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType type, PassRefPtr prpFormState)
{
    // ...   
    policyChecker()->stopCheck();
    // ...
    setPolicyDocumentLoader(loader);
    // ..
}

void FrameLoader::continueLoadAfterNavigationPolicy(const ResourceRequest&, PassRefPtr formState, bool shouldContinue)
{
    // ...
    setProvisionalDocumentLoader(m_policyDocumentLoader.get());
    m_loadType = type;
    setState(FrameStateProvisional);
    // ...
    setPolicyDocumentLoader(0);

}

void FrameLoader::transitionToCommitted(PassRefPtr cachedPage)
{
    // ...
    setDocumentLoader(m_provisionalDocumentLoader.get());
    setProvisionalDocumentLoader(0);
    // ...
}

void FrameLoader::checkLoadCompleteForThisFrame()
{
    switch (m_state) {
        case FrameStateProvisional: {
                // ...

                // If we're in the middle of loading multipart data, we need to restore the document loader.
                if (isReplacing() && !m_documentLoader.get())
                    setDocumentLoader(m_provisionalDocumentLoader.get());

                // Finish resetting the load state, but only if another load hasn't been started by the
                // delegate callback.
                if (pdl == m_provisionalDocumentLoader)
                    clearProvisionalLoad();
                
    }

    // ...
}

上面代碼片段可以看出,這三個DocumentLoader的承接關系是一環扣一環。由於index.html加載在WebKit中分為2中方式:如果是前進後退,index.html是從CachedPage中加載的,FrameLoader::transitionToCommitted就是在從CachedPage中加載完成之後被調用的,void FrameLoader::checkLoadCompleteForThisFrame()這是在從網絡加載完成之後被調用的。

// FrameLoader.cpp
void FrameLoader::recursiveCheckLoadComplete()
{
    Vector frames;
    
    for (RefPtr<frame> frame = m_frame->tree()->firstChild(); frame; frame = frame->tree()->nextSibling())
        frames.append(frame);
    
    unsigned size = frames.size();
    for (unsigned i = 0; i loader()->recursiveCheckLoadComplete();
    
    checkLoadCompleteForThisFrame();
}

// Called every time a resource is completely loaded, or an error is received.
void FrameLoader::checkLoadComplete()
{
    ASSERT(m_client->hasWebView());
    
    m_shouldCallCheckLoadComplete = false;

    // FIXME: Always traversing the entire frame tree is a bit inefficient, but 
    // is currently needed in order to null out the previous history item for all frames.
    if (Page* page = m_frame->page())
        page->mainFrame()->loader()->recursiveCheckLoadComplete();
}

需要強調的是,WebKit需要對Page裡面的所有Frame進行確認加載完畢之後,最後將setDocumentLoader()。對於這一點我個人理解是還有優化的空間。

startLoadingMainResource

在m_provisionalDocumentLoader調用startLoadingMainResource之後,就開始準備發送網絡請求瞭。調用棧如下:

bool DocumentLoader::startLoadingMainResource(unsigned long identifier)
bool MainResourceLoader::load(const ResourceRequest& r, const SubstituteData& substituteData)
bool MainResourceLoader::loadNow(ResourceRequest& r)
PassRefPtr ResourceHandle::create(NetworkingContext* context, 
	const ResourceRequest& request,
	ResourceHandleClient* client,
	bool defersLoading,
	bool shouldContentSniff)
bool ResourceHandle::start(NetworkingContext* context)
PassRefPtr ResourceLoaderAndroid::start(
        ResourceHandle* handle, const ResourceRequest& request,
	FrameLoaderClient* client, bool isMainResource, bool isSync)
bool WebUrlLoaderClient::start(bool isMainResource, bool isMainFrame, bool sync, WebRequestContext* context)

需要指出的是,雖然LoadUrl最後是在WebCore線程中執行的,但是最後資源下載是在Chromium_net的IO線程中進行的。在資源下載完畢之後,網絡數據會交給FrameLoaderClientAndroid

網絡數據

Android WebKit數據下載在Chromium_net的IO線程中完成之後會通過WebUrlLoaderClient向WebCore提交數據。WebKt的調用棧如下:

// Finish
void WebUrlLoaderClient::didFinishLoading()
void ResourceLoader::didFinishLoading(ResourceHandle*, double finishTime)
void MainResourceLoader::didFinishLoading(double finishTime)
void FrameLoader::finishedLoading()
void DocumentLoader::finishedLoading()
void FrameLoader::finishedLoadingDocument(DocumentLoader* loader)
void FrameLoaderClientAndroid::finishedLoading(DocumentLoader* docLoader)
void FrameLoaderClientAndroid::committedLoad(DocumentLoader* loader, 
     const char* data, int length)
void DocumentLoader::commitData(const char* bytes, int length)




// Receive Data
void WebUrlLoaderClient::didReceiveData(scoped_refptr buf, int size)
void ResourceLoader::didReceiveData(ResourceHandle*, const char* data, int length, 
     int encodedDataLength)
void ResourceLoader::didReceiveData(const char* data, int length,
     long long encodedDataLength, bool allAtOnce)
void MainResourceLoader::addData(const char* data, int length, bool allAtOnce)
void DocumentLoader::receivedData(const char* data, int length)
void DocumentLoader::commitLoad(const char* data, int length)
void FrameLoaderClientAndroid::committedLoad(DocumentLoader* loader,
     const char* data, int length)
void DocumentLoader::commitData(const char* bytes, int length)

這個過程其實分為兩步,一步是Chromium_net收到數據,另一部是Chromium_net通知WebKit,數據已經下載完畢可以finish瞭。這個兩個過程都會調用FrameLoaderClienetAndroid::committedLoad()。隻不過參數不一樣,在finish的時候,將傳入的length為0,這樣通知WebKit,數據已經傳送完畢,記者WebKit就開始使用commitData拿到的數據進行解析,構建Dom Tree和Render Tree。關於Dom Tree Render Tree的構建過程下一節詳細的講述。

版權申明:
轉載文章請註明原文出處,任何用於商業目的,請聯系譚海燕本人:hyman_tan@126.com

發佈留言