OC底层原理07--Runtime以及objc_msgSend分析(一)

一、感受运行时

什么是runtime?

为OC提供运行机制,用C/C++写成的,通过底层的API、OC 源码、调用方法、基础语法、framework、service接口等为OC层面提供的运行制机制。
它也是为面向对象(OOP)提供运行时机制;在运行过程中,让对象找到真正的执行逻辑,包括内存布局(isa的走位指向)。

再来理解下Apple的
从编译时间和链接时间到运行时,Objective-C语言会尽可能多地推迟决策。 只要有可能,它都会动态执行操作,例如创建对象和确定要调用的方法。 因此,该语言不仅需要编译器,还需要运行时系统来执行编译后的代码。 运行时系统充当Objective-C语言的一种操作系统。 这就是使语言有效的原因。 但是,一般情况下,您不需要直接与运行时进行交互。--Apple

  • 与Runtime交互的三种方式
    1.通过Objective-C源代码;--[book write]
    2.通过Framework & Service的类中定义的方法;--[[Book class] isKindOfClass:[NSObject class]]
    3.通过直接调用运行时函数。--objc_msgSendSuper、sel_registerName
Runtime与OC底层架构关系

Compiler是编译器,即LLVM。比如OC层面的alloc在LLVM的实现就是objc_alloc.

方法的本质

利用clang指令编译main.m文件得到main.cpp,在之前的OC底层原理03— isa探究中稍稍介绍过获取C++的Clang指令:

// 模拟器sdk路径替换自己的即可
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk ViewController.m

定义一个类Book,编写两个方法read,write ;其中read实现,write不实现

Book * book = [Book alloc];
[book read];

执行之后,在main.cpp中找寻read,write方法。

// main.cpp
Book * book = ((Book *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Book"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)book, sel_registerName("read"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)book, sel_registerName("write"));

通过以上,可以知道底层会将方法编译供 objc_msgSend调用,这就是方法的本质:消息的发送(objc_msgSend)
既然这样我们就可以通过直接调用底层的objc_msgSend来调用方法;我们先导入头文件#import <objc/message.h>;为了不报错,我们在target —— Build Setting —— 搜索msgSend, 把 enable strict checking of obc_msgSend calls由YES 改为NO,将严厉的检查机制关掉。

调用方法:NSSeletorFromString() (OC层) = @seletor()(OC层)= sel_registerName(runtime)

在调用Book类方法后调用objc_msgSend(book,sel_registerName("read"));得到以下的结果


再一次验证方法的本质是通过消息的发送。

对象方法是否能执行父类的实现

定义两个类:Book 和 English ,Book中实现read方法,English中实现write方法

@interface Book : NSObject
-(void)read;
@end
@implementation Book
- (void)read{
    NSLog(@"read some book");
}
@end

@interface English : Book
-(void)write;
@end
@implementation English
-(void)write{
    NSLog(@"write A B C");
}
@end

通过调用以下:

        English * english = [English alloc];
        [english read];
        
        struct objc_super mysuper;
        mysuper.receiver = English;
        mysuper.super_class = [Book class];
        
        objc_msgSendSuper(&mysuper, sel_registerName("read"));

执行结果:


子类调用父类方法,这很好理解。消息的发送流程中,消息的接收是english,但是具体的实现,可以执行父类Book中的read实现。
objc_msgSendSuper方法中有两个参数(struct objc_super *,SEL),其结构体类型是objc_super定义的结构体对象,且需要指定receiver 和 super_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_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

objc_super定义是:

/// Specifies the superclass of an instance. 
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

由上面印证了,对象方法能执行父类的实现
但是其底层是怎么找的呢?
基本逻辑是:OC 方法是通过消息中的sel(方法编号),找到函数指针imp,再找到底层汇编执行其内容。
消息接收流程:对象 ->isa -> 方法or类 -> cache_t -> methodlist

objc_msgSend 的汇编流程

objc_msgSend 底层是使用汇编构造的。
因为汇编特性:1)编译速度快 ;2)参数的动态性,易调整。

汇编小补

LSR:逻辑右移
add 加法
b.le :判断上面cmp的值是小于等于执行标号,否则直接往下走
b.eq 等于 执行地址 否则往下
cmp a,b 比较a与b
mov a,b 把b的值送给a
ret 返回主程序
nop无作用,英文“no operation”的简写,意思是“do nothing”(机器码90)
call 调用子程序
je 或jz 若相等则跳(机器码74 或0F84)
jne或jnz 若不相等则跳(机器码75或0F85)
jmp 无条件跳(机器码EB)
jb 若小于则跳
ja 若大于则跳
jg 若大于则跳
jge 若大于等于则跳
jl 若小于则跳
jle 若小于等于则跳
ldr w10 ,[sp] w10 = sp栈内存中的值
pop 出栈
push 压栈

快速查找流程

781源码中搜索objc_msgSend,汇编代码就要找.s文件,找到objc-msg-arm64.s,以下是主要的汇编代码:

    // 消息发送:objc_msgSend的汇编入口,获取receiver的isa
    ENTRY _objc_msgSend 

    UNWIND _objc_msgSend, NoFrame 
    
    // p0与空做对比,判断receiver是否存在,p0为objc_msgSend的第一个参数(消息接收者receiver,第二个参数是_cmd)
    cmp p0, #0          // 判空检查和标记指针检查
// 是否支持taggedpointers对象
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        
#else
    // p0 等于 0 时,直接返回 空
    b.eq    LReturnZero 
#endif 
   // p0即receiver 肯定存在的流程
    // 从x0寄存器指向的地址中取出 isa,存入 p13寄存器
    ldr p13, [x0]      
   // 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
    GetClassFromIsa_p16 p13     
LGetIsaDone:
    // 调用imp或objc_msgSend_uncached
    //如果获取到isa,跳转CacheLookup 执行缓存查找流程(sel-imp的快速查找)
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    //如果为nil,则返回空
    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: // 不支持taggedpointer对象,或者为空,就返回空:returnZero
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    END_ENTRY _objc_msgSend
  • CacheLookup
    看定义.macro CacheLookup,跳转至LLookupStart$1
.macro CacheLookup
    //重新启动协议:
    //一旦我们经过LLookupStart $ 1标签,我们可能已经加载了
    //无效的缓存指针或掩码。
    //
    //调用task_restartable_ranges_synchronize()时,
    //(或当有信号到达我们时),直到我们经过LLookupEnd$1,
    //然后我们的电脑将重置为LLookupRecover $ 1
    //跳转到具有以下内容的cache-miss代码路径
    // 要求:
    //
    // GETIMP:
    //缓存未命中只是返回NULL(将x0设置为0)
    //
    // NORMAL和LOOKUP:
    //-x0包含接收者
    //-x1包含选择器
    //-x16包含isa
    //-根据调用约定设置其他寄存器
LLookupStart$1:
    // #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
    // p1 = SEL, p16 = isa
    //  x16(即isa)中平移16字节得到cache,取出cache 存入p11寄存器; isa(8字节),superClass(8字节),cache(mask高16位 + buckets低48位)
    // p11 = mask|buckets
    ldr p11, [x16, #CACHE]              
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16  
// arm64--对应cache_t的HIGH_16位宏
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets = cacahe & 0x0000ffffffffffff   把mask高16位抹零,得到buckets 存入p10寄存器,去掉mask,留下buckets
    //  把p11逻辑右移48位得到mask,mask & p1,得到sel-imp的下标index(即搜索下标) 
    //  存入p12(cache insert写入时的哈希下标计算是 通过 sel & mask,读取时也需要通过这种方式)
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
    // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // PTRSHIFT: arm64-asm.h 中定义 1 << 3 ,二进制 1 左移3位,结果为8.
    //  buckets 是一个数组,要想获得其中某个元素值,利用内存偏移 ,((_cmd & mask ) << (1 + PTRSHIFT)) = 2 <<  4 =  2 ^4 = 16  。(_cmd & mask )  为2,内存偏移相当于找buckets的第三个元素。
    //(_cmd & mask )  : 由于mask= ocuupi - 1= 4-1 = 3,在0,1,2,3 中去取,& 的结果就相当于在取余数,0,1,2,3
    add p12, p10, p12, LSL #(1+PTRSHIFT)
    // 通过取出p12的bucket结构体得到 * bucket = {imp,sel},p9 存sel, p17 存 imp
    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)
// p11:cache 右移44位,将结果存入p12,p12 = 高16位mask + 一个buckets (2中第一个bucket == 最后一个bucket情况,在下面的情况中依然没有则该循环结束)
#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

      //当缓存损坏时,克隆扫描循环会丢失而不是挂起。
      //缓慢的路径可能会检测到任何损坏并在以后暂停。

    // 再查找一遍缓存()
    // 拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket 
    
   // 比较 sel 与 p1(传入的参数cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd) 
   //如果不相等,即走到第二步
    b.ne    2f          //     scan more 
   // 如果相等 即命中,直接返回imp
    CacheHit $0         // call or return imp  
    
2:  // not hit: p12 = not-hit bucket
   // 如果一直找不到,则CheckMiss
    CheckMiss $0            // miss if bucket->sel == 0 
   // 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到
    cmp p12, p10        // wrap if bucket == buckets 
    b.eq    3f //如果等于,跳转至第3步
   // 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket 
   // 跳转至第1步,继续对比 sel 与 cmd
    b   1b          // loop 

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
   // 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached

    JumpMiss $0 
.endmacro

//以下是最后跳转的汇编函数
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP 
   //如果为GETIMP ,则跳转至 LGetImpMiss
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL 
   // 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP 
    //如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

下面是objc_msgSend的汇编流程图


objc_msgSend汇编流程

总结:Objective-C 方法的实现本质上是一个 C 函数,而方法的调用过程则涉及到消息的传递和转发。
在 Objective-C 中,每个方法都有一个方法选择器(selector),它是一个指向方法名称的指针。调用方法时,实际上是向对象发送一条消息,消息中包含方法选择器和参数列表等信息。Objective-C 运行时系统会根据方法选择器查找对应的方法实现,然后执行该方法。方法实现是一个 C 函数,它接收两个参数,分别是方法的接收者和方法选择器。因此,可以说 Objective-C 方法的实现本质上是一个 C 函数。
但是,方法调用过程并不仅仅涉及到方法实现的调用,还包括消息的传递和转发过程。如果对象无法响应某个消息,Objective-C 运行时系统会尝试将消息转发给其他对象。这种消息传递和转发的机制是 Objective-C 方法的重要特性之一,它允许在运行时动态地修改方法的实现,以及在多态和继承等方面提供灵活性和可扩展性。因此,可以说 Objective-C 方法的本质不仅是一个 C 语言函数,还涉及到消息传递和转发的机制。

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