CollectionView의 성능 향상에 대한 고민: reconfigureItems, cell prefetching, image preparation(iOS 15)(feat. WWDC Make blazing fast lists and collection views)

jane·2022년 9월 5일
2

iOS

목록 보기
31/32

사용자들은 앱을 사용할 때 컬렉션뷰를 스크롤하면서 자연스럽고 부드러운 스크롤을 기대하게 된다. 만약 버벅거리거나 부자연스럽다면 사용자 경험을 해치게 될 것이다.

iOS 15부터 빠른 컬렉션뷰를 만들기 위해서 이 세가지 방법을 적용해볼 수 있게 되었다.
1. structuring data to use reconfigureItems
2. cell prefetching
3. image preparation

1. structuring data

셀의 데이터를 수정해야하는 경우에는 reconfigureItems를 쓰면 좋다.
reconfigureItems는 기존의 셀을 다시 재사용하기때문에,, 기존의 reloadItems처럼 새로운 셀을 dequeue해서 configure하는게 아니라서 성능상 좋다.

iOS 15부터 가능해진 reconfigureItems를 적용하기 우ㅣ해서는...

Diffable data source is built to store identifiers of items in your model, and not the model objects themselves.

Diffable data source의 identifiers를 모델 자체가 아니라 identifiable을 채택한 모델의 id값으로 설정해야한다.

따라서 DestinationPost.ID를 Diffable DataSource의 item identifier로 설정해보자.


이렇게 ID를 identifier로 등록하면, DestinationPost의 프로퍼티중 하나가 바뀌더라도 identifier는 바뀌지 않기때문에 안정성이 있게 된다.

데이터를 불러와서 ID만 뽑은 다음에, snapshot에 appendItems를 통해 Snapshot에 ID들을 append해준다.

여기서 들었던 의문은, diffable datasource에 모델이 아니라 ID타입만 지정해주면, 어떻게 차이를 찾아내지? 였다

걱정마 그거 cell 등록할때 ID로 다시 찾아오면 돼 ㅎ

만약에 cellRegistration을 사용하지 않는다고 해도 아래 코드처럼
UICollectionViewDiffableDataSource 메서드의 클로저로 id가 전달되기 때문에 그 id를 이용해서 모델을 찾아오면 됨

iOS 15 이전에는 애니메이션 없이 Snapshot을 apply하면 내부적으로 reloadData()를 불러서 성능상 그렇게 좋지 않았다고 한다. (아니 ㅋ 이걸 이제야 말하네.. 처음 diffable 소개할때는 성능이 완전 좋아졌다고 말하더니)
왜냐면 컬렉션뷰는 자신이 가진 셀들을 다 버리고 다시 모든 셀들을 만들어야했기 때문에!

그러나, iOS 15 이후부터는 애니메이션 없이 Snapshot을 apply하는 같은 기능을 이 새로운 메서드인 reconfigureItems()로 한다면 변경된 사항만 변경하여 성능이 훨씬 좋아졌다고 한다.

2. cell prefetching

iOS 15부터 Lists, Compsitional Layout using estimated size, UITableView에 무려 cell prefetching이 "자동적용" 된다고 한다.!

cell prefetching을 왜 하는데?
collectionview를 스크롤 하다보면 잠깐잠깐씩 어딘가에서 걸리는 느낌이 들 때가 있다. 이 현상을 "hitch"라고 한다.

이 현상이 발생하는 원인은, 셀이 화면에 보여질때 준비시간이 허락된 시간을 넘을 때 발생한다고 한다.

그러니까 대체 언제?
자세히 알아보자.

셀 라이프사이클은 아래와 같이 dequeue한 후 prepareForReuse에서 초기화 된 후 다시 register해서 configure한 후에 화면에 보여진다.

스크롤뷰를 스크롤하면 스크롤과 동시에 cell들이 보여지게 되는데 이때 화면에 보여질 준비를 하는 것을 "commit" 이라고 한다. 기기에 따라 이 준비시간이 다른데, display의 refresh rate가 높을수록 시간이 짧다.

예를 들어 iPad Pro의 경우 120Hz이고, iPhone의 경우 60Hz라면 iPad에게 주어진 시간이 두배 짧은 것이다.

refresh rate가 높을수록 짧은 시간 내에 보여줘서 디스플레이 퀄리티가 더 좋은것이었군

어쨌든... 다시 셀로 돌아와보면
새 셀이 필요한 시점에서는 기존 셀을 재사용했을때보다 훨~씬 많은 시간이 걸리게 된다. 아래 그림처럼 저 초록색 바가 긴 것이 바로 새로운 셀을 사용했을 때.

만약 저 초록색 준비시간인 "commit" 이 칸막이처럼 되어있는 deadline을 넘기게 된다면,, 끝날때까지 이전의 frame을 가지고 있어서 아까 말했던 "hitch" 현상이 발생하는 것이다.

따라서 이런 상황에 대한 해결방안으로 cell prefeching이 등장한 것이다...
시간이 오래걸리는 cell의 commit을 남는 시간에 미리 땡겨와서 해버려서 나중에 안해도 되는 것!
전체 작업시간은 똑같지만, 중간중간 쉴 때 미리 오래걸리는 다음 작업을 끝내놓았다고 생각하면 쉽겠다 😁

3. Updating cell content

reconfigureItems로 이미지 로딩이 끝난 후 준비되어있는 셀에 이미지만 추가하기

셀의 이미지를 비동기적으로 불러올 때 캐싱된 이미지가 없다면 일단 placeholder 이미지를 가져와 놓고 진짜 이미지를 다운로드 시작하는데,

진짜 이미지를 다운로드한 후에 바로 이미지뷰에 할당하지 말고 reconfigureItems를 불러서 할당하도록 하면, 이미지 빼고 모든 것이 다 준비되어있는 셀을 사용하여 이미지만 추가로 로딩하게 된다.

fetchByID는 내부적으로 id를 이용해 캐시에서 먼저 이미지를 찾고, 없으면 placeholder를 리턴하는 메서드다.

    func fetchByID(_ id: Asset.ID) -> Asset {
        if let image = preparedImages.fetchByID(id) {
            return image
        }
        
        return placeholderStore.fetchByID(id) ?? Asset(id: id, isPlaceholder: true, image: Self.placeholderFallbackImage)
    }

만약 isPlaceholder == true라면, 저 downloadAsset 클로저에 네트워크 통신 결과 이미지가 비동기적으로 가져와질텐데.. 이때 바로 cell의 imageView.image에 할당하지 말아야 한다.

왜?
이미지가 다운로드 되는 그 시점은 비동기이기 때문에 그때 우리가 저 클로저에서 캡쳐했던 cell은 이미 다른곳에 사용되고 있을지도 모르기 때문이다.
따라서 직접적으로 cell에 이미지를 업데이트하기보다는, collectionView DataSource에 업데이트가 필요하다고 알리는 것이 더 안전하다.

그때 사용하는 것이 reconfigureItems ㅎ
reloadItems 사용시 새 cell을 dequeue하기 때문에 별로다.

"calling reconfigureItems on the prepared cell will rerun its registration's configuration handeler"

아래의 reconfigureItems를 부르면 다시 저 위의 cellRegistration config handler을 실행하게 된다.
그럼 이제 fetchByID가 placeholder가 아니라 진짜 이미지를 리턴하게 된다.
(downloadAsset에서 이미지 다운로드해서 캐시해놓아서 캐시에서 찾아오게됨)

image preparation

이미지 다운로드 작업을 비동기적으로 하더라도, 이미지를 display하기 위해서는 한가지 단계가 더 필요하다. 이미지가 비트맵 형태일때만 오직 display할 수 있는데... 다운로드 된 이미지의 형태는 jpeg, png 같은 데이터 형태이다.

*비트맵: 서로 다른 점(픽셀)들의 조합으로 그려지는 이미지 표현 방식
(cf. 벡터 이미지: 점과 점을 연결해 수학적 원리로 그림을 그려 표현하는 방식)

downsampling 포스팅에서 이미지를 화면에 display하려면
아래와 같은 단계가 필요하다고 알아본 적이 있다.
1. UIImage가 data buffer(jpeg, png...) -> image buffer로 decoding하면 얻는 것이 pixel
2. 그 후에 UIImageView가 pixel을 render함

1. UIImage가 pixel을 준비하는 부분 작업이 오래걸리면 또 hitch현상이 발생한다.

따라서, 아래 새로운 API는 1. UIImage가 pixel을 준비하는 부분을 우리가 컨트롤할 수 있도록 만들어줬다!
아래 그림에서 초록색으로 된 image preparation 단계가 바로 그것인데,

이런식으로 원본 이미지를 prepareForDisplay 를 불러서 화면에 바로 디스플레이 될 수 있도록 준비시켜놓는 것이다.

그렇게 하면 메인 스레드를 막지 않고 백그라운드 스레드에서 작업이 일어나기 때문에 hitch현상이 일어나지 않는다.

여기서 주의할점은 pixel 데이터의 특성상 디스크 캐시는 하지 말아야한다.
pixel 데이터는 메모리 캐시에,
원본 데이터를 디스크 캐시에 저장하라고 한다.

Reference

WWDC 20 Make blazing fast lists and collection views
Table and Collection View Cells Reload Improvements in iOS 15

참고한 코드
Building High-Performance Lists and Collection Views

profile
제가 나중에 다시 보려고 기록합니다 ✏️

0개의 댓글