관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Method Swizzling (메소드 스위즐링), 우아하게 dealloc 로그 찍기 (deinit 로그 찍기) 본문

iOS 기본 (swift)

[iOS - swift] Method Swizzling (메소드 스위즐링), 우아하게 dealloc 로그 찍기 (deinit 로그 찍기)

jake-kim 2022. 3. 16. 22:42

* 스위즐링 관련 설명이 더욱 추가된 최신 포스팅 글은 이 포스팅 글 참고

 

사전 지식) Objective-c에서 메소드가 실행되는 과정

  • Objective-C는 동적인 언어(dynamic language)
    • 동적인 언어: 어떤 코드가 실행될지 런타임에 결정 (실행되는 순간가지 코드의 실행을 미룰 수 있다는 의미)
    • 정적인 언어: 어떤 코드가 실행될지 컴파일/링크 시점에 결정
  • Objective-C는 런타임에 메세지를 날리고, 그 메세지를 통해 메소드를 찾아서 실행
    • message 생성 - [receive message]
    • 컴파일러에 의해 컨버젼 - objc_msgSend(receiver, selector, arg1, arg2, ...)
// ex) objective-c 메소드이므로, 런타임 때 코드 실행이 결정

@objc dynamic func someMethod() { ... }

Method Swizzling 개념

* swizzle: "뒤섞다"

  • Objective-C에 의해 만들어진 swift언어도 역시 Objective-C의 성격을 가지고 있으므로 메소드의 실행은 런타임에 결정
  • 메소드의 실행은 런타임때 결정되므로, 런타임에서 메소드가 실행되기전에 원하는 내용으로 뒤섞기가 가능
  • = 런타임 시점에 내가 원하는 코드가 실행되게끔 가능
  • 특정 SDK의 메소드가 있을 때, 이 메소드를 변경하고 싶은 경우 매우 유용하게 사용 (기존 SDk의 메소드를 수정할 수 없을 때)

Method Swizzling 구현 방법

  • 원본 메소드가 @objc dynamic 메소드여야 가능
import UIKit

class ViewController: UIViewController {

  @objc dynamic private func someMethod() {
    print("iOS앱 개발 알아가기 - 원본")
  }
}
  • 위 someMethod() 대신에 다른 메소드를 실행하고 싶은 경우?
    • class_getInstanceMethod라는 함수를 통해 메소드 인스턴스를 획득
    • method_exchangeImplemtations(_:_:) 함수를 통해 런타임 때 메소드 변경
extension ViewController {
  class func swizzleMethod() {
    guard
      let originalMethod = class_getInstanceMethod(Self.self, #selector(Self.someMethod)),
      let swizzledMethod = class_getInstanceMethod(Self.self, #selector(Self.anotherMethod))
    else { return }
    method_exchangeImplementations(originalMethod, swizzledMethod)
  }
  
  @objc private func anotherMethod() {
    print("iOS앱 개발 알아가기 - 스위즐링")
  }
}
  • swizzleMethod 실행 후 이전의 someMethod를 실행하면, anotherMethod가 실행
class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    Self.swizzleMethod()
    self.someMethod() // iOS앱 개발 알아가기 - 스위즐링
  }
  
  @objc dynamic private func someMethod() {
    print("iOS앱 개발 알아가기 - 원본")
  }
}

응용 - Method Swizzling을 이용한 dealloc 로그 찍기

ex) VC의 deinit 코드에 부분에 아무 코드도 구현하지 않아도, Method Swizzling을 통해 모든 VC가 deinit될 때 로그가 출력되도록 구현

- memory leak을 체크하기 위해서 ViewController의 deinit이 실행될 때 로그를 찍도록하는 기능을 Method Swillzing을 사용하여 구현

- 만약 Method Swizzling이라는 기법을 안쓴다면 모든 ViewController의 deinit마다 로그를 찍어주어야하는 상황

  • deinit은 swizzling이 원칙직으로 되지 않으므로, viewDidLoad시에 Deallocator를 내부적으로 할당 해놓고, Deallocator가 deinit되는 시점 (== ViewController가 deinit되는 시점)에 로그를 찍도록 구현
  • Deallocator 추가
//  Swizzling+UIViewController.swift

import UIKit

final private class Deallocator {
  var closure: () -> Void
  
  init(_ closure: @escaping () -> Void) {
    self.closure = closure
  }
  
  deinit {
    closure()
  }
}
  • viewDidLoad에서 Deallocator 클래스를 retain하도록 설정
    • viewDidLoad에서 해당 코드가 호출되도록 swizzling 구현
private var associatedObjectAddr = ""

extension UIViewController {
  class func swizzleMethodForDealloc() {
    let originalSelector = #selector(viewDidLoad)
    let swizzledSelector = #selector(swizzled_viewDidLoad)
    guard
      let originalMethod = class_getInstanceMethod(Self.self, originalSelector),
      let swizzledMethod = class_getInstanceMethod(Self.self, swizzledSelector)
    else { return }
    method_exchangeImplementations(originalMethod, swizzledMethod)
  }
  
  @objc private func swizzled_viewDidLoad() {
    let deallocator = Deallocator { print("deinit: \(Self.self)") }
    objc_setAssociatedObject(self, &associatedObjectAddr, deallocator, .OBJC_ASSOCIATION_RETAIN)
  }
}
  • 스위즐링 활성화를 위해서 swizzleMethodForDealloc()를 AppDelegate에서 호출
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  UIViewController.swizzleMethodForDealloc()
  return true
}

-> 시스템 내부적으로 구현된 viewDidLoad()도 호출되고, 위에서 정의된 deinit도 호출되어 Method Swizzling 부분을 쉽게 뗴었다 붙였다 할 수 있는 모듈성 증가

* 주의할 점: 시스템에서 정의된 viewDidLoad와 같은 메소드는 swizzling해도, swizzling메소드가 불린 후 viewDidLoad가 호출되는것을 주의 (기존 메소드에 확장 가능에 더욱 용이)

 

* 전체 코드: https://github.com/JK0369/ExMethodSwizzling

 

* 참고

https://www.letmecompile.com/objective-c-%EB%9F%B0%ED%83%80%EC%9E%84runtime-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91-%EB%B6%84%EC%84%9D/

https://stackoverflow.com/questions/40647504/is-it-possible-to-swizzle-deinit-using-swift-if-yes-then-how-to-achieve-this

Comments