C与Objective-C混编的一些内存管理问题

问题背景

最近排查一个项目的内存泄露的时候,遇到这样的一个内存泄露的场景,这是一个C和OC混编问题,把问题的模型简化一下,如下所示:

struct TestContext 
{
    dispatch_semaphore_t data1;
    NSString *data2;
};

TestContext * createContext()
{
    TestContext *ctx = (struct TestContext *)calloc(1, sizeof(struct TestContext));
    ctx->data1 = dispatch_semaphore_create(1);
    ctx->data2 = [[NSString alloc] initWithFormat:@"data2:%d", 123];
    return ctx;
}
void freeContext(TestContext *ctx)
{
    free(ctx);
}

使用Xcode的instrument工具定位内存泄露的点在:

ctx->data1 = dispatch_semaphore_create(1);
ctx->data2 = [[NSString alloc] initWithFormat:@"data2:%d", 123];

排查思路

  1. 按照多年摸爬滚打的经验,先查看createContext和freeContext的调用是否平衡,没问题,是平衡的。

  2. 检查是否ARC,没问题,是ARC的。

  3. 初步怀疑是因为使用的calloc和free导致的问题。
    这是一个猜想,证实一下,于是把data1的类型改成如下的自定义对象类型:

@interface TestObj : NSObject
@end
@implementation TestObj
- (void)dealloc
{
    NSLog(@"dealloc %p", self);
}
@end

运行后,确定 dealloc 的输出没有被执行。
至此,基本可以证实calloc和free不能触发ARC的自动释放对象的机制。

问题的原因

先回顾一下ARC的原理:
LLVM的文档
摘录一起中一些说明如下:

Automatic Reference Counting implements automatic memory management for Objective-C objects and blocks, freeing the programmer from the need to explicitly insert retains and releases. It does not provide a cycle collector; users must explicitly manage the lifetime of their objects, breaking cycles manually or with weak or unsafe references.

A retainable object pointer (or “retainable pointer”) is a value of a retainable object pointer type (“retainable type”). There are three kinds of retainable object pointer types:
block pointers (formed by applying the caret (^) declarator sigil to a function type)

  • Objective-C object pointers (id, Class, NSFoo*, etc.)
  • typedefs marked with attribute((NSObject))
  • Other pointer types, such as int* and CFStringRef, are not subject to ARC’s semantics and restrictions.

Apple关于ARC的迁移的文章

  • You cannot use object pointers in C structures.
    Rather than using a struct, you can create an Objective-C class to manage the data instead.

WWDC2011已经给出了这个问题明确解答:

image.png

当然,最好还是别这么用。

解决办法

知道问题的原因,解决办法也是十分简单:

void freeContext(TestContext *ctx)
{
    ctx->data1 = nil;
    ctx->data2 = nil;
    free(ctx);
}

意思就是让ARC能够知道,data1和data2需要释放了。

进一步深挖

Apple不建议C结构体包含Objective-C对象,那么,C++是否可以包含Objective-C对象呢?毕竟,C++是有析构函数的,如果编译器能够知道在析构函数里加入释放Objective-C对象的代码呢?
有这个想法,验证一下:
把文件后缀修改为.mm,让编译器知道使用Objective-C++语法编译,calloc和free修改为new和delete。

TestContext * createContext()
{
    TestContext *ctx = new TestContext;
...
}
void freeContext(TestContext *ctx)
{
    delete ctx;
}

再次profile,果然没有泄露了。
试验的结果表明确实是可以正确的释放了,那么,具体背后的原理是怎么样的呢?

C++析构背后的原理

只有结果,不明白原理,不是我们追求的东西。
为了搞清楚背后的原理,我们用clang编译一下上面这段代码,看看有什么发现?

clang -S -fobjc-arc -emit-llvm TestContext.mm -o TestContext.mm.ll

对比使用 calloc、free 和 new、delete 的组合。
可以发现在使用了new、delete之后,编译器给TestContext类型增加了构造函数和析构函数的实现

; Function Attrs: noinline nounwind optnone ssp uwtable
define linkonce_odr void @_ZN11TestContextD2Ev(%struct.TestContext* %0) unnamed_addr #3 align 2 {
  %2 = alloca %struct.TestContext*, align 8
  store %struct.TestContext* %0, %struct.TestContext** %2, align 8
  %3 = load %struct.TestContext*, %struct.TestContext** %2, align 8
  %4 = getelementptr inbounds %struct.TestContext, %struct.TestContext* %3, i32 0, i32 1
  %5 = bitcast %1** %4 to i8**
  call void @llvm.objc.storeStrong(i8** %5, i8* null) #5
  %6 = getelementptr inbounds %struct.TestContext, %struct.TestContext* %3, i32 0, i32 0
  %7 = bitcast %0** %6 to i8**
  call void @llvm.objc.storeStrong(i8** %7, i8* null) #5
  ret void
}
; Function Attrs: noinline optnone ssp uwtable
define linkonce_odr void @_ZN11TestContextC2Ev(%struct.TestContext* %0) unnamed_addr #1 align 2 personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
;...
}

你可能第一眼没有看出来这里有构造函数和析构函数,不要紧,这是因为涉及到C++的名字修饰。
使用如下命令可以还原修饰之前的名字:

> c++filt -n _ZN11TestContextD1Ev
TestContext::~TestContext()
>c++filt -n _ZN11TestContextC2Ev
TestContext::TestContext()

我们只关注析构函数,大致解读了一下析构函数的实现:

  • 访问属性字段(data1)
  • 强制类型转换
  • 调用objc_storeStrong设置为nil

从这个背后的原来来看,其实,使用new、delete的方式创建C++对象的话,其中的Objective-C对象还是可以正常被ARC管理的。

后记:

在调查C++析构的最初,因为没有找到clang 生成中间代码的参数,是用Xcode直接调试代码,走到delete ctx的时候,选择 Debug Workflow 菜单的 Always show disassembly 查看汇编语言实现的,也能基本看到析构的逻辑:

image.png

其中,寄存器对应的参数顺序可以参考这个链接的文章:
http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/

参考资料:

  1. https://llvm.org/docs/LangRef.html#bitcast-to-instruction
  2. https://developer.apple.com/library/archive/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html
  3. https://download.developer.apple.com/wwdc_2011/adc_on_itunes__wwdc11_sessions__pdf/323_intro_to_arc_304repeat.pdf
  4. https://clang.llvm.org/docs/AutomaticReferenceCounting.html
  5. http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/

你可能感兴趣的:(C与Objective-C混编的一些内存管理问题)