iOS開發(fā)中的Web應(yīng)用概述

為了更好的閱讀體驗(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)想。

目錄

image

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ā)者可以查閱官方文檔。

image
  • 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ù)邏輯。

    1. 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
      };
      
  1. 白屏
    通常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ù)。
  1. 其他WKWebView的系統(tǒng)級(jí)問題如Cookie、POST參數(shù)、異步Javascript等等一系列的問題,可以通過業(yè)務(wù)邏輯的調(diào)整重新適配

  2. 由于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都需要兼容UIWebViewWKWebView,所以目前的使用范圍仍然十分廣泛。

    在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í)的方向,由于篇幅的限制暫且不做深入的討論。

image
  • 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)的屬性和方法。
image
  • 使用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。

image

而不同于國外開發(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)目。

image

而在其中,以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)化。

image

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í)較多。

image

針對(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)求的效率。

image

所以對(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)先渲染。

image

4. 優(yōu)化整體流程

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

image


<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來確保緩存的有效性。

image
  • 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)限控制。

image

插播廣告 —— 幾十行代碼完成資訊類App多種形式內(nèi)容頁

HybridPageKit :一個(gè)針對(duì)資訊類App高性能、易擴(kuò)展、組件化的通用內(nèi)容頁實(shí)現(xiàn)框架。

想和我一起全面了解新聞?lì)怉pp的開發(fā),點(diǎn)我學(xué)習(xí)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容