Fa45977a2c644370ae3df64f1ec2a668
Objective-C Runtime 解析(1) —— 从一个类开始

以下源码均来自 objc runtime 723

目录

一个简单的例子

@interface TestObject : NSObject
@end

这里,我们构建了一个非常简单的 OC 类 —— 仅仅继承了 NSObject,没有任何的属性、方法等。

NSObject

TestObject 这个类在 Runtime 中到底是如何表达的?我们先从 NSObject 看起:

@protocol NSObject

- (BOOL)isEqual:(id)object;
//...
- (id)performSelector:(SEL)aSelector;
//...
- (BOOL)isKindOfClass:(Class)aClass;
//...一大堆方法
@property (readonly, copy) NSString *description;
@end

@interface NSObject <NSObject> {
    /*
     这里的`OBJC_ISA_AVAILABILITY`比较有意思,在`objc-api.h`文件中找到了其定义,其中注释说道`isa`  
     会在未来废弃掉。看来苹果有过大改 Runtime 机制的想法,但最终放弃了。印象中因此才有了 Swift 的诞
     生
     */
    Class isa  OBJC_ISA_AVAILABILITY; 
}
//...一大堆方法
@end

可以看到,NSObject 类是一个遵循 NSObject 协议的大基类。NSObject 协议中包含了如 isEqual:, isKindOfClass:performSelector: 等抽象方法。而 NSObject 类中包含了对 NSObject 协议方法的默认实现,以及如 load, initcopyNSObject 类独有的方法。这些方法大多都是直接调用 Runtime 的私有 API 实现。例如经常用到的 respondsToSelector:方 法实现:

- (BOOL)respondsToSelector:(SEL)sel {
    if (!sel) return NO;
    return class_respondsToSelector_inst([self class], sel, self); //此方法暴露在`objc-private.h`头文件中
}

需要注意 NSObject 类中也有许多需要子类手动实现的抽象接口,例如:

// 子类需要重写`description`方法以返回正确的描述信息
- (NSString *)description {
    return nil;
}

id

提到了 NSObject 则不得不提在 OC 中非常常见的 id 类型 —— id 的定义为一个指向 objc_object 的指针:

typedef struct objc_object *id;

obj_object 的定义大致如下:

struct objc_object {
private:
    isa_t isa;

public:
    //...一大堆方法
private:
    //...一大堆方法

isa_t

可以看到 objc_object 中也有个 isa 指针,但与 NSObject 不同的是,这里的 isa 指针为 isa_t 类型。我们查看其源码发现,isa_t 为一个 union 联合体。(关于 union 的知识可以参考 联合体(union)的使用方法及其本质)

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits; // `uintptr_t`为unsigned long类型
}

除去上面的基础数据结构外,isa_t不同架构下会有不同的附加数据结构。例如在 arm64 环境下有 64 位的附加数据:

struct {
        uintptr_t nonpointer        : 1; // 是否开启指针优化,tagged pointer 相关
        uintptr_t has_assoc         : 1; // 是否有associated object
        uintptr_t has_cxx_dtor      : 1; // 是否有析构器
        uintptr_t shiftcls          : 33; // 类的指针,MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6; // 固定为0xd2
        uintptr_t weakly_referenced : 1; // 对象是否有弱引用
        uintptr_t deallocating      : 1; // 对象是否正在析构
        uintptr_t has_sidetable_rc  : 1; // 对象的引用计数是否过大无法存储在isa中
        uintptr_t extra_rc          : 19; // 对象的(引用计数-1),必须为最高有效位
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

这里用图片的方式更直观的描述下 isa_t 在 arm64 架构下内存中的存储:

isa_t

isa_t

这里非常有意思的是 Tagged Pointer,在 WWDC 2013 的 Advances in Objective-C 中提到了 Runtime 中对于 Tagged Pointer 的应用。Tagged Pointer 主要应用于对指针内存的优化上。关于 Tagged Pointer 的问题,巧哥的一篇 深入理解Tagged Pointer 作了深入解释。这里只简单介绍一下。

在内存存储中,指针的的地址是内存对齐,且与系统架构相关的。例如在 64 位的机器上,一个对象指针为 64 位。而指针中只有 60 位是真正的对象指针,剩余的四个低位主要为了内存对齐。

例如在 Objective-C 中,一个整数类型的 NSNumber 对象在 32 位系统中占用的内存大小为 4 个字节,但迁移到 64 为系统后,其所占的内存大小会翻倍,由 4 个字节变为 8 个字节。

苹果引入了 Tagged Pointed 技术后,对于一些如 NSNumberNSDate 等长度不变且内存占用较小的对象,优化了其指针内存性能。通过对指针最低位设置为 1 来标明 Tagged Pointer,这时指针会被分成两部分 —— 保存数据的部分和附加标记的部分(例如引用计数等)。

Class

NSObject 类的定义中,可以看到其只有 Class 类型一个公开的成员变量 isaClass 的定义十分简单:一个指向 objc_class 结构体的指针:

typedef struct objc_class *Class;
top Created with Sketch.