iOS 对象探究三

300 阅读6分钟

OC 对象的本质

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface LGPerson : NSObject

@property (nonatomic, strong) NSString *KCName;

@end

@implementation LGPerson

@end

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

这里我们在 main.m 文件里面定义一个 LGPerson 类,然后打开终端,cdmain.m 所在文件目录,然后输入 clang -rewrite-objc main.m -o main.cpp 命令,最后会看到生成了一个 main.cpp 文件。

打开 main.cpp 文件我们可以看到,只有几行代码的 main.m 文件生成 c++ 文件后,会有很多行代码,这里还只是刚拖动了一旦就已经一万多行代码了。我们直接全局搜索找到 LGPerson

这里可以看到 LGPerson 类,及 KCName 属性的 getter 方法跟 setter 方法。这里可以看到其实对象本质就是结构体。

这里除了 KCName 属性之外还有一个 NSObject_IVARS 属性,这个其实是 isa。继承于结构体 NSObject_IMPL,这里是伪继承。全局搜索 NSObject_IMPL ,可以看到确实是 isa


![](https://upload-images.jianshu.io/upload_images/2936157-71e2f93eb742c762.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我们都知道在 OC 层面,对象最终都是继承于 NSObjec,这里却是 objc_object,这是因为在真正的下层,objc 的实现就是 objc_object


![](https://upload-images.jianshu.io/upload_images/2936157-21667b7dc1e20ba5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 这里还可以看到 `Class` 本质是 `objc_class *` 类型,是一个结构体指针,`Class` 只是一个别名。`id` 是 `objc_object *` 类型,这也是我们平时用 `id` 声明对象不用加 `*` 号的原因。
static NSString * _I_LGPerson_KCName(LGPerson * self, SEL _cmd) { 
    return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)); 
}
static void _I_LGPerson_setKCName_(LGPerson * self, SEL _cmd, NSString *KCName) {
 (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)) = KCName;
 }

我们再看一下 getter 方法,会发现有 LGPerson * self, SEL _cmd 这两个参数,但是我们平时写 getter 方法的时候是没有的,这其实是 oc 方法的隐藏参数。这里为什么能通过 return 返回 KCName 的值呢,其实就是拿到 person 的首地址加上IVAR的地址偏移量才能获取到 KCName 的地址,通过地址来获取 KCName 的值。setter 方法也是同样的方式赋值。


补充:Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。 Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器 2013年4月,Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。 [2] Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。


联合体位域

  1. 案例 1 LGCar1

这里定义一个代表汽车方向的结构体 LGCar1,分别有 4 个方向,总共需要 4 个字节,也就是 32 位。但是其实每个方向用 1 跟 0 就可以表示,所以只需要 4 位就行,但是一个字节是 8 位,所以需要一个字节的空间。这里会有 3 个字节的浪费。那么怎么优化呢,这里有一种位域的方式。

  1. 案例 2 LGCar2 这里可以看到,采用位域的方式只占用了一个 1 字节(其实只用到了 4 位 0000 1111)。大大的优化了内存。

3. 案例 3 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c333b1246c3246f58906a4852780c8c2~tplv-k3u1fbpfcp-watermark.image)

通过打印我们可以发现相比于 teacher1 teacher2每次只能有一个属性有值,这是因为联合体的互斥特性。

union,中文名“联合体、共用体”,在某种程度上类似结构体struct的一种数据结构,共用体(union)和结构体(struct)同样可以包含很多种数据类型和变量。 不过区别也挺明显: 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。 而联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使用更为精细灵活,也节省了内存空间。


isa

我们在iOS 对象探究一中讲过,当走到这里的时候就会把堆内存申请的结构体指针跟 Class 类进行绑定。那么 isa 是怎么实现的呢,这里我们进到代码看一下。

objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor) 方法中我们可以看到 isa_t, 这里我们再来看一下 isa_t

这里可以看到 isa_t 本质是一个联合体。这里可以看到 isa_t 的构造方法,跟属性 bits, cls

我们都知道指针类型是 8 字节,64 位系统下就是 8 * 8 一共 64 个字节。如果 64 位都只是存储一个指针,就会存在空间的浪费,而且基本上每个类基本上都有 isa,那么这里是否可以优化呢?所以苹果会把跟类息息相关的存放在 64 位里面,例如引用计数, 是否正在释放, weak, 关联对象, 析构函数等信息。所以这里有了一个概念 nonPointerIsa。具体是不是这么存的呢,我们进到 ISA_BITFIELD 里面来看一下。

arm64 isa 位域信息.png

  • nonpointer 表示是否对 isa 指针开启指针优化 0:纯isa 指针,1:不止是类对象地址,isa 中包含了类信息, 对象的引用计数等
  • has_assoc 关联对象标志位,0:没有,1:存在
  • has_cxx_dtor 该对象是否有 C++或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
  • shiftcls 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced 表示对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存
  • unused 是否使用散列表
  • has_sidetable_rc:当对象引用计数大于 2 的 19 次方(2^19) 时,则需要借用该变量存储进位
  • extra_rc: 表示对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 2 的 19 次方, 则需要使用到下面的 has_sidetable_rc。

这里我们可以看到 x86 64 位下 联合体的存储信息,及相关位域下存储的值代表的含义,下面也附上了注释。