BLE蓝牙完成手机之间的通信,记一次技术预研_2

背景

接上文Android手机之间使用经典蓝牙进行通信,记一次技术预研_1 - 掘金 (juejin.cn)

iOS开发总算是搞明白经典蓝牙和低功耗蓝牙BLE的关系了,也给我带来了一个坏消息,iOS设备因为历史原因(要收硬件的苹果税)不支持经典蓝牙,只能使用BLE进行通信,直接把上篇文章经典蓝牙的路线给堵上了。

迫不得已再来看看BLE,却发现BLE其实也是可以用来给手机通信的,上次其实是被他的描述给坑了。经过一番折腾,我这边也算是又整了个demo完成了通信。GATT协议不需要预先配对,也算是意外之喜吧,至于半双工的特性对我的需求完全没有什么影响。

概述

名词解释

  • 服务端:BluetoothGattServer,创建好蓝牙服务,等待客户端前来连接,并在收到消息后给出响应的一端,类似HTTP协议的服务端。
  • 客户端:BluetoothGatt,主动连接蓝牙服务,发送消息并接收响应的一端,类似HTTP协议的客户端。

流程

截图

公共部分

一个工具类,提供了BLE硬件的检查、蓝牙开关的检查、定位开关和定位权限的检查。后续双端实现代码中出现的manageradapter均为这个工具类get出来的。双方约定客户端以UUID_SERVER_CHAR_WRITE对应的特征值(简称写特征)发送消息,服务端使用UID UUID_SERVER_CHAR_READ对应的特征值(简称读特征)响应消息供客户端读取。因此需要给读特征添加一个描述,使用的UUID是UUID_SERVER_CHAR_READ_DESC

公共部分:硬件检查、蓝牙开关检查

客户端特需:定位开关和权限

Java 复制代码
public class BleHelper {
    public static final String TAG = "BleHelper";
    
    public static final UUID UUID_SERVICE = UUID.fromString("");
    public static final UUID UUID_SERVER_CHAR_READ = UUID.fromString("");
    public static final UUID UUID_SERVER_CHAR_READ_DESC = UUID.fromString("");
    public static final UUID UUID_SERVER_CHAR_WRITE = UUID.fromString("");

    public static boolean checkHasBleHardware(Context context) {
        if (context == null) {
            return false;
        }
        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
    }

    public static boolean checkAll(Context context) {
        if (context == null) {
            return false;
        }

        if (!enable(context)) {
            Log.e(TAG, "BleHelp初始化失败:" + "(用户操作)未打开手机蓝牙,蓝牙功能无法使用......");
            Toast.makeText(context, "未打开手机蓝牙,蓝牙功能无法使用...", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }

    /**
     * 蓝牙客户端需要GPS开启、定位权限,否则搜索不到设备
     * @param context
     * @return
     */
    public static boolean checkBleClient(Context context) {
        if (context == null) {
            return false;
        }

        if (!isOPenGps(context)) {
            Log.e(TAG, "BleHelp初始化失败:" + "(用户操作)GPS未打开,蓝牙功能无法使用...");
            Toast.makeText(context, "GPS未打开,蓝牙功能无法使用", Toast.LENGTH_LONG).show();
            return false;
        }
        return checkAll(context);
    }

    /**
     * 打开手机蓝牙
     *
     * @return true 表示打开成功
     */
    public static boolean enable(Context context) {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
        if (!getBluetoothAdapter().isEnabled()) {
            //若未打开手机蓝牙,则会弹出一个系统的是否打开/关闭蓝牙的对话框,禁止或者未处理返回false,允许返回true
            //若已打开手机蓝牙,直接返回true
            if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
            boolean enableState = getBluetoothAdapter().enable();
            Log.d(TAG, "(用户操作)手机蓝牙是否打开成功:" + enableState);
            return enableState;
        } else {
            return true;
        }
    }

    /**
     * 判断GPS是否开启,GPS或者AGPS开启一个就认为是开启的
     *
     * @return true 表示开启
     */
    public static boolean isOPenGps(Context context) {
        LocationManager locationManager
                = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        // 通过GPS卫星定位,定位级别可以精确到街(通过24颗卫星定位,在室外和空旷的地方定位准确、速度快)
        boolean gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
        // 通过WLAN或移动网络(3G/2G)确定的位置(也称作AGPS,辅助GPS定位。主要用于在室内或遮盖物(建筑群或茂密的深林等)密集的地方定位)
        boolean network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
        if (gps || network) {
            Log.d(TAG, "GPS状态:打开");
            return true;
        }
        Log.e(TAG, "GPS状态:关闭");
        return false;
    }

    public static BluetoothManager getBluetoothManager(Context context) {
        return (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
    }

    public static BluetoothAdapter getBluetoothAdapter() {
        return BluetoothAdapter.getDefaultAdapter();
    }
}

服务端实现

1,对外开启广播

Java 复制代码
    //广播设置(必须)
    private final AdvertiseSettings settings = new AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //广播模式: 低功耗,平衡,低延迟
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //发射功率级别: 极低,低,中,高
            .setTimeout(0)
            .setConnectable(true) //能否连接,广播分为可连接广播和不可连接广播
            .build();

    //广播数据(必须,广播启动就会发送)
    private final AdvertiseData advertiseData = new AdvertiseData.Builder()
            .setIncludeDeviceName(true) //包含蓝牙名称
            .setIncludeTxPowerLevel(true) //包含发射功率级别
            .addManufacturerData(1, new byte[]{23, 33}) //设备厂商数据,自定义
            .build();

    //扫描响应数据(可选,当客户端扫描时才发送)
    private final AdvertiseData scanResponse = new AdvertiseData.Builder()
            .addManufacturerData(2, new byte[]{66, 66}) //设备厂商数据,自定义
            .addServiceUuid(new ParcelUuid(BleHelper.UUID_SERVICE)) //服务UUID
//                .addServiceData(new ParcelUuid(UUID_SERVICE), new byte[]{2}) //服务数据,自定义
            .build();

    // BLE广播Callback
    private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.d(BleHelper.TAG, "BLE广播开启成功");
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.e(BleHelper.TAG, "BLE广播开启失败,错误码:" + errorCode);
        }
    };

    /**
     * 1, 启动广播
     */
    private void startBroadcast() {
        // 注意:必须要开启可连接的BLE广播,其它设备才能发现并连接BLE服务端!
        BluetoothLeAdvertiser bluetoothLeAdvertiser = adapter.getBluetoothLeAdvertiser();
        bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseCallback);
    }

2,初始化服务端

Java 复制代码
    private void initBleService() {
        // =============启动BLE蓝牙服务端======================================
        BluetoothGattService service = new BluetoothGattService(BleHelper.UUID_SERVICE,
                BluetoothGattService.SERVICE_TYPE_PRIMARY);
        //添加可读+通知characteristic
        characteristicRead = new BluetoothGattCharacteristic(BleHelper.UUID_SERVER_CHAR_READ,
                BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                BluetoothGattCharacteristic.PERMISSION_READ);
        characteristicRead.addDescriptor(new BluetoothGattDescriptor(BleHelper.UUID_SERVER_CHAR_READ_DESC,
                BluetoothGattCharacteristic.PERMISSION_WRITE));
        service.addCharacteristic(characteristicRead);

        //添加可写characteristic
        BluetoothGattCharacteristic characteristicWrite = new BluetoothGattCharacteristic(BleHelper.UUID_SERVER_CHAR_WRITE,
                BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        service.addCharacteristic(characteristicWrite);

        if (manager != null) {
            bluetoothGattServer = manager.openGattServer(this, mBluetoothGattServerCallback);
        }
        bluetoothGattServer.addService(service);
    }
    
    /**
     * 服务事件的回调
     */
    private final BluetoothGattServerCallback mBluetoothGattServerCallback= new BluetoothGattServerCallback() {

        /**
         * 1.连接状态发生变化时
         */
        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            Log.d(BleHelper.TAG, String.format("1.onConnectionStateChange:device name = %s, address = %s", device.getName(), device.getAddress()));
            Log.d(BleHelper.TAG, String.format("1.onConnectionStateChange:status = %s, newState =%s ", status, newState));
            if (newState == BluetoothAdapter.STATE_CONNECTED) {
                Log.d(BleHelper.TAG, "1.onConnectionStateChange:设备连接成功");
                changeStatus("连接成功");
            } else if (newState == BluetoothAdapter.STATE_DISCONNECTED) {
                Log.d(BleHelper.TAG, "1.onConnectionStateChange:设备断开连接");
                changeStatus("连接断开");
            }
        }

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            Log.d(BleHelper.TAG, String.format("onServiceAdded:status = %s", status));
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            Log.d(BleHelper.TAG, String.format("onCharacteristicReadRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
            Log.d(BleHelper.TAG, String.format("onCharacteristicReadRequest:requestId = %s, offset = %s", requestId, offset));

            bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
        }

        /**
         * 3. onCharacteristicWriteRequest,接收具体的字节
         */
        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] requestBytes) {
            Log.d(BleHelper.TAG, String.format("3.onCharacteristicWriteRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
            Log.d(BleHelper.TAG, String.format("3.onCharacteristicWriteRequest:requestId = %s, preparedWrite=%s, responseNeeded=%s, offset=%s, value=%s", requestId, preparedWrite, responseNeeded, offset, requestBytes.toString()));
            bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, requestBytes);

            //4.处理响应内容
            onResponseToClient(requestBytes, device, requestId, characteristic);
        }

        /**
         * 2.描述被写入时,在这里执行 bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS...  收,触发 onCharacteristicWriteRequest
         */
        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.d(BleHelper.TAG, String.format("2.onDescriptorWriteRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
            Log.d(BleHelper.TAG, String.format("2.onDescriptorWriteRequest:requestId = %s, preparedWrite = %s, responseNeeded = %s, offset = %s, value = %s,", requestId, preparedWrite, responseNeeded, offset, value.toString()));

            // now tell the connected device that this was all successfull
            bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
        }

        /**
         * 5.特征被读取。当回复响应成功后,客户端会读取然后触发本方法
         */
        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            Log.d(BleHelper.TAG, String.format("5.onDescriptorReadRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
            Log.d(BleHelper.TAG, String.format("5.onDescriptorReadRequest:requestId = %s", requestId));

            bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
        }

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            super.onNotificationSent(device, status);
            Log.d(BleHelper.TAG, String.format("5.onNotificationSent:device name = %s, address = %s", device.getName(), device.getAddress()));
            Log.d(BleHelper.TAG, String.format("5.onNotificationSent:status = %s", status));
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            super.onMtuChanged(device, mtu);
            Log.d(BleHelper.TAG, String.format("onMtuChanged:mtu = %s", mtu));
        }

        @Override
        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
            super.onExecuteWrite(device, requestId, execute);
            Log.d(BleHelper.TAG, String.format("onExecuteWrite:requestId = %s", requestId));
        }
    };

    /**
     * 4.处理响应内容
     *
     * @param reqeustBytes
     * @param device
     * @param requestId
     * @param characteristic
     */
    private void onResponseToClient(byte[] reqeustBytes, BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic) {
        Log.e(BleHelper.TAG, String.format("4.onResponseToClient:device name = %s, address = %s", device.getName(), device.getAddress()));
        Log.e(BleHelper.TAG, String.format("4.onResponseToClient:requestId = %s", requestId));
        Log.e(BleHelper.TAG, "4.收到:");

        String str = etContent.getText().toString();
        characteristicRead.setValue(str.getBytes());
        bluetoothGattServer.notifyCharacteristicChanged(device, characteristicRead, false);

        Log.i(BleHelper.TAG, "4.响应:" + str);
        Message msg = handler.obtainMessage(MSG_WHAT_RECEIVER);
        msg.obj = new String(reqeustBytes);
        handler.sendMessage(msg);
    }

注释都写的比较清楚了:

  1. 当客户端连接或者断开连接服务时,回调onConnectionStateChange
  2. 收到客户端发来的消息是,回调onCharacteristicWriteRequest,并调用onResponseToClient方法响应客户端
  3. 客户端读取响应内容,回调onDescriptorReadRequest

3,结束

记得该关的关一下,如果需要把广播也关闭的话,就再添加一个bluetoothLeAdvertiser.stopAdvertising,传入和开启广播相同的AdvertiseCallback即可。

Java 复制代码
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (bluetoothGattServer != null) {
            Log.d(BleHelper.TAG, "关闭蓝牙服务端");
            bluetoothGattServer.close();
        }
    }

客户端实现

1,扫描蓝牙设备

这里的first和上一篇文章一样,是个demo用的临时变量,避免重复扫设备重复连接用的。

Java 复制代码
    private void startScanDevice() {
        changeStatus("正在搜索设备");
        Log.i(BleHelper.TAG, "正在搜索设备");
        adapter.getBluetoothLeScanner().startScan(mScanCallback);
    }
    
    // 扫描结果Callback
    private final ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();
            // result.getScanRecord() 获取BLE广播数据
            if (device == null || TextUtils.isEmpty(device.getName())) {
                return;
            }
            Log.i(BleHelper.TAG, "发现设备" + device.getName() + " " + device.getAddress());
            if (TextUtils.equals(deviceName, device.getName()) && first) {
                first = false;
                changeStatus("正在连接设备");
                adapter.getBluetoothLeScanner().stopScan(mScanCallback);
                mBluetoothGatt = device.connectGatt(BleSubActivity.this, false, gattCallback);
            }
        }
    };

2,找到服务端后连接

其实上一步的最后一句device.connectGatt就已经在连接了,下面代码是连接后的回调。

Java 复制代码
    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {

        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,int newState) {
            //连接状态改变的Callback
            if (newState == BluetoothGatt.STATE_CONNECTED) {
                Log.d(BleHelper.TAG, "设备连接上 开始扫描服务");
                // 连接成功后,开始扫描服务
                changeStatus("连接成功,开始发现服务");
                mBluetoothGatt.discoverServices();
            }
            if (newState == BluetoothGatt.STATE_DISCONNECTED) {
                // 连接断开
                /*连接断开后的相应处理*/
                changeStatus("连接断开");
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            //服务发现成功的Callback
            //获取服务列表
            servicesList = mBluetoothGatt.getServices();
            if (servicesList == null || servicesList.size() == 0) {
                Log.e(BleHelper.TAG, "未发现服务");
                changeStatus("未发现服务");
                return;
            }
            for (BluetoothGattService service : servicesList) {
                Log.i(BleHelper.TAG, "找到服务 " + service.getUuid().toString());
            }
            changeStatus("连接中......");
            BluetoothGattService service = mBluetoothGatt.getService(BleHelper.UUID_SERVICE);
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                Log.e(BleHelper.TAG, "找到特征 " + characteristic.getUuid().toString());
            }
            readCharacteristic = service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_READ);
            writeCharacteristic =  service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_WRITE);
            mBluetoothGatt.setCharacteristicNotification(readCharacteristic, true);
            BluetoothGattDescriptor descriptor = readCharacteristic.getDescriptor(BleHelper.UUID_SERVER_CHAR_READ_DESC);
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(descriptor);
            //若开启监听成功则会回调BluetoothGattCallback中的onDescriptorWrite()方法
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            //写入Characteristic
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.d(BleHelper.TAG, "发送成功");
                changeStatus("发送成功");
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            //读取Characteristic
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
            // 通知Characteristic
            byte[] value = characteristic.getValue();
            if (value != null) {
                String str = new String(value);
                Log.d(BleHelper.TAG, "收到消息:" + str);
                setTvReceiver(str);
            }
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            //写入Descriptor
            if (status == BluetoothGatt.GATT_SUCCESS) {
                //开启监听成功,可以向设备写入命令了
                Log.d(BleHelper.TAG, "开启监听成功");
                changeStatus("准备就绪");
            }
        }

        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            //读取Descriptor
        }

    };

3,取出特征值和描述

因为是一个回调,也不方便拆开来发,代码已经在上面了,这里简单解释一下。

Java 复制代码
            BluetoothGattService service = mBluetoothGatt.getService(BleHelper.UUID_SERVICE);
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                Log.e(BleHelper.TAG, "找到特征 " + characteristic.getUuid().toString());
            }

这里是GATT服务本身的UUID,意义不大。

Java 复制代码
            readCharacteristic = service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_READ);
            writeCharacteristic =  service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_WRITE);
            mBluetoothGatt.setCharacteristicNotification(readCharacteristic, true);
            BluetoothGattDescriptor descriptor = readCharacteristic.getDescriptor(BleHelper.UUID_SERVER_CHAR_READ_DESC);

这是取出读写特征值、读特征的描述

Java 复制代码
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(descriptor);

这里往读特征的描述设置一下有变化了通知,设置完成后回调onDescriptorWrite方法就说明设置完成,可以发消息了。

4,发送接收消息

发消息,有资料的说法是单次传输不超过20个字节,但是我实际使用下来并不是硬性限制。传输的字节太多,协议会自动修改MTU值来适应,只要传输的频率不高完全不用自己手动控制。

Java 复制代码
                // 发消息
                String content = etContent.getText().toString();
                writeCharacteristic.setValue(content);
                mBluetoothGatt.writeCharacteristic(writeCharacteristic);

接收响应是上面代码中的onCharacteristicChanged回调

Java 复制代码
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
            // 通知Characteristic
            byte[] value = characteristic.getValue();
            if (value != null) {
                String str = new String(value);
                Log.d(BleHelper.TAG, "收到消息:" + str);
                setTvReceiver(str);
            }
        }

5,结束

关闭扫描和关闭客户端。虽然在连接成功里面已经关闭过一次扫描了,但是这里最好还是再关一次,毕竟可能存在扫不到服务端设备的情况。

Java 复制代码
    @Override
    protected void onDestroy() {
        super.onDestroy();
        adapter.getBluetoothLeScanner().stopScan(mScanCallback);
        if (mBluetoothGatt != null) {
            Log.d(BleHelper.TAG, "关闭蓝牙客户端");
            mBluetoothGatt.disconnect();
            mBluetoothGatt.close();
            mBluetoothGatt = null;
        }
    }

已知问题

  1. UUID.randomUUID()生成的是128bit的UUID,iOS用在读写特征上没有问题,但是一旦用来生成描述就会闪退。目前有别的工作在处理,等过阵子继续研究一下。iOS开发找的代码是4位16进制数的UUID,咱也不知道和其他蓝牙程序的重复概率有多大,不敢用。
相关推荐
冰帝海岸1 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象1 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了2 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·2 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic2 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王2 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康3 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神3 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
qq_327342733 小时前
Java实现离线身份证号码OCR识别
java·开发语言
阿龟在奔跑5 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list