Objective-C Ivar探究

2019-03-01  本文已影响0人  petyou

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还能算是同一类对象吗?所以从逻辑上讲,也不能允许添加实例变量.

上一篇下一篇

猜你喜欢

热点阅读