안드로이드 서비스

이영준·2023년 3월 15일
0

📌서비스

일반적으로 화면 없이 동작하는 프로그램으로 백그라운드 프로세스

  • 액티비티 응용프로그램은 화면이 종료되면 동작하지 않지만 서비스는 백그라운드에서 실행 되어 화면과 상관없이 계속 동작한다.

서비스 특징

  • 유저와 상호작용 불가

  • 액티비티의 생명주기에 비종속

  • 별도의 스레드에서 동작하지 않고 '메인스레드'에서 작동

  • 액티비티가 비활성화 되면 액티비티보다 우선순위 높음

    서비스는 background에서 장시간 동작하면 시스템에서 강제종료하므로 app의 작동을 notification 영역에 표시하는 foreground service가 되어야 한다.

🔑서비스 실행 방법

1. StartService
앱 내의 액티비티 같은 컴포넌트가 호출했을 때 실행됨
백 그라운드에서 한가지 일을 하며, 결과를 호출했던 컴포넌트에게 보내지 않음

2. bindService
자신을 호출했던 컴포넌트와 인터랙션을 주고받음
처리한 결과를 주고 받음
서로 다른 프로세스상에 있어도 처리 가능

📌 startService

🔑startService 생명주기

onStartCommand

서비스를 다른 컴포넌트가 호출 했을 시 수행되고, 이 메서드를 통해 서비스가 백그라운드에서 동작

리턴 해줘야 되는 3가지 값이 있음

  • START_NOT_STICKY
  • START_STICKY
  • START_REDELIVER_INTENT

📌 bindService

서비스를 실행시켜 놓고 필요할 때마다 서비스의 메서드에 접근하여 통신할 수 있는 구조

  • startService() 와 달리 하나 이상의 클라이언트 컴포넌트 간의 통신을 구현 가능
  • 서비스가 백그라운드에서 실행되고 있을 때 액티비티에서 서비스의 메서드를 호출/결과를 받아서 보여주는 등의 interaction이 필요할 때 사용

🔑 bindService 생명주기

서비스가 bindService() 함수로 시작되면
onCreate()-> onBind() 함수 호출
unbindService() 함수로 종료
종료하면서 onUnbind() -> onDestroy() 함수 실행

또한 startService로 실행한 서비스를 bindService()함수로 이에 바인딩 할 수도 있음.
이때는 바인딩을 해제해도 서비스가 소멸되지 않으므로 stopSelf() 혹은 stopService() 호출이 필요하다.

🔑 bindService 구현 방법

1. Local

  • 바인드 서비스와 클라이언트가 동일 앱에서 실행되면서 private이라면 Binder의 서브클래스 생성하여 구현 가능

2. Local X (다른 프로세스 실행, 즉 다른 apk)

  • Messenger와 Handler 사용
  • AIDL(Android Interface Definition Language)을 사용하여 구현

Binder는 기본적으로 프로세스간 통신을 제공한다.
IPC(Inter Process Communication)

  • 프로세스 통신, 리눅스 커널에서 Binder 이용하여 프로세스간 메시지를 주고 받도록 구현되어 있음
  • 안드로이드는 리눅스 커널 기반으로 만들어져 있으므로, process는 커널 내부의 일정 공간을 공유하여 함수를 호출하며 이를 Binder Driver가 수행. 데이터를 parcel 형태로 전달하기 때문에 parcelable 클래스를 활용한다.(serializable도 있는데 parcelable 사용이 더 정형화돼있음)

🔑 local bindService 연결

Service에서 Binder 만들기

class BoundService : Service() {

    override fun onBind(intent: Intent?): IBinder {
        return MyLocalBinder()
    }

    inner class MyLocalBinder : Binder(){
        //Service의 레퍼런스를 반환
        fun getService() : BoundService{
            return this@BoundService
        }
    }

    fun getCurrentTime(): String {
        return Date().toString()
    }
}

Activity에서 서비스와 연결

class BindActivity : AppCompatActivity() {
    private lateinit var binding : ActivityBindBinding
    lateinit var myService: BoundService
    var isBound = false
    val connection = object : ServiceConnection{
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as BoundService.MyLocalBinder
            myService = binder.getService()
            isBound = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            isBound = false
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityBindBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.button.setOnClickListener {
            if(isBound){
                val result = myService.getCurrentTime()
                binding.textView.text = result
            }
        }
    }

    override fun onStart() {
        super.onStart()
        // binding 처리(연결)
        val intent = Intent(this, BoundService::class.java)
        bindService(intent, connection, BIND_AUTO_CREATE)
    }
}

🔑 AIDL 사용한 bindService 연결

Messenger & Handler을 사용하하거나 AIDL을 사용하면 별도의 프로세스에서 서비스 부르기 가능

  • Handler는 멀티쓰레드 상황에서 동시처리가 불가능하나 AIDL은 여러 요청을 동시 요청 및 처리


AIDL 파일을 만들고


서비스에서 넣고 싶은 메서드를 넣어주고 Make Project를 하면 자동으로 Interface 파일이 생성된다.

외부 서비스 파일

package com.ssafy.service.c_aidl_binder

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import java.text.SimpleDateFormat
import java.util.*

private const val TAG = "AIDLService_싸피"
class AIDLService : Service() {

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate: ")
    }
    // AIDL로 정의해서 생성한 Stub() 구현
    private val myBinder = object : IMyAidlInterface.Stub(){
        override fun getCurrentTime(): String {
            Log.d(TAG, "getCurrentTime: ")
            return Date().toString()
        }
    }


    override fun onBind(p0: Intent?): IBinder {
        Log.d(TAG, "onBind: ")
        //위에서 생성한 binder 리턴.
        return myBinder
    }
}


서비스 호출하는 앱의 클래스

class AIDLActivity : AppCompatActivity() {
    private var bound = false
    private var timeService: IMyAidlInterface? = null

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
            Log.d(TAG, "onServiceConnected: $p1 ")
            //binder 를 asInterface로 casting.
            timeService = IMyAidlInterface.Stub.asInterface(p1)

            bound = true
        }

        override fun onServiceDisconnected(p0: ComponentName?) {
            Log.d(TAG, "onServiceDisconnected: ")
            timeService = null
            bound = false
        }
    }

    override fun onStart() {
        super.onStart()
        if (!bound) {
            Log.d(TAG, "onStart: ")
            //Component Name 생성하고, bindService  호출.
            val target = ComponentName("com.ssafy.service.c_aidl_binder" , "com.ssafy.service.c_aidl_binder.AIDLService")
            val intent = Intent()
            intent.setComponent(target)
            bindService(intent, connection, BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        if (bound) {
            unbindService(connection)
            bound = false
        }
    }

    private lateinit var binding: ActivityAidlactivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityAidlactivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.button2.setOnClickListener{
//            val time = timeService?.getCurrentTime()?:"00:00"
            val format = SimpleDateFormat("yyyy-MM-dd kk:mm:ss E", Locale.KOREAN) //format 설정
            format.timeZone = TimeZone.getTimeZone("Asia/Seoul") //TimeZone  설정 (GMT +9)

            binding.textView2.text = "AIDL Service 호출. \n한국시간은 ${format.format(Date().time)} 입니다."
        }
    }
}

📌 Foreground Service

항상 상태바 알림에 표시되는 서비스
활성화된 액티비티와 동급의 우선순위를 가져 메모리가 부족하더라도 시스템에 의해 종료될 확률이 적음.

일반 권한으로, manifest에 선언만 하면 자동 승인된다.

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

Foreground 서비스 선언

class ForegroundMusicService : Service() {
    lateinit var mp: MediaPlayer

    override fun onCreate() {
        super.onCreate()
        mp = MediaPlayer.create(this,R.raw.jazzbyrima)
        Log.d(TAG, "onCreate()")
    }
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d( TAG, "Action Received = ${intent?.action}" )
        when (intent?.action) {
            Actions.START_FOREGROUND -> {
                Log.d(TAG, "Start Foreground 인텐트를 받음")
                if( !mp.isPlaying ){
                    mp.isLooping = true
                    mp.start()
                    startForegroundService()
                }
            }
            Actions.STOP_FOREGROUND -> {
                Log.d(TAG, "Stop Foreground 인텐트를 받음")
                if( mp.isPlaying ) {
                    stopForegroundService()
                    mp.stop() //음악 중지
                }
            }
            Actions.PLAY ->{
                Log.d(TAG, "start music from notification : ${mp.isPlaying}")
                if( !mp.isPlaying ){
                    Log.d(TAG, "start music from notification")
                    mp.start()
                }
            }
            Actions.STOP ->{
                Log.d(TAG, "stop music from notification")
                if( mp.isPlaying ) mp.pause() //음악 중지
            }
        }; return START_STICKY
    }

    private fun startForegroundService() {
        val notification = MusicNotification.createNotification(this)
        startForeground(NOTIFICATION_ID, notification)
    }

    private fun stopForegroundService() {
        stopForeground(true)
        stopSelf()
    }

    override fun onBind(intent: Intent?): IBinder? {
        // bindservice가 아니므로 null
        return null
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy()")
    }

    companion object {
        const val NOTIFICATION_ID = 20
    }
}

📌 Notification

상태바 알림기능

  • 새로운 상태바 아이콘을 생성
  • 확장된 상태바에 추가정보를 표현할 수도 있으며
  • 진동 등을 추가해서 알림을 줄 수도 있음

🔑 Notification 구현

  1. Channel 생성
  2. Notification 생성
  3. Notification Manager로 전송

Notification Manager을 통해 채널 생성

        // Oreo 부터는 Notification Channel을 만들어야 함
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                CHANNEL_ID, //임의로 만든 채널 ID 값
                "Music Player Channel", // 채널표시명
                NotificationManager.IMPORTANCE_HIGH
            )
            val manager = context.getSystemService(NotificationManager::class.java)
            manager?.createNotificationChannel(serviceChannel)
        }

Notification 생성하여 notification manger로 전송

        val intent = Intent(applicationContext, SimpleNotification::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        val pendingIntent = PendingIntent.getActivity(applicationContext, 0,
            intent, PendingIntent.FLAG_IMMUTABLE)

        val builder = NotificationCompat.Builder(this, channelId)
        builder.setSmallIcon(R.drawable.ic_baseline_add_comment_24)
        builder.setContentTitle(title)
        builder.setContentText(content)
        builder.priority = NotificationCompat.PRIORITY_DEFAULT
        builder.setAutoCancel(true)
        builder.setContentIntent(pendingIntent)

        val notificationManager = NotificationManagerCompat.from(this)
        notificationManager.notify( 100, builder.build())
profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글