국비교육(22-23)

85일차(2)/Android App(50) : Service / Binder 객체로 mp3파일 재생하기

서리/Seori 2023. 2. 10. 00:16

85일차(2)/Android App(50) : Service / Binder 객체로 mp3파일 재생하기

 

새 모듈 생성-Empty Activity

step22service

 

 

- 안드로이드에서 mp3 음악파일을 플레이할 수 있는 객체

 MediaPlayer 로 음악 파일을 재생하는 예제를 만들어볼 예정!

 

- 인터넷 상의 특정 파일을 로딩할 수도 있지만 이번에는 앱 안에 파일을 넣어놓고 로딩해서 써볼 것

 

- /res/raw 폴더 생성

- raw 라는 정해진 이름을 사용해야 한다.

- 여기에 집어넣은 파일은 이후 어플리케이션 파일을 만들어내도 변형되지 않고 원형 그대로 유지된다.

- 그대로 유지되어야 하는 파일들을 raw 폴더 안에다가 넣어놓고 사용하면 된다.

 

 

- R.raw.mp3piano 로 참조 가능!

 


 

MainActivity (mp 포함된 상태)

package com.example.step22service;

import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    MediaPlayer mp;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //음악 로딩하기
        mp=MediaPlayer.create(this, R.raw.mp3piano);
        //음악 재생 버튼을 눌렀을 때 감시할 리스너 등록
        Button playBtn=findViewById(R.id.playBtn);
        playBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.playBtn:
                mp.start();
                break;
        }
    }
    //액티비티가 종료되기 직전에 호출되는 메소드
    @Override
    protected void onDestroy() {
        //음악 종료 및 자원 해제
        mp.stop();
        mp.release();
        //종료되기 전에 할 작업은 super.onDestroy() 를 호출하기 전에 한다.
        super.onDestroy();
    }
}

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="MusicService 시작"
        android:id="@+id/startBtn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="MusicService 종료"
        android:id="@+id/stopBtn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="일시중지"
        android:id="@+id/pauseBtn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/playBtn"
        android:text="재생"/>

</LinearLayout>

 

** play 버튼 기능 구현 (playBtn)

 

- 액티비티가 리스너 역할을 하도록 MainActivity에 리스너 구현

 

- 다른 버튼도 리스너로 만들 예정이기 때문에

 이 버튼을 누른 경우를 switch문으로 분기했다.

 

- MediaPlayer mp; 를 필드로 빼기. 아래 onClick 메소드에서도 사용하기 위해!

 

 

- 버튼을 누르면 음악이 재생되도록 하려면 이렇게 3개의 작업의 필요하다.

1) MediaPlayer 객체 create → 2) start → 3) 종료 및 자원 해제(release)

 

 

 

- 이 mp가 지배하는 컨텍스트는 액티비티이다.

- 액티비티가 활성화되어 있을 때에만 음악이 나오는 것이 맞다.

 

- 하지만 화면(액티비티) 밖으로 나가도 계속 재생된다.

 액티비티가 메모리상에 남아있기 때문에!!(onStop상태)

- 앱을 아예 제거해주면(onDestroy) 그때 정지된다.

 

- 이렇게 액티비티와 상관없이 음악이 계속 진행되도록 하고싶다면? Service 가 필요하다.

- 서비스는 액티비티와 상관없이 안드로이드 운영체제하에서 계속 진행시킬 수 있는 것이다.

 안드로이드의 4대 컴포넌트 중 하나!

 

- 액티비티의 활성화/비활성와 유무와 상관없이 백그라운드에서 음악을 재생하려면 Service가 필요하다.

- 단, Service의 동작을 액티비티에서 제어할 수 있어야 한다.

  그래야 원하는 시점에 재생/일시정지/정지 등이 가능하다.

 

- 하지만 서비스와 액티비티 사이의 소통이 그리 간단하지는 않다.

- 서비스는 운영체제가 객체를 생성해서 활성화시키는데,

 서비스의 참조값을 어떻게 얻어와야 할까? 어떻게 제어할 수 있을까?

 

- 서비스에는 UI가 없다. 백그라운드에서 돌아가기 때문에 당연히 없다!

 


 

 

- 새 서비스 만들기

MusicService

package com.example.step22service;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

/*
    - Service 는 안드로이드 4대 component 중 하나이다.
    - Service 추상클래스를 상속받아서 만든다.
    - UI 없이 액티비티와는 별개로 백그라운드에서 동작이 가능하다.
    - Service 를 활성화시키기 위해서는 Intent 객체가 필요하다.
 */
public class MusicService extends Service {
    //필드
    MediaPlayer mp;

    //생성자
    public MusicService() {
        Log.e("MusicService", "MusicService()");
    }
    //서비스가 활성화(서비스 객체가 생성)될 때 최초 한번만 호출되는 메소드
    @Override
    public void onCreate() {
        super.onCreate();
        //음악 로딩하기
       mp=MediaPlayer.create(this, R.raw.mp3piano);
    }
    //서비스가 시작될 때 호출되는 메소드
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e("MusicService","onStartCommand()");
        //음악 재생
        mp.start();
        /*
            서비스는 원칙적으로 백그라운드에서 계속 실행되는 component 이지만
            운영체제가 자원이 부족하면 임의로 비활성화시켰다가
            자원의 여유가 생기면 해당 서비스를 다시 시작시켜 주기도 한다.
         */
        //운영체제가 강제로 종료시켜도 다시 시작되지 않도록
        return START_NOT_STICKY;
    }
    //음악을 재생하고 일시정지하는 메소드 추가
    public void playMusic(){
        mp.start();
    }
    public void pauseMusic(){
        mp.pause();
    }

    @Override
    public void onDestroy() {
        Log.e("MusicService","onDestroy()");
        //중지 및 자원 해제
        mp.stop();
        mp.release();
        super.onDestroy();
    }

    //액티비티(혹은 다른 Component)에서 서비스에 연결되면 호출되는 메소드
    @Override
    public IBinder onBind(Intent intent) {

        //필드에 있는 Binder 객체를 리턴해준다.
        return binder;
    }

    //필드에 바인더 객체의 참조값 넣어두기
    final IBinder binder=new LocalBinder();

    //Binder 클래스를 상속받아서 LocalBinder 클래스를 정의한다.
    public class LocalBinder extends Binder{
        //MusicService 객체의 참조값을 리턴해주는 메소드
        public MusicService getService(){
            return MusicService.this;
        }
    }
}

 

- 서비스는 추상클래스 Service 를 상속받아서 만든다.

 

 

- 오버라이드한 onBind 추상 메소드에서는 Binder 객체를 리턴해준다.

 

- 서비스는 개별 액티비티가 아니라 운영체제의 것이므로,

 서비스에서 어떤 작업을 하려면 서비스를 연결해야 된다.

- 이 때 연결고리가 되는것이 onBind() 메소드이다.

 

- 3개 메소드 오버라이드

 onCreate() , onStartCommand() , onDestroy()

- 액티비티의 생명 주기 메소드처럼 서비스에도 이런 메소드들이 있다.

 

- 서비스도 manifest 에 표기되어야 사용할 수 있다.

- 아까 File-New Service 에서 만들었기 때문에 자동으로 등록되어 있다.

 (다른 경로로 서비스를 직접 만든다면 직접 manifest에 추가해주어야 한다.)

 

- Service 의 장점은? 액티비티의 활성화/비활성화 여부와 상관없이 어떤 기능을 계속 살아있게 할 수 있다.

- 백그라운드에서 계속 음악 재생이 가능하다.

 

- onCreate 는 서비스가 활성화(서비스 객체가 생성)될 때 최초 한번만 호출되는 메소드이다.

 

- 운영체제가 강제로 종료시키면 다시 시작되지 않도록 상수값으로 정의된 설정을 사용한다.

 

- Service는 원칙적으로 백그라운드에서 계속 실행되는 component 이지만,
  운영체제가 자원이 부족하면 임의로 비활성화시켰다가
  자원의 여유가 생기면 해당 서비스를 다시 시작시켜 주기도 한다.

- 그런데 언제 다시 시작될지는 보장할 수 없다... 나중에 조용한곳에 있는데 갑자기 시작돼버릴수도 있다..

 

- 저 START_NOT_STICKY 코드는 이런 경우를 대비해 서비스에 어떤 옵션을 전달하는 것이다.

 운영체제가 서비스를 강제 종료시키면 다시 시작시킬 필요가 없다는 뜻!

 

- 비활성화 시, 객체는 사라지지 않고 남아있는 상태다.

- 서비스가 재시작되면 startCommand() 만 호출된다.

 

- onDestroy() 는 완전한 종료. 자원해제를 말한다.

 

 

- onDestroy() 메소드 안에서 끝내기 직전에 수행할 코드를 작성할 수 있다.

- 단, super.onDestoy(); 보다 위에서!!

 


 

- MainActivity 에서 MediaPlayer 객체 관련해서는 전부 삭제해주기

 (이제 MusicService를 생성했으므로)

package com.example.step22service;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    //서비스의 참조값을 저장할 필드
    MusicService service;
    //서비스에 연결되었는지 여부
    boolean isConnected;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //서비스 시작 버튼
        Button startBtn=findViewById(R.id.startBtn);
        //서비스 종료 버튼
        Button stopBtn=findViewById(R.id.stopBtn);
        //일시정지 버튼
        Button pauseBtn=findViewById(R.id.pauseBtn);
        startBtn.setOnClickListener(this);
        stopBtn.setOnClickListener(this);
        pauseBtn.setOnClickListener(this);

        //음악 재생 버튼을 눌렀을 때 감시할 리스너 등록
        Button playBtn=findViewById(R.id.playBtn);
        playBtn.setOnClickListener(this);

        //액티비티 종료 버튼을 눌렀을 때 감시할 리스너 목록
        Button endBtn=findViewById(R.id.endBtn);
        endBtn.setOnClickListener(this);

    }
    //서비스에 연결한다.
    @Override
    protected void onStart() {
        super.onStart();
        // MusicService 에 연결할 인텐트 객체
        Intent intent=new Intent(this, MusicService.class);
        //액티비티의 bindService() 메소드를 이용해서 연결한다.
        // 만일 서비스가 시작이 되지 않았으면 서비스 객체를 생성해서
        // 시작할 준비만 된 서비스에 바인딩이 된다.
        bindService(intent, sConn, Context.BIND_AUTO_CREATE);

    }

    //서비스 연결 해제
    @Override
    protected void onStop() {
        super.onStop();
        if(isConnected){
            //서비스 바인딩 해제
            unbindService(sConn);
            isConnected=false;
        }
    }

    //서비스 연결 객체를 필드로 선언한다.
    ServiceConnection sConn=new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            //IBinder 객체를 원래 타입으로 casting
            MusicService.LocalBinder IBinder=(MusicService.LocalBinder)binder;
            //MusicService의 참조값을 필드에 저장
            service=IBinder.getService();
            //연결되었다고 표시
            isConnected=true;
        }
        //서비스와 연결 해제되었을 때 호출되는 메소드
        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.startBtn:
                //MusicService를 시작 시킨다.
                Intent intent=new Intent(this, MusicService.class);
                //액티비티의 메소드를 이용해서 서비스를 시작시킨다.
                startService(intent);
                break;
            case R.id.stopBtn:
                //MusicService를 비활성화시키기 위한 객체
                Intent intent2=new Intent(this, MusicService.class);
                //액티비티의 메소드를 이용해서 서비스 종료 시키기
                stopService(intent2);
                break;
            case R.id.pauseBtn:
                //필드에 저장되어 있는 MusicService 객체의 참조값을 이용해서 메소드 호출
                service.pauseMusic();
                break;
            case R.id.playBtn:
                //필드에 저장되어 있는 MusicService 객체의 참조값을 이용해서 메소드 호출
                service.playMusic();
                break;
            case R.id.endBtn:
                finish(); //액티비티 종료
                break;

        }
    }
    //액티비티가 종료되기 직전에 호출되는 메소드
    @Override
    protected void onDestroy() {
        Log.e("MainActivity","onDestroy()");
        //종료되기 전에 할 작업은 super.onDestroy() 를 호출하기 전에 한다.
        super.onDestroy();
    }
}

 

- startService(intent); 로 서비스를 시작한다.

 

 

- startService() 를 누르면 이렇게 로그가 출력된다.

 

- 백 버튼으로 액티비티를 비활성화시키고 액티비티를 직접 종료시켜야 음악이 종료된다.

 


 

 

- xml파일에 버튼을 하나 추가해준다.

 

 

- finish() 는 액티비티를 끝내는 것 (완전 종료)

- onDestroy() 는 액티비티를 비활성화시키는 것. 두가지를 구분하기!

 

- 액티비티가 destroy 되어도 음악은 계속 나온다.

- 프로세스 자체를 죽이려면 해당 앱의 실행 창에서 완전히 제거해야 한다.

 

- 액티비티가 종료되어도 재생되게 하려면 서비스에서 실행하면 된다.

 


 

- 다른 버튼에도 전부 리스너 등록해주기

 

 

- MusicService에서 메소드 추가

 

- 미디어 플레이어를 제어하는 메소드(play, pause)

 

- 서비스의 객체 생성은 운영체제가 한다. 우리는 메인액티비티에서 그것을 달라고 요청하는 것이다

- 직접 new 해서 사용하지 않는다!

- 그래서 위의 playMusic() 등의 메소드를 직접 호출할 수는 없다.

- 객체의 참조값이 있어야만 메소드를 점찍어서 직접 호출할 수 있는 것이다.

 

- 그러면 서비스의 참조값은 어떻게 얻어낼까?

onBind() 메소드 활용

 

 

- MusicService 클래스 안에 Binder 클래스 생성

 

 

- 필드에 있는 바인더 객체를 리턴하도록 한다.

 

 

- 메인액티비티에서 이 객체를 받아간다.

 

- 바인더 객체의 참조값을 넣어 리턴해주면

 getService() 메소드를 호출해서 MusicService 객체의 참조값을 얻어낼 수 있다.

 


 

- onStart 오버라이드

 

- 액티비티에 bindService() 라는 메소드가 있다. 3개의 인자를 받는다!

 

 

- ServiceConnection 객체를 추상클래스로 객체의 참조값을 익명의 이너클래스를 사용해서 얻어내기

 

 

- override된 메소드 안의 인자로 IBinder 객체가 들어온다.

 이 메소드안에서 IBinder 객체의 참조값을 얻어낼 수 있다.

(변수명이 service로 되어있는데 헷갈리니 이름을 바꿔준다)

 

- service, isConnected 필드 선언

 

- 이렇게 얻어낸 값을 bindService 메소드에 넣어준다! sConn은 필드에 있던 값을 넣어주기

 

 

- 이 코드에서 가장 중요한 부분은 MusicService의 참조값을 구하는 것이다.

- 액티비티에서 직접 생성했다면 참조값을 구하기 어렵지 않지만,

 서비스는 운영체제가 생성한 것이기 때문에 참조값을 얻어내는 과정이 좀 복잡하다

 

 

- 바로 이 일시중지, 재생 버튼을 누르면 나오는 메소드를 호출하기 위해 참조값이 필요해서 binding 객체를 활용한 것이다.

 

- 서비스 시작이 아니라 재생만 눌러도 재생이 된다.(MusicService가 대신해주는 것이다.)

- 일시정지를 누르면 정지되고, 액티비티를 종료해도 음악이 정지된다.

- 앱을 완전종료한 후 다시 해당 어플로 들어가서 재생하면 다시 재생된다.

 


 

 

* 실행 구조 참고 링크 : https://link2me.tistory.com/1343

 

 

 

- 위에서 음악이 재생되도록 만든 두가지 방법은 프로세스가 다르다!

왼) Activity를 start시키고 재생하기

오) Service를 binding해서 재생하기

 

- 바인딩으로 연결한 경우 바인딩이 끝나면 서비스가 죽어버린다.

- 위 코드처럼 unbindService()로 바인딩을 해제하게 되면 서비스가 끝난다.

 

1) MusicService 실행 → MusicService 시작(startCommand 메소드를 호출해서 음악재생) → 액티비티 종료하면 음악이 계속 나온다.

2) 재생버튼을 눌러서 실행(bind를 사용해서 음악 재생) → 액티비티를 종료하면 음악도 같이 꺼진다.

 

 

- 연결방식과 생명주기에 따른 두 가지의 차이 기억하기!

1) 은 액티비티에서 서비스를 실행한 것이므로 액티비티가 종료되어도 서비스가 백그라운드에서 계속 유지되지만

2) 의 경우 서비스와의 바인딩이 끝나면 더이상 바인딩이 돌지 않아서 서비스가 유지되지 않는다.

 

- 용도에 따라 미묘한 차이가 있기 때문에 구분해서 사용해야 한다.

 

- 백그라운드에서 실행하고 싶은 프로세스가 있다면 Service를 사용하면 된다.