文章目录
对象的底层结构
要了解OC对象的底层结构,那么我们就得知道:OC本质底层实现转化其实都是C/C++代码。
- 使用该指令将代码转换为C/C++代码。
clang -rewrite-objc main.m
对应文件目录下会生成main.cpp文件
对象的本质
- 对象类的本质是结构体
OC源文件
@interface TestPerson : NSObject
{
//成员变量
//@public
NSString* age; //4个字节
}
@property (nonatomic,copy) NSString *name; //属性
@end
@implementation TestPerson
@end
分析编译生成的main.cpp文件
#ifndef _REWRITER_typedef_TestPerson
#define _REWRITER_typedef_TestPerson
typedef struct objc_object TestPerson;
typedef struct {} _objc_exc_TestPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_TestPerson$_name;
struct TestPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *age;
NSString *_name;
};
// @property (nonatomic,copy) NSString *name;
/* @end */
// @implementation TestPerson
static NSString * _I_TestPerson_name(TestPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_TestPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_TestPerson_setName_(TestPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct TestPerson, _name), (id)name, 0, 1); }
// @end
通过这个TestPerson类我们发现:
- OC中的
TestPerson
类底层是struct TestPerson_IMPL
结构体。 - OC中
@interface TestPerson : NSObject
,TestPerson
继承NSObject
底层是typedef struct objc_object TestPerson
;这样体现的。 - 首先数入参
TestPerson * self,SEL _cmd
,所有OC方法都有这两个隐藏参数,所以我们可以在OC的方法中使用self指针,但是这两个参数对外是不显示的。 - 看到getter和setter里是通过首地址指针+对应成员变量的地址值指针的偏移量的方式取和存的,最终通过
(*(NSString **)
还原为string
类型。取值的过程就是:先拿到当前成员变量的地址,再去取这个地址里面所存的值。
那struct NSObject_IMPL;
具体实现是什么?
查找后发现
struct NSObject_IMPL {
Class isa;
};
此时发现其结构体内存储的是一个Class
类型的isa
指针。
TestPerson
这个类在底层编译成了TestPerson_IMP
结构体。- 成员
NSObjct_IVARS
,这是一个结构体,里面只包含了isa
。 - 成员
age
,是我们类里面的成员变量。 - 成员
_name
,是我们类里面的属性,编译成了带有_下划线的成员。 - 函数
_I_TestPerson_name
,实际上就是getter方法,包含两个默认参数self
、_cmd
。 - 函数
_I_TestPerson_setName_
,实际上就是setter方法,包含两个默认参数self
、_cmd
,与一个形参name
。
根据编译的结构我们可以得出下面结论:
- 对象的本质在底层就是一个
objc_object
结构体。 - 属性与成员变量的区别,属性是由成员变量+getter方法+setter方法组成。
类结构
objc_class
部分
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// 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
从上面的结构体,可以看出:类是一个结构体,里面存放了superClass
、cache
、bits
等
objc_class
继承于objc_object
,换句话说,类的本质也是个对象,其中isa
指针式其继承自objc_object
的,所以代码里才注释了// Class ISA;
isa
是结构体,其中会存储类的地址,其并不是指针,“isa指向”这句话严格来说是不正确的,但是方便理解
对象、类、父类、元类关系
类的定义
对象的本质是一个结构体,而id,是指向这个结构体的指针,也就是我们平时Person *p = [Person alloc] init];
的话,代表我们创建了一个Person
对象分配了地址,并且把他的地址给了p这个指针,我们是通过p来操作对象,或者说p代表了对象,但p并不是对象本身。而对象是存在于内存里的一个结构体。
元类的定义
其原理就是OC对象在发送消息时,运行时库会追寻着对象的
isa
指针得到对象所属的类。这个类包含了能应用于这个类的所有实例方法以及指向父类的指针,以便可以找到父类的实例方法。运行时库检查这个类和其父类的方法列表,找到与消息对应的方法。编译器会将消息转换为消息函数objc_msgSend
进行调用
同样我们也会有对类发送消息的情况:
NSString *testString = [NSString stringWithFormat:@"%d",3];
- 从此处我们可以知道,OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个
isa
指针,指向其所属的类。那么类的类是什么?就是我们所说的元类(MetaClass),所以,元类就是类的所属类。
所以从消息机制的层面来讲:
- 当你给对象发消息时,消息会寻找这个对象的类的方法列表
- 当你给类发消息时,消息是在寻找这个类的元类的方法列表
元类的类
元类的类(根类)
有一个问题,如果按类的类是元类这个逻辑理解,是否可以一直循环下去?
- 答案是肯定不行的,因为一定会有一个坐为最原始的那个类,那么这个类就是元类的类:所有的元类都使用根元类作为他们的类。根元类的
isa
指针指向了它自己。
关系图
使用最常见的这张图进行分析 instance
(实例) Subclass
(子类)
每一个实例变量的isa
都指向自己所属的类,每一个类的isa
都指向自己所属的元类,同时,父类的元类也是子类的元类的父类,父类的元类也是根元类的子类。而父类的元类和子类的元类都属于根元类。根元类的isa
指向自己,同时根元类的父类也是自己的下属类(NSObject
元类的父类是NSObject
类)。
isa走位
- 实例对象(Instance of Subclass)的
isa
指向类(class) - 类对象(class)的
isa
指向元类(Meta class) - 元类(Meta class)的
isa
指向根元类(Root metal class) - 根元类(Root metal class)的
isa
指向它自己本身,形成闭环,这里的根元类就是NSObject
superclass走位
类之间的继承关系:
- 类(subClass)继承自父类(superClass)
- 父类(superClass)继承自根类(RootClass),此时的根类是指NSObject
- 根类继承自nil,所以根类即
NSObject
可以理解为万物起源,即无中生有
元类也存在继承,元类之间的继承关系如下:
- 子类的元类(metal SubClass)继承自父类的元类(metal SuperClass)
- 父类的元类(metal SuperClass)继承自根元类(Root metal Class)
- 根元类(Root metal Class)继承于根类(Root class),此时的根类是指NSObject
isa详解
继承自objc_object
的isa
指针,不仅实例对象中有,类对象中也有,占8字节。
什么是isa?
- isa指针保存着指向类对象的内存地址,类对象全局只有一个,因此每个类创建出来的对象都会默认有一个isa属性,保存类对象的地址,也就是class,通过class就可以查询到这个对象的属性和方法,协议等;
isa分两种类型:
- 指针型
isa
:64位的0或者1的整体内容代表所指向的Class
的地址,也就是可以通过isa的内容来获得类对象的地址。 - 非指针型
isa
:isa
的值的部分代表Class
的地址,之所以这样是因为我们在寻址过程中,只有三四十位数就可以保证我们寻找到所有Class
地址了,多出来的位可以用来存储其他相关内容,来达到节省内存的目的。
每个OC对象都含有一个isa
指针,__arm64__之前,isa
仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa
进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。
此处我们主要研究__arm64__下共用体结构类型isa
isa指针,即isa_t结构体
union isa_t
{
Class cls;
uintptr_t bits;
//bits的结构体
struct {
uintptr_t nonpointer : 1;//->表示使用优化的isa指针
uintptr_t has_assoc : 1;//->是否包含关联对象
uintptr_t has_cxx_dtor : 1;//->是否设置了析构函数,如果没有,释放对象更快
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针
uintptr_t magic : 6;//->固定值,用于判断是否完成初始化
uintptr_t weakly_referenced : 1;//->对象是否被弱引用
uintptr_t deallocating : 1;//->对象是否正在销毁
uintptr_t has_sidetable_rc : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
uintptr_t extra_rc : 19; //->存储引用计数,实际的引用计数减一,存储的是其对象以外的引用计数
};
};
从isa_t
的定义中可以看出:提供了两个成员,cls
和bits
,由联合体的定义所知:这两个成员是互斥的,也就意味着,当初始化isa指针时, 有两种初始化方式
- 通过
cls
初始化,bits
无默认值 - 通过
bits
初始化,cls
有默认值
bits
的结构体里的变量:
nonpointer
:用来标记这个对象是不是tagpointer
类型的对象,因为iOS对oc对象进行了优化处理,有些对象是tagpointer
类型的,因此这些对象是没有isa
指针的,tagpointer
的内存一般是在栈中的,而不是在堆里面;tagpointer
对象一般是NSNumber
类型的数值较小的数,或NSString
类型的较小的字符串。has_assoc
:用来标记有没有关联对象。has_cxx_dtor
:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。shiftcls
:存储的isa
指针地址,也就是类对象的地址。magic
:用于调试器判断当前对象是真的对象还是没有初始化的空间。weakly_referenced
:对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。deallocating
:标志对象是否正在释放内存。has_sidetable_rc
:标记对象是否使用了Sidetable
,当对象引用计数大于10时,则需要借用该变量存储进位。extra_rc
:当表示该对象的引用计数值,实际上是引用计数值减1, 例如:如果对象的引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用到下面的has_sidetable_rc
。
isa的初始化过程
看过了isa_t
的底层结构,现在我们来看一下isa
的初始化过程,来探索isa_t
的底层实现,初始化过程发生在alloc
方法的底层流程中的这一步:
// 将类和指针做绑定
obj->initInstanceIsa(cls, hasCxxDtor);
此过程包含了 isa
的初始过程,要探究 isa
的底层实现,我们就从 isa 的创建开始:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
//下方函数调用就是isa的初始过程
initIsa(cls, true, hasCxxDtor);
}
initInstanceIsa
的源码实现中,主要是调用了initIsa
,继续跳到initIsa
的源码:
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
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;
}
我们可以看到方法内创建了一个 isa_t
类型的 newisa
实例, 做了赋值操作后,返回了 newisa
。
该源码解释了上面初始化isa的两种方式:
- 通过cls初始化:非
nonpointer
,存储着Class、Meta-Class对象的内存地址信息。 - 通过bits初始化:
nonpointer
,进行一系列的初始化操作。
isa初始化流程图
class方法
首先Class这里需要注意几点
class ObjectClass = [[nsobject class]class];
返回的还是class对象,并不是meta-class对象。- (Class)class
,+(Class)class
返回的就是类对象- 元类对象和
class
内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的 - 类对象在内存中有且仅有一个对象 主要包括
isa
指针super Class
指针 类的属性信息 类的对象方法信息 类的协议信息 类的成员变量信息
//instance实例对象
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
//class对象,类对象
//objectClass1~5都是NSObject的类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
//元类对象(将类对象当作参数传入进去)
Class objectMetaClass = object_getClass([NSObject class]);
Class objectMetaClass2 = [[NSObject class] class];
//判断是不是元类对象
NSLog(@"instance - %p %p", object1, object2);
NSLog(@"class - %p %p %p %p %p %d", objectClass1,objectClass2, objectClass3, objectClass4, objectClass5, class_isMetaClass(objectClass3 ));
NSLog(@"mateClass - %p %p %d",objectMetaClass, objectMetaClass2, class_isMetaClass(objectMetaClass));
输出情况:
instance - 0x100511920 0x10050e840
class - 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0
mateClass - 0x7fff91da20f0 0x7fff91da2118 1
无论多少次class方法得到的都还是类函数。
我们再来看一下objc_class
的源码部分
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存 formerly cache pointer and vtable
class_data_bits_t bits; // 用于获取具体的类信息 class_rw_t * plus custom rr/alloc flagsflags
...
bits
里面存储了类的方法列表等等的信息
- 这里的
bits
是class_data_bits_t
类型的,上面objc_object
的isa_t
类型数据中也有一个uintptr_t
类型的bits
,但是这是两种结构。
我们先看bits
的数据结构 class_data_bits_t
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
}
其实最最重要的是其中的2个方法。data
和 safe_ro
,两个方法分别返回class_rw_t
和class_ro_t
。
- 这里
ro_t
的获取也是通过data
方法获取的,所有可以理解为ro_t
也在rw_t
之中。
内存的分配
获取内存大小
- 类的实例对象的大小(本质是对象中成员变量的大小)
函数返回对齐过的实例变量(传入类)
class_getInstanceSize
- 获得系统实际分配的内存大小(也存在内存对齐),
obj
指针所指向内存的大小
malloc_size((__bridge const void *)obj)
- 针对类型(
int/double/struct
)是一个运算符,计算类型的大小(编译时确定数值)
//例如
int 4字节/(对象)指针 8字节/bool 2字节
//某个结构体类型的大小
sizeof(struct Person_IMPL)
一个NSObject对象占用多少内存?
由刚才对对象本质的探索可知:
//NSOject本质
struct NSObject_IMPL {
Class isa;
};
首先通过分析对象类的本质得出其本质其实是结构体,由于NSObject
结构体中的isa
本质上是一个指针,在64位中指针占8个字节,而通过malloc
得出他的实际内存是16,剩下的8位没有用。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject* obj = [[NSObject alloc] init];
//类的实例对象的大小
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
//obj指针所指向内存的大小
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
}
return 0;
}
运行如下
系统分配了16字节给NSObject
对象(通过malloc_size
函数获得)
但NSObject
对象内部只使用了8字节的空间(64bit环境下,通过class_getInstanceSize
函数获得,返回内存对齐后成员变量的大小)
内存对齐规则
对齐系数
每个特定的平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。我们可以通过预编译命令#pragma pack(n),n=1、2、4、8、16
来改变这一系数,其中的n就是要指定的“对齐系数”。我们iOS编译器Xcode的对齐系数就是8。
对齐规则
- 数据成员对齐规则:(
Struct
或者Union
的数据成员)第一个数据成员放在偏移为0的位置。以后每个数据成员的位置为min(对齐系数,自身长度)的整数倍,下个位置不为本数据成员的整数倍位置的自动补齐。 - 数据成员为结构体:该数据成员的内最大长度的整数倍的位置开始存储。
- 整体对齐规则:数据成员按照1,2步骤对齐之后,其自身也要对齐,对齐原则是min(对齐系数,数据成员最大长度)的整数倍。
内存对齐在OC中的优点
有时候我们会思考为什么系统开辟的内存大小会大于我们申请的内存大小呢?按照8字节对齐的方式,申请的内存就可能已经存在多余的了。
- 按照8字节对齐方式,对象内部里面的成员内存地址是绝对安全的。
- 我们无法确定申请多余的字节就在对象与对象之间,有可能会出现在对象内存段的内部某个位置,这个时候就可能会出现两个对象内存段是挨着的情况,没有那么的安全。系统开辟空间采取16字节方式,保证对象的内存空间会更大,对象与对象之间更加的安全。