今天我们来对OC对象的原理进行最后一篇文章的分析,在这里你讲了解到一下内容:
1、对象的底层本质
2、联合体位域
3、
isa
和Class
的关系4、isa 的Class 的赋值反过程(通过位运算得到
Class
地址)参考文章:C 位域
1、对象的底层本质
对象在底层的本质,实际上是一个结构体,这一点我们可以用C++辅助代码来看一下。还记不记得我们在探索Block
底层原理的时候,用到的指令clang -rewrite-objc XXX.m
。同样的,这里我们也将Person
类,转换成C++来看一下其底层到底是什么。
/******** Person.h ********/
#import
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
/******** Person.m ********/
#import "Person.h"
@implementation Person
@end
执行指令clang -rewrite-objc Person.m
我们得到Person.cpp
(注:在Person.cpp
文件中,查看Person,通常从最下面开始查找比较方便)。
我们在Person.cpp
文件中可以看到,Person
这个类在底层就是一个结构体,那么Person
类所创建的对象,在底层的数据结构就是一个结构体。
- Person_IMPL
Person_IMPL
中struct NSObject_IMPL NSObject_IVARS;
相当于结构体的继承,通过字面意思我们也可以知道,继承的是成员变量(Ivar),那么我们跟进去看一下:
可以看到是一个
isa
指针,这也就意味着,在OC中,每一个对象都会有一个isa
指针,因为这是系统帮我们自动完成的。
1.1 Class
这里的Class
又是什么呢?其实Class
就是一个结构体指针:
1.2 id
大家注意上图,我们还看到
typedef struct objc_object *id;
NSObject
在底层中是objc_object
;看到这里的id
,我突然想明白了一点,为什么我们在写代码的时候,用id
去修饰对象不会报错,比如id person
等等。就是因为id
本身就是一个指针。
1.3 set & get
Person
中name
的set & get
方法:
大家会发现,为什么在底层中
set & get
方法会有参数呢?这就是我们经常说的隐藏参数。
这里跟大家简单讲一下,大家注意看
OBJC_IVAR_$_Person$_name
是什么?它其实是一个unsigned long
:extern "C" unsigned long OBJC_IVAR_$_Person$_name;
这里的取值与赋值,都是对内存地址的操作,拿到Person
对象的首地址,偏移到name
所在的位置,再进行取值 或者 赋值
。
2 联合体位域
2.1 联合体(union)
首先我们来看一下什么是联合体:
union Teacher {
char *name;
int age;
double height ;
};
这就是一个联合体。
- 联合体(union)的特点是:各个变量之间是互斥的,也就是说只能给其中一个变量赋值;其优点是内存使用更为精细灵活,也节省了内存空间。
- 结构体(struct)的特点是:各个变量之间是共存的,也就是说可以同时给多个变量赋值;其缺点是内存空间使用更为粗放,不管用不用,内存权分配。
下面我们通过代码来看一下联合体:
2.2 位域
举个例子,我们有下面一个结构体:
struct Car1 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
此时Car1
的大小是4个字节:
此时就有一个问题,实际我们并不需要这么多内存空间,我们只需要一个字节就可以完整的表达Car1
的意思。
为了达到这个效果,这个时候我们只需要在结构体定义的时候,指定成员变量所占的二进制位数(Bit),这就是位域:
struct Car2 {
BOOL front;
BOOL back : 1;
BOOL left : 6;
BOOL right: 4;
};
:
用来限定成员变量占用的位数;front
没有限制,根据类型推算其站1个字节(Byte);back
,left
,right
被:
后面的数字限制,不能根据数据类型计算长度,其分别占用1(Bit)
,6(Bit)
,4(Bit)
的内存。
3 isa 与 Class之间的关联
3.1 isa_t
我们之前在分析alloc
流程的时候,在_class_createInstanceFromZone
中有这样一段代码:
跟进去:
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 = newisa;
; -
newisa
是isa_t
类型的。
那么我们跟进isa_t
,可以发现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);
};
我们通常在定义一个类对象的时候是不是这样定义的Person *p = [[Person alloc] init]
,其中对象p
也叫做指针;指针有8字节(Byte)
-- 8 * 8 = 64(bit 位)
,如果这64
只是存储一个指针地址就会产生浪费。
由于每个类都有一个isa
,于是就在isa
里面存储了一个类相关信息,比如:是否正在释放、引用计数、weak、关联对象、析构函数等等。
isa_t
是一个联合体,那么我们要确认其其中存储的内存,一般来说,我们要去看位域,于是我们在ISA_BITFIELD
中找到了这个:
这是一个宏,其含义如下(注:有两个不同环境下的宏,下面会做介绍):
变量名 | 值的含义 |
---|---|
nonpointer |
表示是否对isa 指针开启指针优化 。0 :纯isa 指针;1 :不只是类对象地址,isa 中包含了类信息,对象的引用计数等等。 |
has_assoc |
关联对象标志位 0 :没有;1 :存在。 |
has_cxx_dtor |
该对象是否有C++ 或者Objc 的析构器,如果有析构函数,则需要坐析构逻辑,如果没有,则可以更快的释放对象。 |
shiftcls |
存储类指针的值。开启指针优化的情况下,在arm64 架构中,有33位用于存储类指针。 |
magic |
用于调试器判断当前对象是真的对象,还是没有初始化的空间。 |
weakly_referenced |
用于表示对象是否被指向或曾将指向一个ARC的若变量,没有弱引用的对象可以更快释放。 |
deallocating |
标志对象是否正在释放内存。 |
has_sidetable_rc |
当对象引用计数大于10的时候,则需要家用该变量存储进位。 |
extra_rc |
表示该对象的引用计数数值,实际上是引用计数值减1。例如:如果对象的引用计数为10,那么extra_rc 为9;如果引用计数大于10,则需要用到上面的has_sidetable_rc 。 |
其中x86_64
和arm64
两种架构中,ISA_BITFIELD
的对比如下(图片来源isa与类关联的原理):
3.2 isa与Class地址的关系
我们怎么通过isa
指针,得到对应的类地址呢,注意上面有一个ISA_MASK
宏,我们用isa & ISA_MASK
就可以得到类地址,如下:
为什么要&
一下呢,因为Apple并不想让我们直接得到对应的值,也就是说,不想让值直接明文暴露出来。所以加了一个掩码来配合一下。
3.3 initIsa
在initIsa
中,如果nonpointer == 0(纯isa指针)
,那就isa = isa_t((uintptr_t)cls);
;否则就进行一系列bit
的赋值(位域的赋值):
4 isa 的Class 的赋值反过程
这里我们利用位运算来得到我们想要的Class地址。
我们首先来回顾一下上面讲到的x86_64
的ISA_BITFIELD
:
我们要找的就是shifcls
;而shifcls
右边是3个bit
,左边是17个bit
。那么我们先右移3位,再左移20位,最后右移17位就可以得到shifcls
。
具体操作如下: