iOS底層學習 - OC對象前世此生

在平時的開發中,若是說你沒有對象,我是不信的。那麼本着瞭解對象,呵護對象,關心對象的原則,咱們必定要清楚的知道什是事對象,以及對象是怎麼來的和對象內部的構成。so,這篇文章就來剖析一下對象的前世此生c++

準備工做

傳送門☞iOS底層探索-準備工做算法

對象的本質

首先,咱們要知道對象OC的底層中,到底是以什麼方式在存在的數組

1.首先建立對象後,咱們運行clang -rewrite-objc main.m -o test.c++命令,將main文件轉換爲C++代碼,看代碼編譯後的對象是什麼樣子的緩存

2.經過查看源碼,咱們能夠發現,一個對象在通過編譯以後,在底層的是一個結構體的形式存在的,以下圖bash

3.其中咱們能夠發現有一個 struct NSObject_IMPL NSObject_IVARS結構體,這個結構體其實是全部繼承自NSobject的類均有的一個屬性,它是一個指向類對象的isa,如圖

屬性和成員變量

咱們給類添加了一個屬性name和一個成員變量name,那麼這二者有何區別呢,經過編譯後的源碼,咱們能夠發現架構

1.成員變量和屬性都是對象struct結構體中的一個變量,可是屬性會自動變成帶下劃線的變量,如_name,而成員變量不會
2.屬性會自動生成get和set方法,而成員變量不會
複製代碼
get和set源碼分析

底層get和set源碼以下app

能夠看到系統自動給方法增長了兩個參數LGPerson * self, SEL _cmd ide

在類的方法列表中,我麼能夠看到相關綁定實現

  • name 爲上層get方法名
  • @16@0:8 爲符號方法簽名
  • _I_LGPerson_name爲底層方法名

關於方法簽名:函數

第一個符號@爲返回值類型源碼分析

第二個16爲方法所佔用的偏移量,即爲總長度

第三個@爲系統方法生成的id類型參數

第四個0爲@的偏移量,即從0位開始,id類型佔有8個字節(0-7)

第五個:爲SEL參數

第六個8爲SEL方法的偏移量,即SEL參數從第8位開始(8-15) 下圖爲整理的符號表

alloc原理

對象的本質有了初步瞭解後,咱們須要知道對象是怎麼開闢控件,怎麼獲取到的,經常使用的[LGPerson alloc]init]究竟是怎麼工做的呢

根據準備工做,配置到objc的源碼以後,咱們開始探索

alloc流程

經過斷點逐步跟蹤

_objc_rootAlloc(self)

發現alloc的底層調用了 _objc_rootAlloc(self)

可是一個須要注意的點:系統動態庫真是函數地址在共享緩存去,dyld加載的時候綁定一次符號,之後就直接找真實的函數地址,因此,第一次alloc的時候,會調用 objc_alloc(Class cls)方法,後續才走 _objc_rootAlloc(Class cls)

callAlloc(cls, false/checkNil/, true/allocWithZone/)

callAlloc分析

1.hasDefaultAWZ

hasDefaultAWZ( )方法是用來判斷當前class是否有默認的allocWithZone。

bool hasCustomAWZ() {
    return ! bits.hasDefaultAWZ();
}
複製代碼
bool hasDefaultAWZ() {
    return data()->flags & RW_HAS_DEFAULT_AWZ;
}
複製代碼

在對象的數據段data中,class_rw_t中有一個flags,RW_HAS_DEFAULT_AWZ 這個是用來標示當前的class或者是superclass是否有默認的alloc/allocWithZone:。值得注意的是,這個值會存儲在metaclass 中。

若是cls->ISA()->hasCustomAWZ()返回YES,意味着有默認的allocWithZone方法,那麼就直接對class進行allocWithZone,申請內存空間。

+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
複製代碼

2.canAllocFast是否能夠快速建立,若是能夠,直接調用calloc函數,申請1塊bits.fastInstanceSize()大小的內存空間,若是建立失敗,會調用callBadAllocHandler函數,返回錯誤信息。新版本中canAllocFast默認返回false,由於宏定義FAST_ALLOC沒有進行定義

3.在新版本代碼中,會直接走else的class_createInstance方法

class_createInstance

若是沒有快速建立等方法,會走到class_createInstance中, 這個方法是產生對象的關鍵步驟

1. hasCxxCtor 判斷當前class或者superclass 是否有.cxx_construct構造方法的實現

2.instanceSize(extraBytes)這個方法是計算對象中屬性內存對齊的主要方法,其實屬性以8字節內存對齊,對象以16字節內存對齊,相關更詳細的分下會在內存對齊模塊講述

3.(id)calloc(1, size)方法是對象進行申請內存的主要方法,以16字節對齊,後續malloc原理詳細講述

4.initInstanceIsainitIsa兩個方式是給闖將isa並關聯到對象,後續isa模塊詳細講述

流程圖分析

init和new的原理

經過上述流程,一個對象就已經申請內存,並建立了isa指向該類,完成了從無到有的孕育過程,那麼initnew方法是用來幹什麼的呢。

init源碼

經過源碼可知,底層調用了_objc_rootInit方法

經過 _objc_rootInit方法能夠看到,init方法直接返回了 self,並無作其餘任何的操做

new源碼

經過源碼能夠看到,new方法只是調用了callAllocinit方法

因此,init和new方法只是返回了alloc以後就返回了對象自己,沒有作其餘操做,是方便開發者重寫本身的邏輯的一種工廠模式

malloc和內存對齊

上一節咱們對對象的孕育有了一個大致的瞭解,可是有些概念仍是不太熟悉,好比對象是如何開闢內存的,到底開闢多少的內存才合適?,對象的屬性和對象自己是如何進行二進制對齊的?這一節主要解決這個問題,並探尋原理

例子解讀

經過對象的本質咱們知道,對象在底層是以結構體的形式存在的,那麼要計算結構的大小,就須要知道結構體中所包含的全部屬性的大小,可是進行計算並非單純想加各元素所佔字節大小,編譯器會進行優化

struct LGStruct1 {
    char a;     // 1 [0] 
    double b;   // 8 [8,15]
    int c;      // 4 [16,19]
    short d;    // 2 [20,21]
} MyStruct1;

struct LGStruct2 {
    double b;   // 8 [0,7]
    int c;      // 4 [8,11]
    char a;     // 1 [12]
    short d;    // 2 [14,15]
} MyStruct2;

NSLog(@"%lu---%lu---%lu",sizeof(MyStruct1),sizeof(MyStruct2));
複製代碼

上述代碼的打印結果爲 24-16,其內部遵循如下原則 初始位置爲0,以後每一個元素的位置遵循min(當前開始的位置m n)

LGStruct1:
char a [0]; // [1,7]爲空,由於都不是double字節8的倍數
double b [8,15] // 8位double字節8,直接存放
int c [16,19] // 16爲int字節4 的倍數,直接存放
short d [20,21] // 20 爲short 2 的倍數,直接存放
複製代碼

LGStruct1一共佔有了22字節,可是總大小必定要爲元素最大字節數的倍數,裏面最大爲8字節,因此總字節數應爲8的倍數,因此共申請24字節,以此類推,能夠獲得LGStruct2共佔有了16字節

內存對齊

經過以上的例子,咱們基本能夠總結內存對齊的三原則爲:

  • 結構體(struct)或聯合體(union)的數據成員,第一個數據成員放在offset爲0的地方,之後每一個數據成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,好比數組、結構體等)的整數倍開始。 eg: int爲4字節,則要從4的整數倍地址開始存儲

  • 結構體做爲成員:若是一個結構體內部包含其餘結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲。 eg: struct a裏包含struct b,b中包含其餘char、int、double等元素,那麼b應該從8(double的元素大小)的整數倍開始存儲

  • 收尾工做:結構體的總大小,也就是sizeof的結果,必須是其內部最大成員的整數倍,不足的須要補齊。

1. 屬性對齊

經過上一節instanceSize(extraBytes)方法的源碼咱們來首先進行分析

  • 經過判斷咱們可知,一個對象所申請的空間,最低爲16字節
  • alignedInstanceSize方法:

  • word_align函數是一個進行對齊的算法

該算法和上述例子對齊方式是一個道理,其中WORD_MASK爲7,經過二進制的& ~ 運算,即表明該算法爲8字節對齊,即所計算出的內存爲8的倍數,表明對象實際根據屬性數來申請內存的話,實際上是以8的倍數來進行申請的

2.對象對齊和malloc流程

LGTeacher  *p = [LGTeacher alloc];
    p.name = @"LG_Cooci";   // 8
    p.age  = 18;            // 4
    p.height = 185;         // 8
    p.hobby  = @"女";       // 8
    NSLog(@"%lu - %lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
複製代碼

上述例子打印的結果爲 40,48

根據內存原則,咱們能夠知道,類實際有class_getInstanceSize大小爲8倍數40自家,可是爲何malloc_size有48呢,咱們經過追蹤源碼,發現instanceSize(id)calloc(1, size)的size均爲40,那麼calloc函數到底作了什麼操做

咱們能夠經過libmalloc源碼進行分析 首先建立一個下圖所示代碼

找到calloc的底層實現,因爲返回retval,因此調用malloc_zone_calloc方法

因爲返回 ptr,因此要尋找 zone-calloc

直接點擊的話,會找不到方法,因此要使用LLDB命令來尋找,發現對象的調用爲 default_zone_calloc

(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x000000010031cd14 (.dylib`default_zone_calloc at malloc.c:249)
複製代碼

依舊點擊不了,LLDB走起,發現調用爲nano_calloc

(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x000000010031e33f (.dylib`nano_calloc at nano_malloc.c:878)
複製代碼

經過代碼可知,若是在最大值只下,會return p,若是大於纔會走向helper_zone,並遞歸,因此跟蹤_nano_malloc_check_clear

經過方法能夠發現 segregated_size_to_fit是用來計算的大小的主要方法,而且返回了48,因此對齊方法在此
跟蹤 slot_bytes,此時 NANO_REGIME_QUANTA_SIZE爲15, SHIFT_NANO_QUANTUM爲4,因此slot_bytes爲[目標值 > > 4 < < 4]的位運算,是16字節對齊的,因此申請爲40,16字節對齊後爲48

經過上述mallco的流程咱們能夠知道,對象是以16字節對齊的,因此在屬性對齊時,纔會要求最小16字節

流程圖和總結

malloc的流程圖以下 calloc流程.jpg

n字節對齊方式算法(x爲初始值):

1.(x + (2^n - 1))& ~(2^n - 1)

2.(x + (2^n - 1) 位運算:>>n <<n

總結:根據內存原則,對象在申請內存空間時,首先會進行屬性對齊,此時會已8字節進行對齊,最後會對象對齊,此時已16字節進行對齊,因此對象最小爲16字節,並已16字節對齊

isa的原理和走向

經過上面的小結,咱們已經知道對象是如何建立,內存時如何申請的了,那麼咱們都知道,每一個集成字NSObject的類的默認屬性isa,那麼isa本質究竟是個什麼,它是怎麼綁定的類,以及做用究竟是啥?

isa本質和類綁定

經過alloc流程分析,咱們找到initIsa源碼中堆isa的定義

咱們能夠看到isa實際是一個isa_t結構,看源碼可知,isa是一個聯合體,聯合體中各元素共享內存,並互斥,且isa總共佔有8字節,64位,在類中以Class 對象存在,是用來指向類的地址

那麼 ISA_BITFIELD中存有那些元素麼,這個會根據系統架構的不一樣,有不一樣的元素,咱們以arm64結構來解讀,根據各元素站的位數可得,一共爲64位8字節

  • nonpointer表示是否對 isa 指針開啓指針優化 0:純isa指針,1:不⽌是類對象地址,isa 中包含了類信息、對象的引⽤計數等
  • has_assoc關聯對象標誌位,0沒有,1存在
  • has_cxx_dtor該對象是否有 C++ 或者 Objc 的析構器,若是有析構函數,則須要作析構邏輯, 若是沒有,則能夠更快的釋放對象
  • shiftcls存儲類指針的值。開啓指針優化的狀況下,在 arm64 架構中有 33 位⽤來存儲類指針。
  • magic⽤於調試器判斷當前對象是真的對象仍是沒有初始化的空間
  • weakly_referenced指對象是否被指向或者曾經指向⼀個 ARC 的弱變量, 沒有弱引⽤的對象能夠更快釋放。
  • deallocating標誌對象是否正在釋放內存
  • has_sidetable_rc當對象引⽤技術⼤於 10 時,則須要借⽤該變量存儲進位
  • extra_rc當表示該對象的引⽤計數值,其實是引⽤計數值減 1, 例如,若是對象的引⽤計數爲 10,那麼 extra_rc 爲 9。若是引⽤計數⼤於 10, 則須要使⽤到下⾯的 has_sidetable_rc

isa類綁定

經過源碼發現isa有cls和shiftcls屬性是與類相關的 若是時候純isa指針,那麼直接綁定cls便可

若是爲1,由於是結構體,且前面佔有3位,則須要對類指針進行 >>3的位運算,來存儲類的信息,對cls的地址右移動3位的目的是爲了減小內存的消耗,由於類的指針須要按照8字節對齊,也就是說類的指針的大小一定是8的倍數,其二進制後三位爲0,右移三位抹除後面的3位0並不會產生影響。

isa的指向

經過上面咱們知道了isa是綁定類的,那麼咱們能夠經過object_getClass方法來經過對象是怎麼獲類的。源碼以下

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

objc_object::getIsa() 
{
    // 通常都不是TaggedPointer,這是特殊指針
    if (!isTaggedPointer()) return ISA();
}

objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    // 通常狀況下走這裏,獲取到類
    return (Class)(isa.bits & ISA_MASK);
#endif
}
// 64位架構下 ISA_MASK的值爲
# define ISA_MASK 0x00007ffffffffff8ULL
複製代碼

經過以上源碼能夠發現獲取對象的類就是獲取對象的isa,而isa經過位域&上一個mask(isa.bits & ISA_MASK),就能夠獲取類。

那麼讓我經過打印LGPerson *p = [LGPerson alloc];對象的地址來看,首先對象的第一個屬性爲isa,即0x10200c480第一個值爲isa的值,而後獲取到LGPerson.class類的地址。

1.經過驗證咱們能夠獲得,對象的isa是指向對象的類

那麼類對象的isa又指向什麼呢,咱們能夠經過上述命令繼續驗證

2.經過驗證可得,LGPerson類的isa指向了一個地址徹底不一樣,可是也名爲LGPerson的類,咱們通常叫這個類爲 元類,這是由系統建立的,沒法操做

那麼元類又指向什麼呢,繼續驗證

3.經過驗證可得LGPerson元類的isa指向了NSObject類,咱們通常叫此爲 根元類

那麼根元類又指向什麼呢,走起

4.經過驗證可得,根源類的isa就指向了本身,一個流程就此結束

總結一下:

  • 實例對象的isa指向的是類;
  • 類的isa指向的元類;
  • 元類指向根元類;
  • 根元類指向本身;
  • NSObject的父類是nil,根元類的父類是NSObject。

流程圖

經過上述驗證,能夠獲得經典流程圖

總結

至此一個對象的申請內存並建立,且與類之間的關係已經所有探索完畢,一個章節將要進行類的探究

相關文章
相關標籤/搜索