iOS 底层面试题

Posted WeaterMr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 底层面试题相关的知识,希望对你有一定的参考价值。

一,Runtime是什么?

runtime是由CC++汇编实现的一套API,为OC语言加入了 面向对象、以及运行时的功能

运行时是指将数据类型的确定由编译时 推迟到了 运行时

举例:extensioncategory 的区别
平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtimeC语言代码, runtime是OC的幕后工作者

1、category 类别、分类

专门用来给类添加新的方法

不能给类添加成员属性

注意:其实可以通过runtime 给分类添加属性,即属性关联,重写setter、getter方法
分类中用@property 定义变量,只会生成变量的setter、getter方法的声明,不能生成方法实现 和 带下划线的成员变量

2、extension 类扩展

可以说成是特殊的分类 ,也可称作 匿名分类

可以给类添加成员属性,但是是私有变量

可以给类添加方法,也是私有方法

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

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

  • 慢速查找 - 递归自己|父类 - lookUpImpOrForward

  • 查找不到消息:动态方法解析- resolveInstanceMethod

  • 消息快速转发 - forwardingTargetForSelector

  • 消息慢速转发 - methodSignatureForSelector & forwardInvocation

  • sel是方法编号 - 在read_images期间就编译进了内存

+imp是函数实现指针 ,找imp就是找函数的过程

  • sel 相当于 一本书的目录title

  • sel相当于 书本的页码

查找具体的函数就是想看这本书具体篇章的内容

1、首先知道想看什么,即目录 title - sel

2、根据目录找到对应的页码 - imp

3、通过页码去翻到具体的内容

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

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

2、只要类没有注册到内存还是可以添加的

3、可以添加属性+方法

【原因】:编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就完全确定了

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

[self class]就是发送消息 objc_msgSend,消息接收者是self,方法编号 class

[super class] 本质就是objc_msgSendSuper,消息的接收者还是 self,方法编号 class,在运行时,底层调用的是_objc_msgSendSuper2【重点!!!】

只是 objc_msgSendSuper2 会更快,直接跳过self的查找

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


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

  • 其底层是获取对象的isa,当前的对象是LGTeacher,其isa是同名的LGTeacher,所以[self class]打印的是LGTeacher
  • [super class]中,其中super 是语法的 关键字,
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这里我们可以看到, objc_msgSendSuper 的第一个隐藏参数是一个结构体。
可以通过clang 看super的本质,这是编译时的底层源码,当我们clang 发现是调用的objc_msgSendSuper 个函数,但是当我们通过汇编跑流程其实实际上是调用objc_msgSendSuper2
通过clang ,底层源码中搜索__rw_objc_super,是一个中间结构体 临时变量

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame

ldp	p0, p16, [x0]		// p0 = real receiver, p16 = class 取出receiver 和 class
ldr	p16, [x16, #SUPERCLASS]	// p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2//cache中查找--快速查找

END_ENTRY _objc_msgSendSuper2

结论:

[self class]方法调用的本质是 发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_images的readClass时已经加入表中,所以打印为LGTeacher

[super class]打印的是LGTeacher,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2,其实他的消息接收者和[self class]是一模一样的,所以返回的是LGTeacher

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

补充:这里的 self = [super init]; 主要作用是初始化一些相关的数据,目的是在实例化对象后自带一些特性。

五,内存平移问题

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

以上两种方式都能调用saySomething,这里的saySomething 是实例方法。
那为什么能够调用成功能,这里又回归到方法调用的本质,即消息发送并没有区分对象和类方法。
如何更好的理解这里的原理,首相我们先抛开对象和类的概念。类是什么,我是这样比喻的,类似于工厂制作零件的模具,生产不同的零部件,需要不同的模具,当我们想要一个person的模型时就找到对应的LGPerson 模具,通过初始化得到带有空间的person模型。有一点要提到的是,LGPerson *person这只是一个指针指向person模型的指针。我们所用的模型的特性都是从模具获得的,这里为啥没有说复制,因为你可以想象一下,我们每当需要一个模型都复制一个磨具,是不是造成很大的空间浪费,所以对于所有不同的对象都公用一个类磨具,也即是当我们调用方法,其实是调用类中的相关函数。其实我们在alloc本质中也提到,对象的初始化其实就是关联对应的类,然后根据成员变量申请一定的内存空间,用来做自己的扩展。
另外当我们给对应的对象添加属性时,会发现也通过类去取对应的属性值时并不为空。这是为什么呢?
这里又牵扯到一个知识点,当属性修饰不是通过copy修饰的时候,成员变量取值是通过内存平移的方式取值。

    Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];
    NSLog(@"%p - %p",&person,kc);
    // 隐藏参数 会压入栈帧
    void *sp  = (void *)&self;
    void *end = (void *)&person;
    long count = (sp - end) / 0x8;
    
    for (long i = 0; i<count; i++) {
        void *address = sp - 0x8 * i;
        if ( i == 1) {
            NSLog(@"%p : %s",address, *(char **)address);
        }else{
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
    
    
    // LGPerson  - 0x7ffeea0c50f8
    [(__bridge id)kc saySomething]; // 1 2  - <ViewController: 0x7f7f7ec09490>
    
    [person saySomething]; // self.kc_name = nil - (null)

 0x16f11db28 - 0x16f11db38
 0x16f11db58 : <ViewController: 0x14be0b2c0>
 0x16f11db50 : viewDidLoad
 0x16f11db48 : ViewController
 0x16f11db40 : <ViewController: 0x14be0b2c0>
 0x16f11db38 : LGPerson
 0x16f11db30 : <LGPerson: 0x16f11db38>
 -[LGPerson saySomething] - <ViewController: 0x14be0b2c0>
 -[LGPerson saySomething] - (null)

总结:

  • 在打印过程中要打印地址,如果直接打印可能为nil因为类型不同,需要类型强转
  • 是从小到大,即低地址->高地址
  • 是从大到小,即从高地址->低地址分配
  • 函数隐藏参数会从前往后一直压,即 从高地址->低地址 开始入栈,
  • 结构体内部的成员是从低地址->高地址

扩展1:

  • 0x60 开头表示在 堆中
  • 0x70 开头的地址表示在 栈中
  • 0x10 开头的地址表示在全局区域中

扩展2:

  • 我们也可以通过走汇编流程,下符号断点,读取当前寄存器中的值,然后将对应的内存中的数据进行强制类型转换。来验证当前的值。

扩展3:哪些东西在栈里 哪些在堆里

  • alloc的对象 都在堆中
  • 指针、对象 在栈中,例如person指向的空间在堆中,person指针在栈中
  • 临时变量在栈中
  • 成员变量值 在堆,成员变量的指针在栈中

以上是关于iOS 底层面试题的主要内容,如果未能解决你的问题,请参考以下文章

iOS 底层面试题

iOS底层面试题(上篇)

iOS底层面试题(中篇)

iOS一道复合型面试题与底层原理

iOS底层面试题(下篇)

精选面试题教你应对高级iOS开发面试官(提供底层进阶规划蓝图)