越狱设备本地拉取通知

12
由于iOS框架不允许本地通知在发布之前执行代码,因此我正在寻找一种在越狱设备上实现它的方法。
  • 是否有内置功能可在越狱设备上安排代码执行而无需用户交互?
  • 代码应下载更新并确定用户是否应接收通知。
  • 不想使用推送通知,这需要外部服务器将其推送到用户。
更新

好吧,我已经成功创建了一个在启动时启动并保持运行的守护进程。但是,发布通知需要UIApplication对象。根据documentation,这个单例是由UIApplicationMain()方法创建的,对于常规应用程序,该方法由main()调用。由于我希望通知由守护进程发布,因此单例为空。

我可以创建UIApplication的实例吗?还是以其他方式发布通知? 我已经尝试过在应用程序委托中调用UIApplicationMain()并发布通知,以及杀死应用程序,但这会显示一个黑屏片刻; 我猜它正在启动应用程序。此外,当无法启动应用程序时(当手机尚未完全启动时),它会导致守护进程崩溃。
下面是代码草图:
int main(){
   if(launchedBySpringBoard || launchedBynotification)
      UIApplicationMain(...);
   else if(launchedByDaeamon)
      StartRunLoop();
}

void triggerdByRunLoopEveryXhours(){
    downloadData();
    if(isNewData())
       postNotification();
}

你的代码是做什么的? - nielsbot
我创建了一个守护进程,它启动了一个NSRunLoop循环,该循环触发一个方法来下载一些数据并确定是否需要发布通知。 - Kirill Kulakov
@KirillKulakov:为什么你有 launchedBySpringBoard、launchedByNotification 和 launhedByDaemon?如果我没记错,你的问题可以分为两个方面:如何在后台持续运行(这将允许你执行一些代码而无需用户交互),以及如何在发生某些事情时显示本地通知(我认为你主要感兴趣的是这个,只是为了让用户知道发生了什么)。我认为你不需要检查所有这些标志,因为如果你持续运行,那么你的应用程序就会被带到前台。 - Victor Ronin
3个回答

12

......或者以其他方式发布通知?

可以的。您可以使用后台(启动)守护程序触发通知(不一定是UILocalNotification)。当通知向用户显示警报时,您的守护程序可以决定打开常规的UI应用程序(或不打开)。

建立一个启动守护程序。

这是我找到的最好的教程。启动守护程序在手机启动时启动,并作为非图形化后台进程运行。 从那里,您可以安排检查更新。 (我有一个名为HelloDaemon的类,它在run:方法中完成所有工作):

int main(int argc, char *argv[]) {
    @autoreleasepool {
        HelloDaemon* daemon = [[HelloDaemon alloc] init];
        
        // start a timer so that the process does not exit.
        NSTimer* timer = [[NSTimer alloc] initWithFireDate: [NSDate date]
                                                  interval: 1.0
                                                    target: daemon
                                                  selector: @selector(run:)
                                                  userInfo: nil
                                                   repeats: NO];
        
        NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
        [runLoop addTimer: timer forMode: NSDefaultRunLoopMode];
        [runLoop run];
    }    
    return 0;
}

守护进程可以正常使用 NSTimer ,因此在需要时(在 run: 中)可以安排另一个计时器来检查要下载的更新。

从守护进程通知用户

如果守护进程决定应该通知用户,则可以:

1) 打开完整的 UI 应用程序。

#include <dlfcn.h>
#define SBSERVPATH "/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices"

-(void) openApp {
  
    // the SpringboardServices.framework private framework can launch apps,
    //  so we open it dynamically and find SBSLaunchApplicationWithIdentifier()
    void* sbServices = dlopen(SBSERVPATH, RTLD_LAZY);
    int (*SBSLaunchApplicationWithIdentifier)(CFStringRef identifier, Boolean suspended) = dlsym(sbServices, "SBSLaunchApplicationWithIdentifier");
    int result = SBSLaunchApplicationWithIdentifier(CFSTR("com.mycompany.AppName"), false);
    dlclose(sbServices);
}

这段代码需要 com.apple.springboard.launchapplications 权限才能使你的守护程序顺利使用。 请参考这里添加权限。 你需要为你的守护程序可执行文件准备一个 entitlements.xml 文件,像这样:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.springboard.launchapplications</key>
        <true/>
    </dict>
</plist>

2) 从您的守护程序中显示一个简单的警告窗口,通知用户事件并提示他们打开UI应用程序。

#include "CFUserNotification.h"

-(void) showAlert {
  
    NSMutableDictionary* dict = [NSMutableDictionary dictionary];
    [dict setObject: @"Alert!" forKey: (__bridge NSString*)kCFUserNotificationAlertHeaderKey];
    [dict setObject: @"Updates Ready!" forKey: (__bridge NSString*)kCFUserNotificationAlertMessageKey];
    [dict setObject: @"View" forKey:(__bridge NSString*)kCFUserNotificationDefaultButtonTitleKey];
    [dict setObject: @"Cancel" forKey:(__bridge NSString*)kCFUserNotificationAlternateButtonTitleKey];
    
    SInt32 error = 0;
    CFUserNotificationRef alert =
    CFUserNotificationCreate(NULL, 0, kCFUserNotificationPlainAlertLevel, &error, (__bridge CFDictionaryRef)dict);
    
    CFOptionFlags response;
    // we block, waiting for a response, for up to 10 seconds
    if((error) || (CFUserNotificationReceiveResponse(alert, 10, &response))) {
        NSLog(@"alert error or no user response after 10 seconds");
    } else if((response & 0x3) == kCFUserNotificationAlternateResponse) {
        // user clicked on Cancel ... just do nothing
        NSLog(@"cancel");
    } else if((response & 0x3) == kCFUserNotificationDefaultResponse) {
        // user clicked on View ... so, open the UI App
        NSLog(@"view");
        [self openApp];
    }
    CFRelease(alert);
}

要使用我上面展示的代码,你需要一个CFUserNotification.h头文件。你可以通过谷歌搜索或在这里找到。这个旧的维基文档也展示了如何在iOS应用中使用CFUserNotification的一些有用信息。

KennyTM之前链接到的答案也展示了如何使你的弹出提示即使设备被锁定也能显示。


太棒了,这给了我一些想法,不管怎样,我更喜欢通知在通知中心等待用户,而不是立即弹出。 - Kirill Kulakov
@KirillKulakov,尝试在越狱应用程序中使用UILocalNotification存在问题,而不仅仅是守护程序。请参见此其他未回答的问题。到目前为止,我还没有找出如何直接使用它们。如果您有其他要求,请编辑您上面的问题并将它们放在其他人看到的位置。理想情况下,您会在首次提问时这样做,这样我们就不会浪费时间给您提供不符合要求的答案。谢谢。 - Nate
嗯,你的回答非常有帮助,弹出窗口比通知还是好过没有。 - Kirill Kulakov
@KirillKulakov,不幸的是,在越狱开发中,我经常发现自己只有“好过没有”的解决方案 :) - Nate
1
@Nate:明白了,只是觉得这可能是一个快速的答案,并且与这个问题相关。我在这里添加了一个新问题:https://dev59.com/YXDXa4cB1Zd3GeqP_FRS - drewmm
显示剩余2条评论

5
首先,让我说一下BigLex提供的信息非常有趣。然而,我从未尝试为越狱的iPhone编写守护程序。因此,我不知道限制(看起来像是UIApplication sharedApplication为空)。
几个想法:
背景运行
1)如果您计划通过Cydia分发(意味着应用程序最终将在系统卷上),则可以使用两种未记录的后台模式:
"continuos"(这个将在后台持续运行) "unboundedTaskCompletion"(这个将有无限时间,如果您执行[UIApplication beginBackgroundTaskWithExpirationHandler])
您可以查看示例Info.plist here,其中使用了continuous。
2)还有其他方法可以获得永久后台(甚至不需要设备越狱)。
作为例子,常见的方法是在循环中播放无声音频。这里是如何做到的示例。
请注意,这种方法不会被App Store接受。
在情况1)或2)下,您将可以访问[UIApplication sharedApplication]以发布本地通知。
您可能有兴趣查看Backgrounder。我相信它为越狱设备实现了后台能力。但是,它可能已经过时了。
UIApplication中的守护程序问题
关于守护进程问题。如果您仔细阅读文章,您会发现

首先需要注意的是,使用UIApplication类启动守护进程并不好(它占用的内存比我们需要的多),因此我们将编写自己的main方法。

因此,那里的代码是为了节省内存而优化的。但是,我相信你可以用常见的iOS应用程序代码来替换它:

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

因此,我认为你应该拥有UIApplication单例,并能够发布本地通知。
是的...它会占用额外的X千字节内存,但谁在乎呢(如果你没有运行100个这样的守护程序)。

1
运行静音音频肯定会影响电池寿命 - 并且可能会导致您的应用被拒绝。 (我怀疑播放音频会打开音频放大器...我知道在Mac上这是真的)。可能需要越狱才能实现。 - nielsbot
好的,我会检查“Backgrounder”和“beginBackgroundTaskWithExpirationHandler”,但我猜它不会像守护进程那样好,守护进程会在启动时自动重新启动它,如果被杀掉。(我希望我是错的) - Kirill Kulakov
1
给Victor加1!我之前不知道 continuous 或者 unboundedTaskCompletion!对于Kirill,如果你还想坚持使用 launch daemon 的概念,我的回答中有额外的信息 - Nate
@KirillKulakov: 顺便说一下,如果您添加VOIP后台模式,则您的应用程序将在重新启动后启动并自动重启。 - Victor Ronin
@nielsbot:明白了,你说得对。这个应用程序不会被App Store接受。但是,我也是正确的。它可以在越狱设备上运行(只需使用企业证书签署应用程序或仅在开发过程中使用)。我会在我的答案中加入你的注释。 - Victor Ronin
显示剩余4条评论

2

仅是猜测,不是真正的答案,也许你可以使用MobileSubstrate的挂钩功能,钩入操作系统的通知处理过程,并告诉操作系统执行一些代码来检查通知是否来自你的应用程序,如果是这样,则检查更新并决定是否显示通知?

或者,您可以启动一个后台进程,每隔X分钟检查是否有更新,如果有,则立即设置本地通知。不确定如何实现此操作。


您有任何与MobileSubstrate的hooking相关的示例吗?关于后台进程,它是否具有电池效率? - Kirill Kulakov
看一下这个简单的Mobile Substrate钩子教程:http://ios-blog.co.uk/tutorials/how-to-create-a-mobilesubstrate-tweaks-for-ios/,这里有关于theos的一些信息(你应该使用它作为开发环境来避免很多麻烦)http://iphonedevwiki.net/index.php/Theos/Getting_Started#Purpose不幸的是,你需要进行一些研究,以找出在操作系统中确切的钩子位置,因为通知处理过程可能没有很好的文档记录。 - BigLex
至于第二种方法的电池效率,我认为在电池寿命方面不应该是一个大问题(当然这取决于您检查通知的频率),但我也不确定。无论如何,如果您找到了一种启动后台进程的方法(我非常确定这可以做到),我认为第二种方法比第一种方法更容易,因为您不需要找到iOS中的钩子。 - BigLex
1
这篇教程讲解了如何创建一个后台守护进程。链接为:http://chrisalvares.com/blog/7/creating-an-iphone-daemon-part-1/ - BigLex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接