RunloopiOS学习iOS开发

RunLoop基础元素解析

2015-09-04  本文已影响2071人  tripleCC

runloop

深入理解RunLoop这篇文章写的很好!

简介

RunLoop顾名思义,就是运行循环的意思。

基本作用:

内部实现:

注意点:

RunLoop循环示意图:(针对上面的__CFRunLoopRun函数,Mode已经判断非空前提)

接触过微处理器编程的基本上都知道,在编写微处理器程序时,我通常会在main函数中写一个无限循环,然后在这个循环里面对外部事件进行监听,比如外部中断,一些传感器的数据等,在没有外部中断时,就让CPU进入低功耗模式。如果接收到了外部中断,就恢复到正常模式,对中断进行处理。

while (1) {
  // 根据中断决定是否切换模式执行任务
}
// 或者
for (;;) {
}

RunLoop和这个相似,也是在线程的main中增加了一个循环:

int main(int argc, char * argv[]) {
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
             // ......
    } while (running);
    return 0;
}

所以线程在这种情况下,便不会退出。

关于MainRunLoop

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在viewDidLoad中设置断电,然后得到以下主线程栈信息:



可以看到,UIApplicationMain内部启动了一个和主线程相关联的RunLoop(_CFRunLoopRun)。在这里也可以推断,程序进入UIApplicationMain就不会退出了。我稍微对主函数进行了如下修改,并在return语句上打印了断点:


运行程序后,并不会在断点处停下,证实了上面的推断。

上面涉及了一个_CFRunLoopRun函数,接下来说明下iOS中访问和使用RunLoop的API:

因为后者是开源的,且前者是在后者上针对OC的封装,所以一般是对CFRunLoopRef进行研究。

两套API对应获取RunLoop对象的方式:

值得注意的是,获取当前RunLoop都是进行懒加载的,也就是调用时自动创建线程对应的RunLoop。

RunLoop相关类:

类之间的关系

以上图片说明了各个类之间的关系。

CFRunLoopModeRef说明:

系统默认注册了5个Mode:

关于NSRunLoopCommonModes

CFRunLoopTimerRef说明:

// 创建的定时器默认添加到当前的RunLoop中(没有就创建),而且是NSDefaultRunLoopMode模式
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 可以通过以下方法对模型进行修改
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 在UITrackingRunLoopMode模式下定时器才会运行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

CFRunLoopSourceRef说明:

CFRunLoopObserverRef说明:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),        // 进入RunLoop
        kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
        kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
        kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6), //即将唤醒
        kCFRunLoopExit = (1UL << 7),         //即将退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU//所有活动
    };
    // 创建监听着
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"%ld", activity);
    });

    //    [[NSRunLoop currentRunLoop] getCFRunLoop]
    // 向当前runloop添加监听者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放内存
    CFRelease(observer);

CF的内存管理(Core Foundation):

自动释放池释放的时间和RunLoop的关系:

综合上面,可以得到以下结论:

@autoreleasepool {}内部实现

有以下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }

    return 0;
}

查看编译转换后的代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }

    return 0;
}

__AtAutoreleasePool是什么呢?找到其定义:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

可以看到__AtAutoreleasePool是一个类:

结合以上信息,main函数里面的__autoreleasepool是一个局部变量。当其创建时,会调用构造函数创建线程池,出了{}代码块时,局部变量被销毁,调用其析构函数销毁线程池。

RunLoop实际应用

常驻线程

当创建一个线程,并且希望它一直存在时,就需要使用到RunLoop,否则线程一执行完任务就会停止。
要向线程存在,需要有强指针引用他,其他的代码如下:

// 属性
@property (strong, nonatomic) NSThread *thread;

// 创建线程
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[_thread start];

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 点击时使线程_thread执行test方法
    [self performSelector:@selector(test) onThread:_thread withObject:nil waitUntilDone:NO];
}

//
- (void)test
{
    NSLog(@"__test__");
}

就单单以上代码,是不起效果的,因为线程没有RunLoop,执行完test后就停止了,无法再让其执行任务(强制start会崩溃)。

通过在子线程中给RunLoop添加监听者,可以了解下performSelector:onThread:内部做的事情:

    // 这句在主线程中调用
    // _thread就是下面的线程

    [self performSelector:@selector(run) onThread:_thread withObject:nil waitUntilDone:NO];

    // 创建RunLoop即将唤醒监听者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        // 打印唤醒前的RunLoop
        NSLog(@"%ld--%@", activity, [NSRunLoop currentRunLoop]);
    });

    // 向当前runloop添加监听者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放内存
    CFRelease(observer);

    [self performSelector:@selector(setView:) withObject:nil afterDelay:2.0];

      // 使model不为空
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

综合上面的解释,可以知道performSelector:onThread:没有起作用,是因为_thread线程内部没有RunLoop,所以需要在线程内部创建RunLoop。

创建RunLoop并使对应线程成为常驻线程的常见方式有2:

AFNetWorking就使用到了常驻线程:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        // 创建RunLoop并向Mode添加NSMachPort,使RunLoop不会退出
        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;
}
- (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];
}

给子线程开启定时器

_thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[_thread start];


// 子线程添加定时器
- (void)subTimer
{
    // 默认创建RunLoop并向其model添加timer,所以后续只需要让RunLoop run起来即可
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 貌似source1不为空,source0就不为空
//    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

让某些事件(行为、任务)在特定模式下执行

比如图片的设置,在UIScrollView滚动的情况下,我不希望设置图片,等停止滚动了再设置图片,可以用以下代码:

// 图片只在NSDefaultRunLoopMode模式下会进行设置显示
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20150712_39"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];

先设置任务在NSDefaultRunLoopMode模式在执行,这样,在滚动使RunLoop进入UITrackingRunLoopMode时,就不会进行图片的设置了。

控制定时器在特定模式下执行

上文的《CFRunLoopTimerRef说明:》中已经指出

添加Observer监听RunLoop的状态

监听点击事件的处理(在所有点击事件之前做一些事情)

具体步骤在《CFRunLoopObserverRef说明:》中已写明

GCD定时器

注意:

//    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"__");

        NSLog(@"%@", [NSThread currentThread]);

        static NSInteger count = 0;

        if (count++ == 3) {
            // 为什么dispatch_cancel不能用_timer?/
            // Controlling expression type '__strong dispatch_source_t' (aka 'NSObject<OS_dispatch_source> *__strong') not compatible with any generic association type
            // 类型错误,可能dispatch_cancel是宏定义,需要的就是方法调用,而不是变量
//            dispatch_cancel(self.timer);

            dispatch_source_cancel(_timer);
        }
    });
    // 定时器默认是停止的,需要手动恢复
    dispatch_resume(timer);

    // 需要一个强引用保证timer不被释放
    _timer = timer;

最后一点需要说明的是,SDWebImage框架的下载图片业务中也使用到了RunLoop,老确保图片下载成功后才关闭任务子线程。

参考文档

深入理解RunLoop

上一篇下一篇

猜你喜欢

热点阅读