本文做者:oschina_2020javascript
移动开发领域近年来已经逐渐告别了野蛮生长的时期,进入了相对成熟的时代。而一直以来 Native 和 Web 的争论从未中止,经过开发者孜孜不倦的努力,Web 的效率和 Native 的体验也一直在寻求着平衡。本文聚焦 iOS 开发和 Web 开发的交叉点,内容涉及到 iOS 开发中所有的 Web 知识,涵盖从基础使用到 WebKit、从 JSCore 到大前端、从 Web 优化到业务扩展等方面,但愿经过简要的介绍,帮助开发者一窥 Hybrid 和大前端的构想。前端
目前 iOS 系统为开发者提供三种方式来展现 Web 内容,分别是 UIWebView、WKWebView 和 SFSafariViewController:java
UIWebView 从 iOS2 开始就做为 App 内展现 Web 内容的容器,可是长久以来一直遭受开发者的诟病,它存在系统级的内存泄露、极高内存峰值、较差的稳定性、Touch Delay 以及 JavaScript 的运行性能和通讯限制等问题。在 iOS12 之后已经被标记为 Deprecated 再也不维护。react
在 iOS8 中,Apple 引入了新一代的 WebKit framework,同时提供了 WKWebView 用来替代传统的 UIWebView,它更加稳定,拥有 60fps 滚动刷新率、丰富的手势、KVO、高效的 Web 和 Native 通讯,默认进度条等功能,而最重要的是,它使用了和 Safari 相同的 Nitro 引擎极大提高了 JavaScript 的运行速度。WKWebView 独立的进程管理,也下降了内存占用及 Crash 对主 App 的影响。ios
在 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
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,提供了一系列浏览特性的设置,以及简单方便的加载回调。
对于大部分平常使用来讲,开发者须要关注的就是 WKWebView 的建立、配置、加载、以及系统回调的接收。
对于 Web 开发者,业务逻辑通常经过基于 Web 页面中 Dom 渲染的关键节点来处理,而对于 iOS 开发者,WKWebView 提供的的注册、加载和回调时机,没有明确地与 Web 加载的关键节点相关联。准确地理解和处理两个维度的加载顺序,选择合理的业务逻辑处理时机,才能够实现准确而高效的应用。
使用 WKWebView 带来的另一个好处,就是咱们能够经过源码理解部分加载逻辑,为 Crash 提供一些思路,或者使用一些私有方法处理复杂业务逻辑。
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 };
白屏
一般 WKWebView 白屏的缘由主要分两种,一种是因为 Web 的进程 Crash(多见于内部进程通讯);一种就是 WebView 渲染时的错误(Debug 一切正常只是没有对应的内容)。对于白屏的检测,前者在 iOS9 以后系统提供了对应 Crash 的回调函数,同时业界也有经过判断 URL/Title 是否为空的方式做为辅助;后者业界经过视图树对比,判断 SubView 是否包含 WKCompsitingView,以及经过随机点截图等方式做为白屏判断的依据。
其它 WKWebView 的系统级问题如 Cookie、POST 参数、异步 JavaScript 等,能够经过业务逻辑的调整从新适配。
因为 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;
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 的内容展现页。
单纯使用 Web 容器加载页面已经不能知足复杂的功能,开发者但愿数据能够在 Native 和 Web 之间通讯传递来实现复杂的功能,而 JavaScript 就是通讯的媒介。对于有 WebView 的状况,虽然 WKWebView 提供了系统级的方法,可是大部分 App 仍然使用基于 URLScheme 的 WebViewBridge 用以兼容 UIWebView。而脱离了 WebView 容器,系统提供了 JavascriptCore 的框架,它也为以后蓬勃发展的跨平台和热修复技术提供了可能。
基于 WebView 的通讯主要有两个途径,一个是经过系统或私有方法,获取 WebView 当中的 JSContext,使用系统封装的基于 JSCore 的函数通讯;另外一类是经过建立自定义 Scheme 的 iframe Dom,客户端在回调中进行拦截实现。
在 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 里数据的读取。
JavascriptCore 一直做为 WebKit 中内置的 JS 引擎使用,在 iOS7 以后,Apple 对原有的 C/C++ 代码进行了 OC 封装,成为系统级的框架供开发者使用。做为一个引擎来说,JavascriptCore 的词法、语法分析,以及多层次的 JIT 编译技术都是值得深刻挖掘和学习的方向,因为篇幅的限制暂且不作深刻的讨论。
虽然 JavascriptCore.framework 只暴露了较少的头文件和系统函数,但却提供了在 App 中脱离 WebView 执行 JavaScript 的环境和能力。
对于 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);
近几年来国内外移动端各类跨平台方案如雨后春笋般涌现,“Write once, run anywhere”再也不是空话。这些跨平台技术方案的切入点是在 Web 侧 DSL、virtualDom 等方面的优化,以及 Native 侧 Runtime 的应用与封装,但两端通讯的核心,依然是 JavascriptCore。
除了对跨平台技术的积极探索,国内开发者对热修复技术也产生了极大的热情,一样做为 Native 和 Web 的交叉点,JavascriptCore 依然承担着整个技术结构中的通讯任务。
对于国内的 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 通讯只是整个框架中的一个技术点,更多的实现原理和细节因为篇幅的关系暂且不做介绍。
随着 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 仅仅是做为整个体系运转中的一个小小的部分,而整个跨平台的技术方案就须要另开多个篇幅进行介绍了。
随着 Web 技术的不断升级以及 App 动态性业务需求的增多,愈来愈多的 Web 页面加入到了 iOS App 当中,与之对应的,首屏展现速度体验这个相当重要的领域,也成为了移动客户端中 Web 业务最重要的优化方向。
对于单纯的 Web 页面来讲,业界早已有了合理的优化方向以及成熟的优化方案,而对于移动客户端中的 Web 来讲,开发者在进行单一的 Web 优化时,还能够经过优化 Web 容器以及 Web 页面中数据加载方式等多个途径作出优化。
因此对于 iOS 开发中的优化来讲,就是经过 Native 和 Web 两个维度的优化关键渲染路径,保证 WebView 优先渲染完毕。由此咱们梳理了常规 Web 页面总体的加载顺序,从中找出关键渲染路径,继而逐个分析、优化。
对于 Web 的通用优化方案,通常来讲在网络层面,能够经过 DNS 和 CDN 技术减小网络延迟、经过各类 HTTP 缓存技术减小网络请求次数、经过资源压缩和合并减小请求内容等。在渲染层面能够经过精简和优化业务代码、按需加载、防止阻塞、调整加载顺序优化等等。对于这个老生常谈的问题,业内已经有十分红熟和完整的总结,好比能够参考《Best Practices for Speeding Up Your Web Site》。
脱离较为通用的优化,在对代码侵入宽容度较高的场景中,开发者对 Web 优化有着更为激进的作法。例如在 VasSonic 中,除了 Web 容器复用、数据模板分离、预拉取和通用的优化方式外,还经过自定义 VasSonic 标签将 HTML 页面进行划分,分段进行缓存控制,以达到更高的优化效果。
WKWebView 虽然 JIT 大幅优化了 JS 的执行速度,可是单纯的加载渲染 HTML,WKWebView 比 UIWebView 慢了不少。根据渲染的不一样阶段分别对耗时进行测试,同时对比 UIWebView,咱们发现 WKWebView 在初始化及渲染开始前的耗时较多。
针对这种状况,业界主流的作法就是复用 & 预热。预热就是在 App 启动时建立一个 WKWebView,使其内部部分逻辑预热以提高加载速度。而复用又分为两种,较为复杂的是处理边界条件以达到真正的复用,还有一种较为取巧的办法就是常驻一个空 WKWebView 在内存。
HybridPageKit 提供了易于集成的完整 WKWebView 重用机制实现,开发者能够无需关注复用细节,无缝地体验更为高效的 WKWebView。
因为 Web 页面内请求流程不可控以及网络环境的影响,对于 Web 的加载来讲,网络请求一直是优化的重点。开发者较为经常使用的作法是使用 Native 并行代理数据请求,替代 Web 内核的资源加载。在客户端初始化页面的同时,并行开始网络请求数据;当 Web 页面渲染时向 Native 获取其代理请求的数据。
而将并行加载和预加载作到极致的优化,就是离线包的使用。将经常使用的须要下载资源(HTML 模板、JS 文件、CSS 文件与占位图片)打包,App 选择合适的时机所有下载到本地,当 Web 页面渲染时向 Native 获取其数据。
经过离线包的使用,Web 页面能够并行(提早)加载页面资源,同时摆脱了网络的影响,提升了页面的加载速度和成功率。固然离线包做为资源动态更新的一个方式,合理的下载时机、增量更新、加密和校验等方面都是须要进行设计和思考的方向,后文会简单介绍。
当并行请求资源,客户端代理数据请求的技术方案逐渐成熟时,因为 WKWebView 的限制,开发者不得不面对业务调整和适配。其中保留原有代理逻辑、采用 LocalServer 的方式最为广泛。可是因为 WKWebView 的进程间通讯、LocalServer Socket 创建与链接、资源的重复编解码都影响了代理请求的效率。
因此对于一些资讯类 App,一般采用 Dom 节点占位、Native 渲染实现的方式进行优化,如图片、地图、音视频等模块。这样不但能减小通讯和请求的创建、提供更加友好的交互、也能并行地进行 View 的渲染和处理,同时减小 Web 页面的业务逻辑。
HybridPageKit 中就提供封装好的功能框架,开发者能够简单的替换 Dom 节点为 NativeView。
从 App 的维度上看,一个 Web 页面从入口点击到渲染完成,或多或少都会有 Native 的业务逻辑并行执行。因此这个角度的优化关键渲染路径,就是优先保证 WebView 以及其它在首屏直接展现的 Native 模块优先渲染,因此承载 Web 页面的 Native 容器,能够根据业务逻辑的优先级,在保证 WebView 模块展现以后,选择合适的时机进行数据加载、视图渲染等。这样就能保证在 Native 的维度上,关键路径优先渲染。
总体上对于客户端来讲,咱们能够从 Native 维度(容器和数据加载)以及 Web 维度两个方向提高加载速度,按照页面的加载流程,总体的优化方向以下:
为了并行加载数据以及并行处理复杂的展现逻辑,对于非直出类型的 Web 页面,绝大部分 App 都采用数据和模板分离下发的方式。而这样的技术架构,致使在客户端内须要增长替换对应 DSL 的模板标签,造成最终的 HTML 业务逻辑。简单的字符串替换逻辑不但低效,还没法作到合理的组件化管理,以及组件合理地与 Native 交互,而模板引擎相关技术会使这种逻辑和表现分离的业务场景实现得更加简洁和优雅。
基于模板引擎与数据分离,客户端能够根据数据并行建立子业务模块,同时在子业务模块中处理和 Native 交互的部分,如图片裁剪适配、点击跳转等,生成 HTML 代码片断,以后基于模板进行替换生成完整的页面。这样不但减小了大量的字符串替换逻辑,同时业务也获得了合理拆分。
模板引擎的本质就是字符串的解析和替换拼接,在 Web 端不一样的使用场景有不少不一样语法的引擎类型,而在客户端较为流行的,有使用较为复杂的 MGTemplateEngine,它相似于 Smarty,支持部分模板逻辑。也有基于 mustache,Logic-less 的 GRMustache 可供选择。
不管是离线包、本地注入的 JS、CSS 文件,仍是本地化 Web 中的默认图片,目的都是经过提早下载,替换网络请求为本地读取来优化 Web 的加载体验和成功率,而对于这些资源的管理,开发者须要从下载与更新,以及 Web 中的访问这两个方面进行设计优化。
下载与重试:对于资源或是离线包的下载,选择合适的时机、失败重载时机、失败重载次数都要根据业务灵活调整。一般为了增长成功率和及时更新,在冷启动、先后台切换、关键的操做节点,或者采用定时轮循的方式,都须要进行资源版本号或 MD5 的判断,用以触发下载逻辑。固然对于服务端来讲,合理的灰度控制,也是保证业务稳定的重要途径。
签名校验:对于动态下载的资源,咱们都须要将原文件的签名进行校验,防止在传输过程当中被篡改。对于单项加密的办法就是双端对数据进行 MD5 的加密,以后客户端校验 MD5 是否符合预期;而双向加密能够采用 DES 等加密算法,客户端使用公钥对资源验证使用。
增量更新:为了减小资源和离线包的重复下载,业内大部分使用离线包的场景都采用了增量更新的方式。即客户端在触发请求资源时,带上本地已存在资源的标示,服务端根据标示和最新资源作对比,以后只提供新增或修改的 Patch 供客户端下载。
在完成资源的下载与更新后,如何将 Web 请求重定向到本地,大部分 App 都依赖于 NSURLProtocol。上文提到在 WKWebView 中虽然可使用私有函数实现(或者 iOS11+ 提供的系统函数),可是仍然有许多问题。
目前业界一部分 App,都采用了集成 LocalServer 的方式,接管部分 Web 请求,从而达到访问本地资源的目的。同时集成了 LocalServer,经过将本地资源封装成 Response,利用 HTTP 的缓存技术,进一步的优化了读取的时间和性能,实现层次化的缓存结构。而使用了本地资源的 HTTP 缓存,就须要考虑缓存的控制和过时时间,一般能够经过在 URL 上增长本地文件的修改时间、或本地文件的 MD5 来确保缓存的有效性。
排除 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 的设置和业务处理。
随着 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。