본문 바로가기
Mobile : Android/Framework

[Android Component] 서비스(Service) #2 - 스타티드 서비스(Started Service)

by 신숭이 2021. 8. 9.

서비스(Service) #2 - 스타티드 서비스(Started Service)

 

 

 

스타티드 서비스(Started Service)

스타티드 서비스는 공식 문서에선 Unbound Service 라고 하는데, Unbound는 바운딩되었다가 언바운드 한 것인지 처음 부터 바운드를 하지않은 것인지 애매한 단어이기에 스타티드(Started) 서비스라 하겠다. 스타티드 서비스는 단발성 비동기 작업을 수행할 때 사용하는게 일반적이며 작업을 수행하고자 하는 컴포넌트에서 startService() 를 호출하여 시작하는 서비스를 말한다. 

 

여기서 비동기 작업은 메인 작업 A를 수행 중에 B라는 작업을 시작하면 A가 B작업이 끝날떄까지 기다리는게 동기(Synchronous) 방식이고 비동기(Asynchronous) 작업은 B를 시작해도 메인 작업 A는 B의 완료를 기다리지 않고 그대로 진행하며 B 작업의 완료 보고만 받는 콜백 방식을 말한다.

 

 

 

스타티드 서비스 생명주기

 

  • onCreate() : 첫 startService() 에서만 호출 되며, 이후에는 호출되지않는다. 여기선 Intent를 다룰 수 없다.
  • onStartCommand() : 매 startService() 에서 호출된다. 여기서 Intent를 다루고 작업을 수행한다.
  • onDestroy() : 외부에서 stopService() 를 호출하거나, 내부에서 stopSelf()를 호출시 호출된다.

 

 

표준 패턴 

 

액티비티에 결과 리시버를 두고, 스타티드 서비스를 실행하여 서비스에서 수행한 작업의 결과를 리시버로 수신받다. 서비스는 startService() 호출 시 백그라운드 스레드를 실행하여 작업을 수행한다. 서비스는 UI 스레드에서 동작하므로 작업을 수행할 때는 별도 스레드를 두고 수행하는 것이 좋으며, 작업의 결과에 따른 UI 업데이트는 리시버에서 수행하도록 한다.

 

백그라운드 스레드를 사용할 때 주의할 점은 서비스 작업이 종료되지 않은 채 또 startService()를 호출 시 멀티 스레드로 동작하여, 서비스 내부 변수를 사용할 때 주의해야한다. 이런 멀티 스레드 상황에 대한 대처로 IntentService 와 같은 서비스도 있다. 이는 별도 포스트에서 다루겠다.

 

 

 

UI

 

버튼을 누르면 작업을 수행하고 작업 진행중에는 ProgressBar를 통해 시각적으로 보이고 작업 진행도는 textView을 %로 표현하겠다.

 

준비물

 

 

구현 : AndroidManifests.xml

<service android:name=".StartedService"></service>

<activity
	android:name=".MainActivity"
	android:exported="true">
	<intent-filter>
	<action android:name="android.intent.action.MAIN" />
	<category android:name="android.intent.category.LAUNCHER" />
	</intent-filter>
</activity>

 

구현 : MainActivity.class

public class MainActivity extends AppCompatActivity {

    private ProgressBar progressBar;
    private TextView textView;
    private Button btn_started, btn_bound;

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

        progressBar = findViewById(R.id.progressBar);
        progressBar.setVisibility(View.INVISIBLE);
        textView = findViewById(R.id.textView);
        btn_started = findViewById(R.id.btn_started);
        btn_bound = findViewById(R.id.btn_bound);

        // Start "Started Service"
        btn_started.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressBar.setVisibility(View.VISIBLE);
                Intent intent = new Intent(getApplicationContext(), StartedService.class);
                intent.putExtra(EXTRA_RECEIVER,resultReceiver); // 결과 수신자 전달
                startService(intent);       // 서비스 시작
            }
        });

        btn_bound.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
    }

    private Handler handler = new Handler();    // 수신측에서 UI 업데이트를 위해 사용하는 핸들러

    // ResultReceiver 는 단방향 메시지 수신시 BroadCastReceiver 보다 간단
    private ResultReceiver resultReceiver = new ResultReceiver(handler){

        int percentage = 0;

        @Override
        protected void onReceiveResult(int resultCode, Bundle resultData) {
            if(resultCode == Constant.SYNC_PROGRESS){
                percentage+=20;
                textView.setText(percentage+" %");
            }
            else if(resultCode == Constant.SYNC_COMPLETED){
                progressBar.setVisibility(View.GONE);
                percentage=0;
                textView.setText("Complete!");
            }
        }
    };
}

여기서 살펴볼 점은 ResultReceiver 사용방식은 다양할 수 있는데, 여기선 Intent 에게 리시버를 전달하여 서비스 내에서 리시버로 단방향 메시지를 보내도록 구현하고 있다는 것이다. ResultReceiver는 단방향 메시지 수신을 할 때 구현이 간단하며, UI를 업데이트 하기 위해선 생성자에 Handler를 전달해야한다.

 

만약 UI 업데이트가 필요없거나, 리시버도 백그라운드 스레드에서 동작한다고 하면 생성자에 null을 전달해도 무방하다.

 

 

구현 : StartedService.class

public class StartedService extends Service {
    private static final long SLEEP_TIME = 5000;

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        Log.d("Service","onCreate()");
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("Service","onStartCommand");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                final ResultReceiver receiver = intent.getParcelableExtra(Constant.EXTRA_RECEIVER);
                SystemClock.sleep(SLEEP_TIME);
                Log.d("Thread","Threading..");
                receiver.send(Constant.SYNC_PROGRESS,null);
                SystemClock.sleep(SLEEP_TIME);
                Log.d("Thread","Threading..");
                receiver.send(Constant.SYNC_PROGRESS,null);
                SystemClock.sleep(SLEEP_TIME);
                Log.d("Thread","Threading..");
                receiver.send(Constant.SYNC_PROGRESS,null);
                SystemClock.sleep(SLEEP_TIME);
                Log.d("Thread","Threading..");
                receiver.send(Constant.SYNC_PROGRESS,null);
                SystemClock.sleep(SLEEP_TIME);
                Log.d("Thread","Threading..");
                receiver.send(Constant.SYNC_COMPLETED,null);
                stopSelf();
            }
        });

        thread.start();
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.d("Service","onDestroy()");
        super.onDestroy();
    }
}

onStartCommand() 에서는 작업을 표현하기위해 5초마다 딜레이와 로그를 띄웠다. 여기서 Intent로 전달 받은 리시버에 지속적으로 메시지를 전달하고 있다. 참고로 이 코드는 멀티스레드 상황에 대한 어떠한 대응도 되어있지않다. 메인 화면에서 Started Service 버튼을 광클해보자. 막장으로 동작할 것이다. 멀티스레드 환경은 다른 포스팅에서 다루겠다.

 

 

부록 : onStartCommand() 의 반환 상수

 

START_NOT_STICKY : onStartCommand() 리턴 후, 서비스가 강제 종료되면 재시작하지 않는다. 명시적으로 startService()를 실행할때만 의미있는 작업에 사용한다.

 

START_STICKY : 기본 반환값. 서비스가 정상 종료되지 않았다면, 재시작한다. 다만 Intent 가 null로 전달되기에 메서드에서 Intent 전달 값을 사용할때 NPE 발생 가능성이 있다. 따라서 서비스 내부 상태 변수를 사용할 때 쓰는 것이 안전하다.

 

START_REDELIVER_INTENT : 재시작 시, Intent를 다시 전달하며 onStartCommand()를 실행한다. (To Do - 좀 더 조사)

댓글