一种避免 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 条评论
登录 后参与评论

相关文章

来自专栏linux驱动个人学习

ALSA声卡驱动的DAPM(二)-建立过程

在上一篇文章中,我们重点介绍了widget、path、route之间的关系及其widget的注册; http://www.cnblogs.com/linhaos...

3964
来自专栏张善友的专栏

使用MongoDB的支持Linq 驱动NoRM

MongoDB的驱动有好几个,分布式文件存储的数据库开源项目MongoDB里使用的是github.com/samus/mongodb-csharp,monogo...

19810
来自专栏领域驱动设计DDD实战进阶

14-TypeScript简单工厂模式

在TypeScript中,要调用功能,通常在调用方通过实例化被调用方对象来调用相关方法,但这种实现在调用方和被调用方形成了强耦合的关系。 另外如果被调用方有种实...

3353
来自专栏张善友的专栏

在Entity Framework 中执行T-sql语句

从Entity Framework  4开始在ObjectContext对象上提供了2个方法可以直接执行SQL语句:ExecuteStoreQuery<T> 和...

18210
来自专栏ImportSource

设计模式-抽象类,只是想为你做更多

如果说面向对象中的接口是把所有的事情扔给你的话,那么抽象类显然是想要为你做一些事情,如果实在有一部分做不了再扔给你。 相信对于大部分业务开发的场景下都不太会需要...

2967
来自专栏xingoo, 一个梦想做发明家的程序员

【Spring实战】—— 6 内部Bean

本篇文章讲解了Spring的通过内部Bean设置Bean的属性。   类似内部类,内部Bean与普通的Bean关联不同的是:   1 普通的Bean,在其...

1887
来自专栏猿份到

LeakCanary源码浅析

在Android开发中最让人们头疼的就是内存泄漏了,今天来介绍一个查看内存是否泄漏的工具LeakCanary,并通过研究源码明白它是如何分析和查找存在泄漏信息的...

3305
来自专栏MasiMaro 的技术博文

枚举进程中的模块

在Windows中枚举进程中的模块主要是其中加载的dll,在VC上主要有2种方式,一种是解析PE文件中导入表,从导入表中获取它将要静态加载的dll,一种是利用查...

1132
来自专栏java一日一条

你真的会写单例模式吗——Java实现

单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对Java中常见的单例模式写法做了一个总结,如有错漏之处,...

682
来自专栏Android开发指南

用最简单的例子说明设计模式(一)之单例模式、工厂模式、装饰模式、外观模式

3925

扫码关注云+社区