美文网首页iOS面试
iOS面试题 - Runtime

iOS面试题 - Runtime

作者: Longshihua | 来源:发表于2019-05-16 17:52 被阅读11次

1、self和super

selfsuper是经常在面试中被问到的,而且有一段典型的代码如下:

@implementation Son
- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

执行结果都为Son

self表示当前这个类的对象,而super只是一个编译器标识符,和self指向同一个消息的接受者。上面代码中,无论是[self class]还是[super class],接受消息者都是Son对象,但是superself不同的是,当self调用class时,是在子类Son中查找对应的方法,而super调用class方法时,是在父类Father中查找方法。

当调用[self class]方法时,会自动转化为objc_msgSend函数进行发送消息,函数定义如下

id objc_msgSend(id self, SEL op, ...)

这时候先会从当前Son类查找,如果没有,就到Father类查找,如果还是没有就到NSObject类中进行查找。我们可以在NSObject.mm文件中查看class方法的实现:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

所以NSLog(@"%@", NSStringFromClass([self class]));方法会输出Son

当调用[super class],会转化为objc_msgSendSuper,函数定义如下:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

objc_msgSendSuper函数的第一个参数为super的数据类型时一个指向objc_super的结构体,从message.h文件得知:

struct objc_super {
    id receiver;
#if !defined(__cplusplus)  &&  !__OBJC2__
    Class class;  /* For compatibility with old objc-runtime.h header */
#else
    Class super_class;
#endif
    /* super_class is the first class to search */
};

结构体中包含两个成员,第一个是reciver,表示某个类的实例,第二个是super_class表示当前类的父类。这时会先构造出objc_super结构体,这个结构体的第一个成员是self,第二个成员是(id)class_getSuperclass(objc_getClass("Son")),实际上该函数会输出Father。然后在Father类查找class,查找不到,最后去NSObject中查找。此时,内部会执行objc_msgSend(objc_super->receiver, @selector(class)),其实于[self class]调用一样,所以结果还是Son.

参考

2、Runtime实现的机制是什么?

Runtime实现的机制是:运行时机制

Runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了Runtime的C语言代码, Runtime是OC的幕后工作者。

比如说,下面一个创建对象的方法中

[[Person alloc] init] 

runtime中为:

objc_msgSend(objc_msgSend("Person" , "alloc"), "init")

3、什么时候会报unrecognized selector的异常?

简单来说:

当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决。

消息转发机制原理

当发送消息的时候,我们会根据类里面的methodLists列表去查询我们要调用的SEL(selector),当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报unrecognized selector错误

具体流程

当向someObject发送某消息,但runtime system在当前类和父类中都找不到对应方法的实现时,runtime system并不会立即报错使程序崩溃,而是依次执行下列步骤:

20160107111521518.png

1、动态方法解析

objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)

2、快速消息转发:

如果目标对象实现了-forwardingTargetForSelector:Runtime这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续标准消息转发。 这里叫快速消息转,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点

3、标准消息转发:

这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

4、objc在向一个对象发送消息时,发生了什么?

根据对象的isa指针找到类对象id,在查询类对象里面的methodLists方法函数列表,如果没有找到,在沿着superClass,寻找父类,再在父类methodLists方法列表里面查询,最终找到SEL,根据idSEL确认IMP(指针函数),在发送消息;

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

Objective-C中向nil发送消息是完全有效的,只是在运行时不会有任何作用,因为在运行时调用时,objc_msgSend函数传过去的receivernil,而内部会判断receiver是否为nil,若为nil则什么也不干。同样,若cmd也就是selectornil,也是什么也不干。

  • 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:
Person * motherInlaw = [[Person spouse] mother];

如果 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。

  • 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。
  • 如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。
  • 如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。

具体原因如下:

objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

那么,为了方便理解这个内容,还是贴一个objc的源代码:

// runtime.h(类在runtime中的定义)

struct objc_class {
  Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
  #if !__OBJC2__
  Class super_class OBJC2_UNAVAILABLE; // 父类
  const char *name OBJC2_UNAVAILABLE; // 类名
  long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
  long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
  long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
  struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
  struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
  struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
  struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
  #endif
  } OBJC2_UNAVAILABLE;

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。

那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。

6、一个objc对象如何进行内存布局?(考虑有父类的情况)

1、所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
2、父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
3、每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息

  • 对象方法列表
  • 成员变量的列表
  • 属性列表

4、每个Objective-C对象都有相同的结构,如下图所示

image.png

5、根类对象就是NSObject,它的super class指针指向nil

6、类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类

4010043-8d1ec32b51e7fa8c.png

Objective-C的对象模型

7、利用Runtime实现一个对象的拷贝?

使用Runtime机制实现对象的拷贝有以下几个步骤:

  1. 导入<objc/runtime.h>头文件
  2. 遵守<NSCopying, NSMutableCopying>协议
  3. 实现mutableCopyWithZone:和copyWithZone:方法
  4. 获取类的属性列表
  5. 对属性列表进行遍历
  • 拿到属性
  • 拿到属性名
  • 通过属性名拿到属性值, 使用valueForKey:方法
  • 判断值对象是否响应协议方法,将值赋值给新对象的相应属性

6.释放属性指针

具体实现

Person.h文件

#import <Foundation/Foundation.h>

@interface Person : NSObject 

@property(nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSString *address;

@end

Person.m文件

#import "Person.h"
#import <objc/runtime.h>

@interface Person() <NSCopying, NSMutableCopying>

@end

@implementation Person

// 浅拷贝
-(id)copyWithZone:(NSZone * )zone{
   return self;
}

// 深拷贝
-(id)mutableCopyWithZone:(NSZone *)zone{
    id objcopy = [[[self class]allocWithZone:zone] init];
    // 1.获取属性列表
    unsigned int count = 0;
    objc_property_t* propertylist = class_copyPropertyList([self class], &count);

    for (int i = 0; i < count ; i++) {
        objc_property_t property = propertylist[i];
        // 2.获取属性名
        const char * propertyName = property_getName(property);
        NSString * key = [NSString stringWithUTF8String:propertyName];
        // 3.获取属性值
        id value = [self valueForKey:key];
        // 4.判断属性值对象是否遵守NSMutableCopying协议
        if ([value respondsToSelector:@selector(mutableCopyWithZone:)]) {
            // 5.设置对象属性值
            [objcopy setValue:[value mutableCopy] forKey:key];
        }else{
            [objcopy setValue:value forKey:key];
        }
    }
    // 需要手动释放
    free(propertylist);
    return objcopy;
}

@end

简单使用

Person *personA = [[Person alloc]init];
personA.name = @"Rose";
personA.address = @"china";
NSLog(@"personA: %@, name: %@, address: %@", personA, personA.name, personA.address);

Person *personB = [personA copy];
NSLog(@"personB: %@, name: %@, address: %@", personB, personB.name, personB.address);

Person *personC = [personA mutableCopy];
NSLog(@"personC: %@, name: %@, address: %@", personC, personC.name, personC.address);

运行结果

personA: <Person: 0x600003721e60>, name: Rose, address: china
personB: <Person: 0x600003721e60>, name: Rose, address: china
personC: <Person: 0x600003721da0>, name: Rose, address: china

8、Runtime动态添加属性、方法?

  • 使用关联对象(AssociatedObject)添加属性

NSObject类创建一个分类,在声明文件中声明一个name属性

// NSObject+Help.h
#import <Foundation/Foundation.h>

// 分类添加属性,这里为系统类NSObject添加属性
@interface NSObject (Help)

@property (nonatomic, copy) NSString *name;

@end

// NSObject+Help.m
#import "NSObject+Help.h"
#include <objc/runtime.h>

@implementation NSObject (Help)

const char *key = "key";

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, key);
}

@end

添加了name属性之后,就可以使用了

NSObject *object = [[NSObject alloc]init];
object.name = @"name";
NSLog(@"%@", object.name);
  • 使用class_addProperty添加属性

对于已经存在的类我们可以使用class_addProperty方法来添加属性。

定义一个Person类有nameaddress属性,接下来使用class_addPropertyPerson添加sex属性

#import <Foundation/Foundation.h>

@interface Person : NSObject 

@property(nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSString *address;

@end

ViewController实现添加属性

#import "Person.h"
#import <objc/runtime.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self addProperty];

}

id getter(id object, SEL _cmd){
    NSString *key = NSStringFromSelector(_cmd);
    return objc_getAssociatedObject(object, (__bridge const void * _Nonnull)(key));
}

void setter(id object, SEL _cmd, id newValue){
    NSString *key = NSStringFromSelector(_cmd);
    key = [[key substringWithRange:NSMakeRange(3, key.length-4)] lowercaseString];
    objc_setAssociatedObject(object, (__bridge const void * _Nonnull)(key), newValue, OBJC_ASSOCIATION_RETAIN);
}

- (void)addProperty {
    // 配置属性性质
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
    objc_property_attribute_t ownership = { "N", "" }; // N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [[NSString stringWithFormat:@"_%@", @"sex"] UTF8String] };  //variable name
    objc_property_attribute_t attrs[] = { type, ownership0, ownership,backingivar};//这个数组一定要按照此顺序才行

    // 添加属性
    BOOL added = class_addProperty([Person class], "sex", attrs, 4);
    if (added) {
        NSLog(@"添加成功\n");
    }else{
        NSLog(@"添加失败\n");
    }

    // 添加方法
    class_addMethod([Person class], NSSelectorFromString(@"sex"), (IMP)getter, "@@:");
    class_addMethod([Person class], NSSelectorFromString(@"setSex:"), (IMP)setter, "v@:@");

    // 获取数据
    unsigned int count;
    objc_property_t *properties =class_copyPropertyList([Person class], &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        NSLog(@"property: %s",property_getName(property));
    }


    Person *person = [Person new];
    person.name = @"what is your name?";
    [person setValue:@"男" forKey:@"sex"];
    NSLog(@"name: %@",person.name);
    NSLog(@"sex: %@",[person valueForKey:@"sex"]);
}

@end

运行结果

添加成功
property: sex
property: name
property: address
name: what is your name?
sex: 男
  • 使用class_addIvar添加属性

对于动态创建的类我们通过class_addIvar添加属性,它会改变一个已有类的内存布局,一般是通过objc_allocateClassPair动态创建一个class,才能调用class_addIvar创建Ivar,最后通过objc_registerClassPair注册class

- (void)viewDidLoad {
    [super viewDidLoad];

    //在运行时创建继承自NSObject的People类
    Class People = objc_allocateClassPair([NSObject class], "People", 0);

    //添加_name成员变量
    BOOL flag1 = class_addIvar(People, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
    if (flag1) {
        NSLog(@"NSString*类型,_name变量添加成功");
    }
    //添加_age成员变量
    BOOL flag2 = class_addIvar(People, "_age", sizeof(int), sizeof(int), @encode(int));
    if (flag2) {
        NSLog(@"int类型,_age变量添加成功");
    }

    //完成People类的创建
    objc_registerClassPair(People);
    unsigned int varCount;
    //拷贝People类中的成员变量列表
    Ivar * varList = class_copyIvarList(People, &varCount);
    for (int i = 0; i<varCount; i++) {
        NSLog(@"%s",ivar_getName(varList[i]));
    }
    //释放varList
    free(varList);

    //创建People对象p1
    id p1 = [[People alloc]init];
    //从类中获取成员变量Ivar
    Ivar nameIvar = class_getInstanceVariable(People, "_name");
    Ivar ageIvar = class_getInstanceVariable(People, "_age");
    //为p1的成员变量赋值
    object_setIvar(p1, nameIvar, @"张三");
    object_setIvar(p1, ageIvar, @33);

    //获取p1成员变量的值
    NSLog(@"%@",object_getIvar(p1, nameIvar));
    NSLog(@"%@",object_getIvar(p1, ageIvar));
}
  • 动态添加方法

说简单点就是在消息转发第一步,添加新的方法来处理未识别器的selector

#import "Person.h"
#import <objc/runtime.h>

@interface Person() 

@end

@implementation Person

// 任何一个函数都会有以下两个参数(默认方法都有两个隐式参数)
void addNewMethod(id self, SEL _cmd) {
    NSLog(@"ok : %@", NSStringFromSelector(_cmd));
}

void addNewMethodWithParameter(id self, SEL _cmd, id obj) {
    NSLog(@"parameter: %@", obj);
}

//当调用的对象方法不存在时会走下面的方法
//当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(addNewMethod)) {
        class_addMethod(self, sel, (IMP)addNewMethod, "v");
    } else if (sel == @selector(addNewMethodWithParameter:)) {
        class_addMethod(self, sel, (IMP)addNewMethodWithParameter, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}

使用performSelector来检测方法是否添加成功

- (void)addMethod {
    Person *person = [[Person alloc]init];
    [person performSelector:@selector(addNewMethod)];
    [person performSelector:@selector(addNewMethodWithParameter: ) withObject:@"object"];
}

运行结果

ok : addNewMethod
parameter: object

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

不能向编译后得到的类中增加实例变量

因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime会调用class_setIvarLayoutclass_setWeakIvarLayout来处理strongweak引用,所以不能向存在的类中添加实例变量;

能向运行时创建的类中添加实例变量

运行时创建的类是可以添加实例变量,调用class_addIvar函数。但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

10、Runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

1、每一个类对象中都有一个对象方法列表(对象方法缓存)
2、类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)
3、方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法,名称,通过这个方法名称就可以在方法列表中找到对应的方法实现
4、当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
5、当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找

在寻找IMP的地址时,runtime提供了两种方法

IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m)

而根据官方描述,第一种方法可能会更快一些

class_getMethodImplementation may be faster than method_getImplementation(class_getInstanceMethod(cls, name)).

对于第一种方法而言,类方法和实例方法实际上都是通过调用class_getMethodImplementation()来寻找IMP地址的,不同之处在于传入的第一个参数不同

类方法(假设有一个类A)

class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));

实例方法

class_getMethodImplementation([A class],@selector(methodName));

通过该传入的参数不同,找到不同的方法列表,方法列表中保存着下面方法的结构体,结构体中包含这方法的实现,selector本质就是方法的名称,通过该方法名称,即可在结构体中找到相应的实现。

struct objc_method {
    SEL method_namechar *method_typesIMP method_imp
}

而对于第二种方法而言,传入的参数只有method,区分类方法和实例方法在于封装method的函数

类方法

Method class_getClassMethod(Class cls, SEL name)

实例方法

Method class_getInstanceMethod(Class cls, SEL name)

最后调用IMP method_getImplementation(Method m)获取IMP地址

11、使用Runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

1、无论在MRC下还是ARC下均不需要
2、被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc调用的object_dispose()方法中释放
3、补充:对象的内存销毁时间表,分四个步骤

// 对象的内存销毁时间表
// 根据 WWDC 2011, Session 322 (36分22秒)中发布的内存销毁时间表 

 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()

对象的内存销毁时间表:参考链接

12、_objc_msgForward函数

_objc_msgForward是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

13、isa指针?

Objective - C中,类也是对象,所属元类。所以经常说: 万物皆对象

  • 对象的isa指针指向所属的类,从而可以找到对象上的方法
  • 类的isa指针指向了所属的元类
  • 元类的isa指向了根元类,根元类指向了自己。
1773988-f3b90f8a4d90e32b.jpg

iOS - 回顾Objective-C的对象模型

14、objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

[obj foo]; 在objc编译时,会被转意为:objc_msgSend(obj, @selector(foo));

具体了解一下

给对象发送消息可以这样写:

id returnValue = [someObject messageName:parameter];

someObject叫做接受者,messageName叫做选择子,选择子与参数合起来叫做消息。编译器看到此消息之后,将其转换为一条标准的c语言函数,叫做objc_msgSend。官方函数如下

/* Basic Messaging Primitives
 *
 * On some architectures, use objc_msgSend_stret for some struct return types.
 * On some architectures, use objc_msgSend_fpret for some float return types.
 * On some architectures, use objc_msgSend_fp2ret for some float return types.
 *
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */
#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

从上面代码可以看到,这是最基本的用于发送消息的函数。注意:objc_msgSend并不能够发送所有类型的消息,只能发送基本消息。上面注释部分也说到,在一些处理器上,使用objc_msgSend_stret来发送返回值类型为结构体的消息,使用objc_msgSend_fpret或者objc_msgSend_fp2ret来发送返回值为浮点类型的消息。

objc_msgSend其原型为

id objc_msgSend(id self, SEL op, ...)

参数

这是一个参数个数可变的函数,能够接收两个或者两个以上的参数.

第一个参数代表接受者,objc_msgSend第一个参数类型id,它是一个指向类实例的指针。typedef struct objc_object *id; objc_object原型为:

 struct objc_object { Class isa ; };

objc_object结构体包含一个isa指针,根据isa指针就可以找到对象所属的类.

第二个参数代表选择子(SEL是选择子的类型),它是selectorObjc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL:

typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你可以用Objc编译器命令@selector()或者 Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。

后续参数就是消息中的那些参数,其顺序不变

编译器会把刚才那个例子中的消息装换成为如下的函数 :

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

objc_msgSend函数会依据接受者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接受者所属的类中搜索其"方法列表",如果能够找到与选择子名称相符合的方法,就跳转至其实现代码。

若是找不到,那就沿着继承体系继续向上査找,等找到了合适的方法之后再调整.如果最终还是找不到相符合的方法,那么就执行"消息转发"。说来,想调用一个方法似乎需要很多步骤。但是幸运的是,objc_msgSend会将匹配的结果缓存到"快速映射表"里面,每个类都有这样一块缓存,若是后面还向该类发送与选择子相同的消息,那么执行起来就很快了

15、什么是方法混淆(method swizzling)?

Objective-C中的Method Swizzling是一项异常强大的技术,它可以允许我们动态地替换方法的实现,实现Hook功能,是一种比子类化更加灵活的“重写”方法的方式。

Method Swizzling 原理

Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

20160108094832765.png
  • 可以利用method_exchangeImplementations来交换2个方法中的IMP
  • 可以利用class_replaceMethod来修改类
  • 可以利用method_setImplementation来直接设置某个方法的IMP

归根结底,都是偷换了selectorIMP,如下图所示:

20160108094900893.png

简单例子

// .h 新方法可以添加至NSString的一个“分类”(category)中:
@interface NSString (EOCMyAdditions)

- (NSString*)eoc_myLowercaseString;

@end

//.m 新方法的实现代码可以这样写:
@impleinentation NSString (EOCMyAdditions)

+ (void)load {

  //方法实现则可通过下列函数获得:
  Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
  Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString)); 

  //交换方法实现
  method_exchangeImplementations(originalMethod, swappedMethod);
}

- (NSString*)eoc_myLowercaseString {
  NSString *lowercase = [self eoc_myLowercaseString];
  NSLog (@"%@ => %@", self, lowercase);
  return lowercase;
)
@end

这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。

实现的一个场景

跟踪程序每个ViewController展示给用户的次数,可以通过Method Swizzling替换ViewDidAppear初始方法。

创建一个UIViewController的分类,重写自定义的ViewDidAppear方法,并在其+load方法中实现ViewDidAppear方法的交换。

#import <UIKit/UIKit.h>

@interface UIViewController (Swizzling)

@end

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"

@implementation UIViewController (Swizzling)

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [UIViewController methodSwizzlingWithOriginalSelector:@selector(viewDidAppear:)
                                           bySwizzledSelector:@selector(my_ViewDidAppear:)];
    });
 }

-(void) my_ViewDidAppear:(BOOL)animated{
    [self my_ViewDidAppear:animated];
    NSLog(@"===== %@ viewDidAppear=====",[self class]);
}
@end

动态交换两个方法的实现(method swizzing)

+ (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);
        }

    });
}
  • Swizzling应该总在+load中执行。在OC中,Runtime会在类初始加载时调用+load方法,在类第一次被调用时实现+initialize方法。由于Method Swizzling会影响到类的全局状态,所以要尽量避免在并发处理中出现竞争情况。+load方法能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。

  • 要使用dispatch_once执行方法交换,方法交换要求线程安全,而且保证在任何情况下只能交换一次。

NSHipster

16、+(void)load; +(void)initialize;有什么用处?

  • 1、在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。
  • 2、首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类
  • 3、loadinitialize方法都应该实现的精简一些,这有助于保持应用程序的响应能力,也能减少引入 “保留环”的几率。
  • 4、无法在编译期设定的全局常量,可以放在 initialize方法里初始化

load和initialize的区别

17、如何访问并修改一个类的私有属性?

1、 一种是通过KVC获取
2、通过runtime访问并修改私有属性,使用runtime可以获取到一个对象的所有成员变量,通过获取到的成员变量即可修改一个对象的私有属性。

定义Person类,拥有私有属性name

// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject

@end

// Person.m
#import "Person.h"
#import <objc/runtime.h>

@interface Person()

// 私有属性
@property(nonatomic, copy) NSString *name;

@end

@implementation Person

- (NSString *)description
{
    return [NSString stringWithFormat:@"name: %@", self.name];
}

@end
  • KVC方法
 Person *person = [Person new];
 [person setValue:@"new name" forKey:@"name"];
 NSString *name = [person valueForKey:@"name"];
  • Runtime方法
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc]init];

    unsigned int count = 0;
    // 获取对象的成员变量数组
    Ivar *ivarList = class_copyIvarList([Person class], &count);

    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        // 获取成员变量名(带 "_"下划线)
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        if ([ivarName isEqualToString:@"_name"]) {
            // 修改成员变量名
            object_setIvar(person, ivar, @"mmmmm");
        }
    }

    NSLog(@"%@", person.description); // name: mmmmm
}

18、Runtime常见的应用场景

1、关联对象(Objective-C Associated Objects)给分类增加属性
2、方法混淆(Method Swizzling)
3、动态方法添加和替换
4、消息转发(热更新)解决Bug(JSPatch)
5、实现NSCoding的自动归档和自动解档
6、实现字典和模型的自动转换
7、获取所有的私有属性和方法,对私有属性修改

参考

iOSInterviewQuestions

相关文章

网友评论

    本文标题:iOS面试题 - Runtime

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