[关闭]
@qidiandasheng 2016-10-08T10:49:35.000000Z 字数 9340 阅读 3268

iOS Runtime的实际应用

iOS运行时 博客 待更新文章


没有实际应用的知识讲解都是耍流氓

交换方法

交换方法也就是Method Swizzling,扩展原有类的方法,简单说就是原有类的方法不够用了,在原有方法上给他添加一些功能。有点类似于继承,但比继承更为强大一些。

那什么情况下会用到呢?
比如我给某个类的方法增加了一些实现,如给ViewControllerviewWillappear里添加一些实现,如果我所有的类都去添加一遍是不是很麻烦。或者我使用继承,在基类里面写一遍,然后所有类继承基类,那样耦合性是不是太强了。这时我们就可以用到runtime的动态交换方法了。
或者我们要修改某个类的私有方法,这时也可以用runtime找到这个私有方法,然后进行动态方法交换来实现我们需要增加的功能。

下面是一些Method Swizzling的实际应用例子。这里我们有几点需要注意一下(以第一个例子为例):

  1. - (void)deallocSwizzle
  2. {
  3. NSLog(@"%@被销毁了", self);
  4. [self deallocSwizzle];
  5. }

查看dealloc是否调用

ViewController Pop的时候查看dealloc是否调用

这是Method Swizzling的一种最简单应用,就是使用了分类,然后在load的时候进行dealloc函数的交换。如果Pop的时候没有输出NSLog(@"%@被销毁了", self);,说明dealloc未被执行,可能存在循环引用等bug导致ViewController不能释放。

  1. #import "UIViewController+LifeCycle.h"
  2. #import <objc/runtime.h>
  3. @implementation UIViewController (LifeCycle)
  4. + (void)load
  5. {
  6. static dispatch_once_t onceToken;
  7. dispatch_once(&onceToken, ^{
  8. Class class = [self class];
  9. SEL originalSelector = NSSelectorFromString(@"dealloc");
  10. SEL swizzledSelector = @selector(deallocSwizzle);
  11. Method originalMethod = class_getInstanceMethod(class, originalSelector);
  12. Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  13. BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
  14. if (success) {
  15. class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  16. } else {
  17. method_exchangeImplementations(originalMethod, swizzledMethod);
  18. }
  19. });
  20. }
  21. - (void)deallocSwizzle
  22. {
  23. NSLog(@"%@被销毁了", self);
  24. [self deallocSwizzle];
  25. }

FDFullscreenPopGesture的实现

这是孙源开源的一个库,这是一个全屏的右划返回机制的实现。使用了UINavigationController的分类和UIViewController的分类,实现了全局基本不用加一行代码的全屏右划返回实现。

这个库里面有两大块是使用了Method Swizzling这种方式的,也是最主要关键的代码。

UINavigationController (FDFullscreenPopGesture)这个分类里面实现了pushViewController:animated:这个函数的交换:
主要就是在push进下一页的时候,把系统的手势替换自己创建的手势。

UIViewController (FDFullscreenPopGesturePrivate)这个分类里实现了viewWillAppear:这个函数的交换:
主要做的就是在viewWillAppear的时候设置NavigationBar的显示与否。

具体的代码可以参看FDFullscreenPopGesture。

UIButton按钮重复点击问题

这种方式也是利用runtime的方法交换,交换了Button的sendAction:to:forEvent:方法。在响应点击方法的时候,在里面判断一下上一次和这次的时间间隔,如果没超过时间间隔就直接return。如果超过了就直接执行原来的点击事件。

主要代码如下:

  1. #import "UIButton+DoubleClick.h"
  2. #import <objc/runtime.h>
  3. // 默认的按钮点击时间
  4. static const NSTimeInterval defaultDuration = 0.5f;
  5. @implementation UIButton (DoubleClick)
  6. + (void)load
  7. {
  8. static dispatch_once_t onceToken;
  9. dispatch_once(&onceToken, ^{
  10. Class class = [self class];
  11. SEL originalSelector = @selector(sendAction:to:forEvent:);
  12. SEL swizzledSelector = @selector(ds_sendAction:to:forEvent:);
  13. Method originalMethod = class_getInstanceMethod(class, originalSelector);
  14. Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  15. BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
  16. if (success) {
  17. class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  18. } else {
  19. method_exchangeImplementations(originalMethod, swizzledMethod);
  20. }
  21. });
  22. }
  23. - (void)ds_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
  24. self.ds_acceptEventInterval = self.ds_acceptEventInterval == 0 ? defaultDuration : self.ds_acceptEventInterval;
  25. if (NSDate.date.timeIntervalSince1970 - self.ds_acceptedEventTime < self.ds_acceptEventInterval) return;
  26. if (self.ds_acceptEventInterval > 0)
  27. {
  28. self.ds_acceptedEventTime = NSDate.date.timeIntervalSince1970;
  29. }
  30. [self ds_sendAction:action to:target forEvent:event];
  31. }

具体的例子我DSCategories这个分类的demo里有写。

给分类添加属性

分类里默认是不能添加属性的。这时我们就可以利用runtime的objc_getAssociatedObjectobjc_setAssociatedObject这两个方法给在属性的存取方法settergetter里实现属性的设置了。

上面交换方法的例子中的FDFullscreenPopGestureUIButton重复点击问题都有给分类添加属性的操作。

得到项目中所有的类

比如模块化里按照module来跳转。每个模块的类实现一个协议,返回模块名(moduleName)。然后在项目启动时找出项目里所有的类并遵循对应协议的类放入cache中使用。

  1. Class *classes;
  2. unsigned int outCount;
  3. classes = objc_copyClassList(&outCount);
  4. NSMutableDictionary *tmpCache = [NSMutableDictionary dictionary];
  5. for (unsigned int i = 0; i < outCount; i++) {
  6. Class cls = classes[i];
  7. if (class_conformsToProtocol(cls, @protocol(ModuleProtocol))) {
  8. NSString *moduleName = [cls moduleName];
  9. [tmpCache setObject:NSStringFromClass(cls) forKey:moduleName];
  10. }
  11. }
  12. free(classes);

万能跳转的实现

在项目中我们常常会有这样的需求,在列表中点击不同的cell跳转到不同的ViewController。最普通的做法就是:服务端告诉你跳转的ViewController的类型,然后我们做switch判断调整到对应的ViewController。那这样我们是不是每次有新的控制器加进来,switch都需要多加一种类型呢,而且还需要重新发布,这样是不是太恶心了。

所以我们可以用runtime来实现万能跳转的方式,服务器传过来的数据可以如以下这样:

  1. NSDictionary *userInfo = @{
  2. @"class": @"HSFeedsViewController",
  3. @"property": @{
  4. @"ID": @"123",
  5. @"type": @"12"
  6. }
  7. };

一个类名一个属性的字典。客户端可以根据类名生成所需要的对象,使用kvc给对象赋值。

跳转实现:

  1. - (void)push:(NSDictionary *)params
  2. {
  3. // 类名
  4. NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
  5. const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
  6. // 从一个字串返回一个类
  7. Class newClass = objc_getClass(className);
  8. if (!newClass)
  9. {
  10. // 创建一个类
  11. Class superClass = [NSObject class];
  12. newClass = objc_allocateClassPair(superClass, className, 0);
  13. // 注册你创建的这个类
  14. objc_registerClassPair(newClass);
  15. }
  16. // 创建对象
  17. id instance = [[newClass alloc] init];
  18. // 对该对象赋值属性
  19. NSDictionary * propertys = params[@"property"];
  20. [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
  21. // 检测这个对象是否存在该属性
  22. if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
  23. // 利用kvc赋值
  24. [instance setValue:obj forKey:key];
  25. }
  26. }];
  27. // 获取导航控制器
  28. UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
  29. UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
  30. // 跳转到对应的控制器
  31. [pushClassStance pushViewController:instance animated:YES];
  32. }

检测对象是否存在该属性:

  1. - (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
  2. {
  3. unsigned int outCount, i;
  4. // 获取对象里的属性列表
  5. objc_property_t * properties = class_copyPropertyList([instance
  6. class], &outCount);
  7. for (i = 0; i < outCount; i++) {
  8. objc_property_t property =properties[i];
  9. // 属性名转成字符串
  10. NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
  11. // 判断该属性是否存在
  12. if ([propertyName isEqualToString:verifyPropertyName]) {
  13. free(properties);
  14. return YES;
  15. }
  16. }
  17. free(properties);
  18. return NO;
  19. }

具体使用和代码

利用runtime实现字典转模型

runtime与KVC字典转模型的区别:
1.KVC:遍历字典中所有的key,去模型中查找有没有对应的属性名。
2.runtime:遍历模型中的属性名,去字典中查找。

最简单的一种字典转模型当然就是KVC了,当然效率不太高,而且限制也挺多的:

  1. #import <Foundation/Foundation.h>
  2. @interface Student : NSObject
  3. @property (nonatomic, copy) NSString *name;
  4. @property (nonatomic, copy) NSString *sex;
  5. @end
  6. //使用
  7. NSDictionary* dic = @{
  8. @"name":@"齐滇大圣",
  9. @"sex":@"男",
  10. };
  11. Student* model = [Student new];
  12. [model setValuesForKeysWithDictionary:dic];

runtime实现dictionary转model,主要就是用利用class_copyIvarList找出所有的属性,然后再去遍历字典,设置属性值。

  1. @interface NSObject (Model)
  2. + (instancetype)modelWithDict:(NSDictionary *)dict;
  3. @end
  4. #import "NSObject+Model.h"
  5. #import <objc/runtime.h>
  6. @implementation NSObject (Model)
  7. + (instancetype)modelWithDict:(NSDictionary *)dict{
  8. // 创建对应类的对象
  9. id objc =[[self alloc] init];
  10. /**
  11. runtime:遍历模型中的属性名。去字典中查找。
  12. 属性定义在类,类里面有个属性列表(即为数组)
  13. */
  14. unsigned int count = 0;
  15. Ivar *ivarList = class_copyIvarList(self, &count);
  16. // 遍历
  17. for (int i = 0; i< count; i++){
  18. Ivar ivar = ivarList[i];
  19. // 获取成员名(获取到的是C语言类型,需要转换为OC字符串)
  20. NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
  21. // 成员属性类型
  22. NSString *propertyType = [NSString stringWithUTF8String: ivar_getTypeEncoding(ivar)];
  23. // 首先获取key(根据你的propertyName 截取字符串)
  24. NSString *key = [propertyName substringFromIndex:1];
  25. // 获取字典的value
  26. id value = dict[key];
  27. // 给模型的属性赋值
  28. // value : 字典的值
  29. // key : 属性名
  30. if (value){ // 这里是因为KVC赋值,不能为空
  31. [objc setValue:value forKey:key];
  32. }
  33. NSLog(@"%@ %@",propertyType,propertyName);
  34. }
  35. NSLog(@"%zd",count); // 这里会输出self中成员属性的总数
  36. free(ivarList); //释放
  37. return objc;
  38. }
  39. @end
  40. //使用
  41. NSDictionary* dic = @{
  42. @"name":@"齐滇大圣",
  43. @"sex":@"男",
  44. };
  45. Student* model = [Student modelWithDict:dic];

当然现在有比较不错的第三方库可以用在字典转模型上,比如YYModel。其实底层也是用了runtime,只是做了很多其他的工作,我这里只是写了一个最简单的使用runtimemodel的例子。

Runtime中的Category

我们在Runtime源码地址里下载最新的Runtime源码objc4-680.tar.gz
然后我们在objc-runtime-new.h文件中看到如下定义:

  1. struct category_t {
  2. const char *name;
  3. classref_t cls;
  4. struct method_list_t *instanceMethods;
  5. struct method_list_t *classMethods;
  6. struct protocol_list_t *protocols;
  7. struct property_list_t *instanceProperties;
  8. method_list_t *methodsForMeta(bool isMeta) {
  9. if (isMeta) return classMethods;
  10. else return instanceMethods;
  11. }
  12. property_list_t *propertiesForMeta(bool isMeta) {
  13. if (isMeta) return nil; // classProperties;
  14. else return instanceProperties;
  15. }
  16. };

这里有一篇讲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

Runtime如何实现weak属性?中有详细解释如何实现的。我这里就讲一个简单的介绍。

其实原理就是在初始化一个weak变量的时候,runtime会调用objc_initWeak函数,weak 对象会放入一个 hash 表中。 用 weak指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

如何实现ARC中weak功能?这篇文章写了个demo用简单的代码模拟系统对weak的实现。

参考

iOS 万能跳转界面方法

刨根问底Objective-C Runtime(1)- Self & Super

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注