关闭

Objective-C之方法调用机制(消息传递)

416人阅读 评论(0) 收藏 举报
分类:

在 Objective-C之Meta-class和isa指针 中我提到,当一个对象调用方法的时候,objective-c的运行时会去这个对象的isa指针缩指向的Class的方法列表中去寻找对应的方法。我们再看一下objc_Class的定义,这次我加上注释。
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE; //指向父类的指针
    const char *name                                         OBJC2_UNAVAILABLE; //类名
    long version                                             OBJC2_UNAVAILABLE; //版本
    long info                                                OBJC2_UNAVAILABLE; //类信息
    long instance_size                                       OBJC2_UNAVAILABLE; //实例大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE; //实例参数列表
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE; //方法链表
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE; //方法缓存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE; //协议链表
#endif
    
} OBJC2_UNAVAILABLE;
我们知道,对象接受到消息的时候会去methodList中寻找可以调用的方法,可以推测,methodLists里面存储的对象至少要具备两个元素,一个代表方法名,一个代表方法的具体实现,即指向具体实现该方法的函数的函数指针。

1.什么是IMP SEL Method
我们看一下objc_method_list类型链表的实现
struct objc_method_list {
    struct objc_method_list *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;
}                                                            OBJC2_UNAVAILABLE;


可以看到methodLists链表中以objc_method类型存储方法,再看一下Method的定义
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

可以清楚地看到,Method类型的结构体,包含一个代表方法名的SEL,一个代表方法参数类型的char*,一个指向具体实现函数的IMP指针。
什么是SEL:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
他是一个指向objc_selector类型的指针,不同的类可以拥有相同的selector, 同样地方法名对应同一个selector。当我们内存中的对象受到消息时,会到自己的方法链表中根据selector找到具体的实现函数IMP,最终执行。这是一个动态绑定的过程,在编译的时候我们不知道最终会执行哪些代码。
什么是IMP:
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif
结合之前的结论,很明显:IMP是一个函数指针,这个函数包含一个接受消息的对象id,调用方法的选标SEL,以及不定个数的参数,并返回一个id类型的返回值。我们其实可以直接调用这个函数指针,稍后我用一段测试代码来展示如何调用函数指针。

2.什么是objc_msgSend
通过之前的分析,我们大概知道,在一个方法调用的时候,我们的编译器会将它转换为对IMP的调用,这个寻找的过程是通过在对象的Class的methodLists中通过SEL进行的,那么具体的实现代码是怎样的呢。
先写一个简单的方法调用:
[self class];
在命令行中用clang重写:
liweipengdeMacBook-Air:TestIMP liweipeng$ clang -rewrite-objc testObj.m
在重写后的文件中找到方法的实现:
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"));
很明显,self指针调用的方法,转换成了objc_msgSend的函数调用,我们看一下objc_msgSend的定义:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
我们可以看到,这个函数需要的参数,一个是receiver, 一个是sel,还有函数调用所需要的参数。我们现在已经知道我们的方法调用是通过向一个函数传递调用的对象和SEL来找到对硬的IMP,但是寻找的过程是怎样的呢,我们看一下苹果官方公开的源码实现:
static Method look_up_method(Class cls, SEL sel, BOOL withCache, BOOL withResolver)
{
    Method meth = NULL;

    if (withCache) {
        meth = _cache_getMethod(cls, sel, &_objc_msgForward_internal);
        if (meth == (Method)1) {
            // Cache contains forward:: . Stop searching.
            return NULL;
        }
    }

    if (!meth) meth = _class_getMethod(cls, sel);

    if (!meth  &&  withResolver) meth = _class_resolveMethod(cls, sel);

    return meth;
}
通过分析上面的代码,并且结合objc_class的定义,我们可以知道查找过程 是这样的:
1.首先到该类的方法cache中去寻找,如果找到了,返回改函数
2.如果没有找到就到该类的方法列表中查找,如果找到了,将该IMP返回并且将它加入到cache中缓存起来,这样可以节省再次调用的开销。
3.如果还没有找到,通过该类结构中的super_class指针去它的弗雷的方法列表中寻找,如果找到了了对应的IMP,返回IMP并加入cache
4.如果在自身及父类的方法列表中都没有找到,则看是否可以动态决议(本文先不讲)
5.如果动态方法决议没有解决,进入消息转发流程(本文先不讲)

3.验证几个问题
我们先建两个基础类:
@interface TestFather : NSObject
- (int)testMethod:(int)pa1 andPa2:(int)pa2;
@end

@implementation TestFather
- (int)testMethod:(int)pa1 andPa2:(int)pa2
{
    NSLog(@"Father pa1:%d pa2:%d", pa1, pa2);
    return 0;
}
@end

@interface TestSon : TestFather
@end

@implementation TestSon
- (int)testMethod:(int)pa1 andPa2:(int)pa2
{
    NSLog(@"Son pa1:%d pa2:%d", pa1, pa2);
    return 0;
}
@end

(1)SEL是否只和方法名有关
 TestFather *father = [[TestFather alloc] init];
 TestSon *son = [[TestSon alloc] init];
        
 SEL sel = @selector(testMethod:andPa2:);
 SEL sel1 = NSSelectorFromString(@"testMethod:andPa2:");
        
 IMP imp1 = [father methodForSelector:sel];
 IMP imp2 = [son methodForSelector:sel];
        
 NSLog(@"sel -> %p  sel1 -> %p", sel, sel1);
 NSLog(@"imp1 -> %p  imp2 -> %p", imp1, imp2);
log结果:
2015-11-19 13:32:35.715 TestIMP[15252:493881] sel -> 0x100003bad  sel1 -> 0x100003bad
2015-11-19 13:32:36.398 TestIMP[15252:493881] imp1 -> 0x100000e70  imp2 -> 0x100000eb0
结论:相同名字的SEL指向同一块内存地址,不通的类可以拥有相同的SEL, 根据同一个SEL找到的对应的IMP是不同的。
(2)如何直接调用函数指针
  int (*doTestMethod)(id ,SEL , int, int);        
  doTestMethod = (int(*)(id, SEL, int, int))imp2;
  doTestMethod(son, sel, 3, 4);
(3)直接调用函数会节省方法调用时消息传递的开销,那么直接调用函数是否比oc本身的消息传递效率更高?
  long j = 1000;
  NSDate *date = [NSDate date];
  for(int i = 0; i < j; i++ )
  {
      doTestMethod(son, sel, 3, 4);
  }
  NSDate *date1 = [NSDate date];
  double time = [date1 timeIntervalSinceDate:date];
  NSLog(@"直接调用函数指针消耗时间: %f", time);
        
  date = [NSDate date];
  for(int i = 0; i < j; i++ )
  {
      [son testMethod:3 andPa2:4];
  }
  date1 = [NSDate date];
  time = [date1 timeIntervalSinceDate:date];
  NSLog(@"消息发送调用消耗时间: %f", time);
 j = 1000时,log:
2015-11-19 13:46:13.528 TestIMP[15441:505641] 直接调用函数指针消耗时间: 0.000026
2015-11-19 13:46:13.530 TestIMP[15441:505641] 消息发送调用消耗时间: 0.000017
j = 1000000时,log:
2015-11-19 13:47:20.208 TestIMP[15471:506875] 直接调用函数指针消耗时间: 0.005341
2015-11-19 13:47:20.217 TestIMP[15471:506875] 消息发送调用消耗时间: 0.007045
j = 1000000000时, log:
2015-11-19 13:49:08.250 TestIMP[15505:509402] 直接调用函数指针消耗时间: 5.165577
2015-11-19 13:49:15.282 TestIMP[15505:509402] 消息发送调用消耗时间: 7.030475
上述的测试结果跟设备有关,而且每一次的结果也不同。
结论:在调用次数少于10000次时,直接调用函数指针的效率优势并不明显,有时候还是oc的消息传递效率更好,只有循环次数非常大的时候,直接调用函数指针才有效率优势。




0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:2905次
    • 积分:54
    • 等级:
    • 排名:千里之外
    • 原创:2篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类
    文章存档