iOS 內(nèi)存管理

在開發(fā)中,內(nèi)存管理是一個必要的技能,研究iOS 開發(fā),我們通過內(nèi)存布局、內(nèi)存管理方案、數(shù)據(jù)結(jié)構(gòu)、ARC/MRC、引用計數(shù)、弱引用、自動釋放池、循環(huán)引用這個八個方面去了解iOS 的內(nèi)存管理。

內(nèi)存布局

內(nèi)存布局

Stack:方法調(diào)用
Heap:通過alloc分配的對象
Bss:未初始化的靜態(tài)變量和全局變量
Data:已初始化的全局變量
Test:程序代碼

內(nèi)存管理方案

isa指針

OC對象里提到最多的就是isa指針了,大家都知道isa是指向該對象的內(nèi)存地址,其實(shí)這個地址里有一些特殊的含義。
在arm64架構(gòu)之前,isa就是一個普通的指針,存儲著Class、Meta-Class對象的內(nèi)存地址
從arm64架構(gòu)開始,對isa進(jìn)行了優(yōu)化,變成了一個共用體(union)結(jié)構(gòu),還使用位域來存儲更多的信息


  • 1位:nonpointer,如果是0就代表著是一個純的isa指針,直接代表了當(dāng)前類對象的直接地址,如果是1代表著不僅存儲著地址,而且還包含著一些內(nèi)存管理方面的數(shù)據(jù),就是非指針類型的isa.
  • 2位:has_assoc,0和1代表著是否含有關(guān)聯(lián)對象。如果沒有,釋放時會更快。
  • 3位:has_cxx_dtor,是否有C++的析構(gòu)函數(shù)(.cxx_destruct),通過這個也能代表是通過ARC來進(jìn)行內(nèi)存管理的。如果沒有,釋放時會更快。
  • 接下來33位:shiftcls,代表了當(dāng)前類對象的地址,需要把對應(yīng)位置的值拿出來,再去計算實(shí)際對應(yīng)的指針地址。存儲著Class、Meta-Class對象的內(nèi)存地址信息
  • 后6位:magic用于在調(diào)試時分辨對象是否未完成初始化
  • 1位:weakly_refrenced,標(biāo)識了該對象是否有弱引用指針。如果沒有,釋放時會更快。
  • 1位:deallocting,標(biāo)識了當(dāng)前對象是否在進(jìn)行dealloc操作
  • 1位:has_sidetable_rc,標(biāo)識了如果當(dāng)前對象如果已經(jīng)超過了引用計數(shù)上限,需要外掛一個sidetable去存儲相關(guān)的引用計數(shù)內(nèi)容,就是一個散列表
  • 后續(xù)位:extra_rc,代表了額外的引用計數(shù),當(dāng)我們的引用計數(shù)在一個很小的范圍內(nèi)就會存在isa指針當(dāng)中,而不是存在另一個表里。
    關(guān)于內(nèi)存管理不僅僅有散列表,其實(shí)還有extra_rc來存儲相關(guān)的引用計數(shù)值。
散列表內(nèi)存管理方案

iOS中是主要通過散列表去管理內(nèi)存。源碼當(dāng)中利用SideTables()結(jié)構(gòu)來實(shí)現(xiàn),sidetables實(shí)際上是一個哈希表,我們可以通過引用對象指針,來找到對應(yīng)的sidetable.
SideTables結(jié)構(gòu)

這里設(shè)計成多個sideTable的思想是,如果有一個table的話,我們在程序內(nèi)申請的所有對象的引用計數(shù)或者弱引用存儲就會放在一個大表中,這個時候如果我們要操作某個對象的引用值去進(jìn)行修改,包括release,retain,而且對象都是在不同的線程中,這個時候?qū)Ρ聿僮鞯臅r候需要進(jìn)行加鎖去處理,會十分影響效率。系統(tǒng)為了解決這個問題,引用了分離鎖的計數(shù)方案。
分離鎖:我們可以把內(nèi)存對象對應(yīng)的引用計數(shù)表,拆成多個部分,這樣對多個對象操作時候,就可以分散對多個表分別加鎖。比如某個對象在A表里,另個對象在B表里,那如果兩個對象要同時進(jìn)行引用計數(shù)操作的時候,就可以并發(fā)操作,如果在一張表里就需要挨著順序去操作表。

實(shí)現(xiàn)快速分流

那怎么去通過isa指針去快速的定位到是哪個sidetable表中,這里就提到了快速分流的方案Hash查找。
sidetables的本質(zhì)是一張hash表,一共有64張,來存儲引用計數(shù)。
hash的具體操作,就是以對象指針作為key,通過hash函數(shù),獲得對應(yīng)的sidetable作為value。我們回想一下OC中字典的使用方法,就基本對哈希有一個大概的認(rèn)識。

數(shù)據(jù)結(jié)構(gòu)

spinlock_t自旋鎖

是忙等的鎖,忙等指的是如果當(dāng)前線程已被其他鎖獲取,那么當(dāng)前線程就會不斷的探索鎖是否被釋放,如果釋放掉了,會去第一時間獲取這個鎖。自旋鎖適合輕量訪問。

refcountTable引用計數(shù)表

引用計數(shù)表實(shí)際上就是哈希算法的使用,通過哈希查找去定位引用計數(shù)的值,這個值就是一個unsigned long 類型值,共64位

  • 1位 weakly_referenced,表示是否有弱引用。
  • 2位 deallocing 表示是否正在釋放
  • 其他位數(shù) 代表了引用計數(shù)值,實(shí)際上使用的時候需要向右平移兩位,才能算出正確的值
weak_table_t弱引用表

弱引用表也是一個哈希表,通過key去查找value,這里的value是一個結(jié)構(gòu)體數(shù)組


弱引用表

ARC MRC

這里只提一嘴,ARC實(shí)際上是有編譯器llvm和runtime共同作用才可以

引用計數(shù)管理

  • alloc實(shí)現(xiàn)
    經(jīng)過一系列調(diào)用,最終調(diào)用了c函數(shù)calloc
    此時并沒有設(shè)置引用計數(shù)為1
  • retain實(shí)現(xiàn),實(shí)際上是兩次哈希查找
SideTable& table = SideTables()[this] // 第一次哈希查找是從所有表中定位出當(dāng)前對象引用計數(shù)所在的那張表
size_t& refcntStorage = table.refcnts[this]// 第二次哈希是從表中找出引用計數(shù)的值
refcntStorage += SIDE_TABLE_RC_ONE // 這個函數(shù)是增加引用計數(shù),SIDE_TABLE_RC_ONE這個常量通過前邊提到的引用計數(shù)值的講解可以知道這個值是4,因?yàn)?4位中后兩位并不是引用計數(shù)的含義
  • release實(shí)現(xiàn),方式同retain
SideTable& table = SideTables()[this] 
RefcountMap::iterator it = table.fefcnts.find(this)
refcntStorage += SIDE_TABLE_RC_ONE
  • retainCount實(shí)現(xiàn),從代碼中可以看出alloc的情況下,雖然引用計數(shù)沒有設(shè)置為1,但是retainCount的結(jié)果依然唯一,就是因?yàn)橛衦efcnt_result的存在
SideTable& table = SideTables()[this] ;
size_t refcnt_result = 1;
RefcountMap::iterator it = table.fefcnts.find(this)
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT
  • dealloc實(shí)現(xiàn)


    dealloc實(shí)現(xiàn)
object_dispose實(shí)現(xiàn)
objc_destructInstance實(shí)現(xiàn)
clearDeallpcating實(shí)現(xiàn)

弱引用管理

id __weak obj1 = obj; //編譯前
id obj1; obj_initWeak(&obj1, obj)//編譯后
  • 添加weak變量調(diào)用棧:
    obj_initWeak()->storeWeak()->weak_register_no_lock()
    通過NSObject的源碼可以看到是按上邊的三個函數(shù)的調(diào)用順序來處理,最終是由weak_register_no_lock函數(shù)進(jìn)行弱引用變量的添加,具體添加的位置是通過hash算法來查找,如果所查找的對象已經(jīng)查找到了所對應(yīng)的弱引用數(shù)組,那么就進(jìn)行數(shù)組的添加,如果沒有查找到就新建一個數(shù)組,添加到第0個位置,后邊的位置初始化為Nil。
  • 清除weak變量,同時設(shè)置指向?yàn)閚il
    dealloc()->...->weak_clear_no_lock()
    通過源碼可以看出,當(dāng)一個對象在dealloc之后,會調(diào)用weak_clear_no_lock方法,首先對當(dāng)前對象進(jìn)行一個哈希查找,如果沒有在弱引用表中找到這個弱引用數(shù)組,那么直接返回,如果找到了,就遍歷這個數(shù)組,挨個設(shè)置為Nil。

自動釋放池AutoreleasePool

首先編譯器會將@autoreleasepool{}改寫為:

void * ctx = obj_autoreleasePoolPush(); 這個函數(shù)會調(diào)用void *AtuorelasePoolPage::push(void)
中間是{}中的代碼
之后objc_autoreleasePoolPop(ctx); 這個函數(shù)會調(diào)用 AutorelasePoolPage::pop(void *ctxt)

一次pop實(shí)際上相當(dāng)于一次批量的pop操作,就是說添加到autoreleasepool{}內(nèi)的變量,會在pop時候一次都釋放掉。

自動釋放池的數(shù)據(jù)結(jié)構(gòu)
  • 是以棧為節(jié)點(diǎn)通過雙向鏈表的形式組合而成
    AotoreleasePoolPage中包含的結(jié)構(gòu)信息
    id *next; //指向棧當(dāng)中下一個可填充的位置。
    AotoreleasePoolPage *const parent;//雙向鏈表中的父指針
    AotoreleasePoolPage *child;//孩子指針
    pthread_t const thread;//線程
  • AutoreleasePoolPage::push



  • AutoorelleaasePoolPage::pop
    根據(jù)傳入的哨兵對象找到對應(yīng)位置
    給上次push操作之后添加的對象依次發(fā)送release消息
    回退next指針到正確位置
  • 是和線程一一對應(yīng)的
    在當(dāng)次runloop將要結(jié)束的時候調(diào)用AutoreleasePoolPage::pop()
    AutoreleasePool可以多層嵌套的原因就是多層嵌套就是插入哨兵對象
demo
函數(shù)中加入一個autoreloasePool時候,會在函數(shù)的開始和結(jié)束分別加入push和pop方法,這個是在編譯期就決定的
int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        for (int i = 0; i < 1000; i++) {
            MJPerson *person = [[[MJPerson alloc] init] autorelease];
        } // 8000個字節(jié)
        
//        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}
 // 下邊是將.m類編譯成c++文件后的源碼,編譯器會生成__AtAutoreleasePool的c++代碼,
 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 構(gòu)造函數(shù),在創(chuàng)建結(jié)構(gòu)體的時候調(diào)用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析構(gòu)函數(shù),在結(jié)構(gòu)體銷毀的時候調(diào)用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };
 
// main的核心代碼
 {
    __AtAutoreleasePool __autoreleasepool;
    MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
 }
 
 
    atautoreleasepoolobj = objc_autoreleasePoolPush();
 
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
 
    objc_autoreleasePoolPop(atautoreleasepoolobj);

循環(huán)引用

三種循環(huán)引用
  • 自循環(huán)引用
    id __strong obj = self;
  • 相互循環(huán)引用
    在B類中:id __strong obj = A;
    在A類中:id __strong obj = B;
  • 多循環(huán)引用
    A引用B,B引用C,C引用D,D引用A
如何破除循環(huán)引用
  • 避免循環(huán)引用
    __weak
    __block:MRC下,__block修飾對象不回增加引用計數(shù),避免了循環(huán)引用。在ARC下,__block修飾對象會被強(qiáng)引用,無法避免循環(huán)引用,需要手動解環(huán)。
    __unsafe_unretained:修飾對象不回增加引用計數(shù),避免了循環(huán)引用,如果被修飾對象在某一時機(jī)被釋放,會產(chǎn)生懸垂指針。
  • 在合適的時機(jī)手動斷環(huán)
NSTimer的循環(huán)引用問題

增加一個中間對象,持有NSTimer和原對象的弱引用。在中間對象里持有的target進(jìn)行判斷,如果值存在就說明沒被釋放,就把NSTimer的值回調(diào)給target.

@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;

- (void)fire:(NSTimer *)timer;
@end

@implementation TimerWeakObject
- (void)fire:(NSTimer *)timer
{
    if (self.target) {
        if ([self.target respondsToSelector:self.selector]) {
            [self.target performSelector:self.selector withObject:timer.userInfo];
        }
    }
    else{
        [self.timer invalidate];
    }
}

@end

@implementation NSTimer (WeakTimer)

+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)repeats
{
    TimerWeakObject *object = [[TimerWeakObject alloc] init];
    object.target = aTarget;
    object.selector = aSelector;
    object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
    
    return object.timer;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、內(nèi)存布局 stack:方法調(diào)用 heap:通過alloc等分配對象 bss:未初始化的全局變量等。 data:...
    AKyS佐毅閱讀 1,712評論 0 19
  • iOS中內(nèi)存管理機(jī)制是開發(fā)中一項很重要的知識,了解iOS中內(nèi)存管理的規(guī)則不管是在開發(fā)中還是在學(xué)習(xí)中都能很大程度的幫...
    Horson19閱讀 1,254評論 0 4
  • iOS中內(nèi)存管理機(jī)制是開發(fā)中一項很重要的知識,了解iOS中內(nèi)存管理的規(guī)則不管是在開發(fā)中還是在學(xué)習(xí)中都能很大程度的幫...
    Horson19閱讀 2,001評論 0 7
  • iOS中內(nèi)存管理機(jī)制是開發(fā)中一項很重要的知識,了解iOS中內(nèi)存管理的規(guī)則不管是在開發(fā)中還是在學(xué)習(xí)中都能很大程度的幫...
    Mr_Atom閱讀 3,483評論 1 4
  • 文章目錄 一.內(nèi)存管理準(zhǔn)則 二.屬性內(nèi)存管理修飾符全解析 三.block中的weak和strong 四.weak是...
    YouKnowZrx閱讀 1,117評論 5 10

友情鏈接更多精彩內(nèi)容