前言
我们经常说,OC是一个面向对象的编程语言,对象
就是我们整个编写代码的过程中,最为频繁接触到的一个东西,那么什么是对象呢?在上一篇文章iOS探索底层-结构体内存对齐中,我提到过一点,对象的本质就是结构体。那么这个结论对不对呢,我们今天就来探索一下。
探索对象的底层
我们都知道Objcet-C是一种在C的基础上加入面向对象特性扩充而成的编程语言,它的底层实际上是C/C++的代码。在C/C++中是没有对象这个概念的,因此OC中的对象在C/C++中一定会转换成一个C/C++中的存在的东西,我们可以通过这个线索来进行探索。
探索前的准备
我们首先来准备一个对象DMPerson
,来开始我们的探索历程。然后在我们的main.m
中,初始化它
转换成C/C++代码
通过clang轻量级编译器的代码还原,我们可以将OC的代码,转换成C/C++代码查看OC的代码的底层结构。
拓展:
Clang是一个由
Apple
主导,以C++
编写、基于LLVM
、 发布于LLVM BSD
许可证下的C
/C++
/Objective-C
/Objective-C++
编译器。
通过下面的命令,我们可以将main.m文件,转换成C/C++的代码
//模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
//真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
打开main.app
文件后,可以看到里面是一大串的C/C++
的底层代码,在其中搜索我们的DMPerson
类,可以看到如下代码
通过结构体中的成员变量_dmName
和_dmAge
我们可以确认,这个就是我们要找的DMPerson
在C/C++中的底层实现,再仔细看下,没错,他就是一个struct
也就是结构体
。到此,我们就可以得出我们一开始的结论:
结论一:对象在底层的本质就是结构体 在上面的
C/C++
代码的分析过程中,我们会很明显的发现一个问题 在我们定义的DMPerson
类中,只含有dmName
和dmAge
两个属性。但是在
C/C++
的底层代码中,我们的DMPerson_IMPL
结构体中,却含有三个成员变量,除了dmName
和dmAge
之外,还有一个NSObject_IMPL
结构体类型的NSOBject_IVARS
成员变量。
从他们的名字我们可以猜想,NSObject_IMPL
是不是就是OC
中NSObject
的底层实现,而这种结构体内嵌套结构体
的方式,就是类似我们OC
中继承
的实现呢。下面我们来继续探索
首先找到NSObject_IMPL
这个结构体的实现
在这里可以看到,NSObject
实际上是objc_object
,再来寻找下objc_object
的实现
看到这是不是觉得有点似曾相识,打开OC
代码中NSObjcet.h
我们可以看到如下
它就是我们要找的NSObject
,我们可以得出两个结论:
结论二:
NSObject
在C/C++的底层中本质是objc_object
结构体。结论三: 而
OC
中的继承,在底层中是使用结构体嵌套
的方式进行实现的。
isa分析
在上面的代码里,我们可以看到NSObject
里面只有一个成员变量,那就是Class
类型的isa
。那么这isa
是什么呢,我们就这个问题,继续探索下去,首先我们先看看他的类型Class
在底层中的定义。
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到,Class
实际上就是一个objc_class *
类型的结构体指针。
拓展
同样我们常用的
id
类型,也可以在这里找到定义
typedef struct objc_object *id;
它是一个
objc_object *
类型的结构体指针,因此可以指向任何实例
接下来我们来看看isa
,这个东西实际上我们在之前iOS - 探索底层alloc流程
的文章里有接触到过。在alloc
流程的最后一步,就是通过initIsa
方法将我们申请的内存地址和我们的Class
绑定起来。
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;
}
isa = newisa;
}
在这些代码中间,有个非常重要的东西,就是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);
};
联合体和位域
联合体
在上面的代码中,我们可以看到一个之前没有接触过的结构union
,我们称之为联合体,那么他到底是什么,有什么特性呢,我们用下面这个例子来说明,首先看一段代码
struct DMStudent1 {
char *name;
int age;
double height ;
};
union DMStudent2 {
char *name;
int age;
double height ;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct DMStudent1 student1;
student1.name = "mantou";
student1.age = 15;
student1.height = 190.1;
union DMStudent2 student2;
student2.name = "mantou";
student2.age = 15;
student2.height = 190.1;
}
return 0;
}
逐行打印对应的student1
和student2
所存储的内容可以看到如下结果
每次对student1
进行赋值的时候,所赋予的数据全部都会存储下来。而每次对student2
进行赋值以后,我们每次可以正确访问的数据,永远是最后一次赋值的数据,其他的数据都可以理解为是脏数据,没有任何意义。因此我们可以这么说
结构体(
sturct
)中的所有变量是“共存”的优点:海纳百川,有容乃大。只要你来,我都给你存下来
缺点:内存空间的分配是粗放的,不管你用不用全都给你分配好位置
联合体(
union
)中每个变量之间是“互斥”的优点:就是不够“包容”
缺点:使用内存更为精细灵活,也节省了内存空间
位域
说完联合体,我们再来看看另外一个知识点位域
。我们可以看到isa_t
这个联合体
中含有一个结构体,而结构里里面是一个叫做ISA_BITFIELD
的成员,他的定义如下
// arm64位架构为例
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
在这里,就运用到了位域
这个知识,我们来举个🌰
@interface DMPerson : NSObject
@property (nonatomic, assign) BOOL fat;
@property (nonatomic, assign) BOOL rich;
@property (nonatomic, assign) BOOL handsome;
@end
#import "DMPerson.h"
@implementation DMPerson
{
struct {
char fat : 1; //是否胖
char rich : 1; //是否有钱
char handsome : 1; //是否帅
}myself;
}
- (void)setFat:(BOOL)fat {
myself.fat = fat;
}
- (void)setRich:(BOOL)rich {
myself.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
myself.handsome = handsome;
}
- (BOOL)fat {
BOOL ret = myself.fat;
return ret;
}
- (BOOL)rich {
BOOL ret = myself.rich;
return ret;
}
- (BOOL)handsome {
BOOL ret = myself.handsome;
return ret;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
DMPerson *p = [[DMPerson alloc] init];
p.fat = YES;
p.rich = NO;
p.handsome = YES;
NSLog(@"fat : %d,rich : %d,handsome : %d",p.fat, p.rich, p.handsome);
}
return 0;
}
我们通过断点,来查看我们的 myself 结构体的值
再来看看结构体的具体内容
是不是与我们设置的数据一样,这就是所谓的位域
,正常来说,一个这样的结构体需要占用3个字节
来表示所存储的数据,但是当使用了位域了以后,我们只需要3位
也就是1个字节
就能够把内容给存储下来。因此位域
的作用,也是为了让内存更加优化
。
NonpointerIsa&TaggedPoint
说完位域,我们继续回过头来看刚刚的代码
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
……
对位域的一系列赋值
……
newisa.setClass(cls, this);
}
isa = newisa;
我们可以看到,当nonpointer
为0
的时候,直接绑定类和地址的对应关系,我们称这种为TaggedPoint。而当nonpointer
为1
的时候,除了保存类的信息以外,还会保存一些额外的特殊信息,我们称之为NonpointerIsa。
在2013年9月,苹果推出了iPhone5s配备了 64 位架构的处理器。64位CPU下,指针所占用的位数为8个字节64位。一个内存地址实际上用不了64位去存储,一般32位即可存储一个20亿的数(2^31=2147483648,另外1位作为符号位)。所以,苹果把isa根据需要进行了区分,苹果提出了
TaggedPointer
和NonpointerIsa
。对于小对象采用TaggedPointet
方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa
来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。
TaggedPointer
对于NSDate
、NSNumber
这样的小对象存储的值,绝大多数情况并不会大于20亿这个量级。如果采用指针、堆内存的方式,那势必会造成内存的浪费和性能损耗。苹果采用将value
值直接存储在isa_t
中的uintptr_t bits
上,并且用一些特殊标识来标明此isa
是TaggedPoint
类型的。这样用isa
就存储了值,而不需要在堆上分配内存再去存储值。要知道堆内存的分配、释放及访问,要比栈内存慢很多的。
NonpointerIsa
isa其实并不单单是一个指针,以arm64架构为例,实际上只有33位用于存储对象地址。其余位用来存储一些特殊的值。
uintptr_t nonpointer : 1; //标识是否为nonpointer
uintptr_t has_assoc : 1; //是否有关联对象
uintptr_t has_cxx_dtor : 1; //该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
uintptr_t shiftcls : 33; //对象地址
uintptr_t magic : 6; //⽤于调试器判断当前对象是真的对象还是没有初始化的空间
uintptr_t weakly_referenced : 1; //志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放
uintptr_t deallocating : 1; //是否正在释放
uintptr_t has_sidetable_rc : 1; //实际使用过程中,uintptr_t的19位已经足够储存,完全用不到这个特殊位置
uintptr_t extra_rc : 19 //当表示该对象的引⽤计数值,实际上是引⽤计数值减 1
isa还原类信息
通过掩码来还原类信息
在查看ISA_BITFIELD
定义的时候,我们还看到了一个有意思的东西,就是ISA_MASK
。那么这个ISA_MASK
是什么东西呢?我们称之为掩码
。我们知道NonpointerIsa
中除了含有类信息,还含有一些别的特殊信息,掩码
就是为我们屏蔽其他的特殊信息,直接找到类信息的直接方法,你可以理解为下图这样
同样我们举个🌰
int main(int argc, const char * argv[]) {
@autoreleasepool {
DMPerson *p = [DMPerson alloc];
NSLog(@"%@",p);
}
return 0;
}
还是上面的DMPerson
这个类,我们对他进行alloc
方法的调用,然后打上断点,打印出相信的信息
我们发现,当我们获取到的isa的值与掩码按位与
之后,出来的结果就是我们类的信息。
通过位运算来还原类信息
由于测试机是x86_64的架构,因此我先把x86_64架构的NonpointerIsa
结构贴出来
# 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
我们分析一下,我们需要寻找的类信息,在shiftcls
这个位置存储,在他的前面有3位
是特殊的存储位,而在他后面有6+1+1+1+8=17
位特殊存储位。我们想到得到shiftcls
位置存储的类信息,那么需要把其他的特殊信息位给剔除掉,这样就很简单了
用图来说明的话,大概就是下面这样
总结
我们通过clang将OC代码转换成底层的C/C++的代码,从而确认了:
- 对象在底层的本质就是结构体
NSObject
在C/C++的底层中本质是objc_object
结构体。- 而
OC
中的继承,在底层中是使用结构体嵌套
的方式进行实现的。
接着我们又从底层代码中,找到了isa的定义,从而引申出了联合体(union)
和位域
的概念;
接着我们了解到了NonpointerIsa
和TaggedPoint
的不同;
最后我们通过两种不同的方式来逆向还原了从isa到类的过程。