美文网首页重拾iOS
重拾iOS-Runtime

重拾iOS-Runtime

作者: Jack1105 | 来源:发表于2020-06-10 23:11 被阅读0次

    关键词:
    运行时isasuperclass消息传递消息转发meta-class

    面试题:

    1. 什么是Runtime?
    2. 方法的本质是什么?
    3. 实际开发中有哪些地方用到了Runtime?

    一. Runtime介绍

    Runtime顾名思义,运行时;
    OC是一种动态性比较强的语言,它是对C语言进行了扩展,并集成了smalltalk的动态语言特性,能够让程序在运行时做更多的事情。
    Runtime是由C和C++、汇编实现的一套API,是OC面向对象和动态机制的基石;

    二. Runtime消息机制

    在Runtime机制中,方法的本质是消息发送objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);
    receive为消息的接收者,sel为方法名;

    消息发送主要会经过「消息传递」和「消息转发」两大阶段;
    其中,「消息转发」阶段有:「动态方法解析」、「备用接受者」和「完整的消息转发」三个过程;

    1. 消息传递

    流程大致为:
    1)通过对象的isa指针,找到对应的类或者元类;
    2)在类中的method_list中查找,如果找到调用方法实现,没找到则通过superClass在其父类中查找,就这样一直向上查找,如果都没找到就进入消息转发阶段;
    3)但这种实现有个问题,设想一下,加入每次我调用这个方法的时候,都这么找的话,效率其实会很低。
    4)所以呢,就引出了另外一个重要的成员:objc_cache
    5)objc_cache会第一次在方法找到的时候,以方法名为key,方法的实现为value进行一个缓存。等下一次在调用的时候呢,会先在缓存中查找,这样一来效率不就提高了么。

    具体的流程如下如:


    2. 消息转发

    消息转发会经历三个过程:
    1)首先是「动态方法解析」,Objective-C运行时会调用 resolveInstanceMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
    2)然后是「备用接收者」,如果目标对象实现了forwardingTargetForSelector:,RunTime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
    3)最后是「完整的消息转发」,首先它会发送methodSignatureForSelector:消息获得函数的参数和返回值类型。如果methodSignatureForSelector:返回nil ,Runtime则会发出 doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 forwardInvocation:消息给目标对象。

    具体的流程如下如:


    三. Runtime运用场景

    1. 关联对象,给分类category添加属性

    #import <UIKit/UIKit.h>
    
    @interface UIImage (downLoadURL)
    @property (nonatomic, strong) NSString *downLoadURL;
    @end
    
    #import "UIImage+downLoadURL.h"
    #import <objc/runtime.h>
    @implementation UIImage (downLoadURL)
    
    -(void)setDownLoadURL:(NSString *)downLoadURL{
        objc_setAssociatedObject(self, @selector(downLoadURL), downLoadURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    -(NSString *)downLoadURL{
        return objc_getAssociatedObject(self, @selector(downLoadURL));
    }
    @end
    

    2. 方法替换

    #import <Foundation/Foundation.h>
    
    @interface NSObject (Swizzling)
    
    +(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector;
    @end
    
    #import "NSObject+Swizzling.h"
    #import <objc/runtime.h>
    @implementation NSObject (Swizzling)
    
    +(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                            bySwizzledSelector:(SEL)swizzledSelector{
       
        Class class = [self class];
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzleMethod = class_getInstanceMethod(class, swizzledSelector);
     
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else{
            method_exchangeImplementations(originalMethod, swizzleMethod);
        }
    }
    @end
    

    3. KVO

    KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

    4. 字典转模型

    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSNumber *age;
    
    - (instancetype)modelWithDict:(NSDictionary *)dict;
    
    @end
    
    #import "Person.h"
    #import <objc/runtime.h>
    
    @implementation Person
    
    - (instancetype)modelWithDict:(NSDictionary *)dict
    {
        if (self = [super init]) {
            // 获取类的属性及属性对应的类型
            NSMutableArray *keys = [NSMutableArray array];
            NSMutableArray *attributes = [NSMutableArray array];
    
            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);
    
            // 根据类型给属性赋值
            for (NSString *key in keys)
            {
                id value = [dict valueForKey:key];
                if (value == nil) {
                    continue;
                }
                [self setValue:value forKey:key];
            }
        }
        return self;
    }
    
    @end
    
    // 模型转换
    NSDictionary *dict = @{@"name":@"Jack",@"age":@(18)};
    Person *person = [[Person alloc] modelWithDict:dict];
    NSLog(@"name: %@", person.name);
    
    

    说明:这里的代码只是简单的提现一下思路,实际的转模型要复杂的多,比如对于数组、字典、自定义对象等类型的属性要如何处理等;

    5. 自动归档解档

    // 归档
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        if (self = [super init]) {
            Class c = self.class;
            // 截取类和父类的成员变量
            while (c && c != [NSObject class]) {
                unsigned int count = 0;
                Ivar *ivars = class_copyIvarList(c, &count);
                for (int i = 0; i < count; i++) {
                    NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
                    id value = [aDecoder decodeObjectForKey:key];
                    if (value){// 容错
                         [self setValue:value forKey:key];
                    }
                }
                // 获得c的父类
                c = [c superclass];
                free(ivars);
            }
        }
        return self;
    }
    // 解档
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        Class c = self.class;
        // 截取类和父类的成员变量
        while (c && c != [NSObject class]) {
            unsigned int count = 0;
            Ivar *ivars = class_copyIvarList(c, &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivars[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                id value = [self valueForKey:key];
                if (value){
                     [aCoder encodeObject:value forKey:key];
                 }
            }
            c = [c superclass];
            // 释放内存
            free(ivars);
        }
    }
    

    说明:也可写成宏定义;

    6. 利用消息转发机制解决方法找不到的异常问题

    四. Runtime相关API

    1. 类

    // 1)动态创建一个类(参数:父类,类名,额外的内存空间)
    Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
    
    // 2)注册一个类(要在类注册之前添加成员变量)
    void objc_registerClassPair(Class cls) 
    
    // 3)销毁一个类
    void objc_disposeClassPair(Class cls)
    
    // 4)获取isa指向的Class
    Class object_getClass(id obj)
    
    // 5)设置isa指向的Class
    Class object_setClass(id obj, Class cls)
    
    // 6)判断一个OC对象是否为Class
    BOOL object_isClass(id obj)
    
    // 7)判断一个Class是否为元类
    BOOL class_isMetaClass(Class cls)
    
    // 8)获取父类
    Class class_getSuperclass(Class cls)
    

    2. 成员变量

    // 1)获取一个实例变量信息
    Ivar class_getInstanceVariable(Class cls, const char *name)
    
    // 2)拷贝实例变量列表(最后需要调用free释放)
    Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
    
    // 3)设置和获取成员变量的值
    void object_setIvar(id obj, Ivar ivar, id value)
    id object_getIvar(id obj, Ivar ivar)
    
    // 4)动态添加成员变量(已经注册的类是不能动态添加成员变量的)
    BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
    
    // 5)获取成员变量的相关信息
    const char *ivar_getName(Ivar v)
    const char *ivar_getTypeEncoding(Ivar v)
    

    3. 属性

    // 1)获取一个属性
    objc_property_t class_getProperty(Class cls, const char *name)
    
    // 2)拷贝属性列表(最后需要调用free释放)
    objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    
    // 3)动态添加属性
    BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
    
    // 4)动态替换属性
    void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
    
    // 5)获取属性的一些信息
    const char *property_getName(objc_property_t property)
    const char *property_getAttributes(objc_property_t property)
    

    4. 方法

    // 1)获得一个实例方法、类方法
    Method class_getInstanceMethod(Class cls, SEL name)
    Method class_getClassMethod(Class cls, SEL name)
    
    // 2)方法实现相关操作
    IMP class_getMethodImplementation(Class cls, SEL name) 
    IMP method_setImplementation(Method m, IMP imp)
    void method_exchangeImplementations(Method m1, Method m2) 
    
    // 3)拷贝方法列表(最后需要调用free释放)
    Method *class_copyMethodList(Class cls, unsigned int *outCount)
    
    // 4)动态添加方法
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
    
    // 5)动态替换方法
    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
    
    // 6)获取方法的相关信息(带有copy的需要调用free去释放)
    SEL method_getName(Method m)
    IMP method_getImplementation(Method m)
    const char *method_getTypeEncoding(Method m)
    unsigned int method_getNumberOfArguments(Method m)
    char *method_copyReturnType(Method m)
    char *method_copyArgumentType(Method m, unsigned int index)
    
    // 7)选择器相关
    const char *sel_getName(SEL sel)
    SEL sel_registerName(const char *str)
    
    // 8)用block作为方法实现
    IMP imp_implementationWithBlock(id block)
    id imp_getBlock(IMP anImp)
    BOOL imp_removeBlock(IMP anImp)
    

    五. Runtime相关面试题

    1. objc中向一个nil对象发送消息将会发生什么?

    如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会发生崩溃。
    详解:
    1)如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
    2)如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
    3)如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
    4)如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

    2. 什么时候会报unrecognized selector的异常?

    objc在向一个对象发送消息时:
    1)首先会经过「消息传递」阶段,会先在对象所在类/元类中的cache中查找方法,如未找到则在method_list中查找,还未找到则根据superclass指针在父类中重复查找;
    2)如果「消息传递」阶段仍未找到,则进入「消息转发阶段」,消息转发有三大阶段;

    • 动态方法解析;
      在这个阶段允许开发者动态的添加方法实现,此时系统会重新启动一次消息发送;
    • 备用接收者;
      在这个阶段允许开发者将方法的实现传递给其他对象;
    • 完整的消息转发;
      在这个阶段允许开发者提供一个函数签名invocation来实现方法;

    如果在以上阶段都没找到或实现消息发送的方法,最终会发送崩溃,报unrecognized selector异常;

    3. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

    不能向编译后得到的类中增加实例变量;
    能向运行时创建的类中添加实例变量;
    1)因为编译后的类已经注册在Runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,同时Runtime会调用class_setvarlayoutclass_setWeaklvarLayout来处理strong weak引用.所以不能向存在的类中添加实例变量;
    2)运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上;

    4. isKindOfClassisMemberOfClass的区别

    isKindOfClass来确定一个对象是否是一个类的成员,或者是派生自该类的成员。
    isMemberOfClass只能确定一个对象是否是当前类的成员。

    isKindOfClass的实现代码大致如下:

    -(BOOL)isKindOfClass:(Class)aClass {
        for (Class tcls = isa; tcls; tcls-> superclass) {
            if (tcls == aClass) {
                return YES;
            }
        }
        return NO;
    }
    
    

    isMemberOfClass的实现代码大致如下:

    -(BOOL)isMemberOfClass:(Class)aClass {
        return isa == aClass;
    }
    

    5. isasuperclass的关系

    Runtime.png

    6. 以下代码的输出结果是什么,为什么?

    @interface Person : NSObject
    @end
    
    @implementation Person
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
            BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
            BOOL res3 = [(id)[Person class] isKindOfClass:[Person class]];
            BOOL res4 = [(id)[Person class] isMemberOfClass:[Person class]];
            NSLog(@"%d %d %d %d", res1, res2, res3, res4);
        }
        return 0;
    }
    

    输出:1000

    分析:
    (id)[NSObject class](id)[Person class]获取的都是对应的类对象,而不是实例对象;

    7. 以下代码能不能正常运行,为什么?

    @interface NSObject (Test) 
    + (void)test; 
    - (void)test; 
    @end 
    
    @implementation NSObject (Test) 
    - (void)test { 
        NSLog(@"%s", __func__);
    } 
    @end
    // 测试代码
    [[NSObject new] performSelector:@selector(test)];
    [NSObject test];
    

    结果:全都正常输出,编译和运行都没有问题;

    分析如下:
    [[NSObject new] performSelector:@selector(test)]能正常输出没什么好说的;
    主要是为什么[NSObject test]也能正常输出呢?
    首先,[NSObject test]方法调用时,会根据NSObject类对象的isa指针找到其元类对象,开始查找test的方法实现;
    显然,在NSObject元类中没有test的方法实现,则会根据superclass指针,开始向上查找。
    恰巧的是,我们这里的代码又是NSObject的category。

    superclass.jpg

    根据上图,我们可以看到NSObject元类的superclass指针,指向了NSObject类对象。
    所以,此时[NSObject test]便会调用NSObject中的test对象方法;

    8. 以下代码的输出结果是什么,为什么?

    NSArray *arr = [NSArray array];
    NSArray *mtArr = [NSMutableArray array];
     
    NSLog(@"%d", [arr isKindOfClass:[NSObject class]]);
    NSLog(@"%d", [arr isMemberOfClass:[NSObject class]]);
    NSLog(@"%d", [mtArr isKindOfClass:[NSObject class]]);
    NSLog(@"%d", [mtArr isMemberOfClass:[NSObject class]]);
        
    NSLog(@"%d", [arr isKindOfClass:[NSArray class]]);
    NSLog(@"%d", [arr isMemberOfClass:[NSArray class]]);
    NSLog(@"%d", [mtArr isKindOfClass:[NSArray class]]);
    NSLog(@"%d", [mtArr isMemberOfClass:[NSArray class]]);
        
    NSLog(@"%d", [mtArr isKindOfClass:[arr class]]);
    NSLog(@"%d", [mtArr isMemberOfClass:[arr class]]);
        
    

    输出:1010101000

    你可能会对以下输出产生疑问:

    NSLog(@"%d", [arr isMemberOfClass:[NSArray class]]); 
    // 输出0
    NSLog(@"%d", [mtArr isKindOfClass:[arr class]]);
    // 输出0
    

    原因在于NSArray是一个类簇,意味着每个NSArray的实例都是NSArray内部子类的一个实例,我们会发现[[NSArray array] class]返回值为__NSArray0,[[NSMutableArray array] class]返回值为__NSArrayM, 而[NSArray class]返回值为NSArray,所以二者不等。

    8. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

    无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。

    1)调用-release
    引用计数变为零 对象正在被销毁,生命周期即将结束.
    不能再有新的__weak弱引用,否则将指向nil.
    调用[self dealloc];
    2)父类调用-dealloc
    继承关系中最直接继承的父类再调用-dealloc
    如果是 MRC 代码 则会手动释放实例变量们(iVars)
    继承关系中每一层的父类 都再调用-dealloc
    3)NSObject 调用-dealloc
    只做一件事:调用 Objective-C runtime 中object_dispose()方法
    4)调用object_dispose()
    为 C++ 的实例变量们(iVars)
    调用destructors为 ARC 状态下的 实例变量们(iVars) 调用 -release
    解除所有使用 runtime Associate方法关联的对象
    解除所有__weak引用
    调用free()

    相关参考
    1. iOS Runtime详解
    2. iOS 开发:『Runtime』详解(一)基础知识

    相关文章

      网友评论

        本文标题:重拾iOS-Runtime

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