Android 기능_Service

소정·2023년 3월 2일
0

Android_with_Java

목록 보기
21/34

[1] Service (background 동작)

  • 백그라운드(앱은 실행중이지만 화면에서 안보이는 상황)에서 코드를 동작하게 하고 싶을 때 사용 ex) 뮤직플레이어 앱

스레드 VS Service

1. 스레드

  • 스레드는 백그라운동작을 할 수 있다
  • 스레드는 메인액티비에 참조변수를 가지고 있다 따라서 메인액티비티가 종료되면 더이상 스레드를 참조할 방법이 없어진다-> 제어할 수가 없어진다
  • 참조변수를 잃은 스레드는 가비지컬렉터도 일을하고 있으니 지우지 않는다 영원히 끌수 없는 문제가 생긴다,,

스레드 백그라운드 실행

main.java

package com.bsj0420.ex61backgroundtaskbythread;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    MyThread myThread;

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

        findViewById(R.id.btn_start).setOnClickListener(v->{
            //백그라운드에서 반복작업을 수행하는 별도의 스레드 객체를 생성 및 실행

            if (myThread != null) return; //이미 동작하는 상황

            myThread = new MyThread();
            myThread.start(); //myThread.run() 하면 메인스레드가 해버림 
            // start() : 자동 run 메소드 발동

        });

        findViewById(R.id.btn_stop).setOnClickListener(v->{
            //스타트 버튼 안누르고 눌렀을 때 대비
            if(myThread != null){
                //myThread.stop(); //리소스 관리가 안돼서 안드로이드는 금지
                
                //스레드는 run()메소드가 종료되면 멈춤, 1회성 객체라서 다시 실행불가
                //while문 때문에 run메소드가 종료되지 않고 있음 그러니 멈추려면
                //while문의 조건값 isRun을 false로 변경
                myThread.isRun = false;
                //스레드는 멈추나 스레드(1회성) 객체는 살아있어서 나중에 또 눌러도 동작 안함
                //때문에 참조변수를 null을 넣어서 연결을 끊어준다
                myThread = null;
                
            }else {
                Toast.makeText(this, "Thread 객체를 참조하고 있지 않습니다", Toast.LENGTH_SHORT).show();
            }
        });

    }
    
    //디바이스의 뒤로가기버튼을 클릭했을 떄 반응하는 콜백 메소드
    @Override
    public void onBackPressed() {
        //super.onBackPressed(); // 얘가 안꺼지게 만드는 애
        //기본 메인 액티비티는 백버튼을 눌러도 종료되지않아 강제 종료
        finish();
    }

    //백그라운드 동작을 하는 별도 스레드 클래스 설계
    class MyThread extends Thread{

        boolean isRun = true;

        @Override
        public void run() {

            while (isRun) {
                //별도 스레드는 토스트 못띄움 UI작업 불가!!! -> runOnUiThread
                runOnUiThread( () ->{ //new Runnable 매개변수 없으니까 () 써야함
                    Toast.makeText(MainActivity.this, "백그라운드 작업", Toast.LENGTH_SHORT).show();
                    Log.i("Ex61","백그라운드 작업");
                });

                //5초 정도 대기
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }//myThread

}

스레드 멈추기
스레드는 run()메소드가 종료되면 멈춤, 1회성 객체라서 다시 실행불가 같은 스레드를 다시 동작하게 만들려면 참조변수를 null로 만들고 새롭게 객체를 만들어 줘야함~!

2. Service

  • 4대 컴포넌트
  • Intent를 활용해 실행(제어)한다
  • 메인 액티비티엔 그 어디에도 service의 참조변수가 없다 그래서 메인 액티비티가 종료되어도 참조변수는 Service가 붙잡고 있기 때문에 화면을 껐다 켜도 여전히 제어가능하다
  • Service는 별도의 메인스레드가 동작시킴 운영체제는 서로 다른 앱이라고 본다
  • 서비스는 Context를 상속받아 만들었기때문에 context의 능력을 갖고 있다!!!
  • 오레오 버전부터는 "Forground(전경) Service" 라는 개념이 도입됨 사용자에게 서비스가 구동되고 있다는 느낌을 주기 위한 것 반드시 알림(Notification)을 보이도록 강제하는 서비스 개념 -> 퍼미션 필요

☝ 서비스를 실행하는 방법은 2종류가 있다
1. 'startService()로 서비스가 실행되면' 자동 실행하는 콜백 메소드 존재 : onStartCommand()
2. bindService()로 서비스가 시작되면 자동으로 실행되는 메소드 : onBind()


사용방법

1. Service를 상속받은 java class 만들기

package com.bsj0420.ex62service;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import androidx.annotation.Nullable;

public class MyService extends Service {
    
    //서비스는 안드로이드의 4대 주요 컴포넌트이므로 매니패스트에 등록해야함

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

2. 매니패스트에 만든 서비스 등록

3. 메인 액티비티에서 인텐트를 이용해 서비스 실행
다만 26 버전 이후 인텐트를 서비스로 보내려면 startForegroundService(intnet)로 보내야함 근데 얘는 퍼미션이 필요하다...
근데 하필 동적 퍼미션이라 사용자에게 앱 사용전에 허용을 받아야함,,,
-> 여기서 동적 퍼미션 허용/거부 선택하는 다이아로그 띄워준다

package com.bsj0420.ex62service;

import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

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

        findViewById(R.id.btn_start).setOnClickListener(v->{
            //백그라운드 작업을 Service 컴포넌트 이용
            
            //서비스 제어할 인텐트
            Intent intent= new Intent(this, MyService.class);

            //2.
            //오레오부터 Foregreaoun Service(26버전) 도입 - 퍼미션 필요
            if(Build.VERSION.SDK_INT>=26){
                startForegroundService(intent);
            }else{
                startService(intent);
            }

        });

        findViewById(R.id.btn_stop).setOnClickListener(v->{
            Intent intent= new Intent(this, MyService.class);
            stopService(intent);
        });

        //3.알림에 대한 동적 퍼미션 체크 및 요청
        //3-1. 스스로 체크
        int checkResult = checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS);
        //checkSelfPermission(매니패스트에 퍼미션 등록한 이름)

        if(checkResult == PackageManager.PERMISSION_DENIED) { //퍼미션이 거부되면...
            //4-2 대행사로
            //퍼미션 요청 결과를 받아주는 대행사 객체를 활용
            permissionResultLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
        }
        
    }
    
    //4. 퍼미션 요청 결과를 받아주는 대행사 객체 생성 및 계약 등록 : 반드시 멤버로
    ActivityResultLauncher<String> permissionResultLauncher= registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>() {
        @Override
        public void onActivityResult(Boolean result) {
            if(result) Toast.makeText(MainActivity.this, "알림 허용", Toast.LENGTH_SHORT).show();
            else Toast.makeText(MainActivity.this, "알림 불가. 서비스도 불가", Toast.LENGTH_SHORT).show();
        }
    });

    @Override
    public void onBackPressed() {

        finish();
    }
}

4. 서비스에서 동작

  • 오레오 버전부터는 "Forground(전경) Service" 라는 개념이 도입됨
  • 근데 서비스는 백그라운드에서 작업하는 애임
  • 사용자에게 서비스가 구동되고 있다는 느낌을 주기 위한 알림(Notification)을 무조건 보이도록 강제 한다
  • 알림 객체 만들고 ForgroundService로 실행하라고 요청!!

① 매니 패스트한테 Notification 퍼미션 등록

② 26버전 이상은 startForegroundService(intent)로 인텐트 보내야함
startForegroundService로 실행된 서비스 객체는 반드시! startForeground(Notification이 필요)라는 메소드를 호출해야만 한다!!!!!!
③ 매개변수로 들어갈 NotificationManagerCompat 먼저 생성
④ 알람객체 생성
⑤ startForeground(id(암거나), notification);


#### 서비스.java
package com.bsj0420.ex62service;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

public class MyService extends Service {
    
    MediaPlayer mp; //스레드 따로 만들 필요없은 자체가 스레드상속받고 있음
    
    //서비스는 안드로이드의 4대 주요 컴포넌트이므로 매니패스트에 등록해야함
    
    //'startService()로 서비스가 실행되면' 자동 실행하는 콜백 메소드 존재 : onStartCommand
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Toast.makeText(this, "onStartCommand", Toast.LENGTH_SHORT).show();

        //오레오 버전부터는 "Forground(전경) Service" 라는 개념이 도입됨
        //사용자에게 서비스가 구동되고 있다는 느낌을 주기 위한 것 반드시 알림(Notification)을 보이도록 강제하는 서비스 개념
        // 알림 객체 만들고 ForgroundService로 실행하라고 요청
        // startForeground()라는 메소드를 호출해야만 함.

        //startForegroundService로 실행된 서비스 객체는 반드시
        //startForeground(Notification이 필요)라는 메소드를 호출해야만 한다!!!!!!
        // 알림객체를 만들고 포어그라운드 서비스로 실행하라고 요청!!

        //3.
        NotificationManagerCompat manager= NotificationManagerCompat.from(this);
        NotificationCompat.Builder builder= null;
        if(Build.VERSION.SDK_INT>=26){
            NotificationChannelCompat channel= new NotificationChannelCompat.Builder("ch1", NotificationManager.IMPORTANCE_HIGH).setName("Ex62 알림채널").build();
            manager.createNotificationChannel(channel);

            builder= new NotificationCompat.Builder(this, "ch1");
        }else{
            builder= new NotificationCompat.Builder(this, "");
        }

        builder.setSmallIcon(R.drawable.ic_noti);
        builder.setContentTitle("Ex62 뮤직 서비스");
        builder.setContentText("뮤직 서비스 실행중");
        
        //음악 재생/정지 버튼을 가진 메인액티비티를 알림창이 클릭됐을 때 실행 되도록
        Intent i = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent= PendingIntent.getActivity(this, 11, i, PendingIntent.FLAG_IMMUTABLE);//잠시 보류
        builder.setContentIntent(pendingIntent);

        //4.알림객체 생성
        Notification notification = builder.build();

        //5.
        //포어그라운드로 실행하도록...
        startForeground(1, notification); //startForegroundService로 시작한건 이걸로 만들어야함

        if(mp == null) {
            //서비스도 Context를 상속받아 만들었기때문에 context 자리에 this 사용 가능
            mp = MediaPlayer.create(this,R.raw.kalimba);
            //아래 두가지 속성은 한다
            mp.setVolume(0.7f,0.7f); //소프트웨어의 볼륨 0.0 ~ 1.0
            mp.setLooping(true);
        }

        mp.start(); //일시정지 했을 경우도 있으니 if문 밖에서 스타트
        
        //메모리 문제로 프로세스가 강제로 서비스를 kill 시켜버리는 경우가 있음
        //데스크탑은 마지막 넘이 천천히 실행되나 
        // 모바일은 액티비티보다 서비스를 먼저 죽임
        //다시 메모리문제가 해결되면 운영체제가 자동으로 다시 서비스를 실행시키도록 설정 :START_STICKY
        // 죽임 그냥 순응하고 갈래... : START_NOT_STICKY
        return START_STICKY;
    }

    //stopService()를 실행하여 서비스가 종료도면 자동으로 실행되는 메소드
    @Override
    public void onDestroy() {

        if(mp != null) {
            mp.stop(); //스레드를 상속받아서 stop() 메소드 있음
            mp.release(); //미디어는 용량이 커서 gpu의 램이 감당하는데 cpu램에선 stop하면 지우는데 
            //미디어 프로그램은 release() 해줘야 GPU에서 삭제됨
            mp = null;
        }

        super.onDestroy();
    }

    //bindService()로 서비스가 시작되면 자동으로 실행되는 메소드
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}



3. 서비스의 onBind

일시정지 기능 만들어보기

  • 일시정지를 위해 액티비티에서 마이서비스를 참조해서 서비스의 메소드 제어 이 둘을 연결해주는 애가 : onBind()
  • 참조변수를 대신할 바인드를 사용할 때 주소값을 가져오기 위해 커넥트를 만듦(액티비티가) 그 커넥터를 Binder(서비스가 만듦) 객체를 만들어 왔다 갔다함

💡 스타트서비스와 바인드서비스의 라이프사이클

  • 스타트서비스는 onCreate를 한번만 한다

🔨 사용방법

이번 예제는 서비스를 실행하고 바인드를 만들것이다
서비스 클래스를 만들면 반드시 매니패스트에 등록

서비스 스타트를 실행 뒤 바인드를 하면 동작되는 라이프 사이클 순서



1. 액티비티에서 할 일

  • 서비스를 제어할 수 있는 참조변수 만들고 터널(ServiceConnection new로)을 만든다
  • bindService() 메소드를 통해 Service 객체와 바인드 연결(bind)
  • 터널을 통해 서비스(.java)에서 바인드 객체(서비스의 주소) 넘겨 받는다
  • 만들어둔 참조변수에 바인드 객체가 받아온 서비스 주소를 대입한다
  • 참조변수로 서비스의 메소드들을 가져다 쓴다

💡 bindService() 의 flag : 설정
1) BIND_AUTO_CREATE : 서비스 객체가 없으면 자동으로 만들겠다 라는 뜻
스타트 서비스 안하고 바로 바인드 할 때 이거 쓰면 됨
2) 0 = 만들지않겠다

  • 백그라운드 작업이 여러개 있을 수 있다 바인드가 여러개 있을 수 있다
  • 만약 메인액티비티가 없어지면 서비스스타트 안하고 BIND_AUTO_CREATE로 바인드로만 만들면 메인액티비티가 사라질 때 바인드도 같이 사라짐
  • 스타트 서비스로 시작하면 독립단위로 움직이기 떄문에 바인드는 영향을 받지 않는다

main.java

서비스와 바인드 각각 호출

📢 바인드는 노티피케이션 안만들어도 된다!!!!
-> 근데 동작하고 있다는 것을 알기위해 개발자가 그냥 만듦


package com.bsj0420.ex63servicebind;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    //1. 서비스 참조
    MyMusicService musicService = null;

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

        findViewById(R.id.btn_play).setOnClickListener(view -> clickPlay());
        findViewById(R.id.btn_pause).setOnClickListener(view -> clickPause());
        findViewById(R.id.btn_stop).setOnClickListener(view -> clickStop());

    }

    private void clickStop() {
        if(musicService != null) {
            musicService.stopMusic();
            unbindService(connection); //바인드도 끊어버리기 - 터널 끊어버림
            musicService = null;
        }

        //바인드로만 실행 해으면 위 if문으로 끝내면 사라짐
        //하지만 현재 내 코드에선 스타트 서비스로 시작했기때문에
        //onUnbind 하면 다시 액티비티로 돌아옴 때문에
        //완전하게 서비스를 종료하기 위해서 인텐트로 종료를 보내야함
        //바인드를 하면 포어그라운드 안해도 됨
        Intent intent = new Intent(this, MyMusicService.class);
        stopService(intent);

        //액티비티도 종료
        finish();
    }

    private void clickPause() {
        if(musicService != null) musicService.pauseMusic();
    }

    private void clickPlay() {
        if(musicService != null) musicService.playMusic();
        //보통 개발자들은 이때 노티피케이션 생성한다 -> 바인드는 안만들어도 돼서 
        // 동작하고 있다는 것을 알기위해 개발자가 그냥 만듦
    }

    //2. clickPlay() 클릭하기 전에 서비스 만들것
    //액티비티가 화면에 보여질 때 자동으로 발동하는 콜백 메소드
    @Override
    protected void onResume() {
        super.onResume();

        if(musicService == null){
            //뮤직 서비스를 실행하고 연결하기
            Intent intent = new Intent(this,MyMusicService.class);
            startService(intent); //서비스 객체가 없으면 ceate하고 onStartCommond()를 호출함
            // 하지만 이미 서비스가 있는데 또 스타트 하라고 하면 onStartCommond()만 호출함

            //Service 객체와 바인드 연결(bind)
            bindService(intent,connection,0); //flag : 설정
            // BIND_AUTO_CREATE : 서비스 객체가 없으면 자동으로 만들겠다 라는 뜻
            // 스타트 서비스 안하고 바로 바인드 할 때 이거 쓰면 됨
            // 0 = 만들지않겠다
            //백그라운드 작업이 여러개 있을 수 있다 바인드가 여러개 있을 수 있다
            //만약 메인액티비티가 없어지면
            //스타트 안하고 BIND_AUTO_CREATE로 바인드로만 만들면 메인액티비티가 사라질 때 바인드도 같이 사라짐
            //스타트 서비스로 시작하면 독립단위로 움직이기 떄문에 바인드는 영향을 받지 않는다

        }
    }

    //마이 뮤직서비스와 연결하는 터널 객체 : ServiceConnection
    ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) { //개통됐을 때
            //여기가 발동해야 연결된것 - 그것을 확인하기 위한 토스트
            Toast.makeText(MainActivity.this, "바인드 연결", Toast.LENGTH_SHORT).show();

            // 매개변수 IBinder : 뮤직서비스 onBind()메소드에서 터널을 통해 넘어올 객체
            // onServiceConnected 매개변수  iBinder를 내 바인드로 형변환
            MyMusicService.MyBinder binder = (MyMusicService.MyBinder) iBinder; //내가 만든 바인드로 형변환
            //이너 클래스라서 아웃터 네임 자동 붙음
            musicService = ((MyMusicService.MyBinder) iBinder).getServiceObject();
            // .getServiceObject() : 서비스클래스 내에 만들어 둔 주소값 가져오는 메소드
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {//막혔을 떄

        }
    };
}



2. 서비스에서 할 일

  • 바인더에게 서비스의 주소값을 리턴하는 Binder 클래스를 만들어서 onBind에 리턴
  • 백그라운드에서 할 동작 만들기

Service.java

package com.bsj0420.ex63servicebind;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.IBinder;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyMusicService extends Service {
    
    //서비스 객체가 생성되면 자동으로 발동하는 콜백 메소드
    @Override
    public void onCreate() {
        super.onCreate();
        //라이브사이클 확인을 위한 토스트
        Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show();
    }

    //startService()로 실행하면 자동으로 발동하는 콜백메소드
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //라이브사이클 확인을 위한 토스트
        Toast.makeText(this, "onStartCommand", Toast.LENGTH_SHORT).show();


        return START_STICKY;
    }

    //bindService()를 실행하면 자동으로 발동하는 콜백메소드
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder(); //객체 생성하여 터널로 넘어가기
        //서비스 객체의 주소를 리턴해주는 기능을 가진 객체
    }

    //터널을 통해서 메인액티비티로 넘어갈 IBinder 객체 클래스
    class MyBinder extends Binder {
        //마이뮤직 서비스클래스 객체의 주소값을 리턴 해주는 기능 메소드 설계
        public MyMusicService getServiceObject(){
            return MyMusicService.this; //.this : 본인 안에서 본인 주소값 부르는 키워드
        }
    }

    //음악 재생 객체 및 기능 메소드
    MediaPlayer mp; //액티비티에서 참조할 참조변수

    public void playMusic(){
        if(mp == null){
            mp = MediaPlayer.create(this,R.raw.dragon_flight);
            mp.setVolume(0.7f,0.7f); //소프트웨어의 볼륨 0.0 ~ 1.0
            mp.setLooping(true);
        }

        if(!mp.isPlaying()) mp.start(); //플레이 중이 아니면 스타트
    }

    public void pauseMusic(){
        if(mp != null && mp.isPlaying()){
            mp.pause();
        }
    }

    public void stopMusic(){
        if(mp != null){
            mp.stop();
            mp.release();
            mp = null;
        }
    }
}
profile
보조기억장치

0개의 댓글