@qidiandasheng
2016-10-08T10:49:35.000000Z
字数 9340
阅读 3292
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([instance
class], &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];
// 获取字典的value
id 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的实现。