美文网首页
Objective-C Ivar探究

Objective-C Ivar探究

作者: petyou | 来源:发表于2019-03-01 17:25 被阅读0次

    Ivar作为一个对象中实际储存信息的变量,它实际上是一个指向ivar_t结构体的指针

    typedef struct ivar_t *Ivar;
    struct ivar_t {
        int32_t *offset;    
        const char *name;
        const char *type;
        uint32_t size;
        ...
    };
    

    ivar_t 这个结构体中, offset 代表了这个变量在内存中相对所属对象内存空间起始地址的偏移量,偏移量大小根据类型来定.

    unsigned int count;
    Ivar * ivars =  class_copyIvarList([Person class], &count);
    for (NSInteger i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        NSLog(@"[%s] [%td]", ivar_getName(ivar), ivar_getOffset(ivar));
    }
        
    2019-02-28 17:30:30.755280+0800 funnyTry[5459:1822231] [_age] [8]
    2019-02-28 17:30:30.755321+0800 funnyTry[5459:1822231] [_height] [16]
    2019-02-28 17:30:30.755405+0800 funnyTry[5459:1822231] [_name] [24]
    
    

    比如现在有一个 Person *obj 对象

    @interface Person : NSObject
    
    @property (nonatomic, assign) int age;
    
    @property (nonatomic, assign) long height;
    
    @property (nonatomic, assign) char *name;
    
    @end
    
    

    我们创建一个对象

    Person *personObject = [Person new];
    personObject.age = 18;
    personObject.height = 180;
    personObject.name = "xiaoming";
    NSLog(@"%p", personObject);
    

    打印出personObject 在内存的地址为 0x280ea4780, 那么就可以推测出这个对象的成员的内存地址

    86F655F2-14AF-4C05-BF97-1CC8C0048A85.png

    通过 watchpoint 调试出相关的属性地址.可以看出,和预期的一样.这里解释下各个偏移量, age 偏移量为8, 是因为 personObject 里面还有一个从 NSObject继承过来的 isa 指针占据了8个字节,那么 age 作为第二个成员变量,偏移量自然为 isa 的长度8. 同时 age 又占据了4个字节, 此时放置 height, 但是 height需要占据8个字节, 无法直接放在 age 后面(字节对齐),于是另起一个整8字节, 偏移量 = isa长度 + 8 = 16.同理 name 的偏移量 = isa长度 + 8 + height长度 = 24.

    watchpoint set variable personObject->_age
    Watchpoint created: Watchpoint 1: addr = 0x280ea4788 size = 4 state = enabled type = w
        declare @ '/Users/wen/Documents/GitHub/funnyTry/funnyTry/funnyTry/Class/JustForFun/Playground/FTPlaygroundVC.m:229'
        watchpoint spec = 'personObject->_age'
        new value: 18
        
    (lldb) watchpoint set variable personObject->_height
    Watchpoint created: Watchpoint 2: addr = 0x280ea4790 size = 8 state = enabled type = w
        declare @ '/Users/wen/Documents/GitHub/funnyTry/funnyTry/funnyTry/Class/JustForFun/Playground/FTPlaygroundVC.m:229'
        watchpoint spec = 'personObject->_height'
        new value: 180
      
    (lldb) watchpoint set variable personObject->_name
    Watchpoint created: Watchpoint 3: addr = 0x280ea4798 size = 8 state = enabled type = w
        declare @ '/Users/wen/Documents/GitHub/funnyTry/funnyTry/funnyTry/Class/JustForFun/Playground/FTPlaygroundVC.m:229'
        watchpoint spec = 'personObject->_name'
        new value: 0x000000010104b420
    (lldb) 
    
    BED844FA-10D8-413E-BEEC-9A400B43346D.png

    ivar_t 这个结构体中, name & type & size 都很好理解.分别代表了名称 & 类型 & 大小. 同时也能看出成员变量是按顺序排列的.由父类到子类,有编码顺序由上而下,再结合字节对齐优化等规则进行排列. 成员变量按顺序排列在一起也可以通过 getIvar 这个方法知晓一二.

    static ivar_t *getIvar(Class cls, const char *name)
    {
        runtimeLock.assertLocked();
    
        const ivar_list_t *ivars;
        // cls必须初始化
        assert(cls->isRealized());
        // 拿到 class_ro_t 中的 ivars地址
        if ((ivars = cls->data()->ro->ivars)) {
            // 自增依次检测名称匹配
            for (auto& ivar : *ivars) {
                if (!ivar.offset) continue;  // anonymous bitfield
    
                // ivar.name may be nil for anonymous bitfields etc.
                if (ivar.name  &&  0 == strcmp(name, ivar.name)) {
                    return &ivar;
                }
            }
        }
    
        return nil;
    }
    

    那么 Ivar 在类中是怎么存储的呢?

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }
    ...
    }
    
    struct class_data_bits_t {
        // Values are the FAST_ flags above.
        uintptr_t bits;
    
        class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
    ...
    }
    

    可见类中除了 ISA superclass cache 的数据全都存储在 bits 中. bitsdata()方法 返回的是 class_rw_t 结构, 表示一个类可读可写的数据. 而我们寻找的 Ivar 存储在其中的只读数据部分, 即 const class_ro_t *ro.

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
        
        
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        const uint8_t * ivarLayout;
        
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;  // find you 
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    ...
    };
    ...
    }
    

    那么这个 ivar_list_t是个什么结构呢?

    struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
        bool containsIvar(Ivar ivar) const {
            return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
        }
    };
    
    /***********************************************************************
    * entsize_list_tt<Element, List, FlagMask>
    * Generic implementation of an array of non-fragile structs.
    *
    * Element is the struct type (e.g. method_t)
    * List is the specialization of entsize_list_tt (e.g. method_list_t)
    * FlagMask is used to stash extra bits in the entsize field
    *   (e.g. method list fixup markers)
    **********************************************************************/
    template <typename Element, typename List, uint32_t FlagMask>
    struct entsize_list_tt {
        uint32_t entsizeAndFlags;
        uint32_t count;
        Element first;
    
        Element& getOrEnd(uint32_t i) const { 
            assert(i <= count);
            return *(Element *)((uint8_t *)&first + i*entsize()); 
        }
     ...
    }
    

    􏱼􏱽􏱀􏳗􏰫􏰐􏷲
    􏱎􏱫􏱬􏱭􏰔􏱎􏱫􏱬􏱭􏰔􏱎􏱫􏱬􏱭􏰔􏱎􏱫􏱬􏱭􏰔􏱎􏱫􏱬􏱭􏰔􏱎􏱫􏱬􏱭对象的 isa_t 指针会指向它所属的类, 对象中并不包括 method protocol property ivar等信息, 从一个实例对象的内存占用大小也能看出来. 32 = isa指针(8) + age(4) (+4对齐) + height(8) + name(8). 这些信息在编译时都保存到了只读结构体 class_ro_t 中, 在app启动时 imageloadcopyclass_rw_t 中, 但是没有 copy ivars, 并且 class_rw_t中也没有定义 ivars 字段.
    在访问对象的某个成员变量是, 比如 personObject_age 成员变量. 先根据通过 static ivar_t *getIvar(Class cls, const char *name)函数获取到 ivar_t, 读取 ivar_t 的偏移量, 再根据 personObject 的内存首地址做偏移, 定位 _age 成员变量的实际内存地址, 就可以读取它的值了.

    NSLog(@"InstanceSize:%ld", class_getInstanceSize([Person class]));
    // 2019-03-01 16:09:59.182753+0800 funnyTry[5781:1964765] InstanceSize:32
    

    向一个类添加Ivar

    先看一下runtime.h中关于添加Ivar的接口声明

    /** 
     * Adds a new instance variable to a class.
     * 
     * @return YES if the instance variable was added successfully, otherwise NO 
     *         (for example, the class already contains an instance variable with that name).
     *
     * @note This function may only be called after objc_allocateClassPair and before objc_registerClassPair. 
     *       Adding an instance variable to an existing class is not supported.
     * @note The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
     * @note The instance variable's minimum alignment in bytes is 1<<align. The minimum alignment of an instance 
     *       variable depends on the ivar's type and the machine architecture. 
     *       For variables of any pointer type, pass log2(sizeof(pointer_type)).
     */
    OBJC_EXPORT BOOL class_addIvar(Class cls, const char *name, size_t size, 
                                   uint8_t alignment, const char *types) 
    

    文档中要求 class_addIvar 必须在 objc_allocateClassPair 之后且 objc_registerClassPair 之前调用, 向一个已经注册的类添加 Ivar 是不支持的.
    经过编译过程的类, 在加载的时候已经注册了, 根本没有时机让你添加实例变量; 而运行时创建的新类, 可以在 objc_registerClassPair 之前通过 class_addIvar 添加实例变量, 一旦注册完成后. 也不能添加实例变量了.

    Class bbqClass = objc_allocateClassPair([NSObject class], "BBQ", 0);
    BOOL addSuccess = class_addIvar(bbqClass, "name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    objc_registerClassPair(bbqClass);
    if (addSuccess) {
        id obj = [[bbqClass alloc] init];
        [obj setValue:@"xiaoming" forKey:@"name"];
        NSLog(@"%@",[obj valueForKey:@"name"]);
    }
    #2019-03-01 16:09:59.182470+0800 funnyTry[5781:1964765] xiaoming
    

    为什么只能向运行时创建的类添加 ivars, 不能向已经存在的类添加呢?
    因为编译时只读结构 class_ro_t就被确定, 在运行时时不可以修改的. class_ro_t中有一个字段 instanceSize表示当前类在创建对象时需要分配的内存空间,所有创建出来的对象都是这个大小.如果允许向一个类已经存在的类添加 ivars, 那么它的内存结构就会被破坏.

    C2E108B7-5719-4090-88CB-1E7F91CFC57E.png

    比如你向 Person 这个类增加一个 bool 类型的 sex 成员, 那么在添加之前由该类创建出来的对象占 8(isa_t) + 8(height) + 8(name) = 24 个字节, 在添加之后由该类创建出来的对象占 8(isa_t) + 8(height) + 8(name) + 1(sex) + 7 (对齐) = 32 个字节,那么如果这时候之前的对象访问了 sex 成员就会导致地址越界.所以从设计上, 就将这能情况给禁止掉了.

    且假设一个已经注册过的类创建了对象A, 然后我们又给这个类增加了一个实例变量,并用这个类又创建了对象B,那么A和B的存储结构都不一样, 那么A和B还能算是同一类对象吗?所以从逻辑上讲,也不能允许添加实例变量.

    相关文章

      网友评论

          本文标题:Objective-C Ivar探究

          本文链接:https://www.haomeiwen.com/subject/ldcjuqtx.html