- Hybrid作为古老的跨端解决方案,在很多业务中依旧有很强的生命力。在iOS13这样的大背景下,WKWebview已经成为我们Hybrid方案中官方指定Webview组件;
- 从UIWebview切换到WKWebview,遇到一些WKWebview上一些坑,下面简记之。
一、WKWebivew调试
1、利用Safari调试
- 手机端开启Web 检查器:
设置 -> 通用 -> Safari -> 高级 -> Web 检查器
- Mac端Safari显示开发菜单:Safari 浏览器默认没有显示“开发”菜单,需要通过:
Safari 浏览器 -> 偏好设置 -> 高级 -> 勾选在菜单中显示“开发”设置
。 - 设置完后,启动 APP ,加载 WKWebView 后即可看到 H5页面。这时即可通过Safari中断点进行调试,可以查看当前的 HTML 代码,JS 代码,网络情况等。
2、eruda工具移动端调试神器
- 目前,前端同学开发阶段,会在本地开发环境中注入调试工具vconsole或eruda;但是可以在WKWebview中内置eruda工具,不需要依赖前端开发环境中的调试工具。
- 在页面加载完成后,执行JS代码,植入eruda工具
NSString *JSCode = @"(function() {var script = document.createElement('script');script.src = \'https://cdn.jsdelivr.net/npm/eruda\';document.body.appendChild(script);})();";
[self.webView evaluateJavaScript:JSCode completionHandler:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.webView evaluateJavaScript:@"eruda.init();" completionHandler:nil];
[self.webView evaluateJavaScript:@"eruda.get(\"snippets\").run(\"Load Dom Plugin\")" completionHandler:nil];
});
复制代码
二、iPhone X上适配H5相关
1、背景
- iPhone X 由于有刘海儿和底部Home Indicator上滑指示条的存在,有了安全区域(safeArea)的概念;
- 安全区域(safeArea)是指:当竖屏的时候,安全区的顶端始于屏幕顶端
44pt
处,而下端距离屏幕底端34pt
长;当横屏时,有刘海的一侧留出44pt
,无刘海的一侧34pt
- 为了让前端更好得定制H5页面,在所有设备中,包括iPhone X,Webview都是全屏展示,然后给前端足够的施展空间。
2、viewport-fit属性
viewport-fit
和safe-area-inset-*
是 iOS 11 新增的内容,其中viewport-fit
用于设置网页在可视窗口的布局方式,属性值可以是:
contain: The viewport should fully contain the web content. 可视窗口完全包含网页内容
cover: The web content should fully cover the viewport. 网页内容完全覆盖可视窗口
auto: The default value, contain
复制代码
viewport-fit=auto
即为viewport-fit=contain
,在 iPhone X 中,相当于网页内容展示在 Safe area内,也就是说:横屏显示时会出现白边,设置viewport-fit=cover
即可解决问题。
3、WKWebview适配 viewport-fit=cover
-
开发中发现,即使设置了
viewport-fit=cover
,但是H5页面在WKWebview也无法全屏展示(在UIWebview是可以的;虽然WKWebview是全屏,但是负责渲染的WKContentView却在全安全区域内), -
iOS11开始,UIScrollview新增加了一个属性contentInsetAdjustmentBehavior用来配置adjustedContentInset的行为,
contentInsetAdjustmentBehavior
默认是UIScrollViewContentInsetAdjustmentAutomatic
,是计算了内边距的;具体取值如下:typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) { UIScrollViewContentInsetAdjustmentAutomatic, // 自动计算内边距 UIScrollViewContentInsetAdjustmentScrollableAxes, // 自动计算内边距 UIScrollViewContentInsetAdjustmentNever, // 不计算内边距 UIScrollViewContentInsetAdjustmentAlways, // 根据safeAreaInsets 计算内边距 } API_AVAILABLE(ios(11.0),tvos(11.0)); 复制代码
-
WKWebview使得
viewport-fit=cover
生效的技巧:禁止计算内边距,代码设置如下://禁止WKWebview计算内边距,使WKWebview和WKContentView同样的展示区域 if (@available(iOS 11.0, *)) { _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } 复制代码
4、constant 函数
- iOS11 新增的CSS函数,用于设定安全区域与边界的距离,适配iPhonex时候可用,如下:
constant(safe-area-inset-left)
:安全区域距离左边界距离constant(safe-area-inset-right)
:安全区域距离右边界距离constant(safe-area-inset-top)
:安全区域距离顶部边界距离constant(safe-area-inset-bottom)
:安全区域距离底部边界距离
- 需要注意的事情,必须设置
viewport-fit=cover
,constant
函数才会生效。
三、拦截请求
1、支持NSURLProtocol 拦截
- 离线包方案关键之一:需要拦截请求,并返回本地资源;使用UIWebview时候,因为能通过NSURLProtocol可以拦截UIWebView的网络请求,问题不大。
- WKWebview使用离线包方案,遇到最大问题:在WKWebView上无法直接利用NSURLProtocol拦截请求;这是因为WKWebview在独立的进程(App进程之外)中执行网络请求,请求数据不经过App进程。
- WKWebview上的解决办法:使用私有API解决,iOS 11 之前使用
[WKBrowsingContextController registerSchemeForCustomProtocol:schema]
来注册http/https,iOS之后可以通过 hook+(BOOL)handlesURLScheme
方式注册http/https; - 但是一旦注册http(s) scheme了,网络请求将从
Network Process
发送到App Process
,然后被NSURLProtocol 拦截网络请求。Network Process
将请求encode成一个Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode的时候HTTPBody和HTTPBodyStream这两个字段被丢弃掉了
2、WKURLSchemeHandler使用
-
iOS 11 中,Apple 为 WebKit framework 增加了
WKURLSchemeHandler
协议,用于加载自定义 URL Scheme。当WebKit
遇到无法识别的 URL时,会调用WKURLSchemeHandler
协议。该协议包括以下两个必须实现的方法@protocol WKURLSchemeHandler <NSObject> //开始加载特定资源时调用 - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask; //停止载特定资源时调用 - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask; @end 复制代码
-
使用
WKURLSchemeHandler
协议处理完任务后,调用WKURLSchemeTask
协议内方法加载资源。WKURLSchemeTask
协议属性和方法如下:@protocol WKURLSchemeTask <NSObject> @property (nonatomic, readonly, copy) NSURLRequest *request; //设置当前任务的response。每个 task 至少调用一次该方法。如果尝试在任务终止或完成后调用该方法,则会抛出异常。 - (void)didReceiveResponse:(NSURLResponse *)response; //设置接收到的数据。当接收到任务最后的 response 后,使用该方法发送数据。每次调用该方法时,新数据会拼接到先前收到的数据中。如果尝试在发送 response 前,或任务完成、终止后调用该方法,则会引发异常。 - (void)didReceiveData:(NSData *)data; //将任务标记为成功完成。如果尝试在发送 response 前,或将已完成、终止的任务标记为完成,则会引发异常。 - (void)didFinish; //将任务标记为失败。如果尝试将已完成、失败,终止的任务标记为失败,则会引发异常。 - (void)didFailWithError:(NSError *)error; 复制代码
3、离线资源更新能力
- 很多项目中,为了优化Webview加载H5效果,使用了离线包方案,不仅需要拦截请求的能力,还需要更新离线资源的能力;比较有意思的是:ReactNative、Weex和小程序等跨端方案也需要依赖离线资源更新能力。
- 基于此,很多公司打造了离线资源打包、diff计算、动态下发,全量更新和增量更新等能力。以帮助更好地支持端上离线资源更新能力。
四、WKWebview中OC和JS通信
1、Message Handler
机制
-
iOS 2引入UIWebview,iOS7引入
JavaScriptCore
框架,它提供了 JS 代码与原生代码交互的能力;至此iOS7之后,UIWebview可以通过KVC方式获取JSContext; 但是iOS 8引入的WKWebview
由于独立在App进程之外,不能获得JSContext,不能通过JSContext实现 JS 代码与原生代码通信。(iOS13开始,不再支持UIWebview) -
WKWebview提供了新的 JS 代码 和 原生代码通信的方式,这就是
Message Handler
这种机制,当JS执行window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
时,OC端被添加的ScriptMessageHandler
就会执行实现的WKScriptMessageHandler
协议中的方法@protocol WKScriptMessageHandler <NSObject> @required - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message; @end 复制代码
2、传统通信方式--URL拦截方式
- 基于URL拦截方式,实现JS和Native的交互,iOS非常经典的实现有: WebViewJavascriptBridge, 在很多业务中,选择URL拦截方式也是个不错的选择;
五、WebView性能优化总结
1、加载性能优化思路
- 节省Webview初始化时间:提前初始化WebView,or 复用Webview对象
- 预先加载资源:
离线包方案
orlink prefetching方案
- 节约资源请求时间: DNS缓存、静态资源存放在CDN上
- H5页面优化:CSS、JavaScript、HTML优化等
2、禁止WKWebview中长按弹出UIMenuController
-
在WebView中,长按文字会使得WebView默认开始选择文字;长按链接会弹出提示是否在新页面打开。
-
解决方法:可以通过给body增加CSS来禁止这些默认规则。
// 禁止选择CSS NSString *css = @"body{-webkit-user-select:none;-webkit-user-drag:none;}"; // CSS选中样式取消 NSMutableString *javascript = [NSMutableString string]; [javascript appendString:@"var style = document.createElement('style');"]; [javascript appendString:@"style.type = 'text/css';"]; [javascript appendFormat:@"var cssContent = document.createTextNode('%@');", css]; [javascript appendString:@"style.appendChild(cssContent);"]; [javascript appendString:@"document.body.appendChild(style);"]; // javascript注入 WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:javascript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; WKUserContentController *userContentController = [[WKUserContentController alloc] init]; [userContentController addUserScript:noneSelectScript]; WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; configuration.userContentController = userContentController; // WKWebView 初始化 WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration]; //... 复制代码
3、点击延迟优化
-
点击延迟的原因:早期苹果为了判断移动端上的双击缩放事件而加的,在
touchend
和click
事件之间加300-350ms
的延迟,来判断用户到底是点击还是双击。 -
优化方案1:使用
fastclick
库,其原理是:在检测到touchend
事件时,通过DOM自定义事件立即触发模拟一个click事件,并把浏览器在300ms
之后的click事件阻止掉; -
优化方案2:禁用缩放,
WKWebView
上能解决延迟问题【不太适用于UIWebView
,但是在WKWebview上非常推荐】<meta name="viewport" content="user-scalable=no" /> 复制代码
4、禁止Webview放大和缩小
-
方案1:实现
UIScrollViewDelegate
的viewForZoomingInScrollView:
的办法- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{ return nil; } 复制代码
-
方案2:HTML中的mata标签加入
user-scalable = no
,若是原生js交互的话可采用注入js的方法更改meta。NSString *injectionJSString = @"var metaScript = document.createElement('meta');" "metaScript.name = 'viewport';" "metaScript.content=\"width=device-width, initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0, user-scalable=no\";" "document.head.appendChild(metaScript);"; WKUserScript *userScript = [[WKUserScript alloc] initWithSource:injectionJSString injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; WKUserContentController *userContentController = [[WKUserContentController alloc] init]; [wkuController addUserScript:userScript]; WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init]; webViewConfig.userContentController = userContentController; self.webView = [[WKWebView alloc] initWithFrame:webviewFrame configuration:webViewConfig]; 复制代码
5、自动弹出键盘
-
H5页面 focus 获得焦点状态下弹出键盘,UIWebView 中
keyboardDisplayRequiresUserAction
设置为 NO 就可以;(默认为YES,必须用户点击才可以弹出键盘) -
WKWebview没有此属性,需要通过hook私有API实现,代码如下:
- (void)allowDisplayingKeyboardWithoutUserAction { Class class = NSClassFromString(@"WKContentView"); char * methodSignature = "_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"; if (@available(iOS 11.3, *)) { methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:"; } else if (@available(iOS 12.2, *)) { methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"; } if (@available(iOS 11.3, *)) { SEL selector = sel_getUid(methodSignature); Method method = class_getInstanceMethod(class, selector); IMP original = method_getImplementation(method); IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); }); method_setImplementation(method, override); } else { SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:"); Method method = class_getInstanceMethod(class, selector); IMP original = method_getImplementation(method); IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) { ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); }); method_setImplementation(method, override); } } 复制代码
6、WKWebview白屏优化
-
WKWebView是一个多进程组件,Network Loading以及UI Rendering在其它进程中执行,当WKWebView总体的内存占用比较大时,WebContent Process会crash,从而出现白屏现象。
-
解决办法1:KVO监听URL, 当URL为nil,重新reload
-
解决办法2:在进程被终止回调中,重新reload
// 此方法适用iOS9.0以上 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){ //reload } 复制代码
六、Webview安全
1、WebView被运营商劫持、注入问题
-
因为WebView加载的页面代码是从Server获取的,这些代码将会很容易被中间环节所窃取或者修改,其中最主要的问题是运营商劫持问题。主要表现为:页面被注入广告、页面被重定向等。
-
目前比较主流解决办法:使用HTTPS可以防止页面被劫持或者注入。
2、App内WebView打开第三方App能力和收
-
放:本质上,WKWebView限制H5页面打开三方 APP 的能力,但是我们可以绕开这个限制,打开第三方App。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{ // 判断URL 中的 Scheme 或 host,然后通过 [[UIApplication sharedApplication] openURL:] 方法打开。 } 复制代码
-
收:泛滥H5页面打开App能力比如必然不是不好的,可以设置白名单机制,只允许打开友商的App。