1. 程式人生 > >移動開發中的 Web:WebView、WebKit、JSCore、Web 優化、熱修復、跨平臺、Native、Hybrid……

移動開發中的 Web:WebView、WebKit、JSCore、Web 優化、熱修復、跨平臺、Native、Hybrid……

移動開發領域近年來已經逐漸告別了野蠻生長的時期,進入了相對成熟的時代。而一直以來 Native 和 Web 的爭論從未停止,通過開發者孜孜不倦的努力,Web 的效率和 Native 的體驗也一直在尋求著平衡。本文聚焦 iOS 開發和 Web 開發的交叉點,內容涉及到 iOS 開發中全部的 Web 知識,涵蓋從基礎使用到 WebKit、從 JSCore 到大前端、從 Web 優化到業務擴充套件等方面,希望通過簡要的介紹,幫助開發者一窺 Hybrid 和大前端的構想。

iOS 中 Web 容器與載入

1. iOS 中的 Web 容器

目前 iOS 系統為開發者提供三種方式來展示 Web 內容,分別是 UIWebView、WKWebView 和 SFSafariViewController:

  • UIWebView

    UIWebView 從 iOS2 開始就作為 App 內展示 Web 內容的容器,但是長久以來一直遭受開發者的詬病,它存在系統級的記憶體洩露、極高記憶體峰值、較差的穩定性、Touch Delay 以及 JavaScript 的執行效能和通訊限制等問題。在 iOS12 以後已經被標記為 Deprecated 不再維護。

  • WKWebView

    在 iOS8 中,Apple 引入了新一代的 WebKit framework,同時提供了 WKWebView 用來替代傳統的 UIWebView,它更加穩定,擁有 60fps 滾動重新整理率、豐富的手勢、KVO、高效的 Web 和 Native 通訊,預設進度條等功能,而最重要的是,它使用了和 Safari 相同的 Nitro 引擎極大提升了 JavaScript 的執行速度。WKWebView 獨立的程序管理,也降低了記憶體佔用及 Crash 對主 App 的影響。

  • SFSafariViewController

    在 iOS9 中,Apple 引入了 SFSafariViewController,其特點就是在 App 內可以開啟一個高度標準化的、和 Safari 一樣介面和特性的頁面。同時 SFSafariViewController 支援和 Safari 共享 Cookie 和表單資料。

這幾中容器如何選擇呢?

對於 SFSafariViewController,由於其標準化程度之高,使之介面和互動邏輯無法和 App 統一,基於 App 整體體驗的考慮,一般都使用在相對獨立的功能和模組中,最常見的就是在 App 內開啟 App Store 或者廣告、遊戲推廣的頁面。

對於 UIWebView/WKWebView,如果說之前由於 NSURLProtocol 的問題,好多 App 都在繼續使用 UIWebView,那麼隨著 App 放棄維護 UIWebView(iOS12),全部的 App 應該會陸續地切換到 WKWebView 中來。當然,最初 WKWebView 也為開發者們帶來了一些難題,但是隨著系統的升級與業務邏輯的適配也逐步得到修復,後文會列舉幾個最為關注的技術點。

UIWebView/WKWebView 對主 App 記憶體的影響:

2. WebKit 框架與使用

WebKit.framework

WebKit 是一個開源的 Web 瀏覽器引擎,每當談到 WebKit,開發者常常迷惑於它和 WebKit2、Safari、iOS 中的框架,以及 Chromium 等瀏覽器的關係。

廣義的 WebKit 其實就是指 WebCore,它主要包含了 HTML 和 CSS 的解析、佈局和定位這類渲染 HTML 的功能邏輯。而狹義的 WebKit 就是在 WebCore 的基礎上,不同平臺封裝 JavaScript 引擎、網路層、GPU 相關的技術(WebGL、視訊)、繪製渲染技術以及各個平臺對應的介面,形成我們可以用的 WebView 或瀏覽器,也就是所謂的 Webkit Ports。

比如在 Safari 中 JS 的引擎使用 JavascriptCore,而 Chromium 中使用 v8;渲染方而 Safari 使用 CoreGraphics,而 Chromium 中使用 skia;網路方而 Safari 使用 CFNetwork,而 Chromium 中使用 Chromium stack 等等。而 Webkit2 是相對於狹義上的 Webkit 架構而言,主要變化是在 API 層支援多程序,分離了 UI 和 Web 介面的程序,使之通過 IPC 來進行通訊。

iOS 中的 WebKit.framework 就是在 WebCore、底層橋接、JSCore 引擎等核心模組的基礎上,針對 iOS 平臺的專案封裝,它基於新的 WKWebView,提供了一系列瀏覽特性的設定,以及簡單方便的載入回撥。

Web 容器使用流程與關鍵節點

對於大部分日常使用來說,開發者需要關注的就是 WKWebView 的建立、配置、載入、以及系統回撥的接收。

對於 Web 開發者,業務邏輯一般通過基於 Web 頁面中 Dom 渲染的關鍵節點來處理,而對於 iOS 開發者,WKWebView 提供的的註冊、載入和回撥時機,沒有明確地與 Web 載入的關鍵節點相關聯。準確地理解和處理兩個維度的載入順序,選擇合理的業務邏輯處理時機,才可以實現準確而高效的應用。

WKWebView 常見問題

使用 WKWebView 帶來的另外一個好處,就是我們可以通過原始碼理解部分載入邏輯,為 Crash 提供一些思路,或者使用一些私有方法處理複雜業務邏輯。

  1. NSURLProtocol

    WKWebView 最為顯著的改變,就是不支援 NSURLProtocol,為了相容舊的業務邏輯,一部分 App 通過 WKBrowsingContextController 中的非公開方法實現了 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 中,系統增加了 setURLSchemeHandler 函式用來攔截自定義的 Scheme,但是不同於 UIWebView,新的函式只能攔截自定義的 Scheme(SchemeRegistry.cpp),對使用最多的 HTTP/HTTPS 依然不能有效地攔截。

     //SchemeRegistry
     static const StringVectorFunction functions[] {
         builtinSecureSchemes,                // about;data...
         builtinSchemesWithUniqueOrigins,     // javascript...
         builtinEmptyDocumentSchemes,
         builtinCanDisplayOnlyIfCanRequestSchemes,
         builtinCORSEnabledSchemes,           //http;https
     };
    
  2. 白屏

    通常 WKWebView 白屏的原因主要分兩種,一種是由於 Web 的程序 Crash(多見於內部程序通訊);一種就是 WebView 渲染時的錯誤(Debug 一切正常只是沒有對應的內容)。對於白屏的檢測,前者在 iOS9 之後系統提供了對應 Crash 的回撥函式,同時業界也有通過判斷 URL/Title 是否為空的方式作為輔助;後者業界通過檢視樹對比,判斷 SubView 是否包含 WKCompsitingView,以及通過隨機點截圖等方式作為白屏判斷的依據。

  3. 其它 WKWebView 的系統級問題如 Cookie、POST 引數、非同步 JavaScript 等,可以通過業務邏輯的調整重新適配。

  4. 由於 WebKit 原始碼的開放性,我們也可以利用私有方法來簡化程式碼邏輯、實現複雜的產品需求。例如在 WKWebViewPrivate 中可以獲得各種頁面資訊、直接取到 UserAgent、 在 WKBackForwardListPrivate 中可以清理掉全部的跳轉歷史、以及在 WKContentViewInteraction 中替換方法實現自定義的 MenuItem 等。

     @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 中的應用場景

WKWebView 系統提供了四個用於載入渲染 Web 的函式,這四個函式從載入的型別上可以分為兩類:載入 URL & 載入 HTML\Data。所以基於此也延伸出兩種不同的業務場景:載入 URL 的頁面直出類和載入資料的模板渲染類,同時兩種型別各自也有不同的優化重點及方向。

頁面直出類

  //根據URL直接展示Web頁面
  - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;

通常各類 App 中的 Web 頁面載入都是通過載入 URL 的方式,比如嵌入的運營活動頁面、廣告頁面等等。

模板渲染類

  //根據模板&資料渲染Web頁面
  - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
  ...

需要使用 WebView 展示,且互動邏輯較多的頁面,最常見的就是資訊類 App 的內容展示頁。

iOS 中 Web 與 Native 的通訊

單純使用 Web 容器載入頁面已經不能滿足複雜的功能,開發者希望資料可以在 Native 和 Web 之間通訊傳遞來實現複雜的功能,而 JavaScript 就是通訊的媒介。對於有 WebView 的情況,雖然 WKWebView 提供了系統級的方法,但是大部分 App 仍然使用基於 URLScheme 的 WebViewBridge 用以相容 UIWebView。而脫離了 WebView 容器,系統提供了 JavascriptCore 的框架,它也為之後蓬勃發展的跨平臺和熱修復技術提供了可能。

1. 基於 WebView 的通訊

基於 WebView 的通訊主要有兩個途徑,一個是通過系統或私有方法,獲取 WebView 當中的 JSContext,使用系統封裝的基於 JSCore 的函式通訊;另一類是通過建立自定義 Scheme 的 iframe Dom,客戶端在回撥中進行攔截實現。

UIWebView & WKWebView 系統級

在 UIWebView 時代沒有提供系統級的函式進行 Web 與 Native 的互動,絕大部分 App 都是通過 WebViewJavascriptBridge(下節介紹)來進行通訊,而由於 JavascriptCore 的存在,對於 UIWebView 來說只要有效的獲取到內部的 JSContext,也可以達到目的。目前已知的有效獲取 Context 的私有方法如下:

  //通過系統廢棄函式獲取context
  - (void)webView:(WebView *)webView didCreateJavaScriptContext:(JSContext *)context forFrame:(WebFrame *)frame;
	
  //通過valueForKeyPath獲取context
  self.jsContext = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

在 WKWebView 中提供了系統級的 Web 和 Native 通訊機制,通過 Message Handler 的封裝使開發效率有了很大的提升。同時系統封裝了 JavaScript 物件和 Objective-C 物件的轉換邏輯,也進一步降低了使用的門檻。

  // js端傳送訊息
  window.webkit.messageHandlers.{NAME}.postMessage()
	
  //Native在回撥中接收
  - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

攔截自定義 Scheme 請求 - WebViewJavascriptBridge

由於私有方法的穩定性與稽核風險,開發者不願意使用上文提到的 UIWebView 獲取 JSContext 的方式進行通訊,所以通常都採用基於 iframe 和自定義 Scheme 的 JavascriptBridge 進行通訊。雖然在之後的 WKWebView 提供了系統函式,但是大部分 App 都需要相容 UIWebView 與 WKWebView,所以目前的使用範圍仍然十分廣泛。

類似的開源框架有很多,但是無外乎都是 Web 側根據固定的格式建立包含通訊資訊的 Request,之後建立隱式 iframe 節點請求;Native 側在相應的 WebView 回撥中解析 Request 的 Scheme,之後按照格式解析資料並處理。

而對於資料傳遞和回撥處理的問題,在相容兩種 WebView、持續更新的 WebViewJavascriptBridge 中,iframe Request 沒有直接傳遞資料,而是 Web 和 Native 側維護共同的引數或回撥 Queue,Native 通過 Request 中 Scheme 的解析觸發對 Queue 裡資料的讀取。

2. 脫離 WebView 的通訊 JavaScriptCore

JavascriptCore

JavascriptCore 一直作為 WebKit 中內建的 JS 引擎使用,在 iOS7 之後,Apple 對原有的 C/C++ 程式碼進行了 OC 封裝,成為系統級的框架供開發者使用。作為一個引擎來講,JavascriptCore 的詞法、語法分析,以及多層次的 JIT 編譯技術都是值得深入挖掘和學習的方向,由於篇幅的限制暫且不做深入的討論。

JavascriptCore.framework

雖然 JavascriptCore.framework 只暴露了較少的標頭檔案和系統函式,但卻提供了在 App 中脫離 WebView 執行 JavaScript 的環境和能力。

  • JSVirtualMachine:提供了 JS 執行的底層資源及記憶體。雖然 Java 與 JavaScript 沒有一點關係,但是同樣作為虛擬機器,JSVM 和 JVM 做了一部分類似的事情,每個 JSVirtualMachine 獨佔執行緒,擁有獨立的空間和管理,但是可以包含多個 JSContext。
  • JSContext:提供了 JS 執行的上下文環境和介面,可以不準確地理解為,就是建立了一個 JavaScript 中的 Window 物件。
  • JSValue:提供了 OC 和 JS 間資料型別的封裝和轉換 Type Conversions。除了基本的資料型別,需要注意 OC 中的 Block 轉換為 JS 中的 function、Class 轉換為 Constructor 等等。
  • JSManagedValue:JavaScript 使用 GC 機制管理記憶體,而 OC 採用引用計數的方式管理記憶體。所以在 JavascriptCore 使用過程中,難免會遇到迴圈引用以及提前釋放的問題。JSManagedValue 解決了在兩種環境中的記憶體管理問題。
  • JSExport:提供了類、屬性和例項方法的呼叫介面。內部實現是在 ProtoType & Constructor 中實現對應的屬性和方法。 

使用 JavascriptCore 進行通訊

對於 JavascriptCore 粗淺的理解,可以認為使用 Block 方法,內部是將 Block 儲存到一個 Web 環境中的全域性 Object 中,例如 Window,而使用 JSExport 方法,則是在 Web 環境中 Object 的 prototype 中建立屬性、例項方法,在 constructor 物件中建立類方法,從而實現 Web 中的呼叫。

  • Native - Web:通過 JavascriptCore,Native 可以直接在 Context 中執行 JS 語句,和 Web 側進行通訊和互動。

      JSValue *value = [self.jsContext evaluateScript:@"document.cookie"];
    
  • Web - Native:對於 Web 側向 Native 的通訊,JavascriptCore 提供兩種方式,註冊 Block & Export 協議。

      //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);
    

3. App 中的應用場景

  • 基於 WebView 的通訊,主要用於 App 向 H5 頁面中注入的 JavaScript Open Api,如提供 Native 的拍照、音視訊、定位,以及 App 內的登入與分享等功能。 
  • JavascriptCore,則催生了動態化、跨平臺以及熱修復等一系列技術的蓬勃發展。

跨平臺與熱修復

近幾年來國內外移動端各種跨平臺方案如雨後春筍般湧現,“Write once, run anywhere”不再是空話。這些跨平臺技術方案的切入點是在 Web 側 DSL、virtualDom 等方面的優化,以及 Native 側 Runtime 的應用與封裝,但兩端通訊的核心,依然是 JavascriptCore。

除了對跨平臺技術的積極探索,國內開發者對熱修復技術也產生了極大的熱情,同樣作為 Native 和 Web 的交叉點,JavascriptCore 依然承擔著整個技術結構中的通訊任務。

1. 基於 Web 的熱修復技術

對於國內的 iOS 開發者來說,稽核週期、敏感業務、支付分成以及 bug 修復都催生了熱修復方向的不斷探索。在蘋果加強稽核之前,幾乎所有大型的 App 都把熱修復當成了 iOS 開發的基礎能力,最近在《移動開發還有救麼》一文中也詳細地介紹了相關黑科技的前世今生。在所有 iOS 熱修復的方案中,基於 JavaScript、同時也是影響最大的就是 JSPatch。

基於上文的分析,對於脫離 WebView 的 Native 和 Web 間的通訊,我們只能使用 JavascriptCore。而在 JavascriptCore 中提供了兩種方式用於通訊,即 Context 註冊 Block 的回撥,以及 JSExport。對於熱修復的場景來說,我們不可能把潛在需要修復的函式都一一使用協議進行註冊,更不能對新增方法和刪除方法等進行處理,所以在 Native 和 Web 通訊這個維度,我們只能採用 Context 註冊 Block 的方式。

  // 註冊回撥
  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 回撥的方式後,熱修復就面臨著如何在 JS 中呼叫類以及類的方法的問題。由於沒有使用 JSExport 等方式,JS 是無法找到相應類等屬性和方法的,在 JSPatch 中,通過簡單的字串替換,將所有方法都替換成通用函式 (__c),然後就可以將相關資訊傳遞給 Native,進而使用 runtime 介面呼叫方法。

  // 替換全部方法呼叫
  static NSString *_replaceStr = @".__c(\"$1\")(";
	
  // 呼叫方法
  __c: function(methodName) {
      ...
      return function(){
          ...
      	var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                       		 _OC_callC(clsName, selectorName, args)
  		return _formatOCToJS(ret)
    }

當然對於 JSPatch 以及其它熱修復的專案來說,Web 和 Native 通訊只是整個框架中的一個技術點,更多的實現原理和細節由於篇幅的關係暫且不作介紹。

2. 基於 Web 的跨平臺技術

隨著 Google 開源了基於 Dart 語言的 Flutter,跨平臺的技術又進入了一個新的發展階段。對於傳統的跨平臺技術來講,各個公司以 JavascriptCore 作為通訊橋樑,圍繞著 DSL 的解析、方法表的註冊、模組註冊通訊、引數傳遞的設計以及 OC Runtime 的運用等不同方向,封裝成了一個又一個跨平臺的專案。

而在其中,以 JavaScript 作為前端 DSL 的跨平臺技術方案裡,Facebook 的 react-native 以及阿里(目前託管給了 Apache 軟體基金會)的 Weex 最為流行。在網路上兩者的比較文章有很多,集中在學習成本、框架生態、程式碼侵入、效能以及包大小等方面,各個業務可以根據自己的重點選擇合理的技術結構。

而不管是 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; 

和熱修復技術一樣,跨平臺又是一個龐大的技術體系,JavascriptCore 僅僅是作為整個體系運轉中的一個小小的部分,而整個跨平臺的技術方案就需要另開多個篇幅進行介紹了。

iOS 中 Web 相關優化策略

隨著 Web 技術的不斷升級以及 App 動態性業務需求的增多,越來越多的 Web 頁面加入到了 iOS App 當中,與之對應的,首屏展示速度體驗這個至關重要的領域,也成為了移動客戶端中 Web 業務最重要的優化方向。

1. 不同業務場景的優化策略

對於單純的 Web 頁面來說,業界早已有了合理的優化方向以及成熟的優化方案,而對於移動客戶端中的 Web 來說,開發者在進行單一的 Web 優化時,還可以通過優化 Web 容器以及 Web 頁面中資料載入方式等多個途徑做出優化。

所以對於 iOS 開發中的優化來說,就是通過 Native 和 Web 兩個維度的優化關鍵渲染路徑,保證 WebView 優先渲染完畢。由此我們梳理了常規 Web 頁面整體的載入順序,從中找出關鍵渲染路徑,繼而逐個分析、優化。

2. Web 維度的優化

通用 Web 優化

對於 Web 的通用優化方案,一般來說在網路層面,可以通過 DNS 和 CDN 技術減少網路延遲、通過各種 HTTP 快取技術減少網路請求次數、通過資源壓縮和合並減少請求內容等。在渲染層面可以通過精簡和優化業務程式碼、按需載入、防止阻塞、調整載入順序優化等等。對於這個老生常談的問題,業內已經有十分成熟和完整的總結,比如可以參考《Best Practices for Speeding Up Your Web Site》

其它

脫離較為通用的優化,在對程式碼侵入寬容度較高的場景中,開發者對 Web 優化有著更為激進的做法。例如在 VasSonic 中,除了 Web 容器複用、資料模板分離、預拉取和通用的優化方式外,還通過自定義 VasSonic 標籤將 HTML 頁面進行劃分,分段進行快取控制,以達到更高的優化效果。

3. Native 維度的優化

容器複用和預熱

WKWebView 雖然 JIT 大幅優化了 JS 的執行速度,但是單純的載入渲染 HTML,WKWebView 比 UIWebView 慢了很多。根據渲染的不同階段分別對耗時進行測試,同時對比 UIWebView,我們發現 WKWebView 在初始化及渲染開始前的耗時較多。

針對這種情況,業界主流的做法就是複用 & 預熱。預熱就是在 App 啟動時建立一個 WKWebView,使其內部部分邏輯預熱以提升載入速度。而複用又分為兩種,較為複雜的是處理邊界條件以達到真正的複用,還有一種較為取巧的辦法就是常駐一個空 WKWebView 在記憶體。

HybridPageKit 提供了易於整合的完整 WKWebView 重用機制實現,開發者可以無需關注複用細節,無縫地體驗更為高效的 WKWebView。

Native 並行資源請求 & 離線包

由於 Web 頁面內請求流程不可控以及網路環境的影響,對於 Web 的載入來說,網路請求一直是優化的重點。開發者較為常用的做法是使用 Native 並行代理資料請求,替代 Web 核心的資源載入。在客戶端初始化頁面的同時,並行開始網路請求資料;當 Web 頁面渲染時向 Native 獲取其代理請求的資料。

而將並行載入和預載入做到極致的優化,就是離線包的使用。將常用的需要下載資源(HTML 模板、JS 檔案、CSS 檔案與佔位圖片)打包,App 選擇合適的時機全部下載到本地,當 Web 頁面渲染時向 Native 獲取其資料。

通過離線包的使用,Web 頁面可以並行(提前)載入頁面資源,同時擺脫了網路的影響,提高了頁面的載入速度和成功率。當然離線包作為資源動態更新的一個方式,合理的下載時機、增量更新、加密和校驗等方面都是需要進行設計和思考的方向,後文會簡單介紹。

複雜 Dom 節點 Native 化實現

當並行請求資源,客戶端代理資料請求的技術方案逐漸成熟時,由於 WKWebView 的限制,開發者不得不面對業務調整和適配。其中保留原有代理邏輯、採用 LocalServer 的方式最為普遍。但是由於 WKWebView 的程序間通訊、LocalServer Socket 建立與連線、資源的重複編解碼都影響了代理請求的效率。

所以對於一些資訊類 App,通常採用 Dom 節點佔位、Native 渲染實現的方式進行優化,如圖片、地圖、音視訊等模組。這樣不但能減少通訊和請求的建立、提供更加友好的互動、也能並行地進行 View 的渲染和處理,同時減少 Web 頁面的業務邏輯。

HybridPageKit 中就提供封裝好的功能框架,開發者可以簡單的替換 Dom 節點為 NativeView。

按優先順序劃分業務邏輯

從 App 的維度上看,一個 Web 頁面從入口點選到渲染完成,或多或少都會有 Native 的業務邏輯並行執行。所以這個角度的優化關鍵渲染路徑,就是優先保證 WebView 以及其它在首屏直接展示的 Native 模組優先渲染,所以承載 Web 頁面的 Native 容器,可以根據業務邏輯的優先順序,在保證 WebView 模組展示之後,選擇合適的時機進行資料載入、檢視渲染等。這樣就能保證在 Native 的維度上,關鍵路徑優先渲染。

4. 優化整體流程

整體上對於客戶端來說,我們可以從 Native 維度(容器和資料載入)以及 Web 維度兩個方向提升載入速度,按照頁面的載入流程,整體的優化方向如下:

iOS 中 Web 相關延伸業務

1. 模板引擎

為了並行載入資料以及並行處理複雜的展示邏輯,對於非直出型別的 Web 頁面,絕大部分 App 都採用資料和模板分離下發的方式。而這樣的技術架構,導致在客戶端內需要增加替換對應 DSL 的模板標籤,形成最終的 HTML 業務邏輯。簡單的字串替換邏輯不但低效,還無法做到合理的元件化管理,以及元件合理地與 Native 互動,而模板引擎相關技術會使這種邏輯和表現分離的業務場景實現得更加簡潔和優雅。

基於模板引擎與資料分離,客戶端可以根據資料並行建立子業務模組,同時在子業務模組中處理和 Native 互動的部分,如圖片裁剪適配、點選跳轉等,生成 HTML 程式碼片段,之後基於模板進行替換生成完整的頁面。這樣不但減少了大量的字串替換邏輯,同時業務也得到了合理拆分。

模板引擎的本質就是字串的解析和替換拼接,在 Web 端不同的使用場景有很多不同語法的引擎型別,而在客戶端較為流行的,有使用較為複雜的 MGTemplateEngine,它類似於 Smarty,支援部分模板邏輯。也有基於 mustache,Logic-less 的 GRMustache 可供選擇。

2. 資源動態更新和管理

無論是離線包、本地注入的 JS、CSS 檔案,還是本地化 Web 中的預設圖片,目的都是通過提前下載,替換網路請求為本地讀取來優化 Web 的載入體驗和成功率,而對於這些資源的管理,開發者需要從下載與更新,以及 Web 中的訪問這兩個方面進行設計優化。

下載與更新

  • 下載與重試:對於資源或是離線包的下載,選擇合適的時機、失敗過載時機、失敗過載次數都要根據業務靈活調整。通常為了增加成功率和及時更新,在冷啟動、前後臺切換、關鍵的操作節點,或者採用定時輪循的方式,都需要進行資源版本號或 MD5 的判斷,用以觸發下載邏輯。當然對於服務端來說,合理的灰度控制,也是保證業務穩定的重要途徑。

  • 簽名校驗:對於動態下載的資源,我們都需要將原檔案的簽名進行校驗,防止在傳輸過程中被篡改。對於單項加密的辦法就是雙端對資料進行 MD5 的加密,之後客戶端校驗 MD5 是否符合預期;而雙向加密可以採用 DES 等加密演算法,客戶端使用公鑰對資源驗證使用。

  • 增量更新:為了減少資源和離線包的重複下載,業內大部分使用離線包的場景都採用了增量更新的方式。即客戶端在觸發請求資源時,帶上本地已存在資源的標示,服務端根據標示和最新資源做對比,之後只提供新增或修改的 Patch 供客戶端下載。

基於 LocalServer 的訪問

在完成資源的下載與更新後,如何將 Web 請求重定向到本地,大部分 App 都依賴於 NSURLProtocol。上文提到在 WKWebView 中雖然可以使用私有函式實現(或者 iOS11+ 提供的系統函式),但是仍然有許多問題。

目前業界一部分 App,都採用了整合 LocalServer 的方式,接管部分 Web 請求,從而達到訪問本地資源的目的。同時集成了 LocalServer,通過將本地資源封裝成 Response,利用 HTTP 的快取技術,進一步的優化了讀取的時間和效能,實現層次化的快取結構。而使用了本地資源的 HTTP 快取,就需要考慮快取的控制和過期時間,通常可以通過在 URL 上增加本地檔案的修改時間、或本地檔案的 MD5 來確保快取的有效性。

GCDWebServer 淺析

排除 Socket 型別,業界流行的 Objc 版針對 HTTP 開源的 WebServer,不外乎年久失修的 CocoaHTTPServer 以及 GCDWebServer。GCDWebServer 是一個基於 GCD 的輕量級伺服器,擁有簡單的四個模組:Server/Connection/Request/Reponse,它通過維護 LIFO 的 Handler 佇列傳入業務邏輯生成響應。在排除了基於 RFC 的 Request/Response 協議設計之後,關鍵的程式碼和流程如下:

  //GCDWebServer 埠繫結
  bind(listeningSocket, address, length)
  listen(listeningSocket, (int)maxPendingConnections)
    
  //GCDWebServer 繫結Socket埠並接收資料來源
  dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, dispatch_get_global_queue(_dispatchQueuePriority, 0));
	
  //GCDWebServer 接收資料並建立Connection
  dispatch_source_set_event_handler(source, ^{
      ...
     GCDWebServerConnection* connection = [(GCDWebServerConnection*)[self->_connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; 
	
  //GCDWebServerConnection 讀取資料
  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),以及前後臺切換時 suspendInBackground 的設定和業務處理。

3. JavaScript Open Api

隨著 App 業務的不斷髮展,單純的 Web 載入與渲染無法滿足複雜的互動邏輯,如拍照、音視訊、藍芽、定位等,同時 App 內也需要統一的登入態、統一的分享邏輯以及支付邏輯等,所以針對第三方的 Web 頁面,Native 需要註冊相應的  JavaScript 介面供 Web 使用。

對於 Api 需要提供的能力、介面設計和文件規範,不同的業務邏輯和團隊程式碼風格會有不同的定義,微信 JS-SDK 說明文件就是一個很好的例子。而脫離 JavaScript Open Api 對外的介面設計和封裝,在內部的實現上也有一些通用的關鍵因素,這裡簡單列舉幾個:

注入方式和時機

對於 JavaScript 檔案的注入,最簡單的就是將 JS 檔案打包到專案中,使用 WKWebView 提供的系統函式進行注入。這種方式無需網路載入,可以合理地選擇注入時機,但是無法動態地進行修改和調整。而對於這部分業務需求需要經常調整的 App 來說,也可以把檔案儲存到 CDN,通過模板替換或者和 Web 合作者約定,在 Web 的 HTML 中通過 URL 的方式進行載入,這種方式雖然動態化程度較高,但是需要合作方的配合,同時對於 JS Api 也不能做到拆分地注入。

針對上面的兩種方式的不足,一個較為合理的方式是 JavaScript 檔案採用本地注入的方式,同時建立資源的動態更新系統(上文)。這樣一方面支援了動態更新,同時也無需合作方的配合,對於不同的業務場景也可以拆分不同的 Api 進行注入,保證安全。

安全控制

JavaScript Open Api 設計實現的另一個重要方面,就是安全性的控制。由於完整的 Api 需要支援 Native 登入、Cookies 等較為敏感的資訊獲取,同時也支援一些對 UI 和體驗影響較多的功能,如頁面跳轉、分享等,所以 App 需要一套許可權分級的邏輯控制 Web 相關的介面呼叫,保證體驗和安全。

常規的做法就是對 JavaScript Open Api 建立分級的管理,不同許可權的 Web 頁面只能呼叫各自許可權內的介面。客戶端通過 Domain 進行分級,同時支援動態拉取許可權 Domain 白名單,靈活地配置 Web 頁面的許可權。在此基礎上 App 內部也可以通過業務邏輯劃分,在 Native 層面使用不同的容器載入頁面,而容器根據業務邏輯的不同,注入不同的 JS 檔案進行 Api 許可權控制。

回顧一下,本文聚焦 iOS 開發和 Web 開發的交叉點,內容涉及到 iOS 開發中全部的 Web 知識,涵蓋從基礎使用到 WebKit、從 JSCore 到大前端、從 Web 優化到業務擴充套件等方面,希望通過這樣簡要的介紹,幫助開發者一窺 Hybrid 和大前端的構想,如果覺得本文對你有所幫助,歡迎點贊。

作者介紹

朱德權,個人 GitHub:https://github.com/dequan1331

---------------------------------------------

讀完文章先別走,歡迎大家轉發就文章相關內容進行討論與轉發,我們後續會從評論區抽取 9 名評論優質的讀者贈送《狼書(卷1):更了不起的Node.js》一書。感謝 @博文視點 提供的獎品。

簡介:《狼書(卷1):更了不起的Node.js》以 Node.js 為主,講解了 Node.js 的基礎知識、開發除錯方法、原始碼原理和應用場景,旨在向讀者展示如何通過最新的 Node.js 和 npm 編寫出更具前端特色、更具工程化優勢的程式碼。本書還講解了 Node.js 中最核心、最複雜的非同步流程控制,展望了未來非同步流程的發展方向,非常適合大前端領域及後端領域的測試、運維及軟體開發從業者閱讀、學習。

同時也歡迎大家踴躍投稿,每一期的投稿文章作者也會獲得贈書噢!

投稿要求見:

https://my.oschina.net/editorial-story/blog/1814725

可參考已釋出文章: