【iOS】—— RunLoop初学

news2024/9/26 5:17:46

RunLoop

文章目录

  • RunLoop
    • RunLoop简介
      • RunLoop基本使用
      • Runloop伪代码
      • Runloop模型图
    • Runloop对象
      • Runloop对象的获取
      • _CFRunLoopGet0方法
    • RunLoop的相关类
      • RunLoop相关类的实现
        • CFRunLoopRef
        • CFRunLoopModeRef
          • 五种运行模式
          • CommonModes
          • 什么是Mode Item?Mode到底包含哪些类型的元素?
        • CFRunLoopSourceRef
          • Source0
          • Source1
        • CFRunLoopTimerRef
          • 对于NSTimer:
          • NSTimer在滑动时停止工作的问题
        • CFRunLoopObserverRef
          • RunLoop的六种状态
    • RunLoop实现逻辑
      • __CFRunLoopRun源码实现
      • RunLoop回调(流程)
      • RunLoop启动方法
      • RunLoop关闭方法
        • ImageView延迟显示

RunLoop简介

简单的说 RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
在App运行的过程中,主线程的Runloop保证了主线程不被销毁从而保证应用的存活,从而能实时接收到用户的响应事件,能够触发定时事件。如果没有Runloop的话,程序执行完代码就会立马return。

RunLoop基本使用

  • 保持程序的持续运行
  • 处理app中各种事件
  • 节省CPU资源,提高程序性能:该做事时做事,该休眠时休眠。并且休眠时不占用CPU

Runloop伪代码

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

Runloop会一直在do-while循环中执行,这也就是我们写的程序不会在执行完一次代码之后就退出的原因了。

Runloop模型图

官方给的图:
请添加图片描述

Runloop对象

runloop实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。

Runloop对象的获取

Runloop对象主要有两种获取方式:

// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。

// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象

CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。
请添加图片描述
CoreFoundation框架中这两个函数的具体实现:

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

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

这两个方法里都是调用了_CFRunLoopGet0这个方法

_CFRunLoopGet0方法

//全局的Dictionary,key是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0是始终有效的“主线程”的同义词

//获取pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    //pthread为空时,获取主线程
    t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //如果这个__CFRunLoops字典不存在,即程序刚开始运行
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
    //第一次进入时,创建一个临时字典dict
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    //根据传入的主线程,获取主线程对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    //保存主线程的Runloop,将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
    	//释放dict,因为我们已经将dict的内存保存了,该临时变量也就没用了,要及时的释放掉
        CFRelease(dict);
    }
    //释放mainRunLoop,刚才用于获取主线程的Runloop,已经保存了,就可以释放了
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建

	//从全局字典里获取对应的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    //如果找不到对应的Runloop
    if (!loop) {
    //创建一个该线程的Runloop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
    //再次在__CFRunLoops中查找该线程的Runloop
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //如果在字典中还是找不到该线程的Runloop
    if (!loop) {
    	//把刚创建的该线程的newLoop存入字典__CFRunLoops,key是线程t
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        //并且让loop指向刚才创建的Runloop
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
    	//loop已经指向这个newLoop了,他也就可以释放了
    	CFRelease(newLoop);
    }

	//如果传入线程就是当前线程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
        	//注册一个回调,当线程销毁时,销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    //返回该线程对应的Runloop
    return loop;
}

流程大概是这个样子:
请添加图片描述

从这部分源码我们可以得知:

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取线程的RunLoop时创建,RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
  • RunLoop会在线程结束时销毁

RunLoop的相关类

与RunLoop相关的类有5个

  • CFRunLoopRef 代表了RunLoop的对象
  • CFRunLoopModeRef RunLoop的运行模式
  • CFRunLoopSourceRef 就是RunLoop模型图中提到的输入源(事件源)
  • CFRunLoopTimerRef 定时源
  • CFRunLoopObserverRef观察者,监听RunLoop状态的改变

1.一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
在这里插入图片描述
2.每次调用RunLoop的主函数时,只能指定其中的一个Mode,这个Mode被称作CurrentMode
3.如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
4.如果一个mode中一个Sourcr/Timer/Observer都没有,则RunLoop会直接退出,不进入循环

RunLoop相关类的实现

CFRunLoopRef

看一下CFRunLoopRef的源码:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort; // 使用 CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
    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; // do blocks时用到
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

Runloop中除了记录了一些属性外,重点还是一下几个:

pthread_t _pthread; 		// runloop对应的线程
CFMutableSetRef _commonModes;  // 存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems; // 存储所有common标记的mode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; // 当前运行的mode
CFMutableSetRef _modes;           // 装着一堆CFRunLoopModeRef类型,runloop中的所有模式

RunLoop中主要的变量就是_pthread_currentMode_modes_currentMode主要就是为了在_modes中找当前对应的mode的item,然后发送消息。而_commonModes_commonModeItems完全就是为了common标记mode准备的,如果我们选择的mode是commonMode,那么就不用在_modes中找每个mode对应的item了,因为被标记的mode的item都在_commonModeItems中,直接给他里边的所有item发消息就完了。

CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

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; // Sources0
    CFMutableSetRef _sources1; // 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 */
};

  • 一个CFRunLoopModeRef对象有一个name,若干source0source1timerobserverport,可以看出来事件都是由mode在管理,而RunLoop管理着Mode。
  • Mode实际上是Source,Timer 和 Observer 的集合,不同的Mode把不同组的Source、timer、Observer隔绝开来。runloop在某一时刻只能运行在一个mode下,处理这一个mode中的source、timer、observer。
    在这里插入图片描述
五种运行模式

系统默认注册的五个mode:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到
  • kCFRunLoopCommonModes:并不是一种模式
    只是一个标记,当mode标记为common时,将mode添加到runloop中的_commonModes中。runloop中的_commonModes实际上是一个Mode的集合,可使用CFRunLoopAddCommonMode()将Mode放到_commonModes中。每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的同步到具有Common标记的所有的Mode里
CommonModes

在RunLoop对象中,前面有一个有一个叫CommonModes的概念,它记录了所有标记为common的mode:

//简化版本
struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;//当前运行的mode
    CFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
};
  • 一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoop的commonModes中。
  • 每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。其底层原理如下:
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
    //获取所有的_commonModeItems
    CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
    //获取所有的_commonModes
    CFSetAddValue(rl->_commonModes, modeName);
    if (NULL != set) {
        CFTypeRef context[2] = {rl, modeName};
        //将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
        CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
        CFRelease(set);
    }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

CFRunLoop对外暴露的管理Mode接口只有下面两个:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
什么是Mode Item?Mode到底包含哪些类型的元素?
  • RunLoop需要处理的消息,包括time以及source消息,他们都属于Mode item
  • RunLoop也可以被监听,被监听的对象是observer对象,也属于Mode item
  • 所有的mode item都可以被添加到Mode中,Mode中可以包含多个mode item,一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会退出,不进入循环
  • mode暴露的mode item的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
  • 我们仅能通过操作mode name来操作内部的mode,当你传入一个新的mode name但RunLoop内部没有对应的mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef
  • 对于一个RunLoop来说,其内部的mode只能增加不能删除

CFRunLoopSourceRef

根据官方描述,CFRunLoopSourceRefinput sources的抽象。
CFRunLoopSource分为Source0Source1两个版本。
它的结构如下:

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

可一通过共用体union看出,它有两个版本,Source0Source1

Source0

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

Source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行时,必须要先把它标为signal状态,以下是source0结构体:

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);
    void	(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*perform)(void *info);
} CFRunLoopSourceContext;

Source0 :非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件。

Source1

Source1包含了mach_port和一个回调(函数指针),Source1可以监听系统端口,通过内核和其他线程通信,接收、分发系统事件,他能主动唤醒RunLoop(由操作系统内核进行管理)

Source1在处理的时候会分发一些操作给Source0去处理。

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_OSX || 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的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发sourse1。

例:
一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source1(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。

CFRunLoopTimerRef

CFRunLoopTimer是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行那个回调。
并且CFRunLoopTimerNSTimertoll-free bridged(对象桥接),可以相互转换。其结构如下:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;//包含timer的mode集合
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    CFIndex _order;            /* immutable */
    CFRunLoopTimerCallBack _callout; //timer的回调   
    CFRunLoopTimerContext _context;   //上下文对象
};
对于NSTimer:

scheduledTimerWithTimeIntervalRunLoop的关系:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以它就等同于下面代码:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSTimer在滑动时停止工作的问题

在之前写仿写知乎日报的时候,首页一个tableView里嵌套了一个scrollView,scrollView是一个自动轮播图,当我们拖动tableView的时候,scrollView也不动了,这个问题当时学长帮我解决的,一直不知道原因:

    self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timeOut:) userInfo:nil repeats:YES];
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addTimer:self.timer forMode:NSRunLoopCommonModes];

我们来写个例子:

    self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
    self.scrollView.scrollEnabled = YES;
    self.scrollView.pagingEnabled = YES;
    self.scrollView.bounces = YES;
    self.scrollView.alwaysBounceHorizontal = NO;
    self.scrollView.alwaysBounceVertical = NO;
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.contentSize = CGSizeMake(900, 200);
    self.scrollView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:self.scrollView];
    
    static int count = 0;
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];

输出结果:
请添加图片描述
在5和6秒之间我们拖动,timer就停止了。

造成这种问题的原因就是:

  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode
  • 当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了
  • 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了

想要解决这个问题也很简单,我们直接让NSTimer在两种mode下都能工作就完了,这就用到我们之前不太清楚其用法的NSRunLoopCommonModes了:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

当然你也可以把NSTimer分别加入到NSDefaultRunLoopModeUITrackingRunLoopMode,这两种写法是相同的,因为系统的mode是默认在_commonModes中的:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

修改之后就会一直正常运行了。

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者可以观察Runloop的各种状态,每个Observer都包含了一个回调(函数指针),当runloop的状态发生变化时,观察者就能通过回调接收到这个变化。

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;//监听的RunLoop
    CFIndex _rlCount;//添加该Observer的RunLoop对象个数
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;//同时间最多只能监听一个
    CFRunLoopObserverCallBack _callout;//监听的回调
    CFRunLoopObserverContext _context;//上下文用于内存管理
};
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
};

这六种状态都可以被observer观察到,我们也可以利用这一方法写一些特殊事件,创建监听,监听RunLoop的状态变化:

// 创建observer
CFRunLoopObserverRef ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    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;
    }
});
// 添加observer到runloop中
CFRunLoopAddObserver(CFRunLoopGetMain(), ob, kCFRunLoopCommonModes);
CFRelease(ob);

输出结果:
在这里插入图片描述
RunLoop是在不停的监听状态并做出反应

RunLoop实现逻辑

RunLoop的内部逻辑如下:
在这里插入图片描述

__CFRunLoopRun源码实现

精简后的__CFRunLoopRun函数,保留了主要代码,看一下具体实现:

//用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

//用指定的Mode启动,允许设置RunLoop的超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

//RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
	//首先根据modeName找到对应的mode
	CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
	//如果该mode中没有source/timer/observer,直接返回
	if (__CFRunLoopModeIsEmpty(currentMode)) return;
	
	//1.通知Observers:RunLoop即将进入loop
	__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

	//调用函数__CFRunLoopRun 进入loop
	__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
			//2.通知Observers:RunLoop即将触发Timer回调
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
			//3.通知Observers:RunLoop即将触发Source0(非port)回调
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
			///执行被加入的block
			__CFRunLoopDoBlocks(runloop, currentMode);

			//4.RunLoop触发Source0(非port)回调,处理Source0
			sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
			//执行被加入的Block
			__CFRunLoopDoBlocks(runloop, currentMode);

			//5.如果有Source1(基于port)处于ready状态,直接处理这个Source1然后跳转去处理消息
			if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

			//6.通知Observers:RunLoop的线程即将进入休眠
			if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

			//7.调用mach_msg等待接收mach_port的消息。线程将进入休眠,直到被下面某个事件唤醒:
			// 一个基于port的Source的事件
			// 一个Timer时间到了
			// RunLoop自身的超时时间到了
			// 被其他什么调用者手动唤醒
			__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

			//8.通知Observers:RunLoop的线程刚刚被唤醒
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

			//收到消息,处理消息
			handle_msg:
			//9.1 如果一个Timer时间到了,触发这个timer的回调
			if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
			//9.2 如果有dispatch到main_queue的block,执行block
			else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
            //9.3 如果一个Source1(基于port)发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            //执行加入到loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);

			//设置do-while之后的返回值
			if (sourceHandledThisLoop && stopAfterHandle) {
                // 进入loop时参数说处理完事件就返回
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                // 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                // 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                // source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }

			// 如果没超时,mode里没空,loop也没被停止,那继续loop。
	 	} while (retVal == 0);
    }

	//10. 通知Observers:RunLoop即将退出
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

实际上RunLoop就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回。

RunLoop回调(流程)

  • 当App启动时,系统会默认注册五个上面说过的5个mode
  • 当RunLoop进行回调时,一般都是通过一个很长的函数调出去(call out),当在代码中加断点调试时,通常能在调用栈上看到这些函数。这就是RunLoop的流程:
{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

RunLoop启动方法

  • 1.run,无条件
    • 无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。
  • 2.runUntilDate, 设置时间限制
    • 设置了超时时间,超过这个时间runloop结束,优于第一种
  • 3.runMode:beforeDate:,在特定模式下
    • 相对比较好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
    • 实际上run方法的实现就是无限调用runMode:beforeDate:方法
    • runUntilDate:也会重复调用runMode:beforeDate:方法,区别在于它超时就不会再调用

RunLoop关闭方法

  • 1.将运行循环配置为使用超时值运行。
  • 2.手动停止。

这里需要注意,虽然删除runloop的输入源和定时器可能会导致运行循环的退出,但这并不是个可靠的方法,系统可能会添加输入源到runloop中,但在我们的代码中可能并不知道这些输入源,因此无法删除它们,导致无法退出runloop。

我们可以通过上述2、3方法来启动runloop,设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制,而并不是依赖超时机制,这时我们可以通过 CFRunLoopStop()方法来手动结束一个 RunLoop。但是 CFRunLoopStop()方法只会结束当前正在执行的这次runMode:beforeDate:调用,而不会结束后续runloop的调用。

ImageView延迟显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况。
我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束在显示:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动tableview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。

这是因为限定了方法setImage只能在NSDefaultRunLoopMode模式下使用。而滚动tableview的时候,程序运行在tracking模式下面,所以方法setImage不会执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/495148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SWAT水文模型】SWAT水文模型建立及应用第四期: 气象数据的准备(中国区域高精度同化气象站CMADS)

SWAT水文模型建立及应用&#xff1a; 气象数据的准备 1 简介2 气象数据的准备&#xff08;中国区域高精度同化气象站CMADS&#xff09;2.1 数据说明2.2 数据下载 3 CMADS 数据集SWAT子集使用说明3.1 SWAT2009版本3.2 SWAT2012版本 参考 本博客主要介绍气象数据的准备&#xff0…

HulaCWMS呼啦企业网站管理系统 v3.0.4

源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/87630654 HulaCWMS(呼啦企业网站管理系统)是基于ThinkPHP5框架开发&#xff0c;安全高效&#xff0c;包括ThinkPHP5的所有特性。专注于企业、政府单位网站建设&#xff0c;以免费开源的方式&#xff0c;帮…

python - 模块使用详解

前言 Python有非常强大的第三方库&#xff0c;也有非常多的内置模块帮助开发人员实现某些功能&#xff0c;无需开发人员自己造轮子。本文介绍Python的模块。 什么是模块 模块简单来说就是一系列功能的集合体&#xff0c;如果将程序的开发比喻成拼图&#xff0c;模块就是各种…

读懂海尔智家大脑:深度体验的本质是深度生活

了解科技行业的读者&#xff0c;应该都对“大脑”这个名词不陌生。 “黑灯工厂”里指挥生产的“工业大脑”&#xff0c;繁忙机场里运筹帷幄的“航空大脑”&#xff0c;还有智慧城市建设的灵魂“城市大脑”…… 如果家也有一颗总揽全局的大脑&#xff0c;生活会发生什么改变呢&a…

SuperMap GIS基础产品三维GIS FAQ集锦(2)

SuperMap GIS基础产品三维GIS FAQ集锦&#xff08;2&#xff09; 【WebGL】桌面对三维缓存设置了最大最小可见高度&#xff0c;在iServer发布三维服务并进行预览是可以看到该效果的&#xff0c;但在前端代码打开该服务&#xff0c;最大最小可见高度效果丢失&#xff0c;请问怎…

Makefile零基础教学(一)初识makefile

从这篇文章开始就开始进入 Makefile 的零基础教程&#xff0c;相信只要看了本教程的都可以对 Makefile 有一个清晰的理解和正确的运用。那么现在就开始我们的 Makefile 学习之路。 文章目录 一、什么是 Makefile&#xff0c;优点&#xff1f;二、什么是 make, 为什么使用make?…

可拓展哈希

可拓展哈希 借CMU 15445的ppt截图来说明问题。 我们传统静态hash的过程是hash函数后直接将值存入对应的bucket&#xff0c;但是在可扩展hash中&#xff0c;得查询Directory&#xff08;左&#xff09;&#xff0c;存入directory指向的bucket&#xff08;右&#xff09;。 下面…

linux线程池、基于线程池的单例模式、读者写者问题

线程池&单例模式 一、什么是线程池二、设计思路三、代码实现四、基于线程池的单例模式4.1 懒汉模式设计思路 五、读者写者问题及读写锁 一、什么是线程池 线程池: 一种线程使用模式。线程过多会带来调度开销&#xff0c;进而影响缓存局部性和整体性能。而线程池维护着多个…

flashback database 深入学习

一.FlashbackDatabase 和 Restore Points 说明 OracleFlashback Database and restore points are related data protectionfeatures that allow you to rewind data back in time tocorrect any problems caused by logical data corruption or user errors within adesignate…

gopher 初探

文章目录 gopher协议简介发送GET请求curlgopher gopher发POST请求 gopher协议 简介 Gopher是Internet上一个非常有名的信息查找系统&#xff0c;它将Internet上的文件组织成某种索引&#xff0c;很方便地将用户从Internet的一处带到另一处。在WWW出现之前&#xff0c;Gopher是…

【AI绘画】——Midjourney关键词格式解析(常用参数分享)

目前在AI绘画模型中&#xff0c;Midjourney的效果是公认的top级别&#xff0c;但同时也是相对较难使用的&#xff0c;对小白来说比较难上手&#xff0c;主要就在于Mj没有webui&#xff0c;不能选择参数&#xff0c;怎么找到这些隐藏参数并且触发它是用好Mj的第一步。 今天就来…

Motrix 全能型下载神器(时隔两年,重新焕发生机)

简介 Motrix 是一款功能强大的下载管理器&#xff0c;支持 HTTP、FTP、BT、磁力链等多种下载协议&#xff0c;同时还具备浏览器插件和跨平台&#xff08;Windows、macOS、Linux&#xff09;的特点。它拥有多线程下载、断点续传、任务调度、自动更新等多种实用特性&#xff0c;…

学习笔记:基于SpringBoot的牛客网社区项目实现(六)之发表帖子 + 评论 + 私信

一、过滤敏感词 二、发布帖子 业务流程&#xff1a; 1、先创建帖子的实体DiscussPost&#xff0c;属性有帖子id&#xff0c;user_id&#xff0c;title&#xff0c;content&#xff0c;type&#xff0c;status&#xff0c;create_time&#xff0c;comment_count&#xff0c;scor…

【Python入门知识】NumPy 数组搜索,案例+理论讲解

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 搜索数组 可以在数组中搜索&#xff08;检索&#xff09;某个值&#xff0c;然后返回获得匹配的索引。 要搜索数组&#xff0c;请使用 where() 方法。 实例 查找值为 4 的索引&#xff1a; import numpy as nparr np.…

[ tool ] Xpath选择器和selenium工具基本使用

XPath xpath介绍 是一门在XML文档中查找信息的语言 html文档准备 doc <html><head><base hrefhttp://example.com/ /><title>Example website</title></head><body><div idimages><a hrefimage1.html aabb>Name: My…

车载激光雷达原理+双激光雷达方案+纯激光雷达感知3Dbonding box

集度汽车透露了首款汽车机器人概念车车头部位的设计细节&#xff0c;车前灯和车前盖处激光雷达的设计清晰可见。至此&#xff0c;集度汽车的车前盖、前向双激光雷达自动驾驶技术方案首次曝光。 集度汽车官方也称&#xff0c;“双激光雷达”设计方案极具创新性&#xff0c;激光…

求中位数?

文章目录 如何求一个中位数&#xff1f;[295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/)常规思路&#xff1a;解决思路&#xff1a; 如何求一个中位数&#xff1f; 如果输入一个数组&#xff0c;让你求中位数&#xff0c;简单的解决方法就…

不合格机器人工程讲师如何坦然面对失败

能坦然面对的重要原因是我自己从一开始就知道注定要失败的&#xff0c;或者说失败率接近99.9%。 能够从失败中成长&#xff0c;不断挑战自我&#xff0c;收获更多更大的失败也是一种体验。 失败是客观事实&#xff0c;坦然是主观上的正能量的情绪。 用客观态度对待客观事实。 …

JavaScript class和继承的原理

&#xff08;对于不屈不挠的人来说&#xff0c;没有失败这回事。——俾斯麦&#xff09; class 相关链接 MDN链接 有关类的详细描述 关于构造函数&#xff0c;原型和原型链的说明 类的概述 类是用于创建对象的模板。他们用代码封装数据以处理该数据。JS 中的类建立在原型上…

4月跳槽进字节跳动了,面试真简单...

前言: 最近金三银四跳槽季&#xff0c;相信很多小伙伴都在面试找工作&#xff0c; 怎样才能拿到大厂的offer&#xff0c;没有掌握绝对的技术&#xff0c;那么就要不断的学习&#xff0c;没有绝对的天才&#xff0c;只有持续不断的付出。对于我们每一个平凡人来说&#xff0c;…