it is hard to see the issue without seeing the code. We would do the same as we suggest: use heap dump on the Java side and Instruments on the native side. One thing we would try to confirm is that the retain count of the stuck views is actually 1.
Nat/J’s logic is that when the retain count drops to 1, then it will replace its strong native Objc to Java reference with a WeakReference, because that last retain is actually the one held by the Java side, so when the finalizer frees that object, it will be released.
If you can confirm this, then the next place to check is whether the finalize() method of your ViewController Java object is actually called. If you follow up the inheritance chain, you will see that the finalize() in NativeObject will call the peer.release(), which will do the actual release of the native object. You should be able to follow it there to see if the release actually happens. (e.g. checking the retain count of the peer’s address in Xcode at the same time).
I have created a basic project that does some of the things my troublesome main project does: load images, scale them and put them in UIImageViews. I figured that would introduce the problem, but the memory usage stabilizes at some point. I am going to look further, and see if I can find a reproduction scenario.
No, I used the marking of generations of allocations profiling. This showed me an increase of about 3 MB per screen.
It looks very impressive, but there are so many memory tags and allocs, that I do not have a clue where the problem is.
Here are the backtraces of the first screen (a tree of views, image views and labels, without event listeners) that we show. I exercised the application a lot, during which there were many explicit garbage collection requests. But the first screen is still in memory after about a minute. I have no idea how the notification center got involved for this screen:
And here are the references to and from the same object on the JVM heap:
I do not observe anything of this screen directly. I do have global observers (keyboard shown/hidden, text field/view begin/end editing) that I register in applicationDidBecomeActive(...) and unregister in applicationWillResignActive(...) of my application delegate. The first screen does not have text fields or text views.
When I disable all global observers, the backtrace to the notification center is gone, but the iOS object for the screen still exists.
Note that at this point I have disabled almost all UI elements of the screens that I test with, but I still see that iOS and JVM objects are not being freed. Even though the JVM heap dump shows that there are no more incoming references from my own objects. I would expect that JVM peers would get garbage collected, and that that will decrease the reference count. If the iOS object is not disposed of at this point, and rertrieved again by Java code later, I expect a new JVM peer to be created because the previous one was garbage collected.
Considering you use notification server for your application delegate, the node to which the notification points to might be your application delegate which in turn transitively holds a reference to the stuck object.
Are you sure, your application delegate does not have any field that may capture (directly or indirectly) the view?
Definitely. I may have a variable somewhere that may hold on to a single screen/view, but I definitely do not have collections of screens/views in my own objects. See the instance of SplashScreen in my previous screenshot. It does not have incoming references from any of my classes.
If you want, I can send you a JVM heap dimp so you can inspect it yourself.
I set a breakpoint on the following line of warnIfOpen(), but it is never reached:
String message =
("A resource was acquired at attached stack trace but never released. "
+ "See java.io.Closeable for information on avoiding resource leaks.");
If I set a breakpoint at the start if warnIfOpen(), the debugger halts at this line. I can see that ENABLED is true, so I correctly enabled CloseGuard.
I also enabled all (Java) exception breakpoints. These get triggered a lot during start-up. If I disable them until after I visited each screen at least once, they are nog longer triggered. Not even when the application crashes eventually.
right before the view should be released and then check if it gets invoked or not? If not, then that will probably mean that there is a reference somewhere from the native side.
Thanks a lot for your advice. The combination of adding the @Owned annotations, asking the runtime to collect garbage when switching screens, and the dismantling of the view hierarchy, have resolved a lot of the retained memory. I still see some memory being retained when I use specific screens, but I will look into that.