从实现下载来认识NSURLConnection

NSURLConnection简介

  • NSURLConnection是2003年随着第一版Safari的发布而发布的,它不单单是一个网络请求类,而是指代Foundation框架的URL系统中的一系列关联的组件:NSURLRequest、NSURLResponse、NSURLProtocol、NSHTTPCookieStorage、NSURLCredentialStorage以及同名类NSURLConnection。
  • 从iOS9开始,NSURLConnection中发送请求的两个方法已经过期(同步请求,异步请求),初始化网络连接的方法也被设置为过期,系统不再推荐使用,苹果建议使用NSURLSession发送网络请求。

简单下载

使用NSURLConnection实现简单下载只需三步

    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
    //创建请求对象request
    /*
     1. cachePolicy - 缓存策略
       - NSURLRequestUseProtocolCachePolicy = 0,
         -(常用)默认缓存策略,若使用requestWithURL方法,默认使用该缓存策略;它会根据HTTP头中的信息进行缓存处理,服务器可以在HTTP头中加入Expires和Cache-Control等来告诉客户端应该施行的缓存策略。
       - NSURLRequestReloadIgnoringLocalCacheData = 1,
         -(偶尔使用)顾名思义,忽略本地缓存,直接加载服务器数据
       - NSURLRequestReturnCacheDataElseLoad = 2,
         -(不用)一直尝试读取缓存数据,若没有缓存,才会去请求网络,该策略的重大缺陷是无法直到缓存的刷新时机。
       - NSURLRequestReturnCacheDataDontLoad = 3,
         - (不用)该策略之读取缓存数据,无论何时都不会进行网络请求。
     2. timeoutInterval - 超时时间 一般设置在15-30秒 AFNetworking中超时时间默认60s
     */
    NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
    //连接服务器,发送网络请求
    /*
     queue - 这里使用主线程还是子线程由执行的代码块决定
     该参数决定block代码块在哪个线程上执行,若block中有刷新UI的操作,则必须放在主线程上执行;若有一些耗时操作,则放在子线程上执行
     */
    //开始下载
    [NSURLConnection sendAsynchronousRequest:req queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        //下载完成,将数据写入磁盘
        /*
         atomically 原子属性,保证线程安全
         */
        [data writeToFile:@"/Users/jsby-yf007/Desktop/test.MOV" atomically:YES];
        NSLog(@"下载完成");
    }];

下载完成后,可以在命令行通过获取文件的MD5来验证文件是否下载完整
图1

上面代码在实际开发中所带来的问题

1.内存会暴涨,出现一个峰值
图2

出现图2的情况是因为NSURLConnection下载文件时,先是将整个文件下载到内存,然后再写入到沙盒,如果文件比较大,就会出现内存暴涨的情况。在执行

[data writeToFile:@"/Users/jsby-yf007/Desktop/test.MOV" atomically:YES];

这句代码的时候,data是整个文件的完整数据,在文件写入的过程中,data是存在于内存中的,然后一次性写入到本地,如此大的数据存入内存中,当然会出现内存暴增的情况,当写入完成后,系统会自动释放这些内存,所以会出现一个内存峰值。本例中的视频文件只有两百多兆,所以不会出现crash,但是要是下载一个十几个G的文件的时候,不用想,肯定crash!

2.没有下载进度以及暂停/继续

在实际开发中,要下载一个很大的文件,没有下载进度和暂停/继续,基本是不可能顺利的下载下来的,大大影响用户体验!

设置代理实现下载

使用代理实现进度跟进

  • 在响应方法中获取到文件总大小。
  • 在接收数据的方法中,根据每次接收到的数据长度计算数据的总进度。
    设置代理<NSURLConnectionDataDelegate>
//设置请求路径url
    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
    //添加请求request
    /*
     1. cachePolicy - 缓存策略
     - NSURLRequestUseProtocolCachePolicy = 0,
     -(常用)默认缓存策略,若使用requestWithURL方法,默认使用该缓存策略;它会根据HTTP头中的信息进行缓存处理,服务器可以在HTTP头中加入Expires和Cache-Control等来告诉客户端应该施行的缓存策略。
     - NSURLRequestReloadIgnoringLocalCacheData = 1,
     -(偶尔使用)顾名思义,忽略本地缓存,直接加载服务器数据
     - NSURLRequestReturnCacheDataElseLoad = 2,
     -(不用)一直尝试读取缓存数据,若没有缓存,才会去请求网络,该策略的重大缺陷是无法直到缓存的刷新时机。
     - NSURLRequestReturnCacheDataDontLoad = 3,
     - (不用)该策略之读取缓存数据,无论何时都不会进行网络请求。
     2. timeoutInterval - 超时时间 一般设置在15-30秒 AFNetworking中超时时间默认60s
     */
    NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
    //3.创建连接并设置代理
    NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
    //4.启动连接
    [conn start];

实现NSURLConnectionDataDelegate的几个代理方法

#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头  做一些准备工作
/*
 NSURLResponse
   - expectedContentLength 服务器给的预期数据长度 long long 类型
   - suggestedFilename 服务器建议保存的文件名称 NSString 类型
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.expectedContentLength = response.expectedContentLength;
    self.currentLength = 0;
}

//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    self.currentLength += data.length;
    float progress = (float)self.currentLength/self.expectedContentLength;
    NSLog(@"下载进度%f",progress);
}

//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"下载完成");
}

//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"出错了!!!");
}

为实现进度跟进,声明两个变量

/* 文件总大小 */
@property (nonatomic, assign) long long expectedContentLength;
/* 当前已下载的文件大小 */
@property (nonatomic, assign) long long currentLength;

以上代码即可实现下载进度跟进

使用代理实现数据保存

先拼接数据,再写入

这里我们先声明两个变量

/* 保存的目标路径 */
@property (nonatomic, copy) NSString *saveFilePath;
/* 保存的数据 */
@property (nonatomic, strong) NSMutableData *saveData;

接下来,我们在代理方法中实现数据的保存

#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头  做一些准备工作
/*
 NSURLResponse
   - expectedContentLength 服务器给的预期数据长度 long long 类型
   - suggestedFilename 服务器建议保存的文件名称 NSString 类型
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.expectedContentLength = response.expectedContentLength;
    self.currentLength = 0;
    //设置保存的目标路径
    self.saveFilePath = [@"/Users/jsby-yf007/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
}

- (NSMutableData *)saveData
{
    if (!_saveData) {
        _saveData = [[NSMutableData alloc] init];
    }
    return _saveData;
}

//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    self.currentLength += data.length;
    float progress = (float)self.currentLength/self.expectedContentLength;
    NSLog(@"下载进度%f",progress);
    //将获取到的数据拼接
    [self.saveData appendData:data];
}

//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    //将数据写入磁盘
    [self.saveData writeToFile:self.saveFilePath atomically:YES];
    //由于saveData为strong类型,使用完之后不会立即释放,故,需手动置nil
    self.saveData = nil;
    NSLog(@"下载完成");
}

//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"出错了!!!");
}

以上代码实现了数据的保存,但我们发现,内存依然会出现暴增
图3

由此我们可以推断,苹果的sendAsynchronousRequest异步方法内部也是通过这种方式来实现文件的保存

边下载,边保存

从上面的代码中我们发现,将数据统一拼接好后再写入依然会出现内存暴增的情况,所以,边下载,变保存不失为一个比较好的办法,因为每段数据的长度比较小,保存完之后,再释放这部分内存,顾不会出现内存暴增的情况!

1. 使用NSFileHandle实现

#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头  做一些准备工作
/*
 NSURLResponse
   - expectedContentLength 服务器给的预期数据长度 long long 类型
   - suggestedFilename 服务器建议保存的文件名称 NSString 类型
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.expectedContentLength = response.expectedContentLength;
    self.currentLength = 0;
    //设置保存的目标路径
    self.saveFilePath = [@"/Users/jsby-yf007/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
    /*
     在使用NSFileHandle进行文件保存的时候,若文件已存在,继续保存的话,数据将继续向后拼接;
     因此,这里采用比较粗暴的方式,直接删除已存在的文件(实际开发中不建议这么做)
     在实际开发中,我们可以使用NSFileManager对文件进行一系列的判断
     */
    [[NSFileManager defaultManager] removeItemAtPath:self.saveFilePath error:nil];
}

//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    self.currentLength += data.length;
    float progress = (float)self.currentLength/self.expectedContentLength;
    NSLog(@"下载进度%f",progress);
    //将获取到的数据拼接
    [self writeToFileWithData:data];
}

/*
 将数据写入文件 -- 将每段数据按顺序写入文件(拼接数据)
 NSFileManager - 文件管理器,主要功能:创建目录,检查目录或文件是否存在,删除目录或文件,遍历目录。。。 主要是针对文件的操作  类似于Mac中的Finder
 NSFileHandle - 文件“句柄”(文件指针)对同一文件二进制的读/写操作
 这里使用 NSFileHandle 来进行文件的写入
 */
- (void)writeToFileWithData:(NSData *)data
{
    /*
     NSFileHandle也是对文件指针的操作
     注意:当self.saveFilePath目录下的文件不存在时,fileHandleForWritingAtPath方法返回的NSFileHandle对象为nil,因此,我们在使用时,需要进行判断
     */
    NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.saveFilePath];
    //判断文件是否存在,NSFileHandle是对文件的操作,因此,我们先写入一段数据到磁盘
    if (fp == nil) {
        //如果文件不存,我们先执行写入操作
        [data writeToFile:self.saveFilePath atomically:YES];
    } else {
        //如果文件存在,将data追加到文件的末尾(拼接)
        /*
         NSFileHandle指针默认指向文件的起始位置,当我们需要追加数据的时候,首先我们需要将文件指针指向文件的末尾,这里,NSFileHandle为我们提供了一个方法seekToEndOfFile,可以将指针移向文件的末尾
         */
        [fp seekToEndOfFile];
        //写入文件 NSFileHandle 提供了写入文件的方法
        [fp writeData:data];
        //关闭 -- 在c语言开发中,关于文件的读、写操作,都会涉及到文件的打开和关闭;这里是为了文件数据的安全,同时不关闭打开的文件会占用系统资源
        [fp closeFile];
    }
}

//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"下载完成");
}

//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"出错了!!!");
}

以上是使用NSFileHandle来实现的数据写入操作,其中,data只是一个局部变量,使用完即释放,文件是分段写入,则不会出现内存暴增(图4)
图4

2. 使用NSOutputStream实现

NSOutputStream文件输出流写入文件的方式是,每段数据会自动向后追加,不需要像NSFileHandle一样操作指针来追加数据
先声明一个文件的输出流对象

/* 文件的输出流 */
@property (nonatomic, strong) NSOutputStream *fileStream;

代理中的实现

#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头  做一些准备工作
/*
 NSURLResponse
   - expectedContentLength 服务器给的预期数据长度 long long 类型
   - suggestedFilename 服务器建议保存的文件名称 NSString 类型
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.expectedContentLength = response.expectedContentLength;
    self.currentLength = 0;
    //设置保存的目标路径
    self.saveFilePath = [@"/Users/jsby-yf007/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
    /*
     在使用NSFileHandle进行文件保存的时候,若文件已存在,继续保存的话,数据将继续向后拼接;
     因此,这里采用比较粗暴的方式,直接删除已存在的文件(实际开发中不建议这么做)
     在实际开发中,我们可以使用NSFileManager对文件进行一系列的判断
     */
    [[NSFileManager defaultManager] removeItemAtPath:self.saveFilePath error:nil];
    
    //创建输出流 append(追加)
    self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.saveFilePath append:YES];
    //打开输出流
    [self.fileStream open];
}

//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    self.currentLength += data.length;
    float progress = (float)self.currentLength/self.expectedContentLength;
    NSLog(@"下载进度%f",progress);
    //将数据追加到文件流中
    /*
    第一个参数 uint8_t *类型  数据的传输都是通过二进制流的方式传输,uint8_t即8位也就是一个ASCII值,该参数是一个数组类型,NSData提供了一个属性bytes
    第二个参数 即数据长度
     */
    [self.fileStream write:data.bytes maxLength:data.length];
}

//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//    数据流写入完毕后,关闭输出流
    [self.fileStream close];
    NSLog(@"下载完成");
}

//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"出错了!!!");
}

NSURLConnection在多线程下的问题

在我们使用NSURLConnection的异步方法时,下载小文件没有问题,当我们下载大文件时,出现了内存暴增的问题,为解决此问题,我们使用了NSURLConnection的代理方法,但是我们在使用代理方法时,缺忽略了线程问题,那么接下来,问题来了,我们知道,NSURLConnection的代理默认是在主线程中执行的,但是,为了不阻塞UI,我们需要将执行放在子线程上,查看NSURLConnection的方法,我们发现,NSURLConnection提供了一个方法

- (void)setDelegateQueue:(nullable NSOperationQueue*) queue

灵机一动,我们可以在创建连接的之后,开始连接之前来设置一下DelegateQueue将其放入新建队列中也就是子线程中

[conn setDelegateQueue:[[NSOperationQueue alloc] init]];

但是!!!经过测试,我们发现,问题依然存在,测试发现,在下载过程中,UI的操作会阻塞下载
查看connectionWithRequest:方法的注释发现这么一句话

For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
翻译:为了使连接正常工作,调用线程的runloop必须在默认runloop模式下运行

也就是说,我们创建NSURLConnection连接是在哪个模式下运行,下载任务就在哪个线程
setDelegateQueue这个方法只是将代理方法中的任务放入了子线程中执行,下载任务仍然在主线程中!
接下来要如何解决这个问题呢???
我们首先想到的是,将整个下载任务放在子线程中

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"开始了");
        //1.设置请求路径url
        NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
        //2.创建请求
        NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
        //3.创建连接并设置代理
        NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
        //将代理任务放在子线程中执行
        [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
        //4.启动连接
        [conn start];
        NSLog(@"结束了");
    });

执行完上面的语句后,我们会发现,下载任务根本没执行;这个问题涉及到了一个知识点runloop!每个线程都有一个实际已经存在的runloop(运行循环)。但是,子线程的runloop默认不开启!
那解决方案出来了,我们可以手动来开启。这里使用coreFoundation框架CFRunLoopRef
1.首先声明一个CFRunLoopRef

/* 下载所在线程的runloop */
@property (nonatomic, assign) CFRunLoopRef downloadRunLoop;

2.启动runloop

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"开始了");
        //1.设置请求路径url
        NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
        //2.创建请求
        NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
        //3.创建连接并设置代理
        NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
        //将代理任务放在子线程中执行
        [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
        //4.启动连接
        [conn start];
        //5.启动runloop
        /*
         使用coreFoundation框架 中的 CFRunLoopRef
         其中有三个我们需要用到的方法
         CFRunLoopRun            启动当前线程的runloop
         CFRunLoopStop           停止指定线程的runloop
         CFRunLoopGetCurrent  拿到当前线程的runloop
         */
        //1.拿到当前线程的runloop
        self.downloadRunLoop = CFRunLoopGetCurrent();
        //2.启动runloop
        CFRunLoopRun();
        NSLog(@"结束了");
    });

3.停止runloop

//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//    数据流写入完毕后,关闭输出流
    [self.fileStream close];
    //停止下载线程所在的runloop
    CFRunLoopStop(self.downloadRunLoop);
    NSLog(@"下载完成");
}

到此,使用NSURLConnection实现下载已经基本实现。
本篇文章旨在学习NSURLConnection的原理,如有任何疑问或写的有问题的地方,欢迎大家留言,共同进步!下载中的断点续传功能将在下一篇关于NSURLSession的文章中进行详细描述。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,339评论 8 265
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,259评论 0 11
  • iOS面试题目100道 1.线程和进程的区别。 进程是系统进行资源分配和调度的一个独立单位,线程是进程的一个实体,...
    有度YouDo阅读 29,891评论 8 137
  • 监狱题材的电影不少,满足观众的猎奇心,正常人也不大可能想体验下监狱生活的,涉及这方面的题材都走暗黑风,要么也是很沉...
    诗影亮画阅读 285评论 0 0