iOS - 康康就行了 之 WKWebView支持Webp文件展示

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文件的下载,我们平时熟悉的SDWebImageYYImage都对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];
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342