Objective-C Runtime浅析

  • 前言
  • Runtime是什么
  • Runtime的实现原理
    • 消息传递机制
    • Runtime基础数据结构
      • NSObject & id
      • objc_object
      • Class
      • 元类(Meta Class)
      • Category
      • Ivar
      • Method
    • SEL与IMP的区别是什么? IMP如何寻址?
    • Category与Extension的区别?
  • Runtime的使用

前言

编译型的语言都需要经过编译之后再运行,OC编译器部分由Clang+ LLVM组成,编译的过程通常包括:预处理(Preprocessor)、编译(Compiler)、汇编(Assembler)、链接(Linker)这几个过程,然后才是加载运行。

预处理: 简化代码,过滤注释,处理宏定义(#开头关键字一般都和预处理有关);
编译:将预处理之后的文件编译成汇编语言文件;
汇编:将汇编文件转成机器可识别指令;
链接:将文件链接成可以执行的程序。

实际上编译的过程涉及很多繁琐复杂的内容,感兴趣的话可以看看编译原理。
正常情况下,静态语言,比如C语言,会在编译阶段确定数据类型,函数逻辑等等,从main()开始自上而下执行,这个过程中你将无法再修改执行的函数,事实上,这也正是C语言被称为面向过程的原因,你需要通过逻辑控制执行过程。
Objective-C是一门动态语言,它将从编译到链接执行的内容推迟到了实际运行之前决定。Objective-C通过消息传递机制确定执行的类型和方法,通过其运行时机制,你可以将消息重定向给适当的对象,甚至还可以交换方法的实现。Objective-C具有动态性都要归功于Runtime

Runtime是什么

Runtime是苹果公司设计的支持Objective-C动态性的库,Runtime是主要由C语言编写的,通过这个库为Objective-C言语添加了面向对象的特性和动态机制。所有的OC程序都会链接到Runtime库,它提供了动态类型、动态加载、动态绑定等一系列基础。

Runtime的实现

消息传递机制

Objective-C面向对象的实现是继承于Smalltalk的,即将所有的东西都当作对象,通过向对象发送消息来执行程序。例如:

[self doSomthing: var1];

上面调用方法,会被编译器转化为C语言的:

objc_msgSend(self, @selector(doSomething:), var1);

即向self(消息的接收者),发送@selector(doSomething:), var1是传递的参数。
对象self收到消息之后,具体调用哪个方法则会在运行时决定。换句话说,Objective-C在编译时并没有真正的连接要调用的方法,而是通过发送消息给对象的方式,在运行时去连接调用的方法。这也就是实现Objective-C动态性的核心原理。
而对象接收消息后又是如何连接到方法的?回答这个问题之前,我们不得不先看一下Runtime中是如何定义类,对象的。

Runtime基础数据结构

下面的定义都可以在Runtime源码Runtime源码中看到,源码中可以找到Class , NSObject, Protocol, Category, SEL等等概念的结构,下面我们一一分析。

NSObject & id

我们使用的大多数类都是继承自NSObject,那我们可以先查看下NSObject.h.

@protocol NSObject

- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;

@property (readonly) Class superclass;
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (instancetype)self;

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
...


@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
+ (void)load;

+ (void)initialize;
- (instancetype)init
...

从以上代码,可以看到NSObject类是遵循NSObject协议的,并且其中包含一个特殊的Class类型的成员变量isa, 这个isa其实是一个指向objc_class的指针,也就是指向自己类的指针。

如果继续查看objc.h, 我们还会发现特殊类型id的定义:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

所以id就是一个指向类实例的指针,所以我们可以用id类型来指代不确定类型的实例。 下面继续看objc_object.

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();
    ...

私有成员isa是一个union类型,C语言的union类型表示几个变量公用一个内存位置, 在不同的时间保存不同的数据类型和不同长度的变量。在arm64,Objective-C2.0下, isa的作用不仅仅是指向一个类实例的指针,它包含了引用计数、析构状态、关联对象、weak引用等等更多地信息。

isa_t源码定义如下:

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

    Class cls;    
    uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // indexed must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t indexed           : 1;  //0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
        uintptr_t has_assoc         : 1;  //表示该对象是否包含 associated object,如果没有,则析构时会更快
        uintptr_t has_cxx_dtor      : 1;  //表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
        uintptr_t shiftcls          : 33; //类的指针
        uintptr_t magic             : 6;  //固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
        uintptr_t weakly_referenced : 1;  //表示该对象是否有过 weak 对象,如果没有,则析构时更快
        uintptr_t deallocating      : 1;  //表示该对象是否正在析构
        uintptr_t has_sidetable_rc  : 1;  //表示该对象的引用计数值是否过大无法存储在 isa 指针
        uintptr_t extra_rc          : 19; //存储引用计数值减一后的结果
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
...

  #if __OBJC2__
  typedef struct method_t *Method;
  typedef struct ivar_t *Ivar;
  typedef struct category_t *Category;
  typedef struct property_t *objc_property_t;
};
....

SUPPORT_NONPOINTER_ISA宏定义表名这个isa不再只是指向类的指针,而是经过优化,包含更多信息。
其中clsobjc_class结构体指针类型,bits可以操作整个内存区,下面的结构体声明位域,上面只复制了arm64环境下的代码。
在Objective-C2.0时,对于Method, Category, Ivar, 属性进行了新的定义,后面我们一一来看这些结构体。

另外,

Class

看源码中的定义,Classobjc_class *,而objc_class继承自objc_object。(以前objc_class是单独定义的, Objective-C2.0做了修改)。

typedef struct objc_class *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();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }   
    ... 
  1. superclass指向父类。
  2. cache处理已调用方法的缓存。
  3. bits存储class_rw_t地址,并且定义了一些基本操作。
    class_rw_t 是非常重要的一个结构体,定义如下:
struct class_rw_t {
    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;
    ...

这里定义了方法、属性和协议列表,但是注意常量ro,是一个class_ro_t结构体指针,其定义如下:

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

其中也定义了带着"base"的方法、属性和协议列表,它们之间是什么关系呢?
我也十分困惑,但在这里找到了一些答案。编译class时, objc_classbits是指向ro的,然后创建rw时将bits里的ro赋值给rw,作为它的一个常量,最后bitsrw替换原先存储的ro
当然,类初始化后,这些属性、方法和协议列表都是空的,运行时会动态添加修改。

元类(Meta Class)

为了引出元类的概念,我们不得不重点提一下objc_classisa,这是从objc_object继承下来的。一个对象实例的方法是通过isa结构体保存的,那么一个类的类方法保存在哪?这就是objc_classisa存在的意义。
一个objc_classisa所指向的类实例,我们称之为元类(Meta Class)。看下面objc_class中判断元类和获取元类的几个方法:

    bool isMetaClass() {
        assert(this);
        assert(isRealized());
        return data()->ro->flags & RO_META;
    }

    // NOT identical to this->ISA when this is a metaclass
    Class getMeta() {
        if (isMetaClass()) return (Class)this;
        else return this->ISA();
    }

    bool isRootClass() {
        return superclass == nil;
    }
    bool isRootMetaclass() {
        return ISA() == (Class)this;
    }

isMetaClass的语句data()->ro->flags & RO_META, 表面这个flag字段是标识一个class是否是元类的。getMeta()是获取一个类的元类的方法,一个类如果是元类,它的元类会指向它本身,如果是一个普通类,则它的元类是指向同类型的类。
我们也引入一个经典的图片来做解释:

4618178-48f7c8b79f4c6e9a.png
class diagram.png

一个类的元类的父类,是其父类的元类,通常NSObject类是Root class(根类), 其superclass指向nil, 其元类也是NSObject类,而Root Meta class(根元类)的父类则指向NSObject(根类),这样形成一个闭环。

Category

Category就是我们常用的类别,查看源码定义,我们可以通过类别向已存在的类添加实例方法,实例属性,类方法,协议。
当Application启动时,加载类时会调用attachCategories(Class cls, category_list *cats, bool flush_caches)方法按类别加载顺序向类追加所有Category的相关内容。

/// An opaque type that represents a category.
typedef struct objc_category *Category;
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; // 添加的实例属性

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

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

关于Category方法调用覆盖问题,需要知道的是Category的方法是在运行时追加到类的方法列表顶部,IMP查找顺序:Category->Class->Super Class, 当在多个Category中定义同名方法时,当查找到第一个IMP时,就会立即返回,而不会继续查找,也就是会“覆盖”后面的方法。

Ivar

Ivar 代表类的实例对象。

typedef struct objc_ivar *Ivar;
Method

方法的定义,引出两个重要的概念,方法名标志类型SEL和入口指针类型IMP

typedef struct method_t *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
typedef struct objc_selector *SEL;

SEL本质是映射到方法的C字符串,也就是SEL字符串包含了方法名、参数等,它是方法选择器(selector)在Objc中的表示类型。

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

IMP是个函数指针,它指向的函数体就是对象接收消息后最终执行的代码。


SEL与IMP的区别是什么?IMP如何寻址?

SEL是方法选择器,它其实保存的是方法编号,而IMP是指向函数地址的指针,方法编号与方法地址一一对应存储在类的isa的dispatch table中。

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(cls, sel, nil, 
                         YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

上面是获取方法实现的函数,根据类和SEL来查找IMP,如果clssel传入的是nil, 直接返回nil, 否则通过IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)。继续查找:

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

这个方法主要还是调用lookUpImpOrForward方法,只不过用nil替换_objc_msgForward_impcache
终于找到核心实现的代码,我们看看lookUpImpOrForward方法的实现, 我将部分解释放在了注释里。


/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup  如果使用了 Optimistic cache,会先从这个cache中查找
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    // be added but ignored indefinitely because the cache was re-filled 
    // with the old value after the cache flush on behalf of the category.
 retry:
    runtimeLock.read();

    // Ignore GC selectors如果是忽略方法,goto执行done部分
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }

    // Try this class's cache. 从本类的缓存中查找(前面提过,一个类的在程序执行过的方法,会被添加到其cache_t中,这样再次调用时这个方法时会加快速度。)

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists. 从本类的方法列表查找

    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }

    // Try superclass caches and method lists.从父类的缓存中查找

    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.从父类的方法列表查找
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }

    // No implementation found. Try method resolver once. 从运行时添加的方法中去查找

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    // paranoia: look for ignored selectors with non-ignored implementations
    assert(!(ignoreSelector(sel)  &&  imp != (IMP)&_objc_ignored_method));

    // paranoia: never let uncached leak out
    assert(imp != _objc_msgSend_uncached_impcache);

    return imp;
}

总结一下查找流程:

  1. Optimistic cache 查找
  2. 忽略的方法中查找
  3. 当前类的缓存中查找
  4. 当前类的方法列表中查找
  5. 父类的缓存中查找
  6. 父类的方法列表中查找
  7. 动态添加的方法找查找

Category 和 Extension 在Objective-C中的区别

1,Category是运行时加载的,Category中的方法是动态添加到类的方法列表的。Extension是类的扩展,扩展中的方法会在编译时添加的。
2,类的成员变量在编译后不可变的,而方法列表是可变的,所以我们可以在Extension中定义成员变量,但是在Category中不可以。为Category添加属性时,我们也只能通过运行时实现属性的Setter和getter方法。(通过objc_setAssociatedObjectobjc_getAssociatedObject这两个方法)。
3, 我们可以为系统类型添加Category,但是不能为其添加Extension,因为Extension只能局限于类的实现中。

Runtime的使用

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

展开理解一下这三个层级:
Objective-C 源代码:OC源码底层都是需要和Runtime交互的,我们已经知道OC的类、方法、协议等等都是在Runtime中通过数据结构定义的,可以说我们所有的代码都是在Runtime上编写的。
NSObject类 : 我们在调用isKindOfClass:respondsToSelectorconformsToProtocol等等类型检查、方法检查时,都是在运行时进行的,这是从NSObject类的层面上,诸多方法是在使用Runtime。
Runtime的函数: #import <objc/runtime.h>之后,我们可以直接调用Runtime开放的接口,为我们的类动态添加方法,交互方法实现等等,比如MJRefresh,就是使用Runtime的方法为scrollview添加刷新headerfooter
runtime.h中,runtime通过OBJC_EXPORT宏将部分方法暴露出来给开发者直接使用,我们也可以查看苹果官方文档查看如何使用这部分API。

参考文章:
Objective-C Runtime 这篇介绍的很系统、很详细
iOS开发教程之Objc Runtime笔记 这篇也很不错,剖析源码,很深入。
Objective-C Runtime 苹果文档
Runtime源码
元类Mate-Class
Objective-C引用计数原理
https://github.com/DeveloperErenLiu/RuntimePDF

猜你喜欢

转载自blog.csdn.net/weixin_33935505/article/details/86807401