天天看点

iOS-底层原理 20:OC底层面试解析

iOS 底层原理 文章汇总

【面试-1】Runtime Asssociate方法关联的对象,需要在dealloc中释放?

当我们对象释放时,会调用

dealloc

  • 1、C++函数释放 :

    objc_cxxDestruct

  • 2、移除关联属性:

    _object_remove_assocations

  • 3、将弱引用自动设置nil:

    weak_clear_no_lock(&table.weak_table, (id)this);

  • 4、引用计数处理:

    table.refcnts.erase(this)

  • 5、销毁对象:

    free(obj)

所以,

关联对象

不需要我们手动移除,会在对象析构即

dealloc

时释放

dealloc 源码

dealloc的源码查找路径为:

dealloc

->

_objc_rootDealloc

->

rootDealloc

->

object_dispose

(释放对象)->

objc_destructInstance

->

_object_remove_assocations

  • 在objc源码中搜索

    dealloc

    的源码实现
    iOS-底层原理 20:OC底层面试解析
  • 进入

    _objc_rootDealloc

    源码实现,主要是对

    对象进行析构

    iOS-底层原理 20:OC底层面试解析
  • 进入

    rootDealloc

    源码实现,发现其中有

    关联属性时设置bool值

    ,当有这些条件时,需要进入else流程
    iOS-底层原理 20:OC底层面试解析
  • 进入

    object_dispose

    源码实现,主要是

    销毁实例对象

    iOS-底层原理 20:OC底层面试解析
  • 进入

    objc_destructInstance

    源码实现,在这里有移除关联属性的方法
    iOS-底层原理 20:OC底层面试解析
  • 进入

    _object_remove_assocations

    源码,关联属性的移除,主要是

    从全局哈希map中找到相关对象的迭代器,然后将迭代器中关联属性,从头到尾的移除

    iOS-底层原理 20:OC底层面试解析

【面试-2】方法的调用顺序

类的方法 和 分类方法 重名,如果调用,是什么情况?

  • 如果同名方法是

    普通方法

    ,包括

    initialize

    – 先调用分类方法
    • 因为

      分类的方法是在类realize之后 attach进去的

      ,插在类的方法的前面,所以

      优先调用分类的方法

      (注意:不是分类覆盖主类!!)
    • initialize

      方法什么时候调用?

      initialize

      方法也是主动调用,即

      第一次消息时

      调用,为了不影响整个load,可以将需要

      提前加载的数据

      写到

      initialize

  • 如果同名方法是

    load

    方法 – 先

    主类load

    ,后

    分类load

    (分类之间,看编译的顺序)
    • 原因:参考iOS-底层原理 18:类的加载(下)文章中的

      load_images

      原理分析
iOS-底层原理 20:OC底层面试解析

【面试-3】Runtime是什么?

  • runtime

    是由

    C和C++

    汇编实现的一套

    API

    ,为OC语言加入了

    面向对象、以及运行时的功能

  • 运行时是指将

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

    • 举例:extension 和 category 的区别
  • 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtime的C语言代码,

    runtime是OC的幕后工作者

1、category 类别、分类

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

  • 不能给类添加成员属性

    ,添加了成员属性,也无法取到
  • 注意:其实

    可以通过runtime 给分类添加属性

    ,即属性关联,重写setter、getter方法
  • 分类中用

    @property

    定义变量,

    只会生成

    变量的

    setter、getter

    方法的

    声明

    不能生成方法实现 和 带下划线的成员变量

2、extension 类扩展

  • 可以说成是

    特殊的分类

    ,也可称作

    匿名分类

  • 可以

    给类添加成员属性

    ,但

    是是私有变量

  • 可以

    给类添加方法

    ,也

    是私有方法

【面试-4】方法的本质,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、通过页码去翻到具体的内容

【面试-5】能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量

  • 1、

    不能

    向编译后的得到的类中增加实例变量
  • 2、

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

  • 3、可以

    添加属性+方法

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

【经典面试-6】 [self class]和[super class]的区别以及原理分析

  • [self class]

    就是发送消息

    objc_msgSend

    ,消息接收者是

    self

    ,方法编号

    class

  • [super class]

    本质就是

    objc_msgSendSuper

    ,消息的接收者还是

    self

    ,方法编号

    class

    ,在运行时,底层调用的是

    _objc_msgSendSuper2

    【重点!!!】
  • 只是

    objc_msgSendSuper2

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

代码调试

  • LGTeacher

    中的

    init

    方法中打印这两种class调用
    iOS-底层原理 20:OC底层面试解析
    运行程序,打印结果如下
    iOS-底层原理 20:OC底层面试解析
  • 进入

    [self class]

    中的

    class

    源码
- (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

    是语法的

    关键字

    ,可以通过

    clang

    super

    的本质,这是

    编译时

    的底层源码,其中第一个参数是消息接收者,是一个

    __rw_objc_super

    结构
    iOS-底层原理 20:OC底层面试解析
    • 底层源码中搜索

      __rw_objc_super

      ,是一个中间结构体
      iOS-底层原理 20:OC底层面试解析
    • objc中搜索

      objc_msgSendSuper

      ,查看其隐藏参数
      iOS-底层原理 20:OC底层面试解析
    • 搜索

      struct objc_super

      iOS-底层原理 20:OC底层面试解析
      通过

      clang

      的底层编译代码可知,当前

      消息的接收者

      等于

      self

      ,而

      self

      等于

      LGTeacher

      ,所以

      [super class]

      进入

      class

      方法源码后,其中的

      self

      即为

      LGTeacher

      ,所以最后还是

      获取LGTeacher的isa

      ,即

      同名LGTeacher元类

  • 我们再来看[super class]在运行时是否如上一步的底层编码所示,是

    objc_msgSendSuper

    ,打开汇编调试,调试结果如下
    iOS-底层原理 20:OC底层面试解析
    • 搜索

      objc_msgSendSuper2

      ,从注释得知,是

      从 类开始查找

      ,而不是父类
      iOS-底层原理 20:OC底层面试解析
    • 查看

      objc_msgSendSuper2

      的汇编源码,是从

      superclass

      中的

      cache

      中查找方法
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

  • [class class]

    打印的是

    LGTeacher

    ,原因是当前的super是一个关键字,在这里只调用

    objc_msgSendSuper2

    ,其实他的消息接收者和

    [self class]

    是一模一样的,所以返回的是LGTeacher

【面试-7】内存平移问题

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

LGPerson中有一个属性

kc_name

和一个实例方法

saySomething

,通过上面代码这种方式,能否调用实例方法?为什么?

代码调试

  • 我们在日常开发中的调用方式是下面这种
LGPerson *person = [LGPerson alloc];
[person saySomething];
           
  • 通过运行发现,是可以执行的,打印结果如下
    iOS-底层原理 20:OC底层面试解析
  • [person saySomething]

    的本质是

    对象发送消息

    ,那么当前的person是什么?
    • person

      isa

      指向类

      LGPerson

      person的首地址 指向 LGPerson的首地址

      ,我们可以通过LGPerson的内存平移找到

      cache

      ,在cache中查找方法
      iOS-底层原理 20:OC底层面试解析
  • [(__bridge id)kc saySomething]

    中的

    kc

    是来自于

    LGPerson

    这个类,然后有一个指针

    kc

    ,将其

    指向LGPerson的首地址

    iOS-底层原理 20:OC底层面试解析

所以,

person

是指向

LGPerson

类的结构,

kc

也是指向

LGPerson

类的结构,然后都是在

LGPerson

中的

methodList

中查找方法

iOS-底层原理 20:OC底层面试解析

修改:saySomething里面有属性 self.kc_name 的打印

代码如下所示

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

//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void  *kc = &cls; 
[(__bridge id)kc saySomething]; 
 
//方式二:常规调用
LGPerson *person = [LGPerson alloc];
 [person saySomething];
           
  • 查看这两种调用方式的打印结果,如下所示
    • kc

      方式的调用打印的

      kc_name

      <ViewController: 0x7fe29170b560>

    • person

      方式的调用打印的

      kc_name

      (null)

      iOS-底层原理 20:OC底层面试解析

为什么会出现打印不一致的情况?

  • 其中person方式的

    kc_name

    是由于

    self指向person的内存结构

    ,然后通过

    内存平移8字节,取出去kc_name

    ,即

    self指针首地址平移8字节获得

    iOS-底层原理 20:OC底层面试解析
  • 【方式一】其中

    kc

    指针中没有任何,所以

    kc表示8字节指针

    self.kc_name

    的获取,相当于

    kc首地址的指针也需要平移8字节找kc_name

    ,那么此时的kc的指针地址是多少?平移8字节获取的是什么?
    • kc

      是一个指针,是存在

      中的,栈是一个

      先进后出

      的结构,参数传入就是一个不断压栈的过程,
      • 其中

        隐藏参数会压入栈

        ,且每个函数都会有两个隐藏参数

        (id self,sel _cmd)

        ,可以通过

        clang

        查看底层编译
      • 隐藏参数压栈

        的过程,其地址是

        递减

        的,而

        栈是从高地址->低地址 分配

        的,即

        在栈中,参数会从前往后一直压

      • super通过clang查看底层的编译,是

        objc_msgSendSuper

        ,其第一个参数是一个结构体

        __rw_objc_super(self,class_getSuperclass)

        ,那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况
        • p &person3

        • p *(NSNumber **)0x00007ffee83a8090

        • p *(NSNumber **)0x00007ffee83a8098

          iOS-底层原理 20:OC底层面试解析
          所以图中可以得出 20先加入,再加入10,因此

          结构体内部

          的压栈情况是

          低地址->高地址

          递增

          的,栈中

          结构体内部

          的成员是

          反向

          压入栈,即

          低地址->高地址

          ,是递增的,
  • 所以到目前为止,栈中

    从高地址到低地址

    的顺序的:

    self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person

    • self

      _cmd

      viewDidLoad

      方法的两个隐藏参数,是高地址->低地址

      正向压栈

    • class_getSuperClass

      self

      objc_msgSendSuper2

      中的结构体成员,是从最后一个成员变量,即低地址->高地址

      反向压栈

可以通过下面这段代码打印下栈的存储是否如上面所说

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

运行结果如下

iOS-底层原理 20:OC底层面试解析

其中为什么

class_getSuperclass

ViewController

,因为

objc_msgSendSuper2

返回的是

当前类

,两个

self

,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间

  • [(__bridge id)kc saySomething]

    调用时,此时的kc是

    LGPerson: 0x7ffeec381098

    ,所以

    saySomething

    方法中传入的

    self

    还是LGPerson,但并不是我们通常认为的LGPerson,使我们当前

    传入的消息接收者

    ,即

    LGPerson: 0x7ffeec381098

    ,是LGPerson的实例对象,此时的操作与普通的LGPerson是一致的,即

    LGPerson的地址内存平移8字节

    • 普通person流程:

      person -> kc_name - 内存平移8字节

    • kc流程:

      0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0

      ,即为

      self

      ,指向

      <ViewController: 0x7fac45514f50>

      ,如下图所示
      iOS-底层原理 20:OC底层面试解析

其中

person

LGPerson

的关系是

person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isa,isa指向LGPerson

,它们之间关联是有一个

isa指向

而kc也是指向LGPerson的关系,编译器会认为

kc也是LGPerson的一个实例化对象

,即

kc相当于isa,即首地址,指向LGPerson

,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即

kc

也有

kc_name

。由于

person查找kc_name是通过内存平移8字节

,所以kc也是通过内存平移8字节去查找kc_name

哪些东西在栈里 哪些在堆里

  • alloc

    的对象 都在

  • 指针、对象

    中,例如

    person指向的空间

    中,person所在的空间在栈中
  • 临时变量

  • 属性值

    ,属性随对象是在

注意:
  • 是从小到大,即低地址->高地址
  • 栈是从大到小,即从高地址->低地址分配
    • 函数隐藏参数会

      从前往后

      一直压,即

      从高地址->低地址 开始入栈

    • 结构体内部的成员是

      从低地址->高地址

  • 一般情况下,内存地址有如下规则
    • 0x60

      开头表示在

    • 0x70

      开头的地址表示在

    • 0x10

      开头的地址表示在

      全局区域

### 【面试-8】 Runtime是如何实现weak的,为什么可以自动置nil

  • 1、通过

    SideTable

    找到我们的

    weak_table

  • 2、

    weak_table

    根据

    referent

    找到或者创建

    weak_entry_t

  • 3、然后

    append_referrer(entry,referrer)

    将我的新弱引用的对象加进去entry
  • 4、最后

    weak_entry_insert

    ,把

    entry

    加入到我们的

    weak_table

底层源码调用流程如下图所示

iOS-底层原理 20:OC底层面试解析

继续阅读