목표
메인 화면에서 에뮬레이터에서는 드물게, 실기기에서는 때때로 화면의 미세한 버벅거림이 나타났다. (정말 자세히 눈이 빠지도록 봤음..)
이유는 단순히 '데이터를 한 번에 가져와서 버벅거리나 보다!'라고만 생각했다. 그러나 꽤 많은 요소들이 화면을 버벅거리는데 기여(?)하고 있었다. 천천히 알아보자
페이징을 도입했지만..
페이징은 언젠가 도입해야 할 거라 생각했고, 때마침? 화면 버벅거림을 발견했기 때문에 , 페이징을 도입했다.
RecyclerView.Adapter를 PagingDataAdpater로 바꾸고 PagingSource를 만들었다. 그리고 테스트해 봤는데.. 미세하게 버벅거린다.. 물론 가끔씩..이다..
뭐가 문제지 보다가 안드로이드 스튜디오에서 제공하는 Profiler로 분석해 보았다.
퍼포먼스 분석
퍼포먼스 분석을 위해 안드로이드 스튜디오에서 제공하는 Profiler를 활용해 보았다. 기록을 시작하고, 화면을 조작하면 화면에 대한 정보들이 기록된다. 자세한 부분은 공식 문서 참고
Janky Frame의 빨간색 부분은 프레임이 렌더링 기한을 초과하는 기간을 나타낸다. 버벅거림이 발생하는 부분이라고 봐도 될 것 같다.
클릭해 보면, 자세하게 볼 수 있다.
앱의 메인 스레드에서 해당 추적 부분도 볼 수 있다.
그래서 뭘 하면 되는데..?
본인이 비효율적이라고 생각했던 부분이 화면마다 background 설정해 주었던 부분이었다. 화면마다 background를 설정해 주면, 실수가 잦아지고, 무엇보다 OverDraw 가능성이 높아진다.
일단은 이 부분을 수정해 주고, ReclcyerView 최적화에 대한 글들을 기반으로 코드 수정을 할 것이다.
OverDraw를 줄입시다
앱이 한 프레임 내에 같은 픽셀을 두 번 이상 그리는 경우를 OverDraw라고 한다.
일단 내 앱에서 얼마나 많은 overDraw가 발생하고 있는지 알아보니..
와.. 너무 빨갛다.. 미치도록 빨갛다 빨리 수정해야겠다는 생각이 들었다..
안드로이드 스튜디오의 Layout Inspector에서 3D 모드로 본 화면이다. 겹겹이 쌓여있는 모습.. 이러니 빨갛지..
화면에 있는 background 설정은 되도록 모두 수정하였다.
그리고 themes.xml 부분에 해당 코드를 추가해 기본 화면 색을 지정해 줬다.
<item name="android:windowBackground">@color/mainBackground</item>
RecyclerView 최적화
OverDraw 도 문제가 있겠지만, RecyclerView에도 문제가 있다 생각했다. 다행히 RecyclerView 최적화에 대한 글들이 많았고 그중에 적합해 보이는 것들을 적용했다.
setItemViewCacheSize로 캐시 크기 조정하기
스크롤을 통해 화면에서 UI가 사라지면, 사라진 View를 다시 사용하는 recycled view pool에 들어가게 되는데, 이 메서드를 사용하게 되면 Cache에 저장되어 다시 화면에 나왔을 때 onBindViewHolder 호출 없이 View가 보인다. 동일한 뷰를 다시 그리지 않는다는 뜻이다.
본인은 페이징 사이즈의 2배를 적용했다. 아이템 크기가 좀 있기 때문에, 스크롤 범위가 그렇게 클 것 같지 않다고 생각했기 때문이다.
setItemViewCacheSize((2 * PAGE_SIZE).toInt())
onBindViewHolder 최적화
onBindViewHolder에서 복잡한 계산이나 콜백 및 리스너를 set 하지 말라고 했다. 본인은 정확히 콜백 및 리스너를 set 하고 있었기 때문에 뜨끔했다..
그래서 adapter 안에 onClick 관련된 interface를 정의하고 override해주는 방식으로 진행했다.
interface OnItemClickListener{
fun onItemClick(view: View, albumItem: AlbumItem)
fun onLinkClick(view: View, albumItem: AlbumItem)
fun onLongClick(fade: View, albumItem: AlbumItem)
}
viewHolder에서 다음과 같이 정의해 주었다.
albumCardView.setOnClickListener { view ->
val pos = absoluteAdapterPosition
val item = getItem(pos)
if(pos != RecyclerView.NO_POSITION && item != null) {
mOnItemClickListener.onItemClick(view, item)
}
}
그래서 adapter의 클릭이벤트를 호출했다.
private fun setOnAdapterClickListener() {
albumAdapter.setOnItemClickListener(object: AlbumAdapter.OnItemClickListener {
override fun onItemClick(view: View, albumItem: AlbumItem) {
NavControllerManager.navigateMainToDetail(albumItem)
}
override fun onLinkClick(view: View, albumItem: AlbumItem) {
onClickLinkButton(albumItem)
}
override fun onLongClick(fade: View, albumItem: AlbumItem) {
onLongClickItem(albumItem = albumItem, view = fade)
}
})
}
AlbumAdapter 코드
class AlbumAdapter(): PagingDataAdapter<AlbumItem, AlbumAdapter.AlbumViewHolder>(differ) {
private lateinit var mOnItemClickListener: OnItemClickListener
fun setOnItemClickListener(onItemClickListener: OnItemClickListener){
mOnItemClickListener = onItemClickListener
}
inner class AlbumViewHolder(val binding: ItemAlbumBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.apply {
albumCardView.setOnClickListener { view ->
val pos = absoluteAdapterPosition
val item = getItem(pos)
if(pos != RecyclerView.NO_POSITION && item != null) {
mOnItemClickListener.onItemClick(view, item)
}
}
albumCardView.setOnLongClickListener { view ->
val pos = absoluteAdapterPosition
val item = getItem(pos)
if(pos != RecyclerView.NO_POSITION && item != null) {
fade.apply {
mOnItemClickListener.onLongClick(this, item)
visibility = View.VISIBLE
background = getDrawable(view.context, R.drawable.ripple_card_view)
}
}
true
}
linkButton.setOnClickListener { view ->
val pos = absoluteAdapterPosition
val item = getItem(pos)
if(pos != RecyclerView.NO_POSITION && item != null) {
mOnItemClickListener.onLinkClick(view, item)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumViewHolder = AlbumViewHolder(
ItemAlbumBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
val currentItem = getItem(position) ?: return
holder.binding.apply {
data = currentItem
}
}
companion object {
private val differ = object : DiffUtil.ItemCallback<AlbumItem>() {
override fun areItemsTheSame(oldItem: AlbumItem, newItem: AlbumItem): Boolean = oldItem.documentId == newItem.documentId
override fun areContentsTheSame(oldItem: AlbumItem, newItem: AlbumItem): Boolean = oldItem == newItem
}
}
interface OnItemClickListener{
fun onItemClick(view: View, albumItem: AlbumItem)
fun onLinkClick(view: View, albumItem: AlbumItem)
fun onLongClick(fade: View, albumItem: AlbumItem)
}
}
기타
앞서 설명한 부분 외에, 백그라운드에서 작업할 수 있는 부분들은 Dispatchers.IO나 Dispatchers.Default로 설정해 주었다. 이런 작은 부분들도 신경 써야 꼼꼼한 개발자가 되는 것 같다.
결과
과연 결과는..?
퍼포먼스
앞서 봤던 것보다 빨간색이 훨씬 줄었다. 여러 번 테스트해 봤을 때, 눈이 빠지도록 봐야 보였던 미세한 버벅거림은 찾아볼 수 없었고, Janky Frames에서 나타나던 프레임들도 현저히 줄었다.
프레임 시간도 비교해 보면 , 전과 비교 했을 때 오차가 작게는 30ms, 크게는 100ms 정도 줄었다
OverDraw
OverDraw가 생기는 지점을 가능한 줄였다. 덕분에 아이템을 제외한 부분에는 OverDraw가 생기지 않았다.
이미지 최적화 등 더 개선해야 할게 많지만 버벅거림 줄일 수 있었다.
Profiler 등 성능 점검 기능에 대해 아직은 미숙하지만 앞으로 더 영리하게 성능 개선 및 원인 분석을 할 수 있을 것 같다.
성능 개선은 꾸준하게 신경 써야 할 부분인 것 같다.
더 개선해야 할 부분이 많지만 쾌적한 앱을 위해 앞으로도 킵고잉~
참고자료
https://developer.android.com/studio/profile
https://developer.android.com/studio/profile/jank-detection
https://developer.android.com/topic/performance/rendering/overdraw
https://android-developers.googleblog.com/2017/08/understanding-performance-benefits-of.html
'안드로이드' 카테고리의 다른 글
[Android] 전략 패턴(Strategy Pattern)을 적용하여 소셜 로그인 리팩토링 하기 (0) | 2024.08.12 |
---|---|
[Android] 네트워크 예외 처리 (에러 핸들링)- Retrofit CallAdapter (0) | 2024.06.16 |
[Android] MVI 패턴을 알아볼까? (1) | 2024.06.07 |
[Android] Animation Transition 시 이미지 로딩 개선하기 (0) | 2024.04.14 |
[Android] Intent, Content URI 와 File Provider 를 사용하여 외부(크롬, 갤러리 등)에서 이미지 가져오기 (이미지 공유 받기) (0) | 2024.04.11 |