오늘은 코루틴 스코프에 대해 정리 해보려 한다.
코루틴 스코프는 코루틴이 언제 어디서 실행 될지, 그리고 어떻게 생명주기를 관리할지를 결정하는 데 사용된다.
실행 범위와 생명주기를 제어하는건데, 다양한 종류의 스코프가 있다.
애플리케이션의 생명주기와 연결되어 있다.
코루틴은 앱이 종료될 떄까지 계속 존재할 수 있는데, 메모리 누수의 위험이 있으니 함부로 쓰지 않는걸 권장한다.
주로 앱의 백그라운드에서 지속적으로 데이터를 수집하거나 업데이트하는 작업,
예를 들면 사용자의 위치를 주기적으로 추적하고 데이터베이스에 저장하는 서비스등을 구현할때 사용된다.
GlobalScope.launch {
while(isActive) {
updateLocationData()
delay(1000L) // 1초마다 반복
}
}
가장 기본적인 코루틴 스코프다.
사용자가 커스텀해서 사용할 수 있는 장점이 있다.
선언부를 들어가보면 주석으로 다음과 같이 설명되어 있다.
지정된 코루틴 콘텍스트를 감싸는 코루틴 범위를 만듭니다. 지정된 콘텍스트에 Job 요소가 없으면 기본 Job()이 생성됩니다. 이렇게 하면 이 범위에서 하위 코루틴이 실패하거나 범위 자체를 취소하면 coroutineScope 블록 내와 마찬가지로 모든 스코프의 하위 코루트가 취소됩니다.
CoroutineScope(Dispatchers.IO).launch {
//code
}
다중 job설정 또한 가능하며 Job을 설정하면서 스코프를 제어할 수 있다.
class DownloadManager {
private var job: Job = Job()
private val scope = CoroutineScope(Dispatchers.IO + job)
fun startDownload(url: String) {
scope.launch {
downloadFile(url)
}
scope.launch {
downloadImage(url)
}
}
fun cancelDownload() {
job.cancel()
}
}
다음과 같이 선언한경우 cancelDowload함수를 이용해서 downloadFile과 downloadImage를 같이 취소 할 수 있다.
cancel이외에도
job.isActive 를 통해 코루틴이 실행 중인지 확인할 수 있다.
job.isCancelled 를 통해 코루틴이 취소되었는지 확인할 수 있다.
job.isCompleted 를 통해 코루틴이 완료되었는지 확인할 수 있다.
안드로이드 컴포넌트의 생명주기에 맞춰 비동기 작업을 실행 할 때 사용한다.
컴포넌트의 생명주기에 따라 코루틴을 취소해 메모리 누수를 방지한다.
액티비티나 프래그먼트에서 인터페이스를 업데이트 할 때 주로 사용된다.
코드에서 선언부를 보면 다음과 같이 정의 되어 있다.
coroutineScope는 이 라이프사이클 소유자의 라이프사이클과 연계되어 있습니다.
라이프사이클이 destoryed 되면 이 스코프가 취소됩니다.
이 스코프는 Dispatchers.Main.immediate에 바인딩되어있습니다
소유자는 선언되는 액티비티나 프래그먼트를 말한다.
라이프사이클이 destoryed 되면 이 스코프가 취소됩니다 라고 되어 있는대
lifecycleScope.launch(Dispatchers.IO){
repeat(MAX_VALUE) {
Log.D("", "on")
}
}
이렇게 선언하고 앱을 끄면 어떨까?
로그를 보면 앱을 꺼도 계속 찍힐 것 이다.
이미 반복문이 수행되고 나면 작업이 완료될때까지 수행하기 때문에 반복되는 중간에 끊기진 않는다.
lifecycleScope.launch(Dispatchers.IO){
flow{
list.forEach { item ->
emit(item)
delay(1000)
}
}.collect { user ->
Log.d("",user)
}
}
Flow를 collect하는 경우엔 앱이 종료되는 순간 멈추게 된다.
Flow와 lifecycleScope를 사용할 땐 주의가 필요하다.
위에서도 설명 했듯 onDestroy시 Job이 cancel된다고 한다.
그말은 즉 onStop 되었을때고 Job이 계속 수행된다고 볼 수 있다.
그게 왜 문제가 될까?
class MainActivity: Activity() {
private val exFlow = flow {
for (i in 0..100) {
emit("$i")
delay(1000)
}
}
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
exFlow.collect {
Log.d("",it)
1초에 1씩 증가하는 로그를 찍을때, 앱을 종료하지 않고 홈키를 눌러서 바탕화면으로 이동하면 어떻게 될까
그러면 Activity는 Destroyed가 되지 않고 onStop상태가 되어 계속 로직을 수행할 것 이다.
저게 만약 화면의 Ui를 구현하는 데이터 스트림이라면 리소스가 낭비되고 앱이 비정상 종료 될 수 있다.
이를 해결하기 위해 뷰모델의 생명주기에 맞춰 onStart에 Job을 실행하고 onStop되었을때 멈추게 할 수 도 있다.
override fun onStart() {
super.onStart()
job = lifecycleScope.launch {
exFlow.collect {
...
override fun onStop() {
super.onStop()
job?.cancel()
이렇게 구현하면 홈키를 눌러 화면을 이동할때 데이터 스트림이 발생하지 않는다.
이런 상황을 위해 API를 안드로이드 스튜디오에서 제공해주는 repeatOnLifecycle함수를 사용한다.
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
viewModel.someDataFlow.collect {
// Process item
}
}
}
}
}
디벨로퍼의 가이드에서 제공하는 코드이다
보면 lifecycleScope안에 repeatOnLifecycle을 사용해 코루틴 스코프를 관리하는걸 볼 수 있다.
선언부는 어떻게 구현되어 있을까?
// Runs the block of code in a coroutine when the lifecycle is at least STARTED.
// The coroutine will be cancelled when the ON_STOP event happens and will
// restart executing if the lifecycle receives the ON_START event again.
public suspend fun LifecycleOwner.repeatOnLifecycle(
state: Lifecycle.State,
block: suspend CoroutineScope.() -> Unit
): Unit = lifecycle.repeatOnLifecycle(state, block)
소유자의 Lifecycle이 최소 onStart가 되었을 때 블록에 있는 메서드들이 수행되며, onStop 되었을땐 Job이 취소 된다.
간단히 말하면 생명주기가 포그라운드에 있을때만 진행된다고 보면된다.