iOS面试总结
1. 网络
-
HTTP协议(HyperText Transfer Protocol)的请求和响应
- 请求: 请求头, 请求行, 请求体
- 请求行: 指定请求方法, 请求路径 协议版本等信息
- 请求头: 描述客户端环境, 例如: host要请求的主机地址, UserAgent客户端类型, Accept 可接受数据类型, Accept_language可接受语言
- 请求体: 客户端要发送的具体数据, 例如上传时的上传数据
- 响应: 状态行,响应头, 响应数据
- 状态行: 包含http协议版本, 状态码
- 响应头: 服务器描述, 数据类型描述例如 Sever, Content-Type
- 响应数据: 请求所获得的后台数据
- 请求: 请求头, 请求行, 请求体
-
HTTP的特点
无状态: 服务器不会保留客户端状态, 不会记忆上次的状态, 不受前面请求的影响, 客户端每次请求独立, 每次请求需带上自己的状态
-
持久链接: 每个链接可以处理多个事务, 特殊强调http1.0及其之前版本都是非持久链接(每个链接只能处理一个事务)
持久链接如何结束?
- 如果有Content-length字段, 可以判断接受数据是否完成,
- 如果是分块传输, 响应头中不包含Content-length, 服务器传输完成万发送一个空数据块, 当客户端收到空数据块时算作数据接受完成
-
HTTP的请求方式
GET、POST、PUT、DELETE、HEAD、OPTIONS等
-
GET和和POST的区别:
get请求参数拼接在url后面, post的参数拼接在请求体里面,相对安全(在http抓包情况下依然不安全)
get参数长度有限制2048, post无限制
get获取资源是安全的(不会修改服务器资源), 幂等的(多次执行结果相同), 可缓存的(可以直接有CDN缓存, 减轻服务器负担), post相反
-
HTTP常用状态码含义 参考文档点击 HTTP状态码总结
-
1xx 表示消息
- 100 初始的请求已经接受,客户应当继续发送请求的其余部分, 例如post请求(两段)的第一段请求收到的就是100
- 101 服务器将遵从客户的请求转换到另外一种协议
-
2xx 表示成功
- 200: OK, 完全正常
3xx 表示重定向
-
4xx 表示请求错误
- 400 请求出现语法错误
- 401 访问收到密码保护的页面
- 403 服务器拒绝执行,
- 404 服务器资源未找到
- 405 请求方法(get, post等)对指定资源不适用,
-
5xx 表示服务器错误
- 500 服务器遇到意料不到的错误, 不能完成客户端请求
-
-
TCP和UDP的特点, 参考链接 简单理解TCP/IP协议
-
TCP协议(Transport Control Protocol,传输控制协议): 是一种面向连接、可靠的、基于字节流的传输层协议,采用了确认机制、超时重传机制,还会对接收到的TCP报文段进行重新排列整理。(TCP报头含20字节定长、选项和填充<选项和填充小于等于40字节>)(TCP是一种面向连接的传输层协议。它可以保证两端通信主机之间的通信可达。TCP能够正确处理传输过程中丢包、传输顺序乱掉等异常情况。)
TCP能保证可靠性、稳定性, 适用于可靠性较高的服务
-
UDP协议:(User Datagram Protocol,用户数据报协议)是一种不可靠无连接的传输层协议,不考虑流控制、错误控制,没有重传机制,不会对分组进行顺序检查和排序。
UDP控制选项少,无须建立连接,从而使得数据传输过程中的延迟小、数据传输效率高, 适用于实时性要求高的程序
-
-
网络七层模型
应用层, 表示层, 会话层, 传输层, 网络层, 数据链路层, 物理层
- 应用层: 网络应用如: http ftp, pop, smtp等应用协议
- 表示层: 定义数据格式及加密, 例如,FTP允许你选择以二进制或ASCII格式传输。如果选择二进制,那么发送方和接收方不改变文件的内容。如果选择ASCII格式,发送方将把文本从发送方的字符集转换成标准的ASCII后发送数据。在接收方将标准的ASCII转换成接收方计算机的字符集。示例:加密,ASCII等。对应网络协议: Telnet, Rlogin, SNMP, Copher等
- 会话层: 它定义了如何开始、控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时可以通知应用,从而使表示层看到的数据是连续的,在某些情况下,如果表示层收到了所有的数据,则用数据代表表示层。示例:RPC,SQL等
- 传输层: 这层的功能包括是否选择差错恢复协议还是无差错恢复协议,及在同一主机上对不同应用的数据流的输入进行复用,还包括对收到的顺序不对的数据包的重新排序功能。示例:TCP,UDP,SPX。
- 网络层: 这层对端到端的包传输进行定义,它定义了能够标识所有结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。示例:IP,IPX, UUCP等。
- 数据链路层: 它定义了在单个链路上如何传输数据。这些协议与被讨论的各种介质有关。示例:ATM,FDDI等。
- 物理层: OSI的物理层规范是有关传输介质的特性,这些规范通常也参考了其他组织制定的标准。连接头、帧、帧的使用、电流、编码及光调制等都属于各种物理层规范中的内容。物理层常用多个规范完成对所有细节的定义。示例:Rj45,IEEE 802.3 等。
-
Socket
客户端: socket()->connect()->write()/read()->close()
服务端: socket()->bind()->listen()->accept()->read()/write()->close()
-
数据解析:
- JSON: 使用NSJSONSerialization解析
-
XML: SAX解析, DOM解析
- SAX: NSXMLParser 实现代理方法解析, 特点从上往下依次解析
- DOM: 特点一次性读取解析
-
网络安全
Base64加密, 可逆加密
MD5/SHA1/SHA256等 不可逆加密
-
对称加密和非对称加密
对称加密: 加解密使用同一个秘钥, 代表: DES, AES等, 有点效率高, 缺点, 秘钥交换时安全不能保障
非对称加密: 加解密双方使用不同的秘钥, 加密使用公钥加密, 解密是用私钥解密, 公钥是公开的 代表: RSA, ECC, DSA(数字签名用)
-
HTTPS
-
HTTPS中的S含义
S代表的是SSL/TLS协议, 即: Secure Sockets layer(安全套接层)和Transport Layer Security(安全传输层)
-
HTTPS的建立流程
HTTPS为了兼顾安全与效率,同时采用了对称加密算法和非对称加密算法, 通信过程主要会设计三个秘钥: 服务器端的公钥和私钥, 用来进行非对称加密, 客户端随机生成的随机秘钥, 用来进行对称加密
- 客户端访问HTTPS链接, 告诉服务器客户端支持的加密算法列表, 和随机数C
- 服务器选择一种对称算法(如AES), 一种非对称算法, 一种MAC算法发送给客户端, 同时把数字证书和随机数S发给客户端
- 客户端验证服务器的数字证书, 客户端生成前主秘钥, 并使用服务器的公钥加密发送个服务器, 并使用前主秘钥/随机数C/随机数S, 生成会话秘钥
- 服务器解密得到前主秘钥, 通过前主秘钥/随机数C/随机数S生成会话秘钥
- 数据加密传输
-
-
NSURLSession
-
NSURLSession的优势
- 支持后台上传下载
- 可以暂停, 停止, 重启网络任务
- 可以对缓存策略,session类型、任务类型(比如上传、下载等任务)进行单独的配置
- 支持block回调使用方便
- 支持IPV6网络
-
NSURLsession的使用
NSURLsession 是一个管理类, 可以通过NSURLSessionConfiguration进行配置
URLSessionTask是任务的父类, 包含两个子类URLSessionDataTask和URLSessionDownloadTask, 其中URLSessionDataTask有一个子类URLSessionUploadTask, URLSessionDataTask用来处理一般网络请求
URLSessionDownloadTask处理下载任务, URLSessionUploadTask上传任务
-
2. 多线程
-
iOS开发常用的多线程方式
- pthread: C语言实现, 可以跨平台, 线程生命周期需要手动管理
- NSThread: OC实现, 线程生命周期需要手动管理
- GCD: 苹果对多核性能优化, C语言, 线程生命周期自动管理
- NSOperation: 对GCD封装, 线程生命周期自动管理
-
NSThread简单介绍
创建线程对象: 显示创建(alloc init)和隐式创建(performSelector.....)
线程状态:新建, 就绪(start), 阻塞(sleep), 运行, 死亡(exit)
常用属性: name(当前线程名字), threadPriority(线程优先级), isMainThread等
-
GCD和NSOperation对比
- GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
- GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
- NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
- NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceled)
-
什么情况下会出现死锁
-
在主线程中将同步任务添加到主队列会导致死锁
- (void)viewDidLoad { [super viewDidLoad]; dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"deallock"); }); // Do any additional setup after loading the view, typically from a nib. }
-
在一个串行队列任务中将 同步任务添加到当前的队列中
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ dispatch_sync(serialQueue, ^{ NSLog(@"deadlock"); }); });
-
-
GCD中的线程栅栏
dispatch_barrier_async/dispatch_barrier_sync 分别表示同步栅栏和异步栅栏
栅栏的作用是可以将任务分块执行, 条件是任务必须在同一个队列上, 否则无效
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT); for (NSInteger i = 0; i < 10; i++) { dispatch_async(concurrentQueue, ^{ NSLog(@"%zd",i); }); } dispatch_barrier_async(concurrentQueue, ^{ NSLog(@"barrier"); }); NSLog(@"哈哈哈"); for (NSInteger i = 10; i < 20; i++) { dispatch_async(concurrentQueue, ^{ NSLog(@"%zd",i); }); } 执行结果: 哈哈哈 0-9 乱序打印 barrier 10-19乱序打印
由于采用的是异步栅栏, 所以栅栏后的任务会"哈哈哈"会提起执行, 如果将栅栏换成同步栅栏则 "哈哈哈"一定是在"barrier"之后执行
通过线程栅栏可以实现多度单写, 即允许多个地方同时读取数据, 但是在写入数据时只允许一个地方写入
- (id)readDataForKey:(NSString *)key {
__block id result;
dispatch_sync(_concurrentQueue, ^{
result = [self valueForKey:key];
});
return result;
}
- (void)writeData:(id)data forKey:(NSString *)key {
dispatch_barrier_async(_concurrentQueue, ^{
[self setValue:data forKey:key];
});
}
-
GCD线程组的使用
需求多个网络请求完成之后一并刷新UI
- (void)testGCDGroup { __block NSInteger number = 0; dispatch_group_t group = dispatch_group_create(); //A耗时操作 dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(3); number += 2222; NSLog(@"A:%zd", number); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self sendRequestWithCompletion:^(id response) { number += [response integerValue]; NSLog(@"B:%zd", number); }]; }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self sendRequestWithCompletion:^(id response) { number += [response integerValue]; NSLog(@"C:%zd", number); }]; }); // //B网络请求 // dispatch_group_enter(group); // [self sendRequestWithCompletion:^(id response) { // number += [response integerValue]; // NSLog(@"B:%zd", number); // dispatch_group_leave(group); // // }]; // // //C网络请求 // dispatch_group_enter(group); // [self sendRequestWithCompletion:^(id response) { // number += [response integerValue]; // NSLog(@"C:%zd", number); // dispatch_group_leave(group); // }]; // dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"finish: %zd", number); }); } - (void)sendRequestWithCompletion:(void (^)(id response))completion { //模拟一个网络请求 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ sleep(2); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(@1111); }); }); } 调用结果: B:1111 C:2222 A:4444 finish: 4444
使用方式如代码所示, 先创建线程组, 然后使用dispatch_group_async, 将任务绑定到线程组, 最后使用dispatch_group_notify接受线程组任务完成的回调. 其中也可以使用dispatch_group_enter(group)这种方式将任务添加到线程组, 不过任务结束之后要配合使用 dispatch_group_leave(group);
-
Dispatch Semaphore 信号量
Dispatch Semaphore 提供了三个函数
- dispatch_semaphore_create: 创建一个Semaphore并初始化总量
- dispatch_semaphore_signal: 发送信号, 让信号总量加1
- dispatch_semaphore_wait: 可以是信号总量减1, 信号总量为0是进入等待
Dispatch Semaphore 在实际开发中主要用于:
-
保持线程同步,将异步执行任务转换为同步执行任务
- (void)testSemaphore { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); __block NSInteger number = 0; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ number = 100; NSLog(@"执行异步任务"); dispatch_semaphore_signal(semaphore); }); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"semaphore---end,number = %zd",number); } //打印结果如下: //执行异步任务 //semaphore---end,number = 100
创建信号量初始化总量为0, 由于是异步执行,所以直接执行到dispatch_semaphore_wait, 如果信号量为0则一直等待阻断线程, 当异步任务执行完之后将信号量加1, 此时信号量等待函数终结, 继续执行后面的任务
-
保证线程安全,为线程加锁
- (void)testSemaphore2 { _semaphore = dispatch_semaphore_create(1); for (NSInteger i = 0; i < 100; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self asyncTask]; }); } } - (void)asyncTask { dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); _count++; sleep(1); NSLog(@"执行任务:%zd",_count); dispatch_semaphore_signal(_semaphore); } ///执行结果发现, 是顺序从1-100执行
原因: 在子线程执行并发任务时, 由于第一次执行任务将信号量减1, 信号总量变为0, 当第二个任务进来时需要由于信号总量为0所以进入等待状态, 任务一执行完之后将信号量加1, 任务二开始执行, 并立即将信号总量减1变为0, 任务三继续等待, 依次类推, 实现了线程加锁目的
-
使用dispatch_once实现单例
//手写单例 - (id)sharedInstance { static id instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }) return instance; }
-
NSOperationQueue/NSOperation简介
-
优势
可以添加任务依赖,方便控制执行顺序
可以设定操作执行的优先级
可以设置最大并发量
-
任务执行状态控制:isReady,isExecuting,isFinished,isCancelled
如果只是重写NSOperation的main方法,由底层控制变更任务执行及完成状态,以及任务退出
如果重写了NSOperation的start方法,自行控制任务状态
系统通过KVO的方式移除isFinished==YES的NSOperation
-
基本操作
NSOperation是抽象类, 主要使用两个子类: NSBlockOperation和NSInvocationOperation
可以直接添加Block任务NSOperation到队列中执行, 默认在当前线程执行, NSBlockOperation添加多任务时会自动开启新线程执行
也可以将NSOperation添加到NSOperationQueue中执行操作, 当maxConcurrentOperationCount = 1时为串行队列, 大于1时为并发队列
-
示例代码
/** * 设置 MaxConcurrentOperationCount(最大并发操作数) */ - (void)setMaxConcurrentOperationCount { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.设置最大并发操作数 queue.maxConcurrentOperationCount = 1; // 串行队列 // queue.maxConcurrentOperationCount = 2; // 并发队列 // queue.maxConcurrentOperationCount = 8; // 并发队列 // 3.添加操作 [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程 } }]; }
执行结果为顺序执行1,1,2,2,3,3,4,4
如果将maxConcurrentOperationCount改为2, 则并并发执行, 这里就不再验证
-
NSOperation的操作依赖设置
/** * 操作依赖, 操作2依赖操作1 * 使用方法:addDependency: */ - (void)addDependency { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.创建操作 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程 } }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程 } }]; // 3.添加依赖 [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2 // 4.添加操作到队列中 [queue addOperation:op1]; [queue addOperation:op2]; }
-
NSOperation 提供了
queuePriority
属性, 新建操作的默认优先级是Normal// 优先级的取值 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) { NSOperationQueuePriorityVeryLow = -8L, NSOperationQueuePriorityLow = -4L, NSOperationQueuePriorityNormal = 0, NSOperationQueuePriorityHigh = 4, NSOperationQueuePriorityVeryHigh = 8 };
-
线程间通信示例
/** * 线程间通信 */ - (void)communication { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc]init]; // 2.添加操作 [queue addOperationWithBlock:^{ // 异步进行耗时操作 for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程 } // 回到主线程 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 进行一些 UI 刷新等操作 for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程 } }]; }]; }
-
-
RunLoop
-
什么是RunLoop
RunLoop是通过内部维护的事件循环来对事件进行管理的一个对象
-
为什么main函数不会退出
UIApplicationMain函数内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码
-
RunLoop的数据结构
NSRunLoop(Foundation)
是CFRunLoop(CoreFoundation)
的封装,提供了面向对象的API
RunLoop 相关的主要涉及五个类:CFRunLoop
:RunLoop对象-
CFRunLoopMode
:运行模式-
kCFRunLoopDefaultMode
:默认模式,主线程是在这个运行模式下运行 -
UITrackingRunLoopMode
:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) -
UIInitializationRunLoopMode
:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用 -
GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到 -
kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
苹果对外开放的主要有
kCFRunLoopDefaultMode
和kCFRunLoopCommonModes
一个比较常见的问题:滑动tableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode
下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode
,而Timer是在kCFRunLoopDefaultMode
下的,就无法接受处理Timer的事件。 怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode
上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到UITrackingRunLoopMode
和kCFRunLoopDefaultMode
上。
那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes
了[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
CFRunLoopSource
:输入源/事件源CFRunLoopTimer
:定时源CFRunLoopObserver
:观察者
-
RunLoop常见问题
- (void)test { NSLog(@"1"); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [[NSRunLoop currentRunLoop] run]; [self performSelector:@selector(test) withObject:nil afterDelay:10]; NSLog(@"3"); }); NSLog(@"4"); } - (void)test { NSLog(@"5"); }
输出顺序? 答案是: 1423, 5不会执行, 原因是runloop开启时应该有任务执行mode中应该有item才行, 否则会退出, 所以应调整成如下代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); //先添加任务 [self performSelector:@selector(test) withObject:nil afterDelay:10]; //再开启循环 [[NSRunLoop currentRunLoop] run]; NSLog(@"3"); });
-
* 如何创建一个常驻线程
```objc
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
```
* 怎样保证子线程数据更新回到主线程更新UI时不影响滑动操作
答: 将更新UI的任务添加到主线程的**NSDefaultRunLoopMode** 上执行即可, 这样主线程将会在用户停止滑动之后由UITrackingRunLoopMode切换到NSDefaultRunLoopMode之后更新UI
```objc
[self performSelectorOnMainThread:@selector(reloadData)
withObject:nil waitUntilDone:NO
modes:@[NSDefaultRunLoopMode]];
```
-
自旋锁与互斥锁
-
自旋锁
当任务被另一个线程锁定是, 尝试执行的线程会进入等待(不会休眠), 等上一个线程解除锁定时, 立即执行下一个线程任务,
优点: 因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁
缺点: 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用
常用自旋锁: atomic, dispatch_semaphore_t
-
互斥锁
当上一个线程锁定时, 下一个尝试执行的线程会进入休眠, 等上一个线程解除锁定, 下一个线程自动唤醒然后执行任务
常用互斥锁: NSLock, NSCondition, @ synchronized
-
3. UI
-
UIView与CALayer的关系
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents -
事件传递与响应者链
-
事件传递过程
触摸事件发生后, 系统会将事件加入到UIApplication管理的事件队列
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常 先发送事件给应 用的主窗口 keyWindow,主窗口再把事件发送给rootViewController
rootViewController再把事件发送给他的根View,然后会在View的层次结构中找到一个最合适的视图来处 理触摸事件, 找到合适的视图控件后,就会调用视图控件的touches方法来做具体的事件处理, 注意如果
-
找到合适的视图控件后,就会调用视图控件的touches方法来做具体的事件处理
touchesBegan touchesMoved touchesEnded
-
响应者链, 找到最合适处理事件的view
- 判断当前view是否能够接收触摸事件
- 判断当前的点,是否在当前view中
- 如果点在当前的view中 遍历当前view的所有子view,遍历的过程是从后往前遍历的 如果找到子控件,返回子控件 如果没有子控件满足条件,返回当前view
// hitTest:withEvent:方法的处理流程如下: // • hitTest:withEvent:会忽略隐藏、不和用户交互的、透明度小于0.01的视图 // • 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内; // • 若返回NO,则hitTest:withEvent:返回nil; // • 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有 // 子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图 // 返回非空对象或者全部子视图遍历完毕; // • 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束; // • 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ //1 判断当前view,是否可以接收事件 if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden) { return nil; } //2 判断点击的点,是否在当前view中 if (![self pointInside:point withEvent:event]) { return nil; } //3 从后往前遍历当前view的子view,是否是最合适的view,如果不是返回self NSInteger count = self.subviews.count; for (NSInteger i =count-1;i>=0 ; i--) { UIView *childView = self.subviews[i]; //把当前坐标系的点,转换成子view坐标系的点 CGPoint subPoint = [self convertPoint:point toView:childView]; //寻找最合适的view // //判断点是否在子控件中 if ([self pointInside:subPoint withEvent:event]) { UIView *fitView = [childView hitTest:subPoint withEvent:event]; if (fitView) { return fitView; } } } return self; }
-
-
图像显示原理
-
CPU工作
- Layout: 布局
- Display: 绘制
- Prepare: 图片解码
- Commit: 提交位图
-
GPU工作
顶点着色,图元装配,cell光栅化,片段着色,片段处理
-
-
滚动视图优化:
-
CPU角度
- 对象创建销毁,调整, 对于开销大的对象进行优化处理
- 预排版: 布局计算, 文本计算, 采用缓存高度等
- 预渲染: 文本图片异步绘制等
-
GPU角度
考虑是有有不必要的CPU渲染
是否有太多的离屏渲染
是否有图层的混合操作(透明度尽量都不要使用)
view的层次结构是否合理
-
cell光栅化处理
// 光栅化 layer.shouldRasterize = true layer.rasterizationScale = UIScreen.mainScreen().scale // 异步绘制 layer.drawsAsynchronously = true
-
-
什么是离屏渲染(Off-Screen Rendering)
与之对应的就是On-Screen Rendering(在屏渲染), GPU的渲染操作主要用于显示当前屏幕缓冲去进行的操作, 而离屏渲染指的是GPU在当前屏幕缓冲区外新开辟缓冲区进行渲染操作
-
离屏渲染什么时候会触发?
答案: 圆角, 蒙版, 阴影, 等
4. OC语言特性
-
分类category
分类作用: 声明私有方法, 给已存在类扩展方法(实例方法, 类方法, 协议, 属性), 属性需要借助运行时的关联对象, 不能直接添加属性
分类特点: 运行时决议
-
扩展
声明私有属性,声明方法(没什么意义),声明私有成员变量
-
代理
代理是一种设计模式,以@protocol形式体现,一般是一对一传递。
一般以weak关键词以规避循环引用。 -
通知
使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。
-
KVO
-
实现原理 :
KVO的实现依赖于Objective-C强大的runtime,KVO的底层实现是监听setter方法。当观察某对象A时,KVO动态机制会动态创建一个A类的子类NSKVONotifying_A,并为这个新的子类重写父类的属性的setter方法, 方法内容如下
[super setAge:age]; ` [self willChangeValueForKey:@"age"]; [self didChangeValueForKey:@"age"]; ///其中后面两个方法会调用 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
-
作用:
能够监听某个对象属性值的改变
-
-
KVC
通过Key值直接访问对象属性,或者给属性赋值的操作, 常用操作valueforkey: / setValue: forKey:
底层执行机制: (属性name举例子)
- 程序优先调用属性setName: 方法,代码通过setter方法完成设置。
- 如果没有属性name, 会按照_name,_iskey,key,iskey的顺序搜索成员变量
- 如果以上都没有会调用setValue:forUndefinedKey: 方法抛出错误
5 RunTime
RunTime是OC语言底层运行原理, OC代码最终都要通过runtime去调用,
-
常见的runtime应用如下
-
通过交换方法可以实现拦截系统方法, 添加自己的需求
//常用方法 class_getClassMethod 获取类方法 class_getInstanceMethod 获取实例方法 method_exchangeImplementations 方法交换 在load方法中执行狡猾
-
const char* name = "rylsj"; - (void) setNick:(NSString *)nick { objc_setAssociatedObject(self, &name, nick, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString*) nick { return objc_getAssociatedObject(self, &name); }
-
- 获取属性列表
-
- (void) rylsj_AddMethod { //"v@:@": v表示void, @表示id, :表示 SEL class_addMethod([self.persion class], @selector(run:), (IMP)runMethod, "v@:@"); } void runMethod(id self, SEL _cmd, NSString* rylsj) { NSLog(@"%@", rylsj); } //调用 if ([self.persion respondsToSelector:@selector(run:)]) { [self.persion performSelector:@selector(run:) withObject:@"66 rylsj"]; } else { NSLog(@"方法没有实现!!"); }
-
-
OC 消息转发机制 (文中代码有错误, 可以c)
首先根据receiver对象的isa指针获取它对应的class
优先在class的cache查找message方法,如果找不到,再到
methodLists查找如果没有在class找到,再到super_class查找
一旦找到message这个方法,再依据receiver 中的self 指针找到当前的对象,调用当前对象的具体实现的方法(IMP),然后传递参数,调用实现方法。
-
当对象收到无法解读的消息后, 就会启动"消息转发(Message forwarding)"机制, 程序员可以通过消息转发告诉对象应该如何处理位置消息, 过程如下:
-
系统会调用
resolveInstanceMethod
方法, 如果是类方法则会调用resolveClassMethod
, 在这个方法中我们可以增加调用方法的实现, 我们可以通过一个Person类来实践一下@interface Person : NSObject - (void)run; @end @implementation Person ///要添加的方法 void run (id self, SEL _cmd) { NSLog(@"人跑"); } //在这个方法中添加自己的方法实现 + (BOOL)resolveInstanceMethod:(SEL)sel { if(sel == @selector(run)){ //关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self、_cmd,self代表方 法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表 返回值为void,@表示self,:表示_cmd。 class_addMethod(self, sel, (IMP)run, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *person = [Person new]; [person run]; } @end //调用结果打印: "人跑"
如果未实现以上方法, 则会进入步骤二
-
forwardingTargetForSelector:
调用在这个方法中可以返回你需要转发消息的对象, 这里我们可以新建一个对象Car来演示, 在Person中不是现方案一中的resolveInstanceMethod方法 改成实现forwardingTargetForSelector方法 如下:
@interface Car : NSObject - (void)run; @end @implementation Car - (void)run { NSLog(@"车跑"); } @end @implementation Person - (id)forwardingTargetForSelector:(SEL)aSelector { return [Car new]; } //执行结果: 车跑
通过forwardingTargetForSelector把消息转发给我们认为合适的对象去执行, 如果此步骤未实现, 则会进入下一步
-
通过
forwardInvocation
函数 设置我们自己生成的函数签名和对象//生成签名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { NSString *sel = NSStringFromSelector(selector); if([sel isEqualToString:@"run"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:selector]; } //设置 - (void)forwardInvocation:(NSInvocation *)invocation { SEL selector = [invocation selector]; //新建需要转发消息的对象 Car *car = [[Car alloc] init]; if([car respondsToSelector:selector]){ [invocation invokeWithTarget:car]; } } //执行结果: 车跑
以上是当前类未实现方法的三种不就措施, 调用顺序从前往后, 如果实现了第一种, 后面就不会执行以此类推, 如果都没实现程序就会crash
-
6. 内存
-
内存布局, 从代码区到栈区地址一次从低到高
- 栈区(stack): 方法调用,局部变量等,是连续的,高地址往低地址扩展, 系统自动管理
- 堆区(heap): 通过alloc等分配的对象,是离散的,低地址往高地址扩展,需要我们手动控制
- 未初始化数据(bss): 未初始化的全局变量等
- 已初始化数据(data): 已初始化的全局变量等
- 代码段(text): 程序代码 (常量区)
-
static关键字的作用
-
用于修饰存储类型使之成为静态存储类型
在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。 在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。
-
用于修饰链接属性使之成为内部链接属性
静态函数只能在声明它的源文件中使用。
-
-
const关键字
-
声明常变量,使得指定的变量不能被修改。
const int a = 5;/*a的值一直为5,不能被改变*/ const int b; b = 10;/*b的值被赋值为10后,不能被改变*/ const int *ptr; /*ptr为指向整型常量的指针,ptr的值可以修改,但不能修改其所指向的值*/ int *const ptr;/*ptr为指向整型的常量指针,ptr的值不能修改,但可以修改其所指向的值*/ const int *const ptr;/*ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改*/
-
-
修饰函数形参,使得形参在函数内不能被修改,表示输入参数
int fun(const int a);或int fun(const char *str);
-
修饰函数返回值,使得函数的返回值不能被修改。
const char *getstr(void);使用:const *str= getstr(); const int getint(void); 使用:const int a =getint();
-
iOS 得内存管理机制?
采用的是引用计数的方式进行内存管理, MRC下需要用户手动管理引用计数, ARC下, 系统禁用retain,release,retainCount,autorelease等关键字
-
自动释放池
在当次runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool, AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。
实现原理:
编译器会将 @autoreleasepool {} 改写为:
void * ctx = objc_autoreleasePoolPush; {} objc_autoreleasePoolPop(ctx);
-
objc_autoreleasePoolPush:
把当前next位置置为nil,即哨兵对象,然后next指针指向下一个可入栈位置,
AutoreleasePool的多层嵌套,即每次objc_autoreleasePoolPush,实际上是不断地向栈中插入哨兵对象。 -
objc_autoreleasePoolPop:
根据传入的哨兵对象找到对应位置。给上次push操作之后添加的对象依次发送release消息。回退next指针到正确的位置。
-
-
循环引用场景
-
代理(delegate)引起的相互循环引用
解决方案: 声明地阿里是,使用weak关键字修饰
-
NSTimer引起的循环引用
解决方案: 在合适的时候进行invalidate 并将指针指向nil
-
Block引起的循环引用
对block中使用的self进行弱化 __weak typeof(self) weakSelf = self;
注意: 并不是所有block都会造成循环引用, 只有被强引用了的block才会产生循环引用
-
-
Block的内存问题
-
Block的三种形式
-
全局Block(_NSConcreteGlobalBlock), 存储在已初始化的数据区(.data)
不使用外部变量的是全局Block, 如下:
NSLog(@"%@",[^{ NSLog(@"globalBlock"); } class]); //输出: __NSGlobalBlock__
-
栈Block(_NSConcreteStackBlock), 存储在栈区(Stack)
使用外部变量但是未进行copy操作的Block是栈Block, 如下:
NSInteger num = 10; NSLog(@"%@",[^{ NSLog(@"stackBlock:%zd",num); } class]); //输出: __NSStackBlock__
-
堆Block(_NSConcreteMallocBlock), 存储在堆区(Heap)
对栈Block进行copy操作,就是堆block,而对全局Block进行copy,仍是全局Block, 如下:
//对堆Block copy操作, 依然是全局Block void (^globalBlock)(void) = ^{ NSLog(@"globalBlock"); }; NSLog(@"%@",[globalBlock class]); //输出: __NSGlobalBlock__ //对栈Block进行copy,是堆Block NSInteger num = 10; void (^mallocBlock)(void) = ^{ NSLog(@"stackBlock:%zd",num); }; NSLog(@"%@",[mallocBlock class]); //输出: __NSMallocBlock__
注意: 对栈Block copy之后,并不代表着栈Block就消失了,左边的mallock是堆Block,右边被copy的仍是栈Block
- (void)testWithBlock:(dispatch_block_t)block { block(); dispatch_block_t tempBlock = block; NSLog(@"%@,%@",[block class],[tempBlock class]); } //输出: __NSStackBlock__,__NSMallocBlock__
-
-
7. 数据存储
-
iOS中常用数据持久化方式
-
NSUserDefaults, 适用于轻量数据存储
偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:@"jack" forKey:@"firstName"]; [defaults setInteger:10 forKey:@"Age"]; ///现在可不手动调用同步方法 [defaults synchronize];
-
文件写入本地(数组, 字典, 字符串等) 如下:
NSString *filePath = [[self getDocumentPath] stringByAppendingString:@"fileTest.txt"]; NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"798293@qq.com", @"email", @"787907@qq.com", @"emailDisplay", nil]; [dictionary writeToFile:filePath atomically:YES];
-
对象归档
需要对象实现NSCoding协议
NSString *filePath = [[self getDocumentPath] - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_name forKey:kAddressCardName]; } - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { _name = [aDecoder decodeObjectForKey:kAddressCardName]; } //存储 [NSKeyedArchiver archiveRootObject:obj toFile:filePath];
-
数据库(SQLite, CoreData)
-
SQLite 常用函数
sqlite3_open() //创建并打开数据库连接 sqlite3_exec() //执行数据库操作(crud)
-
-
* SQLite常用语句
* DDL(Data Definition Language) 数据库定义语句
```sql
--create创表,
create table t_student (id integer, name text, age inetger, score real);
--drop删除表
drop table if exists t_student;
```
* DML(Data Manipulation Language) 数据库操作语句
```sql
--insert、
insert into t_student (name, age) values (‘wg’, 10);
--update、
update t_student set name = ‘jack’, age = 20;
--delete
delete from t_student;
```
* DQL(Data Query Language) 数据库查询语句
```sql
-- select
-- 查指定字段
select name, age from t_student;
-- 查全部
select * from t_student ;
-- 查数量
select count (age) from t_student ;
```
* 常用关键字: where,order by,group by和having等
```sql
--where关键字示例
delete from t_student where age <= 10 or age > 30 ;
select * from t_student where age > 10 ;
-- 排序
select * from t_student order by age desc
-- limit条件限制
select * from t_student limit 4, 8 ;
```
* 别名
```sql
-- select 字段1 别名 , 字段2 别名 , … from 表名 别名 ;
select name myname, age myage from t_student;
```
8. 音视频开发
-
音频
-
音效播放 (AVFoundation), 支持acc , wav 等格式, 特征: 比较短 30秒以内
//创建音效 NSString *path = [[NSBundle mainBundle]pathForResource:"music.acc" ofType:nil]; NSURL *url = [NSURL fileURLWithPath:path]; AudioServicesCreateSystemSoundID((__bridge CFURLRef _Nonnull)(url), &soundID); //播放音效, 静音无振动 AudioServicesPlaySystemSound(soundID); //静音有震动 //AudioServicesPlayAlertSound(soundID);
-
音乐播放 (AVAudioPlayer: 只能播放本地音乐, 不支持网络媒体)
//1.创建一个音乐对象 NSString *path = [[NSBundle mainBundle]pathForResource:@"music.mp3" ofType:nil]; NSURL *url = [NSURL fileURLWithPath:path]; AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil]; //2. 准备, 播放 [audioPlayer prepareToPlay]; //播放 [audioPlayer play];
-
音频录制(AVAudioRecorder)
//保存路径 NSURL *url = [NSURL fileURLWithPath:[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject]stringByAppendingPathComponent:@"recoder001.wav"]]; //录音参数设置: AVSampleRateKey采样率 AVNumberOfChannelsKey音频通道数, AVLinearPCMBitDepthKey线性音频深度, NSMutableDictionary *settings = [NSMutableDictionary dictionary]; // 音频采样率 settings[AVSampleRateKey] = @(8000.0); //创建音频录音机 AVAudioRecorder *recorder = [[AVAudioRecorder alloc]initWithURL:url settings:settings error:&error]; //录音 [recorder record]; //停止录音 [self.recorder stop];
网络媒体流播放(AVPlayer, 见下文视频播放描述)
-
-
视频
-
几种视频播放
首先在iOS平台使用播放视频,可用的选项一般有这四个,他们各自的作用和功能如下:
使用环境 优点 缺点 MPMoviePlayerController MediaPlayer 简单易用, 自带页面 不可定制 AVPlayerViewController AVKit 简单易用, 自带页面 不可定制 AVPlayer AVFoundation 可定制度高,功能强大 不支持流媒体 IJKPlayer IJKMediaFramework 定制度高,支持流媒体播放 使用稍复杂 -
AVPlayer
-
播放音频
NSURL *playUrl = [NSURL URLWithString:@"http://www.xxx.com/music.mp3"]; self.player = [[AVPlayer alloc] initWithURL:playUrl]; [self.player play];//播放 [self.player pause];//暂停 self.player.rate = 1.5//倍速
播放视频 需要创建显示层AVPlayerLayer
-
-
-
直播
-
主播端:
获取音视频授权
-
配置采样参数
- 音频: 码率和采样率
- 视频: 分辨率, 帧率, 码率
-
采集数据
- 音频采集: AVAudioSession
- 视频采集: GPUImageVideoCamera
-
编码
- 音频编码: 软编码(h.264, mpeg等,第三方SDK)/硬编码(AudioToolbox, 原生)
- 视频编码: 软编码(mp3, acc等, 第三方sdk)/硬编码(VideoToolbox, 原生)
-
推流
- 音视频封装(flv或者TS格式)
- 协议(RTMP, HLS, FLV)
- 数据推送
-
服务器端
- 数据分发(CDN)
- 截屏
- 录制
- 转码
-
用户端
- 拉流: RTMP, HLS, FLV
- 解码: 软解码和硬解码
- 播放: IJKPlayer 开源播放器播放
直播常见问题 请参考文章 iOS开发之移动直播技术秒开、直播优化经验、直播问题解析、直播知识解惑
-
9. iOS开发中的动画
-
核心动画(CoreAnimation)
-
核心动画的分类(CAAnimation类的继承结构)
CAAnimation有三个子类: CAAnimationGroup(组动画), CAPropertyAnimation(属性动画), CATranstion(转场动画), 其中属性动画又分为: CABasicAnimation(基础动画)和CAKeyframeAnimation(关键帧动画)
-
-
基础动画