推送通知(三)-iOS>10的UNNotificationServiceExtension

UNNotificationServiceExtension简介

  1. Notification Service Extension是Xcode8中加入众多extension的其中一种,Extension实际上是App提供了一个额外插件功能,以供iOS操作系统调用,与App是宿主关系。
  2. UNNotificationServiceExtension是iOS10推出后的一个新特性,先看这张图: 图1
  3. 从这张图上,我们可以看到,原先直接从APNs推送到用户手机的消息,中间添加了ServiceExtension处理的一个步骤(当然你也可以不处理),通过这个ServiceExtension,我们可以把即将给用户展示的通知内容,做各种自定义的处理,最终,给用户呈现一个更为丰富的通知,当然,如果网络不好,或者附件过大,可能在给定的时间内,我们没能将通知重新编辑,那么,则会显示发过来的原版通知内容。
  4. ServiceExtension 只能用于远程推送
  5. ServiceExtension 作用
    1. 使得推送的数据在iOS系统展示之前,经过App开发者的Extension,可以在不启动App的情况下,完成一些快捷操作逻辑,比如上面的例子,如果你是个社交App,可以在不启动App的情况下,直接点赞回复,而不用打开App,提高效率
    2. 虽然iOS10的推送数据包已经达到4k,但是对于一些图片视频gif还是无力的,有了Extension,可以在此下载完毕然后直接展示,丰富的图片和视频可以在此显示
    3. 可以在此Extension中如果要完成1中所述的用户行为操作,则必须加强安全性,服务端可以对推送的数据配合RSA算法用服务端的私钥加密,在Extension中使用服务端私钥解密,其实APNs从SSL数字安全证书到Json Web Token令牌,已经非常安全,但是大量的App使用第三方诸如JPush的推送服务,来跟APNs交互,业务数据跑在别人的管道上,当然有所顾忌,所以,这个地方加密的更多现实意义是防止业务数据被第三方服务商窥探。
  6. 缺点:
    1. Notification Service Extension非常容易崩溃crash和内存溢出out of memory
    2. 更加坑的是debug运行的时候和真机运行的时候,Notification Service Extension性能表现是不一样的,真机运行的时候Notification Service Extension非常容易不起作用,我做了几次实验,图片稍大,Notification Service Extension就崩溃了不起作用了,而相同的debug调试环境下则没问题
    3. 比如说你下载资源的时候最好分段缓存下载,真机环境下NSURLSessionDataTask下载数据不好使,必须使用NSURLSessionDownloadTask才可以,这点很无奈。

如何新建一个UNNotificationServiceExtension

  1. 我们不能通过创建UNNotificationServiceExtension的类来使用服务扩展,我们应当创建一个Target,这个Target自带一个模板
    图1
  2. 选择模板 图1
  3. 然后写名字,下一步,即可,此时我们的目录结构里面,已经多出了一个文件夹了。 图1
  4. 注意看下图图,这里的bundleID是你的工程名字的bundleID加上.名称 图1
    1. 不要修改,系统创建的时候就创建好了
  5. 注意事项:
    1. 开启当前target的Capability->Push Notification。
    2. 当前targetinfo.plist添加

       <key>NSAppTransportSecurity</key>
        <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        </dict>
      
    3. 没有第一条远程通知不会走这个ServiceExtension,没有第二条,从网络下载不到数据

如何使用

  1. ServiceExtension文件夹会多出NotificationService类,该类继承自:UNNotificationServiceExtension
  2. NotificationService重写了UNNotificationServiceExtension的两个方法
  3. 方法1:

     - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *contentToDeliver))contentHandler;
    
    1. 你需要通过重写这个方法,来重写你的通知内容,也可以在这里下载附件内容
    2. 如果在服务的时间过期(30s)之前没有调用该contentHandler,则将发送未修改的通知。
  4. 方法2:

     - (void)serviceExtensionTimeWillExpire;
    
    1. 调用时刻: 扩展即将结束的时候调用
    2. bestAttemptContent是你修改后的通知内容,如果没有修改完毕,那么就回使用原始的推送.
    3. 即: 如果处理时间太长,等不及处理了,就会把收到的apns直接展示出来
    4. 也就是说等到一定的时间,上面的方法还没有调用contentHandler,那就执行这个方法
  5. 代码如下:
    1. 步骤分析
      1. 将通知内容的重组,逻辑就是有附件的url,我就下载,如果没有url我就直接展示通知。
      2. 用系统自带类,下载图,存图,找到filePath,创建通知的附件内容。(创建附件的url,必须是一个文件路径,也就是说,必须下载下,才能获取文件路径,开头是file://)
      3. 判断文件的后缀类型,然后前端好处理。这里我的代码是写死了,因为我就测试一张图。最好的方式是服务器返回的 推送内容中,带有附件的类型。
     #import "NotificationService.h"
    
     @interface NotificationService ()
        
     @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
     @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
        
     @end
        
     @implementation NotificationService
        
     /**
      你需要通过重写这个方法,来重写你的通知内容,也可以在这里下载附件内容
      如果在服务的时间过期之前没有调用该contentHandler,则将发送未修改的通知。
      @param request 拿到apns推送的请求
      @param contentHandler 交付修改后的推送内容
      */
     - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
         self.contentHandler = contentHandler;
          // copy发来的通知,开始做一些处理
         self.bestAttemptContent = [request.content mutableCopy];
            
         // 在这里修改通知的内容
         self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
         // 重写一些东西
         self.bestAttemptContent.title = @"我是修改后的标题";
         self.bestAttemptContent.subtitle = @"我是修改后的子标题";
         self.bestAttemptContent.body = @"这里是修改后的推送内容";
         NSLog(@"----------------------");
         // 附件
         NSDictionary *dict =  self.bestAttemptContent.userInfo;
         NSDictionary *notiDict = dict[@"aps"];
         //拿到图片的url
         NSString *imgUrl = [NSString stringWithFormat:@"%@",notiDict[@"imageAbsoluteString"]];
         // 这里添加一些点击事件,可以在收到通知的时候,添加,也可以在拦截通知的这个扩展中添加
         self.bestAttemptContent.categoryIdentifier = @"seeCategory1";
         //没有资源就结束
         if (!imgUrl.length) {
             self.contentHandler(self.bestAttemptContent);
         }
         //下载图片
         [self loadAttachmentForUrlString:imgUrl withType:@"image" completionHandle:^(UNNotificationAttachment *attach) {
             if (attach) {
                 self.bestAttemptContent.attachments = [NSArray arrayWithObject:attach];
             }
             self.contentHandler(self.bestAttemptContent);
         }];
     }
        
     /**
      调用时刻: 扩展即将结束的时候调用
      bestAttemptContent是你修改后的通知内容,如果没有修改完毕,那么就回使用原始的推送.
      即: 如果处理时间太长,等不及处理了,就会把收到的apns直接展示出来
      也就是说等到一定的时间,上面的方法还没有调用contentHandler,那就执行这个方法
      */
     - (void)serviceExtensionTimeWillExpire {
         self.contentHandler(self.bestAttemptContent);
     }
             
     //这是下载附件通知的方法:
     - (void)loadAttachmentForUrlString:(NSString *)urlStr
                             withType:(NSString *)type
                     completionHandle:(void(^)(UNNotificationAttachment *attach))completionHandler{
            
            
         __block UNNotificationAttachment *attachment = nil;
         NSURL *attachmentURL = [NSURL URLWithString:urlStr];
         NSString *fileExt = [self fileExtensionForMediaType:type];
         //根据url下载数据到本地
         NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
         [[session downloadTaskWithURL:attachmentURL
                     completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
                         if (error != nil) {
                             NSLog(@"%@", error.localizedDescription);
                         } else {
                             //将下载的文件存储到本地
                             NSFileManager *fileManager = [NSFileManager defaultManager];
                             NSURL *localURL = [NSURL fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileExt]];
                             [fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
                                
                             NSError *attachmentError = nil;
                             //创建附件
                             attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:localURL options:nil error:&attachmentError];
                             if (attachmentError) {
                                 NSLog(@"%@", attachmentError.localizedDescription);
                             }
                         }
                         //返回附件
                         completionHandler(attachment);
                     }] resume];
     }
        
     //判断文件类型的方法(很多情况下是服务器发送数据类型)
     - (NSString *)fileExtensionForMediaType:(NSString *)type {
         NSString *ext = type;
         if ([type isEqualToString:@"image"]) {
             ext = @"jpg";
         }
         if ([type isEqualToString:@"video"]) {
             ext = @"mp4";
         }
         if ([type isEqualToString:@"audio"]) {
             ext = @"mp3";
         }
         return [@"." stringByAppendingString:ext];
     }
        
     @end
    
  6. 如何调试UNNotificationServiceExtension
    1. 选择ServiceExtension这个Target运行-> Run->选择你的项目就可以在NotificationService中打断点了(可是我这样做却没用)
  7. 在SmartPush中写的推送内容如下
    1. 这里我们要注意一定要有"mutable-content": "1",以及一定要有Alert的字段,否则可能会拦截通知失败。
    2. 除此之外,我们还可以添加自定义字段,比如,图片地址,图片类型等
     {
     "aps": {
         "alert": "This is some fancy message.",
         "badge": 1,
         "sound": "default",
         "mutable-content": "1",
         "imageAbsoluteString": "http://upload.univs.cn/2012/0104/1325645511371.jpg"
            
     }
     }
    
Table of Contents