Memory leak on iOS

Environment:
Elements 8.2.88.1871
XCode 7.1 + iOS 9.1 64bit simulator

Firstly, try below code:

public class BigObjectWontLeak{
    private NSMutableArray data;

    public override void dealloc(){
        NSLog("BigObjectWontLeak dealloc!!");   // this will be called immediately when a = null
    }
}

public class BigObjectWillLeak{
    private NSMutableArray data = new NSMutableArray withCapacity(128);  // the only difference

    public override void dealloc(){
        NSLog("BigObjectWillLeak dealloc!!");   // this will never be called, just try to add breakpoint
    }
}

public static class Program
{
    public int Main(int argc, AnsiChar** argv)
    {
        // memory leak test
        var a = new BigObjectWontLeak();
        a = null;
        var b = new BigObjectWillLeak();
        b = null;
    }
}

I’ve found different object sizes will cause different ARC behaviors, and that’s ok, but the second one is leaked absolutely.

Then I put the test code into an endless loop to prove this leak which can be observed by XCode Instruments easily, and which won’t happen on equivelent objc code.

RO C# version:

ROCS_MemoryLeakTest.zip (112.2 KB)

   while(true){
      var a = new BigObject();
      a = null;
   }

Objective-C version:

Objc_MemoryLeakTest.zip (32.6 KB)

   while(true){
      __attribute__((objc_precise_lifetime)) BigObject *a = [[BigObject alloc]init];
      a = nil;
   }

  1. Change the test code to:

         for(int i = 0; i<1024; ++i){
             var a = new BigObject();
             a = null;
         }
    

The allocated memory in the loop will be recycled in the future, but the BigObject.dealloc() will never be called even after its memory is recycled. So although it’s not a memory leak strictly, there’s a bug.

  1. This “delayed release” behavior is very different from what the lateast objective-c’s ARC performs, which means I have to take care when use loop or allocate temporary variables. Sometimes an explicit ARP is necessary to solve OOM which is needless in objc.
    Maybe it’s by design, but it’s not a good design as what objc/swift does.

what does that mean? how does the memory “get recycled” (and how do yo tell)

does it get called if you add an ARP around the loop or inside the loop?

“Get recycled” means that memory is freed and can be reused for new objects. Maybe I should say the old objects are destroyed instead of “get recycled”, but no dealloc methods are called so I’m not sure whether old objects are destroyed properly.
I’m just using XCode Instruments to tell this: the “Persistent Bytes” will be reduced after the loop.

So I can’t ensure the dealloc calls without an explicit ARP?
As I said, I need no explicit ARP to get things done in objc/swift, so Elements’ ARC is not good as the objc/swift?

How do you tell, if dealloc isn;t being called?

No. as i explained in the other thread, ARC is a very complicated thing, and there ear zlotys of retains and releases and auto-releases going on for what can look like a simple line of code. You cannot always assume no ARP will be involve din a transaction. For example, many/most merited calls will return an unopened reference that’s in the ARP already.

Could we in theory optimize tis one simple corner case you are providing to not touch the ARP, ate the risk of breaking lots off things that work just fine? Sure — but why, the code you’re providing (created an instance and are it right away) is not something use din real life.

So ask again — whats the purpose here, except for having an academic discussion? Nothing is broken.

ok, if “dealloc isn’t being called” means “objects are not destroyed yet”, then there’s an actual leak.

Please just run this test:
MemoryLeakTest.zip (112.0 KB)

And tell that when those temporary BigObjects’ dealloc will be called (lots of log will be output by dealloc) until you quit the app manually.

Hm, something does indeed seem to be wrong here. i see the following retain/releases:

#	Event Type	∆ RefCt	RefCt	Timestamp	Responsible Library	Responsible Caller
0	Malloc	+1	1	00:04.828.966	ConsoleApplication17	+[NSObject allocWithZone:]

1	Retain	+1	2	00:04.834.515	ConsoleApplication17	+[NSObject allocWithZone:]
2	Retain	+1	3	00:04.834.518	ConsoleApplication17	-[__ConsoleApplication17_Program Main:]

3	Release	-1	2	00:04.834.518	ConsoleApplication17	-[__ConsoleApplication17_Program 
4	Release	-1	1	00:04.834.519	ConsoleApplication17	-[__ConsoleApplication17_Program 

what’s odd is our generate code dopes the right thing (it gets/expects a retained object, retains it, releases it twice. So those zero out. But for some reason +[NSObject allocWithZone:] already does a second retain — i.e. the object we get back for allocating it already has a retain out of 2, it seems — which we don’t know about and thus ofc don’t compensate for.

I’ll have to pass this to carlo to have a look on compiler level, whats going on here.

very oddly, if i remove the private NSMutableArray data = new NSMutableArray withCapacity(1024/**1024*/);, the then object deallocates fine. It seems that having that field there causes the extra retain somehow,

Actually…

yep, this sound like it’s known issue “bugs://72928: Nougat: pre-initialized field causes class to not be deallocated properly” from thread No memory release for UViewController when variable is initialized.

Cool! But very surprised to see such a high-priority bug is still opened after 2 months…
Anyway, thanks for your overtime work on weekend to confirm my question :slight_smile:

I’m also wondering how to print those malloc/retain/release events. Is there a convenient tool to do that?

btw, it seems I’ve also found the cause why an object returned from a method is autoreleasing which is kept alive until the end of an ARP in Elements’ ARC, but it won’t happen in objc’s ARC. I’ll explain it in my previous thread.

run in Instruments, in Leaks mode