美文网首页
Runtime 应用

Runtime 应用

作者: _既白_ | 来源:发表于2019-04-23 00:27 被阅读0次

    Method Swizzling

    OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。
    在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SELIMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP

    作者:刘小壮
    链接:https://www.jianshu.com/p/ff19c04b34d0
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    Method Swizzling

    页面统计的需求来说吧,我们先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于+ load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
    定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。

    
    #import "UIViewController+swizzling.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (swizzling)
    
    + (void)load {
        // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
        Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
        Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
        /**
         我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
         而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
         所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
         */
        if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
            method_exchangeImplementations(fromMethod, toMethod);
        }
    }
    
    // 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
    - (void)swizzlingViewDidLoad {
        NSString *str = [NSString stringWithFormat:@"%@", self.class];
        // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
        if(![str containsString:@"UI"]){
            NSLog(@"统计打点 : %@", self.class);
        }
        [self swizzlingViewDidLoad];
    }
    @end
    

    NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?
    这是因为Method SwizzlingNSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArrayobjectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
    所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。

    #import "NSArray+LXZArray.h"
    #import "objc/runtime.h"
    
    @implementation NSArray (LXZArray)
    
    + (void)load {
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
        method_exchangeImplementations(fromMethod, toMethod);
    }
    
    - (id)lxz_objectAtIndex:(NSUInteger)index {
        if (self.count-1 < index) {
            // 这里做一下异常处理,不然都不知道出错了。
            @try {
                return [self lxz_objectAtIndex:index];
            }
            @catch (NSException *exception) {
                // 在崩溃后会打印崩溃信息,方便我们调试。
                NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
                NSLog(@"%@", [exception callStackSymbols]);
                return nil;
        }
            @finally {}
        } else {
            return [self lxz_objectAtIndex:index];
        }
    }
    @end
    
    

    Method Swizzling 错误剖析

    在上面的例子中,如果只是单独对NSArrayNSMutableArray中的单个类进行Method Swizzling,是可以正常使用并且不会发生异常的。如果进行Method Swizzling的类中,有两个类有继承关系的,并且Swizzling了同一个方法。例如同时对NSArrayNSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致父类Swizzling`失效的问题。
    对于这种问题主要是两个原因导致的,

    • 首先是不要在+ (void)load 方法中调用[super load]方法,这会导致父类的Swizzling被重复执行两次,这样父类的Swizzling就会失效。例如下面的两张图片,你会发现由于NSMutableArray调用了[super load]导致父类NSArraySwizzling代码被执行了两次。
    #import "NSMutableArray+LXZArrayM.h"
    
    @implementation NSMutableArray (LXZArrayM)
    
    + (void)load {
        // 这里不应该调用super,会导致父类被重复Swizzling
        [super load];
        
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
        method_exchangeImplementations(fromMethod, toMethod);
    }
    

    这样就会导致程序运行过程中,子类调用Swizzling的方法是没有问题的,父类调用同一个方法就会发现Swizzling失效了.....具体原因我们后面讲!
    还有一个原因就是因为代码逻辑导致Swizzling代码被执行了多次,这也会导致Swizzling失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。

    问题原因

    我们上面提到过Method Swizzling的实现原理就是对类的Dispatch Table进行操作,每进行一次Swizzling就交换一次SELIMP(可以理解为函数指针),如果Swizzling被执行了多次,就相当于SELIMP被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了😄,这也是好多人说Method Swizzling不好用的原因之一。

    解决方案

    Swizzling的代码被重复执行,为了避免这样的原因出现,我们可以通过GCDdispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。

    Method Swizzling源码分析

    下面是Method Swizzling的实现源码,从源码来看,其实内部实现很简单。核心代码就是交换两个Method的imp函数指针,这也就是方法被swizzling多次,可能会被换回去的原因,因为每次调用都会执行一次交换操作。

    void method_exchangeImplementations(Method m1, Method m2)
    {
        if (!m1  ||  !m2) return;
    
        rwlock_writer_t lock(runtimeLock);
    
        IMP m1_imp = m1->imp;
        m1->imp = m2->imp;
        m2->imp = m1_imp;
    
        flushCaches(nil);
    
        updateCustomRR_AWZ(nil, m1);
        updateCustomRR_AWZ(nil, m2);
    }
    
    

    二、实现分类添加新属性

    我们在开发中常常使用类目Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。
    遗憾的是,OC的分类并不支持直接添加属性,如果我们直接在分类的声明中写入Property属性,那么只能为其生成set与get方法声明,却不能生成成员变量,直接调用这些属性还会造成崩溃。
    所以为了实现给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上,下面是相关的三个方法:

    /**
     1.给对象设置关联属性
     @param object 需要设置关联属性的对象,即给哪个对象关联属性
     @param key 关联属性对应的key,可通过key获取这个属性,
     @param value 给关联属性设置的值
     @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
     OBJC_ASSOCIATION_ASSIGN             @property(assign)。
     OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
     OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
     OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
     OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
     */
    void objc_setAssociatedObject(id _Nonnull object,
                                  const void * _Nonnull key,
                                  id _Nullable value,
                                  objc_AssociationPolicy policy)
    /**
     2.通过key获取关联的属性
     @param object 从哪个对象中获取关联属性
     @param key 关联属性对应的key
     @return 返回关联属性的值
     */
    id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                          const void * _Nonnull key)
    /**
     3.移除对象所关联的属性
     @param object 移除某个对象的所有关联属性
     */
    void objc_removeAssociatedObjects(id _Nonnull object)
    

    注意:key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key
    现在演示一个代码示例:为UIImage增加一个分类:UIImage+Tools,并为其设置关联属性urlString(图片网络链接属性),相关代码如下:

    //UIImage+Tools.h文件中
    UIImage+Tools.m
    @interface UIImage (Tools)
    //添加一个新属性:图片网络链接
    @property(nonatomic,copy)NSString *urlString;
    @end
    
    //UIImage+Tools.m文件中
    #import "UIImage+Tools.h"
    #import <objc/runtime.h>
    @implementation UIImage (Tools)
    //set方法
    - (void)setUrlString:(NSString *)urlString{
        objc_setAssociatedObject(self,
                                 @selector(urlString),
                                 urlString,
                                 OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    //get方法
    - (NSString *)urlString{
        return objc_getAssociatedObject(self,
                                        @selector(urlString));
    }
    //添加一个自定义方法,用于清除所有关联属性
    - (void)clearAssociatedObjcet{
        objc_removeAssociatedObjects(self);
    }
    @end
    

    测试文件中:

    UIImage *image = [[UIImage alloc] init];
    image.urlString = @"http://www.image.png";
    NSLog(@"获取关联属性:%@",image.urlString);
        
    [image clearAssociatedObjcet];
    NSLog(@"获取关联属性:%@",image.urlString);
    //打印:
    //获取关联属性:http://www.image.png
    // 获取关联属性:(null)
    

    三、获取类的详细信息

    1.获取属性列表

    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[I]);
        NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
    }
    free(propertyList);
    

    2.获取所有成员变量

    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (int i= 0; i<count; i++) {
        Ivar ivar = ivarList[I];
        const char *ivarName = ivar_getName(ivar);
        NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
    }
    free(ivarList);
    

    3.获取所有方法

    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i = 0; i<count; i++) {
        Method method = methodList[I];
        SEL mthodName = method_getName(method);
        NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
    }
    free(methodList);
    

    4.获取当前遵循的所有协议

    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (int i=0; i<count; i++) {
        Protocol *protocal = protocolList[I];
        const char *protocolName = protocol_getName(protocal);
        NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
    }
    free(propertyList);
    

    五、方法动态解析与消息转发

    1.动态方法解析:动态添加方法
    Runtime足够强大,能够让我们在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:
    场景1:动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
    场景2:利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法。方法动态解析主要用到的方法如下:

    //OC方法:
    //类方法未找到时调起,可于此添加类方法实现
    + (BOOL)resolveClassMethod:(SEL)sel
    
    //实例方法未找到时调起,可于此添加实例方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel
    
    //Runtime方法:
    /**
     运行时方法:向指定类中添加特定方法实现的操作
     @param cls 被添加方法的类
     @param name selector方法名
     @param imp 指向实现方法的函数指针
     @param types imp函数实现的返回值与参数类型
     @return 添加方法是否成功
     */
    BOOL class_addMethod(Class _Nullable cls,
                         SEL _Nonnull name,
                         IMP _Nonnull imp,
                         const char * _Nullable types)
    
    
    

    2.解决方法无响应崩溃问题

    执行OC方法其实就是一个发送消息的过程,若方法未实现,我们可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:

    除了上述的方法动态解析,还使用到的相关方法如下:

    消息接收者重定向
    //重定向类方法的消息接收者,返回一个类
    - (id)forwardingTargetForSelector:(SEL)aSelector
    
    //重定向实例方法的消息接受者,返回一个实例对象
    - (id)forwardingTargetForSelector:(SEL)aSelector
    
    消息重定向
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
    

    六、动态操作属性

    1.动态修改属性变量
    现在假设这样一个情况:我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName,这样的操作我们就可以使用Runtime可以动态修改对象属性。
    基本思路:首先使用Runtime获取Peson对象的所有属性,找到nickName,然后使用ivar的方法修改其值。具体的代码示例如下:

    Person *ps = [[Person alloc] init];
    NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); //null
    //第一步:遍历对象的所有属性
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([ps class], &count);
    for (int i= 0; i<count; i++) {
        //第二步:获取每个属性名
        Ivar ivar = ivarList[I];
        const char *ivarName = ivar_getName(ivar);
        NSString *propertyName = [NSString stringWithUTF8String:ivarName];
        if ([propertyName isEqualToString:@"_nickName"]) {
            //第三步:匹配到对应的属性,然后修改;注意属性带有下划线
            object_setIvar(ps, ivar, @"梧雨北辰");
        }
    } 
    NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); //梧雨北辰
    

    总结:此过程类似KVC的取值和赋值

    2.实现NSCoding的自动归档和解档
    归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。
    归档操作主要涉及两个方法:encodeObjectdecodeObjectForKey,现在,我们可以利用Runtime来改进它们,关键的代码示例如下:

    //原理:使用Runtime动态获取所有属性
    //解档操作
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        self = [super init];
        if (self) {
            unsigned int count = 0;
            
            Ivar *ivarList = class_copyIvarList([self class], &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivarList[I];
                const char *ivarName = ivar_getName(ivar);
                NSString *key = [NSString stringWithUTF8String:ivarName];
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
            free(ivarList); //释放指针
        }
        return self;
    }
    
    //归档操作
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        unsigned int count = 0;
        
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (NSInteger i = 0; i < count; i++) {
            Ivar ivar = ivarList[I];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivarList); //释放指针
    }
    

    下面是有关归档的测试代码:

    //--测试归档
    Person *ps = [[Person alloc] init];
    ps.name = @"梧雨北辰";
    ps.age  = 18;
    NSString *temp = NSTemporaryDirectory();
    NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
    [NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];
    
    //--测试解档
    NSString *temp = NSTemporaryDirectory();
    NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
    Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
    NSLog(@"person-name:%@,person-age:%ld",person.name,person.age); 
    //person-name:梧雨北辰,person-age:18
    
    

    3.实现字典与模型的转换

    字典数据转模型的操作在项目开发中很常见,通常我们会选择第三方如YYModel;其实我们也可以自己来实现这一功能,主要的思路有两种:KVC、Runtime,总结字典转化模型过程中需要解决的问题如下:


    现在,我们使用Runtime来实现字典转模型的操作,大致的思路是这样:
    借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。

    
    

    相关文章

      网友评论

          本文标题:Runtime 应用

          本文链接:https://www.haomeiwen.com/subject/oiypgqtx.html