一种避免 iOS 内存碎片的方法

一、引言

在和服务器传输文本的时候,可能会因为某一个字符的编码格式不同、少了一个字节、多了一个字节等原因导致整段文本都无法解码。而实际上如果可以找到这个字符,然后替换成其他字符的话,那整段文本其他字符都是可以解码的,用户在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.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

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏岑玉海

Carbondata源码系列(二)文件格式详解

在上一章当中,写了文件的生成过程。这一章主要讲解文件格式(V3版本)的具体细节。 1、字典文件格式详解 字典文件的作用是在存储的时候将字符串等类型转换为int类...

3866
来自专栏Java爬坑系列

【JAVA零基础入门系列】Day5 Java中的运算符

  运算符,顾名思义就是用于运算的符号,比如最简单的+-*/,这些运算符可以用来进行数学运算,举个最简单的栗子:   已知长方形的长为3cm,高为4cm,求长方...

1877
来自专栏FreeBuf

代码审计 | DedeCMS v 5.7 sp2 RemoveXSS bypass

DedeCMS 简称织梦CMS,当前最新版为 5.7 sp2,最近又去挖了挖这个CMS,发现过滤XSS的RemoveXSS函数存在缺陷导致可以被绕过。

763
来自专栏魂祭心

原 What Every Dev need

2698
来自专栏北京马哥教育

十分钟完成Bash 脚本进阶!列举Bash经典用法及其案例

? 前言:在linux中,Bash脚本是很基础的知识,大家可能一听脚本感觉很高大上,像小编当初刚开始学一样,感觉会写脚本的都是大神。虽然复杂的脚本是很烧脑,但...

3388
来自专栏CDA数据分析师

产品运营数据分析——SPSS数据分组案例

当我们的样本量过大,譬如以前讲过的,EXCEL2010最大只支持1048576行、16384列,尤其是当行数大于30万,一般的办公电脑处理都比较吃力,所以推荐做...

1915
来自专栏技术博客

C# DataTable 转换成List<T>

661
来自专栏小樱的经验随笔

【批处理学习笔记】第十七课:截取字符串

    在批处理中,set的功能有点繁杂:设置变量、显示环境变量的名及值、做算术运算、等待用户的输入、字符串截取、替换字符串,是我们常用的命令之一。   在字...

2964
来自专栏FreeBuf

Win10 Edge浏览器从越界写到任意内存读写

*本文原创作者:BoA,本文属FreeBuf原创奖励计划,未经许可禁止转载 在刚刚发布的微软11月份安全更新中,比较幸运刷了两个Edge的脚本引擎漏洞,对微软...

1847
来自专栏MasiMaro 的技术博文

SQL语句执行与结果集的获取

title: SQL语句执行与结果集的获取 tags: [OLEDB, 数据库编程, VC++, 数据库] date: 2018-01-28 09:22:1...

1002

扫码关注云+社区