iOS-OC对象原理_objc_msgSend(一)

前言

在前面我们探索了objc_class,以及该结构体内部的cache_t cacheclass_data_bits_t bits,class_rw_t data等成员内部的结构分布,通过结合之前探索的基础,本片文章就深入探索下非常基础且重要的objc_msgSend方法。

开始

我们都知道,在OC中,我们定义的方法通常被称之为消息(Message),调用方法的过程就是发送消息的过程。首先我们在main.m下添加一些简单的代码:

@interface ZZPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end

@implementation ZZPerson
- (void)sayHello
{
    NSLog(@"sayHello");
}
- (void)sayNB
{
    NSLog(@"sayNB");
}
@end
@interface ZZStudent : ZZPerson
@end
@implementation ZZStudent
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        ZZPerson *person = [ZZPerson alloc];
        [person sayHello];
        ZZStudent *student = [ZZStudent alloc];
        [student sayNB];
    }
    return 0;
}

在上面的代码中,我们创建了一个ZZPerson类,添加并实现了两个实例方法:
- (void)sayHello- (void)sayNB;然后在main方法中创建实例并调用实例方法。然后我们通过clang方式,将main.m转换为main.cpp代码,看发生了什么变化:
终端执行:

zeze@localhost ZZObjc % clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.4.sdk main.m

回车后,会在main.m同级位置生成main.cpp,打开main.cpp,并找到对应的main函数:

int main(int argc, const char * argv[]) {
     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

         ZZPerson *person = ((ZZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZZPerson"), sel_registerName("alloc"));
         ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
         ZZStudent *student = ((ZZStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZZStudent"), sel_registerName("alloc"));
         ((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("sayNB"));
     }
     return 0;
}

此时我们会发现,我们之前调用的alloc,sayHello,sayNB等方法都被转换为了objc_msgSend()的方式调用,比如:

ZZPerson *person = [ZZPerson alloc];
//编译前
[person sayHello]; 
//编译后
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

这也就意味着我们OC代码中调用的方法在编译后都会转化为objc_msgSend()的方式,而且我们可以通过#import <objc/message.h>直接访问这些方法完成方法的调用,也能达到同样的效果,我们修改下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        ZZPerson *person = [ZZPerson alloc];
        [person sayHello];
        objc_msgSend(person,sel_registerName("sayHello"));
        
        ZZStudent *student = [ZZStudent alloc];
        [student sayNB];
        objc_msgSend(student,sel_registerName("sayNB"));
    }
    return 0;
}
2020-09-23 14:38:48.415766+0800 KCObjc[34398:60843655] sayHello
2020-09-23 14:38:48.417288+0800 KCObjc[34398:60843655] sayHello
2020-09-23 14:38:48.417697+0800 KCObjc[34398:60843655] sayNB
2020-09-23 14:38:48.417830+0800 KCObjc[34398:60843655] sayNB

注意:可能在编译时会遇到以下报错:Too many arguments to func call....

截屏2020-09-23 下午2.35.53.png

此时打开TARGETS->Build Setting:
截屏2020-09-23 下午2.36.56.png

Enable Strict Checking of objc_msgSend Calls设置为NO,报错就消失了。

objc_msgSend()

objc_msgSend()是如何实现消息发送的呐?或者是怎么通过sel找到对应的imp的呐?
首先,我们来看下objc_msgSend()的源码实现:

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这里我们发现根本无法跳转到objc_msgSend的实现代码。全局搜索下:

截屏2020-09-23 下午2.55.29.png

此时发现message.h中只有方法声明,然后就没有对应的.cpp.mm文件了,再往下看是一些xx.s文件(汇编代码文件),难道是用汇编写的?尝试打开objc-msg-arm64.s,然后会看到:

/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17
 * x16 reserved for our use but not used
 *
 ********************************************************************/

#if SUPPORT_TAGGED_POINTERS
    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0
#endif

    ENTRY _objc_msgSend            //ENTRY 入口
    UNWIND _objc_msgSend, NoFrame
............ 省略

果不其然,还真的是用汇编实现的,苹果你真牛逼。大家都知道汇编的执行效率:更快,更高,更强。所以苹果使用了C++ , 汇编等混编的方式,高效完成一些业务。关于汇编的简单知识,可以看下我的这篇文章:iOS中常见的汇编指令
关于汇编我也只是了解,但是结合之前的文章和汇编代码旁边的注释就会非常清晰它在做什么,尽管我们不是很懂汇编指令。接下来我们就详细的看下:

    ENTRY _objc_msgSend       //开始入口
    UNWIND _objc_msgSend, NoFrame   // 无窗口界面的方法

    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  这里是isa_t
    GetClassFromIsa_p16 p13     // p16 = class = clsShift
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

简单解释:
cmp p0, #0: p0 = person实例对象,p0 == 0 ?
ldr p13, [x0]: p13 = isa(isa_t) 读取对象首地址也就是isa_t isa
GetClassFromIsa_p16 p13: 从p13中读取Class,即isa.ISA()(64bit中的clsShift),并将class赋值给p16
CacheLookup NORMAL, _objc_msgSend:开始根据p16(Class) 查询。
读取到class后执行CacheLookup指令:

.macro CacheLookup

LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //.....省略
#else
#error Unsupported cache mask storage for ARM64.
#endif

    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

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

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0
.endmacro

ldr p11, [x16, #CACHE] : p11 = x16 + #CACHE = *cache(这里就是从isa平移16个字节)p11 = (cache_t *)cache = _maskAndBuckets;

#define CACHE            (2 * __SIZEOF_POINTER__)

and p10, p11, #0x0000ffffffffffff : p10 = p11 & 0x0000ffffffffffff = buckets
add p12, p10, p12, LSL #(1+PTRSHIFT): 从begin位置的bucket开始查询,也就是:

mask_t begin = cache_hash(sel, m);
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

这里的((_cmd & mask) << (1+PTRSHIFT)) ,做了一个左移的操作其实就是为了防止越界,在cache_t::insert()中体现在:

mask_t m = capacity - 1;

ldp p17, p9, [x12]: p9 = sel , p17 = imp
cmp p9, p1 : p9 != _cmd ? : b.ne 2f : CacheHit $0,如果成立就跳转到2:否则就是找到CacheHit $0 并返回,其实这里的逻辑跟cache_t::insert()是一样的,我就不一一解释了,代码注释已经很清楚。这里执行完1:后会进入一个循环,知道条件成立后执行CheckMiss $0CacheHit $0;
CacheHit $0:找到了sel执行,并通过TailCallCachedImp指令返回

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp

CheckMiss $0:当没有找到sel执行该指令

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached

此时,将进入__objc_msgSend_uncached指令:

.endmacro

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
//............省略.........

这里执行MethodTableLookup指令:

.macro MethodTableLookup
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp
    // save parameter registers: x0..x8, q0..q7
       // 省略........
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward
    // IMP in x0
    mov x17, x0
        //省略 .......
.endmacro

这里再次发生跳转_lookUpImpOrForward,但该指令在当前汇编源码中已经看不到具体实现了,这里推测应该是又跳转回C++代码了,关于_lookUpImpOrForward的具体实现,我们在另一篇文章中详细说。这里我们先通过简单的流程图做一下总结:

总结

objc_msgSend()被调用后,首先会根据传入的对象找到他的类,在objc_class中首先会进入cache_t cache中找到buckets,在buckets中查找是否有存在bucket_t.sel == _cmd,如果找到,就返回,快速查找流程结束;如果没有找到会进入__objc_msgSend_uncached ->MethodTableLookup->_lookUpImpOrForward慢速查找流程
iOS-OC对象原理_objc_msgSend(二)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352