日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

swift語言AutoreleasePool原理及使用場景_Swift

作者:劉小壯 ? 更新時間: 2022-11-05 編程語言

使用場景

ARC下,AutoreleasePool主要應用在大量創建臨時對象的場景,通過AutoreleasePool控制內存峰值,是一個很好的選擇。

NSAutoreleasePool

MRC可以調用NSAutoreleasePool使對象延遲釋放,在ARC下這個API已經被禁用。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// ...
[pool release];

@autoreleasepool

除了NSAutoreleasePool還可以使用@autoreleasepool,并且蘋果推薦使用@autoreleasepool,因為這個API性能更好,在ARC下依然可以使用@autoreleasepool

無論是MRC還是ARCautorelease最大的作用,是在大量創建對象的同時,通過修飾讓內存得到提前釋放,從而降低內存峰值。

@autoreleasepool {
    NSMutableArray *channelItemsJSONArray = [NSMutableArray arrayWithContentsOfFile:[self cachedChannelItemsFile]];
    NSArray *items = [self channelItemsJSONArray];
    if (![items writeToFile:[self cachedChannelItemsFile] atomically:YES]) {
        [channelItemsJSONArray writeToFile:[self cachedChannelItemsFile] atomically:YES];
    }
    items = nil;
}

__autoreleasing

ARC下,需要被自動釋放的對象,可以用__autoreleasing修飾,讓對象延遲釋放。

+ (NSArray *)parseString:(NSString *)originalM3U8Str m3u8Host:(NSString *)m3u8url error:(NSError *__autoreleasing *)errorPtr;

源碼分析

__AtAutoreleasePool結構體

struct __AtAutoreleasePool {
    __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    ~__AtAutoreleasePool() {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    void * atautoreleasepoolobj;
};

@autoreleasepool本質上會被系統轉換成C++__AtAutoreleasePool結構體,@autoreleasepool的大括號開始,對應著objc_autoreleasePoolPush函數。大括號結束,對應著objc_autoreleasePoolPop函數。通過clang命令將OC代碼轉成C++代碼,可以看到有一個__AtAutoreleasePool結構體。

__AtAutoreleasePool結構體在創建的時候會執行objc_autoreleasePoolPush函數,在釋放的時候會執行析構函數,并執行objc_autoreleasePoolPop函數。在這兩個函數內部,會調用AutoreleasePoolPagepushpop函數。

AutoreleasePoolPage

在運行時代碼中,objc_autoreleasePoolPopobjc_autoreleasePoolPush,都調用了AutoreleasePoolPage類的實現。

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage的定義中,可以看到有parentchild的定義,當page中對象太多存儲不下時,會創建其他的page對象來存儲,AutoreleasePoolPage的結構是一個雙向鏈表。在插入新的autorelease對象時,也會從鏈表頭向后查找,直到找到未滿的page。

class AutoreleasePoolPage 
{
    magic_t const magic;                // 校驗page的結構是否完整
    id *next;                           // 指向下一個可以存放autorelease對象的地址
    pthread_t const thread;             // 當前所在的線程
    AutoreleasePoolPage * const parent; // 當前page的父節點
    AutoreleasePoolPage *child;         // 當前page的子節點
    uint32_t const depth;               // page的深度
    uint32_t hiwat;
}

AutoreleasePoolPage是一個C++的類,每個page4096個字節,也就是16進制的0x1000,也就是4kb的空間。這些空間中,其自身的成員變量只占56個字節,也就是下面七個成員變量,每個占8字節,總共56個字節。其他的四千多個字節,都用來存放被autorelease修飾的對象內存地址。

POOL_BOUNDARY

POOL_BOUNDARY的作用是,區分不同的自動釋放池,也就是不同的@autoreleasepool。調用push時,會傳入POOL_BOUNDARY并返回一個地址例如0x1038,0x1038是不存儲@autorelease對象的地址的,起到一個標識作用,用來分割不同的@autoreleasepool。

調用pop時,會傳入end的地址,并從后到前調用對象的release方法,直到POOL_BOUNDARY為止。如果存在多個page,會從childpage的最末尾開始調用,直到POOL_BOUNDARY。page的結構是一個棧結構,釋放的時候也是從棧頂開始釋放。

next指針指向棧頂,是棧里面很常見的一個設計。AutoreleasePoolPagePOOL_BOUNDARY的區別在于,AutoreleasePoolPage負責維護存儲區域,而POOL_BOUNDARY則負責分割存儲在page中的對象地址,以@autoreleasepool為單位進行分割。

多層嵌套

@autoreleasepool {
    NSObject *p1 = [[NSObject alloc] init];
    NSObject *p2 = [[NSObject alloc] init];
    @autoreleasepool {
        NSObject *p3 = [[NSObject alloc] init];
        @autoreleasepool {
            NSObject *p4 = [[NSObject alloc] init];
        }
    }
}

如果是多層@autoreleasepool的嵌套,會用同一個AutoreleasePoolPage對象。以下面的三個嵌套為例,在同一個page中的順序是下圖這樣。不同的@autoreleasepoolPOOL_BOUNDARY做分割。

push

創建一個autoreleasePool之后,就會調用push函數。在push函數中會判斷是否調試模式下,如果調試模式會每次生成一個新的pagedebug環境代碼可以直接忽略,只保留autoreleaseFast函數。

static inline void *push() 
{
    id *dest;
    if (DebugPoolAllocation) {
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    return dest;
}

autoreleaseFast

在函數內部,會通過hotPage獲取當前的pagehotPage函數內部本質上是一個pagekey的映射。

  • 如果page不為空并且有空間,則調用pageadd函數將對象添加到page中,并將POOL_BOUNDARY添加在當前的位置。
  • 如果page已經被創建但沒有空間,會調用autoreleaseFullPage函數創建新的page,并且將鏈表的末尾指向新創建的page
  • 如果沒有創建page,則調用autoreleaseNoPage函數創建一個新的page,并且將當前線程的hotPage設置為新創建的page。
static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

autoreleaseFullPage

  • autoreleaseFullPage函數中,會從page的鏈表中,從前往后找到末尾的節點。
  • 創建一個新的page,在創建函數AutoreleasePoolPage中會處理parentchild指針的問題,返回的page可以直接用。
  • 調用setHotPagepage設置到哈希表中,并且調用pageadd函數將autorelease修飾的對象,添加到page中。
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    setHotPage(page);
    return page->add(obj);
}

autoreleaseNoPage

autoreleaseNoPage函數的核心代碼比較簡單,就是創建一個新的page,隨后設置POOL_BOUNDARY標志,并且把對象添加進去。在函數中需要留意POOL_BOUNDARY標志,很多地方都用來做page是否為空的判斷。

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    return page->add(obj);
}

add

add函數比較簡單,核心邏輯就是將obj放入next指針的位置,并且對next指針進行++,指向下一個位置。*next++表示先用后加,先將obj存入next的地址,隨后+1。

id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next;  
    *next++ = obj;
    protect();
    return ret;
}

pop

調用pop函數時,有三步處理。

  • 判斷autoreleasepool是否為空,通過EMPTY_POOL_PLACEHOLDER占位符判斷,為空則清空這個page。
  • 傳入的stop是否不等于POOL_BOUNDARY標識,如果不等于則可能是一個有問題的page
  • 調用popPage方法,釋放對象。
static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    // 1.
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        page = hotPage();
        if (!page) {
            return setHotPage(nil);
        }
        page = coldPage();
        token = page->begin();
    } else {
        page = pageForPointer(token);
    }
    // 2.
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
        } else {
            return badPop(token);
        }
    }
    // 3.
    return popPage<false>(token, page, stop);
}

popPage

popPage函數核心代碼就是調用releaseUntil函數,在最開始會調用releaseUntil函數去完成釋放操作。

按照page達到一半就擴容的原則,后面的if語句會判斷執行poppage鏈表的狀態。

如果少于半滿,就將子節點刪除。

如果大于半滿,則保留子節點,并刪除后面的節點。

static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    page->releaseUntil(stop);
    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

releaseUntil

releaseUntil函數內部,核心邏輯是從當前page,從后到前調用objc_release,釋放被autorelease修飾的對象。

  • 獲取當前的hotPage
  • 判斷page是否為空,如果為空則表示里面的對象被釋放完,則將page的父節點page設置為hotPage
  • 獲得上一個節點,->的算數優先級比--要高,所以是先通過next獲取當前節點地址,這是一個為空的待存入節點,隨后執行--操作獲取上一個對象地址。
  • 通過memset將上一個節點釋放。
  • 判斷上一個節點是否占位符號POOL_BOUNDARY,如果不是則調用objc_release釋放對象。
  • while循環結束后,將當前page設置為hotPage。
void releaseUntil(id *stop) 
{
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        page->unprotect();
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }
    setHotPage(this);
}

autorelease

對象調用autorelease方法會被編譯器轉換為objc_autoreleaseReturnValue方法,并且經過多層調用,會來到底層的autorelease函數。

在這個函數中會判斷傳入的對象是否tagged pointer,因為tagged pointer沒有引用計數的概念。隨后會調用autoreleaseFast函數,函數內部調用add函數將obj對象加入到page中,并且會判斷是否需要創建新的page

static inline id autorelease(id obj)
{
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    return obj;
}

hotPage

hotPage可以被理解為,page鏈表的末尾,也就是調用push函數被插入的位置。執行hotPage函數獲取,以及調用setHotPage設置,都是操作的鏈表的末尾page。

AutoreleasePoolPage對象和線程一一對應,并且都被存儲在tls的哈希表中。通過tls_get_direct函數并傳入key可以獲取到對應的自動釋放池。

static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

hotPage函數中的判斷是下面的定義,這個標示意思是當前page為空,也就是從未存儲過任何對象。是一個標志位,下面是標志位的定義。

# define EMPTY_POOL_PLACEHOLDER ((id*)1)

coldPage

coldPage只有獲取函數,沒有設置函數。這是因為coldPage函數本質上,就是尋找page鏈表的根節點,從源碼中的while循環可以看到。

static inline AutoreleasePoolPage *coldPage() 
{
    AutoreleasePoolPage *result = hotPage();
    if (result) {
        while (result->parent) {
            result = result->parent;
            result->fastcheck();
        }
    }
    return result;
}

調試

_objc_autoreleasePoolPrint

如果想調試自動釋放池,可以通過_objc_autoreleasePoolPrint私有API來進行。將項目改為MRC,并且在命令行項目中增加下面這些調試代碼。

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();     // print1
    @autoreleasepool {
        _objc_autoreleasePoolPrint(); // print2
        Person *p1 = [[[Person alloc] init] autorelease];
        Person *p2 = [[[Person alloc] init] autorelease];
        _objc_autoreleasePoolPrint(); // print3
    }
    _objc_autoreleasePoolPrint();     // print4
    return 0;
}

打印結果如下,可以看到POOL_BOUNDARYpage中也占了一個位置。

objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. // 當前自動釋放池中沒有任何對象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: ##############
objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. // 當前自動釋放池中有1個對象,這個對象為POOL_BOUNDARY
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68122]: ##############
objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 3 releases pending. // 當前自動釋放池中有3個對象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: [0x102802038]  ################  POOL 0x102802038  //POOL_BOUNDARY
objc[68122]: [0x102802040]       0x100704a10  HTPerson          //p1
objc[68122]: [0x102802048]       0x10075cc30  HTPerson          //p2
objc[68122]: ##############
objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156]: 0 releases pending. // 當前自動釋放池中沒有任何對象,因為@autoreleasepool作用域結束,調用pop方法釋放了對象
objc[68156]: [0x100810000]  ................  PAGE  (hot) (cold)
objc[68156]: ##############

UIApplicationMain

項目中經常會看到下面的代碼,很多人的解釋是“這個autoreleasepool是為了釋放主線程的autorelease對象的”。但是,這個說法是錯誤的。autoreleasepool只負責自己作用域中添加的對象,而主線程在運行過程中,也會隱式創建autoreleasepool對象,這個pool是包含在main函數的pool里面的。

所以,主線程runloop每次執行循環后,釋放的對象是主線程的。而main函數的autoreleasepool釋放的,是main函數中直接創建的對象。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

釋放時機

區分

如果是在viewDidLoad方法中創建一個autorelease對象,并不是在這個方法結束后釋放對象,這個說法是錯誤的。即便執行到viewDidAppear,依然不會釋放對象。

autorelease修飾的對象,釋放時機有兩種。

  • 如果通過代碼添加一個autoreleasepool,在作用域結束時,隨著pool的釋放,就會釋放pool中的對象。這種情況是及時釋放的,并不依賴于runloop。
  • 另一種就是由系統自動進行釋放,系統會在runloop開始的時候創建一個pool,結束的時候會對pool中的對象執行release操作。

runloop

如果是系統創建的pool,需要手動開啟runloop,主線程默認已經開啟并運行,子線程需要調用currentRunLoop方法開啟并運行runloop,子線程中系統創建pool的流程才會正常工作。

包括主線程在內的每個線程,如果在線程中使用到了AutoreleasePool,則會創建兩個Observer并添加到當前線程的Runloop中,通過這兩個Observer進行對象的自動內存管理。

// activities = 0x1,kCFRunLoopEntry
<CFRunLoopObserver 0x60000012f000 [0x1135c2bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
// activities = 0xa0,kCFRunLoopBeforeWaiting | kCFRunLoopExit
<CFRunLoopObserver 0x60000012ef60 [0x1135c2bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}

首先會創建一個Observer并監聽kCFRunLoopEntry消息,時機是在進入Runloop前,此Observer的優先級設置為-2147483647的最高優先級,以保證回調發生在Runloop其他事件前。

然后創建另一個Observer,并監聽kCFRunLoopBeforeWaitingkCFRunLoopExit消息,時機分別在進入Runloop休眠和退出Runloop時,將Observer的優先級設置為2147483647,以保證回調發生在Runloop其他事件之后。

兩個Observer都有相同的回調函數_wrapRunLoopWithAutoreleasePoolHandler,在第一次回調時會在內部調用_objc_autoreleasePoolPush函數,創建自動釋放池。

kCFRunLoopBeforeWaiting將要進入休眠前,調用_objc_autoreleasePoolPop函數釋放自動釋放池中的對象,并調用_objc_autoreleasePoolPush函數創建一個新的釋放池。在kCFRunLoopExit將要退出Runloop時調用_objc_autoreleasePoolPop函數,釋放自動釋放池中的對象。

原文鏈接:https://juejin.cn/post/7141285896250195982

欄目分類
最近更新