运行时(Runtime)篇幅四

130 阅读3分钟

为什么说Objective-C是一门动态的语言?篇幅中讲解动态绑定时提到在运行时才判断需要调用什么方法这一特性。而且在篇幅一也进一步从消息转发机制来证实了这一特性。

动态方法解析通过动态添加方法的手段来尝试处理未被调用的方法,然而“方法的动态性”不仅仅依靠添加方法来实现,方法的互换也是Runtime的一大特色被称之为———Method Swizzling

Runtime通过把类中Method包含的SEL(方法名)IMP(方法实现)的对应关系进行断开,然后Runtime会通过改变类中调度表中选择器最终函数间的映射关系,并和需要互换方法的SELIMP进行重组从而实现方法互换的过程。先看个例子:

@implementation Person (More)

+ (void)load {
    Method logSecondName = class_getInstanceMethod(self, @selector(logSecondName));

    Method addobject = class_getInstanceMethod(self, @selector(addObject));
    
    method_exchangeImplementations(logSecondName, addobject);

}

- (void)logSecondName {
    self.secondName = @"ryan";
    NSLog(@"exchange=%@",self.secondName);
}

- (void)addObject {
    NSLog(@"exchange=%@",self.secondName);
}

@end

输出

Person[20364:928561] exchange=ryan

从例子中可以看出Method Swizzling只用到了两个Runtime库中的方法

class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

前者即指定的选择器的实现对应的方法,后者则是传入需要交换的方法进行交换。 从方法实现的代码量来看其实并不复杂,但有几点还是需要注意的。

1.为什么要在load方法中实现?

首先load方法在App启动后在Runtime库加载类、分类时,此时load函数就会被调用,时机是很早的,比main函数还是提前。OC的方法都是通过objc_msgSend函数调用每个类、分类,但+load方法并不通过objc_msgSend来调用,而是根据方法地址直接调用,且在不主动调用的情况下+load方法只调用一次

在load中实现方法交换的目的就是为了防止方法IMP又被交换回来,从而导致交换失败。因为+load方法只会被调用一次,从而也不用当下以上问题了。当然如果有特殊情况,还可以使用单例模式来避免方法被交换回来。

关于+load方法还是需要了解一下几点:

  • 会首先调用当前类的父类的load方法,在调用当前类的+load方法:一来是为了让Runtime在App启动是就能加载出类的信息,二来是为了确保其子类+load方法中的内容有效

  • 优先加载类中的+load方法,再加载分类的+load方法:这点其实跟上面一条是相似道理的

  • 类和分类的+load方法在程序运行过程中只会加载一次,但是各各类之间加载+load方法的顺序是跟编译顺序有关的,先编译的会先加载;分类之间(父子类关系的分类)的+load加载顺序同理

2.当子类替换父类子类共同方法

该点是很容易引发隐式问题的,当子类的方法被替换后,而当父类在去实现该方法时,如果在替换的方法中存在不属于父类属性或者实例时就会导致崩溃。例:

@interface Person : NSObject
@interface Student : Person

共同方法名

- (void)logName;

不同属性

// Person
@property (nonatomic, copy) NSString *name;

// Student
@property (nonatomic, copy) NSString *studentName;
@property (nonatomic, assign) int age;

方法

- (void)logName;

- (void)logName;
- (void)logAge;

正常调用固然没有问题,而当把Student类中的logName方法与logAge方法进行交换后,再次调用Person类的logName方法时,则会因为无法找到age属性的setAge方法而报错

-[Person setAge:]: unrecognized selector sent to instance 0x10057a330"

因此在使用方法交换时一定要注意被交换的方法是否是唯一性的,应尽可能的避免去交换系统方法普遍使用的方法