在category中,我们可以添加我们需要的类方法和实例方法,并且可以在其中使用需要扩展的类中的实例变量,但是我们在category中添加property是不提倡,这似乎已经成为iOS开发中的常识,但是今天我想带着这个问题来进行一下研究,为什么苹果不提倡在category中添加property?

前言

在category中,我们可以添加我们需要的类方法和实例方法,并且可以在其中使用需要扩展的类中的实例变量,但是我们在category中添加property是不提倡,这似乎已经成为iOS开发中的常识,但是今天我想带着这个问题来进行一下研究,为什么苹果不提倡在category中添加property?

首先我们可以在iOS的文档中,找到一句这样的话来说明原因:

Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class. ```

翻译过来大概的意思是:

category可以被用来申明实例方法或者类方法,但是我们在其中添加额外的property一般是不合适的,虽然你我们在接口中申明一个property是符合语法的,但是这里不可能声明一个额外的实例变量。这就意味着编译器不会合成任何实例变量,也不会合成任何property的访问方法,你可以在代码中实现自己的访问方法,但是你还是不能跟踪到这个property的值除非在原始的类中就有已经保存了。

从文档中我们可以大概知道原因,因为在category中申明的propery,编译器不会生成property的访问方法和实例变量,似乎问题到这里已经找到了答案,但是我们不经要问,为什么不能生成变量呢?如何我们想要在category中实现一个property,我们应该怎么做呢?带着这些问题我们继续往下看。

问责编译器

既然一切都是编译器的错,那就让我们看看,编译器到底对category中的property做了什么事情?

举个栗子看,定义下面一个类和它的category,实现忽略,保存为Shylock.h和Shylock.m

@interface Shylock : NSObject

@property (nonatomic, copy) NSString *startTime;

@end

@interface Shylock (Watson)

@property (nonatomic, copy) NSString *endTime;

@end

使用clang的重写命令:

clang -rewrite-objc sark.m

同级目录下会生成Shylock.cpp,这就是objc代码重写成c++(基本就是c)的实现。

在这么多代码中,我们只能通过搜索关键字来查找了,首先我们来搜索,startTime,有涉及到的代码有这么多

extern "C" unsigned long OBJC_IVAR_$_Shylock$_startTime;
struct Shylock_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_startTime;
};

static NSString * _I_Shylock_startTime(Shylock * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Shylock$_startTime)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Shylock_setStartTime_(Shylock * self, SEL _cmd, NSString *startTime) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Shylock, _startTime), (id)startTime, 0, 1); }

extern "C" unsigned long int OBJC_IVAR_$_Shylock$_startTime __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Shylock, _startTime);

static struct /*_ivar_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count;
    struct _ivar_t ivar_list[1];
} _OBJC_$_INSTANCE_VARIABLES_Shylock __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_ivar_t),
    1,
    { {(unsigned long int *)&OBJC_IVAR_$_Shylock$_startTime, "_startTime", "@\"NSString\"", 3, 8} }
};


static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_Shylock __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    { {(struct objc_selector *)"startTime", "@16@0:8", (void *)_I_Shylock_startTime},
    {(struct objc_selector *)"setStartTime:", "v24@0:8@16", (void *)_I_Shylock_setStartTime_} }
};

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Shylock __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    { {"startTime","T@\"NSString\",C,N,V_startTime"} }
};

static struct _class_ro_t _OBJC_CLASS_RO_$_Shylock __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, __OFFSETOFIVAR__(struct Shylock, _startTime), sizeof(struct Shylock_IMPL), 
    (unsigned int)0, 
    0, 
    "Shylock",
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Shylock,
    0, 
    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Shylock,
    0, 
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Shylock,
};

让我们看看编译器对startTime(类中的property)做了什么

_ivar_list_t中添加了_startTime变量_method_list_t中添加了startTimesetStartTime两个方法 在_prop_list_t中添加了startTime这个property

而关于endTime(category中的property)呢?

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Shylock_$_Watson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    { {"endTime","T@\"NSString\",C,N"} }
};

只是在category中的_prop_list_t中增加了endTime这个property,寥寥几笔带过,真是为category中的property感到心寒呀,果然和文档中说的一样,编译器并没有生成任何访问方法和实例变量。

但是我们还是不死心,让我们自己实现一下访问方法后,编译器会不会有所心动呢。

下面我们来添加Getter和Setter方法,再通过clang的重写命令,发现只是在category中的方法列表中添加这两个方法

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Shylock_$_Watson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    { {(struct objc_selector *)"setEndTime:", "v24@0:8@16", (void *)_I_Shylock_Watson_setEndTime_},
    {(struct objc_selector *)"endTime", "@16@0:8", (void *)_I_Shylock_Watson_endTime} }
};

到了这一步,我们可以发现编译器对category中的property是非常偏心的。

其实我们可以看看runtime下的category_t结构体

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

_class_ro_t的结构体

struct _class_ro_t {
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    const unsigned char *ivarLayout;
    const char *name;
    const struct _method_list_t *baseMethods;
    const struct _objc_protocol_list *baseProtocols;
    const struct _ivar_list_t *ivars;
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties;
};

我们发现在categoyr_t本身就少了ivars这样的数组变量,这也不能怪编译器,后面我们再讨论一下为什么category中不添加变量列表呢?

改造Property

既然编译器不帮他,那让我们看看还有谁可以帮助的呢。

既然我们能帮他实现访问方法,那我们能不能给他添加一个变量呢,于是很多人就会想到用runtime中的Associated Objects也就是关联对象,这里看一下nshipster对它的介绍,这里我们从代码出发,在runtime的源码中看看相关代码,我们先看看objc_getAssociatedObject这个方法里做了什么, 我们可以看到最后调用的是 void _object_get_associative_reference(id object, void *key, id value, uintptr_t policy) 这个方法

id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;

大概思路是通过维护Map,通过对象来生成一个唯一的 unsigned long 的变量来作为横坐标,查找到之后,再通过key做纵坐标去查找,这样就能找到对应的变量,也就是说只要在某个对象中key是唯一的,就能设置和获取对应的变量,这样就与class无关。

同样的思路, 1. 我们也可以在需要category的类中,添加一个字典或者其他映射表,来作为映射,来提供后面的扩展。 2. 也可以通过一个常住于内存的映射表对象来实现类似与objectsetassociativereference这个的功能,

延伸的问题:为什么不能在category中添加变量呢?

参考这个答案中的一个评论,我认为的原因是这样的,category是在runtime时被添加到class中的,这里可以阅读sunnyxx的objc category的秘密,也就是说这个时候class已经被注册成功,storage layout也已经确定,这时候category中再添加实例变量,对原来的storage layout并没有用。就像是在class_addIvar只能添加在objc_allocateClassPair之后,和objc_registerClassPair之前,而在objc_registerClassPair添加的变量,是不能保留的。

Jazz Hands是IFTTT发布的一个基于关键帧的动画框架,可以用于手势,滚动视图,KVO或者ReactiveCocoa,还是非常好用的。

Jazz Hands是IFTTT发布的一个基于关键帧的动画框架,可以用于手势,滚动视图,KVO或者ReactiveCocoa,还是非常好用的。

一.原理分析

IFTTTAnimator是动画的执行者,但其实其中的代码量非常少,而它的重要部门就是被实时调用,而这里就体现在scrollView的delegate中的- (void)scrollViewDidScroll:(UIScrollView *)aScrollView方法,因为这个方法,只要scrollView滑动就会调用,所以每次被调用,就会去执行animatore中的动画,所以animator只是负责管理动画对象,和在对应位置执行动画就可以了,其他的都不用去管。 IFTTTAnimationFrame是动画中参数的集合,比如像位置,颜色等用于动画的参数 IFTTTAnimationKeyFrame是用来将这些参数和时间对应起来,也就是某一帧的动画效果效果 IFTTTAnimation才是对应具体某个动画的对象,他将动画对象,时间和动画参数结合起来,有了后面比较酷炫的动画效果。

接下来着重来介绍一下IFTTTAnimation中的方法 - (void)addKeyFrame:(IFTTTAnimationKeyFrame *)keyFrame将keyFrame添加到KeyFrames的数组中,为了保证时间顺序,要根据时间顺序来排列,并将每个时间段的参数都按顺序排列。 - (IFTTTAnimationFrame *)animationFrameForTime:(NSInteger)time是获取某一时间的keyFramte - (void)animate:(NSInteger)time在某一个时刻执行相应时刻的动画,这里不同的动画效果是不一样,所以要在继承的子类被实现。

- (CGFloat)tweenValueForStartTime:(NSInteger)startTime endTime:(NSInteger)endTime startValue:(CGFloat)startValue endValue:(CGFloat)endValue atTime:(CGFloat)time

这个是比较重要的衔接作用,这个方法是计算出当前时间的动画参数的值,

- (IFTTTAnimationFrame *)frameForTime:(NSInteger)time startKeyFrame:(IFTTTAnimationKeyFrame *)startKeyFrame endKeyFrame:(IFTTTAnimationKeyFrame *)endKeyFrame

这个获取在某一个时间区间中的某个时间的动画参数

二.分析执行过程

下面分析一段Demo中的代码:

// apply a 3D zoom animation to the first label
IFTTTTransform3DAnimation * labelTransform = [IFTTTTransform3DAnimation animationWithView:self.firstLabel];
IFTTTTransform3D *tt1 = [IFTTTTransform3D transformWithM34:0.03f];
IFTTTTransform3D *tt2 = [IFTTTTransform3D transformWithM34:0.3f];
tt2.rotate = (IFTTTTransform3DRotate){ -(CGFloat)(M_PI), 1, 0, 0 };
tt2.translate = (IFTTTTransform3DTranslate){ 0, 0, 50 };
tt2.scale = (IFTTTTransform3DScale){ 1.f, 2.f, 1.f };
[labelTransform addKeyFrame:[IFTTTAnimationKeyFrame keyFrameWithTime:timeForPage(0) andAlpha:1.0f]];
[labelTransform addKeyFrame:[IFTTTAnimationKeyFrame keyFrameWithTime:timeForPage(1) andTransform3D:tt1]];
[labelTransform addKeyFrame:[IFTTTAnimationKeyFrame keyFrameWithTime:timeForPage(1.5) andTransform3D:tt2]];
[labelTransform addKeyFrame:[IFTTTAnimationKeyFrame keyFrameWithTime:timeForPage(1.5) + 1 andAlpha:0.0f]];
[self.animator addAnimation:labelTransform];

首先动画的执行者是animator,是属于IFTTTAnimator类。 IFTTTTransform3DAnimation是一个某一动画效果对象 IFTTTTransform3D是其中的一些参数值

首先是配置动画,上面的labelTransform、tt1、tt2都是动画参数。labelTransform也是动画对象,将上面的动画参数转化成帧参数。并添加到动画对象中。使用addKeyFrame:的过程中,首先先将这些keyFrame进行排序,再用 ``` - (CGFloat)tweenValueForStartTime:(NSInteger)startTime endTime:(NSInteger)endTime startValue:(CGFloat)startValue endValue:(CGFloat)endValue atTime:(CGFloat time

计算出每个位置时间对应的动画参数,添加到对应数组中。
因为`IFTTTJazzHandsViewController`是继承`IFTTTAnimatedScrollViewController`类的,而其中最重要的就是,用了scrollView中的`scrollViewDidScroll`代理方法中,操作`animator`,将他里面的动画对象都按时间来展现出来,也就是用了`- (void)animate:(NSInteger)time`方法。

##三.总结

这个开源库还是非常精简,而且思路非常清晰,依然基于Core Animation之上,因为它只是针对于UIKit上去做帧的配置,对帧的封装上更加灵活,但是缺点是实现复杂的动画时,代码量比较大。

生活最后还是要靠自己去判断。互联网是好的,也是坏的,在于我们如何去使用他。即使是坏的,我们也做不了任何改变,因为我们已经身处于互联网之中。

一次在技术大会上听了一场玉伯的讲座,那是我第一次见到玉伯,虽然听我的好基友无数次提起他,但他之前也并没有见过真人,在那里遇到这样一个外面瘦弱的人,其实对他的了解仅限于基友的介绍,sea.js 支付宝前端总架构师等等吧,就知道他是牛人。在那场演讲中,讲了一些开源框架的发展等问题,与我的体会不深,但是他最后抛出的一句话却是让我一直回味思考,“我们真的一定要去改变世界么?”,这是我很久以来一直思考的问题,也是这一年在互联网行业中一直在问自己的问题,我不知道在那个会场有多少人会和我有同样的感触,那一刻其实我内心对他是非常敬佩的。

在互联网这个时代,所有人都在寻求改变世界的方法,都在迭代,都在创新,无论是大的还是小的,一个行业内竞争,每个公司都有每个公司的口号,而每个口号都与改变有关,但是对我们生活的改变真的有那么大么?这些真的是用户的需求么?还是我们给用户强加的我们自己的需求呢?我们的那些创意真的是为了改变世界么?还是只是为了自己内心的私欲呢?就像iPhone一样,被认为现在最成功的手机,是它揭开了移动互联网时代,但是这个时代的人们都开始低头看手机,而不去关注身边的人情温暖。就像很多网站为了吸引更多的用户,而不断降低下限。每天都有那么多重复的信息和多余的信息。这样的世界真的好么?

这个世界的浮躁是什么呢?是不是因为我们还没有了解好这个世界,却又急着去改变世界,这样的想法是不是也是一种危险。所以是人的问题,我一直记得高中时语文老师的一句话,“人是很复杂的”,互联网让人的欲望得到了更多的发挥,人的复杂心理,在互联网的带动下产生了很多需求,而作为互联网行业中的我们又在为每个复杂需求服务。或许每个时代的进步都是如此,只是在这个时代更加快速,更加直接。

这些都是我一直在思考的问题,就像之前一直反感成功这个词,可是后来发现并不是这个词错了,而是用它的人错了,其实在字典中它的意思很简单,就是把事情做好,可是现在他有太多的意思了,被赋予太多的光环,权利、金钱等,当然这是每个人羡慕的,但是并不应该成为每个人的追求,并不应该成为我们每天都要讨论的话题。如果这个世界只有这一个梦想,是不是我们的生活就太无聊了呢。

生活最后还是要靠自己去判断。互联网是好的,也是坏的,在于我们如何去使用他。即使是坏的,我们也做不了任何改变,因为我们已经身处于互联网之中。

我知道我不能改变这个世界,所以我只是把这些想法写下来,让未来的我来做判断,现在的我只是希望我的生活更加丰富多彩。

Dropbox 的 iOS 应用中的每一行代码,都是开始于被添加到 Maniphest 中的一个 bug 或者功能任务,Maniphest 是我们的任务管理系统。当一位工程师在上面接受一个任务,那么在开始写代码前相应的责任就已经赋予他。

原文 The Art of Code Review: A Dropbox Story

Dropbox 的 iOS 应用中的每一行代码,都是开始于被添加到 Maniphest 中的一个 bug 或者功能任务,Maniphest 是我们的任务管理系统。当一位工程师在上面接受一个任务,那么在开始写代码前相应的责任就已经赋予他。Phabricator 这个平台包含了我们的代码审查工具,这个代码审查工具有很多很好的功能,但它在评估对象之间的相互协作上不是做的很好。为了弥补这点,我们的工程师在开始他们的工作之前需要知道审查他们的任务的人是谁[1]。对于被审查代码的工程师来说,这样能确保在他们的团队中有一个橡皮鸭,这个橡皮鸭知道项目中一些改动代码的背景和原因,并且对代码的设计决策上起到协助的作用。对于审查者来说,这有助于他们将一些变化考虑进他们的开发周期评估中,这样有助于开发周期评估的准确。如果不出意外的话,我们的经验会告诉我们提前做好计划可以有效地避免审查代码过程中的重复劳动。针对项目中的变化做计划可以像在白板前做交流一样简单,也可以像写一篇建设性文档一样深入。这都取决于我们自己的选择。

[1] 我的团队中每个人都要审查代码。新来的同事在可以独立审查较大的任务之前,会先被分配一些比较少的代码量。

随着任务的展开,工程师需要一直谨记我们的代码规范。这个规范是一个最佳实践和一致规范的大融合,它的存在使我们不用去猜测我们应该怎样编码,也使审查变得更容易[2]。因为这是一个大项目,开发团队中没有一个人能对整个项目有完美的映射或理解。所以我们的工程师需要依赖团队中其他工程师的帮助,将这些代码的功能表现拼成一个整体,这有助与我们在阅读代码时能理解其中的逻辑。

[2] 即使这样,每当一个新成员加入时,总还是不免要展开一次关于使用 property 还是 ivar 的辩论。

当这个任务的工作进行到某个阶段时,我们的工程师很可能会做出一些明显不合理的或者不受欢迎的决定。捕获这个心理的最佳时间就是发生这一刻 -- 为将来向审查者做好解释的准备。去解释这些变化,说起来容易做起来难,我们的工程师被鼓励使用 //TODO//HAX,和 //FIXME 来在代码中写注释。//TODO//FIXME 从字面上就可以理解它的意义,尽管后者会产生编译警告,所以必须在下一次发布之前要被解决。//HAX 这个注释比较有趣的地方。我们用它标注那些用来绕过 Apple 的 API 里的 bug 但又不容易一眼看明白的方法[3]。我们的注释会写上日期和写这个注释人的名字[4],在之后很多时候我们总会感激这些额外的上下文的[5]。

[3] 标注里通常是第三方来源或者 radar 的链接,还有特殊的重现步骤。

[4] 比如像 //HAX:(ashleynh) 2015-03-09

[5] Hello 👋 iOS 7

随着开发的继续,我们的工程师可能会陷入那些看似对现有功能的快速优化中去,但这是一个陷阱,这个额外的优化不可避免地可能会存在很多潜在后果,就像是一个兔子洞。这是一个很典型的 DoingTooMuch™ 。我们唯一的解决方式就是,针对这个优化创建一个新的任务,然后回过头来专注到当前这个被分配的任务上。

如果我们的工程师已经做到这一步,那真是太棒了!这个任务的要求就已经达到了。然而,写代码只是任务的一部分而已。接下来要开始针对变化进行工作。

我们的工程师会用命令行工具 Arcanist 来将变化的代码上传到 Differential 这个代码审查工具中。在这个过程中,我们会运行脚本和单元测试。其中脚本用来将我们的代码格式化,这样有助于让我们专注于阅读功能性的改变,而且不用对代码格式再吹毛求疵。我们尤其还会用 clang-format 这个插件来对间距和行距进行格式化,然后用 homegrown script 这个脚本将我们的 import 按字母排序。这些脚本将这些琐碎的事情像魔法般简单地实现,但是我们的工程师在提交这些代码变化之前,也还可以再非常认真地检查几遍。

代码被自动格式化之后,现有那些有改动的单元测试将会被执行。发现任何有失败的用例时,我们的工程师都会在上传代码之前先去解决它们。

虽然这些改动的代码被上传了,但是在这些代码被审查之前,我们的工程师还是有些表格需要去填写。首先,写这些代码的目的是什么?这些目的是怎样被达到的?接下来,我们的工程师需要附上一个测试计划。我们的工程师确实创建了一个测试计划了么?这个计划能考虑到所有可能引起代码出错的边界情况了么?这个测试计划设计的足够模块化么,是否有可能进行单元测试?如果这些问题的答案有任何一个是 “NO” 的话,那么我们的工程师就不得不关掉 Differential,然后重新打开 Xcode 去修改了。

现在,有了这份经过我们工程师深思熟虑之后写好的测试计划,那些改动的代码可以准备被提交去测试。

这个时候,我们的目光就要移到负责审查的工程师的身上,他们将努力用友善的方式给出一份建设性的反馈。使用类似 meme 的文本配合图片会非常有帮助。请记住负责这个代码的工程师投入了很多精力在这些代码上,他们在很努力地解决这些问题,所以在回馈中,要注意语气,要和善。

其实在一个任务开始的时候,负责审查的工程师就已经参与进来了,所以他们希望,当自己问负责这个任务的工程师一些大的框架性问题的时,比如说 “这些代码已经尽可能的模块化了么?” 或者 “代码中有避免那些没必要的重复么?”,他们希望他们听到的回答是 “当然!”,如果是这样的话,负责审查的工程师就会开始往更深的层面去挖掘,来完成一个全面的评估,这个评估中可能包括一些补充和修改。一般情况下,除非代码是很明显的,否则如果你提交的代码没有注释或没有对应修改请求的话,这些代码是不会被接受的。所以这个时候我们的目光又回到了提交代码的工程师身上。

当被审查的工程师在收到的反馈中读到对于修改进行的注释的时候,我们的工程师需要谨记的是,这些反馈并不是针对他个人的。代码在任何规模的项目中都不可能绝对正确,在较大的项目中更是如此。审查代码可以促进工程师之间的交流,这样提供了一个很好的成长机会。一个完整的代码审查流程需要有效的工程系统的努力,而这正是有效沟通的文化的象征。

经过几次代码审查的迭代后,根据代码改动的量,工程师们的代码就可以准备合并[6]。Dropbox 的 iOS 应用中的每一行代码都是开始于 Maniphest 的一个任务里,而完结于我们的工程师的自豪感中。现在,让我们再选择另外一个任务吧。

[6] 这对于审查代码和被审查代码的工程师来说,这些都是非常好的机会,在他们提交或者审查以后的代码时,他们可以将这些问题添加到一个名为“需要考虑的常见问题”的列表中


RunLoop,从字面上拆开就很容易理解,Run,Loop,就是一直循环地在运行,它就像App中跳动的心脏一样,一直伴随着App的生命周期。

图片

从事iOS应用开发也有快两年了,但是对iOS的了解还是停留在很表面的地方,遇到问题就去找答案,但是不求甚解,这虽然安稳的度过平时的工作,但是一旦遇到更加复杂的问题,就不能冷静应对了,所以新的一年,打算深入底层,来修炼自己的内功。

那就先从RunLoop开始吧,RunLoop,从字面上拆开就很容易理解,Run,Loop,就是一直循环地在运行,它就像App中跳动的心脏一样,一直伴随着App的生命周期。

到底什么是RunLoop ?

简单地说,RunLoop就是一个消息传送机制,用于异步的或线程内部的通信,它提供了一套机制来处理系统的输入源(像socekts,ports,files,keyboard,mouse,定时器等),你可以把它想象成是一个邮局,在等待信件和传递信件给收件人,每个NSThread都有属于它自己的RunLoop。

一个RunLoop做两件事情:

  • 处于等待的状态直到某件事情发生(比如想接受一个信息)
  • 分配信息给需要接收的对象

RunLoop可以用来区分交互式App和命令行。命令行通过一些参数运行后,执行完他们的程序,这个程序结束了,就像“Hello world”, 只要打印出“Hello world”,这个程序也就结束了。但是交互式的App就会一直等待用户输入,然后做出反应,然后再等待用户输入,知道某一个触发条件发生,才会退出程序。

所以从上面的讲述就可以知道,RunLoop的工作实际上就是在等待触发事件的发生。这些触发事件可以是外部的事件,比如用户的一些行为或者像网络请求,又或者像App内部的信息,比如线程内部的通知,异步代码的执行,定时器等等,一旦一个触发事件发生,并且RunLoop接受到这个信息,它就会去寻找相关的收信人,并把信息发送给这个收信人。

RunLoop的时间线

图片

  1. 通知观察者RunLoop已经被启动
  2. 通知观察者一些定时器已经准备开始
  3. 通知观察者一些不是基于端口的输入源准备开始
  4. 启动那些已经准备好的不是基于端口的输入源
  5. 如果一个基于端口的输入源已经准备好,正等待被启动,那么就会马上启动这个输入源。进入第9步。
  6. 通知观察者这个线程准备休眠。
  7. 把这个线程变成休眠状态直到下面一个事件发生:
    • 一个事件到达了一个基于端口的源
    • 一个定时器启动
    • RunLoop设置的时间已经到时
    • 该RunLoop被唤醒
  8. 通知观察者该线程被唤醒
  9. 处理等待事件
    • 如果一个用户定义的定时器启动,处理这个定时器并且进入下一个RunLoop,进入第2步。
    • 如果一个输入源启动,传递这个事件
    • 如果这个RunLoop被唤醒,但是还超过设置的超时时间,那么就进入下一个RunLoop,进入第2步。
  10. 通知观察者RunLoop退出。

参考文章

understanding-nsrunloop

RunLoop

RunLoop vs Thread

Run RunLoop Run