Android 9.0 PIE 에서 죽지않는 서비스 만들기 TIP
Android 9.0 이상에서 죽지 않는 서비스 동작하기
Android 8.0 Oreo 이상 되면서 Background Service 동작이 점점 어려워지고 있습니다.
여러 가지 이유가 있겠지만 사용자가 인지하지 못하는 상황에서 특정 프로세스가 자원을 고갈시키고 악의적인 행위를 하는 것을 방지하려는 것이 가장 큰 이유가 아닐까요
죽지 않는 프로세스를 만들기 위해선 여러 가지 방법이 있겠지만, Background Service를 동작시키려면 편법을 이용해야 하고,
Foreground Service를 동작시키게 되면 Android에서 권장되는 방법이지만 Notification Bar에 1개의 제거되지 않는 Notification Message가 표시되게 됩니다.
그중 이번에 소개해드릴 방법은 Android에서 권장되는 방법으로 OS가 업데이트되더라도 오랜 기간 동안 유지될 수 있는 방법입니다.
Android 8.0 변경사항 중
백그라운드 실행 제한
배터리 수명을 개선하기 위해 Android 8.0에서 도입된 변경 사항 중 하나입니다. 앱이 캐시된 상태로 진입하고 활성 구성 요소가 없으면 시스템은 앱이 보유한 모든 가동 잠금(wakelock)을 해제합니다.
또한, 기기 성능을 개선하기 위해 시스템에서는 포그라운드에서 실행 중이지 않은 앱의 특정한 동작을 제한합니다. 구체적인 사항은 다음과 같습니다.
백그라운드에서 실행 중인 앱은 이제 백그라운드 서비스에 얼마나 자유롭게 액세스할 수 있는지에 대한 제한이 있습니다.
앱은 대부분의 암시적 브로드캐스트(즉, 앱을 특정해서 대상으로 하지는 않는 브로드캐스트)에 등록할 때 앱의 매니페스트를 사용할 수 없습니다.
기본적으로, 이들 제한은 O를 대상으로 하는 앱에만 적용됩니다. 그러나 앱이 O를 대상으로 하지 않더라도 사용자가 Settings 화면에서 이들 앱에 대한 제한을 활성화할 수 있습니다.
Android 8.0에는 또한 다음과 같은 특정 메서드에 대한 변경 사항이 있습니다.
백그라운드 서비스 생성이 허용되지 않는 상황에서 Android 8.0를 대상으로 하는 앱이 startService() 메서드를 사용하려고 시도할 경우 이제 이 메서드가 IllegalStateException을 발생합니다. 새로운 Context.startForegroundService() 메서드가 포그라운드 서비스를 시작합니다. 앱이 백그라운드에 있는 중에도 시스템은 앱이 Context.startForegroundService()를 호출하도록 허용합니다. 그러나 앱은 서비스가 생성된 후 5초 이내에 해당 서비스의 startForeground() 메서드를 호출해야 합니다.
MainActivity.java
//
//...
//
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
mTextMessage = findViewById(R.id.message);
navView.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
if (null == UndeadService.serviceIntent) {
foregroundServiceIntent = new Intent(this, UndeadService.class);
startService(foregroundServiceIntent);
Toast.makeText(getApplicationContext(), "start service", Toast.LENGTH_LONG).show();
} else {
foregroundServiceIntent = UndeadService.serviceIntent;
Toast.makeText(getApplicationContext(), "already", Toast.LENGTH_LONG).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != foregroundServiceIntent) {
stopService(foregroundServiceIntent);
foregroundServiceIntent = null;
}
}
UndeadService.java
//
//...
//
public class UndeadService extends Service {
public static Intent serviceIntent = null;
// ...
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
serviceIntent = intent;
initializeNotification();
//
// Todo.
//
// Thread, Timer 등으로 처리
return START_STICKY;
}
public void initializeNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "1");
builder.setSmallIcon(R.mipmap.ic_launcher);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle();
style.bigText("설정을 보려면 누르세요.");
style.setBigContentTitle(null);
style.setSummaryText("서비스 동작중");
builder.setContentText(null);
builder.setContentTitle(null);
builder.setOngoing(true);
builder.setStyle(style);
builder.setWhen(0);
builder.setShowWhen(false);
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
builder.setContentIntent(pendingIntent);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(new NotificationChannel("1", "undead_service", NotificationManager.IMPORTANCE_NONE));
}
Notification notification = builder.build();
startForeground(1, notification);
}
@Override
public void onDestroy() {
super.onDestroy();
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.add(Calendar.SECOND, 3);
Intent intent = new Intent(this, AlarmReceiver.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), sender);
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.add(Calendar.SECOND, 3);
Intent intent = new Intent(this, AlarmReceiver.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), sender);
}
AlarmReceiver.java
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent in = new Intent(context, UndeadService.class);
context.startForegroundService(in);
} else {
Intent in = new Intent(context, UndeadService.class);
context.startService(in);
}
}
}
해당 방법처럼 서비스를 실행하고 프로세스 종료, 태스크 아이콘 종료시에도 다시 실행되도록 처리하여 서비스가 종료되지 않고 지속적으로 실행될 수 있도록 할 수 있습니다.
또 다른 방법으로는 프로세스가 종료될때마다 다시 실행시켜주는 방법인데, 해당 방법은 1~2분마다 OS에서 서비스를 종료시키고 그걸 다시 실행시키는 형태의 방법이라서 추천드리지 않습니다.