美文网首页iOS 底层原理 iOS 进阶之路
OC底层原理八:剖析isa & clang的使用

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

作者: markhetao | 来源:发表于2020-09-14 00:06 被阅读0次

    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来继承父类信息

    现在,让我们揭开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的使用

        本文链接:https://www.haomeiwen.com/subject/wyvnektx.html