CoreBluetooth writeValue:forCharacteristic:type: retains the data

For a personal project, I have been writing some library to interface the CoreBluetooth API with the go language. Because of what it does, that library is written in objective-C with ARC disabled.

One function I had to write takes a memory buffer (allocated with malloc() on the go side), creates an NSData object out of it to pass the data to the writeValue:forCharacteristic:type: CoreBluetooth method, and then releases the NSData as well as the original memory buffer:

void Write(CBPeripheral *p, CBCharacteristic *c, void *bytes, int len) {
  NSData *data = [NSData dataWithBytesNoCopy:bytes length:len freeWhenDone:true];
  [p writeValue:data forCharacteristic:c type:CBCharacteristicWriteWithoutResponse];
  [data release];
}

One thing I noticed is that the retainCount for data increases during the writeValue:forCharacteristic:type: API call. It is 1 before, and 2 after. This is surprising to me, because the documentation says "This method copies the data passed into the data parameter, and you can dispose of it after the method returns."

I suspects this results in a memory leak. Am I missing something here ?

Answered by DTS Engineer in 809996022

The issue here is this pairing:

NSData *data = [NSData dataWithBytesNoCopy:bytes length:len freeWhenDone:true];
…
[data release];

In manual retain-release you only call -release if you’re responsible for the object, that is:

  • You retained the object.

  • You created a fresh object with an +alloc / -init pair

  • Or with +new

  • You created a copy with -copy or related methods

In this case you’re not doing any of those. Rather, you’re calling a convenience method, +dataWithBytesNoCopy:length:freeWhenDone:. This retains an autoreleased value, which isn’t something you need to release.

I touch on this in Objective-C Memory Management for Swift Programmers, but it’s also well covered by older documentation, like the How Memory Management Works section of Cocoa Fundamentals Guide.

The fix here is to either:

  • Switch to an +alloc / -init pair.

  • Stop calling -release.

I think the first is your best option because it’ll likely generates less autorelease pool traffic.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The issue here is this pairing:

NSData *data = [NSData dataWithBytesNoCopy:bytes length:len freeWhenDone:true];
…
[data release];

In manual retain-release you only call -release if you’re responsible for the object, that is:

  • You retained the object.

  • You created a fresh object with an +alloc / -init pair

  • Or with +new

  • You created a copy with -copy or related methods

In this case you’re not doing any of those. Rather, you’re calling a convenience method, +dataWithBytesNoCopy:length:freeWhenDone:. This retains an autoreleased value, which isn’t something you need to release.

I touch on this in Objective-C Memory Management for Swift Programmers, but it’s also well covered by older documentation, like the How Memory Management Works section of Cocoa Fundamentals Guide.

The fix here is to either:

  • Switch to an +alloc / -init pair.

  • Stop calling -release.

I think the first is your best option because it’ll likely generates less autorelease pool traffic.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks a lot for the help and additional pointers. I do have a few additional questions though.

To get straight to the point, I believe the following code fixes the issue I was seeing:

void Write(CBPeripheral *p, CBCharacteristic *c, void *bytes, int len) {
  @autoreleasepool {
    NSData *data = [[NSData alloc] initWithBytesNoCopy:bytes length:len];
    [p writeValue:data forCharacteristic:c type:CBCharacteristicWriteWithoutResponse];
    [data release];
  }
}

I changed the way the NSData is allocated so that the alloc..release pattern is more clear. I still don't understand if that was actually an issue though - my mental model was that [NSData dataWithBytesNoCopy:bytes length:len] would be functionally equivalent to the explicit alloc/init way, so am I still missing something there ?

The other issue, which I added the @autoreleasepool block for, was that the CBPeripheral writeValue:forCharacteristic:type: method was retaining a reference to data, and then autoreleasing it (if I reduced the scope of my autorelease block to just around that API call, I could see the retainCound drop after exiting the block). That is surprising to me given that the documentation mentions that API copies the data so one wouldn't expect it to be retained. In my particular use case, I think it lead to the data being leaked because my application (again, the main code is in go language) does not have a cocoa main event loop that would be responsible for draining the (implicit) autorelease pool.

I wonder if there are any other gotchas that I would need to know about for interfacing with objective-C APIs when my main application is not written with a cocoa framework ? So far the other one I've hit is that I needed to explicitly create a dispatch queue for my delegate objects or they wouldn't receive any messages.

Reading through Memory Management Policy, I now understand your comment about the difference between the two NSData creation methods. Using dataWithBytesNoCopy:length returns an autoreleased NSData pointer, which is fine if I am going to need an @autorelease block anyway.

Accepted Answer

I think you’re on the right track here but I want to address a few points.

That is surprising to me given that the documentation mentions that API copies the data so one wouldn't expect it to be retained.

The API probably does call -copy, or one of its variants, but for immutable data [1] the -copy just does a -retain.

I think it lead to the data being leaked because my application … does not have a cocoa main event loop that would be responsible for draining the … autorelease pool.

Right. Well, technically it’s not a leak but something we call abandoned memory. The system still has a reference to it, but nothing can clean it up.

Note that you don’t necessarily need a Cocoa main event loop to clean this up. For example, if your Go code is running on a Dispatch queue, you can set up the queue to drain the autorelease pool. However, I think your current plan — do this in your Go > Objective-C glue — is the best option for your situation.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] So, NSData not NSMutableData.

CoreBluetooth writeValue:forCharacteristic:type: retains the data
 
 
Q