만족

[Android] CorotineScope 본문

[Android] CorotineScope

FrontEnd/Android Satisfaction 2021. 6. 14. 17:46

android에서 app의 스레드는 main thread와, sub thread가 있다.

 

기본 스레드는 main thread이고, 여기에서 ui update 가 진행된다.

 

 

하지만 file IO, network IO와 같이 오랫동안 스레드 상태를 block으로 만드는 작업들의 경우

해당 작업을 main thread에서 진행하면 ui update를 할 수 없는 상태가 되어

사용자가 바라보기에는 앱이 멈췄다고 인식하게 된다.

 

(사실 network io의 경우에는 아주 구버전의 안드로이드가 아니라면 main thread에서 아예 실행할 수 없다. 가능하다면 발생할 상황이다.)

 

 

따라서 오래 걸리는 작업들은 별도의 스레드를 이용하여

메인 스레드가 오랫동안 블락되지 않게 해 주어야 한다.

 

CorotineScope

기존에는 Thread를 직접 생성하거나, AsyncTask를 사용해 

main Thread와 sub Thread를 각각 핸들링 해주었는데

CorotineScope를 이용해 더 편하게 할 수도 있다.

 

class AsyncActivity : AppCompatActivity() {
    lateinit var binding: ActivityAsyncBinding
    //scope will be started in Main context
    val scope = CoroutineScope(Dispatchers.Main)

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

        binding = ActivityAsyncBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.apply {
            requestBtn.setOnClickListener {
                //launch with main context
                scope.launch {
                    var data = ""
                    connectProgressBar.visibility = View.VISIBLE

                    //switch context to io
                    withContext(Dispatchers.IO) {
                        //some network IO
                        data = loadNetworkSomething(URL(urlEditText.text.toString()))
                    }
                    //same withContext(Dispatchers.IO)...
//                    CoroutineScope(Dispatchers.IO).async {
//                        data = loadNetworkSomething(URL(urlEditText.text.toString()))
//                    }.await()

                    responseText.text = data
                    connectProgressBar.visibility = View.GONE
                }
            }
        }
    }
  }
  fun loadNetworkSomething(url: URL): String{
    //load data from network
    //...
    return "..."
  }
}

scope 부분만 따로 떼어서 보자

val scope = CoroutineScope(Dispatchers.Main)
...
scope.launch {
  //1
  var data = ""
  connectProgressBar.visibility = View.VISIBLE
  withContext(Dispatchers.IO) {
    //2
    //some network IO
    data = loadNetworkSomething(URL(urlEditText.text.toString()))
  }
  //3
  responseText.text = data
  connectProgressBar.visibility = View.GONE
}
...

 

1.

scope의 기본 컨텍스트는 Main이므로,

scope.lauch를 하면 Main Thread에서 실행된다.

 

2.

withContext를 이용해 지정한 블록을 IO 컨텍스트에서 실행한다.

이 블록에서는 어떤 네트워크 로딩 작업을 하게 된다.

 

3.

그리고 작업이 끝나면 다시 Main으로 돌아와서 ui update를 진행한다.

 

기존 방식(Sub Thread 내에서 ui 작업을 하려면 Handler나 runOnUiThread를 사용해야 했었다)에 비해

코드 줄수가 압도적으로 줄어들었고 가독성도 높아졌다.

 

Running other scope as Async in scope

위 코드에서는 실행 순서가 반드시 1->2->3이다.

(2번 작업이 끝날 때 까지 Main이 blocked되긴 하지만,

Main Thread 자체를 잠그는 것이 아니라 스코프 내에서만 잠그는 것이라 UI update가 멈추지는 않는다)

 

그러나 2번 블록(network io)을 비동기로 실행시켜, 1->3->2 순서 처럼로 작동하게끔 만들어야 할 때도 있다.

(ex: 여러 개의 비동기 작업을 처리해야 하는 경우)

 

scope.launch {
  //1
  var data = ""
  connectProgressBar.visibility = View.VISIBLE

  CoroutineScope(Dispatchers.IO).async{
    //2
    //some network IO
    data = loadNetworkSomething(URL(urlEditText.text.toString()))
    withContext(Dispatchers.Main){
      //2-1
      //some ui update
      responseText.text = data
      connectProgressBar.visibility = View.GONE
    }
  }
  CoroutineScope(Dispatchers.IO).async{
    //3
    //some network IO
    data = loadNetworkSomething(URL(urlEditText.text.toString()))
    withContext(Dispatchers.Main){
      //3-1
      //some ui update
      responseText.text = data
      connectProgressBar.visibility = View.GONE
    }
  }
  //4
  Toast.makeText(this@AsyncActivity, "로딩 중 입니다", Toast.LENGTH_LONG).show()
}

 

이런 식으로 작성하면 

1->4-> (2->2-1)-> (3-> 3-1) 또는

1->4-> (3-> 3-1)-> (2->2-1) 순서로 작동하게 된다.

 

물론 두 개의 IO context scope에 대해서는 동기화를 진행하지 않았기 때문에 3->2->3-1->2-1 처럼 작동될 수도 있는데,

아무튼 중요한 점은 스코프 내에서도 새로운 스코프를 만들어 자유자재로 컨텍스트를 이동할 수 있다는 것이다.

 



Comments