RunLoop
1. 讲讲RunLoop,项目中有用到过吗?
RunLoop 的基本作用:保持程序的持续运行,节省 CPU 的资源,提高程序的性能 ( 没有事情,就请休眠,不要功耗。有事情,就处理)。
简单举个例子,如果用Xcode的Command Line Tool文件来写OC,在代码里创建一个NSTimer,它并不能正常运行,因为这个程序和APP不同,程序运行一次直接结束,这时候我们需要把Runloop给Run起来,NSTimer就能用了。
2. RunLoop内部实现逻辑
3. runloop与线程的关系
- 每条线程,都有唯一的一个,与之对应的
RunLoop
对象 runloops[thread] = runloop
RunLoop
保存在一个全局的Dictionary
里,线程作为Key
,RunLoop
作为Value
- 线程刚创建时,并没有
RunLoop
对象,RunLoop
会在第一次获取它时创建 RunLoop
会在线程结束时,销毁- 主线程的
runloop
对象是默认就存在且默认是打开的,其他线程的runloop
对象只有在第一次获取runloop
对象时才会创建而且默认是关闭状态,需要我们手动调用run
方法运行。
4. timer与runloop的关系
我们创建timer
时,之所以timer
能运行,是因为创建timer
时,一般情况下,是在主线程中创建,这时会默认将timer以defaultRunloopModel
的类型加入主线程,而主线程的runloop
对象默认是打开的,从而timer
可以运行。
系统会将NSTimer
自动加入NSDefaultRunLoopMode
模式中,所以以下两段代码含义相同:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
对于NSTimer在滑动时停止工作的问题
- 当我们不做任何操作的时候,
RunLoop
处于NSDefaultRunLoopMode
下 - 当我们进行拖拽时,
RunLoop
就结束NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加该NSTimer
以及其事件,所以我们的NSTimer
就不工作了 - 当我们松开鼠标时候,
RunLoop
就结束UITrackingRunLoopMode
模式,又切换回NSDefaultRunLoopMode
模式,所以NSTimer
就又开始正常工作了
想要解决这个问题也很简单,我们直接让NSTimer
在两种mode
下都能工作就完了,这就用到我们之前不太清楚其用法的NSRunLoopCommonModes
了:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
当然你也可以把NSTimer
分别加入到NSDefaultRunLoopMode
和UITrackingRunLoopMode
,这两种写法是相同的,因为系统的mode
是默认在_commonModes
中的:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
5. 如何解决timer不准确的问题
可能造成NSTimer
不准确的原因:
- 在
RunLoop
循环过程中,被NSTimer
触发事件阻塞了,导致循环不能及时进行下去,延误之后NSTimer
触发时间。 - 在
RunLoop
循环过程中,在某一时刻主线程发生了阻塞情况,导致循环不能及时进行下去,厌恶NSTimer
触发时间。 - 在
RunLoop
循环过程中,发生了模式的转换,(比如UIScrollView
的滑动) 导致原有模式下的NSTimer
不会正常触发。
以上情况都是由于NSTimer
所依赖的run loops
会被多种原因干扰正常循环,所以要想解决NSTimer
精度问题,就要避免所依赖的run loops
被外界干扰。
- 注意:虽然第三种情况可以指定
NSTimer
所处模式为NSRunLoopCommonModes
,但是这种解决方法并不能改变run loops
在特定模式下不能处理其余模式事件的本质。
终极解决办法:
- 尽量避免将
NSTimer
放入容易受到影响的主线程run loops
中。 - 尽量避免将耗时操作放入
NSTimer
依赖的线程中。 - 尽量避免在
NSTimer
触发事件中进行耗时操作,如果不能避免,将耗时操作移至其余线程进行。
6. RunLoop是怎么响应用户操作的,具体流程是怎么样的
在 Objective-C
中,RunLoop
是一个事件循环机制,用于处理用户操作、定时器事件、网络请求等异步任务RunLoop
的主要作是监听事件并分发给相应的处理器。
下面是 RunLoop
响应用户操作的大致流程:
- 当用户进行某个操作(例如点击按钮)时,操作系统会将该事件发送给应用程序。
- 应用程序主线程会收到这个事件,并将其添加当前线程的
RunLoop
中。 RunLoop
开始运行,并进入一个循环状态,不断地检查是否事件需要处理。- 如果有事件需要处理,
RunLoop
会将事件从队列中取出,并将其分发给相应的处理器(例如事件响应方法)。 - 处理器执行相应的代码处理事件,可能包括更新用户界面、执行业务逻辑等操作。
- 处理完事件后,
RunLoop
继续循环,等下一个事件的到来。 RunLoop
的关键是在循环中不断检查事件,并及时分发给处理器这样可以保证用户操作的响应速度,并且免阻塞主线程。
需要注意的是RunLoop
并不是只处理用户操作,它还负责处理其他步任务,如定时器事件、网络请求等。RunLoop
的设计目的是为了提供一种效的事件处理制,使应用程序能够时响应用户操作其他异步任务,同时保持界面的畅性。
7. 说说RunLoop的几种状态
五种状态:
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode
:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响UIInitializationRunLoopMode
:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
GSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,通常用不到kCFRunLoopCommonModes
:并不是一种模式
只是一个标记,当mode
标记为common
时,将mode
添加到runloop
中的_commonModes
中。runloop
中的_commonMode
s实际上是一个Mode
的集合,可使用CFRunLoopAddCommonMode()
将Mode放到_commonModes
中。每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems
里的同步到具有Common
标记的所有的Mode
里
8. RunLoop的mode有什么作用
用来控制一些特殊操作只能在指定模式下运行,一般可以通过指定操作的运行mode 来控制执行时机,以提高用户体验。
系统默认注册了 5 个 Mode
kCFRunLoopDefaultMode
:App 的默认 Mode,通常主线程是在这个 Mode下运行,对应 OC 中的:NSDefaultRunLoopMode
UITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响kCFRunLoopCommonModes
:这是一个标记 Mode,不是一种真正的 Mode,事件可以运行在所有标有 common modes 标记的模式中,对应 OC 中的NSRunLoopCommonModes
, 带 有common modes
标 记 的 模 式 有 :UITrackingRunLoopMode
和kCFRunLoopDefaultMode
UIInitializationRunLoopMode
:在启动 App 时进入的第一个 Mode,启动完成后就不再使用GSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,通常用不到
AutoreleasePool
在RunLoop这块讲到了很多AutoreleasePool的知识,在这补充一下。
自动释放池的PUSH流程:
自动释放池的POP流程:
1.临时变量什么时候释放?
- 如果在正常情况下,一般是超出其作用域就会立即释放
- 如果将临时变量加入了自动释放池,会延迟释放,即在
runloop
休眠或者autoreleasepool
作用域之后释放
2.AutoreleasePool原理
- 自动释放池的本质是一个
AutoreleasePooIPage
结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage
都是以双向链表的形式连接 - 自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的
obic autoreleasePoolPudh
和obic_autoreleasePoolPop
,实际上是调用AutoreleasePoolPage
的push
和pop
两个方法 - 每次调用push操作其实就是创建一个新的
AutoreleasePooIPage
,而AutoreleasePoolPage
的具体操作就是插入一个POOL BOUNDARY
,并返回插入POOL BOUNDARY
的内存地址。而push内部调用autoreleaseFast
方法处理,主要有以下三种情况 -
- 当
page
存在,且不满时,调用add
方法将对象添加至page
的next
指针处,并next
递增
- 当
-
- 当
page
存在,且已满时,调用autoreleaseFulIPage
初始化一个新的page
,然后调用add
方法将对象添加至page
栈中
- 当
-
- 当
page
不存在时,调用autoreleaseNoPage
创建一个hotPage
,然后调用add
方法将对象添加至page栈中
- 当
- 当执行
pop
操作时,会传入一个值,这个值就是push
操作的返回值,即POOL_BOUNDARY
的内存地址token
,所以pop
内部的实现就是根据token
找到哨兵对象所处的page
中,然后使用objc_release
释放token
之前的对象,并把next
指针到正确位置
3.AutoreleasePool能否嵌套使用?
- 可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高
- 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的
- 自动释放池的多层嵌套其实就是不停的
pushs
哨兵对象,在pop
时,会先释放里面的,在释放外面的
4.哪些对象可以加入AutoreleasePool?alloc创建可以吗?
- 在
MRC
下使用new
、alloc
、copy
关键字生成的对象和retain
了的对象需要手动释放,不会被添加到自动释放池中 - 在
MRC
下设置为autorelease
的对象不需要手动释放,会直接进入自动释放池 - 所有
autorelease
的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中 - 在
ARC
下只需要关注引用计数,因为创建都是在主线程进行的,系统会自动为主线程创建AutoreleasePool
,所以创建会自动放入自动释放池
5.AutoreleasePool的释放时机是什么时候?
在没有手动添加AutoreleasePool的情况下,Auturelease对象是在当前的runloop迭代结束的时候进行释放;而它能否释放的原因是系统在每个runloop的迭代中都加入了自动释放池的push和pop。
- App启动后,苹果在主线程
RunLoop
里注册了两个Observer
,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。 - 第一个
0bserver
监视的事件是Entry
即将进入Loop
,其回调内会调用_objc_autoreleasePooIPush()
创建自动释放池,其order
是-2147483647,优先级最高保证创建释放池发生在其他所有回调之前。 - 第二个Observer监视了两个事件:
BeforeWating
(准备进入休)时调用obic_autoreleasePoolPop()
和obic autoreleasePooPush
释放旧的池井创建新池; ·Exit·即将很出Loop)时调用obic_autoreleasePooIPop
(来释放自动释放池。这个Obserer
的order
是 2147483647优先级最低,保证其释放池子发生在其他所有回调之后。
6.thread和AutoreleasePool的关系
- 每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构
- 新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除
- 对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池
总结:每个线程都有与之关联的自动释放池堆栈结构。新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈。对于当前线程来说。释放对象会被压栈到栈顶线程停止时,会自动释放与之关联的自动释放池
7.RunLoop和AutoreleasePool的关系
- 主程序的
RunLoop
在每次事件循环之前,会自动创建一个AutoreleasePool
。 - 并且会在事件循环结束时,执行
drain
操作,释放其中的对象。
多线程
1. 你理解的多线程
好处:
- 1.使用线程可以把占据时间长的程序中的任务放到后台去处理
- 2.用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以d出一个进度条来显示处理的进度
- 3.程序的运行速度可能加快
- 4.在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。
缺点:
- 1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
- 2.更多的线程需要更多的内存空间。
- 3.线程的中止需要考虑其对程序运行的影响。
- 4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
2. iOS多线程的方式有哪几种,你更倾向于哪一种
实现多线程的方法:
更倾向于GCD和NSOperation,封装更高级,使用起来更方便。
3. 有用过GCD吗?
- 1.在其他线程请求完数据,在主线程刷新UI
//让处理在主线程中执行
dispatch_async(dispatch_get main_queue(), ^{
/*
*只在主线程可以执行的处理
*例如用户界面更新
*/
});
- 2.Manager封装网络请求时候,可以用GCD实现单例
- (void)once { //GCD的一次性代码(只执行一次)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
}
- 3.GCD的定时器
- (void)after {
NSLog(@"run -- 0");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
NSLog(@"run -- 2");
});
}
- 4.信号量加锁保证线程安全
dispatch_semaphore_t semalook = dispatch_semaphore_create(1);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semalook, DISPATCH_TIME_FOREVER);
NSLog(@"1开始");
sleep(2);
NSLog(@"1结束");
dispatch_semaphore_signal(semalook);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semalook, DISPATCH_TIME_FOREVER);
NSLog(@"2开始");
sleep(2);
NSLog(@"2结束");
dispatch_semaphore_signal(semalook);
});
4. GCD的队列类型
可以使用dispatch_queue_create
来创建对象,需要传入两个参数,第一个参数表示队列的唯一标识符,用于DEBUG,可为空;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
5. 说一下OperationQueue和GCD的区别,以及各自的优势
区别
GCD
执行效率更高,而且由于队列中执行的是由block
构成的任务,这是一个轻量级的数据结构,写起来更方便GCD
只支持FIFO的队列而NSOperationQueue
可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序NSOperationQueue
甚至可以跨队列设置依赖关系,但是GCD
只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)
任务,才能控制执行顺序,较为复杂NSOperationQueue
因为面向对象,所以支持KVO
,可以监测operation
是否正在执行(isExecuted
)、是否结束(isFinished
)、是否取消(isCanceld
)
- 实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的
GCD
是首选 - 如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,
GCD
需要自己写更多的代码来实现,而NSOperationQueue
已经内建了这些支持 - 不论是
GCD
还是NSOperationQueue
,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread
需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销
6. 线程安全的处理手段有哪些?
- 互斥锁(
Mutex
):使用互斥锁可以确保同一时间只有一个线程访问共享资源。在Objective-C中,可以使用@synchronized
关键字来创建互斥锁。
@synchronized (self) {
// 访问共享资源的代码
}
- 自旋锁(
Spin Lock
):自旋锁一种忙等待的锁,它会不断地尝试获取锁,直到成功为止。在Objective-C中,可以使用os_unfair_lock
来创建自旋锁。
os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(lock);
// 访问共享资源的代码
os_unfair_lock_unlock(lock);
- 信号量(
Semaphore
):信量是一种数器,用于控制同时访问某个资源的线程数量。在Objective-C中可以使用dispatch_semaphore
来创建信号量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源的代码
dispatch_semaphore_signal(semaphore);
- 原子操作(
Atomic Operations
):原子操作是一种特的操作,可以确保对共享资源的读写操作是原子的,即不会被其他线程中断。在Objective-C中,可以使用atomic
键字来声明属性原子类型。
@property (atomic, strong) NSObject *sharedObject;
- 串行队列(
Serial Queue
):使用串行队列可以确保任务按顺序执行,从而避多个线程同时访问共享资源。可以使用GCD(Grand Central Dispatch
)来创建串行队列。
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
// 访问共享资源的代码
});
7. OC的锁有哪些
【iOS】—— iOS中的相关锁