欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~
作者: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方法分配的字符串是堆空间,如果数据过长,则很容易产生内存碎片。
解决这个问题有两种思路:一是在栈空间分配内存,二是分配一个可以重复利用的堆空间。
从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平台)。
- static void * __CFAllocatorCPPMalloc(CFIndex allocSize, CFOptionFlags hint, void *info)
- {return malloc(allocSize); }
- static void __CFAllocatorCPPFree(void *ptr, void *info)
- {free(ptr);}
- static void *__CFAllocatorNullAllocate(CFIndex size, CFOptionFlags hint, void *info)
- { return NULL;}
- const CFAllocatorRef kCFAllocatorUseContext = (CFAllocatorRef) 0x03ab;
看完系统提供的分配器后发现都是在堆空间分配内存,没有合适的。后发现系统提供了另外一个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…
来源: https://juejin.im/post/59f05b69f265da43333d9bc8