Runtime 总结

作者: 随风__陈坪__ | 来源:发表于2015-09-14 13:42 被阅读700次

    Runtime 定义

    1. 我们写的Objc代码在运行时都会被转化成 runtime 的C代码执行,比如:[obj doSomeThing] 会被转化成 (objc_msgSend(obj, @selector(doSomething))。

    2. OC中一切都被设计成对象,我们知道一个类被初始化一个实例,这个实例就是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。

    3. runtime 相关重要定义如下:

       // 描述类中的一个方法
       typedef struct objc_method *Method;
      
       // 实例变量
       typedef struct objc_ivar *Ivar;
      
       // 类别Category
       typedef struct objc_category *Category;
      
       // 类中声明的属性
       typedef struct objc_property *objc_property_t;
          
       //类在runtime中的表示
       struct objc_class {
       Class isa;//指针,顾名思义,表示是一个什么,
       //实例的isa指向类对象,类对象的isa指向元类
      
       #if !__OBJC2__
       Class super_class;  //指向父类
       const char *name;  //类名
       long version;     // 版本
       long info;       // 实例信息
       long instance_size // 实例大小
       struct objc_ivar_list *ivars //成员变量列表
       struct objc_method_list **methodLists; //方法列表
       struct objc_cache *cache;//缓存
       //一种优化,调用过的方法存入缓存列表,下次调用先找缓存
       struct objc_protocol_list *protocols //协议列表
       #endif
       } OBJC2_UNAVAILABLE;
       
       struct objc_method {  
           SEL method_name                             
           char *method_types                          
           IMP method_imp                                
       }
      

    你可以理解为 class 的范围最大,其中包含这 mehodimp 最小,包含在 method 中。

    runtime.png

    对于 SEL method IMP 的区别,从上面 objc_method 的结构体我们就可以看出,一个方法 Method,其实包含一个

    1. 方法名 SEL – 表示该方法的名称;
    2. 一个types – 表示该方法参数的类型;
    3. 一个 IMP – 指向该方法的具体实现的函数指针,说白了IMP就是实现方法。

    相关方法

    1. 添加方法 class_addMethod @note:如果类中不存在这个方法的实现,添加成功;存在这个方法的实现,添加不成功

    2. 替换方法 class_replaceMethod @note:如果以name标识的method不存在,就会添加这个method(就好像调用了class_addMethod);如果以name标识的method存在,替换imp

    3. 获取class中的某个method class_getInstanceMethod

    4. 获取method中的某个imp method_getImplementation or + (IMP)instanceMethodForSelector:(SEL)aSelector;

    5. 交换两个method中的imp method_exchangeImplementations

    6. 关联对象 objc_setAssociatedObject

    7. 获取对象 objc_getAssociatedObject

    8. 移除对象 objc_removeAssociatedObjects

    获取属性、方法、成员变量、协议列表

    + (void)load
    {
        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(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
        }
    
        //获取方法列表
        Method *methodList = class_copyMethodList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Method method = methodList[i];
            NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
        }
    
        //获取成员变量列表
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Ivar myIvar = ivarList[i];
            const char *ivarName = ivar_getName(myIvar);
            NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
        }
    
        //获取协议列表
        __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Protocol *myProtocal = protocolList[i];
            const char *protocolName = protocol_getName(myProtocal);
            NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
        }
    }
    

    PS: 调用这些获取列表的方法别忘记导入头文件 #import <objc/runtime.h>

    方法调用

    实例对象实例方法时,会到实例的 isa 指针指向的对象(类对象)操作

    调用类方法时,就会到类对象的 isa 指针指向的对象 (元对象) 操作

    调用的过程和步骤如下:

    1. 首先,在相应操作对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
    2. 如果没找到,就在相应操作对象中的方法列表中找到方法,如果找到,转向相应实现并执行。
    3. 如果没找到,去父类指针所指向的对象中执行 1、2操作。
    4. 以此类推,如果一直到根类还没有找到,转向拦截调用。
    5. 如果没有重写拦截调用的方法,程序会报错。

    以上的调用过程给我们带来的启发:

    1. 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
    2. 如果想调用已经重写过的方法的父类的实现,只需要使用 super这个编译器标识,它会在运行时跳过在当前的类对象中找方法的过程。

    拦截调用

    在方法调用中说到了,如果没有找到方法就会转向拦截调用。那么什么是拦截调用呢。拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。

    + (BOOL)resolveClassMethod:(SEL)sel;
    + (BOOL)resolveInstanceMethod:(SEL)sel;
    //后两个方法需要转发到其他的类处理
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    1. 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
    2. 第二个方法和第一个方法相似,只不过处理的是实例方法。
    3. 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
    4. 第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。

    动态添加方法

    重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?
    有一个办法是根据传进来的SEL类型的selector动态添加一个方法。

    首先从外部隐式调用一个不存在的方法:

    //隐式调用方法
    [target performSelector:@selector(resolveAdd:) withObject:@"test"];
    

    然后,在target对象内部重写拦截调用的方法,动态添加方法。

    void runAddMethod(id self, SEL _cmd, NSString *string){
        NSLog(@"add C IMP ", string);
    }
    + (BOOL)resolveInstanceMethod:(SEL)sel{
    
        //给本类动态添加一个方法
        if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
            class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
        }
        return YES;
    }   
    

    其中class_addMethod的四个参数分别是:

    1. Class cls 给哪个类添加方法,本例中是self
    2. SEL name 添加的方法,本例中是重写的拦截调用传进来的selector。
    3. IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
    4. "v@:*"方法的签名,代表有一个参数的方法。

    关联对象

    现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。这种情况的一般解决办法就是继承。但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。这个时候,runtime的关联属性就发挥它的作用了。

    //首先定义一个全局变量,用它的地址作为关联对象的key
    static char associatedObjectKey;
    //设置关联对象
    objc_setAssociatedObject(target, &associatedObjectKey, @"添加的字符串属性", OBJC_ASSOCIATION_RETAIN_NONATOMIC); //获取关联对象
    NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
    NSLog(@"AssociatedObject = %@", string);
    

    objc_setAssociatedObject 的四个参数:

    1. id object 给谁设置关联对象。

    2. const void *key 关联对象唯一的key,获取时会用到。

    3. id value 关联对象。

    4. objc_AssociationPolicy 关联策略,有以下几种策略:

       enum {
           OBJC_ASSOCIATION_ASSIGN = 0,
           OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
           OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
           OBJC_ASSOCIATION_RETAIN = 01401,
           OBJC_ASSOCIATION_COPY = 01403 
       };  
      

    熟悉OC的你,不难看出这几种策略的意思

    objc_getAssociatedObject 的两个参数。

    id object 获取谁的关联对象。

    const void *key 根据这个唯一的key获取关联对象。

    其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能的类的类别中,方便使用。

    NSObject+AssociatedObject.h

    #import <Foundation/Foundation.h>
    
    @interface NSObject (AssociatedObject)
    
    @property (nonatomic, strong) id associatedObject;
    
    @end    
    

    NSObject+AssociatedObject.m

    #import "NSObject+AssociatedObject.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (AssociatedObject)
    
    
    @dynamic associatedObject;
    
    - (void)setAssociatedObject:(id)object {
        
        // 设置关联对象
        objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id)associatedObject {
        
        // 得到关联对象
        return objc_getAssociatedObject(self, @selector(associatedObject));
    }
    
    @end
    

    方法交换

    方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
    话不多说,这是参考Mattt大神在NSHipster上的文章自己写的代码。

    #import "UIViewController+swizzling.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (swizzling)
    
    //load方法会在类第一次加载的时候被调用
    //调用的时间比较靠前,适合在这个方法里做方法交换
    + (void)load{
        //方法交换应该被保证,在程序中只会执行一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            //获得viewController的生命周期方法的selector
            SEL systemSel = @selector(viewWillAppear:);
            //自己实现的将要被交换的方法的selector
            SEL swizzSel = @selector(swiz_viewWillAppear:);
            //两个方法的Method
            Method systemMethod = class_getInstanceMethod([self class], systemSel);
            Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
    
            //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
            BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
            if (isAdd) {
                //如果成功,说明类中不存在这个方法的实现
                //将被交换方法的实现替换到这个并不存在的实现
                class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
            }else{
                //否则,交换两个方法的实现
                method_exchangeImplementations(systemMethod, swizzMethod);
            }
    
        });
    }
    
    - (void)swiz_viewWillAppear:(BOOL)animated{
        //这时候调用自己,看起来像是死循环
        //但是其实自己的实现已经被替换了
        [self swiz_viewWillAppear:animated];
        NSLog(@"swizzle");
    }
    
    @end    
    

    在一个自己定义的viewController中重写viewWillAppear

    - (void)viewWillAppear:(BOOL)animated{
        [super viewWillAppear:animated];
        NSLog(@"viewWillAppear");
    }
    

    动态创建对象

       // 1.动态创建对象 创建一个Person类 继承自NSObject类
        Class newClass = objc_allocateClassPair([NSObject class], "Student", 0);
        
        // 使用block作为方法的IMP
        IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {
            NSLog(@"Hello %@", _self);
        });
        
        // 为该类增加名为Report的方法
        class_addMethod(newClass, @selector(report), (IMP)myIMP, @"v@:");
        
        // 注册该类
        objc_registerClassPair(newClass);
        
        // 创建一个 Student 类的实例
        id instantOfNewClass = [[newClass alloc] init];
        
        // 调用方法
        [instantOfNewClass report];
    

    总结:

    1. 方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程。
    2. 既然是切面,就一定不要忘记,交换完再调回自己。
    3. 一定要保证只交换一次,否则就会很乱。
    4. 最后,据说这个技术很危险,谨慎使用。

    参考了:

    1. iOS~runtime理解
    2. Runtime 隐藏Status Bar背景

    相关文章

      网友评论

      • 93d451992f07:弱弱的问一下,用runtime怎么替换字符串, 例如如果字符串是@"A"的话,我就把他换成@"B"呢,
        系统是有个方法但是不想用那个,因为字符串@"A"可能会有很多!

      本文标题:Runtime 总结

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