OC底层原理八:剖析isa & clang的使用

OC底层原理 学习大纲

对象的本质

1. Clang探索

  • Clang 是一个由Apple主导编写,基于LLVMC/C++/Objective-C轻量级编译器。源代码发布于LLVM BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
  • 它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC

2. 操作指令:

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

3. 探索

  • 构建测试代码
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 打开终端cdmain.m的文件夹。

  • 输入clang指令: clang -rewrite-objc main.m -o main.cpp 将objc语言的main.m文件重写为cpp格式的文件

    image.png

  • 忽略warnings警告,查看main.m文件夹。发现已生成main.cpp文件
    cpp的意思是 c plus plus

    image.png

  • 打开main.cpp文件。 内容很长。我们搜索自定义类HTPerson

image.png
  • 可以发现,对象在底层已经编译成struct结构体
  • _I_HTPerson_name是属性name的get方法
  • _I_HTPerson_setName_是属性name的set方法。 set方法内调用objc_setProperty方法。

同时我们发现,HTPerson_IMPL struct 中有一个NSObject_IMPL struct。 这就是表明HTPerson继承自NSObject

不相信?

我们在main.m中创建一个HTCar类继承自HTPerson

@interface HTCar : HTPerson
@end

@implementation HTCar
@end

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        
        NSLog(@"%@", person);
    }
    return 0;
}

使用clangmain.m编译为main.cpp

clang -rewrite-objc main.m -o main.cpp

main.cpp中搜索HTCar:

image.png

服不服?

结论

  • OC结构体没有继承关系,但是CC++语言中,结构体是有继承关系的。

  • OC对象在底层被C语言编程成结构体。而C语言结构体的继承方式,是每一个结构体第一个属性都包含父结构体的所有信息。如此,实现了OC类的继承关系

  • OC对象的本质就是结构体,结构体中包含了所有属性方法和父类信息

4. 探究属性get、set方法

  • 对象核心功能就是信息的存取,即getset方法。

  • 我们没有独立实现getset方法,工程中也找不到属性的getset方法,但这2个方法为什么可以直接使用呢?

其实上面已经有答案了。对象在底层将属性都进行了记录。并自动实现了他们的getset方法。

image.png
  • get方法我们看懂,就是直接访问指针地址,返回指针地址的值

  • set方法调用了objc_setProperty函数进行赋值。

我们打开objc4源码。搜索objc_setProperty

image.png
  • 点击进入内部,发现所有objc_setProperty方法都调用了reallySetProperty

    image.png

  • 所有值变更(set),都是reallySetProperty在处理。(同时管理引用计数)

image.png

所有外层属性的set方法。都会来到objc_setProperty方法,调用了reallySetProperty实现set功能。

  • 外部set方法: 个性化定制层(例如setName、setAge等)
  • objc_setProperty:接口隔离层 (将外界信息转化为对内存地址和值的操作)
  • reallySetProperty:底层实现层 (赋值和内存管理)

这是一个典型的封装设计思维。


isa

  • oc对象的本质是结构体,我们在main.cpp的文件中了解到。HTPerson_IMPL继承自NSObject_IMPL

  • 我们搜索struct NSObject_IMPL

image.png
  • 发现NSObject对象在底层就是编译为 isa, 其类型为Class

到这里,我们可以肯定对象在底层,是通过继承isa来继承父类信息

  • 在OC底层原理三:探索alloc (你好,alloc大佬 )中,我们知道了alloc的三大核心函数,包含initInstanceIsa创建isa并完成类的关联

现在,让我们揭开isa的神秘面纱。

1. union联合体位域

首先了解union联合体位域,isa的类型结构就是union。

小案例
如果我们创建Car对象,我们需要控制它的前后左右4个方向。我们可以这样定义:

@interface Car : NSObject

@property(nonatomic, assign) BOOL front;   // 2字节
@property(nonatomic, assign) BOOL back;   // 2字节
@property(nonatomic, assign) BOOL left;    // 2字节
@property(nonatomic, assign) BOOL right;    // 2字节

@end
  • BOOL类型占用2字节, 每个字节是8位(Bit)。
  • Car对象所有属性占用内存大小: 4属性 * 2字节 * 8位 = 64位

系统层面,我们会考虑极致的性能。用4位就实现前后左右的处理,每1位记录一个方向的信息。极大的节约内存空间

image.png

  • 2位更节省,每1位可记录2个信息.
  • 但使用4位存储,每1位独立记录1个信息。可以使用位运算来高效处理,在性能上更有优势)
    image.png

这就是我们要介绍的union联合体位域。

image.png
  • 结构体的类型大小大于等于内部所有变量的类型大小总和(参考结构体内存优化)
  • 联合体类型大小等于最大成员类型大小
  • 位域: 每一个二进制位均表示不同信息

2. isa结构

我们在objc4源码中找到initIsa

image.png

发现isa的赋值是isa_t结构,进入查看:

image.png

发现isa_t就是使用的union联合体结构。

通常来说,isa指针占用内存大小是8字节,即64位。对于系统来说已经足够了。

  • 我们知道union联合体内部属性是互斥关系。 所以clsbits不共存。

进入ISA_BITFILED宏定义,可以看到isa全部结构。 庐山真面目揭开了。

isa结构图
  • nonpointer: 表示是否对 isa 指针开启 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等

  • has_assoc: 关联对象标志位,0没有,1存在

  • has_cxx_dtor: 该对象是否有 C++Objc的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象

  • magic:用于调试器判断当前对象是真对象还是未初始化的空间

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

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

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

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。
    如: 如果对象的引用计数为 10,那么extra_rc 为 9。如果引用计数大于 10, 则需要使用到has_sidetable_rc

  • isa中最重要的是shiftcls,它存储了类指针的值。但是除了这个信息,isa还存储了很多其他标志性信息。

现在我们了解了isa的结构,让我们运行objc4源码来完整了解信息

3. 检验isa

  • main.m中测试代码,在HTPerson初始化一行加入断点
#import "HTPerson.h"

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        
        NSLog(@"%@", person);
    }
    return 0;
}

进入alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone-> _class_createInstanceFromZone,加入断点:

image.png

继续进入initInstanceIsa->initIsa,加断点:

image.png

需要确保clsHTPerson

SUPPORT_INDEXED_ISA宏定义:

image.png

我们现在是电脑端,所以是这个条件为false,宏的值为0

  • isa_t是上面讲到的isa默认初始化的方法

  • 所以isa_t newisa(0)后,newisa已经完成默认初始化,但还未赋值,我们打印p newisa

    image.png

  • 进入ISA_MAGIC_VALUE宏,看到

    image.png

  • 继续断点,打印:


    image.png
  • 为何magci是59?
    打开计算器,将显示改为编程器,选择16进制,粘贴cls的地址

    image.png

我们看到1 1101 1

image

  • 将计算器改为10进制,输入59,看二进制结果:
    image.png

这个59是在默认值中设定的。

  • 接着往下走,断点设在shiftcls后一行,打印:

    image.png

  • 此时,我们已将HTPerson类信息完整的存到isashiftcls中。isaHTPerson完成绑定。

为何要右移3位?
因为 (uintptr_t)cls是将cls初始化为uintptr_t格式。但是初始化时,前3位是标记符,shiftcls是从第四位才开始。所以要移除前三位。

image.png

我回到上一层_class_createInstanceFromZone,加断点。继续走。

image.png

  • 打印 objx/4gx查看地址信息。 首地址就是isa
  • 我们取isa地址。按照isa的初始化格式,我们&mask偏移值(查上面isa结构图)。就得到了shiftcls
  • shiftcls存储的就是类信息。 所以直接打印出了类信息。
    image.png
  • 当我们对isa的结构完全熟悉后。就能理解为什么首地址符有时候打印不出类名了

  • 因为标记符可能存在数据,影响了地址的读取。类的信息只存储在isa的shiftcls中。

  • 我们可以手动左移右移,将前3后17位置的信息全部移除。这样就可以直接读取了。

image.png

拓展

runtime运行时object_getClass(perosn)返回的也是isa地址。

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

梳理了一份 【对象、类、isa 的逻辑关系】

拓展答疑:

  • 属性修饰符strongweakretaincopyassign:
    clang编译文件,打开cpp文件,可以发现:
  1. retaincopy都是调用了objc_setProperty。 不同的是objc_setProperty内部实现不同
    (详看objc4源码中的objc_setProperty代码)
  • copymutableCopy:是新开辟空间,旧值release;
  • 其他修饰类型:是新值retain,旧值release。
  1. strongassign类型都是直接使用地址进行赋值(通过对象地址偏移相应字节找到属性地址)

  2. 如果在set方法后加入断点,可以在汇编层看到所有属性赋值后,会调用objc_storeStrong

image.png

image.png

运行代码,进入断点,可以看到:
image.png

(在所有赋值完成后,objc_storeStrong在最后执行一次)

  • objc4源码中查看objc_storeStrong代码。可以发现它内部就是对对象进行了retainrelease
    image.png

下一节:OC底层原理九:类的原理分析

你可能感兴趣的:(OC底层原理八:剖析isa & clang的使用)