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

本文做者:oschina_2020javascript

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

iOS 中 Web 容器與加載

1. iOS 中的 Web 容器

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

  • UIWebView

    UIWebView 從 iOS2 開始就做爲 App 內展現 Web 內容的容器,可是長久以來一直遭受開發者的詬病,它存在系統級的內存泄露、極高內存峯值、較差的穩定性、Touch Delay 以及 JavaScript 的運行性能和通訊限制等問題。在 iOS12 之後已經被標記爲 Deprecated 再也不維護。react

  • WKWebView

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

  • SFSafariViewController

    在 iOS9 中,Apple 引入了 SFSafariViewController,其特色就是在 App 內能夠打開一個高度標準化的、和 Safari 同樣界面和特性的頁面。同時 SFSafariViewController 支持和 Safari 共享 Cookie 和表單數據。git

這幾中容器如何選擇呢?github

對於 SFSafariViewController,因爲其標準化程度之高,使之界面和交互邏輯沒法和 App 統一,基於 App 總體體驗的考慮,通常都使用在相對獨立的功能和模塊中,最多見的就是在 App 內打開 App Store 或者廣告、遊戲推廣的頁面。web

對於 UIWebView/WKWebView,若是說以前因爲 NSURLProtocol 的問題,好多 App 都在繼續使用 UIWebView,那麼隨着 App 放棄維護 UIWebView(iOS12),所有的 App 應該會陸續地切換到 WKWebView 中來。固然,最初 WKWebView 也爲開發者們帶來了一些難題,可是隨着系統的升級與業務邏輯的適配也逐步獲得修復,後文會列舉幾個最爲關注的技術點。算法

UIWebView/WKWebView 對主 App 內存的影響:macos

2. WebKit 框架與使用

WebKit.framework

WebKit 是一個開源的 Web 瀏覽器引擎,每當談到 WebKit,開發者經常迷惑於它和 WebKit二、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。

原文連接地址:https://developer.baidu.com/topic/show/290637

相關文章
相關標籤/搜索