[iOS]runloop 轻量级实现LightWeightRun
Basic Information
- Name : LightWeightRunLoop
- Site : https://github.com/wuyunfeng/LightWeightRunLoop
- Description :
Using BSD kqueue realize iOS RunLoop and some Runloop-Relative Fundation API such as perform selector(or delay some times) on other thread , Timer, URLConnection etc..
Global Note
1.CFRunLoopRef 的代码是开源的,你可以在这里
http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz
下载到整个 CoreFoundation 的源码.
2.github 上搜runloop,选择语言Objective-C,搜到43个结果
按照star排序,第一个就是这个,一个简单版本的runloop
3.读了源码才明白这个其实相当于android的lopper一种实现方式,关于android的runloop和iOS的runloop的对比,可以参照这里
从安卓的Looper到iOS的RunLoop
http://www.jianshu.com/p/7a970fc5343b
File Notes
屏幕快照 2016-04-21 上午1.03.04.png1. LWSystemClock.m
- Path : /LightWeightLibrary/LightWeightBasement/LWSystemClock.m
- Line : 11 - 17
- Note :
@implementation LWSystemClock
+ (NSInteger)uptimeMillions
{
NSInteger now = (NSInteger)([NSProcessInfo processInfo].systemUptime * 1000);
return now;
}
polen:
NSProcessInfo用于获取当前正在执行的进程信息,包括设备的名称,操作系统版本,进程标识符,进程环境,参数等信息。
e.g.
NSString *processName = [[NSProcessInfo processInfo] processName];
这里的LWSystemClock 是为了拿到系统从启动开始运行的时间,和Unix时间戳是有区别的,可以说是一个相对时间
2. LWRunLoop.m
- Path : /LightWeightLibrary/LightWeightBasement/LWRunLoop.m
- Line : 44 - 54
- Note :
+ (instancetype)currentLWRunLoop {
int result = pthread_once(&mTLSKeyOnceToken, initTLSKey);
NSAssert(result == 0, @"pthread_once failure");
LWRunLoop* instance = (__bridge LWRunLoop*)pthread_getspecific(mTLSKey);
if (instance == nil) {
instance = [[[self class] alloc] init];
[[NSThread currentThread] setLooper:instance];
pthread_setspecific(mTLSKey, (__bridge const void*)(instance));
}
return instance;
}
polen:
通过currentLWRunLoop获得当前线程的LWRunLoop
pthread_getspecific方法是C语言的方法,对应于pthread_setspecific用于线程存储/读取局部变量.
这个你可以理解为类似一个Dictionary,本例中的mTLSKey就是key,对应存储一个value,然后需要的时候取出来这个value.
这个是属于这个线程自己的局部变量,其他线程不可以访问,这种机制称之为
线程特有数据(TSD: Thread-Specific Data
或者 线程局部存储(TLS: Thread-Local Storage).
okey,
show me the code:
//在Linux中提供了如下函数来对线程局部数据进行操作
#include <pthread.h>
*// Returns 0 on success, or a positive error number on error*
int pthread_key_create (pthread_key_t **key*, void (**destructor*)(void *));
*// Returns 0 on success, or a positive error number on error*
int pthread_key_delete (pthread_key_t *key*);
*// Returns 0 on success, or a positive error number on error*
int pthread_setspecific (pthread_key_t *key*, const void **value*);
*// Returns pointer, or NULL if no thread-specific data is associated with key*
void *pthread_getspecific (pthread_key_t *key*);
3. LWRunLoop.m
- Path : /LightWeightLibrary/LightWeightBasement/LWRunLoop.m
- Line : 56 - 65
- Note :
#pragma mark run this loop forever
#LWRunLoop.m
- (void)run {
while (true) {
LWMessage* msg = [_queue next];
@autoreleasepool {
[msg performSelectorForTarget];//polen:下面有具体代码
[self necessaryInvocationForThisLoop:msg];//polen:下面有具体代码
}
}
}
polen:
此为LWRunLoop类中的run方法,通过该方法让_lwRunLoopThread这个线程进入 Event-Driver-Mode模式.
可以看到
从_queue中获取到next Message,然后,循环执行对应的事件:
next,下一件,下一件,下一件... (循环下去...)
然后,我们再看while循环里的具体执行的操作:
# LWMessage.m
//1. 首先队列中取到下一条消息,消息LWMessage去执行对应target的selector
- (void)performSelectorForTarget
{
if (_mTarget == nil) {
NSLog(@"------%@ is released !", _mTarget);
}
if ([_mTarget respondsToSelector:_mSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[_mTarget performSelector:_mSelector withObject:_mArgument];
#pragma clang diagnostic pop
} else {
NSLog(@"xxxxx %@", NSStringFromSelector(_mSelector));
}
}
#LWRunLoop.m
//2. 消息执行完之后,runloop就会去检查是否是定时器LWTimer,如果是定时器则采用定时器需要的一些检查和操作
- (void)necessaryInvocationForThisLoop:(LWMessage*)msg {
if ([msg.data isKindOfClass:[LWTimer class]]) { // LWTimer: periodical
// perform selector
LWTimer* timer = msg.data;
if (timer.repeat) {
msg.when = timer.timeInterval; // must
[self postMessage:msg];
}
}
}
4. NSObject+post.h
- Path : /LightWeightFoundation/NSObject+post.h
- Line : 11 - 43
- Note :
@interface NSObject (post)
- (void)postSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg;
- (void)postSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg afterDelay:(NSInteger)delay;
- (void)postSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg afterDelay:(NSInteger)delay modes:(NSArray<NSString *> *)modes;
polen:
这个是个对NSObject的扩展
可以指定线程,指定方法,指定对象,指定模式,并在一定的延时时间之后执行.
这个库对于使用者来说,可能最需要的就是这个3个方法:
根据自己的需求,让所需要的方法在指定的线程中去延时(或者时时)执行
5. LWRunLoop.m
- Path : /LightWeightLibrary/LightWeightBasement/LWRunLoop.m
- Line : 19 - 23
- Note :
NSString* const LWDefaultRunLoop = @"LWDefaultRunLoop";
NSString* const LWRunLoopCommonModes = @"LWRunLoopCommonModes";
NSString* const LWRunLoopModeReserve1 = @"LWRunLoopModeReserve1";
NSString* const LWRunLoopModeReserve2 = @"LWRunLoopModeReserve2";
NSString* const LWTrackingRunLoopMode = @"LWTrackingRunLoopMode";
polen:
LightWeightRunloop 中runloop 的mode,大致上面几种
默认是在LWDefaultRunLoop,
可以对比下iOS中自己的runloop mode:
1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
6. LWRunLoop.m
- Path : /LightWeightLibrary/LightWeightBasement/LWRunLoop.m
- Line : 78 - 81
- Note :
- (void)changeRunLoopMode:(NSString*)targetMode {
_currentRunLoopMode = targetMode;
_queue.queueRunMode = _currentRunLoopMode;
}
polen:
切换当前runloop的mode,任何时间都可以切换
7. LWMessageQueue.h
- Path : /LightWeightLibrary/LightWeightBasement/LWMessageQueue.h
- Line : 12 - 54
- Note :
@interface LWMessageQueue : NSObject
@property (nonatomic) NSString *queueRunMode;
@property (nonatomic, assign) BOOL allowStop;
+ (instancetype)defaultInstance;
- (BOOL)enqueueMessage:(LWMessage *)msg when:(NSInteger)when;
- (LWMessage *)next;
- (LWMessage *)next:(NSString *)mode;
7.1消息队列
polen:
这个LWMessageQueue是消息队列,每个runloop里面有个消息队列_queue和runloop mode,如下,
@implementation LWRunLoop {
LWMessageQueue* _queue;
NSString* _currentRunLoopMode;
}
从第3条( LWRunLoop.m)可以看出来, runloop执行的时候,就是靠自己的消息队列,将一条条消息取出来执行掉。
不想网上翻页的童鞋,我把代码再贴一遍:
#LWRunLoop.m
- (void)run {
while (true) {
//这里面不断循环取下一条,下一条...
LWMessage* msg = [_queue next];
@autoreleasepool {
[msg performSelectorForTarget];
[self necessaryInvocationForThisLoop:msg];
}
}
}
7.2 next 消息
来看一下消息队列获取next 消息的代码:
- (LWMessage*)next {
NSInteger nextWakeTimeoutMillis = 0;
while (YES) {
[_nativeRunLoop nativeRunLoopFor:nextWakeTimeoutMillis];
@synchronized(self) {
NSInteger now = [LWSystemClock uptimeMillions];
LWMessage* msg = _messages;
if (msg != nil) {
if (now < msg.when) {
//polen:获取下次唤醒时间
nextWakeTimeoutMillis = msg.when - now;
} else {
//polen:当前状态ok,可获取到下一条消息
_isCurrentLoopBlock = NO;
_messages = msg.next;
msg.next = nil;
return msg;
}
} else {
nextWakeTimeoutMillis = -1;
}
//polen:完成了
_isCurrentLoopBlock = YES;
}
}
}
polen:
@synchronized(self) 是为了保证线程安全, 代码的逻辑还是比较清晰的,如果message的待执行时间还未到,就获取下次唤醒时间nextWakeTimeoutMillis,如果已经到了,则还是执行,并将该runloop的_messages指向next消息(准备下一轮的执行)
其中关键的是这一端代码:
while (YES) {
[_nativeRunLoop nativeRunLoopFor:nextWakeTimeoutMillis];
...
}
//polen:点进去详情是这样的
- (void)nativeRunLoopFor:(NSInteger)timeoutMillis {
struct kevent events[MAX_EVENT_COUNT];
struct timespec* waitTime = NULL;
if (timeoutMillis == -1) {
waitTime = NULL;
} else {
waitTime = (struct timespec*)malloc(sizeof(struct timespec));
waitTime->tv_sec = timeoutMillis / 1000;
waitTime->tv_nsec = timeoutMillis % 1000 * 1000 * 1000;
}
int ret = kevent(_kq, NULL, 0, events, MAX_EVENT_COUNT, waitTime);
NSAssert(ret != -1, @"Failure in kevent(). errno=%d", errno);
free(waitTime);
waitTime = NULL; // avoid wild pointer
for (int i = 0; i < ret; i++) {
int fd = (int)events[i].ident;
int event = events[i].filter;
if (fd == _mReadPipeFd) { // for pipe read fd
if (event & EVFILT_READ) {
//polen:EVFILT_READ 下面会解释
// must read mReadWakeFd, or result in readwake always wake
[self nativePollRunLoop];
} else {
NSLog(@"other event happend.");
}
}
}
}
polen:
这个其实就是读取事件列表的事情了,里面有几个重要的点:
7.2.1 kevent函数
int ret = kevent(_kq, NULL, 0, events, MAX_EVENT_COUNT, waitTime);
说明下:第一个参数_kq是LWNativeLoop结构中的消息队列
//基本的类型结构
@implementation LWNativeLoop {
int _mReadPipeFd; //polen:pip 管道的读端
int _mWritePipeFd;//polen:pip 管道的写端
int _kq; //polen:对,就是这个
NSMutableArray* _fds;
}
// 初始化函数里面有:
_kq = kqueue();
NSAssert(_kq != -1, @"Failure in kqueue(). errno=%d", errno);
polen:
利用Linux系统中的管道(pipe)进程间通信机制来实现消息的等待和处理。通过kevent函数可以知道剩余消息事件的个数值ret,从而遍历这些消息. runloop的等待就是通过这个函数实现的,如果waitTime不是null,则会等待waitime,如果为null,kevent将会阻塞,一直等待直到有事件发生为止...
|
p.s. 关于kevent的说明:
kevent函数用于和kqueue的交互。第一个参数是kqueue返回的描述符。changelist参数是一个大小为nchanges的 kevent结构体数组。changelist参数用于注册或修改事件,并且将在从kqueue读出事件之前得到处理。
eventlist 参数是一个大小为nevents的kevent结构体数组。kevent通过把事件放在eventlist参数中来向调用进程返回事件。如果需要的 话,eventlist和changelist参数可以指向同一个数组。最后一个参数是kevent所期待的超时时间。如果超时参数被指定为 NULL,kevent将阻塞,直至有事件发生为止。如果超时参数不为NULL,则kevent将阻塞到超时为止。如果超时参数指定的是一个内容为0的结 构体,kevent将立即返回所有当前尚未处理的事件。
kevent的返回值指定了放在eventlist数组中的事件的数目。如果事件 数目超过了eventlist的大小,可以通过后续的kevent调用来获得它们。在处理事件的过程中发生的错误也会在还有空间的前提下被放到 eventlist参数中。带有错误的事件会设置EV_ERROR位,系统错误也会被放到data成员中。对于其它的所有错误都将返回-1,并相应地设置 errno。
针对这个查了下android对应runloop的实现代码:
#ifdef LOOPER_USES_EPOLL
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
bool acquiredLock = false;
#else
......
#endif
一看一目了然,epoll_wait 和 我们的kevent是一样一样的...
http://blog.csdn.net/luoshengyang/article/details/6817933/
老罗语录 (罗升阳):
首先是调用epoll_wait函数来看看epoll专用文件描述符mEpollFd所监控的文件描述符是否有IO事件发生,它设置监控的超时时间为timeoutMillis
当mEpollFd所监控的文件描述符发生了要监控的IO事件后或者监控时间超时后,线程就从epoll_wait返回了,否则线程就会在epoll_wait函数中进入睡眠状态了。返回后如果eventCount等于0,就说明是超时了.
7.2.2 EVFILT_READ
polen:
接着往后看
if (event & EVFILT_READ) {
// must read mReadWakeFd, or result in readwake always wake
[self nativePollRunLoop];
}else{
...
}
kevent结束之后,后面是事件的读操作,里面判断条件有一句EVFILT_READ这个,EVFILT_READ是什么东西呢?
EVFILT_READ过滤器用于检测什么时候数据可读。kevent的ident成员应当被设成一个有效的描述符。尽管这个过滤器的行为和select 或这poll很像,但它返回的事件将是特定于所使用的描述符的类型的。
7.2.3 关于管道pip
#pragma mark - Process two fds generated by pipe()
- (void)nativeWakeRunLoop {
ssize_t nWrite;
do {
nWrite = write(_mWritePipeFd, "w", 1);
} while (nWrite == -1 && errno == EINTR);
if (nWrite != 1) {
if (errno != EAGAIN) {
NSLog(@"Could not write wake signal, errno=%d", errno);
}
}
}
- (void)nativePollRunLoop {
char buffer[16];
ssize_t nRead;
do {
nRead = read(_mReadPipeFd, buffer, sizeof(buffer));
} while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}
polen:
这里面消息循环和处理的代码实现是基于pip的,不过只有亲自看源码,才能明白其中的要以,不想看源码,只想知道原理的,
可以直接参照我们牛逼的老罗同学(罗升阳)语录:
http://blog.csdn.net/luoshengyang/article/details/6817933/
管道是Linux系统中的一种进程间通信机制,具体可以参考前面一篇文章Android学习启动篇推荐的一本书《Linux内核源代码情景分析》中的第6章--传统的Uinx进程间通信。
简单来说,管道就是一个文件,在管道的两端,分别是两个打开文件文件描述符,这两个打开文件描述符都是对应同一个文件,其中一个是用来读的,别一个是用来写的,一般的使用方式就是,一个线程通过读文件描述符中来读管道的内容,当管道没有内容时,这个线程就会进入等待状态,而另外一个线程通过写文件描述符来向管道中写入内容,写入内容的时候,如果另一端正有线程正在等待管道中的内容,那么这个线程就会被唤醒。
这个等待和唤醒的操作是如何进行的呢?
这就要借助Linux系统中的epoll机制了。
Linux系统中的epoll机制为处理大批量句柄而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。但是这里我们其实只需要监控的IO接口只有mWakeReadPipeFd一个,即前面我们所创建的管道的读端,为什么还需要用到epoll呢?有点用牛刀来杀鸡的味道。其实不然,这个Looper类是非常强大的,它除了监控内部所创建的管道接口之外,还提供了addFd接口供外界面调用,外界可以通过这个接口把自己想要监控的IO事件一并加入到这个Looper对象中去,当所有这些被监控的IO接口上面有事件发生时,就会唤醒相应的线程来处理,不过这里我们只关心刚才所创建的管道的IO事件的发生。
Summarize
看的有点累,不过学了不少东西...
另,
1.想补充下基础runloop知识的同学,可以看看这里
[iOS]runloop - iOS界的EventLoop
http://www.jianshu.com/p/033087def3a4
2.想知道这篇文章格式是怎么出来的,请下载插件 XSourceNote ,这个是我们的everettjf同学写的,用起来也是很舒服😄
by polen