一些收藏

Runtime从入门到进阶一

2020-11-03  本文已影响0人  pro648

Objective-C 语言尽可能将决策从编译时间、链接时间推迟到运行时。只要有可能,它就会动态地执行任务。这意味着 Objective-C 不仅需要编译器,还需要运行时系统(runtime system)执行编译的代码。Objective-C 的动态性就是由 runtime 来支撑和实现的。

借助 runtime 可以实现很多功能,如字典转模型(MJExtension),查看私有成员变量,替换方法实现(method swizzling),为分类增加属性(associated objects)等。JSPatch热更新也是利用了 runtime,以便实现动态添加、改变方法实现。

关于 Objective-C runtime 的内容有很多,这里将分两篇文章介绍。本篇文章涉及内容如下:

1. Runtime 预览

Runtime API 提供的接口基本都是 C 语言的,源码由C、C++、汇编语言编写。Runtime 库为 C 语言添加了动态功能,还添加了使面向对象(object-oriented programming,简称OOP)成为可能所需要的支持。

1.1 动态、静态语言 Dynamic vs Static Language

Objective-C 是一种动态语言(dynamic language),它尽可能将决策从编译时间、链接时间推迟到运行时。

这一点与静态语言(如 C 语言)不同。在 C 语言中,调用函数意味着跳转到内存特定位置,其在编译时已经决定。因此,与诸如 Objective-C 这样的动态语言相比,灵活性要差很多。

先看下面代码:

    Engineer *engineer = [[Engineer alloc] initWithName:@"pro648"];
    
    [engineer sayHi];
    // compiler translates above line to:
    objc_msgSend(engineer, @selector(sayHi));

有一个名称为Engineer的类,调用sayHi方法。sayHi方法的实现并不会立即执行,编译器会将其转换为 C 语言的函数调用。objc_msgSend()函数向engineer实例对象发送消息,Objective-C 对象可能无法处理该消息,当无法处理时,会进入动态方法解析(dynamic method resolution)、消息转发(message forwarding)阶段。

Objective-C 中的方法调用都是转成objc_msgSend函数调用,给 receiver(方法调用者)发送一条消息(selector方法名)。

1.2 与 Runtime 交互

开发者在没有意识到的情况下已经在使用 runtime 了。从开始编写 iOS、macOS 程序起,我们就被告知需要继承自NSObject。这是因为许多麻烦的功能(如内存管理)都集成在NSObject中。只要使用NSObject的子类,就会自动获得这些基础功能。

第二种与 runtime 交互的情况是调用 runtime 函数。大多数时候,我们不需要使用 runtime 函数,但 runtime 有时可以帮我们解决一些棘手的问题。导入<objc/runtime.h>即可使用 runtime。

2. 对象和类

在面向对象的程序中,类(class)是可扩展的代码模版,是逻辑和数据的抽象;对象(object)是 class 的特定实例。下面将介绍 Objective-C 中对象和类的表示方式。

2.1 对象 Object

在objc4源码中,object 定义如下:

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA(bool authenticated = false);

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

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    uintptr_t isaBits() const;

    // initIsa() should be used to init the isa of new objects only.
    // If this object already has an isa, use changeIsa() for correctness.
    // initInstanceIsa(): objects with no custom RR/AWZ
    // initClassIsa(): class objects
    // initProtocolIsa(): protocol objects
    // initIsa(): other objects
    void initIsa(Class cls /*nonpointer=false*/);
    void initClassIsa(Class cls /*nonpointer=maybe*/);
    void initProtocolIsa(Class cls /*nonpointer=maybe*/);
    void initInstanceIsa(Class cls, bool hasCxxDtor);

    // 省略...
    
#if DEBUG
    bool sidetable_present();
#endif
}

可以看到,Objective-C 中的对象本质上是结构体。

这篇文章使用objc4-818.2版本源码。

实例(instance)对象在内存中存储了以下信息:

2.2 类 Class

Objective-C 是一个基于类的对象系统。每个 instance 对象都是某个类的实例,instance 对象的isa指针指向 class。

类对象存储信息如下:

Class是指向objc_class结构体的指针。

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

objc_class结构体如下:

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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 getSuperclass() const {
#if __has_feature(ptrauth_calls)
#   if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
        if (superclass == Nil)
            return Nil;

#if SUPERCLASS_SIGNING_TREAT_UNSIGNED_AS_NIL
        void *stripped = ptrauth_strip((void *)superclass, ISA_SIGNING_KEY);
        if ((void *)superclass == stripped) {
            void *resigned = ptrauth_sign_unauthenticated(stripped, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
            if ((void *)superclass != resigned)
                return Nil;
        }
#endif
            
        void *result = ptrauth_auth_data((void *)superclass, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
        return (Class)result;

#   else
        return (Class)ptrauth_strip((void *)superclass, ISA_SIGNING_KEY);
#   endif
#else
        return superclass;
#endif
    }

    void setSuperclass(Class newSuperclass) {
#if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
        superclass = (Class)ptrauth_sign_unauthenticated((void *)newSuperclass, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
#else
        superclass = newSuperclass;
#endif
    }

    class_rw_t *data() const {
        return bits.data();
    }
    
    // 省略部分...

    unsigned classArrayIndex() {
        return bits.classArrayIndex();
    }
}
2.2.1 isa

objc_class继承自objc_object。因此,objc_class结构体第一个成员也是isa_t,这表明 Objective-C 中类本质上也是一个对象。

这意味着可以将消息发送给类对象,就像发送给实例对象一样。当给实例对象发送消息时,runtime 会查询其类对象是否可以响应该消息。objc_class结构体中的class_data_bits_t bits;包含了方法列表,这使添加、移除、交换方法得以实现。

    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

isa_t共用体结构如下:

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

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

在arm64架构之前,isa就是普通指针,直接存储类对象、元类对象地址值。arm64架构开始,对isa进行了优化,变成了共用体(union)结构,使用位域来存储更多的信息。

2.2.2 superclass

superclass指针指向父类。如果它已经是最顶级的类(如NSObjectNSProxy),则superclass指针为NULL

在消息传递时,如果在当前类找不到该方法,会根据superclass指针进入父类查找。

2.2.3 cache_t

向实例对象发送消息时,runtime 根据isa指针找到类对象,然后在类对象class_rw_t中查找;如果找不到方法,继续在父类class_rw_t中查找,直到找到方法或查找失败。如果每次都需要进行这样的查找,会非常耗时。

为了提高查找性能,runtime 使用哈希表存储了当前类已经查找过的方法。使用selector & mask做为 key,将selector存储到buckets中。不同方法 & mask 后可能产生相同 key。如果遇到已经被占用,其会减一再次尝试,直到循环到初次计算出的位置。取方法时,取出后会先比较selector。如果不同,key减一再次比较。哈希表用空间换时间,牺牲内存提高效率。

最终,发送消息时,类会先查找cache_t是否存在该方法。如果存在,则直接调用;如果不存在,首先进入objc_method_list查找;如果找到,调用该方法并添加到当前类的cache_t;如果找不到,则根据super_class指针,进入父类查找,这里也会先在cache_t查找。如果找到,调用该方法并添加到消息接受者类的cache_t(不是父类的cache_t)。依此类推,直到找到该方法,或根类也找不到,进入方法动态解析阶段。

2.3 元类 meta class

Objective-C 的 class 也是一个对象,有isa指针和其他数据,可以响应 selector。当调用[NSObject alloc]类似的类方法时,本质上是向类对象发送消息。

类是元类(metaclass)的实例。metaclass 是类对象的描述,就像类是对实例对象的描述。类对象的isa指针指向元类。metaclass 的 method list 包含类方法,当向类对象发送消息时,objc_msgSend()根据 metaclass(和其父类) 的 method list 查找方法实现。

类对象、元类对象都是 Class 类型。因此,内存结构是一样的,但用途不同。meta class 在内存中存储信息如下:

类对象描述实例对象的行为,元类描述类对象的行为。

将变量值存储在实例对象,可以满足不同实例有不同值的需求。而实例方法、变量描述(类型、名称)信息、协议信息等,不同实例间没有区别,放到类对象中可以减少实例对象内存占用。否则,每个实例都要存储一份实例方法、变量信息等。

内存中,只有一个类对象、元类对象,可能有多个实例对象。

meta-meta class?

你或许会想 meta class 的isa指针指向哪里?是否有元类的元类?

为避免这种无限递归,Objective-C 的创建者让所有元类的isa指针指向根元类,根元类的isa指针指向自身。

现在,已经对类结构有了完整的了解。Runtime 工程师 Greg Parker 在他的博客贴了张非常清晰的图表,如下:

RuntimeClassDiagram.png

metaclass 的父类与类的父类链条平行。因此,查找类方法与查找实例方法类似。

root meta class 的父类是 root class。因此,层级结构中的所有实例、类、元类都将继承自基类,root class 的实例方法对所有实例、类、元类均有效。root class 的类方法对所有类、元类都有效。

下面是两个简单的类,Person继承自NSObjectEngineer继承自Person。在Engineer类实现了一些方法,testMetaClass方法查找isa指针指向并输出;testSuperClass方法查找super_class指针指向并输出:

- (void)testMetaClass {
    NSLog(@"----- %s -----", __func__);
    NSLog(@"This object is %p", self);
    NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
    
    Class currentClass = [self class];
    for (int i=0; i<4; ++i) {
        NSLog(@"Following the isa pointer %d times gives %p", i+1, currentClass);
        currentClass = object_getClass(currentClass);
    }
    
    // 不能通过[Person class]获得元类
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

- (void)testSuperClass {
    NSLog(@"----- %s -----", __func__);
    NSLog(@"This object is %p.", self);
    NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
    
    Class currentClass = [self class];
    Class currentMetaClass = object_getClass(currentClass);
    for (int i=0; i<4; ++i) {
        NSLog(@"Following the super pointer %d times gives %p", i+1, currentClass);
        currentClass = class_getSuperclass(currentClass);
    }
    
    for (int i=0; i<5; ++i) {
        NSLog(@"Following the meta class super pointer %d times gives %p", i+1, currentMetaClass);
        currentMetaClass = class_getSuperclass(currentMetaClass);
    }
    
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

执行以下代码:

    Engineer *engineer = [[Engineer alloc] initWithName:@"pro648"];
    
    [engineer testMetaClass];
    [engineer testSuperClass];

输出如下:

----- -[Engineer testMetaClass] -----
This object is 0x6000037b7950
Class is Engineer, and super is Person.
Following the isa pointer 1 times gives 0x10f7956c0
Following the isa pointer 2 times gives 0x10f7956e8
Following the isa pointer 3 times gives 0x1100de1d8
Following the isa pointer 4 times gives 0x1100de1d8
NSObject's meta class is 0x1100de1d8

----- -[Engineer testSuperClass] -----
This object is 0x6000037b7950.
Class is Engineer, and super is Person.
Following the super pointer 1 times gives 0x10f7956c0
Following the super pointer 2 times gives 0x10f795648
Following the super pointer 3 times gives 0x1100de200
Following the super pointer 4 times gives 0x0
Following the meta class super pointer 1 times gives 0x10f7956e8
Following the meta class super pointer 2 times gives 0x10f795620
Following the meta class super pointer 3 times gives 0x1100de1d8
Following the meta class super pointer 4 times gives 0x1100de200
Following the meta class super pointer 5 times gives 0x0
NSObject's meta class is 0x1100de1d8

指针指向的具体地址并不重要,但可跟踪isa指向。

engineer实例内存地址是0x6000037b7950,它的类对象地址是0x10f7956c0,它的元类地址是0x10f7956e8,root meta class 地址是0x1100de1d8,root meta class 的isa指针指向自身。

RuntimeClassDiagramB.png

通过testSuperClass方法的输出,可以跟踪super_class层级结构。root meta class 的 super class 是0x1100de200,也就是NSObject类对象。NSObject的父类是NULL

3. Method

类的方法列表是实例对象方法的集合。当向实例对象发送消息时,objc_msgSend()在其类对象(和类对象的父类)的method_array_t中查找方法。

class_rw_t里面的methods、properties、protocols数组是二维的,是可读可写的。源码如下:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    // 省略...

    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }

    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }

    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
}

method_array_t源码如下:

class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
        return beginLists();
    }
    
    const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};

method_array_t里包含method_list_tmethod_list_t里包含method_tmethod_t源码如下:

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    
    // 省略...

    SEL name() const {
        if (isSmall()) {
            return (small().inSharedCache()
                    ? (SEL)small().name.get()
                    : *(SEL *)small().name.get());
        } else {
            return big().name;
        }
    }
    const char *types() const {
        return isSmall() ? small().types.get() : big().types;
    }
    
    // 省略...

    void setName(SEL name) {
        if (isSmall()) {
            ASSERT(!small().inSharedCache());
            *(SEL *)small().name.get() = name;
        } else {
            big().name = name;
        }
    }

    void setImp(IMP imp) {
        if (isSmall()) {
            remapImp(imp);
        } else {
            big().imp = imp;
        }
    }
};

method_t包含SELtypesMethodListIMP

3.1 SEL

在 Objective-C 中,selector 是一个 C 的数据结构,可以把它看作是方法的 id。在 runtime 中定义如下:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

object_selector是不透明类型,可以把它当作方法名称,但 runtime 不是直接存储方法名称,而是将其映射为层级结构中唯一的字符串。这也是为什么类中不能有名称相同、参数类型不同的方法。

3.2 IMP

IMP指针指向函数的实现:

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

如果将 signature 与objc_msgSend进行比较,会发现其实际上是相同的。其参数都包含一个对象、一个 selector,外加可变数量的参数。按照约定,runtime 将self作为第一个参数传递,将当前 selector 作为第二个参数传递。

这就是为什么可以在方法内调用self_cmd,以及添加 C 函数时,需要添加self_cmd参数。

3.3 Method Type

method_types存储方法返回值类型、参数类型,runtime 将这些信息编码为一个字符串。具体规则可以查看Type Encodings文档。

也可以通过@encode指令获取编码后的字符。例如:

    char *intTypeCode = @encode(int);
    char *voidTypeCode = @encode(void);
    
    NSLog(@"int: %s, void:%s",intTypeCode, voidTypeCode);

输出如下:

int: i, void:v

3. 消息发送

结合前面的介绍,我们已经知道 runtime 如何发送消息:

  1. 根据实例对象的isa指针找到类对象。

  2. 类对象的消息解析:

    1. 查看类对象的cache是否存在该方法。如果存在,直接调用;如果不存在,进入下一步。
    2. 查看类对象class_rw_t是否有该方法。如果存在,调用并添加到cache;如果不存在,进入下一步。
    3. 查看父类的cache是否存在该方法。如果存在,调用并添加到消息接收者的cache;如果不存在,进入下一步。
    4. 查看父类class_rw_t是否有该方法。如果存在,调用并添加到消息接收者cache;如果不存在,进入下一步。
    5. 以此类推,直到找到根类。

    如果在3、4及其它父类中找到该方法,会将其添加到消息接收者的cache,即 receiver 的cache

  3. 动态方法解析。

  4. 消息转发。

下一篇文章Runtime从入门到进阶二将介绍动态方法解析、消息转发,以及runtime在项目中的具体应用。

Demo名称:Runtime
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Runtime

参考资料:

  1. [objc explain] - Non-pointer isa
  2. [objc explain] - Classes and metaclasses
  3. Greg Parker objc
  4. Digging Into the Objective-C Runtime - Part I
  5. Understanding the Objective-C Runtime
  6. 从 NSObject 的初始化了解 isa

欢迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/Runtime%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E8%BF%9B%E9%98%B6%E4%B8%80.md

上一篇下一篇

猜你喜欢

热点阅读