15. [서비스 컴포넌트]

728x90

[15. 서비스 컴포넌트]

*서비스 : 백그라운드 작업 목적 컴포넌트

[15-1. 서비스 이해]

[서비스]

-서비스 컴포넌트 : 오래 걸리는 작업을 백그라운드에서 처리 가능하게 해주는 컴포넌트

-화면 구현 X. 역시 컴포넌트의 한 종류이므로 시스템에서 생명주기 관리


[서비스 생성. 등록]

-서비스 생성: Service 클래스 상속받은 클래스로 작성

              -> 내부에 onBind() 생명주기 함수 재정의 필수

class MyService: Service() {  //Service 클래스 상속받아 작성 
      override fun onBind(intent: Intent) : IBinder? {  //반드시 재정의
           return null
      }
}

 

-서비스 등록 : 역시 컴포넌트이므로 매니페스트 파일에 <service>태그로 등록

         -> 실행할 인텐트 지정.등록

명시적 인텐트로 실행 클래스명만 등록
암시적 인텐트로 실행 <intent-filter> 등록
<service
      android:name = “.MyService”
      android:enabled=“true”
      android:exported=“true” ></service>

[서비스 실행]

-서비스를 실행하려면 시스템에 인텐트 전달하여 실행 O

-시스템에 인텐트 전달하여 ‘서비스 실행 함수 2가지

①     실행 : startService() 함수 | 바인딩되지 않은 서비스
        종료 : stopService() 함수 . 실행 중인 서비스 종료함수

    1) 내부 앱 서비스 실행

-해당 서비스를 인텐트에 담아서 startService()의 매개변수로 전달함

val intent = Intent(this, MyService::class.java)
startService(intent)  //전달

    2) 외부 앱 서비스 실행

- setPackage() 함수로 외부 앱의 패키지명 명시 필수

-외부 앱 서비스는 암시적 인텐트로 실행해야 함

        단, 외부 앱이 백그라운드 상태일 경우 서비스 실행불가

val intent = Intent(this, MyService::class.java)
intent.setPackage(“com.example.test_outter”) //외부앱 패키지명 명시 
startService(intent)  //전달

②              bindService() 함수 | 바인딩된 서비스
                 unbindService() 함수. 실행 중인 서비스 종료 함수

   <필수 준비>

- ServiceConnection 인터페이스 구현한 객체 준비

- 내부에 인터페이스의 추상메소드 2개 재정의 필수

val connection: ServiceConnection = object : ServiceConnection {  //인터페이스 구현 객체 준비 
    //(1) onServiceConnected() : 서비스 구동시 자동 호출 함수 
    override fun onServiceConnected (name:ComponentName?, service: IBinder?) {}

    //(2) onServiceDisconnected() : 서비스 종료시 자동 호출 함수 
    override fun onServiceDisconnected (name: ConponentName?) { }
}

    1) 내부 앱 서비스 실행
-bindService() 함수 매개변수 3개 -> ( 인텐트 /ServiceConnection 구현 객체/ flags 상수값 전달)

-flags 상수값 대부분 ‘Context.Bind_AUTO_CREATE’ 지정  -> 서비스 실행상태 아니어도 객체 생성하여 실행함

    2) 외부 앱 서비스 실행

추가로 setPackage() 함수로 외부앱 패키지명 명시 필수


[서비스 생명주기]

-위의 2가지 실행함수 중 어떤 것 이용했는지에 따라 생명주기도 다름


[15-2. 바인딩 서비스]

*서비스 실행 함수 2개 제공 이유 : 서비스 이용 상황 2가지로 구분 위함

        ⇒ startService() 함수 사용 : 데이터 상호작용 불필요한 경우 (백그라운드 작업만 수행)

        ⇒ bindService() 함수 사용 : 데이터 상호작용 필요한 경우 (객체 바인딩)


[IBinder 객체 바인딩]

-bindService() 함수는 서비스를 실행한 곳에도 객체를 전달(바인딩) 하는 함수

-bindService() 함수 실행 -> onBind() 함수 자동 실행됨

-onBind() 함수의 반환타입은 IBinder() 인터페이스의 구현 객체임.

-이 구현객체는 서비스를 bindService()로 실행한 곳에서 onServiceConnected() 함수 매개변수로 받아서 데이터 상호작용 가능


[프로제스 간 통신하는 방법] (2)

  ① 메신저 바인딩

-API에서 제공하는 Messenger 객체를 바인딩하는 방법

   <서비스 코드>

-외부에서 전달해온 데이터를 Message 타입으로 받고,
       전달받은 Messagewhat값으로 데이터 구분. obj 속성으로 전달받은 데이터 가져옴

-내부에 onBind() 함수 재정의하는데,
    이 함수의 반환타입은 Messenger 객체의 binder 속성으로 지정하여 데이터 전달함

   <내부 앱 연동>

   <즉, 액티비티 코드> | bindService() 로 서비스 실행하는 곳

-서비스에서 넘어온 객체 받기
        : bindService()로 서비스 실행하여 자동 호출된 onBind() 함수의 반환 객체
     (, 서비스에서 넘어온 객체)onServiceConnected() 의 매개변수로 받고, Messenger() 생성자 매개변수에 지정해줌.


-서비스에 데이터 전달하기

: Messenger send() 함수에 전달할 데이터를 Message 객체에 담아서 전달

    <외부 앱 연동> | bindService()로 서비스 실행하는 곳

-외부 앱에서 서비스를 bindService()로 실행하기 위해서는
                           해당 외부앱의 매니페스트에 <intent-filter> 선언 필수
-서비스 등록된 매니페스트에는 <package> 태그의 name 속성값으로 연동할 앱 패키지명을 지정
-코드에서 실행할 외부 앱의 패키지명을 setPackage() 에 명시

     <외부 앱과의 데이터 상호작용>

-Parcelable 타입/ Bundle 타입의 데이터만 주고받을 수 있음
-따라서 상호작용할 데이터를 Bundle에 담고,  Bundle 객체를 다시 Message 객체에 담아서 send() 로 보낼 것


  ② AIDL 기법 사용

-AIDL : 프로세스 간 통신 구현 시 사용 기법

-안드로이드는 기본적으로 하나의 프로세스에서 직접 다르 프로제스 접근 불가능

-따라서 전달할 데이터를 시스템에 전달하고, 시스템이 다른 프로세스에 전달.

-데이터 종류 많을 경우, 동시에 멀티 스레드 환경에서 실행 가능한 AIDL 이 더 적합


  <서비스 제공 앱> 

확장자 aidl 파일 필요

-확장자만 다를 뿐, 자바 인터페이스, 추상 함수만 정의된 파일

서비스 컴포넌트 : aidl 파일 속 추상 함수를 구현한 구체적인 로직 작성 역할

-AIDL 은 바인드 서비스 이용하므로 onBind() 함수에서 서비스를 인텐트로 실행한 곳에 객체 전달해주어야 함. 이때 객체는 프로세스 간 통신 대행 객체 Stub 객체를 전달

외부 앱과의 연동을 위해 매니페스트에 <intent-filter> 추가함


  <서비스 이용 외부 앱> 

AIDL 서비스를 이용하는 앱에서도 똑같은 AIDL 파일 가지고 있어야 함

bindService() 함수로 외부 앱의 서비스 실행함

bindService()로 호출 시 자동 호출되는 onServiceConnected() 함수의 두 번째 매개변수로 서비스에서 전달한 Stub 객체임.

-AIDL은 프로세스 간 통신 대행을 위해 Stub 객체 사용하므로 Stub 객체로 받은 것.

-Stub 객체 받을 때 AIDL 파일에 선언한 인터페이스 타입으로 받으면 됨


[15-3. 백그라운드 제약]

-‘화면 구현하는 액티비티 컴포넌트 제외.

나머지 컴포넌트들은 모두 백그라운드에서 작업 처리 목적으로 사용됨.


[‘브로드캐스트 리시버’의 백그라운드 제약]

-매니페스트에 <intent-filter>로 등록한 리시버를 암시적 인텐트로 실행 시 -> 실행 X

-코드에서 registerReceiver() 함수로 등록한 리시버는 암시적 인텐트도 잘 실행 O


[‘서비스의 백그라운드 제약]

-기본적으로 앱이 백그라운드 상태일 때 인텐트 전달하면 오류 발생

   <서비스 정상 실행되는 포그라운드 상황>

보이는 액티비티가 있는 경우
포그라운드 서비스 있는 경우
또 다른 포그라운드 앱 연결되어 있는 경우

   <백그라운드 상황 . 정상 실행되는 일부 상황>

우선순위 높은 FCM(파이어베이스 클라우드 메시징) 처리하는 경우
브로드캐스트 수신하는 경우
알림에서 PendingIntent 실행하는 경우
VPN 앱이 포그라운드로 승격되기 전 VpnService 시작하는 경우

   <앱 백그라운드 상황에서 서비스 실행하는 방법>

startForegroundService() 함수로 인텐트 시작하기
     -, 이 함수로 실행한 서비스는 얼마 후 오류 발생한 뒤 강제 종료됨
     -제공 이유 : 사용자에게 앱 실행 중임을 알림,
                       즉, 빨리 startForeground() 호출하여 (알림 발생) 앱을 포그라운드 상황으로 만들라는 의미

결국 알림 객체를 사용해야 하므로 매니페스트에 퍼미션 등록 필요

[15-4. 잡 스케줄러]

-앱 백그라운드 상황의 작업 제약을 -> ’잡 스케줄러이용하여 보완 가능

-모든 상황 처리 X

-일부 상황에 한해서 어떤 상황에서 서비스 실행할지 조건 명시하여 보완 처리 O


[잡 스케줄러에 명시 가능한 실행 조건]

네트워크 타입
배터리 충전 상태
특정 앱 콘텐츠 프로바이더 갱신
실행주기
최소 지연 시간
시스템 재구동 시 현재 조건 유지 여부

[잡 스케줄러 3가지 구성요소]

잡 서비스 백그라운드 작업 구현
잡 인포 잡 서비스 실행 조건 지정
잡 스케줄러 잡 인포를 시스템에 등록.
잡 서비스 등록 시 데이터 전달

 ▶[잡 서비스]

    <잡서비스 생성>

  ① 매니페스트에 잡 서비스 등록

-<service> 태그로 등록.
-android.permission.BIND_JOB_SERVICE 퍼미션 포함

  ② 코드에서 JobService() 상속받은 클래스 작성

-여기에서 생명주기 함수 onCreate() / onStartCommand() / onDestroy() 재정의

-onStartJob() / onStopJob() 재정의 필수

-두 함수 모두 Boolean 타입 반환하는데, 반환값에 따라 다르게 동작함

onStartJob() 백 그라운드에서 처리할 작업 구현
onStopJob() 작업 비정상 처리된 경우 호출됨

     ▸[onStartJob() 함수]

-false 반환 : 작업 완벽히 종료됨을 의미

-true 반환 : 오랜 시간 살아있는 서비스 의미

     [onStopJob() 함수]

-> onStartJob() 함수가 true 반환해서 오랜 시간 서비스 살아있는 상황에서

            갑자기 잡 스케줄러 실행 조건 변경되거나 cancle() 함수로 취소된 경우에 한해서만 호출됨

-false 반환 : 잡 스케줄러 등록 취소

-true 반환 : 잡 스케줄러 재등록


 ▶잡 인포

     <잡 서비스를 잡스케줄러 이용 -> 시스템에 등록>

JobInfo 객체에 잡 서비스 실행 조건을 담음

JobInfo.Builder() 생성자의 매개변수에 (등록작업 식별값/등록할 잡 서비스) 순서로 담음

이렇게 만든 JobInfo 객체를 JobServiceschedule() 함수로 시스템에 등록

     <등록 시 실행 조건 명시 setter() 함수 종류>

setPersisted() 기기 재부팅해도 작업 등록 유지 여부 설정
setPeriodic() 작업 실행 주기 설정
setMinimumLatency() 작업 실행 지연시간 설정
setOverideDeadline() 작업 최대 실행 시간 설정
setRequiredNetworkType() 네트워크 타입 설정
setRequiresBatteryNotLow() 배터리 낮은 상태 아님을 설정
setRequresCharging() 배터리 충전상태인지 설정

 ▶잡 스케줄러

         [잡 서비스 등록 시 데이터 전달]

-JobInfo 객체 이용하여 시스템 등록 시, 조건에 만족할 때만 실행됨
-잡 서비스에 데이터 전잘하려면 JobInfo.Builder  setExtras() 함수 이용
-setEctras()의 매개변수로 PersistableBundle 타입 객체 지정
-PersistableBundle 객체에 키-값 형태로 잡 서비스에 전달 데이터 담음

         [잡 서비스에서 데이터 가져오기]

-onStartJob() 함수의 매개변수에 접근하여 받음
-매개변수 타입은 JobParameters 인데, 이 타입의 extras 속성에 접근하면 데이터 담겨있음

[15-5. MP3 재생 앱 만들기] : 실습

[MP3 재생 앱]
-프로세스 간 통신 실습 : (1) 메신저 바인딩 기법 (2) AIDL 통신 기법 
-잡 스케줄러 사용 : 스마트폰이 와이파이 이용 상황에 잡스케줄러 실행되어 알림 띄우기 
<준비> 
-프로세스 간 통신을 실습해야 하므로 '앱 2개' 필요
-Ch15_Outer //음악 재생 앱 
-Ch15_Service //Outer 앱 조종 역할 
<프로세스 간 통신 실습> 

    (1) 메신저 서비스 

[MyMessengerService.kt] 파일 

class MyMessengerService : Service() {  //(1) 메신저 서비스 

    // 액티비티의 데이터를 전달받는 메신저
    lateinit var messenger: Messenger

    // 액티비티에 데이터를 전달하는 메신저
    lateinit var replyMessenger: Messenger
    lateinit var player: MediaPlayer //음악 재생용 객체 선언 

    //'서비스'의 생명주기 함수 재정의 
    override fun onCreate() {  //시작 
        super.onCreate()
        player = MediaPlayer() //재생 객체 초기화 생성 
    }
    override fun onDestroy() { //종료
        super.onDestroy()
        player.release()
    }

    // 액티비티로부터 받은 데이터 메시지 처리할 Handler를 inner 클래스로 생성 
    inner class IncomingHandler(  
        context: Context,
        private val applicationContext: Context = context.applicationContext
    ) : Handler(Looper.getMainLooper()) { //Handler() 상속받음 

        override fun handleMessage(msg: Message) {  //외부에서 메시지 데이터 받으면 자동 호출되는 함수 재정의 

            when (msg.what) { //받은 msg의 what 속성별 처리 분리 

                10 -> {   //액티비티에서 서비스 요청 들어오자마자 보내줄 데이터 처리 
                    replyMessenger = msg.replyTo

                    if (!player.isPlaying) { 
                        player = MediaPlayer.create(this@MyMessengerService, R.raw.music)
                        try {
			   //액티비티로 넘겨줄 데이터 준비
                            val replyMsg = Message()  //Message() 객체
                            replyMsg.what = 10 //식별값은 10

                            val replyBundle = Bundle()  // Bundle 타입 객체 준비
                            replyBundle.putInt("duration", player.duration) 
                            replyMsg.obj = replyBundle //번드 객체를 다시 Message 객체의 obj에 담고 

                            replyMessenger.send(replyMsg) //보낼 데이터를 Message 객체에 담아서 messenger의 send()로 전달

                            // 데이터 모두 넘겨줬으면 이제 음악 재생
                            player.start()
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                }
                20 -> {
                        // 멈춤 메시지
                    if (player.isPlaying)
                        player.stop()
                }
                else -> super.handleMessage(msg)
            }
        }
    }
    override fun onBind(intent: Intent): IBinder {
        messenger = Messenger(IncomingHandler(this))
                     //이 서비스 사용하는 곳에서 데이터 넘기면 처리할 내부의 Handler 지정 
        return messenger.binder  //IBinder 구현 객체를 반환 
    }
}
 

    (2) AIDL 서비스 작성 

[MyAIDLInterface.aidl] //확장자만 .aidl인 인터페이스 형태 
interface MyAIDLInterface {
    int getMaxDuration();
    void start();
    void stop();
}

 

[MyAIDLService.kt]

class MyAIDLService : Service() {   //(2) AIDL 이용한 서비스 클래스 
    lateinit var player: MediaPlayer
    //서비스의 생명주기 함수 재정의 
    override fun onCreate() {  //시작 
        super.onCreate()
        player = MediaPlayer()
    }
    override fun onDestroy() { //종료 
        player.release()
        super.onDestroy()
    }
    override fun onBind(intent: Intent): IBinder? {
       //반환할 객체 = Stub() 객체
        return object : MyAIDLInterface.Stub() {

            override fun getMaxDuration(): Int {  // 외부에서 이 함수 자동호출 된 경우 
                return if (player.isPlaying)  //반환할 데이터 처리 
                    player.duration
                else 0
            }
            override fun start() {  //외부에서 이 함수 자동호출 된 경우 
                if (!player.isPlaying) { //플레이 준비 
                    player = MediaPlayer.create(this@MyAIDLService, R.raw.music)
                    try {
                        player.start()  //플레이 시작 
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
            override fun stop() { //외부에서 이 함수 호출된 경우 
                if (player.isPlaying)
                    player.stop() //중지
            }
        }
    }
}
 
[참고] : Do It 안드로이드 앱 프로그래밍 with 코틀린 

728x90