【iOS RunLoop】

文章目录

  • 前言-什么是RunLoop?
    • 默认情况下主线程的RunLoop原理
  • 1. RunLoop对象
      • RunLoop对象的获取
    • CFRunLoopRef源码部分(引入线程相关)
  • 2. RunLoop和线程
  • 3. RunLoop相关的类
    • RunLoop相关类的实现
    • CFRunLoopModeRef
      • 五种运行模式
      • CommonModes
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
      • 定时器滑动不准确
    • CFRunLoopObserverRef
      • 什么是Mode Item?
    • RunLoop内部逻辑
    • RunLoop休眠的实现原理
      • RunLoop小结
  • 4. RunLoop实际应用
    • RunLoop的启动方法
    • RunLoop关闭
    • imageView延迟显示
    • 常驻线程
    • 线程保活
    • NSTimer不准

前言-什么是RunLoop?

什么是RunLoop? 跑圈?字面上理解确实是这样的。

Apple官方文档这样解释RunLoop

RunLoop是与线程息息相关的基本结构的一部分。RunLoop是一个调度任务和处理任务的事件循环。RunLoop的目的是为了在有工作的时候让线程忙起来,而在没有工作的时候让线程进入休眠状态。

之所以iOSapp能够持续的响应从而让程序保持运行状态,在于其存在一个事件循环(Event Loop)机制: 线程能够随时响应并处理事件的机制,这种机制要求线程不能退出从而高效的完成事件调度和处理。

在iOS这种事件循环机制就叫做RunLoop

RunLoop实际上是一个对象,对象在循环中处理程序运行过程出现的各种事件(比如触摸事件,UI刷新事件,定时器事件,Selector事件)从而保持程序的持续运行并且让程序在没有事件处理的时候进入休眠状态,从而节省CPU资源达到提升程序性能的目的。

默认情况下主线程的RunLoop原理

#import <UIKit/UIKit.h>
#import "AppDelegate.h"int main(int argc, char * argv[]) {NSString * appDelegateClassName;@autoreleasepool {// Setup code that might create autoreleased objects goes here.appDelegateClassName = NSStringFromClass([AppDelegate class]);}return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

return UIApplicationMain(argc, argv, nil, appDelegateClassName);: 这是 iOS 应用程序的主运行循环,它负责处理用户事件、界面更新和应用程序的主要逻辑UIApplicationMain 函数创建应用程序对象和主运行循环,并传递控制权给应用程序的委托类(AppDelegate)来处理应用程序的逻辑。

  • 其中的UIApplicationMain函数内部帮我们开启了主线程的RunLoop
  • UIApplicationMain内部拥有一个无限循环的代码。
function loop() {initialize();do {var message = get_next_message();process_message(message);} while (message != quit);
}

程序会一直在do-while循环中执行.

Apple官方的RunLoop模型图
请添加图片描述

RunLoop就是线程中的一个循环,RunLoop在循环中不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受消息,然后对接收到的事件通知线程进行处理,并在没有事件的时候进行休息。

1. RunLoop对象

RunLoop是一个对象。

RunLoop对象的获取

RunLoop对象是基于CFFoundation框架的CFRunLoopRef类型封装的对象。

  • NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
[NSRunLoop currentRunLoop];//获得当前RunLoop对象
[NSRunLoop mainRunLoop];//获得主线程的RunLoop对象

CoreFoundation框架的 CFRunLoopRef对象

  • CFRunLoopRef是在CoreFoundation框架内的,其提供了纯C语言函数的API,所有这些API都是线程安全的。
CFRunLoopGetCurrent();//获得当前线程的RunLoop对象
CFRunLoopGetMain();//获得主线程的RunLoop对象

请添加图片描述

那么对应的两种方式就是

- (void)getRunLoop {NSRunLoop *runloop  = [ NSRunLoop currentRunLoop];NSRunLoop *manRlp = [NSRunLoop mainRunLoop];CFRunLoopRef cfRlp = CFRunLoopGetCurrent();CFRunLoopRef mainCfRlp = CFRunLoopGetMain();
}

看下CFRunLoopGetCurrentCFRunLoopGetMain的具体实现

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 neededif (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS neededreturn __main;
}

发现在过程都调用了_CFRunLoopGet0这个函数,后面再进行讲解。

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 looppthread_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;CFTypeRef _counterpart;
};

对于一些属性之外,重点需要关注三个成员变量

pthread_t _pthread;【RunLoop对应的线程】
CFRunLoopModeRef _currentMode;【当前运行的mode】
CFMutableSetRef _modes;【存储的是CFRunLoopModeRef】

看看RunLoop和线程的关系

2. RunLoop和线程

先看一下_CFRunLoopGet0这个函数是怎么实现的,和RunLoop和线程有什么关系。

//全局的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);if (!__CFRunLoops) {__CFSpinUnlock(&loopsLock);//第一次进入时,创建一个临时字典dictCFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//根据传入的主线程获取主线程对应的RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//保存主线程,将主线程-key和RunLoop-Value保存到字典中CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoopsif (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//释放dictCFRelease(dict);}//释放mainRunLoopCFRelease(mainLoop);__CFSpinLock(&loopsLock);}//以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建//从全局字典里获取对应的RunLoopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFSpinUnlock(&loopsLock);if (!loop) {//如果取不到,就创建一个新的RunLoopCFRunLoopRef newLoop = __CFRunLoopCreate(t);__CFSpinLock(&loopsLock);loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//创建好之后,以线程为key,runLoop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runLoopif (!loop) {//把newLoop存入字典__CFRunLoops,key是线程tCFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);loop = newLoop;}// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it__CFSpinUnlock(&loopsLock);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);}}return loop;
}

这段源码告诉我们:

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

3. RunLoop相关的类

与RunLoop相关的类有5个。

  1. CFRunLoopRef: 代表了RunLoop对象
  2. CFRunLoopModeRef: 代表了RunLoop的运行模式
  3. CFRunLoopSourceRefRunLoop模型中提到的输入源。
  4. CFRunLoopTimerRef: 定时源
  5. CFRunLoopObserverRef: 观察者,监听RunLoop状态的改变。
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source / Timer / Observer
    请添加图片描述
  • 每次调用RunLoop的主函数的时候, 只允许指定其中的一个运行模式(CFRunLoopModeRef),就是被称作CurrentMode;
  • 如果需要切换Mode,只能通过退出Loop,然后需要重新指定一个Mode进入,这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响
  • 如果一个mode中一个Sourcr/Timer/Observer都没有,则RunLoop会直接退出,不进入循环。

RunLoop的结构和套娃一样,RunLoop里面装着ModeMode里面装着Souce / Observer / Timer

RunLoop相关类的实现

一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
这句话其实就是5个相关类的关系

CFRunLoopModeRef

代表了RunLoop的运行模式,但这里请理清概念,我们的RunLoop里可以装多个Mode,只是我们在指定运行的时候要指定一个Mode

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;//Sources0CFMutableSetRef _sources1;//Sources1CFMutableArrayRef _observers;//观察者CFMutableArrayRef _timers;//定时器
------------------------------------------CFMutableDictionaryRef _portToV1SourceMap;//字典    key是mach_port_t,value是CFRunLoopSourceRef__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中CFIndex _observerMask;
};
  • 一个CFRunLoopModeRef对象有name属性,若干source0source1timerobserverport,可以看出来事件都是由mode在管理,而RunLoop负责管理Mode

五种运行模式

系统默认注册的五个Mode

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响(与用户交互事件的Mode)
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultModeUITrackingRunLoopMode用,并不是一种真正的Mode (伪模式,并不是一种真正的模式)

其中kCFRunLoopDefaultModeUITrackingRunLoopModekCFRunLoopCommonModes是我们开发中需要用到的模式。

CommonModes

在RunLoop对象中,前面有一个CommonModes成员变量。

//简化版本
struct __CFRunLoop {pthread_t _pthread;CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的modeCFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)CFRunLoopModeRef _currentMode;//当前运行的modeCFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
};
  • 一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoopcommonModes中。
  • 每当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)) {//获取所有的_commonModeItemsCFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;//获取所有的_commonModesCFSetAddValue(rl->_commonModes, modeName);if (NULL != set) {CFTypeRef context[2] = {rl, modeName};//将所有的_commonModeItems逐一添加到_commonModes里的每一个ModeCFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);CFRelease(set);}} else {}__CFRunLoopUnlock(rl);
}

总体来说,这个方法的主要作用是将一个指定的 commonModeItems 集合添加到运行循环的共同模式集合中,并将 commonModeItems 添加到共同模式集合中的每个 Mode 中,以确保共同模式的事件源在多个 Mode 下都能得到处理。它涉及到 CoreFoundation 框架中运行循环的底层操作,用于管理运行循环中的事件和模式。

CFRunLoopSourceRef

CFRunLoopSourceRefRunLoop模型中提到的输入源,也就是事件产生的地方。

struct __CFRunLoopSource {CFRuntimeBase _base;uint32_t _bits;pthread_mutex_t _lock;CFIndex _order;   //执行顺序CFMutableBagRef _runLoops;//包含多个RunLoop//版本union {CFRunLoopSourceContext version0;    /* immutable, except invalidation */CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */} _context;
};

刚才上面提到souce存在 source0 和 source1两个版本,他们分别做了什么?

  • Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source), 将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件
  • Source1包含了一个mach_port和一个回调(函数指针),可以用于内核和其他线程相互发送消息,这种Source能主动唤醒RunLoop线程。
  • ⚠️:对于button的点击事件属于Source0函数的执行内容。点击事件就是Source0进行处理的。
  • Source1则是用来接受和分发事件,分发到Souce0进行处理。

CFRunLoopTimerRef

CFRunLoopTimerRef: 定时源- 基于时间的触发器。

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

当我们调用NSTimer的scheduledTimerWithTimeInterval的时候

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

系统会自动加入NSDefaltRunLoopMode.

等于如下代码

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

定时器滑动不准确

在知乎日报的时候,滑动tableView造成了上面自动轮播图的定时器失效问题。
常见的问题就是:当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况

原因就是:

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

解决:

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

在 iOS 中,当你滑动 UITableView 或其他滚动视图时,主线程上的 RunLoop 切换到 UITrackingRunLoopMode,这是一个特殊的运行循环模式,用于处理用户交互事件,例如滚动手势。在默认情况下,如果你在主线程上使用定时器,它会在默认的运行循环模式 NSDefaultRunLoopMode 下运行。由于 RunLoop 一次只能处理一个运行循环模式,当你滑动时,NSDefaultRunLoopMode 被切换到 UITrackingRunLoopMode,导致定时器事件暂停,直到滑动结束。

为了解决这个问题,可以使用 kCFRunLoopCommonModes。代表了一个 “common mode set”,它同时包括了 NSDefaultRunLoopModeUITrackingRunLoopMode。通过在定时器的添加中使用 kCFRunLoopCommonModes,可以使定时器在默认模式和追踪模式下都得到触发,从而避免滑动导致定时器暂停的问题。

CFRunLoopObserverRef

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

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {CFRuntimeBase _base;pthread_mutex_t _lock;CFRunLoopRef _runLoop;//监听的RunLoopCFIndex _rlCount;//添加该Observer的RunLoop对象个数CFOptionFlags _activities;		/* immutable */CFIndex _order;//同时间最多只能监听一个CFRunLoopObserverCallBack _callout;//监听的回调CFRunLoopObserverContext _context;//上下文用于内存管理
};//观测的时间点有一下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoopkCFRunLoopBeforeTimers = (1UL << 1), // 即将处理TimerkCFRunLoopBeforeSources = (1UL << 2), // 即将处理SourcekCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7),// 即将退出RunLoopkCFRunLoopAllActivities = 0x0FFFFFFFU
};

什么是Mode Item?

Mode到底包含哪些类型的元素?
前面提到过 CFMutableSetRef _commonModeItems:存储所有commonModeitem (source、timer、observer)

上面的 Source/Timer/Observer 被统称为 mode item

  • 所有的mode item都可以被添加到Mode中,Mode中可以包含多个mode item,一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会退出,不进入循环.

RunLoop内部逻辑

在这里插入图片描述

RunLoop休眠的实现原理

从用户态切换到内核态, 在内核态让线程进行休眠,有消息的时候唤起线程,回到用户态处理消息

RunLoop小结

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

  • RunLoop的运行必定要指定一种mode,并且该mode必须注册任务事件。
  • RunLoop是在默认mode下运行的,当然也可以指定一种mode运行,但是只能在一种mode下运行。
  • RunLoop内部实际上是维护了一个do-while循环,线程就会一直留在这个循环里面,直到超时或者手动被停止。
  • RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态,否则线程处理事件

4. RunLoop实际应用

  1. 控制线程生命周期(线程保活)
  2. 解决NSTimer在滑动时停止工作的问题
  3. 监控应用卡顿
  4. 性能优化

RunLoop的启动方法

- (void)run; // 默认模式
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
  1. run没有任何条件就可以启动,虽然简单,但是实际的操控结果并不影响,RunLoop是一个do while循环,倘若无条件的运行RunLoop将线程永远的放入循环,这就使我们没有办法控制循环本身,只能靠杀死进程来停止RunLoop
  2. runUnitDate:设置时间限制。
  • 设置了超时的时间,超过这个时间RunLoop就结束了。
  1. runMode:beforeDate:在特定模式下启动。
  • 可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的

⚠️:

  1. 第一种会一直运行下去,并且一直在NSDefaultRunLoopMode模式下,重复调用runMode:beforeDate:方法。
  2. 第二种会在超时时间之前一直在NSDefaultRunLoopMode模式下调用runMode:beforeDate:方法。
  3. 第三种则会在超时时间到达或者第一个inputsource被处理前一直调用runMode:beforeDate:方法。

RunLoop关闭

  1. 将运行的循环配置设置为超时。
  2. 手动停止

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

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

imageView延迟显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况。

我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束再显示:

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

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

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

常驻线程

开发应用程序的过程中,如果后台操作十分频繁,比如后台播放音乐、下载文件等等,我们希望执行后台代码的这条线程永远常驻内存,我们可以添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop

@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@endself.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];[self.thread start];
- (void)runThread {NSLog(@"开启子线程:%@", [NSThread currentThread]);
// 子线程的RunLoop创建出来需要手动添加事件输入源和定时器 因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。//下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop] run];// 测试开始RunLoop// 未进入循环就会执行该代码NSLog(@"failed");
}// 同时在我们自己新建立的这个线程中写一下touchesBegan这个方法测试点击空白处会不会在子线程相应方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event  {[self performSelector:@selector(runTest) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)runTest {NSLog(@"子线程点击空白:%@", [NSThread currentThread]);
}

请添加图片描述
可以看到RunLoop成功启动进入循环,点击屏幕的时候也是在子线程调用方法,这样子子线程启动完成之后就达到了常驻线程的目的。

线程保活

场景:
平时创建子线程时,线程上的任务执行完这个线程就会销毁掉。
有时我们会需要经常在一个子线程中执行任务,频繁的创建和销毁线程就会造成很多的开销,这时我们可以通过runloop来控制线程的生命周期。

在下面的代码中,因为runMode:beforeDate:方法是单次调用,我们需要给它加上一个循环,否则调用一次runloop就结束了,和不使用runloop的效果一样。

这个循环的条件默认设置成YES,当调用stop方法中,执行CFRunLoopStop()方法结束本次runMode:beforeDate:,同时将循环中的条件设置为NO,使循环停止,runloop退出。

@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL stopped;- (void)viewDidLoad {[super viewDidLoad];self.view.backgroundColor = [UIColor greenColor];UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];[self.view addSubview:button];[button addTarget:self action:@selector(pressPrint) forControlEvents:UIControlEventTouchUpInside];[button setTitle:@"执行任务" forState:UIControlStateNormal];button.frame = CGRectMake(100, 200, 100, 20);UIButton *stopButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];[self.view addSubview:stopButton];[stopButton addTarget:self action:@selector(pressStop) forControlEvents:UIControlEventTouchUpInside];[stopButton setTitle:@"停止RunLoop" forState:UIControlStateNormal];stopButton.frame = CGRectMake(100, 400, 100, 20);self.stopped = NO;//防止循环引用__weak typeof(self) weakSelf = self;self.thread = [[NSThread alloc] initWithBlock:^{NSLog(@"Thread---begin");//向当前runloop添加Modeitem,添加timer、observer都可以。因为如果mode没有item,runloop就会退出[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];while (!weakSelf.stopped) {[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];}NSLog(@"Thread---end");}];[self.thread start];
}
- (void)pressPrint {//子线程中调用print[self performSelector:@selector(print) onThread:_thread withObject:nil waitUntilDone:NO];
}//子线程需要执行的任务
- (void)print {NSLog(@"%s, %@", __func__, [NSThread currentThread]);
}- (void)pressStop {//子线程中调用stopif (_stopped == NO ) {[self performSelector:@selector(stop) onThread:_thread withObject:nil waitUntilDone:YES];}}//停止子线程的runloop
- (void)stop {//设置标记yesself.stopped = YES;//停止runloopCFRunLoopStop(CFRunLoopGetCurrent());NSLog(@"%s, %@", __func__, [NSThread currentThread]);//解除引用, 停止runloop这个子线程就会deallocself.thread = nil;
}- (void)dealloc {NSLog(@"%s", __func__);
}

NSTimer不准

timer在实际的开发中一般不在主线程的RunLoop里面存在,因为主线程在执行阻塞任务的时候timer的计时器也会导致不准确。

如果timer在主线程里面阻塞 如何解决timer不准确的问题。

  1. 放入子线程中,但是需要开辟线程和控制线程的生命周期,成本较大。
  2. 使用GCD的定时器计时,避免阻塞。

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

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

相关文章

【MMCV】mmpretrain/mmclassification概览、环境安装与验证

概览 MMPretrain 是一个全新升级的预训练开源算法框架,旨在提供各种强大的预训练主干网络, 并支持了不同的预训练策略。MMPretrain 源自著名的开源项目 MMClassification 和 MMSelfSup,并开发了许多令人兴奋的新功能。 目前,预训练阶段对于视觉识别至关重要,凭借丰富而强…

C高级--day2(用户相关操作 磁盘相关操作 shell脚本 修改环境变量)

#include<myhead.h>void fun(int n) {if(n>9){fun(n/10);printf("%d\t",n%10);putchar(10);return;}else{printf("%d\n",n%10);return;} } int main(int argc, const char *argv[]) {int num;printf("请输入一个整数&#xff1a;");sca…

Maven引入本地第三方Jar包

背景 引入第三方的Jar包&#xff0c;本来是在项目结构的库里面引入的jar包&#xff0c;但是用maven打包时报错&#xff0c;所以后面想着用maven引入本地地址的方式。 解决方法 1.将Jar包放到根目录里面&#xff0c;我这边放在了lib文件中,如图所示&#xff1a; 2.在pom文件…

Mybatis引出的一系列问题-Mybatis缓存机制的探究

Mybatis 使用到了两种缓存&#xff1a;本地缓存&#xff08;local cache&#xff09;和二级缓存&#xff08;second level cache&#xff09;。 一级缓存默认是开启的&#xff0c;而且不能关闭&#xff0c;MyBatis的一些关键特性&#xff08;例如通过<association>和<…

机器人“瓦力”近在咫尺?谷歌最新的RT-2 AI模型简介

“首创”的机器人 AI 模型能够识别垃圾并执行复杂的动作。 上周五&#xff0c;谷歌 DeepMind 宣布了机器人变形器 2&#xff08;RT-2&#xff09;&#xff0c;这是一种“首次推出”的视觉-语言-行动&#xff08;VLA&#xff09;模型&#xff0c;利用从互联网上抓取的数据&…

Linux学习之延时计划任务anacontab和锁文件flock

cat /etc/redhat-release看到操作系统的版本是CentOS Linux release 7.6.1810 (Core)&#xff0c;uname -r可以看到内核版本是3.10.0-957.21.3.el7.x86_64 参考的博客有&#xff1a; 1.《Linux anacron命令用法详解》 2.《详解anacron 命令》 3.《Anacron的用法》 4.《shell脚…

Libevent开源库的介绍与应用

libeventhttps://libevent.org/ 一、初识 1、libevent介绍 Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库&#xff0c;主要有以下几个亮点&#xff1a;事件驱动&#xff08; event-driven&#xff09;&#xff0c;高性能;轻量级&#xff0c;专注于网络&#xff…

【雕爷学编程】MicroPython动手做(37)——驱动LCD与图文显示

MixPY——让爱(AI)触手可及 MixPY布局 主控芯片&#xff1a;K210&#xff08;64位双核带硬件FPU和卷积加速器的 RISC-V CPU&#xff09; 显示屏&#xff1a;LCD_2.8寸 320*240分辨率&#xff0c;支持电阻触摸 摄像头&#xff1a;OV2640&#xff0c;200W像素 扬声器&#…

【Ajax】笔记-设置CORS响应头实现跨域

CORS CORS CORS是什么&#xff1f; CORS(Cross-Origin Resource Sharing),跨域资源共享。CORS是官方的跨域解决方案&#xff0c;它的特点是不需要在客户端做任何特殊的操作&#xff0c;完全在服务器中进行处理&#xff0c;支持get和post请求。跨域资源共享标准新增了一组HTTP首…

页面技术基础-html

页面技术基础-html 环境准备&#xff1a;在JDBC中项目上完成代码定义 1. 新建一个 Module:filr->右键 -》Module -》Java-》next->名字(html_day1)->finish 2. 在 Moudle上右键-》第二个选项&#xff1a;add framework .. -> 选择JavaEE下第一个选项 Web Apllicat…

Vue系列第六篇:axios封装,登录逻辑优化,404页面实现,Go语言跨域处理

第五篇利用vue实现了登录页面&#xff0c;用go语言开发了服务端并最后在nginx上进行了部署。本篇将axios封装&#xff0c;登录逻辑优化&#xff0c;404页面实现。 目录 1.前端 1.1代码结构 1.2源码 2.服务端 2.1源码 3.运行效果 4.注意事项 4.1webpack.config.js和vue…

Docker安装RabbitMQ集群

一、安装单机版 1、更新yum源安装 vim、net-tools等工具 yum update -yyum install vim -yyum install net-tools -y 2、安装单机版 #创建挂载路径 mkdir /data/rabbitmq -p#拉取镜像 docker pull rabbitmq:3.9-management#创建容器并启动 docker run -d -it --name rabbi…

从k8s 的声明式API 到 GPT的 提示语

命令式 命令式有时也称为指令式&#xff0c;命令式的场景下&#xff0c;计算机只会机械的完成指定的命令操作&#xff0c;执行的结果就取决于执行的命令是否正确。GPT 之前的人工智能就是这种典型的命令式&#xff0c;通过不断的炼丹&#xff0c;告诉计算机要怎么做&#xff0…

Cesium 加载ArcGIS Server切片服务错级问题

1.首先上官方api说明 ArcGisMapServerImageryProvider - Cesium Documentation 里面没有 zoomoffset参数!!! 2.如果按照互联网栅格切片规则 3857、4326、4490常用切片层级参数,则直接加载显示地图 viewer.imageryLayers.addImageryProvider(new Cesium.ArcGisMapServerI…

三种方式创建对象的几种方式及new实例化时做了什么?

创建对象的几种方式 利用对象字面量创建对象 const obj {}2.利用 new Object创建对象 const obj new Object()3.使用 构造函数实例化对象 function Fn(name) {this.name name} const obj new Fn(张三) console.log(obj.name); //张三为什么要用构造函数的形式&#xff1…

node.js系列-常见问题处理方案(持续更新)

问题1&#xff1a;nodejs 如何使用 atob、btoa 解决方案&#xff08;base64与uint8array转换&#xff09;&#xff0c;btoa和atob在nodejs中应该怎么写&#xff1f; 浏览器中我们可以这样使用&#xff1a; btoa(123456) MTIzNDU2 atob(MTIzNDU2) 123456node.js中实现方案 con…

Java版Spring Cloud+Spring Boot+Mybatis+uniapp知识付费平台讲解+免费搭建 qt

&#xfeff;Java版知识付费源码 Spring CloudSpring BootMybatisuniapp前后端分离实现知识付费平台 提供职业教育、企业培训、知识付费系统搭建服务。系统功能包含&#xff1a;录播课、直播课、题库、营销、公司组织架构、员工入职培训等。 提供私有化部署&#xff0c;免费售…

【1.4】Java微服务:服务注册和调用(Eureka和Ribbon实现)

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; 微服务 ✨特色专栏&#xff1a; 知识分享 &#x…

如何用python做自然语言处理

如何用python做自然语言处理 使用Python进行自然语言处理&#xff08;NLP&#xff09;是非常常见和强大的。以下是一些基本步骤&#xff1a; 安装所需的库&#xff1a; 首先&#xff0c;您需要安装一些用于自然语言处理的Python库&#xff0c;如NLTK&#xff08;自然语言工具包…

云原生之使用Docker部署homer静态主页

云原生之使用Docker部署homer静态主页 一、homer介绍1.1 homer简介1.2 homer特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载homer镜像五、部署homer静态主页5.1 创建挂…