Objective-C Ivar探究

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335