Android 공부

Android RecyclerView에서 OOM 방지하기



0. 상황과 궁금증 설명


상황 : RecyclerView에서 리스트를 보여줄 때, 많은 양의 이미지를 사용한다. 나는 그러던 중, 스크롤이 버벅이는 현상을 발견하거나 심하면 OOM에러를 마주한 경험이 있다. 그 때, 내가 알고 있던 몇 가지 방법에 의해서 쉽게 스크롤이 버벅이거나 OOM이 발생하는 에러를 해결하곤 했다.


하지만, Cache에 의해서 RecyclerView의 이미지가 관리되는 것인가, 혹은 Glide의 trimMemory, lowMemrory에 의해서 관리되는 것인가 의문이 들게 되었다.


결과 : Glide를 사용해서 image를 url을 통해서 접근할 경우에는 다운로드 받는 시간이나 이런 것 때문에, OOM이 발생되지 않았다. 하지만, 밑의 실험결과는 해당 결과를 얻기까지, 다양한 테스트를 진행해봤고, 그래도 조금의 성능 향상을 얻을 수는 있었다.


1. 디바이스 환경


저의 디바이스는 LG G6를 사용하고 있습니다. 이제 다음달이면 24개월 약정에서 해방되는 녀석이죠.

해당 디바이스의 스펙은 아래와 같습니다. 


 CPU

RAM 

DISPLAY 

 프로세서 퀄컴 스냅드래곤 821 2.34GHz + 1.6GHz 쿼드코어

 메모리 RAM 4GB , ROM 32 / 64GB.

 디스플레이 5.7인치 QHD+ IPS.


2. Gradle 환경


Gradle은 특이사항이 없다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apply plugin: 'com.android.application'
 
apply plugin: 'kotlin-android'
 
apply plugin: 'kotlin-android-extensions'
 
android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "hbs.com.androidbitmapmemorytest"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'com.github.bumptech.glide:glide:4.9.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
}
 
cs


3. xml


RecyclerView를 사용하고 있으며, 아이템에는 ImageView가 parentView가 되도록 했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/rv_list">
 
    </androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
cs


1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<ImageView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="240dp"/>
cs


4. Activity


Activity단에서는 thumbnail을 초기화하는 과정의 로직과 adapter와 layoutManager를 setting하는 구조이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainActivity : AppCompatActivity() {
    private val thumbnails :MutableList<String> = mutableListOf()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initThumbnails()
        rv_list.run {
            adapter = TestListAdapter(thumbnails, Glide.with(this@MainActivity))
            layoutManager = LinearLayoutManager(this@MainActivity)
        }
    }
 
    private fun initThumbnails(){
        val thumbnail = "https://www.gstatic.com/webp/gallery/1.jpg"
        for(index in 0..100){
            thumbnails.add(thumbnail)
        }
    }
}
cs


5-1. Adapter 비교군 1 (Cache를 사용하지 않으면서, Glide의 Request Manager를 item 갯수만큼 유지하는 것)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestListAdapter<T>(private val thumbnails:List<T>private val requestManager:RequestManager) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView=LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false);
        return TestViewHolder(itemView)
    }
 
    override fun getItemCount(): Int {
        return thumbnails.size;
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val testViewHolder = holder as TestViewHolder
        val requestOptions = RequestOptions
            .skipMemoryCacheOf(true)//memory cache 사용하지 않음
            .diskCacheStrategy(DiskCacheStrategy.NONE)//disk cache 사용하지 않음
        Glide.with(testViewHolder.itemView.iv_thumbnail)//glide item x n
            .load(thumbnails[position])
            .apply(requestOptions)
            .into(testViewHolder.itemView.iv_thumbnail)
    }
 
    class TestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
cs

 


No Cache , RequestManager x n = 150~160MB

생각했던 것과 다르게 cache와 RequestManager가 단일객체가 아님에도 불구하고, Glide는 좋은 성능을 내고 있다.

필자는 Glide 3.x 기준으로 Glide의 RequestManager가 단일객체가 아니었을 때, 메모리적으로 많은 손해를 봤었는데, 그 당시 코드가 잘못된 것인지, Glide의 메모리 효율성이 좋은 지 잘 모르겠지만, 크게 손해는 안 본다.


5-2. Adapter 비교군 2 (Cache를 사용하지 않으면서, Glide의 Request Manager를 단일객체로 유지하는 것)


Adapter는 가변적으로 여러가지 방법을 사용할 것인데 해당 구조가 기본 구조이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestListAdapter<T>(private val thumbnails:List<T>private val requestManager:RequestManager) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView=LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false);
        return TestViewHolder(itemView)
    }
 
    override fun getItemCount(): Int {
        return thumbnails.size;
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val testViewHolder = holder as TestViewHolder
        val requestOptions = RequestOptions
            .skipMemoryCacheOf(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
        requestManager
            .load(thumbnails[position])
            .apply(requestOptions)
            .into(testViewHolder.itemView.iv_thumbnail)
    }
 
    class TestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
cs


No Cache , RequestManager x 1 = 140~150MB

해당 RequestManage가 단일객체로 인해서 근소하게 줄어든 것을 볼 수 있다.

하지만 메모리 효율이 크게 차이나지 않아서, 단일객체이든, 여러개이든 상관이 없다.


5-3. Adapter 비교군 3 (Cache를 사용하면서, Glide의 Request Manager를 item 갯수만큼 유지하는 것)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestListAdapter<T>(private val thumbnails:List<T>private val requestManager:RequestManager) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView=LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false);
        return TestViewHolder(itemView)
    }
 
    override fun getItemCount(): Int {
        return thumbnails.size;
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val testViewHolder = holder as TestViewHolder
        val requestOptions = RequestOptions
            .skipMemoryCacheOf(false)//memory cache 사용
            .diskCacheStrategy(DiskCacheStrategy.NONE)//disk cache 사용하지 않음
        Glide.with(testViewHolder.itemView.iv_thumbnail)//glide item x n
            .load(thumbnails[position])
            .apply(requestOptions)
            .into(testViewHolder.itemView.iv_thumbnail)
    }
 
    class TestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
cs

 


Using Cache, RequestManager x n = 113~114MB

메모리 효율이 Memory Cache를 사용함으로 인해서, 정말 좋아졌다. 하지만, 특이한 점은 단일 객체와 여러개의 객체가 되었을 때 차이가 정말 없다.


5-4. Adapter 비교군 4(Cache를 사용하면서, Glide의 Request Manager를 단일객체로 유지하는 것)


Adapter는 가변적으로 여러가지 방법을 사용할 것인데 해당 구조가 기본 구조이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestListAdapter<T>(private val thumbnails:List<T>private val requestManager:RequestManager) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView=LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false);
        return TestViewHolder(itemView)
    }
 
    override fun getItemCount(): Int {
        return thumbnails.size;
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val testViewHolder = holder as TestViewHolder
        val requestOptions = RequestOptions
            .skipMemoryCacheOf(false)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
        requestManager
            .load(thumbnails[position])
            .apply(requestOptions)
            .into(testViewHolder.itemView.iv_thumbnail)
    }
 
    class TestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
cs


Using Cache, RequestManager x 1 = 113~114MB

메모리 효율이 Memory Cache를 사용함으로 인해서, 정말 좋아졌다. 하지만, 특이한 점은 단일 객체와 여러개의 객체가 되었을 때 차이가 정말 없다.


6. 혹시, 썸네일의 url이 하나라서 그런것인가?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private fun initThumbnails(){
        val test1 = "https://postfiles.pstatic.net/20130523_164/obihinxkza_1369272807877TKEGx_JPEG/%B0%B6%B8%AE%B0%B6%B8%AE8.jpg?type=w3"
        val test2 = "https://postfiles.pstatic.net/20130523_14/obihinxkza_1369272808236FhnRm_JPEG/%B0%B6%B8%AE%B0%B6%B8%AE5.jpg?type=w3"
        val test3 = "https://postfiles.pstatic.net/20130523_148/obihinxkza_1369272808568i8Xld_JPEG/%B0%B6%B8%AE%B0%B6%B8%AE6.jpg?type=w3"
        val test4 = "https://postfiles.pstatic.net/20130523_40/obihinxkza_1369272808826Dc7AP_JPEG/%B0%B6%B8%AE%B0%B6%B8%AE7.jpg?type=w3"
        val test5 = "https://postfiles.pstatic.net/MjAxODAzMThfMTE1/MDAxNTIxMzc2MjAzNjc5.msDSMKw0-0iDkLKN1hchou_2-7nVICzYJ-gDHDm15xEg.rwPC_upCAQoMmG6cZ_bAf1W1OFQU5bwazx20l90Yre4g.JPEG.wldms_3428/20183616616432.jpg?type=w580"
        val test6 = "https://postfiles.pstatic.net/MjAxODAzMThfMTc1/MDAxNTIxMzc2MjA0MDc4.5GVeVwD36YVTNgDdke0noztLElouTEKhTHTBpiYIsx0g.RMr3xzm6LA3-9i8DHk2FEjDTSsyTY8z4MRp3UjZJ3WAg.JPEG.wldms_3428/2018361667517.jpg?type=w580"
        val test7 = "https://postfiles.pstatic.net/MjAxODAzMThfMTMw/MDAxNTIxMzc2MjA0NDY2.TP43XK2qip-Zas382vOVYmmmuv-mY0BZWZuBOT3tF74g.5NfJaEevR7QDcXnG2-FTd4ZDh_TFsu0MBJ0dYJ2qEIog.JPEG.wldms_3428/20183616612366.jpg?type=w580"
        val test8 = "https://postfiles.pstatic.net/20160411_120/love_n2_1460363599869xuHQJ_JPEG/%C1%F8%C1%D6%C7%C7%C0%DA-16.jpg?type=w2"
        val test9 = "https://postfiles.pstatic.net/20160411_224/love_n2_1460363600526UwGYr_JPEG/%C1%F8%C1%D6%C7%C7%C0%DA-13.jpg?type=w2"
        val test10 = "https://postfiles.pstatic.net/20160411_171/love_n2_1460363602360YCkff_JPEG/%C1%F8%C1%D6%C7%C7%C0%DA-9.jpg?type=w2"
        val tests = arrayListOf<String>(
            test1, test2, test3, test4, test5, test6, test7, test8, test9, test10
        )
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
        for(thumbnail in tests){
            thumbnails.add(thumbnail)
        }
    }
cs


위의 썸네일 결과를 통해 5항의 테스트를 다시 진행해봤지만, 결과는 동일했다.


7. 시사점


Glide를 통해서 url을 접근하는 경우에는 OOM이 대부분 발생하지 않는다.

또한 Glide의 객체가 1개일 때와 여러개일 때는 크게 성능에 좌우되지 않았고, 오히려 Cache를 사용하느냐의 여부가 메모리에 관여되는 것을 볼 수 있었다.


그래서, Cache를 사용하자,

 

8. 추후 테스트


현재는 url를 통해서 무엇인가 했지만, 디바이스에 Bitmap을 처리해야하는게 많고, 이 때, Glide를 쓴다면 OOM에 닿지 않을까?