Утечка памяти Нативная библиотека Kotlin в iOS

Я создаю библиотеку Kotlin для использования в своем приложении iOS, используя Kotlin/Native. После того, как я вызываю некоторые методы в библиотеке из Swift, что работает, я также хочу вызывать методы в Swift из библиотеки. Для этого я реализовал интерфейс в библиотеке:

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
    }
}

Это реализовано на стороне Swift следующим образом:

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
    }
  }
}

TrackingWrapper выглядит следующим образом:

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()
    }
  }
}

Функция runOnMatchingLibraryThread необходима, потому что каждый вызов TrackingLibrary должен вызываться из одного и того же потока, поэтому класс Worker инициализирует поток и ставит в очередь каждый метод этого потока. Bitmap в данном случае — это просто оболочка для UIImage, к которой я уже обращался с помощью вызова .bitmap, поэтому я попытался получить доступ к обернутой UIImage и сохранить ее в переменной test. Библиотека получает текущий кадр камеры со стороны Swift каждые несколько кадров и отправляет текущее изображение в виде Bitmap методу calcFeatureVector, изображенному здесь. Проблема: моя нагрузка на память начинает увеличиваться, как только приложение запускается, пока не произойдет сбой. Это не тот случай, если я не получу доступ к обернутому UIImage (var test : Any? = (bitmap as! Bitmap)). Таким образом, происходит огромная утечка памяти, просто при доступе к обернутой переменной на стороне Swift. Я что-то пропустил или есть способ освободить память?


440
1

Ответ:

Решено

Похоже, у вас здесь циклическая зависимость:

TrackingWrapper.instance?.runOnMatchingLibraryThread {
    self.t = Outbound()
    self.t!.registerInterface(hostInterface: self)
}

Вы просите свойство внутри HostInterfaceForTracking поддерживать сильную ссылку на тот же экземпляр HostInterfaceForTracking. Вы должны использовать [weak self], чтобы избежать циклической ссылки.

Обновлено:

Хорошо, увидев остальную часть вашего кода, есть много чего распаковать. Существует много ненужного переключения между классами, функциями и потоками.

  1. Нет необходимости использовать runOnMatchingLibraryThread, чтобы просто создать экземпляр чего-либо. Вам нужно использовать это только для кода, обрабатывающего само изображение (я предполагаю, что до сих пор я не видел ничего, что требовало бы разделения на другой поток). Внутри TrackingWrapper вы можете проще создать синглтон и сопоставить шаблон Swift, просто сделав это в качестве первой строки:
static let shared = TrackingWrapper()

И везде, где вы хотите его использовать, вы можете просто вызвать TrackingWrapper.shared. Это более распространено и позволит избежать одного из уровней косвенности в коде.

  1. Я не уверен, что такое Worker или Inbound, но опять же, их можно и нужно создавать внутри инициализации TrackingWrapper, а не разветвлять инициализацию Inbound, чтобы использовать другой поток.

  2. Внутри initInboundInterface вы создаете экземпляр HostInterfaceForTracking(), который нигде не сохраняется. Единственная причина, по которой HostInterfaceForTracking продолжает оставаться в памяти после его создания, заключается в внутренней циклической зависимости внутри него. Это на 100% вызывает у вас проблемы с памятью. Вероятно, это также должно быть свойством TrackingWrapper, и опять же, его Init не должен вызываться внутри runOnMatchingLibraryThread.

  3. Наличие инициализации HostInterfaceForTracking, а также использование runOnMatchingLibraryThread проблематично. Если мы встроим весь код, то произойдет следующее:

TrackingWrapper

init() {
   self.runOnMatchingLibraryThread {
       TrackingWrapper.instance = self
       self.inbound = Inbound()
    
       TrackingWrapper.instance?.runOnMatchingLibraryThread {
           self.t = Outbound()
           self.t!.registerInterface(hostInterface: self)
       }
   }
}

Если все эти классы будут без необходимости возвращаться в TrackingWrapper, это вызовет проблемы.

  1. Внутри инициализации HostInterfaceForTracking нет необходимости создавать Outbound в отдельном потоке. Первая строка в этом классе может быть просто:
var t : Outbound = OutBound()

Или сделайте это в init, если хотите. В любом случае также будет устранена проблема необходимости развернуть Outbound перед его использованием.

  1. Внутри Outbound вы храните 2 ссылки на экземпляр hostInterface:
this.hostInterface = hostInterface
instance.hostInterface = hostInterface

Я бы предположил, что должно быть только 1. Если сейчас есть несколько копий класса, который имеет циклическую зависимость, которая имеет несколько вызовов для отдельных потоков. Это снова вызовет проблемы.

  1. Я до сих пор не уверен в различиях между Swift и Kotlin. В Swift при передаче self в функцию для сохранения класс, хранящий его, пометит свойство как weak, например:
weak var hostInterface: ......

Что позволит избежать формирования круговой зависимости. Быстрый поиск в Google говорит, что в Котлине все работает иначе. Возможно, было бы лучше взглянуть на сторону Swift, проходящую через замыкание (лямбда на kotlin), и на сторону kotlin, выполняющую это. Это может избежать необходимости хранить сильную ссылку. В противном случае вам нужно изучить какую-то часть вашего кода, устанавливающую hostInterface обратно на ноль. Опять же, немного сложно сказать, только видя часть кода и не зная, как он работает.

Короче говоря, похоже, что код слишком сложен, и его нужно упростить, чтобы все эти движущиеся части можно было легче отслеживать.