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];
排查思路
-
按照多年摸爬滚打的经验,先查看createContext和freeContext的调用是否平衡,没问题,是平衡的。
-
检查是否ARC,没问题,是ARC的。
-
初步怀疑是因为使用的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.
- 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/
参考资料:
- https://llvm.org/docs/LangRef.html#bitcast-to-instruction
- https://developer.apple.com/library/archive/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html
- https://download.developer.apple.com/wwdc_2011/adc_on_itunes__wwdc11_sessions__pdf/323_intro_to_arc_304repeat.pdf
- https://clang.llvm.org/docs/AutomaticReferenceCounting.html
- http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/