对象的本质是什么?
其实在上篇iOS -内存对齐中已经提过啦,那么已知对象的本质就是结构体,那么我们应该怎么验证这个结论呢?
在探索oc对象本质前,先了解一个编译器:clang
-clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器
-主要是用于底层编译,将一些文件``输出成c++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。
开始操作
1.在main中自定义一个类YPPerson
@interface YPPerson : NSObject
@property (nonatomic, copy) NSString *name;//这个属性方便我们定位相关代码
@end
@implementation YPPerson
@end
2.打开终端,使用clang编译main.m文件,命令:clang -rewrite-objc main.m -o main.cpp
3.在main.cpp中搜索YPPerson,(main.cpp里东西很多,我们只看关键的)
//关键字METACLASS,猜测这是编译器自动生成的元类相关,后续会继续探究,今天就不偏题了。
static struct _class_ro_t _OBJC_METACLASS_RO_$_YPPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1, sizeof(struct _class_t), sizeof(struct _class_t),
(unsigned int)0,
0,
"YPPerson",
0,
0,
0,
0,
0,
};
//这里才是YPPerson类对象的相关
static struct _class_ro_t _OBJC_CLASS_RO_$_YPPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0, __OFFSETOFIVAR__(struct YPPerson, _name), sizeof(struct YPPerson_IMPL),
(unsigned int)0,
0,
"YPPerson",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_YPPerson,
0,
(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_YPPerson,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_YPPerson,
};
//YPPerson的底层编译
struct YPPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
//NSObject 的底层编译
struct NSObject_IMPL {
Class isa;
};
通过上述分析,理解了OC对象的本质,但是看到NSObject的定义,会产生一个疑问:为什么isa的类型是Class?(在探究对象alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的)
-其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换,类似于swift中的 as 的强转。源码中isa的强转如下图所示:
了解了对象的本质,我们再来看看其核心isa的分析吧。
在分析isa之前需要先了解一下结构体
和联合体
以及位域
相关知识。
结构体
结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。
缺点:所有属性都分配内存,比较浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费
优点:存储容量较大,包容性强,且成员之间不会相互影响
联合体(共用体)
联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉
缺点:包容性弱
优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
位域
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为"位域"或"位段"。
所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
了解了这些基础知识后,我们来看看isa在源码中是怎么定义的吧。
在alloc流程学习中,我们发现isa实际是一个isa_t的联合体,其定义如下:
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
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式
通过cls初始化,bits无默认值
通过bits初始化,cls有默认值
还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示
其中ISA_BITFIELD
宏定义为
当前环境为x86,主要看这块定义
*/
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
可以看出在不同架构下,ISA_BITFIELD
的定义是不同的,由于现在可编译的源码是运行在mac下的,我们现在主要看x86架构下的定义
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
很明显这里用到了前面提到的位域。
接下来我们来仔细看看在一个对象创建过程中isa的初始化流程吧。
找到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);
if (!nonpointer) {
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
newisa.extra_rc = 1;
}
// 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;
}
这里面涉及到taggedPointer
以及nonpointer
,
taggedPointer
-Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
-Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
-在内存读取上有着3倍的效率,创建时比以前快106倍。
nonpointer
-表示是否对 isa 指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
断点发现会走入以下逻辑,并输出了newisa
当走过setClass
后,会发现newisa中已经有了类信息
让我们来看看setClass
都做了些什么吧
如图当拿到YPPerson
的地址后,将其右移三位赋值给了shiftcls
,那么为什么要右移三位呢?
在x86架构下
1、由MACH_VM_MAX_ADDRESS为0x7fffffe00000 知虚拟内存最大寻址空间为47位
2、由于内存对齐的原因,对象内存地址后三位必定为0
基于以上两条,为了节省内存空间,省略后三位的0,shiftcls设计为44位,故需要将类地址右移3位,
参考资料:
1.https://www.jianshu.com/p/7fd6241a7124
2.https://juejin.cn/post/6973582933126823950/#heading-7