제목: Catching Leaky View Controllers Without Instruments

레테인 사이클(retain cycles)에의해 생긴 메모리 누수를 찾아내는 방법 중 잘 알려진 방법은, 더이상 화면에 있지 않을때 모든 뷰컨트롤러들이 디얼록되었는지 확인하는 것이다. 이 과정은 각각이 할당해제되기 전에 직접 반복해서 시행해보아야하는데, 재밌지도 않을 뿐더러 여기서 에러를 만들어내기도 한다. 만약 좀 더 일찍 UIViewController 누수에대한 
과정을 알 수 있었다면, 매일 개발하는동안 훨씬 낫지 않겠는가?

UIViewController의 잘 알려지지 않은 두개 프로퍼티에대해 감사하게 될 수도 있다.
  • isBeingDismissed: 모달로 나타난 뷰컨트롤러가 dismiss될 때, 이 프로퍼티는 true이다.
  • isMovingFromParentViewController: 부모 뷰컨트롤러로부터 이 뷰컨트롤러가 제거될 때, true가 된다. 이것은 UINavigationController 스택에서 뷰컨트롤러가 pop될 때와 같은, 시스템 컨테이너에서 제거되는 것도 포함된다.
이 프로퍼티 중 하나가 true면, 곧 뷰컨트롤러가 디얼록될 수 있다는 것을 알 수 있다. 정확히 얼마나 있어야 내부적으로 정리된 상태를 만들고 ARC가 디얼록할지는 모른다. 간단하게 생각해서 2초보다 더 길지는 않을 것이라 가정하자.

우리가 생각한 것을 코드로 만들어보자.
extension UIViewController {
   public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
       let rootParentViewController = dch_rootParentViewController

       // We don’t check isBeingDismissed simply on this view controller because it’s common

       // to wrap a view controller in another view controller (e.g. in UINavigationController)

       // and present the wrapping view controller instead.

       if isMovingFromParentViewController || rootParentViewController.isBeingDismissed {
           let type = type(of: self)
           let disappearanceSource: String = isMovingFromParentViewController ? "removed from its parent" : "dismissed"

           DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { [weak self] in

               assert(self == nil, "\(type) not deallocated after being \(disappearanceSource)")
           })
       }
   }

   private var dch_rootParentViewController: UIViewController {
       var root = self

       while let parent = root.parent {
           root = parent
       }

       return root
   }
}
흥미로운 부분은 asyncAfter(deadline:execute:) 호출에서 일어난다. 먼저 sefl를 weak로 만들면([weak self]), 나중에 호출할 클로저에의해 리테인되지 않는다. 그리고 self가 nil이면 assert를 건다(여기서 self는 UIViewController). 뷰컨트롤러가 살아있게 잡아두는 리테인 사이클을 가질때만 nil이 아니다.

이제 마지막으로 해야할 일은 모든 뷰컨트롤러의 viewDidDisappear(_:)에서 dch_checkDeallocation()을 호출하는 것이다(부모로부터 제거되거나 dismiss된 후에도 살아있도록 잡아두는 것을 제외하고)
override func viewDidDisappear(_ animated: Bool) {
   super.viewDidDisappear(animated)

   dch_checkDeallocation()
}
만약 누수가 일어나고 있으면, assert에서 실패나 나올 것이다(-Onone 빌드에서만 가능).

여기서 우리는 그냥 메모리 그래프 디버거(Memory Graph Debugger)를 열어 사이클의 원인을 찾고 고치면 된다.

이 방법을 통해 새로 소개된 리테인 사이클을 빠르게 배우는데 도움이 될거라 생각한다. 여러분도 이 방법이 즐거웠길 바란다! production-ready 코드(더 많은 주석과 if DEBUG 확인으로)는 GitHub의 DeallocationChecker에서 확인해볼 수 있다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,