본문 바로가기
Android

Android Studio, JAVA] AIDL 서비스 바인딩

by 김마리님 2020. 8. 18.

서비스의 문제점이 있다.

ActivityA와 ServiceB는 보기에는 자바클래스와 같아보여도 서로 다른 프로세스에서 동작하기 때문에 데이터 통신을 제외하고는 두 클래스에서 정보를 주고받을 수 없다.

데이터야... 영속성을 포기하고 두 데이터를 함께 사용할 수 있지만 매서드는 그렇지 못하다. 같은 매서드를 복사해서 사용해도 A와 B의 상태가 동일하지 못한데, 이를 위해 사용해야 하는것이 마샬링 기법이다.

안드로이드에서는 이것을 비교적 쉽게 해결할 수 있는데, 바로 AIDL 서비스 바인딩이다.

https://developer.android.com/guide/components/aidl?hl=ko

 

안드로이드 인터페이스 정의 언어(AIDL)  |  Android 개발자  |  Android Developers

AIDL(Android Interface Definition Language)은 전에 다뤄본 다른 IDL과 유사합니다. 클라이언트와 서비스가 모두 동의한 프로그래밍 인터페이스를 정의하여 프로세스 간 통신(IPC)으로 서로 통신하게 할 수 �

developer.android.com

 

먼저, 바인딩 되지 않을때의 한계를 알아보기 위해, 간단한 프로그램을 통해 바인딩 되지 않은 서비스의 결과물을 보자.

- 뷰

 

activitiy_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"
    >

    <TextView
        android:id="@+id/tv_count"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="1"
        android:textSize="100sp"
        android:gravity="center"
        />

    <Button
        android:id="@+id/btn_start"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start"
        android:textSize="50sp"/>

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="stop"
        android:textSize="50sp"/>


</LinearLayout>

 

- 자바 코드

MainActivity.java

package com.mary.counterapp;

import androidx.appcompat.app.AppCompatActivity;

import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Main_Activity";

    private TextView tvCount;
    private Button btnStart, btnStop;
    private ICounterService bind;

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

    private void init(){
        tvCount=findViewById(R.id.tv_count);
        btnStart=findViewById(R.id.btn_start);
        btnStop=findViewById(R.id.btn_stop);
    }

    private void Listener(){
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent=new Intent(MainActivity.this,CounterService.class);
 				startService(intent);
            }
        });
        btnStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent=new Intent(MainActivity.this,CounterService.class);
                stopService(intent);
            }
        });
    }
}

 

CounterService.java

package com.mary.counterapp;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class CounterService extends Service {
    private static final String TAG = "CounterService";

    private int count;
    private boolean isStop = false;

    @Override
    public void onCreate() {
        super.onCreate();
    }

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


    class Counter implements Runnable {
        @Override
        public void run() {
            for (count = 0; count < 20; count++) {
                if (isStop) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "run: " + count);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        isStop = true;
    }
}

 

이 코드의 문제점은 다음과 같다.

1. 스레드가 돌아간다고 해서, 서비스에서 메인 액티비티의 뷰를 참조할 수 없기 때문에 뷰가 1에서 증가하지 않는다(앞서 말했지만 두 액티비티가 서로 다른 프로세스이기 때문에 일반적인 방법으로는 참조할 수 없다.).

2. stop를 누를 시 count가 저장되지 않기 때문에 다시 start를 누르게 될 경우 0으로 돌아간다.

 

 

이를 해결하기 위해 AIDL을 이용한다.

AIDL은 이렇게 만든다.

[우클릭] - [New] - [AIDL] - [AIDL File]

다음과 같이 들어가서, 인터페이스의 이름을 설정하고 finish를 누르면 aidl 파일이 생긴걸 확인할 수 있다.

(뿁)

여기서 끝나면 안되고, ctrl+F9 or [Build-Make Project]를 클릭해 프로젝트를 리빌드 하면, 다음과 같은 경로에 인터페이스가 생성된다.

이걸.. 이 인터페이스를 손대는게 아니라... aidl 파일 수정 - 리빌드를 통해 인터페이스를 수정해야한다.

내가 바꾸고 싶으면 aidl 파일 수정이다.. 저 인터페이스는 이미 만들어진 것이라 손대면 안된다.. ㅠㅠㅠ

 

- AIDL파일

// ICounterService.aidl
package com.mary.counterapp;

// Declare any non-default types here with import statements

interface ICounterService {
    int getCount();
}

 

int getCount를 통해 서로 동기화 할 수 없었던 Count 값을 동기화한다.

이것을 참조하기 위해 서비스에 연결할 스텁을 생성한다.

 

-서비스

CounterService.java

--중략--

	private int count;
    private boolean isStop = false;

    ICounterService.Stub binder = new ICounterService.Stub() {
        @Override
        public int getCount() throws RemoteException {
            return count;
        }
    };

    public CounterService() {
        Thread counterThread = new Thread(new Counter());

--하략--

 

그리고, 바인딩 함수에 만든 스텁을 바인딩 하고, 언바인드 함수를 만들어 스레드를 멈추도록 한다.

--중략--

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

    @Override
    public boolean onUnbind(Intent intent) {
        isStop=true;
        return super.onUnbind(intent);
    }

--하략--

 

그리고 스레드 내부에서 값이 바뀌므로, 메인스레드가 아닌 이상 UI를 건들 수 없으므로 Handler 함수를 통해 값을 건든다. 

 

MainActivity.java의 btnStartListener 부분

    private void Listener(){
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "onClick: 111");
                final Intent intent=new Intent(MainActivity.this,CounterService.class);
                bindService(intent, conn,BIND_AUTO_CREATE);
                Log.d(TAG, "onClick: 222");
               new Thread(new Runnable() {
                   @Override
                   public void run() {
                       while (running){
                           if(binder!=null){
                               handler.post(new Runnable() {
                                   @Override
                                   public void run() {
                                       try {
                                           tvCount.setText(binder.getCount()+"");
                                           if(binder.getCount()==20){
                                               running=false;
                                           }
                                       } catch (RemoteException e) {
                                           e.printStackTrace();
                                       }

                                   }
                               });
                               try {
                                   Thread.sleep(500);
                               } catch (InterruptedException e) {
                                   e.printStackTrace();
                               }
                           }
                       }
                   }
               }).start();
            }
        });

 

이러면, 뷰는 계속 변화하지만 멈추고 재실행할 경우 카운트가 0으로 시작한다.

따라서, 값을 서비스로, intent로 보내고, 그 값을 서비스에서 참조하도록 한다.

 

MainActivity.java 의 stopButtonListener

      btnStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    startCount = binder.getCount();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                unbindService(connection);
                running = false;
            }
        });
    }

 

Service 부분

package com.mary.counterapp;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class CounterService extends Service {

    private static final String TAG = "CounterService";
    private int count;
    private int startCount;
    private boolean isStop = false;

    ICounterService.Stub binder = new ICounterService.Stub() {
        @Override
        public int getCount() throws RemoteException {
            return count;
        }
    };

    public CounterService() {
        Log.d(TAG, "CounterService: 생성자 실행됨");
    }

    @Override
    public void onCreate() {
        super.onCreate();

        Log.d(TAG, "onCreate: 서비스 시작");
        Thread counterThread = new Thread(new Counter());
        counterThread.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        startCount = intent.getIntExtra("startCount", 0);
        return binder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "onUnbind: count : "+count);
        isStop = true;
        return super.onUnbind(intent);
    }

    class Counter implements  Runnable {

        @Override
        public void run() {
            for (count=startCount; count<20; count++){

                if(isStop){
                    break;
                }

                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "run: count : "+count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        isStop = true;
        Log.d(TAG, "onDestroy: 서비스 파괴");
    }
}

반응형