理解Objective-C中的对象

本文以同步发布到个人博客,为获得更好的阅读体验,请访问这里

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

对象的理解

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

对象是类的具体体现

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

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

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

/// 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结构体:

/// 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类型的原始声明时,就看到了下面这句:

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

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

再瞅瞅objc_class结构体:

/// 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

然后我们执行:

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

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

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变成了这样:

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方法调用流程:

class_getInstanceSize
|
--> alignedInstanceSize
  |
  --> word_align(unalignedInstanceSize)

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

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
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对象真正需要的内存时多少?系统分配的内存又是多少?

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值