bluetooth_serviceクラスは別スレッドを実装したほうがいいのか?

 


下記の androidの bluetooth デバイスに接続する際の処理に関する

公式のサンプルコードについてお尋ねします。


serviceクラスのサブクラス、というのは


内部で別にスレッド上で動作させたほうがいい、


というようなことを公式の解説のどこかで見たような気がするのですが、


このserviceのクラスを呼び出す側で


bind serviceのメソッドで実行すると、


別のスレッド上で動作するのでしょうか?


それとも、やはりスレッドを実装したほうがいいのでしょうか?





import android.app.Service;

import android.bluetooth.BluetoothAdapter;

import android.bluetooth.BluetoothDevice;

import android.bluetooth.BluetoothGatt;

import android.bluetooth.BluetoothGattCallback;

import android.bluetooth.BluetoothGattCharacteristic;

import android.bluetooth.BluetoothGattDescriptor;

import android.bluetooth.BluetoothGattService;

import android.bluetooth.BluetoothManager;

import android.bluetooth.BluetoothProfile;

import android.content.Context;

import android.content.Intent;

import android.os.Binder;

import android.os.IBinder;

import android.util.Log;


import java.util.List;

import java.util.UUID;


/**

 * Service for managing connection and data communication with a GATT server hosted on a

 * given Bluetooth LE device.

 */

public class BluetoothLeService extends Service {

    private final static String TAG

= BluetoothLeService.class.getSimpleName();


    private BluetoothManager mBluetoothManager;

    private BluetoothAdapter mBluetoothAdapter;

    private String mBluetoothDeviceAddress;

    private BluetoothGatt mBluetoothGatt;

    private int mConnectionState = STATE_DISCONNECTED;


    private static final int STATE_DISCONNECTED = 0;

    private static final int STATE_CONNECTING = 1;

    private static final int STATE_CONNECTED = 2;


    public final static String ACTION_GATT_CONNECTED

= "com.example.bluetooth.le.ACTION_GATT_CONNECTED";

    public final static String ACTION_GATT_DISCONNECTED

="com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";



    public final static String ACTION_GATT_SERVICES_DISCOVERED

= "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";



    public final static String ACTION_DATA_AVAILABLE

= "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";



    public final static String EXTRA_DATA

= "com.example.bluetooth.le.EXTRA_DATA";


    public final static UUID UUID_HEART_RATE_MEASUREMENT

= UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);




    // Implements callback methods for GATT events 

that the app cares about.  

For example,

    // connection change and services discovered.

    private final BluetoothGattCallback mGattCallback

= new BluetoothGattCallback() {

        @Override

        public void onConnectionStateChange(

BluetoothGatt gatt, int status, int newState) {

            String intentAction;

            if (newState == BluetoothProfile.STATE_CONNECTED) {

                intentAction = ACTION_GATT_CONNECTED;

                mConnectionState = STATE_CONNECTED;

                broadcastUpdate(intentAction);

                Log.i(TAG, "Connected to GATT server.");

                // Attempts to discover services after successful connection.

                Log.i(TAG, "Attempting to start service discovery:" +

                        mBluetoothGatt.discoverServices());


            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {

                intentAction = ACTION_GATT_DISCONNECTED;

                mConnectionState = STATE_DISCONNECTED;

                Log.i(TAG, "Disconnected from GATT server.");

                broadcastUpdate(intentAction);

            }

        }


        @Override

        public void onServicesDiscovered(

BluetoothGatt gatt, int status) {

            if (status == BluetoothGatt.GATT_SUCCESS) {

                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);

            } else {

                Log.w(TAG, "onServicesDiscovered received: " + status);

            }

        }


        @Override

        public void onCharacteristicRead(

BluetoothGatt gatt,

            BluetoothGattCharacteristic characteristic,

            int status) {

            if (status == BluetoothGatt.GATT_SUCCESS) {

                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);

            }

        }


        @Override

        public void onCharacteristicChanged(

BluetoothGatt gatt,

            BluetoothGattCharacteristic characteristic) {

            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);

        }

    };





    private void broadcastUpdate(final String action) {

        final Intent intent = new Intent(action);

        sendBroadcast(intent);

    }





    private void broadcastUpdate(

final String action,

        final BluetoothGattCharacteristic characteristic) {

        final Intent intent = new Intent(action);


        // This is special handling for the Heart Rate Measurement profile.  

Data parsing is

        // carried out as per profile specifications:

        // http://developer.bluetooth.org/gatt/characteristics/

Pages/CharacteristicViewer.aspx?u=org.bluetooth.

characteristic.heart_rate_measurement.xml

        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {

            int flag = characteristic.getProperties();

            int format = -1;

            if ((flag & 0x01) != 0) {

                format = BluetoothGattCharacteristic.FORMAT_UINT16;

                Log.d(TAG, "Heart rate format UINT16.");

            } else {

                format = BluetoothGattCharacteristic.FORMAT_UINT8;

                Log.d(TAG, "Heart rate format UINT8.");

            }

            final int heartRate

= characteristic.getIntValue(format, 1);

            Log.d(TAG, String.format("Received heart rate: %d", heartRate));

            intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));

        } else {

            // For all other profiles, writes the data formatted in HEX.

            final byte[] data = characteristic.getValue();

            if (data != null && data.length > 0) {

                final StringBuilder stringBuilder

= new StringBuilder(data.length);

                for(byte byteChar : data)

                    stringBuilder.append(

String.format("%02X ", byteChar));

                intent.putExtra(

EXTRA_DATA, new String(data) 

+ "\n" + stringBuilder.toString());

            }

        }

        sendBroadcast(intent);

    }





    public class LocalBinder extends Binder {

        BluetoothLeService getService() {

            return BluetoothLeService.this;

        }

    }





    @Override

    public IBinder onBind(Intent intent) {

        return mBinder;

    }



    @Override

    public boolean onUnbind(Intent intent) {

        // After using a given device, 

you should make sure that BluetoothGatt.close() is called

        // such that resources are cleaned up properly.  

In this particular example, close() is

        // invoked when the UI is disconnected from the Service.

        close();

        return super.onUnbind(intent);

    }


    private final IBinder mBinder = new LocalBinder();


    /**

     * Initializes a reference to the local Bluetooth adapter.

     *

     * @return Return true if the initialization is successful.

     */

    public boolean initialize() {

        // For API level 18 and above, 

get a reference to BluetoothAdapter through

        // BluetoothManager.

        if (mBluetoothManager == null) {

            mBluetoothManager 

= (BluetoothManager) getSystemService(

Context.BLUETOOTH_SERVICE);

            if (mBluetoothManager == null) {

                Log.e(TAG, "Unable to 

initialize BluetoothManager.");

                return false;

            }

        }


        mBluetoothAdapter = mBluetoothManager.getAdapter();

        if (mBluetoothAdapter == null) {

            Log.e(TAG, "Unable to obtain a BluetoothAdapter.");

            return false;

        }


        return true;

    }


    /**

     * Connects to the GATT server hosted on the Bluetooth LE device.

     *

     * @param address The device address of the destination device.

     *

     * @return Return true if the connection is initiated successfully. 

The connection result

     *         is reported asynchronously through the

     *         {@code BluetoothGattCallback#

onConnectionStateChange(

android.bluetooth.BluetoothGatt, int, int)}

     *         callback.

     */

    public boolean connect(final String address) {

        if (mBluetoothAdapter == null || address == null) {

Log.w(TAG, "BluetoothAdapter 

not initialized or unspecified address.");

            return false;

        }


        // Previously connected device.  Try to reconnect.

        if (mBluetoothDeviceAddress != null

&& address.equals(mBluetoothDeviceAddress)

                && mBluetoothGatt != null) {

            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");

            if (mBluetoothGatt.connect()) {

                mConnectionState = STATE_CONNECTING;

                return true;

            } else {

                return false;

            }

        }


        final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);

        if (device == null) {

            Log.w(TAG, "Device not found.  Unable to connect.");

            return false;

        }

        // We want to directly connect to the device, so we are setting the autoConnect

        // parameter to false.

        mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

        Log.d(TAG, "Trying to create a new connection.");

        mBluetoothDeviceAddress = address;

        mConnectionState = STATE_CONNECTING;

        return true;

    }


    /**

     * Disconnects an existing connection 

or cancel a pending connection. 

The disconnection result

     * is reported asynchronously through the

     * {@code BluetoothGattCallback#onConnectionStateChange(

android.bluetooth.BluetoothGatt, int, int)}

     * callback.

     */

    public void disconnect() {

        if (mBluetoothAdapter == null || mBluetoothGatt == null) {

            Log.w(TAG, "BluetoothAdapter not initialized");

            return;

        }

        mBluetoothGatt.disconnect();

    }


    /**

     * After using a given BLE device, 

the app must call this method to ensure resources are

     * released properly.

     */

    public void close() {

        if (mBluetoothGatt == null) {

            return;

        }

        mBluetoothGatt.close();

        mBluetoothGatt = null;

    }


    /**

     * Request a read on a given {@code BluetoothGattCharacteristic}. 

The read result is reported

     * asynchronously through the {

@code BluetoothGattCallback#onCharacteristicRead(

android.bluetooth.BluetoothGatt, 

android.bluetooth.BluetoothGattCharacteristic, int)}

     * callback.

     *

     * @param characteristic The characteristic to read from.

     */

    public void readCharacteristic(BluetoothGattCharacteristic characteristic) {

        if (mBluetoothAdapter == null || mBluetoothGatt == null) {

            Log.w(TAG, "BluetoothAdapter not initialized");

            return;

        }

        mBluetoothGatt.readCharacteristic(characteristic);

    }


    /**

     * Enables or disables notification on a give characteristic.

     *

     * @param characteristic Characteristic to act on.

     * @param enabled If true, enable notification.  False otherwise.

     */

    public void setCharacteristicNotification(

BluetoothGattCharacteristic characteristic,

            boolean enabled) {

        if (mBluetoothAdapter == null || mBluetoothGatt == null) {

            Log.w(TAG, "BluetoothAdapter not initialized");

            return;

        }

        mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);


        // This is specific to Heart Rate Measurement.

        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {

            BluetoothGattDescriptor descriptor

= characteristic.getDescriptor(

               UUID.fromString(

SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));

            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);

            mBluetoothGatt.writeDescriptor(descriptor);

        }

    }


    /**

     * Retrieves a list of supported 

GATT services on the connected device. This should be

     * invoked only after {@code BluetoothGatt#discoverServices()}

completes successfully.

     *

     * @return A {@code List} of supported services.

     */

    public List<BluetoothGattService> getSupportedGattServices() {

        if (mBluetoothGatt == null) return null;


        return mBluetoothGatt.getServices();

    }

}







`Service` クラスを使用している場合、

デフォルトではその処理は **メインスレッド(UIスレッド)** 上で実行されます。


つまり、特にスレッドを指定せずに重たい処理(Bluetooth通信など)を実行すると、


アプリのUIがブロックされてしまう可能性があります。



---


### **公式の推奨事項**


Googleの公式ドキュメントでは、`Service` の場合でも

重い処理はバックグラウンドスレッドで実行することを推奨しています。


そのため、Bluetooth通信や

データ処理などの長時間実行される可能性のある処理については、


以下の方法でスレッドを分けるべきです:



1. **`Thread` または `HandlerThread` を利用する**



2. **`AsyncTask` や `ExecutorService` を利用する**



3. **Jetpackの`WorkManager` を利用する**

(タスクが長時間にわたる場合や、確実な再実行が必要な場合)


---


### **`bindService` メソッドとスレッド**


`bindService` を使って `Service` をバインドした場合でも、

`Service` のコードは **メインスレッド** で実行されます。


そのため、`bindService` によって別スレッドに移行することは **ありません**。


もしスレッド分離が必要であれば、以下のように実装してください。


---


### **スレッド分離のサンプル**

`HandlerThread` を使ってバックグラウンド処理を行う方法を以下に示します:


```java



public class BluetoothLeService extends Service {


    private HandlerThread handlerThread;

    private Handler backgroundHandler;


    @Override

    public void onCreate() {

        super.onCreate();

        // バックグラウンドスレッドを開始

        handlerThread

= new HandlerThread("BluetoothLeServiceThread");

        handlerThread.start();

        backgroundHandler

= new Handler(handlerThread.getLooper());

    }


    // バックグラウンドでBluetooth接続を試みる

    public void connectToDevice(String address) {

        backgroundHandler.post(() -> {

            // ここにBluetoothの接続処理を記述

            boolean result = connect(address);

            Log.d(TAG, "Connection result: " + result);

        });

    }


    @Override

    public void onDestroy() {

        super.onDestroy();

        // スレッドを終了

        if (handlerThread != null) {

            handlerThread.quitSafely();

      

  }

  

    }

}



```


---



### **補足**


Bluetooth LE通信では、API呼び出しがすべて非同期的に処理されるため、


サービス内部の `BluetoothGattCallback` などのコールバックは、

別スレッドで動作します。


ただし、これらのコールバックを利用してさらにUIの更新や追加の処理を行う場合、

適切にスレッド間の処理を切り替える必要があります。



- **UI更新時の注意点**: 


コールバックから直接UI操作を行わないようにし、

`Handler` や `runOnUiThread()` を使って

UIスレッドに切り替える必要があります。



---


### **まとめ**


- `Service` はデフォルトではメインスレッドで動作します。



- 重い処理を行う場合は明示的に別スレッド

(`HandlerThread` や `ExecutorService`)を作成する必要があります。



- `bindService` だけではスレッド分離は行われません。



- Bluetooth通信処理の部分も

バックグラウンドスレッドで実行するのがベストプラクティスです。




コメント