iOS 앱 개발 중 앱이 예상치 않게 멈추거나 강제 종료되는 문제는 종종 발생할 수 있습니다.
이러한 문제의 원인은 메모리 부족, 메모리 누수, 교착 상태, 경쟁 상태 등 다양합니다.
이런 상황에서는 프로파일링과 디버깅이 필요합니다. 에러가 로그를 통해 나타나면 로그 분석으로 문제를 해결할 수 있지만, 로그 없이 문제가 발생하는 경우 프로파일링을 통해 메모리 사용량, 성능 이슈, 실행 흐름 등을 분석하여 문제의 원인을 정확히 파악하는 것이 가장 효과적입니다.
1. XCODE 프로파일링
XCODE 프로파일링이란 앱 성능 및 리소스 사용을 분석하고 최적화하는 과정을 뜻합니다. 앱이 실행하는 중에 발생하는 이슈에 대해서 모니터링 할 수 있고 메모리누수, 에너지효율성, 메모리할당 과정 등을 전반적으로 훓어볼 수 있습니다. 단순 출력으로는 잡아내기 힘든 성능 이슈나 메모리 부분을 주로 관찰 및 분석합니다.
디버깅과 차이점은 디버깅은 내가 원하는 위치나 소스 쪽을 집중적으로 관찰하는 것이고 프로파일링은 앱이 시작할 때부터 끊임없이 전체적인 과정을 살펴보는 것입니다. 2가지 방법 전부 앱을 분석하는데 알고 있는 것이 좋으며 XCODE에서 참 잘해놓은 기능이라고 생각합니다.
1. XCODE에서 프로파일링을 시작하는 법은 XCODE -> Product 메뉴 -> Profile 선택 해줍니다.
2. 선택해준다면 아래와 같은 창이 뜹니다. 어떤 것을 위주로 프로파일링을 할 것인지 요소를 선택하는 것입니다.
대표적으로 자주 사용하는 것만 설명하자면 Allocation, Leaks, Time Profiler, Energy Log 등입니다.
Allocation : 앱의 메모리를 추적하고 메모리 누수, 사용 후 해제되지 않은 객체 등의 문제를 찾아내는데 좋습니다.
Leaks : 메모리 누수를 경고표시로 감지하고 원인을 추적합니다. 소스까지 표시되어서 아주 유리합니다.
Time Profiler : 앱의 CPU 사용량과 실행 시간을 분석하여 병목 현상을 찾고 성능을 개선합니다.
Energy Log : 일반 디버그할 때 대략적으로 표시되긴 하지만 이 요소에는 에너지 사용량을 측정하고 에너지 효율을 개선하는데 필요한 정보를 줍니다.
2. Allocation, Leaks
Allocation은 유독 이번에 제대로 쓰이기도 하고 자주 쓰기도 하는 요소라서 자세히 설명드리려고 합니다.
Allocation 그래프
앱을 Profile -> Instrument로 실행한다면 다음과 같은 화면이 뜹니다. Allocation은 처음부터 (앱이 시작할 때) 메모리가 할당되는 것을 볼 수 있고 점차올라가서 대기상태일 때는 수평선인 것을 볼 수 있습니다.
마우스 커서를 그래프에 가져간다면 현재 메모리가 몇메가 할당되어 있는지 확인할 수 있습니다. 이 메모리는 Heap에 쌓이는 메모리며 너무 가파르게 올라가거나 껐는데도 떨어지지 않는다면 문제가 있는 것입니다.
너무 그래프가 확 올라간다면 올라간 그래프 부근을 클릭하면 그 부분에서 메모리가 어떻게 할당되었는지 정보를 알 수 있습니다. 그 정보를 통해 내가 작성한 코드로 들어가볼 수 있는 것이고 메모리해제가 되어있지 않은 부분을 확인할 수 있습니다.
현재 그래프에서 잡은 곳은 힙에 지속적으로 소모되는 메모리가 49.26 MiB 잡혀있습니다. 그리고 현재 사용된 메모리의 양은 270.62 MiB 입니다. 생각보다 앱 초반인데도 불구하고 조금 많이 잡고 넘어가는 편입니다. 쌓이는 메모리보다는 메모리 증가율을 보시는게 근본적인 오류를 해결하는데 도움이 됩니다.
다음은 Leaks를 살펴보겠습니다. 앞서 설명한 것과 같이 메모리누수에 대해서 필요한 정보를 알려줍니다.
Xcode Profile Leaks 표시 및 상세정보
Leak과 함께 Allocation을 선택한다면 위와같은 그래프, 표시를 볼 수 있습니다. 초록색은 Leak이 생기지 않았다는 뜻이며 빨간색 X는 메모리 누수가 발생했다는 뜻입니다. 그것을 클릭하면 어디서 몇개 발생했는지 쓰레드의 주소가 나오고 Responsile Frame이라고 이 소스를 Trace할 수 있습니다.
Xcode에서 만들어놓은 Profile의 제대로 된 사용성을 Leaks을 통해 느꼈습니다. X라고 표시된 메모리 누수는 꼭 디버깅하거나 코드를 찾아서 잡아줘야합니다. 크기도 얼마나 되는지 제대로 알려줍니다. 현재 갯수가 몇개되지 않아서 괜찮아보이지만 반복되다보면 메모리 누수가 심해지면서 해제도 되지않고 앱이 죽게 됩니다.
3. XCODE 디버깅, Pause
Xcode에서도 프로파일링으로 잡아야하는 버그가 있고 디버깅으로 잡아야하는 버그가 있습니다. 순서에 관한 것과 부분적인 버그의 경우 로그출력이나 Break Point로 잡아도 되지만 멀티쓰레드 문제에서는 디버그 후 Pause 방법이 좋습니다. 앱을 순식간에 멈춰서 현재 상태의 쓰레드 상황을 살펴봅니다.
앱을 실행시킨 후 Debug -> Pause 눌러줍니다. 앱을 무조건 실행시켜야 이 버튼이 활성화됩니다.
이렇게 눌렀다면 왼쪽에 현재 앱에서 실행되는 쓰레드가 전부 보입니다. 어느 곳에서 어떤 기능을 하고 있는지 보입니다. 그리고 현재 소스와 접근하는 부분도 보이며 제가 원하는 큐에 들어가 있는 쓰레드도 보이게 됩니다.
보면 사용되는 쓰레드가 엄청나게 많습니다. 이 중에서 쓸모있는 값은 나의 코드에서 현재 작동하고 있는 부분, 걸리고 있는 부분을 살펴보는 것이 중요합니다. 저번 포스팅에서 겪었던 쓰레드 동기화에서 어디 꼬인 곳이 있는지 그 부분을 중점적으로 풀어주는 부분이 중요합니다.
Debug -> Pause를 하는 시점은 앱이 실행도중 정지되었을 때나 갑자기 느려졌을 때 해보는 것이 좋습니다. 강제종료되면 에러가 확실하게 뜨면서 원인이 분명한 경우로 쉽게 버그가 잡힙니다. 하지만 앱이 화면이 멈춰버린다거나 느려지는 부분이 있다면 어디에서 리소스 과부하, 데이터 과부하, 성능 저하가 되었는지 찾아야 합니다.
iWaver 메모리 누수
프로파일링을 통해 메모리 누수 발생 확인
1. 메모리 누수 상황 1 - NSImage메모리 해제 안됨
원인 파악
참고
Allocation: 앱의 한 사이클을 다 돌고 난 후에도 CGImage가 VM영역에서 해지되지 못함
Leak: 여러가지 원인으로 메모리 누수 발생 가장 큰 부분을 차지 하는 것은 Image및 Event리스너
VM Tracker: Swift의 메모리 관리에 따라 자동으로 Dirty Size를 조절하여주고(Dirty Size감소 및 동시에 Swapped Size 증가) 이로 인해 Resident Size의 크기를 관리해주는 것을 확인
→ 이미지를 VM영역에서 해지해주어야 한다.
오류 분석
Xcode에서 제공해주는 프로파일링 툴인 Instrument를 통해 NSImage가 메모리에서 해지가 되지 않는 오류를 파악하였고, iWaver 전체 코드를 분석하여 NSImage를 사용하는 코드를 하나하나 주석처리해가며 디버깅을 진행해보았습니다.
뷰 단위로 NSImage를 저장하는 Model인 pdfImageVM을 사용하는 코드를 주석처리 후 계속 Run해보며 메모리를 관찰하여보았습니다.
현재, ImageListArea에서 filter를 사용하여 뷰에서 이미지를 보여주는 부분에서 메모리가 해제가 안되는 것을 파악하였습니다.
// MARK: 이게 원인!!!! 메모리 해제가 안됨❗️
selectedPDFImages = pdfImageVM.pdfImages.filter { pdfImages in
pdfImages.PDFId == selectedPDFFileID
} // id가 일치하는 PDFImages모델의 image들을 보여줌
메모리 비교
한 사이클을 모두 돈 후, 처음 뷰로 이동하면 메모리가 해제되지 않아 계속 쌓이는 것을 확인할 수 있음.
주석 후 - 3번의 사이클 진행
한 사이클을 모두 돈 후, 처음 뷰로 이동하면 메모리가 자동으로 해제되어 감소하는 것을 확인할 수 있음
오류 수정
2. 메모리 누수 상황2 - NSEvent
메모리 누수 가장 심각
// MARK: ❗️메모리 누수❗️
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didUpdateNotification), perform: { _ in
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (event) -> NSEvent? in
if !shouldEnableTabKey && event.keyCode == 48 { // Tab key's keyCode is 48
return nil // Ignore Tab key press
}
return event
}
})
3. 메모리 누수 상황 3 - NSEvent, 기타 변수 할당
.onAppear {
/// shift, cmd키가 press되었는지 check
//MARK: ❗️메모리 누수❗️
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { event -> NSEvent? in
if event.modifierFlags.contains(.shift) {
isShiftKeyPressed = true
} else {
isShiftKeyPressed = false
}
if event.modifierFlags.contains(.command) {
isCmdKeyPressed = true
} else {
isCmdKeyPressed = false
}
return event
}
/// 이미지 로딩 관련
selectedSubviews = [] // 선택된 영역 초기화
selectedPDFFileID = pdfFileVM.pdfFile[selectedPDFFileIndex].id // 선택된 PDF의 이미지 //MARK: ❗️메모리 누수❗️
/// 현재 pdf의 이미지 시작 index
for ImageIndex in 0...pdfImageVM.pdfImages.count-1 {
if pdfImageVM.pdfImages[ImageIndex].PDFId == selectedPDFFileID {
startImageIndex = ImageIndex
break
}
}
print("startImageIndex: \(startImageIndex)")
selectedPDFImages = pdfImageVM.pdfImages.filter { pdfImages in
pdfImages.PDFId == selectedPDFFileID
} // id가 일치하는 PDFImages모델의 image들을 보여줌
/// 이미지 height값 구하기
var maxImageHeight = 0.0
var maxImageWidth = 0.0
for i in 0..<selectedPDFImages.count {
maxImageHeight = max(maxImageHeight, selectedPDFImages[i].image.size.height)
maxImageWidth = max(maxImageWidth, selectedPDFImages[i].image.size.width)
}
/// 이미지 offset관련
let row: Int = (selectedPDFImages.count % 3 == 0) ? (selectedPDFImages.count / 3) : (selectedPDFImages.count / 3 + 1)
// 축소 비율: maxImageWidth/64 128/64 -> 2배만큼 ..
scrollHeight = Double(row * (90+gap))
//print("scrollHeight: \(scrollHeight)")
imageRects = []
for i in 0..<row { // 행
for j in 0..<3 { // 열
imageRects.append(CGRect(x: j*(Int(64)+space), y: i*(90+gap), width: 0, height: 0))
}
}
}// onApppear
참고
https://commnetall.tistory.com/127
https://velog.io/@kirri1124/iOS-Memory-Deep-Dive-Part-2
앱 만들다가 발생하는 메모리 누수 발견하는 방법 (그래프)
https://youtu.be/b2AgibUg47k?si=yVzvXO-CWAsnXa9V
'SwiftUI > 개발' 카테고리의 다른 글
[SwiftUI] UI 최적화 문제 (1) | 2024.01.12 |
---|---|
[SwiftUI] 코드 중복 실행 문제 (0) | 2023.11.01 |
[Swift] 이미지 저장 기능 (0) | 2023.10.31 |
[Swift] 폴더 이름 중복 검사 (1) | 2023.10.28 |
[SwiftUI] View가 멈추는 현상 (0) | 2023.10.25 |