RunLoop篇-事件循环的实现机制

在Runloop启动后,首先会发出一个通知.告知观察者即将启动,之后将要处理Timer / Source0事件,然后进入正式的source0处理,如果有source1要处理,会通过一条goto语句实现,进行代码逻辑跳转,去处理唤醒时收到的消息,如果没有source1处理,此时线程即将进入休眠,同时也会发送通知给Observer,然后发生从用户态到内核态的切换,然后线程正式进入休眠,等待唤醒
唤醒线程或者RunLoop的条件有三个

  1. 通过Source1进行相关事件的唤醒
  2. Timer事件的回调到了
  3. 外部手动唤醒

当一个处于休眠状态的RunLoop,可以通过上面三个方式唤醒它

当线程被唤醒后,也会发送通知告诉观察者
然后处理唤醒时收到的消息
然后又会回到将要处理Timer以及Source0事件的代码逻辑
然后通知observer,之后顺次向下执行
线程再次进入休眠,等待唤醒

程序从点击图标到程序启动,到程序被杀死这个过程中,系统是怎样实现的?
当调用Main函数之后,会调用UIApplicationMain函数,在内部启动主线程的RunLoop,经过一系列的处理,最终主线程的RunLoop处于休眠状态,之后如果点击了屏幕,会转成Source1, 就会把主线程唤醒,进行后续处理,当我们把线程杀死的时候,会发生RunLoop的退出通知,退出之后,线程就被销毁掉了


用户态和核心态是怎么切换的
main函数经过一系列处理后,系统内部会调用mach_msg函数,发生系统调用,经过这个调用,当前用户线程就把控制权转交给核心态
mach_mag在一定条件下会返回给调用方,触发返回的逻辑就是唤醒线程的逻辑
1,通过Source1进行相关事件的唤醒
2,Timer事件的回调到了
3,外部手动唤醒
然后就从核心态回到用户态的切换,当前APP的主线程循环就会被唤醒

RunLoop与NSTimer

滑动tableView时,定时器还会生效吗?
当前tableView正常情况下,是运行在kCFRunLoopDefaultMode模式下的,当我们对tableView进行滑动的时候,会发生mode切换,切换到UITrackingRunLoopMode模式下,

之前说,当我们把source / Timer / Observer添加到某个mode上面之后,如果当前Runloop是运行在另一个Mode上面的话,对应的这些Timer和Source和Observer是没有办法进行后续处理回调的,因为Mode有多个解决的问题就是Timer和Source和Observer的隔离,这种隔离产生的问题就是创建的Timer默认情况下添加到kCFRunLoopDefaultMode模式下,切换到UITrackingRunLoopMode后,定时器就不会再生效了
解决的办法是:

  1. void CFRunLoopAddTimer(runLoop, timer, commonMode)
    通过这个函数,将NSTimer添加到当前RunLoop的commonMode中, commonMode不是一个世纪的mode,只是把一些mode打上common的标记,然后可以把某个事件源比如说是Timer同步到多个mode中
//参数: runloop,准备要添加的参数,模式名称
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {    
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
    __CFRunLoopLock(rl);
    //如果当前模式名称是CommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //首先会提取RunLoop的commonModes
    CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        //同时判断runloop对应的commonItems是否为空,若为空,会重新创建集合
    if (NULL == rl->_commonModeItems) {
        rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    }
        //把timer添加到commonModeItems数组中
    CFSetAddValue(rl->_commonModeItems, rlt);
    if (NULL != set) {
        //再把timer和Runloop封装成一个context,
        CFTypeRef context[2] = {rl, rlt};
        //之后对集合中的每一个元素,都调用__CFRunLoopAddItemToCommonModes函数(对象元素,context)
        CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
        CFRelease(set);
    }

static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
    //取当前mode的名字,然后吧Runloop和Item取出来
    CFStringRef modeName = (CFStringRef)value;
    CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
    CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
    //根据当前item类型判断,来决定调用CFRunLoopAddSource还是CFRunLoopAddObserver还是CFRunLoopAddTimer,并不是循环调用,因为传进来的modeName 参数已经从commonMode变成被打上了标记的具体实际的mode
    if (CFGetTypeID(item) == __kCFRunLoopSourceTypeID) {
    CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
    } else if (CFGetTypeID(item) == __kCFRunLoopObserverTypeID) {
    CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
    } else if (CFGetTypeID(item) == __kCFRunLoopTimerTypeID) {
    CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
    }
}

如果CFRunLoopAddTimer传递进来的是个具体的mode,那么在后续就会把timer最终添加到对应的RunLoop Mode下面的timers数组中,就可以实现:通过把多个mode中都添加同一个Timer
因为调用CFSetApplyFunction函数,实际上是对于集合当中每个元素都调用了CFRunLoopAddItemToCommonMode函数,就可以实现把多个timer同步到多个mode 下面
的效果
所以,可以通过addTimer到一个commonMode上面来实现把一个Timer添加到多个mode下面,同时UITrackingRunLoopMode是被打上common标记的,就可以把timer同步到了UITrackingRunLoopMode上,如果列表继续滑动,timer是可以正常响应的

所以,如果我们在设置Timer模式时
[[NSRunLoopcurrentRunLoop] addTimer:self.timerforMode:NSDefaultRunLoopMode];
当我们不与UI进行交互时NSTimer有效
当我们与UI交互那么主线程runloop就会转到UITrackingRunLoopMode模式下,不能处理timer,就失效了.
所以我们可以

  1. [[NSRunLoopcurrentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  2. 开启新线程,在新线程中定义timer,这样就会被新线程的runloop处理。
怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作

在用户进行滑动的过程中,当前的RunLoop运行在UITrackingRunLoopMode模式下,而我们一般对网络请求是放在子线程中,子线程返回给主线程的数据要抛给主线程用来更新UI,可以把这部分逻辑包装起来,提交到主线程的default模式下,这样的话,当用户滑动时,default模式下的任务不会执行,当用户手停止时,mode就切换到了default模式下,就会处理子线程的数据了,这样就不会打断用户的滑动操作了

RunLoop与多线程
  1. 线程和Runloop是一一对应的

  2. 自己创建的线程默认是没有RunLoop的,需要自己手动创建某个线程的RunLoop

实现常驻线程:
  1. 为当前线程开启一个RunLoop
    可以通过[CFRunLoop getCurrent]或者 [NSRunLoop currentRunLoop]来创建,因为获取RunLoop这个方法本身会查找,如果当前线程没有runloop,会在系统内部为我们创建
  2. 向该RunLoop中添加一个port / Source等维护RunLoop的事件循环
    RunLoop如果没有事件需要处理的话,默认情况下,是不能自己维持事件循环,会直接退出,所以需要添加port / Source来维持事件循环机制
  3. 启动该RunLoop
    调用run方法

#import "MCObject.h"

@implementation MCObject
//定义两个k静态全局变量

// 自定义线程
static NSThread *thread = nil;
// 标记当前线程是否要继续事件循环
static BOOL runAlways = YES;

+ (NSThread *)threadForDispatch{
    if (thread == nil) {
        @synchronized(self) {
            if (thread == nil) {//采用线程安全的方式去创建thread,入口方法为runRequest
                // 线程的创建
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"com.imooc.thread"];
                // 启动
                [thread start];
            }
        }
    }
    return thread;
}

+ (void)runRequest
{
    // 创建一个Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    // 为thread线程创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    //while循环维持RunLoop的事件循环
    // 如果可以运行
    while (runAlways) {
        //确保每次yRunLoop运行一圈的时候能够对内存进行释放
        @autoreleasepool {
            /* 令当前RunLoop运行在DefaultMode下面,注意这个运行的mode和上面添加资源的mode必须是同一个mode
               否则把事件源添加到另一个mode上,而运行的defaultMode下,是无法维持运行的
               函数内部会调用mach_msg,发生由用户态到核心态的切换,当前线程就会休眠,就停在里面,不是死循环
               1.0e10是让循环运行到指定时间退出,这个代表很久远的时间
               true代表资源被处理后是否马上返回
             */
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    // 如果Runloop的mode中没有对应的事件源可以处理,runloop就会自动退出
    // 所以我们在某一时机 将source移除,静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出并s释放掉
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355