深入理解iOS开发之RunLoop

RunLoop属于iOS进阶开发中的一个重要技术点,本文会重点讲解我在开发过程中总结的对RunLoop的理解。

RunLoop概念

RunLoop是与多线程相关的一个事件处理机制,用来调度操作和处理协调即将发生的事件。iOS Developer Library关于RunLoop的解释是,RunLoop机制的四个作用:

RunLoop的管理并不是自动进行的,你需要编写线程代码去开始一个RunLoop,并在合适的时机响应事件。但是Cocoa和Core Foundation库都提供了RunLoop对象来帮助开发者配置和管理线程中的RunLoop。你不需要明确如何去创建这些RunLoop对象,并且每一个线程包括主线程都有一个与之相关联的RunLoop对象。只有一些次要的线程需要明确运行与之关联的RunLoop,然而RunLoop是App的一个开始进程,App框架会在主线程上自动运行和设置RunLoop。

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

我们来看一看上面的代码。代码的逻辑是让线程能随时处理事件但并不被系统强制退出。理解RunLoop如何做到保持这种能随时处理事件的状态,我们必须弄清楚两个内部逻辑,RunLoop如何判断线程是否休眠和是否退出。为了弄清楚这些,我们先来了解一下RunLoop与线程的关系;

RunLoop 与线程的关系

你可以通过 pthread_main_thread_np() 、pthread_self() 或 [NSThread currentThread] 来获取当前线程。CFRunLoop 是基于 pthread 来管理的。苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

static CFMutableDictionaryRef loopsDic;
static CFSpinLock_t loopsLock;

CFRunLoopRef _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,如果你不主动获取,那它一直都不会有。当然,这里不代表线程就不会执行事件了,如果你的获取了RunLoop的话 ,函数体中的事件可以通过RunLoop去执行,当然也可以通过你自己写的逻辑去处理事件。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

iOS中RunLoop对外的接口

Core Foundation中关于RunLoop的类有5个:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

通过图表来看看这几个类跟RunLoop的关系:

RunLoop

Mode Mode
Source Source
Observer Observer
timer timer

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

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

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop处理事件

界面刷新

当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。

苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

事件响应

当一个硬件事件(触摸/锁屏/摇晃/加速等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收, 随后由mach port 转发给需要的App进程。

苹果注册了一个 Source1 (基于 mach port 的) 来接收系统事件,通过回调函数触发Sourece0(所以UIEvent实际上是基于Source0的),调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。

手势识别

如果上一步的 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

GCD任务

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。

NSTimer

modeItems:- CFRunLoopSourceRef:数据结构(source0/source1);

  • source0 :
  • source1 :
  • CFRunLoopTimerRef:数据结构;创建与生效;相关类型(GCD的timer与CADisplayLink)
  • CFRunLoopObserverRef:数据结构;创建与添加;监听的状态;

网络请求

关于网络请求的接口:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLSession 是 iOS7 中新增的接口,也用到NSURLConnection的loader线程。所以还是以NSURLConnection为例。

当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

RunLoop应用

滑动与图片刷新, UITableView的优化

当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

- (void)viewDidLoad {
  [super viewDidLoad];
  // 只在NSDefaultRunLoopMode下执行(刷新图片)
  [self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];
}

常驻子线程,保持子线程一直处理事件

为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出。

+ (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;
}
- (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];
}

RunLoop避免App闪退

通过获取当前的runloop避免进程crash从而保证应用不闪退,当然也可以告知用户选择退出应用避免因此产生的一些应用内的操作隐患,同时清空监听信息,以便之后继续循环。

CFRunLoopRef runloop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while(1){
    for (NSString *mode in allModes){
        CFRunLoopInMode((CFStringRef)mode,0.001,false);
    }
}
时间: 2024-11-05 15:57:16

深入理解iOS开发之RunLoop的相关文章

李洪强iOS开发之RunLoop的原理和核心机制

李洪强iOS开发之RunLoop的原理和核心机制 搞iOS之后一直没有深入研究过RunLoop,非常的惭愧.刚好前一阵子负责性能优化项目,需要利用RunLoop做性能优化和性能检测,趁着这个机会深入研究了RunLoop的原理和特性. RunLoop的定义 当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程.RunLoop就是控制线程生命周期并接收事件进行处理的机制. RunLoop是iOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统. Foundation: NSRunLo

李洪强IOS开发之-iOS经典面试题

李洪强IOS开发之-iOS经典面试题 写这篇文章的目的是因为前两天同学想应聘iOS开发,从网上找了iOS面试题和答案让我帮忙看看.我扫了一眼,倒吸了一口冷气,仔细一看,气的发抖.整篇题目30多个没有一个答案是对的,总结这篇面试题的作者对iOS机制根本就是一知半解就敢发到网上,不管有心还是无心都是坑害新人.所以在这里总结一下这几年面试别人和被别人面试遇到的一些我认为比较好的基础题目分享给大家,进阶题目在后续补充.我的理解如果有错漏请一定指出,非常感谢! 从12年开始做面试官到现在已经三个年头了,这

iOS开发之多XIB之间相互关联

Xib link Xib 1.直接加载xib中的UIView 创建一个View1.xib, 随便设一个背景色,加一个标识UILabel, 这样好知道是这个view是哪一个view. 你可以在这个view上加作意的subview,我只是说明原理,所以这儿并没有加作何subview. 最终我的View1如下图: 由于View1会放到其它View上作为subview,所以这儿size是Freeform, Status Bar是:None. 将下面代码放到viewDidLoad中: &1这行代码就是加载

iOS开发之MVVM在项目中的应用

今天写这篇博客是想达到抛砖引玉的作用,想与大家交流一下思想,相互学习,博文中有不足之处还望大家批评指正.本篇博客的内容沿袭以往博客的风格,也是以干货为主,偶尔扯扯咸蛋(哈哈~不好好工作又开始发表博客啦~). 由于本人项目经验有限,关于架构设计方面的东西理解有限,我个人对MVVM的理解主要是借鉴于之前的用过的MVC的Web框架~在学校的时候用过ThinkPHP框架,和SSH框架,都是MVC的架构模式,今天MVVM与传统的MVC可谓是极为相似,也可以说是兄弟关系,也就是一家人了. 说到架构设计和团队

iOS开发之Socket通信实战--Request请求数据包编码模块

实际上在iOS很多应用开发中,大部分用的网络通信都是http/https协议,除非有特殊的需求会用到Socket网络协议进行网络数 据传输,这时候在iOS客户端就需要很好的第三方CocoaAsyncSocket来进行长连接连接和传输数据,该第三方地 址:https://github.com/robbiehanson/CocoaAsyncSocket,读者可以自行google或者baidu搜索 这个库的用法,网上有很多资料,而且用法不难. 在一些对Socket通信使用需求不是很高的应用中,比如需要

iOS开发之Block

iOS开发之Block 1.block的含义和作用 UI开发和网络常见功能实现回调, 按钮的事件处理方法是回调方法, 网络下载后的回调处理 (1) 按钮 target-action 一个方法传入按钮中 (2) 表格视图 传入一个指针self, 回调视图控制器中的方法 (3) block 语句块, 解决回调, 理解为"匿名函数", 定义在方法里面 2.block的基本使用(语法) 涉及知识点: 定义block变量,定义block语句块 block参数和返回值 block捕获外部变量(包括

IOS开发之XML解析以及下拉刷新上拉加载更多的分享

IOS开发之XML解析 1.XML格式 <?xml version="1.0" encoding="utf-8" ?> 表示XML文件版本, 内部文本使用的编码 <root> 表示根节点 <CityName>北京</CityName>  一个结点, CityName是结点名, 北京结点值 <Item key="1" value="A"></Item>  

李洪强IOS开发之iOS好项目收集

李洪强IOS开发之iOS好项目收集 在这里收集一些最近出现的比较实用好玩的框架或者项目,会不断更新 项目 简述 日期 SCTableViewCell 类似与QQ侧滑删除Cell的Demo 201501018 JHChainableAnimations 可读性好使用方便的动画库,语法类似与Masonry,使用链式编程 20150506 awesome-ios-chart iOS平台下的各种图表组件 20150513 DevArticles iOS Animation 主流炫酷动画框架(特效)收集整

【IOS开发之Objective-C】协议和代理

在现实生活中我们存在各种各样的协议,但是都有一个共同点,就是拟定的协议,就要遵守,不遵守就是违约.在OC中也有协议这一个概念而且和我们现实生活中的协议的特点是类似的.有时我们自己想做什么事,但是现在没这个能力自己去做,亲力亲为,我们就需要找代理来帮我们做了.那么在OC中也有代理这个概念.下面就简单的说说OC中的协议和代理. 一.协议 在<[IOS开发之Objective-C]对象的交互>中实现了一种对象的交互的方式.在OC中还有其他方式,比如说协议,在OC中用协议来规范接口,是实现对象之间的交