iOS 开发面试攻略(KVO、KVC、多线程、锁、runloop、计时器)
KVO & KVC
KVO用法和底层原理
使用方法:添加观察者,然后怎样实现监听的代理
KVO底层使用了isa-swizling的技术.OC中每个对象/类都有isa指针,isa表示这个对象是哪个类的对象.当给对象的某个属性注册了一个 observer,系统会创建一个新的中间类(
intermediate class)继承原来的class,把该对象的isa指针指向中间类。然后中间类会重写
setter方法,调用setter之前调用willChangeValueForKey, 调用setter之后调用didChangeValueForKey,以此通知所有观察者值发生更改。重写了
-class方法,企图欺骗我们这个类没有变,就是原本那个类。
KVO 的优缺点
优点
1、可以方便快捷的实现两个对象的关联同步,例如
view & model2、能够观察到新值和旧值的变化
3、可以方便的观察到嵌套类型的数据变化
缺点
1、观察对象通过
string类型设置,如果写错或者变量名改变,编译时可以通过但是运行时会发生crash2、观察多个值需要在代理方法中多个
if判断3、忘记移除观察者或重复移除观察者会导致
crash
怎么手动触发KVO
KVO机制是通过willChangeValueForKey:和didChangeValueForKey:两个方法触发的。在观察对象变化前调用
willChangeValueForKey:在观察对象变化后调用
didChangeValueForKey:所以只需要在变更观察值前后手动调用即可。
给 KVO 添加筛选条件
重写
automaticallyNotifiesObserversForKey,需要筛选的key返回NO。setter里添加判断后手动触发KVO
使用 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 语言)@synchronizedNSLock对象锁NSRecursiveLock递归锁NSCondition & NSConditionLock条件锁dispatch_semaphoreGCD 信号量实现加锁OSSpinLock自旋锁(不建议使用)os_unfair_lock自旋锁(IOS10 以后替代OSSpinLock)
你接触到的项目,哪些场景运用到了线程安全?
在线列表的增员和减员,需要加锁保持其线程安全。
iOS开发中有多少类型的线程?分别说说
1、
pthreadC 语言实现的跨平台通用的多线程 API
使用难度大,没有用过
2、
NSThreadOC面向对象的多线程API简单易用,可以直接操作线程对象。
需要手动管理生命周期
3、
GCDC 语言实现的多核并行 CPU 方案,能更合理的运行多核
CPU可以自动管理生命周期
4、
NSOperationOC基于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
为什么只在主线程刷新 UI
UIKit是线程不安全的,UI 操作涉及到渲染和访问View的属性,异步操作会存在读写问题,为其加锁则会耗费大量资源并拖慢运行速度。程序的起点
UIApplication在主线程初始化,所有的用户事件(触摸交互)都在主线程传递,所以view只能在主线程上才能对事件进行响应。
一个队列负责插入数据操作,一个队列负责读取操作,同时操作一个存储的队列,如何保证顺利进行
使用
GCD栅栏函数实现多度单写读取的时候使用
dispatch_sync立刻返回数据写入的时候使用
dispatch_barrier_async阻塞其他操作后写入注意尽量不要使用全局队列,因为全局队列里还有其他操作
锁
为什么需要锁?
多线程编程中会出现线程相互干扰的情况,如多个线程访问一个资源。
需要一些同步工具,确保当线程交互的时候是安全的。
什么是互斥锁
如果共享数据已经有了其他线程加锁了,线程会进行休眠状态等待锁
一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
任务复杂的时间长的情况建议使用互斥锁
优点
获取不到资源时线程休眠,cpu 可以调度其他的线程工作
缺点
存在线程调度的开销
如果任务时间很短,线程调度降低了 cpu 的效率
什么是自旋锁
如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁
一旦被访问的资源被解锁,则等待资源的线程会立即执行
适用于持有锁较短时间
优点:
自旋锁不会引起线程休眠,不会进行线程调度和 CPU 时间片轮转等耗时操作。
如果能在很短的时间内获得锁,自旋锁效率远高于互斥锁。
缺点:
自旋锁一直占用 CPU,未获得锁的情况下处于忙等状态。
如果不能在很短的时间内获得锁,使 CPU 效率降低。
自旋锁不能实现递归调用。
读写锁
读写锁又被称为
rw锁或者readwrite锁不是最常用的,一般是数据库操作才会用到。
具体操作为多读单写,写入操作只能串行执行,且写入时不能读取;读取需支持多线程操作,且读取时不能写入
说说你知道的锁有哪些
pthread_mutex互斥锁(C 语言)@synchronizedNSLock对象锁NSRecursiveLock递归锁NSCondition & NSConditionLock条件锁dispatch_semaphoreGCD 信号量实现加锁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,一次唤醒全部需要使用broadcastNSConditionLock是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
OSSpinLock是自旋锁,忙等锁自旋锁存在优先级反转的问题,线程有优先级的时候可能导致下列情况。
一个优先级低的线程先访问某个数据,此时使用自旋锁进行了加锁。
一个优先级高的线程又去访问这个数据,优先级高的线程会一直占着 CPU 资源忙等访问
结果导致优先级低的线程没有 CPU 资源完成任务,也无法释放锁。
由于自旋锁本身存在的问题,所以苹果已经废弃了
OSSpinLock。
说说 os_unfair_lock
iOS10 以后替代
OSSpinLock的锁,不再忙等获取不到资源时休眠,获取到资源时由内核唤醒线程
没有加强公平性和顺序,释放锁的线程可能立即再次加锁,之前等待锁的线程唤醒后可能也没能加锁成功。
虽然解决了优先级反转,但也造成了饥饿(
starvation)starvation指贪婪线程占用资源事件太长,其他线程无法访问共享资源。
5 个线程读一个文件,如何实现最多只有 2 个线程同时读这个文件
dispatch_semaphore信号量控制
Objective-C 中的原子和非原子属性
OC 在定义属性时有
nonatomic和atomic两种选择atomic:原子属性,为setter/getter方法都加锁(默认就是atomic),线程安全,需要消耗大量的资源nonatomic:非原子属性,不加锁,非线程安全
atomic 修饰的属性 int a,在不同的线程执行 self.a = self.a + 1 执行一万次,这个属性的值会是一万吗?
不会,左边的点语法调用的是
setter,右边调用的是getter,这行语句并不是原子性的。
atomic就一定能保证线程安全么?
不能,只能保证
setter和getter在当前线程的安全一个线程在连续多次读取某条属性值的时候,别的线程同时在改值,最终无法得出期望值
一个线程在获取当前属性的值, 另外一个线程把这个属性释放调了,有可能造成崩溃
nonatomic是非原子操作符,为什么用nonatomic不用atomic?
如果该对象无需考虑多线程的情况,请加入这个属性修饰,这样会让编译器少生成一些互斥加锁代码,可以提高效率。
使用
atomic,编译器会在setter和getter方法里面自动生成互斥锁代码,避免该变量读写不同步。
有人说能atomic耗内存,你觉得呢?
因为会自动加锁,所以性能比
nonatomic差。
atomic为什么会失效
atomic修饰的属性靠编译器自动生成的get/set方法实现原子操作,如果重写了任意一个,atomic关键字的特性将失效
nonatomic实现
atomic 的实现
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 的状态
runLoop 卡顿检测的方法
NSRunLoop处理耗时主要下面两种情况kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间kCFRunLoopAfterWaiting之后上述两个时间太长,可以判定此时主线程卡顿
可以添加
Observer到主线程Runloop中,监听Runloop状态切换耗时,监听卡顿用一个
do-while循环处理路基,信号量设置阈值判断是否卡顿dispatch_semaphore_wait返回值 非 0 表示timeout卡顿发生获取卡顿的堆栈传至后端,再分析
怎么启动一个常驻线程
计时器
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 计时器
同时我也整理了一些面试题,有需要的朋友可以加 QQ 群:1012951431 获取










评论