OC對象的始源 - alloc。
前言
我們都知道,創(chuàng)建OC對象的2種方式: [[ClassName alloc]init] 或[ClassName new]
當被問起他們的作用時,可能你的回答是: alloc + init給對象開辟內存空間并完成對象初始化,new是類方法,實現(xiàn)的功能一樣。
這個描述沒有錯,但請詳細描述下他們的作用。
可能你一臉懵逼。 心里已經開罵: 你丫有病吧,我已經說完了呀?。?/strong>
今天,我將幫你扯開alloc、init、new的這塊遮羞布。 讓你深入了解alloc、init、new。
前期準備
方法一:github直接下載我編譯好的objc4-781編譯包
方法二: 參考上一章 手動配置objc4-781編譯包
課前問題
打開objc4-781包,在HTTest文件夾中,創(chuàng)建HTPerson測試文件(繼承自NSObject),切換項目target為HTTest,在main.m文件中加入測試代碼。

%@打印對象%p打印地址&p指針地址
問題:
-
p1、p2、p3對象和地址打印都一致, 為何&p打印不一致 -
p4的地址為什么和p1、p2、p3都不一樣。
學完本章,你就徹底懂了
本節(jié)內容:
- alloc流程
- alloc核心函數
- alloc的地位(init、new)
1. alloc流程
打開源碼工程,跟隨alloc函數,一步步深入。流程如下:

當出現(xiàn)分支時,我們可以添加斷點,輔助查看主流程是進入哪個分支。
不知道打斷點,可參考OC底層原理一:定位源碼(歡迎來到底層世界)內的三種斷點技巧。
在callAlloc處出現(xiàn)了分支。斷點后發(fā)現(xiàn)程序走向_objc_rootAllocWithZone分支,繼而進入_class_createInstanceFromZone函數。

關于
fastpath和slowpath的作用,請移步OC底層原理四: 編譯器優(yōu)化
allocwithZone: 和alloc一樣,為對象分配足夠的內存, cocoa 會遍歷該對象所有的成員變量,通過成員變量的類型來計算所需占用的內存。從iOS8以后,Zone的外層API已被廢棄,僅底層源碼做兼容處理。
_class_createInstanceFromZone是最底層的包工頭。?? 終于找到真正干活的人了。它實現(xiàn)三大核心方法,然后將成品obj返回給外層。

2.alloc核心函數
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;
// 1. 計算開辟的內存大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 2. 申請內存空間
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
// 初始化isa并與objc關聯(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);
}
- hasCxxCtor、hasCxxDtor、fast等 后續(xù)剖析isa會詳細講解
1. 計算內存大小: 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;
}
if (size < 16) size = 16 :做了小于16字節(jié)的判斷。
跟斷點,發(fā)現(xiàn)主流程進入cache.fastInstanceSize(extraBytes):
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);
}
}
繼續(xù)跟斷點,進入align16:
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
align16的實現(xiàn),就是使用位運算算法完成16字節(jié)對齊。
算法
(x + size_t(15)) & ~size_t(15)
以x=8為例,計算過程如下:
8 + size(15) = 23二進制->0000 0000 0001 0111size_t(15)二進制->0000 0000 0000 1111- 取反
~size_t(15)二進制->1111 1111 1111 0000- 求交
&:
0000 0000 0001 0111&1111 1111 1111 0000=0000 0000 0001 0000- 結果表示為十進制:
16
目的:
-
提高性能,加快存儲速度
通常內存是由一個個字節(jié)組成,cpu在存儲數據時,是以固定字節(jié)塊為單位進行存取的。這是一個空間換時間的優(yōu)化方式,這樣不用考慮字節(jié)未對齊的數據,極大節(jié)省了計算資源,提升了存取速度。 -
更安全
在一個對象中,isa占8字節(jié),對象屬性也占8字節(jié)。蘋果公司現(xiàn)在采用16字節(jié)對齊,當對象無屬性是,會預留8字節(jié),即16字節(jié)對齊。 如果不預留,CPU存取時以16字節(jié)為單位長度去訪問,會訪問到相鄰對象,造成訪問混亂。
執(zhí)行完后,回到上層函數size = cls->instanceSize(extraBytes)可打印size值。
此時已完成內存大小的計算。

2. 分配內存 calloc
根據size 大小進行內存分配

執(zhí)行前打印
obj只有cls類名,執(zhí)行后打印,已成功申請內存首地址。但并不是我們想象中的格式
<HTPerson: 0x10069eff0>,這是因為這一步只是單純的完成內存申請,返回首地址。類和地址的綁定是下一步
initInstanceIsa的工作
3. initInstanceIsa
初始化isa,完成與類的綁定
obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
具體的
isa結構和綁定關系,后續(xù)會作為單獨章節(jié)進行講解
在isainit之后加斷點,打印obj,此時發(fā)現(xiàn)地址與類完成綁定

總結: 至此,我們已對alloc有了完整的認知
3. alloc的地位(init、new)
可能你有疑問,alloc把活都干完了,init和new干啥?
init
進入init:
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
進入_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;
}
- 我們發(fā)現(xiàn)
init的類方法和對象方法返回的都是id對象本身。 - 不同的是類方法返回了一個
id類型的self,這是為了可以給開發(fā)者提供自定義構造方法的入口,通過id強轉類型實現(xiàn)工廠設計,返回我們定義的類型。
new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
實際上就是完成調用了callAlloc,走的時alloc流程。
唯一區(qū)別:
-
alloc + init允許對init進行重寫,可自定制init完成工廠設計 -
new是完整封裝,無法在初始化這一步加入自定制需求
答案

- 問題1: p1、p2、p3對象和地址打印都一致, 為何&p打印不一致
alloc是真正開辟內存和綁定對象的,p1、p2、p3共用1個alloc,所以他們都是指向同一目的地址。但是他們本身也是對象,在init時傳入他們自身id,&p打印的是他們自身的地址。
通俗的說:
我有一個房子出售,A、B、C三個都是我員工,他們都領著客戶來看我這套房子。但是他們三個雖然都是我公司員工,但工號(id)不一樣。
如果客戶問他們房子在哪(等同于%@和%p打印),他們都會告訴我房子的具體位置(三人說的一定相同)。
如果顧客問他們是誰(等同于打印&p),他們就會各自回答A、B、C。
- 問題2. p4的地址為什么和p1、p2、p3都不一樣。
因為p1、p2、p3是同一個alloc打印的,而p4是new出來的,new會單獨調用alloc。 所以他們打印肯定不一樣
下一節(jié): OC底層原理五: NSObject的alloc分析