目录


1. iVar VS Property

Obective C 的类,本质是struct objc_class结构体,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct objc_class *Class; 
struct objc_class { 
   Class isa; 
   Class super_class; 
   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 ; 
} ;

objc_ivar_list *ivars是一个存储了objc_ivar的list。在Objective C中我们知道@property是声明了setter和getter的语法糖, @synthesis 是实现了setter和getter的语法糖(Xcode 4.4及之后的版本可以省略)。在类中声明的每一个property都被一个iVar backup(property 名字前加_)。objc_method_list **methodLists是一个指针的指针。通过修改该指针指向的指针的值,就可以实现动态地为某一个类增加成员方法。这也是Category实现的原理。同时也说明了为什么Category只可为对象增加成员方法,却不能增加成员变量。

关于objc_ivar_list, 我们看下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Person.h

@interface Person : NSObject{
    NSString *ivarName;
}
@property (nonatomic, strong) NSString *propertyName;
@end


Person.m

@implementation Person
@synthesize propertyName = _propertyName;//可省略
-(instancetype)init{
    if(self = [super init]){
        ivarName = @"ivar";
        _propertyName = @"property";
    }
    return self;
}
@end

我们创建了一个Person类,其有一个iVar为NSString *类型的ivarName,一个property为NSString *类型的propertyName。我们打印Person类的objc_ivar_list *ivars:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Person *person = [[Person alloc] init];
    
id personClass = [person class];
unsigned int ivarNumber = 0;
printf("Person objc_ivar_list: ");
Ivar *ivarArray = class_copyIvarList(personClass, &ivarNumber);
for(int i = 0; i < ivarNumber; i++){
    Ivar ivar = ivarArray[i];
    printf("%s, ",ivar_getName(ivar) );
}

//输出为:
//Person objc_ivar_list: ivarName, _propertyName, 
//

可见在一个类的本身定义中, iVar和Property都被加在objc_ivar_list中。

2 Associated Objects增加属性

2.1在匿名类中增加属性

我们知道Category可以用来扩展方法,但扩展不了类的iVar。Associated Objects 的出现就是为了解决这一问题。它让Category增加属性,就好像类本身定义中的属性一样可以用dot notation进行存取。但是它不加入类的objc_ivar_list中,而是存储在一个哈希表中。我们来看下面代码,给Person+AssociatedObjects增加一个属性类型为NSString *的associatedObjctName:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Person+AssociatedObjects.h

#import "Person.h"
#import <objc/runtime.h>

@interface Person (AssociatedObjects)
@property (nonatomic, copy) NSString *associatedObjcName;
@end


Person+AssociatedObjects.m

@implementation Person (AssociatedObjects)

-(NSString *)associatedObjcName{
    return objc_getAssociatedObject(self, @selector(associatedObjcName));
}

-(void)setAssociatedObjcName:(NSString *)associatedObjcName{
    objc_setAssociatedObject(self, @selector(associatedObjcName), associatedObjcName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

我们在Person(AssociatedObjects)中增加了属性类型为NSString *的associatedObjctName,并用runtime的API实现了其setter和getter方法。我们再来运行下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Person *person = [[Person alloc] init];
person.associatedObjcName = @"associated objects";
NSLog(@"associatedObjcName: %@", person.associatedObjcName);
    
id personClass = [person class];
unsigned int ivarNumber = 0;
Ivar *ivarArray = class_copyIvarList(personClass, &ivarNumber);
printf("Person objc_ivar_list: ");
for(int i = 0; i < ivarNumber; i++){
    Ivar ivar = ivarArray[i];
    printf("%s, ",ivar_getName(ivar) );
}

//输出为:
//associatedObjcName: associated objects
//Person objc_ivar_list: ivarName, _propertyName, 
//

我们可以看见objc_ivar_list并没有新加入的associated objects。

2.2 在Runtime API中动态增加属性

当用runtime API 动态创建类,添加方法和实例变量过程中,当类创建完毕后(调用objc_registerClassPair后)再用class_addIvar(...)添加的iVar不会出现在类的objc_ivar_list中,见如下代码。

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
 //dynamically create a Peron Class
 Class person = objc_allocateClassPair([NSObject class], "Person", 0);
     // add a instance method
 Method description = class_getInstanceMethod([NSObject class], @selector(description));
 const char *methodType = method_getTypeEncoding(description);
 class_addMethod([person class], NSSelectorFromString(@"greeting"), (IMP)greeting, methodType);
        // add a instance variable before register
 const char *nameBeforeRegister = "nameBeforeRegister";
 class_addIvar(person, nameBeforeRegister, sizeof(id), rint(log2(sizeof(id))), @encode(id));
    
  //register Person Class
 objc_registerClassPair(person);
    
     // add a instance variable after register
 const char *nameAfterRegister = "nameAfterRegister";
 class_addIvar(person, nameAfterRegister, sizeof(id), rint(log2(sizeof(id))), @encode(id));
    
 //create a instance
 id personInstance = [[person alloc] init];
     //call the method
 NSLog(@"%@", ((id (*)(id, SEL))objc_msgSend)(personInstance, NSSelectorFromString(@"greeting")));
     //dynamically add a variable(an associated object) to the personInstance and get it
 NSString *firstname = @"Johnson";
 objc_setAssociatedObject(personInstance, nameBeforeRegister, firstname, OBJC_ASSOCIATION_COPY_NONATOMIC);
 NSLog(@"%@", objc_getAssociatedObject(personInstance,nameBeforeRegister));
    
 NSString *lastname = @"Lu";
 objc_setAssociatedObject(personInstance, nameAfterRegister, lastname, OBJC_ASSOCIATION_COPY_NONATOMIC);
 NSLog(@"%@", objc_getAssociatedObject(personInstance,nameAfterRegister));
    

    
 int ivarNum = 0;
 Ivar *ivars = class_copyIvarList([person class], &ivarNum);
 for(int i = 0; i < ivarNum; i++){
     Ivar ivar = ivars[i];
     printf("%s ",ivar_getName(ivar));
 }
 printf("\n");

//输出为:
//Hello World!
//Johnson
//Lu
//nameBeforeRegister 
//

可见当用runtime API动态创建类时,当创建完毕后,添加的iVar就不加入到objc_ivar_list.

2.3. Runtime API介绍

以上介绍了Associated Objects的实现,我们现在来看看其runtime API,包括以下3个方法:

  1. objc_setAssociatedObject 用于给对象添加关联对象,传入 nil 则可以移除已有的关联对象;
  2. objc_getAssociatedObject 用于获取关联对象;
  3. objc_removeAssociatedObjects 用于移除一个对象的所有关联对象。

关于前两个函数中的 key 值是我们需要重点关注的一个点,一般来说,有以下三种推荐的 key 值:

  1. 声明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作为 key 值;
  2. 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作为 key 值;
  3. 用 selector ,使用 getter 方法的名称作为 key 值。

由于第3种方式最为优雅和简洁,在上面的例子中我们用的就是selector。

3 Non-Fragile iVars

Fragile Base Class Problem:在某些编程语言中,父类必须得通过重新编译所有子类才可以改变ivar Layout。例如,C++在父类里增加data members或virtual member functions会打破子类的二进制兼容,即使增加的members是私有的和对子类不可见的。

3.1 为什么Non Fragile ivars很关键

Runtime有两个版本,LegacyModern。当编译类的时候,ivar layout已经确定下来,比如object的ivar的offset和size等。所以你的ivar layout很可能看起来是下图中的base class。当我们子类化NSViewPetShopView的时候,增加了kittenspuppies两个ivar,我们期待的ivar layout是下图中的expected class

Legacy版本,当Mac OS X Def Leopard出来后,AppKit开发者在NSView里增加了touchedPaws iVar。这个时候,原来的PetShopView里的kittens就会被新加的touchedPaws所覆盖。这种Non Fragile ivars的特性必须通过重新编译PetShopView才能运行。如果更不幸一点,PetShowView类是来自第三方提供的静待库,我们就只能干等库作者跟新版本了。

Modern版本。程序启动后,runtime加载MyObject列的收,通过计算基类的大小,runtime动态调整了PetShowView ivar的layout,即向后移动了8个字节。于是我们的程序无序编译,就能运行。

SendingMessage

3.2 如何寻址类成员变量

主要通过class_ro_t里的instanceStartinstanceSize来判断, 当子类的instanceStart小于父类的instanceSize时,需要调整。

1
2
3
4
5
6
struct class_ro_t {
    ...
    uint32_t instanceStart;
    uint32_t instanceSize;
    ...
}

3.3 为什么Objective-C类不能动态添加成员变量而可以添加方法属性和协议

为什么可以通过分类动态添加方法,属性,协议而不能添加添加成员变量呢。简单的回答是方法,属性,协议属于Procedure,存储在类和元类(都是单例)里,内存中只有一份;而实例变量属于Data,存储在同样属于Data的对象(可以有多个)内存里。若给类增加ivar_t,来动态修改类成员变量布局,那么已经创建出的对象就不符合类定义了,变成了无效对象。

这也解释了为什么2.2 在Runtime API中动态增加属性,在通过objc_registerClassPair注册了类后,后面用class_addIvar方法增加的实例变量是无效的。

4 总结

Associated Objects用于扩展类的属性,使得外部在用dot notation存取属性时分不出两者的区别,这解决了Objective C属性扩展的问题。但是在内部实现中,Associated Objects和类本身的iVar和Property还是有区别的:

  1. 类的定义结束后(无论是Objective-C还是runtime API动态创建类)objc_ivar_list是不能变得。
  2. 关联类和被关联类在内存中是分开存储的。被关联类的objc_ivar_list只存储其本身的iVar和Property。
  3. 这同样体现在用runtime动态创建类中,class_addIvar(...)objc_registerClassPair前和后调用时的情况。当class_addIvar(...)objc_registerClassPair前调用时,加入的iVar是在类本身的定义中,存储在其objc_ivar_list;在objc_registerClassPair后调用,class_addIvar(...)不可以再动态增加类属性(因为如果可以动态增加,之前创建的类就无效了,data可以有多份,但是procedure只能有一份,ivar的定义在类的单例里,可以看做是procedure,这也解释了为什么不能在分类里添加ivar而只能添加方法或属性(方法的一种)。

全文总结参考该图


Share Post

Twitter Google+

Shunmian

The only programmers in a position to see all the differences in power between the various languages are those who understand the most powerful one.