외로운 Nova의 작업실

안드로이드 앱 프로그래밍 - 24(액티비티 ANR 문제와 코루틴) 본문

Programming/Kotlin - Android

안드로이드 앱 프로그래밍 - 24(액티비티 ANR 문제와 코루틴)

Nova_ 2023. 2. 13. 11:34

- ANR 문제

ANR(activity not response)는 액티비티가 응답하지 않는 오류 상황을 의미합니다. 액티비티를 작성할때 ANR을 고려하지않으면 앱이 수시로 종료될 수 있습니다. 액티비티가 사용자 이벤트에 5초이내에 반응하지않으면 ANR 오류가 발생합니다. 즉, 시스템의 수행 흐름에서 시간이 오래 걸리는 작업이 끝나지 않으면 사용자 이벤트에 반응하지 못합니다. 오래 걸리는 작업이 있다면 사용자가 기다려주면 되지만 사용자가 언제 터치할지 모르기때문에 항상 ANR 오류를 고려해야합니다. 특히 액티비티에서 시간이 오래 걸리는 대표적인 작업은 네트워크입니다. 물론 앱은 대부분 네트워크 통신을 지원하는 전문 라이브러리(Volley 또는 Retrofit2)를 사용해서 만들며 이 라이브러리를 사용할때는 개발자가 ANR 문제를 고려하지 않아도됩니다. 하지만 네트워크쪽이 아닌 다른 작업을 할때 오래 걸리는 작업이 있다면 ANR문제를 고려해야합니다.

 

- ANR 해결

ANR 문제를 해결하는 방법은 메인 스레드(액티비티 담당 스레드) 이외에 실행 흐름을 따로 만들어서 오래걸리는 작업을 담당하게 만들면됩니다. 이 스레드를 안드로이드 개발자들은 코루틴을 통해서 구현하고 있습니다.

 

- 코루틴

코루틴은 스레드처럼 비동기 처리방식으로 수행흐름을 여러갈래로 만들어 여러 작업을 함께 처리합니다. 스레드와 비슷하지만 다른점이 있습니다. 아래는 코루틴의 특징입니다.

  1. 경량입니다.
  2. 메모리 누수가 적습니다.
  3. 취소 등 다양한 기능을 지원합니다.
  4. 많은 제트팩 라이브러리에 적용되어 있습니다.

코루틴은 스코프라는 곳에서 일어납니다. 스코프는 성격이 같은 코루틴을 묶는 개념이고, 한 스코프에서 여러 코루틴을 구동시킬 수 있습니다. Coroutine Scope 는 새로운 코루틴을 생성함과 동시에 실행되어야 할 Job을 그룹핑합니다. 그래서 하나의 작업이 끝나고 다른 작업을 호출하다가 실패하게 된다면 전체가 취소 처리 됩니다.

CoroutineScope(Main).launch {
    // do something
}

CoroutineScope(IO).launch {
    // do something
}

CoroutineScope(Default).launch {
    // do something
}

코루틴 컨텍스트 CoroutineContext 에는 Main, IO, Default의 세 가지가 있습니다.

  • Main은 말 그대로 메인 쓰레드에 대한 Context이며 UI 갱신이나 Toast 등의 View 작업에 사용됩니다.
  • IO는 네트워킹이나 내부 DB 접근 등 백그라운드에서 필요한 작업을 수행할 때 사용됩니다.
  • Default는 크기가 큰 리스트를 다루거나 필터링을 수행하는 등 무거운 연산이 필요한 작업에 사용됩니다.

 

- ANR이 일어나는 코드

실제 ANR을 일어나게 코드를 작성해보겠습니다. 먼저 메인 액티비티 kt 파일입니다. 1부터 90억까지 더하는 코드입니다.

package com.example.undersatnd_intetn

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import com.example.undersatnd_intetn.databinding.ActivityMainBinding
import kotlin.system.measureTimeMillis
import kotlin.time.measureTime

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var Binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(Binding.root)

        Binding.routineBtn.setOnClickListener(){
            var sum = 0L
            var time = measureTimeMillis {
                for (i in 1..9_000_000_000){ //1에서 90억까지 연산
                    sum += i
                }
            }
            Log.d("log", "time : $time")
            Binding.resultView.text = "sum : $sum"
        }
    }
}

아래는 xml 파일입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/result"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/routine_Btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/result_view"
        android:layout_width="107dp"
        android:layout_height="135dp"
        android:text="result_view"
        android:textAlignment="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/routine_Btn"
        app:layout_constraintVertical_bias="0.099" />
</androidx.constraintlayout.widget.ConstraintLayout>

이제 실행하고 연산이 되는동안 클릭이벤트를 발생시켜 ANR이 발생하는지 보겠습니다.

ANR이 발생하는 것을 알 수 있습니다.

 

- 코루틴 이용

위 코드를 코루틴을 이용해서 ANR이 발생하지않도록 변경해보겠습니다. 먼저 그래들파일에 코루틴 사용을 등록해야합니다.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

이제 코루틴으로 코드를 수정해보겠습니다. 메인액티비티.kt 파일입니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var Binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(Binding.root)

        val chennel = Channel<Int>() //채널을 통해 코루틴끼리 데이터 이동이 됩니다.

        CoroutineScope(Main).launch {
            chennel.consumeEach { //채널을통해 데이터를 받습니다.
                Binding.resultView.text = "sum : $it"
            }
        }
        CoroutineScope(Default).launch {
            var sum = 0L
            var time = measureTimeMillis {
                for(i in 1..9_000_000_000){
                    sum += i
                }
            }
            chennel.send(sum.toInt()) //채널로 데이터를 보냅니다.
        }
    }
}

한번 작동이 되는지 실행해보겠습니다.

1~90억 계산 처리시간동안 버튼 이벤트를 눌러도 반응이 오고, 한 15초정도 걸린거같은데 ANR이 발생하지 않은 걸 볼 수 있습니다.

Comments