重拾iOS

重拾iOS-Runtime

2020-06-10  本文已影响0人  Jack1105

关键词:
运行时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)如果「消息传递」阶段仍未找到,则进入「消息转发阶段」,消息转发有三大阶段;

如果在以上阶段都没找到或实现消息发送的方法,最终会发送崩溃,报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』详解(一)基础知识
上一篇下一篇

猜你喜欢

热点阅读