寫在前面:本文并非原創(chuàng),再此使用也僅為學(xué)習(xí)記錄,以便后期復(fù)習(xí)涮毫,原文作者:Style_月月,地址:http://www.reibang.com/p/b72018e88a97
一贷屎、學(xué)習(xí)底層原理罢防,我們首先要找到Apple開源的代碼庫地址:
1、Apple 所有開源源碼匯總地址唉侄,根據(jù)相應(yīng)的版本查找對應(yīng)的源碼咒吐,以mac 10.15為例: macOS --> 10.15 --> 選擇10.15 --> 搜索 objc
2、Apple 比較直接的源碼下載地址属划,直接搜索想要下載的源碼名稱即可恬叹,例如objc
:直接搜索 objc --> objc4/ --> 選擇相應(yīng)的objc的版本
二、源碼分析
在分析alloc源碼之前同眯,先來看看一下3個變量 內(nèi)存地址 和 指針地址區(qū)別:分別輸出3個對象的內(nèi)容绽昼、內(nèi)存地址、指針地址
须蜗,下圖是打印結(jié)果
結(jié)論:通過上圖可以看出硅确,3個對象
指向的是同一個內(nèi)存空間
,所以其內(nèi)容
和內(nèi)存地址
是相同的明肮,但是對象的指針地址
是不同的補(bǔ)充
%p -> &p1:是對象的
指針地址
%p -> p1: 是對象指針指向的的
內(nèi)存地址
這就是本文需要探索的內(nèi)容菱农,alloc做了什么?init做了什么柿估?
三循未、alloc 源碼探索
alloc + init 整體源碼的探索流程如下
- 【第一步】首先根據(jù)
main
函數(shù)中的YXPerson
類的alloc
方法進(jìn)入alloc方法的源碼實(shí)現(xiàn)(即源碼分析開始),
//alloc源碼分析-第一步
+ (id)alloc {
return _objc_rootAlloc(self);
}
- 【第二步】跳轉(zhuǎn)至
_objc_rootAlloc
的源碼實(shí)現(xiàn)
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
- 【第三步】跳轉(zhuǎn)至
callAlloc
的源碼實(shí)現(xiàn)
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)// alloc 源碼 第三步
{
#if __OBJC2__ //有可用的編譯器優(yōu)化
/*
參考鏈接:http://www.reibang.com/p/536824702ab6
*/
// checkNil 為false秫舌,!cls 也為false 的妖,所以slowpath 為 false,假值判斷不會走到if里面足陨,即不會返回nil
if (slowpath(checkNil && !cls)) return nil;
//判斷一個類是否有自定義的 +allocWithZone 實(shí)現(xiàn)嫂粟,沒有則走到if里面的實(shí)現(xiàn)
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available. // 沒有可用的編譯器優(yōu)化
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
如上所示,在calloc
方法中钠右,當(dāng)我們無法確定實(shí)現(xiàn)走到哪步時赋元,可以通過斷點(diǎn)調(diào)試,判斷執(zhí)行走哪部分邏輯。這里是執(zhí)行到_objc_rootAllocWithZone
- 【第四步】跳轉(zhuǎn)至
_objc_rootAllocWithZone
的源碼實(shí)現(xiàn)
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)// alloc 源碼 第四步
{
// allocWithZone under __OBJC2__ ignores the zone parameter
//zone 參數(shù)不再使用 類創(chuàng)建實(shí)例內(nèi)存空間
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
- 【第五步】跳轉(zhuǎn)至
_class_createInstanceFromZone
的源碼實(shí)現(xiàn)搁凸,這部分是alloc
源碼的核心操作媚值,由下面的流程圖及源碼可知,該方法的實(shí)現(xiàn)主要分為三部分 -
cls->instanceSize
:計(jì)算需要開辟的內(nèi)存空間大小 -
calloc
:申請內(nèi)存护糖,返回地址指針 -
obj->initInstanceIsa
:將 類 與isa
關(guān)聯(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)實(shí)現(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;
//計(jì)算需要開辟的內(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) 關(guān)聯(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);
}
四、內(nèi)存字節(jié)對齊原則
Apple的分配內(nèi)存是16字節(jié)對齊嫡良,在解釋為什么需要16字節(jié)對齊之前锰扶,首先需要了解內(nèi)存字節(jié)對齊的原則,主要有以下三點(diǎn)
-
數(shù)據(jù)成員對?規(guī)則
:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員寝受,第?個數(shù)據(jù)成員放在offset為0的地?坷牛,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員??或者成員的?成員??(只要該成員有?成員,?如說是數(shù)組很澄,結(jié)構(gòu)體等)的整數(shù)倍開始(?如int為4字節(jié)京闰,則要從4的整數(shù)倍地址開始存儲。 比如:當(dāng)前開始的位置m = 9 接下來的數(shù)據(jù)成員n = 4甩苛,則n從12的位置開始蹂楣。 -
結(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ù)倍,不?的要補(bǔ)?辆雾。
為什么需要16字節(jié)對齊
需要字節(jié)對齊的原因,有以下幾點(diǎn):
- 通常內(nèi)存是由一個個字節(jié)組成的月劈,cpu在存取數(shù)據(jù)時度迂,并不是以字節(jié)為單位存儲,而是以塊為單位存取猜揪,塊的大小為內(nèi)存存取力度惭墓。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能而姐,所以可以通過
減少存取次數(shù)
來降低cpu的開銷
- 16字節(jié)對齊腊凶,是由于在一個對象中,第一個屬性isa占8字節(jié),當(dāng)然一個對象肯定還有其他屬性钧萍,當(dāng)無屬性時褐缠,會預(yù)留8字節(jié),即16字節(jié)對齊风瘦,如果不預(yù)留队魏,相當(dāng)于這個對象的isa和其他對象的isa緊挨著,容易造成訪問混亂
- 16字節(jié)對齊后万搔,可以
加快CPU讀取速度
胡桨,同時使訪問更安全
,不會產(chǎn)生訪問混亂的情況
總結(jié) - 通過對
alloc
源碼的分析瞬雹,可以得知alloc的主要目的就是開辟內(nèi)存昧谊,而且開辟的內(nèi)存需要使用
16字節(jié)對齊算法,現(xiàn)在開辟的內(nèi)存的大小基本上都是
16`的整數(shù)倍 - 開辟內(nèi)存的核心步驟有3步:
計(jì)算 -- 申請 -- 關(guān)聯(lián)
五酗捌、init 源碼探索
alloc源碼探索完了揽浙,接下來探索init源碼,通過源碼可知意敛,inti的源碼實(shí)現(xiàn)有以下兩種
類方法 init
+ (id)init {
return (id)self;
}
這里的init
是一個構(gòu)造方法 馅巷,是通過工廠設(shè)計(jì)(工廠方法模式)
,主要是用于給用戶提供構(gòu)造方法入口
。這里能使用id
強(qiáng)轉(zhuǎn)的原因草姻,主要還是因?yàn)?code>內(nèi)存字節(jié)對齊后钓猬,可以使用類型強(qiáng)轉(zhuǎn)為你所需的類型
實(shí)例方法 init
- 通過以下代碼進(jìn)行探索實(shí)例方法 init
LGPerson *objc = [[LGPerson alloc] init];
- 通過
main
中的init
跳轉(zhuǎn)至init的源碼實(shí)現(xiàn)
- (id)init {
return _objc_rootInit(self);
}
- 跳轉(zhuǎn)至
_objc_rootInit
的源碼實(shí)現(xiàn)
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ū)別,以下是objc中new的源碼實(shí)現(xiàn)剧劝,通過源碼可以得知橄登,new函數(shù)中直接調(diào)用了callAlloc
函數(shù)(即alloc中分析的函數(shù)),且調(diào)用了init
函數(shù)讥此,所以可以得出new
其實(shí)就等價(jià)于[alloc init]
的結(jié)論拢锹。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
但是一般開發(fā)中并不建議使用new
,主要是因?yàn)橛袝r會重寫init
方法做一些自定義的操作萄喳,例如initWithXXX
卒稳,會在這個方法中調(diào)用[super init]
,用new
初始化可能會無法走到自定義的initWithXXX
部分他巨。
總結(jié)
- 如果子類沒有重寫父類的
init
充坑,new
會調(diào)用父類的init
方法 - 如果子類重寫了父類的
init
减江,new
會調(diào)用子類重寫的init
方法 - 如果使用
alloc + 自定義的init
,可以幫助我們自定義初始化操作捻爷,例如傳入一些子類所需參數(shù)等辈灼,最終也會走到父類的init
,相比new
而言役衡,擴(kuò)展性更好茵休,更靈活。