优化 WebView 的加载速度之URL 拦截替换

前言

接着上一篇文章《优化 WebView 的加载速度实例》,记录一下本地缓存加载以及在没有缓存的情况下重定向请求线上资源的处理逻辑。

思路

为了保证网页正常加载,在没有缓存的情况下,不仅需要进行缓存模板的下载(为下次加载页面做准备),还需要同时在此刻加载线上的网页已达到当前页面正常展示的目的。流程如下:

加载逻辑

在加载一个 HTML 页面的时候,页面会加载一些资源文件,所以对于每个加载的资源文件我们都需要判断本地是否存在其缓存模板,如果没有则需要请求线上资源。所以对于 webView 中的每一个 request 请求,我们需要进行过滤,对 request 进行拦截转发处理,如果请求的资源文件本地存在缓存,那么加载本地资源,否则请求线上资源内容。

实现

为了保证每次请求的 request 都能够捕获拦截,可以通过继承NSURLProctol 方式创建系统拦截,通过查看该类介绍可以看出来该类为一个抽象类,介绍如下:

/*!
    @class NSURLProtocol
    
    @abstract NSURLProtocol is an abstract class which provides the
    basic structure for performing protocol-specific loading of URL
    data. Concrete subclasses handle the specifics associated with one
    or more protocols or URL schemes.
*/

大致意思就是系统在每次进行 URL 请求的时候会去实例一个 NSURLProtocol 对象进而处理 URL 请求内容。如果不去实例这个对象,系统会默认去实例一个默认的对象去处理URL 请求。

所以为了去拦截每次的 URL,我们可以去继承这个抽象类,进而达到拦截过滤处理的目的。

  • 继承 NSURLProtocol
#import <Foundation/Foundation.h>

@interface LCWebCacheURLProtocol : NSURLProtocol

@end

  • 实现抽象类的提供的方法
    查看NSURLProtocol介绍,要求我们必须实现标记之间的所有方法,所以,我们直接实现就好了,介绍如下
/*======================================================================
  Begin responsibilities for protocol implementors

  The methods between this set of begin-end markers must be
  implemented in order to create a working protocol.
  ======================================================================*/
#import "LCWebCacheURLProtocol.h"

@implementation LCWebCacheURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
        return NO;
}

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

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b{
    
    return YES;
}

- (void)startLoading{
}

- (void)stopLoading{
}

@end

这里说一下这几个方法的含义

  • + (BOOL)canInitWithRequest:(NSURLRequest *)request
    该实现决定当前请求是否需要进行处理,如果需要处理返回 Yes 即可,不需要直接返回 No。这里需要注意一下,该方法是将当前页面的所有请求都遍历拦截一遍之后,才会去对后续的拦截方法进行回调。

  • + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
    这个方法是在上一个方法返回 Yes 之后,对需要处理的 request进行操作的方法,但是由于形式参数与类中定义request 名字一样,这里操作 request 会引发歧义,为了避免出错,尽量不要在这里进行 request 的加工,这里直接返回 request

  • + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
    这个方法主要用来判断两个请求是否是同一个请求,如果是,则可以选择使用缓存数据,这里一般都是默认支持的,所以直接返回 Yes,且不做数据处理

如果说前面是对于请求的处理以及配置,那么以下的两个方法则是数据请求的过程了

  • - (void)startLoading
    在这里可以对 request 进行转发,包括 NSURLSessionNSURLConnection 甚至使用 AFNetworking 等二次转发请求,最后将数据请求的结果回传给Client来响应请求的发起者

  • - (void)stopLoading
    看名字就知道这个与startLoading是对应的,这个方法是用来断开Connection链接的,当请求的发起者(NSURLProtocolClient)的协议方法都回调完毕后,该方法会执行

实现上面的方法之后,在使用的时候通过注册的形式去将自定义NSURLProtocol给系统,这样系统在进行 request 请求的时候就能通过注册传递的对象找到自定义的拦截内容了。

[NSURLProtocol registerClass:[LCWebCacheURLProtocol class]];

通过NSURLProtocolregisterClass方法来进行注册,最后拦截转发内容逻辑统一在startLoading进行操作即可。整体请求的处理也就变成了酱紫。

#import "LCWebCacheURLProtocol.h"

static NSString * const filiterKeyword = @"gc";
static NSString * const replaceKeyword = @"gc://";
static NSString * const dealRequestKey = @"dealRequestKey";

@interface LCWebCacheURLProtocol()<NSURLSessionDelegate>

@property (nonatomic,strong) NSURLSession *session;

@property (nonatomic, strong) NSURLConnection *connection;

@property (atomic, strong) NSMutableData *data;

//是否处于下载模板中
@property (nonatomic, assign) BOOL isDownloadingTemplate;

@end

@implementation LCWebCacheURLProtocol

+ (void)registCacheUrlProtocol{
    
    [self registerClass:[self class]];
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
    
    
    NSString *scheme = request.URL.scheme;
    
    //如果处理过请求,放行
    if ([NSURLProtocol propertyForKey:dealRequestKey inRequest:request]) {
        NSLog(@"已经处理,放行");
        return NO;
    }
    
    //拦截带有特定标识的请求
    if ([scheme isEqualToString:filiterKeyword]) {
        
        return YES;
    }
    
    return NO;
}

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

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b{
    
    return YES;
}

- (void)startLoading{
    
    NSString *scheme = self.request.URL.scheme;

    NSMutableURLRequest *modifityRequest = [self.request mutableCopy];
    NSString *url = modifityRequest.URL.absoluteString;

    //处理需要处理的请求
    if ([scheme isEqualToString:filiterKeyword]) {

        /*** 获取项目中的文件路径 ***/
        NSString  *file = [url lastPathComponent];
        //扩展名
        NSString *suffix = [file pathExtension];
        //文件名字
        NSString *fileName = [file stringByDeletingPathExtension];

        NSString *modifiyPath = [[NSBundle mainBundle] pathForResource:fileName ofType:suffix];

        //判断本地文件是否存在
        BOOL isVaild = [[NSURL fileURLWithPath:modifiyPath] checkResourceIsReachableAndReturnError:nil];

        if (isVaild) {

            NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:modifiyPath]];
            NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL fileURLWithPath:modifiyPath] MIMEType:suffix expectedContentLength:data.length textEncodingName:nil];

            //响应请求者数据
            [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
            [self.client URLProtocol:self didLoadData:data];
            [self.client URLProtocolDidFinishLoading:self];

        }

        //文件不存在,加载线上地址
        else{
            
            self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
            NSMutableURLRequest *changeRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];


            NSLog(@"modifityURL:%@",changeRequest.URL.description);

            //设置当前处理的请求的标志,防止重复处理请求
            [NSURLProtocol setProperty:@YES
                                forKey:dealRequestKey
                             inRequest:changeRequest];

            NSURLSessionDataTask *task = [self.session dataTaskWithRequest:self.request];
            [task resume];
            
            /*********** 以下是处理异步下载模板的逻辑(为下次加载当前页面的模板做准备)****/
            /*
            self.isDownloadingTemplate = YES;
            //异步下载模板
            WeakObj(self);
            [GCHtmlTemplateTool updateDetailHtmlStyle:^{
                NSLog(@"下载模板成功");
                selfWeak.isDownloadingTemplate = NO;
            }];
             */
            

        }

    }

    
}

- (void)stopLoading{
    
    [self.session invalidateAndCancel];
    
}

#pragma -mark- NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    completionHandler(NSURLSessionResponseAllow);
    
    self.data = [NSMutableData new];

    
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
   didReceiveData:(NSData *)data {
 
    [self.data appendData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
    if (error) {
        
        [self.client URLProtocol:self didFailWithError:error];
    }
    
    else {
        [self.client URLProtocol:self didLoadData:self.data];
        [self.client URLProtocolDidFinishLoading:self];
    }
    
}


@end

通过URL 的拦截可以过滤需要处理的请求,进而将请求替换成本地缓存的内容,没有缓存直接去加载线上的文件内容(保证本次网页内容的正常加载),以这样拦截的方式结合本地模板加载使网页加载变得更为灵活不是吗?

需要Demo点这里下载

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

推荐阅读更多精彩内容