Android Kotlin&BLE 低功耗蓝牙笔记

  • BLE 与经典蓝牙的区别
  • BLE 的 Kotlin 下实践
  • BluetoothGattCallback 不回调异常
  • 一些不常见的问题和暴力解决的方法
  • 经典蓝牙自动配对,关闭系统配对弹窗

经典蓝牙(Classic Bluetooth)& 低功耗蓝牙(Bluetooth Low Energy)


  • 经典蓝牙可以用与数据量比较大的传输,如语音,音乐,较高数据量传输等。

  • BLE 特点就如其名,功耗更低的同时,对数据包做出了限制。所以适用于实时性要求比较高,但是数据速率比较低的产品,如鼠标,键盘,传感设备的数据发送等。

蓝牙 4.0 支持单模和双模两种部署方式,其中单模即是我们说的 BLE,而双模指的是 Classic Bluetooth + BLE 。
实际上,BLE 和经典蓝牙的使用等各方面都像是没有关联的两个东西,甚至因为 BLE 的通讯机制不同,所以是不能向下兼容的;经典蓝牙则可以兼容到蓝牙 3.0 / 2.1。

BLE


同样,有条件一定要去看官方文档,然而这一次并没有中文版,或许可以找一些国内大佬们翻译的版本。
还有就是大佬 JBD 写的 Android BLE 蓝牙开发入门 ,而且还用 RxJava 封装成一个库可以直接调用:RxBLE ,是真的厉害,不妨去学习学习。

  • 概念与常用 API

UUID:每个服务和特征都会有唯一的 UUID ,由硬件决定。
服务(Service):蓝牙设备中可以定义多个服务,相当于功能的集合。
特征(Characteristic):一个服务可以包含多个特征,可以通过 UUID 获取到对应的特征的实例,通过这个实例就可以向蓝牙设备发送 / 读取数据。

BluetoothDeivce:调用 startLeScan()获取该实例,用于连接设备。
BluetoothManager:蓝牙管理器,调用 getSystemService() 获取,用于获取蓝牙适配器和管理所有和蓝牙相关的东西。
BluetoothAdapter:蓝牙适配器,通过 BluetoothManager 获取,用于打开蓝牙、开始扫描设备等操作。
BluetoothGatt:通用属性协议, 定义了BLE通讯的基本规则,就是通过把数据包装成服务和特征的约定过程。
BluetoothGattCallback:一个回调类,非常重要而且会频繁使用,用于回调 GATT 通信的各种状态和结果。
BluetoothGattService:服务,通过 BluetoothGatt 实例调用 getService(UUID) 获取
BluetoothGattCharacteristic:特征,通过 BluetoothGattService 实例调用 getCharacteristic(UUID) 获取,是 GATT 通信中的最小数据单元。
BluetoothGattDescriptor:特征描述符,对特征的额外描述,包括但不仅限于特征的单位,属性等。

  • 声明权限
<uses-permission android:name="android.permission.BLUETOOTH"/> 
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 
<!-- Android 5.0 及以上需要添加 GPS 权限 -->
<uses-feature android:name="android.hardware.location.gps" />
<!-- Android 6.0 及以上需要添加定位权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  • 初始化
fun initBluetoothAdapter(){
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    val bluetoothAdapter = bluetoothManager.adapter
    //如果蓝牙没有打开则向用户申请
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled)
        bluetoothAdapter.enable()
}
  • 扫描设备与停止扫描
var mDevice : BluetoothDevice ?= null
//扫描结果的回调,开始扫描后会多次调用该方法
val mLeScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
    //通过对比设备的 mac 地址获取需要的实例
    if(device.address == "50:F1:4A:A1:77:00"){
        mDevice = device
    }
}
//开始扫描之前判断是否开启了蓝牙,enable 为 false 可以停止扫描
fun scanLeDeviceWithBLE(enable:Boolean = true){
    if (mBluetoothAdapter == null)
        initBluetoothAdapter()

    if (mBluetoothAdapter?.isEnabled as Boolean){
        mBluetoothAdapter?.enable()
    }
    if (enable){
        mScanning = true
        mBluetoothAdapter?.startLeScan(mLeScanCallback)
        TimeUtilWithoutKotlin.Delay(8,TimeUnit.SECONDS).setTodo {
            mBluetoothAdapter?.stopLeScan(mLeScanCallback)
            mScanning = false
        }
    }else {
        //停止扫描,在连接设备时最好调用 stopLeScan()
        mBluetoothAdapter?.stopLeScan(mLeScanCallback)
        mScanning = false
    }
}

其实 startLeScan() 已经被声明为过时,所以开始扫描可以以广播的形式接受:

private fun startDiscover() {
    //这种方法需要注册接收广播,获取扫描结果。
    val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
    bluetoothAdapter?.startDiscovery()
}
//注册广播,监听 BluetoothDevice.ACTION_FOUND 获取扫描结果
private inner class DeviceReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            val device = intent
                    .getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
            Log.e("Service","device: ${device.address}")
        }
    }
}

或者使用新的 API:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // startScan(List<ScanFilter> filters, ScanSettings settings, final ScanCallback callback)
    // 过滤器 filters: new ScanFilter.Builder().setDeviceName(deviceName).build();
    // 扫描设置 settings: new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
    mBluetoothAdapter.bluetoothLeScanner.startScan(object : ScanCallback() {
        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            // 启用了批量扫描模式后的回调
            super.onBatchScanResults(results)
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            // startScan() 失败的回调
        }

        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            // 默认的扫描方式的回调
        }
    } )
}
  • 连接蓝牙设备
    此时已经获取到了蓝牙设备的实例:mDevice,开始连接
fun connectWithBluetoothDevice(){
    if (null == mDevice){
        toast("can not find device")
        return
    }
    if(mScanning){
        //如果正在扫描设备,则停止扫描
        scanLeDeviceWithBLE(false)
    }
    mDevice?.connectGatt(this,false,mBluetoothGattCallback)
}

关于 connectGatt() 的几个参数:

public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                     BluetoothGattCallback callback)

第二个参数,autoConnect 为 true 时,如果设备断开了连接将会不断的尝试连接。
第三个 BluetoothGattCallback 是一个接受回调的对象,也是这一部分的重点。
先看一下完整的 BluetoothGattCallback:

val mBluetoothGattCallback = object :BluetoothGattCallback(){

    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        super.onConnectionStateChange(gatt, status, newState)
        if (newState == BluetoothProfile.STATE_CONNECTED){
            //开始搜索服务
            gatt?.discoverServices()
        }
        // 接受到设备断开的状态后,还要手动调用 close(),否则可能出现连接未断开导致的重连失败等问题;
        if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            gatt?.close()
        }
        if(newState == BluetoothProfile.STATE_CONNECTING){
            //设备在连接中
        }
    }

    //成功发现服务的回调
    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
        super.onServicesDiscovered(gatt, status)

        if (gatt == null) {
            return
        }

        //设置回调,打开 Android 端接收通知的开关,用 Descriptor 开启通知的数据开关
        //这里的三个 UUID 都是由硬件决定的
        val bluetoothGattService = gatt.getService(UUID_0)
        val characteristic = bluetoothGattService.getCharacteristic(UUID_1)
        val descriptor = characteristic.getDescriptor(UUID_2)
        if (descriptor == null) {
            gatt.disconnect()
            return
        }

        //打开 Android 端开关
        if (!gatt.setCharacteristicNotification(characteristic, true)) {
            //打开失败
        }

        //假如写入数据成功,则会回调下面的 onDescriptorWrite() 方法
        //所以在 onDescriptorWrite() 方法中向硬件写入数据
        descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
        if (!gatt.writeDescriptor(descriptor)) {
            //写入失败
        }
      
    }

    //调用 writeDescriptor 的回调
    override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
        super.onDescriptorWrite(gatt, descriptor, status)

        val bluetoothGattService = gatt?.getService(UUID_SERVICE_CHANNEL)
        val characteristic = bluetoothGattService?.getCharacteristic(UUID_CHARACTERISTIC_CHANNEL)
        if (characteristic == null){
            //获取特征失败,直接断开连接
            gatt?.disconnect()
            return
        }
        //mSendValue 即要往硬件发送的数据
        //如果这里写入数据成功会回调下面的 onCharacteristicWrite() 方法
        characteristic.value = mSendValue
        if (!gatt.writeCharacteristic(characteristic)){
            //写入数据失败,断开连接
            gatt.disconnect()
        }
    }

    //调用 writeCharacteristic 的回调
    override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
        super.onCharacteristicWrite(gatt, characteristic, status)

        val stringBuilder = StringBuilder()
        characteristic?.value
                ?.filter  { it > 0 }
                ?.forEach { stringBuilder.append(String.format("%c", it)) }
        //这时候 stringBuilder 应该和上面 mSendValue 是一样的
   }

    //硬件返回数据的回调,由于设置了回调,所以当硬件返回数据时会调用这个方法
    override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
        super.onCharacteristicChanged(gatt, characteristic)

        val stringBuilder = StringBuilder()
        characteristic?.value?.forEach {
            val b = it
            hexStringBuilder.append(Integer.toHexString(b.toInt()))
            stringBuilder.append(String.format("%c",b))
        }
        runOnUiThread { toast("$stringBuilder") }
        //接受到数据之后就可以断开连接了
        gatt?.disconnect()
    }
}

首先是 onConnectionStateChange(gatt,status,newState)
这个方法在成功连接、断开连接等状态改变的时候回调,所以一开始会先进入这个方法。
参数中, newState 代表当前设备的连接的状态:

/** The profile is in disconnected state */
public static final int STATE_DISCONNECTED  = 0;
/** The profile is in connecting state */
public static final int STATE_CONNECTING    = 1;
/** The profile is in connected state */
public static final int STATE_CONNECTED     = 2;
/** The profile is in disconnecting state */
public static final int STATE_DISCONNECTING = 3;

所以当 newState 为 2 的时候就是刚连上设备的时候,这时候可以调用
gatt.discoverServices() 开始异步的查找蓝牙服务:

if (newState == BluetoothProfile.STATE_CONNECTED){
      //发现服务
      gatt?.discoverServices()
} 

执行了discoverServices()后,若找到可用的服务,系统又会回调 mBluetoothGattCallback 里的onServicesDiscovered() 方法,所以添加:

override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {

    val bluetoothGattService = gatt?.getService(UUID_0)
    val characteristic = bluetoothGattService?.getCharacteristic(UUID_1)
    if (characteristic == null){
        //获取特征的实例失败,断开连接
        gatt?.disconnect()
        return
    }
    //向硬件写入数据
    characteristic.value = mSendValue
    if (!gatt.writeCharacteristic(characteristic)){
        //当上面的方法返回 false 时,写入数据失败
        gatt.disconnect()
    }
}

如果成功写入数据,系统回调mBluetoothGattCallbackonCharacteristicWrite()方法:

override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
    super.onCharacteristicWrite(gatt, characteristic, status)
    
    //这里遍历 characteristic 中的 value,拼接在一起后成为一个 stringBuilder
    //stringBuilder 应该和发送给硬件的数据是一样的
    val stringBuilder = StringBuilder()
    characteristic?.value
            ?.filter  { it > 0 }
            ?.forEach { stringBuilder.append(String.format("%c", it)) }

     //断开连接,这一句最好延迟几秒后执行
     gatt?.disconnect()
}

上面的代码可以成功往硬件发送数据,但是不能接受硬件返回的数据。
如果想要接受硬件返回的数据,需要在 onServicesDiscovered(),也就是连上服务后,先不发送数据而是设置硬件返回数据的开关:

//设置回调:打开 Android 端接收通知的开关;并且向 Descriptor 写入数据来开启通知
val bluetoothGattService = gatt.getService(UUID_SERVICE_CHANNEL)
val characteristic = bluetoothGattService.getCharacteristic(UUID_CHARACTERISTIC_CHANNEL)
val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID)
val descriptors = characteristic.descriptors
if (descriptors == null) {
    //获取特征描述符失败,断开连接
    gatt.disconnect()
    return
}
//打开 Android 端开关
if (!gatt.setCharacteristicNotification(characteristic, true)) {
    //失败的处理
}
//向硬件写入一些数据,打开硬件返回数据的开关
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (!gatt.writeDescriptor(descriptor)) {
    //写入数据失败
}
  • 实际上向硬件写入数据这一段代码有时候是可以省略的,只需要打开 Android 段的开关即可接收到返回的数据,可能是和硬件有关。这样一来,就不能继续在 onServicesDiscovered() 执行写入数据的代码,改为在 onDescriptorWrite() 中执行。

还有就是用 Kotlin 写的 MainActivity 部分:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        verticalLayout {
            gravity = CENTER
            linearLayout {
                orientation = LinearLayout.VERTICAL
                button("搜索设备"){
                    setOnClickListener {
                        mBinder?.startScanLeDevice()
                    }
                }.lparams(width = matchParent,height = wrapContent){
                    padding = dip(5)
                    margin = dip(10)
                }

                button("发送开锁指令"){
                    padding = dip(10)
                    setOnClickListener{
                        mBinder?.connect()
                    }
                }.lparams(width = matchParent,height = wrapContent){
                    padding = dip(5)
                    margin = dip(10)
                }
            }

        }
        val intent = Intent(this, BluetoothService::class.java)
        bindService(intent,mConnect,Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        super.onDestroy()
        mDisposable?.dispose()
    }

    var mBinder : BluetoothService.MBinder ?= null
    var mDisposable : Disposable ?= null
    val mConnect = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            mBinder = service as BluetoothService.MBinder
        }
        override fun onServiceDisconnected(name: ComponentName) {

        }
    }
}

BLE 相关的代码是写在了 Service 中,通过绑定时返回的 mBinder 来调用 Service 中的方法。

connectGatt() 不触发回调 (BluetoothGattCallback) 的异常


  • 在实际的操作过程中遇到过一些蓝牙设备,在调用了 connectGatt 会无响应,既不回调 callback,也不抛出异常;
    后续的 debug 中偶然发现 6.0 及以上的 connectGatt() 新增了可选参数 transport,支持设置连接设备的传输模式的;
@param transport preferred transport for GATT connections to remote dual-mode devices
     *             {@link BluetoothDevice#TRANSPORT_AUTO} or
     *             {@link BluetoothDevice#TRANSPORT_BREDR} or {@link BluetoothDevice#TRANSPORT_LE}

这一参数在 5.0 及 5.1 中是无法直接设置的,通过反射调用后解决了以上出现的无回调、无响应问题:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    device.connectGatt(mActivity, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE)
} else {
    val d = mBluetoothAdapter.getRemoteDevice(device.address)
    val creMethod = d.javaClass.getDeclaredMethod("connectGatt",
            Context::class.java, Boolean::class.javaPrimitiveType,
            BluetoothGattCallback::class.java, Int::class.javaPrimitiveType)
    creMethod.isAccessible = true
    val transport = d.javaClass.getDeclaredField("TRANSPORT_LE").getInt(null)

    mBluetoothAdapter.getRemoteDevice(d.address)
    val res = creMethod.invoke(d, mActivity, true, bluetoothGattCallback, transport) as BluetoothGatt
}

连接未彻底断开


首先遇到的问题是 gatt.disconnect 无法彻底断开设备的连接,蓝牙设备的状态为已连接,但 gatt.getConnectionState() 的状态缺为已断开;

参考了一下各路方法,通过反射 BluetoothDevice 的内部类判断是否连接

public static final int CONNECTION_STATE_DISCONNECTED = 0;
public static final int CONNECTION_STATE_CONNECTED = 1;
public static final int CONNECTION_STATE_UN_SUPPORT = -1;
    
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @SuppressLint("PrivateApi")
    public static int getInternalConnectionState(String mac) {
        //该功能是在21 (5.1.0)以上才支持, 5.0 以及以下 都 不支持
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return CONNECTION_STATE_UN_SUPPORT;
        }
        if(Build.MANUFACTURER.equalsIgnoreCase("OPPO")){//OPPO勿使用这种办法判断, OPPO无解
            return CONNECTION_STATE_UN_SUPPORT;
        }
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        BluetoothDevice remoteDevice = adapter.getRemoteDevice(mac);
        Object mIBluetooth = null;
        try {
            Field sService = BluetoothDevice.class.getDeclaredField("sService");
            sService.setAccessible(true);
            mIBluetooth = sService.get(null);
        } catch (Exception e) {
            return CONNECTION_STATE_UN_SUPPORT;
        }
        if (mIBluetooth == null) return CONNECTION_STATE_UN_SUPPORT;

        boolean isConnected;
        try {
            Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected");
            isConnectedMethod.setAccessible(true);
            isConnected = (Boolean) isConnectedMethod.invoke(remoteDevice);
            isConnectedMethod.setAccessible(false);
        } catch (Exception e) {
        //如果找不到,说明不兼容isConnected, 尝试去使用getConnectionState 判断
            try {
                Method getConnectionState = mIBluetooth.getClass().getDeclaredMethod("getConnectionState", BluetoothDevice.class);
                getConnectionState.setAccessible(true);
                int state = (Integer) getConnectionState.invoke(mIBluetooth, remoteDevice);
                getConnectionState.setAccessible(false);
                isConnected = state == CONNECTION_STATE_CONNECTED;
            } catch (Exception e1) {
                return CONNECTION_STATE_UN_SUPPORT;
            }
        }
        return isConnected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED;

    }

BluetoothGattCallback 各回调中不可有耗时操作,否则会影响下一个回调的执行。


读写问题


  • 首先 BLE 的所有操作都是通信的结果,所以基本都是异步操作,在频繁读 / 写的过程中不免要等待上一次结束再继续读 / 写;

  • Android BLE 默认单次传输数据包最大为 20 字节,在实际场景中明显不够,一般有两种思路:

1.设置 MTU 修改单次传输包大小上限: gatt.requestMtu(),但不同设备有失败的可能;

2.分包传输,注意连接写入时的间隔问题,实际上设备是每隔一定时间去读取一次特征值来获取写入的数据,BLE 默认这个时间间隔为 7.5ms (与设备相关),如果写入的时间间隔小于这个读取间隔则会导致丢包。
因此可以在写入成功回调后 (onCharacteristicWrite()) 再继续下一个写入,或者粗暴的加一个时间间隔,考虑不同设备的差异 200ms 一般足够稳妥。

3.对于 (2) 中提到的读 / 写的时间间隔,其实可以通过 requestConnectionPriority() 来修改,参数为:
BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER
BluetoothGatt#CONNECTION_PRIORITY_BALANCED
BluetoothGatt#CONNECTION_PRIORITY_HIGH
分别对于低功耗、中等、高功耗三种模式。

经典蓝牙


参考官方文档(基础的应用基本上看这一篇文档就可以了。

其中有些细节,经典蓝牙连接设备是需要配对的,而很多蓝牙设备采用了默认的 pin 码:0000 或 1234 等。
这里就存在优化空间,我们可以通过代码去设置并关闭系统弹出的配对窗口 (这里的实践基于 Android 5.1);

fun connect(device: BluetoothDevice) {
    var isBond = false
    try {
        //检查是否处于未配对状态
        if (device.bondState == BluetoothDevice.BOND_NONE) {
            // 监听配对弹窗的广播
            activity.registerReceiver(object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    // 关闭配对弹窗
                    abortBroadcast()
                    // 设置 Pin 码, 默认为 0000
                    val removeBondMethod = device.javaClass.getDeclaredMethod("setPin", ByteArray::class.java)
                    removeBondMethod.invoke(device, "0000".toByteArray())
                    activity.unregisterReceiver(this)
                }
            }, IntentFilter().apply { addAction("android.bluetooth.device.action.PAIRING_REQUEST") })

            // 开始配对
            val creMethod = device.javaClass.getMethod("createBond")
            isBond = creMethod.invoke(device) as Boolean
        } else {
            isBond = true
        }
    } catch (e: Exception) {
        e.printStackTrace()
        // onConnectError("连接设备失败, 请手动配对蓝牙;", 272)
    }
    if (!isBond) {
        // onConnectError("连接设备失败, 请手动配对蓝牙;", 275)
        return
    }

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

推荐阅读更多精彩内容