iOS底层原理 03:OC对象初始化之本质与iSA

884 阅读8分钟

学习对象的本质,先引入工具Clang

一、Clang

1. 什么是Clang

Clang是一个C语言C++Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是一个由Apple主导编写,基于LLVMC/C++/Objective-C编译器

2013年4月,Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。

Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC

2. Clang 的使用

clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件

  • UIKit报错问题

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

  • xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了 一些封装,要更好用一些

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp (手机)

二、探究对象在底层的本质是否为结构体 ?

1. LGPerson对象分析

main.m 中自定义一个对象LGPerson,有一个属性KCName

截屏2021-10-03 下午4.37.52.png

进入main.m文件目录的终端,执行clang -rewrite-objc main.m -o main.cpp命令将main.m件编译成main.cpp文件。
打开编译好的 main.cpp 源码文件,找到LGPerson的定义,发现LGPerson在底层会被编译成 struct 结构体

截屏2021-10-03 下午5.04.22.png

截屏2021-10-03 下午5.01.30.png

结果分析:

  1. LGPerson的底层是结构体
  2. LGPerson_IMPL中的第一个属性 其实就是 isa,是继承自NSObject,属于伪继承,伪继承的方式是直接将NSObject结构体定义为LGPerson中的第一个属性,意味着LGPerson 拥有 NSObject中的所有成员变量
  3. LGPerson中的第一个属性 NSObject_IVARS 等效于 NSObject中的 isa

2. NSObject分析

截屏2021-10-03 下午5.18.16.png

源码分析:

  1. NSObject 同样指向 struct objc_object
  2. NSObject结构体中,有Class类型的成员变量isa

3. Class类型的底层本质分析

截屏2021-10-03 下午5.23.34.png

截屏2021-10-03 下午5.35.51.png

isa的类型为Class,被定义为指向 objc_class的结构体指针
开发中id来表示任意对象,原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针.

4. get/set方法

截屏2021-10-03 下午5.40.29.png

通过源码分析可发下,无论是getorset,都含有隐藏参数。获取当前值是是通过当前对象的首地址+ 变量的偏移值

总结

对象的本质是结构体
LGPersonisa是继承NSObject中的isa
NSObject中只有一个成员变量isa

三、联合体位域

学习isa之前,先了解联合体位域,为什么isa的类型isa_t是使用联合体定义

案列分析,引入一个结构体分析

struct LGCar1 {
    BOOL front; // 0 1
    BOOL back;
    BOOL left;
    BOOL right;
};

截屏2021-10-03 下午10.47.47.png

查看打印结果,当前结构体占用4个字节占用了32位,使用4个字节提现单一个功能,浪费了3倍空间.

改进如下:

// 位域
struct LGCar2 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};

截屏2021-10-03 下午10.51.35.png

改进代码使用了位域BOOL front: 1表示占用了1位,这样LGCar2只需要4位即可,整体1个字节就可以满足需求了,0000 1111

1. 结构体struct特点分析分析

截屏2021-10-03 下午11.21.44.png

当前的结构体占用了24个字节,结构体必须是内部最大 成员的整数倍,不足的要补⻬。

变量占用字节offsetmin位置
char *name80min(0, 8)0 ~ 7
int age48min(8, 4)8 ~ 11
double height816min(16, 8)16 ~ 24

开始阶段成员变量都是赋值为空的,执行完成,2个成员变量都能同时赋值的,处于共存状态。

2. 联合体union特点分析

// 联合体 : 互斥
union LGTeacher2 {
    char        *name;
    int         age;
    double      height ;
};

截屏2021-10-03 下午11.26.10.png

根据打印结果分析,联合体只能有一个被使用

第一次打印,都为 0 ,第二次打印,赋值了name字段,其他字段都不为0了,原因是其他字段不被使用了(脏数据、脏内存) 第三次打印,赋值了agename字段为空了,联合体只能有一个被使用

3.总结分析

结构体

结构体(struct)是指把不同的数据组合成一个整体,其变量共存的,变量不管是否使用,都会分配内存。

  • 优点:存储有容乃大包容性强,且成员之间不会相互影响

  • 缺点:struct内存空间的分配是粗放的,不管用不用,所有属性全分配内存。比较浪费,假设有4个BOOL成员,一共分配了4字节的内存,但是在使用时,你只使用了1字节,剩余的3字节就是属于内存的浪费

联合体

联合体(union)也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉

  • 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
  • 缺点:不够“包容”

两者的区别

  • 内存占用情况

    • 结构体的各个成员会占用不同的内存,互相之间没有影响
    • 共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
  • 内存分配大小

    • 结构体内存 >= 所有成员占用的内存总和(成员之间可能会有缝隙)
    • 共用体占用的内存等于最大的成员占用的内存

四、isa分析

1. isa的类型isa_t

以下就是isa指针的类型isa_t的定义,从定义中可以看出是通过联合体(union)定义的

union isa_t {  // 联合体
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    // 提供了cls和bits,两者是互斥关系
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t类型使用联合体的原因也是基于内存优化isa指针占用的内存大小是8字节

从isa_t定义中可看出:

  • 提供了两个成员变量,clsbits,由联合体的定义可知,这两个成员是互斥的,

  • 还提供了一个结构体定义的位域,结构体成员ISA_BITFIELD,是一个定义,有2个版本__arm64__(iOS移动端)和__x86_64__(macOS),如下是他们的宏定义,如下图所示

截屏2021-10-04 下午3.33.47.png

  • nonpointer: 表示是否对 isa 指针开启指针优化

    • 0: 纯isa指针
    • 1: 不止是类对象地址,isa 中包含了类信息、对象的引用计数等
  • has_assoc: 关联对象标志位

    • 0: 没有关联对象
    • 1: 存在关联对象
  • has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器

    • 如果析构函数,则需要做析构逻辑
    • 如果没有,则可以更快的释放对象
  • shiftcls: 存储类指针的值,既类的信息

    • arm64中占33位,开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
    • x86_64中占44位
  • magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间

  • weakly_referenced: 志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。

  • deallocating: 标志对象是否正在释放内存

  • has_sidetable_rc: 当对象引用技术大于 10 时,则需要借用该变量存储进位

  • extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1,

    • 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

原理探索

  • 探索路径 alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone,然后找到 initInstanceIsa进入其实现方法。

    inline void 
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
        ASSERT(!cls->instancesRequireRawIsa());
        ASSERT(hasCxxDtor == cls->hasCxxDtor());
    
        initIsa(cls, true, hasCxxDtor);
    }
    
  • 进入initIsa函数,主要初始化 isa指针

    截屏2021-10-04 下午4.14.51.png

2. isa 关联类探索

cls 与 isa 关联类的原理就是isa指针中的shiftcls存储了类信息,主要有以下几种验证方式

  • 方式①:通过initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3 验证
  • 方式②:isa&ISA_MASK
  • 方式③:isa位运算

方式①:通过initIsa方法

设置断点,调试程序。如下图所示:

截屏2021-10-04 下午6.55.54.png

isa_t为联合体,初始化nonpointer isacls属性为空,bits结构体初始化,默认都为0, 继续调试代码 到 ISA_MAGIC_VALUE后一步,可观察到第一位值位1

image.png

对isa开启指针优化,从47位到53位,可得出magic=59,表示当前对象初始化。通过计算得出59的二进制为0011 1011

截屏2021-10-04 下午7.14.53.png

继续执行代码,将类的地址 右移动3位,赋值给shiftcls,见下图。

image.png

方式②:isa&ISA_MASK

ISA_MASK是一个宏定义

  • __arm64__ISA_MASK宏定义的值为0x0000000ffffffff8ULL

  • __x86_64__ISA_MASK宏定义的值为0x00007ffffffffff8ULL

    截屏2021-10-04 下午8.13.39.png

    图中打印po 0x011d80010000820d & 0x0000000ffffffff8ULL(__arm64__)po 0x011d80010000820d & 0x00007ffffffffff8ULL(__x86_64__)结果都是LGPerson。说明isa已经关联了类

方式③:isa位运算

  • 类的信息存储在isa指针中,且isa中的shiftcls此时占44(__x86_64__),想要读取shiftcls44类信息,需进过位运算,位运算是目的是只保留shiftcls44位信息,其它位的信息都抹零,计算结果如下:

    截屏2021-10-04 下午8.49.44.png

  • isa位运算过程图

    截屏2021-10-04 下午8.58.27.png