iOS底層原理探索 一 類結構分析

iOS底層原理探索篇 主要是圍繞底層進行源碼分析-LLDB調試-源碼斷點-彙編調試,讓本身之後回顧複習的😀😀數組

目錄以下:緩存

iOS底層原理探索 — 開篇bash

iOS底層原理探索 — alloc&init探索iphone

iOS底層原理探索 — 內存對齊&malloc源碼分析ide

iOS底層原理探索 一 isa原理與對象的本質函數

iOS底層原理探索 一 類結構分析源碼分析

1、章前複習

經過前面篇章的探索,咱們已成功的從對象過渡到類了.但在探索類以前,還須要補充一下咱們在前面篇章中沒有細講的一些小細節.post

1.1 alloc的一個小細節

咱們在iOS底層原理探索 — alloc&init探索一文中留下了一個細節沒有細說,就是在分析alloc源碼分析流程的時候,在調用callAlloc方法時,咱們只是簡單的說了:此方法內部有一系列的判斷條件,其中因爲方法canAllocFast()的內部調用了bits.canAllocFast(),其返回值爲固定值false,因此能夠肯定以後建立對象只會走class_createInstance方法.即:callAllocif (fastpath(cls->canAllocFast()))方法不走直接走的else後面的代碼.那麼爲何會這樣呢?來看源碼:測試

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__ //這個表示object-c 2.0 版本纔有的功能
    /*
        這裏的hasDefaultAWZ()方法是用來判斷當前class是否有默認的allocWithZone。
        if (fastpath(!cls->ISA()->hasCustomAWZ())):
        意思就是若是該類實現了allocWithZone方法,那麼就不會走if裏的邏輯,直接走如下邏輯
        if (allocWithZone) return [cls allocWithZone:nil];
     */
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);//initInstanceIsa 裏面是初始化 isa 指針的操做。
            return obj;
        }
        else {
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
複製代碼
  • 第一個判斷fastpath(!cls->ISA()->hasCustomAWZ())的決定條件就是你是否有重寫allocWithZone的方法:即if (fastpath(!cls->ISA()->hasCustomAWZ())):意思就是若是該類實現了allocWithZone方法,那麼就不會走if裏的邏輯,直接走if (allocWithZone) return [cls allocWithZone:nil];
  • 第二個判斷fastpath(cls->canAllocFast())就是關於宏定義的設置:咱們沿着源碼點進去能夠看到:
    bool canAllocFast() {
          assert(!isFuture());
          return bits.canAllocFast();
      }
    複製代碼
    順着bits.canAllocFast();點進去能夠看到:
    #if FAST_ALLOC
      size_t fastInstanceSize() 
      {
          assert(bits & FAST_ALLOC);
          return (bits >> FAST_SHIFTED_SIZE_SHIFT) * 16;
      }
      void setFastInstanceSize(size_t newSize) 
      {
          // Set during realization or construction only. No locking needed.
          assert(data()->flags & RW_REALIZING);
    
          // Round up to 16-byte boundary, then divide to get 16-byte units
          newSize = ((newSize + 15) & ~15) / 16;
          
          uintptr_t newBits = newSize << FAST_SHIFTED_SIZE_SHIFT;
          if ((newBits >> FAST_SHIFTED_SIZE_SHIFT) == newSize) {
              int shift = WORD_BITS - FAST_SHIFTED_SIZE_SHIFT;
              uintptr_t oldBits = (bits << shift) >> shift;
              if ((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) {
                  newBits |= FAST_ALLOC;
              }
              bits = oldBits | newBits;
          }
      }
    
      bool canAllocFast() {
          return bits & FAST_ALLOC;
      }
      #else // 通常都會走這裏
      size_t fastInstanceSize() {
          abort();
      }
      void setFastInstanceSize(size_t) {
          // nothing
      }
      // 通常流程都會走這個false的返回
      bool canAllocFast() {
          return false;
      }
      #endif
    複製代碼
    通常都會走#else後面的代碼,也就是bool canAllocFast(){return false}.爲何會這樣呢?,這就要去看條件控制:#if FAST_ALLOC這個宏定義的走向了. 在全局搜索宏定義FAST_ALLOC,發現#define FAST_ALLOC (1UL<<2)而這個宏定義外面還加了一層條件判斷:
#if !__LP64__
   ...
   #elif 1
   ...
   #else
   ...
   #define FAST_ALLOC (1UL<<2)
   #endif
複製代碼

由於咱們的環境都是在64位環境下,因此能夠判斷上面的判斷只會走#elif 1裏面的代碼,而#define FAST_ALLOC的定義是在#else裏面,即FAST_ALLOC永遠都不會define了.即只會走bool canAllocFast(){return false},進而就有callAllocif (fastpath(cls->canAllocFast()))方法不走,直接走的else{}裏面的代碼.即走的下面紅框裏面的代碼 優化

1.2 聯合體互斥

咱們在iOS底層原理探索 一 isa原理與對象的本質一文中有分析到,isa的結構實際上是一個聯合體,而聯合體有一大特性,就是其內部屬性是共享同一片內存的,也就是說屬性之間都是互斥的.

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製代碼

所以也就能解釋初始化isa的時候,一個分支是賦值cls屬性,一個分支是賦值bits屬性了.

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}
複製代碼

2、類和元類的建立時機

咱們在探索類和元類的時候,對於其建立時機還不是很清楚,這裏咱們先拋出結論:類和元類是在編譯期建立的,即在進行alloc操做以前,類和元類就已經被編譯器建立出來了. 那麼如何來證實呢,咱們有兩種方式來證實:

2.1 經過LLDB指令打印類和元類指針

咱們在 main函數開始以前打上斷點,也就沒有來到 TCJPerson *obj = [TCJPerson alloc];,可是咱們經過 LLDB能打印出 TCJPerson的類和元類.這就證實了,類和元類的建立時機是在編譯期.

2.2 經過MachoView軟件輔助證實:

MachoView 密碼:kx8c 編譯項目後,使用MachoView打開程序二進制可執行文件查看:

經過上面兩種方式證實了:類和元類的建立時機是在編譯期.

3、指針內存偏移

3.1 普通指針 - 值拷貝

咱們觀察上面的代碼,雖然整型變量 ab都是被賦值爲10,可是 ab內存地址是不同的,這種方式被稱爲 值拷貝.

3.2 對象 - 指針拷貝或引用拷貝

經過運行結果,能夠知道 obj1obj2對象不光自身內存地址不同,連指向的對象的內存地址也不同,這種方式被稱爲 指針拷貝引用拷貝.

咱們能夠用一幅圖來總結上面的兩個例子:

3.3 用數組指針引出 - 內存偏移

經過運行結果能夠看到:

  • &a&a[0]的地址是相同的.即首地址就表明數組的第一個元素的地址.
  • 第一個元素地址0x7ffeefbff400和第二個元素地址0x7ffeefbff404相差4個字節,也就是int的所佔的4字節.
  • dd+1d+2這個地方的指針相加就是偏移地址.地址加1就是偏移,偏移一個位數所在元素的大小.
  • 能夠經過地址,取出對應地址的值.

4、類的結構分析

OC中的類其實也是一種對象,怎麼來證實呢,很簡單,咱們只須要用clang命令重寫咱們的OC代碼將其轉化爲C++代碼看其底層便可.

4.1 建立TCJPerson對象,並獲取到TCJPerson的類,而後利用LLDB指令查看

經過上面結構能夠得知:

  • 輸出第二個內存地址獲得NSObject,繼續輸出第三個發現輸出不了.
  • 經過前面iOS底層原理探索 一 isa原理與對象的本質一文的分析,咱們知道第二個內存地址存儲的是Class superclass,它表明的是繼承關係,也即證實了TCJPerson是繼承自NSObject的.

4.2 將OC代碼轉化爲C++代碼幫助分析

原文件main.c:

#import <Foundation/Foundation.h>
    #import <objc/runtime.h>

    @interface TCJPerson : NSObject

    @end

    @implementation TCJPerson

    @end

    int main(int argc, const char * argv[]) {
        @autoreleasepool {

            TCJPerson *obj = [TCJPerson alloc];
            Class objClass = object_getClass(obj);
            NSLog(@"%@ - %p", obj, objClass); //0x00007ffffffffff8ULL
        }
        return 0;
    }
複製代碼

在終端執行clang指令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
複製代碼

便可將OC原文件main.c轉化爲C++文件mian.cpp文件後可看到:

至此,咱們能夠得出一個結論,Class類型在底層是一個結構體類型的指針,這個結構體類型爲objc_classs. 咱們再在libObjc的源碼中能夠找到objc_classs的詳細定義:

經過 objc_classs的定義,咱們能夠知道, objc_classs是繼承於 objc_object的.這就證實了 萬物皆對象,也從本質上說明 類是一種對象,而且第一個屬性是從 objc_object上繼承而來的 isa. 除了 isa,類還包含了 superclass父類:表達繼承關係; cache:方法緩存重要結構體; bits:存儲數據的結構體.

至此咱們能夠總結得出:

  • 類是一種對象,而且幫咱們定義了一些屬性和方法.

  • OC是對C的底層封裝,進而有下面的關係:

    C OC
    objc_object NSObject
    objc_class NSObject(Class)

最後咱們知道Class的基本結構類型爲:

到這有一個疑問:爲何在外面 isaClass?

  • 萬物皆對象,isa是能夠由Class接收的.
  • 早期調用isa是用來返回類的,後面是經過nonpointer區分純淨isa和優化的isa.
  • 用源碼查看有:return (Class)(isa.bits & ISA_MASK),進行了Class類型強轉.

5、類的屬性存儲探索

OC中的類都會有屬性及成員變量,那麼它們到底是怎麼存在於類裏面的呢?

5.1 類結構的isa、superclass、cache屬性

這裏咱們須要對類的內存結構有一個比較清晰的認識:

類的內存結構 大小(字節)
isa 8
superclass 8
cache 16

前面兩個的大小很好理解,由於isasuperclass都是結構體指針,而在arm64環境下,一個結構體指針的內存佔用大小爲8字節.而第三個屬性cache則須要咱們進行抽絲剝繭了. 來看源碼:

從上面的代碼咱們能夠看出, cache屬性實際上是 cache_t類型的結構體,其內部有一個 8字節的結構體指針,有2個各爲4字節的 mask_t.因此加起來就是16個字節,也就是說前三個屬性總共的內存偏移量爲 8 + 8 + 16 = 32 個字節,32 是 10 進制的表示,在 16 進制下就是 20.

5.2 bits屬性結合上文提到的內存偏移一塊兒探索

利用LLDB命令來探索類結構的第四個屬性bits.

咱們爲了獲得 bits的指針地址,就須要進行指針偏移,這裏進行一下16進制下的地址偏移計算:

0x100001200 + 0x20 = 0x100001220
複製代碼

咱們繼續打印這個地址有:

經過輸出結果,得知 bits並非一個對象,而是一個結構體,這裏須要進行強轉一下:
又由 objc_class源碼可知,其內部有 data()方法:

因此接着調用 data()方法拿到 class_rw_t:

接着咱們繼續查看 libObjc中關於 class_rw_t的源碼:得知 class_rw_t也是一個結構體.

由源碼推測出相關的屬性應該存放在 properties裏面,咱們在打印一下:

接着打印 properties:

咦,竟然爲空.爲何會這樣呢?由於這裏咱們漏掉了一個重要的線索就是 const class_ro_t *ro;.咱們來到其源碼:
能夠看到 ro的類型是 class_ro_t結構體,它包含了 baseMethodListbaseProtocolsivarsbaseProperties 等屬性.咱們剛纔在 class_rw_t 中沒有找到咱們聲明在 TCJPerson類中的實例變量 titleStr 和屬性 helloName,那麼但願就在 class_ro_t身上了,咱們接着打印看看它的內容:
經過打印結果,咱們猜想,屬性應該存在 baseProperties裏面,咱們接着打印看看:
嗯哼,還有誰?咱們的屬性 helloName被找到了,就存放在 class_ro_tbaseProperites 裏面.咦,怎麼沒有看到咱們的實例變量 titleStr?咱們從 $10count 爲 1 能夠得知確定不在 baseProperites 裏面根.據名稱咱們猜想應該是在 $8ivars裏面.那咱們接着打印:
嗯哼,實例變量 titleStr也找到了,那爲何這裏的 count是2呢?咱們接着打印第二個元素看看:
結果爲 _helloName.這一結果證明了編譯器會幫助咱們給屬性 helloName 生成一個帶下劃線前綴的實例變量 _helloName. 至此,咱們能夠處處一下結論:

  • class_rw_t 是能夠在運行時來拓展類的一些屬性、方法和協議等內容.
  • class_ro_t 是在編譯時就已經肯定了的,存儲的是類的成員變量、屬性、方法和協議等內容.

6、類的方法存儲探索

研究完了類的屬性是怎麼存儲的,咱們再來看看類的方法又是怎麼存儲的. 在TCJPerson類裏面增長一個readBook的實例方法和一個writeBook的類方法.

按照前面的思路,咱們直接讀取 class_ro_t 中的 baseMethodList 的內容:
嗯哼, readBook方法被找出來了,這說明 baseMethodList就是存儲實例方法的地方.咱們接着打印剩下的內容:
能夠看到 baseMethodList中除了咱們的實例方法 readBook外,還有屬性 helloNamegettersetter 方法以及一個 C++ 析構方法.而咱們的類方法 writeBook 並無被打印出來.那麼類方法存儲在哪呢?

7、類的類方法存儲探索

咱們上面已經獲得了屬性,實例方法是怎麼樣存儲的了,可是還留下了一個疑問點,就是類方法是怎麼存儲的,接下來咱們用 RuntimeAPI 來實際測試一下.

首先 testInstanceMethod_classToMetaclass 方法測試的是分別從類和元類去獲取實例方法、類方法的結果.由打印結果咱們能夠知道:

  • 對於類對象來講,readBook 是實例方法,存儲於類對象的內存中,不存在於元類對象中.而 writeBook 是類方法,存儲於元類對象的內存中,不存在於類對象中.
  • 對於元類對象來講,readBook 是類對象的實例方法,跟元類不要緊;writeBook 是元類對象的實例方法,因此存在元類中. 咱們再測試另外的一個方法:
    從結果咱們能夠看出,對於類對象來講,經過 class_getClassMethod 獲取writeBook是有值的,而獲取 readBook 是沒有值的;對於元類對象來講,經過 class_getClassMethod 獲取writeBook也是有值的,而獲取 readBook 是沒有值的.這裏第一點很好理解,可是第二點會有點讓人糊塗,不是說類方法在元類中是體現爲對象方法的嗎?怎麼經過 class_getClassMethod 從元類中也能拿到 writeBook,咱們進入到 class_getClassMethod 方法內部能夠解開這個疑惑:

能夠很清楚的看到, class_getClassMethod 方法底層其實調用的是 class_getInstanceMethod,而 cls->getMeta() 方法底層的判斷邏輯是若是已是元類就返回,若是不是就返回類的 isa.這也就解釋了上面的 writeBook 爲何會出如今最後的打印中了. 除了上面的這種方式,咱們還能夠經過 isa 的方式來驗證類方法存放在元類中.

  • 經過 isa 在類對象中找到元類.

  • 打印元類的 baseMethodsList. 咱們也來驗證一下: 首先咱們獲取objClass的內存段:

    接着經過 & ISA_MASK拿到其元類,而且打印其內存段:
    接着按照上面類的屬性存儲探索的思路,進行指針偏移,獲取bits屬性:這裏進行一下16進制下的地址偏移計算:

    0x100001280 + 0x20 = 0x1000012a0
    複製代碼

    查找步驟都在圖中標明瞭.這也驗證了類方法存放在元類中.

章後總結

  • 類和元類建立於編譯時,能夠經過 LLDB 來打印類和元類的指針,或者用 MachOView軟件查看二進制可執行文件
  • 萬物皆對象:類的本質就是對象
  • 類在 class_ro_t 結構中存儲了編譯時肯定的屬性、成員變量、方法和協議等內容,而且對於屬性helloName:底層編譯會生成相應的settergetter方法,且幫咱們轉化爲_helloName,對於成員變量titleStr:底層編譯不會生成相應的settergetter方法,且沒有轉化爲_titleStr
  • 實例方法存放在類中
  • 類方法存放在元類中

在這一章中咱們完成了對 iOS 中類的結構的探索,下一章咱們將對類的緩存進行探索,敬請期待~

相關文章
相關標籤/搜索