많은 Android앱들이 죽지 않는 서비스를(Immortal Service) 이용하는데,

개발 중인 제품에 필요하여 인터넷을 검색해 사용해 보니 제대로 작동하지 않거나 관련 자료가 부족해서 직접 개발하였다 (해야 했다...).

국내외국 자료를 검색해서 찾은 결과들을 토대로 구현하였고,

소스는 Github에서 받을 수 있다.

(따라서, 충분하게 검증된 기술이 아니기 때문에 문제가 발생할 수 있으며, 관련 문제를 공유하거나 더 나은 방법을 공유해줬으면 하는 기대로 이 글을 작성합니다.)


Android의 죽지 않는 서비스는 일반적인 서비스를 startForeground로 실행하면 간단하게 구현 할 수 있다.

이 방법의 문제는 Notification을 이용하여 알림창에 표시해야만 한다는 것이다.

알림창을 제거하기 위해 다양한 방법들이 논의 되었지만 [참고],

Android Oreo 버전에서는 작동되지 않는다.


이 내용들을 토대로 운영체제가 서비스를 죽이면 다시 실행하는 방법으로 죽지 않는 서비스를 구현하였다.

다시 실행시키는 방법은 알람(Alarm)을 이용한다.

서비스가 종료되면(onDestroy) 1초 뒤에 알람이 실행되게 하고,

알람에서 해당 서비스를 다시 실행한다.

이때, Android Oreo에서는 서비스의 백그라운드 실행을 금지하기 때문에 문제가 발생한다.

따라서, Oreo 이전과 이후 버전으로 나누어서 구현한다.


Oreo 이전 버전은 startService로 해당 서비스를 실행하고,

Oreo 이후 버전은 한 단계를 더 거쳐서 구현한다.

Oreo에서는 서비스를 백그라운드(Background)에서 실행하는 것을 금지하기 때문에

포그라운드(Foreground)에서 실행해야 한다.

즉, 알람은 백그라운드이기 때문에 서비스를 실행할 수 없다.

따라서 알람에서 포그라운드인 startForegroundService(Notification)으로 서비스를 실행하고, 이 서비스 안에서 해당 서비스를 실행한다.

그리고, startForegroundService으로 실행한 서비스는 죽이는 방식으로 구현한다.

그림으로 정리하면 다음과 같다.


기본 개념을 코드로 구현하면,

먼저, 앱을 실행하고 종료(onDestroy)할 때 서비스(RealService)를 종료(stopService)한다.

public class MainActivity extends AppCompatActivity {
private Intent serviceIntent;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (RealService.serviceIntent==null) {
serviceIntent = new Intent(this, RealService.class);
startService(serviceIntent);
} else {
serviceIntent = RealService.serviceIntent;//getInstance().getApplication();
Toast.makeText(getApplicationContext(), "already", Toast.LENGTH_LONG).show();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (serviceIntent!=null) {
stopService(serviceIntent);
serviceIntent = null;
}
}

MainActivity.java


무엇인가 백그라운드에서 서비스를 제공할 서비스 클래스는 RealService.java이다.

메신저 앱의 경우 메시지를 수신하는 등의 처리를 구현하는데, 여기서는 1분에 한번씩 시간을 알려주도록 (Notification) 했다.


가장 처음에는 앱(MainActivity)이 실행되기(Foreground) 때문에 RealService는 startService로 실행하면 된다.

다만, 앱이 다시 실행되었을때 충돌을 방지하기 위해 RealService에 serviceIntent를 static 변수로 두어 서비스 실행시 intent를 가지고 있도록 한다.

serviceIntent의 값이 있으면 이미 서비스가 실행 중이니 넘어가고 없을 때만 서비스를 실행한다.


앱을 종료(onDestroy)할 때 서비스(RealService)를 종료(stopService)하여

서비스의 종료(onDestroy) 이벤트가 실행되게 작성한다.

public void onDestroy() {
super.onDestroy();

serviceIntent = null;
setAlarmTimer();
Thread.currentThread().interrupt();

if (mainThread != null) {
mainThread.interrupt();
mainThread = null;
}
}

protected void setAlarmTimer() {
final Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
c.add(Calendar.SECOND, 1);
Intent intent = new Intent(this, AlarmRecever.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);

AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mAlarmManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), sender);
}

RealService.java

RealService 서비스가 종료(onDestroy)될때 마다 1초뒤 알람이 실행되게 작성한다(setAlarmTimer).

public class AlarmRecever extends BroadcastReceiver{

@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent in = new Intent(context, RestartService.class);
context.startForegroundService(in);
} else {
Intent in = new Intent(context, RealService.class);
context.startService(in);
}
}

}

AlarmRecever.java

알람(AlarmRecever)에서는

Android가 Oreo 이후 버전이면 (SDK_INT >= O)

Android에서 제공하는 죽지않는(Foreground) 서비스인 RestartService를 startForegroundService로 실행한다.

이전 버전은 RealService 서비스를 실행하면 끝이다.


Oreo 이후 버전은 startForegroundService로 실행한 RestartService의 코드는 다음과 같다.

public class RestartService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default");
builder.setSmallIcon(R.mipmap.ic_launcher);
~~ 생략 ~~

Notification notification = builder.build();
startForeground(9, notification);

/////////////////////////////////////////////////////////////////////
Intent in = new Intent(this, RealService.class);
startService(in);

stopForeground(true);
stopSelf();

return START_NOT_STICKY;
}

RestartService.java

RestartService 의 코드는 Android에서 제공하는 죽지않는(Foreground) 서비스의 코드이다.

포그라운드(Foreground)로 실행되지만, Android 알림창에 표시되는 문제가 있다.

따라서, RestartService를 startForeground로 실행하고,

RealService를 실행한다 (startService).

그리고 실행된 RestartService를 stopForeground와 stopSelf를 실행하여 RestartService를 종료한다.

RestartService가 실행되고 종료되는 시간이 짧기 때문에 알림창에는 표시가 생기지 않는다.


이외에 부팅후 실행을 위한 코드(RebootRecever)도 있지만 알람과 동일한 코드라 생략한다.


이상의 죽지 않는 서비스를 구현하기 위해 필요한 2개의 서비스와 2개의 Recever를

AndroidManifest.xml 파일에 다음과 같이 등록해야 한다.

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

<application
~~ 생략 ~~
<activity android:name="com.damonet.service9.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service android:name="com.damonet.service9.RealService"
~~ 생략 ~~
<service android:name="com.damonet.service9.RestartService"
~~ 생략 ~~
<receiver android:name="com.damonet.service9.AlarmRecever"/>

<receiver
android:name=".RebootRecever"
~~ 생략 ~~
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>

AndroidManifest.xml

추가적으로 부팅이 완료되면 서비스를 시작하기 위하여 부팅이 완료되었다는 것을 수신할 권한(permission)을 등록한다(RECEIVE_BOOT_COMPLETED).

또, Oreo API 28 부터는 FOREGROUND_SERVICE권한을 등록해야 포그라운드(Foreground) 서비스를 실행할 수 있다.


테스트 결과

Oreo 이전 버전에서는 큰 이상을 찾을 수 없었고,

Oreo에서는 2~5분 마다 서비스가 다시 실행되는 것을 확인 할 수 있었다.


---------------------------- 2019년 8월 19일 추가 ----------------------------

2~5분 마다 서비스가 다시 실행되는 문제는 절전 모드(doze) 문제로,

REQUEST_IGNORE_BATTERY_OPTIMIZATION를 이용하여 절전 모드를 사용하지 않는 예외 앱으로 처리하면 된다.

상세한 내용은 다른 자료를 참고 하고,

이 기능은 채팅 또는 통화 앱 등에만 허용하는 것으로, 메신저가 아니라면 웹 스토어 등록이 거절 될 수 있다 (참고).


먼저, AndroidManifest.xml에 다음 권한을 추가한다.

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

그리고, MainActivity.java 파일에 절전 모드를 해제하는 권한을 얻는 코드를 추가해 준다.

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
boolean isWhiteListing = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
isWhiteListing = pm.isIgnoringBatteryOptimizations(getApplicationContext().getPackageName());
}
if (!isWhiteListing) {
Intent intent = new Intent();
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getApplicationContext().getPackageName()));
startActivity(intent);
}

MainActivity.java

추가된 코드는 기존의 github에 추가되어 있고,

계속 테스트 중이지만 현재까지 성능 문제 없이 잘 사용하고 있다.




+ Recent posts