Objective-C对象的本质
- OC 底层实现都是 C, C++
- OC面相对象都是基于C,C++的结构体实现的
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
以上代码, 将main.m
文件转换为c++
文件
-
NSObject
的底层实现
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class
从以上就可以说明, isa
是一个指针, 在64位操作系统中, 占8
个字节
搜索objc4
下载后打开
搜索class_getInstanceSize
发现下面的代码:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
Class's ivar size rounded up to a pointer-size boundary.
上面这句话的意思是, class_getInstanceSize
这个方法返回的其实是类的成员变量的大小, 并不是指针指向的那片内存空间的大小, 那么, NSObject
的成员变量isa
占8
个字节, 那么这个方法返回的就是8
alloc
本质调用的是allocWithZone
用allocWithZone
去搜源码
发现最调用的是
_objc_rootAllocWithZone
然后继续搜
image.png
发现调用的是
_class_createInstance
然后继续搜
image.png image.png
然后发现决定size
大小的是instanceSize
这个方法
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
CF requires all objects be at least 16 bytes.
这里的逻辑很关键, 当size
小于16
时, 将size
强制置为16
. 这是CoreFoundation
这个框架决定的
而 alignedInstanceSize
这个方法, 返回的是8
(之前上文提到过的), 而extraBytes
通常为0
, 所以 size
初始赋值的时候是8+0
, 但CoreFoundation
要求至少16
, 所以加了代码
if (size < 16) size = 16;
所以最终返回16
, 也就是说NSObject
的alloc
方法开辟的内存空间为16
所以回答问题
一个NSObject对象占用多少个字节?
- 系统分配了
16
个字节给NSObject
对象(通过malloc_size
函数获得) - 但
NSObject
对象内部只使用了8
个字节(64
位环境下, 通过class_getInstanceSize
函数获得)
只使用8
个字节放下面这个结构体
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
就是说这个结构体只占8
个字节, 理解这一点非常重要
那我们再来看一个自定义的类的本质
现在定义一个Student
类
@interface Student : NSObject {
@public
int _no;
int _age;
}
@end
@implementation Student
@end
然后用依然用如下命令行命令, 将文件转换成C++
代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
最后我们发现, Student
类的本质是:
struct NSObject_IMPL {
Class isa;
};
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
因为NSObject_IMPL
这个结构体里只有一个isa
指针, 那么Student_IMPL
就相当于
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
所以总的字节数就为8+4+4 = 16
个字节
下面, 我将对象的指针赋值给结构体的指针
image.png
指针的地址当然是一样的, 但是, 我通过结构体可以访问到内存中的值吗? 答案是可以的:
image.png这也从侧面说明, Student
这个对象的本质是一个结构体
当我们打了断点, 可以看到对象的指针地址:
image.png image.png image.png上图中可以很清晰地看到_no
和_age
的值, iOS
采用的是小端模式, 所以读取的时候是从高位到低位, 即
0x00000005
(5)
0x00000012
(18)
上图中也可以很清晰地知晓Student
对象的内存布局
前面8
个字节是存放isa
指针的值(8
个字节), 紧接着是_no
成员变量的值(4
个字节),最后是成员变量_age
的值(4
个字节)
通过打印也可以知道Student
成员变量所占内存的大小是16
字节, 存放Student
对象所开辟的内存空间也是16
字节
思考: Person
对象和Student
对象各占几个字节?
@interface Person : NSObject {
@public
int _no;
}
@end
@implementation Person
@end
@interface Student : Person {
@public
int _age;
}
@end
@implementation Student
@end
首先看Person
对象, 它的本质是结构体
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
};
那么这个结构体占几个字节呢?
看起来好像是isa
的8
字节, 加上int
类型的4
个字节, 是12
个字节, 但其实不是, 因为这中间涉及到一个概念叫做内存对齐
也就是说, 内存的大小必须是结构体内所占字节数最大的成员变量所占字节的倍数, 那么isa
占8
个字节, 那么就必须是8
的倍数, 也就是最小是16
.
从上图也可以看出,
class_getInstanceSize
这个方法返回的是对齐过的实例对象的大小
image.png
alignedInstanceSize
这个方法也是将未对齐的实例对象大小进行内存对齐
所以内存对齐后, Person
对象所占的内存大小为16
个字节
那么Student
又占多少个字节呢?
Student
的本质是:
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _age;
};
看起来, Person_IMPL
结构体占16
个字节, 再加int
类型的_age
是4
个字节, 再因为内存对齐, 应该是32
个字节.
其实不是, 还是16
个字节, 这是因为, Person_IMPL
的最后4
个字节是空的, 没有存放东西, 编译器优化, 会将_age
放在后面4
个字节中, 所以最终还是16
个字节.
那么再思考, 假设, Student
这个对象后面的成员变量不是4
个字节的int _age
, 而是long long
类型的, 那么, Student
这个对象又占用多少个字节呢?
我理解的是, 现在Student
这个对象的本质是:
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
long long _score;
};
首先Person_IMPL
这个结构体是占16
个字节, 后面4
个空字节放不下_score
, 那么就必须再开辟4
个字节的空间, 那就是20
个字节, 但由于内存对齐, 所以最后是16 * 2 = 32
个字节
让我们来看看是不是这样吧?
image.png
发现对象创建所开辟的内存空间确实是32
个字节,但对象中成员变量的值所占的字节数并不是20
个字节, 而是24
个字节
查看内存发现, 当
long long
类型占8
个字节, 最后的4
个字节存不下时, 并不会占用最后的4
个字节, 而是将那4
个字节留空, 另外开辟16
个字节, 然后存在低位的8
个字节.
所以这里是要特别注意的!
如果Student
类是这样的结构, 它的内存布局又是怎样的呢?
@interface Student : Person {
@public
int _age;
int _score;
}
@end
@implementation Student
@end
这里16+4+4 = 24
, 成员变量占24
个字节, 看起来是没什么问题的. 但是, 最后的_score
这个成员变量是存在最后高位的4
个字节? 还是倒数第二的4
个字节, 最后4
个字节留空?
看起来是后面一种情况, 这和上一个例子中long long
的那个属性的内存布局又有点不同.
另外还有一点, 属性和方法
如果对象中不仅仅有成员变量, 还有属性, 又会是怎样的呢?
@interface Person : NSObject {
@public
int _no;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
其实, 这个Person
对象的本质是:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _height;
};
那么它所占的字节数很明显是16
个字节.
因为@property (nonatomic, assign) int height;
这个属性的本质就是生成了带下划线的成员变量 int _height
以及set, get
方法.
创建这个对象所开辟的内存空间里不存在方法吗?
是的, 方法不在创建这个对象所开辟的内存空间里, 因为成员变量的值是可能会变化的, 但方法不会变, 所以方法只有一份, 它不会随着创建对象开辟内存空间而又去占用内存空间, 方法是存在于类方法列表里的.
最后一点要记住的是:
class_getInstanceSize
(创建一个实例对象, 至少需要多少内存? )就是那个结构体多少空间存储就够了.
malloc_size
(创建一个实例对象, 实际上分配了多少内存? )
要提高内存的分配速度, 优化内存的分配, 会进行内存对齐, 这里的内存对齐和结构体那里的内存对齐还不太一样(这里是16的倍数)
sizeof
实际上是个运算符, 传进去的是类型, 比如下图, 传进去的是struct Student_IMPL
这样一个结构体, 那么它在编译阶段就直接转换成24
了, 不会在运行时阶段做任何运算.
但这里如果传进去的是student
, 本质上是传进去一个指针, 那指针类型就是8
个字节, 返回的也就是8
网友评论