iOS平台利用 NSURLProtocol 实现网络数据缓存

熟悉Windows开发的人,大都知道钩子HOOK函数, 他能挂钩某些函数,
让所有传递的数据信息都经过钩子函数的过滤。
钩子挂钩的函数,即使是别人开发的模块或者系统模块,也照样能过滤到数据信息,这点非常重要。
但在处理 HTTP,HTTPS,FTP等通用网络协议的时候,Windows平台的wininet函数库,
却没有提供一个统一的系统的接口来过滤这些协议,
当然可以使用微软开发的detours或者类似的函数库来挂钩所有系统函数来达到目的,
但是毕竟没有使用一个统一的接口来的正规和简洁。

iOS平台提供了一个接口来过滤处理这些网络协议,
它是建立在 URL Load System框架基础上,凡是使用NSURL协议的都经过这个框架。
 URL加载系统就是一个层次结构,每层提供一个子接口完成一个功能需求。
 最上层的就是NSURLConnection, 而NSURLProtocol处于底层,它提供一个抽象接口
来完成类似Windows平台那种钩子函数的功能,
所有 NSURLConnection的 子调用函数,都将被 NSURLProtocol挂钩过滤到。
也许这么比喻不太确切,但是可以比较快的让像我这样的熟悉Windows开发的人尽快熟悉iOS程序。
因为很多基础的东西,都是相通的。

 为了使用NSURLPRotocol,我们需要子类化NSURLProtocol,并且把子类化的类注册到URL加载系统中。

大致伪代码如下:

////头文件

#define SPCKEY @"SimpleProtocolCacheKey"    
 /////这个用来分辨网络请求是NSURLProtocol发出的,还是上传应用发出的,具体看下文。

@interface SimpleURLProtocolCache : NSURLProtocol


@end

////////   实现文件
@interface  SimpleURLProtocolCache()

@end

/////////////////////////////////////////////////////////////////

@implementation SimpleURLProtocolCache

/////////////////////////////////////////////////以下是为了过滤上层 NSURLCOnnection发来和接收到的请求时候,需要处理的函数。

+(void)initialize
{
       /////这个是在注册到URL加载系统时候调用的函数,主要用来对一些全局变量的初始化等操作。

 

+(BOOL) canInitWithRequest:(NSURLRequest *)request
{
        /////////根据request判断是不是需要我们过滤的请求,如果是则返回YES,这时候 URL加载系统会创建一个 

        //////// NSURLProtocol子类化实例来接着响应下面的请求。
 
      /////
       if( ![request.URL.scheme isEqualToString:@"http"] &&
        ![request.URL.scheme isEqualToString:@"https"] ) //只处理http https 协议
       {
              return NO;
       }
       //////
       if(  [NSURLProtocol propertyForKey:SPCKEY inRequest:request]  ){  
           /// 因为在NSURLProtocol子类中,我们会再次使用 NSURLConnection来发起真正的网络请求,
               所以需要区别是上层调用NSURLConnection的请求,还是本子类调用的请求。
               SPCKEY是定义的一个字符串,在本子类调用NSURLConnection的地方一律添加 KEY
               为 SPCKEY的属性,在这里就可以判断属性来确定是不是本子类发起的请求,这样就避免了重复循环调用。
            ////
            return NO;
       }
       /////////
}
////////返回规范化后的request,一般就只是返回当前request即可。
+(NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}
 
/////返回基类的请求即可
+(BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
    return [super requestIsCacheEquivalent:b toRequest:b];
}

////////////////////////////////////////////////////////////////////////////

-(void) startLoading
{       
        ///上层发起网络请求时候,这个函数会被调用。
        ///在这里进行真正的网络数据请求或者查询缓存是否存在数据
        ///这里是网络通讯请求的开始,缓存判断也是从这里开始,首先查询请求的URL是否存在缓存,如果是,
       ////则直接从缓存里把数据返回给请求者。否则就在这里发起真正的网络请求,
      
       大致伪代码如下:

       cache = [self getCache]; /////获取缓存数据,这里可以根据请求的URL做某种算法获取缓存。

       if(cache){ //////缓存中存在数据,因此直接把缓存数据返回给请求者。cache缓存中,应包含HTTP响应头和响应内容。
            
             //////下面调用客户端代理,直接把数据返回给上层网络数据请求者。
              [[self client] URLProtocol:self didReceiveResponse:httpRes cacheStoragePolicy:NSURLCacheStorageNotAllowed];
              ////调用客户端请求代理,回答HTTP响应头,httpRes是真正网络请求后的应答头,是在 didReceiveResponse 回调函数中
                  保存到缓存中的头信息
             
              [[self client] URLProtocol:self didLoadData:data]; ///请求的数据内容,data是 在 didReceiveData 回调函数中保存到缓存中的数据
    
              [[self client] URLProtocolDidFinishLoading:self]; ////成功完成数据的请求
       }
      else{ //////缓存中不存在URL对应的缓存数据,因此得发起真正的网络请求,
          
           NSMutableURLRequest*  newRequest = [[NSMutableURLRequest alloc] initWithURL:self.request.URL
                                                      cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                                  timeoutInterval:10.0];

            ///////标志 SPCKEY属性,用来在canInitWithRequest回调函数中判断网络请求是不是本子类发出去的。
            [NSURLProtocol setProperty:@"YES" forKey:SPCKEY inRequest:newRequest]; /////////!!!

             [NSURLConnection connectionWithRequest:newRequest delegate:self]; /////发起网络请求,并且以本子类为请求代理类。
                
            代理函数包括 willSendRequest, didReceiveResponse, didReceiveData,didFailWithError,               
             connectionDidFinishLoading 等等就会被调用,
             主要在didReceiveResponse和didReceiveData回调函数中对从网络端传来的数据做
              缓存处理,等待下次同样的请求,再次进入到 startLoading 函数中,就可以直接从缓存中获取数据,直接返回给请求者。

      }
      ///////
}
 -(void)stopLoading
{
       ///////停止网络请求
      
       //////////
}

///////////
-(NSURLRequest*)connection:(NSURLConnection*)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
        /////  在这里解决 HTTP 的 302 等重定向问题。如果不处理,遇到 302重定向的网页,会出现一些奇葩问题,
               具体处理办法,可以查看我稍后提供的源代码。
       ///////
}

//////接收数据回应头,网络数据
 -(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse *)response
{
      ////在这里做各种判断,并且把请求头缓存起来,如果对206等断点续传提供支持,还需要再做更进一步的分析处理。
      //////
}

//////接收数据内容
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData *)data
{
     ///这里接收网络数据内容,把这个数据缓存起来,等待下次可以直接从缓存获取数据。
     ////
}

//////////通讯过程中出差错
-(void)connection:(NSURLConnection*)connection didFailWithError:(NSError *)error
{
      ////程序通讯出错,做出错处理
      ////
}

///////////数据接收成功完成
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
       //////通讯成功完成。
       //////
}

@end

其实也比较简单的接口,只要弄明白各个接口函数的具体含义,
然后配合自己的基本技术功底,就能随心所欲的实现自己想要的功能。

你也可以在处理缓存的时候,处理206部分缓存数据的问题。
稍后提供的代码,已经处理了这个问题,其实对于一般的网页和图片缓存,倒是没多大必要处理 206 部分缓存,
但是对于 视频数据,大文件下载等,处理206部分缓存,就显得十分必要了。
 
再说说缓存数据的保存和获取;
对于每个URL请求,我采用两个文件保存缓存信息,
一个文件是 plist,用来保存缓存数据的信息,包括HTTP的响应头,文件大小,缓存大小, 206部分缓存的”空洞“信息等。
一个文件是真正的数据缓存文件,用来保存从网络端下载下来的数据。
文件名采用 URL字符串的 SHA1来定义,这样既能保证每个URL的唯一性,也能保证文件名不会出现乱七八糟的字符。

我是 根据URL的后缀名来决定某个URL是否需要缓存 ,比如 某个URL, http://xxx/xx.png, 它的后缀是 png,
如果在代码中指定 png 后缀需要缓存,那么 所有带 png后缀的URL都将被缓存住。

普通的缓存还好处理,只要在 didReceiveResponse 回调函数中,根据请求的URL做SHA1,生成文件名,获取plist配置信息,
把响应头写入配置文件,在 didReceiveData 中把数据内容追加到另一个缓存数据文件中,这样写缓存部分就搞定了。
读缓存的时候,等再次请求 同样的 URL的时候,在 startLoading 中 根据URL的SHA1,查看是否存在缓存,并且是已经完整缓存住的数据,
然后直接读取缓存文件和响应头返回即可。
 
对于 HTTP的 206 部分缓存,就稍微麻烦点了,
写缓存的时候,需要在 didReceiveResponse 回调函数中解析头,根据 HTTP响应头的Content-Range 字段,
判断服务端回复的数据是文件的哪部分的内容,然后保存这些位置信息,
等在  didReceiveData 回调函数,还需要把文件指针移到指定的位置,把从服务端获取的数据写入,
因为文件位置根据请求不同的产生位置偏移,
从而在缓存数据文件中很容易产生”空洞“, 就是缓存文件中,一部分不是有效数据,而另一部分是有效数据。
因此需要在 plist配置文件中,确定这些空洞位置,防止读取缓存的时候,读取空洞数据。
这些”空洞“位置是根据 206 响应头的  Content-Range 和写入缓存文件的位置等来决定的。

然后在读缓存的时候,根据请求的位置,和缓存空洞位置,来确定哪些是可以直接从缓存获取。
如果整个请求都可以从缓存获取,就直接返回缓存数据,
如果只是一部分在缓存中,就先返回那部分数据,然后再发起真正的网络请求,请求剩下的一部分数据。
然后在 didReceiveResponse 和 didReceiveData 中,一边把数据写到缓存,一边调整Content-Range位置,返回给上层请求者。
整个过程稍微有些复杂,具体可以查看稍后发布到CSDN的代码。

至此,这就是利用NSURLProtocol实现的离线缓存部分,
 当初做这个库的时候,是在模拟器中运行的,因此十分顺利的,除了能缓存 UIWebView控件的网页数据之外,还能缓存视频数据。
当时是十分高心,就一个库,就能处理全部的缓存,只要是调用者使用 NSURLCOnnection来请求网络数据都能办到。
可是等我移植到真机上测试的时候,才发现一个大问题。
真机上,不能缓存视频数据,不论是使用 AVPlayer 或者是 MPMovie控件,都不能缓存,查阅资料之后才发现,
苹果在实现在线视频的时候,除了表面是使用NSURL概念外,在内部并不是走的 NSURL 加载系统,
估计他们是直接使用 socket请求的网络数据。
  
因此在一阵纠结之后,寻找能利用开发好的库的解决办法,
下章简单介绍解决办法,虽然显得有点罗嗦,但是也能解决边播放视频,边缓存的问题。


提供的源代码下载地址
http://download.csdn.net/detail/fanxiushu/9055095




你可能感兴趣的:(ios,objC)