본문 바로가기
카테고리 없음

[Android] Service를 사용하여 백그라운드에서도 끊김없이 음악 재생하기

by 베어 그릴스 2021. 11. 21.

MediaPlayer()

음악 재생 기능을 구현할 때에는 MediaPlayer클래스를 사용합니다.MediaPlayer의 사용법에 대해서는 어렵지 않으니 금방 습득할 수 있을 거라 생각하고 생략하고 진행하겠습니다.

MediaPlayer를 액티비티 위에서 구현 할 시 그 액티비티에 대한 포커스를 잃으면 MediaPlayer에 대해 접근또한 불가능해진다는 큰 문제점이 있습니다.따라서 백그라운드에서, 그리고 다른 액티비티로 넘어갔을 때에도 끊김없이 음악이 재생되게 하기 위해 Service 를 사용합니다.

Service란?

사용자가 해당 어플리케이션을 사용하고 있지 않더라도 백그라운드에서 계속해서 동작합니다. 서비스는 사용자가 특정 행동을 통해 종료시킬 수도 있으며, 종료되지 않는 서비스 또한 구현이 가능합니다.대표적으로

  • 카카오톡 푸시알림 서비스
  • 헬스케어 어플의 자동 걸음 측정 서비스
  • 다운로드 진행상황을 보여주는 알림
  • 음악 스트리밍 어플의 경우 백그라운드에서도 음악 플레이어 작동

과 같은 것들이 Service를 사용하여 구현할 수 있는 것들입니다.

Service의 생명주기

서비스의 생명주기는 액티비티보다 훨씬 간단합니다.

  • 왼쪽
  • 액티비티와 바인딩 되지 않고 단독으로 사용된 서비스의 수명주기입니다.startService()를 통해 시작되며, onStartCommand()에서 정의된 코드가 액티비티와 상호작용없이 혼자서 계속 동작하기만 하다가 액티비티에서 stopService()를 호출하면 onDestroy()가 호출되며 소멸합니다.
  • 오른쪽
  • 액티비티와 바인딩 되어 상호작용하는 서비스의 수명주기입니다.bindService()를 통해서 생성되며, onBind() 가 호출되면 액티비티와 통신이 가능한 상태가 됩니다. Service 클래스 안에 구현된 함수들을 액티비티에서 호출할 수 있으며, 액티비티에서 unbindService()가 호출되면 onDestroy()가 호출되며 소멸합니다.

단독으로 사용되는 서비스의 경우 구현이 비교적 간단하지만 액티비티에서 생성과 파괴에만 관여할 수 있다는 큰 단점이 있습니다.바인딩된 서비스(바운드서비스)의 경우 구현이 비교적 복잡한 반면, 액티비티와 상호작용하며 액티비티에서 서비스 내의 변수와 함수를 호출할 수 있다는 장점이 있습니다.

서비스를 사용하려면 항상 서비스 클래스를 만든 후 Manifest 파일에 등록해주어야 합니다.

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

단독으로 사용되는 서비스

먼저 바인드 되지 않고 단독으로 사용되는 서비스 입니다.

  • MusicPlayerService.class
class MusicPlayerService : Service() {

// mediaPlayer 객체 생성
    var music = 0
    var millis = 0
    var mediaPlayer : MediaPlayer? = null

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

        override fun onCreate() {
        super.onCreate()
    }

        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (intent != null) {
            val stringmusic = intent.getStringExtra("musicService")!!
            music = resources.getIdentifier(stringmusic, "raw", this.packageName)
            millis = intent.getIntExtra("millisService", 0)
        }
        mediaPlayer = MediaPlayer.create(this, music)
        mediaPlayer?.setLooping(false) // 반복재생
        mediaPlayer?.seekTo(millis)
        mediaPlayer?.start()
        return START_NOT_STICKY
    }

        override fun onDestroy() {
        mediaPlayer?.stop()
        mediaPlayer?.release()
        mediaPlayer = null
        super.onDestroy()
    }
  • MainActivity.class
class MainActivity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		//....
		binding.playBtn.setOnClickListener{
        		val serviceIntent = Intent(this,MusicPlayerService::class.java)
                	startService(serviceIntent)
       		 }

         	binding.pauseBtn.setOnClickListener{
        		val serviceIntent = Intent(this,MusicPlayerService::class.java)
                	stopService(serviceIntent)
       		 }

		//....
		}
	}

바인드 되지 않는 서비스는 onBind를 구현할 필요없이 null을 리턴해주면 됩니다.메인 액티비티를 보면 플레이 버튼과 일시중지 버튼을 눌렀을 때 startService와 stopService가 호출되며 서비스가 생성, 소멸 되는 것을 알 수 있습니다.중요한 것은 이 두 메서드를 호출할 때는 꼭 intent를 매개변수로 넘겨주어야 합니다.안드로이드 4대 컴포넌트인

  • Activity
  • Service
  • Content Provider
  • Broadcast Receiver

끼리 통신을 할 때는 항상 intent가 인자로 넘어다닌다 기억해두시면 좋습니다.서비스 클래스에 대한 intent 를 인자로 넘겨받아 startService()가 호출되면 MusicPlayerService의 onCreate() -> onStartCommand()가 호출되어 서비스가 실행중인 상태로 됩니다.위의 코드에서는 intent로 음악에 대한 정보를 넘겨받아 mediaPlayer를 생성하고, 정보로 넘어온 위치로 가서 음악을 재생합니다.

onStartCommand()의 반환값은 무엇을 의미하나요?

위 코드를 보시면 onStartCommand()에서 정수값을 반환하는 것을 알 수 있습니다.반환값에는 세가지가 있으며 이 중 하나를 선택합니다.

  • START_NOT_STICKY
  • 시스템에서 이 서비스를 강제종료 했다면 다시 서비스가 실행되지 않습니다. 이 옵션을 사용하면 더이상 필요하지 않은 서비스가 실행되어 불필요한 자원을 소비하는 것을 막을 수 있습니다.가장 안전한 옵션이며, 애플리케이션이 완료되지 않은 모든 작업을 단순히 다시 시작할 수 있을 때 유용합니다.
  • START_STICKY
  • 시스템에서 이 서비스를 강제종료 했다면 서비스를 다시 생성하고 onStartCommand()를 호출하되, 마지막 intent는 전달하지 않습니다.명령을 실행하지는 않지만 무한히 실행 중이며 작업을 기다리고 있는 미디어 플레이어(또는 그와 비슷한 서비스)에 적합합니다.
  • START_REDELIVER_INTENT
  • 시스템에서 이 서비스를 강제종료 했다면 서비스를 다시 생성하고 onStartCommand()를 호출하며, 서비스가 강제종료되기 전에 전달된 마지막 intent를 다시 전달해주는 기능까지 포함합니다.즉시 재개되어야 하는 작업을 능동적으로 수행 중인 서비스(예: 파일 다운로드 등)에 적합합니다.

서비스는 백그라운드에서도 계속해서 동작하기 때문에 자원 사용량이나 메모리 환경에 따라 시스템이 강제로 종료시키는 경우가 발생합니다. 따라서 이런 경우가 발생 했을 때 서비스를 다시 실행시킬지, 그대로 종료상태로 둘지를 알려주는 값이라고 이해하시면 됩니다.

바인딩된 서비스(바운드 서비스)

액티비티와 상호작용을 하기 위해서는 위의 방식과 작동방식이 조금 다릅니다.먼저 코드를 보겠습니다.

  • MediaPlayerService.class
class MediaPlayerService : Service() {
    private val mBinder = LocalBinder()
    var music = 0
    var playTime = 0
    var mediaPlayer : MediaPlayer? = null

    inner class LocalBinder : Binder() {
        fun getService() : MediaPlayerService = this@MediaPlayerService
    }

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

    override fun onCreate() {
        super.onCreate()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        if(!mediaPlayer?.isPlaying!!){
            mediaPlayer?.release()
            mediaPlayer = null
        }
        super.onDestroy()
    }

    fun playMusic(song : Song){
        mediaPlayer?.seekTo(song.currentMillis)
        mediaPlayer?.start()
    }

    fun stopMusic(){
        mediaPlayer?.pause()
    }

    fun initService(song : Song){
        if(mediaPlayer == null){
            music = resources.getIdentifier(song.music, "raw", this.packageName)
            mediaPlayer = MediaPlayer.create(this, music)
            mediaPlayer?.isLooping = false // 반복재생
        }
        playTime = mediaPlayer?.duration!!
    }
}
  • MainActivity.class
class MainActivity : AppCompatActivity() {

    private lateinit var mediaPlayerService : MediaPlayerService
    private var isServiceBound = false

    override fun onCreate(savedInstanceState: Bundle?) {
        //....
        // bindService()가 실행되면 액티비티와 서비스가 바인딩되어 상호작용 가능한 상태가 된다.
        val serviceIntent = Intent(this,MediaPlayerService::class.java)
        bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)

        binding.playBtn.setOnClickListener{
      // song(data class)에 대한 객체 초기화 작업
      // 그 다음 서비스에 song으로 음악정보 전달
            mediaPlayerService.playMusic(song)
            }

         binding.pauseBtn.setOnClickListener{
            // ....
            mediaPlayerService.stopMusic()
        }
        /....
    }

    override fun onDestroy(){
        unbindService(connection)
        super.onDestroy()
    }

    // bindService 에 인자로 넘겨줄 connection 정의
    private val connection = object : ServiceConnection{
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as MediaPlayerService.LocalBinder
            mediaPlayerService = binder.getService()
            mediaPlayerService.initService(song)
            isServiceBound = true
        }

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

    }
}

바운드 서비스의 경우 binService 매서드를 호출하기에 service class 에서 onStartCommand()를 정의해줄 필요는 없습니다.(단, 액티비티 코드 내에서 startService()를 사용하는 경우가 있다면 그에 맞는 코드를 구현해주어야 합니다.)

onBind() 매서드를 필수로 구현해야 하기에 onBind()가 리턴하고 있는 IBinder 또한 구현해주어야 합니다.이는 Binder() 클래스를 상속받고 있는 LocalBinder inner class 를 구현하여 mBinder 변수에 담아주었습니다.

메인 액티비티에서 서비스와 바인딩을 할 때 getService()가 호출되므로 이해가 잘 안된다면 그냥 저렇게 쓰면 되는구나 하고 외워서 써도 됩니다.

이제 메인액티비티를 보겠습니다.위에서 단독으로 사용되는 서비스에 접근할 때는 인텐트 하나만 인자로 넘겨주었지만, 이번에는 intent, connection, flag상수 세개가 인자로 전달됩니다.

서비스를 바인딩 하기 위해 필요한 인자

  • intent
  • 위의 방법과 같습니다. 서비스 클래스에 대한 intent를 생성해서 넘겨줍니다.
  • connectionobject로 정의 되어 있는 ServiceConnection 객체를 상속받아onServiceConnected와 onServiceDisconnected를 오버라이딩 하여 구현합니다.우리가 Service class에서 구현한 LocalBinder()의 getService를 여기있는 onServiceConnected에서 호출하여 액티비티와 서비스를 바인딩하게 됩니다. onServiceDisconnected에서는 사용했던 자원을 정리하는 작업을 해주면 됩니다.
  • isServiceBound 변수의 경우 현재 코드에서는 사용하지 않았지만 나중에 어플리케이션을 나갔다 들어오거나 하는경우 서비스의 중복생성을 방지하기 위한 방어코드를 짤 때 활용하면 됩니다.
  • 이름을 보면 알겠지만 onServiceConnected는 bindService를 할 때 호출되며onServiceDisconnected는 unBindService를 할 때 호출됩니다.
  • 서비스와 연결될 때 필요한 콜백 인자로써 bindService(), unbindService() 두 매서드가 호출될 때 항상 필요합니다.
  • flag가장 많이 쓰이는 상수가 Context.BIND_AUTO_CREATE이고 다른 여러가지 상수들이 많긴 하지만 굳이 여기서 다루진 않겠습니다.따라서 BIND_AUTO_CREATE를 사용했을 때 에는 모든 액티비티에서 unBindService()가 호출되었을 때 서비스가 onDestroy()가 되며 소멸하게 됩니다. 즉 하나의 액티비티에서라도 서비스에 바인딩 되어있다면 서비스는 계속해서 동작합니다.
  • 바운드 서비스는 동시에 여러 액티비티에서 바인딩 될 수 있습니다. 따라서 하나의 액티비티에서 unBindService()가 호출 되었을 때 서비스도 onDestroy()되어 소멸한다면 남아있는 다른 액티비티에서 이 서비스에 대한 바인딩을 잃어버리게 됩니다.
  • Context 의 상수등을 넣어 서비스의 시작방법 등을 정의합니다.

따라서 서비스 내에서의 IBinder()와 액티비티에서의 connection만 잘 정의해 준다면 액티비티와 서비스가 바인딩 되어 상호작용 가능한 상태가 됩니다.

바운드 서비스의 경우bindService()를 통해 생성되며, 모든 액티비티에서 unbindService()가 호출되면 그제서야 소멸합니다.만약 bindService()와 함께 startService()도 사용했다면(이 경우 onStartCommnd()가 구현되어 있어야 합니다.), 모든 액티비티에서 unbindService()가 호출되고, stopSelf()나 stopService()로 서비스를 완전히 종료시켜주어야 소멸됩니다.

바인딩 서비스를 쓴다면 MainActivity 코드에서 볼 수 있듯이 서비스에 있는 함수나 변수를 마음대로 가져다 쓸 수 있게 됩니다.