안드로이드 스튜디오 :: Broadcast Receiver

Android/공통 2020.10.14 댓글 moonsu
728x90

브로드캐스트

특정 상황에서 시스템이나 다른 앱으로 메세지를 주고 받을 수 있는 개념이 Broadcast 이다. 예를들어 기기는 재부팅 또는 충전 시작과 같은 다양한 이벤트가 발생할 때 시스템은 브로드캐스트를 전송한다. 이 때 내가 만든 앱에서 이것을 수신할 수 있다. '배터리가 15% 이하다!' 라는 시스템의 브로드캐스트 메세지를 수신받아 '절전' 상태로 변경해주는 앱을 만들 수 있다는 얘기다. 반대로 내가 메세지를 전송하는 것도 가능하다.

 

먼저 Broadcast 를 수신하기 위한 두 가지 방법을 소개한다. 첫째는 manifest에 선언하는것, 둘째는 context에 등록하는 것.

 

1. manifest 에 선언

manifest에서 다음과 같이 작성한다.

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
</receiver>

(1) exported : 외부 앱이나 시스템에서 해당 서비스를 사용할 수 있는지 결정. default값은 true이며, false로 지정하면 오로지 앱 내부에서만 실행 가능하다.

(2) BOOT_COMPLETED : 기기가 부팅 될 때 Broadcast가 한번 호출된다.

(3) INPUT_METHOD_CHANGED : 입력 수단이 바뀔 때 호출된다.

(참조 :: 기타 안드로이드 스튜디오 :: Broadcast 관련 인텐트 액션)

 

앱이 실행되지 않은 상태일 때 manifest에 선언된 Broadcast 는 앱을 자동으로 실행시킨다. (BOOT_COMPLETED 액션에 의해서)

 

manifest에 선언 후 BroadcastReceiver 서브클래스를 선언, onReceive(Context, Intent) 를 구현한다.

Java 코드
public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        
        @Override
        public void onReceive(Context context, Intent intent) {
            // Broadcast로 호출할 메세지 작성
            StringBuilder sb = new StringBuilder();
            sb.append("Action: " + intent.getAction() + "\n");
            sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
            
            String log = sb.toString();
            
            // 메세지 호출
            Toast.makeText(context, log, Toast.LENGTH_LONG).show();
        }
}
Kotlin 코드
private const val TAG = "MyBroadcastReceiver"

class MyBroadcastReceiver : BroadcastReceiver() {
        
    override fun onReceive(context: Context, intent: Intent) {
        // Broadcast로 호출할 메세지 작성
        StringBuilder().apply{
            append("Action: ${intent.action}\n")
            append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
                
            // 메세지 호출
            toString().also { log ->
                Toast.makeText(context, log, Toast.LENGTH_LONG).show()
            }
        }
    }
}

앱이 설치되면 작성한 수신자가 등록되고 수신자는 앱으로 별도 진입할 수 있다. 한마디로 위 작성한 수신자를 통해 시스템이 앱을 실행시킬 수 있다는 의미이다. 수신자는 onReceive(Context, Intent) 가 지속되는 동안만 유효하며 임의로 반환시킬 수 있다.

 

2. Context에 등록

BroadcastReceiver 인스턴스를 생성 후 호출을 원하는 위치에 registerReceiver(BroadcastReceiver, IntentFilter) 를 호출하여 수신자를 등록한다.

더보기
// BroadcastReceiver 인스턴스 생성
BroadcastReceiver br = new MyBroadcastReceiver();

// IntentFilter 생성
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);

// 수신자 등록
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);
더보기
// BroadcastReceiver 인스턴스 생성
val br: BroadcastReceiver = MyBroadcastReceiver()

// IntentFilter 생성
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
    addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
}

// 수신자 등록
registerReceiver(br, filter)

context 에 등록된 브로드캐스트는 수신을 등록하고 취소하는 과정이 중요하다. 예를 들어 onCreate(Bundle) 에 수신자를 등록했으면 다른 context 에서 수신자가 유출되지 않도록 onDestroy() 에서 수신자 등록을 취소해야하고, onResume() 에 수신자를 등록했으면 onPause() 에서는 등록을 취소해 여러 번 등록되지 않도록 해야한다. 이렇게 하면 낭비를 줄일 수 있다.

 

수신을 중지하려면 unregisterReceiver(android.content.BroadcastReceiver) 를 호출한다. 하지만 onSaveInstanceState(Bundle) 에서 등록을 취소해선 안된다. 사용자가 스택으로 되돌아갔을 때 호출되지 않기 때문이다.

 

3. manifest와 context의 차이

manifest는 정적, context는 동적 리시버이다.

정적 리시버는 수신을 해제할 일이 거의 없을 때, 동적 리시버는 수신의 등록, 해제가 빈번하게 일어날 때 각각 유리하다. 동적 리시버는 등록한 context의 메서드나 변수 접근에 용이하지만 메모리 낭비를 막기 위해 일을 마치면 해제를 반드시 시켜줘야한다. 리시버가 해제 되지 않고 반복적을 등록되면 앱중지 현상이 발생한다. 정적 리시버는 앱을 종료해도 계속해서 실행되어야 하는 서비스(알람, 메신저 등)에 사용된다. 정적 리시버 역시 반복적인 작업은 피해야 하며 작업 시간이 오래걸리는 일은 피하는 것이 좋다.

 

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

BroadcastReceiver 는 수신자가 등록될 때 포그라운드 프로세스로 간주된다. 메모리 부족이 심한 상황을 제외하면 시스템은 프로세스를 계속 실행한다. 하지만 코드가 onReceive() 에서 끝나면 수신자의 호스트인 BroadcastReceiver 가 비활성 상태가 되기 때문에 시스템은 해당 수신자를 우선 순위가 낮다고 판단하여 다른 프로그램의 수신자를 사용할 수 있도록 프로세스를 종료시킬 수 있다.

 

이러한 이유로 BroadcastReceiver 를 이용하여 작업 시간이 오래 걸리는 일을 해선 안되지만 goAsync() 를 호출해 프로세스가 계속해서 일하고 있음을 시스템에게 알려줘 작업 시간을 늘릴 수 있다.

더보기
public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        
        @Override
        public void onReceive(Context context, Intent intent) {
            // 시간이 오래 걸리는 작업임을 알려준다.
            final PendingResult pendingResult = goAsync();
            
            /*
               작업 -----
            */
            
            // finish() 를 반드시 호출해야 한다.
            pendingResult.finish();
        }
}
더보기
private const val TAG = "MyBroadcastReceiver"

class MyBroadcastReceiver : BroadcastReceiver() {
        
    override fun onReceive(context: Context, intent: Intent) {
            // 시간이 오래 걸리는 작업임을 알려준다.
            val pendingResult: PendingResult = goAsync()
            
            /*
               작업 -----
            */
            
            // finish() 를 반드시 호출해야 한다.
            pendingResult.finish();
    }
}

 

시간이 오래 걸리는 작업은 별도의 스레드를 생성해 작업하는 경우가 많다. 아래 코드는 BroadcastReceiver 에서 비동기식 스레드인 AsyncTask 를 통해 작업하는 것을 보여준다. 주의깊게 봐야할 것은 작업이 끝난 이후 (onPostExecute) 반드시 finish() 함수를 호출한다는 점이다.

더보기
    public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";

        @Override
        public void onReceive(Context context, Intent intent) {
            final PendingResult pendingResult = goAsync();
            Task asyncTask = new Task(pendingResult, intent);
            asyncTask.execute();
        }

        private static class Task extends AsyncTask<String, Integer, String> {

            private final PendingResult pendingResult;
            private final Intent intent;

            private Task(PendingResult pendingResult, Intent intent) {
                this.pendingResult = pendingResult;
                this.intent = intent;
            }

            @Override
            protected String doInBackground(String... strings) {
                StringBuilder sb = new StringBuilder();
                sb.append("Action: " + intent.getAction() + "\n");
                sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
                String log = sb.toString();
                Log.d(TAG, log);
                return log;
            }

            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
                
                // finish() 를 반드시 호출해야 한다. 
                pendingResult.finish();
            }
        }
    }
더보기
    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)
                
                // finish() 를 반드시 호출해야 한다.
                pendingResult.finish()
            }
        }
    }
728x90
반응형

댓글