深入理解 Objective-C ☞ Class
栏目: Objective-C · 发布时间: 5年前
内容简介:从事 iOS 开发已有 3 年多时间,大部分时间都是在用 Objective-C 开发 App(最近也在做 OC 与 Swift 的混编实践),虽然对 OC 底层知识有一定的了解,不过都是零散的片段,计划趁着过年的时间将这些片段梳理串联起来,于是便有了这个系列。本文是第 1 篇,从我们平常用的最多的
从事 iOS 开发已有 3 年多时间,大部分时间都是在用 Objective-C 开发 App(最近也在做 OC 与 Swift 的混编实践),虽然对 OC 底层知识有一定的了解,不过都是零散的片段,计划趁着过年的时间将这些片段梳理串联起来,于是便有了这个系列。
本文是第 1 篇,从我们平常用的最多的 类
和 对象
开始,深入探究他们的实现机理。
1.概述
我们平常编写的 OC
代码都会先编译成 C/C++代码
,然后再依次翻译成 汇编代码
、 机器码(01代码)
,最后,机器会自动运行该机器语言程序,并将计算结果输出。为了探究 OC
的本质,通过 C/C++
是比较合适的方式,因为之后的汇编和01代码看着太费劲(主要是自己只了解点皮毛(⊙﹏⊙)b),而 OC 本身又不是开源的。
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++ 代码,整理相关代码后,我们可以得出下图的关系:
其中,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
方法创建出来的,如下边的staffA
、staffB
:
HHStaff *staffA = [[HHStaff alloc] init]; HHStaff *staffB = [[HHStaff alloc] init]; NSLog(@"实例对象:%p - %p", staffA, staffB);
实例对象在内存中存储的信息包括:isa 指针 和 其他成员变量。
- 类对象(class),如下边的
staffClassA
、staffClassB
:
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);
类对象中包含的信息如下图所示,其中,成员变量信息指的是成员变量的描述信息,而非成员变量的值(在实例对象里边)。
- 元类对象(meta-class),如下边的
staffMetaClassA
、staffMetaClassB
:
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
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 是用来在继承体系中搜寻父类的,如下图所示:
- 对于类对象:子类(HHManager)的类对象的 superclass 指向父类(HHStaff)的类对象,父类的类对象的 superclass 指向它的父类的类对象;
- 对于元类对象:子类(HHManager)的元类对象的 superclass 指向父类(HHStaff)的元类对象,父类的元类对象的 superclass 指向它的父类的元类对象;
3.3 应用
下面我们来看看在消息发送过程中,这 3 种对象之间是如何亲密协作的。
先贴一张经典的关系图,实际就是将上一节中的 isa 和 superclass 指针放到了一起:
现在以 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 *ro
, class_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 位真机为例,详细说明了各位的作用:
前边 3.2 说过,从 64 位架构开始,需要通过 isa & ISA_MASK
才能得到对应类对象或元类对象的地址,其实就是为了取出 shiftcls 部分。
5.小结
关于 Class 的讨论就先讨论到这里,可能有些地方理解的还不是很到位,后边会及时更新的 O(∩_∩)O~
# 参考
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入理解 SecurityConfigurer
- 深入理解 HTTP 协议
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Lambda Calculus, Its Syntax and Semantics . Revised Edition
H.P. Barendregt / North Holland / 1985-11-15 / USD 133.00
The revised edition contains a new chapter which provides an elegant description of the semantics. The various classes of lambda calculus models are described in a uniform manner. Some didactical impr......一起来看看 《The Lambda Calculus, Its Syntax and Semantics . Revised Edition》 这本书的介绍吧!