Objects, Messageing, and the Runtime

6. Understand Properties

7. Access Instance Variables Primarily Directly When Accessing Them Internally

当在类的内部访问成员变量的时候,直接访问成员变量和使用property访问有几点区别:
直接访问变量可以直接获得希望获得的数据或直接修改对应的值,而使用property访问将会出发消息传递的机制,使得可能出现因运行时而产生的意外,因此,从某种程度上说,直接访问将会更加“安全”。
直接访问将会绕过变量的内存管理机制。(似乎最新的版本中并不会绕过)
直接访问将不会触发KVO通知。(存疑)
使用property会更加方便调试,可以在setter和getter中加断点。(直接访问的话,watch似乎也能解决一部分问题)

类的内部直接访问成员变量有几个需要注意的点:
在初始化函数中给成员变量赋值的时候,使用直接访问的方式进行赋值:子类可能会重载setter函数,从而产生意外的情况。
对于一些Lazy初始化的变量,如果使用直接访问的话,可能导致这个变量无法被正常初始化。

8. Understand Object Equality

在OC里使用==对两个对象进行比较的时候我们比较的这两个指针本身而不是这两个指针指向的内容。因此,一般情况下我们应该使用isEqual:
NSString实现了自己的isEqual方法:isEqualToString:,他将会比isEqual:更加高效,因为isEqual:不知道要比较的对象是什么类型的,所以在比较的时候需要使用额外的一些步骤来进行判断。
NSObject默认的==方法只有当且仅当两个对象的指针完全一致的时候才能成立。
当我们自己实现一个类的相等比较的时候,有两个函数是其中的核心:

1
2
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

其中需要注意的是,当两个对象相等的时候,他们的hash函数的返回值必须相等,但是hash函数返回值相等的两个对象不一定是相等的。
另外,当一个对象被加入到容器里的时候,hash函数通常会被用来作为hash table的索引。例如,当对象被加入到set中的时候,这个对象的hash值将会被用做set中是否已经包含这个对象的判断——如果被加入的对象的hash值和set中其他对象的hash都不相同,那么说明这个set中不存在这个对象。注意如果被加入的对象的hash值已经存在,并不能说明这个set中已经包含这个对象(参照上面说的hash相同的对象不一定相等),这时候就需要拿hash相等的对象一个个进行比较。因此,如果一个对象的hash函数如下返回的话,这个对象在被加入到容器中的时候,就可能会有性能问题。

1
2
3
-(NSUInteger)hash{
return 1234;
}

对于一个Person类而言,一个可能的hash函数可以是:

1
2
3
4
-(NSUInteger)hash{
NSString *stringToHash = [NSString stringWithFormat:@“%@:%@:%@:%i”, _firstName, _lastName, _age];
return [stringToHash hash];
}

但是,如果这么实现的话,同样存在性能问题——这样比单纯返回一个固定的数字要慢得多。当这个类的对象被加入到容器中的时候,每次都需要进行这些计算。
另一个可行(平衡性能)的方案如下:

1
2
3
4
5
6
-(NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}

9. Use the Class Cluster Pattern to Hide Implementation Detail

参照工厂模式

10. Use Associated Objects to Attach Custom Data to Existing Classes

当希望对一个已经存在的类增加自定义数据的时候,可以使用下面这一组函数来进行:

1
2
3
4
5
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

id objc_getAssociatedObject(id object, void *key)

void objc_removeAssociatedObjects(id object)

11. Understand the Role of objc_msgSend

OC中对于函数的调用实际上本质都是向对象发送消息。

1
2
3
id returnValue = [someObject messageName:parameter];

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

为了能够往正确的对象发送正确的消息,objc_smgSend函数将会查看消息接受对象的函数实现列表,如果这个对象的函数实现中没有对应的函数,就沿着这个对象的父类继续往上,如果一路往上都没有找到,那么消息转发的机制就会被触发。
看起来这种消息发送的方式需要在每次OC函数调用的时候都需要做很多额外的事情,可能会影响效率。但是实际上objc_msgSend会为每一个类缓存一个索引表用来快速判断某个对象是否实现了某个函数,所以只有在一个类第一次调用方法的时候才会实际执行上述的查询工作。因此函数调用通常情况下不会成为程序的瓶颈,当然了,如果真的需要避免运行时的这些额外工作从而提高效率的话,可以使用纯C函数来实现某个函数。

12. Understand Message Forwarding

因为OC强大的运行时机制(可以在运行时动态为某个类添加方法),使得编译器很难在编译时发现某个对象调用特定函数是否违法,这时候消息转发机制(Message Forwarding)就为开发者提供了机会来告诉对象如何处理那些“不认识”的消息。
当那些“不认识”的消息出现之后,消息转发机制提供两条路径供开发者处理这些有问题的消息。

  1. 消息的接受者可以动态地在运行时添加方法来处理这些消息(Dynamic method resolution)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    id autoDictionaryGetter(id self, SEL _cmd);
    void autoDictionaryGetter(id self, SEL _cmd, id value);

    + (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFormSelector(selector);
    if (/* selector is from a @dynamic property*/) {
    if ([selectorString hasPrefix:@“set”]) {
    class_addMethod(self, selector, (IMP)autoDictionarySetter, “v@:@”);
    } else {
    class_addMethod(self, selector, (IMP)autoDictionaryGetter, “@@:”);
    }
    return YES;
    }
    return [supser resolveInstanceMethod:selector];
    }
  2. 当1依旧无法处理这些消息之后,运行时进入了所谓的完成的消息转发机制(Full forwarding mechanism)。这种情况下,运行时会让消息的接受者来自己处理这个Invocation:首先,它会询问是不是有其他什么对象可以处理这个消息,如果有,那么将这条消息转发给对应的对象。如果依旧没有可以代为处理消息的对象,那么运行时将会把消息所有的细节通过NSInvocation进行包装,然后给予最后处理这条消息的机会,如果依旧没有处理,就会抛出unrecognized selector sent to instance xxxx的错误。

13. Consider Method Swizzling to Debug Opaque Methods

基于OC强大的运行时能力,每个OC的函数调用都是在运行的时候才决定究竟哪一个方法会被调用。正基于此,我们可以在运行时动态地修改某个函数的具体实现,为一个对象增加新的方法,甚至交换两个方法的实现。
每个类的方法列表中会保存那个类实现了哪些方法,这些方法以函数指针的形式进行存储在一张表中(IMPs):

1
id (*IMP)(id,SEL,...)
函数 IMP
lowercaseString IMPa
uppercaseString IMPb
capitalizedString IMPc

交换两个函数的实现:

1
2
3
4
5
6
//void method_exchangeImplementations(Method m1, Method m2)
//Method class_getInstanceMethod(Class aClass, SEL aSelector)

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

为NSString的lowercaseString添加log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface NSString(EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end

@implementation NSString(EOCMyAdditions)
- (NSString *)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@“%@=>%@”,self, lowercase);
return lowercase;
}
@end

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eco_MyLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

看起来eoc_myLowercaseString像是会产生循环调用,但是实际上运行时在进行eoc_myLowercaseString调用的时候,因为eoc_myLowercaseString的函数IMP已经被换成了NSString默认的lowercaseString,所以并不会产生循环调用的问题。

14. Understand What a Class Object IS

一个OC对象的本质:

1
2
3
typedef struct objc_object {
Class isa;
} *id;

Class类型的本质:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char* name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}

可以看到Class对象的第一个变量就是isa,这说明Class类型本身也是一个OC对象。super_class对象中存储了对象父类的信息。另外,isa变量存储了一个对象的类型。