萌新讲故事之对苹果源码CustomHTTPProtocol的解读

以下为自言自语可以跳过

由于公司需求需要对WebView进行离线缓存,之前也在网上看了许多的帖子和文章下载了几个Demo,有用JS把网页的图片以及内容缓存到本地的这个感觉有点麻烦放弃了,有个老外封装的通过HTTPProtocol来实现离线缓存的功能,感觉还是屌屌的,本来缓存功能还是能使用,由于苹果爸爸先是要让支持ipv6 马上又要必须使用 HTTPS,加上公司还要对接口使用 Des加密,无奈之下放弃了 AFN 转而使用Session,但是使用Session之后 老外的那个封装的协议缓存不使用AFN竟然不能用了,可能AFN做了内部处理, 我使用的是苹果爸爸的Session啊。妈蛋我哪知道怎么处理的。于是在网上找到了 DKNight的作者的这篇文章iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求竟然在里面发现了苹果爸爸的源码点击这里下载,如果想看大神的文章还是点上门的传送门吧。

第一天

以下是我的解读视角

打开工程后


机智的我发现这些画红框的基本没啥用 Redme啦 HTML资源啦 图片资源啦啥的
但是我发现这个HTML中的内容还是有点用的
因为我发现storyboard中的页面中只有一个WebView,所以加载页面还是得看这3个页面啊

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Root</title>
</head>
<body>
<ul>
    <li><a href="http://www.apple.com">Apple</a></li>
    <li><a href="https://www.apple.com">Apple (HTTPS)</a></li>
    <li><a href="https://apple.com">Apple (HTTPS, redirect)</a></li>
    <li><a href="http://www.cacert.org/">CAcert</a></li>
    <li><a href="https://www.cacert.org/">CAcert (HTTPS)</a></li>
    <li><a href="http://www.cacert.org/certs/root.der">Install CAcert Anchor</a></li>
</ul>
</body>
</html>

打开Root文件就发现是几个链接
运行一下工程看看啥效果


哦好简洁,点击链接上面3个能跳转到苹果官网,下面3个都是报错。
那么我们看看对照着看一下,上面3个链接图片上有对应的文字注释妈蛋,忽然发现有个不认识的单词(redirect)。我才不会承认英语不好呢。百度了一下,应该是重新定向的意思,其对应的网址是https://apple.com ,应该是把这个网址重新定向成为https://www.apple.com 好吧这个页面已经没啥看的了 对代码解读也没啥意义。
接下来看看代码
首先我打开了main.m 至于为啥没有main.h 这个我哪知道。。
打开一看,满篇的绿,我操这么多英文,不知道我有密集恐惧症吗。


光英文我都不说啥,还有不少纯大写的,虽然都是小写的我也不认识!
学编程时老师就教过我一个秘诀!没用的东西,删! 妈蛋我发现每个页面上面都有这么一大段,我感觉到对我深深的恶意我就是看个代码而已竟然还得往下拉这么久,所以,我挨个把所有文件中的注释都删掉了。


哦,我的世界充满了整洁。妈蛋注释一删掉发现一共就12行代码。。
苹果爸爸的代码和我们也差不多吗 main函数不过如此!
但是还是发现和普通工程有一些区别的比如**argv这个 大家可以和自己的main对比一下就好
接下来是AppDelegate
定义了几个属性目前不知道干嘛的
定义了2个静态变量

static BOOL sAppDelegateLoggingEnabled = YES;
static NSTimeInterval sAppStartTime; 

一个是开启日志的一个是记录时间的。。看名字就知道了
而具体是干嘛的呢。就是在下面的协议方法中控制输出内容的

- (void)logWithPrefix:(NSString *)prefix format:(NSString *)format arguments:(va_list)arguments

接下来看

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//一进方法就碰到了个不认识的东西#pragma unused
//度娘告诉我主要负责遇到没有使用的变量不报错
    #pragma unused(application)
    #pragma unused(launchOptions)
    WebViewController *   webViewController;
// 度娘告诉我 assert这个是负责在DEBUG状态下如果参数为NO编译时会提示是否继续
//如果在release状态下不编译这个宏    
    assert(self.window != nil);
    
    sAppStartTime = [NSDate timeIntervalSinceReferenceDate];
    //管理受信任的锚证书列表 具体干嘛目前还没看到
    self.credentialsManager = [[CredentialsManager alloc] init];
//原文翻译下来是下面的意思 但是也不知道干嘛的
//准备我们的日志代码所需的全局变量。 调用-threadInfoForCurrentThread
 //设置主线程线程信息记录,并确保它具有线程号0。
    self.threadInfoByThreadID = [[NSMutableDictionary alloc] init];
  //进入方法看了一下 主要是把所有使用过的线程信息储存到字典中
    (void) [self threadInfoForCurrentThread];
    //上面那些代码以及没啥用
//核心代码就这两句。
//接下来进入内部看看
//CustomHTTPProtocol 不能直接从头看,应该从调用的地方开始看
    [CustomHTTPProtocol setDelegate:self];
    if (YES) {
        [CustomHTTPProtocol start];
    }
    //下面的也没啥用
    webViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle bundleForClass:[self class]]] instantiateViewControllerWithIdentifier:@"webView"];
    assert(webViewController != nil);
    webViewController.delegate = self;
    if (NO) {
        webViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Test" style:UIBarButtonItemStyleBordered target:self action:@selector(testAction:)];
    }
    [((UINavigationController *) self.window.rootViewController) pushViewController:webViewController animated:NO];

    [self.window makeKeyAndVisible];
    
    return YES;
}

CustomHTTPProtocol.h中有下面几个方法
就是重写的setter方法和getter方法

static id<CustomHTTPProtocolDelegate> sDelegate;

+ (void)start
{
    [NSURLProtocol registerClass:self];
}

+ (id<CustomHTTPProtocolDelegate>)delegate
{
    id<CustomHTTPProtocolDelegate> result;

    @synchronized (self) {
        result = sDelegate;
    }
    return result;
}

+ (void)setDelegate:(id<CustomHTTPProtocolDelegate>)newValue
{
    @synchronized (self) {
        sDelegate = newValue;
    }
}

这句才是关键。
在每一个 HTTP 请求开始时,URL 加载系统创建一个合适的NSURLProtocol 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 NSURLProtocol 的类,并通过 - registerClass: 方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。

+ (void)start
{
    [NSURLProtocol registerClass:self];
}

还有几个协议目前先不看
在AppDelegate中注册开始后每个请求都会按照现在这个协议方法执行了
接下来看CustomHTTPProtocol.m内部实现
上面属性定义和一些方法先不看 没啥用 往下看 直接到 canInitWithRequest:
下面的方法用于决定请求是否需要当前协议对象处理

//定义一个字符串用来判断是否在请求中添加个这个标识
static NSString * kOurRecursiveRequestFlagProperty = @"com.apple.dts.CustomHTTPProtocol";

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    BOOL        shouldAccept;//判断是否可以相应请求的变量
    NSURL *     url;
    NSString *  scheme;
    

    //判断请求是否存在
    shouldAccept = (request != nil);
    if (shouldAccept) {
        url = [request URL];
        shouldAccept = (url != nil);//判断请求中的url是否非空
    }
    if ( ! shouldAccept ) {//出现上面两种情况通过代理输出错误信息
        [self customHTTPProtocol:nil logWithFormat:@"decline request (malformed)"];
    }
    
    
    if (shouldAccept) {//请求存在后判断这个请求是否做过标记
        shouldAccept = ([self propertyForKey:kOurRecursiveRequestFlagProperty inRequest:request] == nil);
        if ( ! shouldAccept ) {//如果做过标记返回信息而且下面的代码都不会执行了
            [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (recursive)", url];
        }
    }
        
    if (shouldAccept) {
        scheme = [[url scheme] lowercaseString];
//后面的lowercaseString是字母都变成小写但是对scheme我不是很理解所以百度了一下
//下面有scheme讲解的地址
        shouldAccept = (scheme != nil);
        //scheme是否为空,估计几率比较小
        if ( ! shouldAccept ) {
            [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (no scheme)", url];
        }
    }
    
    if (shouldAccept) {
        shouldAccept = NO && [scheme isEqual:@"http"];
//这段代码会出现问题肯定是NO不知道为什么这么写
        if ( ! shouldAccept ) {
            shouldAccept = YES && [scheme isEqual:@"https"];
//这段代码如果scheme为http返回NO  https返回YES
        }

        if ( ! shouldAccept ) {
            [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (scheme mismatch)", url];
        } else {
            [self customHTTPProtocol:nil logWithFormat:@"accept request %@", url];
        }
    }
    
    return shouldAccept;
}

每一次请求都会有一个 NSURLRequest 实例,上述方法会拿到所有的请求对象,我们就可以根据对应的请求选择是否处理该对象;而上面的代码实现的功能有,请求不能为空,请求中的URL不能为空,请求中的的自定义标记应该为空,做过标记则不再处理, scheme不能为空,HTTPS则每次都做处理而HTTP则永不做处理。

上面我提到的文章是这样写下面的方法的

请求经过 + canInitWithRequest: 方法过滤之后,我们得到了所有要处理的请求,接下来需要对请求进行一定的操作,而这都会在 + canonicalRequestForRequest: 中进行,虽然它与 + canInitWithRequest: 方法传入的 request 对象都是一个,但是最好不要在 + canInitWithRequest: 中操作对象,可能会有语义上的问题;所以,我们需要覆写 + canonicalRequestForRequest: 方法提供一个标准的请求对象

所以接下来是解读苹果内部是怎样处理这个需要处理的请求的
方法中主要作用在于CanonicalRequestForRequest()函数

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSURLRequest *      result;
    
    assert(request != nil);
//可以在任何线程上调用
//规范化请求是相当复杂的,所以所有的繁重工作
//被拖放到一个单独的模块。    
    result = CanonicalRequestForRequest(request);
    [self customHTTPProtocol:nil logWithFormat:@"canonicalized %@ to %@", [request URL], [result URL]];
    
    return result;
}

接下来根据这个规范化请求跳转到CanonicalRequest.h类
只有一个接口

extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request);

接下来到.m查看实现方式

extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request)
{
    NSMutableURLRequest *   result;
    NSString *              scheme;

    assert(request != nil);
//请求不能为空,虽然之前的方法已经判断过了,这里苹果又判断了一次    
    result = [request mutableCopy];
//将请求拷贝一份
    scheme = [[[request URL] scheme] lowercaseString];
    assert(scheme != nil);
//获取scheme
    
    if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) {
        assert(NO);//如果不是http或者https什么也不处理进行报错
    } else {
/*
typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted);
在上方有定义一个函数指针
*/
        //这三个参数用于给函数赋值
        CFIndex         bytesInserted;
        NSURL *         requestURL;
        NSMutableData * urlData;
        //通过在上面定义的函数指针 指向一个函数指针数组
        //数组中放着5个函数的实现
        static const CanonicalRequestStepFunction kStepFunctions[] = {
            FixPostSchemeSeparator, 
            LowercaseScheme, 
            LowercaseHost, 
            FixEmptyHost, 
            // DeleteDefaultPort,       -- 内置规范化程序已停止这样做,所以我们也不这样做。
            FixEmptyPath
        };
        size_t          stepIndex;
        size_t          stepCount;
        
       // 通过执行我们的每个步骤功能来规范化URL。
        //kCFNotFound 表示搜索操作未能成功定位目标值的常量。
        bytesInserted = kCFNotFound;
        urlData = nil;
        requestURL = [request URL];
        assert(requestURL != nil);
        //size_t 类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版。它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。
        //通过函数指针数组  除以单个数组中的元素地址 获取个数
        stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions);
        for (stepIndex = 0; stepIndex < stepCount; stepIndex++) {
        
            // 如果我们没有有效的网址数据,请从网址中创建。
            assert(requestURL != nil);
            if (bytesInserted == kCFNotFound) {//每个调用一个数组中的函数返回的结果都是bytesInserted == kCFNotFound
                NSData *    urlDataImmutable;
                //下面这个方法是从请求的url访问网址获取一个新的URL的Data
                urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) );
                assert(urlDataImmutable != nil);
                
                urlData = [urlDataImmutable mutableCopy];
                assert(urlData != nil);
                
                bytesInserted = 0;
            }
            assert(urlData != nil);
            
            //通过函数调用下面数组中的函数,将返回的URL符合每一种规范
            bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted);            
            //输出返回的URL内容            
            if (NO) {
                fprintf(stderr, "  [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]);
            }
            //翻译了一下注释
            //如果步骤使您的网址无效,请从网址资料重新建立网址。 
            //(或者我们在最后一步,因此我们需要在循环之外的URL)
            
            if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) {
                requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) );
                assert(requestURL != nil);
                
                urlData = nil;
            }
        }
        [result setURL:requestURL];        
        // 添加一些header        
        CanonicaliseHeaders(result);
    }    
    return result;
}

那个函数指针数组中的对应函数就不要看啦,都是一些网址的规范,通过这些函数把我们请求中的地址变为符合规范的地址
妈蛋这个类看了好久好久,结果让我失望的是竟然没什么屌用。 仅仅是自动添加标题啦各种各样的方法 妈蛋啊妈蛋

看了这么久才看了2个方法好尴尬,不过终于到了关键方法了。终于可以初始化一个 NSURLProtocol 对象了

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client
{
    assert(request != nil);
    assert(client != nil);
  //这里其实可以直接返回父类的初始化方法就行了。。
    self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
    if (self != nil) {
        [[self class] customHTTPProtocol:self logWithFormat:@"init for %@ from <%@ %p>", [request URL], [client class], client];
    }
    return self;
}

。。非常简单直接调用父类方法。

第二天

接下来开始 - (void)startLoading 方法
看到了这个方法出现了卡顿。因为我有点不理解为什么在这个方法中又执行了Session的请求。网上查看了一些文章其中一篇名字为定制实现NSURLProtocol提到

使用canInitWithRequest:我们可以筛选出可以使用当前协议的request,其它的忽略掉,直接走其它协议或者默认实现。

我才好些明白了点什么原来并不是我使用协议以后所有请求都是按照我们的协议进行处理了,而是我这个协议中的canInitWithRequest:不处理也会进行默认的实现以及处理
还有一边文章提到

NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Request timeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。

经过测试通过NSURLSession进行的网络请求截取的Request中并没有存储到HTTPBody(真的是内牛满面我还要做缓存呢这可咋整)
由于assert方法对逻辑没有什么影响我将代码中的assert方法都去掉了看着能方便些

- (void)startLoading
{
    NSMutableURLRequest *   recursiveRequest;//用来拷贝请求
    NSMutableArray *        calculatedModes;//用来记录Runloop中的模式
    NSString *              currentMode;//获取当前Runloop中的模式

    calculatedModes = [NSMutableArray array];
    [calculatedModes addObject:NSDefaultRunLoopMode];//添加默认的RunLoopMode
    currentMode = [[NSRunLoop currentRunLoop] currentMode];//获取当前的模式
    if ( (currentMode != nil) && ! [currentMode isEqual:NSDefaultRunLoopMode] ) {
        [calculatedModes addObject:currentMode];
//这个时候肯定是添加NSRunLoopCommonModes类型因为只有两种类型
    }
    self.modes = calculatedModes;//将Runloop类型存入Modes中 可能是1种或者2种    
    recursiveRequest = [[self request] mutableCopy];//将请求进行拷贝
    
    [[self class] setProperty:@YES forKey:kOurRecursiveRequestFlagProperty inRequest:recursiveRequest];
//在这里对我们拷贝的请求进行标记,以免陷入死循环
//我们需要在canInitWithRequest方法中进行判断如果做过标记的请求则不进行处理,否则将死循环

//下面的时间以及判断也没什么用 都是用来输出log的
    self.startTime = [NSDate timeIntervalSinceReferenceDate];
    if (currentMode == nil) {
        [[self class] customHTTPProtocol:self logWithFormat:@"start %@", [recursiveRequest URL]];
    } else {
        [[self class] customHTTPProtocol:self logWithFormat:@"start %@ (mode %@)", [recursiveRequest URL], currentMode];
    }
    
    // 获取当前线程具体干嘛目前不知道 继续往下看    
    self.clientThread = [NSThread currentThread];
    
    // 谷歌翻译说 一旦一切准备就绪,请使用新请求创建数据任务。
    //那么现在分别进入接下来这段代码的两个方法中看看是干嘛的
    self.task = [[[self class] sharedDemux] dataTaskWithRequest:recursiveRequest delegate:self modes:self.modes];
    
    [self.task resume];
}

首先是类方法sharedDemux

+ (QNSURLSessionDemux *)sharedDemux
{
    通过GCD创建一个单例  这个单例会控制我们之后的多次请求都会在这个队列中等待进行顺序执行
    static dispatch_once_t      sOnceToken;
    static QNSURLSessionDemux * sDemux;
    dispatch_once(&sOnceToken, ^{
        NSURLSessionConfiguration *     config;
/*
 NSURLSessionConfiguration对象定义在使用NSURLSession对象上传和下载数据时要使用的行为和策略。
 在上传或下载数据时,创建配置对象始终是必须执行的第一步。 
 您可以使用此对象来配置超时值,缓存策略,连接要求以及要与NSURLSession对象一起使用的其他类型的信息。
 在使用NSURLSessionConfiguration对象初始化会话对象之前,必须正确配置它。
 会话对象制作您提供的配置设置的副本,并使用这些设置配置会话。
 配置后,会话对象将忽略对NSURLSessionConfiguration对象所做的任何更改。
 如果需要修改传输策略,则必须更新会话配置对象,并使用它来创建新的NSURLSession对象。
 API中是这样对NSURLSessionConfiguration进行解释的
 */
         //获取一个默认的配置策略
        config = [NSURLSessionConfiguration defaultSessionConfiguration];
        //你必须在这里显式配置会话使用你自己的协议子类
        //否则,您看不到重定向<rdar:// problem / 17384498>。
        // 苹果爸爸的意思是必须配置否则重新定向时就没内容了
        config.protocolClasses = @[ self ];//把我们自定义的协议设置成这个策略的协议
        //进行初始化
        sDemux = [[QNSURLSessionDemux alloc] initWithConfiguration:config];
    });
    return sDemux;
}

虽然刚才说要看dataTaskWithRequest: delegate: modes:方法但是发现这个类方法中还有一个初始化方法,所以需要先看初始化方法.
看到这里其实我们已经完成一半的解读了因为这个Demo中核心的类只有4个
最重要的是CustomHTTPProtocol这个类,上面我们已经稍微解读了一下CanonicaalRequest这个类,这个类主要是用来处理我们的Request返回一个标准的符合规定的Request.然而我觉得没啥用啊。。 就是加一些请求头啥的接下来就是CacheStoragePolicy和QNSURLSessionDemux两个类了,接下来从QNSURLSessionDemux的初始化开始看

- (instancetype)init
{
//苹果爸爸为了健壮性把init的初始化也进行调用initWithConfiguration:的初始化方法
    return [self initWithConfiguration:nil];
}

- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration
{
    // 所以configuration也是有可能为空的
    self = [super init];
    if (self != nil) {
        if (configuration == nil) {//如果为空则再创建一个默认的配置策略。估计基本调用不到了
            configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        }
/*
@property (atomic, copy,   readonly ) NSURLSessionConfiguration *   configuration; 
因为把属性设置成这个鬼样子,只读并且是原子性的所以需要使用
self->_configuration的方式调用
*/
        self->_configuration = [configuration copy];
 //初始化一个存储任务信息的和任务ID的字典
        self->_taskInfoByTaskID = [[NSMutableDictionary alloc] init];
//又搞了个队列
        self->_sessionDelegateQueue = [[NSOperationQueue alloc] init];
//设置可同时执行的排队操作的最大数量为1
        [self->_sessionDelegateQueue setMaxConcurrentOperationCount:1];
//设置队列的名称
        [self->_sessionDelegateQueue setName:@"QNSURLSessionDemux"];
//创建具有指定的会话配置,委派和操作队列的会话。
        self->_session = [NSURLSession sessionWithConfiguration:self->_configuration delegate:self delegateQueue:self->_sessionDelegateQueue];
//设置个描述竟然和刚才的名字一样 不知道干嘛
        self->_session.sessionDescription = @"QNSURLSessionDemux";
    }
    return self;
}

接下来看dataTaskWithRequest: delegate: modes:

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
    NSURLSessionDataTask *          task;
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    //和上面一样如果Mode啥也没有 给添加个默认的
    if ([modes count] == 0) {
        modes = @[ NSDefaultRunLoopMode ];
    }
    //创建根据指定的URL请求对象检索URL内容的任务。
    task = [self.session dataTaskWithRequest:request];
    //根据Task,代理和Model创建一个Task信息
    taskInfo = [[QNSURLSessionDemuxTaskInfo alloc] initWithTask:task delegate:delegate modes:modes];    
    @synchronized (self) {
//加锁来保护内容的安全性,将task的taskIdentifier设置为字典的Key存储taskInfo信息
        self.taskInfoByTaskID[@(task.taskIdentifier)] = taskInfo;
    }    
    return task;
}

第三天

接下来看initWithTask:delegate:modes:方法 这个方法是在.m中创建了一个内部的类

- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
    self = [super init];
    if (self != nil) {//都是很简单的代码就不写注释了
        self->_task = task;
        self->_delegate = delegate;
        self->_thread = [NSThread currentThread];
        self->_modes = [modes copy];
    }
    return self;
}

这个类有2个方法4个属性,4个属性没啥说的

@interface QNSURLSessionDemuxTaskInfo : NSObject

- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes;

@property (atomic, strong, readonly ) NSURLSessionDataTask *        task;
@property (atomic, strong, readonly ) id<NSURLSessionDataDelegate>  delegate;
@property (atomic, strong, readonly ) NSThread *                    thread;
@property (atomic, copy,   readonly ) NSArray *                     modes;

- (void)performBlock:(dispatch_block_t)block;

- (void)invalidate;

@end
//制空的方法
- (void)invalidate
{
    self.delegate = nil;
    self.thread = nil;
}
//接下来看看下面2个方法是干嘛的
- (void)performBlock:(dispatch_block_t)block
{
//可以使用此方法将消息传递到应用程序中的其他线程。在这种情况下,消息是要在目标线程上执行的当前对象的方法。
    //参数分别为  执行方法;选择线程;Object则代表传递的参数;如果当前线程和目标线程相同,并且您为此参数指定了YES,则会立即执行选择器。如果指定NO,此方法将消息排队并立即返回,而不管线程是相同还是不同;最后是Runloop的哪种执行模式下
    [self performSelector:@selector(performBlockOnClientThread:) onThread:self.thread withObject:[block copy] waitUntilDone:NO modes:self.modes];
}
//调用的方法
- (void)performBlockOnClientThread:(dispatch_block_t)block
{
    block();
}

接下来是一大波的方法,都是URLSession的回调

//第一个不是回调但是是每个回调都会用到的方法,这个方法主要是返回刚才存储的TaskInfo
- (QNSURLSessionDemuxTaskInfo *)taskInfoForTask:(NSURLSessionTask *)task
{
    QNSURLSessionDemuxTaskInfo *    result;
         @synchronized (self) {
//取出字典中存储的taskInfo类
        result = self.taskInfoByTaskID[@(task.taskIdentifier)];
    }
    return result;
}
/* 
 告诉代理远程服务器请求HTTP重定向。
 仅对缺省和临时会话中的任务调用此方法。 后台会话中的任务会自动跟随重定向。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
//这个block将其中的代码段加到了队列之中等待执行
        [taskInfo performBlock:^{
//主要通过代理将协议方法回调到CustomHTTPProtocol类中..
//结果代码看起来很多结果下面的方法和这个一样 都是将HTTPURLSession的回调
//再通过回调的方法传递到CustomHTTPProtocol类中
            [taskInfo.delegate URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler];
        }];
    } else {
        completionHandler(newRequest);
    }
}//接下来主要看看其他的HTTPURLSession的回调都是干嘛用的
//请从委托凭证响应来自远程服务器的认证请求。  英语不好只能谷歌翻译了
/** 
 此方法处理任务级身份验证挑战。 NSURLSessionDelegate协议还提供了会话级身份验证委托方法。调用的方法取决于身份验证质询的类型:
 
 对于会话级挑战 - NSURLAuthenticationMethodNTLM,NSURLAuthenticationMethodNegotiate,NSURLAuthenticationMethodClientCertificate或NSURLAuthenticationMethodServerTrust - NSURLSession对象调用会话委托的URLSession:didReceiveChallenge:completionHandler:方法。如果您的应用程序不提供会话委托方法,那么NSURLSession对象将调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。
 
 对于非会话级挑战(所有其他),NSURLSession对象调用会话委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。如果您的应用程序提供了一个会话委托,并且您需要处理身份验证,那么您必须在任务级别处理身份验证或提供一个明确调用每会话处理程序的任务级处理程序。会话代理的URLSession:didReceiveChallenge:completionHandler:方法不是为非会话级的挑战调用。
 */

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
        }];
    } else {
//对挑战使用默认处理,就像未实现此委派方法一样。 将忽略提供的凭据参数。
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
    }
}
//当任务需要新的请求主体流发送到远程服务器时告诉代理。
/*
在两种情况下调用此委托方法:
如果任务是使用uploadTaskWithStreamedRequest创建的,则提供初始请求正文流:
如果任务由于认证质询或其他可恢复的服务器错误而需要重新发送具有正文流的请求,则提供替换请求主体流。
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:needNewBodyStream:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task needNewBodyStream:completionHandler];
        }];
    } else {
        completionHandler(nil);
    }
}
//定期向代表通知将正文内容发送到服务器的进度。
/*
 bytesSent
 自上次调用此委托方法以来发送的字节数。
 totalBytesSent
 到目前为止发送的字节总数。
 totalBytesExpectedToSend
 主体数据的预期长度。 URL加载系统可以通过三种方式确定上传数据的长度:
 
 从作为上传正文提供的NSData对象的长度。
 从作为上载任务(而不是下载任务)的上载主体提供的磁盘上的文件的长度。
 从请求对象中的Content-Length,如果您明确设置它。
 否则,如果您提供了流或主体数据对象,值为NSURLSessionTransferSizeUnknown(-1),如果没有提供,则值为零。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task didSendBodyData:bytesSent totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend];
        }];
    }
}
//告诉代理任务完成了数据传输。
/**
 不通过error参数报告服务器错误。 代理通过error参数接收的唯一错误是客户端错误,例如无法解析主机名或连接到主机。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:task];

   //这是我们的最后一个委托回调,所以我们删除了我们的任务信息记录。
    
    @synchronized (self) {
        [self.taskInfoByTaskID removeObjectForKey:@(taskInfo.task.taskIdentifier)];
    }

     //如果需要,调用委托。 在这种情况下,我们使客户端线程上的任务信息无效
     //在调用委托后,否则客户端线程端的-performBlock:代码就可以了
     //发现自己带有无效的任务信息。
    
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session task:task didCompleteWithError:error];
            [taskInfo invalidate];
        }];
    } else {
        [taskInfo invalidate];
    }
}
//告诉代理数据任务从服务器接收到初始答复(头)。
/**
 此方法是可选的,除非您需要支持(相对模糊的)multipart / x-mixed-replace内容类型。 使用该内容类型,服务器发送一系列部件,每个部件旨在替换以前的部件。 会话在每个部分的开始调用此方法,然后您应该根据需要显示,放弃或以其他方式处理上一部分。
 
 如果您不提供此委派方法,则会话始终允许任务继续。
 您的代码调用以继续传输的完成处理程序,传递常量以指示传输是作为数据任务继续还是应成为下载任务。
 completionHandler参数
 如果通过NSURLSessionResponseAllow,任务将正常继续。
 如果通过NSURLSessionResponseCancel,任务将被取消。
 如果您通过NSURLSessionResponseBecomeDownload作为处置,则调用代理的URLSession:dataTask:didBecomeDownloadTask:方法来为您提供取代当前任务的新下载任务。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
        }];
    } else {
        completionHandler(NSURLSessionResponseAllow);
    }
}
//告诉代理数据任务已更改为下载任务。
/**
  当代理的URLSession:dataTask:didReceiveResponse:completionHandler:方法决定将处置从数据请求更改为下载时,会话将调用此委派方法为您提供新的下载任务。 在此调用之后,会话委托不接收与原始数据任务相关的进一步委托方法调用。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didBecomeDownloadTask:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask];
        }];
    }
}
//告诉代理数据任务已收到一些预期的数据。
/**
 因为NSData对象通常从许多不同的数据对象拼凑而成,所以尽可能使用NSData的enumerateByteRangesUsingBlock:方法来遍历数据,而不是使用bytes方法(将NSData对象拉平到单个内存块中)。
 
 这个委托方法可以被多次调用,并且每个调用仅提供自从上一次调用以来接收的数据。 应用程序负责累积这些数据(如果需要)。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveData:data];
        }];
    }
}
//向代理询问数据(或上传)任务是否应将响应存储在缓存中。
/**
 在任务完成接收所有预期数据后,会话调用此委托方法。如果不实现此方法,则默认行为是使用会话配置对象中指定的缓存策略。此方法的主要目的是防止缓存特定的URL或修改与URL响应相关联的userInfo字典。
 
 仅当处理请求的NSURL协议决定缓存响应时,才调用此方法。通常,只有当以下所有条件都为真时,才会缓存响应:
 
 1请求是针对HTTP或HTTPS URL(或您自己的支持缓存的自定义网络协议)。
 
 2请求已成功(状态码位于200-299范围内)。
 
 3提供的响应来自服务器,而不是从缓存中。
 
 4会话配置的缓存策略允许缓存。
 
 5提供的NSURLRequest对象的缓存策略(如果适用)允许缓存。
 
 6服务器响应中与缓存相关的头(如果存在)允许缓存。
 
 7响应大小足够小以合理地适合缓存。 (例如,如果提供磁盘缓存,响应必须不大于磁盘缓存大小的大约5%)。
 proposedResponse
 默认缓存行为。 该行为基于当前高速缓存策略和某些所接收的报头(例如,Pragma和高速缓存控制报头)的值来确定。
 completionHandler
 您的处理程序必须调用的块,提供原始的建议响应,该响应的修改版本或NULL以防止缓存响应。 如果你的委托实现这个方法,它必须调用这个完成处理程序; 否则,您的应用程序泄漏内存。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    QNSURLSessionDemuxTaskInfo *    taskInfo;
    
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
        [taskInfo performBlock:^{
            [taskInfo.delegate URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
        }];
    } else {
        completionHandler(proposedResponse);
    }
}

好啦。这个类就全部解读完成。全部看完感觉大概,应该,可能是把所有的Task在当前线程添加一个队列的网络请求。一个一个来请求。。

接下来回到我们的HTTPProtocol类中继续看stopLoading

- (void)stopLoading
{
//我进去看了一下这个方法的实现。。我感觉是没啥用,也可能是我比较菜。
//和这些关联的方法我就不解读了有好几个 但是主要都是用来输出log的
    [self cancelPendingChallenge];
    if (self.task != nil) {
        [self.task cancel];
        self.task = nil;
    }
}

接下来开始看那些NSURLSession的回调是怎么处理的

为了方便查看还是把上面查询到的内容再复制一份到方法的上方

/* 
 告诉代理远程服务器请求HTTP重定向。
 仅对缺省和临时会话中的任务调用此方法。 后台会话中的任务会自动跟随重定向。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    NSMutableURLRequest *    redirectRequest;

    redirectRequest = [newRequest mutableCopy];
 //先移除请求中的标记
    [[self class] removePropertyForKey:kOurRecursiveRequestFlagProperty inRequest:redirectRequest];
 //发送以向URL加载系统指示协议实现已重定向。
    [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response];
    [self.task cancel];
 //当加载请求由于错误而失败时发送。
    [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
}
//请从委托凭证响应来自远程服务器的认证请求。这是干嘛的不懂。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
    BOOL        result;
    id<CustomHTTPProtocolDelegate> strongeDelegate;    
    strongeDelegate = [[self class] delegate];
    
    result = NO;
    if ([strongeDelegate respondsToSelector:@selector(customHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) {
        result = [strongeDelegate customHTTPProtocol:self canAuthenticateAgainstProtectionSpace:[challenge protectionSpace]];
    }

    if (result) {
        [self didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler];
    } else {
        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
    }
}
//告诉代理数据任务从服务器接收到初始答复
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    NSURLCacheStoragePolicy cacheStoragePolicy;

    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
//CacheStoragePolicy中实现了CacheStoragePolicyForRequestAndResponse这个函数主要是返回一个缓冲方式
//这个函数的判断方式基本和上面canInitWithRequest一样的判断方式
//一个变量反复判断最后给出一个类型
        cacheStoragePolicy = CacheStoragePolicyForRequestAndResponse(self.task.originalRequest, (NSHTTPURLResponse *) response);
    } else {//如果不是NSHTTPURLResponse类型则报错啦 所以基本下面的else不会执行
        assert(NO);
        cacheStoragePolicy = NSURLCacheStorageNotAllowed;
    }
    //发送以向URL加载系统指示协议实现已为请求创建了响应对象。
    //实现应使用提供的高速缓存存储策略来确定是否将响应存储在高速缓存中。
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:cacheStoragePolicy];    
    completionHandler(NSURLSessionResponseAllow);
}
//告诉代理数据任务已收到一些预期的数据。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
//NSURLProtocol子类实例协议在加载数据时将此消息发送到[协议客户端]。
    [[self client] URLProtocol:self didLoadData:data];
}
//向代理询问数据(或上传)任务是否应将响应存储在缓存中。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *))completionHandler
{

    completionHandler(proposedResponse);
}
//告诉代理任务完成了数据传输。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
   // NSURLSession委托回调。 我们把它传递给客户。
{

    if (error == nil) {
  //发送以向URL加载系统指示协议实现已完成加载。
        [[self client] URLProtocolDidFinishLoading:self];
    } else if ( [[error domain] isEqual:NSURLErrorDomain] && ([error code] == NSURLErrorCancelled) ) {
          //苹果解释如下
          //这发生在两种情况:
         // 在重定向期间,在这种情况下重定向代码已经告诉客户端失败
         // 如果请求被调用-stopLoading取消,在这种情况下客户端不会想知道失败
    } else {
        //当加载请求由于错误而失败时发送。
        [[self client] URLProtocol:self didFailWithError:error];
    }

    // We don't need to clean up the connection here; the system will call, or has already called, 
    // -stopLoading to do that.
}

好吧到这里基本上核心代码都解读完了。。不知道大家有没有看懂。。反正我自己是没太懂。

总结

CustomHTTPProtocol这个Demo中有4个核心类,其中CustomHTTPProtocol为主要控制的类,首先通过canInitWithRequest:来判断这次Request是否通过我们写的这个协议进行处理和判断,并且,我们拦截的Request需要打上标记 以免冲重复调用变成死循环,因为我们发起的请求会再次调用我们这次的方法,之后调用canonicalRequestForRequest:来设置我们的请求头或者更标准化我们的请求(其中的函数封装到了CanonicalRequest类中),之后进行初始initWithRequest:cachedResponse:client:初始化过后进行startLoading开始请求后,通过当前线程和当前Runloop进行初始化了QNSURLSessionDemux这个类,我觉得应该是把这些任务排列在了线程里面,通过QNSURLSessionDemux的代理方法再把Session的代理返回到CustomHTTPProtocol类中进行处理,处理时用到了CacheStoragePolicy类 主要作用是一个函数用来判断什么样的内容进行怎样的缓存机制。。好了大概这些。。谢谢大家看自言自语到这里。。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,174评论 11 349
  • AFHTTPRequestOperationManager 网络传输协议UDP、TCP、Http、Socket、X...
    Carden阅读 4,319评论 0 12
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,861评论 6 13
  • 文章转载:https://onevcat.com/2016/11/pop-cocoa-1/ (作者非常棒,建议大家...
    Buddha_like阅读 429评论 0 0