이동식 저장소

[Android Fundamentals] Broadcast Receiver - 1. Overview 본문

Primary/Android

[Android Fundamentals] Broadcast Receiver - 1. Overview

해스끼 2024. 7. 7. 17:46

안드로이드 앱은 broadcast 메시지를 보내거나, 시스템 또는 다른 앱에서 보낸 메시지를 수신할 수 있다. 예를 들어 안드로이드 시스템은 시스템이 부팅됐을 때나 충전이 시작됐을 때 등 시스템 이벤트가 발생할 때 broadcast를 보낸다. 일반 앱에서도 커스텀 이벤트가 발생할 때 broadcast를 보낼 수 있다. Publish-subscribe 패턴과 비슷하다.

 

Broadcast는 시스템 상황에 따라 조금 늦게 전달될 수도 있다. 응답시간을 줄여야 한다면 bound service를 고려하는 것이 좋다.

 

Broadcast를 수신하기 위해서는 receiver를 등록해야 한다. 시스템은 송신된 broadcast의 타입을 구독한 receiver에게 broadcast를 자동으로 전달한다.

 

종합하자면, 일반적인 user flow 밖에서 메시지를 교환하는 수단이라고 요약할 수 있다. 하지만 broadcast를 통해 작업을 너무 많이 실행하면 시스템 성능이 나빠질 수 있으므로 유의해야 한다.

시스템 broadcast

안드로이드 시스템에서 벌어지는 이벤트는 대부분 broadcast로 알려진다. Broadcast를 구독한 모든 receiver에 알림이 가는 방식.

 

Broadcast 메시지는 ``Intent`` 객체 안에 이벤트의 이름(``android.intent.action.AIRPLANE_MODE``)이 포함되는 형태로 주어진다. 이벤트의 종류에 따라 더 많은 정보가 주어질 수도 있다. 예를 들어 비행기 모드 메시지에는 비행기 모드가 켜졌는지(혹은 꺼졌는지)가 boolean 값으로 포함되어 있다.

 

전체 broadcast 목록은 Android SDK의 ``broadcast_actions.txt`` 파일에서 볼 수 있다. 각 broadcast마다 다음과 같이 상수 값이 할당되어 있다.

Android 14에서의 변경사항

Cache 상태인 앱에는 시스템 broadcast가 즉시 전달되지 않을 수 있다. Cache 상태란 딱히 필요하지 않은 프로세스를 뜻한다. (사용자에게 visible하지 않은 activity라던가)

 

예를 들어 ``ACTION_SCREEN_ON`` 등 중요도가 낮은 broadcast는 cache 상태에서는 전달되지 않는다. Cache 상태에서 빠져나와야만 broadcast가 전달된다.

 

Manifest에 선언된 중요도 높은 broadcast를 수신한 경우에는 cache 상태를 잠시 해제한다.

Broadcast 구독

Broadcast receiver를 등록하는 방법에는 manifest에서 등록하는 방법과 Context에서 등록하는 방법이 있다.

Manifest에서 등록

Receiver를 manifest에 등록하면, (앱이 실행되고 있지 않았을 때) 시스템이 앱을 실행하고 receiver를 호출한다.

Android 8.0(API 26) 이상에서는 (몇몇 예외를 제외하면) implicit broadcast의 receiver를 manifest에 등록할 수 없다. 보안상의 이유인 듯하다. 대신 scheduled job를 사용하는 것이 좋다.

Broadcast Receiver를 manifest에 등록하는 방법은 다음과 같다.

  1. Manifest에 ``<receiver>`` 객체를 선언한다.
    <!-- 시스템 혹은 다른 앱의 broadcast를 수신하는 경우에는,
         android:exported 값을 "true"로 설정해야 한다.         -->
         
    <receiver android:name=".MyBroadcastReceiver" android:exported="false">
        <intent-filter>
            <action android:name="APP_SPECIFIC_BROADCAST" />
        </intent-filter>
    </receiver>
  2. ``BroadcastReceiver``를 상속받아 ``onReceiver(Context, Intent)``를 구현한다. 상속받은 클래스의 이름은 manifest에 등록한 이름과 같아야 한다.
    private const val TAG = "MyBroadcastReceiver"
    
    class MyBroadcastReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            // do something with intent
        }
    }

앱이 설치될 때 receiver가 시스템에 등록된다. 이제 ``MyBroadcastReceiver``는 (앱이 실행되고 있지 않더라도) 시스템이 메시지를 전달할 수 있는 진입점의 역할을 하게 된다. 

 

메시지가 전달될 때마다 receiver 객체가 새로 만들어진다. Receiver는 ``onReceive()``가 실행되고 있을 때에만 valid하다. ``onReceive()``가 return한 후에는 active하지 않은 객체로 간주된다.

Context에서 등록

Context에서 등록한 receiver는 해당 context의 생명주기를 따른다. 예를 들어 activity에서 receiver를 등록했다면, activity가 destroy되기 전까지만 메시지를 받을 수 있다. Application context에 receiver를 등록하면 앱이 종료되기 전까지 메시지를 받을 수 있다.

 

Context에서 receiver를 등록하는 과정은 다음과 같다.

  1. Dependency에 AndroidX Core 라이브러리 1.9.0 이상 버전을 추가한다.
  2. ``BroadcastReceiver`` 객체를 만든다. 
    val receiver: BroadcastReceiver = MyBroadcastReceiver()
  3. ``IntentFilter`` 객체를 만든다. 매개변수에 구독할 broadcast 이름을 명시해야 한다.
    val filter = IntentFilter(APP_SPECIFIC_BROADCAST)
  4. Receiver를 다른 앱에 노출할 지 결정한다. 같은 앱의 broadcast만 수신한다면 ``RECEIVER_NOT_EXPORTED``를, 시스템이나 다른 앱의 broadcast를 수신한다면 ``RECEIVER_EXPORTED`` 값을 사용해야 한다.
    val listenToBroadcastsFromOtherApps = false
    val receiverFlags = if (listenToBroadcastsFromOtherApps) {
        ContextCompat.RECEIVER_EXPORTED
    } else {
        ContextCompat.RECEIVER_NOT_EXPORTED
    }
    참고로 블루투스나 전화 같은 시스템 broadcast는 highly privileged apps에서 보낸다. Highly privileged app은 안드로이드 프레임워크에 속해있긴 하지만, 시스템의 프로세스 ID로 실행되지는 않는다. Highly privileged app의 broadcast을 수신하고 싶다면 ``RECEIVER_EXPORTED``를 사용해야 한다.

    Receiver를 export하지 않으면 몇몇 시스템 broadcast와 같은 앱에서 보낸 broadcast를 수신할 수 있지만, highly privileged app의 메시지는 받을 수 없다.

    여러 개의 broadcast를 수신하려 한다면, export할 receiver와 not_export할 receiver를 구분하는 것이 좋다. Export된 receiver에는 모든 앱이 메시지를 보낼 수 있다는 사실을 기억하자. 앱이 공격받을 수도 있다.
  5. Receiver를 등록한다.
    ContextCompat.registerReceiver(context, receiver, filter, receiverFlags)
  6. 메시지를 그만 받고 싶다면 ``unregisterReceiver(BroadcastReceiver)``를 호출하자. Context가 종료될 때 receiver도 반드시 unregister해야 한다. 예를 들어 activity의 ``onCreate()``에서 등록했다면 ``onDestroy``에서 등록 해제해야 한다.

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

BroadcastReceiver의 실행 여부는 receiver가 실행되는 프로세스의 상태에도 영향을 미친다. ``onReceive()`` 함수가 실행 중일 때에는 정말 웬만하면 프로세스를 종료하지 않는다.

 

그러나 ``onReceive()``가 종료된 후에는 receiver가 deactivate되고, 프로세스의 상태도 컴포넌트(receiver)에 따라 달라진다. 프로세스에서 실행하는 receiver가 모두 manifest에서 선언된 receiver라면, 시스템에 의해 언제든지 프로세스가 종료될 수 있다.

 

따라서 receiver에서 ``onReceive()``보다 오랫동안 작업을 수행해서는 안 된다. ``onReceive()``가 return한 후에는 언제든지 프로세스가 종료될 수 있기 때문이다. 대신 ``JobScheduler``나 ``WorkManager`` 등을 사용하여 프로세스가 계속 작업을 수행하고 있음을 시스템에 알리는 것이 좋다.

 

Broadcast 보내기

세 가지 방법으로 broadcast를 보낼 수 있다.

순서대로 보내기

``sendOrderedBroadcast(Intent, String)`` 함수를 호출하면 receiver에게 순서대로 메시지를 전달할 수 있다. 다음 receiver가 실행될 때 이전 receiver의 결과를 전달할 수도 있고, broadcast를 아예 종료해서 다음 receiver의 실행을 막을 수도 있다.

 

Receiver가 실행되는 순서는 manifest에서 ``android:priority`` 속성으로 지정할 수 있다. 우선순위가 같은 receiver는 임의의 순서대로 실행된다.

임의의 순서대로 보내기

``sendBroadcast(Intent)`` 함수를 호출하면 모든 receiver에게 임의의 순서대로 메시지를 전달할 수 있다. 일반적으로 이 방법을 많이 사용한다. Receiver의 실행 순서가 정해져 있지 않으므로 더 효율적이지만, 대신 다른 receiver의 결과를 받을 수 없다.

Intent().also { intent ->
    intent.setAction("com.example.broadcast.MY_NOTIFICATION")
    intent.putExtra("key", "value")
    sendBroadcast(intent)
}

Broadcast 메시지는 ``Intent``로 전달된다. 메시지 이름은 Java 패키지 스타일로 명시해야 한다. Extra 값을 넣어 데이터를 전달할 수도 있다. ``setPackage(String)``을 사용하여 메시지를 전달할 앱을 제한할 수도 있다.

권한을 갖는 앱에게만 수신/송신

특정 권한을 갖는 앱에게만 메시지를 보낼 수도 있고, 반대로 특정 권한을 갖는 앱의 메시지만 받을 수도 있다.

송신

``sendBroadcast()``나 ``sendOrderedBroadcast()``를 호출할 때 권한 이름을 매개변수로 전달할 수 있다. Manifest에 해당 권한을 선언한 앱에서만 메시지를 수신할 수 있다. 시스템 권한과 커스텀 권한 모두 사용할 수 있다.

 

예를 들어 다음과 같이 메시지에 블루투스 권한을 설정하면,

sendBroadcast(Intent(BluetoothDevice.ACTION_FOUND), Manifest.permission.BLUETOOTH_CONNECT)

블루투스 권한을 선언한 앱의 receiver만 메시지를 받을 수 있다.

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

수신

Receiver를 등록할 때 권한 매개변수를 설정하면, 해당 권한을 갖는 앱에서만 receiver에게 메시지를 보낼 수 있다.

 

Manifest에서 등록한 receiver와 context에서 등록한 receiver 모두 가능하다. 다음과 같이 receiver에 블루투스 권한을 설정하면,

<receiver android:name=".MyBroadcastReceiver"
          android:permission="android.permission.BLUETOOTH_CONNECT">
    <intent-filter>
        <action android:name="android.intent.action.ACTION_FOUND"/>
    </intent-filter>
</receiver>
val filter = IntentFilter(Intent.ACTION_FOUND)
registerReceiver(receiver, filter, Manifest.permission.BLUETOOTH_CONNECT, null)

이 Receiver에는 블루투스 권한이 있는 앱의 메시지만 수신된다.

보안 이슈 & Best Practices

  • 여러 앱이 같은 broadcast를 구독하면, 이벤트가 발생했을 때 동시에 여러 개의 앱이 실행되어 시스템 성능이 하락할 수 있다. 영구적으로 등록되는 manifest보다는 일시적으로 등록되는 context에서 등록하자. ``CONNECTIVITY_ACTION`` 같은 몇몇 이벤트는 context에서 등록된 receiver에만 전달되기도 한다.
  • Implicit intent를 통해 민감한 정보를 보내지 말자. 어떤 앱이 수신할 지 모른다.
    • 메시지를 보낼 때 권한을 설정하거나,
    • 메시지를 받을 수 있는 앱의 패키지명을 제한하자. (``setPackage()``)
  • 메시지를 받을 때에도 마찬가지다.
    • 메시지를 받을 수 있는 앱의 권한을 설정하거나,
    • ``android:exported="false"``를 설정하여 같은 앱의 메시지만 수신할 수 있게 제한할 수 있다.
  • Broadcast의 이름은 시스템의 모든 앱이 공유하므로, action 이름이 중복되지 않게 조심하자. Action 이름을 패키지 경로처럼 작성하라는 것도 그 때문.
  • Receiver의 ``onReceive()``는 메인 스레드에서 동작하므로, 최대한 간단한 작업만을 수행해야 한다. 오래 걸리는 작업을 수행해야 한다면, receiver에서 스레드를 만들기보단 JobScheduler나 WorkManager 등을 사용하는 것이 좋다. 
    • ``onReceive()``에서 ``goAsync()``를 호출한 후 ``BroadcastReceiver.PendingResult``를 반환하면 ``onReceive()``가 종료된 후에도 계속 작업을 수행할 수 있다. 하지만 이 경우에도 (시스템 상황에 따라 다르겠지만) 10초 이상 작업을 수행하기는 어렵다.
    • 얌전히 WorkManager 쓰자. 참고로 WorkManager도 JobScheduler 기반으로 작동한다.
  • Receiver에서 activity를 시작하지 말자. 갑자기 여러 개의 activity가 시작되면 사용자가 어떻게 생각할까?
    • Activity를 보여주기보단 알림을 보내는 것이 가볍고 좋다.

 

참고자료

 

브로드캐스트 개요  |  Background work  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 브로드캐스트 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 앱은 게시-구독 디자인 패턴

developer.android.com

 

Comments