본문 바로가기
IOS🍎/WWDC

[WWDC 2021] Make blazing fast lists and collection views

by Jouureee 2022. 5. 27.

전에 정리해둔 글인데 .. advances in collectionview layout을 보기 전에 개선된 collectionView lifecycle를 복습하고자 글을 쓰게 되었다. 이번 2022 wwdc 뭐가 나올지 정말 궁금하다 ..!

https://developer.apple.com/videos/play/wwdc2021/10252/?time=1251 

 

Make blazing fast lists and collection views - WWDC21 - Videos - Apple Developer

Build consistently smooth scrolling list and collection views: Explore the lifecycle of a cell and learn how to apply that knowledge to...

developer.apple.com

 

Performance fundamentals

Diffable data source와 cell registration과 같은 API를 사용할때 strong foundation으로부터 시작하는 방법을 배울 것입니다. collectionviewcell의 lifecycle을 다시 한번 복습할 것입니다.

 

Cell perfecting

그리고 완벽하게 scolling이 smooth하게 이루어지지 않는 이유에 대해 얘기할 것이며 prefetching에서의 개선에 대해 이야기 할 것입니다.

 

Updating cell content

마지막으로 비동기적으로 content를 호출할때 cell을 업데이트 하는 방법에 대해 설명할 것입니다.  또한 모든 디바이스에서 best scolling 성능을 얻기 위해 새 UIImage API를 사용하는 방법에 대해 설명할 것입니다.

 


 

자 이제 샘플 앱을 봐봅시다. 화면에 DestinationPost struct로 post들이 list 되어 있습니다. Diffable data source는 model 그 자체가 아닌, model의 item마다 각각 identifier를 저장하고 있도록 합니다. 따라서 샘플 앱에서 DestinationPost 자체가 아니라 ID 속성을 사용하여 diffable data source가 채워집니다. 

snapshot을 생성하고 main section을 채운 뒤, snapshot에 모든 itemIdentifier를 채웁니다. 만약 이 중 하나의 post 속성이 변한다면, itemIdentifier는 변화하지 않았기 때문에, diffable data source안에 나타내지는 것은 stable하게 유지됩니다. 그리고 apply()를 적용합니다. 

 

iOS 15 이전에 애니메이션 없이 snapshoot을 apply하는 것은 내부적으로 reloadData()하는 것으로 여겨졌습니다. 이것은 collectionview가 모든 cell을 discard하고 재 생성하기 때문에 좋은 성능이 아니지요. iOS 15이후부턴 애니메이션 없이 snapshoot을 apply하는 것이 오직 변화한곳에만 적용 할수 있게 되었습니다. (박수 짝짝짝) 또한 iOS 15부터 diffable data source는 reconfigureItems 메소드가 새로 생겼는데 이것은 visible cell의 내용을 쉽게 업데이트 할 수 있도록 만듭니다. 

 

우선 data source로부터 data를 cell에 표시해봅시다. Cell registration은 각 type의 cell을 한 곳에 보관할 수 있는 좋은 방법이며, 이를 통해 diffable data source의 identifier에 접근이 편리해집니다. 

UICollectionview는 registration의 각 instance에 reuse queue를 유지하여 cell의 각 타입에 대해 오직 한번만 registration을 생성하는 것을 보장합니다.

여기 샘플 registration이 있습니다. 
생성한 CellRegistration으로 UI에 데이터를 주입합니다.

 

registration을 사용하기 위해서 datasource의 cell provider에 dequeueConfigureReusableCell을 호출합니다. 

registration이 cell provider 밖에서 생성 되었지만 내부에서 사용되는 점에 주목하세요. 이것은 성능에서 중요합니다. 

Provider 안에서 registration을 생성하는 것은 collectionview가 cell을 재사용하지 않겠다는 것을 의미하기 때문 !!

cell의 생명주기는 두단계로 이루어집니다 : Preparation, display
Preparation
cell을 prefetching하는 것, collectionview가 cell이 필요로 될때 datasource에게 요청하는 작업을 수행한다. Diffable datasource라면, cell provider를 운영해 result를 반환합니다. 

 

cell provider가 운영될때 collectionview는 registration을 사용해 새로운 cell을 dequeue하도록 요청합니다. 만약 cell이 reuse pool에 존재한다면, collectionview는 prepareForReuse를 호출하여 cell을 dequeue 할것입니다.

 

reuse pool이 비어있다면, 새 cell을 생성 할 것. registration의 configuration handler로 지나게 될 것입니다.

setup이 끝나고 Configured cell이 collectionview로 오면,
Layout attribute와 size property를 설정하게 됩니다. 

 

willDisplayCell은 delegate에 위임하여 메서드가 호출되어 cell이 collectionview에서 visible할수 있게 됩니다. 
여기까지 flow는 cell이 visible될 동안 작업에는 변함이 없습니다.

 

 

화면을 scroll 하여 화면 밖을 벗어나는 경우, didEndDisplayingCell이 호출됩니다. 보여주기가 끝난 cell로 reuse pool로 바로 돌아갑니다. reuse pool에서 cell을 다시 dequeue에서 빼낼 수 있으며 이 flow를 반복합니다.

 

데모로 flow들을 다시 살펴봅시다. scroll하면 부드럽게 scolling 되지 않습니다. 이것을 “hitch”라고 합니다.

무엇이 hitch를 발생시키는지 알아봅시다.
One frame 속, cell에 대해 touch와 같은 이벤트가 전달되면, 그것에 대한 reponse로 layer와 view의 속성을 업데이트 합니다. 
예를 들어 scrollview의 content offset는 모든 뷰에 대해 on-screen location을 변경하면서,  pan gesture동안 변경될 것입니다. 따라서 앱은 view와 layer에 대해 layout을 수행합니다. 이런 process를 commit이라고 합니다. 그러면 layer tree는 render server에 보내질 것입니다. 각 frame은 모든 commit이 끝나져야 하는 commit deadline을 가집니다. 

앱이 각 frame에 대해 commit해야 하는 시간은 display의 refreshing 빈도에 따라 다릅니다.

 

 

collection view 또는 tableview 속 cell 리스트를 스크롤링하는 전형적인 예시입니다. 새로운 cell이 visible되면, 새 cell이 configure되고 layout 되어야 하는 동안의 longer commit을 가집니다. 

 

그리고 화면을 이동하면서, 오직 존재하기만 하는 cell이 있는 frame이 있습니다. 새로운 cell이 필요로 되지 않기 때문에, 이 frame을 위한 commit은 매우 빠릅니다. 

 

결국 scroll 위치가 변경되어 새 cell이 visible되고 이 패턴이 반복됩니다. 그러면 데모에서 봤던 hitch는 어떻게 발생하는 걸까요 ?
frame의 commit이 너무 오래 걸려 deadline을 놓치면, 의도된 frame내에 업데이트가 포함되지 않을 것입니다. 따라서 화면은 commit이 끝날때까지 여전히 화면에 예전 frame을 보여줄 것이고, 이것은 frame이 render되는데 딜레이를 유발합니다. 

이것은 커밋 장애이며 스크롤할 때 일시적인 중단으로 인식됩니다.
이것에 대해 더 자세히 살펴보고 싶다면 https://developer.apple.com/videos/play/tech-talks/10855 를 참고하세요

 

이러한 hitch 문제를 해결하기 위해서 iOS 15에서 UICollectionview와 UITableview에 새롭게 cell을 prefetching하는 메커니즘을 소개합니다.

다시 예시로 돌아와서 expensive cell을 봅시다. 여기서의 요점은 매 frame마다 cell을 필요로 하지 않는다는 점입니다. 보시면 매우 짧은 commit 타임을 가지는 frame이 있습니다. 

 

이 짧은 commit 타임을 가지는 frame을 활용해 다음 cell을 바로 준비하도록 마련할 것입니다. 
cell이 필요해질 시점에, visible하게 하면 됩니다. 

 

prefetch된 cell이 visible될때, frame에 commit이 매우 빠르게 진행되는 이유입니다. cell을 prefetcing하는데 쓰이는 많은 시간은 hitch를 발생하는 시간과 같습니다. 하지만 우리는 head start를 얻을 수 있기 때문에 hitch를 피할 수 있습니다. 

왜 그럴수 있는지 살펴보겠습니다. 
prefetcing하기 전에 각 frame에 commit을 수행하였습니다. cell이 필요로 되지 않기 때문에 빠른 시간에 commit이 끝났고 deadline까지 많은 시간이 남았습니다. iOS 15 시스템에서는 이러한 상황을 인지하고 다음 cell을 prefetching하기 위해 여유 시간을 활용합니다. 

 

 

다음 frame을 봅시다. cell을 prefetching하는 비용은 비싸기 때문에 실제보다 늦게 commit이 이루어집니다. commit이 늦게 시작한다 한들, deadline안에 끝나므로 문제가 되지 않습니다. 

 

전 메커니즘과 비교해봤을때, prefetching 모든 commit이 deadline을 잘 지킴을 볼 수 있습니다. 

 

 

Cell prefetshing 정리 !!

Cell prefetching이 cell life cycle에 어떻게 영향을 끼쳤는지 살펴봅시다. 
cell을 prefetching하기 위해선 prefetch하는 단계에서 완전히 cell이 인지되어야 합니다. 

 

 

cell이 prefetch된 후, cell이 display되길 기다리고 있는 preparedCell state가 추가로 생겼습니다. 이러한 변경사항으로 앱 내 중요한 고려사항 2가지가 있습니다.

사용자가 갑자기 scroll 방향을 변경하면 prepared cell이 표시되지 않을 수 있습니다. 그런 다음 cell이 display되면 화면이 꺼진 후 prepared state로 바로 돌아갈 수 있습니다. 따라서 같은 indexpath에 여러번 같은 cell이 display 될 수 있습니다.
더 이상 cell이 displaying end 될 때 reuse pool에 즉시 추가되는 상황이 아닙니다.

더 높은 frame rate를 가지는 장치에서 여전히 scolling되는 동안 hitch를 발생 시킬 수 있습니다. 

App이 cell을 configure하는 방법에 대해 자세히 설명하고 이미지를 표시할 때 commit 당 시간을 줄이는 전략에 대해서 알아보겠습니다.



Updating cell content


서버에서 image를 가져올때, imageView에 image가 로드되지 않을 경우가 있습니다. 그럴 경우 imageview는 처음에 black image를 보여주고, server request가 complete될때 image가 채워질 것입니다. 

 

이러한 문제에 대한 해결책을 위해 registration handler를 살펴봅시다.
우리는 store에서 이미 이미지들을 fetch 해 두었습니다. 하지만 여기 중에는 download해야 하는 이미지들도 있을 것입니다. 

 

여기에 asset들에 대해 isPlaceholder를 추가합니다. true라면 assetStore에서 이미지를 다운로드 할 것입니다. complete되면 imageView에 image를 업데이트 합니다. 지금 코드에서는 cell의 imageview에 접근하였습니다.

 

cell은 다른 곳에서도 재사용되므로 cell을 직접 업데이트하는 대신 collection view의 data source에 필요한 업데이트를 알려야 합니다.

Prepared cell에 reconfigureItem을 호출함으로서, registration의 configuration handler를 리턴할 것입니다. 

reconfigureItem은 새로운 cell로 configure, dequeue하는 대신, 존재하는 cell을 재사용하기 때문에 realodItem대신에 사용하십시오.

 

Prefetch item 시간을 효율화하기 위해서 prefetchingDatasource내에서 downloadAsset 메서드를 사용 할 수 있습니다. 

 

하지만 아직도 scolling하는데 hitch가 존재합니다. 
새 이미지를 로드할때는 hitch가 없습니다. 오직 이미지를 고해상도로 update할때 hitch가 발생합니다. 그것은 display를 위해 모든 image를 decode하는데 시간이 오래 걸리기 때문입니다. 

 

registration의 configuration handler이 처음 호출되면 && asset이 placeholder라면 코드는 async request를 시작하고 configuration을 완료합니다.

 

그리고 asset이 다운로드되면 update된 이미지와 함께 셀 configuration handler가 다시 실행됩니다. 

 

Imageview가 새 image를 commit하려고 하면 먼저 main 스레드에 표시할 이미지를 준비해야 합니다. 이것은 오랜 시간이 걸릴 수 있으며  commit deadline을 놓쳐 hitch가 발생합니다. Image preparation는 모든 image가 display되기 위해 거쳐야 하는 필수 프로세스입니다.

Image preparation은 image를 decode하는 작업입니다. image는 새 image가 commit될때 main 스레드에서 이 과정을 거칩니다. 

 

이상적으로 우리는 image를 미리 준비할 수 있습니다. 그러면 main 스레드를 방해하지 않도 hitch를 발생시키지 않을 것입니다. 

 

iOS 15에서는 이를 위한 새 API를 소개합니다. 

 

 

하지만 고려해야 할 사항이 있습니다. 오리지널 image로부터 raw pixel data를 가집니다. 이는 메모리에 retain되어 있는 한 imageView에 자유롭게 display할 수 있습니다. 하지만 이는 많은 메모리를 차지함을 의미하고 부분적으로 cache되어야 합니다. 또한 포맷때문에 disk storage에 이상적이지 않습니다. 
또한 어떻게 image preparation이 prefetching을 활용 할 수 있는가입니다. prefetching은 image가 prepare, download하는데 많은 시간을 제공합니다. 하지만 프로세스에게 더많은 시간을 주는것은 유저가 placeholder를 오랫동안 보지 않음 아마도 전혀 보지 못함을 의미합니다. 

download를 한뒤, completion handler를 호출하기 전에 asset을 prepare합니다. 이 asset은 값비싸나 매우 귀중하므로 cache합니다. 그러면 prepareAsset을 할때 우선적으로 cache에서 이미지를 찾을 수 있을 것입니다. 

 

iOS 15에서는 thumbnail image를 위한 새 API를 소개합니다. 
이미지를 더 작은 크기로 조정하고 준비할 수 있습니다. 대상 image 크기를 염두에 두고 읽고 처리하도록 하여 CPU 시간과 메모리를 많이 절약합니다.

 

prepareTumbnail()을 사용했을때의 예시입니다.

 

네 이렇게 collection views cell life cycle의 hitch를 개선하기 위해 cell 당 한개의 registration 등록하고 prepared cell state를 추가하여 frame 내에 commit을 완료, async로 image 다운로드 등을 통해 부드러운 scrolling하는 방법에 대해 조금이나마 이해할 수 있었습니다. 

댓글