Android BLE開(kāi)發(fā)小記

自己封裝的BLE庫(kù)(5.0以上)

這里不記錄具體代碼規(guī)則,后面會(huì)給出參考文章,別人已經(jīng)寫(xiě)很詳細(xì)了,我就單純記錄下踩過(guò)的坑吧;

1. 版本支持

Android 從 4.3(API Level 18) 開(kāi)始支持低功耗藍(lán)牙(Bluetooth low energy),但是只支持作為中心設(shè)備(Central)模式,這就意味著 Android 設(shè)備只能主動(dòng)掃描和鏈接其他外圍設(shè)備(Peripheral),從 Android 5.0(API Level 21) 開(kāi)始兩種模式都支持。
P.S. 不過(guò)也不是5.0以上就全部都支持,之前測(cè)試到魅族M2貌似就開(kāi)不起peripheral模式,畢竟硬件相關(guān),很難保證,我同事之前開(kāi)發(fā)時(shí)候甚至碰到過(guò)某些設(shè)備會(huì)固定少發(fā)一個(gè)字節(jié),也是坑啊...

2. 踩過(guò)的坑

2.1 開(kāi)啟peripheral模式

之前以為開(kāi)啟了手機(jī)藍(lán)牙和gps功能, 手機(jī)就能被central設(shè)備搜索到, 那是經(jīng)典藍(lán)牙, 要想啟用BLE功能并作為peripheral從機(jī),需要使用 BluetoothLeAdvertiser 開(kāi)啟廣播模式:
P.S. BLE鏈接不會(huì)彈出連接請(qǐng)求,比經(jīng)典藍(lán)牙方便,畢竟不打擾用戶,另外,查到的資料說(shuō),BLE central大概最多同時(shí)鏈接7臺(tái)設(shè)備左右;

/**
 * 開(kāi)啟廣播模式,用于本機(jī)被其他central設(shè)備搜索到
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun startAdvertising() {
    if (isBluetoothEnable()
            && !isAdvertising
            && isSupportAdvertisement
            && mBluetoothLeAdvertiser != null
            && mGattServer != null) {
        val success = mGattServerCallBack.setupServices(mGattServer)
        Logger.d("startAdvertising result  = $success ", TAG)
        if (success) {
            mBluetoothLeAdvertiser?.startAdvertising(createAdSettings(true, 0), createAdData(), mAdCallback)
        }
    } else {
        Logger.d("startAdvertising fail", TAG)
    }
}

2.2 藍(lán)牙地址動(dòng)態(tài)變化

參考這篇
Google在Android6.0上修改了獲取設(shè)備標(biāo)識(shí)信息功能:

// 以下方法固定返回:  02:00:00:00:00:00
WifiInfo.getMacAddress()
BluetoothAdapter.getAddress()

坑爹的是,假設(shè)central設(shè)備掃描得到peripheral的藍(lán)牙地址記為: A , 連接同一臺(tái)peripheral設(shè)備時(shí)獲取的藍(lán)牙地址記為B, A跟B還不一致,又動(dòng)態(tài)變化了,真是坑啊:

之所以會(huì)想要記錄設(shè)備藍(lán)牙地址,是想作為唯一標(biāo)識(shí)符,在轉(zhuǎn)傳信息時(shí),不要再回傳到數(shù)據(jù)來(lái)源方, 比如 A 發(fā)送數(shù)據(jù)給 B, B再往其他設(shè)備轉(zhuǎn)傳時(shí),就不需要回傳給A了,但是地址動(dòng)態(tài)變化的話,我就沒(méi)轍了,有解決方案的話麻煩告知我一下;

// 低功耗藍(lán)牙掃描回調(diào)
var mLeScanCallback: ScanCallback? = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        super.onScanResult(callbackType, result)
        //                Logger.d("scan successful $result")
        // 這里通過(guò)ScanResult獲取到的藍(lán)牙地址A,跟通過(guò)手機(jī)系統(tǒng)設(shè)置頁(yè)面查看得到的藍(lán)牙地址是不同的,而且每次重新開(kāi)啟peripheral模式后,同一臺(tái)手機(jī)的藍(lán)牙地址就又變化了
        // 
        // 另外,同一臺(tái)設(shè)備會(huì)在短時(shí)間內(nèi)被掃描到很多次,因此不是需要對(duì)設(shè)備進(jìn)行過(guò)濾判斷
        addBleDevice(result)
    }

      override fun onBatchScanResults(results: MutableList<ScanResult>?) {
          super.onBatchScanResults(results)
          results?.forEach { addBleDevice(it) }
      }

      override fun onScanFailed(errorCode: Int) {
        super.onScanFailed(errorCode)
        if (ScanCallback.SCAN_FAILED_ALREADY_STARTED != errorCode) {
            isScanningBle = false
        }
        Logger.d("scan failed errorCode = $errorCode")
      }
}

2.3 自定義characteristic UUID

之前以為只要符合uuid模式: 00000000-0000-0000-0000-000000000000(8-4-4-4-12)隨便定義即可, 后來(lái)看了 這篇 才發(fā)現(xiàn)不是這樣的,能自定義的只是其中一部分,有興趣的可以去研究下 BLE文檔;
0000????-0000-1000-8000-00805f9b34fb ????就表示4個(gè)可以自定義16進(jìn)制數(shù)

2.4 跟iOS通訊時(shí)循環(huán)寫(xiě)入數(shù)據(jù)失敗

我們是通過(guò) Characteristic 來(lái)寫(xiě)入的, 它有個(gè)屬性來(lái)指明發(fā)送時(shí)不需要響應(yīng): BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE , 而我在跟iOS交互時(shí),貌似這個(gè)字段雙方設(shè)定不一致,導(dǎo)致發(fā)送后一直沒(méi)收到響應(yīng),然后iOS就一直重發(fā);
因此,需要在作為peripheral模式時(shí),添加的characteristic需要設(shè)置為: BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
另外,作為central設(shè)備往其他設(shè)備發(fā)送消息時(shí),也需要添加該屬性:

  1. Android和iOS使用同一套BLE協(xié)議,因此可以通訊,如果是wifi direct的話,就不行了;
  2. Android 4.3雖然也支持central模式,但是查到的文章有說(shuō)在跟iOS參數(shù)交互時(shí)有問(wèn)題,而我使用4.3來(lái)搜索其他Android設(shè)備也經(jīng)常找不到,因此就直接不考慮了,從5.0開(kāi)始;
/**
 * 接收數(shù)據(jù)時(shí),通過(guò)本類回調(diào)處理
 */
class GattServerCallBack : BluetoothGattServerCallback() {

    companion object {
        private val TAG = "GattServerCallBack"
    }

    private var mGattServer: BluetoothGattServer? = null

    /**
     * 初始化需要用來(lái)轉(zhuǎn)傳數(shù)據(jù)的 service/characteristic
     * */
    private val mRelayService by lazy {
        val service = BluetoothGattService(UUID.fromString(BleConstant.RELAY_SERVICE_UUID), BluetoothGattService.SERVICE_TYPE_PRIMARY)
        val characteristic = BluetoothGattCharacteristic(
                UUID.fromString(BleConstant.RELAY_CHARACTERISTIC_UUID),
                BluetoothGattCharacteristic.PROPERTY_READ
                        or BluetoothGattCharacteristic.PROPERTY_WRITE
                        or BluetoothGattCharacteristic.PROPERTY_NOTIFY
                        or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, // 這里設(shè)定不需要回應(yīng),也可選擇需要響應(yīng)模式
                BluetoothGattCharacteristic.PERMISSION_READ
                        or BluetoothGattCharacteristic.PERMISSION_WRITE)// 可寫(xiě)模式,不同ble設(shè)備間通過(guò)本characteristic來(lái)傳輸數(shù)據(jù)
        characteristic.setValue(BlePara.adCharacteristicValue)

        val addCharacteristic = service.addCharacteristic(characteristic)
        Logger.d("addCharacteristic result = $addCharacteristic", TAG)
        service
    }

    /**
     * 廣播開(kāi)始后,設(shè)置一個(gè)用于接收消息的service
     * 后續(xù)有數(shù)據(jù)傳入時(shí),會(huì)觸發(fā) [org.lynxz.ble_lib.callbacks.GattServerCallBack.onCharacteristicWriteRequest]
     * */
    fun setupServices(gattServer: BluetoothGattServer?): Boolean {
        if (gattServer == null) {
            return false
        }

        // 設(shè)置一個(gè)GattService以及BluetoothGattCharacteristic
        mGattServer = gattServer
        val service = mGattServer?.getService(UUID.fromString(BleConstant.RELAY_SERVICE_UUID))
        if (service == null) {
            val addResult = mGattServer?.addService(mRelayService)
            Logger.d("  -> 添加自定義service...result = $addResult", TAG)
        } else {
            Logger.d("  -> 添加自定義service... service已存在,不用重復(fù)添加", TAG)
        }
        return true
    }

    override fun onCharacteristicWriteRequest(device: BluetoothDevice?, requestId: Int, characteristic: BluetoothGattCharacteristic?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) {
        super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value)
        // 按需發(fā)送響應(yīng)
        var responseResult = true
        if (responseNeeded) responseResult = mGattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null) ?: false
        Logger.d("responseNeeded = $responseNeeded ,send response result = $responseResult , receive data length = ${value?.size}")
    }
}
// 作為central設(shè)備,通過(guò)characteristic發(fā)送數(shù)據(jù)時(shí)
val service = gatt.getService(UUID.fromString("*********")) ?: return false
        val relayChar = service.getCharacteristic(UUID.fromString("*********")) ?: return false

val headPackage = ByteArray(20)
relayChar.value = headPackage
        relayChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
val result = gatt.writeCharacteristic(relayChar)

2.5 發(fā)送超過(guò)20字節(jié)數(shù)據(jù)

擴(kuò)展閱讀
BLE默認(rèn)單次傳輸長(zhǎng)度為20字節(jié), 對(duì)于超過(guò)該長(zhǎng)度的數(shù)據(jù),有兩種方式進(jìn)行處理:

  1. 修改MTU值(最大為512字節(jié))
    在跟iOS交互的時(shí)候,發(fā)現(xiàn)它一次性可以往Android發(fā)送512字節(jié)(Android使用默認(rèn)設(shè)定),后來(lái)才發(fā)現(xiàn)Android設(shè)備間也可以重新指定該值,不過(guò)使用這種方式的話,我測(cè)試到有這種現(xiàn)象: mtu設(shè)置回調(diào)成功,central設(shè)備發(fā)送數(shù)據(jù)也成功,但peripheral設(shè)備卻不能完整接收到,比如我設(shè)置512字節(jié),但收到的可能只有140字節(jié),因此我沒(méi)有采用這種方式:
mGattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
        super.onConnectionStateChange(gatt, status, newState)
        val device = gatt.device
        Logger.d("onConnectionStateChange newState =  $newState  ${device.address}")
        if (BluetoothGatt.STATE_CONNECTED == newState) {
            gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
            Logger.d("設(shè)置mtu結(jié)果 : ${gatt.requestMtu(BlePara.mtu)}"
        } else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
            gatt.close()
        }
    }

    // mtu設(shè)置成功后才去搜索service/characteristic,然后才可以傳輸數(shù)據(jù)
    override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
        super.onMtuChanged(gatt, mtu, status)
        Logger.d(" mtu = $mtu  $status")
        if (status == BluetoothGatt.GATT_SUCCESS) {
             gatt.discoverServices();
        }
    }
}
  1. 對(duì)數(shù)據(jù)進(jìn)行分包操作,添加控制信息


    藍(lán)牙數(shù)據(jù)分包.png

分為三部分,每個(gè)分包固定20字節(jié):
a. head包,包含一些控制信息,如傳送的數(shù)據(jù)長(zhǎng)度,用于整合數(shù)據(jù)包
b. 用戶要傳送的數(shù)據(jù)內(nèi)容(可加密);
c. tail包,所有數(shù)據(jù)發(fā)送完成后,發(fā)送一個(gè)結(jié)束信息(主要是避免head包發(fā)送失敗時(shí),接收方一直在等待發(fā)送結(jié)束,當(dāng)然,若是tail包也發(fā)送失敗,則需要通過(guò)接收超時(shí)機(jī)制來(lái)控制)
P.S. 跟iOS的同學(xué)交流后發(fā)現(xiàn),iOS設(shè)備間單次最大也只是能發(fā)送512字節(jié),因此應(yīng)該也有分包的需求;

2.6 分包發(fā)送時(shí)間間隔過(guò)長(zhǎng)的問(wèn)題

stack overflow
連續(xù)通過(guò)characteristic寫(xiě)入數(shù)據(jù)時(shí),相鄰分包之間需要間隔一下,之前測(cè)試發(fā)現(xiàn)100ms失敗率比較大,200ms就比較ok,但是也有一定概率失敗,而且,單包20字節(jié)
,我要傳輸?shù)臄?shù)據(jù)基本都要400字節(jié)左右,總耗時(shí)(包括連接等)就可能達(dá)到5s以上,感覺(jué)時(shí)間還是太長(zhǎng),兩種方式來(lái)避免:

  1. 修改 requestConnectionPriority() 值為 BluetoothGatt.CONNECTION_PRIORITY_HIGH
    這樣設(shè)定后,分包之間設(shè)置為20ms就沒(méi)再發(fā)現(xiàn)有出問(wèn)題過(guò)(至少我手頭的機(jī)型沒(méi)出錯(cuò)過(guò))
private var mGattCallback: BluetoothGattCallback? = null
mGattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
        super.onConnectionStateChange(gatt, status, newState)
        val device = gatt.device
        Logger.d("onConnectionStateChange newState =  $newState  ${device.address}")
        if (BluetoothGatt.STATE_CONNECTED == newState) {
            Logger.d("onConnectionStateChange STATE_CONNECTED = $newState ,gatt == mGatt? = ${gatt == mGatt}")
            // 發(fā)送大數(shù)據(jù)時(shí)設(shè)置如此,有人建議發(fā)送完成后要設(shè)置成默認(rèn)的: CONNECTION_PRIORITY_BALANCED
            gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
            // REFACTOR: 17/06/2017 可以設(shè)置mtu大小,若啟用此方式,則請(qǐng)?jiān)趏nMtuChanged()回調(diào)成功后再搜索及發(fā)送數(shù)據(jù),但Android之間測(cè)試發(fā)現(xiàn)接收方有些只能收到152個(gè)字節(jié),暫時(shí)不考慮,后續(xù)研究
            //  Logger.d("設(shè)置mtu結(jié)果 : ${gatt.requestMtu(BlePara.mtu)}"
            // 連接成功,開(kāi)始搜索service
            gatt.discoverServices()
        } else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
            // gatt連接斷開(kāi)
            Logger.d("onConnectionStateChange STATE_DISCONNECTED = $newState")
            gatt.close()
        }
    }
}
  1. 添加錯(cuò)誤重傳機(jī)制,重傳時(shí)間間隔增加
    發(fā)送分包時(shí)不可避免可能出錯(cuò),若默認(rèn)分包間隔為20ms,發(fā)送失敗后,可嘗試重傳一次,重傳時(shí)的時(shí)間間隔略微設(shè)定大些,如200ms,這樣仍能有效減小總發(fā)送時(shí)間;
var result = true // 發(fā)送數(shù)據(jù)是否成功
val delay = 20 // 分包之間的延時(shí),單位:毫秒
try {
    // 注意,這里需要延時(shí)一下,不然測(cè)試發(fā)現(xiàn),基本上只能收到其中幾幀的數(shù)據(jù),失敗的概率比較大
    Thread.sleep(delay.toLong())

    var i = 0
    while (i < size) {
        var to = i + 20
        if (to >= size) {
            to = size
        }
        val slice = Arrays.copyOfRange(encryptedContentBytes, i, to)
        relayChar.value = slice
        var sliceResult = gatt.writeCharacteristic(relayChar)
        Logger.d("傳送第 $i ~ $to 塊數(shù)據(jù)的結(jié)果: $sliceResult", TAG)
        // 發(fā)送失敗時(shí),嘗試重傳一次就好
        if (!sliceResult) {
            Thread.sleep(200)
            sliceResult = gatt.writeCharacteristic(relayChar)
            Logger.d(" =>重傳第 $i ~ $to 塊數(shù)據(jù)的結(jié)果: $sliceResult", TAG)
        }
        result = result and sliceResult
        i = to
        Thread.sleep(delay.toLong())
        // 由于只重傳一次, 因此如果某個(gè)數(shù)據(jù)分包重傳失敗,則不必要再傳后續(xù)數(shù)據(jù),直接返回失敗
        if (!result) {
            break
        }
    }
} catch (e: Exception) {
    e.printStackTrace()
    result = false
}

2.7 藍(lán)牙抓包,日志查看

之前跟iOS交互出錯(cuò)后,app層回調(diào)可看到的信息比較少, 查到的資料 又都說(shuō)有某個(gè)控制參數(shù)出錯(cuò), 沒(méi)發(fā)現(xiàn)characteristic設(shè)置有問(wèn)題前,就想著要抓包看看具體的參數(shù)交互, 未找到實(shí)時(shí)抓包的簡(jiǎn)單方法, 倒是可以通過(guò)Android手機(jī)的hcidump功能來(lái)獲取日志,然后通過(guò) wireshark 來(lái)查看:

  1. 查看hci日志文件路徑
// 我使用nexus 6p 7.1.1系統(tǒng),配置文件位于如下位置:
adb shell cat /etc/bluetooth/bt_stack.conf
// 文件中有一條配置信息,指示了log文件所在路徑
BtSnoopFileName=/sdcard/btsnoop_hci.log
  1. 抓取/導(dǎo)出hci日志
// 先清除原先的日志
adb shell rm /sdcard/btsnoop_hci.log 
//  通過(guò)手機(jī)系統(tǒng)打開(kāi)日志功能: settings-developer options -- enable bluetooth hci snoop log
// 抓取結(jié)束后,導(dǎo)出log文件到pc上
adb pull /sdcard/btsnoop_hci.log

不過(guò), 一開(kāi)始做ble沒(méi)經(jīng)驗(yàn),可以先下載些軟件來(lái)測(cè)試下ble功能,這里推薦一個(gè) nRF24L01 , 具體請(qǐng)參考 這篇文章, 好用, 搜索/連接/發(fā)送數(shù)據(jù)等功能一應(yīng)俱全, 寫(xiě)完 peripheral 模式后,用它測(cè)試下,確認(rèn)ok了,再來(lái)做central模式;

3. 參考資料

  1. BLE 官方文檔
  2. android ble常見(jiàn)問(wèn)題收集
  3. BLE開(kāi)發(fā)的各種坑
  4. ble address動(dòng)態(tài)變化
  5. wireshark bluetooth簡(jiǎn)要描述
  6. Debugging Bluetooth With An Android App
    介紹了款測(cè)試軟件,使用了,覺(jué)得不錯(cuò)...
  7. Android BLE中傳輸數(shù)據(jù)的最大長(zhǎng)度怎么破
    看完這篇才知道為啥單個(gè)分包20字節(jié),Android傳iOS單次最多可用512字節(jié)....,注意:需要在設(shè)備連接成功后再來(lái)設(shè)置,最大512,但是即使設(shè)置成功也沒(méi)法直接發(fā)送,需要在回調(diào) onMtuChanged() 顯示成功后,再寫(xiě)數(shù)據(jù)即可;
  8. Android BLE MTU調(diào)整
  9. 低功耗藍(lán)牙介紹
    介紹了hci日志中的 host / controller 含義,以及協(xié)議幀結(jié)構(gòu)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窄锅,一起剝皮案震驚了整個(gè)濱河市缩麸,隨后出現(xiàn)的幾起案子郑什,更是在濱河造成了極大的恐慌,老刑警劉巖圾叼,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怀偷,死亡現(xiàn)場(chǎng)離奇詭異家厌,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)椎工,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜀踏,“玉大人维蒙,你說(shuō)我怎么就攤上這事」玻” “怎么了颅痊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)局待。 經(jīng)常有香客問(wèn)我斑响,道長(zhǎng)菱属,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任舰罚,我火速辦了婚禮纽门,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘营罢。我一直安慰自己赏陵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布饲漾。 她就那樣靜靜地躺著蝙搔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪考传。 梳的紋絲不亂的頭發(fā)上吃型,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音僚楞,去河邊找鬼勤晚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛镜硕,可吹牛的內(nèi)容都是我干的运翼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼兴枯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼血淌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起财剖,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悠夯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后躺坟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體沦补,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年咪橙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了夕膀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡美侦,死狀恐怖产舞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情菠剩,我是刑警寧澤易猫,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站具壮,受9級(jí)特大地震影響准颓,放射性物質(zhì)發(fā)生泄漏哈蝇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一攘已、第九天 我趴在偏房一處隱蔽的房頂上張望炮赦。 院中可真熱鬧,春花似錦贯被、人聲如沸眼五。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)看幼。三九已至,卻和暖如春幌陕,著一層夾襖步出監(jiān)牢的瞬間诵姜,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工搏熄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棚唆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓心例,卻偏偏與公主長(zhǎng)得像宵凌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子止后,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,750評(píng)論 25 707
  • Key Terms And Concepts 關(guān)鍵術(shù)語(yǔ)和概念 Here is a summary of key B...
    Jaesoon閱讀 2,427評(píng)論 0 5
  • 安卓4.3(API 18)為BLE的核心功能提供平臺(tái)支持和API瞎惫,App可以利用它來(lái)發(fā)現(xiàn)設(shè)備、查詢服務(wù)和讀寫(xiě)特性译株。...
    風(fēng)雨byt閱讀 14,014評(píng)論 3 43
  • 拾荒者 “gege瓜喇,你看!”老婆用她一貫尖而又翠的聲音喊了起來(lái)歉糜,不得不說(shuō)乘寒,最近她很是熱心觀察周邊事物。 “怎么了匪补?...
    吃熊貓買(mǎi)的魚(yú)閱讀 215評(píng)論 1 2
  • 版本管理工具是干什么用的伞辛? 1)備份文件。就像U盤(pán)備份我們的文件一樣夯缺。我們的代碼也可以進(jìn)行備份始锚。每當(dāng)我們對(duì)代碼有修...
    廖馬兒閱讀 1,521評(píng)論 0 0