isa与类关联的原理

698 阅读6分钟

对象的本质

1.clang的了解

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

  • 它与GNU C语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载(通过__attribute__((overloadable))来修饰函数),其⽬标(之⼀)就是超越GCC.

  • 它主要是用于底层编译,将一些OC文件输出成C++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理

2. Clang操作指令

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

// UIKit报错问题 -- 将 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/iPhoneSimulator14.0.sdk ViewController.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 mainarm64.cpp (⼿机) 

3。探索对象的本质

  • 先在main函数里面写测试代码 10FF81D7-6BF8-4C05-9862-3BA81C6E9DA2.png
  • 通过终端,利用clang将main.m编译成 main.cpp,在终端输入以下命令
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
  • 打开编译好的main-arm64.cpp,找LGPerson的定义,发LGPerson在底层会被编译成struct 结构体
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//NSObject 的底层编译
struct NSObject_IMPL {
	Class isa;
};

//LGPerson的底层编译
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
	NSString *_name;
};

如下图所示

dc4b5ad3e13348d1a9baa6a6491d0e43~tplv-k3u1fbpfcp-zoom-1.image.jpeg

通过编译好的main-arm64.cpp我们可以看到:

  • NSObject的底层实现其实就是一个包含一个isa指针的结构体.

  • Class其实就是一个指针,指向了objc_class类型的结构体.

  • LGPerson_IMPL结构体内有三个成员变量:

  • isa 继承自父类NSObject

  • KCName

  • _name

  • 对于属性name:底层编译会生成相应的setter(I_LGPerson_setName,setter方法内调用objc_setProperty方法)、getter(_I_LGPerson_name)方法,且帮我们转化为_name

通过上述分析,理解了OC对象的本质 -- 结构体,但是看到NSObject的定义,会产生一个疑问:为什么isa的类型是Class?

  • alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的,
  • 而在NSObject定义中isa的类型是Class,其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换,类似于swift中的 as 的强转。。源码中isa的强转如下图所示

d1b74302c42e44bdb23c53304078c289~tplv-k3u1fbpfcp-zoom-1.image.png

总结 所以从上述探索过程中可以得出:

  • OC对象的本质 其实就是 结构体

  • LGPerson中的isa是继承自NSObject中的isa

cls 与 类 的关联原理

联合体(union)

构造数据类型的方式有以下两种:
  • 结构体(struct)

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

  • 缺点:所有属性都分配内存,比较浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费

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

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

  • 缺点:,包容性弱

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

两者的区别

  • 内存占用情况

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

  • 内存分配大小

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

isa的类型 isa_t

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

    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指针中通过char + 位域 (即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能 从isa_t的定义中可以看出:

  • 提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式

    通过cls初始化,bits无默认值

    通过bits初始化,cls有默认值

  • 还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示

image.png

isa原理探索

通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路径,查找到initInstanceIsa,并进入其原理实现

objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
    //初始化isa
    initIsa(cls, true, hasCxxDtor); 
}

initIsa分析

image.png

  • isa_t newisa(0)相当于初始化isa这个东西,newisa.相当于给isa赋值属性.
  • SUPPORT_INDEXED_ISA适用于WatchOS,isa作为联合体具有互斥性,而cls、bits是isa的元素,所以当!nonpointer=true时对cls进行赋值操作,为false是对bits进行赋值操作(反正都是一家人,共用一块内存地址).

验证isa指针 位域(0-64)

根据前文提及的0-64位域,可以在这里通过initIsa方法证明isa指针中有这些位域(目前是处于macOS,所以使用的是x86_64).

  • 首先通过main中的TCJPerson 断点 --> initInstanceIsa --> initIsa --> isa_t newisa(0)完成 isa初始化.
  • 执行LLDB指令: p newisa,得到newisa的详细信息

image.jpeg

继续往下执行,走到newisa.bits = ISA_MAGIC_VALUE;下一行,表示为isa的bits成员赋值,重新执行LLDB命令p newisa,得到的结果如下

image.jpeg