前言

Objective-C 是一门动态语言,它将很多静态语言在编译和链接时期做的事情,放到了运行时来处理。之所以能具备这种特性,离不开 Runtime 这个库。Runtime 很好的解决了如何在运行时期找到调用方法这样的问题。下面话不多说了,来一起学习学习吧。

消息发送

在 Objective-C 中,方法调用称为向对象发送消息:

// MyClass 类
@interface MyClass: NSObject
- (void)printLog;
@end
@implementation MyClass
- (void)printLog {
NSLog(@"print log !");
}
@end
MyClass *myClass = [[MyClass alloc] init];
[myClass printLog];
// 输出: print log !

上面代码中的 [myClass printLog] 也可以这么写:

((void (*)(id, SEL))(void *) objc_msgSend)(myClass, @selector(printLog));

[myClass printLog] 经过编译后就是调用 objc_msgSend 方法。

我们看看这个方法的文档定义:

id objc_msgSend(id self, SEL op, ...);

self:消息的接收者 op: 消息的方法名,C 字符串 ... :参数列表

Runtime 是如何找到实例方法的具体实现的?

基础概念

讲之前,我们需要先明白一些基础概念:Objective-C 是一门面向对象的语言,对象又分为实例对象、类对象、元类对象以及根元类对象。它们是通过一个叫 isa 的指针来关联起来,具体关系如下图:

以我们上文的代码为例:

MyClass *myClass = [[MyClass alloc] init];

整理下相互间的关系:

  • myClass 是实例对象
  • MyClass 是类对象
  • MyClass 的元类就是 NSObject 的元类
  • NSObject 就是 Root class (class)
  • NSObject 的 superclass 为 nil
  • NSObject 的元类就是它自己
  • NSObject 的 superclass 就是 NSObject

对应上图中的位置关系如下:

接着,我们用代码来验证下上文的关系:

MyClass *myClass = [[MyClass alloc] init];

Class class = [myClass class];
Class metaClass = object_getClass(class);
Class metaOfMetaClass = object_getClass(metaClass);
Class rootMetaClass = object_getClass(metaOfMetaClass);
Class superclass = class_getSuperclass(class);
Class superOfSuperclass = class_getSuperclass(superclass);
Class superOfMetaOfSuperclass = class_getSuperclass(object_getClass(superclass));

NSLog(@"MyClass 实例对象是:%p",myClass);
NSLog(@"MyClass 类对象是:%p",class);
NSLog(@"MyClass 元类对象是:%p",metaClass);
NSLog(@"MyClass 元类对象的元类对象是:%p",metaOfMetaClass);
NSLog(@"MyClass 根元类对象是:%p",rootMetaClass);
NSLog(@"MyClass 父类是:%@",class_getSuperclass(class));
NSLog(@"MyClass 父类的父类是:%@",superOfSuperclass);
NSLog(@"MyClass 父类的元类的父类是:%@",superOfMetaOfSuperclass);

NSLog(@"NSObject 元类对象是:%p",object_getClass([NSObject class]));
NSLog(@"NSObject 父类是:%@",[[NSObject class] superclass]);
NSLog(@"NSObject 元类对象的父类是:%@",[object_getClass([NSObject class]) superclass]);

//输出:
MyClass 实例对象是:0x60c00000b8d0
MyClass 类对象是:0x109ae3fd0
MyClass 元类对象是:****0x109ae3fa8
MyClass 元类对象的元类对象是:****0x10ab02e58**
MyClass 根元类对象是:0x10ab02e58
MyClass 父类是:NSObject
MyClass 父类的父类是:(null)
MyClass 父类的元类的父类是:NSObject
NSObject 元类对象是:0x10ab02e58
NSObject 父类是:(null)
NSObject 元类对象的父类是:NSObject

可以发现,输出结果是完全符合我们的结论的!

现在我们能知道各种对象之间的关系:

实例对象通过 isa 指针,找到类对象 Class;类对象同样通过 isa 指针,找到元类对象;元类对象也是通过 isa 指针,找到根元类对象;最后,根元类对象的 isa 指针,指向自己。可以发现 NSObject 是整个消息机制的核心,绝大数对象都继承自它。

寻找流程

上文提到了,一个 Objective-C 方法会被编译成 objc_msgSend,这个函数有两个默认参数,id 类型的 self, SEL 类型的 op。我们先看看 id 的定义:

typedef struct objc_object *id;
struct objc_object {
 Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

我们可以看到,在 objc_object 结构体中,只有一个指向 Class 类型的 isa 指针。

我们再看看 Class 的定义:

struct objc_class {
 Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
 Class _Nullable super_class OBJC2_UNAVAILABLE;
 const char * _Nonnull name OBJC2_UNAVAILABLE;
 long version OBJC2_UNAVAILABLE;
 long info OBJC2_UNAVAILABLE;
 long instance_size OBJC2_UNAVAILABLE;
 struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
 struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
 struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
 struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

里面有很多参数,很显眼的能看到这一行:

struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;

看名字也容易理解,这个 methodLists 就是用来存放方法列表的。我们再看看 objc_method_list 这个结构体:

struct objc_method_list {
 struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
 
 int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
 int space OBJC2_UNAVAILABLE;
#endif
 /* variable length structure */
 struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

里面的 objc_method ,也就是我们熟悉的 Method:

struct objc_method {
 SEL _Nonnull method_name OBJC2_UNAVAILABLE;
 char * _Nullable method_types OBJC2_UNAVAILABLE;
 IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}

Method 里面保存了三个参数:

  • 方法的名称
  • 方法的类型
  • 方法的具体实现,由 IMP 指针指向

经过层层挖掘,我们能明白实例对象调用方法的大致逻辑:

MyClass *myClass = [[MyClass alloc] init];
[myClass printLog];
  • 先被编译成  ((void (*)(id, SEL))(void *) objc_msgSend)(myClass, @selector(printLog));
  • 沿着入参 myClass 的 isa 指针,找到 myClass 的类对象(Class),也就是 MyClass
  • 接着在 MyClass 的方法列表 methodLists 中,找到对应的 Method
  • 最后找到 Method 中的 IMP 指针,执行具体实现

类对象的类方法又是怎么找到并执行的?

由上文,我们已经知道,实例对象是通过 isa 指针,找到其类对象(Class)中保存的方法列表中的具体实现的。

比如:

MyClass *myClass = [[MyClass alloc] init];
[myClass printLog];

可以理解为:printLog 方法就是保存在 MyClass 中的。

那么如果是个类方法,又是保存在什么地方的呢?

我们回顾下 Class  的定义:

struct objc_class {
 Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
 Class _Nullable super_class OBJC2_UNAVAILABLE;
 const char * _Nonnull name OBJC2_UNAVAILABLE;
 long version OBJC2_UNAVAILABLE;
 long info OBJC2_UNAVAILABLE;
 long instance_size OBJC2_UNAVAILABLE;
 struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
 struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
 struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
 struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

可以发现到这一行:

Class _Nonnull isa OBJC_ISA_AVAILABILITY;

这里的 isa 同样是指向一个 Class 的指针。上文中,我们也知道了类对象的 isa 指针是指向元类对象的。那么不难得出:

类对象的类方法,是保存在元类对象中的!

类对象和元类对象都是  Class 类型,仅仅服务的对象不同罢了。找到了元类对象,自然就找到了元类对象中的 methodLists,接下来就和实例对象的方法寻找调用一样的流程了。

关于父类(superclass)

在 Objective-C 中,子类调用一个方法,如果没有子类没有实现,父类实现了,会去调用父类的实现。上文中,找到 methodLists 后,寻找 Method 的过程如下:

如何提高方法查找的效率?

上文中,我们大概知道,方法是通过 isa 指针,查找 Class 中的 methodLists 的。如果子类没实现对应的方法实现,还会沿着父类去查找。整个工程,可能有成万上亿个方法,是如何解决性能问题的呢?

例如:

for (int i = 0; i < 100000;   i) {
 MyClass *myObject = myObjects[i];
 [myObject methodA];
}

这种高频次的调用 methodA,如果每调用一次都需要遍历,性能是非常差的。所以引入了 Class Cache 机制:

Class Cache 认为,当一个方法被调用,那么它之后被调用的可能性就越大。

查找方法时,会先从缓存中查找,找到直接返回 ;找不到,再去 Class 的方法列表中找。

在上文中 Class 的定义中,我们可以发现  cache:

struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;

说明了缓存是存在类中的,每个类都有一份方法缓存,而不是每个类的 object 都保存了一份。

消息转发

如果方法列表(methodLists)没找到对应的 selector 呢?

// ViewController.m 中 (未实现 myTestPrint 方法)
[self performSelector:@selector(myTestPrint:) withObject:@",你好 !"];

系统会提供三次补救的机会。

第一次

  (BOOL)resolveInstanceMethod:(SEL)sel {} (实例方法)
  (BOOL)resolveClassMethod:(SEL)sel {} (类方法)

这两个方法,一个针对实例方法;一个针对类方法。返回值都是 Bool。

使用示例:

// ViewController.m 中
void myMethod(id self, SEL _cmd,NSString *nub) {
 NSLog(@"ifelseboyxx%@",nub);
}

  (BOOL)resolveInstanceMethod:(SEL)sel {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
 if (sel == @selector(myTestPrint:)) {
#pragma clang diagnostic pop
  class_addMethod([self class],sel,(IMP)myMethod,"v@:@");
  return YES;
 }else {
  return [super resolveInstanceMethod:sel];
 }
}

我们只需要在 resolveInstanceMethod: 方法中,利用 class_addMethod 方法,将未实现的 myTestPrint: 绑定到 myMethod 上就能完成转发,最后返回 YES。

第二次

- (id)forwardingTargetForSelector:(SEL)aSelector {}

这个方法要求返回一个 id。使用场景一般是将 A 类的某个方法,转发到 B 类的实现中去。

使用示例:

想转发到 Person 类中的 -myTestPrint: 方法中:

@interface Person : NSObject
@end
@implementation Person
- (void)myTestPrint:(NSString *)str {
 NSLog(@"ifelseboyxx%@",str);
}
@end
// ViewController.m 中
- (id)forwardingTargetForSelector:(SEL)aSelector {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
 if (aSelector == @selector(myTestPrint:)) {
#pragma clang diagnostic pop
  return [Person new];
 }else{
  return [super forwardingTargetForSelector:aSelector];
 }
}

第三次

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {}
- (void)forwardInvocation:(NSInvocation *)anInvocation {}

第一个要求返回一个方法签名,第二个方法转发具体的实现。二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。

这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活,forwardingTargetForSelector: 只能固定的转发到一个对象;forwardInvocation:  可以让我们转发到多个对象中去。

使用实例:

想转发到 Person 类以及 Animal 类中的 -myTestPrint: 方法中:

@interface Person : NSObject
@end
@implementation Person
- (void)myTestPrint:(NSString *)str {
 NSLog(@"ifelseboyxx%@",str);
}
@end
@interface Animal : NSObject
@end
@implementation Animal
- (void)myTestPrint:(NSString *)str {
 NSLog(@"tiger%@",str);
}
@end
// ViewController.m 中

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wundeclared-selector"
 if (aSelector == @selector(myTestPrint:)) {
 #pragma clang diagnostic pop
 return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
 return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
 Person *person = [Person new];
 Animal *animal = [Animal new];
 if ([person respondsToSelector:anInvocation.selector]) {
 [anInvocation invokeWithTarget:person];
 }
 if ([animal respondsToSelector:anInvocation.selector]) {
 [anInvocation invokeWithTarget:animal];
 }
}

⚠️ 如果到了第三次机会,还没找到对应的实现,就会 crash:

unrecognized selector sent to instance 0x7f9f817072b0

总结

到这里,我们大概能了解消息发送与转发的过程了,附上流程图:

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对Devmax的支持。

iOS消息发送和转发示例详解的更多相关文章

  1. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  2. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  4. ios – Testflight无法安装应用程序

    我有几个测试人员注册了testflight并连接了他们的设备……他们有不同的ios型号……但是所有这些都有同样的问题.当他们从“safari”或“testflight”应用程序本身单击应用程序的安装按钮时……达到约90%并出现错误消息…

  5. ibm-mobilefirst – 在iOS 7.1上获取“无法安装应用程序,因为证书无效”错误

    当我的客户端将他们的设备更新到iOS7.1,然后尝试从AppCenter更新我们的应用程序时,我收到了上述错误.经过一番搜索,我找到了一个类似问题的帖子here.但是后来因为我在客户端使用AppCenter更新应用程序的环境中,我无法使用USB插件并为他们安装应用程序.在发布支持之前,是否有通过AppCenter进行下载的解决方法?

  6. ios – 视图的简单拖放?

    我正在学习iOS,但我找不到如何向UIView添加拖放行为.我试过了:它说“UIView没有可见的接口声明选择器addTarget”此外,我尝试添加平移手势识别器,但不确定这是否是我需要的它被称为,但不知道如何获得事件的坐标.在iOS中注册移动事件回调/拖放操作的标准简单方法是什么?

  7. ios – 什么控制iTunes中iPhone应用程序支持的语言列表?

    什么控制iPhone应用程序的iTunes页面中支持的语言?

  8. ios – 获得APNs响应BadDeviceToken或Unregistered的可能原因是什么?

    我知道设备令牌在某些时候是有效的.用户如何使其设备令牌变坏?从关于“未注册”的文档:Thedevicetokenisinactiveforthespecifiedtopic.这是否意味着应用程序已被删除?.您应该看到四种分发方法:如果您选择AppStore或Enterprise,您将在后面的对话框中看到Xcode将APNS权利更改为生产:如果选择AdHoc或Development,则aps-environment下的文本将是开发,然后应与后端的配置匹配.

  9. ios – 当我关闭应用程序时,我从调试器获得消息:由于信号15而终止

    我怎么能解决这个问题,我不知道这个链接MypreviousproblemaboutCoredata对我的问题有影响吗?当我cmd应用程序的Q时,将出现此消息.Messagefromdebugger:Terminatedduetosignal15如果谁知道我以前的问题的解决方案,请告诉我.解决方法>来自调试器的消息:每当用户通过CMD-Q(退出)或STOP手动终止应用程序(无论是在iOS模拟器中还是

  10. ios – NSUbiquityIdentityDidChangeNotification和SIGKILL

    当应用程序被发送到后台时,我们会删除观察者吗?我遇到的问题是,当UbiquityToken发生变化时,应用程序终止,因为用户已经更改了iCloud设置.你们如何设法订阅这个通知,如果你不这样做,你会做什么来跟踪当前登录的iCloud用户?

随机推荐

  1. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  2. iOS – genstrings:无法连接到输出目录en.lproj

    使用我桌面上的项目文件夹,我启动终端输入:cd然后将我的项目文件夹拖到终端,它给了我路径.然后我将这行代码粘贴到终端中找.-name*.m|xargsgenstrings-oen.lproj我在终端中收到此错误消息:genstrings:无法连接到输出目录en.lproj它多次打印这行,然后说我的项目是一个目录的路径?没有.strings文件.对我做错了什么的想法?

  3. iOS 7 UIButtonBarItem图像没有色调

    如何确保按钮图标采用全局色调?解决方法只是想将其转换为根注释,以便为“回答”复选标记提供更好的上下文,并提供更好的格式.我能想出这个!

  4. ios – 在自定义相机层的AVFoundation中自动对焦和自动曝光

    为AVFoundation定制图层相机创建精确的自动对焦和曝光的最佳方法是什么?

  5. ios – Xcode找不到Alamofire,错误:没有这样的模块’Alamofire’

    我正在尝试按照github(https://github.com/Alamofire/Alamofire#cocoapods)指令将Alamofire包含在我的Swift项目中.我创建了一个新项目,导航到项目目录并运行此命令sudogeminstallcocoapods.然后我面临以下错误:搜索后我设法通过运行此命令安装cocoapodssudogeminstall-n/usr/local/bin

  6. ios – 在没有iPhone6s或更新的情况下测试ARKit

    我在决定下载Xcode9之前.我想玩新的框架–ARKit.我知道要用ARKit运行app我需要一个带有A9芯片或更新版本的设备.不幸的是我有一个较旧的.我的问题是已经下载了新Xcode的人.在我的情况下有可能运行ARKit应用程序吗?那个或其他任何模拟器?任何想法或我将不得不购买新设备?解决方法任何iOS11设备都可以使用ARKit,但是具有高质量AR体验的全球跟踪功能需要使用A9或更高版本处理器的设备.使用iOS11测试版更新您的设备是必要的.

  7. 将iOS应用移植到Android

    我们制作了一个具有2000个目标c类的退出大型iOS应用程序.我想知道有一个最佳实践指南将其移植到Android?此外,由于我们的应用程序大量使用UINavigation和UIView控制器,我想知道在Android上有类似的模型和实现.谢谢到目前为止,guenter解决方法老实说,我认为你正在计划的只是制作难以维护的糟糕代码.我意识到这听起来像很多工作,但从长远来看它会更容易,我只是将应用程序的概念“移植”到android并从头开始编写.

  8. ios – 在Swift中覆盖Objective C类方法

    我是Swift的初学者,我正在尝试在Swift项目中使用JSONModel.我想从JSONModel覆盖方法keyMapper,但我没有找到如何覆盖模型类中的Objective-C类方法.该方法的签名是:我怎样才能做到这一点?解决方法您可以像覆盖实例方法一样执行此操作,但使用class关键字除外:

  9. ios – 在WKWebView中获取链接URL

    我想在WKWebView中获取tapped链接的url.链接采用自定义格式,可触发应用中的某些操作.例如HTTP://我的网站/帮助#深层链接对讲.我这样使用KVO:这在第一次点击链接时效果很好.但是,如果我连续两次点击相同的链接,它将不报告链接点击.是否有解决方法来解决这个问题,以便我可以检测每个点击并获取链接?任何关于这个的指针都会很棒!解决方法像这样更改addobserver在observeValue函数中,您可以获得两个值

  10. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

返回
顶部