一、前言
  在维护一个项目时碰见一个问题,调用了一个方法后不起作用,这种问题一般有两个解决思路,一个是看方法有没有起作用,另一个是看是不是其他的方法打断了这个方法,比如设置 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
 
 
 
 
  |