Runloop相关探索

Runloop 和 线程

CFRunloop中已经说明了一个线程及其runloop的对应关系,现在以iOS中NSThread的实际使用来说明runloop在线程中的意义。

在iOS中直接使用NSThread有一下几种方式,但是归根到底,当一个线程需要长时间的去跟踪一个任务的时候,这几种方式做的事情是一样的,只不过接口名称和参数不一样,感觉是为了使用起来更加方便。因为这些接口内部都需要依赖runloop去实现事件的监听,这个可以通过调用堆栈证实。

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait

以上两个方法都是NSObject的方法,可以直接通过一个对象来创建一个线程。第二个方法具有更多的灵活性,它可以让你自己指定线程,第一个方法是自己默认创建一个线程。第二个方法的最后一个参数是指定是否等待aSelector执行完毕。

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

该方法是NSThread的类方法,跟第一个方法是类似的功能。

下面通过在子线程发起一个网络请求,去发现一些问题,然后通过runloop去解释原因,并推测API背后的实现方式。

- (void)viewDidLoad {
 
    [super viewDidLoad];
 
    [self performSelectorInBackground:@selector(multiThread) withObject:nil];
}
- (void)multiThread
 
{
    if (![NSThread isMainThread]) {
        self.request = [[NSMutableURLRequest alloc]
 
                                        initWithURL:[NSURL URLWithString:@"
                                        http://www.baidu.com"]
 
                                        cachePolicy:NSURLCacheStorageNotAllowed
 
                                        timeoutInterval:10];
 
        [self.request setHTTPMethod: @"GET"];
 
        self.connection =[[NSURLConnection alloc] initWithRequest:self.request
 
                                                         delegate:self
 
                                                 startImmediately:YES];
    }
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(
    NSURLResponse *)response{
 
    NSLog(@"network callback");
 
}

运行之后,可以发现在子线程中发起的网络请求,回调没有被调用。大致猜测可能跟runloop有关系,也就是子线程的runloop中没有注册网络回调的消息,所以该子线程自己相关的runloop没有收到回调。实际上- (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL) 这个方法的第三个参数的bool值表示是否在创建完NSURLConnection对象之后立刻发起请求,一般情况下是YES,什么时候会传NO呢。

事实上,对于以上这种方式创建的线程,默认是没有生成该线程对应的runloop的。也就是说这种情况下,需要自己去创建对应线程的runloop,并且让他run起来,去不断监听各种往runloop里注册的消息。但是对于主线程而言,其对应的runloop会由系统建立,并且自己run起来。由于平时工作在主线程下,这些工作大部分情况下不需要人为参与,所以一到子线程就会有各种问题。子线程中起timer没有生效也是相同的原因。所以以上函数第三个参数的意思就是,如果是当前线程已经runloop跑起来的情况下,传YES。除此之外,需要自己创建runloop去run,再将网络请求消息注册到runloop中。

现在根据以上分析修改代码:

self.request = [[NSMutableURLRequest alloc]
 
                                initWithURL:[NSURL URLWithString:@"http://
                                www.baidu.com"]
 
                                cachePolicy:NSURLCacheStorageNotAllowed
 
                                timeoutInterval:10];
 
[self.request setHTTPMethod: @"GET"];
 
self.connection =[[NSURLConnection alloc] initWithRequest:self.request
 
                                                 delegate:self
 
                                         startImmediately:NO];
 
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
 
[runLoop run];
 
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:
NSDefaultRunLoopMode];
 
[self.connection start];

运行之后发现回调仍然没有被调用,其实在这里卡了很久。后来一次偶然的调试中发现,代码运行到 [runLoop run]; 就没有然后了。后面的代码一直就没有被执行,现在修改代码如下:

self.request = [[NSMutableURLRequest alloc]
 
                                initWithURL:[NSURL URLWithString:@"http://
                                www.baidu.com"]
 
                                cachePolicy:NSURLCacheStorageNotAllowed
 
                                timeoutInterval:10];
 
[self.request setHTTPMethod: @"GET"];
 
self.connection =[[NSURLConnection alloc] initWithRequest:self.request
 
                                                 delegate:self
 
                                         startImmediately:NO];
 
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
 
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:
NSDefaultRunLoopMode];
 
[self.connection start];
 
[runLoop run];

然后就发现网络回调被调用了。

之后分析了一下调用堆栈:

第一个:在multiThread里面是这样的:

multiThread.png

第二个:网络回调里面是这样的:

http://7xqgnx.com1.z0.glb.clouddn.com/Runloop2.png

通过堆栈可以得知,这两个函数都是由线程6调用的,也就是创建的子线程,但是堆栈中的内容很不一样。很显然第二个是从runloop调出的,并且是Sources0这个消息调出的。而第一个是线程运行时候的初始化方法。所以当调用runlooprun的时候,其实是线程进入自己的runloop去监听时间了,从此以后,所有的代码都会从runloop CALLOUT出来。所以这种情况下,需要把先把消息注册到runloop中,让runloop跑起来是最后需要做的事情。

以下是开源库AFNetworking网络请求的实现:

- (void)start {
 
    [self.lock lock];
 
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class
        ] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.
        runLoopModes allObjects]];
 
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self 
        class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[
        self.runLoopModes allObjects]];
 
    }
    [self.lock unlock];
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
 
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (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;
}

AFNetworking使用的是- (void)performSelector:(SEL)aSelector onThread:(NSThread*)thr withObject:(id)arg waitUntilDone:(BOOL)wait这个方法,但是为什么它没有使用- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg这个方法呢?

通过断点,发现了AFNetwokring网络请求中一些函数的调用顺序:

1.networkRequestThread

2.networkRequestThreadEntryPoint

3.operationDidStart

为什么operationDidStart会在networkRequestThreadEntryPoint之后调用?

在networkRequestThreadEntryPoint里主要是生成网络线程的runloop并且让它跑起来,里面的 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];这主要是为了在没有任何网络请求的时候让网络线程保持监听状态,否则网络线程的loop会直接返回,之后再调用网络线程请求就没有意义了。再结合调用堆栈,发现operationDidStart是在runloop callout出来的,而networkRequestThreadEntryPoint是网络线程的入口方法。这跟之前的例子是一样的。所以,我猜测- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait这个方法背后是由主线程将aSelector作为消息注册到runloop中时间发生在networkRequestThreadEntryPoint方法调用之前,所以在networkRequestThreadEntryPoint方法中调用,NSRunLoopcurrentRunLoop的时候其实runloop本身应该已经被创建了。原因是因为在这个地方断点 ,打印runloop对象可以发现里面已经注册了source0的消息,如下截图:

http://7xqgnx.com1.z0.glb.clouddn.com/Runloop3.png

也就是说父线程在- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait 函数中将aSelector注册成source0,这是该函数背后的大致实现。通过查阅apple官方文档,基本属实,如下所示:

http://7xqgnx.com1.z0.glb.clouddn.com/Runloop4.png

通过上面的分析,可以得出使用performSelector方法可以将子线程runloop的初始化实现在子线程的初始化方法里实现,如果使用performSelectorInBackground

方法,那么子线程runloop的初始化和业务逻辑就会混到一起,并且每一次都会重新初始化。AFNetworking通过一个静态全局的子线程去管理所有的网络请求,其对应的runloop也只需要初始化一次。

通过以上分析,可以知道如果需要让一个子线程去持续的监听时间,就需要启动它的runloop并且忘其中注册source,timer,oberserver三者之一的消息类型。在默认情况下子线程的runloop是不会自己创建和启动的。

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

推荐阅读更多精彩内容

  • 在CFRunloop中已经说明了一个线程及其runloop的对应关系 ,现在以iOS中NSThread的实际使用来...
    闹鬼的金矿阅读 1,502评论 0 51
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,184评论 0 6
  • 开启线程 分离主线程创建:创建线程后会自动执行,但是线程外部不可获取到该线程对象detachNewThreadWi...
    Mr_Pt阅读 1,030评论 0 1
  • 消息处理之performSelector[爆栈热门 iOS 问题] performSelector may cau...
    lionsom_lin阅读 715评论 0 3
  • 1.OC里用到集合类是什么? 基本类型为:NSArray,NSSet以及NSDictionary 可变类型为:NS...
    轻皱眉头浅忧思阅读 1,292评论 0 3