美文网首页
iOS剖析几道底层经典面试题

iOS剖析几道底层经典面试题

作者: 似水流年_9ebe | 来源:发表于2021-07-23 19:35 被阅读0次

前言

这篇文章会简单介绍OC的几道底层相关的面试题。

load方法什么时候调用?

我们在objc源码中,搜下load_images方法(该方法在前几篇文章有介绍),如图所示:

1
load_images在dyld在调用map_images和load_imges调用的。
dyld会注册_dyld_objc_notify_register这个回调,这个回调在_objc_init这个函数中调用的时候,会有两个参数map_imagesload_imges,这个时候会调起load_images。在iOS底层-dyld加载分析中有介绍。
prepare_load_methods这个函数是load方法的准备,我们看下它的代码:
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, I;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
 
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

** schedule_class_load**这个函数扫描所有类的load方法,它的代码如下:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

这里递归获取类(包括父类)的Load方法,并且调用这个add_class_to_loadable_list这个函数添加到loadable_class这个全局静态变量中(这个表结构是key-value的字典形式存储),我们贴下add_class_to_loadable_list这个函数的代码:

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

schedule_class_load这个函数会调用**cls->setInfo(RW_LOADED); **这个函数设置一个标记。
prepare_load_methods这个函数类搜集完后,开始搜集分类,以下代码就是:

 category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }

会调用add_category_to_loadable_list这个函数据,把load方法以key-value的形存储在loadable_categories这张表中。
类的load和分类的load方法搜索完后,接着回到load_images这个函数中,调用了call_load_methods这个函数。
call_load_methods这个函数中会do while循环调用call_class_loadscall_category_loads
** call_load_methods在这个函数中循环从loadable_classes取出类的load方法,然后往相应的类发送消息,从以上可以看出load方法是在_objc_init的时候加载的**。
它的代码如下:

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int I;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
  
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

分类的Load方法加载call_category_loads这个函数原理基本一样,这里不再介绍,我们只贴下代码:

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[I];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[I];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

从上面可以得出:
1. initialize方法与Load方法哪个先调用?
答案:load方法先调用,initialize是在第一次消息发送的时候调用,也就是在lookUpImpOrForward的时候调用的。
load方法->C++构造函数(这个是在objc源码中实现的)->main函数
load方法->main函数->C++构造函数(这个是在OC中实现的)。

2. 分类的加载顺序?
跟Build Phases中的顺序有关(编译顺序)。

Runtime是什么

runtime 是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能
运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时
平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代码,RuntimeObject-C 的幕后⼯作者。

⽅法的本质,SEL是什么?IMP是什么?两者之间的关系⼜是什么?

SEL 是⽅法编号 ~ 在read_images 期间就编译进⼊了内存
IMP 就是我们函数实现指针 ,找imp 就是找函数的过程
SEL 就相当于书本的⽬录 tittle
IMP 就是书本的⻚码
SEL : ⽅法编号
IMP : 函数指针地址

⽅法的本质:发送消息 ,消息会有以下⼏个流程
1:快速查找 (objc_msgSend)~ cache_t 缓存消息
2:慢速查找~ 递归⾃⼰| ⽗类 ~ lookUpImpOrForward
3:查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod
4:消息快速转发~ forwardingTargetForSelector
5:消息慢速转发~ methodSignatureForSelector & forwardInvocation

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

答案:
1:不能向编译后的得到的类中增加实例变量
2:只要类没有注册到内存还是可以添加
原因:我们编译好的实例变量存储的位置在 ro,⼀旦编译完成,内存结构就完全确定
就⽆法修改,可以添加属性 + ⽅法

[self class]和[super class]的区别以及原理分析

我们在RoTeacher.m文件中加入以下代码:

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

这里的[self class][super class]返回的是什么,我们分析下。
从运行结果来看都是RoTeacher,这j时RoTeacher是继承于NSObject。
我们让RoTeacher继承于RoPerson,这时候运行,发现[self class][super class]打印的依然是RoTeacher
我们知道RoTeacher是继承于RoPerson,按道理来讲[super class]应该输出RoPerson,为什么是RoTeacher呢?我们继续分析。
我们看下class的代码:

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

这个方法是来自于NSObject,因为RoTeacherRoPerson都没有这个方法。
我们先看[self class]这个调用,针对上面的代码,object_getClass(self)这里的self是指RoTeacher,为什么呢,self是什么?

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

这里相当于

- (Class)class (id self, sel  _cmd){
    return object_getClass(self);
}

是隐藏的参数名字。
我们先看下[self class](这里的self是RoTeacher):
class方法在底层会转化为成objc_msgSend(id receive, sel _cmd),这里的receive就是self,所以这里调用class方法中的self是RoTeacher,这里的self是实例变量,我们再看下object_getClass这个函数的代码:

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

这里的obj参数是self,也就是RoTeacher的实例对象,实例对象的isa指针是指向类,所以这里返回的是RoTeacher
我们再看[super class]调用:
super在这里是关键字。
[super class]在底层会走objc_msgSendSuper这个函数,objc_msgSendSuper(void /* struct objc_super super, SEL op, ... / )这个是它的objc_msgSendSuper。
我们看下
objc_super结构体**,如下:

struct objc_super {
    /// 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 */
};
#endif

id receiver是消息接受者,
super_class代表的是第一个查找的类,并不是它的父类。
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
函数,同样也有objc_super结构体。
[super class]在编译的时候是objc_msgSendSuper,在运行的时候是objc_msgSendSuper2(这里可以通过汇编跟踪一下),通过objc_msgSendSuper的汇编查看,在里面有跳转到objc_msgSendSuper2的代码。
[super class]的调用走到objc_msgSendSuper2时,它的objc_super这个参数中的id receiver是消息接受者,在这里就是RoTeacher,所以在走到以下代码时

- (Class)class (id self, sel  _cmd){
    return object_getClass(self);
}

这里的self也就是RoTeacher[super class]打印的也是RoTeacher
在这里:
self是形参名字。
super是关键字。

内存平移相关面试题

首先我们把RoPerson的代码贴出来,如下:
RoPerson.h代码:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RoPerson : NSObject
@property (nonatomic, copy) NSString *ro_name;
//@property (nonatomic, copy) NSString *ro_hobby;
- (void)saySomething;
@end

NS_ASSUME_NONNULL_END

RoPerson.m代码:

#import "RoPerson.h"
@implementation RoPerson
- (void)saySomething{
    NSLog(@"%s",__func__);
}
@end

ViewController.h代码:

#import "ViewController.h"
#import "RoPerson.h"

@interface ViewController ()

@end

@implementation ViewController

// 高地址 -> 低地址
void roFunction(id person, id roSel, id roSel2){
    NSLog(@"person = %p",&person);
    NSLog(@"person = %p",&roSel);
    NSLog(@"person = %p",&roSel2);
}

struct ro_struct{
    NSNumber *num1;
    NSNumber *num2;
} ro_struct;

- (void)viewDidLoad {
    [super viewDidLoad];

    RoPerson *person = [RoPerson alloc];
    [person saySomething];

    Class cls = [RoPerson class];
    void  *ro = &cls;  //
    [(__bridge id)ro saySomething];
}
@end

1. [person saySomething];是否可以调起来并输出打印?
2. [(__bridge id)ro saySomething];是否可以调起来并输出打印?
我们分析下。

首先在[person saySomething];打个断点,并运行项目,
如图:

2
两个都可以正常调起。
[person saySomething];可以正常调起,为什么[(__bridge id)ro saySomething];这里可以调起呢?我们继续分析。
saySomething是在RoPerson类的data里面,person这个对象是如何找到saySomething这个方法呢,是通过首地址isa找到RoPerson类,然后不断的平移,找到methodList,然后二分查找,找到saySomething这个方法,也就是说,person对象是通过isa(即首地址去查找,这里的isa指向了类RoPerson)。
我们再来看下[(__bridge id)ro saySomething];
Class cls = [RoPerson class];获取RoPerson类,void *ro = &cls;把cls这个对象的地址给到ro,这时的ro就是指向RoPerson的首地址即isa指向了RoPerson类,所以可以调起saySomething。

接着我们改动下代码:
RoPerson的代码:

#import "RoPerson.h"

@implementation RoPerson
- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.ro_name);
}
@end

请问这个时候[person saySomething];[(__bridge id)ro saySomething];各打印什么。
我们运行项目后,得到如下图:

3
ro_name没有赋值,[person saySomething];调用的时候为null正常,那么[(__bridge id)ro saySomething];打印的是RoPerson,为什么会这样?我们继续分析,我们终端打下person对象,如图:
4
person对象跟这里的[(__bridge id)ro saySomething];地址一样,我们对比下
personro**。
person是一个对象,开辟了内存空间,也就是具备内存,它的内存结构,如下:
isa
成员变量(这里有ro_name成员变量)

ro是一个指针地址,它没有内存空间,不具备内存。

person是怎么访问ro_name,是通过内存平移,不断的从开辟的平移偏移量获取数据。
接着我们再修改下ViewController的viewDidLoad方法,如下:

 RoPerson *person = [RoPerson alloc];
    person.ro_name = @"robert";
    [person saySomething];

    Class cls = [RoPerson class];
    void  *ro = &cls; 
    [(__bridge id)ro saySomething];

我们把RoPerson的ro_name改为retain。
我们修改ViewController代码,如下:


#import "ViewController.h"
#import "RoPerson.h"


@interface RoPersonP : NSObject
@property (nonatomic, retain) NSString *ro_name;
//@property (nonatomic, copy) NSString *ro_hobby;
- (void)saySomething;
@end


@implementation RoPersonP
- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.ro_name);
}
@end



@interface ViewController ()

@end

@implementation ViewController

// 高地址 -> 低地址
void roFunction(id person, id roSel, id roSel2){
    NSLog(@"person = %p",&person);
    NSLog(@"person = %p",&roSel);
    NSLog(@"person = %p",&roSel2);
}

struct ro_struct{
    NSNumber *num1;
    NSNumber *num2;
} ro_struct;

- (void)viewDidLoad {
    [super viewDidLoad];

    RoPersonP *person = [RoPersonP alloc];
    person.ro_name = @"robert";
    [person saySomething];

    Class cls = [RoPersonP class];
    void  *ro = &cls; 
    [(__bridge id)ro saySomething];

}
@end

接着在终端执行clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.5.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk ViewController.m这个命令,我们看下ViewController.cpp文件。

我们找到ro_name的getter方法static NSString * _I_RoPersonP_ro_name(RoPersonP * self, SEL _cmd) { return (*(NSString *_strong *)((char *)self + OBJC_IVAR$_RoPersonP$_ro_name)); }

它的查找方式,是(char *)self(self是ViewController)+ OBJC_IVAR_$_RoPersonP$_ro_name)(偏移)。

extern "C" unsigned long int OBJC_IVAR__RoPersonP_ro_name attribute ((used, section ("__DATA,__objc_ivar"))) = OFFSETOFIVAR(struct RoPersonP, _ro_name);这就是OBJC_IVAR_$_RoPersonP$_ro_name。也就是首地址平移。

person这个对象找ro_name,也就是person的首地址+平移8字节。

而ro访问ro_name时,也模访person,ro+平移8字节,而ro不具备内存,是Class,是在ViewController中的viewDidLoad方法压栈的地址,也就是cls地址+8字节。
我们再打断点,运行项目,如图:


5

从上图可以看出person与ro刚好差了8字节,ro平移8字节,刚好指向person对象。

我们在RoPerson在加一个属性,RoPerson的代码
.h

@interface RoPerson : NSObject
@property (nonatomic, retain) NSString *ro_name;
@property (nonatomic, copy) NSString *ro_hobby;
- (void)saySomething;
@end

.m

@implementation RoPerson
- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.ro_hobby);
}
@end

我们运行项目,得到如下图:

5
这个时候[(__bridge id)ro saySomething]打印的是ViewController。
[(__bridge id)ro saySomething]这个时候查找的时候,要加上0x10(即16字节),而不是0x8。
viewDidLoad这个方法的隐藏参数(id self, sel _cmd)是可以压栈,[super viewDidLoad];中的super的最终走objc_msgSendSuper有一个结构体参数objc_super,里面又有id receiver和Class super_class两个要入栈。
我们贴下viewDidLoad的代码,里面有验证方式,如图:
6
从上图可以看出结构体是压入栈了,也可以看出是倒着压栈的。
我们再来看下参数是怎么入栈的,如图:
7
参数入栈是正序压栈的。
修改RoPerson.h文件
@interface RoPerson : NSObject
@property (nonatomic, copy) NSString *ro_hobby1;
@property (nonatomic, copy) NSString *ro_hobby;
@property (nonatomic, copy) NSString *ro_height;
@property (nonatomic, copy) NSString *ro_age;
@property (nonatomic, copy) NSString *ro_name;

- (void)saySomething;
@end
8

在viewDidLoad中[(__bridge id)ro saySomething];调起时,访问的id self参数,也就是ViewController

补充 结构体压栈的原理

[super viewDidLoad]和[RoPerson alloc]的隐藏参数id self,SEL _cmd为什么没有压栈,我们这里解释下。
临时变量才会压栈,参数会压栈,会在自己的栈空间。
所以[super viewDidLoad]和[RoPerson alloc]这里面的参数会在自己的栈空间压栈,跟这里的viewDidLoad无关。
objc_msgSendSuper,内部会自动创建 一个objc_super的临时变量,相当于创建一个person对象一样,所以会压栈。

methodswizzling面试题

methodswizzling方法交换在runtime中称为黑魔法,我们今天来介绍一下。
我们先来看张图:


9

从张图中,可以看出methodswizzling是交换了两个方法的IMP。

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [RoRuntimeTool ro_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(ro_studentInstanceMethod)];
    });
}
- (void)ro_studentInstanceMethod{
    [self ro_studentInstanceMethod]; //ro_studentInstanceMethod -/-> personInstanceMethod
    NSLog(@"roStudent分类添加的ro对象方法:%s",__func__);
}

我们看下这段代码。
1.[self ro_studentInstanceMethod];这里调用的时候为什么没有产生递归?
答案:ro_studentInstanceMethod这个已经被交换了,说白了这个是原始的方法,而不是自己。尽量使用单例方式保证交换一次,为这了安全考虑。

2.交换方法时,父类都实现,子数没有实现,会出现什么情况?
这个时候会出现崩溃,报找不到方法的异常,因为父类的方法已经被交换了,指向了交换方法的IMP,[self ro_studentInstanceMethod];调用的时候(这里的sel是RoPerson)这里调用ro_studentInstanceMethod时,RoPerson是没有这个方法,所以找不到这个方法。当然我们可以通过在RoStudent中class_addMethod添加方法,然后替class_replaceMethod替换。

3.如果要交换的方法,子类和父类都没有,会出现什么情况?
同样会产生崩溃,[self ro_studentInstanceMethod];崩溃了,这里产生了死递归,也就是没有交换成功,也就是说ro_studentInstanceMethod。Method oriMethod = class_getInstanceMethod(cls, oriSEL);这里获取的时候,oriMethod就是一个空nil,不存在,class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));这里替换的时候,根本没有替换成功,所以ro_studentInstanceMethod还是指向自身,会产生递归。

我们可以通过以下代码解决

 class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));

添加一个空的IMP 。

相关文章

网友评论

      本文标题:iOS剖析几道底层经典面试题

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