앞서 공부했던 비동기(Async), 동기(Sync), 직렬(Serial), 병행(Concurrency)를 iOS 개발에서는 어떻게 사용할까?
바로 GCD를 통해 큐에 스레드 분배하며 이러한 개념을 적용한다. 보통 메소드 위주의 작업에 이용되고 큐를 이용하여 스레드를 관리하기 때문에 FIFO의 형태를 띄고있다.
1. DispatchQueue.main
2. DispatchQueue.global
3. DistpatchQueue(label: "")
URLSession.shared.dataTask(with: url) { (data, response, error) in //URL Session은 내부적으로 비동기 처리되어있다.
if let error = error {
print("Failed to load image with error", error.localizedDescription)
}
if self.lastImgUrlUsedToLoadImage != url.absoluteString {
return
}
guard let imageData = data else { return }
let photoImage = UIImage(data: imageData)
imageCache[url.absoluteString] = photoImage
// UI 관련은 메인 스레드에서
DispatchQueue.main.async {
self.image = photoImage
}
}.resume()
왜 UI 관련 작업은 메인 스레드에서만 처리해야할까?
클로저를 사용할 때, 비동기 프로그래밍에서 작업 처리가 끝난 시점을 처리하기 위해 Completion Handler와 메모리 관리를 위해 캡처리스트 개념을 쓰게 된다. 캡처리스트는 응용하기 위해서는 순환참조와 ARC에 대해 알아야한다.
ARC는 Automatic Reference Counting 으로 자동 참조 카운팅이라는 의미이다. Heap 메모리에 할당되는 메모리를 관리하기 위해 사용하고 클래스의 인스턴스를 처리할 때 weak, strong 키워드를 이용한다.
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.textLabel.text = "New Posts Updated!"
}
}
이처럼 Reference Counting을 올리지 않으며 강력 순환 참조를 벗어나 내부 self를 사용하기 위해 [weak self] 와 같은 코드를 이용하는데 왜 이러한 캡처 리스트를 사용할까?
차이는 weak 키워드는 인스턴스가 dismiss 되면 크로 보낸 클로저(작업)도 중단되지만 strong 키워드로 된 self는 dismiss되더라도 큐로 보낸 클로저(작업)이 동작하여 오류를 불러올 수 있다.
이전에 프로젝트를 하면서 아래와 같은 동작을 반복하면 앱이 죽어버리는 경우가 생겼다.
lazy var backButton = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
})).then {
$0.setImage(UIImage.btnBack.withRenderingMode(.alwaysTemplate), for: .normal)
$0.tintColor = .white }
ViewController를 Push, pop하면서 버튼 초기화의 클로저 내부의 self 뷰컨에 메모리가 계속 쌓여 문제가 발생한 것이다.
당시 [weak self] 를 추가하여 뷰컨의 작업을 중단시키면서 메모리릭을 해결할 수 있었다.
GCD를 이용한 비동기 처리시, 순환참조를 잘 처리하여 메모리를 효율적으로 사용하는 것이 중요하다. 또한 completion handler를 통해 작업의 끝난 시점을 잘 파악하여 다음 작업을 처리할 수 있도록 해야한다.