안드로이드 연구소

[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(3) - 뷰 레이아웃 최적화 & 이미지 최적화 본문

안드로이드 연구소

[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(3) - 뷰 레이아웃 최적화 & 이미지 최적화

안드로이드 연구원 2023. 5. 8. 21:43

이전 1장에서는 onCreateViewHolder를 최적화하는 방법

2장에서는 onCreateViewHolder를 최적화하는 방법을 살펴보았는데요.

 

이번 마지막 3장에서는 아래와 같은 내용으로 진행해보겠습니다.

-xml과 같은 뷰레이아웃을 최적화

-리싸이클러뷰 안에서 사용하는 이미지들을 최적화

 

아래 내용들은 지난 내용들과 마찬가지로 선배님의 블로그를 바탕으로

저 자신만의 리싸이클러뷰를 만드는데 집중하였습니다.

https://gift123.tistory.com/67

 

안드로이드 개발 (34) RecyclerView 성능 향상

Android 개발을 하다보면 불가피 하게 RecyclerView를 사용해야 하는 순간이 대부분 옵니다. 요새 Compose열풍이 휩쓸고 있지만 아직은 회사에서 사용하는 목록형 UI는 RecyclerView를 통해 만들었을 겁니다

gift123.tistory.com

Q1.  ViewHolder의 아이템을 구현하는 최적화하는 방법을 알려줘.

방법1. item의 xml을 constraintLayout로 뷰 계층 구조를 최대한 1depth로 그리기

LinearLayout을 사용한다면 중첩된 뷰가 발생하여 뷰 계층구조가

1depth이상으로 작성될 수 있습니다.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_launcher" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical"
        android:layout_marginStart="16dp">

        <TextView
            android:id="@+id/titleTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Title" />

        <TextView
            android:id="@+id/descriptionTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Description" />

        <TextView
            android:id="@+id/dateTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Date" />
    </LinearLayout>

    <ImageView
        android:id="@+id/arrowImageView"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:src="@drawable/ic_arrow" />

</LinearLayout>

하지만 ConstraintLayout은 중첩된 뷰의 수를 줄이고 뷰 계층 구조를 사용가능하기 때문에

다른 레이아웃 유형(LinearLayout 또는 RelativeLayout)에 비해 렌더링 시간을 개선할 수 있는 성능에 최적화되어 있습니다.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_launcher"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Title"
        app:layout_constraintStart_toEndOf="@id/imageView"
        app:layout_constraintEnd_toStartOf="@id/arrowImageView"
        app:layout_constraintTop_toTopOf="@id/imageView"
        app:layout_constraintHorizontal_bias="0"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginTop="8dp"/>

    <TextView
        android:id="@+id/descriptionTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Description"
        app:layout_constraintStart_toEndOf="@id/imageView"
        app:layout_constraintEnd_toStartOf="@id/arrowImageView"
        app:layout_constraintTop_toBottomOf="@id/titleTextView"
        app:layout_constraintHorizontal_bias="0"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginTop="8dp"/>

    <TextView
        android:id="@+id/dateTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Date"
        app:layout_constraintStart_toEndOf="@id/imageView"
        app:layout_constraintEnd_toStartOf="@id/arrowImageView"
        app:layout_constraintTop_toBottomOf="@id/descriptionTextView"
        app:layout_constraintHorizontal_bias="0"
        android:layout_marginStart="16dp"
        android:layout... />
        
</ConstraintLayout>

<drawable>파일도 마찬가지로 복잡한 뷰 레이아웃을 가지게하지 말자!

 

 

방법2. ViewStub 사용하기

예를 들어, 사이즈가 없거나 최초에는 보이지 않았다가 클릭 후 레이아웃이 생기도록 구현한 뷰들은

ViewStub을 사용하면 레이아웃 생성 시 뷰를 그리는데 전력으로 투입되는 리소스를 뒤로 배분시켜

느린 렌더링을 예방할 수 있다.

<androidx.constraintlayout.widget.ConstraintLayout 
    ...>

    <ViewStub
        android:id="@+id/toolbar_stub"
        android:layout="@layout/toolbar_layout"
        android:inflatedId="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/toolbar_layout" />

</androidx.constraintlayout.widget.ConstraintLayout>
toolbarStub.inflate()

 

 

방법3. PrecomputedText 사용하기

Textview를 사용할 때 PrecomputedText를 사용한다면

비동기 백그라운드 쓰레드에서 텍스트 레이아웃을 계산하여

Textview에 바인딩할 데이터를 더 빠르게 표시할 수 있습니다. 

그리하여 텍스트 렌더링 성능을 개선하는 데 도움이 될 수 있습니다.

 

짧은 글의 텍스트보다 설명과 같은 긴 글의 텍스트를 만들때 사용하면 매우 효과적입니다.

val params = TextViewCompat.getTextMetricsParams(textView)
            ?.apply {
                text = textView.text
                textView.textSize.let { textSize = it }
                textView.typeface?.let { setStyle(it.style) }
            }
val precomputedText = PrecomputedTextCompat.create(textView.text, params!!)
textView.setText(precomputedText, TextView.BufferType.SPANNABLE)

 

방법4. 투명색 사용하지 않기

레이아웃의 배경색이나 뷰들의 색상을 투명색으로 지정하면

많은 연산처리 작업을 하게 되므로 피하는 것이 좋다고 한다.

 

 

방법5. NestedScrollview와 중첩된 Recyclerview 사용시

recyclerView.setNestedScrollingEnabled(false) 메서드를 사용하면

내부 리싸이클러뷰가 스크롤되는 동안 외부 스크롤(NestedScrollview나 상위 recyclerview)가

스크롤 되지 않습니다.

recyclerView.setNestedScrollingEnabled(false)

이는 내부 스크롤 이벤트가 상위 부모뷰로 이동되는걸 막아 느린 렌더링을 예방할 수 있습니다.

 

Q2. 어떻게 이미지 뷰를 최적화 할 수 있어?

방법1. 네트워크로 불러오는 이미지는 Glide로 불러오기

 

Glide는 네트워크나 디스크와 같은 원격 소스에서 이미지를 로드하고

이를 저장하여 다음에 똑같은 이미지 소스를 다시 호출하면 저장된 소스를 불러와

다시 다운로드할 필요가 없도록 메모리나 디스크에 캐시하는 데 도움이 되는

Android에서 인기있는 이미지 로드 및 캐싱 라이브러리입니다.

 

skipMemoryCache메서드가 false여야지 이미지 캐시 작업이 일어납니다. 

디폴트 값이 false입니다.

Glide.with(context)
    .load(imageUrl)
    .skipMemoryCache(false) // false면 메모리 캐싱 사용, true면 메모리 캐시 사용하지 않음
    .into(imageView);

 

 

방법2. ARGB_8888에서 RGB_565으로 변경

기본적으로 이미지를 표현하는 ARGB_8888방식이에서

이미지 색상의 품질이 떨어지는 RGB_565을 사용하여 메모리를 효율적으로 관리할 수 있다.

 

R.color.Red와 같은 이미지 값을 RGB_565로 바꿔주는 함수이다.

fun colorResourceToRgb565(colorResource: Int): Short {
    val color = ContextCompat.getColor(context, colorResource)
    val red = Color.red(color)
    val green = Color.green(color)
    val blue = Color.blue(color)
    val r = (red * 31 / 255).toShort()
    val g = (green * 63 / 255).toShort()
    val b = (blue * 31 / 255).toShort()
    return (r.toInt() shl 11 or (g.toInt() shl 5) or b.toInt()).toShort()
}

출력할 화면에서 아래와 같이 .setBackgroundColor(rgb565) 사용하면 끝.

val colorResource = R.color.my_color
val rgb565 = colorResourceToRgb565(colorResource)

하지만 저해상도 이미지여서 이미지 품질이 떨어질 수 있으므로 무작정 사용하기 보다는

작은 이미지들을 여러개 보여주는 썸네일 이미지에서 보여주는걸 추천한다.

- 고해상도가 필요한 큰 사이즈의 이미지: ARGB_8888
- 저해상도가 필요한 작은 사이즈의 이미지: RGB_565

 

 

방법3. Glide에서 가로와 세로 크기 wrap_content 사용하지 않기

ImageView 크기가 wrap_content로 설정된 경우 뷰를 두 번 측정됩니다.

첫번째는 콘텐츠를 기반으로 원하는 크기를 결정하기 위해 측정되고,

두번째는 올바른 크기로 뷰를 실제로 배치하기 위해 측정됩니다.

 

Glide 또한 이미지 크기 조정하고 자르는 시간이 소요됩니다.

 

wrap_content와 마찬가지로

가로의 길이를 바탕으로 가로세로 비율을 계산하여 그려주는 아래 메서드도

마찬가지로 많은 시간을 소요합니다.

adjustViewBounds="true"

 

이러한 이유로 Glide에서는 wrap_content보다

match_parent와 고정 사이즈 값으로 크기를 설정하는 것을 추천합니다.

 

 

방법4. Glide.centerInside()로 자동 해상도 확대 막기

네트워크로 불러온 이미지보다 imageView의 크기가 크다면 

자동으로 이미지가 확대되면서 고용량 저품질 이미지가 불러와집니다.

 

이를 막기위해 Glild에서 centerInside()를 사용하면 됩니다.

Glide.with(imageView)
     .load(IMAGE_URL) 
     .centerInside() 
     .into(imageView)

 

 

방법5. Glide.override(width, height)

기본적으로 Glide를 사용하여 이미지를 로드하면

ImageView의 크기를 결정하고 보기에 맞게 이미지 크기를 자동으로 조정합니다.

그러나 경우에 따라 로드된 이미지의 크기를 더 많이 제어해야 할 수도 있습니다.

 
Glide.override()를 사용하면 이미지가 원하는 크기로 로드되도록 ImageView의 정확한 너비와 높이를 픽셀 단위로 지정할 수 있습니다.
이미지 크기를 계산하여 렌더링 리소스를 단축시킬 수 있습니다.
Glide.with(imageView) 
     .load(IMAGE_URL) 
     .override(200,200) 
     .into(imageView)

 

 

방법6. 이미지로 로딩 취소

리싸이클러뷰는 아래의 생명주기는 아래와 같다.

recyclerview 뷰홀더가 생성, 데이터 바인딩, 디스플레이 출력, 디스플레이 미출력, 재활용할 뷰홀더 가져오기

또 생명주기에 맞춰 이미지 로딩은 자동으로 취소된다.

onCreateViewHolder -> onBindViewHolder ->
onViewAttachedToWindow ->
onViewDetachedFromWindow -> onViewRecycled
-> onCreateViewHolder(반복)

 

이미지를 자동으로 로딩이 취소가 되지 않을 경우를 대비한  

수동 이미지 로딩 취소이다. 

override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
    super.onViewRecycled(holder)
    Glide.with(context)
        .clear(holder.imageView)
}

 

 

방법7. OutOfMemory시 onLowMemory()와 onTrimMemory

메모리 부족 현상이 일어날 때 아래 코드를 사용하면

메모리가 부족할 때 호출되는 onLowMemory()가 호출되고

이때 Glide 이미지 캐시를 지웁니다.

 

또 메모리가 부족할 때 onTrimMemory()가 호출되고

메모리 압력 수준에 따라 메모리 보유하고 있는 적정 레벨의 이미지를 해제합니다.

// onLowMemory()는 시스템의 메모리가 부족할 때 호출되는 Android 활동 클래스의 메서드입니다. 
overrid fun onLowMemory() {
    super.onLowMemory();

    // Glide의 이미지 캐시를 지웁니다
    Glide.get(this).clearMemory();
}

// onTrimMemory()는 시스템의 메모리가 부족하여 메모리 사용량을 줄여야 할 때 호출되는 Android 활동 클래스의 메서드입니다.
overrid fun onTrimMemory(int level) {
    super.onTrimMemory(level);
    
    // Glide는 시스템이 겪고 있는 메모리 압력 수준에 따라 현재 메모리에 보유하고 있는 일부 또는 모든 이미지를 해제합니다.
    Glide.get(this).trimMemory(level);
}

 

진짜 끝!

여기까지 보시고 여러분들의 리싸이클러뷰에 적용하신분들은

리싸이클러뷰 박사 자격 취득!

Comments