runtime(三):基本使用场景


一、方法替换

项目代码编写中,常会遇见第三方框架或者原生方法无法满足需求时或是一个方法在工程中大量被调用时,我们想要批量替换或修改,为了避免更改原有功能,**在保持原有方法功能基础上,添加额外的功能**,此时就需要用到Swizzle Method

方法替换Swizzle Method,目的是为了替换方法的实现。 平时实现某一方法时用到@selector,而能够实现方法是在@selector(方法选择器)中取出一方法编号(指向方法的指针),用SEL类型表示,它所指向的是一IMP(方法实现的指针),而方法替换的就是这个IMP,从而实现方法的替换

实例方法替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <objc/runtime.h>

+ (void)swizzleInstanceMethodWithOriginalSEL:(SEL)originalSel SwizzleNewSEL:(SEL)newSel {

Method originalMethod = class_getInstanceMethod(self, originalSel);
Method newMethod = class_getInstanceMethod(self, newSel);
if (!originalMethod || !newMethod) {
return;
}
//加一层保护措施,如果添加成功,则表示该方法不存在于本类,而是存在于父类中,不能交换父类的方法,否则父类的对象调用该方法会crash;添加失败则表示本类存在该方法
BOOL addMethod = class_addMethod(self, originalSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (addMethod) {
//再将原有的实现替换到swizzledMethod方法上,从而实现方法的交换,并且未影响到父类方法的实现
class_replaceMethod(self, newSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, newMethod);
}

}
类方法替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <objc/runtime.h>

+ (void)swizzleClassMethodWithOriginalSEL:(SEL)originalSel SwizzleNewSEL:(SEL)newSel {
Class class = object_getClass(self);
Method originalMethod = class_getInstanceMethod(class, originalSel);
Method newMethod = class_getInstanceMethod(class, newSel);
if (!originalMethod || !newMethod) {
return;
}

BOOL addMethod = class_addMethod(class, originalSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (addMethod) {
class_replaceMethod(class, newSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, newMethod);
}

}
替换方法的使用
1
2
3
4
5
6
7
8
+ (void)load{
//方法替换处load不需要调用super,否则会导致父类被重复Swizzling
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{//保证方法替换只被执行一次

[self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSArrayI") OriginalSEL:@selector(objectAtIndex:) SwizzleNewSEL:@selector(safe_objectAtIndex:)];
});
}
  • 为了确保Swizzle Method方法替换能一定被执行调用,因此需要在load中执行(在装载类文件时就会被调用(程序启动前))

  • 为了避免子类或子类的子类调用了[super load]而导致Swizzling被执行了多次(相当于SEL和IMP被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行…..这样换来换去的结果),需要在load中使用GCD方法dispatch_once确保方法只被执行一次并且**不需要**调用[super load]方法

二、实现分类添加新属性

我们在开发中常常使用类目Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。

遗憾的是,OC的类目并不支持直接添加属性,如果我们直接在分类的声明中写入Property属性,那么只能为其生成set与get方法声明,却不能生成成员变量,直接调用这些属性还会造成崩溃。

所以为了实现给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上,下面是相关的三个方法:

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
/**
1.给对象设置关联属性
@param object 需要设置关联属性的对象,即给哪个对象关联属性
@param key 关联属性对应的key,可通过key获取这个属性,
@param value 给关联属性设置的值
@param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
OBJC_ASSOCIATION_ASSIGN @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN @property(strong,atomic)。
OBJC_ASSOCIATION_COPY @property(copy, atomic)。
*/
void objc_setAssociatedObject(id _Nonnull object,
const void * _Nonnull key,
id _Nullable value,
objc_AssociationPolicy policy)
/**
2.通过key获取关联的属性
@param object 从哪个对象中获取关联属性
@param key 关联属性对应的key
@return 返回关联属性的值
*/
id _Nullable objc_getAssociatedObject(id _Nonnull object,
const void * _Nonnull key)
/**
3.移除对象所关联的属性
@param object 移除某个对象的所有关联属性
*/
void objc_removeAssociatedObjects(id _Nonnull object)

注意:key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key。

现在演示一个代码示例:

创建分类并声明一个属性

1
2
3
4
#import "Persion.h"
@interface Persion (category)
@property (nonatomic,copy) NSString* nick;
@end

分类中属性实现

1
2
3
4
5
6
7
const char* name = "lisi";
- (void) setNick:(NSString *)nick {
objc_setAssociatedObject(self, &name, nick, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString*) nick {
return objc_getAssociatedObject(self, &name);
}

代码调用

1
2
3
4
5
- (void) runtime_Test {
self.persion = [[Persion alloc] init];
self.persion.nick = @"Zhangsan";
NSLog(@"%@", self.persion.nick);
}

三、获取类的详细信息

1.获取属性列表
1
2
3
4
5
6
7
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &amp;count);
for (unsigned int i = 0; i&lt;count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
2.获取所有成员变量
1
2
3
4
5
6
7
Ivar *ivarList = class_copyIvarList([self class], &amp;count);
for (int i= 0; i&lt;count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
3.获取所有方法
1
2
3
4
5
6
7
Method *methodList = class_copyMethodList([self class], &amp;count);
for (unsigned int i = 0; i&lt;count; i++) {
Method method = methodList[i];
SEL mthodName = method_getName(method);
NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
}
free(methodList);
4.获取当前遵循的所有协议
1
2
3
4
5
6
7
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &amp;count);
for (int i=0; i&lt;count; i++) {
Protocol *protocal = protocolList[i];
const char *protocolName = protocol_getName(protocal);
NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList);

四、方法动态解析与消息转发

其实该部分可以参考runtime第一篇中内容,这里不再重复赘述,只是大概做出一些总结。

1.动态方法解析:动态添加方法

Runtime足够强大,能够让我们在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:

场景1:动态添加未实现方法,解决代码中因为方法未找到而报错的问题;

场景2:利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法。方法动态解析主要用到的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel

//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值与参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
2.解决方法无响应崩溃问题

执行OC方法其实就是一个发送消息的过程,若方法未实现,我们可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:消息转发流程图

除了上述的方法动态解析,还使用到的相关方法如下:

消息接收者重定向

1
2
3
4
5
//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector

//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector

消息重定向

1
2
3
- (void)forwardInvocation:(NSInvocation *)anInvocation;

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;