무슨 일로 C 하셨습니까?

[Android Studio:Java]백그라운드 서비스 본문

공부

[Android Studio:Java]백그라운드 서비스

OJJJ 2021. 10. 11. 11:05

안드로이드 백그라운드 서비스를 만들 일이 생겼다.

 

Ui는 Ui대로, 서비스는 서비스 대로 동작 하고 Ui가 죽어도 백그라운드에서 계속 서비스 하는 애플리케이션을 만들어야한다.

 

안드로이드 OS가 최신화 될수록 백그라운드 서비스를 개발하기 어려워졌다고 하는데 일단 만들어 보겠다.

 

테스트 개발은 API 28, Android 9.0 (Oreo) 버전으로 진행하겠다.

 

 


1. Thread

 

 

기본화면에서부터 시작하겠다. 사실 이번에는 Ui는 변경할 건 크게 없다.

 

일단 첫번째 목표는 Ui와 비동기적으로 비동기적으로 돌아가는 애플리케이션이다.

void f(){
        for(int i=0;i<100;i++){
            try {
                Thread.sleep(1000);
                Log.d("test","count: "+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

0부터 99까지 1초에 한번씩 카운트 하는 함수이다.

우선은 이 함수를 비동기적으로 성공적으로 실행시키면 되겠다.

 

 

 

위 함수를 비동기적으로 실행시키는건 매우 간단하다.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                f();
            }
        }).start();
    }

무지성 Thread를 사용해주면 되겠다.

쓰레드 생명주기 관리는 대충 boolean 변수 하나 사용해서 관리하도록 하자

 

비동기적 실행이 되는것을 확인하기 위해

버튼을 하나 만들었다.

 

    public void testbtn(View view){
        Toast.makeText(this, "test good", Toast.LENGTH_SHORT).show();
    }

버튼 이벤트 하나 만들었다. Toast가 출력된다는 것은 클릭 이벤트가 처리되었다는 것이고

그럼 숫자 카운트 함수가 UI를 그리는 메인 쓰레드가 아닌 다른 쓰레드를 사용하고 있다는 것이기 때문에

 

비동기 실행이 정상적으로 이루어 지고 있음을 알 수 있다.

 

 

그렇다면 이렇게 해서 내가 원하는 백그라운드 서비스를 만들었다고 볼 수 있을까

 

애플리케이션을 종료시켜보자

그냥 뒤로가기를 누르는 것은 애플리케이션이 종료되는 것이 아닌

대기 상태에 들어가는 것이기 때문에

앱 리스트에서 지워줌으로써 강제로 종료시킬 수 있다.

 

이러면 기가막히게 쓰레드가 종료되는 것을 볼 수 있다. 

카운트가 되다가 멈춘다.

 

이는 카운팅 쓰레드를 생성한 메인 쓰레드가 종료되었기 때문인데

내가 원하는 건 Ui가 죽어도 계속해서 카운팅을 해야하기 때문에 단순 쓰레드 작업으로는 안될 것 같다.

 

2. Service

 

안드로이드의 4대 컴포넌트로는 Activity, Service, Broadcast reci......

 

각설하고 야매로 보자면 Activity랑 Service랑 동등한 놈이라는 건데

여기서 Activity는 우리가 사용하는 Ui라고 볼 수 있다.

 

그래서 서비스를 왜 쓰냐. 액티비티는 메인 쓰레드를 Ui를 그리는데 사용하기 떄문에 Ui가 종료되면 같이 종료되지만 서비스는 메인 쓰레드가 Ui와 무관하기 떄문에 Ui가 종료되어도 살아 있을 수 있다.

 

일단 하나 만들어보자 서비스를 대충 하나 만들고

[Ctrl+O]단축키로 함수를 오버라이딩 하자. 일단은 OnStartCommand() 하나만 있으면 된다. 

OnStartCommand()는 해당 서비스가 실행될 함수. 즉 서비스의 메인함수라고 보면 되겠다.

 

 

참고로 <AndroidManifest.xml>에 다음과 같이 서비스가 등록되어 있어야한다.

위와 같이 생성하면 자동으로 생성된다.

 

2.1 Service 

백문이 불여일견 일단 서비스로 테스트해보자

약간의 코드 수정이 필요하겠다.

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

        Intent it = new Intent(this,MyService.class);
        startService(it);
    }
//MyService.java
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(intent == null){
            return START_STICKY;
        }
        
        f();
        
        return super.onStartCommand(intent, flags, startId);
    }

 

실행시켜보자

 

 

화면은 그려지지도 않지만 카운팅은 정상적으로 되는 것을 볼 수 있다.

심지어 애플리케이션을 종료시켜도 카운팅이 바로 종료되지는 않는다.

 

액티비티와 서비스가 같은 쓰레드를 사용하기 때문에 비동기적으로 실행은 안되는 것 같다.

그러나 서비스 쓰레드는 Ui의 생명주기와 무관하게 동작하는 것은 알겠다.

 

2.2 Service + Thread

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(intent == null){
            return START_STICKY;
        }

        new Thread(new Runnable() {
            @Override
            public void run() {

                f();
            }
        }).start();

        return super.onStartCommand(intent, flags, startId);
    }

서비스 코드를 조금 수정해보자. 쓰레드를 생성해서 카운팅을 해보면

Ui를 종료하는 순간 카운팅도 종료되는 것을 알 수 있다.

 

이를 통해 대충 유추해 볼 수 있는게 Thread는 Main Thread가 종료되는 경우 같이 종료됨을 알 수 있는데

Main Thread의 생명주기는 Activity에 있을때는 Ui와 연관있지만 Service에 있을땐 Ui와는 무관한 것을 알 수있다.

 

그럼 Activity와 Service를 비동기적으로 실행시키면 되지 않을까 Service를 다른 Thread로 실행하면되지 않을까

 

그러나 불가능하다. Activity와 Service는 둘다 4대 컴포넌트라 무조건 Main Thread에서만 작동되어야 한다. 

라고 한다.

 

 

2.3 Service + Multi Process

 

그렇다면 멀티 프로세스를 사용하면 되지 않을까

 

하고 열심히 구글을 뒤져봐도 안드로이드에선 멀티 프로세스란걸 잘 안쓰는 것 같다.

겨우겨우 해서 찾아낸 방법은 바로 프로세스 이름을 바꿔주는 것이다.

 

//AndroidManifest.xml
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"
            android:process=":myservice"></service>

Manifest파일에 service태그에 process 옵션을 추가해주자. ( 프로세스 이름은 쌍점 ( : ) 으로 시작해야한다. )

그냥 단순히 실행될 서비스의 프로세스 명만 바꿔주는 옵션이다.

 

이것만 추가하고 2.1 버전의 코드. Thread 없는 버전으로 실행해보자.

기가막히게 실행된다.

 

Ui와 카운팅이 비동기적으로 동작하며 Ui가 종료되어도 카운팅은 계속해서 유지된다.

성공적으로 백그라운드 서비스를 만들어 냈다. 그래보인다.

 

그러나

이내 카운팅을 끝까지 하기도 전에 종료가 되버린다.

WaitinInMainSigalCatc어쩌고는 결국 OS에 의해 해당 쓰레드가 종료되었다는걸 의미한다는 것 같다.

 

이 에러가 왜 발생하는가 하면 안드로이드 OS 정책에 의해 백그라운드 서비스가 무분별하게 실행되면 핸드폰에 아무래도 없는 자원을 많이 소모하게 되서 어쩌고 저쩌고 해서 막은 것이라 한다.

 

결국 여기서 내가 더 이상 무언갈 할 순 없다. 

새로운 접근을 시도해 보아야 한다.

 

3. Foreground Service

정책 상 화면 그 어느 구석에도 실행 중이라 표시되지 않는 백그라운드 서비스는 OS에 의해 중지되지만

화면에 표시되는 Foreground Service의 경우 중지되지 않고 계속 서비스할 수 있다고 한다.

 

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

    <application
    ...
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"
            android:process=":myservice"></service>

Manifest파일에 FOREGROUND_SERVICE 권한 요청을 추가해주자.

 

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

        Intent it = new Intent(this,MyService.class);
        //startService(it);
        startForegroundService(it);
    }

ForegroundService로 실행하는건 어렵지 않다. 다음과 같이 서비스 실행 코드를 조금 변경해주면 된다.

 

이렇게 하면 될것 같다.

어림도 없이 안된다. 왜냐

 

안드로이드 "킹책상" Oreo부터 foreground service는 사용자가 볼 수 있도록 Notification을 설정해주어야만 한다.

라고 한다.

 

//MyService.java
@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(intent == null){
            return START_STICKY;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel serviceChannel =
                    new NotificationChannel(CHANNEL_ID, "알림 설정 모드 타이틀", NotificationManager.IMPORTANCE_DEFAULT);
            NotificationManager manager = getSystemService(NotificationManager.class);
            assert manager != null;
            manager.createNotificationChannel(serviceChannel);
        }

        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("알림 타이틀")
                .setContentText("알림 설명")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentIntent(pendingIntent)
                .build();


        startForeground(1, notification);


        new Thread(new Runnable() {
            @Override
            public void run() {

                f();
            }
        }).start();

        return super.onStartCommand(intent, flags, startId);
    }

Notification을 추가해주면 되는데 설정할게 좀 많다. 그냥 복붙해서 조금씩 수정하도록 하자

카운팅 함수가 Service의 메인 쓰레드에서 실행되면 애플리케이션이 응답 없다는 에러가 발생할 수 있으니 쓰레드로 실행시키도록 하자

 

 

애플리케이션을 실행해보면 좌측 상단에 알림이 뜨는 것을 볼 수 있다.

알림이 뜬다는 것은 곧 서비스가 실행중이란 뜻이다.

 

이제는 서비스와 UI가 비동기적으로 동작하고, UI가 종료되도 서비스가 종료되지 않으며

OS가 서비스를 강제종료시키지도 않는다.

 

이제 비로소 내가 원하던 백그라운드(?) 서비스를 만들어냈다.

 

다만 아직 서비스에서 카운팅을 끝마쳐도 알림이 사라지지 않는다.'

 

이는 간단하게 직접 서비를 종료시켜줌으로써 해결할 수 있다.

    void f(){
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(1000);
                Log.d("test","count: "+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            stopSelf();
        }
    }

서비스가 실행할 메인 작업이 종료되면 stopSelf()함수를 통해 서비스가 스스로 종료할수 있게 해주면 되겠다.

stopSelf()는 Service함수 내에서만 사용할 수 있으므로 서비스할 작업을 쓰레드로 실행시키려거든 서비스 내 함수를 통해서 제어하도록 하자

'공부' 카테고리의 다른 글

[C#]WPF::InfluxDB  (0) 2021.02.25
Visual Studio 프로젝트 GitHub에 올리기  (0) 2021.02.15
MongoDB::NoSQL?  (0) 2021.01.22
Comments