深入浅出 Runtime(五):相关面试题

2,273 阅读6分钟

Runtime 系列文章

深入浅出 Runtime(一):初识
深入浅出 Runtime(二):数据结构
深入浅出 Runtime(三):消息机制
深入浅出 Runtime(四):super 的本质
深入浅出 Runtime(五):相关面试题

Q:你了解 isa 指针吗?

  • isa指针用来维护对象和类之间的关系,并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等;
  • 在 arm64 架构之前,isa就是一个普通的指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;
  • 从 arm64 架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储classmeta-class对象的内存地址信息。要通过位运算将isa的值& ISA_MASK掩码,才能得到classmeta-class对象的内存地址;
  • isa指针存储的信息;
  • isa指针的指向。
    传送门:深入浅出 Runtime(二):数据结构

Q:类对象与元类对象的区别和联系。

  • classmeta-class底层结构都是objc_class结构体,objc_class继承自objc_object,所以它也有isa指针,它也是对象;
  • class中存储着实例方法、成员变量、属性、协议等信息,
  • meta-class中存储着类方法等信息;
  • isa指针和superclass指针的指向;
  • 基类的meta-classsuperclass指向基类的class,决定了一个性质:
    当我们调用一个类方法,会通过classisa指针找到meta-class,在meta-class中查找有无该类方法,如果没有,再通过meta-classsuperclass指针逐级查找父meta-class,一直找到基类的meta-class如果还没找到该类方法的话,就会去找基类的class中同名的实例方法的实现。

isa 与 superclass 指针指向

Q:为什么要设计 meta-class ?

目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。

Q:Runtime 的消息机制,objc_msgSend 方法调用流程。

传送门:深入浅出 Runtime(三):消息机制

OC中的方法调用,其实都是转换为objc_msgSend()函数的调用(不包括[super message])。objc_msgSend()的执行流程可以分为 3 大阶段:消息发送、动态方法解析、消息转发。

Q:调用以下 init 方法的打印结果是什么?(super)

@interface HTPerson : NSObject
@end

@interface HTStudent : HTPerson
@end

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

[self class] = HTStudent
[super class] = HTStudent
[self superclass] = HTPerson
[super superclass] = HTPerson

classsuperclass方法的实现在 NSObject 类中,可以看到它们的返回值取决于receiver

+ (Class)class {
    return self;
}
- (Class)class {
    return object_getClass(self);
}
+ (Class)superclass {
    return self->superclass;
}
- (Class)superclass {
    return [self class]->superclass;
}

[self class]是从receiverClass开始查找方法的实现,如果没有重写的情况,则会一直找到基类 NSObject,然后调用。

[super class]是从receiverClass->superclass开始查找方法的实现,如果没有重写的情况,则会一直找到基类 NSObject,然后调用。

由于receiver相同,所以它们的返回值是一样的。

Q:如何防止“调用无法识别的方法导致应用程序崩溃”?

重写doseNotRecognizeSelector方法。

Q:@synthesize 和 @dynamic

  • @synthesize :为属性生成下划线成员变量,并且自动生成settergetter方法的实现。以前 Xcode 还没这么智能的时候就要这么做。而现在默认我们写的属性,会自动进行@synthesize。有时候我们不希望它自动生成,而是在程序运行过程中再去决定该方法的实现,就可以使用@dynamic
  • @dynamic :是告诉编译器不用自动生成settergetter的实现,不用自动生成成员变量,等到运行时再添加方法实现,但是它不会影响settergetter方法的声明。
  • 动态运行时语言与编译时语言的区别:动态运行时语言将函数决议推迟到运行时,编译时语言在编译器进行函数决议。OC 是动态运行时语言。

Q:能否向编译后的类增加实例变量?能否向运行时动态创建的类增加实例变量?

  • 不能向编译后的类增加实例变量。类的内存布局在编译时就已经确定,类的实例变量列表存储在class_ro_t结构体里,编译时就确定了内存大小无法修改,所以不能向编译后的类增加实例变量。
  • 能向运行时动态创建的类增加实例变量。运行时动态创建的类只是通过alloc分配了类的内存空间,没有对类进行内存布局,内存布局是在类初始化过程中完成的,所以能向运行时动态创建的类增加实例变量。

需要注意的是,要在调用注册类的方法之前去完成实例变量的添加,因为注册类的时候,类的结构就生成了。说白了就是class_addIvar()函数不能给已经存在的类动态添加成员变量。

    // 动态创建一对类和元类(参数:父类,类名,额外的内存空间)
    Class newClass = objc_allocateClassPair([NSObject class], "Person", 0);
    // 动态添加成员变量
    class_addIvar(newClass, "_age", 4, 1, @encode(int));
    class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    // 注册一对类和元类(要在类注册之前添加成员变量)
    objc_registerClassPair(newClass);
    // 创建实例
    id person = [[newClass alloc] init];
    [person setValue:@"Lucy" forKey:@"name"];
    [person setValue:@"20" forKey:@"age"];  
    NSLog(@"name:%@, age:%@", [person valueForKey:@"name"], [person valueForKey:@"age"]);    
    // 当类和它的子类的实例存在时,不能调用 objc_disposeClassPair(),否则会 Crash:Attempt to use unknown class 0x1005af5c0.
    person = nil;    
    // 销毁一对类和元类
    objc_disposeClassPair(newClass);

    // name:Lucy, age:20

Q:你是否有使用过 performSelector: 方法?

使用场景:一个类在编译时没有这个方法,在运行的时候才产生了这个方法,这个时候要调用这个方法就要用到performSelector:方法。
关于动态添加方法的实现可以查看:传送门:深入浅出 Runtime(三):消息机制

Q:以下打印结果是什么?(isKindOfClass & isMemberOfClass)

@interface Person : NSObject
@end
......
    BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
    BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL res3 = [[Person class] isKindOfClass:[Person class]];
    BOOL res4 = [[Person class] isMemberOfClass:[Person class]];

    NSLog(@"%d,%d,%d,%d", res1, res2, res3, res4);
......

打印结果:1,0,0,0
以下是isMemberOfClassisKindOfClass方法以及object_getClass()函数的实现。

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
  • isMemberOfClass方法是判断当前instance/class对象的isa指向是不是class/meta-class对象类型;
  • isKindOfClass方法是判断当前instance/class对象的isa指向是不是class/meta-class对象或者它的子类类型。

显然isKindOfClass的范围更大。如果方法调用着是instance对象,传参就应该是class对象。如果方法调用着是class对象,传参就应该是meta-class对象。所以res2-res4都为 0。那为什么res1为 1 呢?

因为 NSObject 的class的对象的isa指向它的meta-class对象,而它的meta-classsuperclass指向它的class对象,所以它满足isKindOfClass方法的判断条件。

总之,[instance/class isKindOfClass:[NSObject class]];都返回 1。