前言
我们之前写到了对象的创建流程,以及计算对象内存的大小,那么究竟什么是对象呢?接下来我们去分析
一、编译成C++文件
我们知道,OC
在编译器作用下,最终会变成C/C++
代码,进而转成汇编
,最后才会生成可以识别的二进制代码
,因此我们可以通过C/C++
来研究它的底层。下面我们用两种方法来将OC代码
转成C++
。
1. clang
clang
是由Apple
主导编写,基于LLVM
的C/C++/Objective-C
编译器。- 将代码转成
C++
需要以下步骤(这里是将main.m
转成main.cpp
):- 首先打开终端
进入到要转换代码的文件
- 然后执行以下代码
- 首先打开终端
clang -rewrite-objc main.m -o main.cpp
复制代码
如果出现如下错误(找不到UIKit/UIKit.h
):
main.m:8:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~~~~
1 error generated.
复制代码
可以将第二步换成:
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk main.m
复制代码
备注1:-o
是输出的意思,输出名称.cpp
,这里是将main.m
输出成main.cpp
备注2:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk
这个路径是自己电脑中iPhoneSimulator
的路径,版本号需要根据自己电脑中真实的版本进行修改。
2. xcrun
- 在安装
XCode
的时候顺带安装了xcrun
命令,xcrun
命令在clang
的基础上进行了一些封装,更好用一些 - 将代码转成
C++
的步骤和clang
一样,命令不同,如下:
在模拟器中:
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
复制代码
二、class的结构
在上述步骤中,我们拿到了C++
文件,接下来我们进行分析:
- 在
main.cpp
文件中搜索WSPerson
(在main.m
中我们定义了一个WSPerson
的类),我们得到了一个结构体:
struct WSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
复制代码
我们再在WSPerson
中定义一个wsName
的属性,再编译成C++
代码,再看这个结构体:
struct WSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_wsName;
};
复制代码
这里也出现了个wsName
,所以 对象的本质就是是结构体 。
再来看下NSObject_IVARS
,这个成员变量是个结构体,那么这个结构体又是什么呢?我们搜索下看看:
struct NSObject_IMPL {
Class isa;
};
复制代码
得出结论:NSObject_IVARS
就是成员变量isa
- 在
WSPerson_IMPL
上面,我们注意到有个objc_object
:
typedef struct objc_object WSPerson;
复制代码
可以看出WSPerson
是继承objc_object
类型,我们知道在OC
中,类都是继承NSObject
,实质的更底层中,都是继承objc_object
。
- 那么我们再来看看
Class
:
typedef struct objc_class *Class;
复制代码
这里的Class
是一个结构体指针,在Class
下面有个id
:
typedef struct objc_object *id;
复制代码
也是一个 结构体指针,此刻有个疑问就引刃而解了:
id person
为什么没有*
?,因为它本身就是个指针
。
- 我们再来看看
wsName
参数:
extern "C" unsigned long int OBJC_IVAR_$_WSPerson$_wsName __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct WSPerson, _wsName);
// get
static NSString * _I_WSPerson_wsName(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_wsName)); }
// set
static void _I_WSPerson_setWsName_(WSPerson * self, SEL _cmd, NSString *wsName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_wsName)) = wsName; }
复制代码
我们看到这两个函数,实质分别是get
和set
方法,在两个方法中都有self
,和 _cmd
,这是函数的隐藏参数。
- 那么我们是怎么拿到参数的呢?
- 首先:
(char *)self
是WSPerson
的指针 OBJC_IVAR_$_WSPerson$_wsName
是offset
偏移- 然后强转得到
wsName
- 首先:
- 图解:
我们通过拿到WSPerson
的 首地址,然后通过属性的偏移值offset
,去拿到对应的属性。
三、位域和联合体
在分析isa
之前,我们先来了解下位域
和联合体
1. 位域
我们先来看个例子:
struct struct1 {
BOOL top;
BOOL left;
BOOL bottom;
BOOL right;
}s1;
复制代码
- 根据上一篇内存对齐,我们可以得到这个
struct1
占用的内存为4字节
,但实质上需要4字节
吗?我们知道BOOL
的值是0或1
,而二进制也是0或1
,用4字节
存储造成比较大的浪费,实质可以这样,如图所示:
// 4字节 = 4 * 8bit = 32 位
00000000 00000000 00000000 00001111
复制代码
我们只需要4位
就能满足struct1
的功能,4位
是半字节,我们至少也是1字节
,那怎么让struct1
占用1字节
呢?位域
就能实现
位域概念:所谓位域就是把一个字节中的
二进位
划分为几个不同的区域,并说明每个区域的位数。 每个域有一个域名,允许在程序中按域名进行操作,这样就可以把几个不同的对象用一个字节的二进制
位域来表示。位域是C语言一种数据结构
。
- 根据概念也就是说,需要将上述结构体中的成员变量
指定位数
,我们来验证下,定义这样一样结构体struct2
:
struct struct2 {
BOOL top: 1;
BOOL left: 1;
BOOL bottom: 1;
BOOL right: 1;
}s2;
复制代码
然后打印二者的size
:
总结:位域
能够通过指定成员变量的位数
来进行内存优化。
2. 联合体(union
)
联合体概念:
union
又称联合体、共用体
,在某种程度上类似struct
的一种数据结构,union
和struct
同样可以包含很多种数据类型和变量,区别也挺明显:
union
和struct
区别:
struct
:struct
中所有的成员是共存
的,优点是有容乃大
,比较全面。缺点是struct
内存空间的分配是粗放
的,不管⽤不⽤,全分配
。union
:union
中是各成员是互斥的
,缺点是不够包容
。但优点是内存使用更为精细
,联合体的成员共用一块内存空间
,这样也节省了内存空间
。
我们来用代码展示下:
// 结构体
struct LGTeacher1 {
char name;
int age;
double weight;
}t1;
// 联合体
union LGTeacher2 {
char name;
double weight;
int age;
}t2;
// 分别赋值
t1.name = 'K';
t1.age = 69;
t1.weight = 180;
t2.name = 'C';
t2.age = 69;
t2.weight = 179.9;
复制代码
然后我们用p
命令,分别打印下
可以看到结构体LGTeacher1
的信息是显示正常,但联合体LGTeacher2
显示的有些异常,我们用p/t
(p/t
是打印二进制信息)来打印下t2
看看:
我们用图来分析下这个结构:
- 赋值的顺序是
name
,age
,weight
, 可以看到weight
的数据覆盖了age
和name
的数据。说明最后赋的值会影响前面赋的值,也体现了联合体不包容
的特性。
isa_t
四、isa的结构
接下来我们来分析isa
的结构。在之前alloc流程中,我们分析了对象的创建,最后一个步骤initInstanceIsa
中创建的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);
};
复制代码
- 原来
isa_t
是个联合体,两个成员变量bits
和cls
公用一块内存空间,也就是互斥
,当第一种isa_t() { }
初始化时,cls
没有默认值,而第二种isa_t(uintptr_t value) : bits(value) { }
初始化时,cls会有值
。 - 在
objc4-818.2
的源码中,是用第二种方法创建的:
isa_t newisa(0)
复制代码
isa_t
还提供了个位域
,用来存储一些信息,这个成员是ISA_BITFIELD
,是一个宏定义
,有__arm64__
和__x86_64__
两种结构,这里分析下下__x86_64__
结构:
# 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; // 表示是否对isa指针开启指针优化,0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
uintptr_t has_assoc : 1; // 关联对象标志位,0没有,1存在
uintptr_t has_cxx_dtor : 1; // 该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ // 存储类指针的值。开启指针优化的情况下,在arm64架构中有33位⽤来存储类指针
uintptr_t magic : 6; // ⽤于调试器判断当前对象是真的对象还是没有初始化的空间
uintptr_t weakly_referenced : 1; // 对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放
uintptr_t unused : 1; // 是否使用
uintptr_t has_sidetable_rc : 1; // 当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
uintptr_t extra_rc : 8 // 当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
复制代码
可以看到isa
分为两种:
nonpointer
为0
:纯isa
指针nonpointer
为1
:不⽌是类对象地址,isa
中包含了类信息、对象的引⽤计数等。
我们用图在展示下ISA_BITFIELD分布
:
- 图中
nonpointer
在第0位
,has_assoc
在第1位
,has_cxx_dtor
在第2位
,shiftcls
在3~46位
,magic
在47~52位
,weakly_referenced
在第53位
,unused
在第54位
,has_sidetable_rc
在第55位
,extra_rc
在56~63
位。
从图中看,很明显shiftcls
是比较核心的数据,接下来我们来分析下
shiftcls
:
- 我们在
initIsa
中,在创建isa
和newisa.bit
赋值后打个断点得到:
- 如图,我们看到赋值前后的变化,因为
newisa
是联合体union
,所以bit
赋值后因为内存共用,其他的也有值,比较明显的是:cls
,nonpointer
和magic
。 - 我们打印下
cls
的二进制显示:
在位域ISA_BITFIELD
中,magic
在47~52位
,所以二进制 11 1011
转换成十进制
得到59
,同理nonpointer
在第0位
,得到nonpointer = 1
。
- 然后我们在
newisa.bit
赋值后,进入到setClass
方法,通过断点知道走了这一步:
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3;
#endif
复制代码
这一步,有个
右移3位的操作
,为什么要右移动三位?右移3位
的目的是为了减少内存消耗,因为类的指针需要按照8字节对齐
,也就是说类的指针的大小必定是8的倍数
,其二进制后三位为0
,右移三位抹除后面的3位0并不会产生影响。
- 在
右移3位
后,再打印newisa
:
(isa_t) $43 = {
bits = 8303516107965569
cls = LGPerson
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 536875152
magic = 59
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
复制代码
这里shiftcls
就有值了,那么我们还有其他方式查看isa
是否关联类
?我们接下来再去尝试。
验证isa
关联类:
1. 通过位与(&
)掩码ISA_MASK
:
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
复制代码
我们先拿到对象的isa
:
(lldb) x/4gx p1
0x100647de0: 0x011d800100008481 0x0000000000000000
0x100647df0: 0x0000000000000000 0x0000000000000000
// p1 是类LGPerson的对象
// 0x011d800100008481 就是 isa
复制代码
我们先打印下类LGPerson.class
的16进制
,然后打印isa
位与(&
) ISA_MASK
:
(lldb) p/x LGPerson.class
(Class) $61 = 0x0000000100008480 LGPerson
(lldb) p/x 0x011d800100008481 & 0x00007ffffffffff8ULL
(unsigned long long) $60 = 0x0000000100008480
复制代码
发现位与
操作得到的16进制
和 LGPerson.class
的16进制
是一样的,我们通过这个方式得知isa
已经关联了LGPerson
。
2. 通过位移(>>
和<<
)操作:
在上述的ISA_BITFIELD分布
分布图中,我们明确知道shiftcls
在64位中的位置
,我们可以进行如下操作:
- 先右移
3位(>> 3)
,将nonpointer
,has_assoc
,has_cxx_dtor
抹0:
(lldb) p/x 0x011d800100008481 >> 3
(long) $63 = 0x0023b00020001090
(lldb)
复制代码
然后将$63
右移动20位(<< 20)
,将magic
,weakly_referenced
,unused
,has_sidetable_rc
,extra_rc
抹0:
(lldb) p/x $63 << 20
(long) $65 = 0x0002000109000000
复制代码
这样就得只剩下shiftcls
了,然后再将shiftcls
的位置还原,用$65右移17位(>> 17)
:
(lldb) p/x $65 >> 17
(long) $66 = 0x0000000100008480
(lldb) p/x LGPerson.class
(Class) $67 = 0x0000000100008480 LGPerson
复制代码
整个过程用图解分析更直观:
得到的结果和LGPerson.class
的16进制
也是一样的,也证明了isa
关联了LGPerson
类。