天天看點

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底層面試解析

繼續閱讀