在長(zhǎng)期iOS開發(fā)中际乘,oc是iOS的基礎(chǔ)也是重中之重,相比runtime井厌,runloop蚓庭,多線程等知識(shí)都要重要的多,更加深入的了解oc的知識(shí)仅仆,才能寫出加健壯的代碼
以下內(nèi)容器赞,摘自網(wǎng)絡(luò),如有版權(quán)墓拜,可以聯(lián)系港柜。
探尋OC對(duì)象的本質(zhì),我們平時(shí)編寫的Objective-C代碼咳榜,底層實(shí)現(xiàn)其實(shí)都是C\C++代碼夏醉。我們通過(guò)clang 將OC代碼轉(zhuǎn)為C\C++代碼
OC如下代碼
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject* objc = [[NSObject alloc] init];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我們通過(guò)clang命令行將OC的mian.m文件轉(zhuǎn)化為c++文件。
輸入指令要注意斜體位置的路徑是本機(jī)Xcode的路徑
clang -x objective-c -rewrite-objc -isysroot /Users/admin/Desktop/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
補(bǔ)充下我的xcode位置在桌面所以是上面鏈接涌韩,正常的應(yīng)該是下面的clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
.cpp文件添加進(jìn)入項(xiàng)目無(wú)法編譯運(yùn)行畔柔,需要去掉它
NSObject_IMPL內(nèi)部
typedef struct objc_class *Class;
struct NSObject_IMPL {
Class isa;
};
NSObjcet的底層實(shí)現(xiàn),點(diǎn)擊NSObjcet進(jìn)入發(fā)現(xiàn)NSObject的內(nèi)部實(shí)現(xiàn)
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end
轉(zhuǎn)化為c語(yǔ)言其實(shí)就是一個(gè)結(jié)構(gòu)體
為了探尋OC對(duì)象在內(nèi)存中如何體現(xiàn)臣樱,我們來(lái)看下面一段代碼
NSObject* objc = [[NSObject alloc] init];
轉(zhuǎn)換后代碼
NSObject* objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)
((NSObject *(*)(id, SEL))(void *)objc_msgSend)
((id)objc_getClass("NSObject"), sel_registerName("alloc")),
sel_registerName("init"));
上述一段代碼中系統(tǒng)為NSObject對(duì)象分配8個(gè)字節(jié)的內(nèi)存空間靶擦,用來(lái)存放一個(gè)成員isa指針。那么isa指針這個(gè)變量的地址就是結(jié)構(gòu)體的地址雇毫,也就是NSObjcet對(duì)象的地址玄捕。
獲取對(duì)象大小
class_getInstanceSize([NSObject class]); // 8 返回的是對(duì)象的大小
malloc_size( (__bridge const void *)(objc));// 16 返回的是分配空間大小
oc分配對(duì)象大小源碼
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
//CF要求所有對(duì)象至少為16個(gè)字節(jié)。當(dāng)小于16后會(huì)分配16棚放。
if (size < 16) size = 16;
return size;
}
那么這個(gè)結(jié)構(gòu)體占多大的內(nèi)存空間呢枚粘,我們發(fā)現(xiàn)這個(gè)結(jié)構(gòu)體只有一個(gè)成員,isa指針飘蚯,而指針在64位架構(gòu)中占8個(gè)字節(jié)馍迄。也就是說(shuō)一個(gè)NSObjec對(duì)象所占用的內(nèi)存是8個(gè)字節(jié)福也。
但是我們發(fā)現(xiàn)NSObject對(duì)象中還有很多方法,那這些方法不占用內(nèi)存空間嗎攀圈?其實(shí)類的方法等也占用內(nèi)存空間拟杉,但是這些方法所占用的存儲(chǔ)空間并不在NSObject對(duì)象中。
那跟復(fù)雜的繼承關(guān)系對(duì)象的大小是如何計(jì)算的
繼承自NSObject的對(duì)象量承,那么底層結(jié)構(gòu)體內(nèi)一定有一個(gè)isa指針。
那么他們所占的內(nèi)存空間是多少呢穴店?單純的將指針和成員變量所占的內(nèi)存相加即可嗎撕捍?上述代碼實(shí)際打印的內(nèi)容是16 16,也就是說(shuō)泣洞,person對(duì)象和student對(duì)象所占用的內(nèi)存空間都為16個(gè)字節(jié)忧风。
其實(shí)實(shí)際上person對(duì)象確實(shí)只使用了12個(gè)字節(jié)。但是因?yàn)閮?nèi)存對(duì)齊的原因球凰。使person對(duì)象也占用16個(gè)字節(jié)狮腿。
編譯器在給結(jié)構(gòu)體開辟空間時(shí),首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類型呕诉,然后尋找內(nèi)存地址能是該基本數(shù)據(jù)類型的整倍的位置缘厢,作為結(jié)構(gòu)體的首地址。將這個(gè)最寬的基本數(shù)據(jù)類型的大小作為對(duì)齊模數(shù)甩挫。
為結(jié)構(gòu)體的一個(gè)成員開辟空間之前贴硫,編譯器首先檢查預(yù)開辟空間的首地址相對(duì)于結(jié)構(gòu)體首地址的偏移是否是本成員的整數(shù)倍,若是伊者,則存放本成員英遭,反之,則在本成員和上一個(gè)成員之間填充一定的字節(jié)亦渗,以達(dá)到整數(shù)倍的要求挖诸,也就是將預(yù)開辟空間的首地址后移幾個(gè)字節(jié)。
我們可以總結(jié)內(nèi)存對(duì)齊為兩個(gè)原則:
原則 1. 前面的地址必須是后面的地址正數(shù)倍,不是就補(bǔ)齊法精。
原則 2. 整個(gè)Struct的地址必須是最大字節(jié)的整數(shù)倍多律。
通過(guò)上述內(nèi)存對(duì)齊的原則我們來(lái)看,person對(duì)象的第一個(gè)地址要存放isa指針需要8個(gè)字節(jié)亿虽,第二個(gè)地址要存放_(tái)age成員變量需要4個(gè)字節(jié)菱涤,根據(jù)原則一,8是4的整數(shù)倍洛勉,符合原則一粘秆,不需要補(bǔ)齊。然后檢查原則2收毫,目前person對(duì)象共占據(jù)12個(gè)字節(jié)的內(nèi)存攻走,不是最大字節(jié)數(shù)8個(gè)字節(jié)的整數(shù)倍殷勘,所以需要補(bǔ)齊4個(gè)字節(jié),因此person對(duì)象就占用16個(gè)字節(jié)空間昔搂。
而對(duì)于student對(duì)象玲销,我們知道sutdent對(duì)象中,包含person對(duì)象的結(jié)構(gòu)體實(shí)現(xiàn)摘符,和一個(gè)int類型的_no成員變量贤斜,同樣isa指針8個(gè)字節(jié),_age成員變量4個(gè)字節(jié)逛裤,_no成員變量4個(gè)字節(jié)瘩绒,剛好滿足原則1和原則2,所以student對(duì)象占據(jù)的內(nèi)存空間也是16個(gè)字節(jié)带族。
對(duì)象alloc的流程圖锁荔,以及調(diào)用方法
+ (id)alloc {
return _objc_rootAlloc(self);
}
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
id
class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
return _class_createInstanceFromZone(cls, extraBytes, zone);
}
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
可以使用斷點(diǎn),Debug Workflow -> viewMemory address中輸入isa的地址去驗(yàn)證上述所說(shuō)
或者 memory read + 對(duì)象地址 或者x +地址 或x/(數(shù)量+格式+字節(jié)數(shù)) +地址(格式x是16進(jìn)制 f是浮點(diǎn) d十進(jìn)制 字節(jié)大小b:byte 1字節(jié) h:half word 2字節(jié) w:word 4字節(jié) g:giant word 8字節(jié))例
momory write + 地址+數(shù)據(jù) (方便調(diào)試)
同時(shí)需要理解一句話蝙砌,isa地址是結(jié)構(gòu)體首地址阳堕,即結(jié)構(gòu)體地址,這個(gè)理解下择克,后面要用
關(guān)于內(nèi)存對(duì)其相關(guān)知識(shí)
補(bǔ)充編譯器在給結(jié)構(gòu)體開辟空間時(shí)恬总,首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類型,然后尋找內(nèi)存地址能是該基本數(shù)據(jù)類型的整倍的位置肚邢,作為結(jié)構(gòu)體的首地址越驻。將這個(gè)最寬的基本數(shù)據(jù)類型的大小作為對(duì)齊模數(shù)。
補(bǔ)充結(jié)構(gòu)體首地址的偏移是否是本成員的整數(shù)倍道偷,若是缀旁,則存放本成員,反之勺鸦,則在本成員和上一個(gè)成員之間填充一定的字節(jié)并巍,以達(dá)到整數(shù)倍的要求,也就是將預(yù)開辟空間的首地址后移幾個(gè)字節(jié)换途。
(換句話說(shuō)懊渡,結(jié)構(gòu)體大小必須是最大成員的倍數(shù))
iOS 是小端 從高地址開始讀取數(shù)據(jù)
接著來(lái)看下oc對(duì)象的信息如何存放的
注意一個(gè)寫法oc對(duì)象->一個(gè)屬性。 t1->_name(通過(guò)指針直接訪問(wèn)成員變量)
蘋果在解釋oc的時(shí)候給出過(guò)一個(gè)圖表
在這張圖表中instance對(duì)象(實(shí)例對(duì)象)class對(duì)象(類對(duì)象)meta-class對(duì)象(元類對(duì)象)
instance 是由alloc方法生成的 军拟,每次調(diào)用alloc都會(huì)產(chǎn)生新的instance對(duì)象剃执。在寫單利時(shí)候要清楚這件事
說(shuō)下alloc和init
alloc方法生成懸掛指針,他指向一個(gè)新的內(nèi)存地址懈息,并將其持有返回地址給指針,所以每次調(diào)用都會(huì)生成新的對(duì)象肾档,在沒(méi)有進(jìn)行init的時(shí)候不建議使用,因?yàn)闆](méi)有合理的內(nèi)存布局
同時(shí)我們需要思考調(diào)用alloc后內(nèi)存是直接映射到堆還是只分配給了虛擬內(nèi)存
而init是在為內(nèi)存地址進(jìn)行初始化,但是init不一定是在alloc產(chǎn)生的內(nèi)存上進(jìn)行的初始化怒见,有可能是在新的內(nèi)存上原因在[super init]
在block被當(dāng)作屬性使用時(shí)俗慈,特別是在多線程中一定要判斷屬性是否為空
iOS里的內(nèi)存是有分類的,它分成Clean Memory和Dirty Memory遣耍。顧名思義闺阱,Clean Memory是可以被操作系統(tǒng)回收的,Dirty Memory是不可被操作系統(tǒng)回收的舵变。
Clean Memory:在閃存中有備份酣溃,能再次讀取重建。如:Code(代碼段),framework揍诽,memory-mapped files
Dirty Memory:所有非Clean Memory,如:被分配了的堆空間,image cache
例如
對(duì)每行分析:
1.Dirty Memory徊哑。
因?yàn)閟tringWithString:是在堆上分配內(nèi)存的,如果我們不回收它的話闪檬,系統(tǒng)會(huì)一直占用這塊內(nèi)存惜索。
2.Clean Memory。
因?yàn)橛眠@樣的方法創(chuàng)建的是一個(gè)常量字符串黔姜,常量字符串是放在只讀數(shù)據(jù)段的拢切,如果這塊內(nèi)存被釋放了,而我們又訪問(wèn)它的時(shí)候秆吵,操作系統(tǒng)可以在只讀數(shù)據(jù)段中把值再讀取出來(lái)重建這塊內(nèi)存淮椰。(所以用這種方法創(chuàng)建的string是沒(méi)有引用計(jì)數(shù)的。)
3.Clean Memory纳寂。
這個(gè)時(shí)候buf指向的100M內(nèi)存區(qū)域是Clean Memory的主穗,因?yàn)?strong>操作系統(tǒng)是很懶的,只有當(dāng)我們要用到這塊區(qū)域的時(shí)候才會(huì)映射到物理內(nèi)存毙芜,沒(méi)有使用的時(shí)候只會(huì)分配一塊虛擬內(nèi)存給buf忽媒。
可以看到虛擬內(nèi)存和物理內(nèi)存沒(méi)有映射關(guān)系,所以是Clean Memory的腋粥。
4.Dirty & Clean Memory混合晦雨。
前3M是Dirty Memory,后97M是Clean Memory隘冲。這句for語(yǔ)句執(zhí)行完成后闹瞧,buf的前3M內(nèi)存被賦值,也就是buf的前3M被使用了展辞,所以這個(gè)時(shí)候的映射關(guān)系是這樣的:
alloc不只分配在虛擬內(nèi)存奥邮,同時(shí)會(huì)在物理內(nèi)存建立映射。
iOS7過(guò)后部分蘋果機(jī)就開始從32位操作系統(tǒng)轉(zhuǎn)到64位了
關(guān)于內(nèi)存可以看https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/MemoryAlloc.html
回歸主線
class對(duì)象 我們通過(guò)class方法或runtime方法得到一個(gè)class對(duì)象罗珍。class對(duì)象也就是類對(duì)象
Class Class1 = [object class];
// runtimeClass
objectClass = object_getClass(object);
相同的類的類對(duì)象在內(nèi)存中只有一個(gè),class對(duì)象在內(nèi)存中存儲(chǔ)的信息主要包括 isa指針 superclass指針 類的屬性信息(@property)漠烧,類的成員變量信息(ivar)類的對(duì)象方法信息(instance method)杏愤,類的協(xié)議信息(protocol)
成員變量的值時(shí)存儲(chǔ)在實(shí)例對(duì)象中的,因?yàn)橹挥挟?dāng)我們創(chuàng)建實(shí)例對(duì)象的時(shí)候才為成員變賦值已脓。但是成員變量叫什么名字珊楼,是什么類型,只需要有一份就可以了度液。所以存儲(chǔ)在class對(duì)象中厕宗。
元類對(duì)象 meta-class
meta-class對(duì)象和class對(duì)象的內(nèi)存結(jié)構(gòu)是一樣的,但是用途不一樣堕担,在內(nèi)存中存儲(chǔ)的信息主要包括isa指針 superclass指針 類的類方法的信息(class method)所以meta-class中也有類的屬性信息已慢,類的對(duì)象方法信息等成員變量,但是其中的值可能是空的霹购。
對(duì)象的isa指針指向哪里
當(dāng)對(duì)象調(diào)用實(shí)例方法的時(shí)候佑惠,我們上面講到,實(shí)例方法信息是存儲(chǔ)在class類對(duì)象中的齐疙,那么要想找到實(shí)例方法膜楷,就必須找到class類對(duì)象,那么此時(shí)isa的作用就來(lái)了贞奋。
[oc ocMethod];
instance的isa指向class赌厅,當(dāng)調(diào)用對(duì)象方法時(shí),通過(guò)instance的isa找到class轿塔,最后找到對(duì)象方法的實(shí)現(xiàn)進(jìn)行調(diào)用特愿。
當(dāng)類對(duì)象調(diào)用類方法的時(shí)候,同上勾缭,類方法是存儲(chǔ)在meta-class元類對(duì)象中的揍障。那么要找到類方法,就需要找到meta-class元類對(duì)象俩由,而class類對(duì)象的isa指針就指向元類對(duì)象
[NSObjc NSObjcClassMethod];
class的isa指向meta-class當(dāng)調(diào)用類方法時(shí)亚兄,通過(guò)class的isa找到meta-class,最后找到類方法的實(shí)現(xiàn)進(jìn)行調(diào)用
同理 當(dāng)t1的instance對(duì)象要調(diào)用父類t的對(duì)象方法時(shí)采驻,會(huì)先通過(guò)isa找到t1的class审胚,然后通過(guò)superclass找到t的class,最后找到對(duì)象方法的實(shí)現(xiàn)進(jìn)行調(diào)用礼旅,同樣如果t發(fā)現(xiàn)自己沒(méi)有響應(yīng)的對(duì)象方法膳叨,又會(huì)通過(guò)Person的superclass指針找到NSObject的class對(duì)象,去尋找響應(yīng)的方法
當(dāng)類對(duì)象調(diào)用父類的類方法時(shí)痘系,就需要先通過(guò)isa指針找到meta-class菲嘴,然后通過(guò)superclass去尋找響應(yīng)的方法
對(duì)isa、superclass總結(jié)
instance的isa指向class,class的isa指向meta-class,meta-class的isa指向基類的meta-class,基類的isa指向自己,class的superclass指向父類的class龄坪,如果沒(méi)有父類昭雌,superclass指針為nil,meta-class的superclass指向父類的meta-class,基類的meta-class的superclass指向基類的class,instance調(diào)用對(duì)象方法的軌跡健田,isa找到class烛卧,方法不存在,就通過(guò)superclass找父類,class調(diào)用類方法的軌跡:isa找meta-class妓局,方法不存在总放,就通過(guò)superclass找父類注意最后沒(méi)找到會(huì)找到類的實(shí)例方法中。