alloc底層原理探索

準備工作

下載 objc4-781 源碼葡幸,選擇mac電腦進行編譯臭蚁。

編譯源碼涧窒,可參考iOS-底層原理 03:objc4-781 源碼編譯 & 調(diào)試

alloc 源碼探索

整體的源碼流程探索如下:

image

首先我們用xcode運行項目菩帝,建立好相關的符號斷點。

image

第一步】根據(jù)main函數(shù)的XXPersion類的alloc方法進入具體源碼的實現(xiàn)。


//alloc源碼分析

+(id)alloc{return_objc_rootAlloc(self);}

第二步】跳進return_objc_rootAlloc()方法查看源碼實現(xiàn)胁附。

//cls就是上面說的XXPersion類
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
//注意第一次會進入這個方法酒繁,調(diào)用callAlloc
id objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

第三步】跳進callAlloc方法查看源碼實現(xiàn)。

 重磅提示 這里是核心方法
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

注意:經(jīng)過調(diào)試發(fā)現(xiàn)先走objc_msgSend方法控妻,向XXPersion類發(fā)送alloc消息然后走到了第一步alloc方法州袒,然后執(zhí)行_objc_rootAlloc方法,最后進入_objc_rootAllocWithZone方法弓候。那么為什么這樣子走兩次呢郎哭?帶著疑問往下走,哈哈~~~~

//第二次進入菇存,調(diào)起`callAlloc`方法夸研,走到第三部的流程。
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

slowpath & fastpath
其中關于是slowpathfastpath這里需要簡要說明下依鸥,這兩個都是objc源碼中定義的宏亥至,其定義如下:

//x很可能為真, fastpath 可以簡稱為 真值判斷
#define fastpath(x) (__builtin_expect(bool(x), 1)) 
//x很可能為假贱迟,slowpath 可以簡稱為 假值判斷
#define slowpath(x) (__builtin_expect(bool(x), 0)) 

其中的__builtin_expect指令是由gcc引入的姐扮,
1、目的:編譯器可以對代碼進行優(yōu)化衣吠,以減少指令跳轉(zhuǎn)帶來的性能下降茶敏。即性能優(yōu)化
2、作用:允許程序員將最有可能執(zhí)行的分支告訴編譯器缚俏。
3惊搏、指令的寫法為:__builtin_expect(EXP, N)。表示 EXP==N的概率很大忧换。
4恬惯、fastpath定義中__builtin_expect((x),1)表示x的值為真的可能性更大;即 執(zhí)行if 里面語句的機會更大
5包雀、slowpath定義中的__builtin_expect((x),0)表示 x 的值為假的可能性更大宿崭。即執(zhí)行else 里面語句的機會更大
6、在日常的開發(fā)中才写,也可以通過設置來優(yōu)化編譯器葡兑,達到性能優(yōu)化的目的,設置的路徑為:Build Setting --> Optimization Level --> Debug -->None 改為 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判斷一個類是否有自定義的 +allocWithZone 實現(xiàn)赞草,這里通過斷點調(diào)試讹堤,是沒有自定義的實現(xiàn),所以會執(zhí)行到 if 里面的代碼厨疙,即走到_objc_rootAllocWithZone洲守。
第四步】進入_objc_rootAllocWithZone方法,其源碼實現(xiàn)如下:

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    //zone 參數(shù)不再使用 類創(chuàng)建實例內(nèi)存空間
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

第五步】進入_class_createInstanceFromZone方法,這是alloc源碼的核心操作梗醇,大致可以分為一下的三部分:

  • cls->instanceSize:計算需要開辟的內(nèi)存空間大小
  • calloc:申請內(nèi)存知允,返回地址指針
  • obj->initInstanceIsa:將 類 與 isa 關聯(lián)
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)// alloc 源碼 第五步
{
    ASSERT(cls->isRealized()); //檢查是否已經(jīng)實現(xiàn)

    // Read class's info bits all at once for performance
    //一次性讀取類的位信息以提高性能
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    //計算需要開辟的內(nèi)存大小,傳入的extraBytes 為 0
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        //申請內(nèi)存
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }
    if (!zone && fast) {
        //將 cls類 與 obj指針(即isa) 關聯(lián)
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags)

根據(jù)以上的源碼分析叙谨,得出以下的流程圖


image.png

重點:calloc方法

cls->instanceSize:計算所需內(nèi)存大小
開辟內(nèi)存大小過程如下:

image.png

如果fastpath為true温鸽,跳轉(zhuǎn)到instanceSize方法:

ize_t instanceSize(size_t extraBytes) const {
    //編譯器快速計算內(nèi)存大小
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    // 計算類中所有屬性的大小 + 額外的字節(jié)數(shù)0
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    //如果size 小于 16,最小取16
    if (size < 16) size = 16;
    return size;
}

然后跳轉(zhuǎn)到fastInstanceSize手负,源碼實現(xiàn):

size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));
    //Gcc的內(nèi)建函數(shù) __builtin_constant_p 用于判斷一個值是否為編譯時常數(shù)涤垫,如果參數(shù)EXP 的值是常數(shù),函數(shù)返回 1竟终,否則返回 0
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        //刪除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8個字節(jié)
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}

然后跳轉(zhuǎn)到alight16蝠猬,這是一個16進制對齊的算法,源碼如下:

//16字節(jié)對齊算法
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

內(nèi)存對齊規(guī)則

每個特定平臺上的編譯器都有自己的默認“對齊系數(shù)”(也叫對齊模數(shù))统捶。程序員可以通過預編譯命令#pragma pack(n)榆芦,n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”瘾境。

1.數(shù)據(jù)成員對齊規(guī)則:struct 或 union (以下統(tǒng)稱結(jié)構體)的數(shù)據(jù)成員歧杏,第一個數(shù)據(jù)成員A放在偏移為 0 的地方,以后每個數(shù)據(jù)成員B的偏移為(#pragma pack(指定的數(shù)n) 與 該數(shù)據(jù)成員(也就是 B)的自身長度中較小那個數(shù)的整數(shù)倍迷守,不夠整數(shù)倍的補齊。

2.數(shù)據(jù)成員為結(jié)構體:如果結(jié)構體的數(shù)據(jù)成員還為結(jié)構體旺入,則該數(shù)據(jù)成員的“自身長度”為其內(nèi)部最大元素的大小兑凿。(struct a 里存有 struct b,b 里有char,int,double等元素茵瘾,那 b “自身長度”為 8)

3.結(jié)構體的整體對齊規(guī)則:在數(shù)據(jù)成員按照上述第一步完成各自對齊之后礼华,結(jié)構體本身也要進行對齊。對齊會將結(jié)構體的大小調(diào)整為(#pragma pack(指定的數(shù)n) 與 結(jié)構體中的最大長度的數(shù)據(jù)成員中較小那個的整數(shù)倍拗秘,不夠的補齊圣絮。

為什么要進行16字節(jié)對齊

  • 通常內(nèi)存是由一個個字節(jié)組成的,cpu在存取數(shù)據(jù)時雕旨,并不是以字節(jié)為單位存儲扮匠,而是以塊為單位存取,塊的大小為內(nèi)存存取力度凡涩。頻繁存取字節(jié)未對齊的數(shù)據(jù)棒搜,會極大降低cpu的性能,所以可以通過減少存取次數(shù)來降低cpu的開銷
  • 16字節(jié)對齊活箕,是由于在一個對象中力麸,第一個屬性isa占8字節(jié),當然一個對象肯定還有其他屬性,當無屬性時克蚂,會預留8字節(jié)闺鲸,即16字節(jié)對齊,如果不預留埃叭,相當于這個對象的isa和其他對象的isa緊挨著翠拣,容易造成訪問混亂
  • 16字節(jié)對齊后,可以加快CPU讀取速度游盲,同時使訪問更安全误墓,不會產(chǎn)生訪問混亂的情況。
    16字節(jié)對齊算法的計算過程
    image.png
  • 首先將原始的內(nèi)存 8size_t(15)相加益缎,得到 8 + 15 = 23
  • size_t(15)15進行~(取反)操作谜慌,~(取反)的規(guī)則是:1變?yōu)?,0變?yōu)?
  • 最后將 23 與 15的取反結(jié)果 進行 &(與)操作莺奔,&(與)的規(guī)則是:都是1為1欣范,反之為0,最后的結(jié)果為 16令哟,即內(nèi)存的大小是以16的倍數(shù)增加的
    calloc:申請內(nèi)存恼琼,返回地址指針
    通過instanceSIze計算內(nèi)存的大小,向內(nèi)存申請為size大小的內(nèi)存屏富,拿到賦值給obj晴竞,obj是指向內(nèi)存地址的指針。
obj = (id)calloc(1, size);

注意:未執(zhí)行calloc時候obj返回的是nil狠半,執(zhí)行完calloc之后返回一個16進制的地址噩死。在平時開發(fā)中,一個對象的打印的格式都是類似于這樣的<XXPerson: 0x01111111f>(是一個指針),而obj打印的是一個地址神年,只要是因為還沒有傳入cls的關聯(lián)已维,同時印證了alloc的根本作用就是開辟內(nèi)存。
obj->initInstanceIsa:類與isa關聯(lián)
當calloc完成之后已日,內(nèi)存空間就申請好了垛耳。然后就進行isa的關聯(lián),流程圖如下:

image.png

這個過程主要是初始化一個isa指針飘千,并將isa指針指向申請的內(nèi)存地址堂鲜,將指針與cls(XXPersion類)進行關聯(lián)。

總結(jié)

  • 通過對alloc源碼的系統(tǒng)學習占婉,可以知道alloc主要目的就是開辟內(nèi)存泡嘴,開辟內(nèi)存需要16字節(jié)對齊算法,開辟的內(nèi)存大小基本上都是16的整數(shù)倍逆济。
  • 開辟內(nèi)存的核心步驟:計算------申請------關聯(lián)酌予。

上面的疑點:為什么第三步會走兩次

通過llvm源碼得知在編譯的過程中磺箕,alloc方法被hook成上面說的objc_alloc方法,這樣做的目的就是標記一個receiver抛虫,在標記完這個類為receiver之后都會進入普通的消息發(fā)送判斷(即第二次進入的alloc方法)松靡,這樣做的目的其實就是間接符號的綁定

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末建椰,一起剝皮案震驚了整個濱河市雕欺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棉姐,老刑警劉巖屠列,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異伞矩,居然都是意外死亡笛洛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門乃坤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苛让,“玉大人,你說我怎么就攤上這事湿诊∮埽” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵厅须,是天一觀的道長仿畸。 經(jīng)常有香客問我,道長九杂,這世上最難降的妖魔是什么颁湖? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮例隆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抢蚀。我一直安慰自己镀层,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布皿曲。 她就那樣靜靜地躺著唱逢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪屋休。 梳的紋絲不亂的頭發(fā)上坞古,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音劫樟,去河邊找鬼痪枫。 笑死织堂,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的奶陈。 我是一名探鬼主播易阳,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼药蜻,長吁一口氣:“原來是場噩夢啊……” “哼阐滩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起狼牺,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤徐勃,失蹤者是張志新(化名)和其女友劉穎事示,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體僻肖,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡肖爵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了檐涝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遏匆。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谁榜,靈堂內(nèi)的尸體忽然破棺而出幅聘,到底是詐尸還是另有隱情,我是刑警寧澤窃植,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布帝蒿,位于F島的核電站,受9級特大地震影響巷怜,放射性物質(zhì)發(fā)生泄漏葛超。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一延塑、第九天 我趴在偏房一處隱蔽的房頂上張望绣张。 院中可真熱鬧,春花似錦关带、人聲如沸侥涵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芜飘。三九已至,卻和暖如春磨总,著一層夾襖步出監(jiān)牢的瞬間嗦明,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工蚪燕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留娶牌,地道東北人奔浅。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像裙戏,于是被迫代替她去往敵國和親乘凸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容