Hybrid技術(shù)現(xiàn)在已經(jīng)是一個常見的技術(shù)方案,它既能享受到Native的能力,同時還能擁有H5技術(shù)低成本、高效率和跨平臺等特性。然而,H5技術(shù)的加載速度一直飽受詬病,相比于Native,app打開H5頁面時,H5會發(fā)起多個HTTP請求去加" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

18143453325 在線咨詢 在線咨詢
18143453325 在線咨詢
所在位置: 首頁 > 營銷資訊 > 建站知識 > 性能提升30%以上 JDHybrid h5加載優(yōu)化實(shí)踐

性能提升30%以上 JDHybrid h5加載優(yōu)化實(shí)踐

時間:2023-02-01 03:00:01 | 來源:建站知識

時間:2023-02-01 03:00:01 來源:建站知識

背景

Hybrid技術(shù)現(xiàn)在已經(jīng)是一個常見的技術(shù)方案,它既能享受到Native的能力,同時還能擁有H5技術(shù)低成本、高效率和跨平臺等特性。然而,H5技術(shù)的加載速度一直飽受詬病,相比于Native,app打開H5頁面時,H5會發(fā)起多個HTTP請求去加載資源,資源大小、網(wǎng)絡(luò)質(zhì)量都會影響頁面打開速度。為了節(jié)省這部分時間,我們可以把首屏的一些靜態(tài)資源(如img、js、css、html等)打包提前加載到本地磁盤,當(dāng)加載頁面時直接從本地磁盤(或內(nèi)存)獲取資源加載。本地的讀寫速度是遠(yuǎn)高于網(wǎng)絡(luò)請求的,尤其是在網(wǎng)絡(luò)不良或資源太大的情況下,離線化方案更能展現(xiàn)出它的優(yōu)勢。此外,在一些大促等高流量場景下,提前將活動頁面資源下載到客戶端本地,可以大幅降低活動當(dāng)天的CDN峰值與帶寬,在降本提效方面有顯著的作用。




618大促效果







從去年11.11開始,JDHybrid開始承接各類大促業(yè)務(wù),無論是沸騰之夜還是春晚項(xiàng)目,JDHybrid在降本提效方面都發(fā)揮了重要的作用。經(jīng)過這些超級流量的大考之后,JDHybrid在業(yè)務(wù)范圍、業(yè)務(wù)服務(wù)質(zhì)量、穩(wěn)定性等多個方面都有了很大的進(jìn)步。今年618期間,618主會場、T級互動、秒殺主會場、百億補(bǔ)貼等多個核心業(yè)務(wù)均使用了JDHybrid,整體首屏加載速度提升30%以上,秒開率提升20%以上,頁面錯誤率降低了60%以上。







下面我們就來看一看數(shù)據(jù)的背后JDHybrid都做了些什么。




離線加載機(jī)制探究

01

Android 離線加載機(jī)制

Android實(shí)現(xiàn)加載本地文件的api相對較少,主要是包括直接load本地文件以及攔截資源請求兩個方案。

1.1 直接load本地文件

通過WebView的loadUrl方法加載本地h5工程




webview.loadUrl(XCache.getApp(id).getHtmlPath());對客戶端來說該方法簡單粗暴,而且加載速度非???,但存在許多因?yàn)閒ile協(xié)議引起的問題,本地文件的url格式為“file:///data/data/pacakagename/xxxxx”,從url上看這類url是不存在域名的,而h5頁面的加載大多與域名相關(guān)聯(lián)。

雖然能通過曲線救國方式解決以上問題,比如將網(wǎng)絡(luò)請求橋接到原生,但整體改動費(fèi)時費(fèi)力,放棄吧。

1.2 攔截資源請求

谷歌提供了攔截資源請求的API:




@Nullablepublic WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { return shouldInterceptRequest(view, request.getUrl().toString());}只需要構(gòu)造WebResourceResponse對象即可實(shí)現(xiàn)本地文件加載,毫無副作用,這也是目前各大廠App的方案,相比iOS,安卓在方案上相對比較簡單。京東商城App最終也是使用了該方案攔截加載本地文件,我們在這里提一下特別需要注意的點(diǎn)。

1.2.1 請求中body丟失

WebResourceRequest中不包含body,該方法中不要攔截post請求,會有丟失body的風(fēng)險。京東只用該方法加載本地文件資源,所以不存在body丟失的情況。

1.2.2 WebResourceResponse的構(gòu)造

先看看WebResourceResponse的構(gòu)造方法,主要包括mimeType、encoding、文件數(shù)據(jù)流三個入?yún)?shù),如谷歌源碼注解中提到的,mimeType和encoding只能是單個值,不能是整個Content-Type值。我們嘗試發(fā)現(xiàn)js、css等資源如果mimeType和encoding是錯誤的,內(nèi)核都按默認(rèn)的utf-8編碼文本進(jìn)行處理。但mimeType對于html來說必須是特定的格式,比如“text/html”,否則內(nèi)核無法解析html。




/** * Constructs a resource response with the given MIME type, character encoding, * and input stream. Callers must implement {@link InputStream#read(byte[])} for * the input stream. {@link InputStream#close()} will be called after the WebView * has finished with the response. * * <p class="note"><b>Note:</b> The MIME type and character encoding must * be specified as separate parameters (for example {@code "text/html"} and * {@code "utf-8"}), not a single value like the {@code "text/html; charset=utf-8"} * format used in the HTTP Content-Type header. Do not use the value of a HTTP * Content-Encoding header for {@code encoding}, as that header does not specify a * character encoding. Content without a defined character encoding (for example * image resources) should pass {@code null} for {@code encoding}. * * @param mimeType the resource response's MIME type, for example {@code "text/html"}. * @param encoding the resource response's character encoding, for example {@code "utf-8"}. * @param data the input stream that provides the resource response's data. Must not be a * StringBufferInputStream. */ public WebResourceResponse(String mimeType, String encoding, InputStream data) { mMimeType = mimeType; mEncoding = encoding; setData(data); }1.2.3 跨域請求資源

除了mimeType、encoding還有一些需要特別注意的,包括access-control-allow-origin、timing-allow-origin等一些跨域的Header。一般情況下,瀏覽器內(nèi)核是默認(rèn)js、css等資源文件是允許跨域的,不排除前端因?yàn)橐恍┨厥庠驈?qiáng)制校驗(yàn)跨域。如:




<script src="user.com/index.js" crossorigin ></script>如果前端限制了跨域,加載本地文件也必須在Response header中增加跨域處理,否則內(nèi)核會拒絕響應(yīng)。




header.put("access-control-allow-origin",*);header.put("timing-allow-origin",*);02

iOS 離線加載機(jī)制

自從Apple廢棄UIWebView之后,iOS端的離線加載技術(shù)變的異常復(fù)雜,無論何種方案,都會帶有先天不足,需要通過各種補(bǔ)丁解決。業(yè)界目前采用的方案也不太統(tǒng)一,方案均帶有明顯的業(yè)務(wù)特征。京東內(nèi)部h5業(yè)務(wù)也有自身的一些特點(diǎn),所以JDHybrid在方案選擇上主要考慮了以下幾點(diǎn):第一、因?yàn)閔5開發(fā)相對開放,所以,沒有一個相對穩(wěn)定的h5開發(fā)平臺可以對接,無法形成統(tǒng)一的規(guī)則約束來簡化方案設(shè)計成本與使用成本;第二、大部分h5業(yè)務(wù)都是比較成熟的業(yè)務(wù),顛覆性的方案設(shè)計會因?yàn)榍秩胄暂^強(qiáng)降低業(yè)務(wù)的接入意愿;所以,JDHybrid的設(shè)計必須建立在“業(yè)務(wù)研發(fā)0修改”的基礎(chǔ)之上。當(dāng)然,在方案設(shè)計過程中,我們也對現(xiàn)有的方案進(jìn)行了摸排,有些后面被棄用的方案甚至在線上進(jìn)行了驗(yàn)證,這里對相關(guān)的探索歷程也總結(jié)一下。

2.1 直接加載本地文件

和Andriod端類似,iOS也可以通過加載本地文件來實(shí)現(xiàn)離線包的加載




- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL但這個方案問題太多,無法推廣使用,具體的問題與副作用和前述Android端類似,這里就不贅述了。

2.2 LocalServer

方案一因?yàn)轫撁娴膮f(xié)議頭是file,它的處理會給方案本身帶來大量的工作,那么我們是否有方案可以讓webview去load一個http的鏈接并加載本地文件?接下來我們來介紹本地server的方案:建立本地server來模擬http(s)場景,并在相應(yīng)時機(jī)返回對應(yīng)的離線資源。

可以使用CocoaHttpServer來開啟本地服務(wù),這個庫可以很好的支持https。除了CocoaHTTPServer外,我們也可以使用GCDWebServer(支持http,oc)和Telegraph(支持http(s),swift)來開啟本地server。

關(guān)于localServer在iOS端的實(shí)現(xiàn)以及https的支持,可以參考《基于 LocalWebServer 實(shí)現(xiàn) WKWebView 離線資源加載》。

webview加載http(s)鏈接:




[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http(s)://127.0.0.1:1000/index.html"]]]


webview加載資源過程中,所有相對路徑的資源如<img src="img/icon.png">都會通過我們的本地服務(wù)器,這時,我們就可以通過攔截這些資源并返回離線包中的資源。LocalServer方案加載離線包有幾個問題:

LocalServer可以實(shí)現(xiàn)http(s)環(huán)境,相比于方案一,它僅僅解決了http協(xié)議的問題,方案一的其他問題仍然存在,業(yè)務(wù)兼容與開發(fā)成本并未出現(xiàn)明顯下降。另外它也會帶來許多額外的問題,如性能消耗、電量消耗、資源訪問權(quán)限安全等。

2.3 NSURLProtocol

加載本地文件和LocalServer的方案在跨域、cookie、js原生api方面的缺陷既對業(yè)務(wù)不夠友好,且可以預(yù)見其他h5層面的問題會層出不窮。所以考慮讓W(xué)KWebView正常的加載業(yè)務(wù)的h5鏈接,然后攔截相關(guān)請求并加載本地資源是避免上述問題的根本方案。在使用UIWebView的時候我們可以通過NSURLProtocol攔截webview中的網(wǎng)絡(luò)請求,那么我們是否可以通過NSURLProtocol攔截WKWebView中的網(wǎng)絡(luò)請求呢?答案是可以的,但是因?yàn)閃ebKit是獨(dú)立于主app之外的進(jìn)行運(yùn)行的,我們不能通過簡單的NSURLProtocol注冊就能攔截到http(s)請求,在進(jìn)行自定義NSURLProtocol的注冊后,我們還需要調(diào)用系統(tǒng)私有api進(jìn)行處理:




Class cls = NSClassFromString(@"WKBrowsingContextController");SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");if ([(id)cls respondsToSelector:sel]) { [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"];}接下來webview發(fā)送的網(wǎng)絡(luò)請求便會被攔截到自定義的NSURLProtocol中,后續(xù)的流程這里不再贅述了。

NSURLProtocol可以幫助我們攔截http(s)請求,但是在實(shí)踐過程中發(fā)現(xiàn)它會有以下問題:

H5 Post請求會丟失body

WebKit是一個多進(jìn)程架構(gòu),網(wǎng)絡(luò)請求發(fā)出是在其他進(jìn)程,在攔截請求后,需要通過IPC將請求從其他進(jìn)程發(fā)送到App進(jìn)程,webkit出于優(yōu)化的目的會丟棄HTTPBody與HTTPBodyStream字段,這就導(dǎo)致了POST請求body丟失的問題。我們可以通過js hook橋接(或者干脆提供js橋接的網(wǎng)絡(luò)請求方式,特別適合h5開發(fā)鏈路集中的團(tuán)隊(duì)),將post請求通過jsbridge轉(zhuǎn)發(fā)到原生請求。

NSURLProtocol的攔截是全局性質(zhì)的

一旦通過私有api注冊https(s) scheme后,在注銷之前,所有WKWebView發(fā)起的post網(wǎng)絡(luò)請求都會丟失body(即使你沒有攔截它,仍然返回給webview去做請求也不例外),這會對三方webview造成影響。

私有api問題

會有審核被拒的風(fēng)險,且WebKit官方明確對相關(guān)api標(biāo)注了廢棄,提示開發(fā)者用WKURLSchemeHandler去替換,長遠(yuǎn)看也有被官方移除的風(fēng)險。

2.4 WKURLSchemeHandler

iOS11之后WebKit框架引入了新特性WKURLSchemeHandler來支持自定義請求的管理。我們可以通過customScheme來攔截webview頁面的請求,如果要攔截http(s)則需要hook WKWebView的。




+ (BOOL)handlesURLScheme:(NSString *)urlScheme方法,在scheme為http(s)時返回NO。同時在webview初始化時,通過WKWebViewConfiguration對需要攔截的scheme進(jìn)行注冊。




WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];[configuration setURLSchemeHandler:(id<WKURLSchemeHandler>)self forURLScheme:@"https"];[configuration setURLSchemeHandler:(id<WKURLSchemeHandler>)self forURLScheme:@"http"];WKURLSchemeHandler協(xié)議只包含兩個方法:




- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;網(wǎng)頁開始數(shù)據(jù)請求,我們需要在這個方法中對網(wǎng)頁中的請求進(jìn)行處理和返回。




- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;網(wǎng)頁取消數(shù)據(jù)請求,我們需要在這個方法中停止請求。

同樣是http(s)攔截方案,通過API對比我們可以發(fā)現(xiàn)方案攔截后雖然需要我們做更多的工作(后面會具體介紹),但是它支持WKWebView實(shí)例級別的攔截,風(fēng)險相對可控。

因?yàn)閃KURLSchemeHandler本身是不支持?jǐn)r截Http(s)的,所以在我們用hook方法使其支持后會有很多問題:

這些問題如何解決,下文我們會逐一介紹。在介紹這個方案之前,我們先來看一下為什么要做這種方案選擇。

2.4.1 方案優(yōu)缺點(diǎn)

我們可以從以下幾個點(diǎn)做下對比:







隔離性:我們希望只會對使用離線包的業(yè)務(wù)進(jìn)行攔截,而非所有webview頁面。顯然以下兩種方案是不滿足的:localServer和NSURLProtocol的攔截都是全局的。

業(yè)務(wù)無入侵:方案的實(shí)施如果要求h5層面去開發(fā)適配,將會導(dǎo)致方案的可用性變差,影響業(yè)務(wù)接入意愿。NSURLProtocol與WKURLSchemeHandler只影響請求方式,不影響請求內(nèi)容,H5代碼無需改動。而本地文件與localServer則需要h5做較多的工作去適配,且對業(yè)務(wù)編碼提出了一些要求,如離線資源使用相對路徑、遠(yuǎn)程資源使用絕對路徑,嚴(yán)重侵入H5業(yè)務(wù)的開發(fā)流程。

系統(tǒng)兼容:方案應(yīng)該可以兼容主流系統(tǒng),使方案發(fā)揮最大的價值。這里需要重點(diǎn)考慮方案覆蓋的范圍是否“充足”,在復(fù)雜性與兼容性之間平衡即可。

擴(kuò)展性:在業(yè)務(wù)無感知的情況下,我們可以通過攔截資源,做出更多加載上的優(yōu)化。顯然,方案一和方案二的擴(kuò)展性是最差的;方案三與方案四的擴(kuò)展性最好,可以在業(yè)務(wù)方無感知的情況下實(shí)現(xiàn)預(yù)加載處理、公共資源離線、資源性能監(jiān)控等能力。

從前面的對比我們可以看出各種方案的優(yōu)缺點(diǎn)。而京東的h5實(shí)際開發(fā)生態(tài)要求我們提供一個對h5無侵入、方案使用范圍可控、擴(kuò)展性良好、長遠(yuǎn)看穩(wěn)定性佳的方案?;谶@些原因我們選擇了基于WKURLSchemeHandler的攔截方案。

2.4.2 方案實(shí)現(xiàn)

WKURLSchemeHandler從iOS 11開始支持,需要我們提供一個實(shí)例對象遵守WKURLSchemeHandler 協(xié)議,并且通過使用 WKWebView Configuration 的方法 setURLSchemeHandler(_:forURLScheme:) 進(jìn)行注冊。在這里面有幾點(diǎn)需要注意:

只支持注冊自定義 scheme

根據(jù)蘋果文檔的說明,這種攔截方式只支持注冊自定義 scheme ,而常見的內(nèi)建協(xié)議如 http 、 https 、file等都不支持。針對自定義的scheme,這里需要注意的是頁面的鏈接必須用自定義scheme。如果在https的頁面內(nèi)針對個別資源添加自定義的scheme一般會被瀏覽器block。瀏覽器認(rèn)為我們自定義的 scheme 不是安全的協(xié)議而禁止加載。

實(shí)現(xiàn)攔截非自定義 scheme

為了減少業(yè)務(wù)方接入成本,最好的方案是攔截 https,這樣不需要業(yè)務(wù)方改動代碼即可使用離線加載能力。默認(rèn)情況下,如果我們嘗試注冊https的攔截,會造成崩潰.原因也很簡單,查看WebKit源碼會發(fā)現(xiàn)。




- (void)setURLSchemeHandler:(id <WKURLSchemeHandler>)urlSchemeHandler forURLScheme:(NSString *)urlScheme{ if ([WKWebView handlesURLScheme:urlScheme]) [NSException raise:NSInvalidArgumentException format:@"'%@' is a URL scheme that WKWebView handles natively", urlScheme]; .....}如果系統(tǒng)檢測到正在注冊內(nèi)建的協(xié)議,則會拋出異常。這里我們直接hook WKWebView 的 handlesURLScheme: 使之在http(s)時返回NO即可。這樣我們就支持了http(s)的攔截。當(dāng)然這里存在一個疑問,handlesURLScheme:的hook是否會影響其他webview的加載過程,這個是不會的。因?yàn)榧虞d行為的改變WebKit是通過檢測是否注冊了對應(yīng)協(xié)議的handler,這里的hook只是為https協(xié)議注冊handler掃清了障礙,如果沒有實(shí)際注冊Handler,就不會改變原來的加載流程。現(xiàn)在WebView的資源請求流程就如下圖所示了。







2.4.3 踩坑記錄

至此,我們終于打開了WKWebView攔截http(s)的魔盒,接下來就是逐個填坑的過程。

iOS 11.3之前丟失body

使用 WKURLSchemeHandler 方案在iOS 11.3之前的系統(tǒng)上攔截post請求是會丟失body的,由于11.3以下的用戶量已經(jīng)占比很少,所以我們并沒有這個點(diǎn)上花費(fèi)太多的精力,直接將支持的系統(tǒng)版本提高了(實(shí)際上iOS12存在WKUrlSchemeTask析構(gòu)時也會崩潰的問題,所以,我們直接將離線加載的起始版本定為了iOS13)。如果你的項(xiàng)目需要支持iOS11.3以下的攔截,可以考慮hook XMLHttpRequest和fetch請求通過jsBridge的方式把整個請求轉(zhuǎn)發(fā)到Native去做處理。

Blob 數(shù)據(jù)類型功能異常

在我們實(shí)踐中發(fā)現(xiàn),只要攔截到的請求使用了 Blob 數(shù)據(jù)類型,就會出現(xiàn)異常(比如文件上傳失敗)。通過WebKit源碼,我們發(fā)現(xiàn)。




ExceptionOr<void> XMLHttpRequest::send(Blob& body){ if (auto result = prepareToSend()) return WTFMove(result.value()); if (m_method != "GET" && m_method != "HEAD") { if (!m_url.protocolIsInHTTPFamily()) { // FIXME: We would like to support posting Blobs to non-http URLs (e.g. custom URL schemes) // but because of the architecture of blob-handling that will require a fair amount of work. ASCIILiteral consoleMessage { "POST of a Blob to non-HTTP protocols in XMLHttpRequest.send() is currently unsupported."_s }; scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Warning, consoleMessage); ...... } return createRequest();}原來是WebKit工程師“偷懶”了,因?yàn)楣ぷ髁看筮€沒有支持。我們的變通辦法是通過注入js代碼,hook XHR和fetch請求解決,把帶有Blob 類型的請求轉(zhuǎn)到Native來解決。

WKURLSchemeTask協(xié)議方法回調(diào)崩潰。

WKURLSchemeTask協(xié)議通過以下幾個方法回傳Native的數(shù)據(jù)給WebKit,但是調(diào)用順序一旦出錯,直接會導(dǎo)致crash。




- (void)didReceiveResponse:(NSURLResponse *)response;- (void)didReceiveData:(NSData *)data;- (void)didFinish;- (void)didFailWithError:(NSError *)error;這里要注意didReceiveData因?yàn)閿?shù)據(jù)可能會分段傳輸,它會調(diào)用多次。所以,要在邏輯和機(jī)制上保證上述順序必須無誤。

WKURLSchemeTask 生命周期問題

我們使用WKURLSchemeTask實(shí)例進(jìn)行數(shù)據(jù)回調(diào)時,如果此時實(shí)例已經(jīng)被釋放就會發(fā)生crash。由于釋放的操作是在WebKit內(nèi)核進(jìn)行,外部無法控制其生命周期,所以需要在使用前檢測是否存活。雖然WKURLSchemeHandler提供了一個協(xié)議方法。




- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;在WKURLSchemeTask不可用時通知我們,但是實(shí)踐下來,這個時機(jī)并不可靠,線上仍然會有一些crash發(fā)生。通過源碼發(fā)現(xiàn),這類case下WebKit拋出的都是 NSException 異常,我們做了一層 try-catch 保護(hù)。

Cookie同步問題

cookie同步問題是WKWebview下的一個很棘手的問題。從Cookie的操作角度來看,Native、H5、服務(wù)端三方均可操作cookie。從進(jìn)程的角度來看,UIProcess、WebContentProcess、NetworkProcess三種進(jìn)程都有cookie的管理。所以,這就導(dǎo)致了Cookie同步的復(fù)雜性。我們先從進(jìn)程的角度來看一下攔截前Cookie的管理模型。







首先說明一下,cookie實(shí)際上是由CFHttpCookieStorage來做的管理,而NSHttpCookieStorage是基于CFHTTPCookieStorage封裝的(可以從WebKit內(nèi)部使用的一些私有API看出),這里用NSHTTPCookieStorage表示是考慮到大多數(shù)人對它比較熟悉。從圖中可以看出NetworkProcess是實(shí)際的cookie管理者,它負(fù)責(zé)多方cookie操作的聚合,這里就解釋了為什么我們從App進(jìn)程中通過NSHttpCookieStorage去讀取cookie時有延時。因?yàn)槟J(rèn)情況下只有在NetworkProcess寫入了,UIProcess才有可能獲取到(不同進(jìn)程間的cookie共享應(yīng)該是通過共享存儲cookie的文件來完成的),這里cookie文件的寫入策略、IPC通信等就會產(chǎn)生時間差?,F(xiàn)在我們再來看cookie同步的需求。

由前面的攔截方案對比,我們知道WKURLSchemeHandler下我們攔截了所有的請求,這里面帶來了一個cookie管理的變化。服務(wù)端操作cookie將首先在UIProcess生效,接口請求攜帶的cookie也是從UIProcess讀取。所以,我們實(shí)質(zhì)上需要把cookie的管理權(quán)轉(zhuǎn)移給UIProcess。UIProcess操作cookie可以通過NSHttpsCookieStorage。服務(wù)端的cookie讀寫由NSURLSession、NSURLRequest、NSHttpsCookieStorage默認(rèn)處理。現(xiàn)在只剩下WebContent進(jìn)程和UIProcess進(jìn)程之間Cookie的同步問題。

UIProcess cookie同步到WebContentProcess

UIProcess的Cookie發(fā)生變化可以通過WKHTTPCookieStore接口去設(shè)置,cookie先到NetworkProcess,然后觸發(fā)監(jiān)聽,通知WebContentProcess,這樣,h5就可以訪問到這類cookie。比如我們的某個請求response會寫cookie,在UIProcess里面,系統(tǒng)會默認(rèn)處理,然后我們需要如下操作就會將它同步給WebContentProcess。response中的“Set-Cookie”字段也是同樣的方式處理。




- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{ NSArray <NSHTTPCookie *>*responseCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL]; if ([responseCookies isKindOfClass:[NSArray class]] && responseCookies.count > 0) { dispatch_async(dispatch_get_main_queue(), ^{ [responseCookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) { // 同步到WKWebView if (@available(iOS 11.0, *)) { [[WKWebsiteDataStore defaultDataStore].httpCookieStore setCookie:cookie completionHandler:nil]; } else { // Fallback on earlier versions } }]; }); } completionHandler(NSURLSessionResponseAllow); }WebContentProcess產(chǎn)生的Cookie同步到UIProcess

當(dāng)h5寫入cookie(通過document.cookie='key=value&...'的形式),WebContentProces會先獲取到,然后會通知NetworkProcess,這個時候如果不做額外的處理又會發(fā)生NetworkProcess可能同步慢,導(dǎo)致UIProcess無法及時拿到cookie的問題。通過監(jiān)控發(fā)現(xiàn),這個不同步發(fā)生的概率在我們業(yè)務(wù)場景下大于萬分之五。關(guān)于這個問題,我們嘗試了兩個方案:一是WKHTTPCookieStore提供了cookie變化的監(jiān)聽,通過這個方案我們可以在cookie發(fā)生變化時及時同步。但遺憾的是如同我們前面介紹的一樣,cookie的變化影響因子很多,所以這個系統(tǒng)回調(diào)會執(zhí)行的很頻繁,導(dǎo)致我們使用之后出現(xiàn)了明顯的app卡頓。所以,我們采用了另一個方案:hook document.cookie的set方法,通過js橋?qū)5想寫入的cookie直接傳給UIProcess,然后解析、寫入NSHttpCookieStorage以備接口請求使用。

總結(jié)下來,現(xiàn)在的cookie管理模型如圖所示。







請求重定向問題

由于 WKURLSchemeTask 協(xié)議沒有處理重定向的方法,所以如果在攔截后請求時發(fā)生重定向,我們只能拿到最后的結(jié)果,導(dǎo)致WebView是無法感知重定向的。我們的解決辦法是針對HTML的重定向,拿到目標(biāo)地址location進(jìn)行重新加載并取消前次加載(這里會產(chǎn)生一次-999的取消請求錯誤,如果做監(jiān)控的話可以屏蔽)。

2.4.4 非離線資源請求

因?yàn)閃KURLSchemeHandler的攔截方案攔截了所有的http的請求,所以我們不僅需要處理離線資源,還需要處理非離線資源的請求。這里我們也設(shè)計了自己的網(wǎng)絡(luò)請求框架,需要注意的是WebKit默認(rèn)的網(wǎng)絡(luò)請求模塊在NetworkProcess,http的緩存協(xié)議的處理、磁盤緩存的能力都是在這部分完成的,我們的網(wǎng)絡(luò)框架要事實(shí)上承擔(dān)這部分職責(zé),顯然一個基本的NSURLSessionDataTask太低效了。如圖是我們的網(wǎng)絡(luò)請求示意圖:







這里簡單介紹一下我們的網(wǎng)絡(luò)層的幾點(diǎn)優(yōu)化措施:

網(wǎng)絡(luò)連接復(fù)用

我們知道在HTTP2.0支持TCP連接復(fù)用,但是在客戶端層面不同的NSURLSession實(shí)例是無法進(jìn)行復(fù)用的。如果連接被復(fù)用,DNS解析、TCP握手、網(wǎng)絡(luò)連接的時間都可以被節(jié)省的,這些時間耗時在50?100ms左右??梢酝ㄟ^Charles抓包就可以驗(yàn)證。如下圖所示:







所以我們的網(wǎng)絡(luò)框架設(shè)計了在底層使用同一個NSURLSession實(shí)例,上層進(jìn)行網(wǎng)絡(luò)請求的管理,盡可能的去復(fù)用網(wǎng)絡(luò)通道,當(dāng)然AFNetworking等網(wǎng)絡(luò)框架也是這么做的。

并發(fā)控制

為了對網(wǎng)絡(luò)框架進(jìn)行并發(fā)控制,在底層使用同一個并發(fā)隊(duì)列,便于總體控制。根據(jù)當(dāng)前系統(tǒng)狀況調(diào)整并發(fā)量,另外自定義異步Operation進(jìn)行任務(wù)的處理。

超時重試機(jī)制

超時重試機(jī)制的超時時間和重試次數(shù)的選擇沒有一個標(biāo)準(zhǔn),針對不同的網(wǎng)絡(luò)環(huán)境和請求類型應(yīng)該有靈活的策略。我們采取快速重試和超時時間遞增的策略,可以解決部分短時網(wǎng)絡(luò)故障下的資源重試問題。

任務(wù)優(yōu)先級

不同的資源和請求類型,優(yōu)先級顯然是不同的,我們設(shè)置HTML文件優(yōu)先級最高,js、css以及超時重試的優(yōu)先級次之,最后才是其他資源優(yōu)先級最低。后續(xù)這部分還會進(jìn)一步細(xì)化,比如將性能、異常類的埋點(diǎn)的優(yōu)先級調(diào)整到最低,避免它們與業(yè)務(wù)請求搶占資源。

自定義網(wǎng)絡(luò)緩存

由于NSURLCache容量較小、不支持自定義策略以及無法使用磁盤緩存等缺點(diǎn),我們自己實(shí)現(xiàn)了一套網(wǎng)絡(luò)緩存,遵守http標(biāo)準(zhǔn)緩存協(xié)議同時添加了一些自定義策略,除了支持內(nèi)存緩存和磁盤緩存外,還支持自定義緩存策略,比如:緩存限制、可動態(tài)分配緩存容量、不影響首屏加載的資源不緩存、HTML不緩存等,另外還實(shí)現(xiàn)了LRU的淘汰策略和緩存清理功能。




京東商城App離線加載實(shí)踐

京東商城將h5資源分為業(yè)務(wù)離線包和公共離線包,離線文件結(jié)構(gòu)如下:







業(yè)務(wù)離線包主要包含業(yè)務(wù)開發(fā)的js、css、圖片等資源,公共離線包主要包含京東通用的功能組件,如互動類可以使用同一個公共離線包,共用互動組件資源,避免業(yè)務(wù)離線包之間資源重復(fù)造成流量及帶寬的浪費(fèi)。




01

離線包的生成

離線包資源作為H5頁面資源在客戶端本地的一份拷貝,在資源內(nèi)容上應(yīng)該是完全一致的。所以在生成離線包的初期,我們需要接入的業(yè)務(wù)將發(fā)布H5頁面的資源,在平臺同步上傳一份來保證資源一致。該方案需要同一份資源在兩個平臺發(fā)布,因?yàn)椴煌脚_發(fā)布策略的不同,會伴隨著帶來接入方改造成本,需要接入的H5業(yè)務(wù)同時維護(hù)兩套打包流程,以滿足離線包規(guī)范和H5頁面規(guī)范。

為了降低對H5業(yè)務(wù)帶來的額外接入成本,我們優(yōu)化了離線包的生成流程。

接入方從一個可訪問的H5頁面URL入手,通過在服務(wù)端模擬瀏覽器的頁面加載過程,攔截所有的頁面加載請求,從中分析出可配置離線的資源URL列表返回到配置平臺?;诓煌Y源參與H5頁面首屏幕渲染的權(quán)重不一樣,在這一步我們也會提供優(yōu)先css/js的返回策略。后續(xù)接入方可以通過可視化的方式在平臺勾選希望離線加載的資源,確認(rèn)后,服務(wù)端會拉取對應(yīng)的URL資源生成離線包,并在包中加入一個資源描述文件,包含用戶客戶端匹配的資源URL,以及真實(shí)的資源請求header,方便用于離線資源的回傳。







經(jīng)過這個流程的優(yōu)化,已經(jīng)大大降低了業(yè)務(wù)接入成本,不需要再維護(hù)兩套打包策略,但是不可避免的還需要接入方手動選擇離線包資源。為進(jìn)一步降低接入難度,我們提供了另一種前端工程自動化的離線包生成方式,通過提供命令行工具來融入前端H5項(xiàng)目中,配置生成離線包目錄,將該目錄中的資源通過一行命令生成合規(guī)離線包,并上傳到發(fā)布平臺。




02

離線包本地管理

2.1 下載分級

為了提高離線包的使用率,對離線包的下載進(jìn)行分級分類,一共分成T級、S級、A級分別對應(yīng)不同的下載策略。

T級:app啟動或app切換前后臺時觸發(fā)下載,主要包括大促等入口在首頁且PV量較高的業(yè)務(wù)。

S級:首頁渲染完成后觸發(fā)下載,主要包括入口比較淺的業(yè)務(wù),但同時避免搶占首頁本身的資源加載。

A級:指定頁面觸發(fā)下載,適合入口比較固定、PV量相對較小的業(yè)務(wù),避免所有用戶下載導(dǎo)致流量及帶寬的浪費(fèi)。

同一級別下不同離線包的下載順序則采用配置權(quán)重+本地權(quán)重的方式進(jìn)行權(quán)重加和計算優(yōu)先級,其中本地優(yōu)先級根據(jù)LRU算法動態(tài)計算,用戶最近使用越多的離線包權(quán)重越高。

最終權(quán)重 = 配置權(quán)重 + 本地權(quán)重(LRU)

除了以上優(yōu)先級的處理,離線包下載也增加了部分前置下載條件,主要包括當(dāng)前線程數(shù)、CPU使用率等,不做壓死駱駝的最后一根稻草。

2.2 差分包策略

京東商城離線包差分使用的是bsdiff差分算法,那么差分包如何進(jìn)行管理呢?一般情況做法都是客戶端將本地離線包的版本號上傳給服務(wù)端,由服務(wù)端返回對應(yīng)版本的差分包地址下發(fā)到客戶端。但是在離線包量較大的情況下,客戶端就需要獲取所有離線包版本進(jìn)行上傳,對于客戶端和服務(wù)端都會增加一些邏輯。所以我們前后端做了規(guī)范約定,只需服務(wù)端按規(guī)范生成差分包地址即可,格式如下:

差分包下載url = 完整包url + "_"+服務(wù)端版本號 + "_" + 客戶端本地版本號,例如:

https://storage.360buyimg.com/hybrid/xxxxx.zip_2_1

客戶端拉到新版本離線包url后,就可以根據(jù)規(guī)范拼接出差分包下載地址。

2.3 自動灰度

通過離線包下載分級、差分包方案在一定程度上減少了離線包下載帶寬流量的減少,為了進(jìn)一步減少離線包下載的流量峰值,我們增加了自動灰度能力,根據(jù)業(yè)務(wù)選擇灰度比例進(jìn)行等差灰度放量。如業(yè)務(wù)選擇1小時完成全量灰度,后臺則自動按每5分鐘放量一次,12次放全量。










03

資源離線加載

3.1 資源匹配邏輯

前面提到離線包壓縮包中會包含離線包對應(yīng)的資源映射配置文件,通過h5頁面的url獲取到離線包后,讀取離線包中映射文件內(nèi)容進(jìn)行資源本地加載匹配。







映射文件內(nèi)容如下:




[ { "filename":"rp_h3aBW.html", "originUrl":"https://h5.m.jd.com/babelDiy/index.html", "type":"html", "header":{ "content-type":"text/html", "content-length":"1370" } }, { "filename":"1HvyKyDC.js", "originUrl":"https://storage.360buyimg.com/1654142210531/vendorJd.fa438901.js", "type":"script", "header":{ "content-type":"application/x-javascript", "content-length":"415690", "access-control-allow-origin":"*", "timing-allow-origin":"*" } }]通過映射文件的originUrl、filename字段就可以將資源請求和本地文件進(jìn)行一一映射,映射對比也需要做一些兼容處理。

為了避免誤傷,這些措施都是通過配置下發(fā)來進(jìn)行精細(xì)化控制的。

3.2 資源實(shí)時性

當(dāng)前京東App的離線包配置是App啟動等特定時機(jī)請求拉取,在拉取配置到用戶實(shí)際進(jìn)入頁面有一定的時間差,在京東這種電商App大促場景下,經(jīng)常會有h5活動切場等情況,對實(shí)時性要求較高,要求用戶打開頁面時請求的頁面資源必須是最新的,我們增加了版本校驗(yàn)接口對離線包中包含html的離線包在進(jìn)入頁面時進(jìn)行離線包更新校驗(yàn)。







這種方案雖然保證了實(shí)時性,但是我們通過監(jiān)控發(fā)現(xiàn),更新的概率非常小,大部分請求流量都是浪費(fèi)的,如果有更新重新加載也會帶來不好的reload體驗(yàn)。于是我們將離線包更新的信息加入到京東網(wǎng)關(guān)接口中,只要App有網(wǎng)關(guān)請求都能獲取到是否有更新,做到了準(zhǔn)實(shí)時。于是流程可以變成:







3.3 兼容重定向

京東App使用的是登錄后臺同步App WebView的登錄態(tài),加載業(yè)務(wù)落地頁時通過加載登錄頁面302重定向到落地頁并同步后臺Cookie。







由于在安卓端谷歌上述攔截方法不支持302的情況,無法攔截302后的鏈接,導(dǎo)致業(yè)務(wù)html無法攔截加載本地文件。這里我們想到的辦法是通過原生網(wǎng)絡(luò)請求登錄打通的鏈接,再通過解析Header中的SetCookie獲取登錄Cookie并將Cookie同步到瀏覽器內(nèi)核,最后直接通過webview加載業(yè)務(wù)鏈接。










public void onSusses(int code, Map<String, List<String>> responseHeaders, String data) if (header != null && (setCookies = header.get("Set-Cookie")) != null && !setCookies.isEmpty()) { saveCookieString(url, setCookies); }}public void saveCookieString(String url, List<String> cookies) { CookieManager cookieManager = CookieManager.getInstance(); if (!cookieManager.acceptCookie()) { return; } for (String cookieSegment : cookies) { if (TextUtils.isEmpty(cookieSegment)) { continue; } cookieManager.setCookie(url, cookieSegment); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { CookieSyncManager.createInstance(HybridSettings.getAppContext()); CookieSyncManager.getInstance().sync(); } else { CookieManager.getInstance().flush(); }}在iOS端同樣存在重定向的問題,我們前面已經(jīng)介紹過了,WKURLSchemeTask并未提供重定向的回調(diào)協(xié)議,所以,當(dāng)攔截到的請求在Native端發(fā)生重定向時,我們會先處理重定向的response里面的cookie信息(如果存在的話),然后直接用當(dāng)前webview去load重定向之后的newRequest,這樣就可以處理重定向的問題。




更多優(yōu)化措施

前面我們主要介紹了通過離線包來解決h5頁面加載過程中實(shí)時拉取資源造成耗時的問題。下面我們再來看看其他影響h5加載性能的一些步驟。讓我們再次回到h5頁面的加載流程。







如上圖所示,通常情況下從用戶點(diǎn)擊到看到頁面內(nèi)容會包括 WebView初始化 、加載HTML、解析HTML、加載、解析JS/CSS資源、數(shù)據(jù)請求、頁面渲染 等流程,我們構(gòu)建的離線加載系統(tǒng)可以節(jié)約多個“下載”過程的耗時。但是這里依然有兩個問題。

雖然我們有離線包系統(tǒng),但是并非所有的業(yè)務(wù)都可以將html做到離線包內(nèi),比如一些SSR的業(yè)務(wù),html往往不是靜態(tài)頁面,這種場景下整個流程幾乎還是串行的(html->js->數(shù)據(jù)請求)。

頁面有意義的渲染一般都是發(fā)生在業(yè)務(wù)數(shù)據(jù)請求之后,上述流程中業(yè)務(wù)數(shù)據(jù)的請求和html、js等串行加載。

針對這兩點(diǎn),我們采用了以下方案來做優(yōu)化:通過html預(yù)加載,解決html串行加載的問題;通過接口預(yù)加載,解決業(yè)務(wù)數(shù)據(jù)串行請求的問題。




01

html預(yù)加載

我們的目標(biāo)是將html下載的時機(jī)盡量提前。一般情況下,我們在html真正加載前不管是應(yīng)用層面還是系統(tǒng)層面或者webkit內(nèi)部都有一些預(yù)處理的邏輯,這些邏輯的耗時和業(yè)務(wù)復(fù)雜度相關(guān),少則幾十毫秒,多則幾百毫秒,提前發(fā)起html請求可以有效的利用這段時間。當(dāng)攔截到html請求時要么直接回調(diào)已下載完成的html,要么等待html返回后再進(jìn)行回調(diào)。無論如何,相比串行請求,這種機(jī)制都有效的利用了html加載前的這段時間。具體的流程見圖










02

接口預(yù)加載

數(shù)據(jù)接口的預(yù)加載的必要性與原理和html預(yù)加載類似,我們希望在頁面初始化之前就開始請求數(shù)據(jù),等攔截到對應(yīng)的請求時直接返回預(yù)加載好的數(shù)據(jù),以節(jié)約頁面有意義的渲染時長。相對于html預(yù)加載,接口預(yù)加載的難點(diǎn)在于接口請求參數(shù)的配置,在最初的版本中,我們支持通過配置來完成請求參數(shù)的下發(fā),可配置的內(nèi)容包括:







在今年618大促期間,秒殺主會場、百億補(bǔ)貼等會場使用了接口預(yù)加載技術(shù),從數(shù)據(jù)看可提升接口加載性能50%以上。但是這里也有一些問題需要注意:

接口數(shù)據(jù)太大,會劣化數(shù)據(jù)預(yù)加載效果

我們的接口預(yù)加載采用了Native請求,然后將結(jié)果通過jsbridge回傳給h5,這個過程涉及到數(shù)據(jù)json化、字符串化、進(jìn)程間通信、js層JSON.parse等一系列操作,這些操作的耗時與數(shù)據(jù)大小成正相關(guān),所以做好數(shù)據(jù)規(guī)模的控制很有必要。如圖:







數(shù)據(jù)在做json字符串化時需要注意特殊字符處理

我們在實(shí)踐中發(fā)現(xiàn),通過js通信回傳信息給h5時,符合兩端統(tǒng)一的做法是均以jsonString進(jìn)行回傳,而采用系統(tǒng)方法直接轉(zhuǎn)化的jsonString存在一些缺陷,特別是數(shù)據(jù)中包括一些特殊字符時會導(dǎo)致jsonString在js層面無法解析,我們在android端就碰到了數(shù)據(jù)中包含單引號時解析異常的問題。目前的解決方案是參考了iOS端的開源庫WebViewJavaScriptBridge的相關(guān)做法,針對系統(tǒng)轉(zhuǎn)化的jsonString進(jìn)一步進(jìn)行特殊字符的處理:




messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"//" withString:@"////"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/"" withString:@"///""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/'" withString:@"///'"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/n" withString:@"//n"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/r" withString:@"//r"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/f" withString:@"//f"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/u2028" withString:@"//u2028"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"/u2029" withString:@"//u2029"];當(dāng)然接口預(yù)加載這個簡單的配置方案在實(shí)踐中存在一些問題,導(dǎo)致目前適用場景比較受限。主要問題表現(xiàn)在:

純配置的系統(tǒng)表達(dá)能力較弱,無法準(zhǔn)確描述請求參數(shù)結(jié)構(gòu),即使一些公共的參數(shù),不同業(yè)務(wù)也有個性化的使用方式(比如一個參數(shù),A業(yè)務(wù)可能在params中,B業(yè)務(wù)在body中),導(dǎo)致我們在不同的請求字段內(nèi)添加了相同的數(shù)據(jù),冗余較嚴(yán)重;

設(shè)備信息、用戶信息等相對穩(wěn)定的字段,不同業(yè)務(wù)會定義個性化的key值,這類參數(shù)要通過配置提供映射;

無法較好的描述接口之間的依賴關(guān)系,多接口預(yù)加載實(shí)現(xiàn)難度大。所以簡單起見,我們前期僅支持了一個接口的預(yù)加載。

對于以上問題,我們也正在通過輕量級的表達(dá)式框架正在改進(jìn)中。




03

內(nèi)存預(yù)熱

除了上述預(yù)加載的優(yōu)化方案之外,我們在資源緩存管理上也進(jìn)行了對應(yīng)的優(yōu)化,因?yàn)殡x線加載的資源存儲在磁盤上,頁面加載時讀取會有一定的io消耗,特別是離線資源較多的情況下,這些消耗累加起來還是比較可觀的。針對這部分消耗,我們采用了內(nèi)存預(yù)熱的方法來進(jìn)行優(yōu)化。

當(dāng)頁面創(chuàng)建時,我們開啟子線程將頁面項(xiàng)目對應(yīng)的離線資源提前讀入內(nèi)存緩存池,攔截到對應(yīng)的請求后,我們先查看內(nèi)存緩存池是否有對應(yīng)的資源,如果沒有才會去讀取磁盤。內(nèi)存緩存池在項(xiàng)目數(shù)量和資源數(shù)量兩個維度均采用LRU的淘汰策略進(jìn)行約束,保證內(nèi)存緩存的上限水位在一個合理的范圍內(nèi)。

這種預(yù)熱策略可以保證大部分的資源在請求時直接從內(nèi)存讀取。而針對訪問量巨大的超級互動業(yè)務(wù),我們也可以選擇首頁渲染完成后就在子線程進(jìn)行預(yù)掃描緩存,這樣可以極大的優(yōu)化大流量業(yè)務(wù)的加載速度。




提效工具

01

調(diào)試工具

在Hybrid開發(fā)的場景下,業(yè)務(wù)面對的最大的困難是加載過程是黑盒的,離線包是否命中,接口預(yù)加載等性能優(yōu)化措施是否生效,業(yè)務(wù)需要一個簡潔明了的工具來查看。結(jié)合業(yè)務(wù)需求與h5研發(fā)的使用習(xí)慣,我們提供了Hybrid下的調(diào)試開發(fā)工具。

我們先來看下調(diào)試工具需要展示的數(shù)據(jù):

離線包是否命中:我們定義的離線包命中口徑為“至少有一個資源從本地獲取成功”。

頁面性能:我們提供了fcp和頁面初始化開始?頁面didfinish時間間隔兩種指標(biāo),以供h5業(yè)務(wù)實(shí)時查看首屏性能與用戶體驗(yàn)感受。

接口預(yù)加載是否生效:h5成功的從客戶端獲取到了預(yù)加載到的接口數(shù)據(jù)之后,接口預(yù)加載生效。

html預(yù)加載是否生效:h5成功的從客戶端獲取到了預(yù)加載到的html數(shù)據(jù)之后,html預(yù)加載生效。

除了以上概括性的信息之外,還有一些細(xì)節(jié)信息也會比較重要 離線包項(xiàng)目信息:如當(dāng)前離線包的配置版本、文件版本 離線包加載信息:當(dāng)前離線包命中的資源列表。

而其他輔助性的信息或測試信息,我們直接歸為log,實(shí)時展示在調(diào)試面板上,業(yè)務(wù)可按需查看。

最終數(shù)據(jù)展示面板如圖所示:







這樣,一個簡單的工具就可以有效的提升業(yè)務(wù)研發(fā)效率,降低團(tuán)隊(duì)對外的答疑與咨詢量。




02

js api自動化測試

越是復(fù)雜的系統(tǒng)越應(yīng)該重視測試的手段與質(zhì)量,自動化的測試能力是最常見的保障系統(tǒng)穩(wěn)定性的手段,Hybrid的功能又會涉及到多端,且功能眾多,日常迭代與修改很容易引發(fā)邊界類的bug,所以,我們將hybrid的各項(xiàng)功能聚合成了一個自動化執(zhí)行的頁面,通過一鍵執(zhí)行,即可覆蓋已知的核心case的測試,如圖:







對于Hybrid的UI類功能,我們正在結(jié)合集團(tuán)的云測平臺,通過腳本執(zhí)行、截圖、圖像識別等能力打通相關(guān)UI類的功能的自動化測試能力。爭取盡早完成Hybrid能力自動化測試全覆蓋。




數(shù)據(jù)監(jiān)控

為了監(jiān)控優(yōu)化成果,并進(jìn)一步幫助h5頁面深入分析性能,我們與燭龍監(jiān)控平臺合作,增加了多維度的性能監(jiān)控。




01

多維度監(jiān)控







監(jiān)控指標(biāo)主要包括性能監(jiān)控、異常監(jiān)控和離線包加載監(jiān)控,每項(xiàng)指標(biāo)都能夠多維度進(jìn)行聚合分析,包括時間、客戶端、版本、系統(tǒng)、廠商、類型、內(nèi)核等維度。










02

離線包信息

離線包信息包括離線包從拉取配置、到下載、再到使用更新整個鏈路的監(jiān)控,能夠?qū)崟r反饋線上用戶使用下載和使用離線包的情況。




03

性能監(jiān)控

性能監(jiān)控通過原生和JS同時采集的方式,更完整的監(jiān)控h5加載過程,原生層面通過WebView回調(diào)等方式進(jìn)行數(shù)據(jù)采集,主要節(jié)點(diǎn)包括:







initStart:WebView實(shí)例開始初始化的時間節(jié)點(diǎn)。

loadUrl:WebView執(zhí)行l(wèi)oad(loadRequest)方法。

pageStart:安卓對應(yīng)WebViewClient.onPageStart方法,iOS對應(yīng)didStartProvisionalNavigation。

pageCommit:安卓對應(yīng)WebViewClient.onPageCommitVisible,iOS對應(yīng)didCommitNavigation。

colorRequestStart:業(yè)務(wù)接口請求開始。

colorRequestEnd:業(yè)務(wù)接口請求結(jié)束。

pageFinish:安卓對應(yīng)WebViewClient.onPageFinish,iOS對應(yīng)didFinishNavigation。

除此以外在頁面加載結(jié)束時,也就是上述的pageFinish節(jié)點(diǎn)通過執(zhí)行js獲取更多性能數(shù)據(jù),主要是通過Performance API獲取PerformanceTiming、FP、FCP、LCP、資源性能等。

PerformanceTiming: w3c引入的api,可獲取h5頁面加載的各個節(jié)點(diǎn)時間。







FP:(First Paint)用于記錄頁面第一次繪制像素的時間。

FCP:(First Contentful Paint)用于記錄頁面首次繪制文本、圖片、非空白 Canvas 或 SVG 的時間。

LCP:(Largest Contentful Paint)用于記錄視窗內(nèi)最大的元素繪制的時間,該時間會隨著頁面渲染變化而變化,因?yàn)轫撁嬷械淖畲笤卦阡秩具^程中可能會發(fā)生改變,該指標(biāo)獲取的是時間區(qū)間內(nèi)的節(jié)點(diǎn),所以需要在頁面開始加載時開始監(jiān)聽,頁面加載結(jié)束后獲取。

以安卓為例,先注冊JS橋:




/** * * @param timing 頁面加載性能 * @param resource 資源網(wǎng)絡(luò)請求性能(包括網(wǎng)絡(luò)接口) * @param paint fp、fcp * @param lcp lcp */@JavascriptInterfacepublic void sendResource(String timing, String resource, String paint, String lcp) { //數(shù)據(jù)上報}頁面開始加載時(上述所提到的pageStart節(jié)點(diǎn)),開始注入LCP監(jiān)聽。




String js = "try{" + "const po = new PerformanceObserver((entryList) => {" + "const entries = entryList.getEntries();" + "const lastEntry = entries[entries.length - 1];" + "window.jdhybrid_performance_lcp = lastEntry.renderTime || lastEntry.loadTime;});" + "po.observe({type: 'largest-contentful-paint', buffered: true});" + "}catch (e) {}";webView.evaluateJavascript(js,null);頁面加載結(jié)束時(上述所提到的pageFinish節(jié)點(diǎn)),獲取所有js性能數(shù)據(jù)。




String js = "try{" + "window.hybridPerformance.sendResource(" + "JSON.stringify(window.performance.timing)," + "JSON.stringify(window.performance.getEntriesByType('resource'))," + "JSON.stringify(window.performance.getEntriesByType('paint'))," + "window.jdhybrid_performance_lcp ? window.jdhybrid_performance_lcp.toString():'');" + "}catch (e) {}";webView.evaluateJavascript(js,null);04

異常監(jiān)控

異常監(jiān)控主要包括JS橋執(zhí)行異常、頁面加載錯誤、頁面響應(yīng)錯誤、資源請求失敗、JS Exception,webview白屏等,這些指標(biāo)即可以用來日常排查問題,也可以作為度量Hybrid離線包接入之后的業(yè)務(wù)價值,關(guān)于h5頁面加載異常的處理策略后續(xù)我們再專門介紹。




開源計劃

我們計劃在下半年對上述寫到的主要能力在github進(jìn)行開源,也誠邀感興趣的大佬們到時候一起進(jìn)來完善、交流。




寫在最后

感謝互動、大促、頻道的開發(fā)團(tuán)隊(duì)對京東h5頁面性能優(yōu)化作出的貢獻(xiàn),現(xiàn)在通過各種優(yōu)化手段已經(jīng)將整體性能有了一定程度的提升,我們也有更多的優(yōu)化方案在輸出中,包括NSR、預(yù)渲染等。歡迎感興趣的小伙伴、有更多更好的優(yōu)化方案的小伙伴可以留言一起討論。




參考文獻(xiàn)

WebKit官方文檔:

https://developer.apple.com/documentation/webkit

MDN:

https://developer.mozilla.org/zh-CN/

WKWebView請求攔截探索與實(shí)踐:

https://juejin.cn/post/6922625242796032007

深入理解 WKWebView(基礎(chǔ)篇)—— 聊聊 cookie 管理那些事:

https://mp.weixin.qq.com/s/jZP2DsAa5OV91wdNMw39cA

WKURLSchemeHandler的能與不能:

https://www.jianshu.com/p/6bae04c91297

Webkit源碼:

https://github.com/WebKit/webkit

Ajax-hook:

https://github.com/wendux/Ajax-hook

基于 LocalWebServer 實(shí)現(xiàn) WKWebView 離線資源加載:

https://www.jianshu.com/p/a69e77bf680c







— THE END —




【免責(zé)聲明】圖文來自網(wǎng)絡(luò),版權(quán)歸原作者所有。如侵權(quán)請聯(lián)系刪除;我們對文中觀點(diǎn)保持中立,僅供參考、交流之目的。

推薦閱讀

微信8.0將好友放開到了一萬,寶寶們可以加我大號了,先到先得。




掃描下方二維碼即可加我微信啦,2022,抱團(tuán)取暖,一起牛逼。







產(chǎn)品+技術(shù)統(tǒng)稱為大技術(shù)。分享優(yōu)秀產(chǎn)品,傳播產(chǎn)品思維;專注技術(shù)分享,包含JS、CSS、 HTML5、Vue、React、Augula、View UI(iView)、Element UI、Flutter、Electron和JAVA、JVM、SpringBoot、Dubbo、Spring Cloud/Alibaba、Docker、Docker Compose、K8S等實(shí)用技術(shù)與框架。







請我

分享、

贊、在看

本文來自公眾號:大技術(shù)

關(guān)鍵詞:實(shí)踐,提升,性能

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉