深入理解 Objective-C ☞ Class

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

内容简介:从事 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~

# 参考


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

查看所有标签

猜你喜欢:

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

Java Web开发实战经典(基础篇)

Java Web开发实战经典(基础篇)

李兴华、王月清 / 清华大学出版社 / 2010-8 / 69.80元

本书用通俗易懂的语言和丰富多彩的实例,通过对Ajax、JavaScript、HTML等Web系统开发技术基础知识的讲解,并结合MVC设计模式的理念,详细讲述了使用JSP及Struts框架进行Web系统开发的相关技术。 全书分4部分共17章,内容包括Java Web开发简介,HTML、JavaScript简介,XML简介,Tomcat服务器的安装及配置,JSP基础语法,JSP内置对象,Java......一起来看看 《Java Web开发实战经典(基础篇)》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具