likes
comments
collection
share

Objective-C底层面试题

作者站长头像
站长
· 阅读数 26

oc底层探索了很多了,今天主要总结一下一些相关的面试题。

load方法什么时候调用。

这个问题很多同学应该都知道,就是main函数之前,但是main之前的哪一步执行的,可能有些同学就疑惑了,,同时还有一个方法的调用时机也会被经常问到就是initialize,我们分别讨论。

load方法

  • load方法是在应用程序加载过程中调用的,确实是在main函数之前调用。
  • 具体是_dyld_objc_notify_register方法的第二个参数load_images回调的。
  • 通过prepare_load_methods递归查找load方法添加到一个load方法的加载表loadable_classes里,注意父类会比子类先加入到表中,查找完类的load方法之后,查找分类的load也会添加到一个loadable_categories表中。
  • 最后是call_load_methods调用load方法,先从loadable_classes表里循环调用类的load方法,然后从loadable_categories表里循环调用分类的load方法。
  • 因为是顺序遍历表调用load方法的。所以load方法的调用次序是父类>本类>分类
  • 如果有多个分类都有load方法,其调用顺序会根据编译的顺序调用。编译顺序可以在Compiles Sources里调整。
  • load方法过多会影响到应用的启动速度。

initialize方法。

  • initialize方法是在第一次objc_msgSend的时候调用的,它的调用时机晚于load
  • 分类的方法是在类realize之后attachCategorys进去的,会在类的方法前面。如果分类实现了initialize方法,会优先调用分类的方法。

Runtime是什么

  • runtime是由cc++汇编实现的一套API,为oc语言加入面向对象运行时功能。
  • 运行时是指讲数据类型的确定有编译时推迟到了运行时
  • 我们写的oc代码,在程序运行过程中,最终都会转换成runtimec语言代码。

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

⽅法的本质

⽅法的本质是消息发送,即objc_msgSend,它的流程是:

  • 快速查找 (objc_msgSend)~ cache_t 缓存消息

  • 慢速查找~ 递归⾃⼰或⽗类 ~ lookUpImpOrForward

  • 查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod

  • 消息快速转发 ~ forwardingTargetForSelector

  • 消息慢速转发 ~ methodSignatureForSelectorforwardInvocation

    sel是什么

    sel是⽅法编号,在read_images期间就加载进⼊了内存。它实际是objc_selector结构体。

    IMP是什么

    imp就是我们函数实现指针,找imp就是找函数实现的过程。

    selIMP的关系

    • sel就相当于书本的⽬录title
    • imp就是书本的⻚码
    • 方法调用的时候首先根据sel找到imp最后到具体函数的实现,完成调用。

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

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

      • 我们编译好的实例变量存储的位置在ro(read only),⼀旦编译完成,内存结构就完全确定。
      • 我们可以通过分类向类中添加方法属性(通过关联对象)。
    • 可以向运行时创建的类中添加实例变量,只要类没有注册到内存还是可以添加。

      这里运行时创建的类指的是通过objc_allocateClassPair方法,创建的,在调用objc_registerClassPair方法之前是可以添加实例变量的。

    [self class]和[super class]区别

    先定义两个类JSPersonJSStudent,其中JSStudent继承于JSPerson

    @interface JSPerson : NSObject
    
    @end
    #import "JSPerson.h"
    
    @implementation JSPerson
    
    @end
    
    @interface JSStudent : JSPerson
    
    @end
    @implementation JSStudent
    
    - (instancetype)init{
        self = [super init];
        if (self) {
            NSLog(@"%@ - %@",[self class],[super class]);
        }
        return self;
    }
    @end
    

    main函数里实例化一个JSStudent对象。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            JSStudent *student = [[JSStudent alloc] init];;
            NSLog(@"%@",student);
        }
        return 0;
    }
    

    发现打印结果为JSStudent - JSStudent。这是为什么呢,我们下面分析一下。

    首先,JSPersonJSStudent类都没有实现class方法,根据消息发送查找流程,会调用NSObject类的class方法,它的实现为

    - (Class)class {
        return object_getClass(self);
    }
    //根据isa找到类
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    

    class方法的作用是返回当前的类,self是调用的对象也就是student实例。

    • [self class]打印的是JSStudent很好理解,因为消息接受者就是JSStudent的实例对象,通过isa找到的就是JSStudent类。
    • [super class]打印的也是JSStudent就让人困惑了,我们打开汇编调试看一下[super class]的底层调用了什么

Objective-C底层面试题 [super class]实际调用的是objc_msgSendSuper2方法,我们在源码看一下这个方法的定义:

// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
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 */
    ///old结构,我们可以忽略 !__OBJC2__使用
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

内存平移问题

@implementation JSPerson
- (void)saySomething{

    NSLog(@"%s",__func__);
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    JSPerson *person = [JSPerson alloc];
    [person saySomething];
    
    Class cls = [JSPerson class];
    void  *js = &cls;
    [(__bridge id)js saySomething];
}
@end
  • [person saySomething]:这种方式没什么疑问,正常的方法调用。它的流程是通过person对象的isa指针找到类JSPerson,首先通过内存平移找到cache里查找,如果找不到,继续平移找到bits查找方法列表查找,最后找到imp调用。
  • [(__bridge id)js saySomething]:运行代码,我们这一行代码也正常执行了,原因是什么呢

void *js = &cls;说明js是一个指向JSPerson类首地址的指针,它和对象的isa指向的是同一个地址,通过内存平移也可以找到对应的方法。

拓展

@interface JSPerson : NSObject

@property (nonatomic, strong) NSString *js_name;

- (void)saySomething;

@end
@implementation JSPerson
- (void)saySomething{

    NSLog(@"%s - %@",__func__,self.js_name);
}
@end
-[JSPerson saySomething] - (null)
-[JSPerson saySomething] - <JSPerson: 0x600003a00380>
  • [person saySomething]:因为我们没有对js_name赋值,[person saySomething]打印(null)正常。

  • [(__bridge id)js saySomething]:这里打印了<JSPerson: 0x600003a00380>很困惑。 我们首先要清楚self.js_name是怎么找到js_name并打印的,它是从person内存地址中平移8位(isa是8 位)找到第一个属性js_name

    • 其中隐藏参数会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),这个我们前面探索过,可以通过clangoc代码转成c++代码查看。

    • 隐藏参数压栈 的过程,其地址是递减的,而栈是从高地址->低地址 分配的,即在栈中,参数会从前往后一直压

    • 前面还有一行[super viewDidLoad];,super调用的压栈我们也需要研究一下,其实上一题我们研究过它实际调用的是objc_msgSendSuper2,有两个参数_objc_supersel。结构体的属性的压栈我们通过自定义一个结构体探索

    struct js_struct{
        NSNumber *num1;
        NSNumber *num2;
    } js_struct;
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        JSPerson *person1 = [JSPerson alloc];
        struct js_struct jst = {@(1),@(3)};
        JSPerson *person = [JSPerson alloc];
        [person saySomething];
        
        Class cls = [JSPerson class];
        void  *js = &cls;
        [(__bridge id)js saySomething];
    }

我们在图示位置添加断点调试

Objective-C底层面试题 使用lldb调试

  (lldb) p &person1
  (JSPerson **) $0 = 0x00007ffeed1d8118
  (lldb) p &jst
  (js_struct *) $1 = 0x00007ffeed1d8108
  (lldb) p &person
  (JSPerson **) $2 = 0x00007ffeed1d8100
  (lldb) p jst.num1
  (__NSCFNumber *) $3 = 0xbab63c269bab4904 (int)1
  (lldb) p &$3
  (NSNumber **) $4 = 0x00007ffeed1d8108
  (lldb) p jst.num2
  (__NSCFNumber *) $5 = 0xbab63c269bab4924 (int)3
  (lldb) p &$5
  (NSNumber **) $6 = 0x00007ffeed1d8110

发现num1的地址<num2的地址,说明num2先入栈。也就是结构体是从后向前入栈的

  • 总结来说题中压栈的顺序是self->_cmd->superClass->self->person->cls->js。地址空间是由高到低。所以这个地方js向高地址平移8字节找到的是person也就是打印是<JSPerson: 0x600003a00380>的原因。
转载自:https://juejin.cn/post/6988688944154034190
评论
请登录