iOS探索 isa面试题分析

5,587 阅读8分钟

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

本文涉及的面试题如下:

  • isKindOfClassisMemberOfClass的区别
  • [self class][super class]的区别
  • isa综合运用——内存偏移

一、isKindOfClass 和 isMemberOfClass

这是一道涉及isa走位图的面试题,大胆猜测下结果

#import <Foundation/Foundation.h>
#import "FXPerson.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];//1
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];// 0
        BOOL re3 = [(id)[FXPerson class] isKindOfClass:[FXPerson class]];//0
        BOOL re4 = [(id)[FXPerson class] isMemberOfClass:[FXPerson class]];// 0
        NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];//1
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];// 1
        BOOL re7 = [(id)[FXPerson alloc] isKindOfClass:[FXPerson class]];//1
        BOOL re8 = [(id)[FXPerson alloc] isMemberOfClass:[FXPerson class]];// 1
        NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}

这里先不揭晓答案,先来探索一下isKindOfClassisMemberOfClass的实现

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
  • 这是一个类似于for (int i = 0; i < 3; i ++)的for循环
    • object_getClass得到当前类对象的类——元类,初始化tcls
    • 只要tcls有值就可以继续循环,即当tclsnil时结束for循环
    • 取得tcls的父类,作为它的新值,继续下一次循环
  • 当for循环中有一次tcls == cls,返回YES
  • 结束for循环时还没满足条件就返回NO

结论一:+isKindOfClass是元类及其父类 vs 类

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
  • object_getClass得到当前类对象的类——元类,和类本身cls进行比较
  • 相较于+isKindOfClass少了父类的比较,因此+isMemberOfClass为YES时可以得到+isKindOfClass为YES

结论二:+isMemberOfClass是元类 vs 类

结合isa走位图(实线为父类走向)可以得出前面四个打印结果:

  • NSObject元类NSObject类不相等,NSObject元类的父类(指向NSObject类)与NSObject类相等——YES
  • NSObject元类NSObject类不相等——NO
  • FXPerson元类FXPerson类不相等,FXPerson元类的父类FXPerson类不相等——NO
  • FXPerson元类FXPerson类不相等——NO

换成实例对象调用-isKindOfClass-isMemberOfClass

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

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

同理可得:-isMemberOfClass是拿实例对象的类(即当前类)和cls作比较,-isKindOfClass多了一步for循环类对象的父类

结论三:-isKindOfClass是类本身及其父类 vs 类

结论四:-isMemberOfClass是类本身 vs 类

后面四个结果分析如下:

  • NSObject类NSObject类相等——YES
  • NSObject类NSObject类相等——YES
  • FXPerson类FXPerson类相等——YES
  • FXPerson类FXPerson类相等——YES

二、[self class] 和 [super class]

FXSon继承于FXFather,主程序初始化FXSon,求问打印内容以及思路

#import "FXSon.h"

@implementation FXSon

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

@end

打印结果如下

emmm...有点出乎意料,[self class]点进去来到NSObject.mm文件查看源码

  • class方法返回类
  • superclass方法返回类的父类
+ (Class)class {
    return self;
}

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

+ (Class)superclass {
    return self->superclass;
}

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

从这段代码能解释[self class][self superclass],但是另外两个又怎么解释呢?

终端clang编译代码得到super.cpp,就能看到初始化的底层代码了

clang -rewrite-objc FXSon.m -o super.cpp

已知调用方法就是发送消息objc_msgSend,那objc_msgSendSuper也是发送消息吗?

查看源码中对objc_msgSendSuper的定义,注释中提示了objc_super

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    
/// Specifies the superclass of an instance. 
struct objc_msgSendSuper {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

从以上源码得出,使用objc_msgSendSuperobjc_super发送消息,而objc_superobjc2.0下有两个元素——id类型的receiverClass类型的super_class

其实早在iOS探索 方法的本质和消息查找流程中就提过这个方法,然后笔者进行了仿写

记得导入<objc/message.h>,报错Too many arguments就去修改编译期配置

重新运行就能得到与[super class]一样的结果了

那苹果为什么要这么设计呢?把消息查找isa走位图联系起来就明白了!

son实例对象实例方法存在FXSon类

  • 调用[self class]就是son照着FXSon->FXFather->NSObject顺序问老爸要-class方法
  • 调用[super class]就是son跳过FXSon,直接通过FXFather->NSObject查找
  • 还有比[super class]更快找到class方法的写法
    • 结构体中[self class]改为[super class],直接找到NSObjct

补充: 当结构体中[self class]改为[FXFather class]时,因为类方法存在元类中,会按FXFather元类->NSObject元类->NSObject根元类+class方法,最后也是会输出FXSon

结论:

  • [self class]就是发送消息objc_msgSend,消息接收者是self,方法编号是class
  • [super class]就是发送消息objc_msgSendSuper,消息接收者是self,方法编号是class,只不过objc_msgSendSuper会跳过self的查找

三、内存偏移

这是一道比较经典的“丧心病狂”的内存偏移面试题,如果你没有研究过,大概率很难答上来

1.原始题

程序能否运行吗?是否正常输出?

#import "ViewController.h"

@interface FXPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation FXPerson

- (void)printMyProperty {
    NSLog(@"当前打印内容为%s", __func__);
}

@end

//——————————————————————————————————————//

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [FXPerson class];
    void *obj= &cls;
    [(__bridge id)obj printMyProperty];
    
    FXPerson *p = [FXPerson new];
    [p printMyProperty];
}

@end

运行结果与普通初始化对象一模一样,可面试的时候不可能只说能或不能,还要说出个所以然来

正常初始化:指针p->实例对象isa->类对象

  • 对象的本质为objc_object,第一个元素为isa
  • 指针p存储着FXPerson类实例出来的对象内存地址,所以指针p指向对象的首地址——p->实例对象isa
  • 实例对象的isa指向类对象——实例对象isa->类对象

骚操作:指针obj->指针cls->类对象

  • id cls = [LGPerson class]获取到类对象指针
  • void *obj= &cls获取到指向该类对象cls的对象obj

2.拓展一

修改打印方法printMyProperty——不但打印方法,同时打印属性name

- (void)printMyProperty {
    NSLog(@"当前打印内容为%s——————%@", __func__, self.name);
}

重新运行代码,得到结果如下

当前打印内容为-[FXPerson printMyProperty]——————<ViewController: 0x7fc72cd09450>
当前打印内容为-[FXPerson printMyProperty]——————(null)

为什么属性name还没有赋值,却打印出了ViewController的内存地址?

  • 由于栈先入后出viewDidLoad入栈先拉伸栈空间,然后依次放入self、_cmd局部变量
  • 调用[super viewDidLoad],继续放入super_class、self
  • 正常情况下获取name,本质是p的内存地址往下偏移8字节
  • 同样的骚操作也是obj的内存地址往下偏移8字节得到self

3.拓展二

修改viewDidLoad——在obj前面加个临时字符串变量

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *temp = @"1";
    id cls = [FXPerson class];
    void *obj= &cls;
    [(__bridge id)obj printMyProperty];
    
    FXPerson *p = [FXPerson alloc];
    [p printMyProperty];
}

重新运行代码,得到结果如下

当前打印内容为-[FXPerson printMyProperty]——————1
当前打印内容为-[FXPerson printMyProperty]——————(null)

同样道理,在obj入栈前已经有了temp变量,此时访问self.name就会访问到temp

4.拓展三

去掉临时变量,FXPerson类新增字符串属性hobby,打印方法改为打印hobby,运行

ViewController就是obj偏移16字节拿到的super_class([super viewDidLoad]压栈进去的)

5.拓展四

①去掉[super viewDidLoad],运行

②临时变量改成NSInteger类型

这两种情况就是野指针——指针偏移的offset不正确,获取不到对应变量的首地址

6.万变不离其宗的理论

int a = 1;
int b = 2;
int c = 3;
int d = 4;
NSLog(@"\na = %p\nb = %p\nc = %p\nd = %p\n",&a,&b,&c,&d);

打印结果

a = 0x7ffee0ebd1bc
b = 0x7ffee0ebd1b8
c = 0x7ffee0ebd1b4
d = 0x7ffee0ebd1b0

局部变量的存放顺序,是根据定义的先后顺序,从函数栈底(高地址)开始,一个一个排列

关于这题还没搞明白的可以看下Runtime笔记(八)—— 面试题中的Runtime里面的图很形象

写在后面

面试题是面试官用知识点变着法玩你的一种手段,同时也能表现出你掌握知识的熟练度。只有在平时多练习多研究,才能在面试的时候给面试官留下一个好的印象