对于一个类的实例变量来说,我们常说他的内存分布是isa + ivars
。为什么内存是这样分布的?他是怎样确定的?
本文采用源码为当前最新:objc4-756.2
与libmalloc-166.251.2
开胃菜
比如有这么段代码:
@interface A : NSObject
@property (nonatomic, assign) BOOL b;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) char c;
@end
@implementation A
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
A *a = [A new];
a.b = YES;
a.name = @"test";
a.c = 'p';
}
return 0;
}
可以看到,变量a存储的内容有如下规律:
-
标记1
与a->isa
数据相同 -
标记2
与a.b
数据相同 -
标记3
与a.c
数据相同 -
标记4
与a.name
数据相同
这就是文章开头说的isa + ivars
。
标记3
后面的空字节是匿名成员变量,为了内存对齐,b
与c
分别是BOOL
与char
类型,都只占1个字节,为了节省内存两者相邻。name
是个指针,占8个字节放在后面。
内存对齐
再来看内存占用情况:
可以看到,A类的实例变量占用内存为24字节(isa 8字节 + b 1字节 + c 1字节 + 匿名成员变量 6字节 + name 8字节
),而变量a实际申请了32字节的内存。
如果不存在内存对齐,变量a占用内存应为isa 8字节 + b 1字节 + name 8字节 + c1字节 = 18字节
,之所以添加6字节匿名成员变量,这与cpu的数据总线相关:
- 对于64位cpu来说,一次可交换64bit数据,即8字节
- 对于32位cpu来说,一次可交换32bit数据,即4字节
在objc-runtime-new.h
中有如下代码:
struct objc_class : objc_object {
...
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
...
}
instanceSize
函数中的参数extraBytes
为0,unalignedInstanceSize
函数中的data()->ro->instanceSize
为当前实例变量占用内存大小。显然,当最终size小于16时,会给size赋值成16,所以oc对象最小占用内存就是16字节(经word_align
函数计算后仍为16)。
再来看word_align
函数,他在objc-os.h
中:
在64位cpu下,WORD_MASK
的值为7
,32位cpu的值为3
。(x + WORD_MASK) & ~WORD_MASK
又代表什么意思?
对于(a + (b -1)) & ~(b - 1)
来说,最终得到就是大于等于a
的最小b的倍数
,举个例子:
a = 7
b = 3
(a + (b -1)) & ~(b - 1) = 9
大于等于7
的3的最小倍数
就是9
,所以(x + WORD_MASK) & ~WORD_MASK
在64位cpu上总是8的倍数,在32位cpu上总是4的倍数。
在objc-class.mm
中有如下代码:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
这与上边的推理相互印证。
申请内存会调用libmalloc
中的代码:
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
可知,SHIFT_NANO_QUANTUM
为4,NANO_REGIME_QUANTA_SIZE
为1<<4
即16
。
这里又出现一个新算法:
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
其实与上文中的(a + (b -1)) & ~(b - 1)
算法相同,这里也是为了得到大于等于size的最小16的倍数。
由于变量a实际占用24字节,并不是16的倍数,所以此处得到32个字节。
运行时注册类与成员变量验证
为了更好地理解这一过程,这里用runtime注册A类,以及添加成员变量:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class A = objc_allocateClassPair(NSObject.class, "A", 0);
class_addIvar(A, "_b", sizeof(BOOL), 0, @encode(BOOL));
class_addIvar(A, "_c", sizeof(char), 0, @encode(char));
class_addIvar(A, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
objc_registerClassPair(A);
id a = [A new];
[a setValue:@(YES) forKey:@"_b"];
[a setValue:@"test" forKey:@"_name"];
[a setValue:@('p') forKey:@"_c"];
NSLog(@"%p", [[a valueForKey:@"_b"] boolValue]);
NSLog(@"%p", [[a valueForKey:@"_c"] charValue]);
NSLog(@"%p", [a valueForKey:@"_name"]);
}
}
先来直观地感受一下:
这与上文的结果完全一致。
class_addIvar
函数的第4个参数,0
、0
、log2(sizeof(NSString *)
究竟是怎样确定的?不知道你发现没有,0
其实与log2(sizeof(char))
、log2(sizeof(BOOL))
是相等的,这里填写的应该都是log2(sizeof([数据类型]))
吗? 成员变量的添加顺序可以调换吗?
回到源码:
BOOL
class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)
{
if (!cls) return NO;
if (!type) type = "";
if (name && 0 == strcmp(name, "")) name = nil;
mutex_locker_t lock(runtimeLock);
checkIsKnownClass(cls);
assert(cls->isRealized());
// No class variables
if (cls->isMetaClass()) {
return NO;
}
// Can only add ivars to in-construction classes.
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}
// Check for existing ivar with this name, unless it's anonymous.
// Check for too-big ivar.
// fixme check for superclass ivar too?
if ((name && getIvar(cls, name)) || size > UINT32_MAX) {
return NO;
}
class_ro_t *ro_w = make_ro_writeable(cls->data());
// fixme allocate less memory here
ivar_list_t *oldlist, *newlist;
if ((oldlist = (ivar_list_t *)cls->data()->ro->ivars)) {
size_t oldsize = oldlist->byteSize();
newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
memcpy(newlist, oldlist, oldsize);
free(oldlist);
} else {
newlist = (ivar_list_t *)calloc(sizeof(ivar_list_t), 1);
newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
}
uint32_t offset = cls->unalignedInstanceSize();
uint32_t alignMask = (1<<alignment)-1;
offset = (offset + alignMask) & ~alignMask;
ivar_t& ivar = newlist->get(newlist->count++);
#if __x86_64__
// Deliberately over-allocate the ivar offset variable.
// Use calloc() to clear all 64 bits. See the note in struct ivar_t.
ivar.offset = (int32_t *)(int64_t *)calloc(sizeof(int64_t), 1);
#else
ivar.offset = (int32_t *)malloc(sizeof(int32_t));
#endif
*ivar.offset = offset;
ivar.name = name ? strdupIfMutable(name) : nil;
ivar.type = strdupIfMutable(type);
ivar.alignment_raw = alignment;
ivar.size = (uint32_t)size;
ro_w->ivars = newlist;
cls->setInstanceSize((uint32_t)(offset + size));
// Ivar layout updated in registerClass.
return YES;
}
static ivar_t *getIvar(Class cls, const char *name)
{
runtimeLock.assertLocked();
const ivar_list_t *ivars;
assert(cls->isRealized());
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的流程很简单,需要注意的有以下几点:
- 参数name与type可以为空,这也就是匿名成员变量(用来占位的)
- 在添加Ivar前有这么句判断
if ((name && getIvar(cls, name)) || size > UINT32_MAX) return NO
,而从getIvar
源码可以看到,匿名成员变量不会被匹配到,所以匿名成员变量可以添加多个(对应着内存优化,多处占位) -
checkIsKnownClass(cls)
与assert(cls->isRealized())
用来检测当前添加成员变量的类是否已经存在,这也是为什么无法给已注册的类添加成员变量的原因(通过objc_setAssociatedObject
添加的关联变量并不在被添加类中) - 添加成员变量时,总是先获取当前变量所占空间,再通过
alignment
参数来控制偏移,并且有如下算法:
uint32_t offset = cls->unalignedInstanceSize();
uint32_t alignMask = (1<<alignment)-1;
offset = (offset + alignMask) & ~alignMask;
显然,在这种算法下,参数alignment
并不总是log2(sizeof([数据类型]))
,你需要计算来达到最优布局,添加成员变量的顺序也不能调换,比如先添加_b
再添加_name
最后添加_c
,那么_c
一定在_name
之后,而不会与_b
相邻,A类在这种成员变量布局下会浪费不必要的内存
- 最终offset的值会绑定到
ivar_t
结构体的offset
指针中存储,结构体定义如下:
struct ivar_t {
#if __x86_64__
// *offset was originally 64-bit on some x86_64 platforms.
// We read and write only 32 bits of it.
// Some metadata provides all 64 bits. This is harmless for unsigned
// little-endian values.
// Some code uses all 64 bits. class_addIvar() over-allocates the
// offset for their benefit.
#endif
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
所以Ivar存有想知道的一切,如偏移量等。从而,可以通过偏移量结合实例变量所在地址定位到成员变量存储数据的位置,通过name
得知成员变量的名称,通过type
得知成员变量的类型(怎样解析存储的数据),通过size
得知成员变量的大小,甚至可以得到alignment
。
isa
前面一直在说ivar
,最后来说isa
,为什么可以通过实例->isa
来取值:
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
...
}
struct objc_object {
private:
isa_t isa;
public:
...
}
typedef struct objc_class *Class;
isa
是结构体objc_class
的成员变量,*Class
是objc_class
的结构体指针,*a
是Class
的指针,所以a
即Class的实例,而Class又是个结构体指针,所以可以通过->
取到结构体的成员变量。
Have fun!