假如想在分拣中,那样三个类能够响应本人接二

2019-09-13 11:20栏目:大奖888官网登录
TAG:

最近用到了sunnyxx的forkingdog系列《UIView-FDCollapsibleConstraints》,纪录下关联对象和MethodSwizzling在实际场景中的应用。

图片 1

基本解释

Runtime 是一套比较底层的纯C语言API 它是OC的幕后工作者 我们平时写的OC代码        在运行时都会编译器转为runtime的C语言代码 其中最主要的是消息机制OC的函数调用成为消息发送 属于动态调用过程 在编译的时候并不能决定真正调用哪个函数事实证明在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错而C语言在编译阶段就会报错 只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

  • 关联对象操作函数

    • 设置关联对象:
    /** * 设置关联对象 * * @param object 源对象 * @param key 关联对象的key * @param value 关联的对象 * @param policy 关联策略 */void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    

到了今天终于要"出院"了,要总结一下住院几天的收获,谈谈Runtime到底能为我们开发带来些什么好处。当然它也是把双刃剑,使用不当的话,也会成为开发路上的一个大坑。

简单实例

obj doSometing其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成:objc_msgSend(obj,@selector(doSomething);首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method(猜测]cache中method列表是以]EL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中 

  • 1.Runtime的优点
    • 实现多继承Multiple Inheritance
    • Method Swizzling
    • Aspect Oriented Programming
    • Isa Swizzling
    • Associated Object关联对象
    • 动态的增加方法
    • NSCoding的自动归档和自动解档
    • 字典和模型互相转换
  • 2.Runtime的缺点

实际应用

 - 获取关联对象: ```objc /** * 获取关联对象 * * @param object 源对象 * @param key 关联对象的key * * @return 关联的对象 */ id objc_getAssociatedObject(id object, const void *key)

在上一篇文章里面讲到的forwardingTargetForSelector:方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。

Json到Model的转化

在开发中相信最常用的就是接口数据需要转化成Model了(当然如果你是直接从Dict取值的话。。。),很多开发者也都使用著名的第三方库如JsonModel、Mantle或MJExtension等,如果只用而不      知其所以然,那真和“搬砖”没啥区别了,下面我们使用runtime去解析json来给Model赋值。原理描述:用runtime提供的函数遍历Model自身所有属性,如在json中有对应的值,则将其赋值。核心方法:在NSObject的分类中添加方法 1  - (instancetype)initWithDict:(NSDictionary *)dict {2    3      if (self = [self init]) {4          //(1)获取类的属性及属性对应的类型5          NSMutableArray * keys = [NSMutableArray array];6          NSMutableArray * attributes = [NSMutableArray array];7          /*8            * 例子9            * name = value3 attribute = T@"NSString",C,N,V_value310          * name = value4 attribute = T^i,N,V_value411          */12          unsigned int outCount;13          objc_property_t * properties = class_copyPropertyList([self class], &outCount);14          for (int i = 0; i < outCount; i ++) {15              objc_property_t property = properties[i];16              //通过property_getName函数获得属性的名字17              NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];18              [keys addObject:propertyName];19              //通过property_getAttributes函数可以获得属性的名字和@encode编码20              NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];21              [attributes addObject:propertyAttribute];22          }23          //立即释放properties指向的内存24          free(properties);25  26          //(2)根据类型给属性赋值27          for (NSString * key in keys) {28              if ([dict valueForKey:key] == nil) continue;29              [self setValue:[dict valueForKey:key] forKey:key];30          }31      }32      return self

其中设置关联对象的策略有以下5种:

在官方文档上记录了这样一段例子。

快速归档

有时候我们要对一些信息进行归档,如用户信息类UserInfo,这将需要重写initWithCoder和encodeWithCoder方法,并对每个属性进行encode和decode操作。那么问题来了:当属性只有几个的时候可以轻松写完,如果有几十个属性呢?那不得写到天荒地老?。。。原理描述:用runtime提供的函数遍历Model自身所有属性,并对性encode和decode操作核心方法:在Model的基类中重写方法:  1  - (id)initWithCoder:(NSCoder *)aDecoder {2      if (self = [super init]) {3          unsigned int outCount;4          Ivar * ivars = class_copyIvarList([self class], &outCount);5          for (int i = 0; i < outCount; i ++) {6              Ivar ivar = ivars[i];7              NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];8              [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];9          }10      }11      return self;12  }13  14  - (void)encodeWithCoder:(NSCoder *)aCoder {15      unsigned int outCount;16      Ivar * ivars = class_copyIvarList([self class], &outCount);17      for (int i = 0; i < outCount; i ++) {18          Ivar ivar = ivars[i];19          NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];20          [aCoder encodeObject:[self valueForKey:key] forKey:key];21      }22  }

  • 和MRC的内存操作retain、assign方法效果差不多
    • 比如设置的关联对象是一个UIView,并且这个UIView已经有父控件时,可以使用OBJC_ASSOCIATION_ASSIGN

图片 2

访问私有变量

我们知道,OC中没有真正意义上的私有变量和方法,要让成员变量私有,要放在m文件中声明,不对外暴露。如果我们知道这个成员变量的名称,可以通过runtime获取成员变量,再通过getIvar来获取它的值。方法:  1.Ivar ivar = class_getInstanceVariable([Model class], "_str1"); 2.NSString * str1 = object_getIvar(model, ivar)

在OC程序中可以借用消息转发机制来实现多继承的功能。 在上图中,一个对象对一个消息做出回应,类似于另一个对象中的方法借过来或是“继承”过来一样。 在图中,warrior实例转发了一个negotiate消息到Diplomat实例中,执行Diplomat中的negotiate方法,结果看起来像是warrior实例执行了一个和Diplomat实例一样的negotiate方法,其实执行者还是Diplomat实例。

给分类(Category)添加属性

遇到一个问题,写了一个分类,但原先类的属性不够用。添加一个属性,调用的时候崩溃了,说是找不到getter、setter方法。查了下文档发现,OC的分类允许给分类添加属性,但不会自动生成getter、setter方法。有没有解决方案呢?有,通过运行时建立关联引用。接下来以添加一个这样的属性为例:@property (nonatomic, copy) NSString *str;在匿名分类或者头文件中添加属性。区别是:匿名分类中添加的是私有属性,只在本类中可以使用,类的实例中不可以使用。头文件中添加的在类的实例中也可以使用。 //分类的头文件@interface ClassName (CategoryName)//我要添加一个实例也可以访问的变量所以就写在这里了@property (nonatomic, strong) NSString *str;@end//匿名分类@interface ClassName ()@end3、在实现里面写要添加属性的getter、setter方法。@implementation ClassName (CategoryName) -(void)setStr:(NSString *)str  {      objc_setAssociatedObject(self, &strKey, str, OBJC_ASSOCIATION_COPY);  }  -(NSString *)str  {      return objc_getAssociatedObject(self, &strKey);  }@end在setStr:方法中使用了一个objc_setAssociatedObject的方法,这个方法有四个参数,分别是:源对象,关联时的用来标记是哪一个属性的key(因为你可能要添加很多属性),关联的对象和一个关联策略。 用来标记是哪一个属性的key常见有三种写法,但代码效果是一样的,如下: //利用静态变量地址唯一不变的特性1、static void *strKey = &strKey;2、static NSString *strKey = @"strKey"; 3、static char strKey;​关联策略是个枚举值,解释如下:enum {    OBJC_ASSOCIATION_ASSIGN = 0, //关联对象的属性是弱引用 OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //关联对象的属性是强引用并且关联对象不使用原子性OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //关联对象的属性是copy并且关联对象不使用原子性OBJC_ASSOCIATION_RETAIN = 01401, //关联对象的属性是copy并且关联对象使用原子性OBJC_ASSOCIATION_COPY = 01403 //关联对象的属性是copy并且关联对象使用原子性};​4、完成后的整体代码如下:.h文件//分类的头文件@interface ClassName (CategoryName)@property (nonatomic, strong) NSString *str;@end.m文件//实现文件static void *strKey = &strKey;@implementation ClassName (CategoryName) -(void)setStr:(NSString *)str  {      objc_setAssociatedObject(self, & strKey, str, OBJC_ASSOCIATION_COPY);  }  -(NSString *)str  {      return objc_getAssociatedObject(self, &strKey);  }@end

OBJC_ASSOCIATION_ASSIGN // 对关联对象进行弱引用OBJC_ASSOCIATION_RETAIN_NONATOMIC // 对关联对象进行强引用OBJC_ASSOCIATION_COPY_NONATOMIC // 对关联对象进行拷贝引用OBJC_ASSOCIATION_RETAIN // 对关联对象进行强引用OBJC_ASSOCIATION_COPY // 对关联对象进行拷贝引用

这使得不同继承体系分支下的两个类可以“继承”对方的方法,这样一个类可以响应自己继承分支里面的方法,同时也能响应其他不相干类发过来的消息。在上图中Warrior和Diplomat没有继承关系,但是Warrior将negotiate消息转发给了Diplomat后,就好似Diplomat是Warrior的超类一样。

Method swizzling(方法交换“黑魔法”)

方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。 话不多说,这是参考Mattt大神在NSHipster上的文章自己写的代码。import "UIViewController+swizzling.h"import@implementation UIViewController (swizzling)

//load方法会在类第一次加载的时候被调用

//调用的时间比较靠前,适合在这个方法里做方法交换

+ (void)load{

//方法交换应该被保证,在程序中只会执行一次

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

//获得viewController的生命周期方法的selector

SEL systemSel = @selector(viewWillAppear:);

//自己实现的将要被交换的方法的selector

SEL swizzSel = @selector(swiz_viewWillAppear:);

//两个方法的Method

Method systemMethod = class_getInstanceMethod([self class], systemSel);

Method swizzMethod = class_getInstanceMethod([self class], swizzSel);

//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败

BOOL isAdd = class_addMethod(self, systemSel,

method_getImplementation(swizzMethod),

method_getTypeEncoding(swizzMethod));

if (isAdd) {

//如果成功,说明类中不存在这个方法的实现

//将被交换方法的实现替换到这个并不存在的实现

class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));

}else{

//否则,交换两个方法的实现

method_exchangeImplementations(systemMethod, swizzMethod);

}

});

}

- (void)swiz_viewWillAppear:(BOOL)animated{

//这时候调用自己,看起来像是死循环

//但是其实自己的实现已经被替换了

[self swiz_viewWillAppear:animated];

NSLog(@"swizzle");

}

@end

在一个自己定义的viewController中重写viewWillAppear

- (void)viewWillAppear:(BOOL)animated{

[super viewWillAppear:animated];

NSLog(@"viewWillAppear");

}

//Run起来看看输出吧!

关联对象在一些第三方框架的分类中常常见到,这里在分析前先看下分类的结构:

消息转发提供了许多类似于多继承的特性,但是他们之间有一个很大的不同:

我的理解:

Swizzling应该写在+load方法中,因为+load是在类被初始化时候就被调用的。+initialize是在收到消息之后才调用,如果应用不发送消息给它,它就永远不可能执行。

Swizzling应该被写在dispatch_once中,保证只被执行一次和线程安全。

如果类中已经有了可替换的方法,那么就调用method_exchangeImplementations交换,否则调用class_addMethod和class_replaceMethod来完成替换。

xxx_viewWillAppear:方法的看似会引发死循环,但其实不会。在Swizzling的过程中xxx_viewWillAppear:已经被重新指定到UIViewController类的-viewWillAppear:中。不过如果我们调用的是viewWillAppear:反而会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程。

既然是切面,就一定不要忘记,交换完再调回自己。

一定要保证只交换一次,否则就会很乱。

最后,据说这个技术很危险,谨慎使用。

防止数组越界 使用交换方法 越界时动态使用方法 但是谨慎使用

struct category_t { // 类名 const char *name; // 类 classref_t cls; // 实例方法 struct method_list_t *instanceMethods; // 类方法 struct method_list_t *classMethods; // 协议 struct protocol_list_t *protocols; // 属性 struct property_list_t *instanceProperties;};

多继承:合并了不同的行为特征在一个单独的对象中,会得到一个重量级多层面的对象。

从以上的分类结构,可以看出,分类中是不能添加成员变量的,也就是Ivar类型。所以,如果想在分类中存储某些数据时,关联对象就是在这种情况下的常用选择。

消息转发:将各个功能分散到不同的对象中,得到的一些轻量级的对象,这些对象通过消息通过消息转发联合起来。

需要注意的是,关联对象并不是成员变量,关联对象是由一个全局哈希表存储的键值对中的值。

这里值得说明的一点是,即使我们利用转发消息来实现了“假”继承,但是NSObject类还是会将两者区分开。像respondsToSelector:和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate消息:

全局哈希表的定义如下:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
class AssociationsManager { static spinlock_t _lock; static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.public: AssociationsManager() { spinlock_lock(&_lock); } ~AssociationsManager() { spinlock_unlock(&_lock); } AssociationsHashMap &associations() { if (_map == NULL) _map = new AssociationsHashMap(); return *_map; }};

结果是NO,虽然它能够响应negotiate消息而不报错,但是它是靠转发消息给Diplomat类来响应消息的。

其中的AssociationsHashMap就是那个全局哈希表,而注释中也说明的很清楚了:哈希表中存储的键值对是(源对象指针 : 另一个哈希表)。而这个value,即ObjectAssociationMap对应的哈希表如下:

如果非要制造假象,反应出这种“假”的继承关系,那么需要重新实现 respondsToSelector:和 isKindOfClass:来加入你的转发算法:

// hash_map和unordered_map是模版类// 查看源码后可以看出AssociationsHashMap的key是disguised_ptr_t类型,value是ObjectAssociationMap *类型// ObjectAssociationMap的key是void *类型,value是ObjcAssociation类型#if TARGET_OS_WIN32 typedef hash_map ObjectAssociationMap; typedef hash_map AssociationsHashMap;#else typedef ObjcAllocator > ObjectAssociationMapAllocator; class ObjectAssociationMap : public std::map { public: void *operator new { return ::_malloc_internal; } void operator delete(void *ptr) { ::_free_internal; } }; typedef ObjcAllocator > AssociationsHashMapAllocator; class AssociationsHashMap : public unordered_map { public: void *operator new { return ::_malloc_internal; } void operator delete(void *ptr) { ::_free_internal; } };#endif
- respondsToSelector:aSelector{ if ( [super respondsToSelector:aSelector] ) return YES; else { /* Here, test whether the aSelector message can * * be forwarded to another object and whether that * * object can respond to it. Return YES if it can. */ } return NO;}

其中的ObjectAssociationMap就是value的类型。同时,也可以知道ObjectAssociationMap的键值对类型为(关联对象对应的key : 关联对象),也就是函数objc_setAssociatedObject的对应的key:value参数。

除了respondsToSelector:和 isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:也一样需要重写。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector:

大部分情况下,关联对像会使用getter方法的SEL当作key(getter方法中可以这样表示:_cmd)。

- (NSMethodSignature*)methodSignatureForSelector:selector{ NSMethodSignature* signature = [super methodSignatureForSelector:selector]; if (!signature) { signature = [surrogate methodSignatureForSelector:selector]; } return signature;}

更多和关联对象有关的底层信息,可以查看Dive into Category

Note: This is an advanced technique, suitable only for situations where no other solution is possible. It is not intended as a replacement for inheritance. If you must make use of this technique, make sure you fully understand the behavior of the class doing the forwarding and the class you’re forwarding to.

MethodSwizzling主要原理就是利用runtime的动态特性,交换方法对应的实现,也就是IMP。通常,MethodSwizzling的封装为:

需要引起注意的一点,实现methodSignatureForSelector方法是一种先进的技术,只适用于没有其他解决方案的情况下。它不会作为继承的替代。如果您必须使用这种技术,请确保您完全理解类做的转发和您转发的类的行为。请勿滥用!

+ load{// 源方法--原始的方法// 目的方法--我们自己实现的,用来替换源方法 static dispatch_once_t onceToken; // MethodSwizzling代码只需要在类加载时调用一次,并且需要线程安全环境 dispatch_once(&onceToken, ^{ Class class = [self class]; // 获取方法的SEL SEL origionSel = @selector(viewDidLoad); SEL swizzlingSel = @selector(tpc_viewDidLoad); // IMP origionMethod = class_getMethodImplementation(class, origionSel); // IMP swizzlingMethod = class_getMethodImplementation(class, swizzlingSel); // 根据SEL获取对应的Method Method origionMethod = class_getInstanceMethod(class, origionSel); Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSel); // 向类中添加目的方法对应的Method BOOL hasAdded = class_addMethod(class, origionSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod)); // 交换源方法和目的方法的Method方法实现 if  { class_replaceMethod(class, swizzlingSel, method_getImplementation(origionMethod), method_getTypeEncoding(origionMethod)); } else { method_exchangeImplementations(origionMethod, swizzlingMethod); } });}

图片 3

为了便于区别,这里列出Method的结构:

提到Objective-C 中的 Runtime,大多数人第一个想到的可能就是黑魔法Method Swizzling。毕竟这是Runtime里面很强大的一部分,它可以通过Runtime的API实现更改任意的方法,理论上可以在运行时通过类名/方法名hook到任何 OC 方法,替换任何类的实现以及新增任意类。

typedef struct method_t *Method;// method_tstruct method_t { SEL name; const char *types; IMP imp; ...}

举的最多的例子应该就是埋点统计用户信息的例子。

实现MethodSwizzling需要了解的有以下几个常用函数:

假设我们需要在页面上不同的地方统计用户信息,常见做法有两种:

// 返回方法的具体实现IMP class_getMethodImplementation ( Class cls, SEL name )// 返回方法描述Method class_getInstanceMethod ( Class cls, SEL name )// 添加方法BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types )// 替代方法的实现IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types )// 返回方法的实现IMP method_getImplementation ( Method m );// 获取描述方法参数和返回值类型的字符串const char * method_getTypeEncoding ( Method m );// 交换两个方法的实现void method_exchangeImplementations ( Method m1, Method m2 );
  1. 傻瓜式的在所有需要统计的页面都加上代码。这样做简单,但是重复的代码太多。
  2. 把统计的代码写入基类中,比如说BaseViewController。这样虽然代码只需要写一次,但是UITableViewController,UICollectionViewcontroller都需要写一遍,这样重复的代码依旧不少。

介绍MethodSwizzling的文章很多,更多和MethodSwizzling有关的信息,可以查看Objective-C的hook方案: Method Swizzling

基于这两点,我们这时候选用Method Swizzling来解决这个事情最优雅。

UIView-FDCollapsibleConstraints是sunnyxx阳神写的一个UIView分类,可以实现仅在IB中对UIView上的约束进行设置,就达到以下效果,而不需要编写改变约束的代码:(图片来源UIView-FDCollapsibleConstraints)

1. Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。而且Method Swizzling也是iOS中AOP的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

Method Swizzling本质上就是对IMP和SEL进行交换。

图片 4UIView下图片 5UITableView下

2.Method Swizzling使用

一般我们使用都是新建一个分类,在分类中进行Method Swizzling方法的交换。交换的代码模板如下:

#import <objc/runtime.h>@implementation UIViewController (Swizzling)+ load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; // When swizzling a class method, use the following: // Class class = object_getClass; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(xxx_viewWillAppear:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } });}#pragma mark - Method Swizzling- xxx_viewWillAppear:animated { [self xxx_viewWillAppear:animated]; NSLog(@"viewWillAppear: %@", self);}@end

Method Swizzling可以在运行时通过修改类的方法列表中selector对应的函数或者设置交换方法实现,来动态修改方法。可以重写某个方法而不用继承,同时还可以调用原先的实现。所以通常应用于在category中添加一个方法。

这里介绍下自己对这个分类的理解:

3.Method Swizzling注意点

图片 6

1.Swizzling应该总在+load中执行

Objective-C在运行时会自动调用类的两个方法+load和+initialize。+load会在类初始加载时调用, +initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize方法是永远不会被调用的。所以Swizzling要是写在+initialize方法中,是有可能永远都不被执行。

和+initialize比较+load能保证在类的初始化过程中被加载。

关于+load和+initialize的比较可以参看这篇文章《Objective-C +load vs +initialize》

2.Swizzling应该总是在dispatch_once中执行

Swizzling会改变全局状态,所以在运行时采取一些预防措施,使用dispatch_once就能够确保代码不管有多少线程都只被执行一次。这将成为Method Swizzling的最佳实践。

这里有一个很容易犯的错误,那就是继承中用了Swizzling。如果不写dispatch_once就会导致Swizzling失效!

举个例子,比如同时对NSArray和NSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致NSArray中的Swizzling失效的。

可是为什么会这样呢?原因是,我们没有用dispatch_once控制Swizzling只执行一次。如果这段Swizzling被执行多次,经过多次的交换IMP和SEL之后,结果可能就是未交换之前的状态。

比如说父类A的B方法和子类C的D方法进行交换,交换一次后,父类A持有D方法的IMP,子类C持有B方法的IMP,但是再次交换一次,就又还原了。父类A还是持有B方法的IMP,子类C还是持有D方法的IMP,这样就相当于咩有交换。可以看出,如果不写dispatch_once,偶数次交换以后,相当于没有交换,Swizzling失效!

3.Swizzling在+load中执行时,不要调用[super load]

原因同注意点二,如果是多继承,并且对同一个方法都进行了Swizzling,那么调用[super load]以后,父类的Swizzling就失效了。

4.上述模板中没有错误

有些人怀疑我上述给的模板可能有错误。在这里需要讲解一下。

在进行Swizzling的时候,我们需要用class_addMethod先进行判断一下原有类中是否有要替换的方法的实现。

如果class_addMethod返回NO,说明当前类中有要替换方法的实现,所以可以直接进行替换,调用method_exchangeImplementations即可实现Swizzling。

如果class_addMethod返回YES,说明当前类中没有要替换方法的实现,我们需要在父类中去寻找。这个时候就需要用到method_getImplementation去获取class_getInstanceMethod里面的方法实现。然后再进行class_replaceMethod来实现Swizzling。

这是Swizzling需要判断的一点。

还有一点需要注意的是,在我们替换的方法- xxx_viewWillAppear:animated中,调用了[self xxx_viewWillAppear:animated];这不是死循环了么?

其实这里并不会死循环。由于我们进行了Swizzling,所以其实在原来的- viewWillAppear:animated方法中,调用的是- xxx_viewWillAppear:animated方法的实现。所以不会造成死循环。相反的,如果这里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就会造成死循环。因为外面调用[self viewWillAppear:animated];的时候,会交换方法走到[self xxx_viewWillAppear:animated];这个方法实现中来,然后这里又去调用[self viewWillAppear:animated],就会造成死循环了。

所以按照上述Swizzling的模板来写,就不会遇到这4点需要注意的问题啦。

  • 实现思路
    • 将需要和UIView关联且需要动态修改的约束添加进一个和UIView绑定的特定的数组里面
    • 根据UIView的内容是否为nil,对这个特定数组中的约束值进行统一设置
4.Method Swizzling使用场景

Method Swizzling使用场景其实有很多很多,在一些特殊的开发需求中适时的使用黑魔法,可以做法神来之笔的效果。这里就举3种常见的场景。

1.实现AOP

AOP的例子在上一篇文章中举了一个例子,在下一章中也打算详细分析一下其实现原理,这里就一笔带过。

2.实现埋点统计

如果app有埋点需求,并且要自己实现一套埋点逻辑,那么这里用到Swizzling是很合适的选择。优点在开头已经分析了,这里不再赘述。看到一篇分析的挺精彩的埋点的文章,推荐大家阅读。iOS动态性可复用而且高度解耦的用户统计埋点实现

3.实现异常保护

日常开发我们经常会遇到NSArray数组越界的情况,苹果的API也没有对异常保护,所以需要我们开发者开发时候多多留意。关于Index有好多方法,objectAtIndex,removeObjectAtIndex,replaceObjectAtIndex,exchangeObjectAtIndex等等,这些设计到Index都需要判断是否越界。

常见做法是给NSArray,NSMutableArray增加分类,增加这些异常保护的方法,不过如果原有工程里面已经写了大量的AtIndex系列的方法,去替换成新的分类的方法,效率会比较低。这里可以考虑用Swizzling做。

#import "NSArray+ Swizzling.h"#import "objc/runtime.h"@implementation NSArray (Swizzling)+ load { Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzling_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod);}- swizzling_objectAtIndex:(NSUInteger)index { if (self.count-1 < index) { // 异常处理 @try { return [self swizzling_objectAtIndex:index]; } @catch (NSException *exception) { // 打印崩溃信息 NSLog(@"---------- %s Crash Because Method %s ----------n", class_getName(self.class), __func__); NSLog(@"%@", [exception callStackSymbols]); return nil; } @finally {} } else { return [self swizzling_objectAtIndex:index]; }}@end

注意,调用这个objc_getClass方法的时候,要先知道类对应的真实的类名才行,NSArray其实在Runtime中对应着__NSArrayI,NSMutableArray对应着__NSArrayM,NSDictionary对应着__NSDictionaryI,NSMutableDictionary对应着__NSDictionaryM。

图片 7

Wikipedia 里对 AOP 是这么介绍的:

An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).

类似记录日志、身份验证、缓存等事务非常琐碎,与业务逻辑无关,很多地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫横向关注点(Cross-cutting concern),AOP作用就是分离横向关注点(Cross-cutting concern)来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。

接下来分析分析AOP的工作原理。

在上一篇中我们分析过了,在objc_msgSend函数查找IMP的过程中,如果在父类也没有找到相应的IMP,那么就会开始执行_class_resolveMethod方法,如果不是元类,就执行_class_resolveInstanceMethod,如果是元类,执行_class_resolveClassMethod。在这个方法中,允许开发者动态增加方法实现。这个阶段一般是给@dynamic属性变量提供动态方法的。

如果_class_resolveMethod无法处理,会开始选择备援接受者接受消息,这个时候就到了forwardingTargetForSelector方法。如果该方法返回非nil的对象,则使用该对象作为新的消息接收者。

- forwardingTargetForSelector:aSelector{ if(aSelector == @selector{ return otherObject; } return [super forwardingTargetForSelector:aSelector];}

同样也可以替换类方法

+ forwardingTargetForSelector:aSelector { if(aSelector == @selector { return NSClassFromString(@"Class name"); } return [super forwardingTargetForSelector:aSelector];}

替换类方法返回值就是一个类对象。

forwardingTargetForSelector这种方法属于单纯的转发,无法对消息的参数和返回值进行处理。

最后到了完整转发阶段。

Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。为接下来的完整的消息转发生成一个 NSMethodSignature对象。NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。

// 为目标对象中被调用的方法返回一个NSMethodSignature实例#warning 运行时系统要求在执行标准转发时实现这个方法- (NSMethodSignature *)methodSignatureForSelector:sel{ return [self.proxyTarget methodSignatureForSelector:sel];}

对象需要创建一个NSInvocation对象,把消息调用的全部细节封装进去,包括selector, target, arguments 等参数,还能够对返回结果进行处理。

AOP的多数操作就是在forwardInvocation中完成的。一般会分为2个阶段,一个是Intercepter注册阶段,一个是Intercepter执行阶段。

而在分类不能增加成员变量的情况下,和UIView绑定的特定的数组就是用关联对象实现的。

1. Intercepter注册

图片 8

首先会把类里面的某个要切片的方法的IMP加入到Aspect中,类方法里面如果有forwardingTargetForSelector:的IMP,也要加入到Aspect中。

图片 9

然后对类的切片方法和forwardingTargetForSelector:的IMP进行替换。两者的IMP相应的替换为objc_msgForward()方法和hook过的forwardingTargetForSelector:。这样主要的Intercepter注册就完成了。

先从分类的头文件开始:

2. Intercepter执行

图片 10

当执行func()方法的时候,会去查找它的IMP,现在它的IMP已经被我们替换为了objc_msgForward()方法,于是开始查找备援转发对象。

查找备援接受者调用forwardingTargetForSelector:这个方法,由于这里是被我们hook过的,所以IMP指向的是hook过的forwardingTargetForSelector:方法。这里我们会返回Aspect的target,即选取Aspect作为备援接受者。

有了备援接受者之后,就会重新objc_msgSend,从消息发送阶段重头开始。

objc_msgSend找不到指定的IMP,再进行_class_resolveMethod,这里也没有找到,forwardingTargetForSelector:这里也不做处理,接着就会methodSignatureForSelector。在methodSignatureForSelector方法中创建一个NSInvocation对象,传递给最终的forwardInvocation方法。

Aspect里面的forwardInvocation方法会干所有切面的事情。这里转发逻辑就完全由我们自定义了。Intercepter注册的时候我们也加入了原来方法中的method()和forwardingTargetForSelector:方法的IMP,这里我们可以在forwardInvocation方法中去执行这些IMP。在执行这些IMP的前后都可以任意的插入任何IMP以达到切面的目的。

以上就是AOP的原理。

前面第二点谈到了黑魔法Method Swizzling,本质上就是对IMP和SEL进行交换。其实接下来要说的Isa Swizzling,和它类似,本质上也是交换,不过交换的是Isa。

在苹果的官方库里面有一个很有名的技术就用到了这个Isa Swizzling,那就是KVO——Key-Value Observing。

官方文档上对于KVO的定义是这样的:

Automatic key-value observing is implemented using a technique called isa-swizzling.The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

官方给的就这么多,具体实现也没有说的很清楚。那只能我们自己来实验一下。

KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听对象有没有调用被监听属性对应的setter方法。具体实现应该是重写其setter方法即可。

官方是如何优雅的实现重写监听类的setter方法的呢?实验代码如下:

 Student *stu = [[Student alloc]init]; [stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

我们可以打印观察isa指针的指向

Printing description of stu->isa:StudentPrinting description of stu->isa:NSKVONotifying_Student

通过打印,我们可以很明显的看到,被观察的对象的isa变了,变成了NSKVONotifying_Student这个类了。

在@interface NSObject(NSKeyValueObserverRegistration) 这个分类里面,苹果定义了KVO的方法。

- addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;- removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);- removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

KVO在调用addObserver方法之后,苹果的做法是在执行完addObserver: forKeyPath: options: context: 方法之后,把isa指向到另外一个类去。

在这个新类里面重写被观察的对象四个方法。class,setter,dealloc,_isKVOA。

@interface UIView (FDCollapsibleConstraints)/// Assigning this property immediately disables the view's collapsible constraints'/// by setting their constants to zero.@property (nonatomic, assign) BOOL fd_collapsed;/// Specify constraints to be affected by "fd_collapsed" property by connecting in/// Interface Builder.@property (nonatomic, copy) IBOutletCollection(NSLayoutConstraint) NSArray *fd_collapsibleConstraints;@end@interface UIView (FDAutomaticallyCollapseByIntrinsicContentSize)/// Enable to automatically collapse constraints in "fd_collapsibleConstraints" when/// you set or indirectly set this view's "intrinsicContentSize" to {0, 0} or absent.////// For example:/// imageView.image = nil;/// label.text = nil, label.text = @"";////// "NO" by default, you may enable it by codes.@property (nonatomic, assign) BOOL fd_autoCollapse;/// "IBInspectable" property, more friendly to Interface Builder./// You gonna find this attribute in "Attribute Inspector", toggle "On" to enable./// Why not a "fd_" prefix? Xcode Attribute Inspector will clip it like a shit./// You should not assgin this property directly by code, use "fd_autoCollapse" instead.@property (nonatomic, assign, getter=fd_autoCollapse) IBInspectable BOOL autoCollapse;
1. 重写class方法

重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。

static NSArray * ClassMethodNames{ NSMutableArray * array = [NSMutableArray array]; unsigned int methodCount = 0; Method * methodList = class_copyMethodList(c, &methodCount); unsigned int i; for(i = 0; i < methodCount; i++) { [array addObject: NSStringFromSelector(method_getName(methodList[i]))]; } free(methodList); return array;}int main(int argc, char * argv[]) { Student *stu = [[Student alloc]init]; NSLog(@"self->isa:%@",object_getClass; NSLog(@"self class:%@",[stu class]); NSLog(@"ClassMethodNames = %@",ClassMethodNames(object_getClass; [stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; NSLog(@"self->isa:%@",object_getClass; NSLog(@"self class:%@",[stu class]); NSLog(@"ClassMethodNames = %@",ClassMethodNames(object_getClass;}

打印结果

self->isa:Studentself class:StudentClassMethodNames = (".cxx_destruct",name,"setName:")self->isa:NSKVONotifying_Studentself class:StudentClassMethodNames = ("setName:",class,dealloc,"_isKVOA")

这里也可以看出,这是object_getClass方法和class方法的区别。

这里要特别说明一下,为何打印 object_getClass 方法和 class 方法打印出来结果不同。

- class { return object_getClass;}Class object_getClass { if  return obj->getIsa(); else return Nil;}

从实现上看,两个方法的实现都一样的,按道理来说,打印结果应该相同,可是为何在加了 KVO 以后会出现打印结果不同呢?

** 根本原因:对于KVO,底层交换了 NSKVONotifying_Student 的 class 方法,让其返回 Student。**

打印这句话 object_getClass 的时候,isa 当然是 NSKVONotifying_Student。

+ respondsToSelector:sel { if  return NO; return class_respondsToSelector_inst(object_getClass, sel, self);}

当我们执行 NSLog 的时候,会执行上面这个方法,这个方法的 sel 是encodeWithOSLogCoder:options:maxLength:,这个时候,self是 NSKVONotifying_Student,上面那个 respondsToSelector 方法里面 return 的 object_getClass 结果还是NSKVONotifying_Student。

打印 [stu class] 的时候,isa 当然还是 NSKVONotifying_Student。当执行到 NSLog 的时候,+ respondsToSelector:sel,又会执行到这个方法,这个时候的 self 变成了 Student,这个时候 respondsToSelector 方法里面的 object_getClass 输出当然就是 Student 了。

分析几点:

2. 重写setter方法

在新的类中会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:

- willChangeValueForKey:(NSString *)key- didChangeValueForKey:(NSString *)key

在didChangeValueForKey:方法再调用

- observeValueForKeyPath:(NSString *)keyPath ofObject:object change:(NSDictionary *)change context:context

这里有几种情况需要说明一下:

1)如果使用了KVC如果有访问器方法,则运行时会在setter方法中调用will/didChangeValueForKey:方法;

如果没用访问器方法,运行时会在setValue:forKey方法中调用will/didChangeValueForKey:方法。

所以这种情况下,KVO是奏效的。

2)有访问器方法运行时会重写访问器方法调用will/didChangeValueForKey:方法。因此,直接调用访问器方法改变属性值时,KVO也能监听到。

3)直接调用will/didChangeValueForKey:方法。

综上所述,只要setter中重写will/didChangeValueForKey:方法就可以使用KVO了。

  • IBOutletCollection,详情参考IBAction / IBOutlet / IBOutlet​Collection
    • 表示将SB中相同的控件连接到一个数组中;这里使用这个方式,将在SB中的NSLayoutConstraint添加到fd_collapsibleConstraints数组中,以便后续对约束进行统一操作
    • IBOutletCollectionh和IBOutlet操作方式一样,需要在IB中进行相应的拖拽才能把对应的控件加到数组中(UIView->NSLayoutConstraint
    • 设置了IBOutletCollection之后,当从storybooard或者xib中加载进行解档时,最终会调用fd_collapsibleConstraints的setter方法,然后就可以在其setter方法中做相应的操作了
  • IBInspectable 表示这个属性可以在IB中更改,如下图
3. 重写dealloc方法

销毁新生成的NSKVONotifying_类。

图片 11Snip20150704_1.png- 还有一个这里没用,IB_DESIGNABLE,这个表示可以在IB中实时显示修改的效果,详情参考@IBDesignable和@IBInspectable

4. 重写_isKVOA方法

这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。

Foundation 到底为我们提供了哪些用于 KVO 的辅助函数。打开 terminal,使用 nm -a 命令查看 Foundation 中的信息:

nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

里面包含了以下这些KVO中可能用到的函数:

00000000000233e7 t __NSSetDoubleValueAndNotify00000000000f32ba t __NSSetFloatValueAndNotify0000000000025025 t __NSSetIntValueAndNotify000000000007fbb5 t __NSSetLongLongValueAndNotify00000000000f33e8 t __NSSetLongValueAndNotify000000000002d36c t __NSSetObjectValueAndNotify0000000000024dc5 t __NSSetPointValueAndNotify00000000000f39ba t __NSSetRangeValueAndNotify00000000000f3aeb t __NSSetRectValueAndNotify00000000000f3512 t __NSSetShortValueAndNotify00000000000f3c2f t __NSSetSizeValueAndNotify00000000000f363b t __NSSetUnsignedCharValueAndNotify000000000006e91f t __NSSetUnsignedIntValueAndNotify0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify00000000000f3766 t __NSSetUnsignedLongValueAndNotify00000000000f3890 t __NSSetUnsignedShortValueAndNotify00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey

Foundation 提供了大部分基础数据类型的辅助函数(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了,但没有 C++中的 bool),此外还包括一些常见的结构体如 Point, Range, Rect, Size,这表明这些结构体也可以用于自动键值观察,但要注意除此之外的结构体就不能用于自动键值观察了。对于所有 Objective C 对象对应的是 __NSSetObjectValueAndNotify 方法。

KVO即使是苹果官方的实现,也是有缺陷的,这里有一篇文章详细了分析了KVO中的缺陷,主要问题在KVO的回调机制,不能传一个selector或者block作为回调,而必须重写-addObserver:forKeyPath:options:context:方法所引发的一系列问题。而且只监听一两个属性值还好,如果监听的属性多了, 或者监听了多个对象的属性, 那有点麻烦,需要在方法里面写很多的if-else的判断。

最后,官方文档上对于KVO的实现的最后,给出了需要我们注意的一点是,永远不要用用isa来判断一个类的继承关系,而是应该用class方法来判断类的实例。

图片 12

Associated Objects是Objective-C 2.0中Runtime的特性之一。众所周知,在 Category 中,我们无法添加@property,因为添加了@property之后并不会自动帮我们生成实例变量以及存取方法。那么,我们现在就可以通过关联对象来实现在 Category 中添加属性的功能了。

NSLayoutConstraint (_FDOriginalConstantStorage)
  • 因为在修改约束值后,需要还原操作,但是分类中无法添加成员变量,所以在这个分类中,给NSLayoutConstraint约束关联一个存储约束初始值的浮点数,以便在修改约束值后,可以还原
/// A stored property extension for NSLayoutConstraint's original constant.@implementation NSLayoutConstraint (_FDOriginalConstantStorage)// 给NSLayoutConstraint关联一个初始约束值- setFd_originalConstant:originalConstant{ objc_setAssociatedObject(self, @selector(fd_originalConstant), @(originalConstant), OBJC_ASSOCIATION_RETAIN);}- fd_originalConstant{#if CGFLOAT_IS_DOUBLE return [objc_getAssociatedObject(self, _cmd) doubleValue];#else return [objc_getAssociatedObject(self, _cmd) floatValue];#endif}@end
1. 用法

借用这篇经典文章Associated Objects里面的例子来说明一下用法。

// NSObject+AssociatedObject.h@interface NSObject (AssociatedObject)@property (nonatomic, strong) id associatedObject;@end// NSObject+AssociatedObject.m@implementation NSObject (AssociatedObject)@dynamic associatedObject;- setAssociatedObject:object { objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- associatedObject { return objc_getAssociatedObject(self, @selector(associatedObject));}

这里涉及到了3个函数:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);OBJC_EXPORT void objc_removeAssociatedObjects(id object) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

来说明一下这些参数的意义:

1.id object 设置关联对象的实例对象

2.const void *key 区分不同的关联对象的 key。这里会有3种写法。

使用 &AssociatedObjectKey 作为key值

static char AssociatedObjectKey = "AssociatedKey";

使用AssociatedKey 作为key值

static const void *AssociatedKey = "AssociatedKey";

使用@selector

@selector(associatedKey)

3种方法都可以,不过推荐使用更加简洁的第三种方式。

3.id value 关联的对象

4.objc_AssociationPolicy policy 关联对象的存储策略,它是一个枚举,与property的attribute 相对应。

Behavior @property Equivalent Description
OBJC_ASSOCIATION_ASSIGN @property / @property (unsafe_unretained) 弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 强引用关联对象,且为非原子操
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 复制关联对象,且为非原子操作
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 强引用关联对象,且为原子操作
OBJC_ASSOCIATION_COPY @property (atomic, copy) 复制关联对象,且为原子操作

这里需要注意的是标记成OBJC_ASSOCIATION_ASSIGN的关联对象和@property 是不一样的,上面表格中等价定义写的是 @property (unsafe_unretained),对象被销毁时,属性值仍然还在。如果之后再次使用该对象就会导致程序闪退。所以我们在使用OBJC_ASSOCIATION_ASSIGN时,要格外注意。

According to the Deallocation Timeline described in WWDC 2011, Session 322, associated objects are erased surprisingly late in the object lifecycle, inobject_dispose(), which is invoked by NSObject -dealloc.

关于关联对象还有一点需要说明的是objc_removeAssociatedObjects。这个方法是移除源对象中所有的关联对象,并不是其中之一。所以其方法参数中也没有传入指定的key。要删除指定的关联对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可。

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

关联对象3种使用场景

1.为现有的类添加私有变量2.为现有的类添加公有属性3.为KVO创建一个关联的观察者。

UIView (FDCollapsibleConstraints)
  • 同样,因为需要对UIView上绑定的约束进行改动,所以需要在分类中添加一个可以记录所有约束的对象,需要用到关联对象

  • 实现fd_collapsibleConstraints属性的setter和getter方法 (关联一个存储约束的对象)

    • getter方法中创建关联对象constraints(和懒加载的方式类似,不过不是创建成员变量)
    • setter方法中设置约束的初始值,并添加进关联对象constraints中,方便统一操作
  • 从IB中关联的约束,最终会调用setFd_collapsibleConstraints:方法,也就是这一步不需要手动调用,系统自己完成(在awakeFromNib之前完成IB这些值的映射)

    - (NSMutableArray *)fd_collapsibleConstraints{ // 获取对象的所有约束关联值 NSMutableArray *constraints = objc_getAssociatedObject(self, _cmd); if (!constraints) { constraints = @[].mutableCopy; // 设置对象的所有约束关联值 objc_setAssociatedObject(self, _cmd, constraints, OBJC_ASSOCIATION_RETAIN); } return constraints;}// IBOutletCollection表示xib中的相同的控件连接到一个数组中// 因为设置了IBOutletCollection,所以从xib进行解档时,最终会调用set方法// 然后就来到了这个方法- setFd_collapsibleConstraints:(NSArray *)fd_collapsibleConstraints{ // Hook assignments to our custom `fd_collapsibleConstraints` property. // 返回保存原始约束的数组,使用关联对象 NSMutableArray *constraints = (NSMutableArray *)self.fd_collapsibleConstraints; [fd_collapsibleConstraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) { // Store original constant value // 保存原始的约束 constraint.fd_originalConstant = constraint.constant; [constraints addObject:constraint]; }];}
    
  • 使用Method Swizzling交换自己的和系统的-setValue:forKey:方

    • 实现自己的KVC的-setValue:forKey:方法
 // load先从原类,再调用分类的开始调用 // 也就是调用的顺序是 // 原类 // FDCollapsibleConstraints // FDAutomaticallyCollapseByIntrinsicContentSize // 所以并不冲突 + load { // Swizzle setValue:forKey: to intercept assignments to `fd_collapsibleConstraints` // from Interface Builder. We should not do so by overriding setvalue:forKey: // as the primary class implementation would be bypassed. SEL originalSelector = @selector(setValue:forKey:); SEL swizzledSelector = @selector(fd_setValue:forKey:); Class class = UIView.class; Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } // xib也就是xml,再加载进行decode时,会调用setValue:forKey:,把他的方法替换成自身的,然后获取添加的约束 // 作者说明不使用重写这个KVC方法的方式,是因为这样会覆盖view本身在这个方法中进行的操作 - fd_setValue:value forKey:(NSString *)key { NSString *injectedKey = [NSString stringWithUTF8String:sel_getName(@selector(fd_collapsibleConstraints))]; if ([key isEqualToString:injectedKey]) { // This kind of IBOutlet won't trigger property's setter, so we forward it. // 作者的意思是,IBOutletCollection不会触发对应属性的setter方法,所以这里执行手动调用 self.fd_collapsibleConstraints = value; } else { // Forward the rest of KVC's to original implementation. [self fd_setValue:value forKey:key]; } }
  • 上面使用Method Swizzling的原因作者认为是这种类型的IBOutlet不会触发其setter方法,但是经过测试,注释掉这段代码后,系统还是自己触发了setter方法,说明这种IBOutlet还是可以触发setter方法的。所以,即使没有这一段代码,应该也是可行的

图片 13操作结果

  • 设置对应的约束值

    • 这里给UIView对象提供一个关联对象,来判断是否将约束值清零
    • 注意,这里只要传入的是YES,那么,这个UIView对应存入constraints关联对象的所有约束,都会置为0
    #pragma mark - Dynamic Properties- setFd_collapsed:collapsed{ [self.fd_collapsibleConstraints enumerateObjectsUsingBlock: ^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) { if (collapsed) { // 如果view的内容为nil,则将view关联的constraints对象所有值设置为0 constraint.constant = 0; } else { // 如果view的内容不为nil,则将view关联的constraints对象所有值返回成原值 constraint.constant = constraint.fd_originalConstant; } }]; // 设置fd_collapsed关联对象,供自动collapsed使用 objc_setAssociatedObject(self, @selector(fd_collapsed), @(collapsed), OBJC_ASSOCIATION_RETAIN);}- fd_collapsedFDAutomaticallyCollapseByIntrinsicContentSize{return [objc_getAssociatedObject(self, _cmd) boolValue];}@end
    
######UIView (FDAutomaticallyCollapseByIntrinsicContentSize)- 使用Method Swizzling交换自己实现的-fd_updateConstraints和系统的updateConstraints方法 - [self fd_updateConstraints]调用的是self的updateConstraints方法,fd_updateConstraints和updateConstraints方法的IMP,即方法实现已经调换了 - 可以看到,加入这里不使用Method Swizzling,那么要实现在更新约束时就需要`重写updateConstraints`方法,而这只能在`继承UIView`的情况下才能完成的;而实用了Method Swizzling,就可以直接在`分类`中实现在`调用系统updateConstraints的前提下`,又`添加自己想要执行的附加代码` - `intrinsicContentSize`默认为UIViewNoIntrinsicMetric,当`控件中没有内容时`,调用intrinsicContentSize返回的即为`默认值`,详情参考([intrinsicContentSize和Content Hugging Priority](http://www.mgenware.com/blog/?p=491)) ```objc #pragma mark - Hacking "-updateConstraints" + load { // Swizzle to hack "-updateConstraints" method SEL originalSelector = @selector(updateConstraints); SEL swizzledSelector = @selector(fd_updateConstraints); Class class = UIView.class; Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } - fd_updateConstraints { // Call primary method's implementation [self fd_updateConstraints]; if (self.fd_autoCollapse && self.fd_collapsibleConstraints.count > 0) { // "Absent" means this view doesn't have an intrinsic content size, {-1, -1} actually. const CGSize absentIntrinsicContentSize = CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric); // 当设置控件显示内容为nil时,计算出来的contentSize和上面的相等 // Calculated intrinsic content size const CGSize contentSize = [self intrinsicContentSize]; // When this view doesn't have one, or has no intrinsic content size after calculating, // it going to be collapsed. if (CGSizeEqualToSize(contentSize, absentIntrinsicContentSize) || CGSizeEqualToSize(contentSize, CGSizeZero)) { // 当控件没有内容时,则设置控件关联对象constraints的所有约束值为0 self.fd_collapsed = YES; } else { // 当控件有内容时,则设置控件关联对象constraints的所有约束值返回为原值 self.fd_collapsed = NO; } } }
  • 设置一些动态属性

    • 给UIView关联一个对象,来判断是否需要自动对约束值进行清零
    #pragma mark - Dynamic Properties - fd_autoCollapse
    

{return [objc_getAssociatedObject(self, _cmd) boolValue];}

- setFd_autoCollapse:autoCollapse

{objc_setAssociatedObject(self, @selector(fd_autoCollapse), @(autoCollapse), OBJC_ASSOCIATION_RETAIN);}

- setAutoCollapse:collapse

{// Just forwardingself.fd_autoCollapse = collapse;}

##总结总体来说,在分类中要想实现相对复杂的逻辑,却`不能添加成员变量`,也`不想对需要操作的类进行继承`,这时就需要runtime中的`关联对象和MethodSwizzling`技术了。forkingdog系列分类都用到了runtime的一些知识,代码简洁注释齐全风格也不错,比较适合需要学习runtime应用知识的我。
2.源码分析
objc_setAssociatedObject方法
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { // retain the new value  outside the lock. ObjcAssociation old_association; id new_value = value ? acquireValue(value, policy) : nil; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations; disguised_ptr_t disguised_object = DISGUISE; if (new_value) { // break any existing association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end { // secondary table exists ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find; if (j != refs->end { old_association = j->second; j->second = ObjcAssociation(policy, new_value); } else { [key] = ObjcAssociation(policy, new_value); } } else { // create the new association (first time). ObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; [key] = ObjcAssociation(policy, new_value); object->setHasAssociatedObjects(); } } else { // setting the association to nil breaks the association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find; if (j != refs->end { old_association = j->second; refs->erase; } } } } // release the old value (outside of the lock). if (old_association.hasValue ReleaseValue()(old_association);}

这个函数里面主要分为2部分,一部分是if里面对应的new_value不为nil的时候,另一部分是else里面对应的new_value为nil的情况。

当new_value不为nil的时候,查找时候,流程如下:

图片 14

首先在AssociationsManager的结构如下

class AssociationsManager { static spinlock_t _lock; static AssociationsHashMap *_map;public: AssociationsManager() { _lock.lock(); } ~AssociationsManager() { _lock.unlock(); } AssociationsHashMap &associations() { if (_map == NULL) _map = new AssociationsHashMap(); return *_map; }};

在AssociationsManager中有一个spinlock类型的自旋锁lock。保证每次只有一个线程对AssociationsManager进行操作,保证线程安全。AssociationsHashMap对应的是一张哈希表。

AssociationsHashMap哈希表里面key是disguised_ptr_t。

disguised_ptr_t disguised_object = DISGUISE;

通过调用DISGUISE方法获取object地址的指针。拿到disguised_object后,通过这个key值,在AssociationsHashMap哈希表里面找到对应的value值。而这个value值ObjcAssociationMap表的首地址。

在ObjcAssociationMap表中,key值是set方法里面传过来的形参const void *key,value值是ObjcAssociation对象。

ObjcAssociation对象中存储了set方法最后两个参数,policy和value。

所以objc_setAssociatedObject方法中传的4个形参在上图中已经标出。

现在弄清楚结构之后再来看源码,就很容易了。objc_setAssociatedObject方法的目的就是在这2张哈希表中存储对应的键值对。

先初始化一个 AssociationsManager,获取唯一的保存关联对象的哈希表 AssociationsHashMap,然后在AssociationsHashMap里面去查找object地址的指针。

如果找到,就找到了第二张表ObjectAssociationMap。在这张表里继续查找object的key。

if (i != associations.end { // secondary table exists ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find; if (j != refs->end { old_association = j->second; j->second = ObjcAssociation(policy, new_value); } else { [key] = ObjcAssociation(policy, new_value); }}

如果在第二张表ObjectAssociationMap找到对应的ObjcAssociation对象,那就更新它的值。如果没有找到,就新建一个ObjcAssociation对象,放入第二张表ObjectAssociationMap中。

再回到第一张表AssociationsHashMap中,如果没有找到对应的键值

ObjectAssociationMap *refs = new ObjectAssociationMap;associations[disguised_object] = refs;[key] = ObjcAssociation(policy, new_value);object->setHasAssociatedObjects();

此时就不存在第二张表ObjectAssociationMap了,这时就需要新建第二张ObjectAssociationMap表,来维护对象的所有新增属性。新建完第二张ObjectAssociationMap表之后,还需要再实例化 ObjcAssociation对象添加到 Map 中,调用setHasAssociatedObjects方法,表明当前对象含有关联对象。这里的setHasAssociatedObjects方法,改变的是isa_t结构体中的第二个标志位has_assoc的值。(关于isa_t结构体的结构,详情请看第一天的解析)

// release the old value (outside of the lock). if (old_association.hasValue ReleaseValue()(old_association);

最后如果老的association对象有值,此时还会释放它。

以上是new_value不为nil的情况。其实只要记住上面那2张表的结构,这个objc_setAssociatedObject的过程就是更新 / 新建 表中键值对的过程。

再来看看new_value为nil的情况

// setting the association to nil breaks the association.AssociationsHashMap::iterator i = associations.find(disguised_object);if (i != associations.end { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find; if (j != refs->end { old_association = j->second; refs->erase; }}

当new_value为nil的时候,就是我们要移除关联对象的时候。这个时候就是在两张表中找到对应的键值,并调用erase方法,即可删除对应的关联对象。

objc_getAssociatedObject方法
id _object_get_associative_reference(id object, void *key) { id value = nil; uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations; disguised_ptr_t disguised_object = DISGUISE; AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find; if (j != refs->end { ObjcAssociation &entry = j->second; value = entry.value(); policy = entry.policy(); if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) objc_msgSend)(value, SEL_retain); } } } if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) { objc_msgSend)(value, SEL_autorelease); } return value;}

objc_getAssociatedObject方法 很简单。就是通过遍历AssociationsHashMap哈希表 和 ObjcAssociationMap表的所有键值找到对应的ObjcAssociation对象,找到了就返回ObjcAssociation对象,没有找到就返回nil。

objc_removeAssociatedObjects方法
void objc_removeAssociatedObjects(id object) { if (object && object->hasAssociatedObjects { _object_remove_assocations; }}void _object_remove_assocations(id object) { vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations; if (associations.size return; disguised_ptr_t disguised_object = DISGUISE; AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end { // copy all of the associations that need to be removed. ObjectAssociationMap *refs = i->second; for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) { elements.push_back(j->second); } // remove the secondary table. delete refs; associations.erase; } } // the calls to releaseValue() happen outside of the lock. for_each(elements.begin(), elements.end(), ReleaseValue;}

在移除关联对象object的时候,会先去判断object的isa_t中的第二位has_assoc的值,当object 存在并且object->hasAssociatedObjects值为1的时候,才会去调用_object_remove_assocations方法。

_object_remove_assocations方法的目的是删除第二张ObjcAssociationMap表,即删除所有的关联对象。删除第二张表,就需要在第一张AssociationsHashMap表中遍历查找。这里会把第二张ObjcAssociationMap表中所有的ObjcAssociation对象都存到一个数组elements里面,然后调用associations.erase删除第二张表。最后再遍历elements数组,把ObjcAssociation对象依次释放。

以上就是Associated Object关联对象3个函数的源码分析。

在消息发送阶段,如果在父类中也没有找到相应的IMP,就会执行resolveInstanceMethod方法。在这个方法里面,我们可以动态的给类对象或者实例对象动态的增加方法。

+ resolveInstanceMethod:sel { NSString *selectorString = NSStringFromSelector; if ([selectorString isEqualToString:@"method1"]) { class_addMethod(self.class, @selector, functionForMethod1, "@:"); } return [super resolveInstanceMethod:sel];}

关于方法操作方面的函数还有以下这些

// 调用指定方法的实现id method_invoke ( id receiver, Method m, ... );// 调用返回一个数据结构的方法的实现void method_invoke_stret ( id receiver, Method m, ... );// 获取方法名SEL method_getName ( Method m );// 返回方法的实现IMP method_getImplementation ( Method m );// 获取描述方法参数和返回值类型的字符串const char * method_getTypeEncoding ( Method m );// 获取方法的返回值类型的字符串char * method_copyReturnType ( Method m );// 获取方法的指定位置参数的类型字符串char * method_copyArgumentType ( Method m, unsigned int index );// 通过引用返回方法的返回值类型字符串void method_getReturnType ( Method m, char *dst, size_t dst_len );// 返回方法的参数的个数unsigned int method_getNumberOfArguments ( Method m );// 通过引用返回方法指定位置参数的类型字符串void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );// 返回指定方法的方法描述结构体struct objc_method_description * method_getDescription ( Method m );// 设置方法的实现IMP method_setImplementation ( Method m, IMP imp );// 交换两个方法的实现void method_exchangeImplementations ( Method m1, Method m2 );

这些方法其实平时不需要死记硬背,使用的时候只要先打出method开头,后面就会有补全信息,找到相应的方法,传入对应的方法即可。

图片 15

现在虽然手写归档和解档的时候不多了,但是自动操作还是用Runtime来实现的。

- encodeWithCoder:(NSCoder *)aCoder{ [aCoder encodeObject:self.name forKey:@"name"];}- initWithCoder:(NSCoder *)aDecoder{ if (self = [super init]) { self.name = [aDecoder decodeObjectForKey:@"name"]; } return self;}

手动的有一个缺陷,如果属性多起来,要写好多行相似的代码,虽然功能是可以完美实现,但是看上去不是很优雅。

用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了。

#import "Student.h"#import <objc/runtime.h>#import <objc/message.h>@implementation Student- encodeWithCoder:(NSCoder *)aCoder{ unsigned int outCount = 0; Ivar *vars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar var = vars[i]; const char *name = ivar_getName; NSString *key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; }}- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{ if (self = [super init]) { unsigned int outCount = 0; Ivar *vars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) { Ivar var = vars[i]; const char *name = ivar_getName; NSString *key = [NSString stringWithUTF8String:name]; id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } } return self;}@end

class_copyIvarList方法用来获取当前 Model 的所有成员变量,ivar_getName方法用来获取每个成员变量的名称。

1.字典转模型

1.调用 class_getProperty 方法获取当前 Model 的所有属性。2.调用 property_copyAttributeList 获取属性列表。3.根据属性名称生成 setter 方法。4.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值

+objectWithKeyValues:(NSDictionary *)aDictionary{ id objc = [[self alloc] init]; for (NSString *key in aDictionary.allKeys) { id value = aDictionary[key]; /*判断当前属性是不是Model*/ objc_property_t property = class_getProperty(self, key.UTF8String); unsigned int outCount = 0; objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount); objc_property_attribute_t attribute = attributeList[0]; NSString *typeString = [NSString stringWithUTF8String:attribute.value]; if ([typeString isEqualToString:@"@"Student""]) { value = [self objectWithKeyValues:value]; } //生成setter方法,并用objc_msgSend调用 NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]]; SEL setter = sel_registerName(methodName.UTF8String); if ([objc respondsToSelector:setter]) {  (id,SEL,id)) objc_msgSend) (objc,setter,value); } free(attributeList); } return objc;}

这段代码里面有一处判断typeString的,这里判断是防止model嵌套,比如说Student里面还有一层Student,那么这里就需要再次转换一次,当然这里有几层就需要转换几次。

几个出名的开源库JSONModel、MJExtension等都是通过这种方式实现的(利用runtime的class_copyIvarList获取属性数组,遍历模型对象的所有成员属性,根据属性名找到字典中key值进行赋值,当然这种方法只能解决NSString、NSNumber等,如果含有NSArray或NSDictionary,还要进行第二步转换,如果是字典数组,需要遍历数组中的字典,利用objectWithDict方法将字典转化为模型,在将模型放到数组中,最后把这个模型数组赋值给之前的字典数组)

2.模型转字典

这里是上一部分字典转模型的逆步骤:

1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性。2.调用 property_getName 获取属性名称。3.根据属性名称生成 getter 方法。4.使用 objc_msgSend 调用 getter 方法获取属性值

//模型转字典-(NSDictionary *)keyValuesWithObject{ unsigned int outCount = 0; objc_property_t *propertyList = class_copyPropertyList([self class], &outCount); NSMutableDictionary *dict = [NSMutableDictionary dictionary]; for (int i = 0; i < outCount; i ++) { objc_property_t property = propertyList[i]; //生成getter方法,并用objc_msgSend调用 const char *propertyName = property_getName; SEL getter = sel_registerName(propertyName); if ([self respondsToSelector:getter]) { id value =   objc_msgSend) (self,getter); /*判断当前属性是不是Model*/ if ([value isKindOfClass:[self class]] && value) { value = [value keyValuesWithObject]; } if  { NSString *key = [NSString stringWithUTF8String:propertyName]; [dict setObject:value forKey:key]; } } } free(propertyList); return dict;}

中间注释那里的判断也是防止model嵌套,如果model里面还有一层model,那么model转字典的时候还需要再次转换,同样,有几层就需要转换几次。

不过上述的做法是假设字典里面不再包含二级字典,如果还包含数组,数组里面再包含字典,那还需要多级转换。这里有一个关于字典里面包含数组的demo.

图片 16

看了上面八大点之后,是不是感觉Runtime很神奇,可以迅速解决很多问题,然而,Runtime就像一把瑞士小刀,如果使用得当,它会有效地解决问题。但使用不当,将带来很多麻烦。在stackoverflow上有人已经提出这样一个问题:What are the Dangers of Method Swizzling in Objective C?,它的危险性主要体现以下几个方面:

  • Method swizzling is not atomic

Method swizzling不是原子性操作。如果在+load方法里面写,是没有问题的,但是如果写在+initialize方法中就会出现一些奇怪的问题。

  • Changes behavior of un-owned code

如果你在一个类中重写一个方法,并且不调用super方法,你可能会导致一些问题出现。在大多数情况下,super方法是期望被调用的。如果你使用同样的思想来进行Swizzling,可能就会引起很多问题。如果你不调用原始的方法实现,那么你Swizzling改变的太多了,而导致整个程序变得不安全。

  • Possible naming conflicts

命名冲突是程序开发中经常遇到的一个问题。我们经常在类别中的前缀类名称和方法名称。不幸的是,命名冲突是在我们程序中的像一种瘟疫。一般我们会这样写Method Swizzling

@interface NSView : NSObject- setFrame:frame;@end@implementation NSView (MyViewAdditions)- my_setFrame:frame { // do custom work [self my_setFrame:frame];}+ load { [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];}@end

这样写看上去是没有问题的。但是如果在整个大型程序中还有另外一处定义了my_setFrame:方法呢?那又会造成命名冲突的问题。我们应该把上面的Swizzling改成以下这种样子:

@implementation NSView (MyViewAdditions)static void MySetFrame(id self, SEL _cmd, NSRect frame);static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);static void MySetFrame(id self, SEL _cmd, NSRect frame) { // do custom work SetFrameIMP(self, _cmd, frame);}+ load { [self swizzle:@selector(setFrame:) with:MySetFrame store:&SetFrameIMP];}@end

虽然上面的代码看上去不是OC(因为使用了函数指针),但是这种做法确实有效的防止了命名冲突的问题。原则上来说,其实上述做法更加符合标准化的Swizzling。这种做法可能和人们使用方法不同,但是这种做法更好。Swizzling Method 标准定义应该是如下的样子:

typedef IMP *IMPPointer;BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) { IMP imp = NULL; Method method = class_getInstanceMethod(class, original); if  { const char *type = method_getTypeEncoding; imp = class_replaceMethod(class, original, replacement, type); if  { imp = method_getImplementation; } } if (imp && store) { *store = imp; } return (imp != NULL);}@implementation NSObject (FRRuntimeAdditions)+ swizzle:original with:replacement store:(IMPPointer)store { return class_swizzleMethodAndStore(self, original, replacement, store);}@end
  • Swizzling changes the method's arguments

这一点是这些问题中最大的一个。标准的Method Swizzling是不会改变方法参数的。使用Swizzling中,会改变传递给原来的一个函数实现的参数,例如:

[self my_setFrame:frame];

会变转换成

objc_msgSend(self, @selector(my_setFrame:), frame);

objc_msgSend会去查找my_setFrame对应的IMP。一旦IMP找到,会把相同的参数传递进去。这里会找到最原始的setFrame:方法,调用执行它。但是这里的_cmd参数并不是setFrame:,现在是my_setFrame:。原始的方法就被一个它不期待的接收参数调用了。这样并不好。

这里有一个简单的解决办法,上一条里面所说的,用函数指针去实现。参数就不会变了。

  • The order of swizzles matters

调用顺序对于Swizzling来说,很重要。假设setFrame:方法仅仅被定义在NSView类里面。

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

当NSButton被swizzled之后会发生什么呢?大多数的swizzling应该保证不会替换setFrame:方法。因为一旦改了这个方法,会影响下面所有的View。所以它会去拉取实例方法。NSButton会使用已经存在的方法去重新定义setFrame:方法。以至于改变了IMP实现不会影响所有的View。相同的事情也会发生在对NSControl进行swizzling的时候,同样,IMP也是定义在NSView类里面,把NSControl 和 NSButton这上下两行swizzle顺序替换,结果也是相同的。

当调用NSButton的setFrame:方法,会去调用swizzled method,然后会跳入NSView类里面定义的setFrame:方法。NSControl 和 NSView对应的swizzled method不会被调用。

NSButton 和 NSControl各自调用各自的 swizzling方法,相互不会影响。

但是我们改变一下调用顺序,把NSView放在第一位调用。

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

一旦这里的NSView先进行了swizzling了以后,情况就和上面大不相同了。NSControl的swizzling会去拉取NSView替换后的方法。相应的,NSControl在NSButton前面,NSButton也会去拉取到NSControl替换后的方法。这样就十分混乱了。但是顺序就是这样排列的。我们开发中如何能保证不出现这种混乱呢?

再者,在load方法中加载swizzle。如果仅仅是在已经加载完成的class中做了swizzle,那么这样做是安全的。load方法能保证父类会在其任何子类加载方法之前,加载相应的方法。这就保证了我们调用顺序的正确性。

  • Difficult to understand (looks recursive)

看着传统定义的swizzled method,我认为很难去预测会发生什么。但是对比上面标准的swizzling,还是很容易明白。这一点已经被解决了。

  • Difficult to debug

在调试中,会出现奇怪的堆栈调用信息,尤其是swizzled的命名很混乱,一切方法调用都是混乱的。对比标准的swizzled方式,你会在堆栈中看到清晰的命名方法。swizzling还有一个比较难调试的一点, 在于你很难记住当前确切的哪个方法已经被swizzling了。

在代码里面写好文档注释,即使你认为这段代码只有你一个人会看。遵循这个方式去实践,你的代码都会没问题。它的调试也没有多线程的调试困难。

经过在“神经病院”3天的修炼之后,对OC 的Runtime理解更深了。

关于黑魔法Method swizzling,我个人觉得如果使用得当,还是很安全的。一个简单而安全的措施是你仅仅只在load方法中去swizzle。和编程中很多事情一样,不了解它的时候会很危险可怕,但是一旦明白了它的原理之后,使用它又会变得非常正确高效。

对于多人开发,尤其是改动过Runtime的地方,文档记录一定要完整。如果某人不知道某个方法被Swizzling了,出现问题调试起来,十分蛋疼。

如果是SDK开发,某些Swizzling会改变全局的一些方法的时候,一定要在文档里面标注清楚,否则使用SDK的人不知道,出现各种奇怪的问题,又要被坑好久。

在合理使用 + 文档完整齐全 的情况下,解决特定问题,使用Runtime还是非常简洁安全的。

日常可能用的比较多的Runtime函数可能就是下面这些

//获取cls类对象所有成员ivar结构体Ivar *class_copyIvarList(Class cls, unsigned int *outCount)//获取cls类对象name对应的实例方法结构体Method class_getInstanceMethod(Class cls, SEL name)//获取cls类对象name对应类方法结构体Method class_getClassMethod(Class cls, SEL name)//获取cls类对象name对应方法imp实现IMP class_getMethodImplementation(Class cls, SEL name)//测试cls对应的实例是否响应sel对应的方法BOOL class_respondsToSelector(Class cls, SEL sel)//获取cls对应方法列表Method *class_copyMethodList(Class cls, unsigned int *outCount)//测试cls是否遵守protocol协议BOOL class_conformsToProtocol(Class cls, Protocol *protocol)//为cls类对象添加新方法BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)//替换cls类对象中name对应方法的实现IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)//为cls添加新成员BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)//为cls添加新属性BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)//获取m对应的选择器SEL method_getName//获取m对应的方法实现的imp指针IMP method_getImplementation//获取m方法的对应编码const char *method_getTypeEncoding//获取m方法参数的个数unsigned int method_getNumberOfArguments//copy方法返回值类型char *method_copyReturnType//获取m方法index索引参数的类型char *method_copyArgumentType(Method m, unsigned int index)//获取m方法返回值类型void method_getReturnType(Method m, char *dst, size_t dst_len)//获取方法的参数类型void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)//设置m方法的具体实现指针IMP method_setImplementation(Method m, IMP imp)//交换m1,m2方法对应具体实现的函数指针void method_exchangeImplementations(Method m1, Method m2)//获取v的名称const char *ivar_getName//获取v的类型编码const char *ivar_getTypeEncoding//设置object对象关联的对象void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)//获取object关联的对象id objc_getAssociatedObject(id object, const void *key)//移除object关联的对象void objc_removeAssociatedObjects(id object)

这些API看上去不好记,其实使用的时候不难,关于方法操作的,一般都是method开头,关于类的,一般都是class开头的,其他的基本都是objc开头的,剩下的就看代码补全的提示,看方法名基本就能找到想要的方法了。当然很熟悉的话,可以直接打出指定方法,也不会依赖代码补全。

还有一些关于协议相关的API以及其他一些不常用,但是也可能用到的,就需要查看Objective-C Runtime官方API文档,这个官方文档里面详细说明,平时不懂的多看看文档。

最后请大家多多指教。

Ps.这篇干货有点多,简书提示文章字数快到上限了,还好都写完了。顺利出院了!

图片 17

版权声明:本文由大奖888-www.88pt88.com-大奖888官网登录发布于大奖888官网登录,转载请注明出处:假如想在分拣中,那样三个类能够响应本人接二