KVO & KVC
KVO 用法和底层原理
- 使用方法:添加观察者,然后怎样实现监听的代理
KVO 底层使用了 isa-swizling 的技术.OC 中每个对象/类都有isa 指针, isa 表示这个对象是哪个类的对象.- 当给对象的某个属性注册了一个 observer,系统会创建一个新的中间类(
intermediate class )继承原来的class ,把该对象的isa 指针指向中间类。 - 然后中间类会重写
setter 方法,调用setter 之前调用willChangeValueForKey , 调用setter 之后调用didChangeValueForKey ,以此通知所有观察者值发生更改。 - 重写了
-class 方法,企图欺骗我们这个类没有变,就是原本那个类。
KVO的优缺点
- 优点
- 1、可以方便快捷的实现两个对象的关联同步,例如
view & model - 2、能够观察到新值和旧值的变化
- 3、可以方便的观察到嵌套类型的数据变化
- 缺点
- 1、观察对象通过
string 类型设置,如果写错或者变量名改变,编译时可以通过但是运行时会发生crash - 2、观察多个值需要在代理方法中多个
if 判断 - 3、忘记移除观察者或重复移除观察者会导致
crash
怎么手动触发KVO
KVO 机制是通过willChangeValueForKey: 和didChangeValueForKey: 两个方法触发的。- 在观察对象变化前调用
willChangeValueForKey: - 在观察对象变化后调用
didChangeValueForKey: - 所以只需要在变更观察值前后手动调用即可。
给KVO添加筛选条件
- 重写
automaticallyNotifiesObserversForKey ,需要筛选的key 返回NO 。 setter 里添加判断后手动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
? if ([key isEqualToString:@"age"]) {
? ? ? return NO;
? }
? return [super automaticallyNotifiesObserversForKey:key];
}
?
- (void)setAge:(NSInteger)age {
? if (age >= 18) {
? ? ? [self willChangeValueForKey:@"age"];
? ? ? _age = age;
? ? ? [self didChangeValueForKey:@"age"];
? }else {
? ? ? _age = age;
? }
}
使用KVC修改会触发KVO吗?
- 会,只要
accessInstanceVariablesDirectly 返回YES ,通过KVC修改成员变量的值会触发KVO。 - 这说明KVC内部调用了
willChangeValueForKey: 方法和didChangeValueForKey: 方法
直接修改成员变量会触发KVO吗?
KVO的崩溃与防护
崩溃原因:
- KVO 添加次数和移除次数不匹配,大部分是移除多于注册。
- 被观察者
dealloc 时仍然注册着 KVO,导致崩溃。 - 添加了观察者,但未实现
observeValueForKeyPath:ofObject:change:context: 。 防护方案1: - 直接使用facebook开源框架
KVOController 防护方案2: - 自定义一个哈希表,记录观察者和观察对象的关系。
- 使用
fishhook 替换 addObserver:forKeyPath:options:context: ,在添加前先判断是否已经存在相同观察者,不存在才添加,避免重复触发造成bug。 - 使用
fishhook 替换removeObserver:forKeyPath: 和removeObserver:forKeyPath:context ,移除之前判断是否存在对应关系,如果存在才释放。 - 使用
fishhook 替换dealloc ,执行dealloc 前判断是否存在未移除的观察者,存在的话先移除。
KVC底层原理
setValue:forKey: 的实现
- 查找
setKey: 方法和_setKey: 方法,只要找到就直接传递参数,调用方法; - 如果没有找到
setKey: 和_setKey: 方法,查看accessInstanceVariablesDirectly 方法的返回值,如果返回NO (不允许直接访问成员变量),调用setValue:forUndefineKey: 并抛出异常NSUnknownKeyException ; - 如果
accessInstanceVariablesDirectly 方法返回YES (可以访问其成员变量),就按照顺序依次查找 _key、_isKey、key、isKey 这四个成员变量,如果查找到了就直接赋值;如果没有查到,调用setValue:forUndefineKey: 并抛出异常NSUnknownKeyException 。
valueForKey: 的实现
- 按照
getKey,key,isKey 的顺序查找方法,只要找到就直接调用; - 如果没有找到,
accessInstanceVariablesDirectly 返回YES (可以访问其成员变量),按照顺序依次查找_key、_isKey、key、isKey 这四个成员变量,找到就取值;如果没有找到成员变量,调用valueforUndefineKey 并抛出异常NSUnknownKeyException 。 accessInstanceVariablesDirectly 返回NO (不允许直接访问成员变量),那么会调用valueforUndefineKey: 方法,并抛出异常NSUnknownKeyException ;
多线程
进程和线程的区别
- 进程:进程是指在系统中正在运行的一个应用程序,一个进程拥有多个线程。
- 线程:线程是进程中的一个单位,一个进程想要执行任务, 必须至少有一条线程。应程序启动默认开启主线程。
进程都有什么状态
Not Running :未运行。Inactive :前台非活动状态。处于前台,但是不能接受事件处理。Active :前台活动状态。处于前台,能接受事件处理。Background :后台状态。进入后台,如果又可执行代码,会执行代码,代码执行完毕,程序进行挂起。Suspended :挂起状态。进入后台,不能执行代码,如果内存不足,程序会被杀死。
什么是线程安全?
怎样保证线程安全?
- 通过线程加锁
pthread_mutex 互斥锁(C语言)@synchronized NSLock 对象锁NSRecursiveLock 递归锁NSCondition & NSConditionLock 条件锁dispatch_semaphore GCD信号量实现加锁OSSpinLock 自旋锁(不建议使用)os_unfair_lock 自旋锁(IOS10以后替代OSSpinLock )
你接触到的项目,哪些场景运用到了线程安全?
iOS 开发中有多少类型的线程?分别说说
- 1、
pthread
- C语言实现的跨平台通用的多线程API
- 使用难度大,没有用过
- 2、
NSThread
OC 面向对象的多线程API - 简单易用,可以直接操作线程对象。
- 需要手动管理生命周期
- 3、
GCD
- C语言实现的多核并行CPU方案,能更合理的运行多核
CPU - 可以自动管理生命周期
- 4、
NSOperation
OC 基于GCD 的封装- 完全面向对象的多线程方案
- 可以自动管理生命周期
GCD有什么队列,默认提供了哪些队列
- 串行同步队列,任务按顺序(串行),在当前线程执行(同步)
- 串行异步队列,任务按顺序(串行),开辟新的线程执行(异步)
- 并行同步队列,任务按顺序(无法体现并行),在当前线程执行(同步)
- 并行异步队列,任务同时执行(并行),开辟新的线程执行(异步)
- 默认提供了主队列和全局队列
GCD 主线程 & 主队列的关系
- 主队列任务只在主线程中被执行的
- 主线程运行的是一个
runloop ,除了主队列的任务,还有 UI 处理和绘制任务。
描述一下线程同步与异步的区别?
- 线程同步是指当前有多个线程的话,必须等一个线程执行完了才能执行下一个线程。
- 线程异步指一个线程去执行,他的下一个线程不用等待他执行完就开始执行。
线程同步的方式
GCD 的串行队列,任务都一个个按顺序执行NSOperationQueue 设置maxConcurrentOperationCount = 1 ,同一时刻只有1个NSOperation 被执行- 使用
dispatch_semaphore 信号量阻塞线程,直到任务完成再放行 dispatch_group 也可以阻塞到所有任务完成才放行
什么情况下会线程死锁
- 串行队列,正在进行的任务A向串行队列添加一个同步任务B,会造成AB两个任务互相等待,形成死锁。
- 优先级反转,
OSSpinlock
dispatch_once 实现原理
dispatch_once 需要传入dispatch_once_t 类型的参数,其实是个长整形- 处理
block 前会判断传入的dispatch_once_t 是否为0,为0表示block 尚未执行。 - 执行后把
token 的值改为1,下次再进来的时候判断非0直接不处理了。
performSelector 和runloop 的关系
- 调用
performSelecter:afterDelay: ,其内部会创建一个Timer 并添加到当前线程的RunLoop 。 - 如果当前线程
Runloop 没有跑起来,这个方法会失效。 - 其他的
performSelector 系列方法是类似的
子线程执行 [p performSelector:@selector(func) withObject:nil afterDelay:4] 会发生什么?
-
上面这个方法放在子线程,其实内部会创建一个NSTimer 定时器。 -
子线程不会默认开启runloop ,如果需要执行func 函数得手动开启runloop dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
? dispatch_async(queue, ^{
? ? ? // [[NSRunLoop currentRunLoop] run]; 放在上面无效
? ? ? // 只开启runloop但是里面没有任何事件,开启失败
? ? ? [self performSelector:@selector(test) withObject:nil afterDelay:2];
? ? ? [[NSRunLoop currentRunLoop] run];
});
为什么只在主线程刷新UI
UIKit 是线程不安全的,UI操作涉及到渲染和访问View 的属性,异步操作会存在读写问题,为其加锁则会耗费大量资源并拖慢运行速度。- 程序的起点
UIApplication 在主线程初始化,所有的用户事件(触摸交互)都在主线程传递,所以view 只能在主线程上才能对事件进行响应。
一个队列负责插入数据操作,一个队列负责读取操作,同时操作一个存储的队列,如何保证顺利进行
- 使用
GCD 栅栏函数实现多度单写 - 读取的时候使用
dispatch_sync 立刻返回数据 - 写入的时候使用
dispatch_barrier_async 阻塞其他操作后写入 - 注意尽量不要使用全局队列,因为全局队列里还有其他操作
锁
为什么需要锁?
- 多线程编程中会出现线程相互干扰的情况,如多个线程访问一个资源。
- 需要一些同步工具,确保当线程交互的时候是安全的。
什么是互斥锁
- 如果共享数据已经有了其他线程加锁了,线程会进行休眠状态等待锁
- 一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
- 任务复杂的时间长的情况建议使用互斥锁
- 优点
- 获取不到资源时线程休眠,cpu可以调度其他的线程工作
- 缺点
- 存在线程调度的开销
- 如果任务时间很短,线程调度降低了cpu的效率
什么是自旋锁
- 如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁
- 一旦被访问的资源被解锁,则等待资源的线程会立即执行
- 适用于持有锁较短时间
- 优点:
- 自旋锁不会引起线程休眠,不会进行线程调度和CPU时间片轮转等耗时操作。
- 如果能在很短的时间内获得锁,自旋锁效率远高于互斥锁。
- 缺点:
- 自旋锁一直占用CPU,未获得锁的情况下处于忙等状态。
- 如果不能在很短的时间内获得锁,使CPU效率降低。
- 自旋锁不能实现递归调用。
读写锁
- 读写锁又被称为
rw锁 或者 readwrite锁 - 不是最常用的,一般是数据库操作才会用到。
- 具体操作为多读单写,写入操作只能串行执行,且写入时不能读取;读取需支持多线程操作,且读取时不能写入
说说你知道的锁有哪些
pthread_mutex 互斥锁(C语言)@synchronized NSLock 对象锁NSRecursiveLock 递归锁NSCondition & NSConditionLock 条件锁dispatch_semaphore GCD信号量实现加锁OSSpinLock 自旋锁(暂不建议使用)os_unfair_lock 自旋锁(IOS10以后替代OSSpinLock )
说说@synchronized
- 原理
- 内部应该是一个可重入互斥锁(
recursive_mutex_t ) - 底层是链表,存储SyncData,SyncData里面包含一个
threadCount ,就是访问资源的线程数量。 objc_sync_enter(obj),objc_sync_exit(obj) ,通过obj 的地址作为hash 传参查找SyncData ,上锁解锁。- 传入的
obj 被释放或为nil ,会执行锁的释放 - 优点
- 不需要创建锁对象也能实现锁的功能
- 使用简单方便,代码可读性强
- 缺点
- 加锁的代码尽量少
- 性能没有那么好
- 注意锁的对象必须是同一个
OC 对象
说说NSLock
- 遵循
NSLocking 协议 - 注意点
- 同一线程
lock 和unlock 需要成对出现 - 同一线程连续
lock 两次会造成死锁
说说NSRecursiveLock
NSRecursiveLock 是递归锁- 注意点
- 同一个程
lock 多次而不造成死锁 - 同一线程当
lock & unlock 数量一致的时候才会释放锁,其他线程才能上锁
说说NSCondition & NSConditionLock
- 条件锁:满足条件执行锁住的代码;不满足条件就阻塞线程,直到另一个线程发出解锁信号。
NSCondition 对象实际上作为一个锁和一个线程检查器
- 锁保护数据源,执行条件引发的任务。
- 线程检查器根据条件判断是否阻塞线程。
- 需要手动等待和手动信号解除等待
- 一个
wait 必须对应一个signal ,一次唤醒全部需要使用broadcast NSConditionLock 是NSCondition 的封装
- 通过不同的
condition 值触发不同的操作 - 解锁时通过
unlockWithCondition 修改condition 实现任务依赖 - 通过
condition 自动判断阻塞还是唤醒线程
说说GCD信号量实现锁
dispatch_semaphore_creat(0) 生成一个信号量semaphore = 0 ( 传入的值可以控制并行任务的数量)dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) 使semaphore - 1 ,当值小于0进入等待dispatch_semaphore_signal(semaphore) 发出信号,使semaphore + 1 ,当值大于等于0放行
说说OSSpinLock
说说 os_unfair_lock
- iOS10以后替代
OSSpinLock 的锁,不再忙等 - 获取不到资源时休眠,获取到资源时由内核唤醒线程
- 没有加强公平性和顺序,释放锁的线程可能立即再次加锁,之前等待锁的线程唤醒后可能也没能加锁成功。
- 虽然解决了优先级反转,但也造成了饥饿(
starvation ) starvation 指贪婪线程占用资源事件太长,其他线程无法访问共享资源。
5个线程读一个文件,如何实现最多只有2个线程同时读这个文件
Objective-C中的原子和非原子属性
- OC在定义属性时有
nonatomic 和atomic 两种选择 atomic :原子属性,为setter/getter 方法都加锁(默认就是atomic ),线程安全,需要消耗大量的资源nonatomic :非原子属性,不加锁,非线程安全
atomic加锁原理:
property (assign, atomic) int age;
- (void)setAge:(int)age
{
? @synchronized(self) { ?
? ? ? _age = age;
? }
}
?
- (int)age {
int age1 = 0;
@synchronized(self) {
? age1 = _age;
}
}
atomic 修饰的属性 int a ,在不同的线程执行 self.a = self.a + 1 执行一万次,这个属性的值会是一万吗?
- 不会,左边的点语法调用的是
setter ,右边调用的是getter ,这行语句并不是原子性的。
atomic 就一定能保证线程安全么?
- 不能,只能保证
setter 和getter 在当前线程的安全 - 一个线程在连续多次读取某条属性值的时候,别的线程同时在改值,最终无法得出期望值
- 一个线程在获取当前属性的值, 另外一个线程把这个属性释放调了,有可能造成崩溃
nonatomic 是非原子操作符,为什么用nonatomic 不用atomic ?
- 如果该对象无需考虑多线程的情况,请加入这个属性修饰,这样会让编译器少生成一些互斥加锁代码,可以提高效率。
- 使用
atomic ,编译器会在setter 和getter 方法里面自动生成互斥锁代码,避免该变量读写不同步。
有人说能atomic 耗内存,你觉得呢?
atomic 为什么会失效
atomic 修饰的属性靠编译器自动生成的get/set 方法实现原子操作,如果重写了任意一个,atomic 关键字的特性将失效
nonatomic 实现
- (NSString *)userName {
? return _userName;
}
?
- (void)setUserName:(NSString *)userName {
? _userName = userName;
}
atomic 的实现
- (NSString *)userName {
? NSString *name;
? @synchronized (self) {
? ? ? name = _userName;
? }
? return name;
}
?
- (void)setUserName:(NSString *)userName {
? @synchronized (self) {
? ? ? _userName = userName;
? }
}
runloop
runloop 是什么?
- 系统内部存在管理事件的循环机制
runloop 是利用这个循环,管理消息和事件的对象。
runloop 是否等于 while(1) { do something ... } ?
- 不是
while(1) 是一个忙等的状态,需要一直占用资源。runloop 没有消息需要处理时进入休眠状态,消息来了,需要处理时才被唤醒。
runloop 的基本模式
- iOS中有五种
runLoop 模式 UIInitializationRunLoopMode (启动后进入的第一个Mode ,启动完成后就不再使用,切换到 kCFRunLoopDefaultMode )kCFRunLoopDefaultMode (App的默认Mode ,通常主线程是在这个 Mode 下运行)UITrackingRunLoopMode (界面跟踪 Mode ,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响)NSRunLoopCommonModes (这是一个伪Mode ,等效于NSDefaultRunLoopMode 和NSEventTrackingRunLoopMode 的结合 )GSEventReceiveRunLoopMode (接受系统事件的内部 Mode ,通常用不到)
runLoop 的基本原理
- 系统中的主线程会默认开启
runloop 检测事件,没有事件需要处理的时候runloop 会处于休眠状态。 - 一旦有事件触发,例如用户点击屏幕,就会唤醒
runloop 使进入监听状态,然后处理事件。 - 事件处理完成后又会重新进入休眠,等待下一次事件唤醒
runloop 和线程的关系
runloop 和线程一一对应。- 主线程的创建的时候默认开启
runloop ,为了保证程序一直在跑。 - 支线程的
runloop 是懒加载的,需要手动开启。
runloop 事件处理流程
- 事件会触发
runloop 的入口函数CFRunLoopRunSpecific ,函数内部首先会通知observer 把状态切换成kCFRunLoopEntry ,然后通过__CFRunLoopRun 启动runloop 处理事件 __CFRunLoopRun 的核心是是一个do - while 循环,循环内容如下
runloop 是怎么被唤醒的
- 没有消息需要处理时,休眠线程以避免资源占用。从用户态切换到内核态,等待消息;
- 有消息需要处理时,立刻唤醒线程,回到用户态处理消息;
source0 通过屏幕触发直接唤醒source0 通过调用mach_msg() 函数来转移当前线程的控制权给内核态/用户态。
什么是用户态、核心态
- 内核态:运行操作系统程序 ,表示一个应用进程执行系统调用后,或I/O 中断,时钟中断后,进程便处于内核执行
- 用户态:运行用户程序 ,表示进程正处于用户状态中执行
runloop的状态
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
? ? ? switch (activity) {
? ? ? ? ? case kCFRunLoopEntry: NSLog(@"runloop启动"); break;
? ? ? ? ? case kCFRunLoopBeforeTimers: NSLog(@"runloop即将处理timer事件"); break;
? ? ? ? ? case kCFRunLoopBeforeSources: NSLog(@"runloop即将处理sources事件"); break;
? ? ? ? ? case kCFRunLoopBeforeWaiting: NSLog(@"runloop即将进入休眠"); break;
? ? ? ? ? case kCFRunLoopAfterWaiting: NSLog(@"runloop被唤醒"); break;
? ? ? ? ? case kCFRunLoopExit: NSLog(@"runloop退出"); break;
? ? ? ? ? default: break;
? ? ? }
? });
? CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
}
runLoop 卡顿检测的方法
NSRunLoop 处理耗时主要下面两种情况
kCFRunLoopBeforeSources 和 kCFRunLoopBeforeWaiting 之间kCFRunLoopAfterWaiting 之后 - 上述两个时间太长,可以判定此时主线程卡顿
- 可以添加
Observer 到主线程Runloop 中,监听Runloop 状态切换耗时,监听卡顿
- 用一个
do-while 循环处理路基,信号量设置阈值判断是否卡顿 dispatch_semaphore_wait 返回值 非0 表示timeout 卡顿发生- 获取卡顿的堆栈传至后端,再分析
怎么启动一个常驻线程
// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(play) object:nil];
[thread start];
?
// runloop保活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 处理事件
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
复制代码
计时器
NSTimer、CADisplayLink、dispatch_source_t 的优劣
-
NSTimer
- 优点在于使用的是
target-action 模式,简单好用 - 缺点是容易不小心造成循环引用。需要依赖
runloop ,runloop 如果被阻塞就要延迟到下一次runloop 周期才执行,所以时间精度上也略为不足 -
CADisplayLink
- 优点是精度高,每次刷新结束后都调用,适合不停重绘的计时,例如视频
- 缺点容易不小心造成循环引用。
selector 循环间隔大于重绘每帧的间隔时间,会导致跳过若干次调用机会。不可以设置单次执行。 -
dispatch_source_t
- 基于
GCD ,精度高,不依赖runloop ,简单好使,最喜欢的计时器 - 需要注意的点是使用的时候必须持有计时器,不然就会提前释放。
NSTimer在子线程执行会怎么样?
NSTimer 在子线程调用需要手动开启子线程的runloop [[NSRunLoop currentRunLoop] run];
NSTimer 为什么不准?
- 如果
runloop 正处在阻塞状态的时候NSTimer 到达触发时间,NSTimer 的触发会被推迟到下一个runloop 周期
NSTimer 的循环引用?
timer 和target 互相强引用导致了循环引用。可以通过中间件持有timer & target 解决
GCD计时器
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
? NSLog(@"GCD timer test");
});
dispatch_resume(_timer);
同时我也整理了一些面试题,有需要的朋友可以加QQ群:1012951431 获取
|