iOS 底层探索 - 类

iOS 底层探索系列编程

iOS 查漏补缺系列缓存

咱们在前面探索了 iOS 中的对象原理,面向对象编程中有一句名言:bash

万物皆对象markdown

那么对象又是从哪来的呢?有过面向对象编程基础的同窗确定都知道是类派生出对象的,那么今天咱们就一块儿来探索一下类的底层原理吧。数据结构

1、iOS 中的类究竟是什么?

咱们在平常开发中大多数状况都是从 NSObject 这个基类来派生出咱们须要的类。那么在 OC 底层,咱们的类 Class 到底被编译成什么样子了呢?app

咱们新建一个 macOS 控制台项目,而后新建一个 Animal 类出来。oop

// Animal.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Animal : NSObject

@end

NS_ASSUME_NONNULL_END

// Animal.m
@implementation Animal

@end

// main.m
#import <Foundation/Foundation.h>
#import "Animal.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        NSLog(@"%p", animal);
    }
    return 0;
}
复制代码

咱们在终端执行 clang 命令:post

clang -rewrite-objc main.m -o main.cpp
复制代码

这个命令是将咱们的 main.m 重写成 main.cpp,咱们打开这个文件搜索 Animal:测试

咱们发现有多个地方都出现了 Animal:ui

// 1
typedef struct objc_object Animal;

// 2
struct Animal_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

// 3
objc_getClass("Animal")
复制代码

咱们先全局搜索第一个 typedef struct objc_object,发现有 843 个结果

咱们经过 Command + G 快捷键快速翻阅一下,最终在 7626 行找到了 Class 的定义:

typedef struct objc_class *Class;
复制代码

由这行代码咱们能够得出一个结论,Class 类型在底层是一个结构体类型的指针,这个结构体类型为 objc_class。 再搜索 typedef struct objc_class 发现搜不出来了,这个时候咱们须要在 objc4-756 源码中进行探索了。

咱们在 objc4-756 源码中直接搜索 struct objc_class ,而后定位到 objc-runtime-new.h 文件

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
}
复制代码

看到这里,细心的读者可能会发现,咱们在前面探索对象原理中遇到的 objc_object 再次出现了,而且此次是做为 objc_class 的父类。这里再次引用那句经典名言 万物皆对象,也就是说类其实也是一种对象

由此,咱们能够简单总结一下类和对象在 COC 中分别的定义

C OC
objc_object NSObject
objc_class NSObject(Class)

2、类的结构是什么样的呢?

经过上面的探索,咱们已经知道了类本质上也是对象,而平常开发中常见的成员变量、属性、方法、协议等都是在类里面存在的,那么咱们是否是能够猜测在 iOS 底层,类其实就存储了这些内容呢?

咱们能够经过分析源码来验证咱们的猜测。

从上一节中 objc_class 的定义处,咱们能够梳理出 Class 中的 4 个属性

  • isa 指针
  • superclass 指针
  • cache
  • bits

须要值得注意的是,这里的 isa 指针在这里是隐藏属性.

2.1 isa 指针

首先是 isa 指针,咱们以前已经探索过了,在对象初始化的时候,经过 isa 可让对象和类关联,这一点很好理解,但是为何在类结构里面还会有 isa 呢?看过上一篇文章的同窗确定知道这个问题的答案了。没错,就是元类。咱们的对象和类关联起来须要 isa,一样的,类和元类之间关联也须要 isa

2.2 superclass 指针

顾名思义,superclass 指针代表当前类指向的是哪一个父类。通常来讲,类的根父类基本上都是 NSObject 类。根元类的父类也是 NSObject 类。

2.3 cache 缓存

cache 的数据结构为 cache_t,其定义以下:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    ...省略代码...
}
复制代码

类的缓存里面存放的是什么呢?是属性?是实例变量?仍是方法?咱们能够经过阅读 objc-cache.mm 源文件来解答这个问题。

  • objc-cache.m
  • Method cache management
  • Cache flushing
  • Cache garbage collection
  • Cache instrumentation
  • Dedicated allocator for large caches

上面是 objc-cache.mm 源文件的注释信息,咱们能够看到 Method cache management 的出现,翻译过来就是方法缓存管理。那么是否是就是说 cache 属性就是缓存的方法呢?而 OC 中的方法咱们如今尚未进行探索,先假设咱们已经掌握了相关的底层原理,这里先简单提一下。

咱们在类里面编写的方法,在底层实际上是以 SEL + IMP 的形式存在。SEL 就是方法的选择器,而 IMP 则是具体的方法实现。这里能够以书籍的目录以及内容来类比,咱们查找一篇文章的时候,须要先知道其标题(SEL),而后在目录中看有没有对应的标题,若是有那么就翻到对应的页,最后咱们就找到了咱们想要的内容。固然,iOS 中方法要比书籍的例子复杂一些,不过暂时能够这么简单的理解,后面咱们会深刻方法的底层进行探索。

2.4 bits 属性

bits 的数据结构类型是 class_data_bits_t,同时也是一个结构体类型。而咱们阅读 objc_class 源码的时候,会发现不少地方都有 bits 的身影,好比:

class_rw_t *data() { 
    return bits.data();
}

bool hasCustomRR() {
    return ! bits.hasDefaultRR();
}    

bool canAllocFast() {
    assert(!isFuture());
    return bits.canAllocFast();
}
复制代码

这里值得咱们注意的是,objc_classdata() 方法实际上是返回的 bitsdata() 方法,而经过这个 data() 方法,咱们发现诸如类的字节对齐、ARC、元类等特性都有 data() 的出现,这间接说明 bits 属性实际上是个大容器,有关于内存管理、C++ 析构等内容在其中有定义。

这里咱们会遇到一个十分重要的知识点: class_rw_tdata() 方法的返回值就是 class_rw_t 类型的指针对象。咱们在本文后面会重点介绍。

3、类的属性存在哪?

上一节咱们对 OC 中类结构有了基本的了解,可是咱们平时最常打交道的内容-属性,咱们还不知道它到底是存在哪一个地方。接下来咱们要作一件事情,就是在 objc4-756 的源码中新建一个 Target,为何不直接用上面的 macOS 命令行项目呢?由于咱们要开始结合 LLDB 打印一些类的内部信息,因此只能是新建一个依靠于 objc4-756 源码 projecttarget 出来。一样的,咱们仍是选择 macOS 的命令行做为咱们的 target

接着咱们新建一个类 Person,而后添加一些实例变量和属性出来。

// Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end

NS_ASSUME_NONNULL_END

// main.m
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        NSLog(@"%s", p);
    }
    return 0;
}
复制代码

咱们打一个断点到 main.m 文件中的 NSLog 语句处,而后运行刚才新建的 target

target 跑起来以后,咱们在控制台先打印输出一下 pClass 的内容:

3.1 类的内存结构

咱们这个时候须要借助指针平移来探索,而对于类的内存结构咱们先看下面这张表格:

类的内存结构 大小(字节)
isa 8
superclass 8
cache 16

前两个大小很好理解,由于 isasuperclass 都是结构体指针,而在 arm64 环境下,一个结构体指针的内存占用大小为 8 字节。而第三个属性 cache 则须要咱们进行抽丝剥茧了。

cache_t cache;

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4
}

typedef uint32_t mask_t; 
复制代码

从上面的代码咱们能够看出,cache 属性实际上是 cache_t 类型的结构体,其内部有一个 8 字节的结构体指针,有 2 个各为 4 字节的 mask_t。因此加起来就是 16 个字节。也就是说前三个属性总共的内存偏移量为 8 + 8 + 16 = 32 个字节,32 是 10 进制的表示,在 16 进制下就是 20。

3.2 探索 bits 属性

咱们刚才在控制台打印输出了 pClass 类对象的内容,咱们简单画个图以下所示:

那么,类的 bits 属性的内存地址瓜熟蒂落的就是在 isa 的初始偏移量地址处进行 16 进制下的 20 递增。也就是

0x1000021c8 + 0x20 = 0x1000021e8
复制代码

咱们尝试打印这个地址,注意这里须要强转一下:

这里报错了,问题实际上是出在咱们的 target 没有关联上 libobjc.A.dylib 这个动态库,咱们关联上从新运行项目

咱们重复一遍上面的流程:

这一次成功了。在 objc_class 源码中有:

class_rw_t *data() { 
    return bits.data();
}
复制代码

咱们不妨打印一下里面的内容:

返回了一个 class_rw_t 指针对象。咱们在 objc4-756 源码中搜索 class_rw_t:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;
    
    ...省略代码...    
}
复制代码

显然的,class_rw_t 也是一个结构体类型,其内部有 methodspropertiesprotocols 等咱们十分熟悉的内容。咱们先猜测一下,咱们的属性应该存放在 class_rw_tproperties 里面。为了验证咱们的猜测,咱们接着进行 LLDB 打印:

咱们再接着打印 properties:

properties 竟然是空的,难道是 bug?其实否则,这里咱们还漏掉了一个很是重要的属性 ro。咱们来到它的定义:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    ...隐藏代码...    
}
复制代码

ro 的类型是 class_ro_t 结构体,它包含了 baseMethodListbaseProtocolsivarsbaseProperties 等属性。咱们刚才在 class_rw_t 中没有找到咱们声明在 Person 类中的实例变量 hobby 和属性 nickName,那么但愿就在 class_ro_t 身上了,咱们打印看看它的内容:

根据名称咱们猜想属性应该在 baseProperties 里面,咱们打印看看:

Bingo! 咱们的属性 nickName 被找到了,那么咱们的实例变量 hobby 呢?咱们从 $8 的 count 为 1 能够得知确定不在 baseProperites 里面。根据名称咱们猜想应该是在 ivars 里面。

哈哈,hobby 实例变量也被咱们找到了,不过这里的 count 为何是 2 呢?咱们打印第二个元素看看:

结果为 _nickName。这一结果证明了编译器会帮助咱们给属性 nickName 生成一个带下划线前缀的实例变量 _nickName

至此,咱们能够得出如下结论:

class_ro_t 是在编译时就已经肯定了的,存储的是类的成员变量、属性、方法和协议等内容。 class_rw_t 是能够在运行时来拓展类的一些属性、方法和协议等内容。

4、类的方法存在哪?

研究完了类的属性是怎么存储的,咱们再来看看类的方法。

咱们先给咱们的 Person 类增长一个 sayHello 的实例方法和一个 sayHappy 的类方法。

// Person.h
- (void)sayHello;
+ (void)sayHappy;

// Person.m
- (void)sayHello
{
    NSLog(@"%s", __func__);
}

+ (void)sayHappy
{
    NSLog(@"%s", __func__);
}
复制代码

按照上面的思路,咱们直接读取 class_ro_t 中的 baseMethodList 的内容:

sayHello 被打印出来了,说明 baseMethodList 就是存储实例方法的地方。咱们接着打印剩下的内容:

能够看到 baseMethodList 中除了咱们的实例方法 sayHello 外,还有属性 nickNamegettersetter 方法以及一个 C++ 析构方法。可是咱们的类方法 sayHappy 并无被打印出来。

5、类的类方法存在哪?

咱们上面已经获得了属性,实例方法的是怎么样存储,还留下了一个疑问点,就是类方法是怎么存储的,接下来咱们用 Runtime 的 API 来实际测试一下。

// main.m
void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        
        testInstanceMethod_classToMetaclass(pClass);
        NSLog(@"%p", p);
    }
    return 0;
}
复制代码

运行后打印结果以下:

首先 testInstanceMethod_classToMetaclass 方法测试的是分别从类和元类去获取实例方法、类方法的结果。由打印结果咱们能够知道:

  • 对于类对象来讲,sayHello 是实例方法,存储于类对象的内存中,不存在于元类对象中。而 sayHappy 是类方法,存储于元类对象的内存中,不存在于类对象中。
  • 对于元类对象来讲,sayHello 是类对象的实例方法,跟元类不要紧;sayHappy 是元类对象的实例方法,因此存在元类中。

咱们再接着测试:

// main.m
void testClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        
        testClassMethod_classToMetaclass(pClass);
        NSLog(@"%p", p);
    }
    return 0;
}
复制代码

运行后打印结果以下:

从结果咱们能够看出,对于类对象来讲,经过 class_getClassMethod 获取 sayHappy 是有值的,而获取 sayHello 是没有值的;对于元类对象来讲,经过 class_getClassMethod 获取 sayHappy 也是有值的,而获取 sayHello 是没有值的。这里第一点很好理解,可是第二点会有点让人糊涂,不是说类方法在元类中是体现为对象方法的吗?怎么经过 class_getClassMethod 从元类中也能拿到 sayHappy,咱们进入到 class_getClassMethod 方法内部能够解开这个疑惑:

Method class_getClassMethod(Class cls, SEL sel) {
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}    
复制代码

能够很清楚的看到,class_getClassMethod 方法底层其实调用的是 class_getInstanceMethod,而 cls->getMeta() 方法底层的判断逻辑是若是已是元类就返回,若是不是就返回类的 isa。这也就解释了上面的 sayHappy 为何会出如今最后的打印中了。

除了上面的 LLDB 打印,咱们还能够经过 isa 的方式来验证类方法存放在元类中。

  • 经过 isa 在类对象中找到元类
  • 打印元类的 baseMethodsList

具体的过程笔者再也不赘述。

6、类和元类的建立时机

咱们在探索类和元类的时候,对于其建立时机还不是很清楚,这里咱们先抛出结论:

  • 类和元类是在编译期建立的,即在进行 alloc 操做以前,类和元类就已经被编译器建立出来了。

那么如何来证实呢,咱们有两种方式能够来证实:

  • LLDB 打印类和元类的指针

  • 编译项目后,使用 MachoView 打开程序二进制可执行文件查看:

7、总结

  • 类和元类建立于编译时,能够经过 LLDB 来打印类和元类的指针,或者 MachOView 查看二进制可执行文件
  • 万物皆对象:类的本质就是对象
  • 类在 class_ro_t 结构中存储了编译时肯定的属性、成员变量、方法和协议等内容。
  • 实例方法存放在类中
  • 类方法存放在元类中

咱们完成了对 iOS 中类的底层探索,下一章咱们将对类的缓存进行深一步探索,敬请期待~

相关文章
相关标签/搜索