iOS中的RunLoop

iOS中的RunLoop

什么是RunLoop

从字面上理解就是一个运行循环,我们一般程序就是执行一个线程,是一条直线.有起点终点.而runloop就是一直在线程上面画圆圈,一直在跑圈,在不断跑圈中,一直在检测一些点击事件、定时器等等,一旦检测到就开始执行,执行结束后再睡眠,睡眠中再检测,除非切断否则一直在运行,否则就一直在循环。其内部的结构是一个do-while循环,在这个循环内部不断处理各种任务(比如timer、source、Observer)

RunLoop基本作用

  • 保持程序的持续运行,App持续运行就是因为有RunLoop
  • 处理App中的各种事件(比如触摸事件、定时事件、Selector事件)
  • 节省CPU资源,提高程序性能,该做事时做事,该休息时休息
    ##RunLoop对象
  • Foundation
    NSRunLoop
  • Core Foundation
    CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表着RunLoop对象,NSRunLoop是基于CFRunLoopRef的一层OC的包装

RunLoop与线程
  • 每条线程都有唯一的一个与之相对应的RunLoop对象
  • 主线程的RunLoop系统已经创建好了,子线程的RunLoop需要自己创建
  • RunLoop在第一次获取时创建,在线程结束时销毁
     // 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
      
     static CFMutableDictionaryRef loopsDic;

    // 访问 loopsDic 时的锁
    
    static CFSpinLock_t loopsLock;
   
   // 获取一个 pthread 对应的 RunLoopCFRunLoopRef _CFRunLoopGet(pthread_t thread) {
       OSSpinLockLock(&loopsLock);
       
       if (!loopsDic) {
            // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
           loopsDic = CFDictionaryCreateMutable();
           CFRunLoopRef mainLoop = _CFRunLoopCreate();
           CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
       }
       
       // 直接从 Dictionary 里获取。
       CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
       
       if (!loop) {
           // 取不到时,创建一个
           loop = _CFRunLoopCreate();
           CFDictionarySetValue(loopsDic, thread, loop);
           // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
       }
       
       OSSpinLockUnLock(&loopsLock);
       return loop;
   }
   
   CFRunLoopRef CFRunLoopGetMain() {
       return _CFRunLoopGet(pthread_main_thread_np());
   }
   
   CFRunLoopRef CFRunLoopGetCurrent() {
       return _CFRunLoopGet(pthread_self());
   }

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

  //获取当前线程的RunLoop
   [NSRunLoop currentRunLoop]; 
   //获取主线程的RunLoop
   [NSRunLoop mainRunLoop];
 
 //获取当前线程的RunLoop
   CFRunLoopGetMain();
   //获取主线程的RunLoop
   CFRunLoopGetCurrent();
   
 //runloop运行,
 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

注:NSRunLoop初始化不需要alloc,只需要在该线程里调用[NSRunLoop currentRunLoop],

RunLoop与相关类

(主要针对Core Foundation中的RunLoop的5个类)
  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:
iOS中的RunLoop_第1张图片

CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式,正是因为runloop里有source、timer、observer,runloop才能一直保持运行,如果没有这些,runloop会直接退出循环

  • 一个RunLoop包含了若干个Mode,每个Mode又包含了若干个Source/Timer/Observer

  • 每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作CurrentMode

  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入循环

  • 每个Mode可以设置自己的 Source/Timer/Observer,让其互不影响

  • 系统默认注册了5个Mode(苹果只开放了前俩个)

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

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

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

  • GSEventReceiveRunLoopMode:接受系统事件内部的Mode(绘图渲染等等),通常用不到

  • kCFRunLoopCommonModes:这是占位的Mode,不是真正的Mode

CFRunLoopTimerRef

  • CFRunLoopTimerRef是一个基于时间触发器(简单说就是设置一个时间,时间到了就去执行一个事件)
//在UI上添加一个TextView,滑动TextView,观察timer是否执行

- (void)viewDidLoad {
    [super viewDidLoad];

//调用scheduledTimer返回定时器,系统已自动添加RunLoop中,而且默认在NSDefaultRunLoopMode模式,这样每隔1s都会执行run
    //但是当你滑动textView时,定时器将失效,run方法停止执行,当你停止滑动时,run方法继续执行
    NSTimer *timer =  [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    //修改模式(当你把这个timer添加到当前的runloop中时,不管你滑不滑动textView,run方法都会执行)
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

-----------------------------------------------   
//调用timerWith返回定时器,需要手动加入Runloop
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];  

-----------------------------------------------   

//1、只运行在NSDefaultRunLoopMode模式,一旦RunLoop进入其他模式(当你滑动textView时),定时器就不会工作了(run方法不执行)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

-----------------------------------------------   
   
//2、只运行在UITrackingRunLoopMode模式,一旦RunLoop进入其他模式(当不滑动textView时),定时器就不会工作了(run方法不执行)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

-----------------------------------------------   
    
//3、定时器会在标记Common modes的模式下会工作,标记为Common modes的模式(不管你滑不滑动textView,run方法都执行):UITrackingRunLoopMode和NSDefaultRunLoopMode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

}

-(void)run{
    NSLog(@"___");
}

CFRunLoopSourceRef

  • CFRunLoopSourceRef是事件源(输入源)
  • 按照官方文档,Source的理论分类
  • **Port-Based Source **
  • **Custom Input Source **
  • Cocoa Perform Selector Sources
  • 按照函数调用栈,Source的实践分类
  • Source0:非基于Prot的
  • Source1:基于Prot的,通过内核和其他线程通信,接受、分发系统事件
    比如一个点击事件,首先它会先包装成一个event,之后传到source1,source1再分发到source0,由source0处理

CFRunLoopObserverRef

  • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
  • 可以监听的时间点有以下几个
 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
       kCFRunLoopEntry = (1UL << 0),//即将进入Runloop
       kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer
       kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source事件
       kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
       kCFRunLoopAfterWaiting = (1UL << 6),//从刚才休眠中唤醒
       kCFRunLoopExit = (1UL << 7),//即将退出runloop
       kCFRunLoopAllActivities = 0x0FFFFFFFU//监听所有状态
   };

如果在runloop中添加Observer,只能用CF的函数,一般用于拦截系统事件等

    /*
      添加Observer
      参数1:默认值
      参数2:监听活动(这里监听了kCFRunLoopAllActivities)
      参数3:重复
      参数4:0
    */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"---监听Runloop状态改变--%lu",activity);
    });
    
    /*
    添加观察者,监听Runloop的在kCFRunLoopDefaultMode模式下的状态
    参数1:当前runloop
    参数2:监听者
    参数3:模式
    */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    
    //释放Observer
    CFRelease(observer);

  • ** CF的内存管理(Core Foundation)**
  • 1、凡是带有Create、copy、retain等字眼的函数,创建出来的对象,都需要在最后做一次release
  • 2、release函数:CFRelease(observer);

RunLoop处理逻辑

iOS中的RunLoop_第2张图片
关于runloop的内存,在runloop中,一个runloop会对应一条线程,而自动释放池是针对当前线程的一些对象,它会在kCFRunLoopBeforeWaiting之前会被释放一次,在下一次启动的时候会重新创建,

RunLoop在开发中的使用场景

1、开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理事件)

当有一个耗时任务需要长期留在子线程里工作,这时需要将这个线程处理为一个常驻线程,所谓常驻线程只需在线程的mode中添加(source、timer、observer任意一个即可),那么这个线程中的runloop就可以跑起来,当你想runloop停掉的时候,直接调用类似- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode方法移除即可。

@interface ThreadViewController ()
@property (strong, nonatomic) NSThread *thread;
@end

@implementation ThreadViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(play1) object:nil];
    [self.thread start];
}


-(void)play1{
    @autoreleasepool{
//    NSLog(@"runloop---start--");
//    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test1) userInfo:nil repeats:YES];
//    //在当前子线程的runloop中的mode中添加定时器(有了Timer,runloop就可以跑起来)
//    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
//
    
    //timer 的scheduledTimerWithTimeInterval方法已经默认添加到了runloop中
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test1) userInfo:nil repeats:YES];
    
    //启动runloop
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"runloop---end--");
    }
}

-(void)test1{
    NSLog(@"test1****%@",[NSThread currentThread]);
    
}


-(void)play{
    @autoreleasepool{
    NSLog(@"runloop---start--");
    //在当前子线程的runloop中的mode中添加端口(其实就是加了source,有了source,runloop就可以跑起来)
    [[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //启动runloop
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"runloop---end--");
    }
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    
    //在self.thread线程中调用test
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)test{
    NSLog(@"test---%@",[NSThread currentThread]);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end

2、可以让某些事件在特定模式下执行

网络下载的图片,如果显示在UIScrollView、UITableView上,当滑动屏幕的时候,UI有时会卡顿,解决这个问题可以用runloop

//假如后台下载图片已经完成,此刻仍在滑动屏幕,那么可以延迟显示,当你停止滑动屏幕的时候再显示,此刻只需将runloop模式改为NSDefaultRunLoopMode。
- (void)viewDidLoad {
    [super viewDidLoad];
    [self showImage];
}
#pragma mark-指定runloop执行
-(void)showImage{
    //只在NSDefaultRunLoopMode模式下显示
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"aa"] afterDelay:3 inModes:@[NSDefaultRunLoopMode]];
}

3、可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)

最后,附上以上的demo,git:(https://github.com/hejiasu/RunLoop)

你可能感兴趣的:(iOS)