OC底層原理02 - alloc & init & new 源碼分析

在分析alloc源碼之前羡疗,先來觀察下以下3個對象:

image

分別輸出3個對象的 內(nèi)容挺物、指針地址、對象地址纺棺,下圖是打印結(jié)果

image

通過打印可以看出榄笙,3個對象指向的是同一個內(nèi)存空間,所以其內(nèi)容對象地址是相同的祷蝌,但是指針地址是不同的

所以接下來將探索茅撞,alloc做了什么?init做了什么巨朦?

alloc源碼探究

alloc大致流程如下

image
  1. 首先根據(jù)main函數(shù)中的HLPerson類的alloc方法進入alloc方法的源碼(即源碼分析開始)
+ (id)alloc {
    return _objc_rootAlloc(self);
}
  1. 跳轉(zhuǎn)至_objc_rootAlloc的源碼
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  1. 跳轉(zhuǎn)至callAlloc的源碼
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

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

callAlloc方法里面可以看到if的判斷條件米丘,那么fastpath(!cls->ISA()->hasCustomAWZ())都做了什么呢?fastpath又是什么呢罪郊?

  • fastpath()
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0)) 

/*
編譯器指令,允許程序員將最有可能執(zhí)行的分支告訴編譯器,讓編譯器在生成指令的時候尚洽,概率很高的分支指令不需要跳轉(zhuǎn)悔橄,而概率低的分支指令需要經(jīng)過跳轉(zhuǎn),從而使得大部分情況下都不需要經(jīng)過跳轉(zhuǎn),提高執(zhí)行效率.
(__builtin_expect(bool(x), 1)):表示bool(x)為真的可能性較大
(__builtin_expect(bool(x), 0)):表示bool(x)為假的可能性較大
*/
__builtin_expect(EXP, N)
  • cls->ISA()->hasCustomAWZ()
    這個函數(shù)是用來判斷一個類是否有自定義的+allocWithZone方法癣疟。
  • fastpath(!cls->ISA()->hasCustomAWZ())
    這里表示挣柬,一個類沒有自定義+allocWithZone方法的可能性比較大。
    HLPerson類中沒有自定義+allocWithZone睛挚,那么callAlloc函數(shù)會執(zhí)行到return _objc_rootAllocWithZone(cls, nil);這里
  1. 跳轉(zhuǎn)至_objc_rootAllocWithZone的源碼
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
  1. 跳轉(zhuǎn)至_class_createInstanceFromZone的源碼
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)
{
    ASSERT(cls->isRealized());

    // 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;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // alloc 開辟內(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) {
        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);
}

這部分是alloc源碼的核心操作邪蛔,該方法的實現(xiàn)主要分為三部分:

  • cls -> instanceSize -> 計算需要開辟的內(nèi)存空間大小
  • calloc -> 申請內(nèi)存
  • obj -> initInstanceIsa -> 將申請的內(nèi)存與isa關(guān)聯(lián)

cls -> instanceSize:計算所需內(nèi)存大小

  • 跳轉(zhuǎn)至instanceSize的源碼
    size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

通過斷點調(diào)試,會執(zhí)行到cache.fastInstanceSize方法扎狱,計算內(nèi)存大小侧到。跳轉(zhuǎn)至fastInstanceSize的源碼

    size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        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
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

通過斷點調(diào)試,會執(zhí)行到align16

  • 跳轉(zhuǎn)至align16的源碼淤击,這個方法是 16字節(jié)對齊算法
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

內(nèi)存字節(jié)對齊原則

  • 數(shù)據(jù)成員對齊規(guī)則:struct 或者 union 的數(shù)據(jù)成員匠抗,第一個數(shù)據(jù)成員放在offset為0的地方,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大形厶А(只要該成員有子成員汞贸,比如數(shù)據(jù)、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機中是4字節(jié)印机,則要從4的整數(shù)倍地址開始存儲)
  • 數(shù)據(jù)成員為結(jié)構(gòu)體:如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員矢腻,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(例如:struct a里面存有struct b,b里面有char射赛、int多柑、double等元素,則b應(yīng)該從8的整數(shù)倍開始存儲)
  • 結(jié)構(gòu)體的整體對齊規(guī)則:結(jié)構(gòu)體的總大小咒劲,即sizeof的結(jié)果顷蟆,必須是其內(nèi)部做大成員的整數(shù)倍,不足的要補齊

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

  • 通常內(nèi)存是由一個個字節(jié)組成的腐魂,cpu在存取數(shù)據(jù)時帐偎,并不是以字節(jié)為單位存儲,而是以塊為單位存取蛔屹,塊的大小為內(nèi)存存取力度削樊。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能兔毒,所以可以通過減少存取次數(shù)來降低cpu的開銷
  • 16字節(jié)對齊漫贞,是由于在一個對象中,第一個屬性isa占8字節(jié)育叁,當然一個對象肯定還有其他屬性迅脐,當無屬性時,會預(yù)留8字節(jié)豪嗽,即16字節(jié)對齊谴蔑,如果不預(yù)留豌骏,相當于這個對象的isa和其他對象的isa緊挨著,容易造成訪問混亂
  • 16字節(jié)對齊后隐锭,可以加快CPU讀取速度窃躲,同時使訪問更安全,不會產(chǎn)生訪問混亂的情況

字節(jié)對齊-總結(jié)

  • 在字節(jié)對齊算法中钦睡,對齊的主要是對象蒂窒,而對象的本質(zhì)則是一個 struct objc_object的結(jié)構(gòu)體
  • 結(jié)構(gòu)體在內(nèi)存中是連續(xù)存放的,所以可以利用這點對結(jié)構(gòu)體進行強轉(zhuǎn)
  • 蘋果早期是8字節(jié)對齊荞怒,現(xiàn)在是16字節(jié)對齊

calloc:申請內(nèi)存洒琢,返回地址指針

通過instanceSize計算的內(nèi)存大小,向內(nèi)存中申請大小為size的內(nèi)存挣输,并賦值給obj纬凤,因此obj是指向內(nèi)存地址的指針

obj = (id)calloc(1, size);

obj -> initInstanceIsa:類與isa關(guān)聯(lián)

經(jīng)過calloc,內(nèi)存已經(jīng)申請好了撩嚼,類也已經(jīng)傳入進來了停士,那么接下來就需要將地址指針isa指針進行關(guān)聯(lián)

alloc總結(jié):alloc的主要目的是開辟一段指定大小的內(nèi)存空間,并將這段內(nèi)存空間關(guān)聯(lián)到對象的isa指針完丽。

init 源碼探索

通過代碼進行探索init方法

HLPerson *p1 = [[HLPerson alloc] init];
  • 跳轉(zhuǎn)至init的源碼
- (id)init {
    return _objc_rootInit(self);
}
  • 跳轉(zhuǎn)至_objc_rootInit的源碼
id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

有此可見涨椒,返回的是傳入的self本身

new 源碼探索

在開發(fā)中兵罢,初始化除了init奔缠,還可以使用new束昵,兩者本質(zhì)上其實并沒有什么區(qū)別,以下是objcnew的源碼實現(xiàn)聘鳞,通過源碼可以得知薄辅,new函數(shù)中直接調(diào)用了callAlloc函數(shù)(即alloc中分析的函數(shù)),且調(diào)用了init函數(shù)抠璃,所以[p1 new] 其實就等價于 [[p1 alloc] init]

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

tips:但是一般開發(fā)中并不建議使用new站楚,主要是因為有時會重寫init方法做一些自定義的操作,例如initWithXXX搏嗡,會在這個方法中調(diào)用[super init]窿春,用new初始化可能會無法走到自定義的initWithXXX部分。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末采盒,一起剝皮案震驚了整個濱河市旧乞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌磅氨,老刑警劉巖尺栖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異烦租,居然都是意外死亡延赌,警方通過查閱死者的電腦和手機货徙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來皮胡,“玉大人,你說我怎么就攤上這事赏迟。” “怎么了甩栈?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵突想,是天一觀的道長袭灯。 經(jīng)常有香客問我,道長姨丈,這世上最難降的妖魔是什么蟋恬? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任矾飞,我火速辦了婚禮,結(jié)果婚禮上申眼,老公的妹妹穿的比我還像新娘。我一直安慰自己屁柏,他們只是感情好雀摘,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般每聪。 火紅的嫁衣襯著肌膚如雪救斑。 梳的紋絲不亂的頭發(fā)上穷娱,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天嫁盲,我揣著相機與錄音缸托,去河邊找鬼哺哼。 笑死,一個胖子當著我的面吹牛咬扇,可吹牛的內(nèi)容都是我干的甲葬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼懈贺,長吁一口氣:“原來是場噩夢啊……” “哼经窖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起梭灿,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤画侣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后堡妒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體配乱,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年皮迟,在試婚紗的時候發(fā)現(xiàn)自己被綠了搬泥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡伏尼,死狀恐怖忿檩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情爆阶,我是刑警寧澤燥透,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站辨图,受9級特大地震影響班套,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜故河,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一孽尽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧忧勿,春花似錦杉女、人聲如沸瞻讽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽速勇。三九已至,卻和暖如春坎拐,著一層夾襖步出監(jiān)牢的瞬間烦磁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工哼勇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留都伪,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓积担,卻偏偏與公主長得像陨晶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子帝璧,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

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