runtime——消息机制

1453882669997129.jpg

本文受权转载,做者:Sindri的小巢(简书java

从异常提及算法

咱们都知道,在iOS中存在这么一个通用类类型id,它能够用来表示任何对象的类型 —— 这意味着咱们使用id类型的对象调用任何一个方法,编译器都不会进行报错。好比下面这段代码:编程

1
2
id wrongArr = @ "This is a NSString instance." ;
[wrongArr addObject: @ "The operate will crash your application" ];

不出意外的,编译器会给你这么一个信息而后华丽丽的崩溃了。相信几乎全部的开发者们在开发生涯中都遇到过这种崩溃信息:数组

1
-[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x10675c060

很简单,咱们朝着一个地址为0x10675c060的实例对象发送了不属于这个对象的方法。这句话不是instance 0x10675c060 called unrecognized selector,而是消息发送错误。实际上,咱们每一次对OC对象的方法调用都是一次消息的发送缓存

783864-d2544915fc753f76.jpg

消息发送异常性能优化

关于静态语言和动态语言app

这里要先介绍计算机的开发语言的一个专业名词:动态语言和静态语言。确切的说,OC是一门动态语言。动态语言和静态语言二者的区别以下:ide

  • 静态语言: 静态语言在运行前会进行类型判断,类的全部成员、方法都会在编译阶段肯定好内存地址。类成员只能访问属于本身的方法和变量,像上面的调用代码没法经过编译,会直接引发编译器报错。但由于如此,静态语言结构规范、便于调试、且能够进行多样的性能优化。常见的静态语言包括java/C++/C等函数

  • 动态语言:大部分的判断工做被推迟到运行时进行,类的成员变量、方法地址都在运行时确认。能够在运行时动态的添加类成员、方法等。具备较高的灵活性和可定制性、便于阅读,但方法一般没法进行内联等优化性能

两种语言孰优孰略本人不在这里作判断,可是要知道的是smalltalk是动态语言的鼻祖,更是OC发展的最大推进力。在smalltalk中,全部的东西都是对象(或者都应该被当作对象),例如表达式2 + 3被理解成向对象2发送了消息+,其中接收的参数是 3

消息发送

在前篇runtime-属性与变量中咱们导入过runtime的头文件实现了一键归档功能,今天咱们要导入另一个文件:

在OC中,调用一个方法的格式以下:

1
[davin playWith: friend];

在方法调用的时候,runtime会将上面的方法调用转换成一个C语言的函数调用,表示朝着davin发送了一个playWith:消息,并传入了friend这个参数:

1
objc_msgSend(davin, @selector(playWith:), friend);

那么在这个C语言函数中发生了什么事情?编译器是如何找到这个类的方法的呢?苹果开源了runtime的实现代码,其中为了高度优化性能,苹果使用汇编实现了这个函数(源码处于Source/objc-msg-arm.s文件下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*****************************************************************
  *
  * id objc_msgSend(id self, SEL    _cmd,...);
  *
  *****************************************************************/
     ENTRY objc_msgSend
     MESSENGER_START
     cbz    r0, LNilReceiver_f   // 判断消息接收者是否为nil
     ldr    r9, [r0]    // r9 = self->isa
     CacheLookup NORMAL   // 到缓存中查找方法
LCacheMiss:          // 方法未缓存
     MESSENGER_END_SLOW
     ldr    r9, [r0,  #ISA]
     b    __objc_msgSend_uncached
LNilReceiver:       // 消息接收者为nil处理
     mov    r1,  #0
     mov    r2,  #0
     mov    r3,  #0
     FP_RETURN_ZERO
     MESSENGER_END_NIL
     bx    lr
LMsgSendExit:
     END_ENTRY objc_msgSend

即便不懂汇编,上面的代码经过注释后也足以让各位一窥究竟。从上述代码中咱们能够看到一个方法调用过程当中发生的事情,包括:

  • 判断接收者是否为nil,若是为nil,清空寄存器,消息发送返回nil

  • 到类缓存中查找方法,若是存在直接返回方法

  • 没有找到缓存,到类的方法列表中依次寻找

查找方法实现是经过_class_lookupMethodAndLoadCache3这个奇怪的函数完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
     return  lookUpImpOrForward(cls, sel, obj,
                           YES /*initialize*/ , NO /*cache*/ , YES /*resolver*/ );
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                    bool initialize, bool cache, bool resolver)
{
     Class curClass;
     IMP methodPC = nil;
     Method meth;
     bool triedResolver = NO;
     methodListLock.assertUnlocked();
     // 若是传入的cache为YES,到类缓存中查找方法缓存
     if  (cache) {
         methodPC = _cache_getImp(cls, sel);
         if  (methodPC)  return  methodPC;
     }
     // 判断类是否已经被释放
     if  (cls == _class_getFreedObjectClass())
         return  (IMP) _freedHandler;
     // 若是类未初始化,对其进行初始化。若是这个消息是initialize,那么直接进行类的初始化
     if  (initialize  &&  !cls->isInitialized()) {
         _class_initialize (_class_getNonMetaClass(cls, inst));
     }
  retry:
     methodListLock.lock();
     // 忽略在GC环境下的部分消息,好比retain、release等
     if  (ignoreSelector(sel)) {
         methodPC = _cache_addIgnoredEntry(cls, sel);
         goto done;
     }
     // 遍历缓存方法,若是找到,直接返回
     methodPC = _cache_getImp(cls, sel);
     if  (methodPC) goto done;
     // 遍历类自身的方法列表查找方法实现
     meth = _class_getMethodNoSuper_nolock(cls, sel);
     if  (meth) {
         log_and_fill_cache(cls, cls, meth, sel);
         methodPC = method_getImplementation(meth);
         goto done;
     }
     // 尝试向上遍历父类的方法列表查找实现
     curClass = cls;
     while  ((curClass = curClass->superclass)) {
         // Superclass cache.
         meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
         if  (meth) {
             if  (meth != (Method)1) {
                 log_and_fill_cache(cls, curClass, meth, sel);
                 methodPC = method_getImplementation(meth);
                 goto done;
             }
             else  {
                 // Found a forward:: entry in a superclass.
                 // Stop searching, but don't cache yet; call method
                 // resolver for this class first.
                 break ;
             }
         }
         // 查找父类的方法列表
         meth = _class_getMethodNoSuper_nolock(curClass, sel);
         if  (meth) {
             log_and_fill_cache(cls, curClass, meth, sel);
             methodPC = method_getImplementation(meth);
             goto done;
         }
     }
     // 没有找到任何的方法实现,进入消息转发第一阶段“动态方法解析”
     // 调用+ (BOOL)resolveInstanceMethod: (SEL)selector
     // 征询接收者所属的类是否可以动态的添加这个未实现的方法来解决问题
     if  (resolver  &&  !triedResolver) {
         methodListLock.unlock();
         _class_resolveMethod(cls, sel, inst);
         triedResolver = YES;
         goto retry;
     }
     // 仍然没有找到方法实现进入消息转发第二阶段“备援接收者”
     // 前后会调用 -(id)forwardingTargetForSelector: (SEL)selector
     // 以及 - (void)forwardInvocation: (NSInvocation*)invocation 进行最后的补救
     // 若是补救未成功抛出消息发送错误异常
     _cache_addForwardEntry(cls, sel);
     methodPC = _objc_msgForward_impcache;
  done:
     methodListLock.unlock();
     assert(!(ignoreSelector(sel)  &&  methodPC != (IMP)&_objc_ignored_method));
     return  methodPC;
}

上面就是一个方法调用的所有过程。主要分为三个部分:

  • 查找是否存在对应的方法缓存,若是存在直接返回调用

为了优化性能,方法的缓存使用了散列表的方式,在下一部分会进行比较详细的讲述

  • 未找到缓存,到类自己或顺着类结构向上查找方法实现,返回的method_t *类型也被命名为Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//非加锁状态下查找方法实现
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
     runtimeLock.assertLocked();
     assert(cls->isRealized());
     // fixme nil cls?
     // fixme nil sel?
     for  (auto mlists = cls->data()->methods.beginLists(),
             end = cls->data()->methods.endLists();
              mlists != end;
                ++mlists)
     {
         method_t *m = search_method_list(*mlists, sel);
         if  (m)  return  m;
     }
     return  nil;
}
// 搜索方法列表
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  {
         // Linear search of unsorted method list
         for  (auto& meth : *mlist) {
             if  (meth.name == sel)  return  &meth;
         }
     }
#if DEBUG
     // sanity-check negative results
     if  (mlist->isFixedUp()) {
         for  (auto& meth : *mlist) {
             if  (meth.name == sel) {
                 _objc_fatal( "linear search worked when binary search did not" );
             }
         }
     }
#endif
     return  nil;
}

若是在这个步骤中找到了方法的实现,那么将它加入到方法缓存中以便下次调用能快速找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 记录而且缓存方法
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
     if  (objcMsgLogEnabled) {
         bool cacheIt = logMessageSend(implementer->isMetaClass(),
                                 cls->nameForLogging(),
                                 implementer->nameForLogging(),
                                 sel);
         if  (!cacheIt)  return ;
     }
#endif
     cache_fill (cls, sel, imp, receiver);
}
//在无加锁状态下缓存方法
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
     cacheUpdateLock.assertLocked();
     if  (!cls->isInitialized())  return ;
     if  (cache_getImp(cls, sel))  return ;
     cache_t *cache = getCache(cls);
     cache_key_t key = getKey(sel);
     // 若是缓存占用不到3/4,进行缓存。
     mask_t newOccupied = cache->occupied() + 1;
     mask_t capacity = cache->capacity();
     if  (cache->isConstantEmptyCache()) {
         cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
     }
     else  if  (newOccupied expand();
     }
     bucket_t *bucket = cache->find(key, receiver);
     if  (bucket->key() == 0) cache->incrementOccupied();
     bucket->set(key, imp);
}

若是在类自身中没有找到方法实现,那么循环获取父类,重复上面的查找动做,找到后再将方法缓存到本类而非父类的缓存中

  • 未找到任何方法实现,触发消息转发机制进行最后补救

其中消息转发分为两个阶段,第一个阶段咱们能够经过动态添加方法以后让编译器再次执行查找方法实现的过程;第二个阶段称做备援的接收者,就是找到一个接盘侠来处理这个事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
     // 非beta类的状况下直接调用 resolveInstanceMethod 方法
     if  (! cls->isMetaClass()) {
         _class_resolveInstanceMethod(cls, sel, inst);
     }
     else  {
         // 先调用 resolveClassMethod 请求动态添加方法
         // 而后进行一次查找判断是否处理完成
         // 若是没有添加,再调用 resolveInstanceMethod 方法
         _class_resolveClassMethod(cls, sel, inst);
         if  (!lookUpImpOrNil(cls, sel, inst,
                       NO /*initialize*/ , YES /*cache*/ , NO /*resolver*/ ))
         {
             _class_resolveInstanceMethod(cls, sel, inst);
         }
     }
}

方法缓存

在上一篇runtime文章中笔者已经说过对于OC的每个对象来讲,本质上都是一个objc_class的结构体封装,在最新的runtime源码的objc-runtime-new.h中,objc_class的结构以下(笔者已经略去了大部分的函数):

1
2
3
4
5
6
7
8
9
10
11
12
struct objc_class : objc_object {
     Class superclass;           // Class ISA;
     cache_t cache;              // formerly cache pointer and vtable
     class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
     class_rw_t *data() {
         return  bits.data();
     }
     void setData(class_rw_t *newData) {
     bits.setData(newData);
     }
     // .........
}

结构一目了然,很明显cache存储着咱们在方法调用中须要查找的方法缓存。做为缓存方法的cache采用了散列表,以此来大幅度提升检索的速度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
struct cache_t {
     struct bucket_t *_buckets;
     mask_t _mask;
     mask_t _occupied;
     // functions
}
// cache method
buckets = (cache_entry **)cache->buckets;
for  (index = CACHE_HASH(sel, cache->mask);
      buckets[index] != NULL;
      index = (index+1) & cache->mask)
{ }
buckets[index] = entry;

在每次调用完未被缓存的方法时,下面的那段缓存方法的代码就会调用。苹果利用了sel的指针地址和mask作了一个简单的位运算,而后找到一个空槽存储起来。 以此咱们能够推出从缓存中查找sel实现的代码CacheLookup,可是为了高度优化性能,苹果一样丧心病狂的使用汇编完成了查找的步骤,官方给出的注释足够咱们大体看明白这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.macro CacheLookup
     ldrh    r12, [r9,  #CACHE_MASK]    // r12 = mask
     ldr    r9, [r9,  #CACHE]    // r9 = buckets
. if  $0 == STRET  ||  $0 == SUPER_STRET
     and    r12, r12, r2         // r12 = index = SEL & mask
. else
     and    r12, r12, r1         // r12 = index = SEL & mask
.endif
     add    r9, r9, r12, LSL  #3    // r9 = bucket = buckets+index*8
     ldr    r12, [r9]         // r12 = bucket->sel
2:
. if  $0 == STRET  ||  $0 == SUPER_STRET
     teq    r12, r2
. else
     teq    r12, r1
.endif
     bne    1f
     CacheHit $0
1:
     cmp    r12,  #1
     blo    LCacheMiss_f         // if (bucket->sel == 0) cache miss
     it    eq             // if (bucket->sel == 1) cache wrap
     ldreq    r9, [r9,  #4]        // bucket->imp is before first bucket
     ldr    r12, [r9,  #8]!        // r12 = (++bucket)->sel
     b    2b
.endmacro

具体的源码能够从苹果开源这里下载,这个方法苹果已经注释的足够清晰了。

上面全部的操做都是对方法的缓存、查找操做,那么方法到底是什么?在OC中方法被抽象成的数据类型是Method,若是了解而且使用过runtime的读者们可能了解这个类型,其结构以下:

1
2
3
4
5
6
struct old_method {
     SEL method_name;
     char *method_types;
     IMP method_imp;
};
typedef struct method_t *Method;
  • method_imp方法的实现代码,你能够把它看作一个block。事实上,后者确实能够转换成一个IMP类型来实现某些黑魔法。

  • method_types方法的参数编码,什么意思?在属性与变量中我说过每一种数据类型有着本身对应的字符编码,这个表示方法返回值、参数的字符编码,好比-(void)playWith:(id)的字符编码为v@:@

  • method_name顾名思义,方法的名字。一般咱们使用@selector()的方式获取一个方法的sel地址,这个被用来进行散列计算存储方法的imp实现。因为SEL类型采用了散列的算法,所以若是同一个类中存在一样名字的方法,那么就会致使方法的imp地址没法惟一化。这也是苹果不容许同名不一样参数类型的方法存在的缘由

消息转发

一般状况下,在咱们调用不属于某个对象的方法的时候,咱们的应用就会崩溃crash,好比笔者经历过好几回由于后台返回的NSNull类型致使了测试反馈应用闪退。经过上面的方法调用源码咱们能够看到并非没有找到方法实现就直接发生了崩溃,在崩溃以前编译器会进行消息转发机制,总共给了咱们三次机会来避免这样的崩溃并尽量的找到方法的响应者。

1464059012694122.jpg

消息转发阶段

首先先看第一阶段。咱们都知道,在iOS开发当中咱们须要很是的注意用户体验。单纯的是由于数据类型错误而致使应用出现闪退,这样的处理会极大的影响使用app的用户。所以,咱们能够经过class_addMethod这个函数来动态的添加这种错误的处理(类能够在objc_registerClassPair完成类的注册以后动态的添加方法,但不容许动态添加属性,参考category机制)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id wrongTypeGetter(id object, SEL sel) {
     return  nil;
}
void wrongTypeSetter(id object, SEL sel, id value) {
     // do nothing
}
+ (BOOL)resolveInstanceMethod: (SEL)selector
{
     NSString * selName = NSStringFromSelector(selector);
     if  ([sel hasPrefix: @ "set" ]) {
         class_addMethod(self, selector, (IMP)wrongTypeSetter,  "v@:@" );
     else  {
         class_addMethod(self, selector, (IMP)wrongTypeGetter,  "@@:" )
     }
}

在第二阶段最开始的时候,这时候已经默许了你并不想使用消息接收者来响应这个方法,因此咱们须要找到消息接盘侠 —— 这并非一件坏事。在iOS中不支持多继承,尽管咱们能够经过协议和组合模式实现伪多继承。伪多继承和多继承的区别在于:多继承是将多个类的功能组合到一个对象当中,而伪多继承多个类的功能依旧分布在不一样对象当中,可是对象彼此对消息发送者透明。那么,若是咱们消息转发给另外一个对象能够用来实现这种伪多继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Person: NSObject
@property (nonatomic, strong) NSNumber * age;
@end
@implementation Person
- (id)forwardingTargetForSelector: (SEL)aSelector
{
     // 甚至能够经过runtime遍历本身属性找到能够响应方法的接盘侠
     NSString * selName = NSStringFromSelector(aSelector);
     if  ([selName hasSuffix: @ "Value" ]) {
         return  self.age;
     }
     return  nil;
}
@end
// View controller
id p = [[Person alloc] init];
[p setAge: @(18)];
NSLog(@ "%lu, %.2f" , [p integerValue], [p doubleValue]);     //18, 18.00

若是你依旧没有为这个方法找到另一个调用者,那么阻止你app闪退的最后时刻到来了。runtime须要生成一个methodSignature变量来组装,这将经过调用消息接收者的-(NSMethodSignature *)methodSignatureForSelector:获取,这个变量包含了方法的参数类型、参数个数以及消息接收者等信息。接着把这个变量组装成一个NSInvocation对象进行最后一次的消息转发,调用接收者的-forwardInvocation:来进行最后的挽救机会。这意味着咱们能够尽情的对invocation作任何事情,包括随意修改参数值、消息接收者等。我最常拿来干的事情就是减小数组的遍历工做:

1
2
3
4
5
6
7
8
9
10
@implementation NSArray(LXDRuntime)
- (void)forwardInvocation: (NSInvocation *)anInvocation
{
     for  (id item  in  self) {
         if  ([item respondsToSelector: anInvocation.selector]) {
             [anInvocation invokeWithTarget: item];
         }
     }
}
@end

总的来讲整个消息发送的过程能够概括成下面这张图:

1464059057867047.jpg

消息发送全过程

虽然消息转发能够帮助咱们显著的减小app的闪退率,可是在开发阶段千万不要加入这些特性。最好是在app申请上架的那个阶段再加,这样不至于app其余消息发送异常被咱们忽略了。

消息机制黑魔法

上面笔者讲解了关于一个调用方法之中发生的事情,确实很是的复杂。一样的这些特性也很是值得咱们去学习使用,runtime提供了一系列关于Method的方法给咱们实现面向切面编程的工做。这些工做包括了替换原有方法实现,交换方法实现等等工做。

假设如今我须要一个圆角按钮,而且保证点击触发事件的范围必需要这个圆以内,那么经过一个UIButton+LXDRuntime的扩展来替换旧有-pointInside:withEvent:方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface UIButton(LXDRuntime)
@property (nonatomic, assign) BOOL roundTouchEnable;
@end
const void * LXDRoundTouchEnableKey = & LXDRoundTouchEnableKey;
@implementation UIButton(LXDRuntime)
- (BOOL)roundTouchEnable
{
     return  [objc_getAssociatedObject(self, LXDRoundTouchEnableKey) boolValue];
}
- (void)setRoundTouchEnable: (BOOL)roundTouchEnable
{
     objc_setAssociatedObject(self, LXDRoundTouchEnableKey, @(roundTouchEnable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)replacePointInside: (CGPoint)point withEvent: (UIEvent *)event
{
     if  (CGRectGetWidth(self.frame) != CGRectGetHeight(self.frame)
         || !self. roundTouchEnable)
     {
         return  [ super  pointInside: point withEvent: event];
     }
     CGFloat radius = CGRectGetWidth(self.frame) / 2;
     CGPoint offset = CGPointMake(point.x - radius, point.y - radius);
     return  sqrt(offset.x * offset.x + offset.y * offset.y) <= radius;
}
// 替换方法实现
+ (void)initialize
{
     [ super  initialize];
     Method replaceMethod = class_getInstanceMethod([self class], @selector(replacePointInside:withEvent:));
     Method originMethod = class_getInstanceMethod([self class], @selector(pointInside:withEvent:));
     method_setImplementation(originMethod, method_getImplementation(replaceMethod));
}
@end

那么当我须要个人按钮只响应圆形点击区域的时候,只须要设置button.roundTouchEnable  = YES,就会自动实现了圆形点击的判断。除了上面的上面的方法替换,还有另外一个经常使用的黑魔法是交换两个方法的实现。归功于Method的特殊结构,将方法名字sel跟代码实现imp分隔开来。你能够把imp当作是一个block代码块,而交换实现的操做就至关于把这两个block交换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface Person : NSObject
@property (nonatomic, strong) NSString * name;
@property (nonatomic, strong) NSNumber * age;
@end
@implementation Person
+ (void)initialize
{
     [ super  initialize];
     Method ageGetter = class_getInstanceMethod([self class], @selector(age));
     Method nameGetter = class_getInstanceMethod([self class], @selector(name));
     method_exchangeImplementations(ageGetter, nameGetter);
}
// View controller
Person * p = [[Person alloc] init];
p.age = @(56);
p.name = @ "Job Steve" ;
NSLog(@ "%@ is %@ year old" , p.name, p.age);
// LXDRuntimeDemo[7316:244912] 56 is Job Steve year old
@end

上面的代码交换了name和age的实现,用图示来表示:

1464059111845457.jpg

方法交换

应该不难看出,method_exchangeImplementations之因此被推崇的缘由在于这种方式交换实现的时候不会致使原有的方法实现发生改变(从头至尾,age的IMP跟name的IMP都没有进行任何的修改),固然了,它的缺点也是很是明显的:

  • 多人开发对同一个方法都进行方法替换/交换时,会使得业务逻辑复杂,很是的不利于调试

  • 被交换的方法实现会直接的影响到全部该类的实例对象以及子类,不适用于单个对象的实现

能够说runtime提供的这些黑魔法都是双刃剑,合理的运用能让咱们更加的强大。另外,除了Method的黑魔法,还要提到一个IMP相关的使用陷阱。上文说过,IMP跟block是很是类似的东西,前者能够跟函数指针强制转换,所以能够看作是一个特殊的block,一样的系统提供了二者相互转换的方法:imp_implementationWithBlock和imp_getBlock。按照上面说的,当调用方法转换成消息转发的时候,objc_msgSend自身已经存在了两个参数id object以及SEL aSelector,那么按照这种思路IMP和block的切换应该是这样的:

1
2
3
4
5
6
7
8
9
10
+ (void)initialize
     void (^requestBlock)(id object, SEL aSelector, id URL, id parameters) =
         ^(id object, SEL aSelector, id URL, id parameters) {
         // do some networking request
     };
     IMP requestIMP = imp_implementationWithBlock(requestBlock);
     class_addMethod([self class], @selector(networkReuqest:parameters:), requestIMP,  "v@:@@" );
}
// View controller
[self performSelector: @selector(networkReuqest:parameters:) withObject: URL withObject: parameters];

上面这段代码会crash的很是无厘头,提示你EXC_BAD_ACCESS错误。重要的事情说三遍:

block参数不存在SEL!!

block参数不存在SEL!!

block参数不存在SEL!!

上面的代码只要去掉了SEL aSelector这个参数,这段代码就能正常执行。

尾话

runtime对于每个iOS开发者来讲,都应该去了解。经过runtime的源码实现,咱们能够看到苹果为了性能优化武装到牙齿的行为,也能看到咱们书写代码深层之中鲜为人知的实现。做为runtime系列第二篇(第一篇),纠结了我好久才开始动工,期间看源码看的头都大了,可是确实对我在开发的认识以及代码的结构上有了更多的了解。最后,本文无代码,仍是奉上苹果的runtime源码地址

相关文章
相关标签/搜索