[iOS]runloop 轻量级实现LightWeightRunloop 源码学习

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
//www.greatytc.com/p/7a970fc5343b

File Notes

屏幕快照 2016-04-21 上午1.03.04.png

1. 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
//www.greatytc.com/p/033087def3a4

2.想知道这篇文章格式是怎么出来的,请下载插件 XSourceNote ,这个是我们的everettjf同学写的,用起来也是很舒服😄


by polen

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 前言 最近离职了,可以尽情熬夜写点总结,不用担心第二天上班爽并蛋疼着,这篇的主角 RunLoop 一座大山,涵盖的...
    zerocc2014阅读 12,372评论 13 67
  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 985评论 0 1
  • 先贴下 apple doc, 本文基本是对照 doc 的翻译:https://developer.apple.co...
    brownfeng阅读 6,851评论 8 111
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 我想我们都是一块块普普通通的石头,不,不是普通的,而是以几乎支离破碎的姿态,离开那座万古的磐石高山。看啊!满布的隐...
    William王汉阅读 230评论 0 0