앱이 죽엇음다 .. - - ;;

Terminated due to memory issue

그것도 memory issue 때문에.. 키득.

눈앞이 캄캄했지만 응 눈 다시 뜨고 개발해~

문제 파악

가장 먼저 한 일은 일단 울기. 그 다음에,
Xcode의 Memory Graph Debugger를 열어보자. 난 사실 이거 신경쓴 적 일절 없음 - - ;;

왠지 Share처럼 생긴 이것을 누르시오.
여기서 앱의 메모리 사용량을 볼 수 있다.

여기서 또 알 수 있는 것 하나는, 현재 메모리에 할당되어있는 인스턴스들과 주소 목록을 확인할 수 있다.

대략 이렇게 생겼다. 현재 메모리에 살아있는 인스턴스들도 볼 수 있다.

자 여기서 앱을 실행해보면서, 어느 순간에 메모리가 폭발하는지 찾으면 된다.

여기서는 약간의 노가다를 곁들였다..

찾는 과정은 대충 이랬음.
크래쉬가 난 뷰에 들어가보면서 메모리가 어떻게 변하는지, 어떤 순간에 변하는지 찾으려고 함.
memory debugger가 터질라고 하는 순간이 있었음ㅠ

저러다가 2GB 넘어가면 진심 대략 난감 .. oTL

내가 지금 개발하고 있는 <비움>이라는 앱은 애니메이션이 키포인트라 Lottie와 UIKit의 Animator같은 것들이 엄청 많이 사용되고 있다. 게다가 3D 렌더링 이미지도 많이 들어가서 메모리 사용량이 좀 큰 듯 ... 🤣

내가 추정해본 이유는 viewController가 push되고 pop되는 순간에 메모리가 제대로 해제되지 않고 적재되어 있어서, 반복적으로 push가 일어날 때마다 메모리에 인스턴스가 남아있는 듯 했다.

이게 바로 강한 참조 사이클.. 아놔

이론은 공부했으니 드디어 현장실습인거야!!!

해결 방법

첫 번째 의심: Delegate

이번 프로젝트에서는 커스텀 클래스로 UIComponent들을 분리해서 사용하고 있어서, push-pop/present-dismiss 를 처리할 때 delegate를 사용해서 처리하곤 했다. 또 데이터 전달에도 용이하게 사용 중 ...

하지만 delegate를 사용할 때 주의할 점이 있는데, 얘가 강한 참조 사이클을 유발하는 주범 중에 하나라는 것임.

왜 사이클이 발생?

class FloatingTabBarController: UITabBarController {
    let floatingTabbarView = FloatingBarView(tabBarItems)

		func setFloatingTabBarView() {
        floatingTabbarView.delegate = self
    }
}

class FloatingBarView: UIView {
    var delegate: FloatingBarViewDelegate?
}

대략 이런 경우가 있겠다..

setFloatingTabBarView()에서 FloatingTabBarController가 floatingTabbarView를 delegate의 대리자로 임명된다. 이때 FloatingTabBarController가 pop되거나, 할당 해제된다면 참조 사이클이 발생하는 것임.

class FloatingBarView: UIView {
    weak var delegate: FloatingBarViewDelegate?
}

이렇게 바꿔줘서 RC가 증가하지 않도록 바꿔줌.

아, 그런데 여기서 delegate protocol은 class 타입에만 위임할 수 있어야함. 그래야 weak, unowned를 쓸 수 있음~~
class, AnyObject 키워드를 통해 클래스 타입에만 이것을 준수할 수 있음을 명시해주삼.
근데 class 키워드는 swift5 이전 표기법임. deprecated 된다는 듯

두 번째 의심: lazy

lazy는 처음 사용되기 전까지는 초기화되지 않다가 실제 사용될 때 초기화 시켜주는 변수 키워드이다.
UIKit property를 작성하면서, self 변수에 접근해서 초기화하기 위해서 lazy로 선언한 것들이 많았다.

lazy var navigationLabel = UILabel().then {
    $0.text = self.trashType.mode
    $0.font = UIFont.nanumSquareFont(type: .extraBold, size: 20)
    $0.textColor = UIColor.white
}

예를 들면 이런 느낌... (Then이라는 라이브러리를 써서 편한 초기화를 해줬다. 클로저로 초기화하는 것보다 코드가 조금 더 줄어들어서 편함👍)

여기서 문제는, 클로저 안에서 self를 접근한다는 것임!
여기서 self는 ViewController 클래스(reference type)이니 클로저 내부에서는 reference capture를 할 것임
그럼 textFieldDividerView에서 self를 강한 참조할 것이니까~

사이클 발생? 응 아니다.

Then을 안 썼을 때 클로저를 통해 초기화하는 코드는 이렇다.

lazy var navigationLabel: UILabel = {
   let label = UILabel()
    label.text = self.trashType.mode
    label.font = .nanumSquareFont(type: .extraBold, size: 20)
    label.textColor = .white
    return label
}()

{ }뒤에 ( )를 붙여줘서 그 즉시 실행하고 결과를 돌려주고 끝내버리는 것임!
그렇기 때문에 클로저에 대한 참조가 일어나지 않아서 여기서는 참조 사이클이 발생하지 않는다.

세 번째 의심: UIAction 클로저 (iOS 14)

애는 좀 찾기 힘들었다.
왜냐면.. 클로저에서 변수 2개를 어떻게 써줘야하는지 모르겠어서ㅠ 찾기 힘들었음
자 그럼 일단 보자. 우리는 보통 버튼을 정의할 때 addTarget을 통해 action을 지정해줬다.

lazy var paperButton = UIButton().then {
    $0.addTarget(self, action: #selector(didTapPaperButton(_:)), for: .touchUpInside)
}

@objc
func didTapPaperButton(_ sender: UIButton) {
	  navigationController?.pushViewController(WritingViewController(), animated: true)
}

그런데 나란 개린이.. button 지정하면서 objc 함수를 새로 써줘야하는게 너무 번거롭고 함수 찾기가 힘들었음.

그래서 iOS14에서 나온 UIButton의 새로운 생성자를 사용했다. 당연하게도 targer version이 14 이상일 때만 사용가능하다는 점...

위의 코드를 UIAction을 사용해서 바꿔보면 이런 모습임.

lazy var paperButton = UIButton(type: .custom, primaryAction: UIAction(handler: { _ in
    navigationController?.pushViewController(WritingViewController(), animated: true)
}))

primaryAction이라는 파라미터를 사용해서 UIAction을 전달받게되고, 여기에 handler라는 변수로 closure를 넣어 줄 수 있음!
#selector, @objc 함수를 따로 정의하지않고 초기화 시에 action을 지정해줄 수 있다는 건 나에게 넘나..

환상 ㅋㅋ

하지만 여기서 클로저가 사용된다는 건.. 이 안에서 self를 접근하게 된다는 건.. retain cycle 발생 ㅋㅋ
근데 이거 알면서도 해결 못한 이유는..
클로저에서 변수를 하나밖에 안주는데 캡처 리스트에 self를 우겨넣어서 자꾸 오류가 났음
이렇게 ㅋㅋ

Contextual type for closure argument list expects 1 argument, which cannot be implicitly ignored

⇒ closure argument는 한 개인데 왜 그러셨냐는 뜻.

그럼 어쩌라고?

위의 typealias가 바로 파라미터로 들어가는 클로저의 함수 타입이다. (UIAction) → Void
Void를 리턴해주는데... 클로저 내부에서 self를 캡쳐하기 땜에 캡처리스트를 쓰고싶을 때는.. 대체 어떻게 써줘야하는 것?

바로 이렇게 써주면 된다 ...

[weak self] ([parameter]) in

lazy var backButton = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
    self?.navigationController?.popViewController(animated: true)
})).then {
    $0.setImage(UIImage.btnBack, for: .normal)
}

앗 참고로 weak 키워드는 언제든 할당 해제할 준비가 되어있어야해서 Optional, var 이어야함.
그래서 self? 로 접근해야한답니다.

아 이렇게 여러 의심을 통해.. 메모리 참조를 해보는 경험을 해봤습니다..
push - pop을 할 때 deinit이 잘 호출되는지 살펴보세요!

틀린 내용 있으면 말해주삼
그럼 안녕

profile
이내임니당 :>

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN