Objective-C 底層對象探究-下

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

目錄

1. 背景

學習不迷茫,無阻我飛揚!你們好我是Tommy!本篇是Objective-C 底層對象探究的最終篇,廢話不說咱們這就開始!git

2.從編譯後的文件理解OC對象

  • 經過xcrun編譯成C++文件
    • 再上一篇內容中咱們是經過clang命令來進行的編譯的,其實還有一種方法就是經過xcrun命令也能夠達到同樣的效果。
    xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    複製代碼
  • 對蘋果開發語言的層次結構的理解
    • 這裏簡要的把我我的對蘋果開發語言的一些理解簡單說明一下,咱們你們都知道目前蘋果提供2種開發語言Objective-CSwift,在2014年以前蘋果都是選用OC來擔任開發語言,直到2014年的WWDC纔將Swift展示在廣大開發者的面前,推出以後也是受到了廣大開發人員的強烈關注;
    • 蘋果之因此大費周章的推出一種新語言,我想是Objective-C已經沒法達到蘋果對於效率方面的知足了,若是你關注每一年的WWDC的話,其實能夠感受出來蘋果在效率的問題上一直是追求極致的,經過上兩篇的學習咱們也能經過底層代碼感受出因爲Objective-C的一些語言特性致使蘋果爲其作出的效率上的犧牲,也就不難理解Objective-C在蘋果追求效率的路上成爲了最大的屏障,因此對其再從新建立一種語言也就合情合理了。
    • 其實對於咱們開發者而言,不管是Objective-C仍是Swift其實只是蘋果給予開發者在上層結構的一種開發方式,舉個例子:就至關於購買了一個電子產品,想要運行這個電子產品功能,就須要閱讀說明指南,而Objective-C仍是Swift就至關於一種操做指令,用戶經過指令來控制這個產品的功能,上層指令無論方式如何改變(無論你是物理按鈕,仍是電子屏幕觸控),都不會對影響到底層的功能。雖然底層功能不會發生改變,可是兩種方式帶來的效率就會產生差距了。

    圖片.png

  • 從編譯後文件咱們能知道什麼?
    • 對象的本質就是結構體,咱們經過查看編譯成C++的文件就能夠發現,一個叫作ZXPerson_IMPL的結構體,這個就是咱們建立的ZXPerson對象

    ps:若是把ZXPerson類的定義放到main.m中會看到更多內容 圖片.png 圖片.pnggithub

    • 若是想驗證一下,咱們能夠經過在ZXPerson類中增長一個成員變量後,再編譯成C++來觀察變化。編譯後咱們就能看到在ZXPerson_IMPL內部新增了一個咱們建立的成員name

    圖片.png 圖片.png

    • 除了咱們新增的成員name外,咱們發現還會有一個默認的成員結構體NSObject_IMPL NSObject_IVARS,這個是什麼呢?經過搜索NSObject_IMPL咱們就一目瞭然——其實就是isa

    圖片.png

    • Isa是指向類的結構體的指針,咱們搜索Class能夠看到實際上是objc_class結構體的指針。

    圖片.png

    • id類型是指向對象的指針,咱們再往下看能夠看到在OC中id類型實際上是一個對象的指針。

    圖片.png

  • 屬性取值的分析
    • 觀察_I_ZXPerson_nikeName函數(在OC中其實就是getNikeName()方法)返回時的語句,咱們看到在底層並非直接將數值進行返回的,而是經過(char *)self(對象首地址)加上OBJC_IVAR_$_ZXPerson$_nikeName(變量的偏移量)來找到實際數值的地址,再進行類型轉換最終返回。 圖片.png
  • 節點小結:
    • 能夠經過clangxcrun方式對.m文件進行編譯,編譯後能夠幫助咱們理解底層對象的實現,本小結內容到此結束。

3. 位域與公用體

  • 位域:
    • 所謂的位域實際上是在struct結構體中的一種表達語法,他的含義是爲結構體中的成員明肯定義其佔用的二進制位數,聽起來有點繞哈,其實一點也不難理解,請看下面的例子:
    圖片.png
    • 例子中結構體ZXStruct1包含4個成員,每一個成員的類型是BOOL型(佔用1個字節),打印佔用的大小結果爲4字節;
    • 例子中結構體ZXStruct2一樣包含4個成員,每一個成員的類型是BOOL型(佔用1個字節),可是因爲定位了位域因此成員聲明後面增長了「:1」,最後打印佔用的大小結果爲1個字節;請看下面的說明圖更便於理解。
    圖片.png
    • ZXStruct1結構體共佔用4字節,32個二進制位,可是BOOL類型的話只須要一個進制位就能夠表達了,其餘進制位都是補零,因此空間方面有所浪費。
    • ZXStruct2結構體共佔用1字節,因爲定義了位域,使每一個成員BOOL只佔用1個二進制位故須要4個二進制位,又因8個二進制位爲1個字節,因此只需1個字節就能夠知足佔用需求,大大節省了空間。
  • 公用體:
    • 咱們知道結構體(Struct)是一種構造類型或複雜類型,它能夠包含多個類型不一樣的成員。在C語言中,還有另一種和結構體很是相似的語法,叫作共用體(Union),它的定義格式爲:
    union 共用體名{
        成員列表
    };
    複製代碼
    • 共用體有時也被稱爲聯合或者聯合體,這也是 Union 這個單詞的本意。
    • 結構體與共用體在內存大小上也存在差別,結構體是各個成員會佔用不一樣的內存,互相之間沒有影響;而共用體是全部成員佔用同一段內存,修改一個成員會影響其他全部成員,而內存的總大小已成員中最大佔用的那個爲準。
    • 主要區別在於:結構體的各個成員會佔用不一樣的內存,互相之間沒有影響;而共用體的全部成員佔用同一段內存,修改一個成員會影響其他全部成員。請看下面的例子:
    圖片.png 圖片.png
    • 例子中結構體 ZXStruct3 包含 4 個成員,打印佔用的大小結果爲 32 字節;而且每一個成員的值都是獨立存放的,不會由於給其餘成員賦值而改變。
    • 例子中共用體ZXStruct4包含4個成員,打印佔用的大小結果爲8字節,是由於成員中含有指針類型namenikeName,因此按照最大成員的大小進行分配。此外當咱們分步驟進行成員變量賦值時,會發生改變其餘成員變量值的現象。請看下面的說明圖更便於理解。
    圖片.png 圖片.png
  • 理解說明:
    • 一、共用體未對任何成員進行賦值操做時成員都是nil
    • 二、當 zx4.name="zhaoxin"進行賦值後內存地址發生變化,因爲是指針類型須要佔用8個字節,這時其實已經將整個共用體的內存佔用滿了;第二個成員nikeName也是指針類型,因此共用了成員name的內存,所以值與name一致;第三個成員age佔用4個字節,因爲IOS是小端模式,因此age的值爲0x3f4c,轉換爲10進制正好是16204;最後一個成員heightdouble類型比較特殊因此值是‘0’;
    • 三、 當zx4.nikeName="zhaoxin"進行賦值後內存地址發生變化,與成員name的值一致;第三個成員age值爲0x3f54;轉換爲10進制是16212;最後一個成員height依舊是‘0’;
    • 四、當zx4.age=20進行賦值後內存地址發生變化,成員namenikeName值爲58 07 00 00,轉換爲ASCII爲‘X’(07的ASCII是BEL (bell)不會被顯示);age2016進制0x0014就是20),height依舊是‘0’;
    • 五、當zx4.height=179.2進行賦值後內存地址發送變化,成員namenikeName值沒法讀取,age則超出了範圍大小了直接顯示了最大數;height經過p/f方式打印能夠讀取到數值。
賦值順序 name nikeName age height
name賦值時 zhaoxin zhaoxin 16240 0
nikeName賦值時 Tommy Tommy 16212 0
age賦值時 X X 20 0
height賦值時 null null 越界了 179.2
  • 節點小結:
    • 經過設置位域能夠定義成員變量佔用的二進制位的大小;
    • 普通結構體:結構體中的全部成員都會分配獨立的內存空間且相互不會干擾,優勢:不會互相影響;缺點:沒有使用到的成員的空間會被浪費掉;
    • 共同體(聯合體):共同體大小以成員中最大的那個爲準,其中全部成員公用內存區域,優勢:節省空間;缺點:成員的取值會發生變化;
    • 本小結內容到此結束。

4. nonPointerIsa的分析

  • 什麼是nonPointerIsa:
    • 咱們都知道Isa是指向類的一個指針,可是Isa也有包含一個特殊的種類,除了包括類信息以外還包含其餘的信息例如:bitshas_cxx_dtorindexcls等信息的Isa,咱們稱做nonPointerIsa。(非單純指針的Isa) (ps:在不設置環境變量OBJC_DISABLE_NONPOINTER_ISA =1的狀況下,咱們所用的Isa都是nonPointerIsa,後文有說明如何設置這個變量)
    • 能夠經過objc源碼來查看,從 _class_createInstanceFromZone() 開闢實例對象方法中對 obj->initIsa(cls) 代碼進行追查。

    圖片.png 圖片.png

  • Isa裏面存放了什麼信息:
    • 經過上面的源碼分析,咱們得知了nonPointerIsa除了類信息以外還會存放其餘數據,源碼中是將數據存放到了名叫newisa的對象裏,newisa是一個叫作isa_t結構體類型,咱們能夠繼續追蹤這個isa_t結構體。

    圖片.png 圖片.png

    • isa_t的結構體比較簡單,包括2個構造方法、一個私有的成員cls、以及對cls操做的相關對外方法、最後就是最關鍵的成員結構體ISA_BItFIELD,這個就是isa真正存放數據的關鍵。此外咱們在上一小結 位域與聯合體 的知識就能夠用到了。

    圖片.png

  • isa_t的特色分析:
    • 首先isa_t是一個共用體,並佔有8個字節大小;
    • isa_t中有2個成員,一個是私有的cls、另外一個就是內部結構體成員 ISA_BItFIELD,它倆共享8字節的大小空間;
    • 若是是非nonPointerIsa8字節大小隻存放cls成員的信息,不然存放ISA_BItFIELD的信息;
    • 1個字節佔用8個二進制位,isa_t佔用8個字節即64個二進制位,ISA_BItFIELD經過定義了位域共佔用64位,因此若是是nonPointerIsa則直接會佔滿。
    • ISA_BItFIELD的位域會根據當前系統進行調整,可是總體的大小不變,只是內部各個成員大小會發生細微變化。

    圖片.png

  • ISA_BItFIELD中成員的含義:
成員 表明的含義
nonpointer 表示是否對 isa 指針開啓指針優化;爲0時: 純isa指針;爲1時:不止是類對象地址,還包含了類信息、對象的引用計數等。
has_assoc 關聯對象標誌位,0:沒有,1:存在 。
has_cxx_dtor 該對象是否有 C++ 或者 Objc 的析構器,若是有析構函數,則須要作析構邏輯, 若是沒有,則能夠更快的釋放對象 。
shiftcls 儲類指針的值。開啓指針優化的狀況下,在arm64 架構中有33 位用來存儲類指針。
magic 用於調試器判斷當前對象是真的對象仍是沒有初始化的空間 。
weakly_referenced 志對象是否被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象能夠更快釋放。
unused 標誌對象是否被使用。(源碼版本objc-723這裏是deallocating表示對象是否正在釋放內存,我這裏源碼版本是objc-818.2使用unused來代替原deallocating;自己的意義應該是一致的)
has_sidetable_rc 當對象引用技術大於 10 時,則須要借用該變量存儲進位 。
extra_rc 當表示該對象的引用計數值,其實是引用計數值減1,例如:若是對象的引用計數爲10,那麼extra_rc 爲9。若是引用計數大於 10,則須要使用到上面的has_sidetable_rc。
  • 經過LLDB打印isa的二進制位
    • 以前咱們能夠經過x/4gx來打印isa的地址,如今咱們已經瞭解了isa是佔用了64個二進制位,若是想驗證一下咱們能夠經過p/t (打印二進制),輸出以後就獲得了完整的二進制了(起始是從右往左)

    圖片.png

  • ISA_MASK是做用
    • ISA_MASK是一個掩碼,他的主要做用是經過掩碼將不想獲得的數據過濾掉,只留下想要的數據。具體實際狀況就是在isa中,最重要是就是類有關的信息也就是shiftcls的內容,因此這個ISA_MASK的做用就是過濾掉其餘信息只返回類信息。

    圖片.png 圖片.png

    • 依舊經過x/4gx來打印isa的地址,而後與上ISA_MASK的值,獲得的就是類信息;在經過p/x ZXPerson.class來進行驗證。結果是兩個值都是一致的。
  • 設置環境變量 OBJC_DISABLE_NONPOINTER_ISA
    • 上文中提到過能夠經過設置環境變量OBJC_DISABLE_NONPOINTER_ISA來改變isa的類型,OBJC_DISABLE_NONPOINTER_ISA的含義是:當設置值爲1時,當前建立的因此isa均爲普通isa。
    • 在Edit_Scheme中添加變量:

    圖片.png

    • 進行對比後咱們發現isa二進制的首位發送了變化;普通的isa首位是‘0’,而且總體只保留了shiftcls的信息。

    圖片.png 圖片.png

  • 我是如何知道這些環境變量的?(2021-7-21補充)
    • 其實相似這種的環境變量還有不少,我是怎麼知道有OBJC_DISABLE_NONPOINTER_ISA這個的呢?
    • 其實很簡單,咱們只需在終端輸入 export OBJC_HELP=1 便可將全部環境變量打印出來,而且每一個環境變量後面還有對應的用途與解釋。你們不妨能夠本身耍一耍。

    圖片.png

5. isa的位運算

  • 經過對isa地址進行位運算獲得類信息
    • 上一節咱們經過ISA_MASK來獲取了類信息,本節我在介紹一種方式:採用對isa地址進行位運算來獲取類信息。
    • 咱們知道shiftcls的位置就存放類信息的地方,他在結構體中佔用33位。我看先按右移3位、左移28+3位;、右移28位;三步驟就能夠將shiftcls先後的數據進行清空,這時isa中剩下的數據就只有類信息了。請看以下示意圖:

    圖片.png

    • 下面是驗證結果,最終結果與咱們料想的一致。

    圖片.png

  • 節點小結:
    • 經過對isa佔位的理解,經過對isa地址進行位運算的方式,一樣能夠獲取到類信息。本小結內容到此結束。

6. init與new的區別

  • init源碼:
    • 咱們經過command + shift + O 來搜索init,找到後點擊進入;
    圖片.png
    • 找到實例對象會調用的入口,可是裏面沒有任何處理直接將obj對象返回了。
    圖片.png 圖片.png
  • new源碼:
    • 經過command + shift + O 來搜索new,找到後點擊進入;
    圖片.png 圖片.png
    • 找到入口後發現此方法就是再調用了callAlloc後再進行init的調用操做,因此驗證了 [[alloc]init] new 是效果是相等的。
  • 節點小結:
    • 經過對源碼的分析,咱們獲得的結論就是 init只是單純的初始化,而new則是 alloc + init 。本小結內容到此結束。

7. 總結

  • 一、能夠經過clang、xcrun等命令對OC源碼進行編譯,編譯後的代碼可讓咱們更明確的分析底層實現。
  • 二、在結構體中能夠經過設定位域來對內部成員進行獨特的設置。
  • 三、共用體的特性:所含成員中最大的佔位就是共用體的大小;內部佔用空間是共享的,給不一樣成員賦值時會改變其餘成員的值;
  • 四、nonPointerIsa是一種特殊的isa,裏面除了包含class信息以外,還有其餘額外的數據;
  • 五、經過isa_t能夠對其進行位運算來獲取想要的數據;
  • 六、init只是單純的初始化方法,蘋果沒有對齊進行特殊處理;newalloc + new的簡便方式。
寫到最後
導航:
相關文章
相關標籤/搜索