[Android] Service (feat. Thread)

2020. 6. 20. 16:32·Android

 

개요

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가 호출되며 초기 인텐트를 그대로 전달

 

 

생명 주기

 

Service Lifecycle

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()
    }
}

[실행 예시화면]

Service, IntentService와 MainActivity간 데이터 통신은 아직 학습하지않아 Log.i만 찍어보았다.

 

 

 

[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
'Android' 카테고리의 다른 글
  • [Android] Service 2편 (Foreground)
  • [Android] Service <-> Activity Communication (feat. Bind Service)
  • [Android] drawable resize, customizing
  • [Android] BroadCast Receiver
M_Falcon
M_Falcon
  • M_Falcon
    Falcon
    M_Falcon
  • 전체
    오늘
    어제
    • 분류 전체보기 (430)
      • Web (16)
        • Nodejs (14)
        • Javascript (23)
        • FrontEnd (4)
      • DataBase (39)
        • Fundamental (1)
        • Redis (4)
        • PostgreSQL (10)
        • NoSQL (4)
        • MySQL (9)
        • MSSQL (3)
        • Error (4)
      • Algorithm (79)
        • Algorithm (문제풀이) (56)
        • Algorithm (이론) (23)
      • JVM (65)
        • Spring (13)
        • JPA (5)
        • Kotlin (13)
        • Java (24)
        • Error (7)
      • 기타 (68)
        • Kafka (3)
        • Kubernetes (3)
        • Docker (12)
        • git (19)
        • 잡동사니 (26)
      • 재테크 (11)
        • 세무 (4)
        • 투자 (3)
        • 보험 (0)
      • BlockChain (2)
        • BitCoin (0)
      • C (32)
        • C (10)
        • C++ (17)
        • Error (3)
      • Low Level (8)
        • OS (3)
        • 시스템 보안 (5)
      • 네트워크 (3)
      • LINUX (30)
        • Linux (26)
        • Error (4)
      • 저작권과 스마트폰의 이해 (0)
      • 생각 뭉치 (6)
      • 궁금증 (2)
      • Private (4)
        • 이직 경험 (0)
        • 꿈을 찾아서 (1)
      • Android (21)
        • OS (4)
  • 블로그 메뉴

    • 홈
    • WEB
    • 알고리즘
    • DataBase
    • Linux
    • Mobile
    • C
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    java
    docker
    Programmers
    javascript
    백준
    algorithm
    PostgreSQL
    알고리즘
    linux
    Git
    JPA
    프로그래머스
    Spring
    Kotlin
    ubuntu
    database
    kafka
    android
    C++
    Bitcoin
  • hELLO· Designed By정상우.v4.10.3
M_Falcon
[Android] Service (feat. Thread)
상단으로

티스토리툴바