美文网首页
iOS底层-对象的本质及isa原理

iOS底层-对象的本质及isa原理

作者: 忻凯同学 | 来源:发表于2021-06-17 22:44 被阅读0次

    前言

    通过分析 alloc原理内存对齐原理,只是了解了如何创建 对象alloc流程内存对齐 原则,却对 对象 的本质及 isa 知之甚少。下面具体理解一下对象的本质及isa的原理。

    基本知识

    位域

    • 产生:有些信息在存储时,并不需要占用一个 完整的字节,而只需占 一个几个 二进制位。例如在存放一个只有 01 两种状态时, 用 一个 二进位即可。基于这种原理,C语言提供了一种叫做 位域 的数据结构。

    • 定义:在定义结构体或联合体时,成员变量后面加 : 数字,用来限定成员变量占用的位数,这就是位域。

    • 目的:节省存储空间,处理简便。

    下面通过代码理解位域的原理。

    创建两个结构体,定义属性如下,一个使用位域前结构体,一个使用位域后结构体。

    打印其内存结果,如下:

    总结:

    • 没有位域 的结构体 s1 占用的内存空间大小为 4 字节;使用位域 的结构的大小为 1 字节。

    • $ p s3 可得 s3 的值:s3 = (a = 255, b = 255, c = NO, d = 255)

    • $ x/1bt &s3 可得内存中二进制数据为 0x00001011。低4位(从低到高)分别对应的是a、b、c、d的值;共占4个二进制位,再进行结构体的内存对齐,总大小为最大成员变量的整数倍,为 1 字节;

    联合体(union)

    联合体的语法和结构体非常类似,但是它们占用内存的情况却不同。

    联合体(union)和结构体(struct)的差异:
    • 结构体的成员之间是 共存 的:各个成员占用不同的内存,它们互相之间没有影响。
    • 联合体的成员之间是 互斥 的:所有成员共用同一段内存,修改一个成员的值,会影响其余所有成员。
    • 结构体占用的内存:大于等于 所有成员占用内存的总和(需要内存对齐)
    • 联合体占用的内存:等于最大 的成员占用的内存,同一时刻 只能 保存一个成员的值

    下面通过代码理解联合体(union)和结构体(struct)的差异。

    创建一个联合体

    对联合体进行赋值,并打印其内存情况:

    1. 没有赋值的情况:

    2. a 赋值的情况:

    3. b 赋值的情况:

    4. c 赋值的情况:

    总结:

    • 联合体可以定义多个不同类型的成员,联合体的内存大小由其中 最大的成员的大小 决定。

    • 联合体中 修改 其中的某个变量会 覆盖 其他变量的值。

    • 联合体所有的变量 公用 一块内存,变量之间 互斥

    • 优缺点:

      • 优点:节省内存。

      • 缺点:不够包容,同一时刻 只能 保存一个成员的值。

    对象的本质

    如何对对象底层进行探究?首先了解一波 编译器

    准备工作

    • 新建一个 Project
    • 创建如下文件,并添加属性

    编译器 Clang

    • Clang 是⼀个C语⾔、C++、Objective-C语⾔的 轻量级编译器
    • Clang 将⽀持其普通 lambda 表达式、返回类型的简化处理以及更好的处理 constexpr 关键字。
    • Clang 是⼀个由 Apple 主导编写,基于 LLVM 的C/C++/Objective-C编译器。

    把⽬标⽂件编译成c++⽂件

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

    如果遇到如下 UIKit 未找到的问题

    执行如下指令

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

    • /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk 为本地 sdk 路径

    • -runtime=ios-14.4:14.4为版本号,可在下面的路径拿到。

    结果如下:

    编译器 xcrun

    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 os-mainarm64.cpp

    模拟器编译如下:

    结果如下:

    真机编译如下:

    结果如下:

    main.cpp文件分析

    1. 分析对象属性

    从源码分析可得:

    • NSObject 的底层实现都是 objc_object 结构体类型。

    • struct ZLObject_IMPL: 对象的底层是结构体

    • 嵌套 NSObject_IMPL 结构体。即 对象 继承于 NSObject

    • 属性以 _ 开头的,代表成员属性。

    其中 NSObject_IVARS 是什么?并不知道。

    2. 分析 NSObject_IMPL

    NSObject_IMPL 里面只有一个成员变量是 Class isa

    3. 分析 Class

    main.cpp 文件中全局搜索 *Class。代码如下:

    从源码分析可得:

    • Class 的底层是 objc_class 类型的结构体指针。

    • objc_object 里面也是只有一个成员变量是 Class isa

    • 泛型指针id:常用的 id是一个 objc_object 结构体指针,这就为什么平时在使用 id 修饰变量时为什么不加 *

    • SELSEL 也是结构体指针。

    4. 分析 getset 方法

    从源码分析可得:

    • 属性的 getset 方法中通过获取当前对象的 首地址 + 变量的偏移值 来得到当前变量,进而实现 getset 对当前变量的修改和获取。

    • 定义的属性,底层自动添加了 getset 方法。get 方法名为 _I_类名_属性名set 方法名为 _I_类名_set属性名_,例如:_I_ZLObject_name_I_ZLObject_setName_

    • 隐含参数:当前对象 self,方法 _cmd

    isa的本质

    在分析 alloc原理 时,跳过了对 isa 的分析,下面具体分析 isa 的原理。

    回忆 alloc 流程,其流程图如下:

    其中 obj->initInstanceIsa 方法如下:

    最终都会执行 objc_object::initIsa 方法:

    这也印证了对象的底层就是 objc_object 结构体的观点,即 对象 在调用 initIsa 方法时,也是底层 objc_object 的结构体调用 initIsa方法。

    initIsa 的流程分析

    1. isa_t 分析

    总结:

    • isa_t 是一个联合体,联合体的成员变量存储是 互斥 的。

    • 有两个成员变量 bitscls,使用同一块内存。

    2. ISA_BITFIELD 分析

    其中 __has_feature(ptrauth_calls) 是什么呢?

    • __has_feature:此函数的功能是判断编译器是否支持某个功能
    • ptrauth_calls:指针身份验证,针对 arm64e 架构;使用 Apple A12 或更高版本 A 系列处理器的设备(如 iPhone XSiPhone XS MaxiPhone XR 或更新的设备)支持 arm64e 架构
    • 参考链接:developer.apple.com
    • 验证流程请参考 jr大神

    针对这三种类型,其 64 位存储分布图如下:

    遍历分析如下:

    • nonpointer:是否对 isa 指针开启指针优化。0 纯isa指针;1 不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等。
    • has_assoc:关联对象标志位,0 没有,1 存在
    • has_cxx_dtor:是否有 C++ 或者 Objc 的析构器,1 有析构函数,需要做析构逻辑;
      0 没有,则可以更快的释放对象。
    • shiftcls: 存储类指针的值。
    • magic:⽤于调试器判断当前对象是真的对象,还是没有初始化的空间。
    • weakly_referenced:是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
    • deallocating:是否正在释放内存。
    • has_sidetable_rc:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。
    • extra_rc:对象的引⽤计数值,实际上是引⽤计数值减 1。例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤ has_sidetable_rc 存储进位。

    3. 关联类的方式

    方式一:位运算 (此处以 __86_64__ 为例)

    类信息是存储在 isa 指针中,其中 shiftcls 是对应的对象信息。按位偏移是目的是只保留shiftcls 信息,其它位的信息 清0。最终 shiftcls 的相对位置要保持不变。如图:

    方式二:与运算 isa & ISA_MASK

    其中 ISA_MASK 为掩码,用于与isa指针地址与运算。值也是区分内核的:

    • __x86_64__ 内核下是 0x00007ffffffffff8

    • arm64e 内核和模拟器下是 0x007ffffffffffff8

    • arm64e 以外的其他 arm64 内核下是 0x0000000ffffffff8

    方式三:原生方法 setClass

    shiftcls = (uintptr_t)newCls >> 3


    补充:为什么类的isa和元类的地址是一样的?

    原理:isa 的结构来看,对于对象来说,没有 是否释放引用计数 等字段,存储的只有 元类 本身,所以类的 isa 和元类的地址是一样。

    相关文章

      网友评论

          本文标题:iOS底层-对象的本质及isa原理

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