乐趣区

关于ios:深度剖析-Runtime

做很多需要或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码剖析相干的文章。
先问几个小问题:

  1. class_rw_t 的构造是数组,数组外面的元素是数组,那它是二维数组吗?
  2. 为什么 16 字节对齐的?
  3. 有类对象、为什么设计元类对象?
  4. Super 原理的什么?

浏览完本文,你会把握 Runtime 的原理和细节

动静语言

Runtime 是实现 OC 语言动静的 API。

动态语言:在编译阶段确定了变量数据类型、函数地址等,无奈动静批改。

动静语言:只有在运行的时候才能够决定变量属于什么类型、办法真正的地址,

对象 objc_object 存了:isa、成员变量的值

类 objc_class: superclass、成员变量、实例变量

@interface Person : NSObject
{NSString *_name;}
@property (nonatomic, strong) NSString *hobby;
@end

malloc_size((__bridge const void *)(p))    // 24 isa 占 8 字节 + _name 指针占 8 字节 + hobby 指针占 8 字节 = 24 
class_getInstanceSize(p.class)             // 32,零碎内存对齐 

为什么零碎是由 16 字节对齐的?

成员变量占用 8 字节对齐,每个对象的第一个都是 isa 指针,必须要占用 8 字节。举例一个极其 case,假如 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用 8 字节,其中的 n- m 个对象既有 isa 指针,又有成员变量。每个类交织排列,那么 CPU 在拜访对象的时候会消耗大量工夫去辨认具体的对象。很多时候会取舍,这个 case 就是工夫换空间。以 16 字节对齐,会放慢访问速度(参考链表和数组的设计)

class_rw_t、class_ro_t、class_rw_ext_t 区别?

class_ro_t 在编译期间生成的,class_rw_t 是在运行期间生成的。

那么什么是 class_rw_ext_t? 首先明确 2 个概念

  • clean memory:加载后不会被批改。当零碎内存缓和时,能够从内存中移除,须要时能够再次加载
  • dirty memory:加载后会被批改,始终处于内存中

Runtime 初始化的时候,遇到一个类,则会利用类的 class_ro_t 中的根底信息(methods、properties、protocols)来创立 class_rw_t 对象。class_rw_t 设计的目标就是为了 Runtime 所需(Category 减少属性、协定、动静减少办法等),然而实际上那么多类大多数状况只有少部分类才须要 Runtime 能力。所以 Apple 为了内存优化,在 iOS 14 对 class_rw_t 拆分出 class_rw_ext_t,用来存储 Methods、Protocols、Properties 信息,会在应用的时候才创立,节俭更多内存。

比方拜访 method 的过程

// 新版
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};
    }
}

有类对象、为什么设计元类对象

复用音讯机制。比方 [Person new]

元类对象: isa、元类办法、

objc_msgSend 设计初衷就是为了音讯发送很快。如果没有元类,则类办法也存储在类对象的办法信息中,则可能须要加额定的字段来标记某个办法是类办法还是对象办法。遍历或者寻找会比较慢。所以引入元类(繁多职责),设计元类的目标就是为了进步 objc_msgSend 的效率。

isa 实质

在 arm64 架构之前,isa 就是一个一般的指针,存储着 Class 或 Meta-Class 对象的内存地址。

在 arm64 之后,对 isa 进行了优化,变成了一个共用体(union)构造,还应用位域来存储更多的信息。

union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        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;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
};

struct 外部的成员变量能够指定占用内存位数,uintptr_t nonpointer : 1 代表占用 1 个字节

其中,构造体外面的属于”位域“

  • nonpointer:0,代表一般的指针,存储着 Class、Meta-Class 对象的内存地址;1,代表优化过,应用位域存储更多的信息
  • has_assoc:是否有设置过关联对象,如果没有,开释时会更快
  • has_cxx_dtor:是否有 C ++ 的析构函数(.cxx_destruct),如果没有,开释时会更快
  • shiftcls:存储着 Class、Meta-Class 对象的内存地址信息
  • magic:用于在调试时分辨对象是否未实现初始化
  • weakly_referenced:是否有被弱援用指向过,如果没有,开释时会更快
  • deallocating:对象是否正在开释
  • extra_rc:外面存储的值是援用计数器减 1(刚创立出的对象,查看这个信息位 0,因为存储着 - 1 之后的援用计数)
  • has_sidetable_rc:援用计数器是否过大无奈存储在 isa 中;如果为 1,那么援用计数会存储在一个叫 SideTable 的类的属性中

下面说的更快,是如何得出结论的?

查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话别离调用析构函数、移除关联对象等逻辑。

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();}

    return obj;
}

isa 在 arm64 之后必须通过 ISA_MASK 去查问 class(类对象、元类对象)真正的地址

0x0000000ffffffff8ULL 用程序员模式关上计算器

其中,构造体中的数据寄存大体是上面的构造:

extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer

晓得构造体能够指定存储大小这个性能后,能够看到 isa_t 联合体与 ISA_MASK 按位与之后的地址,其实就是类实在的地址信息(可能是类对象、也有可能是元类对象)

如果要找出上面两头的 1010 如何实现?按位与即可,且要找的地位补充位 1,其余地位为 0

0b0010 1000

0b0011 1100
-----------
0b0010 1000

论断:依据按位与的成果。ISA_MASK 的后 3 位都是 0,所以咱们找到的类地址二进制示意时后 3 位肯定为 0

咱们能够验证下

Person *p = [[Person alloc] init];
NSLog(@"%p", [p class]);    // 0x1000081d8
NSLog(@"%p", object_getClass([Person class])); // 0x100008200
NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0
NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28

为什么有的结尾是 8?

16 进制的 8 转为二进制,0x1000

对于这部分的调试,须要在真机上运行,真机上 arm64,拷贝对象地址到零碎自带的运算器(程序员模式),查看 64 位地址。依照上面的程序一一查看

extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer

所以能够依据 isa 信息查看对象是否创立过关联对象、有没有设置弱援用、

模拟零碎位运算设计 API

零碎很多 API 都有位或运算。比方 KVO 中的 options,能够传递 NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,那么零碎是如何晓得我到底传递了哪几个值?

按位或运算

0b0000 0001 // 1
0b0000 0010 // 2 
0b0000 0100 // 4
------------
0b0000 0111 // 7

能够看到下面 3 个数,按位或之后的后果为 0b0000 0111

按位与运算。

0b0000 0111
0b0000 0001
-----------
0b0000 0001

0b0000 0111
0b0000 0010
-----------
0b0000 0010

0b0000 0111
0b0000 0100
-----------
0b0000 0100

0b0000 0111
0b0000 1000
-----------
0b0000 0000

咱们发现下面 3 个数按位或之后的数字,别离与每个数按位与,失去的后果就是数据自身。

与一个不是 3 个数之一的数按位与,失去的后果为0b0000 0000。利用这个个性咱们能够判断传递来的参数是不是蕴含了某个值

typedef enum {
    OptionsEast = 1<<0,    // 0b0001
    OptionsSouth = 1<<1,   // 0b0010
    OptionsWest = 1<<2,    // 0b0100
    OptionsNorth = 1<<3    // 0b1000
} Options;

- (void)setOptions:(Options)options
{if (options & OptionsEast) {NSLog(@"我自东边来");
    }

    if (options & OptionsSouth) {NSLog(@"我自南边来");
    }

    if (options & OptionsWest) {NSLog(@"我自西边来");
    }

    if (options & OptionsNorth) {NSLog(@"我自北边来");
    }
}
[self setOptions: OptionsWest | OptionsNorth];
// 我自西边来
// 我自北边来

类对象 Class 的构造

查看 objc4 源代码看看

struct objc_object {
private:
    isa_t isa;
}

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

构造体继承于 objc_object 等同于上面代码

struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    cache_t cache;             // 办法缓存
    class_data_bits_t bits;    // 用于获取具体的类信息
};
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;
};

struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
public:

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

能够看到 objc_class 获取 bits 里的实在数据须要通过按位与 FAST_DATA_MASK

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; // instance 对象占用的内存空间
#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;}
};

具体关系整顿如下图

阐明:

  • class_rw_t外面的 methods、properties、protocols 是数组(数组元素是也是办法组成的 Array),是可读可写的,蕴含了类的初始内容、分类的内容。

    为什么不是二维数组?因为 Array 中的子 Array 长度不统一,且不能补空

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
    
        runtimeLock.assertWriting();
    
        isMeta = cls->isMetaClass();
    
        // Re-methodizing: check for more categories
        if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {if (PrintConnecting) {
                _objc_inform("CLASS: attaching categories to class'%s'%s", 
                             cls->nameForLogging(), isMeta ? "(meta)" : "");
            }
    
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches)
    {if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first
        int mcount = 0;
        int propcount = 0;
        int protocount = 0;
        int i = cats->count;
        bool fromBundle = NO;
        while (i--) {auto& entry = cats->list[i];
    
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();}
    
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {protolists[protocount++] = protolist;
            }
        }
    
        auto rw = cls->data();
    
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists, mcount);
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
        rw->properties.attachLists(proplists, propcount);
        free(proplists);
    
        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }

    查看 objc4 源码发现针对类本身信息、Category 信息会进行组合。

  • class_ro_t 外面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,蕴含了类的 (原始信息) 初始内容

Method_t

method_t 是对办法 \ 函数的封装

struct method_t {
    SEL name; // 函数名、办法名    
    const char *types;    // 编码(返回值类型、参数类型)IMP imp;    // 指向函数的指针(函数地址)}

IMP 代表函数的具体实现

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

SEL 代表办法、函数名,个别叫做选择器,底层构造跟 char * 相似

typedef struct objc_selector *SEL;
  • 能够通过 @selector()sel_registerName() 取得
  • 能够通过 sel_getName()NSStringFromSelector() 转成字符串
  • 不同类中雷同名字的办法,所对应的办法选择器是雷同的

types 蕴含了函数返回值、参数编码的字符串。返回值 | 参数 1 | 参数 2 | ... | 参数 n

Type Encoding

iOS 中提供了一个叫做 @encode 的指令,能够将具体的类型示意成字符串编码

- (int)calcuate:(int)age heigith:(float)height;

比方这个办法的 type encoding 为 i24@0:8i16f20

解读下,下面的办法其实携带了 2 个根底参数。

(id)self _cmd:(SEL)_cmd

i 代表办法返回值为 int

24 代表参数共占 24 个字节大小。4 个参数别离为 id 类型的 selfSEL 类型的 _cmd,int 类型的 age、float 类型的 height。8+8+4+4 共 24 个字节(id、SEL 都为指针,长度为 8)

@ 代表第一个参数为 object 类型,从第 0 个字节开始

:代表第二个参数为 SEL,从第 8 个字节开始

i 代表第三个参数为 int,从第 16 个字节开始

f 代表第四个参数为 float,从第 20 个字节开始

办法缓存

调用办法的实质,比如说对象办法,先依据对象的 isa 找到类对象,在类对象的 method_list_t 类型的 methods 办法数组(Array 中的元素是办法 Array)中(类的 Category1、类的 Category2… 类本身的办法)查找办法,找不到则调用 superclass 查找父类的 methods 办法数组(Array 中的元素是办法 Array),效率较低,所以为了不便,给类设置了办法缓存。比方调用 Student 对象的 eat 办法,eat 在 student 中不存在,通过 isa 一直找,在 Person 类中找到了,则将 Person 类中的 eat 办法缓存在 Student 的 cache_t 类型的 cache 中。

Class 内部结构中有个办法缓存(cache_t),用散列表(哈希表)来缓存已经调用过的办法,能够进步办法的查找速度

所以残缺构造为:先依据对象的 isa 找到类对象,在类对象的 cache 列表中查找办法实现,如果找不到,则去 method_list_t 类型的 methods 办法数组(Array 中的元素是办法 Array)中(类的 Category1、类的 Category2… 类本身的办法)查找办法,找不到则调用 superclass 查找父类的 cache 中查找,找到则调用办法,同时将父类 cache 缓存中的办法,在子类的 cache 中缓存一边。父类 cache 没找到,则在 methods 办法数组(Array 中的元素是办法 Array)查找,找到则调用,同时在子类 cache 中缓存一份。父类 methods 办法数组(Array 中的元素是办法 Array)没找到则持续调用 superclass,顺次类推

struct cache_t {
    struct bucket_t *_buckets; // 散列表
    mask_t _mask;              // 散列表的参数 -1
    mask_t _occupied;          // 曾经缓存的办法数量
}
struct bucket_t {
private:
    cache_key_t _key;    // SEL 作为 key
    IMP _imp;            // 函数的内存地址
}

_buckets -> | bucket_t |bucket_t |bucket_t |bucket_t |…

办法缓存查找原理,散列表查找

objc4 源码 objc-cache.mm

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);
    mask_t i = begin;
    do {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);
}

散列表不够了,则会哈希拓容,此时缓存会开释 cache_collect_free

void cache_t::expand()
{cacheUpdateLock.assertLocked();
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{bool freeOld = canBeFreed();
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this
    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    setBucketsAndMask(newBuckets, newCapacity - 1);
    if (freeOld) {cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

哈希查找元素外围是一个求 key 的过程,Java 中是求余,iOS 中是按位与 key & mask

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{return (mask_t)(key & mask);
}

空间换工夫的一个实现。

查找类的办法缓存 Demo

#import <Foundation/Foundation.h>

#ifndef MockClassInfo_h
#define MockClassInfo_h

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

struct eint main () {GoodStudent *goodStudent = [[GoodStudent alloc] init];
    mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class];
    [goodStudent goodStudentTest];
    [goodStudent studentTest];
    [goodStudent personTest];
    return 0;
}ntsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

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

struct method_list_t : entsize_list_tt {method_t first;};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {ivar_t first;};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {property_t first;};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  // instance 对象占用的内存空间
#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;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 办法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协定列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC 对象 */
struct mock_objc_object {void *isa;};

/* 类对象 */
struct mock_objc_class : mock_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {return bits.data();
    }

    mock_objc_class* metaClass() {return (mock_objc_class *)((long long)isa & ISA_MASK);
    }
};

#endif /* MockClassInfo_h */

@interface Person : NSObject
- (void)personSay;
@end

@interface Student : Person
- (void)studentSay;
@end

@interface GoodStudent : Student
- (void)goodStudentSay;
@end

int main () {GoodStudent *goodStudent = [[GoodStudent alloc] init];
    mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class];
    // breakpoints1
    [goodStudent goodStudentSay];
    // breakpoints2
    [goodStudent studentSay];
    // breakpoints3
    [goodStudent personSay];
    // breakpoints4    
    [goodStudent goodStudentSay];
    // breakpoints5
    [goodStudent studentSay];
    // breakpoints6
    NSLog(@"well donw");
    return 0;
}

流程:

断点 1 的中央能够看到 mock_objc_class 构造体 cache_occupied 为 1,_mask 为 3,初始化哈希表长度为 4

在断点 1 的中央,_occupied 为 1 则代表只有 init 办法被缓存,本行代码执行完,_occupied 为 2.

在断点 2 的中央,_occupied 为 2 则代表只有 init、goodStudentSay 办法被缓存。本行代码执行完,_occupied 为 3

在断点 3 的中央,_occupied 为 3 则代表只有 init、goodStudentSay、studentSay 办法被缓存。本行代码执行完,_occupied 为 1,且 _mask 为 7。

奇了怪了,为什么 _occupied为 1,且_mask 为 7?

因为哈希表长度为 4,缓存 3 个办法后,到第 4 个办法须要缓存的时候会执行哈希表拓容,缓存会生效。拓容策略为乘以 2 即 uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; 所以长度为 8,mask 为 长度 -1,则为 7,第 4 个办法刚好被缓存下来,_occupied 为 1。

void cache_t::expand()
{cacheUpdateLock.assertLocked();
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

持续运行

在断点 4 的中央,_occupied 为 1 则代表只有 personSay 办法被缓存。本行代码执行完,_occupied 为 2,且 _mask 为 7。

在断点 5 的中央,_occupied 为 2 则代表只有 personSay、goodStudentSay 办法被缓存。本行代码执行完,_occupied 为 3,且 _mask 为 7。

在断点 6 的中央,_occupied 为 3 则代表只有 personSay、goodStudentSay、studentSay 办法被缓存,_mask 为 7。

如何依据办法散列表查找某个办法

GoodStudent *student = [[GoodStudent alloc] init];
mock_objc_class *studentClass = (__bridge mock_objc_class *)[GoodStudent class];
[student goodStudentSay];
[student studentSay];
[student personSay];
NSLog(@"Well done");

cache_t cache = studentClass->cache;
bucket_t *buckets = cache._buckets;

bucket_t bucket = buckets[(long long)@selector(personSay) & cache._mask];
NSLog(@"%s %p", bucket._key, bucket._imp);
// personSay 0xbec8

原理就是依据类对象构造体找到 cache 构造体,cache 构造体外部的 _buckets 是一个办法散列表,查看源代码,依据散列表的哈希寻找策略 (key & mask) 找到哈希索引,而后找到办法对象 bucket,其中寻找办法索引的 key 就是 办法 selector。

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{return (mask_t)(key & mask);
}

objc_msgSend

oc 办法 (对象办法、类办法) 调用实质就是 objc_msgSend

[person eat];
objc_msgSend(person, sel_registerName("eat")); 
[Person initialize];
objc_msgSend([Person class], sel_registerName("initialize")); 

objc_msgSend 能够分为 3 个阶段:

  • 音讯发送
  • 动静办法解析
  • 音讯转发

查看源码 objc-msg-arm64.s

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
    // x0 寄存器代表音讯接受者,receiver。objc_msgSend(person, sel_registerName("eat")) 的 person
    cmp    x0, #0            // nil check and tagged pointer check
    // b 代表指令跳转。le 代表 小于等于。<= 0 则跳转到 LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr    x13, [x0]        // x13 = isa // ldr 代表加载指令。这里的意思是将 x0 寄存器信息写入到 x13 中
    and    x16, x13, #ISA_MASK    // x16 = class    // 这里就是将 x13 与  ISA_MASK 按位与,而后失去实在的 isa 信息,而后写入到 x16 中
LGetIsaDone:
    CacheLookup NORMAL        // calls imp or objc_msgSend_uncached // 这里执行 objc_msgSend_uncached 逻辑,CacheLookup 是一个汇编宏,看上面的阐明

LNilOrTagged:
    // 判断为 nil 则跳转到  LReturnZero
    b.eq    LReturnZero        // nil check

    // tagged
    mov    x10, #0xf000000000000000
    cmp    x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add    x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr    x16, [x10, x11, LSL #3]
    b    LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add    x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr    x16, [x10, x11, LSL #3]
    b    LGetIsaDone

LReturnZero:
    // x0 is already zero
    mov    x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    // 汇编中 ret 代表 return
    ret 

    END_ENTRY _objc_msgSend


.macro CacheLookup // 汇编宏,能够看到依据 (SEL & mask) 来寻找真正的办法地址
    // x1 = SEL, x16 = isa
    ldp    x10, x11, [x16, #CACHE]    // x10 = buckets, x11 = occupied|mask
    and    w12, w1, w11        // x12 = _cmd & mask
    add    x12, x10, x12, LSL #4    // x12 = buckets + ((_cmd & mask)<<4)

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #-16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // wrap: x12 = first bucket, w11 = mask
    add    x12, x12, w11, UXTW #4    // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    // 这里是办法查找失败,则走 checkMiss 逻辑,具体看上面
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #-16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // double wrap
    JumpMiss $0

.endmacro

// CheckMiss 汇编宏,下面走 Normal 逻辑,外部走 __objc_msgSend_uncached 流程
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz    x9, LGetImpMiss
.elseif $0 == NORMAL
    cbz    x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz    x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro


// __objc_msgSend_uncached 外部其实走  MethodTableLookup 逻辑
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search

MethodTableLookup
br    x17

END_ENTRY __objc_msgSend_uncached

// MethodTableLookup 是一个汇编宏,外部指令跳转到 __class_lookupMethodAndLoadCache3。.macro MethodTableLookup

    // push frame
    stp    fp, lr, [sp, #-16]!
    mov    fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub    sp, sp, #(10*8 + 8*16)
    stp    q0, q1, [sp, #(0*16)]
    stp    q2, q3, [sp, #(2*16)]
    stp    q4, q5, [sp, #(4*16)]
    stp    q6, q7, [sp, #(6*16)]
    stp    x0, x1, [sp, #(8*16+0*8)]
    stp    x2, x3, [sp, #(8*16+2*8)]
    stp    x4, x5, [sp, #(8*16+4*8)]
    stp    x6, x7, [sp, #(8*16+6*8)]
    str    x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov    x2, x16
    bl    __class_lookupMethodAndLoadCache3

    // imp in x0
    mov    x17, x0

    // restore registers and return
    ldp    q0, q1, [sp, #(0*16)]
    ldp    q2, q3, [sp, #(2*16)]
    ldp    q4, q5, [sp, #(4*16)]
    ldp    q6, q7, [sp, #(6*16)]
    ldp    x0, x1, [sp, #(8*16+0*8)]
    ldp    x2, x3, [sp, #(8*16+2*8)]
    ldp    x4, x5, [sp, #(8*16+4*8)]
    ldp    x6, x7, [sp, #(8*16+6*8)]
    ldr    x8,     [sp, #(8*16+8*8)]

    mov    sp, fp
    ldp    fp, lr, [sp], #16

.endmacro

Tips:c 办法在汇编中应用的时候,须要在办法名前加 _。所以在汇编中某个办法为 _xxx,则在其余中央查找实现,须要去掉 _
此时 __class_lookupMethodAndLoadCache3 在汇编中没有实现,则依照 _class_lookupMethodAndLoadCache3 查找

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search 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.

    runtimeLock.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();}

    if (initialize  &&  !cls->isInitialized()) {runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }


 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {Method 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.
    {unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {_objc_fatal("Memory corruption in class list.");
            }

            // 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.
            Method 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);
        runtimeLock.read();
        // 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();

    return imp;
}

音讯发送阶段

下面的代码走到 getMethodNoSuper_nolock 寻找类里的办法

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    // 这里依据类构造体找到 data(), 而后找到 methods(Array 数组,数组元素是办法 Array)/*
    data() 其实就是 class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    */
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 排好序则调用 `findMethodInSortedMethodList`,其外部是二分查找实现。if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 没排序则线性查找 
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {for (auto& meth : *mlist) {if (meth.name == sel) {_objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;

    for (count = list->count; count != 0; count >>= 1) {probe = base + (count >> 1);

        uintptr_t probeValue = (uintptr_t)probe->name;

        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {probe--;}
            return (method_t *)probe;
        }

        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }

    return nil;
}

cls->data()->methods.beginLists 这里依据类构造体调用到 data() 办法,获取到 class_rw_t

class_rw_t *data() {return bits.data();
}

而后通过 class_rw_t 找到 methods(Array 数组,数组元素是办法 Array)。外部调用 search_method_list 办法。

search_method_list 办法外部判断办法数组是否排好序

  • 排好序则调用 findMethodInSortedMethodList,其外部是二分查找实现。
  • 没排序,则线性查找 (Linear search of unsorted method list)

getMethodNoSuper_nolock 执行完则会将办法写入到以后类对象的缓存中。

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {// Cache is less than 3/4 full. Use it as-is.}
    else {
        // Cache is too full. Expand it.
        cache->expand();}

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

摘出 lookUpImpOrForward 办法中的一段代码

// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{Method 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.

如果代码没有找到,则不会 gotodone,开始走父类缓存查找逻辑

// Try superclass caches and method lists.
{unsigned attempts = unreasonableClassCount();
    // for 循环不断查找,找以后类的父类,直到以后类为 nil。for (Class curClass = cls->superclass;
            curClass != nil;
            curClass = curClass->superclass)
    {
        // Halt if there is a cycle in the superclass chain.
        if (--attempts == 0) {_objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 先在父类的办法缓存中查找(依据 sel & mask)`cache_getImp`,找到则将办法写入到本身类的办法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);`
        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.
        // 如果在父类的办法缓存中没找到,则调用 `getMethodNoSuper_nolock` 父类的 办法数组(Array 元素为办法数组),依照排序好和没排序好别离走二分查找和线性查找。Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
           // 如果找到则持续填充到以后类的办法缓存中去
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
} 

for 循环不断查找,找以后类的父类,直到以后类为 nil。

先在父类的办法缓存中查找(依据 sel & mask)cache_getImp,找到则将办法写入到本身类的办法缓存中去 log_and_fill_cache(cls, imp, sel, inst, curClass);

比方 Person 类有 eat 办法,Student 类有 stduy 办法,调用 Student 对象的 eat 办法,则会走到这里,从父类找到办法后写入到 Student 类的办法缓存中去。

如果在父类的办法缓存中没找到,则调用 getMethodNoSuper_nolock 父类的 办法数组(Array 元素为办法数组),依照排序好和没排序好别离走二分查找和线性查找。

如果找到则持续填充到以后类的办法缓存中去 log_and_fill_cache(cls, meth->imp, sel, inst, curClass);,最初 goto done

下面的流程是整个 objc_msgSend 的音讯发送阶段的整个流程。能够用下图示意

动静办法解析阶段

接着查看源码

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //...
    // No implementation found. Try method resolver once.
    if (resolver  &&  !triedResolver) {runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }
    // ...
}

void _class_resolveMethod(Class cls, SEL sel, id inst)
{if (! cls->isMetaClass()) {// try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {// try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {_class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

判断以后类没有走过动静办法解析阶段,则走动静办法解析阶段,调用 _class_resolveMethod 办法。

外部会判断但前类是不是元类对象、还是类对象走不同逻辑。

类对象走 _class_resolveInstanceMethod 逻辑

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {if (imp) {_objc_inform("RESOLVE: method %c[%s %s]"
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

外围就调用 bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); 运行 resolveInstanceMethod 办法。

元类对象走 _class_resolveClassMethod 逻辑

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {if (imp) {_objc_inform("RESOLVE: method %c[%s %s]"
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

其实就是调用 `bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);`

最初还是走到了 goto retry; 持续走残缺的音讯发送流程(因为增加了办法,所以会依照办法查找再去执行的逻辑)

残缺流程如下

上 Demo

Person *person = [[Person alloc] init];
[person eat];

调用不存在办法则报错 ***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person eat]: unrecognized selector sent to instance 0x101b2d900'**

因为调用对象不存在的办法,所以会 Crash

晓得 objc_msgSend 的流程,咱们尝试给它修改下

- (void)customEat {NSLog(@"我的假的 eat 办法,为了解决奔溃问题");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(eat)) {
        // 对象办法,存在于对象上。Method method = class_getInstanceMethod(self, @selector(customEat));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

也能够增加 c 语音办法

void customEat (id self, SEL _cmd) {NSLog(@"%@-%s-%s", self, sel_getName(_cmd), __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{if (sel == @selector(eat)) {
        // 对象办法,存在于对象上。class_addMethod(self, sel, (IMP)customEat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

因为 c 语言办法名就是函数地址,所以不须要间接传递即可,须要做下类型转换 (IMP)customEat

也能够给类办法做动静办法解析。须要留神的是类办法。

  • 调用 -(BOOL)resolveClassMethod:(SEL)sel
  • class_addMethod 办法中的第一个参数,须要加到类的元类对象中,所以是 object_getClass
Person *person = [[Person alloc] init];
[Person drink];
void customDrink (id self, SEL _cmd) {NSLog(@"假喝水");
}

+ (BOOL)resolveClassMethod:(SEL)sel
{if (sel == @selector(drink)) {
        // 类办法,存在于元类对象上。class_addMethod(object_getClass(self), sel, (IMP)customDrink, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

音讯转发阶段

能走到音讯转发,阐明

  1. 类本身没有该办法(objc_msgSend 的音讯发送)
  2. objc_msgSend 动静办法解析失败或者没有做

阐明类本身和父类没有能够解决该音讯的能力,此时应该将该音讯转发给其余对象。

查看 objc4 的源码

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //...
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
    // ...
}

持续查找 _objc_msgForward_impcache

STATIC_ENTRY __objc_msgForward_impcache

MESSENGER_START
nop
MESSENGER_END_SLOW

// No stret specialization.
b    __objc_msgForward
END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr    x17, [x17, __objc_forward_handler@PAGEOFF]
br    x1

END_ENTRY __objc_msgForward

查找 __objc_forward_handler 没有找到,能够猜测是一个 c 办法,去掉最后面的 _,依照 _objc_forward_handler 查找失去

__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p"
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

音讯转发的代码是不开源的,查找材料找到一份靠谱的 __forwarding 办法实现

为什么是 __forwarding__ 办法。咱们能够依据 Xcode 解体窥探一二

int __forwarding__(void *frameStackPointer, int isStret) {id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 调用 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 调用 methodSignatureForSelector 获取办法签名后再调用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {[receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}

具体地址能够参考 __frowarding

残缺流程如下

上 Demo

Person 类不存在 drink 办法,Bird 类存在

@implementation Bird
- (void)drink
{NSLog(@"一只鸟儿在喝水");
}
@end

Person *person = [[Person alloc] init];
[person drink];

办法 1

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{if (aSelector == @selector(drink)) {return [[Bird alloc] init];
    } 
    return [super forwardingTargetForSelector:aSelector];
}
@end

办法 2

- (id)forwardingTargetForSelector:(SEL)aSelector
{if (aSelector == @selector(drink)) {return nil;} 
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{[anInvocation invokeWithTarget:[[Bird alloc] init]];
}

留神:methodSignatureForSelector 如果返回 nil,则 forwardInvocation 不会执行

给 Person 类办法进行音讯转发解决

办法 1

+ (id)forwardingTargetForSelector:(SEL)aSelector
{if (aSelector == @selector(drink)) {return [Bird class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

办法 2

+ (id)forwardingTargetForSelector:(SEL)aSelector
{if (aSelector == @selector(drink)) {return nil;}
    return [super forwardingTargetForSelector:aSelector];
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{if (aSelector == @selector(drink)) {return [[Bird class] methodSignatureForSelector:@selector(drink)];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{[anInvocation invokeWithTarget:[Bird class]];
}

办法签名的获取

办法 1: 本人依据办法的返回值类型,办法 2 个根底参数参数:id selfSEL _cdm,其余参数类型依照 Encoding 本人拼。相似 v16@0:8

办法 2:依据某个类的对象,去调用 methodSignatureForSelector 办法获取。

[[[Bird alloc] init] methodSignatureForSelector:**@selector**(drink)];

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{if (aSelector == @selector(drink)) {return [[[Bird alloc] init] methodSignatureForSelector:@selector(drink)];
    }
    return [super methodSignatureForSelector:aSelector];
}

Super 原理

@implementation Person
@end

@implementation Student
- (instancetype)init
{if (self = [super init]) {NSLog(@"%@", [self class]);        // Student
        NSLog(@"%@", [self superclass]);   // Person 
        NSLog(@"%@", [super class]);       // Student
        NSLog(@"%@", [super superclass]);  // Person 
    }
    return self;
}
@end

前面 2 个的打印仿佛不合乎预期?转成 c++ 代码看看

static instancetype _I_Student_init(Student * self, SEL _cmd) {if (self = ((Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_3, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("superclass")));
    }
    return self;
}

[super class] 这句代码底层实现为 objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));

__rw_objc_super 是什么?

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

objc_msgSendSuper 如下

/** 
 * Sends a message with a simple return value to the superclass of an instance of a class.
 * 
 * @param super A pointer to an \c objc_super data structure. Pass values identifying the
 *  context the message was sent to, including the instance of the class that is to receive the
 *  message and the superclass at which to start searching for the method implementation.
 * @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
 * @param ...
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method identified by \e op.
 * 
 * @see objc_msgSend
 */
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

所以 objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class")); 等同于上面代码

struct objc_super arg = {self, class_getSuperclass(self)};
objc_msgSendSuper(arg, sel_registerName("class"))

[super class] super 调用的 receiver 还是 self

构造体的目标是为了在类对象查找的过程中,间接从以后类的父类中查找,而不是本类(比方 Student 类的 [super init] 会间接从 Person 的类对象中查找 init,找不到则通过 superclass 向上查找)

大抵揣测零碎的 class、superclass 办法实现如下

@implementation Person
- (Class)class{return object_getClass(self);   
}
- (Class)superclass {return class_getSuperclass(object_getClass(self));
}
@end

class 办法是在 NSObject 类对象的办法列表中的。所以

[self class] 等价于 objc_msgSend(self, sel_registerName("class"))

[super class] 等价于 objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("class"))

其实 2 个办法实质上音讯 receiver 都是 self,也就是以后的 Student,所以打印都是 Student

论断:[super message] 有 2 个特色

  • super 音讯的调用者还是 self
  • 办法查找是依据以后 self 的父类开始查找

通过将代码转为 c++ 发现,super 调用实质就是 objc_msgSendSuper,理论不然

咱们对 iOS 我的项目[super viewDidLoad] 下符号断点,发现objc_msgSendSuper2

查看 objc4 源代码发现是一段汇编实现。

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START

ldp    x0, x16, [x0]        // x0 = real receiver, x16 = class
ldr    x16, [x16, #SUPERCLASS]    // x16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2

所以 super viewDidLoad实质上就是

struct objc_super arg = {
    self, 
    [UIViewController class]    
};
objc_msgSendSuper2(arg, sel_registerName("viewDidLoad"));

objc_msgSendSuper2 和 objc_msgSendSuper 区别在于第二个参数

objc_msgSendSuper2 底层源码(汇编代码 objc-msg-arm64.s 422 行)会将第二个参数找到父类,而后进行办法缓存查找

objc_msgSendSuper 间接从第二个参数查找办法。

总结:clang 转 c++ 能够窥探零碎实现,能够作为钻研参考。super 实质上就是 objc_msgSendSuper2,传递 2 个参数,第一个参数为构造体,第二个参数是 sel。

为什么转为 c++ 和真正实现不一样?思考下

源代码变为机器码之前,会通过 LLVM 编译器转换为中间代码(Intermediate Representation),最初转为汇编、机器码

咱们来验证下 super 在两头码上是什么

clang -emit-llvm -S Student.m

llvm 两头码如下,能够看到的确外部是 objc_msgSendSuper2

; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[Student sayHi]"(%0* %0, i8* %1) #1 {
  %3 = alloca %0*, align 8
  %4 = alloca i8*, align 8
  %5 = alloca %struct._objc_super, align 8
  store %0* %0, %0** %3, align 8
  store i8* %1, i8** %4, align 8
  %6 = load %0*, %0** %3, align 8
  %7 = bitcast %0* %6 to i8*
  %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0
  store i8* %7, i8** %8, align 8
  %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
  %10 = bitcast %struct._class_t* %9 to i8*
  %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1
  store i8* %10, i8** %11, align 8
  %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.6, align 8, !invariant.load !12
  call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12)
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.8 to i8*))
  ret void
}

指令介绍

@ - 全局变量
% - 局部变量
alloca - 在以后执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将主动开释内存
i32 - 32 位 4 字节的整数
align - 对齐
load - 读出,store 写入
icmp - 两个整数值比拟,返回布尔值
br - 抉择分支,依据条件来转向 label,不依据条件跳转的话相似 goto
label - 代码标签
call - 调用函数

isKindOfClass、isMemberOfClass

Demo

Student *student = [[Student alloc] init];
NSLog(@"%hhd", [student isMemberOfClass:[Student class]]); // 1
NSLog(@"%hhd", [student isKindOfClass:[Person class]]);    // 1
NSLog(@"%hhd", [Student isMemberOfClass:[Student class]]); // 0
NSLog(@"%hhd", [Student isKindOfClass:[Student class]]);    // 0

有些人答对了,有些人错了。

下面 2 个判断都是调用对象办法的 isMemberOfClassisKindOfClass

因为 objc4 是开源的,查看 object.mm

- (BOOL)isKindOfClass:(Class)cls {for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {return [self class] == cls;
}

isMemberOfClass 判断以后对象是不是传递进来的对象

isKindOfClass 外部是一个 for 循环,第一次循环先拿以后类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,否则先给 tlcs 赋值以后类的父类,而后走第二次判断,直到 cls 不存在地位(NSObject 的父类为 nil)。所以 isKindOfClass 其实判断的是以后类是传递进来的类,或者传递进来类的子类

上面面 2 个判断都是调用类办法的 isMemberOfClassisKindOfClass

+ (BOOL)isMemberOfClass:(Class)cls {return object_getClass((id)self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {if (tcls == cls) return YES;
    }
    return NO;
}

能够看到 +(BOOL)isMemberOfClass:(Class)cls 办法外部就是对以后类获取类对象,而后与传递进来的 cls 判断是否相等。因为是 [Student isMemberOfClass:[Student class]]) Student 类调用类办法 +isMemberOfClass 所以类对象的类对象也就是元类对象,cls 参数也就是 [Student class] 是一个类对象,元类对象等于类对象吗?显然不是

想让判断成立,能够改为 [Student isMemberOfClass:object_getClass([Student class])] 或者 [[Student **class**] isMemberOfClass:object_getClass([Student class])]

+(BOOL)isKindOfClass:(Class)cls 同理剖析。作用是以后类的元类,是否是左边传入对象的元类或者元类的子类。

来个非凡 case

NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); // NO

输入 1。为什么?

看坐左边的局部,调用 isKindOfClass 办法,实质上就是 Student 类的类对象,也就是 Student 元类,和传入的左边 [NSObject class]判断是否想通过

第一次 for 循环当然不同,所以不能 return,会将 tcls 走步长扭转逻辑 tcls = tcls->superclass,也就是找到以后 Student 元类对象的父类。

第二次 for 循环也一样不相等,Person 元类不等于 [NSObject class] 持续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls  走步长扭转逻辑,tcls = tcls->superclass NSObject 元类的 superclass 还是 NSObject。所以 for 循环外部的判断编委 [NSObject class] == [NSObject class],return YES。

tips:基类的元类对象指向基类的类对象。

+ (BOOL)isKindOfClass:(Class)cls {for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {if (tcls == cls) return YES;
    }
    return NO;
}

Quiz

NSLog(@"%hhd", [NSObject isKindOfClass:[NSObject class]]);  // 1
NSLog(@"%hhd", [NSObject isMemberOfClass:[NSObject class]]);    //0
NSLog(@"%hhd", [Person isKindOfClass:[Person class]]);  // 0
NSLog(@"%hhd", [Person isMemberOfClass:[Person class]]);    //0

Runtime 刁钻题

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHi;
@end
@implementation Person
- (void)sayHi{NSLog(@"hi,my name is %@", self->_name); // hi,my name is 杭城小刘
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *temp = @"杭城小刘";
        id obj = [Person class];
        void *p = &obj;
        [(__bridge id)p sayHi];

        test();}
    return 0;
}

程序运行什么后果?

hi,my name is 杭城小刘

为什么会办法调用胜利?为什么 name 打印出为 @” 杭城小刘 ”

咱们来剖析下:

1.办法调用实质就是寻找 isa 进行音讯发送

Person *person = [[Person alloc] init];
[person sayHi];

[[Person alloc] init]在内存中调配一块内存,而后 isa 指向这块内存,而后 person 指针,指向构造体,构造体的第一个成员。

2.栈空间数据内存向下成长。第一个变量地址高,其次升高。且每个变量的内存地址是间断的。

这个流程其实和下面的代码一样的。所以能够失常调用

void test () {
    long long a = 4;        // 0x7ff7bfeff2d8
    long long b = 5;        // 0x7ff7bfeff2d0
    long long c = 6;        // 0x7ff7bfeff2c8
    NSLog(@"%p %p %p", &a, &b, &c);
}

办法内的变量存储在栈上,堆向上增长,栈向下增长。

3.实例对象的实质就是一个构造体,存储所有成员变量(isa 是一个非凡成员变量,其余的成员变量,这里就是 _name),sayHi 办法外部的 self 就是 obj,找成员变量的实质就是找内存地址的过程(此时就是偏移 8 个字节)

下面代码能够类比类调用办法的流程。obj 指针指向 Person 这块内存,给类对象发送 sayHi 音讯也就是通过 obj 指针找到 isa,恰好 obj 指针指向的地址就是类对象的类构造体的地址,构造体成员变量第一个就是 isa 指针,构造体的其余成员变量就是类的其余属性,这里也就是 _name,所以咱们给自定义的指针 void *p 调用 sayHi 办法,零碎 runtime 在打印 name 的时候,会在 p 左近(下 8 个字节,因为 isa 是指针,长度为 8)找 _name 属性,此时也就找到了 temp 字符串。

struct Person_IMPL {
    Class isa; // 8 字节
    NSString *_name;    // 8 字节
}

再看一个变体 1

NSObject *temp = [[NSObject alloc] init];
id obj = [Person class];
void *p = &obj;
[(__bridge id)p sayHi];
// hi,my name is <NSObject: 0x101129d60>

再看一个变体 2(将代码放在 ViewController 中)

- (void)viewDidLoad {[super viewDidLoad];
    id obj = [Person class];
    void *p = &class;
    NSObject *temp = [[NSObject alloc] init];
    [(__bridge id)p sayHi];
}
// hi,my name is <ViewController: 0x7fe246204fd0>

搞懂的小伙伴不蛊惑了。没搞懂其实就是没搞懂 栈地址由高到低,向下成长super 调用的实质。

再强调一句,依据指针寻找成员变量 _name 的过程其实就是依据内存偏移找对象的过程。在变体 2 中,isa 地址就是 class 的地址,所以依照 地址 +8 的策略,其实前一个局部变量。

[super viewDidLoad]; 实质就是 objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("viewDidLoad"))

struct objc_super arg = {self, class_getSuperclass(self)};
objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));

所以此时的“前一个局部变量”也就是构造体 objc_super 类型的 arg。arg 是一个构造体,构造体第一个成员变量就是 self,所以“前一个局部变量”也就是 self(ViewController)

利用场景

1. 统计 App 中未响应的办法。给 NSObject 增加分类

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 原本能调用的办法
    if ([self respondsToSelector:aSelector]) {return [super methodSignatureForSelector:aSelector];
    }
    // 找不到的办法
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 找不到的办法,都会来到这里
- (void)forwardInvocation:(NSInvocation *)anInvocation
{NSLog(@"找不到 %@办法", NSStringFromSelector(anInvocation.selector));
}
@end

2. 批改类的 isa

object_setClass 实现

Person *p = [Person new];
object_setClass(p, [Student class]);

3. 动态创建类

objc_allocateClassPairobjc_registerClassPair 成对存在

动态创建类、增加属性、办法

void study (id self, SEL _cmd) {NSLog(@"在学习了");
}

void createClass (void) {Class newClass = objc_allocateClassPair([NSObject class], "GoodStudent", 0);
    class_addIvar(newClass, "_score", 4, 1, "i");
    class_addIvar(newClass, "_height", 4, 1, "i");
    class_addMethod(newClass, @selector(study), (IMP)study, "v16@0:8");
    objc_registerClassPair(newClass);
    id student = [[newClass alloc] init];
    [student setValue:@100 forKey:@"_score"];
    [student setValue:@177 forKey:@"_height"];
    [student performSelector:@selector(study)];
    NSLog(@"%@ %@", [student valueForKey:@"_score"], [student valueForKey:@"_height"]);
}

runtime 中 copy、create 等进去的内存,不应用的时候须要手动开释objc_disposeClassPair(newClass>)

4. 拜访成员变量信息

void ivarInfo (void) {Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
    NSLog(@"%s %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar)); //_name @"NSString"
    // 设置、获取成员变量
    Person *p = [[Person alloc] init];
    Ivar ageIvar = class_getInstanceVariable([Person class], "_age");
    object_setIvar(p, ageIvar, (__bridge id)(void *)27);
    NSLog(@"%d", p.age);
}

runtime 设置值 api object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 第三个参数要求为 id 类型,然而咱们给 int 类型的属性设置值,怎么办?能够将 27 这个数字的地址传进去,同时须要类型转换为 id (__bridge id)(void *)27)

KVC 能够依据具体的值,去取出 NSNumber,而后调用 intValue

[p setValue:@27 forKey:@"_age"];

5. 拜访对象的所有成员变量信息

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@end

unsigned int count;
// 数组指针
Ivar *properties = class_copyIvarList([Person class], &count);
for (int i =0 ; i<count; i++) {Ivar property = properties[i];
    NSLog(@"属性名称:%s,属性类型:%s", ivar_getName(property), ivar_getTypeEncoding(property));
}
free(properties);
// 属性名称:_age,属性类型:i
// 属性名称:_name,属性类型:@"NSString"

依据这个能够做很多事件,比方设置解模型、给 UITextField 设 placeholder 的色彩

先依据 class_copyIvarList 拜访到 UITextFiled 有很多属性,而后找到可疑累_placeholderLabel,通过打印 class、superclass 失去类型为 UILabel。所以用 UILabel 对象设置 color 即可,要么通过 KVC 间接设置

[self.textFiled setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];

或者设置字典转模型(不够强壮,轻易写的。具体能够参考 YYModel)

+ (instancetype)lbp_modelWithDic:(NSDictionary *)dict
{id obj = [[self alloc] init];
    unsigned int count;
    Ivar *properties = class_copyIvarList([self class], &count);
    for (int i =0 ; i<count; i++) {Ivar property = properties[i];
        NSString *keyName = [[NSString stringWithUTF8String:ivar_getName(property)] stringByReplacingOccurrencesOfString:@"_" withString:@""];
        id value = [dict objectForKey:keyName];
        [self setValue:value forKey:keyName];
    }
    free(properties);
    return obj;
}

6. 替换办法实现

留神

  • 相似 NSMutableArray 的时候,+load 办法进行办法替换的时候须要留神类簇的存在,比方 __NSArrayM
  • 办法替换个别写在类的 +load 办法中,且为了避免出问题,比方他人手动调用 load,代码须要加 dispatch_once
void studentSayHi (void) {NSLog(@"Student say hi");
}
void changeMethodImpl (void){class_replaceMethod([Person class], @selector(sayHi), (IMP)studentSayHi, "v16@0:8");
    Person *p = [[Person alloc] init];
    [p sayHi];
}
// Student say hi

上述代码能够换一种写法

class_replaceMethod([Person class], @selector(sayHi), imp_implementationWithBlock(^{NSLog(@"Student say hi");
}), "v16@0:8");
Person *p = [[Person alloc] init];
[p sayHi];

imp_implementationWithBlock(id _Nonnull block) 该办法将办法实现替换为包装好的 block

Person *p = [[Person alloc] init];
Method sleep = class_getInstanceMethod([Person class], @selector(sleep));
Method sayHi = class_getInstanceMethod([Person class], @selector(sayHi));
method_exchangeImplementations(sleep, sayHi);
[p sayHi];    // 人生无常,放松睡觉
[p sleep];    // Person sayHi

7. 无痕埋点

对 App 内所有的按钮点击事件进行监听并上报。发现 UIButton 继承自 UIControl,所以增加分类,在 load 办法内,替换办法实现。UIControl 存在办法 sendAction:to:forEvent:

@implementation UIControl (Monitor)
+ (void)load {Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(lbp_sendAction:to:forEvent:));
    method_exchangeImplementations(method1, method2);
}
- (void)lbp_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    // 调用零碎原来的实现
    [self mj_sendAction:action to:target forEvent:event];
//    [target performSelector:action];
}
@end

为了对业务代码无影响,在 hook 代码外部又要调用回去,所以须要调用原来的办法,此时因为替换办法实现,所以原来的办法应该是 lbp_sendAction:to:forEvent:

method_exchangeImplementations 办法实现替换了,零碎会清空缓存,调用 flushCaches 办法,外部调用 cache_erase_nolock 来清空办法缓存。

void method_exchangeImplementations(Method m1, Method m2)
{if (!m1  ||  !m2) return;
    rwlock_writer_t lock(runtimeLock);
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?
    flushCaches(nil);
    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

static void flushCaches(Class cls)
{runtimeLock.assertWriting();
    mutex_locker_t lock(cacheUpdateLock);
    if (cls) {foreach_realized_class_and_subclass(cls, ^(Class c){cache_erase_nolock(c);
        });
    }
    else {foreach_realized_class_and_metaclass(^(Class c){cache_erase_nolock(c);
        });
    }
}

总结:

OC 是一门动态性很强的编程语言,容许很多操作推延到程序运行时决定。OC 动态性其实就是由 Runtime 来实现的,Runtime 是一套 c 语言 api,封装了很多动态性相干函数。平时写的 oc 代码,底层大多都是转换为 Runtime api 进行调用的。

  • 关联对象
  • 遍历类的所有成员变量(能够拜访公有变量,比方批改 UITextFiled 的 placeholder 色彩、字典转模型、主动归档接档)
  • 替换办法实现
  • 扩充点击区域
  • 利用音讯转发机制,解决音讯找不到的问题
  • 无痕埋点
  • 热修复(热修复计划有几大类:内置虚拟机、下发脚本关联到 runtime 批改原始行为、AST 解析解决)
  • 平安气垫(应用场景褒贬不一:比方责任边界问题、尽管兜住了 crash,然而问题没有充沛裸露。一个优雅的策略是线上兜住 crash,然而同时收集案发数据,走业务异样报警,开发立马去依据数据分析这个问题是业务异样还是什么状况,要不要公布热修,还是后端数据 / 逻辑谬误)
退出移动版