앱의 성능을 높이는 iOS 성능 트릭
게시 됨: 2022-03-10최신 iOS 하드웨어는 집약적이고 복잡한 많은 작업을 처리할 만큼 충분히 강력하지만 앱이 어떻게 작동하는지 주의하지 않으면 기기가 여전히 응답하지 않을 수 있습니다. 이 기사에서는 앱의 반응성을 높일 수 있는 5가지 최적화 트릭을 살펴보겠습니다.
1. 재사용 가능한 셀 큐에서 빼기
이전에 tableView(_:cellForRowAt:)
내부에서 tableView.dequeueReusableCell tableView.dequeueReusableCell(withIdentifier:for:)
사용한 적이 있을 것입니다. 셀 배열을 전달하는 대신 이 어색한 API를 따라야 하는 이유가 궁금하신가요? 이에 대한 추론을 살펴보자.
천 개의 행이 있는 테이블 보기가 있다고 가정합니다. 재사용 가능한 셀을 사용하지 않으면 다음과 같이 각 행에 대해 새 셀을 생성해야 합니다.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // Create a new cell whenever cellForRowAt is called. let cell = UITableViewCell() cell.textLabel?.text = "Cell \(indexPath.row)" return cell }
당신이 생각했던 것처럼, 이것은 당신이 맨 아래로 스크롤할 때 장치의 메모리에 천 개의 셀을 추가할 것입니다. 각 셀에 UIImageView
와 많은 텍스트가 포함된 경우 어떤 일이 발생할지 상상해 보세요. 한 번에 모두 로드하면 앱의 메모리가 부족해질 수 있습니다! 그 외에도 모든 단일 셀은 스크롤하는 동안 새 메모리를 할당해야 합니다. 테이블 보기를 빠르게 스크롤하면 많은 작은 메모리 덩어리가 즉시 할당되고 이 프로세스로 인해 UI가 버벅거릴 것입니다!
이 문제를 해결하기 위해 Apple은 dequeueReusableCell(withIdentifier:for:)
메서드를 제공했습니다. 셀 재사용은 더 이상 화면에 표시되지 않는 셀을 대기열에 배치하고 새 셀이 화면에 표시될 때(예: 사용자가 아래로 스크롤할 때 아래에 있는 후속 셀) 테이블 보기가 실행됩니다. 이 대기열에서 셀을 검색하고 cellForRowAt indexPath:
메소드에서 수정하십시오.

큐를 사용하여 셀을 저장하면 테이블 보기에서 천 개의 셀을 만들 필요가 없습니다. 대신 테이블 보기 영역을 덮기에 충분한 셀만 있으면 됩니다.
dequeueReusableCell
을 사용하여 앱에서 사용하는 메모리를 줄이고 메모리 부족을 덜 수 있습니다!
2. 초기 화면과 같은 시작 화면 사용
Apple의 HIG(휴먼 인터페이스 지침)에서 언급했듯이 시작 화면을 사용하여 앱의 응답성에 대한 인식을 높일 수 있습니다.
“빠르게 실행되고 즉시 사용할 수 있도록 앱에 대한 인식을 향상시키기 위한 것입니다. 모든 앱은 시작 화면을 제공해야 합니다.”
시작 화면을 시작 화면으로 사용하여 브랜딩을 표시하거나 로딩 애니메이션을 추가하는 것은 일반적인 실수입니다. Apple에서 언급한 대로 시작 화면을 앱의 첫 번째 화면과 동일하게 디자인합니다.
“앱의 첫 화면과 거의 동일한 시작 화면을 디자인하십시오. 앱 실행이 완료되었을 때 다르게 보이는 요소를 포함하면 사람들이 앱의 시작 화면과 앱 첫 화면 사이에서 불쾌한 깜박임을 경험할 수 있습니다.
“런칭 화면은 브랜딩 기회가 아닙니다. 시작 화면이나 "정보" 창처럼 보이는 진입 환경을 디자인하지 마세요. 로고나 기타 브랜딩 요소가 앱 첫 화면의 정적인 부분이 아니라면 포함하지 마세요."
로딩이나 브랜딩 목적으로 실행 화면을 사용하면 처음 사용하는 시간이 느려지고 사용자가 앱이 느리다고 느낄 수 있습니다.
새 iOS 프로젝트를 시작하면 빈 LaunchScreen.storyboard
가 생성됩니다. 앱이 뷰 컨트롤러와 레이아웃을 로드하는 동안 이 화면이 사용자에게 표시됩니다.
앱이 더 빠르게 느껴지도록 시작 화면을 사용자에게 표시될 첫 번째 화면(뷰 컨트롤러)과 유사하게 디자인할 수 있습니다.
예를 들어 Safari 앱의 시작 화면은 첫 번째 보기와 유사합니다.

시작 화면 스토리보드는 UIViewController, UITabBarController 및 UINavigationController와 같은 표준 UIKit 클래스만 사용할 수 있다는 점을 제외하면 다른 스토리보드 파일과 같습니다. 다른 사용자 정의 하위 클래스(예: UserViewController)를 사용하려고 하면 Xcode에서 사용자 정의 클래스 이름 사용이 금지됨을 알려줍니다.

주목해야 할 또 다른 사항은 iOS가 시작 화면 스토리보드에서 정적 이미지를 생성하고 사용자에게 표시하기 때문에 UIActivityIndicatorView
가 시작 화면에 배치될 때 애니메이션되지 않는다는 것입니다. (이는 WWDC 2014 프레젠테이션 "Platforms State of the Union", 약 01:21:56
에 간략하게 언급되어 있습니다.)
Apple의 HIG는 또한 시작 화면이 고정되어 있고 다른 언어에 맞게 텍스트를 현지화할 수 없기 때문에 시작 화면에 텍스트를 포함하지 말라고 조언합니다.
추천 자료 : 안면 인식 기능이 있는 모바일 앱: 현실화하는 방법
3. 뷰 컨트롤러의 상태 복원
상태 보존 및 복원을 통해 사용자는 앱을 떠나기 직전과 똑같은 UI 상태로 돌아갈 수 있습니다. 때때로 메모리 부족으로 인해 앱이 백그라운드에 있는 동안 운영 체제가 메모리에서 앱을 제거해야 할 수 있으며 앱이 보존되지 않으면 앱의 마지막 UI 상태를 추적하지 못해 사용자가 작업 내용을 잃을 수 있습니다. 진행 중!
멀티태스킹 화면에서 백그라운드에 있던 앱 목록을 볼 수 있습니다. 이러한 앱이 여전히 백그라운드에서 실행되고 있다고 가정할 수 있습니다. 실제로 이러한 앱 중 일부는 메모리 요구로 인해 시스템에서 종료되고 다시 시작될 수 있습니다. 우리가 멀티태스킹 보기에서 보는 앱 스냅샷은 실제로 우리가 앱을 종료했을 때(즉, 홈 또는 멀티태스킹 화면으로 가기 위해) 시스템에서 찍은 스크린샷입니다.

iOS는 이러한 스크린샷을 사용하여 앱이 여전히 실행 중이거나 이 특정 보기를 계속 표시하고 있는 것처럼 보이지만 동일한 스크린샷을 계속 표시하는 동안 앱이 백그라운드에서 이미 종료되거나 다시 시작되었을 수 있습니다.
멀티태스킹 화면에서 앱을 다시 시작할 때 앱이 멀티태스킹 보기에 표시된 스냅샷과 다른 사용자 인터페이스를 표시하는 것을 경험한 적이 있습니까? 앱이 상태 복원 메커니즘을 구현하지 않았고 앱이 백그라운드에서 종료될 때 표시된 데이터가 손실되었기 때문입니다. 이는 사용자가 앱을 종료했을 때와 동일한 상태로 유지하기를 기대하기 때문에 좋지 않은 경험으로 이어질 수 있습니다.
Apple의 기사에서:
“그들은 앱이 종료했을 때와 동일한 상태가 되기를 기대합니다. 상태 보존 및 복원을 통해 앱이 다시 시작될 때 이전 상태로 돌아갈 수 있습니다."
UIKit은 상태 보존 및 복원을 단순화하기 위해 많은 작업을 수행합니다. 적절한 시간에 자동으로 앱 상태의 저장 및 로드를 처리합니다. 우리가 해야 할 일은 앱에 상태 보존 및 복원을 지원하고 어떤 데이터를 보존해야 하는지 알려주는 몇 가지 구성을 추가하는 것입니다.
상태 저장 및 복원을 활성화하려면 AppDelegate.swift
에서 다음 두 가지 방법을 구현할 수 있습니다.
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
이렇게 하면 앱이 애플리케이션의 상태를 자동으로 저장하고 복원하도록 지시합니다.
다음으로 어떤 뷰 컨트롤러를 보존해야 하는지 앱에 알려줄 것입니다. 스토리보드에서 "복원 ID"를 지정하여 이 작업을 수행합니다.

"스토리보드 ID 사용"을 선택하여 스토리보드 ID를 복원 ID로 사용할 수도 있습니다.
코드에서 복원 ID를 설정하기 위해 뷰 컨트롤러의 restorationIdentifier
속성을 사용할 수 있습니다.
// ViewController.swift self.restorationIdentifier = "MainVC"
상태 보존 동안 복원 식별자가 할당된 보기 컨트롤러 또는 보기는 디스크에 상태가 저장됩니다.
복원 식별자는 함께 그룹화되어 복원 경로를 형성할 수 있습니다. 식별자는 루트 뷰 컨트롤러에서 현재 활성 뷰 컨트롤러까지 뷰 계층을 사용하여 그룹화됩니다. MyViewController가 다른 탭 표시줄 컨트롤러에 포함된 탐색 컨트롤러에 포함되어 있다고 가정합니다. 자신의 클래스 이름을 복원 식별자로 사용한다고 가정하면 복원 경로는 다음과 같습니다.
TabBarController/NavigationController/MyViewController
사용자가 MyViewController가 활성 뷰 컨트롤러인 상태에서 앱을 떠날 때 이 경로는 앱에 의해 저장됩니다. 그러면 앱은 표시된 이전 보기 계층 구조를 기억합니다( Tab Bar Controller → Navigation Controller → My View Controller ).
복원 식별자를 할당한 후 보존된 각 뷰 컨트롤러에 대해 encodeRestorableState(with coder:) 및 decodeRestorableState(with coder:) 메서드를 구현해야 합니다. 이 두 가지 방법을 사용하여 저장하거나 로드해야 하는 데이터와 인코딩 또는 디코딩 방법을 지정할 수 있습니다.
뷰 컨트롤러를 살펴보겠습니다.
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { // will be called during state preservation override func encodeRestorableState(with coder: NSCoder) { // encode the data you want to save during state preservation coder.encode(self.username, forKey: "username") super.encodeRestorableState(with: coder) } // will be called during state restoration override func decodeRestorableState(with coder: NSCoder) { // decode the data saved and load it during state restoration if let restoredUsername = coder.decodeObject(forKey: "username") as? String { self.username = restoredUsername } super.decodeRestorableState(with: coder) } }
고유한 메소드의 맨 아래에서 수퍼클래스 구현을 호출하는 것을 기억하십시오. 이렇게 하면 상위 클래스가 상태를 저장하고 복원할 수 있습니다.

객체의 디코딩이 완료되면 applicationFinishedRestoringState()
가 호출되어 상태가 복원되었음을 뷰 컨트롤러에 알립니다. 이 방법으로 뷰 컨트롤러의 UI를 업데이트할 수 있습니다.
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { ... override func applicationFinishedRestoringState() { // update the UI here self.usernameLabel.text = self.username } }
당신은 그것을 가지고 있습니다! 이는 앱의 상태 보존 및 복원을 구현하는 데 필수적인 방법입니다. 앱이 사용자에 의해 강제 종료될 때 운영 체제는 저장된 상태를 제거하여 상태 보존 및 복원에 문제가 발생할 경우 깨진 상태에 빠지는 것을 방지한다는 점을 염두에 두십시오.
또한 모델 데이터(예: UserDefaults 또는 Core Data에 저장되어야 하는 데이터)를 상태에 저장하지 마십시오. 그렇게 하는 것이 편리해 보일 수 있습니다. 사용자가 앱을 강제 종료하면 상태 데이터가 제거되며 이러한 방식으로 모델 데이터를 잃고 싶지는 않을 것입니다.
상태 보존 및 복원이 잘 작동하는지 테스트하려면 다음 단계를 따르세요.
- Xcode를 사용하여 앱을 빌드하고 실행합니다.
- 테스트하려는 상태 보존 및 복원이 있는 화면으로 이동합니다.
- 홈 화면으로 돌아가서(홈 버튼을 위로 스와이프하거나 더블 클릭하거나 시뮬레이터에서 Shift ⇧ + Cmd ⌘ + H 를 눌러) 앱을 백그라운드로 보냅니다.
- 버튼을 눌러 Xcode에서 앱을 중지합니다.
- 앱을 다시 실행하고 상태가 성공적으로 복원되었는지 확인합니다.
이 섹션은 상태 보존 및 복원의 기본 사항만 다루기 때문에 상태 복원에 대한 보다 심층적인 지식을 얻으려면 Apple Inc.의 다음 기사를 추천합니다.
- 상태 보존 및 복원
- UI 보존 프로세스
- UI 복원 프로세스
4. 불투명하지 않은 뷰의 사용을 최대한 줄이십시오
불투명 보기는 투명도가 없는 보기입니다. 즉, 뒤에 배치된 UI 요소가 전혀 표시되지 않습니다. Interface Builder에서 뷰를 불투명하게 설정할 수 있습니다.

또는 UIView의 isOpaque
속성을 사용하여 프로그래밍 방식으로 수행할 수 있습니다.
view.isOpaque = true
보기를 불투명하게 설정하면 화면을 렌더링하는 동안 그리기 시스템이 일부 그리기 성능을 최적화합니다.
보기에 투명도가 있는 경우(즉, 알파가 1.0 미만인 경우) iOS는 보기 계층 구조에서 보기의 여러 레이어를 혼합하여 표시해야 할 항목을 계산하기 위해 추가 작업을 수행해야 합니다. 반면에 보기가 불투명하게 설정된 경우 드로잉 시스템은 이 보기를 앞에 놓고 뒤에 있는 여러 보기 레이어를 혼합하는 추가 작업을 방지합니다.
Debug → Color Blended Layers 를 확인하여 iOS 시뮬레이터에서 어떤 레이어가 혼합되고 있는지(불투명하지 않음) 확인할 수 있습니다.

Color Blended Layers 옵션을 확인한 후 일부 보기는 빨간색이고 일부는 녹색임을 알 수 있습니다. 빨간색은 뷰가 불투명하지 않고 출력 디스플레이가 그 뒤에 혼합된 레이어의 결과임을 나타냅니다. 녹색은 보기가 불투명하고 혼합이 수행되지 않았음을 나타냅니다.

위에 표시된 레이블("친구 보기" 등)은 레이블을 스토리보드로 끌면 배경색이 기본적으로 투명하게 설정되기 때문에 빨간색으로 강조 표시됩니다. 드로잉 시스템이 레이블 영역 근처에서 디스플레이를 합성할 때 레이블 뒤에 있는 레이어를 요청하고 일부 계산을 수행합니다.
앱 성능을 최적화할 수 있는 한 가지 방법은 빨간색으로 강조 표시되는 보기 수를 최대한 줄이는 것입니다.
label.backgroundColor = UIColor.clear
를 label.backgroundColor = UIColor.white
UIColor.white 로 변경하면 레이블과 그 뒤에 있는 뷰 레이어 간의 레이어 혼합을 줄일 수 있습니다.

UIImageView를 불투명하게 설정하고 배경색을 할당한 경우에도 시뮬레이터는 이미지 보기에서 여전히 빨간색으로 표시됩니다. 이는 이미지 보기에 사용한 이미지에 알파 채널이 있기 때문일 수 있습니다.
이미지의 알파 채널을 제거하려면 미리보기 앱을 사용하여 이미지를 복제하고( Shift ⇧ + Cmd ⌘ + S ) 저장할 때 "알파" 확인란의 선택을 취소합니다.

5. 백그라운드 스레드(GCD)에 무거운 처리 기능 전달
UIKit은 메인 스레드에서만 작동하기 때문에 메인 스레드에서 과도한 처리를 수행하면 UI가 느려집니다. 메인 스레드는 UIKit에서 사용자 입력을 처리하고 응답할 뿐만 아니라 화면을 그리는 데에도 사용됩니다.
앱을 반응형으로 만드는 핵심은 가능한 한 많은 처리 작업을 백그라운드 스레드로 이동하는 것입니다. 메인 스레드에서 복잡한 계산, 네트워킹 및 과도한 IO 작업(예: 디스크 읽기 및 쓰기)을 수행하지 마십시오.
터치 입력에 갑자기 응답하지 않는 앱을 사용했는데 앱이 멈춘 것처럼 느껴집니다. 이것은 주로 메인 스레드에서 많은 계산 작업을 실행하는 앱으로 인해 발생합니다.
기본 스레드는 일반적으로 UIKit 작업(예: 사용자 입력 처리)과 일부 가벼운 작업을 짧은 간격으로 번갈아 가며 수행합니다. 무거운 작업이 메인 스레드에서 실행 중인 경우 UIKit은 터치 입력을 처리할 수 있기 전에 무거운 작업이 완료될 때까지 기다려야 합니다.

기본적으로 뷰 컨트롤러 수명 주기 메서드(예: viewDidLoad) 및 IBOutlet 함수 내부의 코드는 기본 스레드에서 실행됩니다. 무거운 처리 작업을 백그라운드 스레드로 이동하기 위해 Apple에서 제공하는 Grand Central Dispatch 대기열을 사용할 수 있습니다.
다음은 대기열 전환을 위한 템플릿입니다.
// Switch to background thread to perform heavy task. DispatchQueue.global(qos: .default).async { // Perform heavy task here. // Switch back to main thread to perform UI-related task. DispatchQueue.main.async { // Update UI. } }
qos
는 "서비스 품질"을 의미합니다. 서비스 품질 값이 다르면 지정된 작업에 대한 우선 순위가 다릅니다. 운영 체제는 QoS 값이 더 높은 대기열에 할당된 작업에 대해 더 많은 CPU 시간과 CPU 전력 I/O 처리량을 할당합니다. 즉, QoS 값이 더 높은 대기열에서는 작업이 더 빨리 완료됩니다. QoS 값이 높을수록 더 많은 리소스를 사용하기 때문에 더 많은 에너지를 소비합니다.
다음은 가장 높은 우선 순위에서 가장 낮은 우선 순위까지의 QoS 값 목록입니다.

Apple은 다양한 작업에 사용할 QoS 값의 예가 있는 편리한 표를 제공했습니다.
한 가지 명심해야 할 것은 모든 UIKit 코드는 항상 메인 스레드에서 실행되어야 한다는 것입니다. 백그라운드 스레드에서 UIKit 개체(예: UILabel
및 UIImageView
)를 수정하면 UI가 실제로 업데이트되지 않거나 충돌이 발생하는 등 의도하지 않은 결과가 발생할 수 있습니다.
Apple의 기사에서:
"메인 스레드가 아닌 스레드에서 UI를 업데이트하는 것은 UI 업데이트 누락, 시각적 결함, 데이터 손상 및 충돌을 초래할 수 있는 일반적인 실수입니다."
반응형 앱을 빌드하는 방법을 더 잘 이해하려면 UI 동시성에 대한 Apple의 WWDC 2012 비디오를 시청하는 것이 좋습니다.
메모
성능 최적화의 단점은 더 많은 코드를 작성하거나 앱 기능 외에 추가 설정을 구성해야 한다는 것입니다. 이렇게 하면 앱이 예상보다 늦게 제공될 수 있으며 앞으로 더 많은 코드를 유지 관리해야 하며 코드가 많을수록 잠재적으로 더 많은 버그가 발생할 수 있습니다.
앱 최적화에 시간을 할애하기 전에 앱이 이미 부드러운지 또는 실제로 최적화해야 하는 응답하지 않는 부분이 있는지 자문해 보십시오. 이미 매끄러운 앱을 최적화하여 0.01초를 단축하는 데 많은 시간을 소비하는 것은 가치가 없을 수 있습니다. 시간을 더 나은 기능이나 다른 우선 순위를 개발하는 데 더 잘 사용할 수 있기 때문입니다.
추가 리소스
- "맛있는 iOS 아이캔디 모음", Tim Oliver, Tokyo iOS Meetup 2018(비디오)
- "iOS에서 동시 사용자 인터페이스 구축", Andy Matuschak, WWDC 2012(비디오)
- "출시 동안 앱의 UI 유지" Apple,
- "동시성 프로그래밍 가이드: 디스패치 큐", 문서 아카이브, Apple
- "메인 스레드 검사기", Apple