前言
之前,咱们在探求动画及烘托相关原理的时分,咱们输出了几篇文章,解答了
iOS动画是怎么烘托,特效是怎么作业的疑惑
。咱们深感体系设计者在创造这些体系结构的时分,是如此脑洞大开,也深深意识到了解一门技能的底层原理对于从事该方面作业的重要性。
因而咱们决议
进一步探求iOS底层原理的使命
。继上一篇文章 介绍了runtime的isa详解、class的结构、办法缓存cache_t 之后, 会逐个探求:objc_msgSend
、音讯转发
、动态办法解析
、super的实质
一、objc_msgSend音讯发送
过一段代码,将办法调用代码转为c++代码检查办法调用的实质是什么样的。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
[person test];
// --------- c++底层代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
经过上述源码能够看出:
- c++底层代码中办法调用其实都是转化为
objc_msgSend
函数 - OC的办法调用也叫
音讯机制
,表示给办法调用者发送音讯
。 - 拿上述代码举例,上述代码中实际为: 给person实例目标发送一条test音讯:
- 音讯接受者:person
- 音讯称号:test
办法调用的进程 办法调用的进程 实际上分为三个阶段:
- 音讯发送阶段: 担任从类及父类的缓存列表及办法列表查找办法;
- 动态解析阶段: 假如音讯发送阶段没有找到办法,则会进入动态解析阶段,担任动态的增加办法完结;
- 音讯转发阶段: 假如也没有完结动态解析办法,则会进行音讯转发阶段,将音讯转发给能够处理音讯的接受者来处理;
- 假如音讯转发也没有完结,就会报办法找不到的过错,无法识别音讯,
unrecognzied selector sent to instance
接下来咱们经过阅览runtime
源码,探寻一下OC的办法调用的三个阶段别离是怎么完结的:
1. 音讯发送
在runtime
源码中查找_objc_msgSend
检查其内部完结,在objc-msg-arm64.s
汇编文件能够知道_objc_msgSend
函数的完结
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
上述汇编源码中会首要判别音讯接受者reveiver
的值。
- 假如传入的音讯接受者为nil则会履行
LNilOrTagged
-
LNilOrTagged
内部会履行LReturnZero
- 而
LReturnZero
内部则直接return0
-
- 假如传入的音讯接受者不为nill则履行
CacheLookup
-
CacheLookup
内部对办法缓存列表进行查找 - 假如找到则履行
CacheHit
,从而调用办法 - 不然履行
CheckMiss
-
CheckMiss
内部调用__objc_msgSend_uncached
-
__objc_msgSend_uncached
内会履行MethodTableLookup
-
MethodTableLookup
也便是办法列表查找 -
MethodTableLookup
内部的中心代码__class_lookupMethodAndLoadCache3
-
__class_lookupMethodAndLoadCache3
也便是C言语函数_class_lookupMethodAndLoadCache3
-
-
- C函数
_class_lookupMethodAndLoadCache3
函数内部则是对办法查找的中心代码
首要经过一张图看一下汇编言语中_objc_msgSend的运转流程
办法查找的中心函数便是_class_lookupMethodAndLoadCache3
函数,接下来要点剖析_class_lookupMethodAndLoadCache3
函数内的源码。
1.1 _class_lookupMethodAndLoadCache3
函数
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
1.2 lookUpImpOrForward
函数
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
// initialize = YES , cache = NO , resolver = YES
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 缓存查找, 由于cache传入的为NO, 这儿不会进行缓存查找, 由于在汇编言语中CacheLookup现已查找过
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
// 避免动态增加办法,缓存会变化,再次查找缓存。
imp = cache_getImp(cls, sel);
// 假如查找到imp, 直接调用done, 回来办法地址
if (imp) goto done;
// 查找办法列表, 传入类目标和办法名
{
// 依据sel去类目标里边查找办法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 假如办法存在,则缓存办法,
// 内部调用的便是 cache_fill 上文中现已详细解说过这个办法,这儿不在赘述了。
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
// 办法缓存之后, 取出imp, 调用done回来imp
imp = meth->imp;
goto done;
}
}
// 假如类办法列表中没有找到, 则去父类的缓存中或办法列表中查找办法
{
unsigned attempts = unreasonableClassCount();
// 假如父类缓存列表及办法列表均找不到办法,则去父类的父类去查找。
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 查找父类的缓存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 在父类中找到办法, 在本类中缓存办法, 留意这儿传入的是cls, 将办法缓存在本类缓存列表中, 而非父类中
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 履行done, 回来imp
goto done;
}
else {
// 跳出循环, 中止查找
break;
}
}
// 查找父类的办法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 相同拿到办法, 在本类进行缓存
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
// 履行done, 回来imp
goto done;
}
}
}
// ---------------- 音讯发送阶段完结 ---------------------
// ---------------- 进入动态解析阶段 ---------------------
// 上述列表中都没有找到办法完结, 则测验解析办法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
triedResolver = YES;
goto retry;
}
// ---------------- 动态解析阶段完结 ---------------------
// ---------------- 进入音讯转发阶段 ---------------------
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
// 回来办法地址
return imp;
}
1.3 getMethodNoSuper_nolock
函数
办法列表中查找办法
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// cls->data() 得到的是 class_rw_t
// class_rw_t->methods 得到的是methods二维数组
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
// mlists 为 method_list_t
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
上述源码中:
-
getMethodNoSuper_nolock
函数中经过遍历办法列表拿到method_list_t
- 终究经过
search_method_list
函数查找办法
1.3.1 search_method_list
函数
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
// 假如办法列表是有序的,则运用二分法查找办法,节省时间
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// 不然则遍历列表查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
1.3.2 findMethodInSortedMethodList
函数内二分查找完结原理
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// >>1 表示将变量n的各个二进制位次序右移1位,最高位补二进制0。
// count >>= 1 假如count为偶数则值变为(count / 2)。假如count为奇数则值变为(count-1) / 2
for (count = list->count; count != 0; count >>= 1) {
// probe 指向数组中间的值
probe = base + (count >> 1);
// 取出中间method_t的name,也便是SEL
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// 取出 probe
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 回来办法
return (method_t *)probe;
}
// 假如keyValue > probeValue 则折半向后查询
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
至此停止,音讯发送阶段现已完结。\
2. 总结
咱们经过一张图来看一下_class_lookupMethodAndLoadCache3
函数内部音讯发送的整个流程:
假如音讯发送
阶段没有找到办法,就会进入动态解析办法
阶段
二、动态办法解析
1. 了解办法的动态解析
当在本类cache
包含class_rw_t
中都找不到办法时会向上找父类的cache
包含class_rw_t
,若一直找不到办法,就会进入动态办法解析
阶段.
咱们来看一下动态解析阶段源码:
动态解析的办法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
_class_resolveMethod
函数内部,依据类目标或元类目标做不同的操作
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
从上述代码能够发现:
- 动态解析办法之后,会将
triedResolver = YES;
- 那么下次就不会在进行动态解析阶段了,之后会从头履行
retry
,会从头对办法查找一遍 - 也便是说:
- 不管咱们是否完结动态解析办法
- 不管动态解析办法是否成功,
retry
之后都不会在进行动态的解析办法了
2. 怎么动态解析办法
- 动态解析
目标办法
时,会调用+(BOOL)resolveInstanceMethod:(SEL)sel
办法; - 动态解析
类办法
时,会调用+(BOOL)resolveClassMethod:(SEL)sel
办法。
这儿以实例目标为例经过代码来看一下动态解析的进程
@implementation Person
- (void) other {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 动态的增加办法完结
if (sel == @selector(test)) {
// 获取其他办法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other));
// 动态增加test办法的完结
class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
// 回来YES表示有动态增加办法
return YES;
}
NSLog(@"%s", __func__);
return [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
[person test];
}
return 0;
}
// 打印成果
// -[Person other]
上述代码中能够看出,person
在调用test
办法时经过动态解析成功调用了other
办法。
经过上面对音讯发送的剖析咱们得知:
- 当本类和父类
cache
和class_rw_t
中都找不到办法时,就会进行动态解析的办法 - 也便是说会自动调用类的
resolveInstanceMethod:
办法进行动态查找 - 因而咱们能够在
resolveInstanceMethod:
办法内部运用class_addMethod
动态的增加办法完结
这儿需求留意class_addMethod
用来向具有给定办法称号
和完结的类
增加新办法
-
class_addMethod
将增加一个办法完结的覆盖,可是不会替换已有的完结 - 也便是说假如上述代码中现已完结了
-(void)test
办法,则不会再动态增加办法,这点在上述源码中也能够表现,由于一旦找到办法完结就直接return imp并调用办法了,不会再履行动态解析办法了。
2.1 动态增加办法
class_addMethod 函数
咱们来看一下class_addMethod
函数的参数别离代表什么。
/**
榜首个参数: cls:给哪个类增加办法
第二个参数: SEL name:增加办法的称号
第三个参数: IMP imp: 办法的完结,函数入口,函数名可与办法名不同(建议与办法名相同)
第四个参数: types :办法类型,需求用特定符号,参考API
*/
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
上述参数上文中现已详细解说过,这儿不再赘述。
需求留意的是咱们在上述代码中经过class_getInstanceMethod
获取Method
的办法
// 获取其他办法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other));
- 其实Method是
objc_method
类型结构体,能够理解为其内部结构同method_t
结构体相同 - 前文中提到过
method_t
是代表办法的结构体,其内部包含SEL、type、IMP
- 咱们经过自界说
method_t
结构体,将objc_method
强转为method_t
检查办法是否能够动态增加成功:struct method_t { SEL sel; char *types; IMP imp; }; - (void) other { NSLog(@"%s", __func__); } + (BOOL)resolveInstanceMethod:(SEL)sel { // 动态的增加办法完结 if (sel == @selector(test)) { // Method强转为method_t struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other)); NSLog(@"%s,%p,%s",method->sel,method->imp,method->types); // 动态增加test办法的完结 class_addMethod(self, sel, method->imp, method->types); // 回来YES表示有动态增加办法 return YES; } NSLog(@"%s", __func__); return [super resolveInstanceMethod:sel]; }
检查打印内容
动态解析办法[3246:1433553] other,0x100000d00,v16@0:8
动态解析办法[3246:1433553] -[Person other]
能够看出的确能够打印出相关信息,那么咱们就能够理解为:
-
objc_method
内部结构同method_t
结构体相同,能够代表类界说中的办法
别的上述代码中咱们经过method_getImplementation
函数和method_getTypeEncoding
函数获取办法的imp
和type
。当然咱们也能够经过自己写的办法来调用,这儿以动态增加有参数的办法为例。
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat:)) {
class_addMethod(self, sel, (IMP)cook, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void cook(id self ,SEL _cmd,id Num)
{
// 完结内容
NSLog(@"%@的%@办法动态完结了,参数为%@",self,NSStringFromSelector(_cmd),Num);
}
上述代码中当调用eat:
办法时,动态增加了cook
函数作为其完结并增加id类型的参数。
2.2 动态解析类办法
当动态解析类办法
的时分,就会调用+(BOOL)resolveClassMethod:(SEL)sel
函数
而咱们知道类办法是存储在元类目标里边的,因而cls榜首个目标需求传入元类目标
以下代码为例:
void other(id self, SEL _cmd)
{
NSLog(@"other - %@ - %@", self, NSStringFromSelector(_cmd));
}
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 榜首个参数是object_getClass(self),传入元类目标。
class_addMethod(object_getClass(self), sel, (IMP)other, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}
咱们在上述源码的剖析中提到过:
- 不管咱们是否完结了
动态解析
的办法,体系内部都会履行retry
对办法再次进行查找 - 那么假如咱们完结了
动态解析办法
,此刻就会顺利查找到办法,从而回来imp
对办法进行调用 - 假如咱们没有完结动态解析办法。就会进行音讯转发。
3. 总结
接下来看一下动态解析办法流程图示
七、音讯转发
1. 音讯转发
假如咱们自己也没有对办法进行动态的解析,那么就会进行音讯转发
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
自己没有才能处理这个音讯的时分,就会进行音讯转发阶段,会调用_objc_msgForward_impcache
函数。
经过查找能够在汇编中找到__objc_msgForward_impcache
函数完结:
-
__objc_msgForward_impcache
函数中调用__objc_msgForward
从而找到__objc_forward_handler
。
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
咱们发现这仅仅是一个过错信息的输出。
其实音讯转发机制是不开源的,可是咱们能够猜测其中可能拿回来的目标调用了objc_msgSend
,重走了一遍音讯发送
,动态解析
,音讯转发
的进程。终究找到办法进行调用。
咱们经过代码来看一下
- 首要创立
Car
类承继自NSObject
,而且Car
有一个- (void) driving
办法, - 当
Person类实例目标
失去了驾车的才能,而且没有在开车进程中动态的学会驾车,那么此刻就会将开车这条信息转发给Car
- 由
Car实例目标
来协助person目标
驾车
#import "Car.h"
@implementation Car
- (void) driving
{
NSLog(@"car driving");
}
@end
--------------
#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 回来能够处理音讯的目标
if (aSelector == @selector(driving)) {
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
--------------
#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
[person driving];
}
return 0;
}
// 打印内容
// 音讯转发[3452:1639178] car driving
由上述代码能够看出:
- 当本类没有完结办法,而且没有动态解析办法,就会调用
forwardingTargetForSelector
函数,进行音讯转发 - 咱们能够完结
forwardingTargetForSelector
函数,在其内部将音讯转发给能够完结此办法的目标
假如forwardingTargetForSelector
函数回来为nil
或许没有完结的话
- 就会调用
methodSignatureForSelector
办法,用来回来一个办法签名(这是咱们正确跳转办法的最后机会) - 假如
methodSignatureForSelector
办法回来正确的办法签名就会调用forwardInvocation
办法 -
forwardInvocation
办法内供给一个NSInvocation
类型的参数-
NSInvocation
封装了一个办法的调用,包含办法的调用者,办法名,以及办法的参数 - 在
forwardInvocation
函数内修正办法调用目标即可
-
- 假如
methodSignatureForSelector
回来的为nil,就会来到doseNotRecognizeSelector:
办法内部 - 程序crash提示无法识别挑选器
unrecognized selector sent to instance
咱们经过以下代码进行验证
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 回来能够处理音讯的目标
if (aSelector == @selector(driving)) {
// 回来nil则会调用methodSignatureForSelector办法
return nil;
// return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
// 办法签名:回来值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving)) {
// return [NSMethodSignature signatureWithObjCTypes: "v@:"];
// return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
// 也能够经过调用Car的methodSignatureForSelector办法得到办法签名,这种办法需求car目标有aSelector办法
return [[[Car alloc] init] methodSignatureForSelector: aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封装了一个办法调用,包含:办法调用者,办法,办法的参数
// anInvocation.target 办法调用者
// anInvocation.selector 办法名
// [anInvocation getArgument: NULL atIndex: 0]; 取得参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// anInvocation中封装了methodSignatureForSelector函数中回来的办法。
// 此刻anInvocation.target 仍是person目标,咱们需求修正target为能够履行办法的办法调用者。
// anInvocation.target = [[Car alloc] init];
// [anInvocation invoke];
[anInvocation invokeWithTarget: [[Car alloc] init]];
}
// 打印内容
// 音讯转发[5781:2164454] car driving
2.总结
上述代码中能够发现办法能够正常调用。接下来咱们来看一下音讯转发阶段的流程图
3.NSInvocation
-
methodSignatureForSelector
办法中回来的办法签名 - 在
forwardInvocation
中被包装成NSInvocation
目标 -
NSInvocation
供给了获取和修正办法名
、参数
、回来值
等办法,也便是说,在forwardInvocation
函数中咱们能够对办法进行最后的修正。
相同上述代码,咱们为driving办法增加回来值和参数,并在forwardInvocation
办法中修正办法的回来值及参数。
#import "Car.h"
@implementation Car
- (int) driving:(int)time
{
NSLog(@"car driving %d",time);
return time * 2;
}
@end
#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 回来能够处理音讯的目标
if (aSelector == @selector(driving)) {
return nil;
}
return [super forwardingTargetForSelector:aSelector];
}
// 办法签名:回来值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving:)) {
// 增加一个int参数及int回来值type为 i@:i
return [NSMethodSignature signatureWithObjCTypes: "i@:i"];
}
return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封装了一个办法调用,包含:办法调用者,办法,办法的参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
int time;
// 获取办法的参数,办法默认还有self和cmd两个参数,因而新增加的参数下标为2
[anInvocation getArgument: &time atIndex: 2];
NSLog(@"修正前参数的值 = %d",time);
time = time + 10; // time = 110
NSLog(@"修正前参数的值 = %d",time);
// 设置办法的参数 此刻将参数设置为110
[anInvocation setArgument: &time atIndex:2];
// 将tagert设置为Car实例目标
[anInvocation invokeWithTarget: [[Car alloc] init]];
// 获取办法的回来值
int result;
[anInvocation getReturnValue: &result];
NSLog(@"获取办法的回来值 = %d",result); // result = 220,阐明参数修正成功
result = 99;
// 设置办法的回来值 从头将回来值设置为99
[anInvocation setReturnValue: &result];
// 获取办法的回来值
[anInvocation getReturnValue: &result];
NSLog(@"修正办法的回来值为 = %d",result); // result = 99
}
#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
// 传入100,并打印回来值
NSLog(@"[person driving: 100] = %d",[person driving: 100]);
}
return 0;
}
音讯转发[6415:2290423] 修正前参数的值 = 100
音讯转发[6415:2290423] 修正前参数的值 = 110
音讯转发[6415:2290423] car driving 110
音讯转发[6415:2290423] 获取办法的回来值 = 220
音讯转发[6415:2290423] 修正办法的回来值为 = 99
音讯转发[6415:2290423] [person driving: 100] = 99
从上述打印成果能够看出:
forwardInvocation
办法中能够对办法的参数及回来值进行修正。
而且咱们能够发现,在设置tagert为Car实例目标时,就现已对办法进行了调用,而在forwardInvocation
办法完毕之后才输出回来值。
经过上述验证咱们能够知道只要来到forwardInvocation
办法中,咱们便对办法调用有了绝对的掌控权,能够挑选是否调用办法,以及修正办法的参数回来值等等。
4. 类办法的音讯转发
类办法音讯转发同目标办法一样,相同需求经过音讯发送,动态办法解析之后才会进行音讯转发机制。
咱们知道类办法是存储在元类目标中的,元类目标本来也是一种特殊的类目标。需求留意的是,类办法的音讯接受者变为元类目标
。
当类目标进行音讯转发时,对调用相应的+号的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation
办法,需求留意的是+号办法仅仅没有提示,而不是体系不会对类办法进行音讯转发。
下面经过一段代码检查类办法的音讯转发机制。
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Person driving];
}
return 0;
}
#import "Car.h"
@implementation Car
+ (void) driving;
{
NSLog(@"car driving");
}
@end
#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
// 回来能够处理音讯的目标
if (aSelector == @selector(driving)) {
// 这儿需求回来类目标
return [Car class];
}
return [super forwardingTargetForSelector:aSelector];
}
// 假如forwardInvocation函数中回来nil 则履行下列代码
// 办法签名:回来值类型、参数类型
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving)) {
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation invokeWithTarget: [Car class]];
}
// 打印成果
// 音讯转发[6935:2415131] car driving
上述代码中相同能够对类目标办法进行音讯转发。需求留意的是类办法的接受者为类目标。其他同目标办法音讯转发模式相同。
总结
OC中的办法调用其实都是转成了objc_msgSend
函数的调用,给receiver(办法调用者)发送了一条音讯(selector办法名)。
办法调用进程中也便是objc_msgSend
底层完结分为三个阶段:音讯发送、动态办法解析、音讯转发。
本文主要对这三个阶段相互之间的关系以及流程进行的探求。上文中现已解说的很详细,这儿不再赘述。
八、super的实质
首要来看一道面试题。 下列代码中Person
承继自NSObject
,Student
承继自Person
,写出下列代码输出内容。
#import "Student.h"
@implementation Student
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"[self class] = %@", [self class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"----------------");
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[super superclass] = %@", [super superclass]);
}
return self;
}
@end
直接来看一下打印内容
Runtime-super[6601:1536402] [self class] = Student
Runtime-super[6601:1536402] [self superclass] = Person
Runtime-super[6601:1536402] ----------------
Runtime-super[6601:1536402] [super class] = Student
Runtime-super[6601:1536402] [super superclass] = Person
上述代码中能够发现不管是self
仍是super
调用class
或superclass
的成果都是相同的。
为什么成果是相同的?
super
关键字在调用办法的时分底层调用流程是怎样的?
咱们经过一段代码来看一下super
底层完结,为Person
类供给run
办法,Student
类中重写run
办法,办法内部调用[super run];
,将Student.m
转化为c++
代码检查其底层完结。
- (void) run
{
[super run];
NSLog(@"Student...");
}
上述代码转化为c++代码
static void _I_Student_run(Student * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("run"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_Student_e677aa_mi_0);
}
经过上述源码能够发现:
-
[super run];
转化为底层源码内部其实调用的是objc_msgSendSuper
函数 -
objc_msgSendSuper
函数内传递了两个参数。__rw_objc_super
结构体和sel_registerName("run")
办法名 -
__rw_objc_super
结构体内传入的参数是self
和class_getSuperclass(objc_getClass("Student"))
也便是Student
的父类Person
首要咱们找到objc_msgSendSuper
函数检查内部结构
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);
能够发现objc_msgSendSuper
中传入的结构体是objc_super
,咱们来到objc_super
内部检查其内部结构。 咱们经过源码查找objc_super
结构体检查其内部结构。
// 精简后的objc_super结构体
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 音讯接受者
__unsafe_unretained _Nonnull Class super_class; // 音讯接受者的父类
/* super_class is the first class to search */
// 父类是榜首个开端查找的类
};
- 从
objc_super
结构体中能够发现receiver
音讯接受者仍然为self
-
superclass
仅仅是用来奉告音讯查找从哪一个类开端。从父类的类目标开端去查找。
咱们经过一张图看一下其中的差异。
从上图中咱们知道 super
调用办法的音讯接受者receiver
仍然是self
,仅仅从父类的类目标开端去查找办法。
那么此刻从头回到面试题,咱们知道class的底层完结如下面代码所示
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
class
内部完结是依据音讯接受者回来其对应的类目标,终究会找到基类的办法列表中
而self
和super
的差异仅仅是self
从本类类目标开端查找办法
super
从父类类目标开端查找办法,因而终究得到的成果都是相同的
别的咱们在回到run
办法内部,很明显能够发现,假如super
不是从父类开端查找办法,从本类查找办法的话,就调用办法本身形成循环调用办法而crash。
同理superclass
底层完结同class
类似,其底层完结代码如下入所示
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
因而得到的成果也是相同的。
1. objc_msgSendSuper2函数
上述OC代码转化为c++代码并不能阐明super
底层调用函数就必定objc_msgSendSuper
。
其实super
底层真实调用的函数时objc_msgSendSuper2函数
咱们能够经过检查super调用办法转化为汇编代码来验证这一说法
- (void)viewDidLoad {
[super viewDidLoad];
}
经过断点检查其汇编调用栈
上图中能够发现super
底层其实调用的是objc_msgSendSuper2
函数,咱们来到源码中查找一下objc_msgSendSuper2
函数的底层完结,咱们能够在汇编文件中找到其相关底层完结。
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
CacheLookup NORMAL
END_ENTRY _objc_msgSendSuper2
经过上面汇编代码咱们能够发现,其实底层是在函数内部调用的class->superclass
获取父类,并不是咱们上面剖析的直接传入的便是父类目标。
其实_objc_msgSendSuper2
内传入的结构体为objc_super2
struct objc_super2 {
id receiver;
Class current_class;
};
咱们能够发现objc_super2
中除了音讯接受者receiver
,另一个成员变量current_class
也便是当时类目标。
与咱们上面剖析的不同_objc_msgSendSuper2
函数内其实传入的是当时类目标,然后在函数内部获取当时类目标的父类,而且从父类开端查找办法。
咱们也能够经过代码验证上述结构体内成员变量究竟是当时类目标仍是父类目标。下文中咱们会经过别的一道面试题验证。
2.isKindOfClass 与 isMemberOfClass
首要看一下isKindOfClass isKindOfClass
目标办法底层完结
- (BOOL)isMemberOfClass:(Class)cls {
// 直接获取实例类目标并判别是否等于传入的类目标
return [self class] == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
// 向上查询,假如找到父类目标等于传入的类目标则回来YES
// 直到基类还不持平则回来NO
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
isKindOfClass isKindOfClass
类办法底层完结
// 判别元类目标是否等于传入的元类元类目标
// 此刻self是类目标 object_getClass((id)self)便是元类
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
// 向上查找,判别元类目标是否等于传入的元类目标
// 假如找到基类还不持平则回来NO
// 留意:这儿会找到基类
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
经过上述源码剖析咱们能够知道。 isMemberOfClass
判别左面是否刚好等于右边类型。 isKindOfClass
判别左面或许左面类型的父类是否刚好等于右边类型。 留意:类办法内部是获取其元类目标进行比较
咱们检查以下代码
NSLog(@"%d",[Person isKindOfClass: [Person class]]);
NSLog(@"%d",[Person isKindOfClass: object_getClass([Person class])]);
NSLog(@"%d",[Person isKindOfClass: [NSObject class]]);
// 输出内容
Runtime-super[46993:5195901] 0
Runtime-super[46993:5195901] 1
Runtime-super[46993:5195901] 1
剖析上述输出内容: 榜首个 0:上面提到过类办法是获取self的元类目标与传入的参数进行比较,可是榜首行咱们传入的是类目标,因而回来NO。
第二个 1:同上,此刻咱们传入Person元类目标,此刻回来YES。验证上述说法
第三个 1:咱们发现此刻传入的是NSObject类目标并不是元类目标,可是回来的值却是YES。 原因是基元类的superclass指针是指向基类目标的。如下图13号线
那么Person元类
经过superclass
指针一直找到基元类,仍是不持平,此刻再次经过superclass
指针来到基类,那么此刻发现持平就会回来YES了。
3. 温习
经过一道面试题对之前学习的常识进行温习。 问:以下代码是否能够履行成功,假如能够,打印成果是什么。
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)test;
@end
// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
NSLog(@"test print name is : %@", self.name);
}
@end
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
Person *person = [[Person alloc] init];
[person test];
}
这道面试题的确很无厘头的一道题,日常作业中没有人这样写代码,可是需求解答这道题需求很齐备的底层常识,咱们经过这道题来温习一下,首要看一下打印成果。
Runtime面试题[15842:2579705] test print name is : <ViewController: 0x7f95514077a0>
Runtime面试题[15842:2579705] test print name is : (null)
经过上述打印成果咱们能够看出,是能够正常运转并打印的,阐明obj
能够正常调用test
办法,可是咱们发现打印self.name
的内容却是<ViewController: 0x7f95514077a0>
。下面person
实例调用test
不做过多解释了,主要用来和上面办法调用做比照。
为什么会是这样的成果呢?首要经过一张图看一下两种调用办法的内存信息。
经过上图咱们能够发现两种办法调用办法很附近。那么obj为什么能够正常调用办法?
3.1 obj为什么能够正常调用办法
首要经过之前的学习咱们知道,person
调用办法时首要经过isa
指针找到类目标从而查找办法并进行调用。
而person
实例目标内实际上是取最前面8个字节空间也便是isa
并经过核算得出类目标地址。
而经过上图咱们能够发现,obj
在调用test
办法时,也会经过其内存地址找到cls
,而cls
中取出最前面8个字节空间其内部存储的刚好是Person
类目标地址。因而obj
是能够正常调用办法的。
3.2 为什么self.name打印内容为ViewController目标
问题出在[super viewDidLoad];
这段代码中,经过上述对super
实质的剖析咱们知道,super
内部调用objc_msgSendSuper2
函数。
咱们知道objc_msgSendSuper2
函数内部会传入两个参数,objc_super2
结构体和SEL
,而且objc_super2
结构体内有两个成员变量音讯接受者和其父类。
struct objc_super2 {
id receiver; // 音讯接受者
Class current_class; // 当时类
};
};
经过以上剖析咱们能够得知[super viewDidLoad];
内部objc_super2
结构体内存储如下所示
struct objc_super = {
self,
[ViewController Class]
};
那么objc_msgSendSuper2
函数调用之前,会先创立局部变量objc_super2
结构体用于为objc_msgSendSuper2
函数传递的参数。
3.3 局部变量由高地址向低地址分配在栈空间
咱们知道局部变量是存储在栈空间内的,而且是由高地址向低地址有序存储。 咱们经过一段代码验证一下。
long long a = 1;
long long b = 2;
long long c = 3;
NSLog(@"%p %p %p", &a,&b,&c);
// 打印内容
0x7ffee9774958 0x7ffee9774950 0x7ffee9774948
经过上述代码打印内容,咱们能够验证局部变量在栈空间内是由高地址向低地址接连存储的。
那么咱们回到面试题中,经过上述剖析咱们知道,此刻代码中包含局部变量以此为objc_super2 结构体
、cls
、obj
。经过一张图展现一下这些局部变量存储结构。
上面咱们知道当person
实例目标调用办法的时分,会取实例变量前8个字节空间也便是isa
来找到类目标地址。那么当拜访实例变量的时分,就越过isa
的8个字节空间往下面去找实例变量。
那么当obj
在调用test
办法的时分相同找到cls
中取出前8个字节,也便是Person类目标
的内存地址,那么当拜访实例变量_name
的时分,会继续向高地址内存空间查找,此刻就会找到objc_super
结构体,从中取出8个字节空间也便是self
,因而此刻拜访到的self.name
便是ViewController目标
。
当拜访成员变量_name
的时分,test
函数中的self
也便是办法调用者其实是obj
,那么self.name
便是经过obj
去找_name
,越过cls的8个指针,在取8个指针此刻自然获取到ViewController目标
。
因而上述代码中cls
就相当于isa
,isa
下面的8个字节空间就相当于_name
成员变量。因而成员变量_name
的拜访到的值便是cls
地址后向高地址位取8个字节地址空间存储的值。
为了验证上述说法,咱们做一个实验,在cls
后高地址中增加一个string
,那么此刻cls
下面的高地址位便是string
。以下示例代码
- (void)viewDidLoad {
[super viewDidLoad];
NSString *string = @"string";
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
Person *person = [[Person alloc] init];
[person test];
}
此刻的局部变量内存结构如下图所示
此刻在拜访_name
成员变量的时分,越过cls
内存往高地址找就会来到string
,此刻拿到的成员变量便是string
了。 咱们来看一下打印内容
Runtime面试题[16887:2829028] test print name is : string
Runtime面试题[16887:2829028] test print name is : (null)
再经过一段代码运用int数据进行实验
- (void)viewDidLoad {
[super viewDidLoad];
int a = 3;
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
Person *person = [[Person alloc] init];
[person test];
}
// 程序crash,坏地址拜访
咱们发现程序由于坏地址拜访而crash,此刻局部变量内存结构如下图所示
当需求拜访_name
成员变量的时分,会在cls
后高地址为查找8位的字节空间,而咱们知道int
占4位字节,那么此刻8位的内存空间一起占据int
数据及objc_super
结构体内,因而就会形成坏地址拜访而crash。
咱们增加新的成员变量进行拜访
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)test;
@end
------------
// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
NSLog(@"test print name is : %@", self.nickName);
}
@end
--------
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *obj1 = [[NSObject alloc] init];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
Person *person = [[Person alloc] init];
[person test];
}
咱们看一下打印内容
// 打印内容
// Runtime面试题[17272:2914887] test print name is : <ViewController: 0x7ffc6010af50>
// Runtime面试题[17272:2914887] test print name is : (null)
能够发现此刻打印的仍然是ViewController目标
,咱们先来看一下其局部变量内存结构
首要经过obj
找到cls
,cls
找到类目标进行办法调用,此刻在拜访nickName
时,obj
查找成员变量,首要越过8个字节的cls
,之后越过name
所占的8个字节空间,终究再取8个字节空间取出其中的值作为成员变量的值,那么此刻也便是self
了。
总结:这道面试题虽然很无厘头,让人感觉无从下手可是调查的内容非常多。 1. super的底层实质为调用objc_msgSendSuper2
函数,传入objc_super2
结构体,结构体内部存储音讯接受者和当时类,用来奉告体系办法查找从父类开端。
2. 局部变量分配在栈空间,而且从高地址向低地址接连分配。先创立的局部变量分配在高地址,后续创立的局部变量接连分配在较低地址。
3. 办法调用的音讯机制,经过isa指针找到类目标进行音讯发送。
4. 指针存储的是实例变量的首字节地址,上述比如中person
指针存储的其实便是实例变量内部的isa
指针的地址。
5. 拜访成员变量的实质,找到成员变量的地址,依照成员变量所占的字节数,取出地址中存储的成员变量的值。
3.4验证objc_msgSendSuper2内传入的结构体参数
咱们运用以下代码来验证上文中留传的问题
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
}
上述代码的局部变量内存结构咱们之前现已剖析过了,真实的内存结构应该如下图所示
经过上面对面试题的剖析,咱们现在想要验证objc_msgSendSuper2
函数内传入的结构体参数,只需求拿到cls
的地址,然后向后移8个地址就能够获取到objc_super
结构体内的self
,在向后移8个地址便是current_class
的内存地址。经过打印current_class
的内容,就能够知道传入objc_msgSendSuper2
函数内部的是当时类目标仍是父类目标了。
咱们来证明他是UIViewController
仍是ViewController
即可
经过上图能够发现,终究打印的内容的确为当时类目标。 因而objc_msgSendSuper2
函数内部其实传入的是当时类目标,而且在函数内部获取其父类,奉告体系从父类办法开端查找的。
专题系列文章
1.前常识
- 01-探求iOS底层原理|综述
- 02-探求iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探求iOS底层原理|LLDB
- 04-探求iOS底层原理|ARM64汇编
2. 根据OC言语探求iOS底层原理
- 05-探求iOS底层原理|OC的实质
- 06-探求iOS底层原理|OC目标的实质
- 07-探求iOS底层原理|几种OC目标【实例目标、类目标、元类】、目标的isa指针、superclass、目标的办法调用、Class的底层实质
- 08-探求iOS底层原理|Category底层结构、App启动时Class与Category装载进程、load 和 initialize 履行、相关目标
- 09-探求iOS底层原理|KVO
- 10-探求iOS底层原理|KVC
- 11-探求iOS底层原理|探求Block的实质|【Block的数据类型(实质)与内存布局、变量捕获、Block的品种、内存办理、Block的修饰符、循环引证】
- 12-探求iOS底层原理|Runtime1【isa详解、class的结构、办法缓存cache_t】
- 13-探求iOS底层原理|Runtime2【音讯处理(发送、转发)&&动态办法解析、super的实质】
- 14-探求iOS底层原理|Runtime3【Runtime的相关使用】
- 15-探求iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探求iOS底层原理|RunLoop的使用
- 17-探求iOS底层原理|多线程技能的底层原理【GCD源码剖析1:主行列、串行行列&&并行行列、大局并发行列】
- 18-探求iOS底层原理|多线程技能【GCD源码剖析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探求iOS底层原理|多线程技能【GCD源码剖析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探求iOS底层原理|多线程技能【GCD源码剖析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探求iOS底层原理|多线程技能【线程锁:自旋锁、互斥锁、递归锁】
- 22-探求iOS底层原理|多线程技能【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探求iOS底层原理|内存办理【Mach-O文件、Tagged Pointer、目标的内存办理、copy、引证计数、weak指针、autorelease
3. 根据Swift言语探求iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、属性
、办法
、swift多态原理
、String
、Array
、Dictionary
、引证计数
、MetaData
等Swift根本语法和相关的底层原理文章有如下几篇:
- Swift5中心语法1-基础语法
- Swift5中心语法2-面向目标语法1
- Swift5中心语法2-面向目标语法2
- Swift5常用中心语法3-其它常用语法
- Swift5使用实践常用技能点
其它底层原理专题
1.底层原理相关专题
- 01-核算机原理|核算机图形烘托原理这篇文章
- 02-核算机原理|移动终端屏幕成像与卡顿
2.iOS相关专题
- 01-iOS底层原理|iOS的各个烘托结构以及iOS图层烘托原理
- 02-iOS底层原理|iOS动画烘托原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏烘托原理
- 04-iOS底层原理|因CPU、GPU资源耗费导致卡顿的原因和解决计划
3.webApp相关专题
- 01-Web和类RN大前端的烘托原理
4.跨渠道开发计划相关专题
- 01-Flutter页面烘托原理
5.阶段性总结:Native、WebApp、跨渠道开发三种计划功能比较
- 01-Native、WebApp、跨渠道开发三种计划功能比较
6.Android、HarmonyOS页面烘托专题
- 01-Android页面烘托原理
-
02-HarmonyOS页面烘托原理 (
待输出
)
7.小程序页面烘托专题
- 01-小程序结构烘托原理