幽灵触发器是钟颖大神的JSBox中的一个功能,在app进程被杀死的状况下,也能够将通知固定在通知栏,即使用户点击清除,也能立刻再弹出,永远不消失,除非用户关闭App的通知权限或者卸载App,才能够消失。这个功能确实比较有意思,并且钟颖大神在介绍视频里有提到是目前JSBox独有的,说明实现得很是巧妙,本身研究的话仍是很难想到的,很是值得学习,并且当你了解它的实现原理的话,会发现其实能够作不少其余的事情。当某天产品经理对App推送点击率不满意时,能够向她祭出这件大杀器(哈哈,开玩笑的,无线推送这种功能其实苹果很不推荐,由于确实有可能会被一些不良App采用,而后无限推送,让用户反感)。如下内容仅供学习讨论,JSBox是一个很强大的App,有不少值得学习的地方,强烈推荐你们去购买使用。c++
https://weibo.com/tv/v/G79vjv...:1f37179499e39dbc8a7472897b9e056c
从2分6秒开始git
由于没有能够用来砸壳的越狱手机,并且PP助手也没有JSBox的包,一开始是去搜幽灵触发器,无限通知的实现,发现没找到答案,stackoverflow上的开发者却是对无限通知比较感兴趣,问答比较多,可是没有人给出答案,基本上也是说由于苹果不但愿开发者用这种功能去骚扰用户。因此只能本身阅读通知文档,查资料来尝试实现了。github
由于看通知清除了仍是一个接一个得出现,很天然就能想到是经过绕过苹果的检测,去改UNTimeIntervalNotificationTrigger的timeInterval属性来实现的,因此写出了一下代码:api
UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"推送标题"; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger]; [center addNotificationRequest:request withCompletionHandler:nil];
经过传入建立时间间隔为1s的实际间隔触发器来实现,运行后,第一个通知能正常显示出来,清除第一个通知后,显示第二个通知时,app崩溃了,时间间隔不能小于60s。服务器
UserNotificationsDemo[14895:860379] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'time interval must be at least 60 if repeating' *** First throw call stack: (0x1ae2a3ea0 0x1ad475a40 0x1ae1b9c1c 0x1aeca7140 0x1b8738d0c 0x1b8738bdc 0x102d508ac 0x1db487658 0x1dad09a18 0x1dad09720 0x1dad0e8e0 0x1dad0f840 0x1dad0e798 0x1dad13684 0x1db057090 0x1b0cd96e4 0x1030ccdc8 0x1030d0a10 0x1b0d17a9c 0x1b0d17728 0x1b0d17d44 0x1ae2341cc 0x1ae23414c 0x1ae233a30 0x1ae22e8fc 0x1ae22e1cc 0x1b04a5584 0x1db471054 0x102d517f0 0x1adceebb4) libc++abi.dylib: terminating with uncaught exception of type NSException
timeInterval是只读属性,看来苹果早有防范
`@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval timeInterval;
`
可是这年头,还能活着作iOS开发的谁没还不会用KVC呀,因此很天然得就能想到使用KVC来改网络
UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"推送标题"; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger]; [timeTrigger setValue:@1 forKey:@"timeInterval"]; [center addNotificationRequest:request withCompletionHandler:nil];
并且我打断点看,确实改为功了,
可是,很快,当我把第一个通知清除时,手机变成这样了
有那么一刻,我内心很慌,我必定好好作人,不去改苹果爸爸的只读属性了。并发
苹果是在显示第二个通知的时候才去判断的,而咱们的代码只能控制到将通知请求request添加到UNUserNotificationCenter这一步,因此不太好绕过。app
UNLocationNotificationTrigger能够经过判断用户进入某一区域,离开某一区域时触发通知,可是我去看了一下设置里面的权限,发现只使用这个功能的时候JSBox并无请求定位的权限,因此应该不是根据地点触发的。框架
而后我就去钟颖大神的JSBox社区仔细查看开发者文档,查看关于通知触发相关的api,结果发现
不是经过repeats字段,而是经过renew这个字段来决定是否须要重复建立通知的,因此颇有可能不是经过时间触发器来实现的,是经过本身写代码去建立一个通知,而后将通知进行发送。
在大部分iOS开发同窗心中(包括我以前也是这么认为的),广泛都认为当app处于运行状态时,这样的实现方案天然没有问题,由于咱们能够获取到通知展现,用户对通知操做的回调。当app处于未运行状态时,除非用户点击通知唤醒app,咱们没法获取到操做的回调,但其实在iOS 10之后,苹果公开的UserNotifications框架,容许开发者经过实现UNUserNotificationCenter的代理方法,来处理用户对通知的各类点击操做。具体能够看苹果的这篇文章Handling Notifications and Notification-Related Actions,
翻译其中主要的一段:
你能够经过实现UNUserNotificationCenter的代理方法,来处理用户对通知的各类点击操做。当用户对通知进行某种操做时,系统会在后台启动你的app而且调用UNUserNotificationCenter的代理对象实现的userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:
方法,参数response中会包含用户进行的操做的actionIdentifier,即使是系统定义的通知操做也是同样,当用户对通知点击取消或者点击打开唤醒App,系统也会上报这些操做。
核心就是这个方法ide
// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:. - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;
因此我就写了一个demo来实现这个功能,核心代码以下:
AppDelegate.m - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; [self applyPushNotificationAuthorization:application];//请求发送通知受权 [self addNotificationAction];//添加自定义通知操做扩展 return YES; } //请求发送通知受权 - (void)applyPushNotificationAuthorization:(UIApplication *)application{ if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error && granted) { NSLog(@"注册成功"); }else{ NSLog(@"注册失败"); } }]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { NSLog(@"settings========%@",settings); }]; } else if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)){ [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound ) categories:nil]]; } [application registerForRemoteNotifications]; } //添加自定义通知操做扩展 - (void)addNotificationAction { UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@"NotificationForeverCategory.action.look" title:@"打开App" options:UNNotificationActionOptionForeground]; UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"NotificationForeverCategory.action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive]; UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"NotificationForeverCategory" actions:@[openAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:notificationCategory]]; } # pragma mark UNUserNotificationCenterDelegate //app处于前台时,通知即将展现时的回调方法,不实现会致使通知显示不了 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{ completionHandler(UNNotificationPresentationOptionBadge| UNNotificationPresentationOptionSound| UNNotificationPresentationOptionAlert); } //app处于后台或者未运行状态时,用户点击操做的回调 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {//点击系统的清除按钮 UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.0001f repeats:NO]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"App探索-NotFound"; content.body = @"[App探索]JSBox中幽灵触发器的实现原理探索"; content.badge = @1; content.categoryIdentifier = @"NotificationForeverCategory"; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil]; } completionHandler(); } - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } - (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } - (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } ViewController.m - (void)viewDidLoad { [super viewDidLoad]; UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; [button addTarget:self action:@selector(sendNotification) forControlEvents:UIControlEventTouchUpInside]; [button setTitle:@"发送一个3s后显示的通知" forState:UIControlStateNormal]; button.frame = CGRectMake(0, 200, [UIScreen mainScreen].bounds.size.width, 100); [self.view addSubview:button]; } //发送一个通知 - (void)sendNotification { UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3.0f repeats:NO]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"App探索-NotFound"; content.body = @"[App探索]JSBox中幽灵触发器的实现原理探索"; content.badge = @1; content.categoryIdentifier = @"NotificationForeverCategory"; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:request withCompletionHandler: nil]; }
必须在didFinishLaunchingWithOptions的方法返回前设置通知中心的代理,这个文档里面都有说起,你们都知道,可是有两个文档里面不曾说起的难点须要注意:
1.必须给通知添加自定义的通知操做,而且给发送的通知指定自定义的通知操做的categoryIdentifier,这样系统在用户对通知进行操做时才会调用这个代理方法,- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler
自定义通知操做是用户长按通知,下方弹出的actionSheet,在咱们的Demo中,是“打开App”和“取消”两个操做,其实不添加这些自定义操做的话,系统的这些“管理”,”“查看”,“清除”也是有的,可是当用户点击“清除”时,咱们的代理方法didReceiveNotificationResponse就不会被调用了,文档里面没有说起这个,我也是试了很久才试出来的。
当用户点击“清除”按钮时,即使app处于未运行状态,系统也会在后台运行咱们的app,而且执行didReceiveNotificationResponse这个代理方法,在这个方法里面咱们会建立一个UNNotificationRequest,把他添加到通知中心去,而后通知会展现出来。可是系统好像对于在app正常运行时添加的UNNotificationRequest跟在didReceiveNotificationResponse方法里添加的UNNotificationRequest作了区分,后者在被用户点击“清除”按钮后,app不会收到didReceiveNotificationResponse回调方法,可能系统也是考虑到开发者可能会利用这个机制去实现无限通知的功能。因此我在建立UNNotificationRequest时,使用的identifier是前一个通知的identifier,这也是实现无限通知的最巧妙的地方,可能不少开发者是知道实现这个代理方法来接受用户点击“清除”的回调,而后作一些通知上报,隔一段时间再次发送通知事情,可是再次建立并发送的通知在被点击“清除”时已经不会再执行didReceiveNotificationResponse回调了。
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];
若是咱们作的是效率工具类型的App,利用这个功能作一些固定通知之类的功能,若是咱们作的是一些资讯类的App,能够作一些不定间隔推送的功能,而不须要每次用户点击“清除”后,将用户操做经过网络请求上报给服务器,而后服务器根据状况给用户发推送。更多的玩法有待咱们探索。
写文章太耗费时间了,能够的话,求你们给我点个关注吧,会按期写原创文章,谢谢了!