Create by Kunming
写在最前面
血与泪的教训,这篇文章所涉及的方法翻车了。按照这个方法是可以实现WKWebView展示Webp文件,但致命的问题在于在一旦注册 http(s) scheme 后。由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了。这样就导致由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body 被清空!你没听错!!是清空。这个问题导致了我们短暂的需要把某些前端的POST请求短暂更改为GET。因此,本来WKWebView都是没有开放对NSURLProtocol的支持,强制让WKWebView支持可能会出现不可预料的问题。老实听一句劝,不要往坑里跳。
以下内容被证实过有坑
留个念想,但不要按以下方法实践。
背景
今天收到一个反馈,我们基于WKWebView开发的浏览没有办法展示包含Webp格式的网页。但是安卓端展示是没有问题的。这就问到了一个直击灵魂的问题”为什么安卓仔可以“。因此,特地研究一下为何WKWebView不能展示webp格式文件以及如何让WKWebView正常展示webp文件。
什么是webp
WebP格式,谷歌开发的一种旨在加快图片加载速度的图片格式(怪不得安卓仔可以支持,原来是同一个老爸生的)。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器宽带资源和数据空间。Facebook Ebay等知名网站已经开始测试并使用WebP格式。同样的图片质量但是占用空间小2/3,这无疑对开发者和设计来说都是福音。但很遗憾,这个格式iOS并不支持。
实现思路
正是因为iOS不支持WebP格式的图片,因此我们就得考虑通过其他方法来展示。通过拦截请求过滤出Webp文件的请求链接,将文件下载后通过重定向将下载下来的数据转换成PNG或JPEG格式然后交付给浏览器渲染。
如何拦截请求
首先拦截请求我们第一时间肯定想到的是通过NSURLProtocol。声明一个继承于NSURLProtocol的类,在该类中实现+ (BOOL)canInitWithRequest:(NSURLRequest*)request
进行拦截。这个方法是自定义NSURLProtocol的入口。如果在这个方法内返回YES,URL loading system
会把这个请求操作都交由这个自定义NSURLProtocol处理
NSString*const kWebPprefix =@".webp";
//该方法返回YES则表示 需要进行处理,返回NO,则 不做任何处理
+ (BOOL)canInitWithRequest:(NSURLRequest*)request {
//拦截.webp后缀文件
if([request.URL.absoluteString hasSuffix:kWebPprefix]) {
//如果是webp自定义协议,则不需要过滤
if([NSURLProtocol propertyForKey:@"URLProtocolHandleKey"inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
这个方法主要是用来返回格式化好的request。这个方法必须实现,不然会直接闪退。如果自己没有特殊需求的话,直接返回当前的request就好了。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
接下来需要重写- (void)startLoading
方法,顾名思义在自定义的NSURLProtocol
拿到了request的操作权之后,在这个方法里就是执行拦截请求后的操作了。
同理- (void)stopLoading
这个就是请求结束时的方法。
如何下载webP文件
其实有挺多第三方库实现了webP文件的下载,我们平时熟悉的SDWebImage
、YYImage
都对webP文件进行支持适配。以SDWebImage为例,SDWebImage 的子库SDWebImage/WebP
就是专门为支持WebP开发的。但值得注意的是,SDWebImage本身不会帮我们加入项目中,需要我们另外引入才能使用。
// 通过pod引入
pod'SDWebImage/WebP'
引入项目后结合上面拦截请求的流程,在自定义的NSURLProtocol
中声明一个@property (nonatomic , strong) id <SDWebImageOperation> downOperation;
属性管理webP文件的下载。
//该方法中对webp图片进行相应的处理
- (void)startLoading {
// 重定向请求
NSMutableURLRequest*mRequest = [[self request]mutableCopy];
self.downOperation= [[SDWebImageManager sharedManager]loadImageWithURL:[self request].URL options:0 progress:nil completed:^(UIImage*_Nullable image,NSData*_Nullable data,NSError*_Nullable error,SDImageCacheType cacheType,BOOL finished,NSURL*_Nullable imageURL) {
// 拦截webp, 将webp格式转为data数据进行重定向加载
// 通知 client 收到响应
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mRequest.URL MIMEType:@"image/jpeg" expectedContentLength:data.length textEncodingName:nil];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
// 通知 client 已经加载完数据
[self.client URLProtocol:self didLoadData:UIImagePNGRepresentation(image)];
// 通知 client 请求完成, 成功或者失败的处理
if(!error) {
//成功
[self.client URLProtocolDidFinishLoading:self];
}else{
//失败
[self.client URLProtocol:self didFailWithError:error];
}
}];
}
- (void)stopLoading {
// 终止任务
if ([self.downOperation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[self.downOperation cancel];
}
}
到这里NSURLProtocol部分就实现了。接下来就是在WKWebView初始化之后调用注册自定义NSURLProtocol的方法。
[NSURLProtocol registerClass:[LMWKWebPURLProtocol class]];
但我们跑代码运行,却发现+ (BOOL)canInitWithRequest:(NSURLRequest*)request
这个方法并没有走。这又是涉及另一个问题了。
WKWebView不支持 NSURLProtocol ?
在 UIWebView 时代,按照上面的方式注册一个自定义的 NSURLProtocol 是完全没有问题的。但在 WKWebView 中的请求却完全不遵从这一规则,网络上文章一般都解释说 WKWebView 的请求是在单独的进程里,所以不走 NSURLProtocol。经过zyl04401大神的文章得出的结论:WKWebView不走NSURLProtocol的原因,最后得出的结论是WebKit是支持NSURLProtocol的,只是WebKit还不够完成。
FOUNDATION_STATIC_INLINE Class ContextControllerClass() {
static Class cls;
if (!cls) {
// 这样的写法其实是避免因为直接使用 Class cls = NSClassFromString(@"WKBrowsingContextController"); 这样的写法,导致审核的时候被认为使用了私有api
cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
}
return cls;
}
FOUNDATION_STATIC_INLINE SEL RegisterSchemeSelector() {
return NSSelectorFromString(@"registerSchemeForCustomProtocol:");
}
FOUNDATION_STATIC_INLINE SEL UnregisterSchemeSelector() {
return NSSelectorFromString(@"unregisterSchemeForCustomProtocol:");
}
+ (void)wk_registerScheme:(NSString *)scheme {
Class cls = ContextControllerClass();
SEL sel = RegisterSchemeSelector();
if ([(id)cls respondsToSelector:sel]) {
// 放弃编辑器警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:scheme];
#pragma clang diagnostic pop
}
}
从 registerSchemeForCustomProtocol: 这个方法名来猜测,它的作用的应该是注册一个自定义的 scheme,这样对于 WebKit 进程的所有网络请求,都会先检查是否有匹配的 scheme,有的话再走主进程的 NSURLProtocol 这一套流程。
配套的,大神通过源码还扒出了注销的方法:
+ (void)wk_unregisterScheme:(NSString *)scheme {
Class cls = ContextControllerClass();
SEL sel = UnregisterSchemeSelector();
if ([(id)cls respondsToSelector:sel]) {
// 放弃编辑器警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:scheme];
#pragma clang diagnostic pop
}
}
加上这两个注册scheme的代码,整体的流程就可以跑通了。
最后
因为NSURLProtocol 是全局生效的。如果所有请求都走一遍这个拦截方法,这样的做法不太合理也比较耗性能。因此我们把scheme的注册实际写在了WKWebViewController的初始化和Dealloc里,确保只对WKWebView的生命周期内生效。
@implementation LMWebViewController
// MARK:- 注册URLProtocol 及scheme
- (void)registerIntercept
{
// LMWKWebPURLProtocol实现WebP图片拦截及重定向
[NSURLProtocol registerClass:[LMWKWebPURLProtocol class]];
// WKWebView不走 NSURLProtocol,得实现分类方法后才能拦截
[NSURLProtocol wk_registerScheme:@"http"];
[NSURLProtocol wk_registerScheme:@"https"];
}
// MARK:- 注消URLProtocol 及scheme
- (void)unregisterIntercept
{
[NSURLProtocol unregisterClass:[LMWKWebPURLProtocol class]];
[NSURLProtocol wk_unregisterScheme:@"http"];
[NSURLProtocol wk_unregisterScheme:@"https"];
}
- (void)viewDidLoad {
[super viewDidLoad];
// 注册URLProtocol 及scheme
[self registerIntercept];
// 初始化WKWebViewController的相关代码
……
}
- (void)dealloc
{
// 注销拦截相关的方法
[self unregisterIntercept];
}