2.1 请求相关及其问题
People Lack Willpower,Rather Than Strength!
1.简单请求.url中的多值参数问题
-
如下代码
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/weather?place=Beijing&place=Guangzhou&place=Shanghai"];
-
这里虽然多个value的key相同,但是是也不可以写成如下形式:
place=Beijing,Guangzhou,Shanghai
-
2.服务器返回数据的打印问题
-
已知通过NSString打印,查看data,是OK的,因为data转换为String时,完成了转码,不会出现中文显示问题,如下:
NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);// ✅ // 2015-09-12 23:51:32.594 06-多值参数与打印字典转码问题[6619:332762] {"weathers":[{"city":"Beijing","status":"晴转多云"},{"city":"Guangzhou","status":"晴转多云"},{"city":"Shanghai","status":"晴转多云"}]}
-
然而,如果我们想打印NSDictionary ,如果字典中有中文,打印效果不佳 ❌
NSLog(@"%@",dict); /* 2015-09-12 23:51:32.595 06-多值参数与打印字典转码问题[6619:332762] { weathers = ( { city = Beijing; status = "\U6674\U8f6c\U591a\U4e91"; }, { city = Guangzhou; status = "\U6674\U8f6c\U591a\U4e91"; }, { city = Shanghai; status = "\U6674\U8f6c\U591a\U4e91"; } ); } */
-
如果我们想很好的显示中文,需要重写NSDictionary的descriptionWithLocale方法;
-
同时,需要注意,NSArray打印中文也有问题,也需要重写;
#import <Foundation/Foundation.h> @implementation NSDictionary (nslog) - (NSString *)descriptionWithLocale:(id)locale { // 创建一个可变字符串用于描述字典打印样式和内容 NSMutableString *stringM = [NSMutableString string]; // 开头 [stringM appendString:@"{\n"]; // 内容,遍历字典中键值对,将他们拼接到字符串中 [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { [stringM appendString:[NSString stringWithFormat:@"\t%@ = %@,\n",key,obj]]; }]; // 末尾 [stringM appendString:@"}\n"]; if (self.allKeys.count) { // 删除字典-->字符串中最后一个逗号 NSRange range = [stringM rangeOfString:@"," options:NSBackwardsSearch]; [stringM deleteCharactersInRange:range]; } return stringM; } @end @implementation NSArray (nslog) - (NSString *)descriptionWithLocale:(id)locale { // 创建一个可变字符串用于描述字典打印样式和内容 NSMutableString *stringM = [NSMutableString string]; // 开头 [stringM appendString:@"(\n"]; // 遍历数组中的元素,将他们拼接到字符串中 [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [stringM appendString:[NSString stringWithFormat:@"\t%@,",obj]]; }]; // 末尾 [stringM appendString:@")\n"]; // 删除最后一个逗号 if (self.count) { NSRange range = [stringM rangeOfString:@"," options:NSBackwardsSearch]; [stringM deleteCharactersInRange:range]; } return stringM; } @end // 那么打印结果如下: /* 2015-09-12 23:59:03.169 06-多值参数与打印字典转码问题[6674:337315] { weathers = ( { status = 晴转多云, city = Beijing } , { status = 晴转多云, city = Guangzhou } , { status = 晴转多云, city = Shanghai } ) } */
-
3.请求还有哪些类型?
3.1文件下载
-
大小文件下载区别:
-
1.小文件由于体积比较小,所以可以一次性全部加载到内存,然后再写入硬盘;
-
2.而大文件则不然,由于体积较大如果一次性都加载到内存,再写入硬盘显然是不行的,所以需要下一点,往硬盘里存一些!
-
3.1.1小文件下载
// 小文件下载
//1.直接使用url下载
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_02.png"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
//2.发请求
// 1.url
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_02.png"];
// 2.request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 3.sendrequest
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
UIImage *image = [UIImage imageWithData:data];
NSLog(@"%@",image);
}];
3.1.2大文件下载
-
对于大文件下载,由于资源较大,我们需要使用代理方法,在代理方法中逐段接收下载data!同时,文件较大时,我们应该将文件逐段写入硬盘,而不是存入内存,这样可以减少内存压力!
-
常见的文件操作方法
-
文件句柄
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 1.使用http get方法发送请求 NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"]; // 2.创建request NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 3.NSURLConnection发送请求,由于资源较大,我们需要使用代理方法分块下载 [NSURLConnection connectionWithRequest:request delegate:self]; } #pragma mark - NSURLConnectionDataDelegate // 接收到服务器响应时调用 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { // 1.获得下载文件存储路径 NSString *path = [response.suggestedFilename cacheDir]; self.path = path; // 2.创建一个空文件 NSFileManager *manager = [NSFileManager defaultManager]; if ([manager createFileAtPath:self.path contents:nil attributes:nil]) { NSLog(@"创建成功"); } // 3.懒加载文件句柄对象,设置句柄操作文件写入方式 // 这样能保证,每次写入到文件中的数据是从紧挨着有数据的空区域开始的!❤️ [self.handle seekToEndOfFile]; // 记录服务器反馈文件总大小 self.totalLength = response.expectedContentLength; } // 接收到服务器的数据时调用(可能调用一次或者多次) - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { // 4.使用文件句柄写入data到空文件中 [self.handle writeData:data]; // 实时计算下载进度 self.currentLength += data.length; self.progressView.progress = 1.0 * self.currentLength / self.totalLength; } // 结束加载时,调用 - (void)connectionDidFinishLoading:(NSURLConnection *)connection { // 5.结束时,记得要关闭文件和句柄 [self.handle closeFile]; self.handle = nil; } // 懒加载句柄 - (NSFileHandle *)handle { if (!_handle) { _handle = [NSFileHandle fileHandleForWritingAtPath:self.path]; } return _handle; }
-
输出流
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 1.使用http get方法发送请求 NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"]; // 2.创建request NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 3.NSURLConnection发送请求,由于资源较大,我们需要使用代理方法分块下载 [NSURLConnection connectionWithRequest:request delegate:self]; } /*==============================代理方法==================================*/ #pragma mark - NSURLConnectionDataDelegate // 接收到服务器响应时调用 (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { // 1.获得下载文件存储路径 NSString *path = [response.suggestedFilename cacheDir]; self.path = path; // 记录服务器反馈文件总大小 self.totalLength = response.expectedContentLength; } // 接收到服务器的数据时调用(可能调用一次或者多次) -(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { // 2.使用输出流写入data到空文件中 /* 参数一:需要写入的数据 参数二:写入数据的大小 */ [self.outputStream write:data.bytes maxLength:data.length]; // 实时计算下载进度 self.currentLength += data.length; self.progressView.progress = 1.0 * self.currentLength / self.totalLength; } // 结束加载时,调用 -(void)connectionDidFinishLoading:(NSURLConnection *)connection { // 3.结束时,记得要关闭文件和输出流 [self.outputStream close]; self.outputStream = nil; } /*==============================代理方法==================================*/ // 懒加载输出流 - (NSOutputStream *)outputStream { if (!_outputStream) { /* 参数一:表示要输出到哪里; 参数二:表示每次输出都是拼接到新下数据后面 */ _outputStream = [NSOutputStream outputStreamToFileAtPath:self.path append:YES]; // 注意:如果想利用输出流写入数据,一定要打开输出流!!⚠️ //如果数据流打开的文件不存在, 那么会自动创建个新的 [_outputStream open]; } return _outputStream; }
- 对比二者,可见输出流略胜一筹!
-
3.1.3大文件断点下载
-
关键: 如果想进行断点下载,我们需要在请求头中设置文件下载的range,告诉服务器,我们需要从文件的哪个位置开始下载❤️
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 1.使用http get方法发送请求 NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"]; // 2.创建request NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; #warning 设置请求头,以便断点下载🆘 // 设置path self.path = [@"minion_01.mp4" cacheDir]; // 设置每次下载时currentLength初始值 self.currentLength = [self fileSize:self.path]; // 设置请求头参数 /* 参数一:只要设置HTTP请求头的Range属性, 就可以实现从指定位置开始下载 表示头500个字节:Range: bytes=0-499 表示第二个500字节:Range: bytes=500-999 表示最后500个字节:Range: bytes=-500 表示500字节以后的范围:Range: bytes=500- */ // 设置每次下载range // 注意这里一定要转换为integerValue ❤️🈲 NSString *range = [NSString stringWithFormat:@"bytes:%zd-",self.currentLength]; [request setValue:range forHTTPHeaderField:@"Range"]; // 3.NSURLConnection发送请求,由于资源较大,我们需要使用代理方法分块下载 [NSURLConnection connectionWithRequest:request delegate:self]; } //==================================================== // fileSize:方法 - (NSUInteger)fileSize:(NSString *)path { NSFileManager *manager = [NSFileManager defaultManager]; NSDictionary *attrDict = [manager attributesOfItemAtPath:path error:nil]; // 注意:第一次调用该方法时,由于还未开始下载,所以没有文件,是null,通过字典取出来的size也是空,服务器不识别,服务器不能默认文件大小是0,只能我们自己转换为integerValue return [attrDict[NSFileSize] integerValue]; } // cacheDir方法 - (NSString *)cacheDir { // 1.获取cache目录 NSString *dir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; return [dir stringByAppendingPathComponent:[self lastPathComponent]]; }
3.2文件上传
3.2.1复杂的文件上传格式
-
设置请求头
[request setValue:@"multipart/form-data; boundary=分割线" forHTTPHeaderField:@"Content-Type"];
-
代码
// 1.使用HTTP中的post方法发送请求 NSURL *urlPost = [NSURL URLWithString:@"http://120.25.226.186:32812/upload"]; // 2.创建可变request NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:urlPost]; // 1⃣️设置上传请求头/请求方法(默认是get) requestM.HTTPMethod = @"POST"; NSString *contentType = @"multipart/form-data; boundary=----seperator"; // multipart/form-data表示请求为上传请求; boundary=分割线,可以使任意中文 [requestM setValue:contentType forHTTPHeaderField:@"Content-Type"];
-
-
设置请求体
-
文件参数
文件上传.png--分割线\r\n Content-Disposition: form-data; name="参数名"; filename="文件名"\r\n Content-Type: 文件的MIMEType\r\n \r\n 文件数据 \r\n
-
代码
// 2⃣️设置请求体 NSMutableData *httpBody = [NSMutableData data]; // 设置文件参数 [httpBody appendData:[@"------seperator" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; /* \r\n区别❤️ >\r是将当前位置移到本行开头,而\n是换行 严格点说, 如果只用\r, 则将当前位置移到本行开头, 但是不换行. 如果只用\n, 则换行, 但当前位置不变. 用C编程的话, 如果用标准输出(printf)或文本方式打开的写文件(fprintf), 用\n足够了, 这不是因为二者作用相同, 而是C的库函数会在\n前自动加上一个\r. 用其他语言编程必须注意这个问题. */ /* name:对应服务端接收的字段类型(服务端参数的名称) filename:告诉服务端当前的文件的名称(就是告诉服务端用什么名称保存当前上传的文件) */ [httpBody appendData:[@"Content-Disposition: form-data; name=\"file\"; filename=\"videos.plist\"" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"Content-Type: application/octet-stream" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; UIImage *image = [UIImage imageNamed:@"abc"]; NSData *imageData = UIImagePNGRepresentation(image); [httpBody appendData:imageData]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
-
-
非文件参数
--分割线\r\n Content-Disposition: form-data; name="参数名"\r\n \r\n 参数值 \r\n
-
代码
// 设置非文件参数 [httpBody appendData:[@"------seperator" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; // name : 对应服务端接收的字段类型(服务端参数的名称) [httpBody appendData:[@"Content-Disposition: form-data; name=\"username\"" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"PJ" dataUsingEncoding:NSUTF8StringEncoding]]; [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
-
-
结束标记
参数结束的标记 --分割线--\r\n // 最后一个分割线可有可无
-
代码
// 设置结束符号 [httpBody appendData:[@"------seperator--" dataUsingEncoding:NSUTF8StringEncoding]]; //=====================上传================= // 如果是普通的请求体,直接将参数转换为二进制就OK了!❤️上传文件请求体蛋疼.... requestM.HTTPBody = httpBody; // 3.sendAsync..request [NSURLConnection sendAsynchronousRequest:requestM queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); }];
-
-
-
注意事项
-
请求体
比请求头
分割线前面多两个--
-
结束标记
比请求体
后面多两个--
-
3.2.2上传文件格式简化
- 宏的抽取o(╯□╰)o
4.其他
4.1压缩与解压
-
框架:ZipArchive
- 下载地址:https://github.com/ZipArchive/ZipArchive
- 需要引入libz.dylib框架
- 需要导入头文件Main.h
-
压缩
- 1.creatZipWithPaths
- 2.creatZipWithDir
//1. createZipWithPaths /* 参数一:创建的压缩路径 参数二:需要压缩的文件路径数组 */ NSArray *arr = @[ @"/Users/PlwNs/Desktop/Snip20150908_1.png", @"/Users/PlwNs/Desktop/Snip20150908_2.png", @"/Users/PlwNs/Desktop/Snip20150908_3.png" ]; if ([Main createZipFileAtPath:@"/Users/PlwNs/Desktop/pj.zip" withFilesAtPaths:arr]) { NSLog(@"压缩成功"); } //2. createZipWithDir /* 参数一:创建的压缩路径 参数二:需要压缩的文件夹路径 */ if ([Main createZipFileAtPath:@"/Users/PlwNs/Desktop/pj.zip" withContentsOfDirectory:@"/Users/PlwNs/Desktop/pj"]) { NSLog(@"压缩成功"); }
-
解压
-
unzipFileAtPath
/* 参数一:需要解压的文件 参数二:解压路径 */ if ([Main unzipFileAtPath:@"/Users/PlwNs/Desktop/pj.zip" toDestination:@"/Users/PlwNs/Desktop/pj"]) { NSLog(@"解压成功"); } // 注意: 如果利用cocoaPods集成, 名称叫做:SSZipArchive (0.3.2)
-
4.2MIMETYPE
-
为什么要获得MIME
-
文件上传时需要用到MIMEType(Content-Type)
// 拼接请求体时,用到MIMEType [httpBody appendData:[@"Content-Type: application/octet-stream" dataUsingEncoding:NSUTF8StringEncoding]];
-
文件的类型不一样, 那么content-type的值也不一样
- 如果不知道数据时什么类型, 直接传application/octet-stream(万能类型)即可
- 开发中为了节省资源,尽量要获得真实的MIMETYPE的!❤️
-
-
如何获得MIMEType
- (NSString *)MIMETypeWithPath:(NSString *)path { // 1.url //注意这里个是绝对路径.不可以用来获取本地文件信息 // NSURL *url = [NSURL URLWithString:path]; NSURL *url = [NSURL fileURLWithPath:path]; // 本地资源路径 // 2.request NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 3.sendSync NSURLResponse *response = nil; [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil]; // 4.return mime return response.MIMEType; }
| 类型 | 文件拓展名 | MIMEType |
| ---- | ---------------- | ---------------------- |
| 图片| png | image/png |
| ... | bmp\dib | image/bmp |
| ... | jpe\jpeg\jpg | image/jpeg |
| ... | gif | image/gif |
| 多媒体 | mp3 | audio/mpeg |
| ... | mp4\mpg4\m4vmp4v | video/mp4 |
| 文本 | js | application/javascript |
| ... | pdf | application/pdf |
| ... | text\txt | text/plain |
| ... | json | application/json |
| ... | xml | text/xml |
### 4.3NSURLConnection与NSRunloop的关系
- 测试发送请求是异步or同步, 回调方法在哪个线程执行
- 1.请求是异步发送;
- 2.回调方法在主线程中执行.
``` objective-c
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 1.创建一个get请求的url
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"];
// 2.request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 3.发送请求
// [NSURLConnection connectionWithRequest:request delegate:self];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
NSLog(@"%s",__func__);
/*有结果可见:
1.由于还未下载完,就打印-[ViewController touchesBegan:withEvent:],所以是异步发送请求;
2.代理方法(回调方法)在主线程中执行,系统默认,考虑到我们可能会在这些回调方法中刷新UI,所以默认安排在主线程;
2015-09-08 16:44:39.259 15-NSURLConnection和NSRunLoop[4493:1058916] -[ViewController touchesBegan:withEvent:]
2015-09-08 16:44:39.413 15-NSURLConnection和NSRunLoop[4493:1058916] <NSThread: 0x7f8d6b406870>{number = 1, name = main}
.........
*/
}
//===== #pragma mark - NSURLConnectionDataDelegate ========
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(@"%@",[NSThread currentThread]);
}
......
-
更改回调方法到子线程中执行
// 1.创建一个get请求的url NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"]; // 2.request NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 3.发送请求 // [NSURLConnection connectionWithRequest:request delegate:self]; NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; // 更改回调方法执行的线程->子线程❤️ [conn setDelegateQueue:[[NSOperationQueue alloc] init]]; // 4.开始发送请求 // 如果创建完NSURLConnection,就直接发送请求,而后再将回调方法加入子线程,显然这是不合逻辑的! [conn start]; /*结果可见: 2015-09-08 17:04:48.732 15-NSURLConnection和NSRunLoop[4638:1067036] <NSThread: 0x7fe22a54c4d0>{number = 3, name = (null)} 2015-09-08 17:04:48.733 15-NSURLConnection和NSRunLoop[4638:1067037] <NSThread: 0x7fe22a62d0b0>{number = 4, name = (null)} 2015-09-08 17:04:48.736 15-NSURLConnection和NSRunLoop[4638:1067036] <NSThread: 0x7fe22a54c4d0>{number = 3, name = (null)} ............. */
- 疑问:NSURLConnection对象属于局部变量,为什么还可以让其代理不断的去执行代理回调方法?😖
- 因为:initWithRequest:/ connectionWithRequest: 等方法创建出来NSURLConnection对象后,系统会默认将其加入当前NSRunloop中,所以NSURLConnection对象不会被销毁;
- 疑问:NSURLConnection对象属于局部变量,为什么还可以让其代理不断的去执行代理回调方法?😖
-
证明系统会默认将创建出来 NSURLConnection对象加入currentRunloop!
-
设计思路: 在子线程中创建NSURLConnection对象,如果系统默认会将该对象加入currentRunloop, 就会造成代理方法没办法执行. 因为子线程默认是不开启RunLoop.❤️
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 子线程中发送请求 dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 1.创建一个get请求的url NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"]; // 2.request NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 3.发送请求 // 手动创建RunLoop! // 这里因为发送了请求,无需再设置source❤️ NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; // [NSURLConnection connectionWithRequest:request delegate:self]; [[NSURLConnection alloc] initWithRequest:request delegate:self]; [runLoop run]; /* 手动开启RunLoop结果 2015-09-08 17:15:59.761 15-NSURLConnection和NSRunLoop[4750:1072239] <NSThread: 0x7fc069f47aa0>{number = 3, name = (null)} 2015-09-08 17:15:59.762 15-NSURLConnection和NSRunLoop[4750:1072239] <NSThread: 0x7fc069f47aa0>{number = 3, name = (null)} 2015-09-08 17:15:59.763 15-NSURLConnection和NSRunLoop[4750:1072239] <NSThread: 0x7fc069f47aa0>{number = 3, name = (null)} ....... */ // 无打印 /* 因为:❤️❤️ 1.对于由 initWithRequest: connectionWithRequest: 创建出来的NSURLConnection对象,系统默认会把他加入到当前线程; 2.如果当前线程为子线程,那么没有开启NSRunLoop,所以,NSURLConnection对象直接死掉,无法持续下载; 3.但是如果使用start开启NSURLConnection对象请求, 那么系统会将NSURLConnection添加到当前线程runloop的默认模式下, 如果当前线程的runloop不存在, 那么系统内部会自动创建一个! 4.如果没有start,那么系统不会自动创建NSRunloop,需要手动创建,否则无法正常下载. */ });
-
}
```