聊几道面试题

2021-03-25 15:07:38 浏览数 (1)

一、Runtime是什么

Runtime System实际上是一个C、C 写的底层库,即一套API,系统在编译完代码之后,在运行的时候还需要依赖Runtime System才能够完整的、确定的代码。这就是Runtime。

我们在日常开发中使用Runtime主要有三个层面Runtime——介绍:

  1. 通过Objective-C源代码
  2. 通过Foundation框架的NSObject类定义的方法
  3. 通过对Runtime库函数的直接调用

在编译的时候,将Mach-o里面的数据读取到ro中,我们只可以获取到ro里面的数据。而在运行的时候才加载到内存中的方法、属性等,都是加到rw中的。可以通过比较Extension和Category来进一步说明:

关于类目的几点探讨

类的加载(三)

二、方法的本质是什么?SEL 和IMP分别是什么,其关系是怎样的?

方法的本质

方法的本质是发送消息(Runtime——OC中的发消息,Effective Objective-C 2.0——理解objc_msgSend的作用)

消息会有以下几个流程:

  1. 消息的快速查找流程(OC类的原理探究(二)——方法的缓存,方法的查找流程——快速查找)
  2. 消息的慢速查找流程(方法的查找流程——慢速查找)
  3. 查找不到消息的时候就会进入动态方法解析
  4. 消息的快速转发流程
  5. 消息的慢速转发流程

关于上面的3、4、5,可以参考下面这几篇文章:

Runtime——消息转发流程

Effective Objective-C 2.0——理解消息转发机制

消息转发流程的源码探究

回答问题的时候,将1~2展开讲完之后,3~5一带而过,暂时不要展开讲,如果面试官问了再展开讲3~5,不问就过。

SEL 和IMP

sel是方法编号,它在read_images期间就编译进入了内存

IMP是函数指针,它指向了具体的函数实现

我们可以将sel理解成是一本书的目录title,将IMP理解成是一本书的页面。通过目录中的title,我们就可以找到具体的页码,进而找到具体的内容(即真正的函数实现)。

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

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

在运行时创建的类,只要还没有注册到内存,就还可以往其中添加实例变量。

具体可以参考类的加载(二)中的【如何动态创建一个类】篇章。

四、[self class]和[super class]的区别

关于[self class]和[super class]的比较,我之前有过总结:

[super class]和[self class]

其他的就不赘述了,这里只补充一点。

class方法的源码如下:

代码语言:javascript复制
- (Class)class {
    return object_getClass(self);
}

这里的self就是class消息的真正接收者。我们看到,class方法里面调用了运行时函数object_getClass:

代码语言:javascript复制
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

我们可以看到,其最根本的就是获取到对象obj的isa所指向的空间,也就是self所指代的实例变量所属的类对象。

五、weak的底层原理

详见weak的底层原理

六、使用方法交换解决数组越界导致的崩溃问题

关于Runtime的方法交换黑魔法,我之前写过好多文章在不同的角度介绍过:

Runtime——修改方法的底层实现函数&&交换两个方法的IMP

Effective Objective-C 2.0——用“方法调配技术”调试“黑盒方法”

当NSDictionary遇见nil

Runtime应用——在不修改原方法的基础上给原方法添加功能

一个Bug所引发的方法交换小讨论

今天我们就来聊聊如何通过方法交换来解决数组越界导致的崩溃问题。

首先在工程中写下如下代码:

代码语言:javascript复制
NSArray *array = @[@"lavie", @"norman", @"lily"];
NSLog(@"%@", array[3]);

运行后崩溃了:

可以看到,是在 objectAtIndexedSubscript 方法中发生了数组越界。

上面是字面量写法,我们接下来使用objectAtIndex方法来获取元素:

代码语言:javascript复制
NSArray *array = @[@"lavie", @"norman", @"lily"];
NSLog(@"%@", [array objectAtIndex:3]);

运行后的崩溃信息如下:

可以看到,是在 objectAtIndex 方法中发生了数组越界。

因此,我就想,可以通过交换上面发生错误的objectAtIndexedSubscriptobjectAtIndex这两个原生方法,然后增加一些容错处理的操作。

此时再运行还是崩溃,信息如下:

实际上,仔细看的话不难看出,报崩溃的类是__NSArrayI而不是NSArray,这里就涉及到类簇的概念了,我之前有过总结:

Effective Objective-C 2.0——以“类族模式”隐藏实现细节

接下来我将类信息稍作调整:

此时再运行就不会奔溃了。

坑点1

上面那样写完之后,一般情况下不会出现什么问题,但是我们程序员界的Bug基本都不是在一般情况下出现的吧?所以此时我们要考虑一下代码的坚固性了。

比如下面这种情况:

代码语言:javascript复制
NSArray *array = @[@"lavie", @"norman", @"lily"];
[NSArray load];
NSLog(@"%@", [array objectAtIndex:3]);
NSLog(@"%@", array[3]);

我在一个隐秘的角落不知道为啥调用了一下NSArray的load方法,此时再运行,完了,崩了。为啥崩了呢?因为调用了两次load方法将交换的方法又给交换了回来,相当于没有交换。

鉴于上面这种情况,我决定给load方法里面的交换操作加一把锁:

这样就能保证交换只会进行一次。

坑点2

LVPerson类:

代码语言:javascript复制
@interface LVPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation LVPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}
@end

LVStudent类,继承自LVPerson

代码语言:javascript复制
@interface LVStudent : LVPerson
@end

@implementation LVStudent
@end

给LVStudent类创建一个LV类目,用于交换方法:

代码语言:javascript复制
@implementation LVStudent (LV)

  (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LVRuntimeTool lv_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lv_studentInstanceMethod)];
    });
}

- (void)lv_studentInstanceMethod{
    [self lv_studentInstanceMethod];
    NSLog(@"LVStudent分类添加的lv对象方法:%s",__func__);
}

@end

交换方法的实现如下:

代码语言:javascript复制
@implementation LVRuntimeTool
  (void)lv_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}
@end

外界调用如下:

代码语言:javascript复制
LVStudent *student = [[LVStudent alloc] init];
[student personInstanceMethod];

此时运行,正常执行,没得问题,也不会蹦。分析如下:

方法交换后,oriSEL指向的swiMethod(即lv_studentInstanceMethod)是存在于LVStudent中;而swizzledSEL指向的oriMethod(即personInstanceMethod)是在LVPerson中。因此,使用LVStudent的实例对象student来调用personInstanceMethod方法,会先执行LVStudent中的lv_studentInstanceMethod,再执行LVPerson中的personInstanceMethod

好,接下来坑点来了,我在外界使用LVStudent和LVPerson的实例对象都调用了personInstanceMethod方法:

代码语言:javascript复制
LVStudent *student = [[LVStudent alloc] init];
[student personInstanceMethod];
    
LVPerson *person = LVPerson.new;
[person personInstanceMethod];

此时再运行,就会崩了:

不难看出,崩溃原因是LVPerson中找不到SEL-lv_studentInstanceMethod对应的实现:

代码语言:javascript复制
reason: '-[LVPerson lv_studentInstanceMethod]: unrecognized selector sent to instance 0x6000003eccd0'

接下来分析一下:

方法交换后,oriSEL指向的swiMethod(即lv_studentInstanceMethod)是存在于LVStudent中;而swizzledSEL指向的oriMethod(即personInstanceMethod)是在LVPerson中。因此,使用LVPerson的实例对象person来调用personInstanceMethod方法,会从LVPerson中开始查找personInstanceMethod对应的实现,本类找不到的话就往上找父类,而personInstanceMethod对应的实现是LVStudent类中的lv_studentInstanceMethod,因此根本就找不到,所以会报出上面?的错误

其实,上面这个问题的关键就在于,我在子类中操作了父类的SEL所对应的IMp,此时子类中是没有这个SEL所对应的IMP的。因此,就需要在对子类中没有实现的方法进行交换的时候做特殊处理

我们对方法交换的工具类做出如下优化

代码语言:javascript复制
  (void)lv_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    // 当oriSEL在本类中没实现的时候会添加成功,否则会添加失败
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    if (success) {
        // 本类中没有oriSEL对应的实现,通过上面一步?会将本类中的oriSEL指向swiMethod
        // 此时swizzledSEL还没有指向oriMethod,下面?这步就是做这个操作
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        // 本类中有oriSEL对应的实现,那么就直接交换就得了
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

坑点3

接着上面的方案说,现在考虑这样一种情况:

LVPerson类:

代码语言:javascript复制
@interface LVPerson : NSObject
@end

@implementation LVPerson
@end

LVStudent类,继承自LVPerson

代码语言:javascript复制
@interface LVStudent : LVPerson
- (void)helloword;
@end

@implementation LVStudent
@end

然后给LVStudent类创建一个LV类目,用于交换方法:

代码语言:javascript复制
@implementation LVStudent (LV)

  (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LVRuntimeTool lv_methodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lv_studentInstanceMethod)];
    });
}

- (void)lv_studentInstanceMethod{
    [self lv_studentInstanceMethod];
    NSLog(@"LVStudent分类添加的lv对象方法:%s",__func__);
}

@end

方法交换的实现如下:

代码语言:javascript复制
  (void)lv_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    // 当oriSEL在本类中没实现的时候会添加成功,否则会添加失败
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    if (success) {
        // 本类中没有oriSEL对应的实现,通过上面一步?会将本类中的oriSEL指向swiMethod
        // 此时swizzledSEL还没有指向oriMethod,下面?这步就是做这个操作
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        // 本类中有oriSEL对应的实现,那么就直接交换就得了
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

外界调用如下:

代码语言:javascript复制
LVStudent *s = [[LVStudent alloc] init];
[s helloword];

此时运行的话,程序会进入死循环,分析如下:

方法交换的时候,swiMethod是可以获取到的,但是oriMethod不能获取到;

class_addMethod执行之后,oriSEL指向swiMethod(即lv_studentInstanceMethod);

class_replaceMethod的时候,由于oriMethod没获取到,因此class_replaceMethod执行不成功,所以此时swizzledSEL指向的仍然是swiMethod(即lv_studentInstanceMethod);

这样的话,当我在外界调用helloword的时候,就会进入lv_studentInstanceMethod,而lv_studentInstanceMethod还是指向lv_studentInstanceMethod,这样就死循环递归了

因此我需要处理一下oriSEL指向的IMP不存在的情况:

七、内存偏移问题

现在定义了一个Norman类:

代码语言:javascript复制
@interface Norman : NSObject
- (void)play;
@end

@implementation Norman
- (void)play {
    NSLog(@"Lavie*** %s",__func__);
}
@end

外界调用代码如下:

代码语言:javascript复制
Norman *norman = Norman.alloc.init;
[norman play];

id cls = Norman.class;
void *p = &cls;
[(__bridge id)p play];// 这里的__bridge id是将p强转为id类型,因为p原本是C类型,需要强转成OC的id类型。

运行之后,执行结果如下:

代码语言:javascript复制
2021-03-09 21:51:27.765661 0800 Test[12422:1710466] Lavie*** -[Norman play]
2021-03-09 21:51:29.376145 0800 Test[12422:1710466] Lavie*** -[Norman play]

[norman play];这行代码的执行结果很好理解;但是面对上面的第4~6行,你是不是一脸黑人问号???

且听我慢慢道来。

norman是一个指针,它指向了一块内存区域,该内存区域里面存储了Norman的实例对象,这块内存区域里面存储的第一块内容就是isa指针,因此该内存区域的地址就是isa指针的地址,因此我们可以说,norman指针指向的是isa指针的地址;而isa指针又指向了Norman类对象内存区域。

cls是一个指针,该指针指向了Norman类对象的地址;p是一个指针,它指向了cls指针的地址。因此,cls指针就好比是isa指针,p就相当于是norman。

接下来我们再接着看。

首先,给Norman类进行下改造,新增了一个name属性,并且在play方法里面打印name:

代码语言:javascript复制
@interface Norman : NSObject
@property (nonatomic, copy)NSString *name;
- (void)play;
@end

@implementation Norman
- (void)play {
    NSLog(@"Lavie*** %s --- %@", __func__, self.name);
}
@end

然后在外界的ViewController中去调用:

运行后执行结果如下:

Lavie*** -[Norman play] --- <ViewController: 0x7fcc37e06dc0>

这里不禁就有疑问了:我明明打印的是self.name,为啥打印出了个<ViewController: 0x7fcc37e06dc0>???

接下来就来分析。

首先要说明的一点是,栈的内存是连续的

在上例中,首先会将ViewController实例对象指针压进栈中,然后将cls指针压进栈中,然后将p指针压进栈中。

给p调用play方法,实际上就是通过p指针找到isa指针,然后找到对应的IMP。

我们知道,一个实例对象的内存结构中,第一个就是isa指针,isa指针占8个字节;从第二个开始,各个实例变量依次排列开来

在play方法里面获取self.name,实际上就是获取isa指针下面(8个字节后面)的第一个内存空间的值

isa指针的下面,也就是cls指针的下面,就是ViewController实例对象指针

接下来我们稍作改动,其他地方不变,调用的地方如下:

代码语言:javascript复制
NSString *gentle = @"male";
id cls = Norman.class;
void *p = &cls;
[(__bridge id)p play];// 这里的__bridge id是将p强转为id类型,因为p原本是C类型,需要强转成OC的id类型。

打印结果为:

Lavie*** -[Norman play] --- male

分析如下:

首先会将ViewController实例对象指针压进栈中,然后将字符串类型的gentle压进栈中,然后将cls指针压进栈中,然后将p指针压进栈中。

在play方法里获取的self.name就是比cls指针更前面压进栈中的gentle,因此打印的是male

接着上面的例子我们继续延伸:

Norman类中增加了一个parents属性,并且在play方法里面打印parents

代码语言:javascript复制
@interface Norman : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *parents;
- (void)play;
@end

@implementation Norman
- (void)play {
    NSLog(@"Lavie*** %s --- %@", __func__, parents);
}
@end

外界控制器中调用如下:

代码语言:javascript复制
NSString *gentle = @"male";
id cls = Norman.class;
void *p = &cls;
[(__bridge id)p play];// 这里的__bridge id是将p强转为id类型,因为p原本是C类型,需要强转成OC的id类型。

打印结果如下:

Lavie*** -[Norman play] --- <ViewController: 0x7fd218d052a0>

分析如下:

首先会将ViewController实例对象指针压进栈中,然后将字符串类型的gentle压进栈中,然后将cls指针压进栈中,然后将p指针压进栈中。

在play方法里获取的parents就是比cls指针往后移16个字节(isa8个 字符串类型的name8个),因此打印的是cls指针前面的前面压进栈中的那个指针,也就是ViewController实例对象指针

接下来再延伸。

Norman类中增加了一个int类型的属性age,并且在play方法里面打印age

代码语言:javascript复制
@interface Norman : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, assign)int age;
- (void)play;
@end

@implementation Norman
- (void)play {
    NSLog(@"Lavie*** %s --- %@", __func__, age);
}
@end

外界控制器中调用如下:

代码语言:javascript复制
NSString *gentle = @"male";
id cls = Norman.class;
void *p = &cls;
[(__bridge id)p play];// 这里的__bridge id是将p强转为id类型,因为p原本是C类型,需要强转成OC的id类型。

运行之后,程序崩溃,原因如下:

首先会将ViewController实例对象指针压进栈中,然后将字符串类型的gentle压进栈中,然后将cls指针压进栈中,然后将p指针压进栈中。

在play方法里获取的age就是比cls指针往后移16个字节(isa8个 字符串类型的name8个),因此打印的是cls指针前面的前面压进栈中的那个指针,也就是ViewController实例对象指针

但是!!!ViewController实例对象指针是8个字节,而age是int类型只有4个字节,这就是典型的脏地址,典型的野指针调用!因此将会崩溃!

以上。

0 人点赞