IGListKitSections 没有被释放

IGListKitSections doesn't get deallocated

我在 IGListKit 部分解除分配时遇到问题。尝试调试 Xcode 内存图的问题。

我的设置是 AuthController -> AuthViewModel -> AuthSocialSectionController -> AuthSocialViewModel 和其他一些部分。

如果用户未登录,

AuthController 会从应用程序的多个部分显示。当我点击关闭时,AuthViewModel 和 AuthController 会获取释放,但它的底层部分没有。在这种情况下,内存图未显示任何泄漏,但未调用 deinit 方法。

但是当我尝试使用社交帐户授权(成功)然后查看内存图时,它显示那些部分不会像这样被释放:

在这种情况下,AuthViewModel 也不会被释放,但一段时间后它会释放,但它可能发生也可能不发生。

我检查了每个闭包和委托的弱引用,但仍然没有成功。

我认为最有意义的代码:

class AuthViewController: UIViewController {
 fileprivate let collectionView: UICollectionView = UICollectionView(frame: .zero,
                                                                     collectionViewLayout: UICollectionViewFlowLayout())
 lazy var adapter: ListAdapter
  = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)

 fileprivate lazy var previewProxy: SJListPreviewProxy = {
  SJListPreviewProxy(adapter: adapter)
 }()

 fileprivate let viewModel: AuthViewModel

 fileprivate let disposeBag = DisposeBag()

 init(with viewModel: AuthViewModel) {
  self.viewModel = viewModel

  super.init(nibName: nil, bundle: nil)

  hidesBottomBarWhenPushed = true
  setupObservers()
 }

 private func setupObservers() {
  NotificationCenter.default.rx.notification(.SJAProfileDidAutoLogin)
   .subscribe(
    onNext: { [weak self] _ in
     self?.viewModel.didSuccessConfirmationEmail()
     self?.viewModel.recoverPasswordSuccess()
   })
   .disposed(by: disposeBag)
 }

 required init?(coder _: NSCoder) {
  fatalError("init(coder:) has not been implemented")
 }

 // MARK: - View Controller Lifecycle

 override func viewDidLoad() {
  super.viewDidLoad()
  setup()
 }

 // MARK: - Private

 @objc private func close() {
  dismiss(animated: true, completion: nil)
 }

 /// Метод настройки экрана
 private func setup() {
  if isForceTouchEnabled() {
   registerForPreviewing(with: previewProxy, sourceView: collectionView)
  }

  view.backgroundColor = AppColor.instance.gray
  title = viewModel.screenName
  let item = UIBarButtonItem(image: #imageLiteral(resourceName: "close.pdf"), style: .plain, target: self, action: #selector(AuthViewController.close))
  item.accessibilityIdentifier = "auth_close_btn"
  asViewController.navigationItem.leftBarButtonItem = item
  navigationItem.titleView = UIImageView(image: #imageLiteral(resourceName: "logo_superjob.pdf"))

  collectionViewSetup()
 }

 // Настройка collectionView
 private func collectionViewSetup() {
  collectionView.keyboardDismissMode = .onDrag
  collectionView.backgroundColor = AppColor.instance.gray
  view.addSubview(collectionView)
  adapter.collectionView = collectionView
  adapter.dataSource = self
  collectionView.snp.remakeConstraints { make in
   make.edges.equalToSuperview()
  }
 }
}

// MARK: - DataSource CollectionView

extension AuthViewController: ListAdapterDataSource {

 func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
  return viewModel.sections(for: listAdapter)
 }

 func listAdapter(_: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
  return viewModel.createListSectionController(for: object)
 }

 func emptyView(for _: ListAdapter) -> UIView? {
  return nil
 }
}

// MARK: - AuthViewModelDelegate

extension AuthViewController: AuthViewModelDelegate {
 func hideAuth(authSuccessBlock: AuthSuccessAction?) {
  dismiss(animated: true, completion: {
   authSuccessBlock?()
  })
 }

 func reload(animated: Bool, completion: ((Bool) -> Void)? = nil) {
  adapter.performUpdates(animated: animated, completion: completion)
 }

 func showErrorPopover(with item: CommonAlertPopoverController.Item,
                       and anchors: (sourceView: UIView, sourceRect: CGRect)) {
  let popover = CommonAlertPopoverController(with: item,
                                             preferredContentWidth: view.size.width - 32.0,
                                             sourceView: anchors.sourceView,
                                             sourceRect: anchors.sourceRect,
                                             arrowDirection: .up)
  present(popover, animated: true, completion: nil)
 }
}

class AuthViewModel {

 fileprivate let assembler: AuthSectionsAssembler

 fileprivate let router: AuthRouter

 fileprivate let profileFacade: SJAProfileFacade

 fileprivate let api3ProfileFacade: API3ProfileFacade

 fileprivate let analytics: AnalyticsProtocol

 fileprivate var sections: [Section] = []

 weak var authDelegate: AuthDelegate?
 weak var vmDelegate: AuthViewModelDelegate?
  
  var authSuccessBlock: AuthSuccessAction?
  
  private lazy var socialSection: AuthSocialSectionViewModel = { [unowned self] in
  self.assembler.socialSection(delegate: self)
 }()

  init(assembler: AuthSectionsAssembler,
      router: AuthRouter,
      profileFacade: SJAProfileFacade,
      api3ProfileFacade: API3ProfileFacade,
      analytics: AnalyticsProtocol,
      delegate: AuthDelegate? = nil,
      purpose: Purpose) {
  self.purpose = purpose
  authDelegate = delegate
  self.assembler = assembler
  self.router = router
  self.profileFacade = profileFacade
  self.api3ProfileFacade = api3ProfileFacade
  self.analytics = analytics
  sections = displaySections()
 }
  
  private func authDisplaySections() -> [Section] {
  let sections: [Section?] = [vacancySection,
                              authHeaderSection,
                              socialSection,
                              authLoginPasswordSection,
                              signInButtonSection,
                              switchToSignUpButtonSection,
                              recoverPasswordSection]
  return sections.compactMap { [=11=] }
 }
}

class AuthSocialSectionController: SJListSectionController, SJUpdateCellsLayoutProtocol {
 fileprivate let viewModel: AuthSocialSectionViewModel

 init(viewModel: AuthSocialSectionViewModel) {
  self.viewModel = viewModel
  super.init()
  minimumInteritemSpacing = 4
  viewModel.vmDelegate = self
 }

 override func cellType(at _: Int) -> UICollectionViewCell.Type {
  return AuthSocialCell.self
 }

 override func cellInitializationType(at _: Int) -> SJListSectionCellInitializationType {
  return .code
 }

 override func configureCell(_ cell: UICollectionViewCell, at index: Int) {
  guard let itemCell = cell as? AuthSocialCell else {
   return
  }
  let item = viewModel.item(at: index)
  itemCell.imageView.image = item.image
 }

 override func separationStyle(at _: Int) -> SJCollectionViewCellSeparatorStyle {
  return .none
 }
}

extension AuthSocialSectionController {
 override func numberOfItems() -> Int {
  return viewModel.numberOfItems
 }
  
 override func didSelectItem(at index: Int) {
  viewModel.didSelectItem(at: index)
 }

}

// MARK: - AuthSocialSectionViewModelDelegate

extension AuthSocialSectionController: AuthSocialSectionViewModelDelegate {
 func sourceViewController() -> UIViewController {
  return viewController ?? UIViewController()
 }
}

protocol AuthSocialSectionDelegate: class {

 func successfullyAuthorized(type: SJASocialAuthorizationType)

 func showError(with error: Error)
}

protocol AuthSocialSectionViewModelDelegate: SJListSectionControllerOperationsProtocol, ViewControllerProtocol {
 func sourceViewController() -> UIViewController
}

class AuthSocialSectionViewModel: NSObject {
 struct Item {
  let image: UIImage
  let type: SJASocialAuthorizationType
 }

 weak var delegate: AuthSocialSectionDelegate?
 weak var vmDelegate: AuthSocialSectionViewModelDelegate?

 fileprivate var items: [Item]

 fileprivate let api3ProfileFacade: API3ProfileFacade
 fileprivate let analyticsFacade: SJAAnalyticsFacade
 fileprivate var socialButtonsDisposeBag = DisposeBag()

 init(api3ProfileFacade: API3ProfileFacade,
      analyticsFacade: SJAAnalyticsFacade) {
  self.api3ProfileFacade = api3ProfileFacade
  self.analyticsFacade = analyticsFacade
  items = [
   Item(image: #imageLiteral(resourceName: "ok_icon.pdf"), type: .OK),
   Item(image: #imageLiteral(resourceName: "vk_icon.pdf"), type: .VK),
   Item(image: #imageLiteral(resourceName: "facebook_icon.pdf"), type: .facebook),
   Item(image: #imageLiteral(resourceName: "mail_icon.pdf"), type: .mail),
   Item(image: #imageLiteral(resourceName: "google_icon.pdf"), type: .google),
   Item(image: #imageLiteral(resourceName: "yandex_icon.pdf"), type: .yandex)
  ]

  if analyticsFacade.isHHAuthAvailable() {
   items.append(Item(image: #imageLiteral(resourceName: "hh_icon"), type: .HH))
  }
 }

 // MARK: - actions

 func didSelectItem(at index: Int) {
  guard let vc = vmDelegate?.sourceViewController() else {
   return
  }

  let itemType: SJASocialAuthorizationType = items[index].type

  socialButtonsDisposeBag = DisposeBag()
  
  api3ProfileFacade.authorize(with: itemType, sourceViewController: vc)
   .subscribe(
    onNext: { [weak self] _ in
     self?.delegate?.successfullyAuthorized(type: itemType)
    },
    onError: { [weak self] error in
     if case let .detailed(errorModel)? = error as? ApplicantError {
      self?.vmDelegate?.asViewController.showError(with: errorModel.errors.first?.detail ?? "")
     } else {
      self?.vmDelegate?.asViewController.showError(with: "Неизвестная ошибка")
     }
   })
   .disposed(by: socialButtonsDisposeBag)
 }
}

// MARK: - DataSource

extension AuthSocialSectionViewModel {
 var numberOfItems: Int {
  return items.count
 }

 func item(at index: Int) -> Item {
  return items[index]
 }
}

// MARK: - ListDiffable

extension AuthSocialSectionViewModel: ListDiffable {
 func diffIdentifier() -> NSObjectProtocol {
  return ObjectIdentifier(self).hashValue as NSObjectProtocol
 }

 func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
  return object is AuthSocialSectionViewModel
 }
}

汇编程序负责创建所有内容,例如 AuthSocialSection:

func socialSection(delegate: AuthSocialSectionDelegate?) -> AuthSocialSectionViewModel {
  let vm = AuthSocialSectionViewModel(api3ProfileFacade: api3ProfileFacade,
                                      analyticsFacade: analyticsFacade)
  vm.delegate = delegate
  return vm
 }

如何正确调试这个问题?非常感谢任何建议或帮助

您的 AuthViewController 中的这些行会导致泄漏吗?

// adapter has viewController: self
lazy var adapter: ListAdapter
        = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)

fileprivate lazy var previewProxy: SJListPreviewProxy = {
    // capture self.adapter ?
    SJListPreviewProxy(adapter: adapter)
}()

我不确定,但至少你可以试试 :)


更新

我想知道这个惰性闭包和 self 内部,它不会创建保留循环,因为 lazy 初始化是 @nonescaping.

AuthSocialSectionController 中发现了一个问题。通过委托从 IGList 上下文传递 viewController 以某种方式导致内存问题。当我注释掉 viewModel.vmDelegate = self 时,问题就消失了。

这就解释了为什么当我在没有尝试授权的情况下点击关闭按钮时 AuthViewModel 被正确解除分配。只有当我点击授权时,才会调用 viewController 属性。

感谢@vpoltave 的帮助