美文网首页iOS经验总结
iOS:RunTime基本使用与实际运用

iOS:RunTime基本使用与实际运用

作者: 笑笑菜鸟 | 来源:发表于2021-05-12 10:07 被阅读0次

    一、RunTime是什么?

    定义:RunTime实际上是一个库,这个库使我们可以在程序运行时动态的创建对象、检查对象,修改类和对象的方法。他的作用其实就是在程序运行时做一些事情。

    下面我们来看看它的常用方法,前提引入头文件\color{red}{import <objc/message.h>} #,再去\color{red}{【XCode】->【Build Settings】 -> 【Enable Strict Checking of objc_msgSend Calls】}这个字段设置为 NO(默认为YES),防止编译器校验语法。

    1:方法交换

    Person *person = [[Person alloc] init];
    Method m1 = class_getInstanceMethod(person.class, @selector(eat));
    Method m2 = class_getInstanceMethod(person.class, @selector(run));
    method_exchangeImplementations(m1, m2);
    

    2:动态添加方法

    注意使用performSelector: 来调用,因为对象在编译阶段是没有addMethodTest方法的。防止编译不过

    //方法一:
    Person *p = [[Person alloc]init];
    IMP imp = class_getMethodImplementation(self.class, @selector(test));
    Boolean success = class_addMethod(p.class, @selector(addMethodTest), imp, "v@");
    [p performSelector:@selector(addMethodTest)];
    
    //方法二:
    Person *p = [[Person alloc]init];
    void(^testImp)(void) = ^{
         NSLog(@"***q*");
    };
    IMP imp = imp_implementationWithBlock(testImp);
    Boolean success = class_addMethod(p.class, @selector(addMethodTest), imp, "v@");
    [p performSelector:@selector(addMethodTest)];
    

    3:动态添加属性

    - (void)setName:(NSString *)name {
        objc_setAssociatedObject(self, "key_name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    - (NSString *)name {
        return (NSString *)objc_getAssociatedObject(self, "key_name");
    }
    

    4:遍历属性

    注意:通过objc_setAssociatedObject 添加的属性,是不能在这里发现的

    Person *person = [[Person alloc]init];
    unsigned count =0;
    Ivar * ivars = class_copyIvarList(person.class, &count);
    for (int i =0; i < count; i ++) {
         const char *s = ivar_getName(ivars[i]);
         NSString * property = [NSString stringWithCString:s encoding:NSUTF8StringEncoding];
         NSLog(@" property = %@", property);
    }
    

    二、它有什么作用

    1、程序crash检测

    当对象调用没有实现的方法,即在程序运行时,Runtime 会监测程序发生的意外,如果有crash发生,出于对用户的体验考虑,runtime允许在即将crash前,让开发者做一些事情去挽救APP,避免crash发生。其中有三个阶段可以处理crash。
    实际开发中没有人傻到会去调用没有实现的方法,如果有当我没说[滑稽]!,但是当项目大了之后,解析数据,版本字段变更,升级后缓存未清理,都有可能发生 unrecognized selector sent to…错误。
    这时可以就可以考虑以下解决方案了!

    方案A:Method resolution-方法解析,解决

    注意:添加方法时:class_addMethod中,实例方法是[self class],类方法是objc_getMetaClass(“ViewController”), 因为类方法最终查找方法实现是去对象的元类对象中查找,所以添加也是去元类对象中添加

    - (void)viewDidLoad {
        [super viewDidLoad];
        //类方法
        [ViewController performSelector:@selector(noMethod)];
        //实例方法
        [self performSelector:@selector(noMethod)];
    }
    - (void)test {
        NSLog(@"拦截到 %@ crash",NSStringFromSelector(_cmd));
    }
    
    //实例方法
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if ([NSStringFromSelector(sel) isEqualToString:@"noMethod"]) {
            IMP imp = class_getMethodImplementation([self class], @selector(test));
            class_addMethod([self class], sel, imp, "v@");
        }
        return [super resolveInstanceMethod:sel];
    }
    
    //类方法
    + (BOOL)resolveClassMethod:(SEL)sel {
        if ([NSStringFromSelector(sel) isEqualToString:@"noMethod"]) {
            IMP imp = class_getMethodImplementation([ViewController class], @selector(test));
            class_addMethod(objc_getMetaClass("ViewController"), sel, imp, "v@");
        }
        return [super resolveClassMethod:sel];
    }
    

    方案B:Fast forwarding-消息快速转发

    这一步比较简单,就是找一个能实现该方法的对象,让这个对象去实现该方法;

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if ([[Person new] respondsToSelector:aSelector]) {
            return [Person new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    方案C:Forwarding-消息慢转发

    当然这里的慢是相对于方案B的,不是说真的就是很慢。

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([super methodSignatureForSelector:aSelector] == nil) {
            NSMethodSignature *methodSign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
            return methodSign;
        } else {
            return [super methodSignatureForSelector:aSelector];
        }
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;
        if ([[Person new] respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:[Person new]];
        } else {
            [super forwardInvocation:anInvocation];
        }
    }
    

    注意:以上三个步骤,效率上 A > B >C,由于对象会缓存方法,如果方案A实现了,addMethod,那么下次调用该方法时,会直接从对象的method cache 里面直接调用,效率更高。A,B,C都实现了,只会执行A!

    2:实现NSDictionary 转 Model

    就是利用runtime 遍历model实例对象的属性,然后通过KVC给这个属性赋值。\color{red}{[instance setValue:@"value" forKey:property];}

    3:Category中添加属性

    这里需要注意:添加的属性不是真正这个属性。而是关联对象。
    真正添加属性如下:

    Class cat_cls = objc_allocateClassPair([NSObject class], "OBCat", 0);
            
    class_addIvar(cat_cls, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString*));
    objc_registerClassPair(cat_cls);
            
    id ob_cat = [[cat_cls alloc] init];
    [ob_cat setValue:@"tom" forKey:@"name"];
            
    

    添加属性需要在实例化之后,注册之前添加,即在\color{red}{objc_allocateClassPair}方法和\color{objc_registerClassPair}之间添加。如果在$\color{red}{objc_registerClassPair}之后添加无效。还可能崩溃。这里涉及到OC中类的成员变量的偏移量, 如果在类注册之后再addIvar的话会破坏原来类成员变量正确的偏移量, 这样的话会导致你访问的那个成员变量并不是你想访问的成员变量

    4:通用埋点

    利用runtime的方法交换,监听ViewController的停留时长,还能做到无侵入式。Hook生命周期的方法,然后交换方法实现,然后在自定义的方法中,调用外部方法,并 invoke 原来生命周期的方法。

    详情请点击埋点专题

    5:KVO

    以上说的是\color{red}{method-swizzling},而KVO则是\color{red}{isa-swizzling}的一种运用;
    kvo底层也是通过runtime来实现的,当某个对象设置kvo时,runtime会在运行时,动态的创建该类的子类,并重写set方法,当set方法调用时,会通知观测者;

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        // key为name,开启手动通知
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }
    

    如果\color{red}{automaticallyNotifiesObserversForKey}返回的是NO;那么需要手动调用\color{red}{willChangeValueForKey},或者\color{red}{didChangeValueForKey}触发通知观测者

    //重写set方法;
    - (void)setName:(NSString *)name {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
    

    kvo底层实现 :isa-swizzling

    先创建一个子类,重写其set方法,然后修改对象的isa指针,指向子类

    // "addObserver:forKeyPath:options:context:"方法的实现 
    // 动态创建子类
    NSString *newName = [@"NSKVONotifying_" stringByAppendingString:NSStringFromClass(object_getClass(self))]; 
    NSString *setterName = [[@"set" stringByAppendingString:[[[keyPath uppercaseString] substringToIndex:1] stringByAppendingString:[keyPath substringFromIndex:1]]] stringByAppendingString:@":"];
    Class subCls = objc_allocateClassPair(object_getClass(self), [newName UTF8String], 0);
    class_addMethod(subCls, NSSelectorFromString(setterName), (IMP)DefaultSetterForKVO, "v@:@");
    objc_registerClassPair(subCls);
    /// 替换子类的isa
    object_setClass(self, subCls);
    

    此文我是搬运工😀,参考原文链接:请点击此处

    相关文章

      网友评论

        本文标题:iOS:RunTime基本使用与实际运用

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