在和服務器傳輸文本的時候,可能會因為某一個字符的編碼格式不同、少了一個字節、多了一個字節等原因導致整段文本都無法解碼。而實際上如果可以找到這個字符,然後替換成其他字符的話,那整段文本其他字符都是可以解碼的,用戶在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
方法分配的字符串是堆空間,如果數據過長,則很容易產生內存碎片。
解決這個問題有兩種思路:一是在棧空間分配內存,二是分配一個可以重復利用的堆空間。
從 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. https://github.com/opensource-apple/CF
2. https://gist.github.com/oleganza/781772
3. https://developer.apple.com/library/prerelease/content/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Tasks/CustomAllocators.html
4. https://developer.apple.com/library/prerelease/content/qa/qa1419/_index.html