[Android] Background Limit 문제

leeeha·2022년 11월 21일
0
post-thumbnail

Background Limit

  • 안드로이드 앱은 액티비티, 서비스, 브로드캐스트 리시버, 컨텐츠 프로바이더 이 4개의 컴포넌트로 구성된다.
  • 안드로이드 8 버전 (API Level 26 버전)부터 서비스와 브로드캐스트 리시버의 백그라운드 작업에 제약이 가해졌다. (이전 버전에서는 앱의 화면이 출력되지 않은 상황에서 백그라운드 작업을 계속 처리할 수 있었음.)
    • 서비스: 너무 오랫동안 백그라운드에서 작업을 처리하면 성능상의 문제가 생길 수 있다.
    • 브로드캐스트 리시버: 어떤 시스템의 특정 상황에 의해 발생한 인텐트를 모두 실행시키므로, 불필요한 것도 실행시키는 문제가 생길 수 있다.
  • 이전에 백그라운드에서 처리가 가능했던 것들이 불가능해지기 시작했다.
    • 시스템 부팅이 완료되자마자 브로드캐스트 리시버를 실행할 수 있는데, 이 상황에서 서비스까지 실행시켜 어떤 업무를 처리하는 게 안드로이드 8 버전부터는 불가능해졌다.

Broadcast Limit

  • 브로드캐스트 리시버를 실행시키기 위해서는 sendBroadcast() 함수로 인텐트를 발생시키면 된다.
  • 그런데, 백그라운드 제약 문제로 브로드캐스트 리시버를 암시적 인텐트로 실행시키는 건 금지되었다.

매니페스트 파일에 정적으로 등록된 브로드캐스트 리시버를, 인텐트 필터 정보를 이용해 암시적 인텐트로 실행시키려고 하면 에러가 발생한다.

Background execution not allowed: receiving Intent { act=ACTION_RECEIVER flg=0x10 } to com.example.test

  • 단, 외부 앱에서는 클래스명 정보를 이용한 명시적 인텐트를 사용할 수 없기 때문에, 암시적 인텐트로 브로드캐스트 리시버를 실행시킬 수밖에 없다.
  • 내부 앱에서는 브로드캐스트 리시버를 명시적 인텐트로 실행시키면 되기 때문에, 백그라운드 제약 문제로 입는 피해는 없다.
  • 그리고, 코드 상에서 registerReceiver() 함수로 리시버를 동적 등록한 경우에도 암시적 인텐트로 리시버를 실행시킬 수 있다.

즉, 매니페스트 파일에 정적으로 등록된 브로드캐스트 리시버를 같은 어플리케이션 내에서 암시적 인텐트로 실행시키는 것만 금지되었다고 보면 된다.


Service Limit

  • 앱이 백그라운드 상태에 있을 때, 서비스를 실행시키기 위해서 인텐트를 발생시키면 에러가 발생한다. (명시적, 암시적 인텐트 모두)
  • 앱이 포그라운드 상태에 있다면 정상적으로 실행된다.

Not allowed to start service Intent { act=ACTION_OUTER_SERVICE pkg=com.example.test }: app is in background uid null

포그라운드 상태란?

  • 보이는 화면(액티비티)이 있는 경우
  • 화면이 보이지는 않지만, 앱이 포그라운드 서비스를 사용하는 경우
  • 외부 앱이 우리 앱의 서비스 중 하나에 바인드 되어 있거나, 컨텐츠 프로바이더를 사용하는 경우에는 우리 앱의 데이터를 외부 앱의 화면에 출력할 수 있기 때문에 포그라운드 상태라고 본다.

앱이 백그라운드 상태라고 하더라도, 아래의 경우에는 서비스가 정상적으로 실행된다.

  • 우선순위가 높은 Firbase Cloud Messaging (FCM)의 메시지 처리
  • SMS/MMS 메시지와 같은 브로드캐스트 수신
  • Notification에서 PendingIntent 실행

백그라운드 상태에서 서비스가 정상적으로 실행되게 하려면?

→ startForegroundService() 함수로 인텐트를 발생시키면 서비스가 정상적으로 실행되긴 하지만, 얼마 지나지 않아 다음과 같은 에러가 발생한다.

Context.startForegroundService() did not then call Service.startForeground()

결국 startForegroundService()에 의해 실행된 서비스는 빠른 시간 내에 startForeground() 함수를 호출하여 포그라운드 상태로 바꿔줘야 한다. 이때 startForeground() 함수의 매개변수는 Notification 객체여서 상태바에 알림이 뜨게 된다.


JobScheduler

  • JobScheduler라는 서비스를 이용하면 앱이 백그라운드 상태여도 업무를 처리할 수 있다.
  • 단, 명시된 조건에 맞는 경우에만 백그라운드 업무를 처리할 수 있다.
  • 네트워크 타입
  • 배터리 충전 상태
  • 특정 앱의 컨텐츠 프로바이더가 갱신되는 경우
  • 실행 주기
  • 최소 지연 시간

JobService

개발자가 매니페스트 파일에 서비스를 등록시킬 때,android.permission.BIND_JOB_SERVICE 퍼미션이 설정되어 있어야 한다.

onStartJob()

  • 백그라운드에서 처리되어야 하는 업무 로직을 갖고 있는 함수
  • 리턴값에 따라 다르게 동작
    • false: Job이 완벽하게 종료되었다는 의미 → onStopJob() 호출 X
    • true: Job이 아직 끝나지 않았다는 의미 → onStopJob() 호출 O

onStopJob()

  • 리턴값에 따라 다르게 동작
    • false: JobScheduler 등록 취소
    • true: 시스템에 JobScheduler 다시 등록

JobInfo

실행되는 조건을 JobInfo 객체에 담아서 시스템에 등록한다.

JobInfo.Builder(1, ComponentName(this, MyService::class.java)).run {
	setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
	jobScheduler?.schedule(build())
}

실습 예제

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tutorial.c79">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidLab">
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE"></service>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MyService.kt

package com.tutorial.c79

import android.app.job.JobParameters
import android.app.job.JobService
import android.util.Log

class MyService : JobService() {
    override fun onCreate() {
        super.onCreate()
        Log.d("haeun", "MyService... onCreate...")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("haeun", "MyService... Destroy...")
    }

    override fun onStartJob(p0: JobParameters?): Boolean {
        Log.d("haeun", "MyService... onStartJob...")
        return false // Job이 종료되었으므로 onStopJob은 호출되지 않음. 
    }

    override fun onStopJob(p0: JobParameters?): Boolean {
        Log.d("haeun", "MyService... onStartJob...")
        return false
    }
}

MainActivity.kt

package com.tutorial.c79

import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.ComponentName
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity

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

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            val scheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler
            JobInfo.Builder(1, ComponentName(this, MyService::class.java)).run {
                setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 와이파이
                scheduler.schedule(build())
            }
        }
    }
}

와이파이에 연결된 경우에만 MyService 함수들의 로그가 출력된다.

profile
습관이 될 때까지 📝

0개의 댓글