之前我们分析了alloc流程和内存对齐算法, 今天我们来研究对象的本质,并对isa进行详细解析。
对象的本质探究方法 - Clang
Clang是一个由苹果主导编写,基于LVVM的C/C++/OC编译器,通过Clang的rewrite命令可以将OC代码还原为源码
新建一个工程,在main函数中创建一个类,随意添加个属性:
打开终端,cd到文件所在目录,执行clang -rewrite-objc main.m -o main.cpp
- 打开main.cpp文件,直接搜索FCPerson类名即可直接定位到我们要关注的位置,可以直接看到对象的本质是结构体:
#ifndef _REWRITER_typedef_FCPerson
#define _REWRITER_typedef_FCPerson
typedef struct objc_object FCPerson;
typedef struct {} _objc_exc_FCPerson;
#endif
struct FCPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_fcName;
};
- 其中objc_object就是NSObject的底层写法,_fcName是我们添加的属性,NSObject_IMPL就是isa:
struct NSObject_IMPL {
Class isa;
};
- 在下面还可以看到FCPerson的setter和getter方法:
static NSString * _I_FCPerson_fcName(FCPerson * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_FCPerson$_fcName));
}
static void _I_FCPerson_setFcName_(FCPerson * self, SEL _cmd, NSString *fcName) {
(*(NSString **)((char *)self + OBJC_IVAR_$_FCPerson$_fcName)) = fcName;
}
setter和getter在内存中读取属性的原理是什么?(*(NSString **)((char *)self + OBJC_IVAR_$_FCPerson$_fcName))
是怎么找到fcName这个属性值的?
在取值时,系统只知道对象首地址,并不知道具体属性的地址信息。方法中(char *)self其实就是FCPerson对象的首地址,而OBJC_IVAR_$_FCPerson$_fcName
则是fcName属性距离首地址的偏移量,系统就是通过首地址+偏移量的方式进行属性的读写的。
接下来我们还可以看到很多类相关的结构体,method,protocol,ivar,class_ro_t,class_t
等等,我也会在以后的文章中为大家做详细介绍,到目前为止,我们已经知道了对象的本质是结构体。之前在介绍alloc流程时,提到过在
_class_createInstanceFromZone
中可以看到将isa与cls绑定的代码:
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
Isa解析
通过initIsa
进入到objc_object::initIsa
方法中,首先看到的就是isa的初始化方法isa_t newisa(0);
,具体看一下isa_t的内部实现,删除掉多余代码后:
union isa_t {
uintptr_t bits;
private:
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
可以看到isa_t是一个联合体(联合体中成员互斥,即bits,cls,struct{ISA_BITFIELD}只能是其中一种),依次分析三个成员变量:
苹果把isa根据需要进行了区分,苹果提出了TaggedPointer和NonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。
对于
NSDate
、NSNumber
这样的小对象存储的值,绝大多数情况并不会大于20亿这个量级。如果采用指针、堆内存的方式,那势必会造成内存的浪费和性能损耗。苹果采用将value值直接存储在isa_t
中的uintptr_t bits;
上,并且用一些特殊标识来标明此isa是TaggedPoint类型的。这样用isa就存储了值,而不需要在堆上分配内存再去存储值。要知道堆内存的分配、释放及访问,要比栈内存慢很多的。在
objc_object::initIsa
方法中,可以看到如果不是nonpointer则会直接进行cls赋值newisa.setClass(cls, this);
-
struct{ISA_BITFIELD}
中ISA_BITFIELD
是一个宏定义:
在objc_object::initIsa
方法中,如果是nonpointer的话,则会依次对ISA_BITFIELD中的属性进行赋值:
#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;
因此,如果是nonPointerIsa的话,isa已经不再是一个单纯只想cls的指针了,携带了很多附加信息。
遗留问题
通过探究源码得知,initIsa方法写死的创建非nonPointerIsa, initInstanceIsa和initClass创建的是nonPointerIsa,具体原因以后研究完会继续补充~