一種避免 iOS 記憶體碎片的方法

騰訊雲加社群發表於2017-10-25

歡迎大家前往騰訊雲社群,獲取更多騰訊海量技術實踐乾貨哦~

作者:QQ音樂技術團隊 

一、引言

在和伺服器傳輸文字的時候,可能會因為某一個字元的編碼格式不同、少了一個位元組、多了一個位元組等原因導致整段文字都無法解碼。而實際上如果可以找到這個字元,然後替換成其他字元的話,那整段文字其他字元都是可以解碼的,使用者在UI上也許能猜測出正確的字元是什麼,這種體驗是好於使用者看到一片空白。

程式碼的思路是對於無法用initWithData:encoding:方法解析的資料,則逐個位元組的進行解析。原始碼的一個分支如下:

while(檢索未超過檔案長度)
{
    if(1位元組長的編碼)
    {/*正確編碼,繼續迴圈*/}
    else if (2位元組長的編碼)
    {
        CFStringRef cfstr = CFStringCreateWithBytes(kCFAllocatorDefault, {byte1, byte2}, 2, kCFStringEncodingUTF8, false);
        if (cfstr)
        {/*正確編碼,繼續迴圈*/}
        else
        {/*替換字元*/}
    }
    else if(3,4,5,6個位元組長的解碼)...
}
複製程式碼

發現無法解析的字元後進行替換。這個方法的弊端在於CFStringCreateWithBytes方法分配的字串是堆空間,如果資料過長,則很容易產生記憶體碎片。

解決這個問題有兩種思路:一是在棧空間分配記憶體,二是分配一個可以重複利用的堆空間。

二、CFAllocatorRef的研究

從CFStringCreateWithBytes提供的引數看,呼叫者可以指定記憶體分配器。查閱官方文件對第一個引數CFAllocatorRef alloc給出的釋義:The allocator to use to allocate memory for the new string. Pass NULL or kCFAllocatorDefault to use the current default allocator。接下來研究下這個記憶體分配器的資料結構以及系統提供的六個分配器的區別。

先看下CFAllocatorRef的資料結構:

typedef const struct CF_BRIDGED_TYPE(id) __CFAllocator * CFAllocatorRef;
struct __CFAllocator {
    CFRuntimeBase _base;
    CFAllocatorRef _allocator;
    CFAllocatorContext _context;
};
複製程式碼

只考慮iOS平臺的話,__CFAllocator只有三個成員。其中CFAllocatorContext _context是分配器的核心,其作用是可以自定義分配和釋放的回撥函式:

typedef void *        (*CFAllocatorAllocateCallBack)(CFIndex allocSize, CFOptionFlags hint, void *info);
typedef void        (*CFAllocatorDeallocateCallBack)(void *ptr, void *info);
typedef struct {
    ...
    CFAllocatorAllocateCallBack        allocate;
    CFAllocatorDeallocateCallBack    deallocate;
    ...
} CFAllocatorContext;
複製程式碼

當系統使用這個分配器進行分配,釋放,重分配等操作的時候會呼叫相應的回撥函式來執行(上面程式碼省略了部分回撥函式,有興趣深入瞭解的同學可檢視CFBase.m的原始碼)。

接下來看系統為提供的一系列分配器的原始碼(只考慮iOS平臺)。

  • kCFAllocatorMalloc:系統的分配和釋放本質就是malloc(),realloc(),free()。
static void * __CFAllocatorCPPMalloc(CFIndex allocSize, CFOptionFlags hint, void *info)
{return malloc(allocSize);    }
static void __CFAllocatorCPPFree(void *ptr, void *info)
{free(ptr);}
複製程式碼
  • kCFAllocatorMallocZone:看原始碼這個分配器在iOS上和kCFAllocatorMalloc是一樣的,但在Mac的作業系統上是有區別的(malloc和malloc_zone_malloc)。

  • kCFAllocatorNull:其實什麼都不會做,直接返回NULL。看文件說明主要是用於在釋放的時候記憶體實際上不應該被釋放。

    static void *__CFAllocatorNullAllocate(CFIndex size, CFOptionFlags hint, void *info)
    { return NULL;}
    複製程式碼
  • kCFAllocatorUseContext:是一個固定的地址,它只用於CFAllocatorCreate()建立分配器的時候。表示建立分配器時使用自身的context->allocate方法來分配記憶體。因為分配器也是一個CF物件。

    const CFAllocatorRef kCFAllocatorUseContext = (CFAllocatorRef)0x03ab;
    複製程式碼
  • kCFAllocatorDefault:這個是取系統當前的預設分配器,這個需要結合另外兩個API來理。解:CFAllocatorGetDefault和CFAllocatorSetDefault方法。(原始碼中set方法有一段有意思的註釋:系統retain了兩次allocator,目的是為了在設定預設分配器的時候,之前的預設分配器不會釋放。那這裡不是會造成記憶體洩漏了嗎?覺得要慎用)。

  • kCFAllocatorSystemDefault:這個才是系統級別的預設分配器,如果不呼叫CFAllocatorSetDefault(),則用CFAllocatorGetDefault()取出的分配器就是這個。從原始碼來看,目前和kCFAllocatorMalloc沒區別(也許很久之前因為__CFAllocatorSystemAllocate不是用malloc實現的。後來相容了,這裡的故事有知道的歡迎告知)

三、自定義分配器

看完系統提供的分配器後發現都是在堆空間分配記憶體,沒有合適的。後發現系統提供了另外一個API:CFAllocatorCreate。這時可以考慮自定義一個分配器,分配器在分配記憶體的時候,返回一塊固定大小的記憶體重複使用。

void *customAlloc(CFIndex size, CFOptionFlags hint, void *info)
{
    return info;
}

void *customRealloc(void *ptr, CFIndex newsize, CFOptionFlags hint, void *info)
{
    NSLog(@"警告:發生了記憶體重新分配");
    return NULL;//不寫這個回撥系統也是返回NULL的。這裡簡單的打句log。
}

void customDealloc(void *ptr, void *info)
{
    //因為alloc的地址是外部傳來的,所以應該由外部來管理,這裡不要釋放
}

CFAllocatorRef customAllocator(void *address)
{
    CFAllocatorRef allocator = NULL;
    if (NULL == allocator)
    {
        CFAllocatorContext context = {0, NULL, NULL, NULL, NULL, customAlloc, customRealloc, customDealloc, NULL};
        context.info = address;
        allocator = CFAllocatorCreate(kCFAllocatorSystemDefault, &context);
    }
    return allocator;
}

int main()
{
    char allocAddress[160] = {0};
    CFAllocatorRef allocator = customAllocator(allocAddress);

    CFStringRef cfstr = CFStringCreateWithBytes(allocator, tuple, 2, kCFStringEncodingUTF8, false);
    if (cfstr)
    {
         //CFRelease(cfstr);//這裡不要釋放,這裡分配的記憶體是allocAddress的棧空間,由系統自己自己回收就好
    }
    CFAllocatorDeallocate(kCFAllocatorSystemDefault, (void *)allocator);
}
複製程式碼

這裡用了一個技巧是重複使用的記憶體首地址利用context的info來傳遞。allocAddress的大小為什麼是160個位元組呢?這個大小隻要取CFStringRef需要的最大長度就可以了。如果自己專案需要引用這個方法,需要考慮這個size需要設定多大。(取決於CFStringCreateWithBytes()的numBytes引數值,這裡會有位元組對齊的知識)。

建立的CFAllocatorRef也是在堆空間上,它也需要被釋放。系統同樣提供了釋放API:CFAllocatorDeallocate。這裡需要注意dealloc的allocator需要和create時是同一個allocator。否則無法釋放,造成記憶體洩漏。

四、結語

自定義分配器讓我們對記憶體的分配擁有了一定的可操作性,文中的應用場景是在建立物件時返回一塊固定的記憶體區域重複使用,避免了重複建立和釋放導致的記憶體碎片問題。這種可操作性相信以後在解決記憶體方面問題時會為你多提供一種解決方案。

CFBase的原始碼最近一次更新是2015.9.11日。這份原始碼最新也是基於iOS9的。在寫這種底層程式碼的時候需要格外小心,作者在寫的時候因為CFAllocatorCreate和CFAllocatorDeallocate的allocator引數傳的不同,導致記憶體洩漏,需要多多測試。釋出到外網的時候需要加上灰度策略以及開關控制。

最後分享一個額外小知識,iOS執行緒的預設棧空間大小是512KB(這個在蘋果出了新系統和新機器後可能會變大,所以使用的時候儘量多測試)。這裡踩過坑,程式原始碼中orignalBytes一開始是臨時變數,分配在棧上,但是由於字串太長,導致棧溢位crash,所以後面分配在堆上了。

參考連結

1.github.com/opensource-…

2.gist.github.com/oleganza/78…

3.developer.apple.com/library/pre…

4.developer.apple.com/library/pre…

相關閱讀

一站式滿足電商節雲端計算需求的祕訣
iOS 開發之動畫中的時間
使用 Skeleton Screen 提升使用者感知體驗

此文已由作者授權騰訊雲技術社群釋出,轉載請註明文章出處
原文連結:https://cloud.tencent.com/community/article/383806


相關文章