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++ 代码,整理相关代码后,我们可以得出下图的关系:
其中,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_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 是用来在继承体系中搜寻父类的,如下图所示:
- 对于类对象:子类(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:
// 此方法返回的不是 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 *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
里边是在运行时扩展了累的方法、属性等信息以后的结构,比如,分类中的方法就是加到了这个结构里。
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_t
和protocol_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 位真机为例,详细说明了各位的作用:
前边 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 个字节)拆成两部分,一部分直接保存数据,另一部分作为特殊标记,标记是否是 “特殊指针”。
当然,这也是有一定限制的,当 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 _ _ 部分