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
, 那么就可以推测出这个对象的成员的内存地址
通过 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
中. bits
的 data()
方法 返回的是 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启动时 imageload
中 copy
到 class_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
, 那么它的内存结构就会被破坏.
比如你向 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还能算是同一类对象吗?所以从逻辑上讲,也不能允许添加实例变量.