본문 바로가기

Android

#Broadcast Receiver 비동기 백그라운드 작업 #doAsync #JobScheduler

반응형

Broadcast Receiver가 프로세스 상태에 미치는 영향

BroadcastReceiver가 onReceive() 메서드를 코드를 실행 중일 때, 해당 수신자는 포그라운드 프로세스로 간주됩니다. 메모리가 부족하지 않은 이상 시스템은 프로세스를 계속 실행합니다.

onReceive() 메서드가 반환되면 BroadcastReceiver는 비활성 상태로 전환되며, 프로세스의 우선순위는 다른 프로세스와 같아집니다. manifest에서 선언된 Static Receiver의 경우 onReceive() 반환 시 프로세스의 우선순위가 낮아지며, 리소스가 부족한 상황에서 우선순위가 높은 프로세스의 리소스 요청에 의해 이 프로세스가 종료될 수 있습니다.

BroadcastReceiver가 장시간 실행되는 백그라운드 작업을 수행한다고 가정해봅시다. 백그라운드 작업은 또다른 스레드에서 실행되고, onReceive()는 종료됩니다. 시스템은 프로세스 우선순위를 낮추고, 우선순위가 낮아진 프로세스는 예기치 않게 종료될 수 있습니다. 이 과정에서 실행 중인 백그라운드 스레드도 종료됩니다.

백그라운드 실행 시간이 비교적 짧은 경우에는 goAsync()를 호출하여 해결할 수 있지만, 시간이 좀더 필요한 경우 Broadcast Receiver를 JobScheduler로 대체하여 JobService를 예약하여야 합니다. 프로세스 및 애플리케이션 수명 주기에 대한 자세한 사항은 다음 링크를 참고해주세요.

 

프로세스 및 애플리케이션 수명 주기  |  Android 개발자  |  Android Developers

대부분의 경우 모든 Android 애플리케이션은 자체 Linux 프로세스에서 실행됩니다. 이 프로세스는 일부 코드를 실행해야 할 때 애플리케이션용으로 생성되며 더 이상 필요하지 않고 시스템이 메모

developer.android.com

goAsync()로 BroadcastReceiver에서 백그라운드 작업하기

Added in API level 11
public final BroadcastReceiver.PendingResult goAsync ()

goAsync()는 애플리케이션이 onReceive(Context, Intent) 메서드에서 호출하여, onReceive(Context, Intent)가 반환된 이후에도 브로드캐스트를 활성 상태로 유지하도록 만드는 메서드입니다. 이는 브로드캐스트에 따른 응답 기대 사항을 바꾸지는 않지만, 디스크 IO로 인해 main UI 스레드에 문제가 생기지 않도록 관련 작업을 다른 스레드로 이동할 수 있습니다.

일반적으로 Broadcast Receiver는 시스템이 응답하지 않는 것으로 간주하고, Application Not Responding 에러를 발생시키기 전까지 최대 10 초 동안 실행될 수 있습니다. 게다가 Broadcast Receiver는 보통 메인 스레드에서 실행되기 때문에 작업 제한 시간은 ~5초로 줄어듭니다.

goAsync를 사용하면 메인 스레드에서는 벗어날 수 있지만 10초의 브로드캐스트 실행 제한은 계속 적용되며, 이 10초에는 메서드 호출과 PendingResult#finish() 사이에 소요된 시간 또한 포함됩니다. goAsync()를 이용하는 작업은 해당 작업이 완료될 때까지 추가 브로드캐스트를 차단하므로, 이 기능을 과도하게 활용하면 이후 이벤트가 더 느리게 수신될 수 있습니다.

다음 코드는 goAsync()를 사용하여 onReceive()가 완료된 후에도 작업을 완료하기 위해 시간이 더 필요하다는 플래그를 지정하는 예시입니다. goAsync()는 onReceive()에서 완료하려는 작업이 UI 스레드가 프레임을 놓칠 정도로 (>16ms) 작업을 백그라운드로 옮길 수 있습니다.

    private const val TAG = "MyBroadcastReceiver"

    class MyBroadcastReceiver : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent) {
            val pendingResult: PendingResult = goAsync()
            val asyncTask = Task(pendingResult, intent)
            asyncTask.execute()
        }

        private class Task(
                private val pendingResult: PendingResult,
                private val intent: Intent
        ) : AsyncTask<String, Int, String>() {

            override fun doInBackground(vararg params: String?): String {
                val sb = StringBuilder()
                sb.append("Action: ${intent.action}\n")
                sb.append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
                return toString().also { log ->
                    Log.d(TAG, log)
                }
            }

            override fun onPostExecute(result: String?) {
                super.onPostExecute(result)
                // Must call finish() so the BroadcastReceiver can be recycled.
                pendingResult.finish()
            }
        }
    }
    

JobScheduler로 BroadcastReceiver 대체하기

Added in API level 21
public abstract class JobScheduler extends Object

JobScheduler는 어플리케이션의 자체 프로세스에서 실행될 프레임워크에 다양한 유형의 작업을 예약하기위한 API입니다. 실행할 수 있는 작업 유형 및 구성 방법에 대한 자세한 설명은 JobInfo를 참조해주세요.

 

JobInfo  |  Android 개발자  |  Android Developers

 

developer.android.com

이러한 JobInfo 객체를 생성하고 schedule(JobInfo)을 사용하여 JobScheduler에 전달합니다. 선언된 기준이 충족되면 시스템은 애플리케이션의 JobService에서 이 작업을 실행합니다. JobInfo.Builder.Builder(int, ComponentName)를 사용하여 JobInfo를 생성 할 때 작업 로직을 구현하는 서비스 구성 요소를 식별합니다.

프레임워크는 작업 실행 시기를 지능적으로 결정하며, 가능한 한 일괄 처리 및 지연을 시도합니다. 일반적으로 작업에 기한을 지정하지 않으면 JobScheduler 내부 대기열의 현재 상태에 따라 언제든지 실행될 수 있습니다.

작업이 실행되는 동안 시스템이 wakelock을 유지합니다. 따라서 작업이 진행되는 동안 장치는 항상 켜져 있습니다.

이 클래스를 직접 인스턴스화하지 못합니다. 대신 Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)를 통해 인스턴스를 가져올 수 있습니다.

참고: API 30 (Build.VERSION_CODES.R)부터 JobScheduler는 엄청난 양의 CPU 시간을 부당하게 사용하는, "폭주" 애플리케이션을 제한합니다. schedule(JobInfo)과 기타 메서드를 매우 높은 빈도로 호출하는 경우 비용이 많이들 수 있으며, 시스템이 과부하되지 않도록 JobScheduler는 대상 SDK 버전에 관계없이 앱의 성능을 조절하기 시작합니다.

다음 코드는 JobScheduler를 이용하여 NETWORK_TYPE이 UNMETERED(무제한 연결)일 경우에 실행되는 작업을 보여줍니다. 작업 조건이 충족되면, 앱은 onStartJob() 메서드를 지정된 JobService.class에서 실행하기 위한 콜백을 수신합니다.

    const val MY_BACKGROUND_JOB = 0
    ...
    fun scheduleJob(context: Context) {
        val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
        val job = JobInfo.Builder(
                MY_BACKGROUND_JOB,
                ComponentName(context, MyJobService::class.java)
        )
                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                .setRequiresCharging(true)
                .build()
        jobScheduler.schedule(job)
    }
    

다음 샘플 코드는 시스템에서 콘텐츠 URI MEDIA_URI의 변경을 보고할 때 트리거할 작업을 예약합니다. 지정된 콘텐츠 URI에서의 변경을 시스템이 보고할 때, 앱이 콜백을 수신하고 JobParameters 객체가 MediaContentJob.class의 onStartJob() 메서드에 전달됩니다.

    const val MY_BACKGROUND_JOB = 0
    ...
    fun scheduleJob(context: Context) {
        val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
        val job = JobInfo.Builder(
                MY_BACKGROUND_JOB,
                ComponentName(context, MediaContentJob::class.java)
        )
                .addTriggerContentUri(
                        JobInfo.TriggerContentUri(
                                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
                        )
                )
                .build()
        jobScheduler.schedule(job)
    }

이외에도 JobScheduler는 Android 7.0부터 적용되는 브로드캐스트 제한 사항을 처리해야 할 때 사용합니다.

반응형