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

今天探索的主要目的是理解類與isa是如何關聯的

探索之前,先了解一個編譯器:clang

Clang
  • clang是一個由Apple主導編寫,基于LLVMC/C++/OC的編譯器

  • 主要是用于底層編譯,將一些文件輸出成c++文件,例如main.m 輸出成main.cpp,其目的是為了更好的觀察底層的一些結構實現的邏輯,方便理解底層原理。

探索OC對象本質
  • main中自定義一個類LGPerson,有一個屬性name
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LGPerson
@end
  • 通過終端,利用clangmain.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的定義,發(fā)現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;
};

如下圖所示


clang編譯后的LGPerson
  • 通過上述分析,理解了OC對象的本質,但是看到NSObject的定義,會產生一個疑問:為什么isa的類型是Class?

  • iOS-底層原理 :alloc & init & new 源碼分析文章中,提及過alloc方法的核心之一的initInstanceIsa方法,通過查看這個方法的源碼實現,我們發(fā)現,在初始化isa指針時,是通過isa_t類型初始化的,

  • 而在NSObject定義中isa的類型是Class,其根本原因是由于isa 對外反饋的是類信息,為了讓開發(fā)人員更加清晰明確,需要在isa返回時做了一個類型強制轉換,類似于swift中的 as 的強轉。源碼中isa強轉如下圖所示

    源碼中`isa`的`強轉`

總結

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

  • OC對象的本質 其實就是 結構體

*LGPerson中的isa是繼承自NSObject中的isa

objc_setProperty 源碼探索

除了LGPersong的底層定義,我們發(fā)現還有屬性 name對應的 setget方法,如下圖所示,其中set方法的實現依賴于runtime中的objc_setProperty

編譯后的set方法

可以通過以下步驟來一步步解開objc_setProperty的底層實現

  • objc4-781中全局搜索objc_setProperty,找到objc_setProperty的源碼實現
    objc_setProperty的源碼實現
  • 進入reallySetProperty的源碼實現,其方法的原理就是新值retain,舊值release
    reallySetProperty源碼實現

總結

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

  • objc_setProperty方法的目的適用于關聯 上層的set方法 以及 底層set方法,其本質就是一個接口

  • 這么設計的原因是,上層set方法有很多,如果直接調用底層set方法中,會產生很多的臨時變量,當你想查找一個set 時,會非常麻煩

  • 基于上述原因,蘋果采用了配器設計模式(即將底層接口適配為客戶端需要的接口),對外提供一個接口,供上層的set方法使用,對內調用底層的set方法,使其相互不受影響,即無論上層怎么變,下層都是不變的,或者下層的變化也無法影響上層,主要是達到上下層接口隔離的目的

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


上層、隔離層、底層之間的關系
cls 與 類 的關聯原理

iOS-底層原理 :alloc & init & new 源碼分析iOS-底層原理 :內存對齊分別分析了alloc中3核心的前兩個,今天來探索initInstanceIsa是如何將clsisa關聯的.

在此之前,需要先了解什么是聯合體,為什么isa的類型isa_t是使用聯合體定義.

聯合體(union)

構造數據類型的方式有以下兩種:

  • 結構體struct
  • 聯合體union,也稱為共用體

結構體

結構體是指把不同的數據組合成一個整體,其變量共存的,變量不管是否使用,都會分配內存。

  • 缺點:所有屬性都分配內存,比較浪費內存,假設有4個int成員,一共分配了16字節(jié)的內存,但是在使用時,你只使用了4字節(jié),剩余的12字節(jié)就是屬于內存的浪費

  • 優(yōu)點:存儲容量較大,包容性強,且成員之間不會相互影響

聯合體

聯合體也是由不同的數據類型組成,但其變量是互斥的,所有的成員共占一段內存。而且共用體采用了內存覆蓋技術同一時刻只能保存一個成員的值,如果對新的成員賦值,就會將原來成員的值覆蓋掉

  • 缺點:,包容性弱

  • 優(yōu)點:所有成員共用一段內存,使內存的使用更為精細靈活,同時也節(jié)省了內存空間

兩者的區(qū)別

  • 內存占用情況

    • 結構體的各個成員會占用不同的內存,互相之間沒有影響
    • 共用體的所有成員占用同一段內存,修改一個成員會影響其余所有成員
  • 內存分配大小

    • 結構體內存 >= 所有成員占用的內存總和(成員之間可能會有縫隙)
    • 共用體占用的內存等于最大的成員占用的內存

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類型使用聯合體的原因也是基于內存優(yōu)化的考慮,這里的內存優(yōu)化是指在isa指針中通過char + 位域(即二進制中每一位均可表示不同的信息)的原理實現。通常來說,isa指針占用的內存大小是8字節(jié),即64位,已經足夠存儲很多的信息了,這樣可以極大的節(jié)省內存,以提高性能

isa_t的定義中可以看出:

  • 提供了兩個成員,clsbits,由聯合體的定義所知,這兩個成員是互斥的,也就意味著,當初始化isa指針時,有兩種初始化方式

  • 通過cls初始化,bits無默認值

  • 通過bits初始化,cls有默認值

  • 還提供了一個結構體定義的位域,用于存儲類信息及其他信息,結構體的成員ISA_BITFIELD,這是一個定義,有兩個版本__arm64__(對應ios 移動端) 和__x86_64__(對應macOS),以下是它們的一些宏定義,如下圖所示

    位域的宏定義

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

    • 0純isa指針
    • 1:不只是類對象地址,isa中包含了類信息、對象的引用計數
  • has_assoc表示關聯對象標志位,占1位

    *0沒有關聯對象

    • 1存在關聯對象
  • has_cxx_dtor表示該對象是否有C++/OC的析構器(類似于dealloc),占1

    • 如果析構函數,則需要做析構邏輯
    • 如果沒有,則可以更快的釋放對象
  • shiftclx表示存儲類的指針的值(類的地址), 即類信息

    • arm64中占 33位,開啟指針優(yōu)化的情況下,在arm64架構中有33位用來存儲類指針
    • x86_64中占44
  • magic 用于調試器判斷當前對象是真的對象 還是 沒有初始化的空間,占6

  • weakly_refrenced指對象是否被指向 或者曾經指向一個ARC的弱變量

    • 沒有弱引用的對象可以更快釋放
  • deallocating 標志對象是是否正在釋放內存

  • has_sidetable_rc表示 當對象引用計數大于10時,則需要借用該變量存儲進位

  • extra_rc(額外的引用計數) --- 導尿管表示該對象的引用計數值,實際上是引用計數值減1

    • 如果對象的引用計數為10,那么extra_rc為9

針對兩種不同平臺,其isa的存儲情況如圖所示

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指針
    initIsa源碼實現
該方法的邏輯主要分為兩部分
*   通過 `cls` 初始化 `isa`
*   通過 `bits` 初始化 `isa`
驗證 isa指針 位域(0-64)

根據前文提及的0-64位域,可以在這里通過initIsa方法中證明有isa指針中有這些位域(目前是處于macOS,所以使用的是x86_64

  • 首先通過main中的LGPerson 斷點 --> initInstanceIsa --> initIsa --> 走到else中的 isa初始化
    else中的isa初始化
  • 執(zhí)行l(wèi)ldb命令:p newisa,得到newisa的詳細信息

    lldb命令結果

  • 繼續(xù)往下執(zhí)行,走到newisa.bits = ISA_MAGIC_VALUE;下一行,表示為isabits成員賦值,重新執(zhí)行l(wèi)ldb命令p newisa,得到的結果如下

    再次執(zhí)行p命令的結果

通過與前一個newsize的信息對比,發(fā)現isa指針中有一些變化,如下圖所示


newisa中bits數據對比
  • 其中magic59是由于將isa指針地址轉換為二進制,從47(因為前面有4個位域,共占用47位,地址是從0開始)位開始讀取6位,再轉換為十進制,如下圖所示

    magic是59的來源
isa 與 類 的關聯

clsisa 關聯原理就是isa指針中的shiftcls位域中存儲了類信息,其中initInstanceIsa的過程是將 calloc 指針 和當前的 類cls 關聯起來,有以下幾種驗證方式:

1、:通過initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗證
  • 運行至newisa.shiftcls = (uintptr_t)cls >> 3;前一步,其中 shiftcls存儲當前類的值信息

    • 此時查看cls,是LGPerson

    • shiftcls賦值的邏輯是將 LGPerson進行編碼后,右移3

      shiftcls的賦值中的cls

  • 執(zhí)行l(wèi)ldb命令p (uintptr_t)cls,結果為(uintptr_t) $2 = 4294975720,再右移三位,有以下兩種方式(任選其一),將得到536871965存儲到newisashiftcls

    • p (uintptr_t)cls >> 3

    • 通過上一步的結果$2,執(zhí)行l(wèi)ldb命令p $2 >> 3

      isa右移3位
  • 繼續(xù)執(zhí)行程序到isa = newisa;部分,此時執(zhí)行p newisa

    執(zhí)行p的結果

    bits賦值結果的對比,bits的位域中有兩處變化

    • cls默認值,變成了LGPerson,將isa與cls完美關聯

    • shiftcls0變成了536871965

      前后結果對比

所以isa中通過初始化后的成員值變化過程,如下圖所示

isa成員值變化過程

為什么在shiftcls賦值時需要類型強轉?

因為內存的存儲不能存儲字符串,機器碼只能識別 0 、1這兩種數字,所以需要將其轉換為uintptr_t數據類型,這樣shiftcls中存儲的類信息才能被機器碼理解, 其中uintptr_tlong

為什么需要右移3位?

主要是由于shiftcls處于isa指針地址的中間部分,前面還有3個位域,為了不影響前面的3個位域的數據,需要右移將其抹零。

2、:通過isa指針地址ISA_MSAK 的值 & 來驗證
  • 在方式一后,繼續(xù)執(zhí)行,回到_class_createInstanceFromZone方法,此時cls 與 isa已經關聯完成,執(zhí)行po objc

  • 執(zhí)行x/4gx obj,得到isa指針的地址0x001d8001000020e9

  • isa指針地址 & ISA_MASK (處于macOS,使用x86_64中的定義),即 po 0x001d8001000020e9 & 0x00007ffffffffff8 ,得出LGPerson

    • arm64中,ISA_MASK 宏定義的值為0x0000000ffffffff8ULL

    • x86_64中,ISA_MASK 宏定義的值為0x00007ffffffffff8ULL

      isa & ISA_MASK的結果
3、通過runtime的方法object_getClass驗證

通過查看object_getClass的源碼實現,同樣可以驗證isa與類關聯的原理,有以下幾步:

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

  • 通過runtime的api,即object_getClass函數獲取類信息

object_getClass(<#id  _Nullable obj#>)

  • 查看object_getClass函數 源碼的實現

    objc4中查找object_getClass函數
  • 點擊進入object_getClass 底層實現

    object_getClass源碼實現
  • 進入getIsa的源碼實現

    getIsa源碼實現
  • 點擊ISA(),進入源碼,可以看到如果是indexed類型,執(zhí)行if流程,反之 執(zhí)行的是else流程

    ISA()源碼實現
    • else流程中,拿到isabits這個位,再 & ISA_MASK,這與方式二中的原理是一致的,獲得當前的類信息
    • 從這里也可以得出 cls 與 isa 已經完美關聯
4、:通過位運算驗證
  • 回到_class_createInstanceFromZone方法。通過x/4gx obj 得到obj的存儲信息,當前類的信息存儲在isa指針中,且isa中的shiftcls此時占44位(因為處于macOS環(huán)境)

    obj內存情況
  • 想要讀取中間的44類信息,就需要經過位運算 ,將右邊3位,和左邊除去44位以外的部分都抹零,其相對位置是不變的。其位運算過程如圖所示,其中shiftcls即為需要讀取類信息

    位運算計算過程
    • 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 是關聯的

    執(zhí)行p的結果
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容