深入理解 Objective-C ☞ Class

栏目: Objective-C · 发布时间: 6年前

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

深入理解 Objective-C ☞ Class

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

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

1.概述

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

深入理解 Objective-C ☞ Class

2.从一个 :chestnut: 开始

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

其中,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

  • 元类对象(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

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

注意,这里并没有说 isa 指向哪里,而是说通过 isa 可以找到哪里,这是因为从 64bit 架构开始,isa 里边存储的不再是类对象或者元类对象的地址,而是需要进行一次位运算( isa & ISA_MASK )才能得到相应的地址,其中 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

  • 对于类对象:子类(HHManager)的类对象的 superclass 指向父类(HHStaff)的类对象,父类的类对象的 superclass 指向它的父类的类对象;
  • 对于元类对象:子类(HHManager)的元类对象的 superclass 指向父类(HHStaff)的元类对象,父类的元类对象的 superclass 指向它的父类的元类对象;

3.3 应用

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

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

深入理解 Objective-C ☞ Class

现在以 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:
    // *** 此处又略去好多行 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~
}

里边有一个只读(ro)的 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 里边是在运行时给扩展了累的方法、属性等信息以后的结构,比如在分类中添加的方法就是加到了这个结构里,前者里边有成员变量,而且是只读的,但后者没有,这也就解释了为什么不能通过分类添加成员变量,当然分类里是可以添加属性的,只不过需要自己借助关联对象实现 setter 和 getter,这个 下一篇 会讲到。

4.3 isa_t

objc_object 这个结构体里边 isa 的类型是个共用体 union isa_t ,其结构如下:

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

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

5.小结

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

# 参考


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Twenty Lectures on Algorithmic Game Theory

Twenty Lectures on Algorithmic Game Theory

Tim Roughgarden / Cambridge University Press / 2016-8-31 / USD 34.99

Computer science and economics have engaged in a lively interaction over the past fifteen years, resulting in the new field of algorithmic game theory. Many problems that are central to modern compute......一起来看看 《Twenty Lectures on Algorithmic Game Theory》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码