Android 低功耗蓝牙(Ble) 开发总结

Android 从 4.3(API Level 18) 开始支持低功耗蓝牙,但是只支持作为中心设备(Central)模式,这就意味着 Android 设备只能主动扫描和链接其他外围设备(Peripheral)。从 Android 5.0(API Level 21) 开始两种模式都支持。

低功耗蓝牙开发算是较偏技术,实际开发中坑是比较多的,网上有很多文章介绍使用和经验总结,但是有些问题答案不好找,甚至有些误导人,比如 :获取已经连接的蓝牙,有的是通过反射,一大堆判断,然而并不是对所有手机有用,关于Ble传输速率问题的解决,都是默认Android每次只能发送20个字节,然而也并不是,,,下面进入正文。

Ble的使用 官网

1. 权限

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--Android 6.0及后续版本扫描蓝牙,需要定位权限(进入GPS设置,可以看到蓝牙定位)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />//9.0以上
<uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="false" />
<!--不支持低功耗的设备也可以安装,否则可以设置 required ="true"-->

2. 初始 使用 (在扫描前要记得先打开蓝牙成功,否则会很长时间不响应)

    fun initBle() {
        // 检查蓝牙开关
        val adapter = BluetoothAdapter.getDefaultAdapter()
        if (adapter == null) {
            ToastUtils.showShortSafe("本机没有找到蓝牙硬件或驱动!")
            return
        } else {
            /*if (!adapter.isEnabled()) {
                //直接开启蓝牙
                adapter.enable();
                //跳转到设置界面
                //startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 112);
            }*/
            if (!adapter.isEnabled) {
                val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
                mContext.startActivityForResult(enableIntent, REQUEST_ENABLE_BT)
                // 在onActivityResult里提示是否开成功根据Activity.RESULT_OK

            }
        }

        // 检查是否支持BLE蓝牙
        if (!mContext.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            ToastUtils.showShortSafe("本机不支持低功耗蓝牙!")
            return
        }

        // Android 6.0动态请求位置权限 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_COARSE_LOCATION)
            for (str in permissions) {
                if (mContext.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
                    mContext.requestPermissions(permissions, REQUEST_ENABLE_LC)
                    break
                }
            }
        }
    }

3. 扫描

这里用的是 Android5.0 新增的扫描API,

  • 尽量不要在扫描结果方法做耗时操作,它会快速连续调用(二期同一个设备连续返回)
  • 添加用服务uuid做过滤,会加快扫描速度节省时间,比自己通过代码刷选好很多
  • 扫描是耗电较大的操作,注意定时停止,官方 Demo 写的10m
 // 扫描BLE蓝牙(不会扫描经典蓝牙)
    private void scanBle() {
        getConnedDevs();
        isScanning = true;
//        BluetoothAdapter bluetoothAdapter = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE).getDefaultAdapter();
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
        // Android5.0新增的扫描API,扫描返回的结果更友好,比如BLE广播数据以前是byte[] scanRecord,而新API帮我们解析成ScanRecord类
        bluetoothLeScanner.startScan(buildScanFilters(), buildScanSettings(), mScanCallback);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                bluetoothLeScanner.stopScan(mScanCallback); //停止扫描
                isScanning = false;
            }
        }, 3000);//(耗电)3秒后停止,根据需要设置
    }

    /**
     * 过滤
     *
     * @return
     */
    private List<ScanFilter> buildScanFilters() {
        List<ScanFilter> scanFilters = new ArrayList<>();

        ScanFilter.Builder builder = new ScanFilter.Builder();
        // 通过服务 uuid 过滤自己要连接的设备
        builder.setServiceUuid(new ParcelUuid(BleUtils.Companion.getUUID_SERVICE()));
        scanFilters.add(builder.build());

        return scanFilters;
    }

    private ScanSettings buildScanSettings() {
        ScanSettings.Builder builder = new ScanSettings.Builder();
        builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);//低耗电模式
//SCAN_MODE_LOW_LATENCY 低延迟 我用的(耗电,无所谓,又不是一直扫描的,3秒就停止了)
        return builder.build();
    }


 private final ScanCallback mScanCallback = new ScanCallback() {// 扫描Callback
        @Override
        public void onScanResult(int callbackType, ScanResult result) {//尽量不做耗时操作,此方法会快速重复多次调用,返回相同的设备
            String address = result.getDevice().getAddress();
            if (!mDevices.contains(dev)) {
                mDevices.add(dev);
                notifyDataSetChanged();
                Log.i(TAG, "onScanResult: " + result); // result.getScanRecord() 获取BLE广播数据
            }
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            super.onBatchScanResults(results);
            Log.i(TAG, "onScanResult: " + "    " + results);
        }
    };

 // 重新扫描
    public void reScan() {
        mDevices.clear();
        notifyDataSetChanged();
        scanBle();
    }

这里说一下,如果做蓝牙设备管理页面,可能区分是否是已连接的设备,网上又通过反射或其他挺麻烦的操作,也不见得获取到,官方Api 就有提供

 BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
 List<BluetoothDevice> connectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);

4. 连接

  1. 在设备列表 点击 dev.connectGatt(mContext, false, mBluetoothGattCallback) -> 这里的false 是不自动连,如果是true 使用时连接很慢。
  2. onConnectionStateChange()(1 成功 继续启动发现服务 2 . 经常断链有时候连接一次不成功,就重复 dev.connectGatt()->
  3. onServicesDiscovered() 服务发现成功好会把设备所有 Service 和 Service 里的 characteristics 及 characteristic.descriptors 全返回来(可以用来展示,调试和判断自己要用的服务 特征 到底有没有成功发现)然后setNotify() 需要接收数据时得打开通知 ->
  4. onDescriptorWrite() 写入通知成功后mBluetoothGatt!!.requestMtu() 请求该设备的Mtu ,默认23个字节 我们用的设备我申请256个字节最后只能拿到244个字节->
  5. onMtuChanged 拿到mtu-3 为什么减3 后面做数据分包->
  6. 最后通知连接成功 mBleConnCallBack!!.onConntionedCallBack(status, isConnected)
    这个callback是我自己写的回调 才能做后面的发送,接收数据


    image.png

 /**
   * 连接设备###
   */
    fun connDev(dev: BluetoothDevice, bleConnCallBack: BleConnCallBack) {
        closeConn()
        mBleConnCallBack = bleConnCallBack
        mBluetoothGatt = dev.connectGatt(mContext, false, mBluetoothGattCallback) // 连接蓝牙设备
    }

   /**
     * BLE中心设备连接外围设备的数量有限(大概2~7个),在建立新连接之前必须释放旧连接资源,否则容易出现连接错误133
     */
    fun closeConn() {
        if (mBluetoothGatt != null) {
            mBluetoothGatt!!.disconnect()
            mBluetoothGatt!!.close()
        }
    }


 private fun initBleClient() {
        mBluetoothGattCallback = object : BluetoothGattCallback() {
            /**
             * 连接 状态
             */
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                val dev = gatt.device
               AppLog.i(TAG, String.format("连接 状态++onConnectionStateChange:%s,%s,%s,%s", dev.name, dev.address, status, newState))
                if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
                    isConnected = true
                    connTimes = 0

                    gatt.discoverServices() //启动服务发现
                } else {
                    isConnected = false
                    closeConn()
                    if (connTimes++ < 5) {//错误连接5次
                        dev.connectGatt(mContext, false, this)
                    } else {
                        connTimes = 0
                        Handler(Looper.getMainLooper()).post { mBleConnCallBack!!.onConntionedCallBack(status, isConnected) }
                    }
                }
                BleDevConnedState.saveBasicID(dev.address, isConnected)
                AppLog.i(TAG,"isConnected : "+ isConnected+"  status:"+status)
                logTv(String.format(
                        if (status == 0)
                            if (newState == 2)
                                "与[%s]连接成功"
                            else "与[%s]连接断开"
                        else "与[%s]连接出错,错误码:$status",
                        dev))
            }

            /**
             * 发现服务
             */
            override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
                AppLog.i(TAG, String.format("发现服务++onServicesDiscovered:%s,%s,%s", gatt.device.name, gatt.device.address, status))
                if (status == BluetoothGatt.GATT_SUCCESS) { //BLE服务发现成功
                    Handler(Looper.getMainLooper()).post{setNotify()}
                    // 遍历获取BLE服务Services/Characteristics/Descriptors的全部UUID
                    for (service in gatt.services) {
                        val allUUIDs = StringBuilder("UUIDs={\nS=" + service.uuid.toString())
                        for (characteristic in service.characteristics) {
                            allUUIDs.append(",\nC=").append(characteristic.uuid)
                            for (descriptor in characteristic.descriptors)
                                allUUIDs.append(",\nD=").append(descriptor.uuid)
                        }
                        allUUIDs.append("}")
                        logTv("发现服务$allUUIDs")

                    }
                }
            }

            /**
             * 读
             */
            override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("onCharacteristicRead:%s,%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr, status))
            }


            /**
             * 写入成功++
             * @param gatt
             * @param characteristic
             * @param status
             */
            override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("写入成功++onCharacteristicWrite:%s,%s,%s,%s,%s,\n" +
                        " %s", gatt.device.name, gatt.device.address, uuid, valueStr, status, Arrays.toString(characteristic.value)))                
                onSendBytes()
            }

            /**
             * 发过来
             * 通知过来的消息++
             * @param gatt
             * @param characteristic
             */
            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("发过来++onCharacteristicChanged:%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr))
                onReadBytes(characteristic.value)
            }

            override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
                val uuid = descriptor.uuid
                val valueStr = Arrays.toString(descriptor.value)

                AppLog.i(TAG, String.format("onDescriptorRead:%s,%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr, status))
            }

            /**
             * 写入通知成功
             */
            override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
                val uuid = descriptor.uuid
                val valueStr = Arrays.toString(descriptor.value)
                Handler(Looper.getMainLooper()).post {
                    mBluetoothGatt!!.requestMtu(256)//我们的是244
                }
            }

            override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
                super.onMtuChanged(gatt, mtu, status)
                mMtu=mtu-3
                AppLog.i(TAG,"连接成功"+"requestMtu"+"onMtuChanged:"+mtu+"  "+status)
                Handler(Looper.getMainLooper()).post {
                    mBleConnCallBack!!.onConntionedCallBack(status, isConnected)
                }
            }
        }
    }



5. 收发数据

与外围设备交互经常每次发的数据大于 mtu的,需要做分包处理,接收数据也要判断数据的完整性最后才返回原数据做处理,所以一般交互最少包含包长度,和包校验码和原数据。当然也可以加包头,指令还有其他完整性校验。下面分享几个公用方法:

   /**
     * 要可接收消息 需要要打开通知###
     */
    fun setNotify() {
        val service = getGattService(UUID_SERVICE)
        if (service != null) {
            // 设置Characteristic通知
            val characteristic = service.getCharacteristic(UUID_CHAR_READ_NOTIFY)//通过UUID获取可通知的Characteristic
            mBluetoothGatt!!.setCharacteristicNotification(characteristic, true)

            // 向Characteristic的Descriptor属性写入通知开关,使蓝牙设备主动向手机发送数据
            val descriptor = characteristic.getDescriptor(UUID_DESC_NOTITY)
            // descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);//和通知类似,但服务端不主动发数据,只指示客户端读取数据
            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE//1,0
            mBluetoothGatt!!.writeDescriptor(descriptor)
        }
    }

    //数据分包处理
    private fun splitPacketForMtuByte(data: ByteArray?): Queue<ByteArray> {
        val dataInfoQueue = LinkedList<ByteArray>()
        if (data != null) {
            var index = 0
            do {
                val currentData: ByteArray
                if (data.size - index <= mMtu) {
                    currentData = ByteArray(data.size - index)
                    System.arraycopy(data, index, currentData, 0, data.size - index)
                    index = data.size
                } else {
                    currentData = ByteArray(mMtu)
                    System.arraycopy(data, index, currentData, 0, mMtu)//从零开始复制++ mMtu
                    index += mMtu
                }
                dataInfoQueue.offer(currentData)
            } while (index < data.size)
        }
        return dataInfoQueue
    }
    private fun intToByteArray(a: Int): ByteArray =
            byteArrayOf((a shr 24 and 0xFF).toByte(),
                    (a shr 16 and 0xFF).toByte(),
                    (a shr 8 and 0xFF).toByte(),
                    (a and 0xFF).toByte())


    private fun bytes2Int(byteArray: ByteArray): Int =
            (byteArray[0].toInt() and 0xFF shl 24) or
                    (byteArray[1].toInt() and 0xFF shl 16) or
                    (byteArray[2].toInt() and 0xFF shl 8) or
                    (byteArray[3].toInt() and 0xFF)

 /**
     * 写数据###
     * @param instruction 指令
     * @param jsonData json数据
     * @param bleServerCallBack 接收完Basic 数据后响应
     */
    fun write(instruction: Byte, jsonData: String, bleServerCallBack: BleServerCallBack) {
        val service = getGattService(UUID_SERVICE)
        if (service != null) {
         // ....
            var bleData = byteMergerAll(PACKAGE_HEAD, intToByteArray, instructionArray,crc32ByteArray, jsonData.toByteArray())
            bleData = byteMergerAll(
                  //...头,length,指令等
                    jsonData.toByteArray(),
                    getShaHead(bleData)) //可以用hash校验数据完整性

            mBleServerCallBack = bleServerCallBack
            resultSize = 0
            mCharacteristic = service.getCharacteristic(UUID_CHAR_WRITE)//通过UUID获取可写的Characteristic
            writeCharacteristic(mCharacteristic, bleData)
        }

    }
   /**
     * 向characteristic写数据
     *
     * @param value
     */
    private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic?, value: ByteArray) {
        this.mCharacteristic = characteristic
        if (dataInfoQueue != null) {
            dataInfoQueue!!.clear()
            writeSize = value.size//这是我自己写的读写进度用的
            writeProgress = 0
            dataInfoQueue = splitPacketForMtuByte(value)
            onSendBytes()
        }
    }
 /**
     * 连续发送
     * 每次从队列里取第一个同时移除
     */
    private fun onSendBytes() {
        if (dataInfoQueue != null && !dataInfoQueue!!.isEmpty()) {
            //检测到发送数据,直接发送
            if (dataInfoQueue!!.peek() != null) {
                mCharacteristic!!.value = dataInfoQueue!!.poll()//移除并返回队列头部的元素
                writeProgress += mCharacteristic!!.value.size
                mBleServerCallBack!!.onWriteProgress(writeProgress * 100 / writeSize)
                Handler(Looper.getMainLooper()).postDelayed({
                    mBluetoothGatt!!.writeCharacteristic(mCharacteristic)
                }, 125)//120ms
            }
        }
    }

            /**
             * 写入成功++   
             * @param gatt
             * @param characteristic
             * @param status
             */
            override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("写入成功++onCharacteristicWrite:%s,%s,%s,%s,%s,\n" +
                        " %s", gatt.device.name, gatt.device.address, uuid, valueStr, status, Arrays.toString(characteristic.value)))
                onSendBytes()//继续发送
            }
        /**
             * 发过来
             * 通知过来的消息++
             * @param gatt
             * @param characteristic
             */
            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("发过来++onCharacteristicChanged:%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr))
                onReadBytes(characteristic.value) //处理发回来的数据,最后校验拼接好
            }

  • 上面的设置通知和写数据都要 用到 mBluetoothGatt的 服务,特征
  • 主动读取和通知 会用到 特征里的描述详见 setNotify() 和 write()
    而操作服务,特征,描述 都通过特定的UUIU来操作,这些uuid都是蓝牙设备从生产出来就不变了, 两手机调试时可以自定义uuid.
        val UUID_SERVICE =UUID.fromString("00000000-0000-0000-0000-0000000000f0")//自定义UUID
        val UUID_CHAR_READ_NOTIFY = UUID.fromString("00000000-0000-0000-0000-0000000000f2")
        val UUID_CHAR_WRITE = UUID.fromString("00000000-0000-0000-0000-000000000001")
        val UUID_DESC_NOTITY =  UUID.fromString("00000000-0000-0000-0000-000000000002")

我自己封装的一个BleUtil ,因为涉及跟公司业务关联性太强(主要是传输包的协议不同)就先不开源出来了,如果这边文章对大家有帮助反馈不错,我会考虑上传个demo到github供大家使用,
在这先给大家推荐一个不错 Demo,里面除了没有分包,协议,和传输速率。基本的功能都有,而且调试数据到打印到界面上了。最主要是它可以用两个个手机一个当中心设备一个当外围设备调试。

如果在实际使用蓝牙, 调用时得姿势应该是这样的
 BleUtils.getInstance(mContext)
            .write(
                BleInstructions.signTx,
                jsonObject.toString(),
                object : BleServerCallBack {
                    override fun onResponse(instruction: Byte?, result: String) {
                        val fromJson = Gson().fromJson(result, xxxBean::class.java)
                     
                    }

                    override fun onWriteProgress(progress: Int) {
                    }

                    override fun onReadProgress(progress: Int) {
                    }
                })

6. 传输速率

首先传输速率优化有两个方向,1 外围设备传输到Android 。2 Android传输到外围设备。
我在开发中首先先使用上面那位仁兄的demo调试,两个Android 设备调试不延时,上一个成功马上下一个,最多一秒发11个20字节的包。

后来和我们的蓝牙设备调试时发现发送特别快,但是数据不完整,他蓝牙模块接收成功了,但是透传数据到芯片处理时发现不完整,我们的硬件小伙伴说因为波特率限制(差不多每10字节透传要耗时1ms)和蓝牙模块的buff (打印时是最多100byte,100打印的)限制,就算蓝牙模块每包都告诉你接收成功,也是没透传完就又接收了。后来通过调试每次发20K数据,最后是 Android 发是 20字节/130ms稳定。给Android 发是20字节/ 8ms。 (天杀的20字节,网上都是说20字节最多了)

后来看了国外一家物联网公司总结的 Ble 吞吐量的文章(上面有连接),知道Android 每个延时是可以连续接收6个包的。就改为 120字节/ 16ms (为啥是16ms,不是每次间隔要6个包吗,怎么像间隔两次,这时因为波特率影响,多了5个包100字节,差不多 我们的单片机透传到蓝牙模块要多耗时不到10ms )
而Android 发数据可以申请 我们设备的mtu 来得到最多每次能发多少字节。延时还是130ms,即:241字节/ 130ms 提高12倍,这个速度还可以。

7. 扩展

BLE的传输速率分析

根据蓝牙BLE协议, 物理层physical layer的传输速率是1Mbps,相当于每秒125K字节。事实上,其只是基准传输速率,协议规定BLE不能连续不断地传输数据包,否则就不能称为低功耗蓝牙了。连续传输自然会带来高功耗。所以,蓝牙的最高传输速率并不由物理层的工作频率决定的。

在实际的操作过程中,如果主机连线不断地发送数据包,要么丢包严重要么连接出现异常而断开。

在BLE里面,传输速度受其连接参数所影响。连接参数定义如下:

1)连接间隔。蓝牙基带是跳频工作的,主机和从机会商定多长时间进行跳频连接,连接上才能进行数据传输。这个连接和广播状态和连接状态的连接不是一样的意思。主机在从机广播时进行连接是应用层的主动软件行为。而跳频过程中的连接是蓝牙基带协议的规定,完全由硬件控制,对应用层透明。明显,如果这个连接间隔时间越短,那么传输的速度就增大。连接上传完数据后,蓝牙基带即进入休眠状态,保证低功耗。其是1.25毫秒一个单位。

2)连接延迟。其是为了低功耗考虑,允许从机在跳频过程中不理会主机的跳频指令,继续睡眠一段时间。而主机不能因为从机睡眠而认为其断开连接了。其是1.25毫秒一个单位。明显,这个数值越小,传输速度也高。

蓝牙BLE协议规定连接参数最小是5,即7.25毫秒;而Android手机规定连接参数最小是8,即10毫秒。iOS规定是16,即20毫秒。

连接参数完全由主机决定,但从机可以发出更新参数申请,主机可以接受也可以拒绝。android手机一部接受,而ios比较严格,拒绝的概率比较高。

参考:
在iOS和Android上最大化BLE吞吐量
最大化BLE吞吐量第2部分:使用更大的ATT MTU

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容

  • 因为自己的项目中有用到了蓝牙相关的功能,所以之前也断断续续地针对蓝牙通信尤其是BLE通信进行了一番探索,整理出了一...
    陈利健阅读 113,652评论 172 294
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,490评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,693评论 2 59
  • 背景 蓝牙历史说到蓝牙,就不得不说下蓝牙技术联盟(Bluetooth SIG),它负责蓝牙规范制定和推广的国际组织...
    徐正峰阅读 12,229评论 6 33
  • 简爱跑步法 这套方法的核心价值观就是:运动最重要的是不能受伤,所以跑步要慢 。简爱跑步法有一个5字要诀:挺、倾、柔...
    向阳的石头阅读 138评论 0 0