UIWebView、WKWebView 支持 WebP 图片显示

移动端应用往往有大量的图片展示场景,图片的大小对企业至关重要。WebP 作为一种更高效的图片编码格式,平均大小比 PNG/JPG/ GIF / 动态 GIF 格式减少 70%(对比测试页面),且质量没有明显的差别,是其他图片格式极佳的替代者。

一、MagicWebViewWebP.framework 架构

主要文件说明
MagicWebViewWebPManager(引用文件)管理 MagicURLProtocol 的注册、销毁 MagicURLProtocol
MagicURLProtocol继承 NSURLProtocol 用于截获 url
NSURLProtocol+MagicWebView扩展 NSURLProtocol 用于注册、销毁 Scheme
依赖说明
SDWebImage使用了 webP 相关部分模块
libwebpwebP 依赖文件

二、说明

MagicURLProtocol

MagicURLProtocol 继承于 NSURLProtocol,实现了对 webp 图片网络请求进行拦截,将拦截的请求使用 NSURLConnection(也可以使用 NSURLSession) 加载数据,然后利用 SDWebImage 中 UIImage+WebP 提供加载 webp 图片的能力,并将加载好的 UIImage 转化为 NSData 返回,实现 webp 图片的加载。

1、NSURLProtocol 主要步骤?

注册—>拦截—>转发—>回调—>结束

2、继承 NSURLProtocol 必须实现的方法?

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;

3、什么是 NSURLProtocol?

NSURLProtocol 作为 URL Loading System 中的一个独立部分存在,能够拦截所有的 URL Loading System 发出的网络请求,拦截之后便可根据需要做各种自定义处理,是 iOS 网络层实现 AOP (面向切面编程) 的终极利器,所以功能和影响力都是非常强大的。

URL Loading System 的图

1.NSURLProtocol 可以拦截的网络请求包括 NSURLSession,NSURLConnection 以及 UIWebVIew。
2. 基于 CFNetwork 的网络请求,以及 WKWebView 的请求是无法拦截的。
3. 现在主流的 iOS 网络库,例如 AFNetworking,Alamofire 等网络库都是基于 NSURLSession 或 NSURLConnection 的,所以这些网络库的网络请求都可以被 NSURLProtocol 所拦截。

//MagicURLProtocol.h
#import <Foundation/Foundation.h>
@interface MagicURLProtocol : NSURLProtocol
  
@end
//MagicURLProtocol.m
#import "MagicURLProtocol.h"

#ifdef SD_WEBP
#import "UIImage+WebP.h"
#endif

static NSString *const MagicURLProtocolKey = @"MagicURLProtocol-already-handled";

@interface MagicURLProtocol()<NSURLConnectionDataDelegate>
@property (strong, nonatomic) NSURLConnection *connection;
@property (strong, nonatomic) NSMutableData *recData;
@end

@implementation MagicURLProtocol

- (void)dealloc{
    self.recData = nil;
}

/**
 判断是否启用SD_WEBP 并且图片格式为webp 如果为YES 则标记请求需要自行处理并且防止无限循环 为NO则不处理
 Build Settings -- Preprocessor Macros, 添加 SD_WEBP=1
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    BOOL useCustomUrlProtocol = NO;
    NSString *urlString = request.URL.absoluteString;
    if (!SD_WEBP || ([urlString.pathExtension compare:@"webp"] != NSOrderedSame)) {
        useCustomUrlProtocol = NO;
    }else {
        if ([NSURLProtocol propertyForKey:MagicURLProtocolKey inRequest:request] == nil) {
            useCustomUrlProtocol = YES;
        }else {
            useCustomUrlProtocol = NO;
        }
    }
    return useCustomUrlProtocol;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

// 将截获的请求使用NSURLConnection | NSURLSession 获取数据
// (此处使用NSURLConnection)
- (void)startLoading{
    NSMutableURLRequest *newRequest = [self cloneRequest:self.request];
    //NSString *urlString = newRequest.URL.absoluteString;
    //NSLog(@"######截获WebP url:%@",urlString);
    [NSURLProtocol setProperty:@YES forKey:MagicURLProtocolKey inRequest:newRequest];
    [self sendRequest:newRequest];
}

- (void)stopLoading{
    if (self.connection) {
        [self.connection cancel];
    }
    self.connection = nil;
}

#pragma mark - dataDelegate
//复制Request对象
- (NSMutableURLRequest *)cloneRequest:(NSURLRequest *)request{
    NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:request.URL cachePolicy:request.cachePolicy timeoutInterval:request.timeoutInterval];
    
    newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
    [newRequest setValue:@"image/webp,image/*;q=0.8" forHTTPHeaderField:@"Accept"];
    if (request.HTTPMethod) {
        newRequest.HTTPMethod = request.HTTPMethod;
    }
    if (request.HTTPBodyStream) {
        newRequest.HTTPBodyStream = request.HTTPBodyStream;
    }
    if (request.HTTPBody) {
        newRequest.HTTPBody = request.HTTPBody;
    }
    newRequest.HTTPShouldUsePipelining = request.HTTPShouldUsePipelining;
    newRequest.mainDocumentURL = request.mainDocumentURL;
    newRequest.networkServiceType = request.networkServiceType;
    return newRequest;
    
}

#pragma mark - 网络请求
- (void)sendRequest:(NSURLRequest *)request{
    self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}

#pragma mark - NSURLConnectionDataDelegate
/**
 * 收到服务器响应
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSURLResponse *returnResponse = response;
    [self.client URLProtocol:self didReceiveResponse:returnResponse cacheStoragePolicy:NSURLCacheStorageAllowed];
}
/**
 * 接收数据
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    if (!self.recData) {
        self.recData = [NSMutableData new];
    }
    if (data) {
        [self.recData appendData:data];
    }
}
/**
 * 重定向
 */
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response{
    if (response) {
        [self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
    return request;
}
/**
 * 加载完毕
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    NSData *imageData = self.recData;
#ifdef SD_WEBP
    UIImage *image = [UIImage sd_imageWithWebPData:self.recData];
    imageData = UIImagePNGRepresentation(image);
    if (!imageData) {
        imageData = UIImageJPEGRepresentation(image, 1);
    }
#endif
    [self.client URLProtocol:self didLoadData:imageData];
    [self.client URLProtocolDidFinishLoading:self];
}
/**
 * 加载失败
 */
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
    [self.client URLProtocol:self didFailWithError:error];
}

@end

MagicWebViewWebPManager

MagicWebViewWebPManager 封装了支持 UIWebView、WKWebView 注册和销毁 MagicURLProtocol。

特别注意:NSURLProtocol 一旦被注册将会使整个 app 的 request 请求都会被拦截,进入 Web 时注册,退出 Web 取消注册。在使用的时候需要特别注意。

//MagicWebViewWebPManager.h
#import <Foundation/Foundation.h>

@interface MagicWebViewWebPManager : NSObject
+ (MagicWebViewWebPManager *)shareManager;
- (void)registerMagicURLProtocolWebView:(id)webView;       //注册 MagicURLProtocol
- (void)unregisterMagicURLProtocolWebView:(id)webView;     //销毁 MagicURLProtocol
@end
//MagicWebViewWebPManager.m
#import "MagicWebViewWebPManager.h"
#import "NSURLProtocol+MagicWebView.h"
#import <WebKit/WebKit.h>

static NSString *const MagicURLProtocol_String = @"MagicURLProtocol";

@implementation MagicWebViewWebPManager

+ (MagicWebViewWebPManager *)shareManager{
    static MagicWebViewWebPManager *manger;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manger = [[MagicWebViewWebPManager alloc] init];
    });
    return manger;
}

#pragma mark - WebView支持webP
// NSURLProtocol 一旦被注册将会使整个App的request请求都会被拦截,进入Web时注册,退出Web取消注册
/**
 注册 MagicURLProtocol
 */
- (void)registerMagicURLProtocolWebView:(id)webView{
    if ([webView isKindOfClass:[WKWebView class]]) {
        [NSURLProtocol registerClass:NSClassFromString(MagicURLProtocol_String)];
        [NSURLProtocol wk_registerScheme:@"http"];
        [NSURLProtocol wk_registerScheme:@"https"];
    }
    if ([webView isKindOfClass:[UIWebView class]]){
        [NSURLProtocol registerClass:NSClassFromString(MagicURLProtocol_String)];
    }
}

/**
 销毁 MagicURLProtocol
 */
- (void)unregisterMagicURLProtocolWebView:(id)webView{
    if ([webView isKindOfClass:[WKWebView class]]) {
        [NSURLProtocol unregisterClass:NSClassFromString(MagicURLProtocol_String)];
        [NSURLProtocol wk_unregisterScheme:@"http"];
        [NSURLProtocol wk_unregisterScheme:@"https"];
    }
    if ([webView isKindOfClass:[UIWebView class]]){
        [NSURLProtocol unregisterClass:NSClassFromString(MagicURLProtocol_String)];
    }
}

@end

NSURLProtocol+MagicWebView

NSURLProtocol+MagicWebView 用于提供 WKWebView 对 NSURLProtocol 的支持能力。

UIWebView 默认支持 NSURLProtocol,如果需要 WKWebView 也支持 NSURLProtocol,则需要扩展,具体代码如下。

//NSURLProtocol+MagicWebView.h
#import <Foundation/Foundation.h>

@interface NSURLProtocol (MagicWebView)
+ (void)wk_registerScheme:(NSString *)scheme;       //注册协议
+ (void)wk_unregisterScheme:(NSString *)scheme;     //注销协议
@end
//NSURLProtocol+MagicWebView.m
#import "NSURLProtocol+MagicWebView.h"
#import <WebKit/WebKit.h>

FOUNDATION_STATIC_INLINE Class ContextControllerClass() {
    static Class cls;
    if (!cls) {
        cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
    }
    return cls;
}
FOUNDATION_STATIC_INLINE SEL RegisterSchemeSelector() {
    return NSSelectorFromString(@"registerSchemeForCustomProtocol:");
}
FOUNDATION_STATIC_INLINE SEL UnregisterSchemeSelector() {
    return NSSelectorFromString(@"unregisterSchemeForCustomProtocol:");
}

@implementation NSURLProtocol (MagicWebView)

+ (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
    }
}

+ (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
    }
}

@end

三、使用

1、将 MagicWebViewWebP.framework 导入工程。

2、引入头文件。

#import <MagicWebViewWebP/MagicWebViewWebPManager.h>

3、webView 加载请求之前,注册 MagicURLProtocol。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view. 
    // 注册协议支持webP
    [[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:self.wkWebView];
    // 加载请求
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:_requestURLString]];
    [self.wkWebView loadRequest:request];
}

4、dealloc 中销毁 MagicURLProtocol。

若有特殊需求,需要在 web 页面退出时销毁 MagicURLProtocol,否则会拦截整个 app 的网络请求。

// 销毁
-(void)dealloc{
    [[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView:self.wkWebView];
}

5、可以加载 http://isparta.github.io/compare-webp/index.html#12345 来检测 webP 显示效果。

三、开源地址

MagicWebViewWebP.framework 已经开源,喜欢的小伙伴儿可以 Star。⭐️⭐️⭐️⭐️⭐️

MagicWebViewWebP 开源

参考文章

探究 WebP 一些事儿
移动端 WebP 兼容解决方案
NSURLProtocol 全攻略
WKWebView 那些坑

 



作者:LuisX
链接:https://www.jianshu.com/p/8639c135a4fe
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值