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를 사용하면 된다.
'국비교육(22-23)' 카테고리의 다른 글
86일차(2)/Android App(52) : mp3 파일 재생 예제 / ProgressBar 사용 (0) | 2023.02.11 |
---|---|
86일차(1)/Android App(51) : mp3 파일 재생 예제 / MediaPlayer(서버) (0) | 2023.02.10 |
85일차(1)/Android App(49) : Notification(2) (0) | 2023.02.09 |
84일차(2)/Android App(48) : Notification(1) (0) | 2023.02.09 |
84일차(1)/Android App(47) : Content Provider (0) | 2023.02.08 |