3~5年开发经验 的 iOS工程师 应该知道的常识点~本篇总结了以下内容
- KVO
- KVC
- 多线程
- 锁
- runloop
- 计时器
导航
iOS 进阶常识总结(一)
- 目标
- 类目标
- 分类
- runtime
- 音讯与音讯转发
iOS 进阶常识总结(二)
- KVO
- KVC
- 多线程
- 锁
- runloop
- 计时器
iOS 进阶常识总结(三)
- 视图渲染和离屏渲染
- 事情传递和呼应链
- crash处理和功能优化
- 编译流程和发动流程
iOS 进阶常识总结(四)
- 内存办理
- 野指针处理
- autoreleasePool
- weak
- 单例、告诉、block、承继和调集
iOS 进阶常识总结(五)
- 网络基础
- AFNetWorking
- SDWebImage
KVO & KVC
KVO
用法和底层原理
- 运用办法:增加调查者并完成监听的代理办法
-
KVO
底层运用了isa-swizling
技能 -
OC
中每个目标/类都有isa
指针,isa
表明这个目标是哪个类的目标 - 当给目标的某个特点注册了一个
observer
,体系会创立一个新的中心类(intermediate class
)承继自本来的class
,把该目标的isa
指针指向中心类。 - 然后中心类会重写
setter
办法,赋值前调用willChangeValueForKey
, 赋值后调用didChangeValueForKey
,告诉一切调查者值产生了更改 - 重写了
-class
办法伪装类没有产生改动
KVO的优缺陷
- 长处
- 1、能够方便快捷的完成两个目标的相关同步,例如
view & model
- 2、能够调查到新值和旧值的改动
- 3、能够方便的调查到嵌套类型的数据改动
- 1、能够方便快捷的完成两个目标的相关同步,例如
- 缺陷
- 1、调查目标经过
string
类型设置,假如写错或者变量名改动,编译时能够经过可是运转时会产生crash
- 2、调查多个值需求在代理办法中多个
if
判别 - 3、屡次注册会屡次触发代理办法
- 4、增加和移除调查者有必要成对呈现,次数不匹配会
crash
- 1、调查目标经过
怎样手动触发KVO
- 在调查目标改动前调用
willChangeValueForKey:
- 在调查目标改动后调用
didChangeValueForKey:
给KVO
增加挑选条件
- 重写
automaticallyNotifiesObserversForKey
,需求挑选的key
回来NO
。 - 手动触发
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
- 直接运用facebook开源框架
- 防护计划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
- 能够主动办理生命周期
- C言语完成的多核并行CPU计划,能更合理的运转多核
- 4、
NSOperation
-
OC
根据GCD
的封装 - 完全面向目标的多线程计划
- 能够主动办理生命周期
-
GCD
有什么行列,默许供给了哪些行列
- 串行同步行列,使命按次序(串行),在当前线程履行(同步)
- 串行异步行列,使命按次序(串行),开辟新的线程履行(异步)
- 并行同步行列,使命按次序(无法表现并行),在当前线程履行(同步)
- 并行异步行列,使命一起履行(并行),开辟新的线程履行(异步)
- 默许供给了主行列和全局行列
GCD
主线程 & 主行列的联系
- 主行列使命只在主线程中被履行的
- 主线程运转的是一个
runloop
,除了主行列的使命,还有UI处理
和呼应处理
。
描述一下线程同步与异步的差异?
- 线程同步是指当前有多个线程的话,有必要等一个线程履行完了才干履行下一个线程。
- 线程异步指一个线程去履行,他的下一个线程不必等候他履行完就开始履行。
线程同步的办法
- 嵌套调用
- 线程加锁
-
GCD
- 运用串行行列,使命都一个个按次序履行
- 运用
dispatch_semaphore
信号量堵塞线程,直到使命完成再放行 -
dispatch_group
也能够堵塞到一切使命完成才放行
-
NSOperationQueue
- 设置
maxConcurrentOperationCount = 1
,同一时刻只有1个NSOperation
被执
- 设置
死锁的四个条件
- 互斥条件,一个资源每次只能被一个线程持有
- 恳求与坚持条件,需求非持有的资源完成使命,对已取得的资源坚持不放
- 不掠夺条件,不能强行掠夺其他线程正在运用的资源
- 循环等候条件,线程间构成循环等候资源联系
你遇到哪些死锁的状况
- 串行行列,正在进行的使命A向串行行列增加一个同步使命B,会构成使命相互等候,构成死锁。
- 优先级反转,
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
是线程不安全的,用户操作涉及到渲染和拜访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
- 原理
- 底层是链表,存储节点
SyncData
,内部包括下列数据
typedef struct SyncData { id object; // 传进来的obj recursive_mutex_t mutex; // 可重入锁 struct SyncData* nextData; // 链表指向下一个Data int threadCount; // 记载拜访资源的线程数量 }
- 运用
obj
的地址作为hash
传参经过id2data
办法查找`SyncData - 经过
objc_sync_enter(obj),objc_sync_exit(obj)
,加锁解锁。 - 传入的
obj
被开释或为nil
,会履行锁的开释
- 底层是链表,存储节点
- 长处
- 不需求创立锁目标也能完成锁的功能
- 运用简略方便,代码可读性强
- 缺陷
- 加锁的代码尽量少
- 功能没有那么好
- 注意锁的目标有必要是同一个
OC
目标
说说NSLock
- 遵从
NSLocking
协议 - 注意点
- 同一线程
lock
和unlock
需求成对呈现 - 同一线程接连
lock
两次会构成死锁
- 同一线程
说说NSRecursiveLock
-
NSRecursiveLock
是递归锁 - 注意点
- 同一个程
lock
屡次而不构成死锁 - 同一线程当
lock & unlock
数量共同的时分才会开释锁,其他线程才干上锁
- 同一个程
说说NSCondition & NSConditionLock
- 条件锁:满足条件履行锁住的代码;不满足条件就堵塞线程,直到另一个线程发出解锁信号。
-
NSCondition
目标实际上是一个锁和一个线程查看器- 锁用于保护数据源。
- 线程查看器根据条件判别是否堵塞线程。
- 需求手动敞开等候和手动发送信号解除等候
- 一个
wait
有必要对应一个signal
,一次唤醒全部需求运用broadcast
-
NSConditionLock
是NSCondition
的封装- 经过
condition
主动判别堵塞线程仍是唤醒线程 - 经过不同的
condition
值触发不同的操作 - 解锁时经过
unlockWithCondition
修正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加锁原理:
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
耗内存,你觉得呢?
- 由于会主动加锁,所以功能比
nonatomic
差。
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 表明卡顿 - 获取卡顿的堆栈传至后端,再剖析
怎样发动一个常驻线程
// 创立线程
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);