之前的三篇文章都讲的是interface和setter/getter,这一篇就讲一下ivar。

什么是成员变量

@interface MyViewController :UIViewController
{
NSString *name;
}
@end

.m文件中,你会发现如果你使用 self.name,Xcode会报错,提示你使用->,改成self->name就可以了。因为OC中,点语法是表示调用方法,而上面的代码中没有name这个方法。

所以在oc中点语法其实就是调用对象的setter和getter方法的一种快捷方式, self.name = myName 完全等价于 [self setName:myName];

那么成员变量是何时分配内存,又储存在何处呢?

所以我们就要先分析一下objc_class结构体。

objc_class

首先我们知道,OC中,所有的对象都可以认为是id类型。那么id类型是什么呢?

typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;

根据runtime源码可以看到,id是指向Class类型的指针
而Class类型是objc_class结构的指针,于是我们可以看到objc_class结构体的定义:

struct objc_class {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
};
// runtime版本不同会有修改,但是本质属性大致如此

可以看到Objective-C对象系统的基石:struct objc_class。

其中,我们可以很快地发现struct objc_ivar_list *ivars,这个就是成员变量列表。

struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
int space;
};
struct objc_ivar_list {
int ivar_count;
int space;
struct objc_ivar ivar_list[1];
}

再深入看就能看到ivar真正的定义了,名字,type,基地址偏移量,消耗空间。

实际上在objc_class结构体中,有ivar_layout这么一个东西。

顾名思义存放的是变量的位置属性,与之对应的还有一个weakIvarLayout变量,不过在默认结构中没有出现。这两个属性用来记录ivar哪些是strong或者weak,而这个记录操作在runtime阶段已经被确定好。

具体的东西可以参考sunnyxx孙源大神的文章Objective-C Class Ivar Layout 探索

所以,我们几乎可以确定,ivar的确是在runtime时期就已经被确定。类型,空间,位置,三者齐全。

所以,这也就是为什么分类不能简单地用@property来添加成员变量的原因。

分类中的成员变量

还是sunnyxx大神的文章objc category的秘密,其中对OC分类的本质探索非常透彻。

先看一下分类的结构:

struct category_t {
const char *name; /// 类名
classref_t cls; /// 类指针
struct method_list_t *instanceMethods; /// 实例方法
struct method_list_t *classMethods; /// 类方法
struct protocol_list_t *protocols; /// 扩展的协议
struct property_list_t *instanceProperties; /// 扩展属性
method_list_t *methodsForMeta(bool isMeta) { ... }
property_list_t *propertiesForMeta(bool isMeta) { ... }
};

可以看到,分类结构本身是不存在ivar的容器的,所以自然没有成员变量的位置。因此也很自然地没有办法自动生成setter和getter。

OC本身是一门原型语言,对象和类原型很像。类对象执行alloc方法就像是原型模式中的copy操作一样,类保存了copy所需的实例信息,这些信息内存信息在runtime加载时就被固定了,没有扩充Ivar的条件。

OC当然也没有封死动态添加成员变量这条路,因为我们有_object_set_associative_reference函数可以用。
那么它的原理又是什么呢?

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
// disguised_ptr_t是包装的unsigned long类型
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

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

属性的内存结构

类的结构在runtime时期就已经确定了,所以按照类结构的角度来看,类中的成员变量的地址都是基于类对象自身地址进行偏移的。

@interface Person: NSObject {
NSString * _name;
NSString * _sex;
char _ch;
}
@property(nonatomic, copy) NSString *pName;
@property(nonatomic, copy) NSString *pSex;
@end
@implementation Person
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%p, %p, %p, %p, %p, %p, %p", self, &_name, &_sex, &_ch, _pName, _pSex);
}
return self;
}
@end

后面三个地址确实相差为8位,但是在类对象self和第一个成员变量之间相差的地址是10位。这0x10位的地址偏移,实际上就是isa指针的偏移。

指针在64位系统中占8位地址很正常,但是char类型的成员变量一样也是偏移了8位,明明char类型只需要1bit。这实际上是为了内存对齐

实际上在给类添加成员变量时,会调用这个函数:

BOOL
class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)

alignment参数就是代表内存对齐方式。

uint32_t offset = cls->unalignedInstanceSize();
uint32_t alignMask = (1<<alignment)-1;
offset = (offset + alignMask) & ~alignMask;

上面这段代码就是地址偏移计算

苹果规定了某个变量它的偏移默认为1 << alignment,而在上下文中这个值为指针长度。

因此,OC中类结构地址的偏移计算与结构体还是有不同的,只要是小于8bit长度的地址,统一归为8bit偏移

具体的绑定ivar都通过addIvar这个函数,包括@synthesize关键字的绑定,具体@synthesize绑定成员变量只需要看一下class_addIvar的具体实现即可。