WkWebView和UIWebView及底层实现

背景:

UIWebView是在iOS2推出的,在iOS8之后,使用WKWebView来取代UIWebView。因为UIWebView加载速度慢,占用内存多,优化困难,所以iOS8以后,苹果推出了新框架Webkit,提供了组件WKWebView。

注意:WKWebView在ios8系统上,JS进行post请求有数据不正确的问题,所以从ios9开始使用WKWebView(静等ios8以下系统被淘汰)

##UIWebView

一、初始化与三种加载方式

UIWebView继承于UIView,因此,其初始化方法和一般的view一样,通过alloc和init进行初始化,其加载数据的方式有三种:

//1,这是加载网页最常用的一种方式,通过一个网页URL来进行加载,这个URL可以是远程的也可以是本地的,例如我加载百度的主页
- (void)loadRequest:(NSURLRequest *)request;

//2,这个方法需要将html文件读取为字符串,其中baseURL是我们自己设置的一个路径,用于寻找html文件中引用的图片等素材。
- (void)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;

//3,这个方式使用的比较少,但也更加自由,其中data是文件数据,MIMEType是文件类型,textEncodingName是编码类型,baseURL是素材资源路径。
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

##二、一些常用的属性和变量

//设置webView的代理
@property (nonatomic, assign) id  delegate;
//内置的scrollView
@property(nonatomic, readonly, retain) UIScrollView *scrollView;
//URL请求
@property(nonatomic, readonly, retain) NSURLRequest *request;

//重新加载数据
- (void)reload;
//停止加载数据
- (void)stopLoading;
//返回上一级
- (void)goBack;
//跳转下一级
- (void)goForward;
//获取能否返回上一级
@property(nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
//获取能否跳转下一级
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
//获取是否正在加载数据
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
//通过javaScript操作web数据
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;       
//设置是否缩放到适合屏幕大小
@property (nonatomic) BOOL scalesPageToFit;

//设置某些数据变为链接形式,这个枚举可以设置如电话号,地址,邮箱等转化为链接
@property (nonatomic) UIDataDetectorTypes dataDetectorTypes NS_AVAILABLE_IOS(3_0);

//设置是否使用内联播放器播放视频
@property (nonatomic) BOOL allowsInlineMediaPlayback NS_AVAILABLE_IOS(4_0);

//设置视频是否自动播放
@property (nonatomic) BOOL mediaPlaybackRequiresUserAction NS_AVAILABLE_IOS(4_0);

//设置音频播放是否支持ari play功能
@property (nonatomic) BOOL mediaPlaybackAllowsAirPlay NS_AVAILABLE_IOS(5_0);

//设置是否将数据加载如内存后渲染界面
@property (nonatomic) BOOL suppressesIncrementalRendering NS_AVAILABLE_IOS(6_0);

//设置用户交互模式
@property (nonatomic) BOOL keyboardDisplayRequiresUserAction NS_AVAILABLE_IOS(6_0);               

##三、iOS7中的一些新特性

//这个属性用来设置一种模式,当网页的大小超出view时,将网页以翻页的效果展示,枚举如下
@property (nonatomic) UIWebPaginationMode paginationMode NS_AVAILABLE_IOS(7_0);

typedef NS_ENUM(NSInteger, UIWebPaginationMode) {
    UIWebPaginationModeUnpaginated,//不使用翻页效果
    UIWebPaginationModeLeftToRight,//将网页超出部分分页,从左向右进行翻页
    UIWebPaginationModeTopToBottom,//将网页超出部分分页,从上向下进行翻页
    UIWebPaginationModeBottomToTop,//将网页超出部分分页,从下向上进行翻页
    UIWebPaginationModeRightToLeft//将网页超出部分分页,从右向左进行翻页
};
//设置每一页的长度
@property (nonatomic) CGFloat pageLength NS_AVAILABLE_IOS(7_0);

//设置每一页的间距
@property (nonatomic) CGFloat gapBetweenPages NS_AVAILABLE_IOS(7_0);
 
//获取分页数
@property (nonatomic, readonly) NSUInteger pageCount NS_AVAILABLE_IOS(7_0);

四、代理方法

UIWebView有一个UIWebViewDelegate

//网页加载之前会调用此方法(这个方法在每次进行加载网页的时候都会执行,可以监听每一次webView发送的请求)
-(BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType
{
//retrun YES 表示正常加载网页
//返回NO 将停止网页加载
return YES;
}

//开始加载网页调用此方法
-(void)webViewDidStartLoad:(UIWebView *)webView
{
}

//网页加载完成调用此方法
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
}

//网页加载失败 调用此方法
-(void)webView:(UIWebView )webView didFailLoadWithError:(NSError )error
{
}

JS交互

#####oc调用js:

[UIWebview stringByEvaluatingJavaScriptFromString:javaScriptString];

######js调用oc:
拦截url scheme(自定义的url)

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL * url = [request URL];
    if ([[url scheme] isEqualToString:@"firstclick"])
    //做参数的拼接
}

而在iOS 7之后,apple添加了一个新的库JavaScriptCore,用来做js交互。

//首先导入JavaScriptCore库, 然后在OC中获取JS的上下文。
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

JSContext:js执行环境,包含了js执行时所需要的所有函数和对象;
js执行时,会在执行环境搜索需要的函数然后执行,或者保存传入的变量或函数;

context[@"share"] = ^() {
        NSLog(@"+++++++Begin Log+++++++");
        NSArray *args = [JSContext currentArguments];
        NSMutableString *strM = [NSMutableString string];
        for (JSValue *jsVal in args) {
            NSLog(@"%@", jsVal.toString);
            [strM appendString:jsVal.toString];
        }

使用JSContext 的方法-evaluateScript,可以实现 OC 调用 JS 方法

// 法一 
- (void)transferJS { 
    JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    NSString *textJS = @"showAlert('这里是JS中alert弹出的message')";
    [context evaluateScript:textJS]; 
}
 // 法二 
- (void)transferJS { 
     NSString *textJS = @"showAlert('这里是JS中alert弹出的message')";
     [[JSContext currentContext] evaluateScript:textJS]; 
}

#WKWebVIew

官方WKWebView优势

1,更多的支持HTML5的特性
2,官方宣称的高达60fps的滚动刷新率以及内置手势
3,将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。
4,占用更少的内存
WkWebView和UIWebView及底层实现_第1张图片

因此,使用WkWebview替换UIWebView还是很有必要的。

##属性和方法

// webview 配置,具体看下面
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
 
// 导航代理 
@property (nullable, nonatomic, weak) id navigationDelegate;
 
// 用户交互代理
@property (nullable, nonatomic, weak) id  UIDelegate;
 
// 页面前进、后退列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;
 
// 默认构造器
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
 
// 已不再使用
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
 
// 与UIWebView一样的加载请求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
 
// 加载URL
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0);
 
// 直接加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
 
// 直接加载data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString*)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);
 
// 前进或者后退到某一页面
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem*)item;
 
// 页面的标题,这昆支持KVO的
@property (nullable, nonatomic, readonly, copy) NSString *title;
 
// 当前请求的URL,它是支持KVO的
@property (nullable, nonatomic, readonly, copy) NSURL *URL;
 
// 标识当前是否正在加载内容中,它是支持KVO的
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
 
// 当前加载的进度,范围为[0, 1]
@property (nonatomic, readonly) double estimatedProgress;
 
// 标识页面中的所有资源是否通过安全加密连接来加载,它是支持KVO的
@property (nonatomic, readonly) BOOL hasOnlySecureContent;
 
// 当前导航的证书链,支持KVO
@property (nonatomic, readonly, copy) NSArray *certificateChainNS_AVAILABLE(10_11, 9_0);
 
// 是否可以招待goback操作,它是支持KVO的
@property (nonatomic, readonly) BOOL canGoBack;
 
// 是否可以执行gofarward操作,支持KVO
@property (nonatomic, readonly) BOOL canGoForward;
 
// 返回上一页面,如果不能返回,则什么也不干
- (nullable WKNavigation *)goBack;
 
// 进入下一页面,如果不能前进,则什么也不干
- (nullable WKNavigation *)goForward;
 
// 重新载入页面
- (nullable WKNavigation *)reload;
 
// 重新从原始URL载入
- (nullable WKNavigation *)reloadFromOrigin;
 
// 停止加载数据
- (void)stopLoading;
 
// 执行JS代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
 
// 标识是否支持左、右swipe手势是否可以前进、后退
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
 
// 自定义user agent,如果没有则为nil
@property (nullable, nonatomic, copy) NSString *customUserAgentNS_AVAILABLE(10_11, 9_0);
 
// 在iOS上默认为NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE(10_11, 9_0);
 
#if TARGET_OS_IPHONE
/*! @abstract The scroll view associated with the web view.
*/
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
#endif
 
#if !TARGET_OS_IPHONE
// 标识是否支持放大手势,默认为NO
@property (nonatomic) BOOL allowsMagnification;
 
// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;
 
// 根据设置的缩放因子来缩放页面,并居中显示结果在指定的点
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;
 

二、简单的使用

//简单使用,直接加载url地址
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];

//自定义配置
//再WKWebView里面注册供JS调用的方法,是通过WKUserContentController类下面的方法:

// 创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];
// 将UserConttentController设置到配置文件
config.userContentController = userContent;
// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"https://developer.apple.com/reference/webkit"];
 NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
 [self.view addSubview:webView];
//实现WKScriptMessageHandler协议方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 判断是否是调用原生的
   if ([@"NativeMethod" isEqualToString:message.name]) {
// 判断message的内容,然后做相应的操作
if ([@"close" isEqualToString:message.body]) {
}

##三、WKWebView有两个delegate,WKUIDelegate和 WKNavigationDelegate

WKNavigationDelegate主要处理一些跳转、加载处理操作,WKUIDelegate主要处理JS脚本,确认框,警告框等。因此WKNavigationDelegate更加常用。

#pragma mark - WKNavigationDelegate
// 页面开始加载时调用
- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation
{
}

// 当内容开始返回时调用
- (void)webView:(WKWebView*)webView didCommitNavigation:(WKNavigation*)navigation
{
}

// 页面加载完成之后调用
- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
{
}

// 页面加载失败时调用
- (void)webView:(WKWebView*)webView didFailProvisionalNavigation:(WKNavigation*)navigation
{
}

// 接收到服务器跳转请求之后调用
- (void)webView:(WKWebView*)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation
{
}

// 在收到响应后,决定是否跳转
- (void)webView:(WKWebView*)webView decidePolicyForNavigationResponse:(WKNavigationResponse*)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler
{
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
      //允许跳转
      decisionHandler(WKNavigationResponsePolicyAllow);
      //不允许跳转
     //decisionHandler(WKNavigationResponsePolicyCancel);
}
// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler
{
NSLog(@"%@",navigationAction.request.URL.absoluteString);   
     //允许跳转
     decisionHandler(WKNavigationActionPolicyAllow);
     //不允许跳转
     //decisionHandler(WKNavigationActionPolicyCancel);
}

#pragma mark - WKUIDelegate
// 创建一个新的WebView
- (WKWebView*)webView:(WKWebView*)webView createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration forNavigationAction:(WKNavigationAction*)navigationAction windowFeatures:(WKWindowFeatures*)windowFeatures
{
     return [[WKWebView alloc] init];
}

// 输入框
- (void)webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt defaultText:(nullableNSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void(^)(NSString* __nullableresult))completionHandler
{
     completionHandler(@"http");
}

// 确认框
- (void)webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void(^)(BOOLresult))completionHandler
{
      completionHandler(YES);
}

// 警告框
- (void)webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void(^)(void))completionHandler
{
      NSLog(@"%@",message); 
      completionHandler();
}

OC 调用 JS

[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@----%@",result, error);
    }];
}

JS调用 OC
拦截url scheme

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL *URL = navigationAction.request.URL;
    NSString *scheme = [URL scheme];
    if ([scheme isEqualToString:@"haleyaction"]) {
        [self handleCustomAction:URL];
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    //注意:必须要实现回调,否则会崩溃。
    decisionHandler(WKNavigationActionPolicyAllow);
}

使用WKWebView的时候,如果想要实现JS调用OC方法,除了拦截URL之外,还有一种简单的方式。那就是利用WKWebView的新特性MessageHandler来实现JS调用原生方法。
这里就要说WKWebViewConfiguration对象

//创建网页配置对象
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
 // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
config.allowsInlineMediaPlayback = YES;
//设置视频是否需要用户手动播放  设置为NO则会允许自动播放
config.requiresUserActionForMediaPlayback = YES;
//设置是否允许画中画技术 在特定设备上有效
config.allowsPictureInPictureMediaPlayback = YES;
//设置请求的User-Agent信息中应用程序名称 iOS9后可用
config.applicationNameForUserAgent = @"ChinaDailyForiPad";
2,WKPreferences创建设置对象
// 创建设置对象 
WKPreferences *preference = [[WKPreferences alloc]init];
//最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
preference.minimumFontSize = 0;
//设置是否支持javaScript 默认是支持的
preference.javaScriptEnabled = YES;
// 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
preference.javaScriptCanOpenWindowsAutomatically = YES;
config.preferences = preference;
3,WKUserContentController这个类主要用来做native与javascript的交互管理。
//这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];
//注册一个name为jsToOcNoPrams的js方法
[wkUController
addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcNoPrams"];
 [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcWithPrams"]; 
config.userContentController = wkUController;

注意:这种方法很可能照成循环引用,所以在适当的时机要移除。

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // 因此这里要记得移除handlers
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"ScanAction"];
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"Location"];
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"Share"];
}

遵守WKScriptMessageHandker协议,调用代理方法

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    // message.body  --  Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.
    if ([message.name isEqualToString:@"ScanAction"]) {
        [self scanAction];
    } else if ([message.name isEqualToString:@"Location"]) {
        [self getLocation];
    } else if ([message.name isEqualToString:@"Share"]) {
        [self shareWithParams:message.body];
    }
}

WKScriptMessage有两个关键属性name 和 body。
因为我们给每一个OC 方法取了一个name,那么我们就可以根据name 来区分执行不同的方法。body 中存着JS 要给OC 传的参数。

##JSBridge
概念:
JSBridge另一个叫法及大家熟知的Hybrid app技术,是指介于web-app、native-app这两者之间的app,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。谈到Hybrid App,JS与Native code的交互就是一个绕不开的话题,这时就需要“一座桥”来连接两端以实现双向通信。

Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通信原理会具有一些 Web 特性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ojK4hxpo-1593511240961)(https://upload-images.jianshu.io/upload_images/19877028-679b59635e6c3d12.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

引入概念:
JS上下文(Context):
JavaScript的运行不是像C++,Java等编译语言编译后直接在操作系统上运行,因为它是脚本语言,运行时必须要借助引擎来运行,所以它可以在封装了引擎的环境下运行,也就是浏览器。而当js运行时,它会有不同的运行环境。
全局级别的代码:Global Code – JavaScript代码开始的默认运行环境
函数级别的代码:Function Code – 代码执行在JavaScript函数中
Eval的代码:Eval Code – 使用eval()执行代码

每到调用执行一个函数时,引擎就会自动新建出一个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。在上述例子的,内部的函数可以访问到外部上下文中的声明的变量,反之则行不通。

当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。

JSBridge 的通信原理
在 JSBridge 的设计中,可以把前端看做 RPC(远程过程调用) 的客户端,把 Native 端看做 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通信调用(Native 与 JS 通信) 和句柄(内核对象的实际地址)解析调用。

JavaScript 调用 Native
JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME。
注入API
注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

对于 iOS 的 UIWebView,实例如下:

JSContext *context = [UIWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; context[@"postBridgeMessage"] = ^(NSArray *calls) { // Native 逻辑 };

前端调用方式:

window.postBridgeMessage(message);

对于 iOS 的 WKWebView 可以用以下方式:

[userCC addScriptMessageHandler:self name:@"nativeBridge"];

前端调用方式:

window.webkit.messageHandlers.nativeBridge.postMessage(message);

Native 调用 JavaScript
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)

//UIWebview
result = [UIWebview stringByEvaluatingJavaScriptFromString:javaScriptString];

WKWebView

[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];

拦截 URL SCHEME
URL SCHEME:URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的

Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。

JSBridge 接口实现

RPC 中有一个非常重要的环节是 句柄解析调用 ,这点在 JSBridge 中体现为 句柄与功能对应关系。同时,我们将句柄抽象为桥名(BridgeName),最终演化为 一个 BridgeName 对应一个 Native 功能或者一类 Native 消息。

那么消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?
是RPC(远程过程调用)的回调机制
当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。
由此可见,callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。

由此可见,callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。

##扩展实战:
此外,JSBridge还可以通过ajax的方式来拦截URL,在Native拦截达到调用的目的。通过自定义NSURLProtocol可以拦截到Ajax请求(native作为服务端,js作为客户端)

######这种方式在url不确定的情况下起到了关键的作用。

NSURLProtocol,是一个抽象类,正确的使用姿势是,创建NSURLProtocol的子类,通过registerClass:方法将其注册到URL Loading System中,系统会创建协议对象来处理相应的URL请求,我们可以通过NSURLProtocol提供的API接口,来达到存储和检索特定协议的请求数据的目的

1.新建类继承自NSURLProtocol,并注册。

self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    // 关键代码:注册自定义NSURLProtocol,拦截网络请求
    [NSURLProtocol registerClass:[CRURLProtocol class]];
    [self.view addSubview:_webView];
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"AjaxExample" ofType:@"html"] ]];
    [_webView loadRequest:request];

2,实现自定义NSURLProtocol,在startLoading方法拦截Ajax请求

- (void)startLoading {
    NSURL *url = [[self request] URL];
    // 拦截“http://__jsbridge__”请求
    if ([url.host isEqualToString:@"__jsbridge__"]) {
       // 处理JS调用Native
    }
}

3.JS发起Ajax请求,URL为提前约定的特殊值,例如:http://jsbridge。请求参数放在Request Body里。

function callNative(action, data) {
        var xhr = new window.XMLHttpRequest(),
        url = 'http://__jsbridge__';
        xhr.open('POST', url, false);
        xhr.send(JSON.stringify({
                    action: action,
                    data: data
                    }));
        return xhr.responseText;
}

4.Naive拦截到请求,获取参数,执行Native方法,最后通过Ajax的Response把结果返回给JS。

// 2. 从HTTPBody中取出调用参数
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:self.request.HTTPBody options:NSJSONReadingAllowFragments error:nil];
NSString *action = dic[@"action"];
NSString *data = dic[@"data"];
NSData *responseData;

// 3. 根据action转发到不同方法处理,param携带参数
if ([action isEqualToString:@"alertMessage"]) {
    responseData = [data dataUsingEncoding:NSUTF8StringEncoding];
} else {
    responseData = [@"Unknown action" dataUsingEncoding:NSUTF8StringEncoding];
}

// 4. 处理完成,将结果返回给js
[self sendResponseWithResponseCode:200 data:responseData mimeType:@"text/html"];
...

- (void)sendResponseWithResponseCode:(NSInteger)statusCode data:(NSData*)data mimeType:(NSString*)mimeType {
    NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[[self request] URL] statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : mimeType}];
    
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    if (data != nil) {
        [[self client] URLProtocol:self didLoadData:data];
    }
    [[self client] URLProtocolDidFinishLoading:self];
}

##遇到的问题
1,WKWebView 白屏问题
WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面,使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory)不见得比 UIWebView 少很多。
在 UIWebView 上当内存占用太大的时候,App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:
解决方案:

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用上面的回调函数,我们在该函数里执行[webView reload](这个时候 webView.URL 取值尚不为 nil)解决白屏问题。在一些高内存消耗的页面可能会频繁刷新当前页面,H5侧也要做相应的适配操作。

2、WKWebView loadRequest 问题
WKWebView 通过loadrequest方法加载Post请求会丢失请求体(body)中的内容,进而导致服务器拿不到body中的内容的问题的发生。这个问题的产生主要是因为WKWebView的网络请求的进程与APP不是同一个进程,所以网络请求的过程是这样的:
由APP所在的进程发起request,然后通过IPC通信(进程间通信)将请求的相关信息(请求头、请求行、请求体等)传递给webkit网络线进程接收包装,进行数据的HTTP请求,最终再进行IPC的通信回传给APP所在的进程的。这里如果发起的request请求是post请求的话,由于要进行IPC数据传递,传递的请求体body中根据系统调度,将其舍弃,最终在WKWebView网络进程接受的时候请求体body中的内容变成了空,导致此种情况下的服务器获取不到请求体,导致问题的产生。
解决问题:
1.将网络请求交由Js发起,绕开系统WKWebView的网络的进程请求达到正常请求的目的
2.改变POST请求的方法为GET方法(有风险,不一定服务器会接受GET方法)
3.将Post请求的请求body内容放入请求的Header中,并通过URLProtocol拦截自定义协议,在拦截中通过NSConnection进行重新请求(重新包装请求body),然后通过回调Client客户端来传递数据内容

3,WKWebView 页面样式问题
在 WKWebView 适配过程中,发现部分H5页面元素位置向下偏移或被拉伸变形,追踪后发现主要是H5页面高度值异常导致:
解决方案:
调整WKWebView布局方式,避免调整webView.scrollView.contentInset。实际上,即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值,这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话,可以通过下面方式让H5页面恢复正常显示:

/**设置contentInset值后通过调整webView.frame让页面恢复正常显示 *参考:http://km.oa.com/articles/show/277372 */
webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); 
webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

4,视频自动播放
WKWebView 需要通过WKWebViewConfiguration.mediaPlaybackRequiresUserAction设置是否允许自动播放,但一定要在 WKWebView 初始化之前设置,在 WKWebView 初始化之后设置无效。

##总结:
1,在ios9以下的系统使用UIWebView,ios9以上使用WKWebView。
2.两个webview都继承于UIVIew,加载方式都有三种,通过string加载本地,通过urlRequest进行加载和通过二进制文件进行加载。
3,交互中分为native调用js,和js调用native,native调用js通常使用拦截url,和注册js代码,而js调用oc使用native执行js代码的方式。
4,通过在引擎中存储id来实现JSBridge的双向通信。

参考文章
https://www.cnblogs.com/demodashi/p/9443213.html
https://www.jianshu.com/p/eff176e220e0
https://www.jianshu.com/p/524bc8699ac2
https://www.jianshu.com/p/78b931dfd614
https://www.jianshu.com/p/8126d1b37316

你可能感兴趣的:(WkWebView和UIWebView及底层实现)