GCD의 개념 및 종류
DispatchQueue (GCD) : 디스패치큐
(글로벌) 메인큐 : DispatchQueue.main
•
메인큐 = 메인쓰레드 (1번 쓰레드를 의미한다)
•
UI 업데이트 내용 처리하는 큐
•
Serial (직렬)
// 메인큐 = 메인쓰레드("쓰레드1번"을 의미), 한개뿐이고 Serial큐
let mainQueue = DispatchQueue.main
Swift
복사
글로벌큐 : DispatchQueue.global()
•
6가지 Qos(작업에 따라 Qos 상승가능)
•
종류가 여러 개, 기본설정 동시(Concurrent)
•
시스템이 우선순위에 따라 더 많은 쓰레드를 배치하고, 배터리를 더 집중해서 사용하도록 함
•
Concurrent (동시)
•
큐의 QoS (Quality Of Service)
→ iOS가 알아서 우선적으로 중요한 일임을 인지하고 쓰레드에 우선순위를 매겨 더 많은 쓰레드를 배치하고 CPU의 배터리를 더 집중해서 사용하도록 일을 빨리 끝내도록 하는 개념
// 6가지의 Qos를 가지고 있는 글로벌(전역) 대기열
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)
let userInitiatedQueue = DispatchQueue.global(qos: .userInitiated)
let defaultQueue = DispatchQueue.global() // 디폴트 글로벌큐
// DispatchQueue.global(qos: .default) 와 동일
let utilityQueue = DispatchQueue.global(qos: .utility)
let backgroundQueue = DispatchQueue.global(qos: .background)
let unspecifiedQueue = DispatchQueue.global(qos: .unspecified)
Swift
복사
•
.userInteractive
◦
유저와 직접적 인터렉티브
▪
UI업데이트 관련(직접X), 애니메이션, UI 반응 관련 어떤 것이든
▪
사용자와 직접 상호작용하는 작업에 권장, 작업이 빨리 처리되지 않으면 상황이 멈춘 것처럼 보일만함
◦
소요 시간 : 거의 즉시
•
.userInitiated
◦
유저가 즉시 필요하긴 하지만, 비동기적으로 처리된 작업
◦
ex) 앱 내에서 PDF 파일을 여는 것과 같은, 로컬 데이터베이스 읽기
◦
소요 시간 : 몇 초
•
.default
◦
일반적인 작업
•
.utility
◦
보통 Progress Indicator와 함께 길게 실행되는 작업, 계산
◦
ex) IO, Networking, 지속적인 데이터 feeds
◦
소요 시간 : 몇 초에서 몇 분
•
.background
◦
유저가 직접적으로 인지하지 않고(시간이 안 중요함) 작업
◦
ex) 데이터 미리 가져오기, 데이터베이스 유지보수, 원격 서버 동기화 및 백업 수행
◦
소요 시간 : 몇 분 이상 (속도보다는 에너지 효율성 중시)
•
.unspecified
◦
legacy API 지원 (스레드를 서비스 품질에서 제외시키는)
프라이빗(Custom)큐 : DispatchQueue(label: “…”)
•
Qos 추론 / Qos 설정 가능
•
디폴트 : 직렬(Serial) (둘다 가능, attributes로 설정)
// 기본적인 설정은 Serial, 다만 Concurrent 설정도 가능
let privateQueue = DispatchQueue(label: "com.inflearn.serial")
Swift
복사
GCD 사용시 주의해야할 사항
1. 반드시 메인큐에서 처리해야하는 작업
•
화면을 다시 그리는 역할에서 UI 관련 일들은 다시 메인 쓰레드로 보내야 한다.
DispatchQueue.global(qos: .utility).async {
...
...
...
self.textLabel.text = "New posts updated!"
}
// 에러!!
Swift
복사
에러 발생 : UI와 관련된 작업들은 메인 쓰레드에서 처리하지 않으면 에러가 발생한다 (메인쓰레드가 아닌 쓰레드는 그림을 다시 그리지 못하기 떄문)
DispatchQueue.global(qos: .utility).async {
...
...
...
// UI 관련 일이기 때문에, 그림을 다시 그리는 작업은 메인 큐에서
DispatchQueue.main.async {
self.textLabel.text = "New posts updated!"
}
}
Swift
복사
메인 쓰레드 : UI와 관련된 작업들을 메인 쓰레드에서 처리할 수 있도록 메인큐를 통해서, 작업을 다시 메인 쓰레드로 보냄
예시)
var imageView: UIImageView? = nil
let url = URL(string: "https://bit.ly/32ps0DI")!
// URL세션은 내부적으로 비동기로 처리된 함수임 -> URLSession 자체가 글로벌 큐에서 동작하고 있음
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("에러있음")
}
guard let imageData = data else { return }
// 즉, 데이터를 가지고 이미지로 변형하는 코드
let photoImage = UIImage(data: imageData)
// 🎾 이미지 표시는 DispatchQueue.main에서 🎾
DispatchQueue.main.async {
imageView?.image = photoImage
}
}.resume()
Swift
복사
DispatchQueue.global().async {
// 비동기적인 작업들 ===> 네트워크 통신 (데이터 다운로드)
DispatchQueue.main.async {
// UI와 관련된 작업은
}
}
Swift
복사
2) completionHandler의 존재 이유 - 올바른 콜백함수의 사용
잘못된 함수설계
•
비동기적인 작업을 해야하는 함수를 설계할 때, return을 통해서 데이터를 전달하려면 항상 nil이 반환되어야 한다.
func getImages(..., c..: String) -> UIImage? {
...
URLSession.shared.dataTask(...) {
...
}.resume()
...
...
return photoImage
}
// 함수 내부의 일이 끝나기 전에 return 하므로 무조건 nil로 반환
Swift
복사
// 잘못된 함수 설계
func getImages(with urlString: String) -> (UIImage?) {
let url = URL(string: urlString)!
var photoImage: UIImage? = nil
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("에러있음: \(error!)")
}
// 옵셔널 바인딩
guard let imageData = data else { return }
// 데이터를 UIImage 타입으로 변형
photoImage = UIImage(data: imageData)
}.resume()
// 항상 nil 이 나옴
return photoImage
}
getImages(with: "https://bit.ly/32ps0DI") // 무조건 nil로 리턴함 ⭐️
Swift
복사
제대로 된 함수설계
•
비동기적인 작업을 해야하는 함수는 항상 클로저를 호출할 수 있도록 함수를 설계해야한다.
func getImages(..., c..: @escaping(UIImage?) -> Void) {
...
...
...
URLSession.shared.dataTask(..) {
...
...
completion(photoImage) // 함수 내부의 일이 끝나면 completion 클로저 호출
}.resume()
}
Swift
복사
올바른 비동기함수의 설계
•
return이 아닌 콜백함수를 통해, 끝나는 시점을 알려줘야 한다.
// 올바른 함수 설계 (클로저 방식으로 설계!)
func properlyGetImages(with urlString: String, completionHandler: @escaping (UIImage?) -> Void) {
let url = URL(string: urlString)!
var photoImage: UIImage? = nil
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("에러있음: \(error!)")
}
// 옵셔널 바인딩
guard let imageData = data else { return }
// 데이터를 UIImage 타입으로 변형
photoImage = UIImage(data: imageData)
completionHandler(photoImage)
}.resume()
}
// 올바르게 설계한 함수 실행
properlyGetImages(with: "https://bit.ly/32ps0DI") { (image) in
// 처리 관련 코드 넣는 곳...
DispatchQueue.main.async {
// UI관련작업의 처리는 여기서
}
}
Swift
복사
// 서버와 통신 ===========================================================
struct MovieDataManager {
let movieURL = "http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?"
let myKey = "7a526456eb8e084eb294715e006df16f"
func fetchMovie(date: String, completion: @escaping ([Movie]?) -> Void) {
let urlString = "\(movieURL)&key=\(myKey)&targetDt=\(date)"
performRequest(with: urlString) { movies in
completion(movies)
}
}
func performRequest(with urlString: String, completion: @escaping ([Movie]?) -> Void) {
print(#function)
// 1. URL 구조체 만들기
guard let url = URL(string: urlString) else { return }
// 2. URLSession 만들기 (네트워킹을 하는 객체 - 브라우저 같은 역할)
let session = URLSession(configuration: .default)
// 3. 세션에 작업 부여
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
completion(nil)
return
}
guard let safeData = data else {
completion(nil)
return
}
// 데이터 분석하기
if let movies = self.parseJSON(safeData) {
//print("parse")
completion(movies)
} else {
completion(nil)
}
}
// 4.Start the task
task.resume() // 일시정지된 상태로 작업이 시작하기 때문
}
func parseJSON(_ movieData: Data) -> [Movie]? {
// 함수실행 확인 코드
print(#function)
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(MovieData.self, from: movieData)
let dailyLists = decodedData.boxOfficeResult.dailyBoxOfficeList
// 고차함수를 이용해 movie배열 생성하는 경우 ⭐️
let myMovielists = dailyLists.map {
Movie(movieNm: $0.movieNm, rank: $0.rank, openDate: $0.openDt, audiCnt: $0.audiCnt, accAudi: $0.audiAcc)
}
return myMovielists
} catch {
//print(error.localizedDescription)
// (파싱 실패 에러)
print("파싱 실패")
return nil
}
}
}
Swift
복사
3. weak, strong 캡처의 주의 - 객체 내에서 비동기 코드 사용시
강한 참조
•
캡처 리스트 안에서 weak self로 선언하지 않으면 강한 참조(strong)
1) 서로를 가리키는 경우 메모리 누수(Memory Leak) 발생 가능
2) (메모리 누수가 발생하지 않아도) 클로저의 수명 주기가 길어지는 현상이 발생할 수 있음
// 캡처리스트 + 약한 참조 선언하지 않으면 기본적으로 강한 참조
DispatchQueue.global(qos: .utility).async {
...
...
...
DispatchQueue.main.async {
self.textLabel.text = "New posts updated!"
}
}
Swift
복사
// 강한 참조가 일어나고, (서로가 서로를 가르키는) 강한 참조 사이클은 일어나지 않지만
// 생각해볼 부분이 있음
class ViewController: UIViewController {
var name: String = "뷰컨"
func doSomething() {
DispatchQueue.global().async {
sleep(3)
print("글로벌큐에서 출력하기: \(self.name)")
}
}
deinit {
print("\(name) 메모리 해제")
}
}
func localScopeFunction() {
let vc = ViewController()
vc.doSomething()
}
//localScopeFunction()
//글로벌큐에서 출력하기: 뷰컨
//뷰컨 메모리 해제
Swift
복사
•
(글로벌큐) 클로저가 강하게 캡쳐하기 때문에, 뷰컨트롤러의 RC가 유지되어 뷰컨트롤러가 해제되었음에도, 3초뒤에 출력하고 난 후 해제됨
•
강한 순환참조가 일어나진 않지만, 뷰컨트롤러가 필요 없음에도 오래 머무름
•
그리고 뷰컨트롤러가 사라졌음에도, 출력하는 일을 계속함
약한 참조
•
대부분의 경우, 캡처 리스트 안에서 weak self로 선언하는 것을 권장 → 불필요한 일이 생기지 않음
// 클로저이므로 캡처리스트 + 약한 참조 선언해야함
DispatchQueue.global(qos: .utility).async {[weak self] in
guard let `self` = self else { return }
...
...
DispatchQueue.main.async {
self.textLabel.text = "New posts updated!"
}
}
Swift
복사
class ViewController1: UIViewController {
var name: String = "뷰컨"
func doSomething() {
// 강한 참조 사이클이 일어나지 않지만, 굳이 뷰컨트롤러를 길게 잡아둘 필요가 없다면
// weak self로 선언
DispatchQueue.global().async { [weak self] in
guard let `self` = self else { return }
sleep(3)
print("글로벌큐에서 출력하기: \(self.name)")
}
}
deinit {
print("\(name) 메모리 해제")
}
}
func localScopeFunction1() {
let vc = ViewController1()
vc.doSomething()
}
localScopeFunction1()
//뷰컨 메모리 해제
//글로벌큐에서 출력하기: nil
Swift
복사
•
뷰컨트롤러를 오래동안 잡아두지 않음
•
뷰컨트롤러가 사라지면 —> 출력하는 일을 계속하지 않도록 할 수 있음
•
(if let 바인딩 또는 guart let 바인딩까지 더해서 return 가능하도록)
4. 동기함수를 비동기적으로 동작하는 함수로 변형하는 방법
일반(동기) 함수
•
오래 걸리는 일반적인 함수를 단순히 동기함수로 만들면 메인쓰레드에 부하가 걸림
func doSomething() {
print("프린트 시작")
sleep(3)
print("프린트 종료")
}
print("1")
doSomething()
print("2")
// 1
// 프린트 시작
// 프린트 종료
// 2
Swift
복사
비동기 함수로
•
오래걸리는 일반적인 함수를 내부에 비동기적 처리를 하면 비동기로 동작하는 함수로 변형 가능
func doSomething(com: @excaping (Void) -> Void) {
DispatchQueue.global().async {
print("프린트 시작")
sleep(3)
print("프린트 종료")
com()
}
}
print("1")
doSomething()
print("2")
// 1
// 2
// 프린트 시작
// 프린트 종료
Swift
복사
func longtimePrint(name: String) -> String {
print("프린트 - 1")
sleep(1)
print("프린트 - 2")
sleep(1)
print("프린트 - 3 이름:\(name)")
sleep(1)
print("프린트 - 4")
sleep(1)
print("프린트 - 5")
return "작업 종료"
}
// 요 함수를
// 작업을 오랫동안 실행하는데, 동기적으로 동작하는 함수를
// 비동기적으로 동작하도록 만들어, 반복적으로 사용하도록 만들기
// 내부적으로 다른 큐로 비동기적으로 보내서 처리
func asyncLongtimePrint(name: String, completionHandler: @escaping (String) -> Void) {
DispatchQueue.global().async {
let n = longtimePrint(name: name)
completionHandler(n)
}
}
//asyncLongtimePrint(name: "잡스", completion: <#T##(String) -> Void#>)
asyncLongtimePrint(name: "잡스") { (result) in
print(result)
// 메인쓰레드에서 처리해야하는 일이라면,
// DispatchQueue.main.async {
// print(result)
// }
}
Swift
복사
5. 비동기 함수 / 메서드의 이해
URLSession.shraed.dataTask
→ 비동기적으로 설계되어 있다(Asynchronicity), 이 함수를 실행하면 메인쓰레드에서 실행되는 것이 아니라 다른 쓰레드로 보내서 일 처리를 한다(기다리지 않는다)
일반적으로 대부분의 네트워킹 등 오래 걸리는 API들은 따로 비동기처리를 하지 않아도 내부적으로 비동기적으로 구현 되어 있다.
URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
self.image = UIImage(data: data!)
...
}.resume()
Swift
복사
URLSession은 비동기 메서드
•
API 중에는 내부적으로 비동기 처리가 된 메서들이 존재
let movieURL = "https://bit.ly/2QF3ID2"
// 1. URL 구조체 만들기
let url = URL(string: movieURL)!
// 2. URLSession 만들기 (네트워킹을 하는 객체 - 브라우저 같은 역할)
let session = URLSession.shared
// 3. 세션에 (일시정지 상태로)작업 부여
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
guard let safeData = data else {
return
}
print(String(decoding: safeData, as: UTF8.self))
}
// 4.작업시작
task.resume() // 일시정지된 상태로 작업이 시작하기 때문
print("출력 - 1")
// 비동기 처리 된 메서드
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
guard let safeData = data else {
return
}
print(String(decoding: safeData, as: UTF8.self))
}.resume()
print("출력 - 2")
Swift
복사