(WWDC 2021) 앱의 차단 이해 및 제거

걸려있는 것은?

하나

상품 영역을 터치했는데 상세 화면으로 이동하는 데 예상보다 훨씬 오래 걸렸습니다.

앱이 정지되고 터치가 작동하지 않았습니다.

2

이 경험은 지연, 부진, 멈춤으로 설명될 수 있으며 앱 사용 경험을 설명하기에는 그리 좋은 단어가 아닙니다.


Apple은 이러한 무응답 기간을 중단(hang)이라고 합니다.

메인 런루프

종료를 이해하려면 앱의 기본 runloop가 무엇인지 알아야 합니다.

삼

메인 스레드가 사용자 상호 작용에 대한 응답으로 이벤트 핸들러를 실행하기 위해 들어가는 루프를 메인 스레드 루프라고 합니다.


사용자가 앱과 상호 작용할 때 실행 루프는 이벤트를 수신 및 처리한 다음 필요에 따라 UI 업데이트를 수행합니다.


이 프로세스는 각 사용자 입력에 대해 반복됩니다.

4

이벤트 처리 시간이 오래 걸리면 사용자 입력과 UI 업데이트 사이에 지연이 발생합니다.


또한 중단 중에 발생하는 이벤트는 버퍼링되어 메인 스레드에서 처리할 수 없으므로 현재 중단된 작업이 완료될 때까지 다음 이벤트가 처리되지 않는 문제가 발생합니다.


정전의 원인은 무엇입니까?

중단의 원인

5

주 스레드에서 너무 많은 작업이 수행되면 중단이 발생합니다.


원인을 찾으려면 이벤트를 처리하는 동안 메인 스레드가 무엇을 하고 있는지 확인해야 합니다.

6

근본 원인은 메인 스레드가 작업으로 바쁜 경우와 메인 스레드가 다른 스레드나 시스템 리소스에 의해 차단되는 경우로 나뉩니다.

메인 스레드가 사용 중입니다.

불필요한 작업을 예방적으로 수행

78일

메인 스레드는 표시된 화면을 제외한 모든 이미지를 한 번에 받으면 이미지를 읽고 준비하고 합성하는 데 많은 시간을 소비합니다.


4개의 이미지만 화면에 표시되기 때문에 대부분의 작업은 사용자의 시야에 영향을 미치지 않습니다.


사용자가 보는 이미지만 작업하고 필요에 따라 나머지 작업을 수행하자!


메인 스레드에서 메인 스레드와 관련되지 않은 작업 수행

메인 스레드는 메인 디스패치 큐에서 블록을 실행하지만 블록을 다른 큐로 디스패치하거나 실행을 위해 다른 큐에서 블록을 받을 수도 있습니다.

maintenanceQueue.sync { loadRecent() }

9

메인 스레드가 동기화를 위해 다른 큐의 블록을 디스패치할 때 메인 큐의 블록을 실행하기 전에 해당 큐에서 대기 중인 모든 블록이 실행될 때까지 기다려야 합니다.


메인 스레드가 수행하는 작업에 필요한 시간은 전체 시간의 일부에 불과하지만 대기 중인 작업을 기다리는 동안 실제로 중단이 발생할 수 있습니다.

DispatchQueue.main.sync { loadAll() }
DispatchQueue.main.async { loadAll() }

또한 메인 스레드에서 실행할 필요가 없는 다른 큐의 작업을 메인 큐로 전달하면 중단이 발생할 수 있습니다.


작업에 적합하지 않은 API 사용

작업을 수행하는 방법에는 여러 가지가 있으며 수행하려는 작업에 가장 적합한 API를 사용해야 합니다.

1011

샘플 앱에서 이미지는 비트맵으로 변환되고 제품 이미지에 둥근 모서리를 추가하기 위해 직접 처리됩니다.


이미지를 비트맵으로 변환하고 처리하려면 많은 CPU와 메모리가 필요합니다.


이것은 작업에 잘못된 하드웨어가 사용되고 있음을 나타내며 CPU 집약적 API 대신 GPU를 사용하는 CoreAnimation의 CALayer를 사용하여 더 적은 리소스로 둥근 모서리를 쉽게 추가할 수 있습니다.

메인 스레드 잠김

메인 스레드에서 값비싼 동기식 API 사용.

동기식 API는 호출된 시간부터 결과를 반환할 때까지 스레드를 차단합니다.


API가 많은 작업을 수행하거나 오랫동안 차단될 가능성이 있는 경우 메인 스레드에서 사용해서는 안 됩니다.


전형적인 예는 메인 스레드에서 동기식 네트워크 요청을 수행하는 것입니다.

작업이 얼마나 걸릴지 알 수 없기 때문에 메인 스레드에서 이러한 동기식 작업을 수행하지 마십시오!


다음과 같은 시스템 리소스 액세스 로직을 실행합니다.

B. 메인 스레드에서 파일 I/O.

시스템 리소스에 액세스할 때 논리의 대기 시간, 예: B. 파일 I/O는 하드웨어 사양 또는 동시 실행 중인 다른 스레드의 읽기/쓰기 작업에 따라 달라지며 앱의 제어 범위를 벗어날 수 있습니다.


동시성을 지원하지 않는 데이터 저장소는 특히 문제가 됩니다.

12

쓰기가 이미 진행 중인 동안 메인 스레드가 읽기를 시도하는 경우 읽기 작업은 모든 현재 쓰기가 완료될 때까지 기다려야 합니다.


메인 스레드에서 파일 I/O 로직을 사용하지 마세요!


메인 스레드에서 잠금 및 세마포어와 같은 동기화 사용

13

기본적으로 동기화 기능은 실행을 차단할 수 있으므로 메인 스레드에서의 동기화는 제한되어야 합니다.


@synchronized, dispatch sync, os unfair lock, posix lock(NSLock) 다음과 같은 동기화 기능을 사용할 때 주의하십시오.


작업, IPC(프로세스 간 통신), 시스템 리소스를 사용하여 드물게 변경되는 값의 주기적 폴링

샘플 앱에는 사용할 때마다 연락처를 폴링하는 소셜 기능이 있어 불필요한 오버헤드가 발생합니다.


연락처와 같은 값은 자주 변경되지 않기 때문에 매번 쿼리할 필요가 없습니다.


필요할 때만 실행되도록 이러한 리소스 집약적인 작업을 구현해 봅시다!

리소스 제한

15

중단은 CPU, 메모리 및 저장소와 같은 시스템 리소스의 상태에 따라 크게 달라집니다.


앱이 배포될 때 앱에서 사용되는 하드웨어 및 환경은 개발 단계의 테스트 시나리오와 매우 다릅니다.


앱에서 지원하는 가장 오래된 기기를 벤치마크로 사용하여 오류를 테스트하는 것이 중요합니다!

끊김 진단 방법

이제 실패의 원인을 알았으니 개발 및 프로덕션 서비스 중에 앱의 실패를 모니터링하고 분류하는 방법을 알아봅시다!

중단을 분석하려면 해당 시간 동안 앱이 수행한 작업을 알아야 합니다.


Time Profiler는 실행 중인 항목과 시간 경과에 따른 앱의 호출 스택을 정확하게 보여주므로 중단 중에 앱이 무엇을 하고 있었는지 알 수 있습니다!

또한 시스템 추적을 통해 시스템 호출, 가상 메모리(VM) 오류, I/O 및 프로세스 상호 작용에 대한 추가 데이터를 볼 수 있습니다.

자세한 내용은 WWDC 2016의 깊이 있는 시스템 추적을 참조하십시오!

https://devstreaming-cdn.apple.com/videos/wwdc/2016/411jge60tmuuh7dolja/411/411_hd_system_trace_in_depth.mp4
(세션이 공식사이트에서 사라진듯..)

깊이 파고들다

개발 단계

16

중단을 감지한 직후의 Instruments 화면입니다.


상단의 시스템 추적 창에서 빨간색 선은 시스템 호출을 나타내고 보라색 그래프는 가상 메모리 오류를 나타내며 파란색 막대는 기본 스레드가 작업을 실행 중임을 나타냅니다.


그리고 아래의 시간 프로파일러를 사용하면 4.7초 동안 일시 중지되는 동안 메인 스레드에서 호출 스택 트리를 볼 수 있습니다.


트리의 강조 표시된 부분은 4.7초 중단 중 4.6초를 보여줍니다.

DetailDrinkViewController~에서 loadAllImages() 방법임을 보여줍니다.


아마도 필요한 것보다 더 많은 이미지를 메인 스레드에 로드하고 있을 것입니다!


운영 서비스 단계

앱이 출시된 후 MetricKit을 사용하여 중단 호출 트리를 수집할 수 있습니다.

자세한 내용은 WWDC 2020에서 MetricKit의 새로운 기능을 참조하세요.
https://developer.apple.com/videos/play/wwdc2020/10081/

17

호출 트리에 강조 표시된 메서드를 보면 방금 Instruments로 조사한 것과 다른 문제인 것 같습니다.


방법 이름으로 _dispatch_sync_f_slow스레드는 연락처 폴링 논리에서 동기식 디스패치를 ​​수행하여 차단된 것으로 간주됩니다.


MetricKit이 없었다면 이 문제를 발견하지 못했을 것입니다!

Xcode 정리함

중단 문제를 해결할 때 명확한 기준을 설정하고 앱의 성능을 정량화하는 것이 중요합니다.


Xcode Organizer는 앱 버전별 중단률을 보여주는 차트 및 성능 지표를 제공합니다.

자세한 내용은 WWDC 2019의 배터리 수명 및 성능 개선 및 WWDC 2021의 앱에서 전력 및 성능 저하 진단을 참조하세요!

https://developer.apple.com/videos/play/wwdc2019/417/
https://developer.apple.com/videos/play/wwdc2021/10087/

막힘을 제거하는 방법

이제 막힌 앱을 없애자!

중단을 없애려면 메인 스레드의 작업량을 줄여야 합니다.

  • 실행 시간을 줄이기 위해 메인 스레드에서 수행되는 작업 최적화
  • 논블로킹 방식으로 작업을 다른 스레드로 이동하여 메인 스레드에서 답변을 유지합니다.

이것은 두 가지 방법으로 해결할 수 있습니다.

메인 스레드 작업 간소화

은닉처

캐시는 자주 사용하는 자산이나 이전에 쿼리한 값에 빠르게 액세스할 수 있는 좋은 방법입니다.


메모리에 있는 경우가 많지만 필요한 경우 디스크 캐싱을 사용할 수도 있습니다.


이미지와 같은 형식이 지정된 자산은 주문형으로 생성하는 데 비용이 많이 들기 때문에 캐싱의 좋은 예입니다!

19

제품 이미지를 NSCache에 캐싱하여 샘플 앱에 로드하면 이미지 생성 로직을 빠른 메모리 판독으로 대체하여 이미지 로드 중단을 제거할 수 있습니다!

그러나 캐시 논리를 구현할 때 부실 데이터와 캐시 업데이트 간의 균형을 맞추기 위해 캐시 무효화 메커니즘을 구현해야 합니다.


그리고 메인 스레드는 항상 이벤트에 응답할 수 있는 상태를 유지해야 하므로 캐시 무효화 논리는 다른 디스패치 큐에서 비동기적으로 실행되어야 합니다.


알림 관찰자

알림을 잘 사용하면 광범위한 계산 논리를 수행하지 않고도 앱의 상태 변경에 대응할 수 있습니다.

Apple에서 제공하는 시스템 알림 목록 NSNotification.Name 공식 문서에서 확인할 수 있으니 시간 날 때 한 번 보도록 합시다!

20

abDatabaseChangedExternally 알림을 위해 감시자를 등록하면 메인 스레드가 연락 요청을 기다릴 필요가 없습니다.


알림을 받으면 관찰자가 트리거되고 캐시된 값이 업데이트됩니다.


다시 말하지만 메인 스레드의 응답성을 유지하려면 작업을 다른 대기열로 비동기식으로 전달해야 합니다.


MetricKit에서 본 멈춤 현상이 이제 수정되었습니다!

기본 스레드에서 작업 이동

중단을 제거하는 또 다른 방법은 메인 스레드에서 작업을 옮기는 것입니다.


모든 뷰와 뷰 컨트롤러는 메인 스레드에서 생성, 수정 및 소멸되어야 하며 일반적으로 UI에 중요한 정보를 제공하는 작업도 메인 스레드에서 수행되어야 합니다.


그러나 UI 요소를 업데이트하는 데 필요한 계산은 완료 핸들러를 사용하여 다른 스레드로 전달할 수 있습니다.

이 핸들러는 주 스레드에 실제 업데이트를 수행할 시기를 알려줍니다.


덜 중요한 유지 관리 또는 시간이 중요하지 않은 작업은 다른 스레드에서 비동기적으로 수행되어야 합니다.


커밋된 작업은 더 낮은 스케줄링 우선순위로 실행되며 메인 스레드에서 실행되는 작업보다 시간이 더 오래 걸릴 수 있지만 이것은 설계에 의한 것이며 메인 스레드는 필수 작업만 수행해야 한다는 생각을 반영합니다.

이제 어떻게 메인 스레드에서 다른 스레드로 작업을 넘겨줄 수 있는지 봅시다!

비동기 API

메인 스레드에서 비동기 작업을 수행하는 가장 쉬운 방법은 제공된 비동기 API를 사용하는 것입니다.

21

대부분 프레임워크는 동기식 API에 매핑된 비동기식 API를 제공합니다.


비동기 API는 함수 이름에서 비동기로 표시되거나 완료 핸들러가 있습니다.


GCD

메인 스레드에서 커밋하려는 작업이 비동기 API를 제공하지 않거나 자신이 작성한 코드가 자신이 작성한 것이라면 GCD를 사용할 수 있습니다!

secondaryQueue.async { update() } 

22

작업 블록을 다른 스레드에서 비동기적으로 실행하려면 해당 블록을 다른 디스패치 대기열에 비동기적으로 디스패치할 수 있습니다.


그리고 완료 시점에 수행할 작업을 메인 큐에 다시 게시함으로써 실제로 메인 스레드에서 작업할 때 알림을 받을 수 있습니다.

23

프리페치 로직은 GCD로 쉽게 구현할 수 있습니다.


prefetchQueue비동기로 전송하여 미리 데이터를 수신하고 수신된 데이터가 필요할 때 동기화를 전송하여 작업이 완료될 때까지 기다릴 수 있습니다!

GCD에 대해 자세히 알아보려면 WWDC 2017에서 Mordernizing Grand Central Dispatch Usage를 확인하십시오!

https://developer.apple.com/videos/play/wwdc2017/706/

타협 이해

지금까지 제시된 솔루션에는 몇 가지 장단점이 있습니다.

24

  • 캐시는 메모리를 사용하기 때문에 메모리가 크게 증가하지 않도록 캐시의 크기를 지정해야 하며 값이 오래되지 않도록 무효화 논리가 올바르게 구현되었는지도 확인해야 합니다.

  • 알림을 모니터링할 때 알림이 트리거되는 빈도를 고려하는 것이 중요합니다.

    알림을 처리하기 전에 필터링 논리를 추가하여 CPU 사용량을 줄일 수 있습니다.

  • 비동기 API를 사용하면 운영체제가 자동으로 비동기 작업의 우선 순위를 낮은 수준으로 설정하므로 사용자 인터페이스 업데이트에 작업이 필수적인지 먼저 확인한 다음 작업을 비동기화할지 여부를 결정해야 합니다.

    .
  • CGD를 사용한다는 것은 작업 수행 순서를 변경할 수 있음을 의미합니다.

    앱이 중단되지 않도록 하려면 어떤 작업을 언제, 어떤 순서로 수행해야 하는지 항상 명확해야 합니다.

중단이 사용자 경험에 미치는 해로운 영향과 비교할 때, 이는 절충할 가치가 있습니다!

나누기를 제거할 때 고려해야 할 사항

  • Apple의 프레임워크 및 API 사용. 이미 다양한 장치와 호환되며 뛰어난 성능을 가지고 있으며 더욱 효율적이 되도록 지속적으로 업데이트되고 있습니다.

  • 코드 개선의 지속적인 반복. 편집할 때 변경 사항의 효과를 볼 수 있습니다.

  • 시스템 리소스를 사용할 때 꼭 필요한 것만 사용하세요. 필요 이상으로 많은 리소스를 사용하면 앱의 성능이 느려질 뿐만 아니라 다른 시스템에도 영향을 미칠 수 있습니다.

결론

  • 앞으로 Xcode Organizer를 사용하여 앱의 성능 기준을 설정하십시오.
  • 중단으로 이어질 수 있는 안티 패턴을 염두에 두고 설계
  • MetricKit을 사용하여 고객 문제의 우선 순위를 지정하고 시간 프로파일러 및 시스템 추적을 사용하여 문제를 진단합니다.

ㅤ• 지금까지 설명한 방법을 사용하여 발견한 끊김을 제거하세요!

스크린샷 2022년 10월 31일 3 42 42

놀아줘서 고마워!

참조