[Android] ANR 문제 해결하기

leeeha·2022년 9월 28일
0
post-thumbnail

ANR 문제란?

  • Application Not Response
  • 유저 이벤트에 액티비티가 5초 이내에 반응하지 못하는 경우 발생하는 에러
  • 유저가 액티비티를 실행시켰을 때, 시스템에서 해당 액티비티를 실행시키는 수행 흐름을 메인 스레드라고 한다.
  • 메인 스레드에서 시간이 오래 걸리는 작업을 수행 중일 때, 유저가 이벤트를 발생시키면 ANR 문제가 발생할 수 있다.
  • ANR 문제가 발생할 수 있는 대표적인 예시가 바로 백엔드 서버와 연동하는 네트워크 프로그래밍이다.

해결 방법

  • (deprecated) Thread의 Handler로 해결
  • (deprecated) AsyncTask로 해결
  • (권장) Coroutine으로 해결

Handler

  • 메인 스레드가 아닌 개발자가 정의한 스레드에서 뷰에 접근하려고 하면 에러가 발생한다.
  • 따라서, 개발자 스레드에서는 화면 출력 요소인 뷰에 접근할 수 없도록 프로그램을 짜야 한다. 즉, 메인 스레드에서만 뷰에 접근할 수 있도록 해야 한다.

Handler로 메인 스레드에게 작업 의뢰

  • sendMessage(Message msg)
  • sendMessageAtFrontOfQueue(Message msg): 뷰 작업에 대한 의뢰가 반복적으로 발생한 경우 UI 스레드에서 순차적으로 처리하는데, 이번 의뢰를 가장 먼저 처리하라는 요청
  • sendMessageAtTime(Message msg, long uptimeMillis): 의뢰를 지정된 시간에 수행
  • sendMessageDelayed(Message msg, long delayMillis): 지연 시간이 지나고 의뢰 수행
  • sendEmptyMessage(int what): 데이터 전달 없이 의뢰하는 경우

Message 객체는 메인 스레드에게 넘기는 데이터

  • what: int형 식별자, 개발자가 임의의 숫자 값으로 요청을 구분하기 위해 사용
  • obj: UI 스레드에게 넘길 데이터, Object 타입의 변수
  • arg1, arg2: UI 스레드에게 넘길 데이터, int 타입으로 간단한 숫자 값은 arg1, arg2 변수에 담아서 전달
// 메시지 객체를 전달하면서 메인 스레드에게 작업을 의뢰하는 코드  
var message = Message()
message.what = 1 
message.arg1 = count 
handler.sendMessage(message) 
// 메인 스레드에서는 핸들러 객체의 handleMessage 함수 오버라이딩 
var handler: Handler = object: Handler() {
  override fun handleMessage(msg: Message) {
  	if(msg.what === 1) { 
      textView.text = msg.arg1.toString() 
    }else if(msg.what === 2) { 
      textView.text = msg.obj as String 
    }
  }
}

실습 예제

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#1a237e">
    <TextView
        android:id="@+id/main_textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="10"
        android:textSize="80dp"
        android:textStyle="bold"
        android:textColor="@android:color/white"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="16dp">
        <ImageView
            android:id="@+id/main_startBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:clickable="true"
            android:src="@drawable/ic_start" />
        <ImageView
            android:id="@+id/main_pauseBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@drawable/ic_pause"
            android:clickable="true"/>
    </LinearLayout>
</RelativeLayout>
package com.tutorial.c47

import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    lateinit var startView: ImageView
    lateinit var pauseView: ImageView
    lateinit var textView: TextView

    var loopFlag = true
    var isFirst = true
    var isRun = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        startView = findViewById(R.id.main_startBtn)
        pauseView = findViewById(R.id.main_pauseBtn)
        textView = findViewById(R.id.main_textView)

        startView.setOnClickListener {
            if(isFirst){
                isFirst = false
                isRun = true
                thread.start() // 개발자가 정의한 별도의 스레드 시작 
            }else{
                isRun = true
            }
        }

        // 정지 버튼 누르면 count가 감소하지 않음. 
        pauseView.setOnClickListener {
            isRun = false
        }
    }

    // 메인 스레드에서는 핸들러 객체의 handleMessage 함수 오버라이딩 
    var handler: Handler = object: Handler(){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            if(msg.what === 1){
                textView.text = msg.arg1.toString() // count 값 보여주기 
            }else if(msg.what === 2){
                textView.text = msg.obj as String // Finish 문자열 보여주기 
            }
        }
    }

    var thread: Thread = object: Thread(){
        override fun run() {
            try{
                var count = 10
                while(loopFlag){
                    sleep(1000) // 1초씩 숫자가 감소하도록
                    if(isRun){
                        count--

                        var message = Message() // 메시지 객체 생성
                        message.what = 1
                        message.arg1 = count
                        handler.sendMessage(message) // 핸들러에게 작업 의뢰

                        if(count == 0){
                            message = Message()
                            message.what = 2
                            message.obj = "Finish!!"
                            handler.sendMessage(message) // 핸들러에게 작업 의뢰
                            loopFlag = false // 반복문 종료
                        }
                    }
                }
            }catch (e: Exception){
                e.printStackTrace()
            }
        }
    }
}

이 예제에서 별도의 스레드를 만들지 않고 메인 스레드에서 모든 작업을 처리했다면, 숫자가 줄어드는 중간에 유저의 클릭 이벤트가 발생했을 때 ANR 문제가 일어났을 수도 있다.


AsyncTask

  • AsyncTask는 기존의 스레드-핸들러 구조를 추상화 시켜서 개발자가 조금 더 쉽게 개발할 수 있도록 방법을 제공해주는 것이라고 생각하면 된다.
override fun doInBackground(vararg p0: Void?): String {
	// ... 
}

override fun onProgressUpdate(vararg values: Int?) {
	// ...
}

override fun onPostExecute(result: String?) {
	// ... 
} 
  • doInBackground(Params... params): Thread에 의해서 처리될 내용을 담기 위한 함수
  • onPreExecute(): AsyncTask의 작업을 시작하기 전에 호출, AsyncTask에서 가장 먼저 한번 호출
  • onProgressUpdate(Progress... values): doInBackground에 의해 처리되는 중간 값을 받아 처리하기 위해서 호출, doInBackground에서 publishProgress 함수로 넘긴 값이 전달됨.
  • onPostExecute(Result result): AsyncTask의 모든 작업이 완료된 후 가장 마지막에 한번 호출, doInBackground 함수의 최종 값을 받기 위해 사용
class MyAsyncTask: AsyncTask<Void?, Int?, String>() 
  • 첫번째 타입: Background 작업 처리를 위한 doInBackground의 매개변수 타입과 동일. AsyncTask에 의해 Background 작업을 의뢰할 때 넘길 데이터의 타입. 없으면 Void로 지정.
  • 두번째 타입: doInBackground 함수 실행에 의해 발생한 데이터를 publishProgress 함수를 이용해 전달하는데, 이때 전달하는 데이터 타입. 데이터를 전달 받을 onProgressUpdate 함수의 매개변수 타입과 동일하게 지정.
  • 세번째 타입: onPostExecute의 매개변수 타입과 동일하게 지정. doInBackground 함수의 리턴 타입이며, 리턴된 데이터가 onPostExecute 함수의 매개변수로 전달됨.

실습 예제

package com.tutorial.c47

import android.os.AsyncTask
import android.os.Bundle
import android.os.SystemClock
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    lateinit var startView: ImageView
    lateinit var pauseView: ImageView
    lateinit var textView: TextView

    var isFirst = true

    lateinit var asyncTask: MyAsyncTask

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        startView = findViewById(R.id.main_startBtn)
        pauseView = findViewById(R.id.main_pauseBtn)
        textView = findViewById(R.id.main_textView)

        startView.setOnClickListener {
            if(isFirst){
                asyncTask.isRun = true
                asyncTask.execute() // 스레드 실행
                isFirst = false
            }else{
                asyncTask.isRun = true
            }
        }

        pauseView.setOnClickListener {
            asyncTask.isRun = false
        }

        asyncTask = MyAsyncTask() // 객체 생성
    }

    inner class MyAsyncTask: AsyncTask<Void?, Int?, String>(){
        var loopFlag = true
        var isRun = false

        // 스레드에 의해 백그라운드에서 처리될 내용을 담는다. 
        override fun doInBackground(vararg p0: Void?): String {
            var count = 10
            while(loopFlag){
                SystemClock.sleep(1000)
                if(isRun){
                    count--
                    publishProgress(count) // onProgressUpdate
                    if(count == 0){
                        loopFlag = false
                    }
                }
            }
            return "Finish!!" // onPostExecute
        }

        // doInBackground에서 publishProgress 함수로 넘긴 값이 전달됨. 
        override fun onProgressUpdate(vararg values: Int?) {
            super.onProgressUpdate(*values)
            textView.text = values[0].toString() // count 값 
        }

        // doInBackground 함수의 최종 결과 값을 받기 위해 사용함. 
        override fun onPostExecute(result: String?) {
            super.onPostExecute(result)
            textView.text = result // Finish 문자열 
        }
    }
}

Coroutine

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:version'
  • Non-Blocking lightweight thread
  • 가벼운 스레드
  • 메모리 누수가 적다.
  • 취소 등 다양한 기법을 지원한다.
  • 많은 JetPack 라이브러리에 적용되어 있다.
val backgroundScope = CoroutineScope(Dispatchers.Default + Job())
  • 스코프: 코루틴이 실행되는 영역
  • CoroutineScope의 구현 클래스
  • GlobalScope, ActorScope, ProducerScope 등 제공
backgroundScope.launch {
	// 시간이 오래 걸리는 작업은 Default 또는 IO 디스패처 
    // 뷰를 건드리는 작업은 Main 디스패처 
} 
  • Dispatchers.Main: 액티비티의 메인 스레드에서 동작하는 코루틴을 만들기 위한 디스패처
  • Dispatchers.IO: 파일 입출력, 네트워크 작업 등에 최적화 된 디스패처
  • Dispatchers.Default: CPU를 많이 사용하는 작업을 백그라운드에서 실행할 목적의 디스패처
  • Coroutine은 launch, async 등의 함수에 의해 실행된다.
  • launch, async는 Coroutine Builder

실습 예제

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click" />

    <TextView
        android:id="@+id/resultView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        android:textStyle="bold"/>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>
package com.tutorial.c49

import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

class MainActivity : AppCompatActivity() {
    val backgroundScope = CoroutineScope(Dispatchers.Default + Job())

    lateinit var button: Button
    lateinit var resultView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button = findViewById(R.id.button)
        resultView = findViewById(R.id.resultView)

        // 버튼 클릭하고 나서도 다른 뷰 (EditText)를 건드릴 수 있음.
        button.setOnClickListener {
            backgroundScope.launch {
                var sum = 0L
                // 시간이 오래 걸리는 작업
                var time = measureTimeMillis {
                    for(i in 1..2_000_000_000){
                        sum += i
                    }
                }
                // UI 작업은 메인 디스패처가 처리
                withContext(Dispatchers.Main){
                    resultView.text = "sum: $sum"
                }
            }
        }
    }
}

위의 예제를 코루틴으로 처리해주지 않으면, sum 연산이 진행 중일 때 뷰를 건드리면 ANR 문제가 발생할 수 있다.

profile
습관이 될 때까지 📝

0개의 댓글