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/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容