对象本质 & isa 结构

本文开始探索 OC 对象的本质是什么?

一、对象 

对象的本质 - 结构体

1、编译后的对象

1、我们在 main.m 文件中做简单代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface MyPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation MyPerson
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}

clang 将 main.m 文件编译为 C++ 文件 main.cpp,命令如下:

我们可从生成的 .cpp 文件中找到关于 MyPerson 的编译后的结构:

 

MyPerson 编译成了 struct MyPerson_IMP {…} 

即:对象 –> 在底层编译成了结构体 struct

问:.cpp 文件中第 112268 行  ‘struct NSObject_IMPL NSObject_IVARS;’ 是什么呢?

伪继承。在 C++ 中结构体是可以继承的,在 C 中可以也可以伪继承,如上图代码:将继承结构体作为当前结构体的第一个元素。此时,结构体 MyPerson_IMP 拥有 结构体 NSObject_IMPL 的所有成员变量。

任何类里面都有 NSObject_IVARS ?–> isa

NSObject_IMPL 结构体:

1
2
3
struct NSObject_IMPL {
Class isa;
};

2、同样在 .cpp 文件中也可以找到 MyPerson 的 name 属性

setter/getter 方法

objc_setProperty(),我们通过 objc 源码跟踪进去具体实现:objc_setProperty() –> reallySetProperty()

从源码可以看出,setter 方法的操作可以简单理解为就是:新值 retain 和 旧值 release . 但需要注意代码中逻辑判断,当 copy 等属性为 true 时是不同操作。详细讲解 链接.

在这里我们可以考虑,我们所有的对象的 setter 方法,都是经历一个 retain release 过程,完全是统一的过程。那么这里我们就考虑到了将其进行类似工厂类的封装,所有的对象无需每个都分开处理,我们把它扔进来即可。即:无论外部对象如何变化,每个对象的 setter 方法都会走到  objc_setProperty() –> 一种思路。

Clang 

Clang 是什么?

Clang 是一个 C 语言、C++、Object-C 语言的轻量化编译器。它是由苹果主导编写 基于 LLVM 的 C/C++/OC 编译器。早起苹果编译器是 GCC。

Clang 常用命令

1
2
3
4
5
6
7
8
9
// 把目标文件 main.m 编译成c++文件
clang -rewrite-objc main.m -o main.cpp

// 若 编译时 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 // (真机)

二、联合体位域

结构体 (struct):结构体中所有变量是共存的,且大小至少是所有变量所占内存的的和。

  优点:变量共存,有容乃大;

  缺点:对内存的分配是粗略的,存在空间浪费的。

联合体 (union):联合体中,变量是互斥关系,即同一时间只能有一个变量。

  优点:内存使用更精细灵活,节省内存空间;

  缺点:互斥性,只有一个变量存在。

示例:

位域:变量每一位所代表的信息。

一字节的分配:

联合体位域大大的优化节省了内存空间。

三、isa 结构

isa 指针 8 字节,64 位,可存储信息:264 ,完全足够我们的地址存储了。

通过之前 alloc 流程探索我们可知,对象与 isa 关联是 initInstanceIsa() 方法,我们通过源码跟踪进去:

initInstanceIsa –> initIsa() –> isa_t:

联合体 isa_t :

1
2
3
4
5
6
7
8
9
10
11
12
union isa_t {
  // 2个初始化
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls; uintptr_t bits;// cls bits 两个互斥的变量
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

ISA_BITFIELD

- 手机端:

  • Mac OS:

以 arm64 为例,位域代表含义:

nonpointer – 占据第 0 位,是否对 isa 开启指针优化。0:纯 isa 指针;1:不只是类对象地址,isa 中包含了类信息、对象的引用计数等。一般我建的类都是 nopointer 的。

shiftcls – 占据第 3~35 位,为存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存类指针。

has_sidetable_rc – 为 1 时,则表明有外部挂的引用计数 sideTable,下面的 extra_rc 存引用计数不够用了。

extra_rc – 引用计数值 - 1 的 值。例:对象的引用计数为 10,则 extra_rc = 9;若 extra_rc 不足以保存引用计数了,则上面的 has_sidetable_rc 为 true,使用 sideTable 管理。

2、验证 isa 与 类 关联

1:源码跟踪 验证 (本质也是位移操作)

执行可编译源码工程进入 initIsa() 函数,cls 为 MyPerson,nonpointer = ture:

newisa.bits = ISA_MAGIC_VALUE;// 赋初值 

migic = 59 验证:

打开系统编程计算器:

二进制 110111 –>  10 进制 59

执行 newisa.shiftcls = (uintptr_t)cls >> 3;

cls –> MyPerson –> 强转为 uintptr_t 类型 –> 值:4294975728,为何?

计算机是不识别字符串的,需要将其转成可识别的二进制的 010101…… 的机器指令。

右移 3 位 >>3: shiftcls 从第 3 号位开始,之前 0 1 2 位有三个变量,我们取 shiftcls 时不能受其影响,故做 >>3 抹零操作。 

继续运行,回到 _class_createInstanceFromZone() 函数。

我们继续验证。

isa 地址 & ISA_MASK

isa 地址 & ISA_MASK –> MyPerson

 

提一下 ISA_MASK:0x00007ffffffffff8ULL –> 第 3~47 位为 1,& 运算即两头抹零。

2:runtime - object_getClass() 验证

跟踪进入 object_getClass() 方法 –> getIsa() –> ISA(): 

1
2
3
4
5
6
7
8
9
10
11
12
13
inline Class 
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;#else
return (Class)(isa.bits & ISA_MASK);// shiftcls
#endif
}

3:手动位运算验证

如下图:

 

补图中漏写文字:$11 = 4294975728 10 进制 –> 16 进制 0x00000001000020f0

1
2
p/u cls
(Class) $15 = 4294975728 MyPerson // 等于它

验证 over。 

NSObject 中 Class isa:

NSObject 中 class 对象,我们拿到的是 class 对象,其实是通过 isa_t 拿到的,只是做了处理转化成了 class。NSObject 中代码:

1
2
3
4
5
6
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

拿到 class 的过程:NSObject –> Class isa –> object_getclass –> ISA():

return 的是有做 Class 强转的 –>  NSObject 中 Class isa OBJC_ISA_AVAILABILITY;


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!