iOS-RunLoop详解(一):底层结构源码学习

image-20210512112630849
image-20210512112700812
image-20210512112721759
image-20210512112742222
image-20210512112806252
image-20210512112838719
image-20210512112855440
image-20210512112912651
image-20210512112931568
image-20210512112951469
image-20210512113013491
image-20210512113049516

RunLoop概念

RunLoop介绍

RunLoop 是什么?RunLoop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

img

没有Runloop的程序

我们通过Xcode新建一个命令行项目,main.m文件里的代码如下

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

程序在执行完代码NSLog(@"Hello, World!");之后,就会通过 return 0;推出程序,这是一种线性的执行流程。

我们再新建一个iOS项目,你看到的main.m文件是这个样子的

#import 
#import "AppDelegate.h"

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

我们会进入app的界面,然后app就不会退出了,会一直运行着。

在命令行工程里面的main.m里面,是没有加Runloop的,而iOS工程的main.m里面,其实在UIApplicationMain()这个方法中,系统加上了Runloop,让程序可以一直循环运行下去不退出。

iOS项目,在main函数中系统就会自动帮我们创建runloop对象:return UIApplicationMain(argc, argv, nil, appDelegateClassName);.
RunLoop的基本作用就是:

  • 保证程序的基本运行.程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行

  • 处理App中的各种事件 (比如:触摸事件,定时器事件 等等).

  • 节省 CPU 资源,提高程序性能: 该做事时做事,没有事的时候就休息.(程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CPU,现在没有事情做,我要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情)

RunLoop工作原理的伪代码大概如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            //睡眠中等待消息
            int message = sleep_and_wait();
            //处理消息
            retVal = process_message(message);
        } while (retVal = 0);
        return 0;
    }
}

流程:条件成立的时候一直循环:有事情就处理事情,没有事情就休眠睡觉.Runloop其实就是一个do-while循环,每次循环一圈,都会判断一次retVal,决定是否结束循环,继续执行循环外的代码。

RunLoop对象

iOS中提供了两套API来访问RunLoop:

  • Foundation : NSRunLoop : OC 框架
  • Core Foundation : CFRunLoopRef : C 语言框架

NSRunLoopCFRunLoopRef都代表Runloop对象,NSRunLoop是基于CFRunLoopRef的一层OC包装,CFRunLoopRef是开源的

我们下载好源代码后新建一个项目,把源代码拖到项目中.

Runloop对象的获取
  • Foundation
    [NSRunloop currentRunLoop];获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop];获得主线程的Runloop对象
  • Core Foundation
    CFRunLoopGetCurrent();获得当前线程的RunLoop对象
    CFRunLoopGetMain();获得主线程的Runloop对象

CFRunLoop.c文件 -> 然后找到CFRunLoopGetCurrent函数 -> 进入_CFRunLoopGet0函数

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//根据线程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    ⚽️⚽️⚽️⚽️⚽️⚽️
    static CFMutableDictionaryRef __CFRunLoops = NULL; //字典
    // 获取 runloop 对象 参数:传入一个 字典 和 key (线程)
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //如果 runloop 不存在 , 就创建,并放到字典中
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    
    ⚽️⚽️⚽️⚽️⚽️⚽️
}

RunLoop是在第一次获取的时候创建的,并且RunLoop和 线程 是 一一对应的关系,RunLoop是存放在一个全局字典中:以线程作为key,RunLoop作为value.

Runloop与线程

为什么聊Runloop一定要搭上线程?我们知道,程序里的每一句代码,都会在线程(主线程/子线程)里面被执行,上面四种获得Runloop对象的代码也不例外,一定是跑在线程里面的。之前我们说到,Runloop是为了让程序不退出,其实更准确地说,是为了保持某个线程不结束,只要还有未结束的线程,那么整个程序就不会退出,因为线程是程序的运行的调度的基本单元。

线程与Runloop的关系是一对一的,一个新创建的线程,是没有Runloop对象的,当我们在该线程里第一次通过上面的API获得Runloop时,Runloop对象才会被创建,并且通过一个全局字典将Runloop对象和该线程存储绑定在一起,形成一对一关系。

Runloop会在线程结束时销毁,主线程的Runloop已经自动获取过(创建),子线程默认没有开启RunLoop(直到你在该线程获取它)。RunLoop对象创建后,会被保存在一个全局的Dictionary里,线程作为keyRunloop对象作为value

Runloop对象底层结构

image-20210512120133677

我们可以在源码CFRunloop.c中找到Runloop的定义

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    // 核心组成 
    pthread_t _pthread;//RunLoop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;//当前运行的mode
    CFMutableSetRef _modes;//存储的是CFRunLoopModeRef
    // 核心组成 
    struct _block_item *_blocks_head;//doblocks的时候用到
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

RunLoop Mode Mode可以视为事件的管家,一个Mode管理着各种事件,它的结构如下:

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name; //mode名称
    Boolean _stopped;  //mode是否被终止
    char _padding[3];
    //几种事件
    // 核心组成 
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers; //通知
    CFMutableArrayRef _timers;//定时器
    // 核心组成 
    CFMutableDictionaryRef _portToV1SourceMap;//字典  key是mach_port_t,value是CFRunLoopSourceRef
    __CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

一个CFRunLoopMode对象有一个name,若干source0、source1、timer、observer和若干port,可见事件都是由Mode在管理,而RunLoop管理Mode。

Runloop相关的5个相关的类

  • CFRunLoopRef——这个就是Runloop对象
  • CFRunLoopModeRef——其内部主要包括四个容器,分别用来存放source0source1observer以及timer
  • CFRunLoopSourceRef——分为source0source1
    source0:包括 触摸事件处理、[performSelector: onThread: ]
    source1:包括 基于Port的线程间通信、系统事件捕捉
  • CFRunLoopTimerRef——timer事件,包括我们设置的定时器事件、[performSelector: withObject: afterDelay:]
  • CFRunLoopObserverRef——监听者,Runloop状态变更的时,会通知监听者进行函数回调,UI界面的刷新就是在监听到Runloop状态为BeforeWaiting时进行的。

对于以上这几个类相互之间的关系,可以通过如下的图来描绘

img

从图中可看出,一个RunLoop对象里面包含了若干个RunLoopModeRunLoop内部是通过一个集合容器_modes来装这些RunLoopMode的。

RunLoopMode内部核心内容是4个数组容器,分别用来装source0source1observertimerRunLoop对象内部有一个_currentMode,它指向了该RunLoop对象的其中一个RunLoopMode,它代表的含义是RunLoop当前所运行的RunLoopMode,所谓“运行”也就是说,RunLoop当前只会执行_currentMode所指向的RunLoopMode里面所包括的事件(source0、source1、observer、timer).RunLoop对象内部还包括一个线程对象_pthread,这就是跟它一一对应的那个线程对象。

RunLoop Source

Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem

CFRunLoopSource

根据官方的描述,CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0和source1两个版本,它的结构如下:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits; // 用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};
source0

source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态.

App自己管理的UIEven,包括触摸事件处理[performSelector: onThread: ],这个也可以通过代码来验证一下。首先看一下触摸事件,在ViewController里面重写方法

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"点击屏幕");
}
image-20210512123111824

#9 0x00007fff2039038a in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()可以看出系统是通过一个CF的函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__来调用UIKit进行事件处理的

source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。官方也指出可以自定义Source,因此对于CFRunLoopSourceRef来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,详细情况可以查看官方文档。

RunLoop 的状态:

RunLoop有以下几种状态:

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

下面我们写代码来验证一下这些状态的切换.首先写代码测试一下,NStimer唤醒RunLoop:

void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}
- (void)viewDidLoad {
    [super viewDidLoad];
    //     创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}

RUN>

image-20210512124423553

可以看出,Runloop的状态切换时,都会被observer监听到。

我们再创建一个UITextView,拖动UITextView看看RunLoop状态的切换情况

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }

            case kCFRunLoopExit: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }

            default:
                break;
        }
    });
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}

RUN>

image-20210512130047036

拖动UITextView看看

2021-05-12 12:46:02.808851+0800 Interview03-RunLoop[2854:125671] kCFRunLoopExit - kCFRunLoopDefaultMode
2021-05-12 12:46:02.809032+0800 Interview03-RunLoop[2854:125671] kCFRunLoopEntry - UITrackingRunLoopMode
2021-05-12 12:46:04.280133+0800 Interview03-RunLoop[2854:125671] kCFRunLoopExit - UITrackingRunLoopMode
2021-05-12 12:46:04.280280+0800 Interview03-RunLoop[2854:125671] kCFRunLoopEntry - kCFRunLoopDefaultMode

可以看到RunLoop频繁的在kCFRunLoopDefaultModeUITrackingRunLoopMode之间切换.

特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

你可能感兴趣的:(iOS-RunLoop详解(一):底层结构源码学习)