天天看點

iOS-底層原理 07:isa與類關聯的原理

是以

isa

中通過

初始化

後的

成員

值變化過程

,如下圖所示

iOS-底層原理 07:isa與類關聯的原理

>iOS 底層原理 文章彙總

本文的主要目的是了解類與isa是如何關聯的

在介紹正文之前,首先需要了解一個概念:

OC對象

本質

是什麼?

OC對象本質

在探索oc對象本質前,先了解一個編譯器:

clang

Clang

  • clang

    是一個由

    Apple

    主導編寫,基于

    LLVM

    C/C++/OC的編譯器

  • 主要是用于

    底層編譯

    ,将一些

    檔案``輸出

    c++檔案

    ,例如

    main.m

    輸出成

    main.cpp

    ,其目的是為了更好的觀察

    底層

    的一些

    結構

    實作

    的邏輯,友善了解底層原理。

探索對象本質

  • main

    中自定義一個類

    LGPerson

    ,有一個屬性name
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LGPerson
@end
           
  • 通過終端,利用

    clang

    main.m

    編譯成

    main.cpp

    ,有以下幾種編譯指令,這裡使用的是第一種
//1、将 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 編譯成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下兩種方式是通過指定架構模式的指令行,使用xcode工具 xcrun
//3、模拟器檔案編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真機檔案編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 
           
  • 打開編譯好的main.cpp,找到

    LGPerson

    的定義,發現

    LGPerson

    在底層會被編譯成

    struct

    結構體
    • LGPerson_IMPL

      中的第一個屬性 其實就是

      isa

      ,是繼承自

      NSObject

      ,屬于

      僞繼承

      ,僞繼承的

      方式

      是直接将

      NSObject

      結構體定義為

      LGPerson

      中的

      第一個屬性

      ,意味着

      LGPerson

      擁有

      NSObject

      中的

      所有成員變量

    • LGPerson

      中的第一個屬性

      NSObject_IVARS

      等效于

      NSObject

      中的

      isa

//NSObject的定義
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//NSObject 的底層編譯
struct NSObject_IMPL {
	Class isa;
};

//LGPerson的底層編譯
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
	NSString *_name;
};
           

如下圖所示

iOS-底層原理 07:isa與類關聯的原理

通過上述分析,了解了OC對象的本質,但是看到

NSObject

的定義,會産生一個疑問:為什麼

isa

的類型是

Class

?

  • 在iOS-底層原理 02:alloc & init & new 源碼分析文章中,提及過

    alloc

    方法的核心之一的

    initInstanceIsa

    方法,通過檢視這個方法的源碼實作,我們發現,在初始化

    isa

    指針時,是通過

    isa_t

    類型初始化的,
  • 而在

    NSObject

    定義中isa的類型是

    Class

    ,其根本原因是由于

    isa

    對外回報的是

    類資訊

    ,為了讓開發人員更加

    清晰明确

    ,需要在

    isa

    傳回時做了一個

    類型強制轉換

    ,類似于

    swift

    中的

    as

    的強轉。源碼中

    isa

    強轉

    如下圖所示
    iOS-底層原理 07:isa與類關聯的原理

總結

是以從上述探索過程中可以得出:

  • OC對象的本質

    其實就是

    結構體

  • LGPerson

    中的

    isa

    是繼承自

    NSObject

    中的

    isa

objc_setProperty 源碼探索

除了

LGPersong

的底層定義,我們發現還有屬性

name

對應的

set

get

方法,如下圖所示,其中

set

方法的實作依賴于runtime中的

objc_setProperty

iOS-底層原理 07:isa與類關聯的原理

可以通過以下步驟來一步步解開

objc_setProperty

的底層實作

  • 在objc4-781中全局搜尋

    objc_setProperty

    ,找到

    objc_setProperty

    的源碼實作
    iOS-底層原理 07:isa與類關聯的原理
  • 進入

    reallySetProperty

    的源碼實作,其方法的原理就是

    新值retain,舊值release

    iOS-底層原理 07:isa與類關聯的原理

總結

通過對

objc_setProperty

的底層源碼探索,有以下幾點說明:

  • objc_setProperty

    方法的目的适用于

    關聯 上層

    set

    方法 以及

    底層

    set

    方法,其本質就是一個

    接口

  • 這麼設計的

    原因

    是,

    上層

    set

    方法有很多,如果

    直接調用底層set

    方法中,會産生很多的

    臨時變量

    ,當你想

    查找

    一個sel時,會非常

    麻煩

  • 基于上述原因,蘋果采用了

    擴充卡設計模式(即将底層接口适配為用戶端需要的接口)

    對外

    提供一個

    接口

    ,供上層的set方法使用,

    對内

    調用底層的

    set方法

    ,使其互相不受影響,即

    無論上層怎麼變,下層都是不變的

    ,或者

    下層的變化也無法影響上層

    ,主要是達到上下層接口隔離的目的

下圖是上層、隔離層、底層之間的關系

iOS-底層原理 07:isa與類關聯的原理

cls 與 類 的關聯原理

在在iOS-底層原理 02:alloc & init & new 源碼分析與iOS-底層原理 05:記憶體對齊原理中分别分析了alloc中3核心的前兩個,今天來探索

initInstanceIsa

是如何将

cls

isa

關聯的

在此之前,需要先了解什麼是

聯合體

,為什麼

isa

的類型

isa_t

是使用

聯合體定義

聯合體(union)

構造資料類型的方式有以下兩種:

  • 結構體

    struct

  • 聯合體

    union

    ,也稱為

    共用體

結構體

結構體

是指把不

同的資料組合成一個整體

,其

變量

共存

的,變量不管是否使用,都會配置設定記憶體。

  • 缺點:所有屬性都配置設定記憶體,比較

    浪費記憶體

    ,假設有4個int成員,一共配置設定了

    16

    位元組的記憶體,但是在使用時,你隻使用了

    4

    位元組,剩餘的

    12

    位元組就是屬于記憶體的浪費
  • 優點:存儲

    容量較大

    包容性強

    ,且成員之間

    不會互相影響

聯合體

聯合體也是由

不同的資料類型組成

,但其變量是

互斥

的,所有的成員

共占一段記憶體

。而且

共用體

采用了

記憶體覆寫技術

同一時刻隻能儲存一個成員的值

,如果

對新的成員指派

,就會将

原來成員的值覆寫掉

  • 缺點:,包容性弱
  • 優點:所有成員共用一段記憶體,使記憶體的使用更為精細靈活,同時也節省了記憶體空間

兩者的差別

  • 記憶體占用情況
    • 結構體的

      各個成員會占用不同的記憶體

      ,互相之間

      沒有影響

    • 共用體的

      所有成員占用同一段記憶體

      ,修改一個成員會

      影響

      其餘所有成員
  • 記憶體配置設定大小
    • 結構體記憶體

      >=

      所有成員占用的

      記憶體總和

      (成員之間可能會有縫隙)
    • 共用體

      占用的

      記憶體

      等于

      最大的成員

      占用的

      記憶體

isa的類型 isa_t

以下是isa指針的類型

isa_t

的定義,從定義中可以看出是通過

聯合體(union)

定義的。

union isa_t { //聯合體
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    //提供了cls 和 bits ,兩者是互斥關系
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
           

isa_t

類型使用

聯合體

的原因也是基于

記憶體優化

的考慮,這裡的記憶體優化是指在isa指針中通過

char + 位域

(即二進制中每一位均可表示不同的資訊)的原理實作。通常來說,

isa指針

占用的記憶體大小是

8

位元組,即

64

位,已經足夠存儲很多的資訊了,這樣可以極大的節省記憶體,以提高性能

isa_t

的定義中可以看出:

  • 提供了兩個成員,

    cls

    bits

    ,由聯合體的定義所知,這兩個成員是

    互斥

    的,也就意味着,當初始化isa指針時,有兩種初始化方式
    • 通過

      cls

      初始化,

      bits無預設值

    • 通過

      bits

      初始化,

      cls有預設值

  • 還提供了一個結構體定義的

    位域

    ,用于存儲類資訊及其他資訊,結構體的成員

    ISA_BITFIELD

    ,這是一個

    定義,有兩個版本

    __arm64__

    (對應ios 移動端) 和

    __x86_64__

    (對應macOS),以下是它們的一些宏定義,如下圖所示
    iOS-底層原理 07:isa與類關聯的原理
    • nonpointer

      有兩個值,表示自定義的類等,占

      1

      • 純isa指針

      • 1

        :不隻是

        類對象位址

        ,isa中包含了

        類資訊

        、對象的

        引用計數

    • has_assoc

      表示

      關聯對象标志

      位,占

      1

      • 沒有關聯

        對象
      • 1

        存在關聯

        對象
    • has_cxx_dtor

      表示該對象是否有C++/OC的

      析構器

      (類似于

      dealloc

      ),占

      1

      • 如果

        析構函數,則需要

        做析構

        邏輯
      • 如果

        沒有

        ,則可以更快的

        釋放

        對象
    • shiftclx

      表示

      存儲類的指針的值

      (類的位址), 即類資訊
      • arm64

        中占

        33

        位,開啟指針優化的情況下,在arm64架構中有

        33

        位用來存儲類指針
      • x86_64

        中占

        44

    • magic

      用于調試器判斷目前對象是

      真的對象

      還是

      沒有初始化的空間

      ,占

      6

    • weakly_refrenced

      是 指對象

      是否被指向

      或者

      曾經指向一個ARC的弱變量

      • 沒有弱引用的對象可以更快釋放
    • deallocating

      标志對象是

      是否正在釋放

      記憶體
    • has_sidetable_rc

      表示 當對象

      引用計數大于10

      時,則需要

      借用該變量存儲進位

    • extra_rc

      (額外的引用計數) — 導尿管表示該

      對象的引用計數值

      ,實際上是引用計數值減1
      • 如果對象的

        引用計數為10

        ,那麼

        extra_rc

        為9

針對兩種不同平台,其

isa

的存儲情況如圖所示

iOS-底層原理 07:isa與類關聯的原理

原理探索

  • 通過

    alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone

    方法路徑,查找到

    initInstanceIsa

    ,并進入其原理實作
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
    //初始化isa
    initIsa(cls, true, hasCxxDtor); 
}
           
  • 進入

    initIsa

    方法的源碼實作,主要是初始化isa指針
    iOS-底層原理 07:isa與類關聯的原理
    該方法的邏輯主要分為兩部分
    • 通過

      cls

      初始化

      isa

    • 通過

      bits

      初始化

      isa

驗證 isa指針 位域(0-64)

根據前文提及的

0-64

位域,可以在這裡通過

initIsa

方法中證明有isa指針中有這些位域(目前是處于

macOS

,是以使用的是

x86_64

  • 首先通過main中的

    LGPerson

    斷點 -->

    initInstanceIsa

    -->

    initIsa

    --> 走到

    else

    中的

    isa

    初始化
    iOS-底層原理 07:isa與類關聯的原理
  • 執行lldb指令:

    p newisa

    ,得到

    newisa

    的詳細資訊
    iOS-底層原理 07:isa與類關聯的原理
  • 繼續往下執行,走到

    newisa.bits = ISA_MAGIC_VALUE;

    下一行,表示為

    isa

    bits

    成員指派,重新執行lldb指令

    p newisa

    ,得到的結果如下
    iOS-底層原理 07:isa與類關聯的原理
    通過與前一個newsize的資訊對比,發現isa指針中有一些變化,如下圖所示
    iOS-底層原理 07:isa與類關聯的原理
    • 其中

      magic

      59

      是由于将

      isa

      指針位址轉換為

      二進制

      ,從

      47

      (因為前面有4個位域,共占用47位,位址是從0開始)位開始讀取

      6

      位,再轉換為

      十進制

      ,如下圖所示
      iOS-底層原理 07:isa與類關聯的原理

isa

的關聯

cls

isa

關聯

原理

就是

isa

指針中的

shiftcls

位域中存儲了

類資訊

,其中

initInstanceIsa

的過程是将

calloc

指針 和目前的

類cls

關聯起來,有以下幾種驗證方式:

  • 【方式一】通過

    initIsa

    方法中的

    newisa.shiftcls = (uintptr_t)cls >> 3;

    驗證
  • 【方式二】通過

    isa指針位址

    ISA_MSAK

    的值

    &

    來驗證
  • 【方式三】通過runtime的方法

    object_getClass

    驗證
  • 【方式四】通過

    位運算

    驗證

方式一:通過 initIsa 方法

  • 運作至

    newisa.shiftcls = (uintptr_t)cls >> 3;

    前一步,其中

    shiftcls

    存儲目前

    類的值資訊

    • 此時檢視

      cls

      ,是

      LGPerson

    • shiftcls

      指派的邏輯是将

      LGPerson

      進行編碼後,

      右移3

      iOS-底層原理 07:isa與類關聯的原理
  • 執行lldb指令

    p (uintptr_t)cls

    ,結果為

    (uintptr_t) $2 = 4294975720

    ,再右移三位,有以下兩種方式(任選其一),将得到

    536871965

    存儲到

    newisa

    shiftcls

    • p (uintptr_t)cls >> 3

    • 通過上一步的結果

      $2

      ,執行lldb指令

      p $2 >> 3

      iOS-底層原理 07:isa與類關聯的原理
  • 繼續執行程式到

    isa = newisa;

    部分,此時執行

    p newisa

    iOS-底層原理 07:isa與類關聯的原理

    bits指派

    結果的

    對比

    ,bits的

    位域

    中有兩處變化
    • cls

      預設值

      ,變成了

      LGPerson

      ,将

      isa與cls完美關聯

    • shiftcls

      由 變成了

      536871965

      iOS-底層原理 07:isa與類關聯的原理
      是以

      isa

      中通過

      初始化

      後的

      成員

      值變化過程

      ,如下圖所示
      iOS-底層原理 07:isa與類關聯的原理

為什麼在shiftcls指派時需要類型強轉?

因為

記憶體

的存儲

不能存儲字元串

機器碼

隻能識别

0 、1

這兩種數字,是以需要将其轉換為

uintptr_t

資料類型,這樣

shiftcls

中存儲的

類資訊

才能

被機器碼了解

, 其中

uintptr_t

long

為什麼需要右移3位?

主要是由于

shiftcls

處于

isa

指針位址的

中間

部分,前面還有

3

個位域,為了不

影響前面的3個位域

的資料,需要

右移

将其

抹零

方式二:通過 isa & ISA_MSAK

  • 在方式一後,繼續執行,回到

    _class_createInstanceFromZone

    方法,此時

    cls 與 isa已經關聯完成

    ,執行

    po objc

  • 執行

    x/4gx obj

    ,得到

    isa

    指針的位址

    0x001d8001000020e9

  • isa

    指針位址 &

    ISA_MASK

    (處于

    macOS

    ,使用

    x86_64

    中的

    定義),即

    po 0x001d8001000020e9 & 0x00007ffffffffff8

    ,得出

    LGPerson

    • arm64

      中,ISA_MASK 宏定義的值為

      0x0000000ffffffff8ULL

    • x86_64

      中,ISA_MASK 宏定義的值為

      0x00007ffffffffff8ULL

      iOS-底層原理 07:isa與類關聯的原理

方式三:通過 object_getClass

通過檢視

object_getClass

的源碼實作,同樣可以驗證isa與類關聯的原理,有以下幾步:

  • main中導入#import <objc/runtime.h>
  • 通過

    runtime

    的api,即

    object_getClass

    函數擷取類資訊
object_getClass(<#id  _Nullable obj#>)
           
  • 檢視

    object_getClass

    函數 源碼的實作
    iOS-底層原理 07:isa與類關聯的原理
  • 點選進入

    object_getClass

    底層實作
    iOS-底層原理 07:isa與類關聯的原理
  • 進入

    getIsa

    的源碼實作
    iOS-底層原理 07:isa與類關聯的原理
  • 點選

    ISA()

    ,進入源碼,可以看到如果是

    indexed

    類型,執行

    if

    流程,反之 執行的是

    else

    流程
    iOS-底層原理 07:isa與類關聯的原理
    • else

      流程中,拿到

      isa

      bits

      這個位,再

      & ISA_MASK

      ,這與方式二中的原理是一緻的,

      獲得目前的類資訊

    • 從這裡也可以

      得出 cls 與 isa 已經完美關聯

方式四:通過位運算

  • 回到

    _class_createInstanceFromZone

    方法。通過

    x/4gx obj

    得到

    obj

    的存儲資訊,目前類的資訊存儲在

    isa

    指針中,且isa中的

    shiftcls

    此時占

    44

    位(因為處于

    macOS

    環境)
    iOS-底層原理 07:isa與類關聯的原理
  • 想要

    讀取

    中間的

    44

    類資訊

    ,就需要經過

    位運算

    ,将右邊

    3

    位,和左邊除去

    44

    位以外的部分都

    抹零

    ,其

    相對位置是不變

    的。其位運算過程如圖所示,其中

    shiftcls

    即為需要

    讀取

    類資訊

    iOS-底層原理 07:isa與類關聯的原理
    • isa

      位址

      右移3

      位:

      p/x 0x001d8001000020e9 >> 3

      ,得到

      0x0003b0002000041d

    • 在将得到的

      0x0003b0002000041d``左移20

      位:

      p/x 0x0003b0002000041d << 20

      ,得到

      0x0002000041d00000

      • 為什麼是

        左移20

        位?因為先

        移了

        3

        位,相當于向右

        偏移了3位

        ,而

        左邊

        需要

        抹零

        的位數有

        17

        位,是以一共需要移動

        20

    • 将得到的

      0x0002000041d00000

      右移17

      位:

      p/x 0x0002000041d00000 >> 17

      得到新的

      0x00000001000020e8

  • 擷取cls的位址 與 上面的進行驗證 :

    p/x cls

    也得出

    0x00000001000020e8

    ,是以由此可以證明 cls 與 isa 是關聯的
    iOS-底層原理 07:isa與類關聯的原理