iOS OC 方法的本质

1,667 阅读3分钟

前言:

前面探究了方法在类中的缓存,那么方法的本质是什么呢?方法调用在底层做了什么呢?今天我们来探索一下:

1. 方法本质初探

看一下一段代码: 先定义一个LGPerson类,然后定义sayNB对象方法,然后在main函数中调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayNB];
    }
    return 0;
}

然后通过clang生成cpp文件,在底层编译的cpp文件中查看main函数如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));imp -  函数
    }
    return 0;
}

由此:我们可以简单得出,方法的本质是通过objc_msgSend发送消息,第一个参数为id消息接受者,第二个参数为sel方法编号。

那么我们定义的函数会调用objc_msgSend发送消息吗? 我们定义下面函数,并在main中调用,

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

通过clang查看cpp文件,发现函数不需要调用objc_msgSend,函数可以直接通过函数名(指针),找到函数的实现,不需要像方法通过sel,找到ipm,再找到方法的实现。

父类发送消息(对象方法):

struct objc_super lgSuper;
        lgSuper.receiver = s;
        lgSuper.super_class = [LGPerson class];
        objc_msgSendSuper(&lgSuper, @selector(sayHello));

父类发送消息(类方法):

struct objc_super myClassSuper;
        myClassSuper.receiver = [s class];
        myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
        objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

objc_super源码:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

因此,在调用Runtim api父类发送消息时,需要设置receiversuper_class

问题:在测试中,不要严格识别参数,需要如下设置:

2. objc_msgSend汇编分析

objc源码中断点

然后Debug -> Debug Workflow ->always Show Disassembly进行汇编分析:

objc_msgSend处断点,

通过control + in,查看libobjc.A.dylib objc_msgSend,发现objc_msgSend底层是用汇编实现的。

补充:

为什么`objc_msgSend`用汇编实现呢?
1. 在性能方面,`汇编`更容易被机器识别
2. 在发送消息时,有很多未知的参数,c 语言中不能通过写一个函数来保留未知的参数并且跳转到一个任意的
函数指针,c语言没有满足做这件事的必要特性。

汇编寄存器
arm64下有31位通用寄存器,x0 - x7,是参数,返回值会放到 x0中

objc_msgSend的汇编分析:

首先,在objc源码中全局搜索objc_msgSend找到汇编源码,

汇编代码:

具体分析:

1. cmp	p0, #0			// nil check and tagged pointer check
  先对比当前0号寄存器是否为空,为空,当前没有接收者
  
2. 判断 SUPPORT_TAGGED_POINTERS 
  直接执行 LNilOrTagged 或者 LReturnZero
3. 当有消息接收者,正常情况下,拿到 p13	// p13 = isa
4. GetClassFromIsa_p16 p13	 通过p13(isa),获取 Class ;
    GetClassFromIsa_p16 先平移,取值 shiftcls,然后得到 Class, 或者 isa & Mask 直接获取 Class

LGetIsaDone 源码:

5. LGetIsaDone 查找isa完毕  开始正常查找 CacheLookup NORMAL
   5.1  ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
        先平移16字节,获得cache,找到缓存方法的 buckets 和 occupied
   5.2 and	w12, w1, w11		// x12 = _cmd & mask
        通过 _cmd & mask 获取哈希的下标,
   5.3 循环查找 bucket  add	p12, p10, p12, LSL
   5.4 ldp p17, p9, [x12] 通过 sel 找到 bucket 中的cmd 对比,相等直接返回 CacheHit $0, 找不到,直接走 b.ne	2f 即:CheckMiss
        cmp	p9, p1			// if (bucket->sel != _cmd) 
6. CheckMiss 中找到后,b.eq 3f,进入步骤三,平移哈希,将方法缓存到 bucket中一份,
如果没有找到则 {imp, sel} = *--bucket,循环递归查找。
然后会在查找一遍 5.4 流程(
防止多线程,缓存更新),找不到缓存,则 JumpMiss $0

CheckMiss代码:

当时NORMAL形式时,进入 __objc_msgSend_uncached,如下:

MethodTableLookup源码:

_class_lookupMethodAndLoadCache3方法:

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

问题:
为什么从汇编调用 C 方法?
_class_lookupMethodAndLoadCache3 是一系列慢速方法查找,没有必要使用汇编

总结:

1. ENTRY _objc_msgSend 进入 
2. TAGGED_POINTERS  判断  
3. GetClassFromIsa_p16 p13 通过 isa 获取 Class
4. 缓存查找 CacheLookup
5. cache_t 处理,处理哈希,查找 buckrt,找到返回{imp,sel} = *buckrt->imp,找不到 JumpMiss
6. 缓存中找不到方法 进入 __objc_msgSend_uncached
7. STATIC_ENTRY __objc_msgSend_uncached
8. MethodTableLookup  调用__class_lookupMethodAndLoadCache3

最后一个遗留问题,调用_class_lookupMethodAndLoadCache3中是怎么慢速查找的呢?下一篇接着探索。