当前位置: 代码迷 >> 综合 >> runloop、自动释放池、线程、GCD
  详细解决方案

runloop、自动释放池、线程、GCD

热度:96   发布时间:2024-01-04 21:46:55.0

runloop

runloop是用来处理事件的循环。NSRunloop是CFRunloop的封装,CFRunloop是一套C接口,源码地址。

runloop处理消息的流程是“接收消息->恢复活跃->处理消息->进入休眠”。

runloop作用

  • 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
  • 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
  • 节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CPU,现在没有事情做,我要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情。

runloop的构成

  • CFRunLoopRef //runloop对象
    • CFRunLoopModeRef//运行模式
      • CFRunLoopSourceRef
      • CFRunLoopTimerRef
      • CFRunLoopObserverRef

它的结构关系如下

struct __CFRunLoop
{pthread_t _pthread;//线程CFMutableSetRef _commonModes; // commonModes下的两个mode(kCFRunloopDefaultMode和UITrackingMode)CFMutableSetRef _commonModeItems; // 在commonModes状态下运行的对象(例如Timer)CFMutableSetRef _modes; // 运行的所有模式(CFRunloopModeRef类)CFRunLoopModeRef _currentMode; //在当前loop下运行的mode...
}struct __CFRunLoopMode
{CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"CFMutableSetRef _sources0; // SetCFMutableSetRef _sources1; // SetCFMutableArrayRef _observers; // ArrayCFMutableArrayRef _timers; // Array...
}

CFRunLoopModeRef

一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。下面是5种Mode。

  • kCFDefaultRunLoopMode App的默认Mode,通常主线程是在这个Mode下运行

  • UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响

  • UIInitializationRunLoopMode 在刚启动App时进入的第一个Mode,启动完成后就不再使用

  • GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到

  • kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode

CommonModes

一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

sources0和_sources1

(1)以前的分法

Port-Based Sources

Custom Input Sources

Cocoa Perform Selector Sources

(2)现在的分法:

Source0 : 触摸事件,PerformSelectors,非基于Port的

Source1 : 基于Port的线程间通信,基于Port的

_timers

定时执行的定时器,底层基于使用mk_timer实现,受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。

timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。

_observers

添加监听的方法:

//创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity)
{switch (activity){case kCFRunLoopEntry:{CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());NSLog(@"kCFRunLoopEntry - %@", mode);CFRelease(mode);break;}case kCFRunLoopExit:{CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());NSLog(@"kCFRunLoopExit - %@", mode);CFRelease(mode);break;}default:break;}
});//添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
//释放
CFRelease(observer);

监听返回的状态:

enum CFRunLoopActivity
{kCFRunLoopEntry              = (1 << 0),    // 即将进入Loop kCFRunLoopBeforeTimers      = (1 << 1),    // 即将处理 Timer  kCFRunLoopBeforeSources     = (1 << 2),    // 即将处理 Source  kCFRunLoopBeforeWaiting     = (1 << 5),    // 即将进入休眠   kCFRunLoopAfterWaiting      = (1 << 6),    // 刚从休眠中唤醒  kCFRunLoopExit               = (1 << 7),    // 即将退出Loop kCFRunLoopAllActivities     = 0x0FFFFFFFU  // 包含上面所有状态 
}

runloop流程

runloop与线程

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

runloop与GCD

  • runLoop 的超时时间就是使用 GCD 中的 dispatch_source_t来实现的
  • 执行GCD MainQueue 上的异步任务

runloop用到了GCD,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

runloop与自动释放池

苹果在主线程 RunLoop 里注册了两个 Observer:

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件:BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;

Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池发生在其他所有回调之后。

Runloop的作用

NSTimer

方式一

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

方式二

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];- (void)newThread
{@autoreleasepool{//在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopModetimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self                     selector:@selector(incrementCounter:) userInfo: nil repeats:YES];//开始执行新线程的Run Loop,如果不启动run loop,timer的事件是不会响应的[[NSRunLoop currentRunLoop] run];}
}

自动释放池

PerformSelecter...

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。

这个过程的详细情况可以参考这里。

SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。

随后苹果注册的那个 Source1 就会触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。

随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

UI更新

即准备进入睡眠和即将退出 loop 两个时间点,会调用函数更新 UI 界面。当在操作 UI 时,某个需要变化的 UIView/CALayer 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。

GCD

RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。

如当调用了 dispatch_async(dispatch_get_main_queue(), block)时,主队列会把该 block 放到对应的线程(恰好是主线程)中,主线程的 RunLoop 会被唤醒,从消息中取得这个 block,回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 来执行这个 block

AFNetWorking 3.0以前的线程保活

子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。AFNetworking 基于 NSURLConnection 时正是这样做的,希望在后台线程能保持活着,从而能接收到 delegate 的回调。

这一点充分体现了:我们控制了runloop ,就是控制了app 的生死。

/* 返回一个线程 */
+ (NSThread *)networkRequestThread
{static NSThread *_networkRequestThread = nil;static dispatch_once_t oncePredicate;dispatch_once(&oncePredicate, ^{// 创建一个线程,并在该线程上执行下一个方法_networkRequestThread = [[NSThread alloc] initWithTarget:self                                                 selector:@selector(networkRequestThreadEntryPoint:) object:nil];// 开启线程[_networkRequestThread start];});return _networkRequestThread;
}/* 在新开的线程中执行的第一个方法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object
{@autoreleasepool{[[NSThread currentThread] setName:@"AFNetworking"];// 获取当前线程对应的 RunLoopNSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 为 RunLoop 添加 source,模式为 DefaultMode[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];// 开始运行 RunLoop[runLoop run];}
}

因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。所以上面的做法是为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort 对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源。

AsyncDisplayKit

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

监控系统卡顿

监控主线程状态,在一定时间内没有变化,就可判定为卡顿。这个会在之后的文章讲。

MachPort

MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该MachPort对象发消息时会在MachPort所关联的线程中执行相关的代理方法。

@interface DPMessageViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) UIAlertAction * ac;
@end@implementation DPMessageViewController- (void)viewDidLoad
{[super viewDidLoad];NSPort *port = [NSMachPort port];port.delegate = self;[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];[NSThread detachNewThreadSelector:@selector(oooooo:) toTarget:        [DPMessageViewModel new] withObject:port];
}- (void)handlePortMessage:(NSPortMessage *)message
{NSLog(@"子线程的消息%@", message);
}@end@interface DPMessageViewModel : NSObject<NSMachPortDelegate>
{NSPort *remotePort;NSPort *myPort;
}
@end@implementation DPMessageViewModel- (void)oooooo:(NSMachPort *)port
{@autoreleasepool{remotePort = port;[[NSThread currentThread] setName:@"MyWorkerClassThread"];[[NSRunLoop currentRunLoop] run];myPort = [NSPort port];myPort.delegate = self;[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];[self sendPortMessage];}
}- (void)sendPortMessage
{NSMutableArray *array  = [[NSMutableArray alloc] initWithArray:@[@"1",@"2"]];[remotePort sendBeforeDate:[NSDate date] msgid:100 components:array         from:myPort reserved:0];
}- (void)handlePortMessage:(NSPortMessage *)message
{NSLog(@"接收到父线程的消息...\n");
}@end

总结

  • 线程是独立调度和分派的基本单位,RunLoop和自动释放池为线程服务;
  • RunLoop是一个事件循环,让线程休眠和线程保活成为了可能,线程休眠可以节省CPU资源;
  • 自动释放池一定存在于线程之中,解决了资源的释放问题。