南峰子的技术博客

攀登,一步一个脚印,方能知其乐


  • 首页

  • 知识小集

  • Swift

  • Objective-C

  • Cocoa

  • 翻译

  • 源码分析

  • 杂项

  • 归档

Objective-C Runtime 运行时之三:方法与消息

发表于 2014-11-03   |   分类于 Objective-C

前面我们讨论了Runtime中对类和对象的处理,及对成员变量与属性的处理。这一章,我们就要开始讨论Runtime中最有意思的一部分:消息处理机制。我们将详细讨论消息的发送及消息的转发。不过在讨论消息之前,我们先来了解一下与方法相关的一些内容。

基础数据类型

SEL

SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:

1
typedef struct objc_selector *SEL;

objc_selector结构体的详细定义没有在<objc/runtime.h>头文件中找到。方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下代码所示:

1
2
SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);

上面的输出为:

1
2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72

两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。如在某个类中定义以下两个方法:

1
2
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;

这样的定义被认为是一种编译错误,所以我们不能像C++, C#那样。而是需要像下面这样来声明:

1
2
-(void)setWidthIntValue:(int)width;
-(void)setWidthDoubleValue:(double)width;

当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。

工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。

我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:

  1. sel_registerName函数
  2. Objective-C编译器提供的@selector()
  3. NSSelectorFromString()方法

IMP

IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:

1
id (*IMP)(id, SEL, ...)

这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。

通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。

Method

介绍完SEL和IMP,我们就可以来讲讲Method了。Method用于表示类定义中的方法,则定义如下:

1
2
3
4
5
6
7
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。具体操作流程我们将在下面讨论。

objc_method_description

objc_method_description定义了一个Objective-C方法,其定义如下:

1
struct objc_method_description { SEL name; char *types; };

方法相关操作函数

Runtime提供了一系列的方法来处理与方法相关的操作。包括方法本身及SEL。本节我们介绍一下这些函数。

方法

方法操作相关函数包括下以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
  • method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快。
  • method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))。
  • method_getReturnType函数,类型字符串会被拷贝到dst中。
  • method_setImplementation函数,注意该函数返回值是方法之前的实现。

方法选择器

选择器相关的操作函数包括:

1
2
3
4
5
6
7
8
9
10
11
// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );
// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
  • sel_registerName函数:在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。

方法调用流程

在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示:

1
objc_msgSend(receiver, selector)

如果消息中还有其它参数,则该方法的形式如下所示:

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

这个函数完成了动态绑定的所有事情:

  1. 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
  2. 它调用方法实现,并将接收者对象及方法的所有参数传给它。
  3. 最后,它将实现返回的值作为它自己的返回值。

消息的关键在于我们前面章节讨论过的结构体objc_class,这个结构体有两个字段是我们在分发消息的关注的:

  1. 指向父类的指针
  2. 一个类的方法分发表,即methodLists。

当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。

下图演示了这样一个消息的基本框架:

image

当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个我们在后面讨论。

为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。这点我们在前面讨论过,不再重复。

隐藏参数

objc_msgSend有两个隐藏参数:

  1. 消息接收对象
  2. 方法的selector

这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。

虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。如下代码所示:

1
2
3
4
5
6
7
8
9
10
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

当然,这两个参数我们用得比较多的是self,_cmd在实际中用得比较少。

获取方法地址

Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。

我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。

NSObject类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。

我们通过以下代码来看看methodForSelector:的使用:

1
2
3
4
5
6
7
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++)
setter(targetList[i], @selector(setFilled:), YES);

这里需要注意的就是函数指针的前两个参数必须是id和SEL。

当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。

消息转发

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform...的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

1
2
3
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}

不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:

1
2
3
-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'

这段异常信息实际上是由NSObject的”doesNotRecognizeSelector“方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

下面我们详细讨论一下这三个步骤。

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}

不过这种方案更多的是为了实现@dynamic属性。

备用接收者

如果在上一步无法处理消息,则Runtime会继续调以下方法:

1
- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[SUTRuntimeMethodHelper alloc] init];
}
return self;
}
- (void)test {
[self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 将消息转发给_helper来处理
if ([selectorString isEqualToString:@"method2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

forwardInvocation:方法的实现有两个任务:

  1. 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
  2. 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

还有一个很重要的问题,我们必须重写以下方法:

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

完整的示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}

NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

消息转发与多重继承

回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。

不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector])
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

小结

在此,我们已经了解了Runtime中消息发送和转发的基本机制。这也是Runtime的强大之处,通过它,我们可以为程序增加很多动态的行为,虽然我们在实际开发中很少直接使用这些机制(如直接调用objc_msgSend),但了解它们有助于我们更多地去了解底层的实现。其实在实际的编码过程中,我们也可以灵活地使用这些机制,去实现一些特殊的功能,如hook操作等。

参考

  1. Objective-C Runtime Reference
  2. Objective-C Runtime Programming Guide
  3. Objective-C runtime之消息(二)
  4. 深入浅出Cocoa之消息

Objective-C Runtime 运行时之二:成员变量与属性

发表于 2014-10-30   |   分类于 Objective-C

在前面一篇文章中,我们介绍了Runtime中与类和对象相关的内容,从这章开始,我们将讨论类实现细节相关的内容,主要包括类中成员变量,属性,方法,协议与分类的实现。

本章的主要内容将聚集在Runtime对成员变量与属性的处理。在讨论之前,我们先介绍一个重要的概念:类型编码。

类型编码(Type Encoding)

作为对Runtime的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。

在Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。

注:Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。

一个数组的类型编码位于方括号中;其中包含数组元素的个数及元素类型。如以下示例:

1
2
float a[] = {1.0, 2.0, 3.0};
NSLog(@"array encoding type: %s", @encode(typeof(a)));

输出是:

1
2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]

其它类型可参考Type Encoding,在此不细说。

另外,还有些编码类型,@encode虽然不会直接返回它们,但它们可以作为协议中声明的方法的类型限定符。可以参考Type Encoding。

对于属性而言,还会有一些特殊的类型编码,以表明属性是只读、拷贝、retain等等,详情可以参考Property Type String。

成员变量、属性

Runtime中关于成员变量和属性的相关数据结构并不多,只有三个,并且都很简单。不过还有个非常实用但可能经常被忽视的特性,即关联对象,我们将在这小节中详细讨论。

基础数据类型

Ivar

Ivar是表示实例变量的类型,其实际是一个指向objc_ivar结构体的指针,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 变量名
char *ivar_type OBJC2_UNAVAILABLE; // 变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}

objc_property_t

objc_property_t是表示Objective-C声明的属性的类型,其实际是指向objc_property结构体的指针,其定义如下:

1
typedef struct objc_property *objc_property_t;

objc_property_attribute_t

objc_property_attribute_t定义了属性的特性(attribute),它是一个结构体,定义如下:

1
2
3
4
typedef struct {
const char *name; // 特性名
const char *value; // 特性值
} objc_property_attribute_t;

关联对象(Associated Object)

关联对象是Runtime中一个非常实用的特性,不过可能很容易被忽视。

关联对象类似于成员变量,不过是在运行时添加的。我们通常会把成员变量(Ivar)放在类声明的头文件中,或者放在类实现的@implementation后面。但这有一个缺点,我们不能在分类中添加成员变量。如果我们尝试在分类中添加新的成员变量,编译器会报错。

我们可能希望通过使用(甚至是滥用)全局变量来解决这个问题。但这些都不是Ivar,因为他们不会连接到一个单独的实例。因此,这种方法很少使用。

Objective-C针对这一问题,提供了一个解决方案:即关联对象(Associated Object)。

我们可以把关联对象想象成一个Objective-C对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void指针(const void *)。我们还需要指定一个内存管理策略,以告诉Runtime如何管理这个对象的内存。这个内存管理的策略可以由以下值指定:

1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY

当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的策略是assign,则宿主释放时,关联对象不会被释放;而如果指定的是retain或者是copy,则宿主释放时,关联对象会被释放。我们甚至可以选择是否是自动retain/copy。当我们需要在多个线程中处理访问关联对象的多线程代码时,这就非常有用了。

我们将一个对象连接到其它对象所需要做的就是下面两行代码:

1
2
static char myKey;
objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);

在这种情况下,self对象将获取一个新的关联的对象anObject,且内存管理策略是自动retain关联对象,当self对象释放时,会自动release关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,这种情况下,先前的关联对象会被妥善地处理掉,并且新的对象会使用它的内存。

1
id anObject = objc_getAssociatedObject(self, &myKey);

我们可以使用objc_removeAssociatedObjects函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。

我们下面来用实例演示一下关联对象的使用方法。

假定我们想要动态地将一个Tap手势操作连接到任何UIView中,并且根据需要指定点击后的实际操作。这时候我们就可以将一个手势对象及操作的block对象关联到我们的UIView对象中。这项任务分两部分。首先,如果需要,我们要创建一个手势识别对象并将它及block做为关联对象。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)setTapActionWithBlock:(void (^)(void))block
{
UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey);
if (!gesture)
{
gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(__handleActionForTapGesture:)];
[self addGestureRecognizer:gesture];
objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
}
objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);
}
```
这段代码检测了手势识别的关联对象。如果没有,则创建并建立关联关系。同时,将传入的块对象连接到指定的key上。注意`block`对象的关联内存管理策略。
手势识别对象需要一个`target`和`action`,所以接下来我们定义处理方法:
```objc
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateRecognized)
{
void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);
if (action)
{
action();
}
}
}

我们需要检测手势识别对象的状态,因为我们只需要在点击手势被识别出来时才执行操作。

从上面的例子我们可以看到,关联对象使用起来并不复杂。它让我们可以动态地增强类现有的功能。我们可以在实际编码中灵活地运用这一特性。

成员变量、属性的操作方法

成员变量

成员变量操作包含以下函数:

1
2
3
4
5
6
7
8
// 获取成员变量名
const char * ivar_getName ( Ivar v );
// 获取成员变量类型编码
const char * ivar_getTypeEncoding ( Ivar v );
// 获取成员变量的偏移量
ptrdiff_t ivar_getOffset ( Ivar v );
  • ivar_getOffset函数,对于类型id或其它对象类型的实例变量,可以调用object_getIvar和object_setIvar来直接访问成员变量,而不使用偏移量。

关联对象

关联对象操作函数包括以下:

1
2
3
4
5
6
7
8
// 设置关联对象
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
// 获取关联对象
id objc_getAssociatedObject ( id object, const void *key );
// 移除关联对象
void objc_removeAssociatedObjects ( id object );

关联对象及相关实例已经在前面讨论过了,在此不再重复。

属性

属性操作相关函数包括以下:

1
2
3
4
5
6
7
8
9
10
11
// 获取属性名
const char * property_getName ( objc_property_t property );
// 获取属性特性描述字符串
const char * property_getAttributes ( objc_property_t property );
// 获取属性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 获取属性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
  • property_copyAttributeValue函数,返回的char *在使用完后需要调用free()释放。
  • property_copyAttributeList函数,返回值在使用完后需要调用free()释放。

实例

假定这样一个场景,我们从服务端两个不同的接口获取相同的字典数据,但这两个接口是由两个人写的,相同的信息使用了不同的字段表示。我们在接收到数据时,可将这些数据保存在相同的对象中。对象类如下定义:

1
2
3
4
5
6
@interface MyObject: NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * status;
@end

接口A、B返回的字典数据如下所示:

1
2
@{@"name1": "张三", @"status1": @"start"}
@{@"name2": "张三", @"status2": @"end"}

通常的方法是写两个方法分别做转换,不过如果能灵活地运用Runtime的话,可以只实现一个转换方法,为此,我们需要先定义一个映射字典(全局变量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static NSMutableDictionary *map = nil;
@implementation MyObject
+ (void)load
{
map = [NSMutableDictionary dictionary];
map[@"name1"] = @"name";
map[@"status1"] = @"status";
map[@"name2"] = @"name";
map[@"status2"] = @"status";
}
@end

上面的代码将两个字典中不同的字段映射到MyObject中相同的属性上,这样,转换方法可如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)setDataWithDic:(NSDictionary *)dic
{
[dic enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSString *propertyKey = [self propertyForKey:key];
if (propertyKey)
{
objc_property_t property = class_getProperty([self class], [propertyKey UTF8String]);
// TODO: 针对特殊数据类型做处理
NSString *attributeString = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
...
[self setValue:obj forKey:propertyKey];
}
}];
}

当然,一个属性能否通过上面这种方式来处理的前提是其支持KVC。

小结

本章中我们讨论了Runtime中与成员变量和属性相关的内容。成员变量与属性是类的数据基础,合理地使用Runtime中的相关操作能让我们更加灵活地来处理与类数据相关的工作。

参考

  1. Objective-C Runtime Programming Guide
  2. Associated Objects

Objective-C Runtime 运行时之一:类与对象

发表于 2014-10-25   |   分类于 Objective-C

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。

这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。

Runtime库主要做下面几件事:

  1. 封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
  2. 找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。

Objective-C runtime目前有两个版本:Modern runtime和Legacy runtime。Modern Runtime覆盖了64位的Mac OS X Apps,还有iOS Apps,Legacy Runtime是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。

在这一系列文章中,我们将介绍runtime的基本工作原理,以及如何利用它让我们的程序变得更加灵活。在本文中,我们先来介绍一下类与对象,这是面向对象的基础,我们看看在Runtime中,类是如何实现的。

类与对象基础数据结构

Class

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

1
typedef struct objc_class *Class;

查看objc/runtime.h中objc_class结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;

在这个定义中,下面几个字段是我们感兴趣的

  1. isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
  2. super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
  3. cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
  4. version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

针对cache,我们用下面例子来说明其执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NSArray *array = [[NSArray alloc] init];
```
其流程是:
1. `[NSArray alloc]`先被执行。因为NSArray没有`+alloc`方法,于是去父类NSObject去查找。
2. 检测NSObject是否响应`+alloc`方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把`isa`指针指向NSArray类。同时,`+alloc`也被加进cache列表里面。
3. 接着,执行`-init`方法,如果NSArray响应该方法,则直接将其加入`cache`;如果不响应,则去父类查找。
4. 在后期的操作中,如果再以`[[NSArray alloc] init]`这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
### objc_object与id
`objc_object`是表示一个类的实例的结构体,它的定义如下(`objc/objc.h`):
```objc
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。

当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。

另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

objc_cache

上面提到了objc_class结构体中的cache字段,它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:

1
2
3
4
5
6
7
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

该结构体的字段描述如下:

  1. mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。
  2. occupied:一个整数,指定实际占用的缓存bucket的总数。
  3. buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

元类(Meta Class)

在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。如:

1
NSArray *array = [NSArray array];

这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念

meta-class是一个类对象的类。

当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。

通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下图所示:

image

对于NSObject继承体系来说,其实例方法对体系中的所有实例、类和meta-class都是有效的;而类方法对于体系内的所有类和meta-class都是有效的。

讲了这么多,我们还是来写个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void TestMetaClass(id self, SEL _cmd) {
NSLog(@"This objcet is %p", self);
NSLog(@"Class is %@, super class is %@", [self class], [self superclass]);
Class currentClass = [self class];
for (int i = 0; i < 4; i++) {
NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
currentClass = objc_getClass((__bridge void *)currentClass);
}
NSLog(@"NSObject's class is %p", [NSObject class]);
NSLog(@"NSObject's meta class is %p", objc_getClass((__bridge void *)[NSObject class]));
}
#pragma mark -
@implementation Test
- (void)ex_registerClassPair {
Class newClass = objc_allocateClassPair([NSError class], "TestClass", 0);
class_addMethod(newClass, @selector(testMetaClass), (IMP)TestMetaClass, "v@:");
objc_registerClassPair(newClass);
id instance = [[newClass alloc] initWithDomain:@"some domain" code:0 userInfo:nil];
[instance performSelector:@selector(testMetaClass)];
}
@end

这个例子是在运行时创建了一个NSError的子类TestClass,然后为这个子类添加一个方法testMetaClass,这个方法的实现是TestMetaClass函数。

运行后,打印结果是

1
2
3
4
5
6
7
8
2014-10-20 22:57:07.352 mountain[1303:41490] This objcet is 0x7a6e22b0
2014-10-20 22:57:07.353 mountain[1303:41490] Class is TestStringClass, super class is NSError
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 0 times gives 0x7a6e21b0
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 1 times gives 0x0
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 2 times gives 0x0
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 3 times gives 0x0
2014-10-20 22:57:07.353 mountain[1303:41490] NSObject's class is 0xe10000
2014-10-20 22:57:07.354 mountain[1303:41490] NSObject's meta class is 0x0

我们在for循环中,我们通过objc_getClass来获取对象的isa,并将其打印出来,依此一直回溯到NSObject的meta-class。分析打印结果,可以看到最后指针指向的地址是0x0,即NSObject的meta-class的类地址。

这里需要注意的是:我们在一个类对象调用class方法是无法获取meta-class,它只是返回类而已。

类与对象操作函数

runtime提供了大量的函数来操作类与对象。类的操作方法大部分是以class_为前缀的,而对象的操作方法大部分是以objc_或object_为前缀。下面我们将根据这些方法的用途来分类讨论这些方法的使用。

类相关操作函数

我们可以回过头去看看objc_class的定义,runtime提供的操作类的方法主要就是针对这个结构体中的各个字段的。下面我们分别介绍这一些的函数。并在最后以实例来演示这些函数的具体用法。

类名(name)

类名操作的函数主要有:

1
2
// 获取类的类名
const char * class_getName ( Class cls );
  • 对于class_getName函数,如果传入的cls为Nil,则返回一个字字符串。

父类(super_class)和元类(meta-class)

父类和元类操作的函数主要有:

1
2
3
4
5
// 获取类的父类
Class class_getSuperclass ( Class cls );
// 判断给定的Class是否是一个元类
BOOL class_isMetaClass ( Class cls );
  • class_getSuperclass函数,当cls为Nil或者cls为根类时,返回Nil。不过通常我们可以使用NSObject类的superclass方法来达到同样的目的。

  • class_isMetaClass函数,如果是cls是元类,则返回YES;如果否或者传入的cls为Nil,则返回NO。

实例变量大小(instance_size)

实例变量大小操作的函数有:

1
2
// 获取实例大小
size_t class_getInstanceSize ( Class cls );

成员变量(ivars)及属性

在objc_class中,所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。runtime提供了丰富的函数来操作这一字段。大体上可以分为以下几类:

1.成员变量操作函数,主要包含以下函数:

1
2
3
4
5
6
7
8
9
10
11
// 获取类中指定名称实例成员变量的信息
Ivar class_getInstanceVariable ( Class cls, const char *name );
// 获取类成员变量的信息
Ivar class_getClassVariable ( Class cls, const char *name );
// 添加成员变量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 获取整个成员变量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
  • class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针(Ivar)。

  • class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。

  • Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是1<<alignment。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))。

  • class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。

2.属性操作函数,主要包含以下函数:

1
2
3
4
5
6
7
8
9
10
11
// 获取指定的属性
objc_property_t class_getProperty ( Class cls, const char *name );
// 获取属性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
// 为类添加属性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 替换类的属性
void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );

这一种方法也是针对ivars来操作,不过只操作那些是属性的值。我们在后面介绍属性时会再遇到这些函数。

3.在MAC OS X系统中,我们可以使用垃圾回收器。runtime提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理strong/weak引用。这几个函数定义如下:

1
2
3
4
const uint8_t * class_getIvarLayout ( Class cls );
void class_setIvarLayout ( Class cls, const uint8_t *layout );
const uint8_t * class_getWeakIvarLayout ( Class cls );
void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );

但通常情况下,我们不需要去主动调用这些方法;在调用objc_registerClassPair时,会生成合理的布局。在此不详细介绍这些函数。

方法(methodLists)

方法操作主要有以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
  • class_addMethod的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用method_setImplementation。一个Objective-C方法是一个简单的C函数,它至少包含两个参数–self和_cmd。所以,我们的实现函数(IMP参数指向的函数)至少需要两个参数,如下所示:
  • 1
    2
    3
    4
    void myMethodIMP(id self, SEL _cmd)
    {
    // implementation ....
    }

与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否已存在。

另外,参数types是一个描述传递给方法的参数类型的字符数组,这就涉及到类型编码,我们将在后面介绍。

  • class_getInstanceMethod、class_getClassMethod函数,与class_copyMethodList不同的是,这两个函数都会去搜索父类的实现。

  • class_copyMethodList函数,返回包含所有实例方法的数组,如果需要获取类方法,则可以使用class_copyMethodList(object_getClass(cls), &count)(一个类的实例方法是定义在元类里面)。该列表不包含父类实现的方法。outCount参数返回方法的个数。在获取到列表后,我们需要使用free()方法来释放它。

  • class_replaceMethod函数,该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现。

  • class_getMethodImplementation函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函数指针可能是一个指向runtime内部的函数,而不一定是方法的实际实现。例如,如果类实例无法响应selector,则返回的函数指针将是运行时消息转发机制的一部分。

  • class_respondsToSelector函数,我们通常使用NSObject类的respondsToSelector:或instancesRespondToSelector:方法来达到相同目的。

协议(objc_protocol_list)

协议相关的操作包含以下函数:

1
2
3
4
5
6
7
8
// 添加协议
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回类实现的协议列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
  • class_conformsToProtocol函数可以使用NSObject类的conformsToProtocol:方法来替代。

  • class_copyProtocolList函数返回的是一个数组,在使用后我们需要使用free()手动释放。

版本(version)

版本相关的操作包含以下函数:

1
2
3
4
5
// 获取版本号
int class_getVersion ( Class cls );
// 设置版本号
void class_setVersion ( Class cls, int version );

其它

runtime还提供了两个函数来供CoreFoundation的tool-free bridging使用,即:

1
2
Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );

通常我们不直接使用这两个函数。

实例(Example)

上面列举了大量类操作的函数,下面我们写个实例,来看看这些函数的实例效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//-----------------------------------------------------------
// MyClass.h
@interface MyClass : NSObject <NSCopying, NSCoding>
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, copy) NSString *string;
- (void)method1;
- (void)method2;
+ (void)classMethod1;
@end
//-----------------------------------------------------------
// MyClass.m
#import "MyClass.h"
@interface MyClass () {
NSInteger _instance1;
NSString * _instance2;
}
@property (nonatomic, assign) NSUInteger integer;
- (void)method3WithArg1:(NSInteger)arg1 arg2:(NSString *)arg2;
@end
@implementation MyClass
+ (void)classMethod1 {
}
- (void)method1 {
NSLog(@"call method method1");
}
- (void)method2 {
}
- (void)method3WithArg1:(NSInteger)arg1 arg2:(NSString *)arg2 {
NSLog(@"arg1 : %ld, arg2 : %@", arg1, arg2);
}
@end
//-----------------------------------------------------------
// main.h
#import "MyClass.h"
#import "MySubClass.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *myClass = [[MyClass alloc] init];
unsigned int outCount = 0;
Class cls = myClass.class;
// 类名
NSLog(@"class name: %s", class_getName(cls));
NSLog(@"==========================================================");
// 父类
NSLog(@"super class name: %s", class_getName(class_getSuperclass(cls)));
NSLog(@"==========================================================");
// 是否是元类
NSLog(@"MyClass is %@ a meta-class", (class_isMetaClass(cls) ? @"" : @"not"));
NSLog(@"==========================================================");
Class meta_class = objc_getMetaClass(class_getName(cls));
NSLog(@"%s's meta-class is %s", class_getName(cls), class_getName(meta_class));
NSLog(@"==========================================================");
// 变量实例大小
NSLog(@"instance size: %zu", class_getInstanceSize(cls));
NSLog(@"==========================================================");
// 成员变量
Ivar *ivars = class_copyIvarList(cls, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSLog(@"instance variable's name: %s at index: %d", ivar_getName(ivar), i);
}
free(ivars);
Ivar string = class_getInstanceVariable(cls, "_string");
if (string != NULL) {
NSLog(@"instace variable %s", ivar_getName(string));
}
NSLog(@"==========================================================");
// 属性操作
objc_property_t * properties = class_copyPropertyList(cls, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSLog(@"property's name: %s", property_getName(property));
}
free(properties);
objc_property_t array = class_getProperty(cls, "array");
if (array != NULL) {
NSLog(@"property %s", property_getName(array));
}
NSLog(@"==========================================================");
// 方法操作
Method *methods = class_copyMethodList(cls, &outCount);
for (int i = 0; i < outCount; i++) {
Method method = methods[i];
NSLog(@"method's signature: %s", method_getName(method));
}
free(methods);
Method method1 = class_getInstanceMethod(cls, @selector(method1));
if (method1 != NULL) {
NSLog(@"method %s", method_getName(method1));
}
Method classMethod = class_getClassMethod(cls, @selector(classMethod1));
if (classMethod != NULL) {
NSLog(@"class method : %s", method_getName(classMethod));
}
NSLog(@"MyClass is%@ responsd to selector: method3WithArg1:arg2:", class_respondsToSelector(cls, @selector(method3WithArg1:arg2:)) ? @"" : @" not");
IMP imp = class_getMethodImplementation(cls, @selector(method1));
imp();
NSLog(@"==========================================================");
// 协议
Protocol * __unsafe_unretained * protocols = class_copyProtocolList(cls, &outCount);
Protocol * protocol;
for (int i = 0; i < outCount; i++) {
protocol = protocols[i];
NSLog(@"protocol name: %s", protocol_getName(protocol));
}
NSLog(@"MyClass is%@ responsed to protocol %s", class_conformsToProtocol(cls, protocol) ? @"" : @" not", protocol_getName(protocol));
NSLog(@"==========================================================");
}
return 0;
}

这段程序的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2014-10-22 19:41:37.452 RuntimeTest[3189:156810] class name: MyClass
2014-10-22 19:41:37.453 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] super class name: NSObject
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] MyClass is not a meta-class
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] MyClass's meta-class is MyClass
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance size: 48
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _instance1 at index: 0
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _instance2 at index: 1
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _array at index: 2
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _string at index: 3
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] instance variable's name: _integer at index: 4
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] instace variable _string
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] property's name: array
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] property's name: string
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] property's name: integer
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] property array
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] method's signature: method1
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] method's signature: method2
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] method's signature: method3WithArg1:arg2:
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: integer
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: setInteger:
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: array
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: string
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: setString:
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: setArray:
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] method's signature: .cxx_destruct
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] method method1
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] class method : classMethod1
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] MyClass is responsd to selector: method3WithArg1:arg2:
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] call method method1
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] protocol name: NSCopying
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] protocol name: NSCoding
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] MyClass is responsed to protocol NSCoding
2014-10-22 19:41:37.468 RuntimeTest[3189:156810] ==========================================================

动态创建类和对象

runtime的强大之处在于它能在运行时创建类和对象。

动态创建类

动态创建类涉及到以下几个函数:

1
2
3
4
5
6
7
8
9
// 创建一个新类和元类
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 销毁一个类及其相关联的类
void objc_disposeClassPair ( Class cls );
// 在应用中注册由objc_allocateClassPair创建的类
void objc_registerClassPair ( Class cls );
  • objc_allocateClassPair函数:如果我们要创建一个根类,则superclass指定为Nil。extraBytes通常指定为0,该参数是分配给类和元类对象尾部的索引ivars的字节数。

为了创建一个新类,我们需要调用objc_allocateClassPair。然后使用诸如class_addMethod,class_addIvar等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用objc_registerClassPair函数来注册类,之后这个新类就可以在程序中使用了。

实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。

  • objc_disposeClassPair函数用于销毁一个类,不过需要注意的是,如果程序运行中还存在类或其子类的实例,则不能调用针对类调用该方法。

在前面介绍元类时,我们已经有接触到这几个函数了,在此我们再举个实例来看看这几个函数的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class cls = objc_allocateClassPair(MyClass.class, "MySubClass", 0);
class_addMethod(cls, @selector(submethod1), (IMP)imp_submethod1, "v@:");
class_replaceMethod(cls, @selector(method1), (IMP)imp_submethod1, "v@:");
class_addIvar(cls, "_ivar1", sizeof(NSString *), log(sizeof(NSString *)), "i");
objc_property_attribute_t type = {"T", "@\"NSString\""};
objc_property_attribute_t ownership = { "C", "" };
objc_property_attribute_t backingivar = { "V", "_ivar1"};
objc_property_attribute_t attrs[] = {type, ownership, backingivar};
class_addProperty(cls, "property2", attrs, 3);
objc_registerClassPair(cls);
id instance = [[cls alloc] init];
[instance performSelector:@selector(submethod1)];
[instance performSelector:@selector(method1)];

程序的输出如下:

1
2
2014-10-23 11:35:31.006 RuntimeTest[3800:66152] run sub method 1
2014-10-23 11:35:31.006 RuntimeTest[3800:66152] run sub method 1

动态创建对象

动态创建对象的函数如下:

1
2
3
4
5
6
7
8
// 创建类实例
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置创建类实例
id objc_constructInstance ( Class cls, void *bytes );
// 销毁类实例
void * objc_destructInstance ( id obj );
  • class_createInstance函数:创建实例时,会在默认的内存区域为类分配内存。extraBytes参数表示分配的额外字节数。这些额外的字节可用于存储在类定义中所定义的实例变量之外的实例变量。该函数在ARC环境下无法使用。

调用class_createInstance的效果与+alloc方法类似。不过在使用class_createInstance时,我们需要确切的知道我们要用它来做什么。在下面的例子中,我们用NSString来测试一下该函数的实际效果:

1
2
3
4
5
6
7
id theObject = class_createInstance(NSString.class, sizeof(unsigned));
id str1 = [theObject init];
NSLog(@"%@", [str1 class]);
id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);

输出结果是:

1
2
2014-10-23 12:46:50.781 RuntimeTest[4039:89088] NSString
2014-10-23 12:46:50.781 RuntimeTest[4039:89088] __NSCFConstantString

可以看到,使用class_createInstance函数获取的是NSString实例,而不是类簇中的默认占位符类__NSCFConstantString。

  • objc_constructInstance函数:在指定的位置(bytes)创建类实例。

  • objc_destructInstance函数:销毁一个类的实例,但不会释放并移除任何与其相关的引用。

实例操作函数

实例操作函数主要是针对我们创建的实例对象的一系列操作函数,我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。这组函数可以分为三小类:

1.针对整个对象进行操作的函数,这类函数包含

1
2
3
4
5
// 返回指定对象的一份拷贝
id object_copy ( id obj, size_t size );
// 释放指定对象占用的内存
id object_dispose ( id obj );

有这样一种场景,假设我们有类A和类B,且类B是类A的子类。类B通过添加一些额外的属性来扩展类A。现在我们创建了一个A类的实例对象,并希望在运行时将这个对象转换为B类的实例对象,这样可以添加数据到B类的属性中。这种情况下,我们没有办法直接转换,因为B类的实例会比A类的实例更大,没有足够的空间来放置对象。此时,我们就要以使用以上几个函数来处理这种情况,如下代码所示:

1
2
3
4
NSObject *a = [[NSObject alloc] init];
id newB = object_copy(a, class_getInstanceSize(MyClass.class));
object_setClass(newB, MyClass.class);
object_dispose(a);

2.针对对象实例变量进行操作的函数,这类函数包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 修改类实例的实例变量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 获取对象实例变量的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 返回指向给定对象分配的任何额外字节的指针
void * object_getIndexedIvars ( id obj );
// 返回对象中实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置对象中实例变量的值
void object_setIvar ( id obj, Ivar ivar, id value );

如果实例变量的Ivar已经知道,那么调用object_getIvar会比object_getInstanceVariable函数快,相同情况下,object_setIvar也比object_setInstanceVariable快。

3.针对对象的类进行操作的函数,这类函数包含:

1
2
3
4
5
6
7
8
// 返回给定对象的类名
const char * object_getClassName ( id obj );
// 返回对象的类
Class object_getClass ( id obj );
// 设置对象的类
Class object_setClass ( id obj, Class cls );

获取类定义

Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取已注册的类定义的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定类的元类
Class objc_getMetaClass ( const char *name );
  • objc_getClassList函数:获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。

下面代码演示了该函数的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int numClasses;
Class * classes = NULL;
numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0) {
classes = malloc(sizeof(Class) * numClasses);
numClasses = objc_getClassList(classes, numClasses);
NSLog(@"number of classes: %d", numClasses);
for (int i = 0; i < numClasses; i++) {
Class cls = classes[i];
NSLog(@"class name: %s", class_getName(cls));
}
free(classes);
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] number of classes: 1282
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] class name: DDTokenRegexp
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: _NSMostCommonKoreanCharsKeySet
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: OS_xpc_dictionary
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSFileCoordinator
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSAssertionHandler
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: PFUbiquityTransactionLogMigrator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSNotification
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSKeyValueNilSetEnumerator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: OS_tcp_connection_tls_session
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: _PFRoutines
......还有大量输出
  • 获取类定义的方法有三个:objc_lookUpClass, objc_getClass和objc_getRequiredClass。如果类在运行时未注册,则objc_lookUpClass会返回nil,而objc_getClass会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass函数的操作与objc_getClass相同,只不过如果没有找到类,则会杀死进程。

  • objc_getMetaClass函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。

小结

在这一章中我们介绍了Runtime运行时中与类和对象相关的数据结构,通过这些数据函数,我们可以管窥Objective-C底层面向对象实现的一些信息。另外,通过丰富的操作函数,可以灵活地对这些数据进行操作。

参考

  1. Objective-C Runtime Reference
  2. Objective-C Runtime的数据类型
  3. 详解Objective-C的meta-class
  4. what are class_setIvarLayout and class_getIvarLayout?
  5. What’s the difference between doing alloc and class_createInstance

instancetype

发表于 2014-10-19   |   分类于 翻译

注:原文由Mattt Thompson发表于nshipster:instancetype。文章是2012年写的,所以有些内容现在已不适用。

在Objective-C中,约定(conventions)不仅仅是编码最佳实践的问题,同时对编译器来说,也是一种隐式说明。

例如,alloc和init两个方法都返回id类型,而在Xcode中,编译器会对它们进行类型检查。这是怎么做到的呢?

在Cocoa中,有一个这样的约定,命名为alloc/init的方法总是返回接收者类的实例。这些方法有一个相关的返回类型。

而类的构造方法(类方法),虽然他们都是返回id类型,但没有从类型检查中获得好处,因为他们不遵循命名约定。

我们可以试试以下代码:

1
2
3
[[[NSArray alloc] init] mediaPlaybackAllowsAirPlay]; // 报错: "No visible @interface for `NSArray` declares the selector `mediaPlaybackAllowsAirPlay`"
[[NSArray array] mediaPlaybackAllowsAirPlay]; // (No error) 注:这个方法调用只在老的编译器上成立,新的编译器会报相同的错误。

由于alloc和init遵循返回相关结果类型的约定,所以会对NSArray执行类型检查。然而等价的类构造方法array则不遵循这一约定,只解释为id类型。

id类型在不需要确保类型安全时非常有用,但一旦需要时,就无法处理了。

而另一种方法,即显示声明返回类型(如前面例子中的(NSArray *))稍微改善了一些,但写起来有点麻烦,而且在继承体系中表现得不是很好。

这时编译器就需要去解决这种针对Objective-C类型系统的边界情况了:

instancetype是一个上下文关键字,可用在返回类型中以表示方法返回一个相关的结果类型,如:

1
2
3
@interface Person
+ (instancetype)personWithName:(NSString *)name;
@end

使用instancetype,编译器可以正确地知道personWithName:的返回结果是一个Person实例。

我们现在看Foundation中的类构造器,可以发现大部分已经开始使用了instancetype了。新的API,如UICollectionViewLayoutAttributes,都是使用instancetype了。

注:instancetype与id不同的是,它只能用在方法声明的返回值中。

更进一步的启示

语言特性是特别有趣的,因为它不清楚在软件设计的更高层次方面会带来什么样的影响。

虽然instancetype看上去非常一般,只是对编译器有用,但也可能被用于一些更聪明的目的。

Jonathan Sterling的文章this quite interesting article,详细描述了instancetype如何被用于编码静态类型集合,而不需要使用泛型:

1
2
3
4
5
6
7
NSURL <MapCollection> *sites = (id)[NSURL mapCollection];
[sites put:[NSURL URLWithString:@"http://www.jonmsterling.com/"]
at:@"jon"];
[sites put:[NSURL URLWithString:@"http://www.nshipster.com/"]
at:@"nshipster"];
NSURL *jonsSite = [sites at:@"jon"]; // => http://www.jonmsterling.com/

静态类型集合使得API更有表现力,这样开发者将不再需要去确定集合中的参数可以使用使用类型的对象了。

不管这会不会成为Objective-C公认的约定,诸如instancetype这样一个低层特性可用于改变语言的形态已是非常棒的一件事了。

OSAtomic原子操作

发表于 2014-10-17   |   分类于 杂项

并发编程一个主要问题就是如何同步数据。同步数据的方式有很多种,这里我们介绍一下libkern/OSAtomic.h。这个头文件包含是大量关于原子操作和同步操作的函数,如果要对数据进行同步操作,这里面的函数可以作为我们的首选项。不同平台这些函数的实现是自定义的。另外,它们是线程安全的。

需要注意的是,传递给这些函数的所有地址都必须是“自然对齐”的,例如int32_t *指针必须是32位对齐的(地址的低位2个bit为0),int64_t *指针必须是64位对齐的(低3位为0)。

这些原子函数的一些版本整合了内存屏障(memory barriers),而另一些则没有。在诸如PPC这样的弱有序(weakly-ordered)架构中,Barriers严格限制了内存访问顺序。所有出现在barriers之前的加载和存储操作完成后,才会运行barriers之后的加载和存储操作。

在单处理器系统中,barriers操作通常是一个空操作。在多处理器系统中,barriers在某些平台上可能是相当昂贵的操作,如PPC。

大多数代码都应该使用barrier函数来确保在线程间共享的内存是正确同步的。例如,如果我们想要初始化一个共享的数据结构,然后自动增加某个变量值来标识初始化操作完成,则我们必须使用OSAtomicIncrement32Barrier来确保数据结构的存储操作在变量自动增加前完成。

同样的,该数据结构的消费者也必须使用OSAtomicIncrement32Barrier,以确保在自动递增变量值之后再去加载这些数据。另一方面,如果我们只是简单地递增一个全局计数器,那么使用OSAtomicIncrement32会更安全且可能更快。

如果不能确保我们使用的是哪个版本,则使用barrier变量以保证是安全的。

另外,自旋锁和队列操作总是包含一个barrier。

这个头文件中的函数主要可以分为以下几类

内存屏障(Memory barriers)

内存屏障的概念如上所述,它是一种屏障和指令类,可以让CPU或编译器强制将barrier之前和之后的内存操作分开。CPU采用了一些可能导致乱序执行的性能优化。在单个线程的执行中,内存操作的顺序一般是悄无声息的,但是在并发编程和设备驱动程序中就可能出现一些不可预知的行为,除非我们小心地去控制。排序约束的特性是依赖于硬件的,并由架构的内存顺序模型来定义。一些架构定义了多种barrier来执行不同的顺序约束。

OSMemoryBarrier()函数就是用来设置内存屏障,它即可以用于读操作,也可以用于写操作。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码来自ReactiveCocoa:RACDisposable类
- (id)initWithBlock:(void (^)(void))block {
NSCParameterAssert(block != nil);
self = [super init];
if (self == nil) return nil;
_disposeBlock = (void *)CFBridgingRetain([block copy]);
OSMemoryBarrier();
return self;
}

自旋锁(Spinlocks)

自旋锁是在多处理器系统(SMP)上为保护一段关键代码的执行或者关键数据的一种保护机制,是实现synchronization的一种手段。

libkern/OSAtomic.h中包含了三个关于自旋锁的函数:OSSpinLockLock, OSSpinLockTry, OSSpinLockUnlock。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 代码来自ReactiveCocoa:RACCompoundDisposable类
- (void)dispose {
#if RACCompoundDisposableInlineCount
RACDisposable *inlineCopy[RACCompoundDisposableInlineCount];
#endif
CFArrayRef remainingDisposables = NULL;
OSSpinLockLock(&_spinLock);
{
_disposed = YES;
#if RACCompoundDisposableInlineCount
for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) {
inlineCopy[i] = _inlineDisposables[i];
_inlineDisposables[i] = nil;
}
#endif
remainingDisposables = _disposables;
_disposables = NULL;
}
OSSpinLockUnlock(&_spinLock);
#if RACCompoundDisposableInlineCount
// Dispose outside of the lock in case the compound disposable is used
// recursively.
for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) {
[inlineCopy[i] dispose];
}
#endif
if (remainingDisposables == NULL) return;
CFIndex count = CFArrayGetCount(remainingDisposables);
CFArrayApplyFunction(remainingDisposables, CFRangeMake(0, count), &disposeEach, NULL);
CFRelease(remainingDisposables);
}

原子队列操作

队列操作主要包含两类:

  1. 不加锁的FIFO入队和出队原子操作,包含OSAtomicFifoDequeue和OSAtomicFifoEnqueue两个函数
  2. 不加锁的LIFO入队和出队原子操作,包含OSAtomicDequeue和OSAtomicEnqueue两个函数。这两个函数是线程安全的,对有潜在精确要求的代码来说,这会是强大的构建方式。

比较和交换

这组函数可以细分为三组函数:

  1. OSAtomicCompareAndSwap**[Barrier](type __oldValue, type __newValue, volatile type *__theValue):这组函数用于比较__oldValue是否与__theValue指针指向的内存位置的值匹配,如果匹配,则将__newValue的值存储到__theValue指向的内存位置。可以根据需要使用barrier版本。
  2. OSAtomicTestAndClear/OSAtomicTestAndClearBarrier(uint32_t __n, volatile void * __theAddress):这组函数用于测试__theAddress指向的值中由__n指定的bit位,如果该位未被清除,则清除它。需要注意的是最低bit位应该是1,而不是0。对于一个64-bit的值来说,如果要清除最高位的值,则__n应该是64。
  3. OSAtomicTestAndSet/OSAtomicTestAndSetBarrier(uint32_t __n, volatile void * __theAddress):与OSAtomicTestAndClear相反,这组函数测试值后,如果指定位没有设置,则设置它。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
void * sharedBuffer(void)
{
static void * buffer;
if (buffer == NULL) {
void * newBuffer = calloc(1, 1024);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, newBuffer, &buffer)) {
free(newBuffer);
}
}
return buffer;
}

上述代码的作用是如果没有缓冲区,我们将创建一个newBuffer,然后将其写到buffer中。

布尔操作(AND, OR, XOR)

这组函数可根据以下两个规则来分类:

  1. 是否使用Barrier
  2. 返回值是原始值还是操作完成后的值

以And为例,有4个函数:OSAtomicAnd32, OSAtomicAnd32Barrier, OSAtomicAnd32Orig, OSAtomicAnd32OrigBarrier。每个函数均带有两个参数:__theMask(uint32_t)和__theValue(volatile uint32_t *)。函数将__theMask与__theValue指向的值做AND操作。

类似,还有OR操作和XOR操作。

数学操作

这组函数主要包括:

  1. 加操作:OSAtomicAdd**, OSAtomicAdd**Barrier
  2. 递减操作:OSAtomicDecrement**, OSAtomicDecrement**Barrier
  3. 递增操作:OSAtomicIncrement**, OSAtomicIncrement**Barrier

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 代码摘自ReactiveCocoa:RACDynamicSequence
- (void)dealloc {
static volatile int32_t directDeallocCount = 0;
if (OSAtomicIncrement32(&directDeallocCount) >= DEALLOC_OVERFLOW_GUARD) {
OSAtomicAdd32(-DEALLOC_OVERFLOW_GUARD, &directDeallocCount);
// Put this sequence's tail onto the autorelease pool so we stop
// recursing.
__autoreleasing RACSequence *tail __attribute__((unused)) = _tail;
}
_tail = nil;
}

小结

相较于@synchronized,OSAtomic原子操作更趋于数据的底层,从更深层次来对单例进行保护。同时,它没有阻断其它线程对函数的访问。

参考

  1. OSAtomic.h User-Space Reference
  2. Memory barrier
  3. Objc的底层并发API
  4. OSATOMIC与synchronized加锁的对比

Tutorial performance and time

发表于 2014-09-23   |   分类于 翻译

原文链接:Tutorial performance and time

在讨论性能之前,先讨论一个重要的话题:时间。为了理解代码中的变化如何影响性能,我们需要一个排序的指标。有许多方法用于时间例程,一些比另一些合适。在本教程中我们将讨论Mach Absolute Time。

为什么是Mach?

时间例程依赖于所需要测量的时间域。某些情况下使用诸如clock()或getrusage()函数来做些简单的数学运算就足够了。如果时间例程将用于实际的开发框架之外,可移植性就很重要了。我不使用这些。为什么?

对于我来说,调试代码的典型问题是:

  1. 我只需要在即时测试时使用时间例程
  2. 我不喜欢依赖于多种函数来包含不同的时间域。它们的行为可能不一致
  3. 有时我需要一个高精度定时器

欢迎了解mach_absolute_time

mach_absolute_time是一个CPU/总线依赖函数,返回一个基于系统启动后的时钟”嘀嗒”数。它没有很好的文档定义,但这不应该成为使用它的障碍,因为在MAC OS X上可以确保它的行为,并且,它包含系统时钟包含的所有时间区域。那是否应该在产品代码中使用它呢?可能不应该。但是对于测试,它却恰到好处。

使用mach_absolute_time时需要考虑两个因素:

  1. 如何获取当前的Mach绝对时间
  2. 如何将其转换为有意义的数字

获取mach_absolute_time

这非常简单

1
2
3
#include <stdint.h>
uint64_t start = mach_absolute_time();
uint64_t stop = mach_absolute_time();

这样就可以了。我们通常获取两个值,以得到这两个时间的时间差。

将mach_absolute_time时间差转换为秒数

这稍微有点复杂,因为我们需要获取mach_absolute_time所基于的系统时间基准。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdint.h>
#include<mach/mach_time.h>
//Raw mach_absolute_times going in,difference in seconds out
double subtractTimes( uint64_tendTime, uint64_t startTime )
{
uint64_t difference = endTime - startTime;
static double conversion = 0.0;
if( conversion == 0.0 )
{
mach_timebase_info_data_t info;
kern_return_t err =mach_timebase_info( &info );
//Convert the timebase into seconds
if( err == 0 )
conversion= 1e-9 * (double) info.numer / (double) info.denom;
}
return conversion * (double)difference;
}

这里最重要的是调用mach_timebase_info。我们传递一个结构体以返回时间基准值。最后,一旦我们获取到系统的心跳,我们便能生成一个转换因子。通常,转换是通过分子(info.numer)除以分母(info.denom)。这里我乘了一个1e-9来获取秒数。最后,我们获取两个时间的差值,并乘以转换因子,便得到真实的时间差。

现在我们可能会想,为什么这比用clock好?看起来做了更多的事情。确实是有点,这便是为什么它在一个函数中。我们只需要传递我们的值到函数中并取得答案。

例子

让我们写个例子。下面是完整的代码清单(包括mach函数)。可以使用gcc mach.c –o mach来编译它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#include<mach/mach_time.h>
//Raw mach_absolute_times going in,difference in seconds out
double subtractTimes( uint64_tendTime, uint64_t startTime )
{
uint64_t difference = endTime -startTime;
static double conversion = 0.0;
if( conversion == 0.0 )
{
mach_timebase_info_data_tinfo;
kern_return_terr = mach_timebase_info( &info ); //Convert the timebaseinto seconds
if(err == 0 )
conversion= 1e-9 * (double) info.numer / (double) info.denom;
}
return conversion * (double)difference;
}
int main()
{
inti, j, count;
uint64_t start,stop;
doublecurrent = 0.0;
doubleanswer = 0.0;
doubleelapsed = 0.0;
intdim1 = 256;
intdim2 = 256;
intsize = 4*dim1*dim2;
//Allocatesome memory and warm it up
double *array =(double*)malloc(size*sizeof(double));
for(i=0;i<size;i++)array = (double)i;
count= 5;
for(i=0;i<count;i++)
{
start = mach_absolute_time();
//dosome work
for(j=0;j<size;j++)
{
answer+= sqrt(array[j]);
}
stop = mach_absolute_time();
current= subtractTimes(stop,start);
printf("Timefor iteration: %1.12lf for answer: %1.12lf\n",current, answer);
elapsed+= current;
}
printf("\nTotaltime in seconds = %1.12lf for answer: %1.12lf\n",elapsed/count,answer);
free(array);
return 0;
}

我们在这里做了什么?在这个例子中,我们有一个适当大小的double数组,当中存放了一些数字,然后获取这些数值的和的开方。为了测试,我们迭代了5次这个计算。每次迭代后我们打印花费的时间,并总结了计算所需的运行时间。在我的PowerMac G5(2.5)机器上,我获得如下结果:

1
2
3
4
5
6
7
8
[bigmac:~/misc] macresearch% gcc mach.c -omach
[bigmac:~/misc] macresearch%./mach
Time for iteration: 0.006717496412for answer: 89478229.125529855490
Time for iteration: 0.007274204955for answer: 178956458.251062750816
Time for iteration: 0.006669191332for answer: 268434687.376589745283
Time for iteration: 0.006953711252for answer: 357912916.502135872841
Time for iteration: 0.007582157340for answer: 447391145.627681851387
Average time in seconds =0.007039352258 for answer: 447391145.627681851387

注意,在这里我没有进行优化,因为编译器有方法避开这样的无脑循环。另外,这只是一个例子。如果是真正的代码,我们会进行优化。

好了,这就是这个例子的两个目的。

首先,我使用的数组大小比我的缓存大。我这样做的目的是因为我们需要注意到数据溢出缓存的情况(正如这个例子一样,至少在我的系统中是这样。如果是在MacPro中,不会出现这种情况)。我们将在以后讨论缓存的事宜。当然,这是一个做作的例子,但有一些东西可供思考。其次,你注意到在内存分配之前我写了一句注释,这是什么意思呢?

这在实际情况下是不需要关心的事情,因为内存总是在需要时已准备好使用。但当做一些小测试时来测试函数的性能时,它却可能是会影响到测试结果的实际问题。

当动态分配内存时,第一次访问内存管理时会将其清0(在OS X中不管使用哪种动态分配函数:malloc, calloc…所有内存在用户使用前都会清0)。内存清零是一种安全预防措施(我们不需要递交一些包含安全信息的内容,如解密密钥)

清零过程产生一个副作用(被系统标记为零填充页面故障)。所以为了让我们的计时更精确些,我们在使用内存之前一次性填充数据,以确保我们不会获取到零填充页面故障的处理时间。

让我们来测试一下,注释下面这行代码

1
for(i=0;i<size;i++) array =(double)i;

为:

1
//for(i=0;i<size;i++) array =(double)i;

再次运行测试

1
2
3
4
5
6
7
[bigmac:~/misc] macresearch% ./mach
Time for iteration: 0.009478866798for answer: 0.000000000000
Time for iteration: 0.004756880234for answer: 0.000000000000
Time for iteration: 0.004927868215for answer: 0.000000000000
Time for iteration: 0.005227029674for answer: 0.000000000000
Time for iteration: 0.004891864428for answer: 0.000000000000
Average time in seconds =0.005856501870 for answer: 0.000000000000

注意第一次迭代的时间比后序的时间多了将近一倍。同时还需要注意所有的answer都是0。再次说明内存被清零了。如果我们从堆上获取了内存,我们获取到的是无意义的数值。

最后,但很重要的一点。不要依赖于内存的清零操作。很有可能获取到的内存是从一个静态分配区而来,那么可能会导致如下这样的问题

1
double array[3][3];

在我的系统上的打印结果是:

1
2
3
-1.99844 -1.29321e-231 -1.99844
-3.30953e-232 -5.31401e+303 0
1.79209e-313 3.3146e-314 0

所以需要特别注意

Binding To A UITableView From A ReactiveCocoa ViewModel

发表于 2014-09-21   |   分类于 翻译

英文作者Colin Eberhardt,原文可查看BINDING TO A UITABLEVIEW FROM A REACTIVECOCOA VIEWMODEL

这篇博客介绍了一个工具类,这个类将ReactiveCocoa中的ViewModels绑定到UITableView,而不需要通常的datasource和delegate。下面是这个辅助类的使用方法:

1
2
3
4
5
6
7
// 创建一个cell
UINib *nib = [UINib nibWithNibName:@"CETweetTableViewCell" bundle:nil];
// 将ViewModels的searchResults属性绑定到table view
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];

介绍

我总是在不断的编写代码:在工作中,在家里,在火车上…如果我不写代码,我就会觉得不快乐!(注:这才是真正的程序员啊)

在过去的几个月中,我开始在我的工程中越来越多地使用ReactiveCocoa了。这个框架可以用来创建一些非常优雅的解决方案,但同时它非常具有挑战性,因为对于任何一个问题,都有许多可用的解决方案。对于像我这样的编码狂人来说,这再好不过了。

几个月之前,我在Ray Wenderlich的网站上发表了两篇关于ReactiveCocoa的文章(第一部分、第二部分),以及一个Tech Talk视频。这些覆盖了ReactiveCocoa的基本用法,希望能让广大读者熟悉ReactiveCocoa。不过,我收到不少请求,希望能讨论一些使用ReactiveCocoa实现MVVM模式的高级话题。

正因此,我开始写这篇文章。不过,在我发布之前,我想先分享一个已纠缠我很久的问题…

如果将一个UITableView绑定到一个ReactiveCocoa的ViewModel中?

视图模式

我以一个简单的例子开头–一个允许我们搜索Twitter的ViewModel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// A view model which provides a mechanism for searching twitter
@interface CETwitterSearchViewModel : NSObject
/// The current search text
@property NSString *searchText;
/// An array of CETweetViewModel instances which indicate
/// the current search results
@property NSArray *searchResults;
/// A command which when executed searches twitter using the current searchText
@property RACCommand *searchCommand;
@end

这个ViewModel的实现重用了我在ReactiveCocoa指南第二部分所创建的信号,所以我不在此重复。如果想要看详细的代码,可以在github上查找。

将ViewModel绑定到一个带有UITextField和UIButton的UI是使用ReactiveCocoa最普通不过工作了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bind the UITextField text updates to the view model
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
// bind a button to the search command
self.searchButton.rac_command = self.viewModel.searchCommand;
// when the search executes hide the keyboard
[self.viewModel.searchCommand.executing subscribeNext:^(id x) {
[self.searchTextField resignFirstResponder];
}];
// show a network activity indicator when the search is being executed
RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) =
self.viewModel.searchCommand.executing;

在上面的代码中,当点击go按钮时,我们处理了诸如隐藏键盘这样的操作,并将网络连接的activity indicator绑定到了searchCommand.executing信号。

image

这样就将ViewModel三个属性中的两个绑定到了UI,到目前为止,一切都还不错!

最后一个属性是searchResults;这个属性是一个数组,包含了搜索结果。我们可以通过RACObserve来观察这个属性的修改,RACObserve创建了一个信号,该信号会在每次更新时发出一个next事件。但不幸的是,我们不能只给UITableView一个对象的数组,并告诉它去渲染自己。

如果我们在StackOverflow上搜索相关帖子,或者查看别人的ReactiveCocoa实例,可以看到传统的方式似乎是我们需要自己去实现table view的代理和数据源。换句话说,我们之前优雅的只需要几行绑定代码的视图类代码会由于需要实现table view的各种逻辑而显示异常丑陋。

不过,我们有更好的方法。

一个Table View绑定辅助类

在MVVM模式中,每一个View都由一个ViewModel支撑着。一个视图可能占据整个屏幕(此时我们将一个视图控制器绑定到一个ViewModel),或者只占据屏幕的一部分。

我们的顶层ViewModel的searchResults属性包含了一个对象数组,其中每一个元素都是一个ViewModel。为了解决这个问题,我们需要的是一个通用的机制来为每个视图创建一个ViewModel,并将这两者绑定在一起。

Nib提供了一种便捷的机制来定义可重用的视图。可以方便地使用nib来定义一个table view的单元格。

一个合理的table view绑定辅助类的接口如下:

1
2
3
4
5
6
7
8
9
/// A helper class for binding view models with NSArray properties
/// to a UITableView.
@interface CETableViewBindingHelper : NSObject
- (instancetype) initWithTableView:(UITableView *)tableView
sourceSignal:(RACSignal *)source
templateCell:(UINib *)templateCellNib;
@end

这个绑定类使用提供的table view来渲染由源信号所提供的view model,另外templeteCell定义了视图。让我们来看看这个辅助类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@interface CETableViewBindingHelper () <UITableViewDataSource>
@end
@implementation CETableViewBindingHelper {
UITableView *_tableView;
NSArray *_data;
UITableViewCell *_templateCell;
}
- (instancetype)initWithTableView:(UITableView *)tableView
sourceSignal:(RACSignal *)source
templateCell:(UINib *)templateCellNib {
if (self = [super init]) {
_tableView = tableView;
_data = [NSArray array];
// each time the view model updates the array property, store the latest
// value and reload the table view
[source subscribeNext:^(id x) {
_data = x;
[_tableView reloadData];
}];
// create an instance of the template cell and register
// with the table view
_templateCell = [[templateCellNib instantiateWithOwner:nil
options:nil] firstObject];
[_tableView registerNib:templateCellNib
forCellReuseIdentifier:_templateCell.reuseIdentifier];
// use the template cell to set the row height
_tableView.rowHeight = _templateCell.bounds.size.height;
_tableView.dataSource = self;
}
return self;
}
#pragma mark - UITableViewDataSource implementation
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return _data.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
id<CEReactiveView> cell = [tableView
dequeueReusableCellWithIdentifier:_templateCell.reuseIdentifier];
[cell bindViewModel:_data[indexPath.row]];
return (UITableViewCell *)cell;
}
@end

注意,初始化方法是内在逻辑所在。在这里,sourceSignal添加了一个subscriber,这样每次ViewModel的数组属性变化时,当前属性值的引用都会被保存,而table view也会重新加载。同样,也会创建templeteCell实例,来确定单元格的高度。

最后,这个类实现了table view的数据源方法,并通过信号来获取数据。

其中,单元格Cell必须实现以下协议,该协议提供了一个信号方法来将Cell绑定到相应的ViewModel上。

1
2
3
4
5
6
7
/// A protocol which is adopted by views which are backed by view models.
@protocol CEReactiveView <NSObject>
/// Binds the given view model to the view
- (void)bindViewModel:(id)viewModel;
@end

将这个用于实际当中,现在只需要几行代码就可以将一个数组属性绑定到一个table view上了。

1
2
3
4
5
6
7
8
// create a cell template
UINib *nib = [UINib nibWithNibName:@"CETweetTableViewCell" bundle:nil];
// bind the view models 'searchResults' property to a table view
[[CETableViewBindingHelper alloc]
initWithTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];

注意,源信号是通过RACObserver宏来创建的。这个信号在每次属性通过setter来改变都会发出一个next事件。

cell的实现类似于视图控制器;它们的UI控件定义在一个nib文件中并连接到相应的outlet属性。下图是该示例程序中定义cell的nib:

image

定义在CEReactiveView协议中的ViewModel绑定方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)bindViewModel:(id)viewModel {
CETweetViewModel *tweet = (CETweetViewModel *)viewModel;
// set the tweet 'status' label, sizing it to fit the text
self.titleTextField.frame =
CGRectInset(self.titleBackgroundView.frame, 5.0f, 5.0f) ;
self.titleTextField.text = tweet.status;
[self.titleTextField sizeToFit];
// set the username
self.usernameTextField.text = tweet.username;
// use signals to fetch the images for each image view
self.profileImage.image = nil;
[[self signalForImage:[NSURL URLWithString:tweet.profileBannerUrl]]
subscribeNext:^(id x) {
self.ghostImageView.image = x;
}];
self.ghostImageView.image = nil;
[[self signalForImage:[NSURL URLWithString:tweet.profileImageUrl]]
subscribeNext:^(id x) {
self.profileImage.image = x;
}];
}

注意,由于CETweetViewModel的属性不会发生变化,因此它们的值直接被拷贝到相应的UI控件上。当然,如果它们的值会改变,我们也可以使用ReactiveCocoa来将两者绑定到一起。

cell的实现同样使用了ReactiveCocoa在后台加载图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// creates a signal that fetches an image in the background, delivering
// it on the UI thread. This signal 'cancels' itself if the cell is re-used before the
// image is downloaded.
-(RACSignal *)signalForImage:(NSURL *)imageUrl {
RACScheduler *scheduler = [RACScheduler
schedulerWithPriority:RACSchedulerPriorityBackground];
RACSignal *imageDownloadSignal = [[RACSignal
createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSData *data = [NSData dataWithContentsOfURL:imageUrl];
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
return nil;
}] subscribeOn:scheduler];
return [[imageDownloadSignal
takeUntil:self.rac_prepareForReuseSignal]
deliverOn:[RACScheduler mainThreadScheduler]];
}

通过这种方式,我们就可以让我们的视图控制器保持少量的代码。看,是不是很整洁。

下面是完整的程序的实现效果:

image

处理选中事件

当前的绑定辅助类允许我们在一个table view中渲染ViewModel的数组,但如果我们需要处理选中事件呢?传统的方法是在视图控制器的手动处理,实现table view的代理方法,并执行相关的ViewModel的命令。

不过,这部分逻辑代码也可以放入到绑定辅助类中。

首先,我们在初始化方法中添加一个选择命令:

1
2
3
4
- (instancetype) initWithTableView:(UITableView *)tableView
sourceSignal:(RACSignal *)source
selectionCommand:(RACCommand *)selection
templateCell:(UINib *)templateCellNib;

这个初始化方法的实现现在存储了这个命令的引用。辅助类同样也实现了table view的代理,即tableView:didSelectRowAtIndexPath:方法的实现如下:

1
2
3
4
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[_selection execute:_data[indexPath.row]];
}

即当命令被调用时,会将选择的ViewModel作为执行参数传入。

在顶层ViewModel中,我已经添加了一个命令,这个操作只是简单地记录一下日志:

1
2
3
4
5
6
// create the tweet selected command, that simply logs
self.tweetSelectedCommand = [[RACCommand alloc]
initWithSignalBlock:^RACSignal *(CETweetViewModel *selected) {
NSLog(selected.status);
return [RACSignal empty];
}];

结论

希望这个table view绑定辅助类能够帮助那些使用MVVM和ReactiveCocoa来开发iOS应用的开发者们。所有的代码都在github上。如果您有任何意见、想法或建议,请让我知道。

iOS8中扫描Wi-Fi时MAC地址的随机化

发表于 2014-09-18   |   分类于 杂项

继在iOS6和iOS7系统中面向开发者关闭IP地址和MAC地址的获取后,苹果在iOS8中又出新招:在扫描Wi-Fi时使用随机的、本地管理的MAC地址。基于苹果保护用户隐私的一贯政策,这一步是必然的,它会封死所以获取用户隐私信息的通道。这对于苹果用户来说,当然是件好事。而对于想通过MAC地址来获取用户信息的商家们或黑客们,可能就得另想办法了。我们在此粗略地总结一下iOS对MAC地址所做的随机化处理。

MAC地址

在当今基于OSI模型的七层网络系统中,所有有网络接口的设备至少都有一个MAC地址(Media Access Control)。MAC地址位于OSI模型的第二层中,用于帮助网络交换机(有机或无线)确定哪个设备正在传输包及哪个设备应该接收这些包。根据设计,MAC地址应该是唯一的,它被写入到设备的物理网络芯片中,两个不同的设备MAC地址是不一样的。由于像智能手机设备中的无线以太网适配器在广播MAC地址时,采用的的类似于“嘿,这里有没有Wi-Fi”这种形式,所以,我们可以很容易地通过记录这个唯一识别来跟踪用户是否到过某个公共区域。

随机MAC地址的技术实现

在WWDC 2014上,Frederic Jacobs在对iOS8新特性的介绍中,提到了如下一条:

image

其大意是在iOS8系统中,Wi-Fi扫描过程中将使用随机的、本地管理的MAC地址,这个MAC地址并不总是设备的真实的MAC地址。

首先需要注意的是MAC地址的随机化。

如果是主动扫描,手机的无线设备会广播一个Probe请求,它包含一个随机的MAC地址。然后手机会等待周围的无线访问接入点(AP)返回Probe响应。一般来说会扫描所有的信道channel1-channel13(或者channel1, 5, 13),每个信道扫描10ms左右。当然,手机也可以通过点对点的方式将请求(Directed Probe)发送给特定的AP。我们一般隐藏一个无线路由的SSID的方法,就是让这个无线路由不响应广播的Probe,不主动发Beacon,只响应Directed Probe。

而如果是被动扫描,则手机不会广播任何Probe请求,只是周期性地在不同的信道上监听AP发出的beacon包。

另外一个需要注意的是这个随机化是发生在扫描过程中的。而在手机与无线接入点进行关联的过程以及数据传输的过程中,使用MAC地址仍然是设备真实的MAC地址。通常只有在关联阶段才是让AP记录手机MAC地址的阶段,这时候记录的MAC地址,才是将来作为数据传输的MAC地址。

更详细的介绍,可以参看@Qiang Meta在知乎上对《iOS 8 设备随机 MAC 地址躲避 Wi-Fi 热点的记录追踪,技术上是怎么实现,有何影响?》的作答。

影响

如果我们是在家中或者在办公区域,我们通常会自己去主动关联无线接入点,那么等到下次再进入这一区域时,我们的手机等设备就会自动去连接无线网络。由于这些Wi-Fi是受我们信任的,所以无所谓。但是当我们到达一个陌生区域或公共区域时,我们的设备就会去搜索可用的无线接入点。这时候就涉及到隐私的问题了。

现在,已经有一些公司已经开发了可以记住所扫描到的MAC地址的Wi-Fi集线器。这种设备可以记住我们的MAC地址,无论我们有没有连接它。这些公司已经在许多地方部署了这些设备,以便他们能在用户不知道的情况下了解用户的一些基本行为。

正如WWDC上所指出的一样,“诸如Euclid或其同行Turnstyle Solutions这样的公司,它们会使用MAC地址这样的数据来记录用户进出商店的一些信息,如人们何时走进一个商店,他们在某个区域停留多久,他们来商店的频率是多少”。而MAC地址的随机化正是为了规避这个问题。

这对于广告商和营销商来说无疑是个不小的打击,如果iOS8设备使用不断变化的MAC地址来广播Wi-Fi Probe请求,则不可能通过MAC地址来跟踪进出商店或其它场所的移动设备。这对于保护用户的隐私来说,又是更进了一步。

苹果的替代方案

不过苹果也没有完全关闭追踪用户并向用户推送广告的通道,它推出了另外一种方案–即基于位置服务的iBeacon。iBeacon已经内置在最近的iOS设备中了。不同于使用设备的MAC地址,iBeacon使用低功耗蓝牙技术来发现那边带有支持iBeacon功能的App的设备,以向这些设备发送广告或通知。iBeacon不同于基于MAC地址跟踪技术(iBeacon发射器不会从设备中获取数据),它只是在匹配到带有基于iBeacon的应用时,才可以察看设备位置。这样就无法推测出一个用户的习惯,从而保护了用户的隐私了。

当然,苹果在随机化MAC地址时,也综合考虑了用户隐私的泄露与商家基于地理位置来发送广告的需求之间的平衡。那些不愿意使用iBeacon的iOS用户可以通过关闭蓝牙来禁用iBeacon感知功能。在iOS8之前,用户只能通过禁用Wi-Fi来避免自己的设备被通过MAC地址的方式跟踪到。

总结

苹果这次对MAC地址的随机化处理,又一次展示了它对保护用户隐私的决定。相信以后类似的事情还会出现。而对于开发商或者开发者来说,在iOS设备上获取用户信息的渠道将会越来越少。我们改变不了苹果,或许也就只能另辟蹊径。

参考

  1. iOS 8 设备随机 MAC 地址躲避 Wi-Fi 热点的记录追踪,技术上是怎么实现,有何影响?
  2. iOS 8 to stymie trackers and marketers with MAC address randomization
  3. Why iOS 8′s MAC address randomizing is a huge win for privacy

Thinking In Terms Of iOS 8 Size Classes

发表于 2014-09-15   |   分类于 翻译

原文链接:Thinking In Terms Of iOS 8 Size Classes

对于最新的iOS8 SDK来说,最性感也最重要的的特性也许莫过于Size Classes了。

在聊Size Classes之前,我们先来回顾下历史。

一堂历史课

最初,iOS推出时,我们只有一种设备:iPhone。它的屏幕大小是320\*480。不过即使如此,它也是同时支付横屏和竖屏。设计同时支持两个方向的App不是像Mobile Safari或Messages那样,简单地拉伸和重新设置视图的大小。在大多数情况下,我们需要移动按钮和其它控件来让其适应横屏(480\*320)。

几年后的现在,我们有了高清屏,iPads和大屏的iPhone。当然,所有这些设备都是支持横屏和竖屏的。解决这个适配问题的传统的方法是在视图控制器和自定义视图中监听设备方向的变化,同时使用多个xib或storyboard。

假设我已经构建了一个同时支持iPhone和iPad的的Glassboard工程。在iOS7和老的版本之前,我们需要针对iPad单独创建一个storyboard,这个storyboard包含重建的视图控制器,outlet属性和target/action。这相当于是重复工作了。任何程序员都知道这不是个好主意。需要在两个不同的地方做相同的改变真是件糟糕的事。

如果是使用代码,则我们需要在代码中检测屏幕方向及设备大小,以便我们能手动调整我们的约束或基于frame的布局。我们的代码会像下面这段代码一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
UIDevice *device = [UIDevice currentDevice];
UIDeviceOrientation currentOrientation = device.orientation;
BOOL isPhone = (device.userInterfaceIdiom == UIUserInterfaceIdiomPhone);
BOOL isTallPhone = ([[UIScreen mainScreen] bounds].size.height == 568.0);
if (UIDeviceOrientationIsPortrait(currentOrientation) == YES)
{
// Do Portrait Things
if (isPhone == YES)
{
// Do Portrait Phone Things
// Don't deny you've done this at least once.
if (isTallPhone)
{
// iPhone 5+
}
else
{
// Old phones
}
}
else
{
// Do Portrait iPad things.
}
}
else
{
// Do Landscape Things.
if (isPhone == YES)
{
// Do Landscape Phone Things
}
else
{
// Do Landscape iPad things.
}
}

Size Classes

显然,上面的这些方案都不理想,而且随着苹果新设备的推出,这种情况会变得越来越糟。在今年的WWDC上,苹果除了介绍自动布局的新特性外,我们同样也看到了许多可变iOS模拟器的事例,以及一种处理所有这些问题和屏幕问题的新技术:Size Classes。

Size Classes是iOS使用的一种新的技术,允许我们为给定的设备自定义我们的程序,而且是基于设备的方向和屏幕大小的。

Size Classes有两个目的:

  1. 让开发人员和设计人员跳出指定设备的范畴,而是以更广义的范畴来思考问题
  2. 为未来做准备

第一个目的也引出了第二个目的。我们看到各种传说,说iPhone 6, 7将会是更大的设备。你也看到了苹果已经开发出了可穿戴设备(Apple Watch)。那么有什么方法可以让为这些设备开发变得更容易呢?那就是Size Classes。

目前从XCode 6上可以看到有四种类型的Size Classes:

image

  1. 宽紧凑(Compact)
  2. 长紧凑
  3. 宽正常(Regular)
  4. 长正常

任意时刻,我们的设备都有一个水平方向的Size Class和一个竖直方向的Size Class。这两者都是用来定义布局属性与物征(trait)的集合,以在屏幕上显示内容给用户。

特征(Traits)

水平和竖直的Size Class被认为是Traits。结合当前界面术语和显示比例,一起组成了一个特征集合。这不只是包含了指定的控制应该放在屏幕的什么地方。

特征(Trait)也可以用于诸如image assets的东西上(假设你正在使用Asset Catalogs)。在asset中,我们不仅可以包含1x和2x版本,我们还可以为不同的size class指定不同的image asset。在代码中,它看着仍然是相同的UIImage调用。Asset Catalogs负责基于当前的特征集合来渲染合适的图片。

为Size Classes设计

Size Classes对于开发人员来讲是一个很好的扩展,因为当我们需要支持多种设备和方向时,它能简化我们的开发。通过简化我们的工作,苹果可以更容易地开发新的设备,并可以让开发者开发能用的应用,而不仅仅是只为iPhone开发程序。

对于开发者来说,最大的改变是我们需要再一次修改我们的关于不同方向的代码。大家已经习惯了吧,谁让我们是开发者呢。

对于设计者来说,特征集合意味着可以少考虑是为哪种设备来做设计,而可以更多的考虑设备的属性。现在,设计者最需要考虑的因素是物理屏幕大小。

由于不能确保每台设备的屏幕尺寸都与Photoshop或测试样机保持一致,所以单独为特定的场景做设计已经站不住脚了。相反,我们的目标是应该为一类设备做通用的设计,主要包括:

  1. 手机上的肖像模式
  2. 平板上的肖像模式
  3. 手机上的景观模式
  4. 平板上的景观模式

现在iPhone 6来了,它的屏幕也变大了,它拥有与iPhone 4s和5一样的特征集合。当然,iPhone 6的尺寸比原来的手机更大了,但是UI应该基于为指定特征集合定义的界面,来做自适应的处理。

这可能意味着设计者需要推翻自己以前的一些设计,但这就是事实。就像软件开发一样,软件设计需要符合这些约束。新的约束就是我们不能再活在只为特定屏幕尺寸做设计的世界里面了。我们不是要像Android一样,但这是苹果希望我们前进的方向。

采用Size Classes

好消息是,Interface Builder可以让我们更好的使用Size Classes。更好的消息是,这些Interface Builder变化是向后兼容的,所以我们可以在合适的地方简化和合并Storyboards和Xibs,而不会落下任何用户。

不太好的消息是,如果需要在代码中使用特征集合,则只支持iOS 8。这是因为苹果很少为老的系统提供新的API接口。这就意味着我们需要在代码中添加一些新的分支来支持不同的系统。例如,为自定义的UIView调整intrinsicContentSize属性。如果系统是iOS8,我们可以使用竖直和水平的size class来确定这个值,但如果设备仍然是iOS 7或老版本,则已存在的代码仍然需要保留。

因为我使用并推荐Interface Builder,所以比起那些仍然活在“将一切写在代码”口号中的人们来说,我的工作明显地减少了。如果你仍然在那个阵营里面,我强烈建议你使用iOS 8, XCode 6和特征集合,并以此为契机加入到Interface Builder阵营中来。这样不仅能减少我们的代码量,同样可以通过提取大量的特征处理到一个视觉UI库来简化代码。

MVVM Tutorial with ReactiveCocoa: Part 2/2

发表于 2014-08-12   |   分类于 翻译

本文由Colin Eberhardt发表于raywenderlich,原文可查看MVVM Tutorial with ReactiveCocoa: Part 2/2

在第一部分中,我们介绍了MVVM,可以看到ReactiveCocoa如何将ViewModel绑定到各自对应的View上。

image

下图是我们程序实现的Flickr搜索功能

image

在这一部分中,我们来看看如何在程序的ViewModel中驱动视图间的导航操作。

目前我们的程序允许使用简单的搜索字符串来搜索Flickr。我们可以在这里下载程序。Model层使用ReactiveCocoa来提供搜索结果,ViewModel只是简单地记录响应。

现在,我们来看看如何在结果页中进行导航。

实现ViewModel导航

当一个Flickr成功返回需要的结果时,程序导航到一个新的视图控制器来显示搜索结果。当前的程序只有一个ViewModel,即RWTFlickrSearchViewModel类。为了实现需要的功能,我们将添加一个新的ViewModel来返回到搜索结果视图。添加新的继承自NSObject的RWTSearchResultsViewModel类到ViewModel分组中,并更新其头文件:

1
2
3
4
5
6
7
8
9
10
11
12
@import Foundation;
#import "RWTViewModelServices.h"
#import "RWTFlickrSearchResults.h"
@interface RWTSearchResultsViewModel : NSObject
- (instancetype)initWithSearchResults:(RWTFlickrSearchResults *)results services:(id<RWTViewModelServices>)services;
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSArray *searchResults;
@end

上述代码添加了描述视图的两个属性,及一个初始化方法。打开RWTSearchResultsViewModel.m并实现初始化方法:

1
2
3
4
5
6
7
- (instancetype)initWithSearchResults:(RWTFlickrSearchResults *)results services:(id<RWTViewModelServices>)services {
if (self = [super init]) {
_title = results.searchString;
_searchResults = results.photos;
}
return self;
}

回想一下第一部分,ViewModel在View驱动程序之前就已经生成了。下一步就是将View连接到对应的ViewModel上。

打开RWTSearchResultsViewController.h,导入ViewModel,并添加以下初始化方法:

1
2
3
4
5
6
7
#import "RWTSearchResultsViewModel.h"
@interface RWTSearchResultsViewController : UIViewController
- (instancetype)initWithViewModel:(RWTSearchResultsViewModel *)viewModel;
@end

打开RWTSearchResultsViewController.m,在类的扩展中添加以下私有属性:

1
@property (strong, nonatomic) RWTSearchResultsViewModel *viewModel;

在同一个文件下面,实现初始化方法:

1
2
3
4
5
6
- (instancetype)initWithViewModel:(RWTSearchResultsViewModel *)viewModel {
if (self = [super init]) {
_viewModel = viewModel;
}
return self;
}

在这一步中,我们将重点关注导航如何工作,回到视图控制器中将ViewModel绑定到UI中。

现在程序有两个ViewModel,但是现在将面临一个难题。如何从一个ViewModel导航到另一个ViewModel中,也就是在对应的视图控制器中导航。ViewModel不能直接引用视图,所示我们应该怎么做呢?

答案已经在RWTViewModelServices协议中给出来了。它获取了一个Model层的引用,我们将使用这个协议来允许ViewModel来初始化导航。打开RWTViewModelServices.h并添加以下方法来协议中:

1
- (void)pushViewModel:(id)viewModel;

理论上讲,是ViewModel层驱动程序,这一层中的逻辑决定了在View中显示什么,及何时进行导航。这个方法允许ViewModel层push一个ViewModel,该方式与UINavigationController方式类似。在更新协议实现前,我们将在ViewModel层先让这个机制工作。

打开RWTFlickrSearchViewModel.m并导入以下头文件

1
#import "RWTSearchResultsViewModel.h"

同时在同一文件中更新executeSearchSignal的实现:

1
2
3
4
5
6
7
8
9
- (RACSignal *)executeSearchSignal {
return [[[self.services getFlickrSearchService]
flickrSearchSignal:self.searchText]
doNext:^(id result) {
RWTSearchResultsViewModel *resultsViewModel =
[[RWTSearchResultsViewModel alloc] initWithSearchResults:result services:self.services];
[self.services pushViewModel:resultsViewModel];
}];
}

上面的代码添加一个addNext操作到搜索命令执行时创建的信号。doNext块创建一个新的ViewModel来显示搜索结果,然后通过ViewModel服务将它push进来。现在是时候更新协议的实现代码了。为了满足这个需求,代码需要一个导航控制器的引用。

打开RWTViewModelServicesImpl.h并添加以下的初始化方法

1
- (instancetype)initWithNavigationController:(UINavigationController *)navigationController;

打开RWTViewModelServicesImpl.m并导入以下头文件:

1
#import "RWTSearchResultsViewController.h"

然后添加一个私有属性:

1
@property (weak, nonatomic) UINavigationController *navigationController;

接下来实现初始化方法:

1
2
3
4
5
6
7
- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {
if (self = [super init]) {
_searchService = [RWTFlickrSearchImpl new];
_navigationController = navigationController;
}
return self;
}

这简单地更新了初始化方法来存储传入的导航控制器的引用。最后,添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
- (void)pushViewModel:(id)viewModel {
id viewController;
if ([viewModel isKindOfClass:RWTSearchResultsViewModel.class]) {
viewController = [[RWTSearchResultsViewController alloc] initWithViewModel:viewModel];
} else {
NSLog(@"an unknown ViewModel was pushed!");
}
[self.navigationController pushViewController:viewController animated:YES];
}

上面的方法使用提供的ViewModel的类型来确定需要哪个视图。在上面的例子中,只有一个ViewModel-View对,不过我确信你可以看到如何扩展这个模式。导航控制器push了结果视图。

最后,打开RWTAppDelegate.m,定位到createInitialViewController方法的RWTViewModelServicesImpl实例创建的地方,用下面的代码替换创建操作:

1
self.viewModelServices = [[RWTViewModelServicesImpl alloc] initWithNavigationController:self.navigationController];

运行后,点击”GO“可以看到程序切换到新的ViewModel/View:

image

现在还是空的。别急,我们一步一步来。不过我们的程序现在有多个ViewModel,其中导航控制器通过ViewModel层来进行控制。我们先回来UI绑定上来。

渲染结果页

搜索结果的视图对应的nib文件中有一个UITableView。接下来,我们需要在这个table中渲染ViewModel的内容。打开RWTSearchResultsViewController.m并定位到类扩展。更新它以实现UITableViewDataSource协议:

1
@interface RWTSearchResultsViewController () <UITableViewDataSource>

重写viewDidLoad的代码:

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
[super viewDidLoad];
[self.searchResultsTable registerClass:UITableViewCell.class
forCellReuseIdentifier:@"cell"];
self.searchResultsTable.dataSource = self;
[self bindViewModel];
}

这段代码执行table view的初始化并将其绑定到view model。先忘记硬编码的cell标识常量,我们会在后面将其移除。

继续在下面添加bindViewModel代码:

1
2
3
- (void)bindViewModel {
self.title = self.viewModel.title;
}

ViewModel有两个属性:上述代码处理的的标题,及渲染到table中的searchResults数组。那么我们该怎么样将数组绑定到table view呢?实际上,我们做不了。ReactiveCocoa可以绑定一些简单的UI控件,但是不能处理这种针对table view的复杂交互。但不需要担心,还有其它方法。卷起袖子开始做吧。

在同一文件中,添加以下两个数据源方法:

1
2
3
4
5
6
7
8
9
10
11
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return self.viewModel.searchResults.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.textLabel.text = [self.viewModel.searchResults[indexPath.row] title];
return cell;
}

这个就不用说了吧。运行后,效果如下:

image

更好的TableView绑定方法

table view绑定的缺失会很快导致视图控制器代码的增加。而手动绑定看上去又不太优雅。从概念上讲,在ViewModel的searchResults数组中的每一项是一个ViewMode,每个cell是对应一个ViewModel实例。在这篇博客中我创建了一个绑定帮助类CETableViewBindingHelper,允许我们定义用于子ViewModel的View,帮助类负责实现数据源协议。我们可以在当前工程的Util分组中找到这个帮助类。

CETableViewBindingHelper的构造方法如下:

1
2
3
4
+ (instancetype) bindingHelperForTableView:(UITableView *)tableView
sourceSignal:(RACSignal *)source
selectionCommand:(RACCommand *)selection
templateCell:(UINib *)templateCellNib;

为了将数组绑定到视图中,我们简单创建一个帮助类的实例。它的参数是:

  1. 渲染ViewModel数组的table view
  2. 处理数组变化的信号
  3. 可选的当某行被选中时的命令
  4. cell视图的nib文件

nib文件定义的cell必须实现CEReactiveView协议。工程已经包含了一个table view cell,我们可以用它来渲染搜索结果。打开RWTSearchResultsTableViewCell.h并导入协议:

1
#import "CEReactiveView.h"

采用协议:

1
@interface RWTSearchResultsTableViewCell : UITableViewCell <CEReactiveView>

下一步是实现协议。打开RWTSearchResultsTableViewCell.m并添加头文件

1
2
#import <SDWebImage/UIImageView+WebCache.h>
#import "RWTFlickrPhoto.h"

添加以下方法:

1
2
3
4
5
6
7
8
- (void)bindViewModel:(id)viewModel {
RWTFlickrPhoto *photo = viewModel;
self.titleLabel.text = photo.title;
self.imageThumbnailView.contentMode = UIViewContentModeScaleToFill;
[self.imageThumbnailView setImageWithURL:photo.url];
}

RWTSearchResultsViewModel的searchResults属性包含RWTFlickrPhoto实例的数组。它们被直接绑定到View,而不是在ViewModel中包装这些Model对象。

bindViewModel方法使用了SDWebImage第三方库,它在后台线程下载并解码图片数据,大大提高了scroll的性能。

最后一步是使用绑定帮助类来渲染table。

打开RWTSearchResultsViewController.m并导入头文件:

1
#import "CETableViewBindingHelper.h"

在该文件下面的代码中移除对UITableDataSource协议的实现,同时移除实现的方法。接下来,添加以下私有属性:

1
@property (strong, nonatomic) CETableViewBindingHelper *bindingHelper;

在viewDidLoad方法中移除table view的配置代码,回归来方法的最初形式:

1
2
3
4
- (void)viewDidLoad {
[super viewDidLoad];
[self bindViewModel];
}

然后我们在[self bindViewModel]后面添加以下代码:

1
2
3
4
5
6
7
UINib *nib = [UINib nibWithNibName:@"RWTSearchResultsTableViewCell" bundle:nil];
self.bindingHelper =
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
selectionCommand:nil
templateCell:nib];

这从nib文件中创建了一个UINib实例并构建了一个绑定帮助类实例,sourceSignal是通过观察ViewModel的searchResults属性改变而创建的。

运行后,得到新的UI:

image

一些UI特效

到目前为止,本指南主要关注于根据MVVM模式来构建程序。接下来,我们做点别的吧:添加特效。

iOS7已经发布一年多了,“运动设计(motion design)”获取了更多的青睐,很多设计者现在都喜欢用这种微妙的对话和流体行为。

在这一步中,我们将添加一个图片滑动的特效,很不错的。

打开RWTSearchResultsTableViewCell.h并添加以下方法:

1
- (void) setParallax:(CGFloat)value;

table view将使用这个方法来为每个cell提供视差补偿。

打开RWTSearchResultsTableViewCell.m并实现这个方法:

1
2
3
- (void)setParallax:(CGFloat)value {
self.imageThumbnailView.transform = CGAffineTransformMakeTranslation(0, value);
}

很不错,这只是个简单的变换。

打开RWTSearchResultsViewController.m并导入以下头文件:

1
#import "RWTSearchResultsTableViewCell.h"

然后在类扩展中采用UITableViewDelegate协议:

1
@interface RWTSearchResultsViewController () <UITableViewDataSource, UITableViewDelegate>

我们只是添加一个绑定辅助类来将将它自己设置为table view的代理,以便其可以响应行的选择。然而,它也转发代理方法调用到它所有的代理属性,这样我们仍然可以添加自定义行为。

在bindViewModel方法中,设置绑定辅助类代理:

1
self.bindingHelper.delegate = self;

在同一文件下面,添加scrollViewDidScroll的实现:

1
2
3
4
5
6
7
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray *cells = [self.searchResultsTable visibleCells];
for (RWTSearchResultsTableViewCell *cell in cells) {
CGFloat value = -40 + (cell.frame.origin.y - self.searchResultsTable.contentOffset.y) / 5;
[cell setParallax:value];
}
}

table view每次滚动时,调用这个方法。它迭代所有的可见cell,计算用于视差效果的偏移值。这个偏移值依赖于cell在table view中可见部分的位置。

运行后,可得到以下效果

image

现在我们回到业务的View和ViewModel。

查询评论及收藏计数

我们应该在列表界面中每幅图片的右下方显示评论的数量和收藏的数量。当前我们只在nib文件中显示一个假数据’123‘。我们在使用真值来替换这些值前,需要在Model层添加这些功能。添加表示查询Flickr API结果的Model对象的步骤跟前面一样。

在Model分组中添加RWTFlickrPhotoMetadata类,打开RWTFlickrPhotoMetadata.h并添加以下属性:

1
2
@property (nonatomic) NSUInteger favorites;
@property (nonatomic) NSUInteger comments;

打开RWTFlickrPhotoMetadata.m并添加description的实现

1
2
3
4
- (NSString *)description {
return [NSString stringWithFormat:@"metadata: comments=%lU, faves=%lU",
self.comments, self.favorites];
}

接下来打开RWTFlickrSearch.h并添加以下方法:

1
- (RACSignal *)flickrImageMetadata:(NSString *)photoId;

ViewModel将使用这个方法来请求给定图片的元数据,如评论和收藏。

接下来打开RWTFlickrSearchImpl.m并添加以下头文件:

1
2
#import "RWTFlickrPhotoMetadata.h"
#import <ReactiveCocoa/RACEXTScope.h>

接下来实现flickrImageMetadata方法。不幸的是,这里有些小问题:为了获取图片相关的评论数,我们需要调用flickr.photos.getinfo方法;为了获取收藏数,需要调用flickr.photos.getFavorites方法。这让事件变得有点复杂,因为flickrImageMetadata方法需要调用两个接口请求以获取需要的数据。不过,ReactiveCocoa已经为我们解决了这个问题。

添加以下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (RACSignal *)flickrImageMetadata:(NSString *)photoId {
RACSignal *favorites = [self signalFromAPIMethod:@"flickr.photos.getFavorites"
arguments:@{@"photo_id": photoId}
transform:^id(NSDictionary *response) {
NSString *total = [response valueForKeyPath:@"photo.total"];
return total;
}];
RACSignal *comments = [self signalFromAPIMethod:@"flickr.photos.getInfo"
arguments:@{@"photo_id": photoId}
transform:^id(NSDictionary *response) {
NSString *total = [response valueForKeyPath:@"photo.comments._text"];
return total;
}];
return [RACSignal combineLatest:@[favorites, comments] reduce:^id(NSString *favs, NSString *coms){
RWTFlickrPhotoMetadata *meta = [RWTFlickrPhotoMetadata new];
meta.comments = [coms integerValue];
meta.favorites = [favs integerValue];
return meta;
}];
}

上面的代码使用signalFromAPIMethod:arguments:transform:来从底层的基于ObjectiveFLickr的接口创建信号。上面的代码创建了一个信号对,一个用于获取收藏的数量,一个用于获取评论的数量。

一旦创建了两个信号,combineLatest:reduce:方法生成一个新的信号来组合两者。

这个方法等待源信号的一个next事件。reduce块使用它们的内容来调用,其结果变成联合信号的next事件。

简单明了吧!

不过在庆祝前,我们回到signalFromAPIMethod:arguments:transform:方法来修复之前提到的一个错误。你注意到了么?这个方法为每个请求创建一个新的OFFlickrAPIRequest实例。然后,每个请求的结果是通过代理对象来返回的,而这种情况下,其代理是它自己。结果是,在并发请求的情况下,没有办法指明哪个flickrAPIRequest:didCompleteWithResponse:调用用来响应哪个请求。不过,ObjectiveFlickr代理方法签名在第一个参数中包含了相应请求,所以这个问题很好解决。

在signalFromAPIMethod:arguments:transform:中,使用下面的代码来替换处理successSignal的管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@weakify(flickrRequest)
[[[[successSignal
filter:^BOOL(RACTuple *tuple) {
@strongify(flickrRequest)
return tuple.first == flickrRequest;
}]
map:^id(RACTuple *tuple) {
return tuple.second;
}]
map:block]
subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];

这只是简单地添加一个filter操作来移除任何与请求相关的代理方法调用,而不是生成当前的信号。

最后一步是在ViewModel层中使用信号。

打开RWTSearchResultsViewModel.m并导入以下头文件:

1
#import "RWTFlickrPhoto.h"

在同一文件中的初始化的末尾添加以下代码:

1
2
3
4
5
6
RWTFlickrPhoto *photo = results.photos.firstObject;
RACSignal *metaDataSignal = [[services getFlickrSearchService]
flickrImageMetadata:photo.identifier];
[metaDataSignal subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

这段代码测试了新添加的方法,该方法从返回的结果中的第一幅图片获取图片元数据。运行程序后,会在控制台输出以下信息:

1
2014-06-04 07:27:26.813 RWTFlickrSearch[76828:70b] metadata: comments=120, faves=434

获取可见cell的元数据

我们可以扩展当前代码来获取所有搜索结果的元数据。然而,如果我们有100条结果,则需要立即发起200个请求,每幅图片2个请求。大多数API都有些限制,这种调用方式会阻塞我们的请求调用,至少是临时的。

在一个table中,我们只需要获取当前显示的单元格所对象的结果的元数据。所以,如何实现这个行为呢?当然,我们需要一个ViewModel来表示这些数据。当前RWTSearchResultsViewModel暴露了一个绑定到View的RWTFlickrPhoto实例的数组,它们的暴露给View的Model层对象。为了添加这种可见性,我们将给ViewModel中的model对象添加view-centric状态。

在ViewModel分组中添加RWTSearchResultsItemViewModel类,打开头文件并各以下代码更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@import Foundation;
#import "RWTFlickrPhoto.h"
#import "RWTViewModelServices.h"
@interface RWTSearchResultsItemViewModel : NSObject
- (instancetype) initWithPhoto:(RWTFlickrPhoto *)photo services:(id<RWTViewModelServices>)services;
@property (nonatomic) BOOL isVisible;
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSURL *url;
@property (strong, nonatomic) NSNumber *favorites;
@property (strong, nonatomic) NSNumber *comments;
@end

看看初始化方法,这个ViewModel封装了一个RWTFlickrPhoto模型对象的实例。这个ViewModel包含以下几类属性:

  1. 表示底层Model属性的属性(title, url)
  2. 当获取到元数据时动态更新的属性(favorites, comments)
  3. isVisible,用于表示ViewModel是否可见

打开RWTSearchResultsItemViewModel.m并导入以下头文件:

1
2
3
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/RACEXTScope.h>
#import "RWTFlickrPhotoMetadata.h"

接下来添加几个私有属性:

1
2
3
4
5
6
@interface RWTSearchResultsItemViewModel ()
@property (weak, nonatomic) id<RWTViewModelServices> services;
@property (strong, nonatomic) RWTFlickrPhoto *photo;
@end

然后实现初始化方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (instancetype)initWithPhoto:(RWTFlickrPhoto *)photo services:(id<RWTViewModelServices>)services {
self = [super init];
if (self) {
_title = photo.title;
_url = photo.url;
_services = services;
_photo = photo;
[self initialize];
}
return self;
}

这基于Model对象的title和url属性,然后通过私有属性来存储服务和图片的引用。

接下来添加initialize方法。准备好,这里有些有趣的事情会发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)initialize {
RACSignal *fetchMetadata =
[RACObserve(self, isVisible)
filter:^BOOL(NSNumber *visible) {
return [visible boolValue];
}];
@weakify(self)
[fetchMetadata subscribeNext:^(id x) {
@strongify(self)
[[[self.services getFlickrSearchService] flickrImageMetadata:self.photo.identifier]
subscribeNext:^(RWTFlickrPhotoMetadata *x) {
self.favorites = @(x.favorites);
self.comments = @(x.comments);
}];
}];
}

这个方法的第一部分通过监听isVisible属性和过滤true值来创建一个名为fetchMetadata的信号。结果,信号在isVisible属性设置为true时发出next事件。第二部分订阅这个信号以初始化到flickrImageMetadata方法的请求。当这个嵌套的信号发送next事件时,favorite和comment属性使用这个结果来更新值。

总的来说,当isVisible设置为true时,发送Flickr API请求,并在将来某个时刻更新comments和favorites属性。

为了使用新的ViewModel,打开RWTSearchResultsViewModel.m并导入头文件:

1
2
#import <LinqToObjectiveC/NSArray+LinqExtensions.h>
#import "RWTSearchResultsItemViewModel.h"

在初始化方法中,移除当前设置_searchResults的代码,并使用以下代码:

1
2
3
4
5
_searchResults =
[results.photos linq_select:^id(RWTFlickrPhoto *photo) {
return [[RWTSearchResultsItemViewModel alloc]
initWithPhoto:photo services:services];
}];

这只是简单地使用一个ViewModel来包装每一个Model对象。

最后一步是通过视图来设置isVisible对象,并使用这些新的属性。

打开RWTSearchResultsTableViewCell.m并导入以下头文件:

1
#import "RWTSearchResultsItemViewModel.h"

然后在下面的bindViewModel方法的第一行添加以下代码:

1
RWTSearchResultsItemViewModel *photo = viewModel;

并在访方法中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
[RACObserve(photo, favorites) subscribeNext:^(NSNumber *x) {
self.favouritesLabel.text = [x stringValue];
self.favouritesIcon.hidden = (x == nil);
}];
[RACObserve(photo, comments) subscribeNext:^(NSNumber *x) {
self.commentsLabel.text = [x stringValue];
self.commentsIcon.hidden = (x == nil);
}];
photo.isVisible = YES;

这个代码监听了新的comments和favorites属性,当它们更新lable和image时会更新。最后,ModelView的isVisible属性被设置成YES。table view绑定辅助类只绑定可见的单元格,所以只有少部分ViewModel去请求元数据。

运行后,以看到以下效果:

image

是不是很酷?

节流

慢着,还有一个问题没有解决。当我们快速地滚动滑动栏,如果不做特殊,会同时加载大量的元数据和图片,这将明显地降低我们程序的性能。为了解决这个问题,程序应该只在照片显示在界面上的的时候去初始化元数据请求。现在ViewModel的isVisible属性被设置为YES,但不会被设置成NO。我们现在来处理这个问题。

打开RWTSearchResultsTableViewCell.m,然后修改刚才添加到bindViewModel:的代码,以设置isVisible属性:

1
2
3
4
photo.isVisible = YES;
[self.rac_prepareForReuseSignal subscribeNext:^(id x) {
photo.isVisible = NO;
}];

当ViewModel绑定到View时,isVisible属性会被设置成YES。但是当cell被移出table view进行重用时会被设置成NO。我们通过rac_prepareForReuseSignal信号来实现这步操作。

返回到RWTSearchResultsItemViewModel中。ViewModel需要监听isVisible属性的修改,当属性被设置成YES后一秒钟,将发送一个元数据的请求。

在RWTSearchResultsItemViewModel.m中,更新initialize方法,移除fetchMetadata信号的创建。使用以下代码来替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 通过监听isVisible属性来创建信号。该信号发出的第一个next事件将包含这个属性的初始状态。
// 因为我们只关心这个值的改变,所以在第一个事件上调用skip操作。
RACSignal *visibleStateChanged = [RACObserve(self, isVisible) skip:1];
// 2. 通过过滤visibleStateChanged信号来创建一个信号对,一个标识从可见到隐藏的转换,另一个标识从隐藏到可见的转换
RACSignal *visibleSignal = [visibleStateChanged filter:^BOOL(NSNumber *value) {
return [value boolValue];
}];
RACSignal *hiddenSignal = [visibleStateChanged filter:^BOOL(NSNumber *value) {
return ![value boolValue];
}];
// 3. 这里是最神奇的地方。通过延迟visibleSignal信号1秒钟来创建fetchMetadata信号,在获取元数据之前暂停一会。
// takeUntil操作确保如果cell在1秒的时间间隔内又一次隐藏时,来自visibleSignal的next事件被挂起且不获取元数据。
RACSignal *fetchMetadata = [[visibleSignal delay:1.0f]
takeUntil:hiddenSignal];

你可以想像一下如果没有ReactiveCocoa,这会有多复杂。

运行程序,现在我们和滑动显示平滑多了。

错误处理

当前搜索Flickr的代码只处理了OFFlickrAPIRequestDelegate协议中的flickrAPIRequest:didCompleteWithResponse:方法。不过,这样网络请求由于多种原因会出错。一个好的应用程序必须处理这些错误,以给用户一个良好的用户体验。代理同时定义了flickrAPIRequest:didFailWithError:方法,这个方法在请求出错时调用。我们将用这个方法来处理错误并显示一个提示框给用户。

我们之前讲过信号会发出next,completed和错误事件。其结果是,我们并不需要做太多的事情。

打开RWTFlickrSearchImpl.m,并定位到signalFromAPIMethod:arguments:transform:方法。在这个方法中,在创建successSignal变量前添加以下代码:

1
2
3
4
5
6
7
RACSignal *errorSignal =
[self rac_signalForSelector:@selector(flickrAPIRequest:didFailWithError:)
fromProtocol:@protocol(OFFlickrAPIRequestDelegate)];
[errorSignal subscribeNext:^(RACTuple *tuple) {
[subscriber sendError:tuple.second];
}];

上面的代码从代理方法中创建了一个信号,订阅了该信号,如果发生错误则发送一个错误。传递给subscribeNext块的元组包含传递给flickrAPIRequest:didFailWithError:方法的变量。结果是,tuple.second获取源错误并使用它来为错误事件服务。这是一个很好的解决方案,你觉得呢?不是所有的API请求都有内建的错误处理。接下来我们使用它。

RWTFlickrSearchViewModel不直接暴露信号给视图。相反它暴露一个状态和一个命令。我们需要扩展接口来提供错误报告。

打开RWTFlickrSearchViewModel.h并添加以下属性:

1
@property (strong, nonatomic) RACSignal *connectionErrors;

打开RWTFlickrSearchViewModel.m并添加以下代码到initialize实现的最后:

1
self.connectionErrors = self.executeSearch.errors;

executeSearch属性是一个ReactiveCococa框架的RACCommand对象。RACCommand类有一个errors属性,用于发送命令执行时产生的任何错误。

为了处理这些错误,打开RWTFlickrSearchViewController.m并添加以下的代码到initWithViewModel:方法中:

1
2
3
4
5
6
7
8
9
[_viewModel.connectionErrors subscribeNext:^(NSError *error) {
UIAlertView *alert =
[[UIAlertView alloc] initWithTitle:@"Connection Error"
message:@"There was a problem reaching Flickr."
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
}];

运行后,处理错误的效果如下:

image

想知道为什么获取收藏和评论的请求不报告错误么?这是由设计决定的,主要是这些不会影响程序的可用性。

添加最近搜索列表

用户可能会回去查看一些重复的图片。所以,我们可以做些简化操作。回想一下本文的开头,最后的程序在搜索输入框下面有一个显示最近搜索结果的列表。

image

现在我们只需要添加上这个功能,这次我要向你发起一个挑战了。我将这一部分的实现留给读者您来处理,来练习练习MVVM技能吧。

在开始之前,我在这些做些总结:

  1. 我将创建一个ViewModel来表示每个先前的搜索,它包含一些属性,这些属性包括搜索文本,匹配的数量和第一个匹配的图片
  2. 我将修改RWTFlickrSearchViewModel来暴露这些新的ViewModel对象的数组做为一个属性。
  3. 使用CETableViewBindingHelper可以非常简单地渲染ViewModel的数组,我已经添加了一个合适的cell(RWTRecentSearchItemTableViewCell)到工程中。

接下来何去何从?

在这里可以下载最终的程序。这两部分的内容已经包含了很多内容,这里我们可以好好回顾一下主要点:

  1. MVVM是MVC模式的一个变种,它正逐渐流行起来
  2. MVVM模式让View层代码变得更清晰,更易于测试
  3. 严格遵守View=>ViewModel=>Model这样一个引用层次,然后通过绑定来将ViewModel的更新反映到View层上。
  4. ViewModel层决不应该维护View的引用
  5. ViewModel层可以看作是视图的模型(model-of-the-view),它暴露属性,以直接反映视图的状态,以及执行用户交互相关的命令。
  6. Model层暴露服务。
  7. 针对MVVM程序的测试可以在没有UI的情况下运行。
  8. ReactiveCocoa框架提供强大的机制来将ViewModel绑定到View。它同时也广泛地使用在ViewModel和Model层中。

怎么样,下次创建程序的时候,是不是试试MVVM?试试吧。

1…567…9
南峰子

南峰子

86 日志
7 分类
© 2017 南峰子
由 Hexo 强力驱动
主题 - NexT.Pisces