ios method swizzling
2016-06-18 16:17
411 查看
最近在整理项目逻辑的时候,发现一个问题:就是打点统计,经常和代码业务逻辑混在了一起,耦合性很强,并且经常容易出错。于是就在思考怎样对这一块进行优化。
其实,对这方面的讨论一直也比较多,比如继承基类,但是这样很容易使代码变得臃肿。另一个比较好的办法就是利用method swizzling, hook住需要打点的方法,将打点统计从业务逻辑中分离出来,而且额外工作量不大。最后就想从这方面去尝试,当然并没有自己造轮子,而是借用了github上的一个开源库,Aspects。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,比如你想统计某个controller的viewwillappear的调用次数,你只需要引入Aspect.h头文件,然后在合适的地方初始化如下代码即可。
看到这段代码大家应该有所感觉了,没错,它基本上就是基于method swizzling实现的。本篇文章暂时并不打算对aspects的代码进行解析(以后,可能会写一篇这样的文字),在这里就简单的记录一下我个人对于method swizzling的理解。
ios开发人员都知道,oc是一门动态语言。这个动态性怎么理解呢,知乎上有网友这么总结过:
1. 类和对象都是id, 在给你一个id的前提下无法直观的知道这个对象是类对象还是类本身. 简单的可以简化成runtime管理的都是id (id的本质其实是objc_object, objc_class头部其实就是id, 也就是isa).
2. Class在objc中是动态创建的, selector, method, imp, protocol等都是随后绑定上去的(即所谓的运行时绑定).
3. 通过runtime能够查出当前运行时环境中所有的类, 每个类中的方法, 每个类消息的绑定, 每个类的实现的协议, 每个协议的定义, 每个类当前的消息缓存等一切你想知道的东西.
4. 类的方法(消息)调用是间接的.
比较常用的地方就是你可以在运行时动态的改变函数调用的执行,可以给对象动态的添加函数,甚至动态生成一个全新的类。method swizzling就是利用这个动态性,在运行时改变了函数调用的指向,从而使函数最终调用到自己定义的方法中去,那么,这个过程是怎样实现的呢。
了解method swizzling之前有必要先了解一下oc函数的调用过程,这里先简单介绍几个概念:
1)oc的类是由Class类型来表示的,定义如下:
2)类的实例 也是一个结构体objc_object
这里的字段含义暂时不做过多的解释,有兴趣的同学可以去网上找找。这里介绍两个属性值:
1) isa:在oc中,类本身也被当成一个对象来处理。对于一个实例对象而言,isa指针指向了这个对象的类(上面的objc_class),而类的isa指针指向了它的元类(metaclass元类其实也是一种objc_class).,关于metaclass可以参考这里的分析
2)methodLists:方法列表,记录了所有的方法。(这里只是实例方法,类方法需要通过isa去元类中寻找)
另外oc中一个方法的调用有如下几个关键的部分:
1)sel又叫选择器,它代表了一个方法的selector的指针。selector用于表达运行时的方法的名字。
sel在一个类中是唯一的,而且是完全依赖方法名,也就是说下面两个函数
2)IMP:一个函数指针,指向了方法实现的首地址
3)Method:它是类定义中表示方法的一个结构体,如下
有了上面的铺垫,就能更好的说明函数调用的整个过程了, 在oc中函数的调用形式是[target ***],可以理解为[receiver message],也就是向receiver发送消息的过程。这个会被解析成如下形式objc_msgSend(receiver,selector,arg1,...),也就是告诉receiver,我要发消息给你selector对应的方法,arg1表示要传递给方法的参数。receiver收到这个通知后,会根据objc_object(这里先以实例方法为例)isa找到对象对应的class结构体,然后便利methodlist找到method,最后通过method找到对应的imp,然后imp最终执行消息。当然具体细节要比这复杂的多。(比如为了提高效率,会对method进行缓存等等)
基于上面的一些基本了解之后,我们来试试怎么用代码进行method swizzling实践。
还是要勾住uiviewcontroller的所有的viewwillappear方法,实现如下:
和上面所说的函数的调用过程对比就会发现其实是一样的,本质上就是在运行时,更改sel对应的imp的指向而已。有几点需要说明:
1)这个swizzling只更改本对象的方法的调用,并不会影响起父类,子类的调用情况。也就是在子类controller调用viewWillAppear还是正常的调用viewWillAppear,但是,当调用[super viewWillAppear:animated]的时候,会调用到上面的[self swizzling_viewwillAppear:animated].
2)细心的朋友或许会发现,上面swizzling_viewwillAppear的实现又调用了[self swizzling_viewwillAppear:animated],这样会不会形成循环调用了?其实不会,因为已经更改了@seletor(swizzling_viewwillAppear:)对应的imp,调用[self swizzling_viewwillAppear:animated],实际上相当于调用了[self viewWillAppear:animated],并不会形成循环调用。
其实,对这方面的讨论一直也比较多,比如继承基类,但是这样很容易使代码变得臃肿。另一个比较好的办法就是利用method swizzling, hook住需要打点的方法,将打点统计从业务逻辑中分离出来,而且额外工作量不大。最后就想从这方面去尝试,当然并没有自己造轮子,而是借用了github上的一个开源库,Aspects。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,比如你想统计某个controller的viewwillappear的调用次数,你只需要引入Aspect.h头文件,然后在合适的地方初始化如下代码即可。
#pragma mark - addKvLogAspect - (void)addKvLogAspect { //想法tab打开 [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { //统计打点 NSLog(@""); }error:NULL]; }
看到这段代码大家应该有所感觉了,没错,它基本上就是基于method swizzling实现的。本篇文章暂时并不打算对aspects的代码进行解析(以后,可能会写一篇这样的文字),在这里就简单的记录一下我个人对于method swizzling的理解。
ios开发人员都知道,oc是一门动态语言。这个动态性怎么理解呢,知乎上有网友这么总结过:
1. 类和对象都是id, 在给你一个id的前提下无法直观的知道这个对象是类对象还是类本身. 简单的可以简化成runtime管理的都是id (id的本质其实是objc_object, objc_class头部其实就是id, 也就是isa).
2. Class在objc中是动态创建的, selector, method, imp, protocol等都是随后绑定上去的(即所谓的运行时绑定).
3. 通过runtime能够查出当前运行时环境中所有的类, 每个类中的方法, 每个类消息的绑定, 每个类的实现的协议, 每个协议的定义, 每个类当前的消息缓存等一切你想知道的东西.
4. 类的方法(消息)调用是间接的.
比较常用的地方就是你可以在运行时动态的改变函数调用的执行,可以给对象动态的添加函数,甚至动态生成一个全新的类。method swizzling就是利用这个动态性,在运行时改变了函数调用的指向,从而使函数最终调用到自己定义的方法中去,那么,这个过程是怎样实现的呢。
了解method swizzling之前有必要先了解一下oc函数的调用过程,这里先简单介绍几个概念:
1)oc的类是由Class类型来表示的,定义如下:
typedef struct objc_class *Class;它其实是一个指向objc_class的指针,结构体如下:
truct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识 long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 #endif } OBJC2_UNAVAILABLE;
2)类的实例 也是一个结构体objc_object
struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; typedef struct objc_object *id;这里也就是我们说的id对象,oc里面所有的对象都能用id表示。
这里的字段含义暂时不做过多的解释,有兴趣的同学可以去网上找找。这里介绍两个属性值:
1) isa:在oc中,类本身也被当成一个对象来处理。对于一个实例对象而言,isa指针指向了这个对象的类(上面的objc_class),而类的isa指针指向了它的元类(metaclass元类其实也是一种objc_class).,关于metaclass可以参考这里的分析
2)methodLists:方法列表,记录了所有的方法。(这里只是实例方法,类方法需要通过isa去元类中寻找)
另外oc中一个方法的调用有如下几个关键的部分:
1)sel又叫选择器,它代表了一个方法的selector的指针。selector用于表达运行时的方法的名字。
SEL sel = @selector(method);
sel在一个类中是唯一的,而且是完全依赖方法名,也就是说下面两个函数
- (void)setDimension:(NSInteger)dimension { } - (void)setDimension:(float)dimension { }会提示Duplicate declaration错误,因为尽管它们有不同的参数类型,但是由于方法名完全相同会导致sel相同,违背了sel唯一性的原则,这也是oc语法和其他语法的不同。
2)IMP:一个函数指针,指向了方法实现的首地址
3)Method:它是类定义中表示方法的一个结构体,如下
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
有了上面的铺垫,就能更好的说明函数调用的整个过程了, 在oc中函数的调用形式是[target ***],可以理解为[receiver message],也就是向receiver发送消息的过程。这个会被解析成如下形式objc_msgSend(receiver,selector,arg1,...),也就是告诉receiver,我要发消息给你selector对应的方法,arg1表示要传递给方法的参数。receiver收到这个通知后,会根据objc_object(这里先以实例方法为例)isa找到对象对应的class结构体,然后便利methodlist找到method,最后通过method找到对应的imp,然后imp最终执行消息。当然具体细节要比这复杂的多。(比如为了提高效率,会对method进行缓存等等)
基于上面的一些基本了解之后,我们来试试怎么用代码进行method swizzling实践。
还是要勾住uiviewcontroller的所有的viewwillappear方法,实现如下:
#import "UIViewController+Swizzling.h" #import <objc/runtime.h> @implementation UIViewController (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; //get sel SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(swizzling_viewWillAppear:); //get method Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); /** * 这里其实是在加了一个保护,如果class_addMethod返回no,说明originalSelector已经有存在的实现了,这个时候,我们将 originalMethod,swizzledMethod直接替换掉就号了,如果还没有对应的实现,那么直接添加进去,并更改原来swizzledSelector对应的实现 */ //exchange imp 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 - (void)swizzling_viewWillAppear:(BOOL)animated { [self swizzling_viewWillAppear:animated]; NSString *classStr = [NSString stringWithFormat:@"%@", self.class]; NSLog(@"viewWillAppear: %@", classStr); } @end
和上面所说的函数的调用过程对比就会发现其实是一样的,本质上就是在运行时,更改sel对应的imp的指向而已。有几点需要说明:
1)这个swizzling只更改本对象的方法的调用,并不会影响起父类,子类的调用情况。也就是在子类controller调用viewWillAppear还是正常的调用viewWillAppear,但是,当调用[super viewWillAppear:animated]的时候,会调用到上面的[self swizzling_viewwillAppear:animated].
2)细心的朋友或许会发现,上面swizzling_viewwillAppear的实现又调用了[self swizzling_viewwillAppear:animated],这样会不会形成循环调用了?其实不会,因为已经更改了@seletor(swizzling_viewwillAppear:)对应的imp,调用[self swizzling_viewwillAppear:animated],实际上相当于调用了[self viewWillAppear:animated],并不会形成循环调用。
相关文章推荐
- 峰回路转,Firefox 浏览器即将重返 iOS 平台
- 峰回路转,Firefox 浏览器即将重返 iOS 平台
- 不可修补的 iOS 漏洞可能导致 iPhone 4s 到 iPhone X 永久越狱
- iOS 12.4 系统遭黑客破解,漏洞危及数百万用户
- 每日安全资讯:NSO,一家专业入侵 iPhone 的神秘公司
- [转][源代码]Comex公布JailbreakMe 3.0源代码
- 讲解iOS开发中基本的定位功能实现
- iOS中定位当前位置坐标及转换为火星坐标的方法
- js判断客户端是iOS还是Android等移动终端的方法
- iOS应用开发中AFNetworking库的常用HTTP操作方法小结
- iOS应用中UISearchDisplayController搜索效果的用法
- iOS App开发中的UISegmentedControl分段组件用法总结
- IOS开发环境windows化攻略
- iOS应用中UITableView左滑自定义选项及批量删除的实现
- iOS中UIAlertView警告框组件的使用教程
- 浅析iOS应用开发中线程间的通信与线程安全问题
- iOS中的UIKeyboard键盘视图使用方法小结
- 检测iOS设备是否越狱的方法
- .net平台推送ios消息的实现方法