안드로이드 앱 개발 시 알림(Notification)을 커스터마이징하기 위해 RemoteViews
를 사용하게 됩니다.
이 RemoteView라는 건 쓸 수 있는 View 종류도 적으면서 툭하면 크래시 내뿜으면서 뻗는 개복치 같은 레이아웃입니다.
이RemoteViews
객체를 재사용할 경우 데이터 크기가 점점 증가하여 알림 서비스가 오류를 일으킬 수 있는데요.
오늘은 RemoteViews
객체 재사용 시 데이터 크기 증가 문제의 원인과 해결 방법을 설명해보겠습니다.
앱 개발 중, Foreground service와 RemoteView로 이루어진 Notification 생성했습니다.
이를 updateProgress 함수를 통해 1초마다 프로그레스바를 업데이트하여 진행도를 유저에게 알려주는 로직이 포함되어있습니다.
private suspend fun updateProgress(
remoteViews: RemoteViews,
expandedRemoteViews: RemoteViews,
eventData: EventData,
) {
withContext(Dispatchers.Main) {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
val currentEventData = eventData.current
val progress = calculateProgress(now, currentEventData)
expandedRemoteViews.setProgressBar(R.id.progressBar, 100, progress, false)
val updateNotification = buildNotification(remoteViews, expandedRemoteViews, eventData)
NotificationManagerCompat.from(context).notify(1, updateNotification)
}
}
. . .
fun buildNotification(. . .) {
NotificationCompat.Builder(. . .)
. . .
.setCustomContentView(remoteViews)
.setCustomBigContentView(expandedRemoteViews)
.build()
}
이 타이머를 돌리던 중 알림 데이터 크기 초과 오류가 발생했습니다. 로그는 다음과 같았습니다:
2024-07-06 19:01:43.027 2535-13342 NotificationService system_server E notification pkg : app.xxx.yyy has too high data size(179500) above 100000
2024-07-06 19:01:43.027 2535-13342 NotificationService system_server E notification key : 0|app.xxx.yyy.android|1|null|11031 has too high data size(179500) above 100000
이는 RemoteViews
객체가 데이터 크기 제한(100000)을 초과했음을 나타냅니다.
이런 어이없는 이슈가 발생하는 이유가 궁금하여 RemoteView가 뷰 객체에 데이터를 set할 때 어떤 로직이 내부에서 동작되는지 확인해보았습니다.
RemoteViews
객체를 재사용하면 데이터 크기가 증가하는 이유는 RemoteViews
가 내부적으로 변경 사항을 누적하여 저장하기 때문입니다.
RemoteViews
는 안드로이드의 View 시스템에서 원격으로 UI를 업데이트할 수 있는 방법을 제공하며, 이 과정에서 변경 사항을 명령(Command) 객체로 저장하고 이를 누적하여 처리합니다.
RemoteViews
의 내부 동작RemoteViews
는 변경 사항을 추적하기 위해 각 변경 사항을 Action 객체로 저장합니다. 예를 들어, 제가 위에서 사용한 setProgressBar라는 Progress를 업데이트하는 명령이 있을 수 있습니다.
이러한 명령 객체는 ReflectionAction
클래스를 통해 RemoteViews.Action
타입으로 구현됩니다.
public void setProgressBar(@IdRes int viewId, int max, int progress,
boolean indeterminate) {
setBoolean(viewId, "setIndeterminate", indeterminate);
if (!indeterminate) {
setInt(viewId, "setMax", max);
setInt(viewId, "setProgress", progress);
}
}
public void setBoolean(@IdRes int viewId, String methodName, boolean value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BOOLEAN, value));
}
public void setInt(@IdRes int viewId, String methodName, int value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value));
}
private void addAction(Action a) {
if (hasMultipleLayouts()) {
throw new RuntimeException("RemoteViews specifying separate layouts for orientation"
+ " or size cannot be modified. Instead, fully configure each layouts"
+ " individually before constructing the combined layout.");
}
if (mActions == null) {
mActions = new ArrayList<>();
}
mActions.add(a);
}
RemoteViews
객체는 이러한 명령 객체들의 리스트를 유지합니다. 이 리스트가 커지면 RemoteViews
객체의 전체 크기도 커지게 됩니다.RemoteViews
객체는 필요할 때 이 명령 리스트를 순차적으로 실행하여 UI를 업데이트합니다. 이는 apply
메서드를 통해 이루어집니다. // 내부에 더 많은 로직이 있지만 이해를 위해 요약하여 가져온 로직입니다.
@Override
public void apply(Context context, ViewGroup parent, OnClickHandler handler) {
for (Action a : mActions) {
a.apply(view, parent, handler);
}
}
}
이와 같이 명령 객체가 누적되면서 RemoteViews
객체의 크기가 증가하고, 데이터 크기 제한을 초과하게 됩니다.
매번 새로운 RemoteViews
객체를 생성하여 사용하면, 변경 사항이 누적되지 않아 데이터 크기 증가 문제를 해결할 수 있습니다.
다음은 기존 코드를 수정하여 매번 새로운 RemoteViews
객체를 생성하는 방식으로 변경한 예시입니다:
private suspend fun updateProgress(eventData: EventData) {
withContext(Dispatchers.Main) {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
val currentEventData = eventData.current
val progress = calculateProgress(now, currentEventData)
// RemoteView 객체 생성
val remoteViews =
createRemoteViews(R.layout.view_foreground_notification_layout, eventData)
val expandedRemoteViews =
createRemoteViews(R.layout.view_foreground_notification_expended_layout, eventData)
expandedRemoteViews.setProgressBar(R.id.progressBar, 100, progress, false)
val updateNotification = buildNotification(remoteViews, expandedRemoteViews, eventData)
NotificationManagerCompat.from(context).notify(1, updateNotification)
}
}
. . .
private fun createRemoteViews(
layoutId: Int,
eventData: EventData
): RemoteViews {
val remoteViews = RemoteViews(context.packageName, layoutId)
updateRemoteViews(remoteViews, eventData)
return remoteViews
}
이제 RemoteViews
객체를 매번 새로 생성하므로, 변경 사항이 누적되는 문제를 방지할 수 있습니다.
즉 RemoteViews
객체를 재사용하면 내부적으로 변경들의 명령 객체들이 누적되어 데이터 크기가 증가할 수 있습니다 🔥
이를 해결하기 위해 매번 새로운 RemoteViews
객체를 생성하여 사용하는 것이 좋습니다ㅋㅋ
아니 객체를 생성하는게 비효율적이라고 생각해서 재활용했더니 더 큰 문제가 발생하네요...
💡 역시 제가 만든 클래스가 아니라면 의도한대로 동작할거라고 지레짐작 하는 것은 위험하다는 것을 오늘 또 깨달았습니다
쨋든 시원한 결말은 아닌 것 같지만, 이렇게 하면 알림 데이터 크기 초과 문제를 해결할 수 있으며, 안정적인 알림 서비스를 제공할 수 있습니다.
감사합니다.