理解Objective-C中的对象

OC 是 C 语言的超集,在其之上提供了面向对象的能力。可我们面向的对象在 OC 中到底是个什么东西,或者说它在内存中如何表现呢?今天我们一起来说道说道。

对象的理解

首先来解决对象是什么的问题。先上结论:对象是类的具体体现;底层以 C 语言的结构体做为支撑;对象所占用的内存存储了结构体中的成员。

对象是类的具体体现

在面向对象中,我们使用来描述具有特定属性和行为的一类事物,它是一份蓝图;而对象是蓝图的具体体现(这里是我对面向对象中类与对象的理解,并非标准定义)。面向对象的理论就说这么多,具体细节还请参考其他权威资料。

底层以 C 语言的结构体做为支撑

打开Runtime源码,我们可以看到这样的定义:

1
2
/// Object.mm line 34
typedef struct objc_object *id;

从这里我们可以知道,id是一个指向struct objc_object的指针类型;OC 中所有继承自NSObject类生成的对象都是struct objc_object类型

为什么可以这样认为? 假设现在有一个变量这样声明:XXX pa = NULL;,此时我们只知道pa是个XXX类型的变量;之后你看到了typedef int* XXX;,是不是瞬间明白了这个XXX就是代表int*。类比下就知道id的场景。

紧接着,我们查看struct objc_object结构体:

1
2
3
4
5
/// objc.h line 40
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

从这里我们可以知道,OC 中所有继承自NSObject的类生成的对象,都具有 Class 类型的isa成员

立马一脸问号,Class又是什么东西?其实在查看id类型的原始声明时,就看到了下面这句:

1
2
/// Object.mm line 33
typedef struct objc_class *Class;

原来Class就是一个指向struct objc_class的指针类型。所以我们平时定义的也就是以struct objc_class作为支撑。

再瞅瞅objc_class结构体:

1
2
3
4
5
6
7
8
9
10
11
12
/// objc-runtime-new.h line 1145
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() const {
        return bits.data();
    }
    ...
}

这里是一个C++定义的结构体,可以继承以及定义方法。根据这个实现,我们可以知道:

  1. Class也是对象,因为它继承自objc_object
  2. Class也有isa成员,继承自objc_object,这点很重要,在方法的调用过程时会用到。
  3. 除了isa,该结构体还包含了父类指针superclass,和该类相关联的缓存cache以及该类的具体信息bits

好了,源码层面的东西先看到这里。下面再瞅瞅有趣的东西。

main.m中定义一个类WGCat:

@interface WGCat : NSObject
// 这里没有任何内容
@end

@implementation WGCat
@end

注意保持main.m文件中只引入了Foundation

然后我们执行:

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_arm64.cpp

.m文件重写为.cpp文件。然后我们找到了下面的代码:

1
2
3
4
5
6
7
struct NSObject_IMPL {
	Class isa;
};

struct WGCat_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

可以看到NSObject_IMPLobjc_object如出一辙,虽然两者的名字不一样,但成员变量都只有Class isa;。可以理解成,这两个家伙在不同范围中表达对象这一概念。

WGCat_IMPL只是有一个成员,NSObject_IMPL。因为我们当初的定义中没有成员或者属性。

接下来我们尝试在WGCat中添加一些属性和成员变量,变成下面这样:

@interface WGCat : NSObject {
@public
    int _friends;
}
@property (nonatomic, readwrite, assign) unsigned int age;
@property (nonatomic, readwrite, assign) float weight;
@end
@end
// 实现省略

之后重新rewrite下,得到的WGCat_IMPL变成了这样:

1
2
3
4
5
6
struct WGCat_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _friends;
	unsigned int _age;
	float _weight;
};

可以看到,我们写的OC类生成的对象其实就是用结构体表示的。

对象所占用的内存存储了结构体中的成员

通过上面一节,我们了解到了对象和结构体的关系,下面我们尝试使用struct WGCat_IMPL指针,指向WGCat实例对象,看看能不能通过结构体的指针读取对象里面的信息,如果可以,那么就说明实例对象的内存确实存储了对应结构体的成员。看如下代码:

int main(int argc, const char * argv[]) {
    WGCat *cat = [[WGCat alloc] init];
    cat.age = 10;
    cat.weight = 2;
    cat->_friends = 3;

    struct WGCat_IMPL *pointer = (__bridge struct WGCat_IMPL *)(cat);
    NSLog(@"age: %u, weight: %.2f, friends: %d", pointer->_age, pointer->_weight, pointer->_friends);
    // 输出 age: 10, weight: 2.00, friends: 3
    pointer = NULL;
    return 0;
}

可以看到通过结构体指针能正确读出对象中的信息。

但是这里任然有个疑问,系统为对象分配的内存只存储其成员变量吗?上面的输出只能说明为对象分配的内存确实存储了成员变量。但不能说明对象所暂用的内存存储成员变量。也就是说存在一种可能:系统分配的内存大小 大于 成员变量需要的内存。下面我们来看看一个WGCat对象所占的内存大小是多数。

三个获取内存大小的方法:

  • C 语言中sizeof运算符
  • C 语言中malloc_size函数
  • OC 中 Runtimeclass_getInstanceSize方法
WGCat *cat = [[WGCat alloc] init];
size_t size_from_sizeof = sizeof(struct WGCat_IMPL);
size_t size_from_runtime = class_getInstanceSize(WGCat.class);
size_t size_from_c = malloc_size((__bridge const void *)(cat));
NSLog(@"size_from_sizeof: %zd, size_from_sizeof: %zd, size_from_sizeof: %zd", size_from_sizeof, size_from_runtime, size_from_c);
// 输出
// size_from_sizeof: 24, size_from_sizeof: 24, size_from_sizeof: 32

三者的输出并不一致。

sizeof 的结果

C 语言中sizeof运算符会返回指定类型需要内存的大小。对于结构体WGCat_IMPL所需要的内存,我们需要知道结构体的内存对齐这个知识点。具体参考这里

  1. Class isa; 本质是指针,占 8 字节,偏移量为 0
  2. int _friends; 4 个字节,偏移量 8,是字节大小的整数倍。
  3. unsigned int _age; 4 个字节,偏移量 12,是字节大小的整数倍。
  4. float _weight;4 个字节,偏移量 16,是字节大小的整数倍。
  5. 整体对齐,全部字节8+4+4+4=20,最大成员变量字节大小 8,20 并不是 8 的整数倍,添加填充至 24

class_getInstanceSize 的结果

class_getInstanceSize方法调用流程:

1
2
3
4
5
class_getInstanceSize
|
--> alignedInstanceSize
  |
  --> word_align(unalignedInstanceSize)

最终落脚到word_align方法上,有一个对齐的操作:

1
2
3
4
define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

这个方法会将入参转换成8的最小倍数。本例中会将20对齐为24

malloc_size 的结果

在苹果的网站上,我找到了这段说明:

The malloc_size() function returns the size of the memory block that backs the allocation pointed to by ptr. The memory block size is always

1
 at least as large as the allocation it backs, and may be larger.

大致意思是:该函数会返回指针指向的内存块大小,并且该内存块大小至少等于指定分配大小,还可以大于指定分配的大小。

iOS 的内存分配最终会落脚到malloc库中,该库中分配内存时会以 16 字节对齐,24 按 16 字节对齐,所以这里分配的内存大小就是 32 了。具体细节还未探索到,有兴趣的查看这里

从这些信息来看,一个WGCat对象在内存中确实占据了32字节,但是这些内存并没有全部使用。

多问几个为什么

善于发现问题,是一种能力!多问几个为什么,你会理解的更加深入!

上面的例子只展示了类中定义属性和成员变量的情况,添加方法或其他元素是否会影响实例对象的内存大小

不会影响。 我在上面的WGCat中实现了协议,添加了类方法,实例方法,最终rewrite后得到的结构并没有变化,说明这些信息不会影响实例对象的内存大小。

对象所占用的内存为什么只存储实例变量

对象是类的具体体现,假设实例对象存储了诸如方法或其他信息,那么每一个实例对象都会包含这些重复的信息,属于浪费。不必将这些固定的信息存储在实例对象的内存中。那么问题又来了,类似于方法这类信息到底存储在哪里?我们下篇继续。TODO

在分配的内存多于需要的时候,多出的内存会存储其他信息吗

继续使用这个例子,在第8行打上短点,然后输出pointer的地址,比如我这里是0x101973650,十进制是4321654352。然后打开Xcode的地址查看器(Debug->Debug Workflow->View Memory),输入十六进制地址:

view memory

  1. 输入查看的地址
  2. 每两个十六进制代表 8bit,一个字节
  3. pointer指针指向的内存块,一共 32 字节,第一行
  4. isa 8 个字节
  5. _friends成员的内存,4 字节
  6. _age成员的内存,4 字节
  7. _weight成员的内存,4 字节
  8. 结构体对齐策略添加的填充,4 字节
  9. malloc库以 16 字节对齐添加的填充,8 字节

可以看到多出的字节(编号 8、9)并没有存储任何信息。注意 iOS 是小端模式,读取字节数据时从高地址开始。比如isa的字节序列应该是0x001d800100002271_friends的字节序列应该是0x000003

小考验

为了使大家更好的理解该篇的内容,留一个小小的问题:一个NSObject对象真正需要的内存时多少?系统分配的内存又是多少?

欢迎大家一起交流。再会!