Chapter 8. Broadcast Receiver
Broadcast Receiver는 Observer Pattern을 안드로이드에서 구현한 방식이다. Observer Pattern에서는 일대다 관계에서 직접 호출하지 않고, 인터페이스를 통한 느슨한 결합을 통해 옵저버를 register/unregister하는 방법을 제공한다. 이 방식은 BroadcastReceiver에서도 마찬가지이다. BroadcastReceiver는 바로 Observer이고, 이벤트는 sendBroadcast()에 전달되는 Intent이다.
Context에는 registerReceiver()와 unregisterReceiver() 메서드가 있는데, 여러 컴포넌트(Activity, Service, Application)에서 사용될 수 있다. 각 컴포넌트는 실행 중인 상태에서 Broadcast를 받으려고 할 때 Broadcast Receiver를 등록한다.
Broadcast Receiver 구현
BroadcastReceiver에는 추상 메서드가 onReceive() 하나뿐이고 이 메서드를 구현하면 된다.
BroadcastReceiver는 ContentProvider와 마찬가지로 ContextWrapper 하위 클래스가 아니다. 그렇지만 Context는 전달되므로 startService(), startActivity() 외에 sendBroadcast()를 다시 호출할 수도 있다.
Broadcast 발생 시 BroadcastReceiver를 거쳐서 Service나 Activity 시작
특정 이벤트가 발생할 때, sendBroadcast()를 통해서 Broadcast가 전달되고, 이때 화면을 띄우려면 BroadcastReceiver의 onReceive() 메서드에서 startActivity()를 실행한다. UI가 없는 내부 작업이 필요하다면 startService()를 실행한다.
onReceive() 메서드는 Main Thread에서 실행
onReceive() 메서드는 Main Thread에서 실행되므로 시간 제한이 있다. 10초(Foreground)/1분(Background: 기본) 내에 onReceive() 메서드는 실행을 마쳐야 한다. 10초/1분이 넘으면 ANR이 발생한다.
하나의 앱에서 단일 이벤트에 여러 BroadcastReceiver가 등록되어 있다면 여러 BroadcastReceiver의 onReceive() 메서드가 순차적으로 하나씩 실행되기 때문에 UI 동작에 문제가 생길 수 있다.
onReceive()에서 Toast 띄우기는 문제가 있음
BroadcastReceiver에서 Toast를 띄우면 잘 동작할까?
동작할 수도 있고 아닐 수도 있다. Toast는 비동기 동작이다. 앱이 Foreground Process라면 Toast는 정상적으로 잘 동작한다. 하지만 Background Process이거나 앱에서 실행 중인 컴포넌트가 BroadcastReceiver밖에 없다면, onReceive() 메서드가 끝나자마자 프로세스 우선순위에 밀려서 프로세스가 종료될 수 있다.
onReceive()에서 registerReceiver()나 bindService() 메서드 호출이 안 됨
onReceive() 메서드에 Context가 전달되지만, Context의 메서드인 registerReceiver()나 bindService()를 호출하면 Runtime Error를 발생시킨다.
onReceive()에 전달된 Context는 구체적으로 ContextWrapper인 Application을 다시 감싼 ReceiverRestrictedContext 인스턴스이다. ReceiverRestrictedContext는 ContextImpl의 내부 클래스이면서 COntextWrappter를 상속하고 registerReceiver()와 bindService()를 오버라이드해서 예외를 발생하게 한 것이다.
API 52 기준으로 확인 했을때, registerReceiver() 메서드는 동작함
Broadcast Receiver 등록
BroadcastReceiver를 등록하는 방식은 statically publish과 dynamically register 2가지가 있다.
Statically publish Boradcast Receiver
statically publish는 AndroidManifest.xml에 BroadcastReceiver를 추가하는 것이다. statically publish으로 만든 receiver는 broadcast가 발생하면 항상 반응한다. 주로 시스템 이벤트를 받을 때 많이 사용하는 것으로 앱이 실행 중이지 않더라도 프로세스가 뜨고서 이벤트를 처리한다.
외부 프로세스의 이벤트를 받는 BroadcastReceiver를 만들 때가 많기 때문에 샘플처럼
intent-filter를 추가해서 암시적 인텐트를 전달 받는다. 하지만 로컬 프로세스에서만
사용하는 경우에는 intent-filter를 넣지 않고 명시적 인텐트를 전달 받을 수 있다.
Intent 클래스에 정의된 Broadcast Action
시스템 이벤트는 앱에서 발생시킬 수 없음
문서를 보면 'This is a protected intent that can only be sent by the system.'라는 메시지가 많이 나온다. 즉 해당 인텐트는 시스템에서만 발생시킬 수 있고 앱에서 발생시킬 수 없다.
아래는 sendBroadcast(new Intent(Intent.ACTION_BOOT_COMPLETED)) 를 실행한 결과 예외 스택이다.
자주 쓰이는 시스템 이벤트
캘린더 앱의 경우 처리해야 하는 이벤트로는 ACTION_TIMEZONE_CHANGED, ACTION_LOCALE_CHANGED 액션이 있다. 많은 앱에서 처리하는 이벤트로는 ACTION_BOOT_COMPLETED 액션이 있다.
시스템 이벤트가 아닌 것도 정의되어 있음
Intent 클래스에 정의된 Broadcast Actino이 모두 시스템 이벤트인 것은 아니다. ACTION_MEDIA_SCANNER_SCAN_FILE 액션 같은 것은 앱에서 발생시키라고 있는 것이다. 이미지를 SD 카드에 저장했는데 Media Scanning이 안 돼서 곧바로 화면에 가져올 수 없는 경우가 있다. 이때 앱에서 사용하는 방식이 ACTION_MEDIA_SCANNER_SCAN_FILE 액션을 Broadcast해서 Media Sacanner가 동작하게 하는 것이다.
Dynamically register Broadcast Receiver
Context의 registerReceiver() 메서드로 BroadcastReceiver를 동적으로 등록한다. 이는 앱 프로세스가 떠 있고 BroadcastReceiver를 등록한 활성화된 컴포넌트가 있을 때만 동작하는 것이다. BroadcastReceiver는 unregisterReceiver() 메서드에서 해제한다. 일반적으로 Activity에서는 Foregournd Life Time인 onResume()/onPause()에서 registerReceiver()/unregisterReceiver()를 호출한다.
볼륨 변경 시 Broadcast 처리
여기서 VOLUME_CHANGED_ACTION 액션은 안드로이드 API 문서에 없는 내용이다. 코드상으로는 AudioManager 클래스에 상수로 있는데 @hide 애너테이션으로 숨겨져 있다. EXTRA_VOLUME_STREAM_VALUE도 마찬가지로 숨겨져 있다. 이 샘플과 같이 액션명을 알 수 있다면 처리할 수 있는 케이스가 많다.
바탕화면에서 숏컷 설치
바탕화면에 숏컷(shortcut)을 설치하는 것도 Broadcast를 사용한다. com.android.launcher.action.INSTALL_SHORTCUT도 API 문서에 없는 액션이다.
숏컷 설치 Broadcast
여기에 필요한 퍼미션은 안드로이드 개발 문서에 나와있다.
FLAG_RECEIVER_REGISTERED_ONLY 상수
Intent에는 동적으로 등록된 BroadcastReceiver만 이벤트를 받게 하는 옵션도 있다. Intent의 setFlags() 메서드에 파라미터로 Intent.FLAG_RECEIVER_REGISTERED_ONLY를 전달하면 정적으로 등록된 BroadcastReceiver는 이벤트를 받지 못한다.
Ordered Broadcast
sendOrderedBroadcast(Intent intent, String receiverPermission) 메서드는 등록된 BroadcastReceiver 가운데 priority 값이 높은 순으로 전달한다.
- priority 넣는 방법
- AndroidManifest.xml에서 intent-filter에 android:priority의 값
- IntentFilter에 setPriority(int priority) 메서드 이용
BroadcastReceiver는 프로세스 간 통신이 필요하므로 가벼운 작업은 아니다. Ordered Broadcast는 여러 BroadcastReceiver 간에 결과를 넘겨가면서 계속 진행하는 용도보다는, 결과를 넘기다가 적정 시점이 되면 나머지 BroadcastReceiver를 skip하는 용도로 적합하다.
그룹 앱이 있을 경우(ex: 오피스) 파일 읽기를 시도한다. 문서 앱에서 읽을 수 있는 파일이 아니면 스프레드시트 앱에서 읽기를 시도하고, 여기서도 안 되면 마지막으로 프리젠테이션 앱으로 읽기를 시도한다. 이때 사용 가능한 방식이 Ordered Broadcast를 발생시키는 것이다. 각각의 앱에서 우선순위가 다른 BroadcastReceiver가 있으면 된다. 우선순위가 높은 BroadcastReceiver에서 정상적으로 처리될 때 abortBroadcast()를 호출하면 된다. 이때 다음 BroadcastReceiver에는 Broadcast가 전달되지 않는다.
Sticky Broadcast
sendStickyBroadcast()는 Intent를 시스템에 등록해놓고, 해당 Intent를 받을 수 있는 BroadcastReceiver가 새로 등록되면 이 시점에 BroadcastReceiver의 onReceive()가 호출된다. 즉, 이벤트를 먼저 발생시키더라도 이벤트 상태를 알고자 하는 BroadcastReceiver가 등록되면 이벤트를 받는다.
시스템에서 보내는 Sticky Broadcast
시스템에서 보내는 Sticky Broadcast는 Intent API 문서에서 'sticky broadcast'로 검색하면 확인할 수 있다.
Sticky Broadcast
- ACTION_BATTERY_CHANGED : 배터리 상태
- ACTION_DEVICE_STORAGE_LOW : 저장소 부족 여부
- ACTION_DOCK_EVENT : 도킹 상태
배터리 레벨 변경 Sticky Broadcast 처리
앱에서는 Sticky Broadcast를 권장하지 않음
앱에서는 sendStickyBroadcast() 메서드 호출이 권장되지 않는다. 롤리팝에서는 이 메서드의 지원이 중단하기도 했다(deprecated). 이벤트를 보내는 용도로 Sticky Broadcast를 쓰지 말고, BraodcastReceiver에서 Sticky Broadcast를 받아서 처리하는 데만 사용해야 한다.
Stciky Broadcast의 보안 문제
Sticky Broadcast는 시스템 메모리에 정보가 계속 남아 있다. 그래서 어디선가 정보를 알고 싶을 때 언제든지 빼낼 수가 있기 때문에 보안 문제를 초래할 수 있다.
여러 앱 간에 SSO(single sign-on) 기능을 구현한다고 하자. 한 앱에서 로그인하면 같은 아이디로 다른 앱에서도 자동 로그인되고, 명시적으로 로그아웃하면 다른 앱에서도 자동 로그아웃 되는게 SSO의 기본 기능이다.
SSO를 구현하기 위해 Intent에 로그인 여부와 로그인 아이디 등을 Sticky Broadcast로 전달하면 어떨까?
onResume()에서 registerReceiver()를 실행한다면, 앱이 Foreground로 올 때마다 최신 정보를 알 수 있고 그에 맞게 처리할 수 있다. 하지만 이 정보는 다른 앱에서도 읽을 수 있고, Intent 정보를 다른 것으로 바꿔치기 할 수도 있다. 이러한 문제 때문에 앱에서는 sendStickyBroadcast()를 쓰지 않는게 좋다.
LocalBroadcastManager 클래스
Context의 sendBroadcast() 메서드는 Binder IPC를 통해 ActivityManagerService를 거쳐야 하므로 속도에서 이점이 크지 않다. 또한 Intent 액션을 안다면 원치 않는 고샤에서도 이벤트를 받아서 예기치 않는 일을 할 가능성도 있다.
(sendBroadcast()에서는 setPackage() 메서드를 사용해서 원하는 패키지만 Broadcast를 전달할 수 있지만 이 방식은 ICS부터 동작한다)
프로세스 간에 Braodcast를 보낼 필요가 없다면 support-v4에 포함된 LocalBroadcastManager의 사용을 고려해야 한다.
LocalBroadcastManager는 로컬 프로세스에서만 이벤트를 주고받을 수 있다. ActivityManagerService를 거치지 않고 Singleton인 LocalBroadcastdManager에서 registerReceiver()와 sendBroadcast()를 실행한다.
LocalBroadcastManager 장점
- Broadcast하는 데이터가 다른 앱에서 catch되지 않아서 안전하다.
- Global Broadcast보다 속도가 빠르다.
LocalBroadcastManager 내부에서 Handler 사용
LocalBroadcastManager에 등록된 BroadcastReceiver의 sendBroadcast() 메서드는 BroadcastReceiver를 바로 실행하지 않는다. Handler에 Message를 보내서 Main Thread에서 가능한 시점에 처리한다. 따라서 Main Looper의 MessageQueue에 쌓여 있는게 많다면 처리가 늦어질 수 있다.
sendBroadcastSync() 메서드
인증 에러 같은 것을 MessageQueue에서 처리하면 문제가 될 수 있다. BroadcastReceiver에서 바로 처리해야 하는데, 그렇지 않고 다른 작업을 먼저한다면 타이밍상 엉뚱한 결과를 만들어 내는 경우가 생긴다. 이때 쓰는 것이 sendBroadcastSync() 메서드이다. 등록된 BroadcastReceiver는 모두 그 순간에 처리하는데, 이때 sendBroadcastSync()와 onReceive()는 동일한 Thread에서 실행된다.
앱 위젯에는 Local Broadcast가 전달되지 않음
LocalBroadcastManager에서 sendBroadcast()를 호출할 때 BroadcastReceiver의 한 종류인 앱 위젯에는 이벤트가 전달되지 않는다. 앱 위젯은 홈 스크린에서 설치되어 별도 프로세스에 있으므로, Global Broadcast를 사용해서 이벤트를 전달해야만 한다. 다만 앱 위젯의 onReceive() 실행 위치는 다시 앱의 프로세스이다.
App Widget
앱 위젯(App Widget)은 다른 애플리케이션에서 내장되어서(embeded), 주기적으로 업데이트하는 작은 애플리케이션(miniature application views)이다.
App Widget의 특성
설치되는 프로세스와 실행되는 프로세스가 다름
설치되는 위치(launch process)와 실행되는 위치(app process)가 다르다. AppWidgetService(앱 위젯 목록 유지, 이벤트 발생)가 실행되는 system_server까지 포함하면 관련 프로세스는 모두 3개이다.
Broadcast를 통해 App Widget 변경
시스템(AppWidgetService)에서 sendBroadcast()를 호출하면 BroadcastReceiver의 onReceive() 메서드에서 앱 위젯에 작업을 하는 방식을 주로 쓴다.
AndroidManifest.xml
res/xml/appwidget_provider
AppWidgetProvider 클래스
App Widget은 BroadcastReceiver로도 만들 수 있지만 대체로 AppWidgetProvider를 상속해서 만든다. AppWidgetProvider는 내부적으로 onReceive()에서 Intent 액션으로 구분한 후 onUpdate(), onDeleted(), onEnabled(), onDisabled() 메서드로 Intent extra 값을 전달한다.
appWidgetId는 앱 위젯을 홈 스크린에 꺼낼 때마다 새로 받는 인스턴스 id 값이다. 동일한 앱 위젯이 홈 스크린에 여러 개 깔리더라도 인스턴스 id 값은 모두 다르다. 즉, 앱 위젯의 종류별로 있는 값이 아니라 전역적인 값이다.
- App Widget Action 종류
- ACTION_APPWIDGET_UPDATE : 부팅 시, 최초 설치 시, 업데이트 간격(update interval) 경과 시에 호출. 앱 위젯에서 가장 중요한 Intent 액션
- ACTION_APPWIDGET_DELETED : 인스턴스가 삭제될 때 호출
- ACTION_APPWIDGET_OPTIONS_CHANGED : 앱 위젯이 새로운 사이즈로 레이아웃될 떄 호출(젤리빈부터)
- ACTION_APPWIDGET_ENABLED : 최초 설치 시에 호출
- ACTION_APPWIDGET_DISABLED : 마지막 인스턴스가 삭제될 때 호출
ACTION_APPWIDGET_DELETED, ACTION_APPWIDGET_OPTIONS_CHANGED, ACTION_APPWIDGET_ENABLED, ACTION_APPWIDGET_DISABLED는 시스템에서만 보낼 수 있다(protected intent). 앱에서는 ACTION_APPWIDGET_UPDATE 액션만 보낼 수 있다.
RemoteViews 클래스
RemoteViews는 다른 프로세스에 있는 뷰 계층을 나타내는 클래스이다. 알림(Notification)이나 앱 위젯에서는 앱에서 만든 레이아웃을 다른 프로세스에 보여줄 때 RemoteViews가 사용된다. RemoteViews는 클래스명 때문에 일종의 ViewGroup으로 생각할 수 있다. 하지만 android.widget 패키지 안에 있을 뿐 View나 ViewGroup을 상속한 것이 아니다. RemoteViews는 Parcelable과 layoutInflater.Filter 인터페이스를 구현한 클래스일 뿐이다.
Toast도 내부적으로 Notification과 동일하게 system_server 프로세스의 NotificationManagerService를 사용한다. 그런데 Toast는 왜 커스텀 레이아웃으로 만들 때 RemoteViews를 쓰지 않을까?
Toast는 바인더 콜백을 전달하고 바인더 콜백에서 띄우는 것이기 때문에 RemoteViews가 필요하지 않다.
RemoteViews에 쓸 수 있는 뷰 클래스
RemoteViews의 레이아웃에 쓸 수 있는 뷰 클래스는 한정되어 있는데, LayoutInflater.Filter 인터페이스의 onLoadClass() 메서드가 뷰 클래스를 제한한다. LayoutInflater.Filter 구현체인 RemoteViews에서 booleanonLoadClass(Class clazz) 메서드를 보면 클래스 선언에 @RemoteView 애너테이션이 있는 것만 true를 리턴한다.
-
@RemoteView 애너테이션이 있는 뷰 클래스
- FrameLayout, LinearLayout, RelativeLayout, GridLayout
- AnalogClock, Button, Chronometer, ImageButton, ImageView, ProgressBar, TextView, ViewFlipper
- ListView, GridView, StackView, AdapterViewFlipper(허니콤부터 지원)
-
빈번하게 발생하는 시행착오
- 애너테이션은 상속된 클래스에는 적용되지 않기 때문에 위 목록에 있는 클래스의 하위 클래스도 RemoteViews에는 쓸 수 없다.
- Custom View도 쓸 수 없다. 클래스 선언에 @RemoteView를 추가하면 될 것 같지만, 설치되는 프로세스인 launcher에서 이 Custom View를 찾을 방법이 없기 때문이다.
- 최상위 클래스인 android.view.View는 쓸 수 없다. 레이아웃에 내용을 구분하기 위한 단순 라인 구분자(line separator)를 만들 때는 View에 배경색(background color)을 넣으면 됐지만, RemoteViews에서는 TextView처럼 지원되는 뷰 클래스로 대체해야 한다. 이 경우 레이아웃에서 Lint 경고를 볼 수 있는데 앱 위젯에서는 다른 방법이 없기 때문에 이런 경고는 무시해도 된다.
뷰 클래스에서 사용 가능한 메서드
RemoteViews의 메서드 가운데서 두 번째 파라미터에 methodName 문자열이 전달되는 것이 있는데, 리플렉션을 통해 새 번째 파라미터 값을 두 번째 파라미터인 methodName 이름의 메서드에 파라미터로 전달하게 된다.
[참고] Java Reflection
Update App Widget
RemoteViews를 써서 앱 위젯을 업데이트하는 코드
그렇다면 앱 위젯을 갱신하는 이 루틴이 반드시 BroadcastReceiver에서 실행되어야 할까?
그렇지는 않다. AppWidgetManager.getInstance(Context context)를 호출하면 Context가 전달되는 어디서든 AppWidgetManager를 얻을 수 있기 때문에, Activity나 Service에서도 동일하게 루틴을 실행할 수 있다. 게다가 Background Thread에서 루틴을 실행해도 문제가 없다. 이것이 Service의 Background Thread에서 앱 위젯을 업데이트해도 되는 이유이다.
updateAppWidget() 호출 스택
Parcelable인 RemoteViews는 계속해서 파라미터에 전달되고, AppWidgetHostView는 Handler에 작업을 전달해서 RemoteViews의 액션 목록을 한꺼번에 처리한다.
Keeping Collection Data Fresh
출처 : Build an App Widget | Android Developers
유의할 점
Main Thread 점유
onReceive() 메서드는 당연히 Main Thread에서 실행된다. 따라서 여기서도 실행 시간에 주의해야 한다. Foreground에서 앱을 사용하는 중에, 앱 위젯을 업데이트하기 위해 onReceive()가 실행된다면 UI 동작이 버벅거리는 원인이 될 수 있다.
onReceive() 메서드 내에서 앱 위젯을 업데이트하기 위해 네트워크 통신이 필요하거나 DB에서 가져올 데이터가 많아 처리 시간이 많이 예상되는 경우에는 어떻게 할 것인가?
이런 경우에도 앱 위젯 갱신 작업을 Service로 넘기고 Service에서는 Background Thread에서 처리해야 한다.
onReceive 메서드에서 AsyncTask를 실행해서 앱 위젯을 업데이트하는 경우가 있는데, BroadcastReceiver는 onReceive() 메서드가 return되면 프로세스가 제거될 수 있기 때문에 실행을 보장할 수 없다. 앱의 Activity가 Foreground에 있다면 프로세스 우선 순위가 높아서 거의 문제가 되지 않는다. 그런데 앱 위젯에 설정한 업데이트 간격(update interval)이 되었거나 특정 Broadcast에 반응해서, 다른 컴포넌트가 실행 중인 것이 없이 BroadcastReceiver가 단독으로 실행된다면 어떨까?
onReceive() 메서드가 끝나자마자 빈(empty) 프로세스로 우선순위가 낮아지는데 이 때문에 프로세스가 종료될 가능성이 있다. 따라서 AsyncTask 결과가 나올때까지 프로세스가 살아 있다는 것을 보장할 수 없다.
결론적으로 앱 위젯을 업데이트할 때 금방 실행되는 단순한 코드가 아니라면 Service에 넘겨서 Background Thread에서 실행하는 것을 권장한다.
부팅 중에는 initalLayout만 보임
단말 전원을 새로 켜면 홈 스크린 화면이 보인다. 사용자는 홈 스크린이 보이면 부팅이 완료된 것으로 생각하지만 디바이스에서 부팅이 완료되었다고 판단하는 시점과는 차이가 있다. 홈 스크린을 보여주고 나서 내부적으로 여러 작업을 한참 진행하고서야 부팅이 끝났다고 ACTION_BOOT_COMPLETED 액션을 발생시키고, 그 이후에야 ACTION_APPWIDGET_UPDATE 액션을 발생시킨다.
화면이 처음 보일 때에 앱 위젯을 initalLayout 상태 그대로를 보여준다. 부팅이 끝나고서야 앱 위젯을 업데이트하므로, 일정 목록이 없다는 메시지가 보이다가 시간이 꽤 지나고서야 제대로 된 일정이 나타나게 된다.
업데이트할 내용이 있지만 보여줄 수 없는 문제를 보완하기 위해서 일반적으로 initialLayout을 만들 때, 보기 상태를 View.GONE으로 하고, ProgressBar를 포함한 로딩 메시지만을 기본으로 보이게 한다.
ICS부터 기본 패딩
ICS 이전에는 셀 경계선까지 꽉 채워서 앱 위젯이 배치됐었다. targetSdkVersion을 14(ICS) 이상으로 하면 앱 위젯 간에 구분을 확실히 하기 위해서 셀 경계와 앱 위젯 테두리 사이에 기본 패딩이 생긴다. 패딩 값은 launcher마다 다를 수 있다.
고해상도 단말에서 Bitmap 생성 시 메모리 문제
앱 위젯의 일반 용도는 단순한 정보를 보여주는 것이다. 그런데 캘린더나 시간표 같은 앱은 사용자 요구에 의해 앱 위젯의 사이즈가 4x4나 5x5가 되는 경우가 있다. 거의 전체 화면 정도에 정보를 보여주는 것이다. 그리고 RemoteViews에서 지원하는 일반적인 뷰 클래스로는 표현이 복잡해서 Bitmap을 사용하기도 한다. Bitmap에 내용을 그리고 ImageView에 setBitmap()을 실행하는 식이다. 게다가 홈 스크린에서는 화면을 크게 차지할 경우 가급적 바탕 화면도 보이도록 투명하게 만든다. 앞에 조건들이 충족된다면 아래 샘플처럼 Bitmap.createBitmap()으로 Bitmap을 생성하고 Bitmap에 그린다.
createBitmap()의 세 번째 파라미터는 투명 비트맵을 생성하기 위해서 ARGB_8888을 적용한 것이다. 그런데 이 옵션은 픽셀당 4 Bytes를 차지한다. 해상도가 2560x1440인 경우 화면을 거의 채운다고 할 때, createBitmap()만으로 14M 가량의 메모리를 사용하게 되는 셈이다. 앱 실행 중에 앱 위젯 업데이트가 발생한다면 OutOfMemoryError의 원인이 될 수 있다.
기존에는 문제가 없었는데 최신 단말에서 앱 위젯을 생성할 경우 Bitmap.createBitmap()에서 OutOfMemoryError가 발생한다면, 큰 사이즈의 비트맵을 생성한 것이 원인일 가능성이 높다. 단말 스펙이 높아지면서 해상도 역시 좋아지는데, 앱 프로세스의 가용 메모리가 비례해서 커지지 않기 때문에 발생하는 문제이다. 이때는 앱 위젯을 별도 프로세스로 분리하는 것을 고려해야 한다. 앱 위젯 개수가 많다면 그 개수만큼 프로세스를 분리하는 것보다, 서비스에 앱 위젯 업데이트 로직을 넘기고 서비스를 별도 프로세스로 분리하는 게 낫다.
remoteViews.setIamgeViewBitmap(R.id.image, bitmap)를 주석하고 remoteViews.setImageViewUri(R.id.image, Uri.fromFile(file))을 쓴 이유는, remoteViews.setIamgeViewBitmap(R.id.image, bitmap)에서는 바인더에 Bitmap을 전달할 때 Bitmap 사이즈가 크면 에러가 발생하기 때문이다(바인더 트랜잭션 버퍼 최대 크기가 1M). 따라서 일부러 파일을 생성해서 RemoteViews에 Uri로 전달하였다. 이런 방법은 Activity 간에 데이터를 전달할 때도 많이 쓰인다. 사진을 찍은 후 피호출자에 사진을 전달할 때 Intent Bundle에 사진 Bitmap을 담아서 전달하는게 아니라 사진의 파일 Uri를 전달하는 식이다.
Reference
Build an App Widget | Android Developers
안드로이드 클라이언트 Reflection 극복기 - VCNC Engineering Blog
Java Reflection 개념 및 사용법
'Book > 안드로이드 프로그래밍 Next Step(일부 공개)' 카테고리의 다른 글
[안드로이드 프로그래밍 Next Step] Chapter 5. Activity (0) | 2018.10.18 |
---|---|
[안드로이드 프로그래밍 Next Step] Chapter 3. Background Thread (0) | 2018.10.04 |
[안드로이드 프로그래밍 Next Step] Chapter 1. 안드로이드 프레임 워크 (0) | 2018.09.25 |
[안드로이드 프로그래밍 Next Step] 목차 (0) | 2018.09.25 |