OC底层原理之-OC对象(下)isa指针结构分析

1,468 阅读6分钟

前言

我们在OC底层原理之-对象alloc理解介绍了alloc方法的过程,其中非常重要的方法,如下图所示: 上篇文章OC底层原理之-内存对齐分析了红框1以及红框2的方法,今天的文章分析一下红框3以及红框4方法,如何让isa指针和申请的内存地址进行绑定的 在探究isa指针和申请内存如何绑定之前,我们先看下对象究竟是什么

对象本质

创建Student类

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

@class Student;
@interface ViewController : UIViewController
@property (nonatomic, strong) Student *student;
@property (nonatomic, copy) NSString *schoolName;
@end

因为OC是C语音的超集,我们需要看下我们写的OC代码,在C语言下是什么样的,这里我们需要使用clang

clang:Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过_ attribute_((overloadable))来修饰函数),其目标(之一)就是超越GCC。

打开终端输入: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController-arm64.cpp

这个是我们制定了架构模式为arm64,要编译的名:ViewController.m,生成新类名:ViewController-arm64.cpp

**有时候会提示UIKit报错,我们需要输入如下命令(iPhoneSimulator13.5.sdk要自己按这个路径去找,看自己的版本) clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk main.m **

完成后我们去ViewController.m所在的目录下,看到生成新的类:ViewController-arm64.cpp 打开cpp文件后,东西太多了,7万多行代码。直接搜ViewController找到如下图所示的内容 上面说明对象是个结构体 上面主要讲的clang的使用,还有下个几个方法

  • clang -rewrite-objc main.m -o main.cpp 把目标文件编译成
  • xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)

下面我们来看看isa指针和内存如何绑定

isa指针和申请内存如何绑定

我们使用的源码为:objc4-781

initInstanceIsa以及initIsa方法的探究

当我们点击initInstanceIsa以及initIsa方法最后都会进initIsa方法 上图中的方法红框中出现isa,我们运行下项目实际看下。 运行后通过断点发现进了红框方法,我们看看红框方法到底是什么

  • union:联合体标识有的说是共用体,
  • isa_t(){} 是初始化方法
  • Class cls 绑定的类
  • uintptr_t bits typedef unsigned long长整形8字节
  • struct是结构体,里面包含ISA_BITFIELD(这里使用宏定义的原因是因为要根据系统架构进行区分的)

再看下ISA_BITFIELD(位域的声明)

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

之所以isa指针这么设计是为了优化性能,节省空间。指针有8字节,64bit,但是单纯的地址指针用不完那么多空间,如果空着就会浪费,所以在空余的地方加入对象的其它信息,节约了空间

上面我们可以看到isa_t类型是一个union联合体,ISA_BITFIELD是位域

联合体(union)

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的:

  • 联合体是一个结构;
  • 它的所有成员相对于基地址的偏移量都为0;
  • 此结构空间要大到足够容纳最"宽"的成员;
  • 其对齐方式要适合其中所有的成员;
位域

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便。

探究联合体(union)以及位域

上面说的可能有些空洞,我们用实际例子来说明,我们创建一个类:Car

@interface Car : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end

@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Car *car = [[Car alloc]init];
    car.front = YES;
    car.back = NO;
    car.left = YES;
    car.right = NO;
}
@end

我们对car打印,看看内存情况 00 00 00 00 01 01 01 01这里我们至少使用4个字节来存储这4个属性 这里的这些属性对于空间来说就有些浪费了

如何解决,我们可以使用char类型来表示这些属性

char 0000 0001 (这里是用二进制表示的) 我们可以使用第一位为1标示向前,第二位为1标示向后,第三位为0标示向左,第四位为1标示向右,这样我们就只用了4个位置(4个位置二进制中标示为8),不到一个字节,这样就大大的节省了内存空间

下面我们对Car进行改造

#import "Car.h"
#define DirectionFrontMask    (1 << 0)
#define DirectionBackMask     (1 << 1)
#define DirectionLeftMask     (1 << 2)
#define DirectionRightMask    (1 << 3)
@interface Car (){
    // 联合体
    union {
        char bits;
        // 位域
        struct { // 0000 1111
            char front  : 1;
            char back   : 1;
            char left   : 1;
            char right  : 1;
        };
    } _direction;
}
@end
@implementation Car
- (instancetype)init {
    self = [super init];
    if (self) {
        _direction.bits = 0b0000000000;
    }
    return self;
}
- (void)setFront:(BOOL)isFront {
    if (isFront) {
        _direction.bits |= DirectionFrontMask;
    } else {
        _direction.bits |= ~DirectionFrontMask;
    }
    NSLog(@"%s",__func__);
}
- (BOOL)isFront {
    return _direction.front;
}
- (void)setBack:(BOOL)isBack {
    _direction.back = isBack;
    NSLog(@"%s",__func__);
}
- (BOOL)isBack {
    return _direction.back;
}
- (void)setRight:(BOOL)right {
    _direction.right = right;
}
- (BOOL)isRight {
    return _direction.right;
}
- (void)setLeft:(BOOL)left {
    _direction.left = left;
}
- (BOOL)isLeft {
    return _direction.left;
}
@end

再次运行打断点,打印如下图 此时我们只占用了一个字节,比之前少了3个,我们用0f,就表示了4个属性。打印的也都是对的。但是有一个front为nil,原因是我们只写了加is前缀的返回值方法

回到上面的方法,我们探究下isa内部逻辑

探究isa指针内部如何赋值

我们将断点放在initIsa中,执行initInstanceIsa方法,下一步 我们让编译器走到对bits赋值时,打印cls以及bits都为nil,说明还未赋值 点击下一步,给bits进行赋值,然后在此打印bits以及cls发现已经有值了(我们进行isa探究创建的事Student对象) 这说明了在给bits赋值的同时,也将cls进行同步的赋值,完成内存跟地址的绑定

我们打印整个newisa 可以看到nonpointer值为1,说明指针包含其他信息,magic为啥为59呢?我们可以把ISA_MAGIC_VALUE值输入到计算器中,显示如下 我们再输入59到计算器,再看二进制 这就是为什么magic为59。 通过上面我们可以知道哪些属性被初始化了。下面我们来验证对象isa的shiftcls存储的是类指针的值。 根据x86结构,对对象的第一个属性去掉低3位与高17位 下面是打印的结果 用图解释吧

从上图可以看出的指针与类对象第一个属性处理后的值是完全一样的,这也验证isa的shiftcls存储的是类指针的值。

其实在源码中,已经有这种操作了 上面的代码就是我们在调用object_getClass(id obj)时源码的处理。

补充:经过将近一个小时的整理,按照上面的isa指针,整理表格,加深印象:

补充:isa指针位置图,arm64和X86

最后

讲到上面isa内部结构基本上写完了,讲了clang,isa内部结构,发现打印内存地址很有意思,闲来无事就继续敲了一下,isa指针内容很多,自己之前了解的对象的isa指针指向类,类的isa指向指向元类,元类指向根元类,根元类指向自身。我们根据这个思路试试,最后成功了,直接上图 解释也用图 诚不我欺,isa指向的确是类对象->类->元类->根元类->根元类 后面了解的多了,可能会玩些别的。