iOS程序的內(nèi)存布局
注:只要是static修飾的變量就相當(dāng)于是全局變量刊驴,整個(gè)項(xiàng)目就只有一份內(nèi)存地址
Tagged Point技術(shù)
從64bit開(kāi)始虏冻,iOS引入了Tagged Point技術(shù)婉陷,用于優(yōu)化NSNumber、NSDate霹陡、NSString等小對(duì)象的存儲(chǔ)皆尔。
在沒(méi)有使用Tagged Pointer之前,NSNumber等對(duì)象需要?jiǎng)討B(tài)分配內(nèi)存矫俺、維護(hù)引用計(jì)數(shù)等吱殉,NSNumer指針存儲(chǔ)的是堆中NSNumber對(duì)象的地址值。
當(dāng)使用了Tagged Point之后厘托,NSNumber指針里面存儲(chǔ)的數(shù)據(jù)變成了:Tag + Data友雳,直接把數(shù)據(jù)存儲(chǔ)在指針里面,比如NSNumber里面存儲(chǔ)10:number = 0xb000a1
最高位是標(biāo)志位铅匹,如果為1表示為T(mén)agged Point指針押赊,如果是MAC平臺(tái)則最低有效位為1
當(dāng)指針不夠存儲(chǔ)數(shù)據(jù)的時(shí)候,才使用動(dòng)態(tài)分配內(nèi)存的方式來(lái)存儲(chǔ)數(shù)據(jù)
objc_msgSend能識(shí)別Tagged Point包斑,比如調(diào)用NSNumber的intValue方法流礁,直接從指針提取數(shù)據(jù),因?yàn)門(mén)agged Point并不是一個(gè)OC對(duì)象舰始,沒(méi)有類對(duì)象崇棠,沒(méi)有isa。所以如果按之前的方法調(diào)用流程的話會(huì)報(bào)找不到方法錯(cuò)誤丸卷。
面試題:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
// 加鎖
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 解鎖
});
}
以上代碼運(yùn)行會(huì)出現(xiàn)閃退枕稀。因?yàn)榻oself.name賦值相當(dāng)于調(diào)用setName方法。
在MRC中setName方法:
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
有可能會(huì)出現(xiàn)多個(gè)子線程同時(shí)訪問(wèn)[_name release];,從而導(dǎo)致報(bào)錯(cuò)萎坷。解決辦法可以將name設(shè)置為atmoic或者在name賦值的前后加鎖凹联。保證只有一個(gè)線程在賦值
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
以上代碼并不會(huì)閃退。因?yàn)閍bc是小數(shù)據(jù)哆档,根據(jù)Tagged Point技術(shù)蔽挠,數(shù)據(jù)是直接存儲(chǔ)在name的地址里面的,并不會(huì)動(dòng)態(tài)生成對(duì)象存儲(chǔ)瓜浸,也就不會(huì)調(diào)用set方法了澳淑。
引用計(jì)數(shù)
在iOS中,使用引用計(jì)數(shù)來(lái)管理OC對(duì)象的內(nèi)存
一個(gè)新建的OC對(duì)象引用計(jì)數(shù)默認(rèn)是1插佛,當(dāng)引用計(jì)數(shù)減為0杠巡,OC對(duì)象就會(huì)銷毀。
當(dāng)調(diào)用alloc雇寇、new氢拥、copy、mutableCopy方法返回一個(gè)對(duì)象锨侯,在不需要使用時(shí)需要進(jìn)行release或者autorelease嫩海。
ARC是LLVM編譯的時(shí)候幫我們處理內(nèi)存管理方面的代碼,然后在RunTime運(yùn)行時(shí)幫我們處理弱引用相關(guān)的操作
引用計(jì)數(shù)的存儲(chǔ)
在64bit中囚痴,引用計(jì)數(shù)可以直接存儲(chǔ)在有劃過(guò)的isa指針中叁怪,也可能存儲(chǔ)在SideTable類中:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;//引用計(jì)數(shù)存儲(chǔ),散列表渡讼,當(dāng)前最新的指針為key骂束,value為引用計(jì)數(shù)
weak_table_t weak_table;//weak引用存儲(chǔ)耳璧,散列表
}
在isa指針中有一個(gè)字段19位存儲(chǔ)對(duì)象的引用計(jì)數(shù)成箫,如果不夠存儲(chǔ)則會(huì)存儲(chǔ)在SideTable表中的refcnts;
weak指針的原理
將weak修飾的對(duì)象存放到SideTable里面的weak_table里面
當(dāng)對(duì)象釋放自動(dòng)調(diào)用dealloc函數(shù)時(shí)會(huì)調(diào)用clearDeallocating()將指向當(dāng)前對(duì)象的弱指針置為nil
自動(dòng)釋放池
autoreleasePool的底層代碼如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();//構(gòu)造函數(shù)旨枯,在創(chuàng)建結(jié)構(gòu)體的時(shí)候調(diào)用
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);//析構(gòu)函數(shù)蹬昌,在結(jié)構(gòu)體銷毀的時(shí)候調(diào)用
}
void * atautoreleasepoolobj;
};
接下來(lái)我們?cè)谑褂玫臅r(shí)候如下代碼:
- (void)test{
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
}
}
以上代碼本質(zhì)是
- (void)test{
atautoreleasepoolobj = objc_autoreleasePoolPush();
MJPerson *person = [[[MJPerson alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
即在autorelease前面調(diào)用了objc_autoreleasePoolPush方法,在銷毀的時(shí)候調(diào)用了objc_autoreleasePoolPush方法攀隔。在這兩個(gè)方法中都是通過(guò)AutoreleasePoolPage對(duì)象來(lái)管理的皂贩。
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
每個(gè)AutoreleasePoolPage對(duì)象占用4096字節(jié),除了用來(lái)存放它內(nèi)部的成員變量昆汹,剩下的空間用來(lái)存放autorelease對(duì)象的內(nèi)存地址明刷。當(dāng)我們一個(gè)對(duì)象調(diào)用了autorelease后會(huì)將這個(gè)對(duì)象的地址存放到AutoreleasePoolPage里面。
所有的AutoreleasePoolPage對(duì)象是通過(guò)雙向鏈表的形式連接在一起满粗。
autoreleasePool調(diào)用push方法:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
當(dāng)我們調(diào)用push方法會(huì)將一個(gè)POOL_BOUNDARY入棧辈末,并且返回其內(nèi)存地址,然后再把對(duì)象的內(nèi)存地址一次入棧。當(dāng)調(diào)用pop方法的時(shí)候會(huì)傳入一個(gè)POOL_BOUNDARY的內(nèi)存地址挤聘,會(huì)從最后一個(gè)入棧的對(duì)象開(kāi)始發(fā)送release消息轰枝,直到遇到這個(gè)POOL_BOUNDARY。比如當(dāng)我們有多個(gè)@autoreleasepool 的時(shí)候:
int main(int argc, const char * argv[]) {
@autoreleasepool {//r1 = push()
MJPerson *p1 = [[MJPerson alloc] init];
MJPerson *p2 = [[MJPerson alloc] init];
@autoreleasepool {//r2 = push()
MJPerson *p3 = [[MJPerson alloc] init];
@autoreleasepool {//r3 = push()
MJPerson *p4 = [[MJPerson alloc] init];
}//pop(r3)
}//pop(r2)
}//pop(r1)
return 0;
}
POOL_BOUNDARY相當(dāng)于一個(gè)標(biāo)志位组去,將多個(gè)autoreleasepool隔開(kāi)r1鞍陨、r2、r3分別是對(duì)應(yīng)每個(gè)autoreleasepool的POOL_BOUNDARY的內(nèi)存地址从隆,釋放的時(shí)候我們調(diào)用pop把這個(gè)POOL_BOUNDARY的內(nèi)存地址傳進(jìn)去诚撵,然后一次釋放直到遇到POOL_BOUNDARY的內(nèi)存地址。
autorelease在什么時(shí)候釋放键闺?
如果是直接被autoreleasePool包住的話砾脑,那么釋放的時(shí)機(jī)就是在autoreleasePool執(zhí)行完之后就釋放了。比如:object將在執(zhí)行完@autoreleasepool的時(shí)候釋放
- (void)test{
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
}
}
如果不是在autoreleasePool里面的話艾杏,那是由runloop控制的韧衣,iOS在主線程的RunLoop中注冊(cè)了2個(gè)Observer,第一個(gè)Observer監(jiān)聽(tīng)了kCFRunLoopEntry事件购桑,會(huì)調(diào)用objc_autoreleasePoolPush()畅铭。第二個(gè)Observer監(jiān)聽(tīng)了kCFRunLoopBeforeWaiting事件,會(huì)調(diào)用objc_autoreleasePoolPop()勃蜘、objc_autoreleasePoolPush()硕噩,還監(jiān)聽(tīng)了kCFRunLoopBeforeExit事件,會(huì)調(diào)用objc_autoreleasePoolPop()缭贡。
注意:在ARC中編譯時(shí)插入的代碼是release炉擅,而不是autorelease,所以在執(zhí)行完當(dāng)前函數(shù)的時(shí)候局部變量就會(huì)立即釋放阳惹。