前言
我们之前写到了对象的创建流程,以及计算对象内存的大小,那么究竟什么是对象呢?接下来我们去分析
一、编译成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类。
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
