@qidiandasheng
2016-10-08T02:49:35.000000Z
字数 9340
阅读 3657
iOS运行时 博客 待更新文章
没有实际应用的知识讲解都是耍流氓
交换方法也就是
Method Swizzling,扩展原有类的方法,简单说就是原有类的方法不够用了,在原有方法上给他添加一些功能。有点类似于继承,但比继承更为强大一些。那什么情况下会用到呢?
比如我给某个类的方法增加了一些实现,如给ViewController的viewWillappear里添加一些实现,如果我所有的类都去添加一遍是不是很麻烦。或者我使用继承,在基类里面写一遍,然后所有类继承基类,那样耦合性是不是太强了。这时我们就可以用到runtime的动态交换方法了。
或者我们要修改某个类的私有方法,这时也可以用runtime找到这个私有方法,然后进行动态方法交换来实现我们需要增加的功能。
下面是一些Method Swizzling的实际应用例子。这里我们有几点需要注意一下(以第一个例子为例):
BOOL success = class_addMethod
这里在执行method_exchangeImplementations方法交换之前,进行了一次判断BOOL success = class_addMethod。我们为什么要这样做呢?
其主要原因就是如果直接通过method_exchangeImplementations来进行的话,可能原有类里并没有originalSelector所代表的方法,你直接进行了交换,这是我们不希望看到的。
因此通过addMethod来判断,如果加成功了,说明原先这个函数在原有类中并不存在,我们现在添加了,只要再把swizzleSelector指向原有函数即可;而如果没成功,说明这个函数在原有类中存在了,我们直接替换也不会有影响。
死循环问题,我们简单看一下下面的代码,好像是死循环了。但是不要担心,其实不会死循环。因为这里在调用[self deallocSwizzle]的时候其实函数已经被交换了,真正调用的其实是[self dealloc]
- (void)deallocSwizzle{NSLog(@"%@被销毁了", self);[self deallocSwizzle];}
ViewController Pop的时候查看dealloc是否调用
这是Method Swizzling的一种最简单应用,就是使用了分类,然后在load的时候进行dealloc函数的交换。如果Pop的时候没有输出NSLog(@"%@被销毁了", self);,说明dealloc未被执行,可能存在循环引用等bug导致ViewController不能释放。
#import "UIViewController+LifeCycle.h"#import <objc/runtime.h>@implementation UIViewController (LifeCycle)+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = NSSelectorFromString(@"dealloc");SEL swizzledSelector = @selector(deallocSwizzle);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if (success) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});}- (void)deallocSwizzle{NSLog(@"%@被销毁了", self);[self deallocSwizzle];}
这是孙源开源的一个库,这是一个全屏的右划返回机制的实现。使用了UINavigationController的分类和UIViewController的分类,实现了全局基本不用加一行代码的全屏右划返回实现。
这个库里面有两大块是使用了Method Swizzling这种方式的,也是最主要关键的代码。
UINavigationController (FDFullscreenPopGesture)这个分类里面实现了pushViewController:animated:这个函数的交换:
主要就是在push进下一页的时候,把系统的手势替换自己创建的手势。
而UIViewController (FDFullscreenPopGesturePrivate)这个分类里实现了viewWillAppear:这个函数的交换:
主要做的就是在viewWillAppear的时候设置NavigationBar的显示与否。
具体的代码可以参看FDFullscreenPopGesture。
这种方式也是利用runtime的方法交换,交换了Button的sendAction:to:forEvent:方法。在响应点击方法的时候,在里面判断一下上一次和这次的时间间隔,如果没超过时间间隔就直接return。如果超过了就直接执行原来的点击事件。
主要代码如下:
#import "UIButton+DoubleClick.h"#import <objc/runtime.h>// 默认的按钮点击时间static const NSTimeInterval defaultDuration = 0.5f;@implementation UIButton (DoubleClick)+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(sendAction:to:forEvent:);SEL swizzledSelector = @selector(ds_sendAction:to:forEvent:);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if (success) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});}- (void)ds_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{self.ds_acceptEventInterval = self.ds_acceptEventInterval == 0 ? defaultDuration : self.ds_acceptEventInterval;if (NSDate.date.timeIntervalSince1970 - self.ds_acceptedEventTime < self.ds_acceptEventInterval) return;if (self.ds_acceptEventInterval > 0){self.ds_acceptedEventTime = NSDate.date.timeIntervalSince1970;}[self ds_sendAction:action to:target forEvent:event];}
具体的例子我DSCategories这个分类的demo里有写。
分类里默认是不能添加属性的。这时我们就可以利用runtime的objc_getAssociatedObject和objc_setAssociatedObject这两个方法给在属性的存取方法setter和getter里实现属性的设置了。
上面交换方法的例子中的FDFullscreenPopGesture和UIButton重复点击问题都有给分类添加属性的操作。
比如模块化里按照module来跳转。每个模块的类实现一个协议,返回模块名(moduleName)。然后在项目启动时找出项目里所有的类并遵循对应协议的类放入cache中使用。
Class *classes;unsigned int outCount;classes = objc_copyClassList(&outCount);NSMutableDictionary *tmpCache = [NSMutableDictionary dictionary];for (unsigned int i = 0; i < outCount; i++) {Class cls = classes[i];if (class_conformsToProtocol(cls, @protocol(ModuleProtocol))) {NSString *moduleName = [cls moduleName];[tmpCache setObject:NSStringFromClass(cls) forKey:moduleName];}}free(classes);
在项目中我们常常会有这样的需求,在列表中点击不同的cell跳转到不同的ViewController。最普通的做法就是:服务端告诉你跳转的ViewController的类型,然后我们做switch判断调整到对应的ViewController。那这样我们是不是每次有新的控制器加进来,switch都需要多加一种类型呢,而且还需要重新发布,这样是不是太恶心了。
所以我们可以用runtime来实现万能跳转的方式,服务器传过来的数据可以如以下这样:
NSDictionary *userInfo = @{@"class": @"HSFeedsViewController",@"property": @{@"ID": @"123",@"type": @"12"}};
一个类名一个属性的字典。客户端可以根据类名生成所需要的对象,使用kvc给对象赋值。
跳转实现:
- (void)push:(NSDictionary *)params{// 类名NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];// 从一个字串返回一个类Class newClass = objc_getClass(className);if (!newClass){// 创建一个类Class superClass = [NSObject class];newClass = objc_allocateClassPair(superClass, className, 0);// 注册你创建的这个类objc_registerClassPair(newClass);}// 创建对象id instance = [[newClass alloc] init];// 对该对象赋值属性NSDictionary * propertys = params[@"property"];[propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {// 检测这个对象是否存在该属性if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {// 利用kvc赋值[instance setValue:obj forKey:key];}}];// 获取导航控制器UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];// 跳转到对应的控制器[pushClassStance pushViewController:instance animated:YES];}
检测对象是否存在该属性:
- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName{unsigned int outCount, i;// 获取对象里的属性列表objc_property_t * properties = class_copyPropertyList([instanceclass], &outCount);for (i = 0; i < outCount; i++) {objc_property_t property =properties[i];// 属性名转成字符串NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];// 判断该属性是否存在if ([propertyName isEqualToString:verifyPropertyName]) {free(properties);return YES;}}free(properties);return NO;}
runtime与KVC字典转模型的区别:
1.KVC:遍历字典中所有的key,去模型中查找有没有对应的属性名。
2.runtime:遍历模型中的属性名,去字典中查找。
最简单的一种字典转模型当然就是KVC了,当然效率不太高,而且限制也挺多的:
#import <Foundation/Foundation.h>@interface Student : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, copy) NSString *sex;@end//使用NSDictionary* dic = @{@"name":@"齐滇大圣",@"sex":@"男",};Student* model = [Student new];[model setValuesForKeysWithDictionary:dic];
runtime实现dictionary转model,主要就是用利用class_copyIvarList找出所有的属性,然后再去遍历字典,设置属性值。
@interface NSObject (Model)+ (instancetype)modelWithDict:(NSDictionary *)dict;@end#import "NSObject+Model.h"#import <objc/runtime.h>@implementation NSObject (Model)+ (instancetype)modelWithDict:(NSDictionary *)dict{// 创建对应类的对象id objc =[[self alloc] init];/**runtime:遍历模型中的属性名。去字典中查找。属性定义在类,类里面有个属性列表(即为数组)*/unsigned int count = 0;Ivar *ivarList = class_copyIvarList(self, &count);// 遍历for (int i = 0; i< count; i++){Ivar ivar = ivarList[i];// 获取成员名(获取到的是C语言类型,需要转换为OC字符串)NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];// 成员属性类型NSString *propertyType = [NSString stringWithUTF8String: ivar_getTypeEncoding(ivar)];// 首先获取key(根据你的propertyName 截取字符串)NSString *key = [propertyName substringFromIndex:1];// 获取字典的valueid value = dict[key];// 给模型的属性赋值// value : 字典的值// key : 属性名if (value){ // 这里是因为KVC赋值,不能为空[objc setValue:value forKey:key];}NSLog(@"%@ %@",propertyType,propertyName);}NSLog(@"%zd",count); // 这里会输出self中成员属性的总数free(ivarList); //释放return objc;}@end//使用NSDictionary* dic = @{@"name":@"齐滇大圣",@"sex":@"男",};Student* model = [Student modelWithDict:dic];
当然现在有比较不错的第三方库可以用在字典转模型上,比如YYModel。其实底层也是用了runtime,只是做了很多其他的工作,我这里只是写了一个最简单的使用runtime转model的例子。
我们在Runtime源码地址里下载最新的Runtime源码objc4-680.tar.gz。
然后我们在objc-runtime-new.h文件中看到如下定义:
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;method_list_t *methodsForMeta(bool isMeta) {if (isMeta) return classMethods;else return instanceMethods;}property_list_t *propertiesForMeta(bool isMeta) {if (isMeta) return nil; // classProperties;else return instanceProperties;}};
这里有一篇讲Category原理的文章可以看看深入理解Objective-C:Category。
我这里简单说一下整个过程:
编译的时候系统应该是把类对应的所有
category方法都找到并前序添加到method list中,也就是说后编译的category的方法在method list的最前面。比如先编译的category1的方法列表为d,后编译的方法列表为c。那么插入之后的方法列表将会是c,d。最后把这个分类的
method list前序添加到类的method list中,如果原来类的方法列表是a,b,Category的方法列表是c,d。那么插入之后的方法列表将会是c,d,a,b。所有说覆盖方法的优先级是:后编译的Category的方法>先编译的Category方法>类的方法。注意:+(void)load;方法的执行顺序是先类,然后是先编译的
Category,最后是后编译的Category。
在Runtime如何实现weak属性?中有详细解释如何实现的。我这里就讲一个简单的介绍。
其实原理就是在初始化一个
weak变量的时候,runtime会调用objc_initWeak函数,weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的weak对象,从而设置为 nil。
如何实现ARC中weak功能?这篇文章写了个demo用简单的代码模拟系统对weak的实现。