大数据

从代码中认识RunLoop

现在网上关于RunLoop的资料真是太多了,而且大同小异,如果只是看一遍不在代码里面实现一下的话,也只能了解点皮毛,当然这样动笔写一些,更能加深印象。
这次学习笔记参考自:
链接1
链接2
链接3
本文代码都可以下载demo调试或者自己编写测试

那么RunLoop是啥?为什么会有这种机制?

首先:一个线程一次只能处理一个任务。执行完之后,线程就会退出,那么在iOS里面,如果主线程走一遍那些viewdidiload神马的,就退出来了,拿程序怎么与用户交互呢。所以,这里引入runloop,不让线程退出。这样才能和相应用户的事件嘛!

这个对象管理了它所需要处理的事件和消息。只要不让他退出,他就一直在里面处于接受消息-等待-处理的循环中。那么线程也就不会退出啦。

NSRunLoop 是CFRunLoopRef 的OC层面的封装。所以那么我们直接从CFRunloopRef学起吧。
现在知道了,runloop的是干嘛的,那么,它里面到底有些啥呢。demo里面有runloop的源码

CFRunLoop

可以看到,一个runloop里面,这里我们需要关心的是它的modes,
commonModes,
_commonModeItems,
currentMode

一个RunLoop,包含了若干个mode,一个mode又包含了若干个CFRunLoopSource,CFRunLoopTimer,CFRunLoopObserver,这里的source/timer/observer统称为mode item,如果一个item都没有的runloop,就会自动退出。

Modes

一个CFRunloop包含若干个mode,每个mode又包含若干个source/timer/observer.这就是mode,代表RunLoop运行的一种模式,切只会在一种模式下运行。

Mode.png
  Mode的组成:
1)CFRunloopSource source是数据源,也是就是 事件产生的地方。
 source0:处理APP内部事件,APP自己负责,管理触发
 source1:由runloop和内核管理,包含一个  mach_port(处理端口类的消
息)和一个回调(函数指针),被用于通过内核和其他线程互发消息,这种source
能主动唤醒runloop的线程。
 2)CFRunLoopTimer:基于时间的触发器。包含一个时间长度和一个回调的函数
指针。加入RunLoop时,会注册对应的时间点。当时间点到时,会被唤醒执行那个
回调。(NSTimer,和Displaylink就是对这个的封装)

currentmode:

每次调用runloop时,必须指定一个mode为current mode,告诉runloop运行在哪种模式下。
系统有很多种模式,我们能把 mode item加入的,只有,NSRunLoopCommonModes,NSDefaultRunLoopMode

commonModels:

我们可以将mode标记为commom属性,每当runloop变化时,runloop会将items同步到具有common标记的mode里。
目前已经有很多教程告诉我们,timer如果要在滑动时,也能回调,要加到commonmodeModes中。但是为什么呢。
我们先代码测试一下吧:

  UIScrollView *myScroll = [[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    myScroll.contentSize = CGSizeMake(375, 10000);
    [self.view addSubview:myScroll];
    myScroll.backgroundColor = [UIColor redColor];
myScroll.delegate = self;
[self defalutTimer];//defalult mode 下的timer
[self commonTimer];//common modes中的timer;
- (void)defalutTimer {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doTime) 
userInfo:nil repeats:YES];//这个timer会默认加到default
}
- (void)commonTimer {
NSTimer *timer =[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doTime) 
userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

然后我们在scrollview的代理打印一下runloop的mode,可以看到,加入forMode:NSRunLoopCommonModes里面的timer,在两种模式下,都能回调,而只加到defaultMode里面的timer,在滑动时,就不能回调了

commoemode的time.png

defaultmode的timer1.png

看到结果之后,知道加到commonmodeModes的item会被runloop自动加到所有有common标志的mode中,而,UITrackingRunLoopMode和defaultMode都有这个标识,所以,在两种情况下,它都会回调。

CFRunLoopObserverRef 是观察者

每个Observer都包含了一个回调,当runloop的状态发生变化时,观察者能通过回调接受变化。

kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop


现在对runloop里面的概念有了大概的了解。下面了解一下runloop大概实现逻辑:
如果需要runloop在一定时间之内要退出,则需要在进入之前,用GCD中的timer定时。

  1,告诉observer准备进loop
  2,看一下source有无消息,去则去执行。
  3,查看GCD中的有没有放到mainqueue中的block,我来帮你实现
  4,睡觉(告诉observer)(卡在这里,等在消息),
  5,唤醒
  6,得到唤醒端口(gcd,或者timer,或者source)
  7,告诉obsver准备执行
  8,处理()
  9,如果线程没有结束或者timer没有到时间,回到第二步

这里的唤醒,实质上一个mach_msg() 这个函数,而这个函数的本质是mach_msg_track(),可以按下暂停,来看一下是否真的像他们说的那样。

哈哈,果真如此

RunLoop调用去接受消息,如果没有source传进来,就一直处于这个等待状态,所以理解为,休息状态。当有信息传进来,获取端口信息,执行程序。

触发它醒来的可能是:

  某一事件到达基于端口的源
  定时器启动
  RunLoop设置的时间已经超时

前面讲到,如果没有任何的item,会自动退出,那么现在在测试一下,如果loop中什么也不加,会不会一直run不会退出呢

- (void)willWorkWithOutSource {
dispatch_async(dispatch_get_global_queue
(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
    [[NSRunLoop currentRunLoop] run];//runloop在获取的时候就创建了,
不用再调用创建的函数
    NSLog(@"1");
});
}

如果你现在也在运行代码,你就知道,会打印1,因为run了之后没有item(source/observer/timer)加入,所以runloop就退出了。

 NSTimer *timer =[NSTimer timerWithTimeInterval:1.0 target:self selector:
@selector(doTime) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

如果给他加一个timer,他就一直都不会打印1,直到退出。

如果你已经抽空看过了前面三篇介绍的博客,你会发现,他们还说了,Dispatch和RunLoop也有关系,我们来看一下。

- (void)testDisPatch {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"1");
});
}

先试一下这个,在这里打下断点,看下调用堆栈的信息。

diaoyong堆栈.png

发现和block没有任何关系啊

(先上一张从链接1接来的图,从里面查一下我们在堆栈里面看到的函数是在干嘛哈!)

方法名.png

看来还不能开多线程,要放在mainqueue中,那再试一下

 dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"1");
        });

再打个断点。

gcd堆栈.png

看到第四行的那一长串,确实在上图中告诉我们,被dispatch唤醒,执行main queue中的方法

目前为止:我们知道了,RunLoop和线程的关系,RunLoop的组成,RunLoop和GCD的关系

最后再动手敲一敲,看能不能自己加个source试试,让两个线程通过RunLoop通信一下试试

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 CFRunLoopSourceContext context = {0,NULL,NULL,NULL,
NULL,NULL,NULL,&isadd,&isCancel,&perfom};
 aRunLoop = CFRunLoopGetCurrent();
//aRunLoop是CFRunLoopRef类型的全局变量
 source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
//source是CFRunLoopSourceRef
CFRunLoopAddSource(aRunLoop, source, kCFRunLoopDefaultMode);
//一看就知道意思是,给runloop设定mode和source
//现在需要runloop跑起来,顺便可以测试一下之前说的给runloop定时,如果时间超出,也会退出。

NSInteger state =  CFRunLoopRunInMode(  (__bridge CFStringRef)NSDefaultRunLoopMode,
 3, YES) ;
    NSLog(@"%ld", state);//第一个参数是,mode,第二个参数是时间限制,
第三个参数是一个bool,runloop是否应该处理一个source后退出。如果no,runloop继续处理事件,
直到时间限制到。如果yes,就退出。这个函数返回的是一个状态,
// kCFRunLoopRunFinished = 1,
// kCFRunLoopRunStopped = 2,
//kCFRunLoopRunTimedOut = 3,
//kCFRunLoopRunHandledSource = 4

现在比较迷惑的应该是一个函数,这个context里面的变量到底需要传什么呢还是进官方文档看吧

屏幕快照 2016-01-28 下午11.50.58.png

第一个必须是0其他都是回调可以为空。倒数第三个是source被加进runloop的回调,倒数第二个是被取消的回调,最后一个是触发的回调
最后在加上我们回调的代码,然后触发

void isadd() {
  NSLog(@"add");
}
void isCancel (){
NSLog(@"cancel");
}
void perfor () {
NSLog(@"perform");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

CFRunLoopSourceSignal(source);
CFRunLoopWakeUp(aRunLoop);
}

可以动手试试,其实不用试也知道,一个打印add,点击一下屏幕打印perform,时间到了打印cancel!

相信到了这里,你和我一样对runloop有了一定的了解,但是还是要自己敲敲代码,看看调用堆栈。(lldb的thread backtrace可以打印堆栈)

写在后面:

1)关于NSMachPort

这是source1的一种触发源,抱着好奇的心,本来也想试走一下,

   NSPort *myPort = [NSMachPort port];//拿到当前线程的端口
   [myPort setDelegate:self];//处理了port的传递,当接受到source时,会调用代理
 dispatch_async(dispatch_get_global_queue
(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; 
//这倒是一种很好的让线程不退出的方式,AFN就用的这种方式,让一个线程一直待命
    [[NSRunLoop currentRunLoop] run];
     });

现在子线程里面已经加好触发源,现在需要在主线程里面,触发这个source1叫醒子线程,这里要用到NSPortMessage。可是这个在头文件里面,只是@class这个类了,并没有import,官方文档说这是私有Api,那我还是不要碰了,但是教程里面又有这个方法,真是纠结。可能可以通过这种方式叫醒其他进程,但是iOS是不准进程间的通信的,哪位同学若是实现了这种方式的叫醒,请告知一下!

2)关于autorelease

- (void)viewDidLoad {
[super viewDidLoad];
 NSArray *array = [NSArray array];
    weekArray = array;
 }

把一个全局的弱指针指向这个array,目前这个array的引用计数应该是两个,一个是被aureleasepool持有,一个是array持有,当viewdidload一完,array就会释放,引用计数减1,都说,runloop在每一圈run的时候会自动创建pool,结束的时候会releasepool,可是我在

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"%p",&weekArray);
}

按理说,这个runloop已经第二次醒了,但是还是没有释放。还是妥妥的打印出了地址