iOS探索 -- 类的结构分析(一)

649 阅读8分钟

iOS 探索系列相关文章 :

iOS 探索 -- alloc、init 与 new 的分析

iOS 探索 -- 内存对齐原理分析

iOS 探索 -- isa 的初始化和指向分析

iOS 探索 -- 类的结构分析(一)

iOS 探索 -- 类的结构分析(二)

iOS 探索 -- 消息查找流程(一)

iOS 探索 -- 消息查找流程(二)

iOS 探索 -- 动态方法决议分析

iOS 探索 -- 消息转发流程分析

iOS 探索 -- 离屏渲染

iOS 探索 -- KVC 原理分析

在前面的几篇文章对 iOS 对象的原理进行了探索, 那么类在底层的实现又是什么样子的呢? 类是以什么样的形式存在的? 他的结构又是什么样子的呢? 接下来进行对类的一些相关内容探索, 看一下他的真面目。

类的定义

说到 , 我相信看这篇文章的人都不会陌生, 那么什么是类呢?

类 (Class) 是面向对象程序设计 (OOP, Object-Oriented Programming) 实现信息封装的基础。类是一种用户自定义的数据类型,
也称类类型。每个类包含数据说明和一组操作数据或传递信息的函数(或者方法)。类的实例称为对象。

iOS 中, 我们知道大多数情况下我们使用的类都是从 NSObject 这个基类所派生出来的, 在 OC 的底层, 我们的类到底是什么样子的呢? 接下来开始我们的探索:

  1. 首先在我们之前的源码里面去尝试去全局搜索一下 objc_class, 搜索发现了一个 objc_class 的结构体实现:
// Class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

// objc_class
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

可以看出 Class 本质其实是一个 objc_class 类型的 结构体指针, 但是有一个东西需要注意 OBJC2_UNAVAILABLE, 从该宏定义的名字可以看出, 这个定义在 OBJC2 中已经废弃掉了。

关于 OBJC2_UNAVAILABLE 的宏定义, 我在 objc 源码中找到了 :

/* OBJC2_UNAVAILABLE: unavailable in objc 2.0, deprecated in Leopard */
#if !defined(OBJC2_UNAVAILABLE)
#   if __OBJC2__
#       define OBJC2_UNAVAILABLE UNAVAILABLE_ATTRIBUTE
#   else
       /* plain C code also falls here, but this is close enough */
#       define OBJC2_UNAVAILABLE                                       \
           __OSX_DEPRECATED(10.5, 10.5, "not available in __OBJC2__") \
           __IOS_DEPRECATED(2.0, 2.0, "not available in __OBJC2__")   \
           __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE __BRIDGEOS_UNAVAILABLE
#   endif
#endif
  1. 接下来继续在上一步的搜索结果中查找, 在 objc-runtime-new 中又发现了新的声明

// objc_class (一个隐藏的 ISA), 继承自 objc_object
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 (包含 isa)
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

可以看到这是一个新的关于 objc_class 定义, 发现他继承自 objc_object , 并且 isa 就是从 objc_object 继承过来的, 并没有自己去定义。那么从类的结构其实也验证了之前的 isa 分析的结论就是: 类也有自己的 isa, 并且类的 isa 是指向的元类, 类其实也是一种对象

万物皆对象

每个实例对象都有个 isa 指针, 指向对象的 (Class)类; 而类对象里也有个 isa 指针, 指向的是 meteClass(元类)。关于 isa 的指向可以去看看我之前的文章 iOS探索--isa的初始化和指向分析

类的结构分析

在上面的过程中我们找到了关于类的定义, 接下来具体看看类的结构是什么样子的? 每一部分的作用又是什么? (注意, 这里内存占用情况默认为 64位情况下)

1. Class isa

// Class (结构体指针)
typedef struct objc_class *Class;

类对象中的 isa 指针, 用于关联 元类 , 关于这一点在之前的 isa 指向流程图中可以看出来。这里的 Class 类型是一个指针, 所以 isa 占用 8 字节。

2. Class superclass

根据名字应该可以看出来表示 当前类的父类 ,同样是 Class 类型, 所以 superclass 也占 8 个字节。

3. cache_t cache

缓存, 用于缓存已经调用的方法, 可以加速方法的调用, 具体分析我们放到以后。接下来分析一下他的内存占用情况:

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

//
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

//
#ifndef _UINT32_T
#define _UINT32_T
typedef unsigned int uint32_t;
#endif /* _UINT32_T */

cache_t 结构体包含 一个结构体指针和两个 mask_t 类型的成员变量, 且 mask_t 在 64位情况下为 int 类型。所以 cache 部分总共占用内存为 16 位。

4. class_data_bits_t bits

bits 也是一个结构体类型, 当我们去查看 objc_class 里面的函数时, 会发现很多地方都跟 bits 有关。然后尝试查看 bits 的函数实现, 发现了一个有趣的东西, 内容如下:

//
class_rw_t* data() {
  return (class_rw_t *)(bits & FAST_DATA_MASK);
}
//
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;
    char *demangledName;
#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
 		
  	/*
  	...函数
  	*/
}

class_data_bits_tdata() 函数返回了一个 class_rw_t 类型的指针对象, 然后在 class_rw_t 结构体里面看到了对 methodspropertiesprotocols 等的声明。难道我们的类声明的方法、属性等存储在这里面吗, 接下来一起着重对这个 bits 来进行研究看看。

类的结构探究

1. 准备

开始探究之前, 先回到我们的 objc 源码里面, 新建一个 target , 新建一个 Person 类, 然后开始我们的探索

// 类
@interface Person : NSObject {
    NSString *name;
}

@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) NSInteger age;

+ (void)person_sayHello;
- (void)person_study;

@end
// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];
        Class pClass = object_getClass(person);
        NSLog(@"%@", pClass);
    }
    return 0;
}

类和元类的创建时机

开始之前我们先看一个东西, 在方法执行之前打一个断点, 然后对 Person 类进行打印, 看一下是否存在。

如上图所示, 此时 person 对象还没有被创建, 但是我们对 Person 的类对象进行打印, 发现已经存在在内存当中了。所以在类进行 alloc 操作之前, 类和元类就已经存在了, 说明类和元类在编译时期就已经被创建了。

2. bits 探索

想要去探索 bits , 首先要解决如何找到他的问题, 然后才能在内存中对其结构进行一一分析。在上面的结构分析中我们知道, objc_class 内部一共包含 isasuperclasscachebits 组成, 前面的三位总共占 32 位, 那么接下来就借助指针平移来找到 bits

2.1 找到 bits 的内存地址

通过指针平移后得到新的地址, 然后进行打印验证得出 bits 的指针地址。

2.2 关于 class_rw_t

找到了 bits , 我们上面提到过, bits 可以通过 data() 函数返回一个 class_rw_t 类型的指针对象。所以为了去查看 class_rw_t 的结构, 继续往下面走

上面通过 data() 函数得到了 class_rw_t 类型的指针, 然后又通过打印发现, 果然是我们想要找的内容, 在里面也发现了我们想要寻找的 methodspropertiesprotocols 。接下来一起看看类的 属性 和 方法是否在这里面。

// 关于指针前面的 " * " 符号
// 对于指针而言, 星号一般出现的场合, 一个是指针定义时, 另一个是使用指针时。

1. int *p
指针定义时前面的星号, 目的是告诉编译器变量 p 是一个指针

2. *p + 1
使用指针时, 可以理解为 指针的值, 比如上面这里就是 指针指向的值 加上 1 。

1. properties 分析

首先直接打印 properties , 发现内部存储的是一个 list_array_tt 类型的东西, 然后打印他的 list, 发现他是一个 property_list_t 类型的指针, 进行值打印, 又出来一个 entsize_list_tt , 下面是 entsize_list_tt 的声明

// 泛型
template <typename Element, typename List, uint32_t FlagMask>
// entsize_list_tt 的声明
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;

    uint32_t entsize() const {
        return entsizeAndFlags & ~FlagMask;
    }
    uint32_t flags() const {
        return entsizeAndFlags & FlagMask;
    }
    Element& getOrEnd(uint32_t i) const { 
        assert(i <= count);
        return *(Element *)((uint8_t *)&first + i*entsize()); 
    }
    Element& get(uint32_t i) const { 
        assert(i < count);
        return getOrEnd(i);
    }
    size_t byteSize() const {
        return byteSize(entsize(), count);
    }
  
  	/*
  	一些函数...
  	*/
}

如上, 关于 template 好像是 C++ 里面的东西, 用来做泛型编程的, 有兴趣的可以去查询一下。 entsizeAndFlagscount 可以直接打印出来, 重要的是 first , 然后发现有两个函数 getOrEnd() 和 get() , 尝试调用之后发现真的可以得到与我们的属性相关的东西。但是, 没有找到类的成员变量。

2. methods

打印结果如上图, 过程跟 第1步的 properties 一样, 这里不做描述了。可以看到定义的 实例方法 和 属性的 getter,setter 方法都可以找到, 除此之外还有一个 C++ 的析构函数 .cxx_destruct但是, 类方法没有在里面

3. 关于 ro

为了寻找 类方法成员变量 , 继续对 class_rw_t 的结构进行查看, 发现最有可能在的地方只有可能是 ro

// 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;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

在其内部同样发现了关于方法和属性的东西 baseMethodListbaseProperties , 还有一个新的东西 ivars , 接下来来打印一下里面到底有什么 (过程省略, 直接上图):

从打印结果可以看出, 不只是声明的成员变量, 属性自动生成的带 "_" 的成员变量也在其中, 所以成员变量存在于 ro 下面的 ivars 中。

注意: 此处省略了对 baseMethodListbaseProperties 的打印, 有兴趣的可以去自己试一下, 你会发现这里同样存储着类的 实例方法 和 属性。

4. rw 与 ro

关于 rwro, 这里猜测 rw 意思为 read write, roread only 。因为动态性的特性, OC 在编译期保存了一份 的数据结构到 ro 中, 然后又在运行时存储另外一份到 rw 中, 给 runtime 去动态的修改使用。

另外, 我们在 rw 中可以发现 roconst 类型也就是不可变的, 所以存储的 ro 中的任何东西都是不能够改变的, 而 rw 中的 methods 、properties、protocols 则是可以改变的。这也证明了 为什么不可以动态添加属性, 因为在添加属性时会伴随着添加成员变量, 而 ivar 存储在 ro 中, 是无法改变的。

3. 类方法的存储位置

前面找到了 属性实例方法成员变量 , 那么 类方法 到底存储在哪里呢? 接下来我们借助 runtime 的 API 来一起测试一下。

1. 作为实例方法

//
// + (void)person_sayHello;
// - (void)person_study;
//
const char *className = object_getClassName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(person_sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(person_sayHello));

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

打印结果:
0x0-0x100001108-0x100001170-0x0

根据上面打印结果可以总结如下:

  • person_sayHello元类对象的实例方法, 所以存在于元类当中, 不存在
  • person_study类对象的实例方法, 所以存在于当中, 不存在元类

2. 作为类方法

//
// + (void)person_sayHello;
// - (void)person_study;
//
const char *className = object_getClassName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(person_sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(person_sayHello));

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

打印结果:
0x100001108-0x100001108-0x0-0x0

根据结果可以看出, 不管是类还是元类, 作为类方法去查找 person_study 方法时都无法获取到, 而去查找 person_sayHello 方法时又都找到了, 这里就有问题了, 那么我们进入到 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();
}
//
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);
#endif
}

可以看到 class_getClassMethod 方法的内部其实也是在调用 class_getInstanceMethod 方法, 不同点在于 getMeta() 方法, 可以看到如果是元类调用会直接返回; 如果是调用的话, 会返回 ISA() , 也就是元类, 所以不管是在中查找 person_sayHello 方法, 还是在元类中查找, 其结果是一样的。

所以, 类方法是存储在元类中的, 实例方法是存储在类中的

总结

本次探索对类进行了一系列的研究, 总结如下:

  1. 我们发现类其实也是一个对象, 并且类和元类是在编译时就创建的。
  2. 对类的结构进行了分析, 并且在 class_rw_tro 里面找到了我们的 属性实例方法, 且仅在 ro 中找到了我们的 成员变量
  3. 然后后面又对 类方法 的存储位置进行了探索, 发现类方法是存在元类中的。

最后希望本次的探索对你有所启发, 如果有不对的地方还请各位指出, 谢谢大家。