為了更好的閱讀體驗(yàn),建議閱讀原文
插播廣告 —— 幾十行代碼完成資訊類App多種形式內(nèi)容頁
HybridPageKit :一個(gè)針對(duì)資訊類App高性能、易擴(kuò)展、組件化的通用內(nèi)容頁實(shí)現(xiàn)框架。想和我一起全面了解新聞?lì)怉pp的開發(fā),點(diǎn)我學(xué)習(xí)
移動(dòng)開發(fā)領(lǐng)域近年來已經(jīng)逐漸告別了野蠻生長的時(shí)期,進(jìn)入了相對(duì)成熟的時(shí)代。而一直以來Native和Web的爭(zhēng)論從未停止,通過開發(fā)者孜孜不倦的努力,Web的效率和Native的體驗(yàn)也一直在尋求著平衡。本文聚焦iOS開發(fā)和Web開發(fā)的交叉點(diǎn),希望能通過簡(jiǎn)要的介紹,幫助開發(fā)者一窺Hybrid和大前端的構(gòu)想。
目錄

iOS中Web容器與加載
1. iOS中的Web容器
目前iOS系統(tǒng)為開發(fā)者提供三種方式來展示W(wǎng)eb內(nèi)容:
-
UIWebView
UIWebView從iOS2 開始就作為App內(nèi)展示W(wǎng)eb內(nèi)容的容器,但是長久以來一直遭受開發(fā)者的詬??;系統(tǒng)級(jí)的內(nèi)存泄露、極高內(nèi)存峰值、較差的穩(wěn)定性、Touch Delay以及Javascript的運(yùn)行性能及通信限制等等。在iOS12以后已經(jīng)標(biāo)記為Deprecated不再維護(hù)。 -
WKWebView
在iOS8中,Apple引入了新一代的WebKit framework,同時(shí)提供了
WKWebView用來替代傳統(tǒng)的UIWebView。它更加的穩(wěn)定、擁有60fps滾動(dòng)刷新率、豐富的手勢(shì)、KVO、高效的Web和Native通信,默認(rèn)進(jìn)度條等等功能,而最重要的,是使用了和safari相同的Nitro引擎極大提升了Javascript的運(yùn)行速度。WKWebView獨(dú)立的進(jìn)程管理,也降低了內(nèi)存占用及Crash對(duì)主App的影響。
-
SFSafariViewController
在iOS9中,Apple引入了
SFSafariViewController。其特點(diǎn)就是在App內(nèi)可以打開一個(gè)高度標(biāo)準(zhǔn)化的、和Safari一樣界面和特性的頁面。同時(shí)SFSafariViewController支持和Safari共享Cookie和表單數(shù)據(jù)等等。 -
Web容器選型
對(duì)于
SFSafariViewController:由于其標(biāo)準(zhǔn)化程度之高,使之界面和交互邏輯無法和App統(tǒng)一,基于App的整體體驗(yàn)的考慮,一般都使用在相對(duì)獨(dú)立的功能和模塊中,最常見的就是在App內(nèi)打開App Store或者廣告、游戲推廣的頁面。對(duì)于
UIWebView/WKWebView:如果說之前由于NSURLProtocol的問題,好多App都在繼續(xù)使用UIWebView,那么隨著App放棄維護(hù)UIWebView(iOS12),全部的App應(yīng)該會(huì)陸續(xù)的切換到WKWebView中來。當(dāng)然,最初WKWebView也為開發(fā)者們帶來了一些難題,但是隨著系統(tǒng)的升級(jí)與業(yè)務(wù)邏輯的適配也逐步的修復(fù),后文會(huì)列舉幾個(gè)最為關(guān)注的技術(shù)點(diǎn)。UIWebView/WKWebView對(duì)主App內(nèi)存的影響:
image
image
2. WebKit框架與使用
-
WebKit.framework
WebKit 是一個(gè)開源的Web瀏覽器引擎。每當(dāng)談到WebKit,開發(fā)者常常迷惑于它和WebKit2、Safari、iOS中的framework、以及Chromium等瀏覽器的關(guān)系。
廣義的
WebKit其實(shí)就是指WebCore,它主要包含了HTML和CSS的解析、布局和定位這類渲染HTML的功能邏輯。而狹義的WebKit就是在WebCore的基礎(chǔ)上,不同平臺(tái)封裝Javascript引擎、網(wǎng)絡(luò)層、GPU相關(guān)的技術(shù)(WebGL、視頻)、繪制渲染技術(shù)以及各個(gè)平臺(tái)對(duì)應(yīng)的接口,形成我們可以用的WebView或?yàn)g覽器,也就是所謂的WebKit Ports。image比如在
Safari中 JS的引擎使用JavascriptCore,而Chromium中使用V8;渲染方面Safari使用CoreGraphics,而Chromium中使用Skia;網(wǎng)絡(luò)方面Safari使用CFNetwork,而Chromium中使用Chromium stack等等。而WebKit2是相對(duì)于狹義上的WebKit架構(gòu)而言,主要變化是在API層支持多進(jìn)程,分離了UI和Web接口的進(jìn)程,使之通過IPC來進(jìn)行通訊。對(duì)于iOS中的WebKit.framework就是在WebCore、底層橋接、JSCore引擎等核心模塊的基礎(chǔ)上,針對(duì)iOS平臺(tái)的項(xiàng)目封裝。它基于新的
WKWebView,提供了一系列瀏覽特性的設(shè)置,以及簡(jiǎn)單方便的加載回調(diào)。而具體類及使用,開發(fā)者可以查閱官方文檔。

-
Web容器使用流程與關(guān)鍵節(jié)點(diǎn)
對(duì)于大部分日常使用來說,開發(fā)者需要關(guān)注的就是
WKWebView的創(chuàng)建、配置、加載、以及系統(tǒng)回調(diào)的接收。image對(duì)于Web開發(fā)者,業(yè)務(wù)邏輯一般通過基于Web頁面中Dom渲染的關(guān)鍵節(jié)點(diǎn)來處理。而對(duì)于iOS開發(fā)者,
WKWebView提供的的注冊(cè)、加載和回調(diào)時(shí)機(jī),沒有明確的與Web加載的關(guān)鍵節(jié)點(diǎn)相關(guān)聯(lián)。準(zhǔn)確的理解和處理兩個(gè)維度的加載順序,選擇合理的業(yè)務(wù)邏輯處理時(shí)機(jī),才可以實(shí)現(xiàn)準(zhǔn)確而高效的應(yīng)用。image -
WKWebView常見問題
使用
WKWebView帶來的另外一個(gè)好處,就是我們可以通過源碼理解部分加載邏輯,為Crash提供一些思路,或者使用一些私有方法處理復(fù)雜業(yè)務(wù)邏輯。-
NSURLProtocol
WKWebView最為顯著的改變,就是不支持NSURLProtocol。為了兼容舊的業(yè)務(wù)邏輯,一部分App通過WKBrowsingContextController中的非公開方法實(shí)現(xiàn)了NSURLProtocol。// WKBrowsingContextController + (void)registerSchemeForCustomProtocol:(NSString *)scheme WK_API_DEPRECATED_WITH_REPLACEMENT("WKURLSchemeHandler", macos(10.10, WK_MAC_TBA), ios(8.0, WK_IOS_TBA));在iOS11中,系統(tǒng)增加了
setURLSchemeHandler函數(shù)用來攔截自定義的Scheme。但是不同于UIWebView,新的函數(shù)只能攔截自定義的Scheme(SchemeRegistry.cpp),對(duì)使用最多的HTTP/HTTPS依然不能有效的攔截。//SchemeRegistry static const StringVectorFunction functions[] { builtinSecureSchemes, // about;data... builtinSchemesWithUniqueOrigins, // javascript... builtinEmptyDocumentSchemes, builtinCanDisplayOnlyIfCanRequestSchemes, builtinCORSEnabledSchemes, //http;https };
-
- 白屏
通常WKWebView白屏的原因主要分兩種,一種是由于Web的進(jìn)程Crash(多見于內(nèi)部進(jìn)程通信);一種就是WebView渲染時(shí)的錯(cuò)誤(Debug一切正常只是沒有對(duì)應(yīng)的內(nèi)容)。對(duì)于白屏的檢測(cè),前者在iOS9之后系統(tǒng)提供了對(duì)應(yīng)Crash的回調(diào)函數(shù),同時(shí)業(yè)界也有通過判斷URL/Title是否為空的方式作為輔助;后者業(yè)界通過視圖樹對(duì)比,判斷SubView是否包含WKCompsitingView,以及通過隨機(jī)點(diǎn)截圖等方式作為白屏判斷的依據(jù)。
其他WKWebView的系統(tǒng)級(jí)問題如Cookie、POST參數(shù)、異步Javascript等等一系列的問題,可以通過業(yè)務(wù)邏輯的調(diào)整重新適配
-
由于WebKit源碼等開放性,我們也可以利用私有方法來簡(jiǎn)化代碼邏輯、實(shí)現(xiàn)復(fù)雜的產(chǎn)品需求。例如在WKWebViewPrivate中可以獲得各種頁面信息、直接取到UserAgent、 在WKBackForwardListPrivate中可以清理掉全部的跳轉(zhuǎn)歷史、以及在WKContentViewInteraction中替換方法實(shí)現(xiàn)自定義的MenuItem等等。
```objc @interface WKWebView (WKPrivate) @property (nonatomic, readonly) NSString *_userAgent WK_API_AVAILABLE(macosx(10.11), ios(9.0)); ... @interface WKBackForwardList (WKPrivate) - (void)_removeAllItems; ... @interface WKContentView (WKInteraction) - (BOOL)canPerformActionForWebView:(SEL)action withSender:(id)sender; ```
3. App中的應(yīng)用場(chǎng)景
WKWebView系統(tǒng)提供了四個(gè)用于加載渲染W(wǎng)eb的函數(shù)。這四個(gè)函數(shù)從加載的類型上可以分為兩類:加載URL & 加載HTML\Data。所以基于此也延伸出兩種不同的業(yè)務(wù)場(chǎng)景:加載URL的頁面直出類和加載數(shù)據(jù)的模板渲染類,同時(shí)兩種類型各自也有不同的優(yōu)化重點(diǎn)及方向。
-
頁面直出類
//根據(jù)URL直接展示W(wǎng)eb頁面 - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;通常各類App中的Web頁面加載都是通過加載URL的方式,比如嵌入的運(yùn)營活動(dòng)頁面、廣告頁面等等。
-
模板渲染類
//根據(jù)模板&數(shù)據(jù)渲染W(wǎng)eb頁面 - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL; ...需要使用WebView展示,且交互邏輯較多的頁面,最常見的就是資訊類App的內(nèi)容展示頁。
iOS中Web與Native的通信
單純的使用Web容器加載頁面已經(jīng)不能滿足復(fù)雜的功能,開發(fā)者希望數(shù)據(jù)可以在Native和Web之間通信傳遞來實(shí)現(xiàn)復(fù)雜的功能,而Javascript就是通信的媒介。對(duì)于有WebView的情況,雖然WKWebView提供了系統(tǒng)級(jí)的方法,但是大部分App仍然使用基于URLScheme的WebViewBridge用以兼容UIWebView。而脫離了WebView容器,系統(tǒng)提供了JavaScriptCore的framework,它也為之后蓬勃發(fā)展的跨平臺(tái)和熱修復(fù)技術(shù)提供了可能。
1. 基于WebView的通信
基于WebView的通信主要有 兩個(gè) 途徑,一個(gè)是通過系統(tǒng)或私有方法,獲取WebView當(dāng)中的 JSContext,使用系統(tǒng)封裝的基于JSCore的函數(shù)通信。另一類就是通過創(chuàng)建自定義Scheme的iframe Dom,客戶端在回調(diào)中進(jìn)行攔截實(shí)現(xiàn)。
-
UIWebView & WKWebView系統(tǒng)級(jí)
在
UIWebView時(shí)代沒有提供系統(tǒng)級(jí)的函數(shù)進(jìn)行Web與Native的交互,絕大部分App都是通過WebViewJavascriptBridge(下節(jié)介紹)來進(jìn)行的通信。但是由于JavascriptCore的存在,對(duì)于UIWebView來說只要有效的獲取到內(nèi)部的JSContext,也可以達(dá)到目的。目前已知有效的幾個(gè)私有方法獲取Context的方法如下://通過系統(tǒng)廢棄函數(shù)獲取context - (void)webView:(WebView *)webView didCreateJavaScriptContext:(JSContext *)context forFrame:(WebFrame *)frame; //通過valueForKeyPath獲取context self.jsContext = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];在
WKWebView中提供了系統(tǒng)級(jí)的Web和Native通訊機(jī)制,通過Message Handler的封裝使開發(fā)效率有了很大的提升。同時(shí)系統(tǒng)封裝了JavaScript對(duì)象和Objective-C對(duì)象的轉(zhuǎn)換邏輯,也進(jìn)降低了使用的門檻。// js端發(fā)送消息 window.webkit.messageHandlers.{NAME}.postMessage() //Native在回調(diào)中接收 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
-
攔截自定義Scheme請(qǐng)求 - WebViewJavascriptBridge
由于私有方法的穩(wěn)定性與審核風(fēng)險(xiǎn),開發(fā)者不愿意使用上文提到的
UIWebView獲取 JSContext的方式進(jìn)行通信,所以通常都采用基于iframe和自定義Scheme的 JavascriptBridge進(jìn)行通信。雖然在之后的WKWebView提供了系統(tǒng)函數(shù),但是大部分App都需要兼容UIWebView與WKWebView,所以目前的使用范圍仍然十分廣泛。在Github中類似的開源框架有很多,但是無外乎都是Web側(cè)根據(jù)固定的格式創(chuàng)建包含通信信息的Request,之后創(chuàng)建隱式iFrame節(jié)點(diǎn)請(qǐng)求;Native側(cè)在相應(yīng)的WebView回調(diào)中解析Request的Scheme,之后按照格式解析數(shù)據(jù)并處理。
而對(duì)于數(shù)據(jù)傳遞和回調(diào)處理的問題,在兼容兩種WebView、持續(xù)的更新的WebViewJavascriptBridge中,iFrame request沒有直接傳遞數(shù)據(jù),而是Web和Native側(cè)維護(hù)共同的參數(shù)或回調(diào)Queue,Native通過Request中Scheme的解析觸發(fā)對(duì)Queue里數(shù)據(jù)的讀取。
image
2. 脫離WebView的通信 JavaScriptCore
-
JavascriptCore
JavascriptCore一直作為WebKit中內(nèi)置的JS引擎使用,在iOS7之后,Apple對(duì)原有的C/C++代碼進(jìn)行了OC的封裝,成系統(tǒng)級(jí)的framework供開發(fā)者使用。作為一個(gè)引擎來講,JavascriptCore的詞法、語法分析,以及多層次的JIT編譯技術(shù)都是值得深入挖掘和學(xué)習(xí)的方向,由于篇幅的限制暫且不做深入的討論。

-
JavascriptCore.framework
雖然JavascriptCore.framework只暴露了較少的頭文件和系統(tǒng)函數(shù),但卻提供了在App中脫離WebView執(zhí)行Javascript的環(huán)境和能力。
-
JSVirtualMachine:提供了JS執(zhí)行的底層資源及內(nèi)存。雖然Java與Javascript沒有一點(diǎn)關(guān)系,但是同樣作為虛擬機(jī),JSVM和JVM做了一部分類似的事情。每個(gè)JSVirtualMachine獨(dú)占線程,擁有獨(dú)立的空間和管理,但是可以包含多個(gè)JSContext。 -
JSContext:提供了JS運(yùn)行的上下文環(huán)境和接口??梢圆粶?zhǔn)確的理解為,就是創(chuàng)建了一個(gè)Javascript中的Window對(duì)象。 -
JSValue:提供了OC和JS間數(shù)據(jù)類型的封裝和轉(zhuǎn)換Type Conversions。除了基本的數(shù)據(jù)類型,需要注意OC中的Block轉(zhuǎn)換為JS中的function,Class轉(zhuǎn)換為Constructor等等。 -
JSManagedValue:Javascript使用GC機(jī)制管理內(nèi)存,而OC采用引用計(jì)數(shù)的方式管理內(nèi)存。所以在JavascriptCore使用過程中,難免會(huì)遇到循環(huán)引用以及提前釋放的問題。JSManagedValue解決了在兩種環(huán)境中的內(nèi)存管理問題。 -
JSExport:提供了類、屬性和實(shí)例方法的調(diào)用接口。內(nèi)部實(shí)現(xiàn)是在ProtoType & Constructor中實(shí)現(xiàn)對(duì)應(yīng)的屬性和方法。
-

-
使用JavascriptCore進(jìn)行通信
-
Native - Web: 通過JavascriptCore,Native可以直接在Context中執(zhí)行JS語句,和Web側(cè)進(jìn)行通信和交互。
JSValue *value = [self.jsContext evaluateScript:@"document.cookie"]; -
Web - Native: 對(duì)于Web側(cè)向Native的通信,JavascriptCore提供 兩種 方式,注冊(cè)Block & Export協(xié)議。
//Native self.jsContext[@"addMethod"] = ^ NSInteger(NSInteger a, NSInteger b) { return a + b; }; //JS console.log(addMethod(1, 2)); //3 //Native @protocol testJSExportProtocol <JSExport> @property (readonly) NSString *string; ... @interface OCClass : NSObject <testJSExportProtocol> //JS var OCClass = new OCClass(); console.log(OCClass.string);
對(duì)于JavascriptCore粗淺的理解,可以認(rèn)為使用Block方法,內(nèi)部是將Block保存到保存到一個(gè)Web環(huán)境中的全局的Object中,例如window。而使用JSExport方法,則是在Web環(huán)境中Object的
prototype中創(chuàng)建屬性、實(shí)例方法;在constructor對(duì)象中創(chuàng)建類方法,從而實(shí)現(xiàn)Web中的調(diào)用。 -
3. App中的應(yīng)用場(chǎng)景
- 對(duì)于基于WebView的通信,主要用于App向H5頁面中注入的Javascript Open Api,如提供Native的拍照、音視頻、定位;以及App內(nèi)的登錄、分享等等功能。
- 對(duì)于JavaScriptCore,則催生了動(dòng)態(tài)化、跨平臺(tái)以及熱修復(fù)等一系列技術(shù)的蓬勃發(fā)展。
跨平臺(tái)與熱修復(fù)
近幾年來國內(nèi)外移動(dòng)端各種方案如雨后春筍般涌現(xiàn),“Write once, run anywhere”不再是開發(fā)者的向往。剝離跨平臺(tái)技術(shù)在Web側(cè)DSL、virtualDom等方面的優(yōu)化,以及Native側(cè)Runtime的應(yīng)用與封裝,對(duì)于兩端通信的核心,依然是JavascriptCore。

而不同于國外開發(fā)者對(duì)跨平臺(tái)技術(shù)的積極探索,國內(nèi)開發(fā)者也對(duì)熱修復(fù)技術(shù)產(chǎn)生了極大的熱情。同樣作為Native和Web的交叉 - JavascriptCore,依然承擔(dān)著整個(gè)技術(shù)結(jié)構(gòu)中的通信任務(wù)。
1. 基于Web的熱修復(fù)技術(shù)
對(duì)于國內(nèi)的iOS開發(fā)者來說,審核周期、敏感業(yè)務(wù)、支付分成以及bug修復(fù)都催生了熱修復(fù)方向的不斷探索。在蘋果加強(qiáng)審核之前,幾乎所有大型的App都把熱修復(fù)當(dāng)成了iOS開發(fā)的基礎(chǔ)能力,最近在《移動(dòng)開發(fā)還有救么》中也詳細(xì)的介紹了相關(guān)黑科技的前世今生。在所有iOS熱修復(fù)的方案中,基于Javascript、同時(shí)也是影響最大的就是JSPatch。
-
基于上文的分析,對(duì)于脫離WebView的Native和Web間的通信,我們只能使用JavascriptCore。而在JavascriptCore中提供了兩種方式用于通信,即Context注冊(cè)Block的回調(diào),以及JSExport。對(duì)于熱修復(fù)的場(chǎng)景來說,我們不可能把潛在需要修復(fù)的函數(shù)都一一使用協(xié)議進(jìn)行注冊(cè),更不能對(duì)新增方法和刪除方法等進(jìn)行處理,所以在Native和Web通信這個(gè)維度,我們只能采用Context注冊(cè)Block的方式。
// 注冊(cè)回調(diào) context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) { return callSelector(nil, selectorName, arguments, obj, isSuper); }; context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) { return callSelector(className, selectorName, arguments, nil, NO); }; -
確定了通信采用Block回調(diào)的方式后,熱修復(fù)就面臨著如何在JS中調(diào)用類以及類的方法問題。由于沒有使用JSExport等方式,JS是無法找到相應(yīng)類等屬性和方法,在JSPathc中,通過簡(jiǎn)單的字符串替換,將所有方法都替換成通用函數(shù)(__c),然后就可以將相關(guān)信息傳遞給Native,進(jìn)而使用runtime接口調(diào)用方法。
// 替換全部方法調(diào)用 static NSString *_replaceStr = @".__c(\"$1\")("; // 調(diào)用方法 __c: function(methodName) { ... return function(){ ... var ret = instance ? _OC_callI(instance, selectorName, args, isSuper): _OC_callC(clsName, selectorName, args) return _formatOCToJS(ret) } 當(dāng)然對(duì)于JSPatch以及其他熱修復(fù)的項(xiàng)目來說,Web和Native通信只是整個(gè)框架中的一個(gè)技術(shù)點(diǎn),更多的實(shí)現(xiàn)原理和細(xì)節(jié)由于篇幅的關(guān)系暫且不做介紹。
2. 基于Web的跨平臺(tái)技術(shù)
隨著Google開源了基于Dart語言的Flutter,跨平臺(tái)的技術(shù)又進(jìn)入了一個(gè)新的發(fā)展階段。對(duì)于傳統(tǒng)的跨平臺(tái)技術(shù)來講,各個(gè)公司以JavascriptCore作為通信橋梁,圍繞著DSL的解析、方法表的注冊(cè)、模塊注冊(cè)通信、參數(shù)傳遞的設(shè)計(jì)以及OC Runtime的運(yùn)用等不同方向,封裝成了一個(gè)又一個(gè)跨平臺(tái)的項(xiàng)目。

而在其中,以Javascript作為前端DSL的跨平臺(tái)技術(shù)方案里,F(xiàn)aceBook的react-native以及阿里(目前托管給了Apache)的Weex最為流行。在網(wǎng)絡(luò)上兩者的比較文章有很多,集中在學(xué)習(xí)成本、框架生態(tài)、代碼侵入、性能以及包大小等,各個(gè)業(yè)務(wù)可以根據(jù)自己的重點(diǎn)選擇合理的技術(shù)結(jié)構(gòu)。
而不管是react-native還是Weex,Web和Native的通信橋梁仍然是JavascriptCore。
//weex 舉例
JSValue* (^callNativeBlock)(JSValue *, JSValue *, JSValue *) = ^JSValue*(JSValue *instance, JSValue *tasks, JSValue *callback){
...
return [JSValue valueWithInt32:(int32_t)callNative(instanceId, tasksArray, callbackId) inContext:[JSContext currentContext]];
};
_jsContext[@"callNative"] = callNativeBlock;
和熱修復(fù)技術(shù)一樣,跨平臺(tái)又是一個(gè)龐大的技術(shù)體系,JavascriptCore僅僅是作為整個(gè)體系運(yùn)轉(zhuǎn)中的一個(gè)小小的部分,而整個(gè)跨平臺(tái)的技術(shù)方案就需要另開多個(gè)篇幅進(jìn)行介紹了。
iOS中Web相關(guān)優(yōu)化策略
隨著Web技術(shù)的不斷升級(jí)以及App動(dòng)態(tài)性業(yè)務(wù)需求的增多,越來越多的Web頁面加入到了iOS App當(dāng)中。與之對(duì)應(yīng)的,首屏展示速度——這個(gè)對(duì)于移動(dòng)客戶端Web的最重要體驗(yàn)優(yōu)化,也成為了移動(dòng)客戶端中Web業(yè)務(wù)最重要的優(yōu)化方向。
這一章節(jié)更為詳細(xì)的設(shè)計(jì)與實(shí)現(xiàn),請(qǐng)移步iOS新聞?lì)怉pp內(nèi)容頁技術(shù)探索;
1. 不同業(yè)務(wù)場(chǎng)景的優(yōu)化策略
對(duì)于單純的Web頁面來說,業(yè)界早已有了合理的優(yōu)化方向以及成熟的優(yōu)化方案,而對(duì)于移動(dòng)客戶端中的Web來說,開發(fā)者在進(jìn)行單一的Web優(yōu)化同時(shí),還可以通過優(yōu)化Web容器以及Web頁面中數(shù)據(jù)加載方式等多個(gè)途徑做出優(yōu)化。
所以對(duì)于iOS開發(fā)中的優(yōu)化來說,就是通過Native和Web兩個(gè)維度的優(yōu)化關(guān)鍵渲染路徑,保證WebView優(yōu)先渲染完畢。由此我們梳理了常規(guī)Web頁面整體的加載順序,從中找出關(guān)鍵渲染路徑,繼而逐個(gè)分析、優(yōu)化。

2. Web維度的優(yōu)化
-
通用Web優(yōu)化
對(duì)于Web的通用優(yōu)化方案,一般來說在網(wǎng)絡(luò)層面,可以通過DNS和CDN技術(shù)減少網(wǎng)絡(luò)延遲、通過各種HTTP緩存技術(shù)減少網(wǎng)絡(luò)請(qǐng)求次數(shù)、通過資源壓縮和合并減少請(qǐng)求內(nèi)容等。在渲染層面可以通過精簡(jiǎn)和優(yōu)化業(yè)務(wù)代碼、按需加載、防止阻塞、調(diào)整加載順序優(yōu)化等等。對(duì)于這個(gè)老生常談的問題,業(yè)內(nèi)已經(jīng)有十分成熟和完整的總結(jié),例如《Best Practices for Speeding Up Your Web Site》,已經(jīng)有了很好的整理和總結(jié)。
-
其他
脫離較為通用的優(yōu)化,在對(duì)代碼侵入寬容度較高的場(chǎng)景中,開發(fā)者對(duì)Web優(yōu)化有著更為激進(jìn)的做法。例如在VasSonic中,除了Web容器復(fù)用、數(shù)據(jù)模板分離、預(yù)拉取和通用的優(yōu)化方式外,還通過自定義VasSonic標(biāo)簽將HTML頁面進(jìn)行劃分,分段進(jìn)行緩存控制,以達(dá)到更高的優(yōu)化效果。
3. Native維度的優(yōu)化
-
容器復(fù)用和預(yù)熱
WKWebView雖然JIT大幅優(yōu)化了JS的執(zhí)行速度,但是單純的加載渲染HTML,WKWebView比UIWebView慢了很多。根據(jù)渲染的不同階段分別對(duì)耗時(shí)進(jìn)行測(cè)試,同時(shí)對(duì)比UIWebView,我們發(fā)現(xiàn)WKWebView在初始化及渲染開始前的耗時(shí)較多。

針對(duì)這種情況,業(yè)界主流的做法就是復(fù)用 & 預(yù)熱。預(yù)熱即時(shí)在App啟動(dòng)時(shí)創(chuàng)建一個(gè)WKWebView,使其內(nèi)部部分邏輯預(yù)熱已提升加載速度。而復(fù)用又分為兩種,較為復(fù)雜的是處理邊界條件已達(dá)到真正的復(fù)用,還有一種較為Triky的辦法就是常駐一個(gè)空WKWebView在內(nèi)存。
HybridPageKit提供了易于集成的完整WKWebView重用機(jī)制實(shí)現(xiàn),開發(fā)者可以無需關(guān)注復(fù)用細(xì)節(jié),無縫的體驗(yàn)更為高效的WKWebView。
-
Native并行資源請(qǐng)求 & 離線包
由于Web頁面內(nèi)請(qǐng)求流程不可控以及網(wǎng)絡(luò)環(huán)境的影響,對(duì)于Web的加載來說,網(wǎng)絡(luò)請(qǐng)求一直是優(yōu)化的重點(diǎn)。開發(fā)者較為常用的做法是使用Native并行代理數(shù)據(jù)請(qǐng)求,替代Web內(nèi)核的資源加載。在客戶端初始化頁面的同時(shí),并行開始網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù);當(dāng)Web頁面渲染時(shí)向Native獲取其代理請(qǐng)求的數(shù)據(jù)。
而將并行加載和預(yù)加載做到極致的優(yōu)化,就是離線包的使用。將常用的需要下載資源(HTML模板、JS文件、CSS文件、占位圖片)打包,App選擇合適的時(shí)機(jī)全部下載到本地,當(dāng)Web頁面渲染時(shí)向Native獲取其數(shù)據(jù)。
通過離線包的使用,Web頁面可以并行(提前)加載頁面資源,同時(shí)擺脫了網(wǎng)絡(luò)的影響,提高了頁面的加載速度和成功率。當(dāng)然離線包作為資源動(dòng)態(tài)更新的一個(gè)方式,合理的下載時(shí)機(jī)、增量更新、加密和校驗(yàn)等方面都是需要進(jìn)行設(shè)計(jì)和思考的方向,后文會(huì)簡(jiǎn)單介紹。
-
復(fù)雜Dom節(jié)點(diǎn)Native化實(shí)現(xiàn)
當(dāng)并行請(qǐng)求資源,客戶端代理數(shù)據(jù)請(qǐng)求的技術(shù)方案逐漸成熟時(shí),由于WKWebView的限制,開發(fā)者不得不面對(duì)業(yè)務(wù)調(diào)整和適配。其中保留原有代理邏輯、采用LocalServer的方式最為普遍。但是由于WKWebView的進(jìn)程間通信、LocalServer Socket建立與連接、資源的重復(fù)編解碼都影響了代理請(qǐng)求的效率。

所以對(duì)于一些資訊類App,通常采用Dom節(jié)點(diǎn)占位、Native渲染實(shí)現(xiàn)的方式進(jìn)行優(yōu)化,如圖片、地圖、音視頻等模塊。這樣不但能減少通信和請(qǐng)求的建立、提供更加友好的交互、也能并行的進(jìn)行View的渲染和處理,同時(shí)減少Web頁面的業(yè)務(wù)邏輯。
HybridPageKit中就提供封裝好的功能框架,開發(fā)者可以簡(jiǎn)單的替換Dom節(jié)點(diǎn)為NativeView。
-
按優(yōu)先級(jí)劃分業(yè)務(wù)邏輯
從App的維度上看,一個(gè)Web頁面從入口點(diǎn)擊到渲染完成,或多或少都會(huì)有Native的業(yè)務(wù)邏輯并行執(zhí)行。所以這個(gè)角度的優(yōu)化關(guān)鍵渲染路徑,就是優(yōu)先保證WebView以及其他在首屏直接展示的Native模塊優(yōu)先渲染。所以承載Web頁面的Native容器,可以根據(jù)業(yè)務(wù)邏輯的優(yōu)先級(jí),在保證WebView模塊展示之后,選擇合適的時(shí)機(jī)進(jìn)行數(shù)據(jù)加載、視圖渲染等。這樣就能保證在Native的維度上,關(guān)鍵路徑優(yōu)先渲染。

4. 優(yōu)化整體流程
所以整體上對(duì)于客戶端來說,我們可以從Native維度(容器和數(shù)據(jù)加載)以及Web維度兩個(gè)方向提升加載速度,按照頁面的加載流程,整體的優(yōu)化方向如下:

<center>- iOS中Web相關(guān)延伸業(yè)務(wù) -</center>
1. 模板引擎
為了達(dá)到并行加載數(shù)據(jù)以及并行處理復(fù)雜的展示邏輯,對(duì)于非直出類型的Web頁面,絕大部分App都采用數(shù)據(jù)和模板分離下發(fā)的方式。而這樣的技術(shù)架構(gòu),導(dǎo)致在客戶端內(nèi)需要增加替換對(duì)應(yīng)DSL的模板標(biāo)簽,形成最終的HTML的業(yè)務(wù)邏輯。簡(jiǎn)單的字符串替換邏輯不但低效,還無法做到合理的組件化管理,以及組件合理的與Native交互,而模板引擎相關(guān)技術(shù)會(huì)使這種邏輯和表現(xiàn)分離的業(yè)務(wù)場(chǎng)景實(shí)現(xiàn)的更加簡(jiǎn)潔和優(yōu)雅。
基于模板引擎與數(shù)據(jù)分離,客戶端可以根據(jù)數(shù)據(jù)并行創(chuàng)建子業(yè)務(wù)模塊,同時(shí)在子業(yè)務(wù)模塊中處理和Native交互的部分如圖片裁剪適配、點(diǎn)擊跳轉(zhuǎn)等等,生成HTML代碼片段。之后基于模板進(jìn)行替換生成完整的頁面。這樣不但減少了大量的字符串替換邏輯,同時(shí)業(yè)務(wù)也得到了合理拆分。
[圖片上傳失敗...(image-a6c40b-1553579995776)]
- 模板引擎的本質(zhì)就是字符串的解析和替換拼接。在Web端不同的使用場(chǎng)景有很多不同語法的引擎類型,而在客戶端較為流行的,有使用較為復(fù)雜的MGTemplateEngine,它類似于Smarty,支持部分模板邏輯。也有基于mustache,Logic-less的GRMustache可供選擇。
2. 資源動(dòng)態(tài)更新和管理
無論是離線包、本地注入的JS、CSS文件、以及本地化Web中的默認(rèn)圖片,目的都是通過提前下載,替換網(wǎng)絡(luò)請(qǐng)求為本地讀取來優(yōu)化Web的加載體驗(yàn)和成功率。而對(duì)于這些資源的管理,開發(fā)者需要從下載與更新,以及Web中的訪問這兩個(gè)方面進(jìn)行設(shè)計(jì)優(yōu)化。
-
下載與更新
下載與重試:對(duì)于資源或是離線包的下載,選擇合適的時(shí)機(jī)、失敗重載時(shí)機(jī)、失敗重載次數(shù)都要根據(jù)業(yè)務(wù)靈活調(diào)整。通常為了增加成功率和及時(shí)更新,在冷啟動(dòng)、前后臺(tái)切換、關(guān)鍵的操作節(jié)點(diǎn),或者采用定時(shí)輪循的方式,都需要進(jìn)行資源版本號(hào)或MD5的判斷,用以觸發(fā)下載邏輯。當(dāng)然對(duì)于服務(wù)端來說,合理的灰度控制,也是保證業(yè)務(wù)穩(wěn)定的重要途徑。
簽名校驗(yàn):對(duì)于動(dòng)態(tài)下載的資源,我們都需要將原文件的簽名進(jìn)行校驗(yàn),防止在傳輸過程中被篡改。對(duì)于單項(xiàng)加密的辦法就是雙端對(duì)數(shù)據(jù)進(jìn)行MD5的加密,之后客戶端校驗(yàn)MD5是否符合預(yù)期;而雙向加密可以采用DES等加密算法,客戶端使用公鑰對(duì)資源驗(yàn)證使用。
增量更新:為了減少資源和離線包的重復(fù)下載,業(yè)內(nèi)大部分使用離線包的場(chǎng)景都采用了增量更新的方式。即客戶端在觸發(fā)請(qǐng)求資源時(shí),帶上本地已存在資源的標(biāo)示,服務(wù)端根據(jù)標(biāo)示和最新資源做對(duì)比,之后只提供新增或修改的Patch供客戶端下載。
-
基于LocalServer的訪問
在完成資源的下載與更新后,如何將Web請(qǐng)求重定向到本地,大部分App都依賴于NSURLProtocol。上文提到在WKWebView中雖然可以使用私有函數(shù)實(shí)現(xiàn)(或者iOS11+提供系統(tǒng)函數(shù)),但是仍然有許多問題。
目前業(yè)界一部分App,都采用了集成LocalServer的方式,接管部分Web請(qǐng)求,從而達(dá)到訪問本地資源的目的。同時(shí)集成了LocalServer,通過將本地資源封裝成Response,利用HTTP的緩存技術(shù),進(jìn)一步的優(yōu)化了讀取的時(shí)間和性能,實(shí)現(xiàn)層次化的緩存結(jié)構(gòu)。而使用了本地資源的HTTP緩存,就需要考慮緩存的控制和過期時(shí)間。通??梢酝ㄟ^在URL上增加本地文件的修改時(shí)間、或本地文件的MD5來確保緩存的有效性。

-
GCDWebServer淺析
排除Socket類型,業(yè)界流行的Objc版針對(duì)HTTP開源的WebServer,不外乎年久失修的CocoaHTTPServer以及GCDWebServer。GCDWebServer是一個(gè)基于GCD的輕量級(jí)服務(wù)器,簡(jiǎn)單的四個(gè)模塊 - Server / Connection / Request / Reponse,以及通過維護(hù)LIFO的Handler隊(duì)列傳入業(yè)務(wù)邏輯生成響應(yīng)。在排除了基于RFC的Request/Response協(xié)議設(shè)計(jì)之外,關(guān)鍵的代碼和流程如下:
//GCDWebServer 端口綁定 bind(listeningSocket, address, length) listen(listeningSocket, (int)maxPendingConnections) //GCDWebServer 綁定Socket端口并接收數(shù)據(jù)源 dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, dispatch_get_global_queue(_dispatchQueuePriority, 0)); //GCDWebServer 接收數(shù)據(jù)并創(chuàng)建Connection dispatch_source_set_event_handler(source, ^{ ... GCDWebServerConnection* connection = [(GCDWebServerConnection*)[self->_connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; //GCDWebServerConnection 讀取數(shù)據(jù) dispatch_read(_socket, length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t buffer, int error) { //GCDWebServerConnection 處理GCDWebServerMatchBlock和GCDWebServerAsyncProcessBlock self->_request = self->_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery); ... _handler.asyncProcessBlock(request, [completion copy]);在LocalServer的使用上,也要注意端口的選擇ports used by Apple,以及前后臺(tái)切換時(shí)suspendInBackground的設(shè)置和業(yè)務(wù)處理。
3. Javascript Open Api
隨著App業(yè)務(wù)的不斷發(fā)展,單純的Web加載與渲染無法滿足復(fù)雜的交互邏輯,如拍照、音視頻、藍(lán)牙、定位等,同時(shí)App內(nèi)也需要統(tǒng)一的登錄態(tài),統(tǒng)一的分享邏輯以及支付邏輯等。所以針對(duì)第三方的Web頁面,Native需要注冊(cè)相應(yīng)的 Javascript接口供Web使用。
-
對(duì)于Api需要提供的能力、接口設(shè)計(jì)和文檔規(guī)范,不同的業(yè)務(wù)邏輯和團(tuán)隊(duì)代碼風(fēng)格會(huì)有不同的定義,微信JS-SDK說明文檔 就是一個(gè)很好的例子。而脫離Javascript Open Api對(duì)外的接口設(shè)計(jì)和封裝,在內(nèi)部的實(shí)現(xiàn)上也有一些通用的關(guān)鍵因素,這里簡(jiǎn)單列舉幾個(gè):
-
注入方式和時(shí)機(jī)
對(duì)于Javascript文件的注入,最簡(jiǎn)單的就是將JS文件打包到項(xiàng)目中,使用WKWebView提供的系統(tǒng)函數(shù)進(jìn)行注入。這種方式無需網(wǎng)絡(luò)加載,可以合理的選擇注入時(shí)機(jī),但是無法動(dòng)態(tài)的進(jìn)行修改和調(diào)整。而對(duì)于這部分業(yè)務(wù)需求需要經(jīng)常調(diào)整的App來說,也可以把文件存儲(chǔ)到CDN,通過模板替換或者和Web合作者約定,在Web的HTML中通過URL的方式進(jìn)行加載,這種的方式雖然動(dòng)態(tài)化程度較高,但是需要合作方的配合,同時(shí)對(duì)于JS Api也不能做到拆分的注入。
針對(duì)上面的兩種方式的優(yōu)點(diǎn)不足,一個(gè)較為合理的方式是Javascript文件采用本地注入的方式,同時(shí)建立資源的動(dòng)態(tài)更新系統(tǒng)(上文)。這樣一方面支持了動(dòng)態(tài)更新,同時(shí)也無需合作方的配合,對(duì)于不同的業(yè)務(wù)場(chǎng)景也可以拆分不同的Api進(jìn)行注入,保證安全。
-
安全控制
對(duì)于Javascript Open Api設(shè)計(jì)實(shí)現(xiàn)的另一個(gè)重要方面,就是安全性的控制。由于完整的Api需要支持Native登錄、Cookies等較為敏感的信息獲取,同時(shí)也支持一些對(duì)UI和體驗(yàn)影響較多的功能如頁面跳轉(zhuǎn)、分享等,所以App需要一套權(quán)限分級(jí)的邏輯控制Web相關(guān)的接口調(diào)用,保證體驗(yàn)和安全。
常規(guī)的做法就是對(duì) Javascript Open Api建立分級(jí)的管理,不同權(quán)限的Web頁面只能調(diào)用各自權(quán)限內(nèi)的接口??蛻舳送ㄟ^Domain進(jìn)行分級(jí),同時(shí)支持動(dòng)態(tài)拉取權(quán)限D(zhuǎn)omain白名單,靈活的配置Web頁面的權(quán)限。在此基礎(chǔ)上App內(nèi)部也可以通過業(yè)務(wù)邏輯的劃分,在Native層面使用不同的容器加載頁面,而容器根據(jù)業(yè)務(wù)邏輯的不同,注入不同的JS文件進(jìn)行Api權(quán)限控制。
-

插播廣告 —— 幾十行代碼完成資訊類App多種形式內(nèi)容頁
HybridPageKit :一個(gè)針對(duì)資訊類App高性能、易擴(kuò)展、組件化的通用內(nèi)容頁實(shí)現(xiàn)框架。
想和我一起全面了解新聞?lì)怉pp的開發(fā),點(diǎn)我學(xué)習(xí)





