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 & model
2、能够观察到新值和旧值的变化
3、可以方便的观察到嵌套类型的数据变化
缺点
1、观察对象通过
string
类型设置,如果写错或者变量名改变,编译时可以通过但是运行时会发生crash
2、观察多个值需要在代理方法中多个
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 语言)@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
为什么只在主线程刷新 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
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 获取
评论