iOS底层面试题

runtime的学习整理

对象

消息

应用程序加载、类、分类初始化

相关面试题

1. loadinitialize方法的调用原则和调用顺序?

  • load方法

    • load方法在应用程序加载过程中(dyld)完成调用,在main函数之前
    • 在底层进行load_images处理时,维护了两个load加载表,一个的表,另一个为分类的表,优先对类的load方法发起调用
    • 在对类load方法进行处理时,进行了递归处理,以确保父类优先被处理
    • 所以load方法的调用顺序为父类子类分类
    • 分类load方法的调用顺序根据编译顺序为准
  • initialize方法

    • initialize第一次消息发送的时候调用,所以load先于initialize调用
    • 分类的⽅法是在类realize之后attach进去的插在前⾯,所以如果分类中实现了initialize方法,会优先调⽤分类的initialize方法
    • initialize内部实现原理是消息发送,所以如果子类没有实现initialize会调用父类的initialize方法,并且会调用两次
    • 因为内部同时使用了递归,所以如果子类父类都实现了initialize方法,那么会优先调用父类的,再调用子类

具体的实现以及底层逻辑在类的加载(上)-- _objc_init&read_images
中。

补充c++构造函数

  • 在分析dyld之后,可以确定这样的一个调用顺序,load->c++->main函数
  • 但是如果c++写在objc工程中,在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_registerdyld注册load_images之后再调用
  • 同时,如果objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况

2.Runtime是什么?

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

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

  • ⽅法的本质:发送消息,消息会有以下⼏个流程:
    • 快速查找objc_msgSend)~ cache_t 缓存消息
    • 慢速查找~ 递归⾃⼰或⽗类 ~ lookUpImpOrForward
    • 查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod
    • 消息快速转发 ~ forwardingTargetForSelector
    • 消息慢速转发 ~ methodSignatureForSelectorforwardInvocation
  • sel⽅法编号,在read_images期间就编译进⼊了内存
    • sel的内存结构:typedef struct objc_selector *SEL;
  • imp就是我们函数实现指针,找imp就是找函数的过程
  • sel就相当于书本的⽬录tittleimp就是书本的⻚码

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

  • 不能向编译后的得到的类中增加实例变量
    • 编译好的实例变量存储的位置在ro,⼀旦编译完成,内存结构就完全确定
    • 可以通过分类向类中添加方法属性关联对象
  • 可以运行时创建的类中添加实例变量,只要内没有注册到内存还是可以添加
    • 可以通过objc_allocateClassPair在运行时创建类,并向其中添加成员变量和属性,见下面代码:
// 使用objc_allocateClassPair创建一个类Class
const char * className = "SelClass";
Class SelfClass = objc_getClass(className);
if (!SelfClass){
    Class superClass = [NSObject class];
    SelfClass = objc_allocateClassPair(superClass, className, 0);
}
        
// 使用class_addIvar添加一个成员变量
BOOL isSuccess = class_addIvar(SelfClass, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *));

class_addMethod(SelfClass, @selector(addMethodForMyClass:), (IMP)addMethodForMyClass, "V@:");
     

5.[self class][super class]区别和解析?
通过代码案例分析这个问题,首先创建LGTeacher继承LGPerson,并在LGTeacherinit初始化方法中,调用了[self class]和[super class],查看输出结果。

    // LGPerson
    @interface LGPerson : NSObject
    @end

    @implementation LGPerson
    @end
    
    // LGTeacher
    @interface LGTeacher : LGPerson
    @end

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

案例分析:
很清楚LGTeaherLGPerson都没有实现class方法,那么根据消息发送的原理,他们最终都会调用到NSObject的实例方法class,该方法实现如下:

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

调用方法的本质是发送消息objc_msgSend,并且有两个隐藏参数,分别是id selfSEL sel,这里的隐藏参数self就是我们要分析的类型。

  • [self class]输出是LGTeacher,这个没有什么问题!因为消息的发送者是LGTeacher对象,通过消息发送机制,找到NSObejct并调用class方法,但是消息的接受者没有发生改变,依然是LGTeacher对象!
  • [super class]就不一样了,同过xcrun查看main.cpp文件,查看底层源码得出以下:
    main.cpp查看super底层

    super关键字,在底层最终使用了objc_msgSendSuper方法,同时其接受者是(id)self,全局搜搜objc_msgSendSuper的逻辑,见下图:
    objc_msgSendSuper

    根据 Objc-818.2源码查看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 */
};

可以看到结构体中只有两个参数,分别是id receiverClass super_class,其中super_class表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiverLGTeacher对象,super_classLGTeacher类通过class_getsuperclass获取的父类,也就是要第一个查找的类。

通过下符号断点--objc_msgSendSuper2,查看寄存器,其中第一个地址为发放的第一个隐藏参数,也就是objc_super,通过类型强制,该结构体封装的recevierLGTeachersuper_classLGPerson,具体看下图:

查看寄存器

得出结论:[super class]的接收者依然是LGTeacher对象,去调用父类的方法。

最后查看运行结果:

查看运行结果

果然输出都是LGTeacher!!!

补充:objc_msgSendSuper为什么会调用到了objc_msgSendSuper2
通过 Objc-818.2源码查看的出:


全局搜索objc_msgSendSuper,进入汇编实现流程中,在汇编流程中,最终会调用objc_msgSendSuper2,见下图:
调用objc_msgSendSuper2

注意:这题还不够明白的话建议参考以上的消息相关文章,写得比较详细哦。

5.指针平移和消息发送原理案例分析
LGPerson类有一个实例方法saySomething,在viewDidLoad中通过两种方式调用该方法,一种是通过创建LGPerson对象调用,另一种是通过桥接调用,见下面代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [LGPerson alloc];
    [person saySomething];
    
    Class cls = [LGPerson class];
    void  *kc = &cls;
    [(__bridge id)kc saySomething];
}

@implementation LGPerson
- (void)saySomething{
    NSLog(@"%s - %@",__func__);
}
@end

问题1:是否能够调用成功?

  • 方法调用的本质是发送消息,通过对象的isa找到地址,进行地址平移,通过sel找到对应的方法实现imp
    • 毋庸置疑,person saySomething];此种方式肯定是没问题的
      通过person对象的isa指针找到对应的类,在类中进行地址平移,首先在
      cache_t快速查找,如果找不到,则在方法列表以及父类的方法列表中查找,总结一下就是:以类的地址作为入口,进行地址平移,最终找到对应的imp

    • [(__bridge id)kc saySomething];是否可以呢?
      首先Class cls = [LGPerson class];cls是什么?cls是一个指针,Class的定义是一个指针,指向一个objc_class的指针,这里就是指向LGPerson类。将cls的地址赋值给kc,此时kccls的地址,也指向了

分析得出:两者调用的入口是一致的,从同一个地址开始进行方法查找流程,肯定是可以调用到的,person除了有地址,还有内存数据结构kc只有一个地址,是一个伪装的person对象。请看下图:

查找流程

通过lldb调试可以发现,kc指向类,见下图:

查看kc指向class

最后运行代码:


运行代码

案例扩展
LGPerson中添加一个属性kc_name,实现代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    LGPerson *person = [LGPerson alloc];
    person.kc_name = @"name123";
    [person saySomething];

    Class cls = [LGPerson class];
    void  *kc = &cls;
    [(__bridge id)kc saySomething];
}

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
- (void)saySomething;
@end

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

那么这样子的输出结果优势怎么样子呢?是不是跟上面的一样都能输出呢?哈哈,以下继续进行lldb调试,请继续走!

lldb调试

经过调试可以知道person进行地址平移获取属性kc_name,此数据结构是在中,而kc只是一个地址,获取kc数据结构只是输出了其在中的数据信息。

引申出压栈的概念
通过上面的案例分析,可以知道根本原因是栈中地址平移的问题,那么在程序运行过程中,压栈逻辑是怎样的呢?先入后出,这个比较清楚,那结构体是如何压栈的呢,函数调用中参数的压栈逻辑又是怎样的?

  • 压栈,地址从大到小,先进去的地址大(栈开辟由高地址到低地址
    image.png
  • 添加结构体,查看栈中的地址
    查看栈中的地址

    添加完结构体后,通过lldb的出下图:
    压栈过程

    明显看出结构体占用了16字节,那么结构体内容在栈中的位置是怎么样子的呢?继续进行lldb调试:
    查看结构体栈中布局

    通过lldb输出结构体中两个属性的地址,发现,num1num2的上面,所以在压栈过程中,按照下图中的方式进行的:
    具体压栈过程

函数参数压栈顺序
通过下面的案例代码进行进一步探索:


有上面可以得出:

  • viewDidLoad方法中person指针的地址和kcFunctionperson指针地址是不一样的,虽然他们都执行了同一片堆区
  • 根据指针的地址发现,参数在压栈时是根据参数的顺序进行的,第一个参数先入栈,然后依次压栈

补充:runtime面试题持续更新中哦。。。敬请期待!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容