从@property说起(四)深入成员变量

《从@property说起(四)深入成员变量》

之前的三篇文章都讲的是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的具体实现即可。

其实通过对成员变量绑定的分析,还有一个相似的问题:为什么OC中的集合类,例如NSArray, NSDictionary等等,都只能存对象而不能存基本类型呢
其实答案不固定,但是思想基本都一致,大家自己思考思考就好。

    原文作者:Curiosity
    原文地址: https://segmentfault.com/a/1190000008848927
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞