iOS Runtime的实际应用

iOS中使用Runtime的实例应用例子解析

Posted by 齐滇大圣 on May 15, 2016

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

交换方法

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

那什么情况下会用到呢? 比如我给某个类的方法增加了一些实现,如给ViewControllerviewWillappear里添加一些实现,如果我所有的类都去添加一遍是不是很麻烦。或者我使用继承,在基类里面写一遍,然后所有类继承基类,那样耦合性是不是太强了。这时我们就可以用到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];
    }
    

查看dealloc是否调用

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];
}

FDFullscreenPopGesture的实现

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

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

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

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

具体的代码可以参看FDFullscreenPopGesture。

UIButton按钮重复点击问题

这种方式也是利用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_getAssociatedObjectobjc_setAssociatedObject这两个方法给在属性的存取方法settergetter里实现属性的设置了。

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

得到项目中所有的类

比如模块化里按照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实现字典转模型

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,只是做了很多其他的工作,我这里只是写了一个最简单的使用runtimemodel的例子。

Runtime中的Category

我们在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;
    }
};
  • name是指class_name而不是category_name
  • cls是要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
  • instanceMethods表示实例方法列表
  • classMethods表示类方法列表
  • protocols表示实现的所有协议的列表
  • instanceProperties表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的

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