美文网首页
Objective-C runtime

Objective-C runtime

作者: 羽裳有涯 | 来源:发表于2020-08-26 18:46 被阅读0次

前言

  • 消息发送(Messaging)Runtime 通过selector快速查找IMP的过程,有了函数指针就可以执行对应的方法实现;
  • 消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个 Objc 运行框架的一块基石。

Runtime其实有两个版本: “modern” 和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 maxOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime 基本是用 C 和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。

与 Runtime 交互

Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C 源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如 objc_msgSend 函数及其参数列表中的 id 和 SEL 都是啥)

NSObject 的方法

Cocoa中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是 NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime 的函数

Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在 Objective-C Runtime Reference 中有对 Runtime 函数的详细文档。

1、消息 - 消息发送
2、Runtime
3、selector
4、IMP
5、函数指针
6、消息转发(Message Forwarding)
7、Class结构
8、元类
9、id

runtime

与runtime交互

Objective-C 源代码
大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如 objc_msgSend 函数及其参数列表中的 id 和 SEL 都是啥)

Runtime 基础数据结构

还记得引言中的objc_msgSend:方法吧,它的真身是这样的:

id objc_msgSend ( id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。熟悉 Objective-C 类的内存模型或看过相关源码的可以直接跳过。

SEL 方法选择器

objc_msgSend函数第二个参数类型为SEL,它是selectorObjc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

typedef struct objc_selector *SEL;

这个objc_selector 结构体没有开源出来

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令 @selector() 或者Runtime系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber 一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。

id

objc_msgSend 第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针

typedef struct objc_object *id;

那objc_object又是啥呢,参考 objc-private.h 文件部分源码:

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ... 此处省略其他方法声明
}

objc_object 结构体包含一个 isa 指针,类型为 isa_t 联合体。

根据 isa 就可以顺藤摸瓜找到对象所属的类。isa 这里还涉及到 tagged pointer 等概念。因为 isa_t 使用 union 实现,所以可能表示多种形态,既可以当成是指针,也可以存储标志位。有关 isa_t 联合体的更多内容可以查看 Objective-C 引用计数原理

PS: isa 指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。
因为KVO的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档

Class

Class 其实是一个指向 objc_class 结构体的指针

typedef struct objc_class *Class;

而 objc_class 包含很多方法,主要都为围绕它的几个成员做文章:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    ... 省略其他方法
}

objc_class 继承于objc_object,也就是说一个ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象而每个类对象仅有一个与之相关的元类。当你发出一个类似 [NSObject alloc] 的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。


上图实线是 superclass 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而 isa 指向了自己,而 NSObject 的超类为 nil,也就是它没有超类。

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。

cache_t
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
      . .. 省略其他方法
}

_buckets 存储 IMP_mask_occupied对应vtable

cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在 cache 中查找Runtime 系统会把被调用的方法存到cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。

bucket_t 中存储了指针与 IMP 的键值对:

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;

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);
};

有关缓存的实现细节,可以查看 objc-cache.mm 文件。

class_data_bits_t

objc_class中最复杂的是 bitsclass_data_bits_t结构体所包含的信息太多了,主要包含 class_rw_t, retain/release/autorelease/retainCount 和 alloc 等信息,很多存取方法也是围绕它展开。
查看 objc-runtime-new.h 源码如下:

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
    class_rw_t* data() {
       return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
... 省略其他方法
}

注意objc_classdata方法直接将 class_data_bits_tdata 方法返回,最终是返回 class_rw_t,保了好几层。

可以看到 class_data_bits_t 里又包了一个 bits,这个指针跟不同的 FAST_ 前缀的 flag 掩码做按位与操作,就可以获取不同的数据。bits 在内存中每个位的含义有三种排列顺序:

其中 64 位不兼容版每个宏对应的含义如下:

// class is a Swift class
#define FAST_IS_SWIFT           (1UL<<0)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<1)
// class or superclass has .cxx_destruct implementation
//   This bit is aligned with isa_t->hasCxxDtor to save an instruction.
#define FAST_HAS_CXX_DTOR       (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL
// class or superclass has .cxx_construct implementation
#define FAST_HAS_CXX_CTOR       (1UL<<47)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_HAS_DEFAULT_AWZ    (1UL<<48)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<49)
// summary bit for fast alloc path: !hasCxxCtor and 
//   !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC              (1UL<<50)
// instance size in units of 16 bytes
//   or 0 if the instance size is too big in this field
//   This field must be LAST
#define FAST_SHIFTED_SIZE_SHIFT 51

这里面除了 FAST_DATA_MASK 是用一段空间存储数据外,其他宏都是只用 1 bit 存储 bool 值。class_data_bits_t提供了三个方法用于位操作:getBit,setBitsclearBits,对应到存储 bool 值的掩码也有封装函数,比如:

bool isSwift() {
   return getBit(FAST_IS_SWIFT);
}

void setIsSwift() {
   setBits(FAST_IS_SWIFT);
}

重头戏在于最大的那块存储区域–FAST_DATA_MASK,它其实就存储了指向 class_rw_t 的指针:

class_rw_t* data() {
   return (class_rw_t *)(bits & FAST_DATA_MASK);
}

对这片内存读写处于并发环境,但并不需要加锁,因为会通过对一些状态(realization or construction)判断来决定是否可读写。

class_data_bits_t甚至还包含了一些对class_rw_t 中 flags 成员存取的封装函数。

class_ro_t

objc_class 包含了 class_data_bits_tclass_data_bits_t 存储了class_rw_t 的指针,而 class_rw_t 结构体又包含class_ro_t 的指针。

class_ro_t 中的 method_list_t, ivar_list_t, property_list_t 结构体都继承自 entsize_list_tt<Element, List, FlagMask>。结构为 xxx_list_t 的列表元素结构为 xxx_t,命名很工整。protocol_list_t 与前三个不同,它存储的是protocol_t * 指针列表,实现比较简单。

entsize_list_tt 实现了 non-fragile特性的数组结构。假如苹果在新版本的 SDK 中向 NSObject 类增加了一些内容,NSObject 的占据的内存区域会扩大,开发者以前编译出的二进制中的子类就会与新的 NSObject 内存有重叠部分。于是在编译期会给 instanceStart 和 instanceSize 赋值,确定好编译时每个类的所占内存区域起始偏移量和大小,这样只需将子类与基类的这两个变量作对比即可知道子类是否与基类有重叠,如果有,也可知道子类需要挪多少偏移量。更多细节可以参考后面的章节 Non Fragile ivars。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    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;
    }
};
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;

    uint32_t entsize() const {
        return entsizeAndFlags & ~FlagMask;
    }
    uint32_t flags() const {
        return entsizeAndFlags & FlagMask;
    }

    Element& getOrEnd(uint32_t i) const { 
        assert(i <= count);
        return *(Element *)((uint8_t *)&first + i*entsize()); 
    }
    Element& get(uint32_t i) const { 
        assert(i < count);
        return getOrEnd(i);
    }

    size_t byteSize() const {
        return byteSize(entsize(), count);
    }
    
    static size_t byteSize(uint32_t entsize, uint32_t count) {
        return sizeof(entsize_list_tt) + (count-1)*entsize;
    }

    List *duplicate() const {
        auto *dup = (List *)calloc(this->byteSize(), 1);
        dup->entsizeAndFlags = this->entsizeAndFlags;
        dup->count = this->count;
        std::copy(begin(), end(), dup->begin());
        return dup;
    }
···· more
};

class_ro_t->flags 存储了很多在编译时期就确定的类的信息,也是 ABI 的一部分。下面这些 RO_ 前缀的宏标记了 flags 一些位置的含义。其中后三个并不需要被编译器赋值,是预留给运行时加载和初始化类的标志位,涉及到与 class_rw_t 的类型强转。运行时会用到它做判断,后面会讲解。

#define RO_META               (1<<0) // class is a metaclass
#define RO_ROOT               (1<<1) // class is a root class
#define RO_HAS_CXX_STRUCTORS  (1<<2) // class has .cxx_construct/destruct implementations
// #define RO_HAS_LOAD_METHOD    (1<<3) // class has +load implementation
#define RO_HIDDEN             (1<<4) // class has visibility=hidden set
#define RO_EXCEPTION          (1<<5) // class has attribute(objc_exception): OBJC_EHTYPE_$_ThisClass is non-weak
// #define RO_REUSE_ME           (1<<6) // this bit is available for reassignment
#define RO_IS_ARC             (1<<7) // class compiled with ARC
#define RO_HAS_CXX_DTOR_ONLY  (1<<8) // class has .cxx_destruct but no .cxx_construct (with RO_HAS_CXX_STRUCTORS)
#define RO_HAS_WEAK_WITHOUT_ARC (1<<9) // class is not ARC but has ARC-style weak ivar layout 

#define RO_FROM_BUNDLE        (1<<29) // class is in an unloadable bundle - must never be set by compiler
#define RO_FUTURE             (1<<30) // class is unrealized future class - must never be set by compiler
#define RO_REALIZED           (1<<31) // class is realized - must never be set by compiler
class_rw_t

class_rw_t提供了运行时对类拓展的能力,而class_ro_t 存储的大多是类在编译时就已经确定的信息。二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。

class_rw_t 中使用的method_array_t,property_array_t, protocol_array_t 都继承自 list_array_tt<Element, List>, 它可以不断扩张,因为它可以存储 list 指针,内容有三种:

  • 1、空
  • 2、一个 entsize_list_tt 指针
  • 3、entsize_list_tt 指针数组

class_rw_t的内容是可以在运行时被动态修改的,可以说运行时对类的拓展大都是存储在这里的。

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
    ... 省略操作 flags 的相关方法
}

class_rw_t->flags 存储的值并不是编辑器设置的,其中有些值可能将来会作为 ABI 的一部分。下面这些 RW_ 前缀的宏标记了 flags 一些位置的含义。这些 bool 值标记了类的一些状态,涉及到声明周期和内存管理。有些位目前甚至还空着。

#define RW_REALIZED           (1<<31) // class_t->data is class_rw_t, not class_ro_t
#define RW_FUTURE             (1<<30) // class is unresolved future class
#define RW_INITIALIZED        (1<<29) // class is initialized
#define RW_INITIALIZING       (1<<28) // class is initializing
#define RW_COPIED_RO          (1<<27) // class_rw_t->ro is heap copy of class_ro_t
#define RW_CONSTRUCTING       (1<<26) // class allocated but not yet registered
#define RW_CONSTRUCTED        (1<<25) // class allocated and registered
// #define RW_24 (1<<24) // available for use; was RW_FINALIZE_ON_MAIN_THREAD
#define RW_LOADED             (1<<23) // class +load has been called
#if !SUPPORT_NONPOINTER_ISA
#define RW_INSTANCES_HAVE_ASSOCIATED_OBJECTS (1<<22) // class instances may have associative references
#endif
#define RW_HAS_INSTANCE_SPECIFIC_LAYOUT (1 << 21) // class has instance-specific GC layout
// #define RW_20       (1<<20) // available for use
#define RW_REALIZING          (1<<19) // class has started realizing but not yet completed it
#define RW_HAS_CXX_CTOR       (1<<18) // class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_DTOR       (1<<17) // class or superclass has .cxx_destruct implementation
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ    (1<<16)
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA   (1<<15) // class's instances requires raw isa
#endif

demangledName 是计算机语言用于解决实体名称唯一性的一种方法,做法是向名称中添加一些类型信息,用于从编译器中向链接器传递更多语义信息。

template <typename Element, typename List>
class list_array_tt {
    struct array_t {
        uint32_t count;
        List* lists[0];

        static size_t byteSize(uint32_t count) {
            return sizeof(array_t) + count*sizeof(lists[0]);
        }
        size_t byteSize() {
            return byteSize(count);
        }
    };

 ..... more
}
realizeClass

在某个类初始化之前,objc_class->data() 返回的指针指向的其实是个 class_ro_t结构体。等到 static Class realizeClass(Class cls) 静态方法在类第一次初始化时被调用,它会开辟class_rw_t的空间,并将class_ro_t指针赋值给 class_rw_t->ro。这种偷天换日的行为是靠 RO_FUTURE标志位来记录的:

ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
   // This was a future class. rw data is already allocated.
   rw = cls->data();
   ro = cls->data()->ro;
   cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
   // Normal class. Allocate writeable class data.
   rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
   rw->ro = ro;
   rw->flags = RW_REALIZED|RW_REALIZING;
   cls->setData(rw);
}

注意之前 RO 和 RW flags 宏标记的一个细节:

#define RO_FUTURE             (1<<30)
#define RO_REALIZED           (1<<31)

#define RW_REALIZED           (1<<31)
#define RW_FUTURE             (1<<30)

也就是说ro = (const class_ro_t *)cls->data(); 这种强转对于接下来的ro->flags & RO_FUTURE 操作完全是 OK 的,两种结构体第一个成员都是 flags,RO_FUTURERW_FUTURE值一样的。

经过 realizeClass 函数处理的类才是『真正的』类,调用它时不能对类做写操作。

Category

Category为现有的类提供了拓展性,它是category_t结构体的指针。

typedef struct category_t *Category;

category_t存储了类别中可以拓展的实例方法、类方法、协议、实例属性和类属性。类属性是 Objective-C 2016 年新增的特性,沾 Swift 的光。所以 category_t 中有些成员变量是为了兼容 Swift 的特性,Objective-C 暂没提供接口,仅做了底层数据结构上的兼容。

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

在 App 启动加载镜像文件时,会在_read_images 函数间接调用到 attachCategories函数,完成向类中添加Category 的工作。原理就是向 class_rw_t中的 method_array_t,property_array_t, protocol_array_t 数组中分别添加 method_list_t, property_list_t, protocol_list_t指针。之前讲过 xxx_array_t 可以存储对应xxx_list_t的指针数组。

在调用 attachCategories 函数之前,会先使用 unattachedCategoriesForClass 函数获取类中还未添加的类别列表。这个列表类型为 locstamped_category_list_t,它封装了 category_t 以及对应的 header_infoheader_info 存储了实体在镜像中的加载和初始化状态,以及一些偏移量,在加载 Mach-O 文件相关函数中经常用到。

struct locstamped_category_t {
    category_t *cat;
    struct header_info *hi;
};

struct locstamped_category_list_t {
    uint32_t count;
#if __LP64__
    uint32_t reserved;
#endif
    locstamped_category_t list[0];
};

所以更具体来说 attachCategories 做的就是将 locstamped_category_list_t.list 列表中每个 locstamped_category_t.cat中的那方法、协议和属性分别添加到类的 class_rw_t 对应列表中。header_info 中的信息决定了是否是元类,从而选择应该是添加实例方法还是类方法、实例属性还是类属性等。源码在 objc-runtime-new.mm 文件中,很好理解。

Method

Method是一种代表类中的某个方法的类型。

typedef struct method_t *Method;

而 objc_method 在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
  • 方法名类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型 types 是个char指针,其实存储着方法的参数类型和返回值类型。
  • imp 指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

参考: Objective-C Runtime

相关文章

网友评论

      本文标题:Objective-C runtime

      本文链接:https://www.haomeiwen.com/subject/cgoctktx.html