深入理解 Objective-C ☞ Class

深入理解 Objective-C ☞ Class_第1张图片

0.前言

从事 iOS 开发已有 3 年多时间,大部分时间都是在用 Objective-C 开发 App(最近也在做 OC 与 Swift 的混编实践),虽然对 OC 底层知识有一定的了解,不过都是零散的片段,计划趁着过年的时间将这些片段梳理串联起来,于是便有了这个系列。

本文是第 1 篇,从我们平常用的最多的对象开始,深入探究他们的实现机理。

1.概述

我们平常编写的 OC 代码都会先编译成 C/C++代码,然后再依次翻译成 汇编代码机器码(01代码),最后,机器会自动运行该机器语言程序,并将计算结果输出。为了探究 OC 的本质,通过 C/C++ 是比较合适的方式,因为之后的汇编和01代码看着太费劲(主要是自己只了解点皮毛(⊙﹏⊙)b),而 OC 本身又不是开源的。

2.从一个 开始

2.1 最简单的例子

我们先来看一个例子:

// 以下代码位于 main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建一个 NSObject 的实例对象
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

如上所示,在 main() 函数里边创建了一个 NSObject 的实例对象,然后终端执行下边的指令,将代码编译成 C/C++ 代码 (新代码在 main.cpp 文件中):

clang -rewrite-objc main.m -o main.cpp //  -o main.cpp 可以忽略

在 main.cpp 中我们发现了下边的结构体,从名字推断,应该是 NSObject 的底层实现:

struct NSObject_IMPL { // NSObject_IMPL <=> NSObject implementation 
    Class isa;
};

而我们直接查看 NSobject 的声明:

@interface NSObject  { // 移除了用于消除警告的代码 
    Class isa  OBJC_ISA_AVAILABILITY;
}

NSObject_IMPL 对比后,进一步印证了 NSObject_IMPL 是 NSObject 的底层结构的推断。这里有一个 Class 类型的 isa,下面是 Class 的定义:

/// An opaque type that represents an Objective-C class. 表示 OC 中的 class。
typedef struct objc_class *Class;

也就是说,isa 实际是一个指向 struct objc_class 的指针,而且 objc_class 就是 Class 的底层结构。

2.2 稍微复杂点的例子

现在来看一种更加复杂的情况:依次创建 HHStaff 和 HHManager 这 2 个类,其中,后者继承自前者,然后在 main() 函数中创建一个 HHManager 的实例。

HHStaff

@interface HHStaff : NSObject {
    NSString *name;
}

- (void)doInstanceStaffWork; // 对象方法
+ (void)doClassStaffWork;    // 类方法

@end

HHManager

@interface HHManager : HHStaff {
    NSInteger officeNum;
}

- (void)doInstanceManagerWork; // 对象方法
+ (void)doClassManagerWork;    // 类方法

main.m 文件

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建实例对象
        HHManager *mgr = [[HHManager alloc] init];
    }
    return 0;
}

终端执行 clang -rewrite-objc main.m 将其转成 C/C++ 代码,整理相关代码后,我们可以得出下图的关系:

深入理解 Objective-C ☞ Class_第2张图片
稍微复杂的情况.png

其中,HHManager_IMPL 是 HHManager 的底层结构,而 HHStaff_IMPL 是其父类 HHStaff 的底层结构,即子类中包含一个父类类型的变量,而父类结构中又包含一个父类的父类(此处是基类)类型变量,而基类中包含一个名为 isa 的指针变量,据此,可以认为子类 HHManger 经编译后的结构是这样的:

struct HHManager_IMPL {
    Class isa;
    NSString *name;
    NSInteger officeNum;
};

我们发现,这里包含了一个 isa 指针,而 isa 来自 NSObject,因为大部分类都是直接或间接继承自 NSObject 的,所以可以认为每一个对象都包含了一个 isa 指针,至于这个 isa 指针到底是干什么用的,下一小节就会讲到。

3.OC 的 3 种对象间的关系

3.1 OC 中的 3 种对象

为了搞清楚 isa 指针的作用,有必要先了解一下 OC 的对象,总共有以下 3 种:

  • 实例对象(instance),通过 +alloc 方法创建出来的,如下边的 staffAstaffB:
HHStaff *staffA = [[HHStaff alloc] init];
HHStaff *staffB = [[HHStaff alloc] init];

NSLog(@"实例对象:%p - %p", staffA, staffB);

实例对象在内存中存储的信息包括:isa 指针 和 其他成员变量。

  • 类对象(class),如下边的 staffClassAstaffClassB:
Class staffClassA = [staffA class]; // <==> Class staffClassA = [[staffA class] class];
Class staffClassB = object_getClass(staffB);
Class staffClassC = [HHStaff class]; // <==> Class staffClassC = [[HHStaff class] class];

NSLog(@"类对象: %p - %p - %p", staffClassA, staffClassB, staffClassC);

类对象中包含的信息如下图所示,其中,成员变量信息指的是成员变量的描述信息,而非成员变量的值(在实例对象里边)。

深入理解 Objective-C ☞ Class_第3张图片
  • 元类对象(meta-class),如下边的 staffMetaClassAstaffMetaClassB:
Class staffMetaClassA = object_getClass(staffClassA);
Class staffMetaClassB = object_getClass(staffClassB);

NSLog(@"元类对象:%p - %p", staffMetaClassA, staffMetaClassB);

元类对象的存储结构与类对象相似,只不过只有 isa、superclass 和 类方法有值,其它均为空。

运行上边的程序后,控制台的输出如下:

2019-01-28 17:36:33.990939+0800 TTTTT[10186:1017842] 实例对象:0x100605920 - 0x100606060
2019-01-28 17:36:33.991128+0800 TTTTT[10186:1017842] 类对象:  0x100001260 - 0x100001260 - 0x100001260
2019-01-28 17:36:33.991180+0800 TTTTT[10186:1017842] 元类对象:0x100001238 - 0x100001238
Program ended with exit code: 0

从上述打印结果可以看出,一个类的实例对象可以有多个,但是类对象和元类对象各自只有一个。

3.2 isa 和 superclass

通过上一小节,我们知道类里边的信息并不是存在一个地方,而是分开存放在实例对象、类对象和元类对象里边。而将这些对象联系起来的纽带就是本小节要重点讨论的 isa 和 superclass 指针。

isa

深入理解 Objective-C ☞ Class_第4张图片
isa.png

isa 是用来联系同一个类的实例对象、类对象和元类对象的(isa 类型是 isa_t,后文会讲到),如上图所示,通过实例对象里边的 isa 指针可以找到类对象,根据类对象里边的 isa 指针可以找到元类对象。

注意,这里并没有说 isa 指向哪里,而是说通过 isa 可以找到哪里,这是因为从 64bit 架构开始,isa 里边存储的不再是类对象或者元类对象的地址,而是需要进行一次位运算 isa.bits & ISA_MASK(依据见后文 isa_t 的介绍)才能得到相应的地址,其中 ISA_MASK 的定义如下:

# if __arm64__      // 64位 真机
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__   // 64位 模拟器
#   define ISA_MASK        0x00007ffffffffff8ULL
# else
#   error unknown architecture for packed isa
# endif

注意到 ISA_MASK 中有些位是 0,而和 0 与的话,结果会被置为 0,所以可以推测,64bit 架构下,isa 里边可能还存储了其它信息。

superclass

superclass 是用来在继承体系中搜寻父类的,如下图所示:

深入理解 Objective-C ☞ Class_第5张图片
superclass.png
  • 对于类对象:子类(HHManager)的类对象的 superclass 指向父类(HHStaff)的类对象,父类的类对象的 superclass 指向它的父类的类对象;
  • 对于元类对象:子类(HHManager)的元类对象的 superclass 指向父类(HHStaff)的元类对象,父类的元类对象的 superclass 指向它的父类的元类对象;

3.3 应用

下面我们来看看在消息发送过程中,这 3 种对象之间是如何亲密协作的。

先贴一张经典的关系图,实际就是将上一节中的 isa 和 superclass 指针放到了一起:

深入理解 Objective-C ☞ Class_第6张图片

现在以 2.2 节中的例子为基础,执行下边的操作,即子类执行父类的对象方法。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建实例对象
        HHManager *mgr = [[HHManager alloc] init];
        // 执行父类的方法
        [mgr doInstanceStaffWork]; 
        // => objc_msgSend(mgr, @selector(doInstanceStaffWork));
    }
    return 0;
}

由于对象方法存放在类对象里边,所以首先根据 mgr 的 isa 指针找到它的类对象,然后在类对象的方法列表里边查找这个方法,发现找不到,接着再根据类对象的 superclass 指针找到父类的类对象,然后在父类的类对象里边查找该方法,如果还找不到,就根据父类的 superclass 指针沿着继承体系继续往上找,直到根类,如果还是找不到,就会执行消息转发的流程(详见 Objective-C 的消息转发机制)。不过,本例中父类的类对象里有这个方法,就不用再往上找了O(∩_∩)O。

如果是类方法,则通过类对象的 isa 指针找到元类对象,然后就依照类似查找对象方法的方式查找类方法,只不过这次是在元类对象的继承体系里边查找。

其实,上边的逻辑省略了一个非常重要的缓存问题,即在每一级查找时,都会先查找缓存,然后才去查找方法列表。找到之后,也会在缓存里边存一份(即使是在父类的类对象或元类对象里边找到的,也要始终缓存在当前类对象或元类对象里),以便提高查找效率。

特例

注意观察上边那张关系图的右上角,就会发现,基类的元类对象的 superclass 指针指向了自己的类对象,真实情况是这样的吗?我们来做一个实验:给 NSObject 添加一个对象方法,代码如下:

@interface NSObject (Extern)

- (void)doInstanceWork;

@end

@implementation NSObject (Extern)

- (void)doInstanceWork {
    NSLog(@"这是 NSObject 的对象方法");
}

@end

然后,在 main.m 中这样调用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [HHStaff doInstanceWork];
    }
    return 0;
}

即调用 HHStaff 的类方法 +doInstanceWork,不过 HHStaff 里边并没有这个类方法,但是运行时并没有报错,控制台输出如下:

2019-02-03 16:09:38.454099+0800 HHH[2667:925051] 这是 NSObject 的对象方法

也就是说,确实如关系图所示,执行了基类的类对象里边存储的对象方法。可以这么来理解,OC 的方法调用经编译后都会转成这样的函数调用:objc_msgSend(object, @selector(methodName)) ,这里并没有指明是类方法还是对象方法,也就是不关心是对象方法还是类方法,如果 object 是实例对象,就会去类对象里查找方法,如果 object 是类对象,就会去元类对象里边查找。

4.Class 的结构

前边我们说过,类中的方法、属性、协议等重要信息都存储在类对象元类对象里边,这两者的结构相同,都是 Class 类型的,而 Class 的结构实际就是 struct objc_class,因此我们的目的就是要弄清楚 struct objc_class 的结构。

在 objc 源码的 objc-runtime-new.h 中找到了 objc_class 的最新定义:

struct objc_class : objc_object {
    // Class ISA;              // isa 不再放这里
    Class superclass;
    cache_t cache;             // 1.缓存
    
    class_data_bits_t bits;    
    class_rw_t *data() {       // 2.class_rw_t
        return bits.data();
    }
    
    // *** 此处略去好多行 O(∩_∩)O~
}

既然 C++ 的结构体是可以继承的,那么我们来看看它继承的结构体 objc_object 里边都有什么:

struct objc_object {
private:
    isa_t isa;                // 3.isa,注意是私有
public: 
    // 此方法返回的不是 tagged pointer 对象
    Class ISA();
    // 此方法返回的可能是一个 tagged pointer 对象
    Class getIsa();
    // *** 此处又略去好多行 O(∩_∩)O~
}

以上就是 objc_class 的表层结构,下面针对其中的 3 各主要部分做一个相对深入点的讨论。

4.1 cache_t

cache_t 就是前文提到的方法缓存,其结构如下所示(做了适当精简):

struct cache_t {
    struct bucket_t *_buckets;  // 散列表
    mask_t _mask;               // 散列表的长度 - 1
    mask_t _occupied;           // 已经缓存的方法数量

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    
    // *** 此处又略去好多行 O(∩_∩)O~
    
    // 扩展空间
    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    // 查询缓存
    struct bucket_t * find(cache_key_t key, id receiver);

    // *** 此处略去好多行 O(∩_∩)O~
};

cache_t 里边有一个散列表(哈希表)_buckets,里边是一个个的 struct bucket_t,用于缓存方法。bucket_t 的结构如下所示:

struct bucket_t {
private:
    cache_key_t _key;   // 用 SEL 做 key
    IMP _imp;           // 函数的内存地址 做 value

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

现在,我们看一下如何查询缓存,即 find() 函数的实现:

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);    // 根据 k 与 m 算出一个下标:begin = k & m
    mask_t i = begin;
    do {                                // 根据下标取值,并验证做了一个异常处理,即不同 key 得到相同下标的问题
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

查询的基本逻辑是:

  • 先根据传入的 k(即key) 和 m(即mask) 算出一个下标 begin = k & m

  • 然后用这个下标 begin 去散列表里取值,用取到的值 (bucket) 里边的 key 与 传入的 k 作比较,

    • 如果相等,就将取到的值 (bucket) 返回;

    • 如果不等,利用 cache_next() 函数 (如下) 算出一个新的下标,再去取值比较;

      #if __arm__  ||  __x86_64__  ||  __i386__  // 各种模拟器
      static inline mask_t cache_next(mask_t i, mask_t mask) {
          return (i+1) & mask;
      }
      
      #elif __arm64__                             // 64bits 真机
      static inline mask_t cache_next(mask_t i, mask_t mask) {
          // 如果 i 不为 0,则返回 i-1;否则返回 mask
          return i ? i-1 : mask;
      }
      
      #else
      #error unknown architecture
      #endif
      
    • 如此循环,最后如果新算出来的下标等于 begin,则退出循环,说明缓存里没有对应的方法。

4.2 class_rw_t

class_rw_t 是通过 bit 的 data() 函数获取的,从名称可以看出来,它是可读可写的(rw),其基本结构及说明如下:

struct class_rw_t {
    
    // *** 此处又略去好多行 O(∩_∩)O~

    const class_ro_t *ro;

    method_array_t methods;         // 方法列表
    property_array_t properties;    // 属性列表
    protocol_array_t protocols;     // 协议列表
    
    // *** 此处又略去好多行 O(∩_∩)O~
}

4.2.1 class_ro_t

上边的 class_rw_t 里有一个 readonly 的 class_ro_t *roclass_ro_t 的结构及各元素的说明如下:

struct class_ro_t {
    
    // *** 此处略去好多行 O(∩_∩)O~
    
    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;
    }
};

class_ro_t 里边存放的是编译完成时类结构里边的方法、属性、协议及成员变量等信息。class_rw_t 里边是在运行时扩展了累的方法、属性等信息以后的结构,比如,分类中的方法就是加到了这个结构里。

class_ro_t 里边有成员变量,而且是只读的,但 class_rw_t 里没有,这也解释了为什么不能通过分类添加成员变量。当然分类里是可以添加属性的,只不过这样添加的属性只能生成 setter 和 getter 的声明,还需要自己设法完成他们的实现,至于如何实现,下一篇 runtimen 会讲到。

4.2.2 method_t

后面的几篇还会用到 '方法' 相关的底层概念 method_t,这里就简单说明一下。上文的源码中与 method_t 关系最近的应该要数 method_list_t 了,现在我们就通过它探究一下 method_t,下边是其源码:

struct method_list_t : entsize_list_tt {
    
    bool isFixedUp() const;
    void setFixedUp();

    // 函数:用于获取具体 method_t 的下标 index
    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        assert(i < count);
        return i;
    }
};

从源码能够看出来, method_list_t 继承自 entsize_list_tt,后者的定义如下,不过这里略去了定义,重点看上方的说明部分:

/***********************************************************************
* entsize_list_tt
* Generic implementation of an array of non-fragile structs. // 一种xxx结构体数组的实现
*
* Element is the struct type (e.g. method_t)
* List is the specialization of entsize_list_tt (e.g. method_list_t)
* FlagMask is used to stash extra bits in the entsize field
*   (e.g. method list fixup markers)
**********************************************************************/
template 
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
    //...
}

从注释我们了解到,模板中的 List 是由一个个的 Element 组成的数组,结合前边 method_list_t 的定义可知,method_list_t 是由 method_t 组成的数组,method_t 的结构如下:

struct method_t {
    SEL name;          // 名字
    const char *types; // 类型
    MethodListIMP imp; // 指针

    //...
};

method_t 的结构中主要包含3个元素:①方法名 name、②方法类型 char *types、③指向方法实现的指针 imp。

注:property_tprotocol_t 与此类似,就不多做介绍了,详见 代码注释。

4.3 isa_t

4.3.1 isa_t

objc_object 这个结构体里边 isa 的类型是个共用体 union isa_t ,其结构如下(其中 struct {...} 的作用只是让各位的含义可视化):

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
    
#if defined(ISA_BITFIELD)   // 位域
    struct {
        ISA_BITFIELD;       // defined in isa.h
    };
#endif
};

从 64 位架构开始引入了位域,可以在isa 中存储更多信息,上边结构体中的 ISA_BITFIELD 定义如下:

// isa.h
# if __arm64__      // 64位真机
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
# elif __x86_64__   // 64位模拟器·  
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
# else
#   error unknown architecture for packed isa
# endif

下面这张图以 64 位真机为例,详细说明了各位的作用:

深入理解 Objective-C ☞ Class_第7张图片
isa位域.png

前边 3.2 说过,从 64 位架构开始,需要通过 isa.bits & ISA_MASK 才能得到对应类对象或元类对象的地址,其实就是为了取出 shiftcls 部分。

在前文 struct objc_object 的结构中,我们发现 isa 是私有的,外部只能通过 ISA()getIsa() 这两个方法访问,下面分别看一下 ISA() 的源码。

#if SUPPORT_NONPOINTER_ISA

inline Class objc_object::ISA()
{
    // 如果是 TaggedPointer 就会中断言
    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
}

上边是 ISA() 的源码,其中条件编译的条件是 SUPPORT_INDEXED_ISA,定义如下:

// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa field as an index into a class table.
#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

其中,_ _ ARM_ARCH_7K _ _ 是 Apple Watch 会用到的,LP64 指的是 Long Pointer 64位,现在绝大多数 Unix 平台均使用 LP64 数据模型,所以一般情况下 SUPPORT_INDEXED_ISA 的值为 0,也就是说 ISA() 会执行 else 中的代码,即 isa.bits & ISA_MASK。

4.3.2 Tagged Pointer

我们注意到 getIsa() 的如下说明,也就是说,getIsa() 允许当前对象(this)是一个 Tagged Pointer 对象。那么,下面我们就来了解一下这个东东。

getIsa() allows this to be a tagged pointer object.

Tagged Pointer 是苹果在发布 iPhone 5s(搭载 64 位架构的 A7处理器)时提出的,它的优势在于,对于小对象(如NSNumber、NSDate等)能够大大地节省内存和提高执行效率。

我们以下面这行代码为例,比较一下引入 Tagged Pointer 前后占用内存的变化。

NSNumber *num = @(2);

如下图所示,当从 32 位机器迁移到 64 位机器后,如果没有引入 Tagged Pointer,虽然逻辑未改变,但是所占用的内存会翻倍;如果引入了 Tagged Pointer,因为存储 NSNumber 变量(此处为 @(2))本身的值常常用不了 8 个字节,于是会将一个对象的指针(8 个字节)拆成两部分,一部分直接保存数据,另一部分作为特殊标记,标记是否是 “特殊指针”。

深入理解 Objective-C ☞ Class_第8张图片

当然,这也是有一定限制的,当 8 个字节可以承载要表示的数值时,系统就会以 Tagged Pointer 的方式生成指针;如果 8 个字节承载不了,则又会用以前的方式来生成普通的指针。

5.小结

关于 Class 的讨论就先讨论到这里,可能有些地方理解的还不是很到位,后边会及时更新的 O(∩_∩)O~

# 参考

  • objc 苹果官方源码 750.1
  • wwdc2013-404 视频:Advances in Objective-C
  • 深入理解 Tagged Pointer
  • 数据模型(LP32 ILP32 LP64 LLP64 ILP64 )
  • _ _ ARM_ARCH_7K _ _ 部分

你可能感兴趣的:(深入理解 Objective-C ☞ Class)