iOS9.0 Swift 远程推送通知教程

版权声明 本文翻译自:raywenderlich.com 原文作者: Jack Wu 译者: JMStack 转载请说明原文及译文出处.

iOS开发者们喜欢想象他的用户们会每时每刻使用他们所开发的APP,但是残酷的事实是他们的用户会有关闭APP来处理其它事情的时候.就像你洗好的衣服总要人去叠吧.

幸好,推送通知功能可以让开发者与用户建立连接并进行简单的交互即使用户当前并没有使用APP!

从推送通知功能第一次问世到现在已经变得越来越强大.在iOS9上,远程推送可以做到:

  • 显示短文本
  • 播放通知提示音
  • 设置APP图标的角标
  • 在不打开APP的情况下,允许用户与APP交互
  • 允许APP在后台静默唤醒来执行任务

这份远程推送通知教程会告诉你远程推送的工作原理的并让你了解它的一些特性.

在开始推送测试之前你需要具备以下条件:

  • 一台iOS设备.远程推送不能在摸拟器上运行,所以你需要一台真机.
  • 一个开发者帐号 从Xcode7开始,在真机上测试APP不再需要加入开发者计划.但是为了配置远程推送,你需要有一个与APP ID对应的推送证书,获得这个证书你需要加开发者计划.

开始

为了接收发送远程推送通知你必须完成以下3个主要的任务:

  1. app必须正确配置并注册APNS(Apple Push Notification Service),以便所有设置都完成时就能马上接收到通知
  2. 服务端必须向APNS发送一条明确指向一个或多个设备的通知
  3. app必需接收服务端发送的通知;app可以执行通知包含的任务或者在application的代理(delegate)回调方法内处理用户交互行为.

任务1和任务3是这份推送通知教程主要关注的内容,因为这两个任务是iOS开发者的工作.

任务2也会在这份教程中简略的提及,并且多数情况仅仅是为了测试目的.发送一个远程通知是app服务端的工作,并且这部分内部会因为App的不同而不同.大多数app都会使用第三方服务(比如Parse.com或者Google ColoudMessaging)推送通知,其它的app或使用定制化的解决方案或使用比较流行的框架(比如: Houston).

正式开始之,下载已经准备好的 WenderCast 开始工程.WenderCast是一个让用户获取raywenderlich.com播客节目和时实消息的应用.

在Xcode中打开WenderCast.xcodeproj简单浏览一下.编绎运行即可查看当前最新播客节目:


BuildAndRun1

这个app的存在的问题是当有新的播客节目可以获取时不能通知到用户.并且也不能显示任何最新的消息.接下来你将用远程推送功能修复这个问题!

为App配置远程推送功能

推送通知需要较高的安全性.这点是非常重要的,因为你不会想让其它人给你的用户发送通知.这也就意味着要实现远程推送功能你必需跳过一些坑.

打开远程推送服务

第一步是更改App ID.在Xcode中进入 App Settings -> GeneralBundle Identifier 改为任意唯一的字符串.

ChangeBundleID

接下来你需要在你的开发者帐号下添加打开了推送通知功能的App ID.幸运的是,Xcode有更简单的方法实现这个步骤.进入 App Settings -> Capabilities 把Push Notifications设置为 On.
在Xcode完成一些下载后,看起应该会是下面的样子
PushNotificaitonCapability

这个步骤背后的操作是: 如果你当前的开发者帐号下没有对应的App ID就会主动创建App ID,并且打开推送通知功能.你可以登陆开发者中心确认是否打开了这个功能:
MemberCenter1

如果这个过程中出现问题,可以手动创建App ID或者点击开发者中心 +Edit 按钮开启推送通知功能.

以上就是你目前需要的配置.

注册远程推送

注册远程推送需要两步.第一步,你必需向用户请求推送通知许可,获得许可之后才能注册远程推送.如果所有步骤进行顺利,系统将会向你提供一个 device token ,你可以把它认为是当前设备的"地址".

在WenderCast应用中你需要用在应用启动后立即注册远程推送.

打开AppDelegate.swift,添加以下代码到AppDelegate末尾.

func registerForPushNotifications(application: UIApplication) {
  let notificationSettings = UIUserNotificationSettings(
    forTypes: [.Badge, .Sound, .Alert], categories: nil)
  application.registerUserNotificationSettings(notificationSettings)
}

这个方法创建了一个 UIUserNotificationSettings 实例对象并把它作为参数传给 registerUserNotificationSettings(_:) .

UIUserNotificationSettings 存储你的应用将到用到的通知类型设置.对于UIUserNotificationType的值你可以用下面几个枚举值的任意组合.

  • .Badge 允许App在图标上显示角标数字
  • .Sound 允许App播放声音
  • .Alert 允许App显示文本

UIUserNotificationCategory 是Set类型参数当前暂时传 nil,以允许你指定你的app能够处理的不同类型的通知.当你需要实现可交互的通知时,这样的设置是必需的.后面的部分你将会用到可交互通知.

application(_:didFinishLaunchingWithOptions:launchOptions:): 方法内的第一行调用 registerForPushNotifications(_:) :

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  registerForPushNotifications(application)
  //...
}

编绎运行.当App启动时你会收到一个弹窗请求通知许可:


BuildAndRun2.png

点击 OK ,现在App可以显示通知了.但是,如果用户拒绝了发送通知的请求该应怎么办?

当用户接受或拒绝请求许可又或者之前做出过是否允许的选择, UIApplicationDelegate 的一个代理方法将会被调用. 添加以下代码到 AppDelegate :

func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {

}

在这个方法中,你会接收到另一个 UIUserNotificationSettings 实现对象.这个实例对象与之前你之前所传入的不同.你之前传入的是你所希望的设置,而当前这个是用户当前授权的设置.

在App每次启动时都调用 registerUserNotificationSettings(_:) 是相当重要的.因为用户在任何时候都有可能在设置应用内改变通知的授权许可. application(_:didRegisterUserNotificationSettings:) 方法会告诉你用户当前给你的App什么样的授权许可.

现在第一步已经完成,你可以注册远程推送通知了.这一步相当简单,因为你不再需要向用户请求什么许可了.用下面的代码更新 application(_:didRegisterUserNotificationSettings:) 方法:

func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
  if notificationSettings.types != .None {
    application.registerForRemoteNotifications()
  }
}

在上面的方法中,首先检查当前用户是否允许通知,如果允许直接调用 registerForRemoteNotifications().

其次, registerForRemoteNotifications() 的请求注册的返回状态会通过 UIApplicationDelegate协议中的某些方法通知你.

添加以下代码到 AppDelegate :

func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
  let tokenChars = UnsafePointer<CChar>(deviceToken.bytes)
  var tokenString = ""

  for i in 0..<deviceToken.length {
    tokenString += String(format: "%02.2hhx", arguments: [tokenChars[i]])
  }

  print("Device Token:", tokenString)
}

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

就像方法名所暗示的那样,当注册通知成功后系统会调用 application(_:didRegisterForRemoteNotificationsWithDeviceToken:) 方法,否则将会调用 application(_:didFailToRegisterForRemoteNotificationsWithError:). 方法.

当前 application(_:didRegisterForRemoteNotificationsWithDeviceToken:) 方法的实现看起来难以理解,其实它仅仅只是获取 deviceToken 然后转换成字符串.deviceToken的值就是这个过程得到的结果.它是由APNs服务器提供用来标识当前设备当前App.当发送时推送通知的时候,App用deviceToken作为"地址"传递到当前设备.

注意 会有很多原因导致注册失败.最常碰到原因是程序运行在模拟器上,或者App ID设置不正确.具体原因打印error值会提供更加详细的信息.

到此,编绎运行.确保你当前运行在真机上,你将会在控制台看到打印出的device token.下面将会是你看到的结果:


DeviceToken

把device token复制到某处保存.

在正式发送通知之前你还需要配置一点点,所以回到开发者中心.

创建一个SSL证书和PEM文件

在开发者中心进入 Certificates, Identifiers & Profiles -> Identifiers -> App IDs 找到你应用的App ID.在 Application Services 下面 Push Notifications 应该为 Configurable :

ConfigurablePushNotifications

点击 Edit 滚动到 Push Notifications :
CreateCertificate

Development SSL Certificate 栏下,点击 Create Certificate… 接下来的步骤就是创建 CSR 文件.创建好CSR文件后点击 continueGenerate,这步会用你创建的CSR文件生成证书.最后下载并运行生成好的证书,证书将被添加到你的钥匙串应用中,并与私钥成对.
KeychainCertificate

在开发者中心,你的App ID现在推送通知功能在development下应该处于Enable状态.


PushNotificationEnabled

在关闭钥匙串应用前还有最后一件事,右击你刚才添加的证书,选择 Export :

ExportCertificate

保存在桌面并命名为WenderCastPush.p12.


SaveP12

你会被提示要求为你的.p12文件设置密码,你可以选择不输或者输入一个你想设置的密码.这里我用"WenderCastPush"作为密码.接下来你需要输入电脑登陆密码来允许导出p12文件.

接下来,打开你的终端并执行以下命令来从p12文件生成PEM文件:

$ cd ~/Desktop
$ openssl pkcs12 -in WenderCastPush.p12 -out WenderCastPush.pem -nodes -clcerts

如果你导出p12文件时输入了密码,在这里你必须输入相同的密码.

到此为止,你已经艰难的跃过了很多坑,这一切都是值得的.接下来你将用你生成的WenderCastPush.pem文件发送第一个通知.

发送通知

之前下载的开始工程会包含一个WenderCastPush文件;里面包含两个用于发送通知简单脚本.你需要用到的是newspush.php.正如文件名所暗示的,这个脚本将会向你的用户发送一个弹窗通知消息.

发送推送通知需要和APNS建立SSL连接,SSL连接是用之前创建的证书进行加密.这就是为什么要生成 WenderCastPush.pem 文件.重命名 WenderCastPush.pemck.pem,并且替换掉当前已经存在于 WenderCastPush 文件夹下的 ck.pem 文件.

打开 newspush.php 并更新之前接收到的 $deviceToken 和导出文件时输入的密码 $passphrase

// Put your device token here (without spaces):
$deviceToken = '43e798c31a282d129a34d84472bbdd7632562ff0732b58a85a27c5d9fdf59b69';

// Put your private key's passphrase here:
$passphrase = 'WenderCastPush';

打开终端, cdnewspush.php 所在的文件夹,输入:

ush.php 'Breaking News' 'https://raywenderlich.com'

如果进行顺利,你的终端将会显示:

Connected to APNS
Message successfully delivered

现在,你应该会收到你的第一条通知:


FirstPush-281x500

注意 如果你的App被打开并处于前台运行状态,你将看不到任何东西.通知已经被投送但是App还不会处理这个通知.你只需要简单的关闭App并重新发送通知即可.

常见问题

也许你会遇到以下问题:

只能接收到部分通知:如果你同时发送多个通知,只有部分通知将会被接收,不用担心!这正是我们想要的结果.当发送通知时APNS会为每一个开启了推送通知的设备保持一个高质量服务(Quality of Service)队列.这个队列的大小是1,所以如果你同时发送多个通知,最后一个通知才会被发送.

连接到APNS出现问题:出现这个问题的原因可能是你的防火墙阻塞了APNS所使用的端口.所以确保你的防火墙没有阻塞住这些端口.另一个可能的原因是私钥和CSR文件不正确.记住,每一个App ID有一个唯一的CSR和配对的私钥.

解剖推送通知的基本原理

在进行任务3之前,需要理解一下你推送的通知,打开 newspush.php 文件理解发送一个通知的基本概念应该是怎么样的.

注意第32-40行,这就是用JSON格式编码的装载体.这就是实际上发送给APNS的东西.在我们当前的例子中,装载体像下面一样:

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

对于一个不懂JSON数据的人来说,用{}括起来的块相当于一个字典类型的数据.

这个装载体是一个至少包含一项内容的字典,这项内容就是 aps, 它本身也是一个字典.在这个例子中"aps"包含"alert","sound"和"link_url"等字段.当接收到一个通知,就会显示一个包含"Breaking News!"文本的提醒视图,并且有标准的提醒音效.

"link_url"实际上是一个自定义的字段.你可以添加类似的自定义字段到装载体中,并且它会被投送到你的应用.因为你并没有在应用中处理这个字段,所以当前接收到这个键值对会什么都不做.

你可以在aps字典中添加以下5个键(key):

  • alert. 这个字段可以是一个字符串,就像当前的例子.或是是一个字典.如果是一个字典,可以是本地化的文本或者通知的其它部分.查看苹果文档所支持的key.

  • badge. 这是一个将被显示在应用图标上的数字.你可以设置这个键为0来清除角标.

  • sound. 通过设置这个建,你可以播放存放在App本地定制的通知提示音来取代系统默认的通知提示音.定制的通知提示音必须在30秒以内并且还有一些其它的限制,你可以查看苹果文档了解更详细信息.

  • content-available.设置这个键为1,当前通知会变成静默通知.这个部分会在这份教程的后面部分探索.

  • category. 这个键定义了通知的分类,用于显示定制通知所包含的交互行为.同样,接下来会探索这部分的内容.

除此之外,你可以添加任意你想要添加的定制化数据,只要装载体不超过4096个字节.

如果你玩够了推送通知,接下来我们进入到下一个章节.

处理接收到的通知

在这个章节,你将会学习当App接收到通知后或者用户点击了通知应该如何执行什么样的操作.

当你接收到一个通知后会发生什么

当你的app接收到一个通知, UIApplicationDelegate 的一个方法将会被调用.
需要根据接到收通知时App所处的状态的进行不同的处理.

  • 如果你的应用当前不在运行,并且用户通过点击推送通知启动应用,通知内容会通过 application(_:didFinishLaunchingWithOptions:) 方法的 launchOptions 参数进行传递.

  • 如果你应用当前正运行在前台,推送通知将不会被显示.但是 application(_:didReceiveRemoteNotification:) 会被立即调用.

  • 如果你的应用正在运行,或者被挂起在后台,并且用户通过点击通知使应用进入前台 application(_:didReceiveRemoteNotification:) 方法会被调用.

在第一种情况下, WenderCast将到创建一个新的section,并直接打开以显示到这个新建section.添加以下代码到 application(_:didFinishLaunchingWithOptions:) 的末尾return语句之前.

// Check if launched from notification
// 1
if let notification = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey] as? [String: AnyObject] {
  // 2
  let aps = notification["aps"] as! [String: AnyObject]
  createNewNewsItem(aps)
  // 3
  (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
}

这段代码做了以下3件事:

  1. 检查 UIApplicationLaunchOptionsRemoteNotificationKey 键对应的值是否存在,如果存在,这个值应该就是你发送的通知装载体.

  2. 如果存在,获取 aps 对应字典并传给 createNewNewsItem(_:) 方法,这个方法根据接收的字典创建一个 NewItem,并刷新表格.

  3. 改变tab控制器当前选中的tab索引值为1,也就是直接显示新闻控制器视图.

为了测试这部分代码,你需要编辑WenderCast的scheme:


EditScheme

Run -> Info 下选择 Wait for executable to be launched:

WaitForLaunch

这个选项会使调试器等待应用程序安装直到应用程序第一次被启动。

编绎运行,完成安装后,发送一些新的动态.点击通知以启动App,启动之后app会显示一些新消息.


BuildAndRunFirstNews

注意 如果你突然接收不到通知,最有可能的原因是device token被改了.如果你删除应用再重新安装就有可能出现这种情况. 确保你的device token是正确的.

为了处理另外两种情况,添加以下代码到 AppDelegate:

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
  let aps = userInfo["aps"] as! [String: AnyObject]
  createNewNewsItem(aps)
}

这个方法直接创建了一个新的 NewsItem. 现在你可以把scheme设置回自动启动App.

编绎运行.保持App运行在前台,并选中新闻页.发送一个通知,你可以看到消息奇迹般的显示在视线内.


NewNews-281x500

就是这样!你的App现在可以处理基本的推送消息.

一些需要注意的事情:很多情况推送通知可能会被遗漏.对于WenderCast应用来说是没有问题的,因为装满消自己的列表对这个应用来说并不是那么重要,但是一般来讲你不应该把推送通知做为传递内容的唯一方式.作为备选项,推送通知应该仅仅只是指示当前有新的内容可以获取并让App从服务器下载这些新的内容.WenderCast应用在这方有一些局限性,因为它并没有合适的服务端.

可交互的通知

可交互的通知允许你添加定制化的按钮在通知上.你也许注意到邮件通知或者Twitter消息通知有一个让你回复或者点赞的部位.

可交互的通知是你通过注册通知时设置 categories 定义的.每一个通知分类都可以有多个预先自定义的交互.

一旦完成注册,就可以发送这个分类的通知.当接收到通知相应的交互就可以被用户获取.

对于 WenderCast 应用,你将定义一个自定义"View"动作的"News"分类,自定义"View"允许用户选择查看,如果用户选择就会在App中直接显示对应的消息详细文章.

添加以下代码到 registerForPushNotifications(_:): 的开头.

let viewAction = UIMutableUserNotificationAction()
viewAction.identifier = "VIEW_IDENTIFIER"
viewAction.title = "View"
viewAction.activationMode = .Foreground

这段代码创建了一个按钮标题名为"View"的新交互通知,当交互通知被用户触发时打开App并让其进入前台.这个交互动作的标识符是 VIEW_IDENTIFIER ,这个标识符被用于区分同一通知的不同交互动作.

添加以下人码片段至前面代码之后:

let newsCategory = UIMutableUserNotificationCategory()
newsCategory.identifier = "NEWS_CATEGORY"
newsCategory.setActions([viewAction], forContext: .Default)

这段代码定义了一个新通知分类,设置交互动作为之前定义的"View"动作,设置标识符为" NEWS_CATEGORY",这个标识符你是装载体要包含的内容以用其指示当前通知属于哪个分类.

最后,通过以下代码,把新建分类传递给UIUserNotificationSettings构造方法.

let notificationSettings = UIUserNotificationSettings(forTypes: [.Badge, .Sound, .Alert], categories: [newsCategory])

编绎运行,应用会注册新通知设定.按Home键来退出当前应用,以使推送通知能够显示.

在你再次运行 newspush.php 之前,首先对指定的分类做一个改动.打开 newspush.php 修改通知装载体使它包含通知的分类标识符:

$body['aps'] = array(
  'alert' => $message,
  'sound' => 'default',
  'link_url' => $url,
  'category' => 'NEWS_CATEGORY',
  );

保存并关闭newspush.php,然后运行以发送通知.如果一切进展顺利,你可以下拉并轻扫显示的通知你会看到View按钮被显示.


ViewAction-281x500

非常好,点击"View"按钮将启动WenderCast但不会做任何事情.为了获取通知装载体显示新的内容项,你需要在代理方法中做更多的操作.

处理通知交互动作事件

回到 AppDelegate.swift,添加另一个方法:

func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forRemoteNotification userInfo: [NSObject : AnyObject], completionHandler: () -> Void) {
  // 1
  let aps = userInfo["aps"] as! [String: AnyObject]

  // 2
  if let newsItem = createNewNewsItem(aps) {
    (window?.rootViewController as? UITabBarController)?.selectedIndex = 1

    // 3
    if identifier == "VIEW_IDENTIFIER", let url = NSURL(string: newsItem.link) {
      let safari = SFSafariViewController(URL: url)
      window?.rootViewController?.presentViewController(safari, animated: true, completion: nil)
    }
  }

  // 4
  completionHandler()
}

当用户通过通知的交互动作打开应用时这个方法将会被调用.这看来起好像做了很多事,但是实际上没有多少新的东西.这段代码做了以下事情:

  • 获取 aps 字典
  • 根据获取到的字典创建 NewItem 并跳到新闻页.
  • 检查以 identifier 为参数传进来的交互动作的标识符.如果View交互动作的标识符和链接有效则用 SFSafariViewController 显示这个链接内容.
  • 在处理完用户交互动用之后调用系统传递给你的 completionHandler 回调.

编绎运行,退出App,发送通知.但请确保下面的URL中有效的:

$ php newspush.php 'New Posts!' 'https://raywenderlich.com'

点击通知的交互动作,在WenderCast应用启动后会立即展示Safari控制器.


SafariVC-281x500

恭喜,你刚刚已经完成了可交互通知的实现!尝试多发送几次通知,并用不同的方法打开通知观察通知的展现行为.

静默推送通知

静默推送通知可以静默方式的唤醒你的app并让它在后台执行任务.WenderCast可以利用这个特性悄悄地刷新播客列表.

正如你所想象的,配合合适的服务端这个功能会非常有用.你不需要不断的主动获取数据,当有数据可获取时仅仅只需要发送一个静默通知.

开始之前进入 App Settings -> Capabilites 并打开 WenderCast的 Background Modes. 检查最后一个选项, Remote Notifications 是否勾选.

BackgroundModes

现在你的app接收到某个静默通知就可以在后台唤醒.

在AppDelegate内,用下面更强大的版本替换 application(_:didReceiveRemoteNotification:) 方法:

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
  let aps = userInfo["aps"] as! [String: AnyObject]

  // 1
  if (aps["content-available"] as? NSString)?.integerValue == 1 {
    // Refresh Podcast
    // 2
    let podcastStore = PodcastStore.sharedStore
    podcastStore.refreshItems { didLoadNewItems in
      // 3
      completionHandler(didLoadNewItems ? .NewData : .NoData)
    }
  } else  {
    // News
    // 4
    createNewNewsItem(aps)
    completionHandler(.NewData)
  }
}

这段代码:

  1. 检查 content-available 是否为1,以确定是否是静默推送.
  2. 刷新播客列表,因为需要访问网络所以刷新列表是异步的.
  3. 当刷新完列表,调用 completionHandler 回调方法,让系统知道数据是否已经下载.
  4. 如果不是静默通知,假定它是消息并创建一个新的消息项.

必需要确保 completionHandler(_:) 方法被调用并传递真实的是否获取到数据的结果.系统会根据回调计算耗电量和app在后台的时间,系统会根据需要调节App的耗电量以及在后台的时间.

以上就是这段代码所做的事.现在你可以用 contentpush.php 给你的应用发送一个静默通知.请务必确认以下设置脚本的正确性:

// Put your device token here (without spaces):
$deviceToken = '43e798c31a282d129a34d84472bbdd7632562ff0732b58a85a27c5d9fdf59b69';

// Put your private key's passphrase here:
$passphrase = 'WenderCastPush';

在终端直接运行:

$ php contentpush.php

如果一切顺利,什么事都不会发生.为了看到这段代码的运行结果,与之前设置的一样结果必须把scheme设置为"Wait for executable to be launched"并在 application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法旁边打上断点,以确认这个方法会被调用.

路在何方?

恭喜你已经完成了这份推送通知教程的内容并且WenderCast应用也有全部的推送功能!

你可以在这里下载完整的工程.记住为了能让工程正常运行你仍然需要更改Bundle ID和证书.

推送通知功能对于现在的App已经是一个不可或缺的部分,但如果你发送的通知太频繁用户仍然会调整你的通知请求许可.对于一个深思熟虑的设计,推送通知会让你的应用保持足够的用户粘性!

cat-1136365_1280-768x512

这只猫接收到"推送通知"后它就会知道它的晚餐已经准备好了


我希望你能喜欢这份推送教程.如果你有任何问题,你可以在下面的评论中随意提问.

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

推荐阅读更多精彩内容

  • 前言 本文是一篇转载文章,在这一篇实用的文章里,你可以按照上面的步骤实现不借助第三方和服务器端,自己给自己的设备发...
    進无尽阅读 1,574评论 6 6
  • 极光推送: 1.JPush当前版本是1.8.2,其SDK的开发除了正常的功能完善和扩展外也紧随苹果官方的步伐,SD...
    Isspace阅读 6,595评论 10 16
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 在每个人的青春里总会发生各种各样不可思议的事情,当你进入青春时,你会发现和你想象的不一样,在我幻想的青春里有一个默...
    阳光明湄阅读 337评论 0 1