一、前言
在维护一个项目时碰见一个问题,调用了一个方法后不起作用,这种问题一般有两个解决思路,一个是看方法有没有起作用,另一个是看是不是其他的方法打断了这个方法,比如设置 UIButton 的隐藏,但在另一个地方,又把它设置成显示了。这种问题在工程里通过调试代码一般都很好解决。
但是当它发生在我调用了一个 SDK 方法不起作用时,按照上文的思路,一查看是否是 SDK 方法的问题,这个除了向 SDK 提供人员反应没有其他更好的解决方法了,二是查看是否其他地方也调用了这个方法,使用了不同的参数导致本次的调用失效了。如果这个方法调用的少,可以全局搜索,然后注释掉方法,但是如果之前项目不是你开发的,你只是后期切入进来,同时代码结构一时半会还不能很好把握到,总之想快速利落的排除问题,这时你可以考虑用 Runtime 的特性,把 SDK 里的方法用自己写的替换掉。
先老调重弹一遍,什么是 Runtime?
二、概念
Objective-C是动态语言,它将很多静态语言在编译和链接时做的事放到了运行时,这个运行时系统就是 Runtime。
Runtime 其实就是一个库,它基本上是用 C 和汇编写的一套 API,这个库使C语言有了面向对象的能力。
静态语言:在编译的时候会决定调用哪个函数。
动态语言(OC):在运行的时候根据函数的名称找到对应的函数来调用。
然后还有一些需要了解的基本概念:
- isa:OC 中,类和类的实例在本质上没有区别,都是对象,任何对象都有isa 指针,它指向类或元类。
- SEL:SEL(又叫选择器)是方法的 selector 的指针。方法的 selector 表示运行时方法的名字。OC在编译时,会依据每一个方法的名字、参数,生成一个唯一的整型标识(Int类型的地址),这个标识就是 SEL。
- IMP:IMP 是一个函数指针,指向方法最终实现的首地址。SEL 就是为了查找方法的最终实现IMP。
- Method:用于表示类定义中的方法,它的结构体中包含一个 SEL 和 IMP,相当于在 SEL 和 IMP 之间作了一个映射。
- 消息机制:任何方法的调用本质就是发送一个消息。编译器会将消息表达式[receiver message]转化为一个消息函数 objc_msgSend(receiver, selector)。
- Runtime 的使用:获取属性列表,获取成员变量列表,获得方法列表,获取协议列表,方法交换(黑魔法),动态的添加方法,调用私有方法,为分类添加属性。
三、应用场景
- 关联对象(Objective-C Associated Objects)给分类增加属性
- 方法交换 / 黑魔法(Method Swizzling)
- 实现 NSCoding 的自动归档和自动解档
- 实现字典和模型的自动转换(MJExtension)
这里介绍前两种。
1、关联对象(Objective-C Associated Objects)给分类增加属性
我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。
关联对象 Runtime提 供了下面几个接口:
Objective-C1 2 3 4 5 6 7 8 9 10 11 12
| void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
id objc_getAssociatedObject(id object, const void *key)
void objc_removeAssociatedObjects(id object)
|
例子:
Objective-C1 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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
|
#import <Foundation/Foundation.h>
@interface NSObject (BRModel)
+ (NSArray *)br_objectProperties;
+ (instancetype)br_objectWithDictionary:(NSDictionary *)dict;
@end
#import "NSObject+BRModel.h" #import <objc/runtime.h>
const char *kPropertyListKey = "kPropertyListKey";
@implementation NSObject (BRModel)
+ (NSArray *)br_objectProperties {
NSArray *pList = objc_getAssociatedObject(self, kPropertyListKey); if (pList != nil) { return pList; }
unsigned int count = 0; objc_property_t *properties = class_copyPropertyList([self class], &count); NSMutableArray *propertyNameArr = [[NSMutableArray alloc]init]; for (unsigned int i = 0; i < count; i++) { objc_property_t property = properties[i]; const char *cName = property_getName(property); NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]; [propertyNameArr addObject:name]; } free(properties);
objc_setAssociatedObject(self, kPropertyListKey, [propertyNameArr copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return [propertyNameArr copy]; }
+ (instancetype)br_objectWithDictionary:(NSDictionary *)dict { id object = [[self alloc]init]; NSArray *pList = [self br_objectProperties]; [pList enumerateObjectsUsingBlock:^(id _Nonnull pName, NSUInteger idx, BOOL * _Nonnull stop) { id pValue = [dict objectForKey:pName]; [object setValue:pValue forKey:pName]; }];
return object; }
@end
|
2、方法交换(黑魔法)
下面就是 runtime 的重头戏了,被称作黑魔法的方法交换 Swizzling。交换方法是在 method_exchangeImplementations 里发生的。
使用 Swizzling 的过程中要注意两个问题:
Swizzling 要在 +load 方法中执行
运行时会自动调用每个类的两个方法,+load 与 +initialize。
+load 会在 main 函数之前调用,并且一定会调用。
+initialize 是在第一次调用类方法或实例方法之前被调用,有可能一直不被调用。
一般使用 Swizzling 是为了影响全局,所以为了方法交换一定成功,Swizzling 要放在 +load 中执行。
Swizzling 要在 dispatch_once 中执行
Swzzling 是为了影响全局,所以只让它执行一次就可以了,所以要放在 dispatch_once 中。
例子:
Objective-C1 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
|
#import <UIKit/UIKit.h>
@interface UIImageView (BRAdd) - (void)br_setImage:(UIImage *)image; @end
#import "UIImageView+BRAdd.h" #import <objc/runtime.h>
@implementation UIImageView (BRAdd)
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method originalMethod = class_getInstanceMethod([self class], @selector(setImage:)); Method swizzledMethod = class_getInstanceMethod([self class], @selector(br_setImage:)); method_exchangeImplementations(originalMethod, swizzledMethod); }); }
- (void)br_setImage:(UIImage *)image { NSLog(@"%s", __FUNCTION__);
UIImage *newImage = [self newImage:image size:self.bounds.size];
[self br_setImage:newImage]; }
- (UIImage *)newImage:(UIImage *)image size:(CGSize)size { UIGraphicsBeginImageContextWithOptions(size, YES, 0); [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return newImage; } @end
|