MJiOS底層筆記--記憶體管理

kirito_song發表於2019-02-23

MJiOS底層筆記--記憶體管理
本文屬筆記性質,主要針對自己理解不太透徹的地方進行記錄。

推薦系統直接學習小碼哥iOS底層原理班---MJ老師的課確實不錯,強推一波。


別的


CADisplayLink與NSTimer

CADisplayLink(保證呼叫頻率和螢幕的刷幀頻率一致,60FPS(60次/s))、NSTimer會對target產生強引用,如果target又對它們產生強引用,那麼就會引發迴圈引用

target導致迴圈引用

如下程式碼是釋放不掉的

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 保證呼叫頻率和螢幕的刷幀頻率一致,60FPS
    self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}


- (void)linkTest
{
    NSLog(@"%s", __func__);
}
複製程式碼

__weak為什麼解決target的強引用

block是捕獲變數,timer是傳遞引數。

  1. block在捕獲變數時根據變數型別自行進行若引用處理。
  2. timer作為引數傳遞時,內部接收到的都是物件的地址值,無法獲取引用型別。

不過如果是NSTimer的block版本用__weak是可以的

中間代理

MJiOS底層筆記--記憶體管理

  1. 用代理隔離self與timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
複製程式碼
  1. 用訊息轉發將selector傳送回self
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
複製程式碼

其他方式釋放timer

比如removeSuperView時之類吧

NSObject與NSProxy

NSProxy專門用來做訊息轉發

  1. 訊息轉發速度快

NSProxy在本類沒有該方法的情況下會直接進入訊息轉發(methodSignatureForSelector:) 與 (forwardInvocation:),並不會去查詢父類,動態方法解析等等。

  1. 大部分方法都能正確轉發

原生方法如果未主動實現,內部直接進入訊息轉發。比如class,isKindOfClass等等

GCD定時器

GCD的定時器會更加準時

NSTimer依賴於RunLoop,如果RunLoop的任務過於繁重,可能會導致NSTimer不準時

而GCD定時器依賴於系統核心,並不依賴Runloop


記憶體佈局

MJiOS底層筆記--記憶體管理


Tagged Pointer

從64bit開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小物件的儲存

  1. 在沒有使用Tagged Pointer之前,NSNumber與正常的OC物件一樣:

    需要動態分配記憶體、維護引用計數等,NSNumber指標儲存的是堆中NSNumber物件的地址值。

  2. 使用Tagged Pointer之後,NSNumber指標裡面儲存的資料變成了:Tag + Data,也就是將資料直接儲存在了指標中

MJiOS底層筆記--記憶體管理

  1. 當指標不夠儲存資料時,才會使用動態分配記憶體的方式來儲存資料

  2. objc_msgSend能識別Tagged

  3. Pointer,比如NSNumber的intValue方法,直接從指標提取資料,節省了以前的呼叫開銷

  4. 如何判斷一個指標是否為Tagged Pointer? class

    iOS平臺,最高有效位是1(第64bit)

    Mac平臺,最低有效位是1(16進位制下為7)

    通常來講,判斷最後一位不是0即可

NSLog(@"Person例項的記憶體地址=%p---指標變數p的記憶體地址=%p---指標變數p儲存的記憶體地址=%p", p, &p, p);
複製程式碼

MRC的記憶體管理

  1. 在iOS中,使用引用計數來管理OC物件的記憶體
  2. 一個新建立的OC物件引用計數預設是1,當引用計數減為0,OC物件就會銷燬,釋放其佔用的記憶體空間
  3. 呼叫retain會讓OC物件的引用計數+1,呼叫release會讓OC物件的引用計數-1

記憶體管理的經驗總結

  1. 當呼叫alloc、new、copy、mutableCopy方法返回了一個物件,在不需要這個物件時,要呼叫release或者autorelease來釋放它
  2. 想擁有某個物件,就讓它的引用計數+1;不想再擁有某個物件,就讓它的引用計數-1

MRC

使用return關鍵字只會管理setget方法中的記憶體,dealloc中仍然需要自己釋放。


copy和mutableCopy

MJiOS底層筆記--記憶體管理


引用計數

  1. 在64bit中,引用計數可以直接儲存在優化過的isa指標中
  2. 如果引用計數過大,isa中改為1並且將計數儲存到SideTable中

SideTable

一個全域性table

MJiOS底層筆記--記憶體管理

refcnts是一個存放著物件引用計數的雜湊表 weak_table存放著若引用的指標與物件


weak

當一個物件A被若引用指標持有,將會以[&A,weak指標表]的形式新增進SideTable中

當物件A被釋放,可以根據&A查詢到所有指向他的weak指標並進行釋放

- (void)dealloc {
    _objc_rootDealloc(self);
}


_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  //新isa指標
                 !isa.weakly_referenced  &&   //檢視該物件是否被若引用了
                 !isa.has_assoc  &&   //關聯物件
                 !isa.has_cxx_dtor  &&  //c++析構器
                 !isa.has_sidetable_rc)) //大額引用計數
    {
        assert(!sidetable_present());
        free(this); //直接釋放
    } 
    else {
        object_dispose((id)this); 
    }
}


void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects(); 

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating(); //將指向當前物件的弱指標置位nil
    }

    return obj;
}


objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this]; //獲得全域性的SideTable
    table.lock();
    if (isa.weakly_referenced) {
        //從表中根據物件地址,釋放所有指向他的弱引用指標
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

複製程式碼

AutoreleasePool

@autoreleasepool {
    for (int i = 0; i < 1000; i++) {
        MJPerson *person = [[[MJPerson alloc] init] autorelease];
    }
}
複製程式碼

cpp中

{ 
    __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"));
}


 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 建構函式,在建立結構體的時候呼叫
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 解構函式,在結構體銷燬的時候呼叫
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };
複製程式碼

所以本質上就等於

atautoreleasepoolobj = objc_autoreleasePoolPush(); //建立釋放池

MJPerson *person = [[[MJPerson alloc] init] autorelease];

objc_autoreleasePoolPop(atautoreleasepoolobj); //釋放釋放池
複製程式碼

AutoreleasePool的結構

每個AutoreleasePoolPage物件佔用4096位元組記憶體,除了用來存放它內部的成員變數,剩下的空間用來存放autorelease物件的地址

MJiOS底層筆記--記憶體管理

push,pop,autorelease

  1. 在呼叫objc_autoreleasePoolPush()時,插入POLL_BOUNDARY並返回地址0x1038

  2. 每個呼叫autorelease的物件都會被插入到AutoreleasePoolPage中

  3. 在呼叫objc_autoreleasePoolPop(0x1038)時,從當前位置到0x1038所有的物件都會被執行release操作。

可以通過以下私有函式來檢視自動釋放池的情況

extern void _objc_autoreleasePoolPrint(void);
複製程式碼

AutoreleasePool的維護

  1. 始終有一個被標記hotPage的活躍AutoreleasePoolPage被系統持有
  2. page之間通過雙向連結串列連結
  3. 如果push/autorelease操作時當前page已滿,將會建立一個page或跳轉到下一個page

runloop與AutoreleasePool

iOS在主執行緒的Runloop中註冊了2個Observer,監聽了三個狀態。並適時操作AutoreleasePool

  1. 第1個Observer監聽了kCFRunLoopEntry事件

    在進入runloop時,會呼叫objc_autoreleasePoolPush()

  2. 第2個Observer監聽了kCFRunLoopBeforeExit事件

    在退出runloop時,會呼叫objc_autoreleasePoolPop()

  3. 第2個Observer還監聽了kCFRunLoopBeforeWaiting事件

    在當前迴圈結束,準備休眠時時,會呼叫objc_autoreleasePoolPop()隨後再呼叫一次objc_autoreleasePoolPush()


autorelease物件

借用群裡一位大佬的解釋

一般除了init其他基本上都是autorelease的,包括C函式返回物件

也就是說init方法放回的物件,預設是會被retain/release,而其他的物件預設會autorelease

很顯然的,二者的釋放時機不同,所以才會有如下情況發生。

MJiOS底層筆記--記憶體管理

相關文章