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

前言

幽靈觸發器是鍾穎大神的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來實現的嗎?

由於看通知清除了仍是一個接一個得出現,很天然就能想到是經過繞過蘋果的檢測,去改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];

並且我打斷點看,確實改爲功了,
image.png
可是,很快,當我把第一個通知清除時,手機變成這樣了

有那麼一刻,我內心很慌,我必定好好作人,不去改蘋果爸爸的只讀屬性了。併發

蘋果是在顯示第二個通知的時候纔去判斷的,而咱們的代碼只能控制到將通知請求request添加到UNUserNotificationCenter這一步,因此不太好繞過。app

難道是使用地點觸發器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,系統也會上報這些操做。
核心就是這個方法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就不會被調用了,文檔裏面沒有說起這個,我也是試了很久才試出來的。
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 https://github.com/577528249/...

Demo 演示Gif

gif

寫文章太耗費時間了,能夠的話,求你們給我點個關注吧,會按期寫原創文章,謝謝了!

相關文章
相關標籤/搜索