AppleWatch

前言

Apple Watch是苹果公司推出的一款比较成熟的智能手表,具有运动追踪、健康监测、消息推送、多媒体、游戏、定位等多种功能。Apple Watch需要配合iPhone手机使用,通过配对的iPhone访问应用商店进行第三方应用的下载和安装。watchOSApple Watch运行的操作系统,watchOS允许开发者使用Objective-CSwift来开发应用。Apple Watch的应用和功能的开发还处在挖掘和探索阶段,希望能以本篇文章为契机,为有兴趣的开发者提供一些参考。

watchOS概述

1.watchOS项目结构
如今的Apple Watch App都要求是原生应用,原生应用即是高于watchOS 2及以上的版本,并作为一个完整的应用包在Apple Watch上独立运行,下图所示的是watchOS App的结构图

watchOS App结构图.png

从上图可以看出Xcode项目包含三个部分分别是iOS AppWatchKit AppWatchKitExtentioniOS App负责iPhone端的所有运行内容,WatchKit App包含界面编辑和手表应用整体参数,WatchKitExtention包含watch端运行的代码及资源。WatchKitExtention包含在WatchKit App中,而手表端App和手机端App通过WatchConnectivity框架在iOSwatchOS之间进行通信。在watchOS 2之前,应用安装的时候,负责逻辑部分的WatchKitExtention将随iOS app的主target被一同安装到iPhone中,而负责界面部分的watchApp将会在主程序安装之后由iPhone检测有没有配对的Apple Watch并提示安装到watchApp中。如今的Apple Watch App是作为一个完整的应用包在Apple Watch上独立运行

2.建立watchOS App实例
打开xcode commond+ shift+ N新建一个project,选中watchOS下的iOS App with Watch App。项目建立成功之后可以看到如下

Xcode项目结构.png

从上图可以看出watchOS手表扩展包Extention包含四个文件
2.1.页面InterfaceController:继承WKInterfaceController,默认有三个函数,分别在不同的时机由系统调用;初始化时调用awake(withContext context: Any?);页面显示时调用willActivate(),失活状态调用didDeactivate()

2.2.扩展代理ExtensionDelegate:继承手表扩展代理WKExtensionDelegate,这与iOS中的AppDelegate有异曲同工之妙。应用启动完成后初始化时调用applicationDidFinishLaunching(),应用激活前台时调用applicationDidBecomeActive(),应用失活时调用applicationWillResignActive(),应用被系统后台启动时调用handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>)

2.3.通知界面NotificationController:手表接到通知时单击通知按钮显示的页面,通知页面有一个接到通知的处理函数didReceive(_ notification: UNNotification)默认该函数的标注是用状态,当要页面动态显示内容时才调用该函数。WatchKit的通知允许开发者自行构建界面,WatchKit App 接收到通知后先会显示一个简短的通知,告诉用户这个 app 有一个通知。如果用户对通知的内容感兴趣的话,可以点击或者抬手观看,这样由开发者自定义的长版本的通知就会显现.

2.4.表盘功能ComplicationController:负责表盘功能栏的设置,里面包含了多个函数分别对应显示功能栏的不同时机,其中最常用的直接设置表盘功能栏函数是getCurrentTimelineEntry,设置iPhone Watch里的功能栏示例的函数是getLocalizableSampleTemplate

3.WKInterfaceController的完整生命周期如下:

生命周期.png

页面的生命周期:当你进入一个页面时, 设备会经历init->awakeWithContext->willActivate->didAppear。当你退出当前页面时, 设备会经历willDisappear->didDeactivate->deinit

4.基础导航:
4.1.WKInterfaceController 的内建的导航关系基本上分为三类。首先是像 UINavigationController控制的类似栈的导航方式。相关的 API 有 - pushControllerWithName:context:-popController以及 -popToRootController。对于第一个方法,我们需要使用目标 controllerIdentifier字符串 (你只能在 StoryBoard 里进行设置) 进行创建。context 参数也会被传递到目标 controller 的 -initWithContext:中,所以你可以以此来在 controller 中进行数据传递。

栈导航.png

4.2.另一种是我们大家熟悉的 modal 形式,对应 API 是 -presentControllerWithName:context:-dismissController。对于这种导航,和 UIKit 中的不同之处就是在目标 controller 中会默认在左上角加上一个 Cancel 按钮,点击的话会直接关闭被 present 的 controller

4.3.最后一种导航方式是类似 UIPageController 的分页式导航.在实现上,page 导航需要在 StoryBoard 中用 segue 的方式将不同 page 进行连接,新添加的 next page segue 就是干这个的

Page-based.png

布局与适配

Watch app 的布局和 iOS 的布局完全不同。你无法自由指定某个视图的具体坐标,当然也不能使用AutoLayout或者SizeClasses这样灵活的界面布局方案。WatchKit提供的布局可能性和灵活性相对较小,你只能在以"行"为基本单位的同时通过Group来在内进行“列”布局。首先所有的WKInterfaceObject对象都必须要设计的时候经storyboard进行添加,运行时我们无法再向界面上添加或者移除元素,如果有移除的可以使用隐藏。基本来说在运行时我们只能够改变视图的内容,以及通过隐藏某些元素来达到有限改变布局的效果。

Group: WatchOS中的一个很特别的类, 它是一个容器性质的控件, 能为其他控件提供额外的布局。可以指定其所包含控件的排列方向, 横向或者纵向或者重叠, 也可以设置间距和内嵌。它还能为自己添加背景图片, 作为一个种控件叠加的效果这是一个不错的选择, 因为在 watchOS中是不允许控件相互重叠的, 除了像Group这样容器类的控件

Assets:在WatchKit AppWatchKitExtention都可以放置图片资源,只是加载的方式不同,放在WatchKit App Assets.xcassets里面的图片要用setImageNamed(_ imageName: String?)加载显示,而在WatchKitExtention里面的图片需要使用setImage(_ image: UIImage?)来加载。推荐放在WatchKit App Assets.xcassets

icon设计:您必须提供在iPhoneApple Watch主屏幕上均可使用的图标资源,同时该图标为不含透明alpha通道的png图。官方给出的尺寸标准戳这里

证书配置

证书的配置是开发过程中必不可少的一个环节,虽然Xcode也可以自动配置证书生成App ID,但是作为一个开发人员还是有必要了解下证书配置流程。下面简单的记录了AppleWatch开发过程中需要用到的证书以及App ID配置,根据项目需要自行选择是否加入App Groups
1.准备App ID
App IDcom.**.**
watch AppIDcom.**.**.watchkitapp
watch extension AppID: com.**.**.watchkitapp.watchkitextension

App ID规范.png

2.准备provisioning profiles,给watch AppIDwatch extension AppID配置开发和生产的provisioning profiles
开发和生产provisioning profiles如下:

描述文件.png

基本控件的使用

1.WKInterfaceLabelWKInterfaceButtonWKInterfacePickerWKInterfaceTableWKInterfaceSwitchWKInterfaceSlider等在iOS中也有,这里就不赘述

2.WKInterfaceImage:关于图片的使用有一个坑需要注意, 当我们为其添加图片时, 可能会遇到图片不显示的问题。这是因为所使用的方法和图片资源库是有一定的关系的。当使用setImageNamed:setBackgroundImageNamed:方法添加图片时, 应该使用 Watch App包内Assets.xcassets中的已有的图片资源;当使用setImage:setImageData:setBackgroundImage:setBackgroundImageData:方法添加图片时, 应该使用 WatchKit Extension包内Assets.xcassets中的图片资源。使用后者方式时, 会先在WatchKit Extension中创建Image, 然后再传输到 WatchKit App中进行显示

3.WKInterfaceTable:可以显示多行类型相同的内容,每一行的内容都需要在代码中设置,并且还要自定义行的标识identifier和类,该类继承于NSObject,最后设置表格的行数和每一行的序列号和内容,如果显示类型不同的内容,可以使用setRowTypes(_ rowTypes: [String])来指定不同的类型的标识identifier

   table.setNumberOfRows(dataArray.count, withRowType: "ItemRowController")
   for (i, info) in dataArray.enumerated() {
         let cell = table.rowController(at: i) as! ItemRowController
         cell.titleLabel.setText(info["title"])
         cell.image.setImageNamed(info["image"])
     }

4.Context Menu:另一个比较好玩的是 Context Menu,这是 WatchKit 独有的交互,在 iOS 中并不存在。在任意一个WKInterfaceController 界面中,长按手表屏幕,如果当前 WKInterfaceController 中存在上下文菜单的话,就会尝试呼出找这个界面对应的 Context Menu。这个菜单最多可以提供四个按钮,用来针对当前环境向用户征询操作。添加 Context Menu 非常简单,在 StoryBoard里向 WKInterfaceController 中添加一个 Menu,并在这个 Menu 里添加对应的 MenuItem 就行了。在 WKInterfaceController 我们也有对应的 API 来在运行时根据上下文环境进行 MenuItem的添加 (这是少数几个允许我们在运行时添加元素的方法之一)

-addMenuItemWithItemIcon:title:action:
-addMenuItemWithImageNamed:title:action:
-addMenuItemWithImage:title:action:
-clearAllMenuItems
Context Menu.png

5.WKGestureRecognizer: Tap LongPress Pan Swipe

6.Alert: WKAlertControllerStyle有三种类型,分别是alert, sideBysideButtonAlertactionSheet,样式如下

alert 类型.png

7.WKInterfacePickerWKInterfacePickerstylelist, stack, sequence三种(具体样式见下图)(titleaccessoryImage只有在类型为list才有用)Focus Style属性也有三种,分别是None Outline outline with Caption;同时系统也提供获取当前选项序列号value的点击事件.使用选择器时需要预先设置显示的选择项,选择项为WKPickerItem,每一项可以包括title和图标contentImage,使用listPicker.setItems(itemArray)赋值。

image.png

与iPhone交互

WatchOS提供框架WatchConnectivity进行WatchiPhone之间的数据交换,支持后台传输和前台传输。WatchConnectivity提供了一个WCSesion对象,通过WCSession进行数据传输。配置WCSession --> 判断连接状态 -->数据传输
1.准备工作:手表和手机配对,如果单独从模拟器中启动【iPhone 模拟器】 和 【Apple Watch 模拟器】是不能配对的,正确的配对方式是
1.1. 在对应的模拟器中添加配对的手表

模拟器配对.png

1.2. 启动iPhone模拟器
1.3. 启动手表模拟器
1.4. 打开iPhone模拟器上的手表应用就可以看到已经配对了
image.png

2.配置WCSession:在Watch Extention端和iPhone端都要先找到默认的WCSession对象,并设置代理,激活activate

3.判断连接状态: 在输出数据时需要判断WatchiPhone的连接状态,WCSession提供了如下状态是否配对isPaired、手表端应用是否安装isWatchAppInstalled、两者间消息是否能相通isReacheable。当Watch已经配对且Watch端应用安装好时,可以进行后台传输数据;当两者isReacheable = true时,可以直接进行前台数据传输。

4.数据传输: WatchKit ExtentioniPhone间通信的方式有很多种,可以分为前台实时传输和后台不定时传输两大传输类型。前台传输,是实时传输<消息字典传输、消息数据传输>,后台传输又分为覆盖式传输、队列式传输<字典传输、文件传输、表盘数据传输>

通信方式.png

5.前台消息字典传输 :从 WatchKit Extension激活并运行时调用此方法会在后台唤醒相应的 iOS App并使其可访问,但若从 iOS App调用此方法则不会唤醒相应的WatchKit Extension。判断信息可达isReachable,对于 WatchKit Extension来说, iOS设备在范围内, 并且 WatchKit Extension在前台运行。对于 iOS来说, 配对且激活的 Apple Watch在范围内, 相应的WatchKit Extension正在运行。只要这样isReachable属性才会为true。<当传输的消息字典中包含非属性列表数据类型, 也会调用errorHandlerblock>

6.前台消息数据传输:此方法与消息字典传输的方法的区别在于所传输的主体内容为Data类型。包含非属性列表数据类型的传输

7.后台覆盖式传输:后台传输不适合数据立即传输,而是当具备数据传输连接条件以后watchiPhone之间自动同步数据,所以后台传输的数据是异步传输的,具有延后性 覆盖式后台传输时使用的方法是updateApplicationContext,当第一次发送的数据还没有传送出去时,如果此时进行第二次数据传输,会覆盖第一次的数据,而真正传输的是第二次的数据,第一次的数据会丢失。当接收完毕时会调用代理WCSessionDelegate的方法session: didReceiveApplicationContex:,系统会把接收的数据存放在WCSession的属性receivedApplicationContext中以供我们需要时读取

8.后台队列式字典传输: 此方法可以传输一个字典, 系统将userInfo字典按序排入队列, 并在适当的时候将其传输到接收方应用中。你还可以通过outstandingUserInfoTransfers属性来获取仍在传输中(即未被接收方取消, 失败或已接收)的userInfo数组。

9.后台队列式文件传输:此方法可以传输一个文件和一个可选字典, 且只有在Session处于激活状态时才能调用此方法。你还可以通过outstandingFileTransfers属性来获取仍在传输中(即未被接收方取消, 失败或已接收)的userInfo数组。

10.后台队列式表盘数据传输:此方法涉及到WatchOS的表盘功能也就是Complication功能, 且只适用于iPhoneWatchKit Extension发送表盘功能相关的数据。此方法将包含表盘功能的最新信息的字典userInfo排入队列中

11.以下展示了手表端和手机端如何通过WatchConnectivity框架进行前台数据传输

   // 手表端
    WCSession.default.sendMessage(message, replyHandler: { (replyMessage) in
        print("回调2 replyMessage = \(replyMessage)")
        DispatchQueue.main.sync {
            self.receiveLab.setText(replyMessage["replyContent"] as? String)
        }
    }) { (error) in
        print(error.localizedDescription)
    }

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        print(message)
        replyHandler(["title": "received successfully", "replyContent": "This is a reply from watch"])
        DispatchQueue.main.sync {
            contentLabel.setText(message["iPhoneMessage"] as? String)
        }
    }
  // 手机端
    guard WCSession.default.isReachable else {
        let alert = UIAlertController(title: "Failed", message: "Apple Watch is not reachable.", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
        alert.addAction(okAction)
        present(alert, animated: true, completion: nil)
        return
    }
        
    let message = ["title": "iPhone send a message to Apple Watch", "iPhoneMessage": message]
    WCSession.default.sendMessage(message, replyHandler: { (replyMessage) in
        print("回调1 = \(replyMessage)")
        DispatchQueue.main.sync {
            self.receiveLabel.text = replyMessage["replyContent"] as? String
        }
    }) { (error) in
        print(error.localizedDescription)
            
    }

     func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        print(message)
        replyHandler(["title": "received successfully", "replyContent": "This is a reply from iPhone"])
        DispatchQueue.main.sync {
            receiveLabel.text = message["watchMessage"] as? String
        }
    }

表盘功能栏

ComplicationswatchOS 2 新加入的特性,它是表盘上除了时间以外的一些功能性的小部件官方文档给出了所有的模板类型示意图

根据用户表盘选择的不同,表盘上对应的可用的 complications 形状也各不相同。如果你想要你的 complication 在所有表盘上都能使用的话,你需要实现所有的形状。掌管 complications 或者说是表盘相关的框架并不是我们一直使用的WatchKit,而是一个 watchOS 2 中全新框架ClockKitClockKit会提供一些模板给我们,并在一定时间点向我们请求数据。我们依照模板使用我们的数据来实现 complication,最后 ClockKit负责帮助我们将其渲染在表盘上。在 ClockKit请求数据时,它会唤醒我们的 watch extension。我们需要在 extension 中实现数据源,并以一段时间线的方式把数据提供给 ClockKit。这样做有两个好处,首先 ClockKit 可以一次性获取到很多数据,这样它就能在合适的时候更新complication 的显示,而不必再次唤醒 extension 来请求数据。其次,因为有一条时间线的数据,我们就可以使用 Time Travel 来查看 complication 已经过去的和即将到来的状况,这在某些场合下会十分方便。

getCurrentTimelineEntryForComplication:withHandler:,我们需要通过这个方法来提供当前表盘所要显示的 complicationgetTimelineStartDateForComplication:withHandler:getTimelineEndDateForComplication:withHandler: 来告诉系统我们所能提供 complication 的日期区间另外,我们还可以通过实现getLocalizableSampleTemplate:withHandler:来提供一个在表盘定制界面是会用到的占位图像

complication.png

通知

当手机收到应用通知时,如果手机息屏手表就会收到通知推送,当前手表必须连接蓝牙或WiFi。通知页面分为静态和动态两种,可以在storyboard页面中见到,同时可以在NotificationController的方法didReceive()中设置动态内容

多媒体

从2.0开始,watchOS开放了多媒体API,包括前台录音,无线播放音频,视频播放器,其中视频播放器使用喇叭外放声音
1.录音:录音文件的扩展名只能取.wav .mp4 .m4a,若设置成其他的则会报错。录音文件要保存在组目录中,记录保存的地址,以供以后播放
2.无线播放音频:采取无线蓝牙播放时系统会提示AirPlay确认连接,播放时依次按照音频文件URLAVAssetAVPlayerItem的顺序进行构建
3.视频播放: 支持播放音频和视频,这里使用视频播放器实现。视频播放器不支持在线视频

运动传感器和GPS

运动.png

Apple Watch配备了加速计和陀螺仪,我们可以通过CMMotionManager来获取相关上述两种传感器的监控数据
1.加速计: 负责检测运动的加速度值,分为XYZ三个方向,当单独启动加速计时,重力加速度会默认附加在Z方向,值为-1.0。启动加速计之前,先要设定加速计的监控刷新时间间隔accelerometerUpdateInterval,该间隔确定了多久刷新一次数据。获取数据分为两种方式,如果要在获取数据之后立即处理,就使用函数回调的方式startAccelerometerUpdates(to queue: OperationQueue, withHandler handler: @escaping CMAccelerometerHandler),此方法会在后台加速计获取新数据后立即执行withHandler;如果只是在需要的时候查询数据,需要读取CMMotionManager的属性accelerometerData来获取加速数据。检测设备是否支持加速计,可用CMMotionManager的属isAccelerometerActive

2.陀螺仪:Gyroscope负责检测旋转运动的旋转速度,同加速器启动方式相同

3.地磁仪: 可以检测磁场强度,但是支持的设备比较少,所以在启用前先查看isMagnetometerAvailable检测本设备是否支持,同加速器启动方式相同。先设置间隔magnetometerUpdateInterval再调用启动方法startMagnetometerUpdates(to queue: OperationQueue, withHandler handler: @escaping CMMagnetometerHandler)

4.设备运动: 启动设备运动Device motion就表示将设备上所有的运动传感器全部启动,可以获取所有的运动数据,包括重力加速度gravity、用户加速度userAcceleration,旋转角度attitude,旋转速率rotationRate,磁场magneticField

5.运动姿势识别: CoreMotion框架还包括对设备运动姿势MotionActivity的识别,静止stationary、步行walking、跑步running、汽车automotive、自行车cycling的真假状态。通过CMMotionActivityManagerstartActivityUpdates(to queue: OperationQueue, withHandler handler: @escaping CMMotionActivityHandler)方法启动刷新检测

6.GPS和定位: 可以调用手表中的CoreLocation来获取位置信息,与iOS中的相同,根据定位服务的使用方式,需要在info.plist配置相应的权限描述字段,使用方式与手机相同

健康

健康.png

AppleWatch提供强大和全面的健康监测功能,如心率、步数、活动能量消耗等,同时AppleWatch会将监测到的健康数据先存储在Watch端的健康库中;然后略微延迟的发送到iPhone端的健康库中,我们打开iPhone上的健康应用就能看到检测到的各种数据;最后等过一段时间大概是一天之后,Watch端会将旧的数据自动删除以便节省存储空间来存储新数据,通过函数earliestPermittedSampleDate()可以获取Watch端保存的最旧数据的时间。健康是一个系统级的框架HealthKit。该框架涉及两个应用--“健康”和"健身记录",其中大部分数据监测都可在"健康"中访问,而“健身记录”可以访问有关"体能训练"的数据。

健康框架HealthKit具有一个健康库HealthStore,此HealthStore负责健康数据的存储、管理和访问,同时也负责健康管理器的管理工作。HealthStore存储的数据包括人体特性数据 样本数据 和病例

1.人体特征数据: 生日/年龄、血型、性别。特征数据Characteristic data可以通过HealthStore直接获得,不必经过复杂查询操作
2.样本数据HKSample: 心率、行走步数、能量消耗等。HKSample具有对应的开始时间、结束时间和数据类型。同时分为四个子类
2.1.样本类别HKCatagorySample:以状态变化为标志的数据,如睡眠和醒着两种状态
2.2.数量样本HKQuantitySample:数值表示如心率为65bmp,涉及对应数量单位HKUnit,如米(HKUnit.meter())、次/分钟(HKUnit(from: "count/min")
2.3.关联样本HKCorrelation:不同数据关联起来放在一起,如心率和能量消耗两个数据关联的样本
2.4.体能训练HKWorkout:运动类型、位置、起始时间、距离、能量消耗是HKWorkout必须要包含的基本数据,步数、平均心率、metaData等其他是额外附加数据。HKWorkout通过Watch端的体能训练作业HKWorkoutSession来检测,HKWorkoutSession可以在手表端创建和激活,激活后Watch的健康传感器就会开始工作(耗电较多),全面检测步数、能量消耗、心率等数据,并且将自动检测到的数据以HKQuantitySample的形式存储起来并发送到手机端。我们需要手动将体能训练HKWorkout及其相关的数据保存到健康库HealthStore中,存储的体能训练可以在手机“健身记录”里查看
2.5. 样本数据类型:为了区别各种数据的不同类型,健康库HealthStore通过健康数据类型HKObjectType来表示数据的类型,而HKObjectType又是通过响应的类型标识identifier来确定的。官方文档介绍戳这里
2.6.病例:健康库HealthStore可以通过第三方应用添加病例文件CDA,用户可以通过“健康”应用查看和管理病例文件病例主要通过手机操作
2.7. 加载健康: Apple产品使用健康功能是通过HealthKit Framework实现,所以需要再xcode项目中的Capabilities开启HealthKit即可。Apple Watch检测健康数据涉及到个人隐私,所以需要申请相应的权限,在info.plist中添加键值对NSHealthUpdateUsageDescription NSHealthShareUsageDescription
2.8.后台模式: 手表在使用时很可能会黑屏或者切换到时间表盘,为了能够持续不断的检测,应用需要开启后台运行模式。在xcodeextention项目的Capabilities打开background modes,勾选workout processing

写在最后

以上章节的实例代码Demo戳这里
Apple Watch为代表的智能穿戴产品虽然远远不如手机那么普及,但是随着设备的进一步成熟和 SDK 的更加开放,Apple Watch开发也逐渐趋于稳定。我想我们最好能不断跟紧watch开发的脚步,尽量多的积累,这样才会在以后的道路上取得先机。

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