Reputation: 1360
I'm building a Kotlin library to use in my iOS app using Kotlin/Native. After I call some methods in the library from Swift, which works, I also want to call methods in Swift from the library. To accomplish this I implemented an interface in the library:
class Outbound {
interface HostInterfaceForTracking {
fun calcFeatureVector(bitmap: Any?): Array<Array<FloatArray>>?
}
var hostInterface: HostInterfaceForTracking? = null
fun registerInterface(hostInterface: HostInterfaceForTracking) {
this.hostInterface = hostInterface
instance.hostInterface = hostInterface
}
}
This is implemented on the Swift side like this:
class HostInterfaceForTracking : OutboundHostInterfaceForTracking {
var t : Outbound? = nil
init() {
TrackingWrapper.instance?.runOnMatchingLibraryThread {
self.t = Outbound()
self.t!.registerInterface(hostInterface: self)
}
}
func calcFeatureVector(bitmap: Any?) -> KotlinArray<KotlinArray<KotlinFloatArray>>? {
do {
var test : Any? = (bitmap as! Bitmap).bitmap
return nil
} catch {
return nil
}
}
}
The TrackingWrapper looks like this:
class TrackingWrapper : NSObject {
static var instance: TrackingWrapper? = nil
var inbound: Inbound? = nil
var worker: Worker
override init() {
self.worker = Worker()
super.init()
initInboundInterface()
}
func initInboundInterface() {
runOnMatchingLibraryThread {
TrackingWrapper.instance = self
self.inbound = Inbound()
HostInterfaceForTracking()
}
}
func runOnMatchingLibraryThread(block: @escaping() -> Void) {
worker.enqueue {
block()
}
}
}
The function runOnMatchingLibraryThread
is needed because every call to the TrackingLibrary needs to be called from the exact same thread, so the Worker
class initializes a thread and enqueues every method to that thread.
The Bitmap
in this case is simply a wrapper for an UIImage
, which I already accessed with the .bitmap
call, so I've tried to access the wrapped UIImage
and save it in the test
variable. The library gets the current camera frame from the Swift side every few frames and sends the current image wrapped as a Bitmap
to the method calcFeatureVector
depicted here.
Problem: My memory load starts increasing as soon as the app starts until the point it crashes. This is not the case if I don't access the wrapped UIImage
(var test : Any? = (bitmap as! Bitmap)
). So there is a huge memory leak, just by accessing the wrapped variable on the Swift side. Is there anything I've missed or is there any way to release the memory?
Upvotes: 0
Views: 685
Reputation: 8465
Looks like you have a circular dependency here:
TrackingWrapper.instance?.runOnMatchingLibraryThread {
self.t = Outbound()
self.t!.registerInterface(hostInterface: self)
}
You are asking a property inside HostInterfaceForTracking
to maintain a strong reference to the same instance of HostInterfaceForTracking
. You should be using [weak self]
to avoid the circular reference.
EDIT:
Ok after seeing the rest of you code theres a lot to unpack. There is a lot of unnecessary bouncing back and forth between classes, functions and threads.
runOnMatchingLibraryThread
to just create an instance of something. You only need to use that for the code processing the image itself (I would assume, I haven't seen anything so far that requires being split off into another thread). Inside TrackingWrapper
, you can create a singleton more easily, and matching the swift pattern by simply doing this as the first line:static let shared = TrackingWrapper()
And everywhere you want to use it, you can just call TrackingWrapper.shared
. This is more common and will avoid one of the levels of indirection in the code.
I'm not sure what Worker
or Inbound
are, but again these can and should be created inside the TrackingWrapper init, rather than branching Inbound
's init, to use another thread.
Inside initInboundInterface
you are creating an instance of HostInterfaceForTracking()
which doesn't get stored anywhere. The only reason HostInterfaceForTracking is continuing to stay in memory after its creation, is because of the internal circular dependency inside it. This is 100% causing some form of a memory issue for you. This should probably also be a property on TrackingWrapper, and again, its Init should not be called inside runOnMatchingLibraryThread
.
Having HostInterfaceForTracking
's init, also using runOnMatchingLibraryThread
is problematic. If we inline all the code whats happening is this:
TrackingWrapper
init() {
self.runOnMatchingLibraryThread {
TrackingWrapper.instance = self
self.inbound = Inbound()
TrackingWrapper.instance?.runOnMatchingLibraryThread {
self.t = Outbound()
self.t!.registerInterface(hostInterface: self)
}
}
}
Having all these classes unnecessarily keep coming back to TrackingWrapper is going to cause issues.
HostInterfaceForTracking
's init, no need to be creating Outbound on a separate thread. First line in this class can simply be:var t : Outbound = OutBound()
Or do it in the init if you prefer. Either way will also remove the issue of needing to unwrap Outbound before using it.
Outbound
you are storing 2 references to the hostInterface instance:this.hostInterface = hostInterface
instance.hostInterface = hostInterface
I would have imagined there should only be 1. If there are now multiple copies of a class that has a circular dependency, which has multiple calls to separate threads. This again will cause issues.
self
into a function to be stored, the class storing it would mark the property as weak
, like so:weak var hostInterface: ......
Which will avoid any circular dependency from forming. A quick google says this isn't how things work in Kotlin. It might be better to look into the swift side passing in a closure (lambda on kotlin) and the kotlin side executing that. This might avoid the need to store a strong reference. Otherwise you need to be looking into some part of your code setting hostInterface back to null. Again its a bit hard to say only seeing some of the code and not knowing how its working.
In short, it looks like the code is very over complicated, and needs to be simplified, so that all these moving pieces can be tracked easier.
Upvotes: 1