782
技術社區[雲棲]
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<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(); }
WebViewCore在接收到LOAD_URL之後,會通過BrowserFrame調用nativeLoadUrl,這個BrowserFrame與C++層的mainFrame對接。這裏順便提一下clearHeapers()的作用:如果當前網頁有對話框dialog,有輸入法之類的,clearHelpers就是用來清理這些東西的。這也是為什麼加載一個新頁麵的時候,但當前頁麵的輸入法以及dialog消失等等。WebViewCore收到消息之後,會直接讓BrowserFrame調用JNI: nativeLoadUrl():
// BrowserFrame.java public void loadUrl(String url, Map<String, String> 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<FormState> prpFormState) void FrameLoader::callContinueLoadAfterNavigationPolicy(void* argument, const ResourceRequest& request, PassRefPtr<FormState> formState, bool shouldContinue) void FrameLoader::continueLoadAfterNavigationPolicy(const ResourceRequest&, PassRefPtr<FormState> 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<DocumentLoader> 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<DocumentLoader> m_documentLoader; RefPtr<DocumentLoader> m_provisionalDocumentLoader; RefPtr<DocumentLoader> 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<FormState> prpFormState) { // ... policyChecker()->stopCheck(); // ... setPolicyDocumentLoader(loader); // .. } void FrameLoader::continueLoadAfterNavigationPolicy(const ResourceRequest&, PassRefPtr<FormState> formState, bool shouldContinue) { // ... setProvisionalDocumentLoader(m_policyDocumentLoader.get()); m_loadType = type; setState(FrameStateProvisional); // ... setPolicyDocumentLoader(0); } void FrameLoader::transitionToCommitted(PassRefPtr<CachedPage> 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<RefPtr<Frame>, 10> 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 < size; i++) frames[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> ResourceHandle::create(NetworkingContext* context, const ResourceRequest& request, ResourceHandleClient* client, bool defersLoading, bool shouldContentSniff) bool ResourceHandle::start(NetworkingContext* context) PassRefPtr<ResourceLoaderAndroid> 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<net::IOBuffer> 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
最後更新:2017-04-03 12:55:21