iOS底层-对象的本质

前言

我们之前写到了对象的创建流程,以及计算对象内存的大小,那么究竟什么是对象呢?接下来我们去分析

一、编译成C++文件

我们知道,OC在编译器作用下,最终会变成C/C++代码,进而转成汇编,最后才会生成可以识别的二进制代码,因此我们可以通过C/C++来研究它的底层。下面我们用两种方法来将OC代码转成C++

1. clang

  • clang是由Apple主导编写,基于LLVMC/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; }
复制代码

我们看到这两个函数,实质分别是getset方法,在两个方法中都有self,和 _cmd,这是函数的隐藏参数。

  • 那么我们是怎么拿到参数的呢?
    • 首先:(char *)selfWSPerson的指针
    • OBJC_IVAR_$_WSPerson$_wsNameoffset偏移
    • 然后强转得到wsName
  • 图解:

截屏2021-06-15 16.59.17.png
我们通过拿到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
截屏2021-06-15 22.06.04.png

总结位域能够通过指定成员变量的位数来进行内存优化。

2. 联合体(union)

联合体概念union又称联合体、共用体,在某种程度上类似struct的一种数据结构,unionstruct同样可以包含很多种数据类型和变量,区别也挺明显:

unionstruct区别:

  • structstruct中所有的成员是共存的,优点是有容乃大,比较全面。缺点是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命令,分别打印下

截屏2021-06-16 16.05.48.png
可以看到结构体LGTeacher1的信息是显示正常,但联合体LGTeacher2显示的有些异常,我们用p/t(p/t是打印二进制信息)来打印下t2看看:

截屏2021-06-16 16.24.42.png
我们用图来分析下这个结构:

截屏2021-06-16 17.10.11.png

  • 赋值的顺序是 nameageweight, 可以看到weight的数据覆盖了agename的数据。说明最后赋的值会影响前面赋的值,也体现了联合体不包容的特性。

isa_t

四、isa的结构

接下来我们来分析isa的结构。在之前alloc流程中,我们分析了对象的创建,最后一个步骤initInstanceIsa中创建的isaisa_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是个联合体,两个成员变量bitscls公用一块内存空间,也就是互斥,当第一种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分为两种:

  • nonpointer0:纯isa指针
  • nonpointer1:不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等。

我们用图在展示下ISA_BITFIELD分布

截屏2021-06-17 09.11.37.png

  • 图中nonpointer第0位has_assoc第1位has_cxx_dtor第2位shiftcls3~46位magic47~52位weakly_referenced第53位unused第54位has_sidetable_rc第55位extra_rc56~63位。

从图中看,很明显shiftcls是比较核心的数据,接下来我们来分析下

shiftcls

  • 我们在initIsa中,在创建isanewisa.bit赋值后打个断点得到:

截屏2021-06-17 09.54.26.png

  • 如图,我们看到赋值前后的变化,因为newisa联合体union,所以bit赋值后因为内存共用,其他的也有值,比较明显的是:clsnonpointermagic
  • 我们打印下cls的二进制显示:

截屏2021-06-17 10.07.35.png
在位域ISA_BITFIELD中,magic47~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.class16进制,然后打印isa位与(&) ISA_MASK

(lldb) p/x LGPerson.class
(Class) $61 = 0x0000000100008480 LGPerson

(lldb) p/x 0x011d800100008481 & 0x00007ffffffffff8ULL
(unsigned long long) $60 = 0x0000000100008480
复制代码

发现位与操作得到的16进制LGPerson.class16进制是一样的,我们通过这个方式得知isa已经关联了LGPerson

2. 通过位移(>><<)操作:

在上述的ISA_BITFIELD分布分布图中,我们明确知道shiftcls64位中的位置,我们可以进行如下操作:

  • 先右移3位(>> 3),将nonpointerhas_assochas_cxx_dtor抹0:
(lldb) p/x 0x011d800100008481 >> 3
(long) $63 = 0x0023b00020001090
(lldb) 
复制代码

然后将$63右移动20位(<< 20),将magicweakly_referencedunusedhas_sidetable_rcextra_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
复制代码

整个过程用图解分析更直观:

截屏2021-06-17 11.47.23.png
得到的结果和LGPerson.class16进制也是一样的,也证明了isa关联了LGPerson类。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享