018-Runtime面试小结

305 阅读4分钟

1、load方法在什么什么时候调用?

类load方法

在dyld应用启动时,load_images的时期,进行单个类的收集,通过add_class_to_loadable_list收集在一张类的load方法表(loadable_class),然后通过call_class_loads统一递归load方法

分类load方法

在dyld应用启动时,load_images的时期,进行单个分类的收集,通过add_category_to_loadable_list收集在一张分类的方法表(loadable_category),然后通过call_category_loads统一递归load方法

关键函数

load_images

读取镜像,只保留了跟load有关部分

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    ...

    // 查找load方法
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }
    
    // 调用 +load 方法(没有 runtimeLock - 可重入)
    call_load_methods();
}

prepare_load_methods

收集load方法,关键函数是schedule_class_load内的add_class_to_loadable_list和add_category_to_loadable_list,对应的分别是类和分类。

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();
    //通过遍历的方式收集类的load方法
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
    
    //通过遍历的方式收集分类的load方法
    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

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

call_load_methods

调用类与分类的load方法

void call_load_methods(void)
{
    ...

    do {
        // 1. 类load调用
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 分类load调用
        more_categories = call_category_loads();

    } while (loadable_classes_used > 0  ||  more_categories);

    ...
}

流程图

load_images.png

2、runtime是什么?

runtime是有C和C++、汇编实现的一套API。为OC语言加入了面向对象、运行时的功能 运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时(例如extension、Category,在底层数据结构对应的是ext,完全运行时加载的)

3、方法的本质,sel、imp是什么?两者间的关系是什么?

方法的本质是发送消息(objc_msg_send)

  1. 快速查找,通过objc_msg_send查找缓存消息(汇编方式,查找cache_t)
  2. 慢速查找:通过递归自己和父类,对应的函数为lookUpImpOrForward
  3. 查找不到消息:要进行动态方法解析,对应函数为resolveInstanceMethod
  4. 消息的快速转发:对应函数forwardingTargetForSelect
  5. 消息的慢速转发:对应函数methodSignatureForSelector和forwardInvocation

sel是方法编号,在read_images期间就编译进了内存,imp是函数实现指针,查找imp就是查找函数的过程

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

  • 不能向编译后的类中添加实例变量
  • 只要类没有注册到内存中是可以添加的 原因:我们编译好的实例变量存储位置是ro,一旦编译完成,内存结构就确定了,无法修改,但是可以添加属性和方法 objc_registerClassPair这个函数调用结束之后就无法再添加ivar了

5、[self class]和[super class]的区别和原理?

示例:下面代码[self class]和[super class]分别打印什么?

FFGirls.h和FFGirls.m

#import "FFPerson.h"

@interface FFGirls : FFPerson

@end


#import "FFGirls.h"

@implementation FFGirls

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

FFPerson.h和FFPerson.m

#import <Foundation/Foundation.h>

@interface FFPerson : NSObject

@end

#import "FFPerson.h"

@implementation FFPerson

@end

main.mm

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        FFGirls *girls = [[FFGirls alloc] init];
    }
    return 0;
}

控制台打印结果:

2021-10-28 14:31:38.085530+0800 KCObjcBuild[27933:9731327] FFGirls - FFGirls
Program ended with exit code: 0

结论分析:

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

在代码层面,类FFGirls的完整init方法应该为- (instancetype)init(id self, SEL _cmd),这里面包含两个隐层参数,self与_cmd,在上述的案例中,self表示为FFGirls,class方法的底层实现为object_getClass(self),参数self为隐藏参数,即FFGirls,所以不管是[self class]或者[super class],本质上调用的都是class函数,传递的隐藏参数都是self,所以打印结果都为FFGirls。

6、下面的代码会输出什么?为什么bblv可以调用likeGirls方法?

FFPerson.h和FFPerson.m

#import <Foundation/Foundation.h>

@interface FFPerson : NSObject
- (void)likeGirls;
@end

#import "FFPerson.h"

@implementation FFPerson

- (void)likeGirls {
    NSLog(@"%@ - %s", self, __func__);
}
@end

ViewController.m

#import "ViewController.h"
#import "FFPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    FFPerson *person = [FFPerson alloc];
    [person likeGirls];
    
    Class cls = [FFPerson class];
    void *bblv = &cls;
    [(__bridge id)bblv likeGirls];
    
}

@end

控制台输出

2021-10-29 15:32:38.457553+0800 001-内存平移问题[56669:10329070] <FFPerson: 0x600001808230> - -[FFPerson likeGirls]
2021-10-29 15:32:38.457636+0800 001-内存平移问题[56669:10329070] <FFPerson: 0x7ffee6fe8060> - -[FFPerson likeGirls]

结论分析: 方法调用的本质就是发送消息,通过objc_msg_send函数,通过类的isa指针找到类的首地址,通过内存平移的方式找到methodList,关于类的内存结构可以浏览WWDC2020关于runtime的优化,所以[person likeGirls]可以打印。至于为什么bblv也可以调用likeGirls?首先void *bblv = &cls;此行代码指针bblv拿到了cls的地址,也就是FFPerson的类地址,调用也就理所应当了,至于FFPerson这个类根本就不清楚是person对象调用它了,还是bblv指针地址指向它了。