본문 바로가기

iOS/Deep dive!!!

[iOS] 컬랙션 뷰cell에서 특정 영역만 다른 ui를 갖는 경우 단일 cell or 여러 개의 cell 중 뭐가 좋을까..? | UI 성능 실험 static vs dynmaic

728x90

 

안녕하세요.

 

오늘은 한 섹션에서 사용되는 reusable cell이 특정 영역만 다른 ui를 갖을 때, 하나의 cell안에 분기처리를 통해 레이아웃을 구성하는 것 vs 여러개의 cell을 등록하고 재사용 큐에서 꺼내오는 것 중 뭐가 성능이 더 좋을지 분석한 것을 정리하려고 합니다.

 

 

현재 개발중인 프로젝트의 피드 화면입니다!!

 

 


피드에서는 컬랙션 뷰를 사용하고 있습니다. 위 사진에서 맨 왼쪽은 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해서 반환하는 경우입니다.

 

 

시간차이는 0.01초? 차이가 별로 안나네요..

 

 

그리고 데이터가 4개인 경우 PostCellWithFourThumbnails 셀의 configure(with:)는 81ms를 수행했습니다. 

 

위에 cell이 한개일 때는 612ms를 수행했는데요.. 확실히 작습니다 +_+. 그 이유는 init시점에 PostCellFourThumbnails 네이밍의 cell은 이미 이미지 4개에 대한 thumbnail이 배치됬기 때문입니다.

 

 

그런데...

 

컼... 결과적으로 비슷합니다.. 결국 서로다른 각각의 셀이 화면에 보여지기 위해 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:) 에서 발생된 다량의 중복코드 리빌딩 과정 자세히 들여다보기!!

728x90