目录

  • 一、Runloop概述
    • Runloop基本作用
    • Runloop伪代码
    • Runloop模型图
  • 二、Runloop对象
    • Runloop对象的获取
      • _CFRunLoopGet0方法
    • Runloop与线程的关系
    • CFRunLoopRef源码部分
      • RunLoop的结构
  • 二、Runloop相关的类
    • RunLoop机制
    • CFRunLoopModeRef类
      • 五种运行模式
      • CommonModes
        • 什么是Mode Item?Mode到底包含哪些类型的元素?
    • CFRunLoopSourceRef类
      • Source0
      • Source1
    • CFRunLoopTimerRef类
      • 对于NSTimer:
      • NSTimer在滑动时停止工作的问题:
    • CFRunLoopObserverRef类
      • Runloop的六种状态
  • 三、Runloop的内部逻辑
    • __CFRunLoopRun源码实现
    • RunLoop回调(流程)
    • RunLoop休眠的实现原理
  • 四、RunLoop的杂七杂八
    • RunLoop在实际开发中的应用
    • RunLoop启动方法
    • RunLoop关闭方法
    • ImageView延迟显示
    • 常驻线程
    • 线程保活
    • 定时器NSTimer

一、Runloop概述

一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。就比如之前学OC时使用的命令行程序,执行完程序就结束了。
而runloop目的就是使线程在执行完一次代码之后不会结束程序,而是使该线程处于一种休眠的状态,等待有事件需要处理的时候,再醒来处理。

简单的来说,runloop可以让线程在需要做事的时候忙起来,不需要的时候让线程休眠,使程序不会结束

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对象的获取

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 neededif (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS neededreturn __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);//第一次进入时,创建一个临时字典dictCFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//根据传入的主线程,获取主线程对应的RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//保存主线程的Runloop,将主线程-key和RunLoop-Value保存到字典中CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoopsif (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//释放dict,因为我们已经将dict的内存保存了,该临时变量也就没用了,要及时的释放掉CFRelease(dict);}//释放mainRunLoop,刚才用于获取主线程的Runloop,已经保存了,就可以释放了CFRelease(mainLoop);__CFSpinLock(&loopsLock);}//以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建//从全局字典里获取对应的RunLoopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFSpinUnlock(&loopsLock);//如果找不到对应的Runloopif (!loop) {//创建一个该线程的RunloopCFRunLoopRef newLoop = __CFRunLoopCreate(t);__CFSpinLock(&loopsLock);//再次在__CFRunLoops中查找该线程的Runlooploop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//如果在字典中还是找不到该线程的Runloopif (!loop) {//把刚创建的该线程的newLoop存入字典__CFRunLoops,key是线程tCFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//并且让loop指向刚才创建的Runlooploop = 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);}}//返回该线程对应的Runloopreturn loop;
}

流程就大概是这样:

(从学姐的学姐那里偷的)通过这段源码,我们可以看到:

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

主线程的runloop在程序运行启动时就会启动,在main.m函数中,通过UIApplicationMain开启主线程的runloop

Runloop与线程的关系

上面也有说到,总结一下:

CFRunLoopRef源码部分

看一下CFRunLoopRef的源码:

struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock;           /* locked for accessing mode list */__CFPort _wakeUpPort; // 使用 CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloopBoolean _unused;volatile _per_run_data *_perRunData;              // reset for runs of the run looppthread_t _pthread;         // runloop对应的线程uint32_t _winthread;CFMutableSetRef _commonModes;  // 存储的是字符串,记录所有标记为common的modeCFMutableSetRef _commonModeItems; // 存储所有commonMode的item(source、timer、observer)CFRunLoopModeRef _currentMode; // 当前运行的modeCFMutableSetRef _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发消息就完了!

RunLoop的结构


二、Runloop相关的类

与Runloop相关的类主要有以下几个:

  • CFRunLoopRef:代表了Runloop的对象(Runloop)
  • CFRunLoopModeRef:Runloop的运行模式(Mode)
  • CFRunLoopSourceRef:Runloop模型图中的输入源/事件源(Source)
  • CFRunLoopTimerRef:Runloop模型图中的定时源(Timer)
  • CFRunLoopObserverRef:观察者,能够监听Runloop的状态变化

RunLoop机制

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

  • 1.一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。
  • 2.每次RunLoop启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作CurrentMode。
  • 3.如果需要切换运行模式(CFRunLoopModeRef),只能退出Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
  • 4.这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响 。

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; // Sources0CFMutableSetRef _sources1; // Sources1CFMutableArrayRef _observers; // 观察者CFMutableArrayRef _timers; // 定时器
------------------------------------------CFMutableDictionaryRef _portToV1SourceMap;//字典    key是mach_port_t,value是CFRunLoopSourceRef__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERSdispatch_source_t _timerSource;dispatch_queue_t _queue;Boolean _timerFired; // set to true by the source when a timer has firedBoolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOOmach_port_t _timerPort;Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWSDWORD _msgQMask;void (*_msgPump)(void);
#endifuint64_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的modeCFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)CFRunLoopModeRef _currentMode;//当前运行的modeCFMutableSetRef _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)) {//获取所有的_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);
}

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_IPHONEmach_port_t    (*getPort)(void *info);void *   (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#elsevoid * (*getPort)(void *info);void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

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

举例说明source0和source1:
一个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在滑动时停止工作的问题:

这是个使用NSTimer的经典问题,当时写NSTimer的时候疯狂折磨我,就是我们在定义一个定时器后,并且界面存在一个滚动视图,当我们拖动滚动视图的时候其NSTimer停止执行事件了,等到拖拽完了之后它才会继续开始执行事件。
举例如下:

self.scr = [[UIScrollView alloc] init];
self.scr.frame = CGRectMake(100, 200, 100, 100);
self.scr.contentSize = CGSizeMake(300, 100);
self.scr.backgroundColor = [UIColor orangeColor];
[self.view addSubview:self.scr];static int count = 0;
// 带有 scheduledTimer 就会将定时器添加到默认模式下
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"%d", ++count);
}];

输出结果:

  • 通过图中圈的内容,我们发现,当我们在拖拽的时候,定时器的事件不执行了,等我们拖拽停止的时候,它又开始运行了,所以其中隔了几秒。

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

  • 当我们不做任何操作的时候,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;//监听的RunLoopCFIndex _rlCount;//添加该Observer的RunLoop对象个数CFOptionFlags _activities;        /* immutable */CFIndex _order;//同时间最多只能监听一个CFRunLoopObserverCallBack _callout;//监听的回调CFRunLoopObserverContext _context;//上下文用于内存管理
};

Runloop的六种状态

//观测的时间点有一下几个
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
};

这六种状态都可以被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找到对应的modeCFRunLoopModeRef 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)回调,处理Source0sourceHandledThisLoop = __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,执行blockelse 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休眠的实现原理

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

四、RunLoop的杂七杂八

RunLoop在实际开发中的应用

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

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:@"imgName.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

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

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

常驻线程

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

@property (nonatomic, strong) NSThread *thread;- (void)viewDidLoad {[super viewDidLoad];self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];[self.thread start];
}
- (void)runThread {NSLog(@"----run-----%@", [NSThread currentThread]);//如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。//下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉//方法1[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];// 方法2
//    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];// 方法3
//    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];// 方法1 ,2,3实现的效果相同,让runloop无限期运行下去[[NSRunLoop currentRunLoop] run];// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。NSLog(@"未开启RunLoop");
}
//我们同时在我们自己新建立的这个线程中写一下touchesBegan这个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// 利用performSelector,在self.thread的线程中调用runTest方法执行任务[self performSelector:@selector(runTest) onThread:self.thread withObject:nil waitUntilDone:NO];
}- (void)runTest {NSLog(@"----runTest------%@", [NSThread currentThread]);
}

点击屏幕后输出结果如下:

我们发现线程启动RunLoop成功了,没有打印未开启RunLoop,并且通过输出线程,发现执行点击事件的也是我们创建的这个线程,这样我们就达到常驻线程的目的了,该线程self.thread一直在等待一个事件加入其中,然后执行。

线程保活

平时创建子线程时,线程上的任务执行完这个线程就会销毁掉。
有时我们会需要经常在一个子线程中执行任务,频繁的创建和销毁线程就会造成很多的开销,这时我们可以通过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__);
}

这样我们就实现了线程保活,其中我们要注意,线程的管理是系统管理的,哪怕是在这个页面新建的线程,线程是否销毁和页面的销毁没有任何关系,这取决于系统
那么我们在某个页面销毁的时候就会存在页面新建的线程没有销毁这个问题。
解决这个问题最简单的办法就是,在销毁这个页面的时候,我们再重新调用一次stop方法,并将我们这个线程指向置为nil。

定时器NSTimer

在实际开发中,一般不把timer放到主线程的RunLoop中,因为主线程在执行阻塞的任务时,timer计时会不准。
如何让计时准确?如果timer在主线程中阻塞了怎么办?

  • 1.放入子线程中(即要开辟一个新的线程,但是成本是需要开辟一个新的线程)
  • 2.写一种跟RunLoop没有关系的计时,即GCD。(不会阻塞,推荐使用这种)
// GCD定时器(常用)
// 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 1.创建一个GCD定时器
/*第一个参数:表明创建的是一个定时器第四个参数:队列*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
// 2.设置定时器的开始时间,间隔时间,精准度
/*第1个参数:要给哪个定时器设置第2个参数:开始时间第3个参数:间隔时间第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能GCD的单位是纳秒 所以要*NSEC_PER_SEC*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);// 3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{NSLog(@"---%@--",[NSThread currentThread]);// 取消定时if (判断条件) {dispatch_source_cancel(timer);self.timer = nil;}
});
// 4.启动
dispatch_resume(timer);

可以看看:RunLoop之线程保活和浅谈RunLoop

【iOS】—— RunLoop详解相关推荐

  1. iOS RunLoop详解

    一.简介 CFRunLoopRef源码 RunLoop是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件.UI刷新事件.定时器事件.Selector事件),从而保持程序的 ...

  2. IOS UIView详解

    文章目录 IOS UIView详解 1.官方类分析 2. UIView 常用的属性 2.1 UIView的圆角加阴影效果的实现 2.2 UIView 属性 2.2.1 UIView 几何属性 2.2. ...

  3. FreeEIM 来点新知识iOS UIScrollView详解

     老程序员FreeEIM 来点新知识iOS UIScrollView详解 UIScrollView 顾名思义也知道这个是和滚动相关的控件,在Android开发时遇到过ScrollView,当内容的 ...

  4. iOS绘图详解-多种绘图方式、裁剪、滤镜、移动、CTM

    iOS绘图详解 摘要: Core Graphics Framework是一套基于C的API框架,使用了Quartz作为绘图引擎.它提供了低级别.轻量级.高保真度的2D渲染.该框架可以用于基于路径的 绘 ...

  5. iOS疯狂详解之AFNetworking图片缓存问题

    AFNetworking网络库已经提供了很好的图片缓存机制,效率是比较高的,但是我发现没有直接提供清除缓存的功能,可项目通常都需要添加 清除功能的功能,因此,在这里我以UIImageView+AFNe ...

  6. iOS多线程详解:实践篇

    iOS多线程实践中,常用的就是子线程执行耗时操作,然后回到主线程刷新UI.在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程.由于在iOS中除了主线程,其他子线程是独立 ...

  7. iOS疯狂详解之开源库

    youtube下载神器:https://github.com/rg3/youtube-dl vim插件:https://github.com/Valloric/YouCompleteMe vim插件配 ...

  8. [iOS] 国际化详解

    PS:修改设备系统语言方法 设置 -> 通用 -> 语言与地区 -> iPhone 语言 Settings -> General -> Language & Re ...

  9. iOS模式详解runtime面试工作

    简书:http://www.jianshu.com/p/19f280afcb24 对于从事 iOS 开发人员来说,所有的人都会答出「runtime 是运行时」,什么情况下用runtime?,大部分人能 ...

  10. UE4 IOS打包详解

    写在前面:因为是详解,所以可能写的有可能啰嗦,也有可能有些步骤是你经历过的,那么请忽略它,向下寻找可能的答案,如果没能解决你的问题,那么对此感到很抱歉,没能帮到你,欢迎你给我邮件: bluecode6 ...

最新文章

  1. 志澄观察:卫星互联网——太空经济新动力
  2. [网页设计]点睛价值
  3. 富士康c语言试卷答案,2015富士康笔试题目及答案
  4. 并发Bug之源有三,请睁大眼睛看清它们
  5. 掉坑里了,小记一下.
  6. 负数在计算机怎样表示与存储
  7. mysql 占用swap_查看swap占用情况
  8. android静态动画,LayoutAnimation给ListView中的item设置动态出场效果(实例)
  9. 关于DevExpress的心得
  10. [CF280D]k-Maximum Subsequence Sum
  11. Windows10重装专业版和mysql缺少dll文件或找不到入口点DLLRegisterServer问题处理
  12. 【AI初识境】从3次人工智能潮起潮落说起
  13. mysql插入数据不成功_mysql插入数据失败原因分析
  14. 三菱凌云3故障代码_上海三菱凌云2故障代码
  15. 求助 关于word安全模式
  16. 化工原理实验,化工原理教学实训设备QY-HGYL
  17. Python培训课程怎么学
  18. 大学计算机应用b,西安理工大学《大学计算机应用》试题 B卷
  19. Crowding Counter 之 可视化h5文件
  20. 《互联网时代》第四集 再构

热门文章

  1. 猫影视TV 2.0.8 附稳定源地址
  2. 判断系统是centos还是ubuntu的linux命令
  3. 知乎cookies的介绍_使用cookie登陆知乎
  4. 十七、缓存预热、缓存雪崩、缓存击穿、缓存穿透、性能指标监控等企业级解决方案
  5. 链路聚合—3种模式 详细
  6. ROS2机器人笔记20-10-24
  7. 如何干净地卸载VMware
  8. EFL+WEBKIT
  9. GBC代理“花生漫画PEANUTS”,遭遇侵权账户冻结应该怎么处理?
  10. 寒假第三周学习总结与反思