QsBle蓝牙操作框架,用kotlin协程带给你不一样的使用体验,Java也能食用

近些年来,物联网技术发展的越来越迅速,蓝牙作为物联网界的一霸,在物联网快速发展的今天越来越收到人们的关注,在如今越来越卷的时代,Android蓝牙开发也成为很多从事Android开发行业人员的必备技能

低功耗蓝牙发展的历史及其优点,我们今天就不谈,等到后面有机会我会和大家彻夜长谈,我们直接切入正题

QsBle是一款Android Ble框架

github链接

点击下载演示demo

特点

  • 相比老的Ble框架,更契合现在Android开发人员的代码风格

  • 完全函数式编程,所有的Ble回调都是函数式的

  • 支持所有的Ble操作的回调

  • 发送的数据超过mtu,QsBle也有完善的分包组包机制

  • 支持Kotlin协程,让你用同步的方式操作异步的代码,不用再为蓝牙的各种回调地狱而烦恼

  • 支持Flow

  • 支持链式编程,让你对蓝牙的各种操作能够顺序执行,以前几百行代码才能实现的逻辑,QsBle只需要一行代码

  • 完善的ota支持,你只需提供你的ota文件,其它所有的操作都交给QsBle

  • 完善的异常处理机制,让你出现异常能够及时的定位问题

  • 核心代码都是使用Java编写,使用Java开发的人也不需要为Java不能使用而担心,并且Kotlin调用Java代码也提供了各种判空机制,让你不用为Kotlin调用Java代码的空指针而担心

  • 其它Ble框架有的,QsBle也都有

  • 最重要的一个特点是,作者这段时间是会一直维护的

添加依赖


allprojects {

    repositories {

        ...

        maven { url 'https://jitpack.io' }

    }

dependencies {

    //QsBle必须添加的依赖

  implementation 'com.github.zqisheng.QsBle:ble:1.0.1'

    //如果要使用kotlin协程功能的话,就要添加下面的依赖,非必须

  //implementation 'com.github.zqisheng.QsBle:ble_coroutines:1.0.1'

}

使用前

添加蓝牙扫描需要添加的权限,并且在运行时申请权限


<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

对于Android12的蓝牙适配,框架中已经默认添加了以下三样权限,所以无须手动添加,但是这三个权限是动态申请权限,在Android12版本及以上,必须在代码中动态申请


<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

初始化

方式1(推荐):


QsBle.getInstance().init(Context context);

方式2:


QsBle.getInstance().init(Context context,Handler handler);

注1:QsBle的所有对Ble的操作和回调都是在一个线程中执行的,方式1的初始化是默认使用框架实现的线程对Ble进行操作.但是如果你想让Ble的所有操作都在自己指定的线程中执行,你也可以传入Handler,这样所有Ble的操作和回调都会在这个指定的Handler中调用了
比如你想让所有的操作都在主线程中回调,你可以传一个主线程的Handler,这样所有Ble的操作和回调都是在主线程中回调了,
但是作者强烈不建议这样做**

注2:QsBle的初始化没有获取任何用户手机的隐私信息,所以放心在任何时候初始化

简单使用

使用方式1:


QsBle.getInstance().startScan();

QsBle.getInstance().connect(mac);

QsBle.getInstance().connect(mac);

QsBle.getInstance().openNotify(mac,serviceuuid,notifyUuid);

QsBle.getInstance().writeByLock(mac,serviceUuid,writeUuid,value)

....

使用方式2(链式操作)(Java推荐):


QsBle.getInstance().chain(mac)

    .connect()...

    .openNotify(serviceUuid,notifyUuid)...

    .requestConnectionToHigh()...

    .writeByLock(serviceUuid,writeUuid,value)...

    .writeByLock(serviceUuid,writeUuid,value)...

    .writeByLock(serviceUuid,writeUuid,value)...

    .disconnect()...

    ...

    .start()

注:以上的链式操作流程是:先连接地址为mac的设备,连接成功后,会打开该设备的notify,打开notify成功后,会设置Connection参数,参数设置成功后,会向特征写数据,框架确认数据写成功后,会断开该设备的连接,其中任何一环没有达到预期结果都不会继续执行

使用方式3(kotlin协程)(kotlin推荐)


//使用kotlin协程实现方式2的操作

bleLifeScope.launch ({

    val chain = ble.chain(mac)

    chain.connect().await()

    chain.openNotify().await()

    chain.requestConnectionToHigh().await()

    chain.writeByLock().await()

    chain.disconnect().await()

},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

注:bleLifeScope是Lifecycle对象的扩展对象,在Lifecycle销毁时,bleLifeScope会自动中断内部的蓝牙协程操作,并且将协程销毁

关于协程和链式操作几个公共操作符的说明

这几个操作符的特点是所有链式操作都支持的

操作符1:dump(boolean dump)

含义:当前的链式段在执行失败时是否结束整条执行链,默认是true,结束整条链

链式操作中作用:


//直接通过链式连接5个设备

QsBle.getInstance().chain()

    .connect(mac1)...

    .connect(mac2).dump(false)...

    .connect(mac3).dump(true)...//默认就是true,可以不设置

    .connect(mac4)

    .connect(mac5)

    .start()

说明:mac1连接成功后会去连接mac2,如果mac2连接失败,比如连接超时或者系统返回错误,那么mac3是否是否会继续连接,答案是会继续连接,因为mac2这条链的dump=false,即使连接失败也不会中断整条链,mac3会在mac2连接失败后继续执行连接操作,如果mac3也连接失败了呢?因为dump=true,直接中断整条链的执行,后面mac4和mac5就不会执行连接操作了

协程中的作用:


bleLifeScope.launch ({

    //直接通过链式连接5个设备

    QsBle.getInstance().chain().apply{

        connect(mac1).await()

        connect(mac2).dump(false).await()

        connect(mac3).dump(true).await()//默认就是true,可以不设置

        connect(mac4).await()

        connect(mac5).await()

    }

},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

说明:和上面链式执行流程是一样的,在协程中唯一不一样的就是,当这条链的dump=true时,发生连接失败,协程中会直接抛出异常,所以在执行到mac3连接失败时,会回调onErro函数,想要执行失败不抛出异常,那执行设备dump=false即可

操作符2:async())

含义:用过kotlin协程的应该都清楚这个操作符的作用,这个操作符的作用和协程的很相似,它的作用是将当前链异步执行,执行这条链的时候立刻返回成功执行下一条链,而不等待这条链的返回结果

链式操作中作用:


//通过链式同时连接5个设备

QsBle.getInstance().chain()

    .connect(mac1).async()...

    .connect(mac2).async().dump(false)...

    //由于async是直接返回成功的,不存在失败,故dump的值多少对整条链的执行没有任何影响

    .connect(mac3).async().dump(true)...//默认就是true,可以不设置

    .connect(mac4).async()

    .connect(mac5).async()

    .start()

协程中的作用:


bleLifeScope.launch ({

    //协程中使用需要注意,即使你调用了async()操作符

    //但是后面接着调用了await()

    //那async()操作符将会失效

    /**

    * 所以执行流程是先等mac1连接成功

    * 当然mac1连接失败会直接抛出异常,因为dump默认为true

    * 接着同时连接后面的四个设备

    * */

    QsBle.getInstance().chain().apply{

        connect(mac1).async().await()

        connect(mac2).async().start()

        connect(mac3).async().start()

        connect(mac4).async().start()

        connect(mac5).async().start()

    }

},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

操作符3:delay(long delay)

含义:当前的链式段会延迟delay ms执行,注意指是当前链

链式操作中作用:


QsBle.getInstance().chain()

    .connect(mac1)...

    .connect(mac2).delay(1000).dump(false)...

    .connect(mac3).delay(2000).dump(true)...//默认就是true,可以不设置

    .connect(mac4).delay(3000)

    .connect(mac5).delay(4000)

    .start()

协程中的作用:


bleLifeScope.launch ({

    QsBle.getInstance().chain().apply{

        connect(mac1).await()

        connect(mac2).await()

        //这个用的是kotlin协程提供的阻塞函数

        delay(1000)

        //这个使用的是框架自己实现的delay()操作符

        //虽然它们执行的结果和时间都是一样的,但是要区分

        connect(mac3).delay(2000).await()

        connect(mac4).await()

        delay(3000)

        connect(mac5).delay(4000).await()

    }

},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

操作符4:retry(int retry)

含义:当前的链式段执行失败重写执行次数

链式操作中作用:


QsBle.getInstance().chain()

    .connect(mac1)...

    //retry操作符作用的是当前的链

    //如果mac2连接全部失败,会尝试重写连接3次

    .connect(mac2).retry(3)...

    //这里是对mac3重连3*3=9次,retry操作符是对当前链式段的重试

    .connect(mac3).reConnectCount(3).retry(3)...//默认就是true,可以不设置

    .start()

协程中的作用:


bleLifeScope.launch ({

    QsBle.getInstance().chain().apply{

        connect(mac1).await()

        connect(mac2).retry(3).await()

        connect(mac3).reConnectCount(3).retry(3).await()

    }

},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

操作符5:timeout(long timeout)

含义:当前的链式段在最大执行的时间,也就是超时时间

链式操作中作用:


QsBle.getInstance().chain()
      /**
     *假设连接设备花费了5000ms,timeout=4000ms,那么这条链就会被判断执行失败,但是设备的状态是连接的
     *只是这条链是执行失败的
    **/
    .connect(mac1).connectTimeout(7000).timeout(4000)...

    .start()

协程中的作用:


bleLifeScope.launch ({

    QsBle.getInstance().chain().apply{

        //该协程最长会等待多久?
        //最长的情况下等待4000ms,只要超过了timeout的时间,还没有结果,就判断执行失败
        connect(mac1).timeout(4000).retry(3).await()

    }

},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

操作符6:withMac(String mac)

含义:当前的链式段及其后面执行的链对应的设备mac地址,这个参数除了扫描的链不是必须,其它的链都是必须的,只不过有的是隐式有的是显式

链式操作中作用:


//以下面一个不恰当的例子为例

QsBle.getInstance().chain()

            //假设将下面的注释去掉,这条链执行到这会直接报错,因为没有默认的mac传入

            //.connect()

            .connect(mac1)

            .connect(mac2)

            //value0是发给mac2的,因为上游传入的是mac2

            .writeByLock(serviceUuid,chacUuid,value0).

            //value1是发给mac1的

            .writeByLock(serviceUuid,chacUuid,value1).withMac(mac1)

            //value2是发给mac1的

            .writeByLock(serviceUuid,chacUuid,value2)

            //value3是发给mac1的

            .writeByLock(serviceUuid,chacUuid,value3)

            //value4是发给mac2的,因为切换了mac的值

            .writeByLock(serviceUuid,chacUuid,value4).withMac(mac2)

            //value5是发给mac2的

            .writeByLock(serviceUuid,chacUuid,value5)

            //连接mac3

            .connect().withMac(mac3)

            //value6是发给mac3的

            .writeByLock(serviceUuid,chacUuid,value6)

            //value7是发给mac3的

            .writeByLock(serviceUuid,chacUuid,value7)

            //断开mac3,因为上游传入的是mac3

            .disconnect()

            //断开mac2

            .disconnect(mac2)

            //断开mac3

            .disconnect(mac3)

            .start()

协程中的作用:


参考上面提供的样例

全局默认配置

改变默认的全局配置,直接设置对应的值就行


public class BleGlobalConfig {

    //默认的扫描时间

    public static long scanTime = 20000;//ms

    //默认的写特征值重写次数

    public static int rewriteCount = 3;

    //默认的发现服务失败重新发现的次数

    public static int discoveryServiceFailRetryCount = 3;

    //默认的服务uuid

    public static UUID serviceUUID;

    //默认的写特特征uuid

    public static UUID writeUUID;

    //默认的通知uuid

    public static UUID notifyUUID;

    //默认的连接超时时间

    public static int connectTimeout = 7000;

    //默认的重连次数

    public static int reconnectCount = 0;

    //默认的单个mtu包的写特征值超时时间

    public static int singlePkgWriteTimeout = 200;

    //最大的连接数量,当连接的设备超过最大连接设备数时,会按照设备连接时间断开最远的设备

    public static int maxConnectCount = 7;

    //默认的单个mtu包的ota写特征值失败重写次数

    public static int otaSingleRewriteCount = 3;

    //ota发送的段尺寸,比如文件大小1000b,otaSegmentSize=200b,那么每发200个长度就会回调一下progress

    public static int otaSegmentSize = 200;

}

Ble回调的监听

所有通过addXXXCallback形式添加的函数,必须在适当的时机调用对应的rmXXXCallback方法将其移除,不然会造成内存泄漏
例如:我在activity的onCreate方法中添加了一个扫描的监听和设备连接状态的监听


    IConnectStatusChangeCallback connectStatusChangeCallback = (device, isConnect, status, profileState) -> {

        //设备连接状态回调

    };

    IScanCallback scanCallback = (device, rssi, scanRecord) -> {

        //扫描到设备回调

    };

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        QsBle.getInstance().addScanCallback(scanCallback);

        QsBle.getInstance().addConnectStatusChangeCallback(mac,connectStatusChangeCallback);

    }

那在这个activity销毁的时候,你必须要将这个回调移除,不然会造成内存泄漏甚至app闪退


    @Override

    protected void onDestroy() {

        QsBle.getInstance().rmScanCallback(scanCallback);

        QsBle.getInstance().rmConnectStatusChangeCallback(mac,connectStatusChangeCallback);

    }

对于和某个Lifecycle生命周期绑定的回调,QsBle也提供了更好的添加方式

例如在activity中,某些activity实现了Lifecycle类的接口,你只需调用addXXXCallback方法,传入Lifecycle参数,这个生命周期对象被销毁的时候会自动移除回调


//kotlin方式

QsBle.getInstance().addConnectStatusChangeCallback(mac,this.lifecycle, IConnectStatusChangeCallback { device, isConnect, status, profileState ->

        //连接状态回调

})

QsBle.getInstance().addScanCallback(this.lifecycle, IScanCallback { device, rssi, scanRecord ->

        //扫描回调

})

//java方式

QsBle.getInstance().addConnectStatusChangeCallback(mac,this.getLifecycle(), (device, isConnect, status, profileState) ->{

        //连接状态回调

});

QsBle.getInstance().addScanCallback(this.getLifecycle(), (device, rssi, scanRecord) ->{

        //扫描回调

});

超过Mtu的分包和组包情况

Android Ble的默认mtu是20byte长度,对于大于20byte的数据,需要app和设备开发人员确定分包的协议,一般分包协议将一个mtu数据分为包头和数据位,包头中规定了这个数据的序号等数据,设备收到一个mtu去掉包头,拼装数据,形成一个完整的包

1.分包的情况

在QsBle中写特征值分为两种,一种是写一个mtu长度的数据,一种是写超过一个mtu长度的数据

写一个mtu长度的数据的方法:


void write(String mac,UUID serviceUuid, UUID chacUuid, byte[] value, int retryWriteCount);

void writeNoRsp(String mac,UUID serviceUuid, UUID chacUuid, byte[] value,int retryWriteCount);

写超过一个mtu长度的数据的方法:


void writeByLock(String mac,UUID serviceUuid, UUID chacUuid, byte[] value, int retryWriteCount, Function2<Boolean,Integer> writeCallbac);

void writeByLockNoRsp(String mac,UUID serviceUuid, UUID chacUuid, byte[] value,int retryWriteCount, Function2<Boolean,Integer> writeCallback);

处理这两种情况有以下需要注意的情况


注:这两种方法在一个类型的设备中最好指使用其中一个,作者建议所有的写特征值都使用writeByLock和writeByLockNoRsp两个方法,方法中带Lock的,都是指向这个设备的特征中写的数据都会进入write队列中,后面的消息会等收到系统的write成功回调或者触发超时后再发送下一个消息,由于不带Lock的方法都是直接发送的,不需要排队,如果你发送了一个长数据,长数据没有一下子发送完,后面又调用write方法发送了一个mtu数据,在设备端就会出现撞包现象,导致这该长数据将在设备端无法识别,除非你们的包头中包含了长数据的标志位或者你们的数据不需要分包

所以作者强烈建议
所有的写特征值都使用writeByLock和writeByLockNoRsp两个方法

2.组包的情况

对于设备端响应的消息大于一个mtu长度,QsBle也提供了组包的接口

你只需实现组包的抽象类BaseMultiPackageAssembly
下面是例子


class MyMulitPkgParser :

    BaseMultiPackageAssembly(){

    override fun onChanged(chac: BluetoothGattCharacteristic?, value: ByteArray?) {

        super.onChanged(chac, value)

    }

    override fun onError(e: Exception?) {

    }

    override fun isLastPkg(value: ByteArray): Boolean {

        //通过判断这个返回的mtu数据是否是最后一个包来判断是否组装成功

        //如果返回true,说明数据全部接受

        //如果返回false,说明还有下一个包,不能判断组装完成

    }

    override fun getPkgIndex(value: ByteArray): Int {

        //从这个返回的mtu数据中取得这个包再整个数据中的包序号

    }

}

实现了组包类后,你需要调用


QsBle.getInstance().setMultiPackageAssembly(mac, MyMulitPkgParser())

设置这个设备的全局组包解析器

设置成功后,你只需在需要获得数据时添加一个组包回调监听就可以获得组装成功的包了


ble.addBleMultiPkgsCallback(mac,MainActivity.nUuid, IBleMultiPkgsCallback { device, characteristic, result ->

    //所以的包组包成功后会回调

    //result是一个List<byte[]>类型,里面是组装后的所有结果

})

注:这个也需要再合适的时候调用rmBleMultiPkgsCallback将回调移除

Ota升级

Ota对app的开发人员来说,就是如何把一个文件传给设备,QsBle提供了完善的Ota升级支持

方式1:

调用QsBle的writeFile方法:


QsBle.getInstance()

    .writeFileNoRsp(String mac, UUID serviceUuid, UUID chacUuid, byte[] fileBytes, IOtaUpdateCallback otaUpdateCallback);

QsBle.getInstance()

    .writeFileNoRsp(String mac, UUID serviceUuid, UUID chacUuid, File file, IOtaUpdateCallback otaUpdateCallback)

//上面所有的方法都是对下面writeFileNoRsp方法的封装,QsBle只关注你传进来的io流

QsBle.getInstance()

    .writeFileNoRsp(String mac, UUID serviceUuid,UUID chacUuid, int fileByteCount, int segmentLength,InputStream datasource,IOtaUpdateCallback otaUpdateCallback);

参数说明:

fileByteCount:要发送ota文件的字节总长度

segmentLength:段长度,发送多少长度回调回调一下文件发送的进度

datasource:传入的ota文件流

otaUpdateCallback:ota升级的回调


public interface IOtaUpdateCallback {

    //开始发送文件之前会回调

    void onStart();

    //文件已经成功发送给设备

    void onSuccess();

    //发送文件的过程中出现错误会回调

    void onError(Exception e);

    //发送文件进度

    void onProgress(int progress);

}

方式2(仅支持kotlin):

使用框架提供的协程操作发送ota文件,相比方式1,这种方式性能较差,但是可以开发人员灵活操作,应对各种复杂ota场景

协程思路:
使用kotlin协程实现方式2的操作
伪代码


bleLifeScope.launch ({

    val datasource:InputStream = ...

    val chain = ble.chain(mac)

    val value = ByteArray()

    while(datasource.available()>0){

        datasource.read(value)

        chain.writeByLock().await()

        //写成功一段数据后处理你的逻辑,比如每个ota包中包括包头,包尾,数据部分

    }



},onError = {

    //协程执行错误会调用,回调在主线程中

},onStart = {

  //协程执行开始之前会调用,回调在主线程中

},onFinally = {

    //不管协程执行成功还是失败,这个方法最终都会调用,回调在主线程中

})

方式3:

使用链式操作递归发送文件
链式递归思路:
伪代码


public void writeFile(byte[] value){

  QsBle.getInstance().chain(mac).writeByLockNoRsp(value).start((isSuccess, e) -> {

            if (isSuccess){

                //填充字节数组

                writeFile(value)

            }

    });

}

TODO

  • 制作文档

  • Android Ble教程

  • 经典蓝牙接入

  • Ble外设功能支持

  • 敬请期待

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

推荐阅读更多精彩内容