[App探索]JSBox中幽靈觸發器的實現原理探索

NotFound9發表於2019-01-02

建了一個面試題解答的專案,大家可以看一看,希望大家幫忙給一個star,謝謝了!

《面試指北》專案地址:github.com/NotFound9/i…

[App探索]JSBox中幽靈觸發器的實現原理探索

前言

幽靈觸發器是鍾穎大神的JSBox中的一個功能,在app程式被殺死的情況下,也可以將通知固定在通知欄,即便使用者點選清除,也能馬上再彈出,永遠不消失,除非使用者關閉App的通知許可權或者解除安裝App,才可以消失。這個功能確實比較有意思,而且鍾穎大神在介紹視訊裡有提到是目前JSBox獨有的,說明實現得非常巧妙,自己研究的話還是很難想到的,非常值得學習,而且當你瞭解它的實現原理的話,會發現其實可以做很多其他的事情。當某天產品經理對App推送點選率不滿意時,可以向她祭出這件大殺器(哈哈,開玩笑的,無限推送這種功能其實蘋果很不推薦,因為確實有可能會被一些不良App採用,然後無限推送,讓使用者反感)。以下內容僅供學習討論,JSBox是一個很強大的App,有很多值得學習的地方,強烈推薦大家去購買使用。

簡短的效果視訊

[App探索]JSBox中幽靈觸發器的實現原理探索

完整的介紹視訊

weibo.com/tv/v/G79vjv… 從2分6秒開始

探索歷程

因為沒有可以用來砸殼的越獄手機,而且PP助手也沒有JSBox的包,一開始是去搜幽靈觸發器,無限通知的實現,發現沒找到答案,stackoverflow上的開發者倒是對無限通知比較感興趣,問答比較多,但是沒有人給出答案,基本上也是說因為蘋果不希望開發者用這種功能去騷擾使用者。所以只能自己閱讀通知文件,查資料來嘗試實現了。

難道是使用時間間隔觸發器UNTimeIntervalNotificationTrigger來實現的嗎?

因為看通知清除了還是一個接一個得出現,很自然就能想到是通過繞過蘋果的檢測,去改UNTimeIntervalNotificationTrigger的timeInterval屬性來實現的,所以寫出了一下程式碼:

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];
複製程式碼

而且我打斷點看,確實改成功了,

image.png
但是,很快,當我把第一個通知清除時,手機變成這樣了
[App探索]JSBox中幽靈觸發器的實現原理探索
有那麼一刻,我心裡很慌,我一定好好做人,不去改蘋果爸爸的只讀屬性了。

蘋果是在顯示第二個通知的時候才去判斷的,而我們的程式碼只能控制到將通知請求request新增到UNUserNotificationCenter這一步,所以不太好繞過。

難道是使用地點觸發器UNLocationNotificationTrigger來實現的嗎?

UNLocationNotificationTrigger可以通過判斷使用者進入某一區域,離開某一區域時觸發通知,但是我去看了一下設定裡面的許可權,發現只使用這個功能的時候JSBox並沒有請求定位的許可權,所以應該不是根據地點觸發的。

繼續閱讀文件

然後我就去鍾穎大神的JSBox社群仔細檢視開發者文件,檢視關於通知觸發相關的api,結果發現

image.png
不是通過repeats欄位,而是通過renew這個欄位來決定是否需要重複建立通知的,所以很有可能不是通過時間觸發器來實現的,是通過自己寫程式碼去建立一個通知,然後將通知進行傳送。 在大部分iOS開發同學心中(包括我之前也是這麼認為的),普遍都認為當app處於執行狀態時,這樣的實現方案自然沒有問題,因為我們可以獲取到通知展示,使用者對通知操作的回撥。當app處於未執行狀態時,除非使用者點選通知喚醒app,我們無法獲取到操作的回撥,但其實在iOS 10以後,蘋果公開的UserNotifications框架,允許開發者通過實現UNUserNotificationCenter的代理方法,來處理使用者對通知的各種點選操作。具體可以看蘋果的這篇文章Handling Notifications and Notification-Related Actions, 翻譯其中主要的一段:
image.png
你可以通過實現UNUserNotificationCenter的代理方法,來處理使用者對通知的各種點選操作。當使用者對通知進行某種操作時,系統會在後臺啟動你的app並且呼叫UNUserNotificationCenter的代理物件實現的userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:方法,引數response中會包含使用者進行的操作的actionIdentifier,即便是系統定義的通知操作也是一樣,當使用者對通知點選取消或者點選開啟喚醒App,系統也會上報這些操作。 核心就是這個方法

// 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就不會被呼叫了,文件裡面沒有提及這個,我也是試了好久才試出來的。

image.png
image.png

隱藏關卡二 必須使用上一個通知的requestIdentifier

當使用者點選“清除”按鈕時,即便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,可以做一些不定間隔推送的功能,而不需要每次使用者點選“清除”後,將使用者操作通過網路請求上報給伺服器,然後伺服器根據情況給使用者發推送。更多的玩法有待我們探索。

Demo github.com/577528249/N…

Demo 演示Gif

gif

寫文章太耗費時間了,求大佬們點個關注,會定期寫原創文章,跟大佬們一起學習進步,有問題或者建議歡迎加我微信ruiwendelll,我們一起探討學習,謝謝了!

相關文章