안녕하세요.
오늘은 한 섹션에서 사용되는 reusable cell이 특정 영역만 다른 ui를 갖을 때, 하나의 cell안에 분기처리를 통해 레이아웃을 구성하는 것 vs 여러개의 cell을 등록하고 재사용 큐에서 꺼내오는 것 중 뭐가 성능이 더 좋을지 분석한 것을 정리하려고 합니다.
이번에 리펙터링한 코드의 PR 링크를 남겨두겠습니다[리펙터링 PR 링크].
현재 개발중인 프로젝트의 피드 화면입니다!!
피드에서는 컬랙션 뷰를 사용하고 있습니다. 위 사진에서 맨 왼쪽은 cell내부에서 사용되는 서브뷰들의 영역입니다. 포스트가 보여지는 하나의 섹션에서, 사용자가 올린 사진에 따라서 총 5개 정도의 사진 배치가 있습니다. 이렇게 회색 영역은 모두 같지만, 포스트 thumbnail 이미지에 개수에 따라 배치가 달라져야 합니다.
이때 궁금증은 하나의 cell타입만 등록 후 collectionView(_:cellForItemAt:) 시점에서 cell을 dequeue한 후 cell의 서브뷰들에게 데이터를 전달할 때마다 포스트 사진 영역의 레이아웃만 다이내믹하게 재 배치하는게 성능상 좋은지, 아니면 서로 다른 5개의 cell을 정적으로 선언 후 컬랙션 뷰에 등록한 다음에 사진의 개수에 따라 서로 다른 cell 중 하나를 dequeue로 꺼내 오는게 성능상 좋은지 궁금했습니다.
초기에 구현한 방법은 전자의 방식을 활용했습니다. Cell에 데이터가 들어올 때마다 한 개의 포스트 Cell의 사진(thumbnail) 영역만 배치를 다시 한 후 화면에 그렸습니다. 그리고 최근에 실험차원에 다시 리빌딩하면서, 한개의 셀이 아닌 5개의 셀을 만들었습니다.
private final class PostFourThumbnailsView: UIStackView {
private var imageViews: [UIImageView] = []
init() {
super.init(frame: .zero)
imageViews = (0...3).map { index -> UIImageView in
return UIImageView(frame: .zero).Then {
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true
let height = index == 0 ? 118 : (118-1)/2
$0.heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
let rightBottomStackView = UIStackView(arrangedSubviews: [imageViews[2], imageViews[3]])
let rightContentStackView = UIStackView(arrangedSubviews: [imageViews[1], rightBottomStackView])
[imageViews[0], rightContentStackView].forEach { addArrangedSubview($0) }
self.configureDefaultPostThumbnail(with: .horizontal)
rightBottomStackView.configureDefaultPostThumbnail(with: .horizontal)
rightContentStackView.configureDefaultPostThumbnail(with: .vertical)
}
...
}
예를들어 이미지가 4개인 경우 스택뷰는 위와 같이 구성했습니다.
extension UIStackView {
func configureDefaultPostThumbnail(with axis: NSLayoutConstraint.Axis) {
self.axis = axis
spacing = 1
distribution = .fillEqually
}
}
이 함수를 통해 간편하게..
이렇게 5개의 cell이 나오게 됬습니다..
여기서 테스트 상황은 iOS 12 pro max 실 기기로 스크롤 할 수 있는 post item은 약 80개이고, 30~35초동안 맨 아래로 ~> 다시 맨 위로 스크롤했습니다.(스크롤은 천천히)
한 개의 Cell안에서 데이터를 받는 시점에 이미지들 레이아웃 결정하는 경우
PostCell 한 개만 컬랙션뷰에 등록한 경우 메인 스레드 분석..
cell이 한개지만, collectionView(_:cellForItemAt:)시점에 데이터를 받아 이미지 개수에 따라 이미지 영역의 뷰 레이아웃을 업데이트 했을때의 call Tree입니다.
cellForRowAt시점에서 configure(with:)로 데이터를 받은 후, 이미지 섬네일 개수에 따라
이렇게 위와 같이 여러 case 중 하나로 레이아웃을 지정합니다.
그래서 컬랙션 뷰와 뷰모델의 데이터를 주입하는 PostViewAdapter에서 컬랙션 뷰의 cellForItemAt 델리게이트 함수 중 post configure에서 많은 비용을 소모하고, 이때 configure(with:)함수에서 레이아웃 배치에 시간을 들인것 같습니다.
5개의 서로 다른 Cell을 컬랙션 뷰에 등록해서 이미지 개수에 따라 특정 cell을 반환하는 경우
이번에는 서로 다른 5개의 cell을 등록한 후 collectionView(_:cellForItemAt:)시점에 이미지 개수에 따라 특정 cell을 dequeue해서 반환하는 경우입니다.
그리고 데이터가 4개인 경우 PostCellWithFourThumbnails 셀의 configure(with:)는 81ms를 수행했습니다.
위에 cell이 한개일 때는 612ms를 수행했는데요.. configure(with:) 가 612ms -> 81ms로 감소했습니다 +_+. 그 이유는 init시점에 PostCellFourThumbnails 네이밍의 cell은 이미 이미지 4개에 대한 thumbnail이 배치됬고 collect view에서 dequeue 할 때 해당 cell identifier를 꺼내오기 때문이라고 생각합니다.
그런데...
서로 다른 각각의 셀이 화면에 보여지기 위해 configure(with:)가 호출되는거, 그리고 화면에 보여지기전에 prepareForReuse()를 각각의 cell에서 호출하는거 호출 시간을 비교하면
결국 cell한 개일때 특정 영역만 다시 재 배치하는 경우와 아니면 각각의 cell타입을 비교하는 경우 둘 다 비슷하네요. 근데 셀 하나가 화면에 보여질 때 prepareForReuse()나 configure(with:)가 훨씬 가볍다는걸 알 수 있었습니다 +_+
스크롤을 빨리하는 경우 메모리 사용량 측정
마지막으로 메모리 점유율을 .. 비교해봤습니다.
한 10초동안 계속 스크롤을 위 아래로 해봤습니다. 그리고 스크롤을 더이상 하지 않게 됬을때 메모리가 살짝 감소한 후 일정하게 유지되고있었습니다. 그리고 132Mb 유지가 됬습니다.
오+_+.. 여러개의 cell을 등록한 경우 스크롤을 빨리하다 멈추자마자(스크롤 감속 0이) 메모리 사용량이 20mb가량 떨어지고 스크롤을 빨리해도 유지가 됩니다. 신기하네요.
결론: 스크롤을 천천히해서 포스트들을 천천히 볼 경우에 cell하나에 collectionVIew(_:cellForRowAt:)시점에 데이터를 받고, 특정 영역만 뷰들을 재 배치하는 경우와 thumbnail 이미지 배치마다 따로 cell을 만드는 경우 둘 다 비슷하다. 그런데 스크롤을 위 동영상처럼 빨리할 경우 여러개의 cell이 configure(with:), prepareForReuse(_:)모두 비용이 적기 때매 메모리 점유율이 상대적으로 낮다!!
즉 cell이 재사용 큐에서 dequeue하기 전에 호출되는 prepareForReuse()나 재사용 큐에서 dequeue 된 후 collectionView(_:cellForRowAt:)시점에 데이터를 주입할 때 비용이 낮게 들 수 록 좋다?!!!!
혹시 내용 중 제가 instruments를 잘못 파악한 내용이 있다면 댓글로 알려주신다면 정말 감사합니다.
cf.
그런데 성능체크 중 생각보다 이미지의 색을 변환하는데... 비용이 많이드네요. 몰랐는데,, 신기하네요..
setColor(_:)함수는 정말 많이 사용하는 함수입니다. prepareForReuse()나 cell's dequeue시점 cell.configure(with:)등.. 여러 곳에서 많이 활용되는데요. 다음 포스트에선 이미지 색 변경할 때 UIGraphicsBeginImageContextWithOptions api말고 다른것을 사용해서 메모리 절약하는 글을 작성해보려고 합니다!
이번 리빌딩의 결과로 피드 화면의 collectionView(_:cellForRowAt:) 에서 발생된 다량의 중복코드 리빌딩 과정 자세히 들여다보기!!
리펙티렁을 진행한 코드 PullRequest [링크 바로가기]