[iOS] runtime 的使用场景--实战篇

作者: pingpong_龘 | 来源:发表于2016-03-30 21:40 被阅读4031次
    1453882015855158.png

    runtime的文档网上多的一塌糊涂,细节原理就不讲了
    这里只是简单整理下,平时可能会用到的一些场景

    申明:
    大部分代码来自:
    文/明仔Su(简书作者): 原文链接
    在此感谢

    1.背景知识

    哪里涉及到runtime

    来源:iOS内功篇:runtime (文/明仔Su)

    1.1 OC的方法调用流程

    下面以实例对象调用方法[blackDog walk]为例描述方法调用的流程:

    1、编译器会把`[blackDog walk]`转化为`objc_msgSend(blackDog,SEL)`,SEL为@selector(walk)。
    
    2、Runtime会在blackDog对象所对应的Dog类的方法缓存列表里查找方法的SEL
    
    3、如果没有找到,则在Dog类的方法分发表查找方法的SEL。(类由对象isa指针指向,方法分发表即methodList)
    
    4、如果没有找到,则在其父类(设Dog类的父类为Animal类)的方法分发表里查找方法的SEL(父类由类的superClass指向)
    
    5、如果没有找到,则沿继承体系继续下去,最终到达NSObject类。
    
    6、如果在234的其中一步中找到,则定位了方法实现的入口,执行具体实现
    
    7、如果最后还是没有找到,会面临两种情况:``(1) 如果是使用`[blackDog walk]`的方式调用方法````(2) 使用`[blackDog performSelector:@selector(walk)]`的方式调用方法`
    
    
    

    1.2 消息转发流程

    1、动态方法解析
    接收到未知消息时(假设blackDog的walk方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)
    
    2、备用接收者
    如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。
    如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。
    适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。
    比如:blackDog让女朋友whiteDog来接收这个消息
    
    3、完整消息转发
    在- (void)forwardInvocation:(NSInvocation *)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。
    比如:blackDog将消息完整转发給主人dogOwner来处理
    
    
    1444814548720164.png

    2.成员变量和属性

    ''你知道成员变量的本质是什么吗?"
    答案在这里:OS runtime实战应用:成员变量和属性

    2.1 json->model

    原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
    核心方法:在NSObject的分类中添加方法

    - (instancetype)initWithDict:(NSDictionary *)dict {
    
        if (self = [self init]) {
            //(1)获取类的属性及属性对应的类型
            NSMutableArray * keys = [NSMutableArray array];
            NSMutableArray * attributes = [NSMutableArray array];
            /*
             * 例子
             * name = value3 attribute = T@"NSString",C,N,V_value3
             * name = value4 attribute = T^i,N,V_value4
             */
            unsigned int outCount;
            objc_property_t * properties = class_copyPropertyList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                objc_property_t property = properties[i];
                //通过property_getName函数获得属性的名字
                NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
                [keys addObject:propertyName];
                //通过property_getAttributes函数可以获得属性的名字和@encode编码
                NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
                [attributes addObject:propertyAttribute];
            }
            //立即释放properties指向的内存
            free(properties);
    
            //(2)根据类型给属性赋值
            for (NSString * key in keys) {
                if ([dict valueForKey:key] == nil) continue;
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
        return self;
    
    }
    

    2.2 一键序列化

    原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
    核心方法:在Model的基类中重写方法:

    - (id)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            unsigned int outCount;
            Ivar * ivars = class_copyIvarList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                Ivar ivar = ivars[i];
                NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
            }
        }
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [aCoder encodeObject:[self valueForKey:key] forKey:key];
        }
    }
    

    2.3 访问私有变量

    我们知道,OC中没有真正意义上的私有变量和方法,要让成员变量私有,要放在m文件中声明,不对外暴露。如果我们知道这个成员变量的名称,可以通过runtime获取成员变量,再通过getIvar来获取它的值。方法:

    Ivar ivar = class_getInstanceVariable([Model class], "_str1");
    NSString * str1 = object_getIvar(model, ivar);
    

    3. 关联对象

    如何給NSArray添加一个属性(不能使用继承)
    答案在这里:iOS runtime实战应用:关联对象
    polen:
    OC的分类允许给分类添加属性,但不会自动生成getter、setter方法
    所以常规的仅仅添加之后,调用的话会crash

    runtime如何关联对象

    //关联对象
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    //获取关联的对象
    id objc_getAssociatedObject(id object, const void *key)
    //移除关联的对象
    void objc_removeAssociatedObjects(id object)
    

    应用,如何关联:

    - (void)setCustomTabbar:(UIView *)customTabbar {
        //这里使用方法的指针地址作为唯一的key
        objc_setAssociatedObject(self, @selector(customTabbar), customTabbar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (UIView *)customTabbar {
        return objc_getAssociatedObject(self, @selector(customTabbar));
    }
    

    4.Method Swizzling

    method Swizzling原理
    每个类都维护一个方法(Method)列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系

    + (void)load {
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            Class selfClass = object_getClass([self class]);
    
            SEL oriSEL = @selector(imageNamed:);
            Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);
    
            SEL cusSEL = @selector(myImageNamed:);
            Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);
    
            BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
            if (addSucc) {
                class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
            }else {
                method_exchangeImplementations(oriMethod, cusMethod);
            }
    
        });
    }
    

    自己使用过例子:
    使用场景,在iOS7中如果viewdidappear还没有完成,就立刻执行push或者pop操作会crash,见我之前写过的一篇文章iOS7中的pop导致的crash

    解决方案就是利用method swizzing, 将系统的viewdidappear替换为自己重写的sofaViewDidAppear

    @implementation UIViewController (APSafeTransition)
    
    + (void)load
    {
        Method m1;
        Method m2;
    
        m1 = class_getInstanceMethod(self, @selector(sofaViewDidAppear:));
        m2 = class_getInstanceMethod(self, @selector(viewDidAppear:));
        method_exchangeImplementations(m1, m2);
    }
    
    

    |

    结束:
    runtime这种消息机制,并非iOS特有,其他系统下也有类似的东西,比如windows下的MFC等等. 这里是cocoachina下所有和runtime相关的文章,请点击查看

    相关文章

      网友评论

      本文标题:[iOS] runtime 的使用场景--实战篇

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