系统推送的集成(十四) —— 发送和处理推送通知流程详解(一)

版本记录

版本号 时间
V1.0 2018.12.07 星期五

前言

我们做APP很多时候都需要推送功能,以直播为例,如果你关注的主播开播了,那么就需要向关注这个主播的人发送开播通知,提醒用户去看播,这个只是一个小的方面,具体应用根据公司的业务逻辑而定。前面已经花了很多篇幅介绍了极光推送,其实极光推送无非就是将我们客户端和服务端做的很多东西封装了一下,节省了我们很多处理逻辑和流程,这一篇开始,我们就利用系统的原生推送类结合工程实践说一下系统推送的集成,希望我的讲解能让大家很清楚的理解它。感兴趣的可以看上面几篇。
1. 系统推送的集成(一) —— 基本集成流程(一)
2. 系统推送的集成(二) —— 推送遇到的几个坑之BadDeviceToken问题(一)
3. 系统推送的集成(三) —— 本地和远程通知编程指南之你的App的通知 - 本地和远程通知概览(一)
4. 系统推送的集成(四) —— 本地和远程通知编程指南之你的App的通知 - 管理您的应用程序的通知支持(二)
5. 系统推送的集成(五) —— 本地和远程通知编程指南之你的App的通知 - 调度和处理本地通知(三)
6. 系统推送的集成(六) —— 本地和远程通知编程指南之你的App的通知 - 配置远程通知支持(四)
7. 系统推送的集成(七) —— 本地和远程通知编程指南之你的App的通知 - 修改和显示通知(五)
8. 系统推送的集成(八) —— 本地和远程通知编程指南之苹果推送通知服务APNs - APNs概览(一)
9. 系统推送的集成(九) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 创建远程通知Payload(二)
10. 系统推送的集成(十) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 与APNs通信(三)
11. 系统推送的集成(十一) —— 本地和远程通知编程指南之苹果推送通知服务APNs - Payload Key参考(四)
12. 系统推送的集成(十二) —— 本地和远程通知编程指南之Legacy信息 - 二进制Provider API(一)
13. 系统推送的集成(十三) —— 本地和远程通知编程指南之Legacy信息 - Legacy通知格式(二)

开始

首先看一下写作环境

Swift 4.2, iOS 12, Xcode 10

在此推送通知教程中,您将学习如何使用操作发送和处理推送通知。

iOS开发人员喜欢想象用户经常使用他们的精彩应用程序。但是,当然,用户有时必须关闭应用程序并执行其他活动。令人高兴的是,推送通知允许开发人员访问用户并执行小任务 - 即使用户没有主动使用应用程序!在本教程中,您将学习如何配置应用程序以接收推送通知并将其显示给用户。

推送通知(Push notifications)自首次推出以来变得更加强大。使用iOS 12,推送通知可以:

  • 显示简短的文本信息。
  • 播放通知声音。
  • 在应用程序的图标上设置badge number
  • 提供用户无需打开应用即可执行的操作。
  • 显示媒体附件。
  • 保持沉默,允许应用程序在后台执行任务。
  • 将通知分组到线程中。
  • 编辑或删除已发送的通知。
  • 在显示之前运行代码以更改通知。
  • 显示通知的自定义交互式UI。

本教程涵盖了其中许多内容。要完成本教程,您需要以下内容来测试推送通知:

  • An iOS device:推送通知在模拟器中不起作用,因此您需要一个实际的设备。
  • An Apple Developer Program membership:要发送推送通知,您需要一个针对您的App ID的推送通知证书,这需要一个program membership
  • PushNotifications:您将使用此工具应用程序向测试设备发送通知。 要安装它,请按照How to install的说明进行操作。

要发送和接收推送通知,您必须执行三个主要任务:

  • 1) 配置您的应用程序并使用Apple Push Notification service (APNs)进行注册。
  • 2) 通过APNs从服务器向特定设备发送推送通知。
  • 3) 在应用程序中使用回调来接收和处理推送通知。

发送推送通知是应用服务器组件的责任。许多应用程序使用第三方发送推送通知,而其他应用程序使用自定义解决方案或流行的库(例如,Houston)。您将只在本教程中触及发送推送消息,主要是为了测试应用程序。

要开始使用,请使用已有的WenderCast启动项目。

WenderCast-Starter文件夹中,使用Xcode打开WenderCast.xcodeproj。在Project导航器中选择WenderCast,然后选择WenderCast目标。在General选项卡中,找到Signing部分,然后选择您的development Team。在您的设备上构建并运行:

WenderCast显示播客列表,并允许用户播放它们。 问题是它不会让用户知道新的播客何时可用且News tab是空的! 您很快就可以通过推送通知的功能解决这些问题。


Sending and Receiving Push Notifications - 发送和接收推送通知

1. Configuring the App - 配置应用程序

安全性对于推送通知非常重要,因为您不希望任何其他人通过您的应用向您的用户发送推送通知。 您需要执行多项任务来配置应用以安全地接收推送通知。

2. Enabling the Push Notification Service - 启用推送通知服务

首先,更改App ID。 在Xcode中,在Project导航器中突出显示WenderCast,然后选择WenderCasttarget。 选择General,然后将Bundle Identifier更改为唯一的,以便Apple’s push notification server可以直接推送到此应用程序。

接下来,您需要在开发人员帐户中创建App ID并启用push notification entitlement。 Xcode有一个简单的方法:在仍然选择WenderCasttarget的情况下,单击Capabilities选项卡并将Push Notifications开关设置为On

加载后,它应如下所示:

注意:如果出现任何问题,请访问Apple Developer Center。 您可能需要同意Apple喜欢更新的新开发者许可证,然后再试一次。

然后,这将创建App ID,然后向其添加push notifications entitlement。 您可以登录Apple开发人员中心进行验证:

这就是你现在需要配置的全部内容。

3. Asking for User Notifications Permission - 请求用户通知权限

注册推送通知需要两个步骤。 首先,您必须获得用户显示通知的权限。 然后,您可以注册设备以接收远程(推送)通知。 如果一切顺利,系统将为您提供device token,您可以将其视为此设备的“地址”。

WenderCast中,您将在应用程序启动后立即注册推送通知。 首先询问用户权限。

打开AppDelegate.swift并将以下内容添加到文件的顶部:

import UserNotifications

然后,将以下方法添加到AppDelegate的末尾:

func registerForPushNotifications() {
  UNUserNotificationCenter.current() // 1
    .requestAuthorization(options: [.alert, .sound, .badge]) { // 2
      granted, error in
      print("Permission granted: \(granted)") // 3
  }
}

看一下上面的代码:

  • 1) UNUserNotificationCenter处理应用程序中与通知相关的所有活动。
  • 2) 您调用requestAuthorization(options:completionHandler :)到请求授权以显示通知。 传递的options表示您希望应用使用的通知类型 - 在这里您要求提供alert, sound and badge
  • 3) completion处理程序接收Bool,指示授权是否成功。 在这种情况下,您只需打印结果。

注意:传递给requestAuthorization(options:completionHandler:)options可以包含UNAuthorizationOptions的任意组合:

  • .badge:在应用程序图标的角落显示一个数字。
  • .sound:播放声音。
  • .alert:显示文字。
  • .carPlay:在CarPlay中显示通知。
  • .provisional:发布非中断通知。 如果您仅使用此选项,则用户将不会收到权限请求,但您的通知只会在通知中心中静默显示。
  • .providesAppNotificationSettings:表明应用程序有自己的用于通知设置的UI。
  • .criticalAlert:忽略静音开关和“请勿打扰(Do Not Disturb)”。 您需要Apple提供的特殊权利才能使用此选项,因为它仅在绝对必要时使用。

return之前,在application(_:didFinishLaunchingWithOptions:)结束时添加以下内容:

registerForPushNotifications()

在此处调用registerForPushNotifications()可确保应用程序在其启动时尝试注册推送通知。

建立并运行。 当应用程序启动时,您应该收到一个提示,要求获得向您发送通知的权限。

点击Allow! 该应用现在可以显示通知。 很好! 但是如果用户拒绝权限呢? 在AppDelegate中添加此方法:

func getNotificationSettings() {
  UNUserNotificationCenter.current().getNotificationSettings { settings in
    print("Notification settings: \(settings)")
  }
}

首先,您指定了所需的设置。 此方法返回用户已授予的设置。 现在你正在打印它们,但你很快就会回到这里。

registerForPushNotifications中,使用以下命令替换对requestAuthorization(options:completionHandler :)的调用:

UNUserNotificationCenter.current()
  .requestAuthorization(options: [.alert, .sound, .badge]) {
    [weak self] granted, error in
      
    print("Permission granted: \(granted)")
    guard granted else { return }
    self?.getNotificationSettings()
}

您已在完成处理程序中添加了对getNotificationSettings的调用。 这很重要,因为用户可以随时进入“Settings”应用并更改其通知权限。 在未授予许可的情况下,guard避免进行了此调用。

4. Registering With APNs - 注册APNs

既然您拥有权限,那么您现在实际上会注册远程通知!

getNotificationSettings()中,在闭包内的print中添加以下内容:

guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
  UIApplication.shared.registerForRemoteNotifications()
}

在这里,您验证authorizationStatus.authorized:用户已授予通知权限。 如果是这样,您调用UIApplication.shared.registerForRemoteNotifications()以启动Apple Push Notification service的注册。 您需要在主线程上调用它,否则您将收到运行时警告。

将以下两种方法添加到AppDelegate的末尾。 iOS会调用这些来通知你registerForRemoteNotifications()的结果:

func application(
  _ application: UIApplication,
  didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
  let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
  let token = tokenParts.joined()
  print("Device Token: \(token)")
}

func application(
  _ application: UIApplication,
  didFailToRegisterForRemoteNotificationsWithError error: Error) {
  print("Failed to register: \(error)")
}

注册完成后,如果成功,iOS会调用application(_:didRegisterForRemoteNotificationsWithDeviceToken :)。 如果没有,则调用application(_:didFailToRegisterForRemoteNotificationsWithError :)

application(_:didRegisterForRemoteNotificationsWithDeviceToken:)的当前实现看起来很神秘,但它只是将deviceToken转换为字符串。 deviceToken是此过程的成果。 它由APNs提供,并在此特定设备上唯一标识此应用程序。 发送推送通知时,服务器将deviceToken用作“地址”以传送到正确的设备。 在您的应用程序中,您现在可以将此token发送到您的服务器,以便它可以保存并稍后用于发送通知。

在设备上构建并运行。 您应该在控制台输出中收到一个token。 这是一个例子:

复制此token,您将需要它进行测试。

注意:注册可能失败的原因有多种。大部分时间是因为应用程序在模拟器上运行,或者因为App ID未正确配置。错误消息通常提供了一个错误的好提示。

5. Creating an Authentication Key - 创建验证密钥

在发送推送通知之前,您还需要做一些配置。前往Apple Developer Member Center并登录。

自Apple推出身份验证密钥(Authentication Keys)以来,发送推送通知变得更加容易。以前,您必须按照复杂的过程为每个需要发送通知的应用创建推送通知证书。现在,您只需创建一个密钥(key),您的任何应用程序都可以使用该密钥。

在会员中心,选择Certificates, Identifiers & Profiles,然后在左侧窗格中查找Keys ▸ All。在右上角,单击+按钮以创建新密钥。

为密钥指定名称,例如Push Notification Key。在Key Services下,选择Apple Push Notifications service (APNs)

单击Continue,然后在下一个页面上Confirm以创建新密钥。 点按Download。 下载的文件将命名为AuthKey_4SVKWF966R.p8。 跟踪此文件 - 您需要它来发送您的通知! 文件名的4SVKWF966R部分是Key ID。 你也需要这个。

您需要的最后一块是您的Team ID。 导航到成员中心的Membership Details页面以查找它。

这要经过很多努力,但这一切都是值得的。 使用新密钥,您现在可以发送第一个推送通知!


Sending a Push Notification - 发送推送通知

如果您尚未下载PushNotifications实用程序,现在是时候了。

启动PushNotifications并完成以下步骤:

  • 1) 在Authentication下,选择Token
  • 2) 单击Select P8按钮,然后从上一节中选择.p8文件。
  • 3) 在相关输入框中输入您的Key IDTeam ID
  • 4) 在Body下,输入您应用的Bundle ID和您的device token
  • 5) 将请求体更改为如下所示:
{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com"
  }
}
  • 6) 在您的测试设备后台应用程序或锁定设备。
  • 7) 单击PushNotifications中的Send按钮。

您应该收到第一个推送通知:

注意:如果应用程序在前台打开并运行,您将看不到任何内容。 通知已发送,但应用程序中没有任何内容可以处理它。 首先,关闭应用并再次发送通知。

1. Common Issues - 常见问题

您可能会遇到几个问题:

  • Some notifications are received but not all - 收到了一些通知但并非全部:如果您同时发送许多推送通知,而您只收到一些通知,请不要担心! 这是预期的行为。 APNs为每个设备维护QoS(服务质量)队列。 此队列的大小为1,因此如果您发送多个通知,则会覆盖最后一个通知。
  • Problem connecting to Push Notification Service - 连接到推送通知服务的问题:一种可能是防火墙阻塞了APNs使用的端口。 确保取消阻止这些端口。

2. Anatomy of a Basic Push Notification - 基本推送通知的剖析

在继续处理推送通知之前,请先查看您发送的通知正文:

{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com"
  }
}

有效负载(payload)是一个JSON字典,至少包含一个项目,aps,它本身也是一个字典。在此示例中,aps包含字段alertsoundlink_url。当设备收到此推送通知时,它会显示带有“Breaking News!”文本的alert视图,并播放标准音效。

link_url实际上是一个自定义字段。您可以像这样将自定义字段添加到有效负载中,它们将被传递到您的应用程序。由于您尚未在应用程序内部处理它,因此此键/值对当前不执行任何操作。

您可以添加到aps词典中的七个内置键:

  • alert:这可以是一个字符串,就像前面的例子一样,或者字典本身。作为字典,它可以本地化文本或更改通知的其他方面。
  • badge:这是一个将显示在应用程序图标角落的数字。您可以通过将其设置为0来删除badge
  • sound:位于应用程序中的自定义通知声音文件的名称。自定义通知声音必须短于30秒,并有一些限制。
  • thread-id:使用此键对通知进行分组。
  • category:定义通知的类别,用于显示通知的自定义操作。您很快就会探索这个。
  • content-available:通过将此键设置为1,推送通知变为静默。您将在Silent Push Notifications部分中了解此信息。
  • mutable-content:通过将此键设置为1,您的应用可以在显示之前修改通知。

除此之外,只要有效负载不超过4,096字节,您就可以根据需要添加任意数量的自定义数据。

一旦您有足够的乐趣将推送通知发送到您的设备,请转到下一部分!


Handling Push Notifications - 处理推送通知

在本部分中,您将学习如何在应用收到通知以及用户点击通知时执行操作。

1. What Happens When You Receive a Push Notification?收到推送通知后会发生什么?

当您的应用收到推送通知时,iOS会调用UIApplicationDelegate中的方法。

您需要以不同的方式处理通知,具体取决于您的应用收到时的状态:

  • 如果您的应用未运行且用户通过点击推送通知启动它,则推送通知将传递给您的application(_:didFinishLaunchingWithOptions:)launchOptions中。
  • 如果您的应用程序在前台或后台运行,系统会通过调用应用程序来通知您的application(_:didReceiveRemoteNotification:fetchCompletionHandler:)。 如果用户通过点击推送通知打开应用程序,iOS可能会再次调用此方法,因此您可以更新UI并显示相关信息。

在第一种情况下,WenderCast将创建news item并直接打开News tab。 将以下代码添加到application(_:didFinishLaunchingWithOptions:)的末尾,就在return语句之前:

// Check if launched from notification
let notificationOption = launchOptions?[.remoteNotification]

// 1
if let notification = notificationOption as? [String: AnyObject],
  let aps = notification["aps"] as? [String: AnyObject] {
  
  // 2
  NewsItem.makeNewsItem(aps)
  
  // 3
  (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
}

这段代码做了三件事:

  • 1) 它检查launchOptions中是否存在UIApplication.LaunchOptionsKey.remoteNotification的值。 如果是,那么您的应用程序是从通知中启动的。 这将是您发送的推送通知有效负载。
  • 2) 如果它存在,它抓取aps字典并创建一个包含其内容的NewsItem
  • 3) 它将tab controller的选定的tab更改为News部分。

要测试这个,你需要编辑WenderCastscheme

单击WenderCastscheme并选择Edit Scheme .... 从侧栏选择Run,然后在Info选项卡中选择Wait for executable to be launched

此选项将使调试器等待安装后第一次启动应用程序以附加到该应用程序。

构建并运行。 安装完成后,发送更多新闻。 点按通知,应用应该打开新闻:

注意:如果您停止接收推送通知,则可能是您的设备令牌已更改。 如果卸载并重新安装应用程序,则会发生这种情况。 仔细检查device token以确保。

要处理接收推送通知的其他情况,请将以下方法添加到AppDelegate

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable: Any],
  fetchCompletionHandler completionHandler:
  @escaping (UIBackgroundFetchResult) -> Void
) {
  guard let aps = userInfo["aps"] as? [String: AnyObject] else {
    completionHandler(.failed)
    return
  }
  NewsItem.makeNewsItem(aps)
}

此方法根据推送消息的内容创建新的NewsItem

请记住,在应用程序运行时会调用此方法。 将scheme更改回自动启动应用程序。 在Scheme编辑器中的Launch下,选择Automatically

构建并运行。 让应用程序在前台和News tab上运行。 发送另一个新闻推送通知并观看Feed中显示的内容:

您的应用程序现在可以神奇地收到突发新闻。

Something important consider:很多时候,可能会错过推送通知。 这对于WenderCast来说是可以的,因为拥有完整的新闻列表对于这个应用程序来说并不重要。 通常,您不应将推送通知用作传递内容的唯一方式。

相反,推送通知应该表明存在可用的新内容,并让应用程序从源(例如,从REST API)下载内容。 WenderCast在这个意义上有点受限,因为它没有真正的服务器端组件。


Actionable Notifications - 可操作的通知

可操作通知允许您向通知本身添加自定义按钮。 您可能已经在电子邮件通知或推文中发现了这一点,让您现场“reply” or “favorite”

当您使用categories注册通知时,您的应用可以定义可操作的通知。 每种categories的通知都可以有一些预设的自定义操作。

注册后,您的服务器可以设置推送通知的categories。 收到后,用户可以使用相应的操作。

对于WenderCast,您将使用名为View的自定义操作定义News categories。 此操作将允许用户在他们选择的情况下查看应用中的新闻文章。

registerForPushNotifications()中,在guard的下方和getNotificationSettings()的调用之上插入以下内容:

// 1
let viewAction = UNNotificationAction(
  identifier: Identifiers.viewAction, title: "View",
  options: [.foreground])

// 2
let newsCategory = UNNotificationCategory(
  identifier: Identifiers.newsCategory, actions: [viewAction],
  intentIdentifiers: [], options: [])

// 3
UNUserNotificationCenter.current().setNotificationCategories([newsCategory])

这是新代码的作用:

  • 1) 创建一个新的notification action,按钮上的标题为View,在触发时在前台打开应用程序。 该操作具有不同的标识符,用于区分同一通知上的其他操作。
  • 2) 接下来,定义news category,其中包含view action。 它还具有您的有效负载需要包含的唯一标识符,以指定推送通知属于此category
  • 3) 最后,通过调用setNotificationCategories,注册新的可操作通知。

就这些! 构建并运行应用程序以注册新的通知设置。

对应用程序进行后台处理,然后通过PushNotifications实用程序发送以下有效内容:

{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com",
    "category": "NEWS_CATEGORY"
  }
}

如果一切顺利,您应该能够下拉通知以显示查看操作:

太好了! 点击它将启动WenderCast,但它不会做任何事情。 要使它显示新闻项,您需要在委托中执行更多事件处理。

1. Handling Notification Actions - 处理通知操作

每当触发通知操作时,UNUserNotificationCenter都会通知其代理。 回到AppDelegate.swift,将以下类扩展添加到文件的底部:

extension AppDelegate: UNUserNotificationCenterDelegate {
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void) {
    
    // 1
    let userInfo = response.notification.request.content.userInfo
    
    // 2
    if let aps = userInfo["aps"] as? [String: AnyObject],
      let newsItem = NewsItem.makeNewsItem(aps) {
      
      (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
      
      // 3
      if response.actionIdentifier == Identifiers.viewAction,
        let url = URL(string: newsItem.link) {
        let safari = SFSafariViewController(url: url)
        window?.rootViewController?.present(safari, animated: true,
                                            completion: nil)
      }
    }
    
    // 4
    completionHandler()
  }
}

这是通过自定义操作(custom action)打开应用程序时获得的回调。 可能看起来有很多事情发生,但这里并没有太多新内容:

  • 1) 获取aps字典。
  • 2) 从字典中创建一个NewsItem并导航到News tab
  • 3) 检查操作标识符(action identifier),作为标识符传入。 如果是“View”操作且链接是有效的URL,则会在SFSafariViewController中显示该链接。
  • 4) 调用系统传递给您的完成处理程序。

最后一点是:您必须在UNUserNotificationCenter上设置代理。 将此行添加到application(_:didFinishLaunchingWithOptions:)的顶部:

UNUserNotificationCenter.current().delegate = self

建立并运行。 再次关闭应用程序,然后使用以下payload发送另一个新闻通知:

{
  "aps": {
    "alert": "New Posts!",
    "sound": "default",
    "link_url": "https://xxx.com",
    "category": "NEWS_CATEGORY"
  }
}

下拉通知并点击View操作,您应该看到WenderCast在启动后立即显示Safari View controller

恭喜,您已经实施了可操作的通知! 再发送一些并尝试以不同方式打开通知,以查看其行为方式。


Silent Push Notifications - 无声推送通知

无声推送通知可以静默唤醒您的应用以在后台执行某些任务。 WenderCast可以使用此功能安静地刷新播客列表。

使用适当的服务器组件,这可以非常有效。 您的应用不需要不断轮询数据。 只要有新数据,您就可以向其发送静默推送通知。

首先,再次选择WenderCast target。 现在点击Capabilities选项卡并将Background Modes开启设置为On。 然后选中Remote notifications选项:

现在,当您的应用收到其中一个推送通知时,它将在后台唤醒。

AppDelegate中,找到application(_:didReceiveRemoteNotification:)。 使用以下内容替换对NewsItem.makeNewsItem()的调用:

// 1
if aps["content-available"] as? Int == 1 {
  let podcastStore = PodcastStore.sharedStore
  // 2
  podcastStore.refreshItems { didLoadNewItems in
    // 3
    completionHandler(didLoadNewItems ? .newData : .noData)
  }
} else  {
  // 4
  NewsItem.makeNewsItem(aps)
  completionHandler(.newData)
}

一起看一下代码:

  • 1) 您检查content-available是否设置为1,以查看它是否是静默通知。
  • 2) 您刷新播客列表,这是一个异步网络调用。
  • 3) 刷新完成后,调用completion处理程序让系统知道是否加载了任何新数据。
  • 4) 如果它不是静默通知,则假定它是新闻并创建news item

务必使用诚实的结果调用completion处理程序。 系统会测量您的应用在后台使用的电池消耗和时间,并可能在需要时限制您的应用。

就这些,测试它,构建和运行,将应用程序置于前台并通过PushNotifications推送以下payload

{
  "aps": {
    "content-available": 1
  }
}

如果一切顺利,除非刚刚将新播客添加到远程数据库,否则不会发生任何事情。 要确认代码按预期运行,请在application(_:didReceiveRemoteNotification:fetchCompletionHandler:)中设置断点并在推送后逐步执行。

后记

本篇主要讲述了发送和处理推送通知流程详解,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容