개요
Background에서 실행되는 애플리케이션 구성요소
특징
자체 프로세스를 갖지 않음. 쓰레드도 아니며
메인 쓰레드에서 동작되는 구성 요소(Component)
용도
- BGM 재생
- Phone 사용량 계산
- App Update 검사
Service 유형
종류 | 설명 |
Foreground | 사용자에게 잘 보이는 작업을 수행. 서비스는 알림을 표시해야함. (User - App Interaction이 직접 없을 때도 계속 실행) ex) Audio App Playing 도중 |
Background | 사용자에게 직접 보이지 않는 작업 수행 API Level 26부터 Background 실행 제한. |
Bind | Application component가 서비스에 바인딩된 서비스 유형 Client - Service Interface를 별도로 제공 Binding된 Component가 존재하지 않을경우 종료됨. |
사용 메소드
onStartCommand (Intent, int, int)
서비스 재시작 방식의 3가지 종류
onStartCommand() 서비스 재시작 방식 | Description |
START_STICKY | 서비스 강제 종료후 재시작, 항상 onStartCommand 콜백 메소드 호출 (이때, Intent 매개변수 null로 자동 지정) |
START_NOT_STICKY | 서비스가 강제 종료되어도 재시작하지 않음. -> 다시 startService를 별도 호출해야만 서비스가 다시 Create 되고 onStartCommand()가 호출됨. |
START_REDELIVER_INTENT | 특정 동작에 대해 stopSelf() 호출 전에 서비스 종료시 시스템이 재시작하며 해당 intent 값 유지 - onStartCommand가 호출되며 초기 인텐트를 그대로 전달 |
생명 주기
onCreate가 최초에 한번 수행되는 Callback Method인것은 Activity, Fragment Lifecycle과 맥을 같이한다,.
두 서비스의 차이점은 서비스 종료에서 두드러지는데
bind Service의 경우 서비스를 사용하는 Component가 존재하지 않을경우 알아서 onUnbind() 콜백메소드를 호출하며
서비스가 종료된다. (별도의 stopSelf() 메소드 호출이 필요없다.)
IntentService
단일 Background thread에서 작업을 실행하기 위한 구조 제공
-> UI Lifecycle과 관련이 없음.
Intent가 Thread Message Queue에 들어가서 순차적(FIFO)으로 수행.
실행되는 작업을 임의 중단할 필요 없음.
시작 요청이 모두 처리된 후 서비스를 중단 (Synchronous Blocking 방식)
생성자에 서비스 이름을 명시하여 IntentService를 상속받아 생성.
onStartCommand -> MessageQueue -> onHandleIntent()
메시지 큐에서 하나씩 꺼내서 onHandleIntent에 전달하는 방식.
[Thread vs Service vs IntentService]
종류 | 실행 위치 | 특징 및 주의사항 |
Thread | 해당 Thread를 부른곳. | UI Update는 반드시 runOnUiThread같은 UI update 전용 thread를 사용해야함. |
Service | Application Background | Multi Thread 지원 client가 별도로 stopSelf()등의 메소드 호출로 destory됨. |
IntentService | 별도의 Worker Thread Message Queue에서 FIFO 방식으로 순차적으로 수행 |
Multi Thread를 지원하지 않음. MainActivity와 관련 없는 작업을 주로 수행 Message Queue에 등록된 컴포넌트가 없으면 자동으로 destroy됨. Background 에서 IntentService 작업은 API 26부터 중단됨. -> Android Support Library 26.0.0에 새로운 JobIntentService 사용 권장 |
테스트 예제 코드
<AndroidManifest.xml>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- Service, IntentService를 Manifest 파일에 등록해야만함. -->
<service
android:name=".MyIntentService"
android:exported="false"></service>
<!-- 자동으로 등록됨. -->
<service
android:name=".MyService"
android:enabled="true"
android:exported="true" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<activity_main.xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.addisonelliott.segmentedbutton.SegmentedButtonGroup
android:id="@+id/button_group_thread"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp"
android:layout_marginHorizontal="50dp"
android:elevation="2dp"
android:background="@color/colorWhite"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:borderWidth="1dp"
app:dividerPadding="10dp"
app:dividerWidth="1dp"
app:position="0"
app:radius="30dp"
app:ripple="true"
app:rippleColor="@color/colorAccent"
app:selectedBackground="@color/colorPrimary">
<com.addisonelliott.segmentedbutton.SegmentedButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="10dp"
app:drawableGravity="top"
app:selectedTextColor="@color/colorWhite"
app:textSize="20sp"
app:text="Thread Off"
app:textColor="@color/defaultFontColor" />
<com.addisonelliott.segmentedbutton.SegmentedButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="10dp"
app:drawableGravity="top"
app:selectedTextColor="@color/colorWhite"
app:text="Thread On"
app:textSize="20sp"
app:textColor="@color/defaultFontColor" />
</com.addisonelliott.segmentedbutton.SegmentedButtonGroup>
<LinearLayout
android:id="@+id/thread_linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_marginHorizontal="50dp"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="@id/button_group_thread"
app:layout_constraintEnd_toEndOf="@id/button_group_thread"
app:layout_constraintTop_toBottomOf="@id/button_group_thread">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_timer_24"/>
<TextView
android:id="@+id/remaining_time_textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_gravity="center"
android:gravity="center"
android:text="thread is not running state"/>
</LinearLayout>
<TextView
android:id="@+id/thread_execution_number_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginTop="20dp"
android:gravity="center"
app:layout_constraintStart_toStartOf="@id/thread_linearLayout"
app:layout_constraintEnd_toEndOf="@id/thread_linearLayout"
app:layout_constraintTop_toBottomOf="@id/thread_linearLayout"
android:layout_marginHorizontal="50dp"
android:text="Number Of Thread Execution : 0"/>
<com.addisonelliott.segmentedbutton.SegmentedButtonGroup
android:id="@+id/button_group_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="150dp"
android:layout_marginHorizontal="50dp"
android:elevation="2dp"
android:background="@color/colorWhite"
app:layout_constraintStart_toStartOf="@id/button_group_thread"
app:layout_constraintEnd_toEndOf="@id/button_group_thread"
app:layout_constraintTop_toBottomOf="@id/button_group_thread"
app:borderWidth="1dp"
app:dividerPadding="10dp"
app:dividerWidth="1dp"
app:position="0"
app:radius="30dp"
app:ripple="true"
app:rippleColor="@color/colorAccent"
app:selectedBackground="@color/colorPrimary">
<com.addisonelliott.segmentedbutton.SegmentedButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="10dp"
app:drawableGravity="top"
app:selectedTextColor="@color/colorWhite"
app:textSize="20sp"
app:text="Service Off"
app:textColor="@color/defaultFontColor" />
<com.addisonelliott.segmentedbutton.SegmentedButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="10dp"
app:drawableGravity="top"
app:selectedTextColor="@color/colorWhite"
app:text="Service On"
app:textSize="20sp"
app:textColor="@color/defaultFontColor" />
</com.addisonelliott.segmentedbutton.SegmentedButtonGroup>
<!-- <LinearLayout-->
<!-- android:id="@+id/thread_service"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="50dp"-->
<!-- android:layout_marginHorizontal="50dp"-->
<!-- android:orientation="horizontal"-->
<!-- app:layout_constraintStart_toStartOf="@id/button_group_service"-->
<!-- app:layout_constraintEnd_toEndOf="@id/button_group_service"-->
<!-- app:layout_constraintTop_toBottomOf="@id/button_group_service">-->
<!-- <ImageView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:src="@drawable/ic_baseline_timer_24"/>-->
<!-- <TextView-->
<!-- android:id="@+id/remaining_time2_textView"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:textSize="15sp"-->
<!-- android:layout_gravity="center"-->
<!-- android:gravity="center"-->
<!-- android:text="thread is not running state"/>-->
<!-- </LinearLayout>-->
<!-- <TextView-->
<!-- android:id="@+id/thread_execution_number2_text"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:textSize="15sp"-->
<!-- android:layout_marginTop="20dp"-->
<!-- android:gravity="center"-->
<!-- app:layout_constraintStart_toStartOf="@id/thread_service"-->
<!-- app:layout_constraintEnd_toEndOf="@id/thread_service"-->
<!-- app:layout_constraintTop_toBottomOf="@id/thread_service"-->
<!-- android:layout_marginHorizontal="50dp"-->
<!-- android:text="Number Of Thread Execution : 0"/>-->
<com.addisonelliott.segmentedbutton.SegmentedButtonGroup
android:id="@+id/button_group_intentService"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="150dp"
android:layout_marginHorizontal="50dp"
android:elevation="2dp"
android:background="@color/colorWhite"
app:layout_constraintStart_toStartOf="@id/button_group_service"
app:layout_constraintEnd_toEndOf="@id/button_group_service"
app:layout_constraintTop_toBottomOf="@id/button_group_service"
app:borderWidth="1dp"
app:dividerPadding="10dp"
app:dividerWidth="1dp"
app:position="0"
app:radius="30dp"
app:ripple="true"
app:rippleColor="@color/colorAccent"
app:selectedBackground="@color/colorPrimary">
<com.addisonelliott.segmentedbutton.SegmentedButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="10dp"
app:drawableGravity="top"
app:selectedTextColor="@color/colorWhite"
app:textSize="20sp"
app:text="IntentService Off"
app:textColor="@color/defaultFontColor" />
<com.addisonelliott.segmentedbutton.SegmentedButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="10dp"
app:drawableGravity="top"
app:selectedTextColor="@color/colorWhite"
app:text="IntentService On"
app:textSize="20sp"
app:textColor="@color/defaultFontColor" />
</com.addisonelliott.segmentedbutton.SegmentedButtonGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
[MainActivity.kt]
package com.example.servicetest
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
var thread : Thread? = null
var threadRunningNumber : Int = 0
lateinit var countDownTimer : CountDownTimer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
segmentedButtonInit()
}
private fun segmentedButtonInit() {
button_group_thread.setOnPositionChangedListener {
when(it) {
// 0: Thread Off
0 -> {
if(thread != null) {
thread?.interrupt()
thread = null
runOnUiThread(Runnable {
countDownTimer?.cancel()
remaining_time_textView.text = "Thread is not running state"
})
}
}
// 1: Thread On
1 -> {
if(thread == null) {
thread = object : Thread("TimerThread") {
override fun run() {
// Android는 UI 처리 Thread / Background Thread 기본적으로 2개의 쓰레드가 분리되어있다.
// 임의의 쓰레드를 생성해서 UI update 처리를 수행할 경우 에러가난다.
// 이때 사용할 수 잇는 thread가 runOnUiThread이다.
runOnUiThread(Runnable {
// 30초 타이머 간격 1초
countDownTimer = object : CountDownTimer (30000, 1000) {
override fun onTick(milliSecond: Long) {
remaining_time_textView.text = "Thread is running.." + (milliSecond / 1000).toString() + "초"
}
override fun onFinish() {
remaining_time_textView.text = "Timer Thread is done!"
}
}.start()
})
}
}
}
thread?.start()
threadRunningNumber++
thread_execution_number_text.text = "Number Of Thread Execution : $threadRunningNumber"
}
}
}
button_group_service.setOnPositionChangedListener {
when (it) {
0 -> {
val intent = Intent(this, MyService::class.java)
stopService(intent)
}
1 -> {
val intent = Intent(this, MyService::class.java)
startService(intent)
}
}
}
button_group_intentService.setOnPositionChangedListener {
when (it) {
0 -> {
val intent = Intent(this, MyIntentService::class.java)
stopService(intent)
}
1 -> {
val intent = Intent(this, MyIntentService::class.java)
startService(intent)
}
}
}
}
}
[MyService.kt]
package com.example.servicetest
import android.app.Service
import android.content.Intent
import android.os.CountDownTimer
import android.os.IBinder
import android.util.Log
//App Background에서 수행되지만
//Reference가 유지되어 앱 중단->재시작 후에도 Handling이 가능하다.
class MyService : Service() {
private var countDownTimerArrayList = ArrayList<CountDownTimer>()
// 사용 X
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Log.i("MyService", "onCreate")
}
// 호출시마다 startCommand 내부 동작이 수행됨.
// MultiThread Process 가능
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("MyService", "onStartCommand")
val currentServiceNumber = countDownTimerArrayList.size
// 10초 카운트다운
countDownTimerArrayList.add( object : CountDownTimer(10000, 1000) {
override fun onFinish() {
Log.i("ServiceNumber$currentServiceNumber", "Finished!")
}
override fun onTick(milliSecond: Long) {
Log.i("ServiceNumber$currentServiceNumber", "${milliSecond / 1000}초")
}
})
val thread = object : Thread("MyService") {
override fun run() {
countDownTimerArrayList.last().start()
}
}
thread.start()
return START_STICKY
}
// 별도의 Stop을 시켜줘야만 onDestroy 호출
// stopSelf() or stopService() Method로 중단가능.
override fun onDestroy() {
if(countDownTimerArrayList.size > 0) {
countDownTimerArrayList.last().cancel()
Log.i("ServiceNumber${countDownTimerArrayList.lastIndex}", "this Service is Destroyed!")
// 마지막 인덱스 countDown Thread 삭제
countDownTimerArrayList.removeAt(countDownTimerArrayList.size-1)
} else {
Log.i("ServiceError", "There isn't Service Thread to delete")
}
// 사실 Thread 선언하고 하나씩 interrupt하면서 컨트롤해줘야하는데 로그확인하는 것은 countDown Stop으로 충분.
super.onDestroy()
}
}
[MyIntentService.kt]
package com.example.servicetest
import android.app.IntentService
import android.content.Intent
import android.os.CountDownTimer
import android.os.SystemClock.sleep
import android.util.Log
import java.util.*
//IntentService는 별도의 Thread가 하나 생성되서 수행됨.
// 따라서 run에서 처리할 로직만 필요.
class MyIntentService : IntentService("MyIntentService") {
var numberOfIntentCall = 0
override fun onCreate() {
Log.i("MyIntentService", "OnCreate")
super.onCreate()
}
// 일반 Service와 다르게 Request가 올 경우 intentService는 MessageQueue에 저장.
// 최초 1회를 제외하고는 onStartCommand 콜백 메소드가 호출되지만
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("MyIntentService", "OnStartCommand")
return super.onStartCommand(intent, flags, startId)
}
// 실제 동작은 onHandleIntent에서 (수행내용)
// 단일 Thread 하나 자동으로 생성되서 작업수행.
// 여러 쓰레드를 동시에 수행하는 것이 아니라
// MessageQueue에서 하나씩 작업을 끝내고 (Synchronous Blocking 방식)
// 작업을 순차적으로 수행할 의도를 가진경우 IntentService 이용 ㄱㄱ
override fun onHandleIntent(intent: Intent?) {
Log.i("MyIntentService", "onHandleIntent!")
numberOfIntentCall++
// 별도의 Thread 생성 X
// 여기서 알 수 있는 점은 CountDownTimer가 MainThread에서 시작되면 Main을 Blocking 하지만
// 별도의 Thread에서 수행되면 메인의 흐름을 기다리지않음.
// The IntentService runs on a separate worker thread.
// object : CountDownTimer(10000, 1000) {
// override fun onFinish() {
// Log.i("IntentServiceNumber$numberOfIntentCall", "Timer Finished!")
// }
//
// override fun onTick(milliSecond: Long) {
// Log.i("IntentServiceNumber$numberOfIntentCall", "${milliSecond / 1000}초")
// }
// }.start()
for(i in 10 downTo 1) {
Thread.sleep(1000)
Log.i("IntentService", "IntentServiceNumber$numberOfIntentCall == $i")
}
// for(i in 1..10) {
// Thread.sleep(1000)
// Log.i("IntentService", "IntentServiceNumber$numberOfIntentCall == $i")
// }
}
// 작업이 끝나면 알아서 onDestroy 해제하며 메시지큐에서 사라짐
// 작업이 끝나는 기준은 onHandleIntent의
override fun onDestroy() {
Log.i("MyIntentServiceNumber$numberOfIntentCall", "this is Destroyed!")
// 별도의 stop Method 필요 X
super.onDestroy()
}
}
[실행 예시화면]
[Reference]
https://developer.android.com/guide/components/services?hl=ko
'Android' 카테고리의 다른 글
[Android] Service 2편 (Foreground) (0) | 2020.06.22 |
---|---|
[Android] Service <-> Activity Communication (feat. Bind Service) (0) | 2020.06.21 |
[Android] drawable resize, customizing (0) | 2020.06.20 |
[Android] BroadCast Receiver (0) | 2020.06.18 |
[Android] AsyncTask (0) | 2020.05.19 |