iOS 底层 runtime之objc_msgSend - AlvinSunny/OC-TheUnderlying GitHub Wiki

了解Objective-C

  • Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大不同;(动态性:在程序运行过程中可以改变和添加一些方法、动态创建实例)
  • Objective-C的动态性是由Runtime的一套API接口来实现的;

Runtime API提供的对外调用接口基本上都是C语言的,其源码由C、C++、汇编语言编写;runtime中很多经常用到的API实现一般都是通过汇编代码实现,比如:objc_msgSend()

runtime底层官方源码下载

Runtime的消息机制

方法调用

在OC中的方法调用都转换为运行时API--> objc_msgSend()函数的调用; objc_msgSend如果找不到合适的方法进行调用,会报unrecognized selector sent to instance(该方法找不到)的错误

关于SEL

  • SEL: @selecter 方法选择器 和 runtime的API-->sel_registerName()函数作用等同

objc_msgSend()的执行流程三阶段:消息发送动态方法解析消息转发

消息发送

  • 消息发送调用成功,是不会进入动态方法解析、消息转发;

消息发送流程@2x.png

逻辑解读:

    1. 判断消息接收者(receiver)是否为空(是否为0),是直接return;
    1. 查找缓存 -- >CacheLookup,找到直接调用结束查找;
    1. 拿到class_rw_t中的methods进行遍历(如果是有序的就直接二分查找(折半查找),无序直接for循环),找到了底层执行 goto done 返回imp,并添加到缓存中并结束查找;
    1. 当前类找不到,通过superclass找到父类的缓存和方法列表中查找;步骤和以上三条相同;
    1. 如果父类方法列表中依然没有找到,就去父类的父类中找;一旦找到基类的方法列表中还是没有,就需要进入下一个阶段:动态方法解析;

动态方法解析

  • 动态方法解析调用成功,是不会进入消息转发;

动态方法解析@2x.png

底层源码:

    // No implementation found. Try method resolver once. (没有实现,尝试一次方法解析器。)

    if ((behavior & LOOKUP_RESOLVER)  &&  !triedResolver) {
        methodListLock.unlock();//解锁
        _class_resolveMethod(cls, sel, inst); //方法解析实现
        triedResolver = YES;
        goto retry; //走消息发送流程:‘从receiverClass的cache中查找方法’ 这一步开始执行
    }

/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
static void
_class_resolveMethod(id inst, SEL sel, Class cls)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            _class_resolveInstanceMethod(inst, sel, cls);
        }
    }
}

逻辑解读:

    1. 判断消息接收者是否曾经进行过动态解析 是:直接走消息转发-->_objc_msgForward_impcache 否:调用[cls resolveInstanceMethod:sel]或者[cls resolveClassMethod:sel]方法来动态解析方法
    1. 解析成功 底层执行 goto retry,标记为已经动态解析;
    1. 走消息发送流程:‘从receiverClass的cache中查找方法’ 这一步开始执行
#import <objc/runtime.h>

### 一个类只做了 - (void) test 方法的声明,没有实现

- (void)other
{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他方法
        Method method = class_getInstanceMethod(self, @selector(other));

        // 动态添加test方法的实现
        class_addMethod(self, sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));

        // 返回YES代表有动态添加方法,注意这里返回的布尔值底层并没有做任何逻辑处理,只是哪来做一个日志输出;
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

### 一个类只做了 + (void) test 方法的声明,没有实现
void c_other(id self, SEL _cmd)
{
    NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 第一个参数是object_getClass(self)
        class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

动态添加方法

动态添加方法@2x.png

@dynamic 关键字

  • @dynamic是告诉编译器不用自动生成setter和getter方法的实现、不自动生成成员变量,等到运行时在添加方法实现,不影setter和getter方法的声明;
// 提醒编译器不要自动生成setter和getter的实现、不要自动生成成员变量
@dynamic age;
void setAge(id self, SEL _cmd, int age)
{
    NSLog(@"age is %d", age);
}

int age(id self, SEL _cmd)
{
    return 120;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(setAge:)) {
        class_addMethod(self, sel, (IMP)setAge, "v@:I");
        return YES;
    } else if (sel == @selector(age)) {
        class_addMethod(self, sel, (IMP)age, "i@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

消息转发

消息转发@2x.png

知识点:

  • _objc_msgForward_impcache 消息转发底层入口函数,函数内部由汇编代码实现,内部核心代码:__objc_msgForward_stret __objc_msgForward 这两个函数是汇编函数; 底层最终会调用 __forwarding__方法,__forwarding__ 方法 __objc_msgForward--> _objc_msgForward--> __forwarding__
  • _objc_msgForward: 是 IMP 类型; 用于消息转发的;
    当向一个对象发送一条消息,但它并没有实现的时候会尝试做消息转发。
STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version
	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band condition register is NE for stret, EQ otherwise.
	
	je	__objc_msgForward_stret

       // int __forwarding__
	jmp	__objc_msgForward    

	END_ENTRY __objc_msgForward_impcache

逻辑解读:

前提:在当前消息接收者体系中找不到方法的具体实现(自己没有能力来处理,将消息转发给别人来处理)

    1. 进入 int forwarding(void *frameStackPointer, int isStret) 函数
    1. 调用forwardingTargetForSelector:方法 返回值为空:表示没有提供消息接收者,这时就需要调用methodSignatureForSelector:方法 返回值不为空:objc_msgSend(返回值,SEL)回到第一阶段, 注意:如果返回的是当前实例就不会回到第一阶段,而是继续来到methodSignatureForSelector

      forwardingTargetForSelector: 用来返回一个可以接收当前消息的消息接收者

    1. 调用methodSignatureForSelector:方法,要求返回方法签名 (所谓的方法签名:方法的返回值类型和参数类型) 返回值为空:调用doesNotRecognizeSelector:方法,表示这是未识别的选择器,抛出 unrecognized selector sent to instance (该方法找不到) 返回值不为空:调用forwardInvocation:方法 , 调用forwardInvocation之前会再次进行动态方法决议(调用resolveInstanceMethod或resolveClassMethod)
  • 4 forwardInvocation:方法中开发者可以自定义任何逻辑处理,这样就不会因为方法找不到而崩溃

doesNotRecognizeSelector:源码实现

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal(
                "-[%s %s]: unrecognized selector sent to instance %p", 
                 object_getClassName(self), 方法调用者类名
                 sel_getName(sel),  方法名
                 self                           方法调用者
                  );
}
  • int forwarding(void *frameStackPointer, int isStret) 函数底层实现是不开源的,并不是所有的runtimeAPI都开放源码,有部分深层次的API还是没有开源。

代码示例

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        // objc_msgSend([[XYHCat alloc] init], aSelector)
        如果返回空,就会触发methodSignatureForSelector:方法
        return [[XYHCat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
//        return [NSMethodSignature signatureWithObjCTypes:"i@:I"];
//        return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

 NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
    anInvocation.target 方法调用者
    anInvocation.selector 方法名
    [anInvocation getArgument:NULL atIndex:0]
//此方法中的所有实现就相当于是当初调用的方法的一个实现,比如:[XYHPerson test] , test方法没有具体实现,当执行到forwardInvocation:方法时,其内部的实现最终就是test的实现
- (void)forwardInvocation:(NSInvocation *)anInvocation
{

不带参数指定target,方法调用
//    anInvocation.target = [[XYHCat alloc] init];
//    [anInvocation invoke];
    [anInvocation invokeWithTarget:[[XYHCat alloc] init]];

带参数指定target,方法调用
// 参数顺序:0  --> receiver、1 --> selector、2 --> other arguments
//    int age;
//    [anInvocation getArgument:&age atIndex:2];
//    NSLog(@"%d", age + 10);
    
    // anInvocation.target == [[XYHCat alloc] init]
    // anInvocation.selector == test:
    // anInvocation的参数:15
    // [[[XYHCat alloc] init] test:15]

    [anInvocation invokeWithTarget:[[XYHCat alloc] init]];
    
    int ret;
   //拿到返回值
    [anInvocation getReturnValue:&ret];
    
    NSLog(@"%d", ret);

}

类方法的消息转发

和对象方法一样,但在forwardingTargetForSelector:、methodSignatureForSelector:、forwardInvocation是需要执行 '+'开头的方法

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    // objc_msgSend([XYHCat class], @selector(test))
    // [[[MJCat alloc] init] test]
    if (aSelector == @selector(test)) return [XYHCat class];

    return [super forwardingTargetForSelector:aSelector];
}

疑问:如果在类方法的forwardingTargetForSelector:中返回的不是类对象,而是一个实例对象且有对应的实例方法,能够调用成功吗 ?为什么?

可以的,因为在这里返回的对象不管是什么对象,只要其已经实现了对应的方法都会通过objc_msgSend()函数调用成功,只要有这个方法就行;

@implementation XHYCat

+ (void)test
{
    NSLog(@"%s", __func__);
}

- (void)test
{
    NSLog(@"%s", __func__);
}

@end

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) 
    //这么写最终会执行   - (void)test
    return [[XHYCat alloc] init];
    return [super forwardingTargetForSelector:aSelector];
}

特殊的NSProxy

  • NSProxy是一个为对象定义接口的抽象父类,并且为其它对象或者一些不存在的对象扮演了替身角色。具体的可以看下NSProxy的官方文档 说白了NSProxy就是一个万能替身
  • NSProxy是专门用来做消息转发的,内部有一个target属性,定位更加精准,效率非常高;
  • NSProxy和NSObject是同一个级别的类,都是基类;

特殊:

  • NSProxy对象不需要调用init初始化,因为它本来就没有init方法
  • 查找方法时先判断当前继承自NSProxy的类自己有没有该方法,如果没有就跳过消息发送和动态解析,直接来到消息转发阶段;
  1. (NSMethodSignature *)methodSignatureForSelector:(SEL)sel方法必须实现
  2. (void)forwardInvocation:(NSInvocation *)invocation必须实现
@interface NSProxy <NSObject> {
    Class	isa;
}
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

应用:

  • 两个对象循环引用,为了不让彼此强引用就使用NSProxy来作为其中一个中间对象:target;
+ (instancetype)proxyWithTarget:(id)target
{
    XYHProxy *proxy = [XYHProxy alloc];
    proxy.target = target;
    return proxy;
}

//消息转发到target

//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

//内部实现方法调用
- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}

objc_msgSend()的执行流程 - 源码跟读

QQ20200402-161206@2x.png

ENTRY _objc_msgSend 汇编源码

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
//x0寄存器:消息接收者-->receiver
	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	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]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// 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
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret //ret相当于c语言的return

	END_ENTRY _objc_msgSend
 

探索:什么情况下会走折半查找 ?

源码:

#define ALWAYS_INLINE inline __attribute__((always_inline)) //添加后声明的函数必然是内联函数
#define NEVER_INLINE __attribute__((noinline)) //添加后声明的函数不在是内联函数

一个快速判断方式,__builtin_expect: 
__builtin_expect((x),1) 表示 x 的值为真的可能性更大 
__builtin_expect((x),0) 表示 x 的值为假的可能性更大

#define fastpath(x) (__builtin_expect(bool(x), 1))

查找方法的内联函数
功能介绍:传入一个方法数组和一个方法名 返回一个method_t
ALWAYS_INLINE static method_t * static method_t * search_method_list_inline(const method_list_t *mlist, SEL sel) {

    1.  isFixedUp:  方法列表是否被修正和排序过  //[共享缓存中的方法列表是1(唯一的)或3(唯一并排序的)]
    2.  isExpectedSize:方法列表是否有预期大小
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    //在iOS 3.1之后苹果启用了共享缓存优化方案,方法查找大多都是通过二分查找算法遍历的,
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) 
    {//满足二分查找条件
        return findMethodInSortedMethodList(sel, mlist);
    } else 
    {//无序方法列表的线性搜索 即:循环遍历查找
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

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

关键源码解读 isFixedUp


 函数声明
 bool isFixedUp() const;

函数实现
//判断是否被修正过
bool method_list_t::isFixedUp() const {
    // flages 和 3 运算后的值 是否等于3 
    return (flags() & 0x3) == fixed_up_method_list;
}

关于fixed_up_method_list

mlist->entry size的低两位用作固定标记。
共享缓存中的方法列表是1(唯一的)或3(唯一并排序的)。
(协议方法列表没有排序,因为它们有额外的并行数据)
运行时修复的方法列表得到3。

高两位协议->标志被用作固定标记。
PREOPTIMIZED(优化前)版本: 
来自共享缓存的协议是1<<30。
运行时修复协议得到1<<30。

UN-PREOPTIMIZED(预优化)版本:
来自共享缓存的协议是1<<30。
共享缓存的修复不受信任。
运行时修复协议得到3<<30。

static const uint32_t fixed_up_method_list = 3;
static const uint32_t uniqued_method_list = 1;

关键源码解读 isExpectedSize

   
    //判断方法数组是否能满足需要 不需要扩容,就是数组空间够不够用
    bool isExpectedSize() const {
        if (isSmallList())
        {//用小方法表示的
            return entsize() == method_t::smallSize;//entsize 是 entry size 的缩写
        } else 
        {//用大方法表示的
            return entsize() == method_t::bigSize;
       }
    }

    //小方法列表标记
    static const uint32_t  smallMethodListFlag = 0x80000000;
   //是否是小方法,即最高位是否是1 
   //0x80000000 = 10000000000000000000000000000000
    bool isSmallList() const {
        //  flags() &  0x80000000
        return flags() & method_t::smallMethodListFlag;
    }
   
   //计算占内存大小
    static const auto bigSize = sizeof(struct big);
    static const auto smallSize = sizeof(struct small);

   // 一个“小”方法的表示,这存储了名称、类型、实现的三个相对偏移量
    struct small {
        //name字段的选择器要么在共享缓存中,要么在其他地方。
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP> imp;
       //是否是共享缓存
        bool inSharedCache() const {
            return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS &&
                    objc::inSharedCache((uintptr_t)this));
        }
    };

   // 表示一个“大”的方法 ,这是传统的;  表示存放选择器类型的三个指针和实现。
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

关于flags()解读:


模板类
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
    uint32_t entsizeAndFlags; //标记📌
    uint32_t count;

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

    //flags函数实现
    uint32_t flags() const {
       //FlagMask:外界传进来的标记掩码 在method_list_t中为0xffff0003
      //0xffff0003  = 0b11111111111111110000000000000011
        return entsizeAndFlags & FlagMask;
    }

 }

entsizeAndFlags 在addMethod(添加方法)时赋值操作片段

 struct big {
      SEL name;   //8字节
       const char *types; //8字节
       IMP imp; //8字节
  };

 // fixme optimize  修复优化
 method_list_t *newlist;
 newlist = (method_list_t *)calloc(method_list_t::byteSize(method_t::bigSize, 1), 1);

 //共享缓存中的方法列表是1(唯一的)或3(唯一并排序的)  fixed_up_method_list = 3 
 //sizeof(struct method_t::big) == 24 == 0b11000
 //fixed_up_method_list == 3 == 0b00011
 //赋值操作:  0b11000 |  0b00011
  newlist->entsizeAndFlags =  (uint32_t)sizeof(struct method_t::big) | fixed_up_method_list;

  newlist->count = 1;
  auto &first = newlist->begin()->big();
  first.name = name;
  first.types = strdupIfMutable(types);
  first.imp = imp;

  addMethods_finish(cls, newlist);

折半查找的魅力

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    //第一个method的位置
    auto first = list->begin();
    auto base = first;
    decltype(first) probe; //相当于mid
    //把key直接转换成uintptr_t 因为修复过后的method_list_t中的元素是排过序的
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // count = 数组个数  count >>= 1 = count = count >> 1)
    // count >> 1 就相当于 (count / 2) 取整
    // 1.假如 count = list->count = 8   //2 count =  7 >> 1 = 3
    for (count = list->count; count != 0; count >>= 1) {
        
        /*
          1. 首地址 + (下标) //地址偏移 中间值 probe = base + 4
          2. 中间值  probe = base(首地址)+ 6
         */
        probe = base + (count >> 1);
        
        //获取中间的sel的值也是强转后的值
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) { // 如果 目标key ==  中间位置的key 匹配成功
            
            //分类覆盖,分类中有相同名字的方法,如果有分类的方法我们就获取分类的方法,多个分类看编译的顺序
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            
            //返回方法的地址
            return &*probe;
        }
        
        //如果 keyValue > 中间的位置的值
        if (keyValue > probeValue) {
            
            /*
               1.base = probe + 1 =  4 + 1 = base(首地址) + 5 向上移 一位
               2.base = probe + 1 ;向上移 一位
             */
            base = probe + 1;
            
            // 8 -1 = 7 因为比过一次没中 然后循环
            count--;//查询完没找到返回nil
        }
    }
    
    return nil;
}

以上代码就是折半查找法的代码,真的很强,那么,代码看的不够直观,我们尝试用几张图来阐述一下上面的过程,让大家一起来领略下 折半查找法的魅力。

折半查找.png

⚠️ **GitHub.com Fallback** ⚠️