目录
- 前言
- clang常用命令
- 对象本质探索
- 属性的本质
- 对象的内存大小
- isa 指针探究
前言
OC 代码的底层实现都是 C/C++代码,OC 的对象都是基于 C/C++ 的数据结构实现的,实际 OC 对象的本质就是结构体,那到底是一个怎样的结构体呢?
clang常用命令
clang是一个C语言、C++、Objective-C语言的轻量级编译器,是由Apple主导编写的
clang主要用于把源文件编译成底层文件,比如把main.m 文件编译main.cpp
、main.o
或者可执行文件
我们可以在终端使用 clang 命令使OC代码转换成 C++ 代码:
# 将 main.m 转换成 main.cpp 文件
clang -rewrite-objc main.m -o main.cpp
打开此 main.cpp 文件,共有 6 万多行代码:
实际上,不同平台支持的代码肯定是不一样的:Windows 、 MacOS 、 iOS 等,模拟器(i386)、 32bit(armv7)、 64bit(arm64)等,如果要指定平台(比如在 arm64 下的 iOS 开发),应在终端输入以下xcrun
命令:
# xcrun 命令基于 clang 进行了封装更好用
# 真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
# 模拟器编译
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
打开生成的 main-arm64.cpp 文件:
行数缩减到了 2 万多行,显然删减掉了冗余的代码
对象本质探索
将含有以下代码的 OC 文件转换成 Cpp 文件查看源码:
OC 代码:
@interface Student : NSObject {
@public
int _no;
int _age;
}
@end
Cpp 代码:
可以看到 Student 的定义(IMPL,即 implementation 实现的意思)其实是 struct ,也就是对象的本质是结构体
在此结构体的定义中,有一个struct NSObject_IMPL
结构体变量NSObject_IVARS
,表明 Student 继承于 NSObject 类(子类的结构都包含父类的结构),即包括 NSObject 的成员变量
在struct NSObject_IMPL
的定义中,可以看到只有isa
一个成员,其类型是 Class ,我们再找到 Class 的定义:
Class类型实际是一个objc_class
类型的结构体指针,我们还可以看到常用的id是objc_object
结构体指针类型,以及SEL是objc_selector
(函数)的一个结构体指针
属性的本质
@interface Person : NSObject
@property (nonatomic, assign)int height; // 自动生成实例变量、 setter/getter 方法
@end
@implementation Person
@end
那么 setter/getter 方法在哪儿呢?
创建出来的实例对象里面只存有实例变量,方法不会放在实例对象里面,而放在类对象中,有关对象的分类因为方法是公用的,方法的代码都是一样的
对象的内存大小
现在我们知道对象的本质其实就是一个结构体,其内存大小与成员变量有关,即isa
+其他成员变量
。对象第 1 个成员变量就是isa
结构体指针,而结构体的地址就是第 1 个成员变量的地址,即isa
变量的地址
下面我们来看看一个NSObject对象占用多少内存,NSObject只有一个成员变量,也就是结构体指针变量isa
:
在 C/C++ 中,结构体指针的内存占用大小通常取决于操作系统和计算机的体系结构,与结构体本身的内容和大小无关。具体来说:
- 在32位系统上,指针通常占用4字节。
- 在64位系统上,指针通常占用8字节。
这个大小是指针自身的大小,与指向的结构体中包含的数据类型或数量无关。指针的主要功能是存储内存地址,所以它的大小应该与系统的地址长度一致
现在绝大多数计算机的都是 64 位,所以 isa 变量占用 8 个字节,也就是说 NSObject 对象占用 8 字节
为检验对象到底占用了多少内存,引入以下两个库:
#import <objc/runtime.h>
#import <malloc/malloc.h>
使用其中的 API :
NSObject* obj = [NSObject alloc] init];
// 下面两个函数的返回值都是size_t(unsigned int),所以占位符使用%zd
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
//NSLog(@"%zd", malloc_size(CFBridgingRetain(obj)));
NSLog(@"%zd", malloc_size((__bridge const void *)(obj))); //OC转C 要进行桥接__bridge
运行结果:
刚分析 NSObject 对象不应该占用 8 字节吗?为什么第 2 个函数返回了 16 个字节?
实际class_getInstanceSize()
返回的是类实例对象的成员变量大小,malloc_size()
返回的是传入对象指针所指向的内存大小
下面来看看class_getInstanceSize()
的实现:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
查看最终的返回值,如果字节数小于 16 ,通过这种方法返回的结果就是 16,可见给 NSObject 分配了 16 个字节,但真正利用起来的只有 8 个字节(用来存放 isa 成员)
注释也有提到:
- CoreFoundation框架要求所有对象至少占用 16 字节,这种硬性规定
- 返回的是内存对齐后的成员变量所占内存大小
所以一般不去过多地关注此函数,而是通过malloc_size()
准确的了解到对象占用内存大小
打断点通过 LLDB 查看内存分布:
上面以十六进制的形式打出:
LLDB 指令:
print 、 p
:打印
po
:打印对象
——
读取内存分布:memory read/数量 格式 字节数 内存地址
或x/数量 格式 字节数 内存地址
格式:x
是 16 进制,f
是浮点,d
是 10 进制;字节大小:b
是byte 1字节,w
是word 4字节,h
是half word 2字节,g
是 giant word 8字节
——
修改内存中的值:memory write 内存地址 数值
通过 ViewMemory 实时查看内存数据:
在此处输入内存地址:
不论是通过 LLDB 还是 ViewMemory 都可以查看到标灰的位置就是obj
对象的内存分布,后面的都是其他不相关的内存分布
由于一个 16 进制位代表 4 个二进制位,两个就代表 8 个二进制位(1 个字节)
71 20 32 E3 01 00 00 01 00 00 00 00 00 00 00 00
,16 个字节中实际就只有 8 个字节有数据(成员变量,这里指 isa 指针变量)
0xe3322071 0x01000001 0x00000000
这是地址分布,怎么跟上面的顺序是相反的呢?这里涉及到一个大小端的问题:
大小端(Endian)是指数据在计算机内存中的存储方式,具体来说是多字节数据的存储顺序问题。它主要涉及两种模式:大端(Big Endian)和小端(Little Endian)。
大端: 数据的高位字节存储在内存的低地址端,低位字节存储在高地址端。也就是说,数据的第一个字节(最重要的字节)存储在起始地址上。例如,如果有一个32位的整数0x12345678存储在地址0x1000开始的位置上,其存储顺序如下:
- 0x1000: 0x12
- 0x1001: 0x34
- 0x1002: 0x56
- 0x1003: 0x78
大端模式直观地反映了数据在文档和其他数字表示中的顺序,因此在阅读内存转储时容易理解。很多网络协议(如TCP/IP)采用大端模式来传输数据。小端: 数据的低位字节存储在内存的低地址端,高位字节存储在高地址端。也就是说,数据的最后一个字节(最重要的字节)存储在起始地址上。同样地,如果有一个32位的整数0x12345678存储在地址0x1000开始的位置上,其存储顺序如下:
- 0x1000: 0x78
- 0x1001: 0x56
- 0x1002: 0x34
- 0x1003: 0x12
小端模式在某些类型的处理器(如x86架构)中使用,因为它可以在处理器将数据加载到寄存器时减少字节重排的操作。一些系统允许在大端和小端之间进行配置,这种灵活性在处理多种硬件平台时特别有用。同时,还存在一些更复杂的端模式,例如双端模式或中间端模式,这些通常用于特定的应用或硬件。
端序的理解对于开发者在处理字节级操作时至关重要,尤其是在进行网络编程、读写文件以及在不同架构之间迁移代码时。不匹配的字节序会导致数据解释错误,从而引发程序错误和数据损坏。因此,开发者必须确保在这些操作中考虑到端序的因素。
总结: 创建一个实例对象,至少需要内存class_getInstanceSize
(对齐后),实际分配内存malloc_size
最后以下例说明内存对齐:
struct Person_IMPL {
struct NSObject_IMPL NSOBJECT_IVARS; // 8
int _age; // 4
int _height; // 4
int _no; // 4
}; // 和为20,内存对齐交后:24
@interface Person : NSObject {
int _age;
int _height;
int _no;
}
@end
@implementation Person
@end
void testAllocSize(void) {
NSLog(@"%zd", sizeof(struct Person_IMPL)); //同class_getInstanceSize 24
// sizeof 运算符关键字 不是一个函数 编译过程中就会查看传入的类型并识别数据类型字节大小 编译时就会确定为一个常数
// 内存分配注意的地方
Person* person = [[Person alloc] init];
NSLog(@"%zd %zd", class_getInstanceSize([Person class]), malloc_size((__bridge const void *)(person))); // 24 32
// 操作系统在分配内存时,也会有“内存对齐”,所以 size 是 24,返回 32
NSLog(@"%zd", sizeof(person)); // 8 指的是person指针本身的大小
}
isa 指针探究
之前在【iOS】alloc、init和new原理文章中提到了对象通过obj->initInstanceIsa
初始化关联isa
指针,下面就来看看isa的结构,如何通过它关联类
initInstanceIsa
方法
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
initIsa
方法
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0); // isa初始化
if (!nonpointer) { // 非nonpointer指针直接绑定cls
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
#if ISA_HAS_INLINE_RC
newisa.extra_rc = 1;
#endif
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa() = newisa;
}
在initIsa
方法中,如果是非nonpointer
类型指针,则直接通过cls调setClass
方法绑定;否则现有一些位域bits赋值操作,再调setClass
方法绑定
我们可以看到方法内创建了一个 isa_t 类型的newisa
实例, 做了赋值操作newisa.bits = ISA_MAGIC_VALUE;
后,返回了newisa
注释中写得很清晰,news.bits
初始化时候只设置了nonpointer
,magic
两个部分,其余部分都没有进行设置,故值都为默认值0
该源码解释了上面初始化isa的两种方式:
- 通过
cls
初始化:非nonpointer,存储着Class、Meta-Class对象的内存地址信息 - 通过
bits
初始化:nonpointer,进行一系列的初始化操作
isa_t
联合体
#include "isa.h"
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#if ISA_HAS_INLINE_RC
bool isDeallocating() const {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif // ISA_HAS_INLINE_RC
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated) const;
Class getDecodedClass(bool authenticated) const;
};
isa_t
是一个联合体类型,其中的变量有bits
、cls
以及结构体里面的ISA_BITFIELD
宏定义变量,ISA_BITFIELD
结构如下,是按位域
定义的:
各个变量的内存分布图:
各个变量的解释:
nonpointer
:表示是否对 isa 指针开启指针优化,0:纯isa
指针,1:不止是类对象地址,isa
中包含了类信息、对象的引用计数等has_assoc
:关联对象标志位,0没有,1存在has_cxx_dtor
:该对象是否有C++
或Objc
的析构器,如果有析构函数,则需要做析构逻辑,没有则可以更快的释放对象shiftcls
:存储类指针
的值,开启指针优化的情况下,在ARM64架构中有33
位来存储类指针值magic
:用于调试判断当前对象是真的对象还是未初始化的空间weakly_referenced
:标志对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用对象则可以更快地释放unused
:对象是否释放has_sidetable_rc
:当对象引用计数大于10时,则需要借用该变量存储进位extra_rc
:对象的引用计数值(实际上是引用计数减1),例如:引用计数是10,那么extra_rc
为9,如果引用计数大于10,则需要使用到has_sidetable_rc
总结
isa
正是通过以上联合体中的变量信息将类关联起来isa
指针分为nonpointer
和非nonpointer
类型,非nonpointer
类型(0)就是一个纯指针,nonpointer
类型(1)指针开启了指针优化
指针优化
的设计,更好地利用isa
的内存,程序中有大量的对象,针对isa
的优化节省了大量内存