1.编译后的方法调用
还是之前的Person
类的源码:
@interface Person : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,strong) NSString *nickname;
@property (nonatomic,assign) float height;
@property (nonatomic,strong) NSString *name;
-(void)laugh;
-(void)cry;
-(void)run;
-(void)jump;
-(void)doNothing;
@end
@interface Saler : Person
@property (nonatomic,strong) NSString *brand;
@end
@implementation Saler
@end
@implementation Person
-(void)laugh
{
NSLog(@"LMAO");
}
-(void)cry
{
NSLog(@"cry me a river");
}
-(void)run
{
NSLog(@"run! Forrest run!");
}
-(void)jump
{
NSLog(@"you jump,I jump!");
}
-(void)doNothing
{
NSLog(@"Today,I dont wanna do anything~");
}
@end
//main方法中添加
Saler *person = [Saler alloc];
person.age = 10;
person.nickname = @"pp";
person.height = 180.0;
person.name = @"ppext";
person.brand = @"apple";
[person laugh];
[person cry];
[person run];
[person jump];
[person doNothing];
[person run];
[person laugh];
[person doNothing];
要研究方法调用就绕不过编译后的代码,这里用clang
编译一下:
//编译命令
clang -rewrite-objc main.m -o main.cpp
并找到main
方法中方法调用这段代码:
Saler *person = ((Saler *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Saler"), sel_registerName("alloc"));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setNickname:"), (NSString *)&__NSConstantStringImpl__var_folders_6h_y4xl5w1s77xfp_vrjn_15k200000gp_T_main_2043d5_mi_5);
((void (*)(id, SEL, float))(void *)objc_msgSend)((id)person, sel_registerName("setHeight:"), (float)180.);
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6h_y4xl5w1s77xfp_vrjn_15k200000gp_T_main_2043d5_mi_6);
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setBrand:"), (NSString *)&__NSConstantStringImpl__var_folders_6h_y4xl5w1s77xfp_vrjn_15k200000gp_T_main_2043d5_mi_7);
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("laugh"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("cry"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("jump"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doNothing"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("laugh"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doNothing"));
可以总结,不论是类方法alloc
,还是对象方法setAge
、laugh
都是通过objc_msgSend
来实现的,翻译翻译什么是这个方法:
(void)objc_msgSend(id self,SEL _cmd,args)
第一个参数是id
类型,大概是实现该方法的对象,包括类和对象;
第二个参数SEL
类型,可以理解为方法名,这是一个包含名字和地址的结构;
后面的参数args
类型不定,数目不定,是根据方法的参数多少来添加的。
分析的这个是c++
的方法,对比Objective-c中,方法只有后续args
的参数,没有id
和SEL
,在实例方法中这个id
参数显然就是这个对象即self
,而_cmd
也是可以在方法中打印的,但是这两个参数在方法中是隐藏的,也许是为了保守类对象
的秘密,不然类方法需要传类对象
,这个可能就暴露的这方面的设计吧,-_-!
如果说这只是一个推测不能当确信的证据,那就直接开启lldb
调试,并且打开显示汇编选项:Debug
——Debug Workflow
——Always Show Disassembly
,然后在方法调用出打个断点,command+R
运行,可以得到:
objc_msgSend
方法的调用,后续参数调用好想只显示了args
部分,和_cmd
的部分,self
部分可能是隐式调用。
2.方法调用的流程
2.1——Cache中查找(快速查找)
在objc-781.1源码中,全局搜索objc_msgSend
可以在objc-msg-arm.s
、objc-msg-arm64.s
、objc-msg-x86_64.s
、objc-msg-i386.s
等文件中找到,由于是iOS设备中,所以这里分析arm64
的。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//判断p0是否是空
cmp p0, #0 // nil check and tagged pointer check
//如果支持tagged pointer类型
#if SUPPORT_TAGGED_POINTERS
//判断 tagged pointer类型或对象是否为nil
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
//上面是从isa的到类信息的流程 下面是获取到类信息后调用CacheLookup方法
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
END_ENTRY _objc_msgSend
上面注释了部分简单的分析,可以知道最后是调用了CacheLookup
方法,在cache_t
中查找方法缓存,在源码中搜索这个方法源码:
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
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
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
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
这里的汇编代码配合注释可以知道就类似之前文章Cache_t分析中的lldb
调试打印,不管是哪个分支,其实大致就是通过位运算将buckets
地址计算出来,对比每个bucket
中的sel
发现找到了bucket
就调用CacheHit
命中方法:
// 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
.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
CacheHit
方法中有三个分支,这里走哪个呢,找到_objc_msgSend
的汇编源码进入CacheLookup
的语句:
CacheLookup NORMAL, _objc_msgSend
其他代码并没有对$0
进行操作,所以这里$0=NORLMAL
,在CacheHit
进入NORMAL
分支,调用TailCallCachedImp
方法,这里通过注释就知道是授权并调用方法的IMP
,IMP
也是方法的具体实现了。
再看未找到bucket
,就继续循环,如果循环比较cmp p12, p10
即if bucket == buckets
是否回到了起始指针位置,然后结束循环,如果回到原始指针位置就进行下一句b.eq 3f
即转到第3
步,也就是调用JumpMiss
:
.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
同CacheHit
一样,这里$0=NORLMAL
,在JumpMiss
进入NORMAL
分支,调用__objc_msgSend_uncached
方法:
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
在__objc_msgSend_uncached
中主要调用的就是MethodTableLookup
——在方法列表中查找:
.macro MethodTableLookup
SAVE_REGS
// 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
RESTORE_REGS
.endmacro
再次在MethodTableLookup
源码中发现了最闪亮的它——_lookUpImpOrForward
,通过注释就知道这里已经准备跳出汇编方法,转到源码方法lookUpImpOrForward
,继续全局搜索就可以在objc_runtime_new.mm
文件中找到这个方法的调用。分析至此可以知道lookUpImpOrForward
方法已经跳出了cache
的查找。
小总结
:
方法调用之cache查找(快速查找):
function()
——objc_msgSend()
——汇编方法调用
——得到isa
——得到cache
——得到buckets
——循环查找
:
查找命中
——CacheHit
——调用方法IMP
查找未命中
——JumpMiss
——__objc_msgSend_uncached
——MethodTableLookup
——lookUpImpOrForward()
网友评论