翻译自:http://www.raywenderlich.com/29948/backgrounding-for-ios

(代码部分若乱码,请移步原链接拷贝)

自ios4开始,用户点击home按钮时,你可以将app设计为挂起状态。app在内存中,除非用户再次返回到app,否则该app暂停运行。都是这种情况吗?

当然不是,在一些例外的情况下,app仍然可以在后台保持运行。这篇文章将介绍如何以及何时应用(几乎)所有这些后台操作功能。

应用后台运行模式实际上有很严格的限制条件,在ios上实现真正的多任务上,这并非一个奇特的解决办法。app退回后台时,比如用户切换到了另一个app,更多的是完全被挂起。

允许app在后台仍然运行的情况仅限于以下几种:

1,播放音频文件(playing audio)

2,获取定位更新(getting location updates)

3,杂志app中下载新的期刊(downloading new issues for newsstand apps)

4,VoIP 呼叫(handing VoIP calls)

假如你的app没有用到如上任一功能,那么非常不幸运。需要特别注意的是:针对所有的app,在它们真正被挂起之前,有最多10分钟的时间去完成其正在执行的任务。

所以后台模式可能并非如你所愿,但仍请看官细品!

接下来你将学习到,在IOS中5种基本的可用的后台模式。本章你创建的项目是一个基于tabbed的简单应用,每个tab展示了上述5种基本后台模式效果之一,即从‘播放音频’开始到‘接听Voice - over - IP’链接。

赘言不絮,开始正文。

初探--”后台模式“

在深入探讨之前,下面预览下这5种基本后台模式:

1,音频播放:在后台app依然可以播放/录制音频。

2,实时接收定位更新:app依然可以获取设备位置更新的回调。

3,执行一个有限时长的任务:通用的情况,在限定的时间内,app可以运行任意作用的代码。

4,杂志下载:对于杂志类app,允许其在后台下载更新的内容。

5,提供VoIP服务:允许app在后台运行任意作用的代码。当然前提是你的app必须提供了VoIP服务。

本文接下来将依次介绍上述5种后台模式,你若仅对其中的一种或几种模式感兴趣,可以选择阅读。

首先下载本文介绍的项目,本文demo下载链接:sample project   ,你也可以follow该项目的GitHub页面 ,其中有详尽的项目创建过程,尽管本文着重介绍的是后台模式的操作。

好消息:用户接口已经为你预先配置好了,这更有利于专注后台模式的学习。

运行项目,效果如下:

上面的tabs将是本节阐述的引导图。首先我们将进行音频后台模式。

附:为了达到测试的最优效果,你英爱在真机上运行本程序,因为有些后台功能在模拟器上不能阐释的那么完善(甚至完全没有效果)。

音频播放:

在IOS上有若干方法进行音频的播放,这些方法中的多数均要求继承回调来提供后续的音频数据进行播放。回调(如委托方法)即是在适当时间进行某种操作,使用音频流填充缓冲区。

假如打算以数据流方式来进行音频播放,你可以开启一个网络连接,并用该连接回调惊醒持续的数据流接受。

当使能音频后台模式播放时,既是app已经在后台,即不是当前活跃的app,IOS仍然可以调用上述回调方法---就是这样,这是本文介绍的后台模式中的4个之一,音频后台模式几乎是自动完成的,你仅需要激活它,并提供合适的基础操作即可。

仅当你的app是真的提供给用户音频播放功能,你才能使用音频后台模式。若我们抱有侥幸心理,为了获得CPU更多时间而利用该模式播放一段无声的音频,apple将会拒绝此类app。

本节,你将添加音频播放到项目,并开启后台模式演示其效果。

为了实现音频播放,首先需要学习AV Foundation,打开 TBFirstViewController.m文件,添加头文件:

  1. <span style="border:0px; font-family:inherit; font-size:16px; font-style:inherit; font-weight:inherit; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(110,55,26)">#import <AVFoundation/AVFoundation.h></span>

在viewDidLoad中,添加如下代码:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_1_9508045" name="code" class="objc">// Set AVAudioSession
  2. NSError *sessionError = nil;
  3. [[AVAudioSession sharedInstance] setDelegate:self];
  4. [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
  5. // Change the default output audio route
  6. UInt32 doChangeDefaultRoute = 1;
  7. AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
  8. sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);</pre>

代码初始化了audio session对象,并确定用扬声器来播放而非听筒。

以下变量用来跟踪播放进程:

  1. @property (nonatomic, strong) AVQueuePlayer *player;
  2. @property (nonatomic, strong) id timeObserver;

声明在如下位置:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_3_7916835" name="code" class="objc">@interface TBFirstViewController ()
  2. // Insert code here
  3. @end</pre><br><br>

项目开始包含了一个音频文件,来自favorit rotalty-free music websites 。你可以免费使用其中的音频文件,所有的文件由Kevin Macleod 提供,所以,感谢Kevin!

在IOS上播放音乐,一种最简单的方法之一即是应用AV Foundations AVPlayer。 故我们的实例将会使用一个AVPlayer的子类叫做AVQueuePlayer 。AVQueuePlayer允许我们设置一个AVPlayerItems队列,用来依次并自动的播放音频文件。

在viewDidLoad结尾:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_4_5899731" name="code" class="objc">NSArray *queue = @[
  2. [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"IronBacon" withExtension:@"mp3"]],
  3. [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"FeelinGood" withExtension:@"mp3"]],
  4. [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"WhatYouWant" withExtension:@"mp3"]]];
  5. self.player = [[AVQueuePlayer alloc] initWithItems:queue];
  6. self.player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
  7. </pre><br>

代码首先创建了一个含有AVPlayerItems对象的数组,接着以该数组初始化AVQueuePlayer对象,并设置其为连续播放。

在播放进程中,为了更新音乐名字,你需要注册监听player的currentItem属性,该功能代码添加至viewDidLoad的结尾:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_5_8342729" name="code" class="objc"> [self.player addObserver:self
  2. forKeyPath:@"currentItem"
  3. options:NSKeyValueObservingOptionNew
  4. context:nil];</pre><br><br>

当player的currentItem属性变化时,将会回调监听事件。添加监听事件到viewDidLoad下面:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_6_1309116" name="code" class="objc">- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context
  2. {
  3. if ([keyPath isEqualToString:@"currentItem"])
  4. {
  5. AVPlayerItem *item = ((AVPlayer *)object).currentItem;
  6. self.lblMusicName.text = ((AVURLAsset*)item.asset).URL.pathComponents.lastObject;
  7. NSLog(@"New music name: %@", self.lblMusicName.text);
  8. }
  9. }
  10. </pre><br><br>

当监听方法被调用时,首先应该确定的是更新的属性是我们需要关注的。但在本例中,因为仅监听一个属性,故判断语句不是必须的。但是判断检测是一个很好的习惯,也是以防后期会添加另外的属性监听。如是‘currentItem’属性变化,测更新lb1MusicName标签。

你或许需要更新当前播放条目的已播时间,实现的最好方法是利用:addPeriodicTimeObserverForInterval:queue:usingBlock:方法,在指定的queue中提供回调block。

在viewDidLoad结尾添加:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_7_5703962" name="code" class="objc">void (^observerBlock)(CMTime time) = ^(CMTime time) {
  2. NSString *timeString = [NSString stringWithFormat:@"%02.2f", (float)time.value / (float)time.timescale];
  3. if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
  4. self.lblMusicTime.text = timeString;
  5. } else {
  6. NSLog(@"App is backgrounded. Time is: %@", timeString);
  7. }
  8. };
  9. self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(10, 1000)
  10. queue:dispatch_get_main_queue()  usingBlock:observerBlock];</pre><br><br>

首先创建一个block,当时间更新时,它将被调用。假如你对block不熟悉,可以阅读:How to User Blocks in IOS5 tutorial 。该block基于app的状态,创建一个显示音乐播放时间的字串。

在此之后,便是调用-(id)addPeriodicTimeObserverForInterval:(CMTime)interval
queue:(dispatch_queue_t)queue usingBlock:(void(^)(CMTime time))block
开始获取更新信息。

附:关于app的状态

你的app始终处在以下5种状态的其中之一,概要如下:

Not running:app在启动前在此状态

Active:一旦app开启,即此状态

Inactive:app在运行时有事件中断它的执行,比如一个电话呼叫到来,它将进入该状态。inactive意味着app仍然在前台运行但却不接收事件。

Backgrounded:此状态,app不在前台,但其仍能执行代码

Suspended:当不执行代码时,app即进入该状态

若你希望了解上述状态的更详尽信息,请移步apple官网:App States and Multitasking

通过调用[[UIApplication sharedApplication]applicationState]来检测app的状态,不过要注意的是你仅可以获取以下3种状态之一:

UIApplicationStateActive;UIApplicationStateInactive;UIApplicationStateBackground。suspended和not
running很明显不可能在运行代码期间检测到,所以没有它们无对应的值。

返回代码,加入app处在active状态,你需要更新label标签,否则,将更新信息打印到控制台即可。在后台状态时仍需要更新label标签,但现在需要演示的是app进入后台后,仍能接收回调。

现在要做的是完成play/pause按钮的工作,即实现didTapPlayPause方法,添加该方法至TBFirstViewController.m中:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_8_4733633" name="code" class="objc">- (IBAction)didTapPlayPause:(id)sender
  2. {
  3. self.btnPlayPause.selected = !self.btnPlayPause.selected;
  4. if (self.btnPlayPause.selected)
  5. {
  6. [self.player play];
  7. }
  8. else
  9. {
  10. [self.player pause];
  11. }
  12. }
  13. </pre><br><br>

所有代码完成,运行之:

点击‘player’按钮,音乐将起,good。

现在我们来测试下后台模式的工作情况,点击home(模拟器:Cmd+shift+H)后,但是此刻音乐也随之停止了。为什么?因为还有关键的一块没有完成。

大多数后台模式(除了3,有限时长任务外),你需要在info.plist中添加一个key,来声明该app在后台时要运行代码。

返回Xcode,操作以下:

1,点击项目

2,点击info

3,点击“+”

4,在出现的列表中,选择‘Required Background Modes’

当选择了4中的条目后,Xcode将会在该条目下创建一个数组,并含有一个空条目。点击该子条目右侧,并选择‘App plays audio’。在显示的列表中,课余ikandao所有本文介绍的后台模式,当然也包含一些基于某些硬件的条目信息。

再次运行项目,播放音乐,然后点击‘Home’键,app进入后台,但音乐照就播放了。

假如仍然没有出现上述效果,可能是因为你用的是模拟器,试着用真机测试,应该没问题。

你也可以通过查看在Xcode控制台的时间更新来证明在后台app仍是工作的。

GitHub上关于本后台模式的演示项目:BackgroundMusic

2,实时接收定位更新

在定位后台模式中,即便app处在后台模式,它仍能根据定位委托事件来接收用户的位置更新信息。

需要提醒的是:仅当你的app确实能够根据后台定位来提供有益于用户的价值,才可使用该模式。否则,你用了该模式,但对apple看来,用户毫无获益,你的app将会被拒。有时apple也会要求你在app添加一段警告,即告知用户你的app会增加电池的使用量。

演示项目的第二个tab就是关于定位更新的。打开TBSecondViewController.m文件,将以下声明

  1. @property (nonatomic, strong) CLLocationManager *locationManager;
  2. @property (nonatomic, strong) NSMutableArray *locations;

添加在:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_10_7144394" name="code" class="objc">@interface TBSecondViewController ()
  2. // add code here
  3. @end</pre><br><br>

中。

CLLocationManager用来获取设备的定位更新信息。

loactions数组用来存贮多个将被标记到map上的位置信息。

在viewDidLoad末尾添加:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_11_5127291" name="code" class="objc">self.locations = [[NSMutableArray alloc] init];
  2. self.locationManager = [[CLLocationManager alloc] init];
  3. self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
  4. self.locationManager.delegate = self;</pre><br><br>

将保存位置更新信息的数组初始化。

初始化CLLocationManager对象,并设置其精度(这个可以根据需要设置其值,接下来将会介绍更多的精度设置)属性。

完成方法:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_12_7918953" name="code" class="objc">- (IBAction)accuracyChanged:(id)sender
  2. {
  3. const CLLocationAccuracy accuracyValues[] = {
  4. kCLLocationAccuracyBestForNavigation,
  5. kCLLocationAccuracyBest,
  6. kCLLocationAccuracyNearestTenMeters,
  7. kCLLocationAccuracyHundredMeters,
  8. kCLLocationAccuracyKilometer,
  9. kCLLocationAccuracyThreeKilometers};
  10. self.locationManager.desiredAccuracy = accuracyValues[self.segmentAccuracy.selectedSegmentIndex];
  11. }
  12. </pre><br><br>

accuracyValue数组包含CLLocationManager的desireAccuracy属性中所有可能的值。

或许你认为这是很笨拙的方法,为什么不能将accuracy属性一直设置为最高精度呐?这是因为考虑到电量的消耗。精度越低,电量使用越少。

所以综上,当你的app不需要精度太高时,尽量选择精度和你需求的最接近的值即可。你也可以根据需要随时修改它。

不考虑desiredAccuracy的值distanceFilter时,还有一个属性用来控制app多久进行一次定位更新。当设备移动达到一定的距离时(m),该属性来控制locationManager接收定位更新。为了节省电量,该属性应该在满足要求情况下,越高越好。

添加如下代码来开始获取/暂停定位更新;

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_13_1441747" name="code" class="objc">- (IBAction)enabledStateChanged:(id)sender
  2. {
  3. if (self.switchEnabled.on)
  4. {
  5. [self.locationManager startUpdatingLocation];
  6. }
  7. else
  8. {
  9. [self.locationManager stopUpdatingLocation];
  10. }
  11. }</pre><br><br>

在xib文件中有UISwitch控件,并链接至该方法,以开启/关闭定位追踪。

实现CLLocationManagerDelegate方法,来获取更新的信息:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_14_4233409" name="code" class="objc">#pragma mark - CLLocationManagerDelegate
  2. /*
  3. *  locationManager:didUpdateToLocation:fromLocation:
  4. *
  5. *  Discussion:
  6. *    Invoked when a new location is available. oldLocation may be nil if there is no previous location
  7. *    available.
  8. *
  9. *    This method is deprecated. If locationManager:didUpdateLocations: is
  10. *    implemented, this method will not be called.
  11. */
  12. - (void)locationManager:(CLLocationManager *)manager
  13. didUpdateToLocation:(CLLocation *)newLocation
  14. fromLocation:(CLLocation *)oldLocation
  15. {
  16. // Add another annotation to the map.
  17. MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
  18. annotation.coordinate = newLocation.coordinate;
  19. [self.map addAnnotation:annotation];
  20. // Also add to our map so we can remove old values later
  21. [self.locations addObject:annotation];
  22. // Remove values if the array is too big
  23. while (self.locations.count > 100)
  24. {
  25. annotation = [self.locations objectAtIndex:0];
  26. [self.locations removeObjectAtIndex:0];
  27. // Also remove from the map
  28. [self.map removeAnnotation:annotation];
  29. }
  30. if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
  31. {
  32. // determine the region the points span so we can update our map's zoom.
  33. double maxLat = -91;
  34. double minLat =  91;
  35. double maxLon = -181;
  36. double minLon =  181;
  37. for (MKPointAnnotation *annotation in self.locations)
  38. {
  39. CLLocationCoordinate2D coordinate = annotation.coordinate;
  40. if (coordinate.latitude > maxLat)
  41. maxLat = coordinate.latitude;
  42. if (coordinate.latitude < minLat)
  43. minLat = coordinate.latitude;
  44. if (coordinate.longitude > maxLon)
  45. maxLon = coordinate.longitude;
  46. if (coordinate.longitude < minLon)
  47. minLon = coordinate.longitude;
  48. }
  49. MKCoordinateRegion region;
  50. region.span.latitudeDelta  = (maxLat +  90) - (minLat +  90);
  51. region.span.longitudeDelta = (maxLon + 180) - (minLon + 180);
  52. // the center point is the average of the max and mins
  53. region.center.latitude  = minLat + region.span.latitudeDelta / 2;
  54. region.center.longitude = minLon + region.span.longitudeDelta / 2;
  55. // Set the region of the map.
  56. [self.map setRegion:region animated:YES];
  57. }
  58. else
  59. {
  60. NSLog(@"App is backgrounded. New location is %@", newLocation);
  61. }
  62. }</pre><br><br>

本文并非关于专注位置相关技术,故简要说明:当app是active时,将依据位置信息更新地图;app是background时,只将位置更新信息打印到Xcode的控制台上。

接下来我们来修改info.plist,以支持后台模式,别重蹈覆辙。添加另外一个条目(‘app register for location updates’)至数组,app便会在进入后台后,仍能继续接收位置更新信息。

运行项目,并切换至第二个tab,讲switch控件置为‘ON’状态。

当app首次运行时,将会弹出一个对话框提示是否允许该app访问location定位服务。点击ok并移动设备,你应该看到位置更新情况,在模拟器上也可以模拟。

大致效果如下:

点击Home键,将app置于后台,移动设备,可以看到在控制台上输出更新信息。片刻,将app置于前台,将会看到所有的地图标注pins都更新为后台获取的新的位置。

假如应用模拟器,可以模拟移动效果,工具如图:

将location设置为Freeway Drive,点击home键。当你定位到一加州高速路上时,将会看到控制台上打印出来的信息。

有关该模式的演示代码:BackgroundLocation 。

第三个tab即第三种后台模式

有限时长的后台任务(performing finite-Length Tasks --or,whatever)

这个模式官方称谓叫‘Executing
a finite-Length Task in the background
’,简称为‘whatever’。

从技术上讲,这不是一个后台模式,因为你不需要在info.plist文件中声明你要用这种后台模式。而是依据一个API,当app进入后台后,在有限的时间内允许执行任意的代码。

比如,你可以应用此模式完成一个上传或者下载任务。形如你创建了一个图片分享app(或者其它),用户选择了一个图片,并切离该app,可能没有时间去将图片传至服务器上,但是利用此模式的API,你将获取一定CPU时间来完成上传。

上述只是一例,该后台执行代码是任意的,你可以利用此API去做任何事。执行很复杂的运算,给图片添加过滤器,渲染一个3D等等。但是需要注意的是你仅获得有限的时间,而非无限制的。

app进入后台后,这个执行时间有多长,将取决于IOS。获取的时长不确定,但是你可以一直检测着UIApplication的backgroundTimeRemaning属性,它讲告诉你还剩有多久。

普遍但非经API文档说明,你可能有10分钟的时间--所以不要依赖这个数字,你也可能只获得5分钟甚至于5秒都有可能,故app需要时刻做好任何可能的打算。

下面是一个普通任务:广为熟知的斐波拉切数列FibonacciSequence。下面将演示在后台进行运算。

打开TBThirdViewController.m文件,添加属性:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_15_9151642" name="code" class="objc">@property (nonatomic, strong) NSDecimalNumber *previous;
  2. @property (nonatomic, strong) NSDecimalNumber *current;
  3. @property (nonatomic) NSUInteger position;
  4. @property (nonatomic, strong) NSTimer *updateTimer;
  5. @property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;</pre><br><br>

  1. @interface TBThirdViewController ()
  2. // add code here
  3. @end

中。

NSDecimalNumber将持有数列中2个上次的值,NSDecimalNumber对象可保存非常大的数,所以此处应用非常适合。

position计数器,在序列中告知你当前数的位置。

应用updateTimer演示持续计算过程,并能减缓计算过程,以方便观察。

加如下代码至viewDidLoad结尾:

  1. self.backgroundTask <span style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,34,0)">=</span> UIBackgroundTaskInvalid;

关键:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_17_482609" name="code" class="objc">- (IBAction)didTapPlayPause:(id)sender
  2. {
  3. self.btnPlayPause.selected = !self.btnPlayPause.selected;
  4. if (self.btnPlayPause.selected)
  5. {
  6. self.previous = [NSDecimalNumber one];
  7. self.current  = [NSDecimalNumber one];
  8. self.position = 1;
  9. self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
  10. target:self
  11. selector:@selector(calculateNextNumber)
  12. userInfo:nil
  13. repeats:YES];
  14. self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
  15. NSLog(@"Background handler called. Not running background tasks anymore.");
  16. [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
  17. self.backgroundTask = UIBackgroundTaskInvalid;
  18. }];
  19. }
  20. else
  21. {
  22. [self.updateTimer invalidate];
  23. self.updateTimer = nil;
  24. if (self.backgroundTask != UIBackgroundTaskInvalid)
  25. {
  26. [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
  27. self.backgroundTask = UIBackgroundTaskInvalid;
  28. }
  29. }
  30. }</pre><br><br>

详解上述代码,探讨如何使用API的。

通过切换按钮的selected属性来决定是否进行计算,或者准备开始或者结束。

首先设置数列,接着创建NSTimer,并开启,使之每隔0.5秒执行一次calculateNextNumber方法。

下面即是最精要代码:调用beginBackgroundTaskWithExpirationHander:方法,告知IOS一旦app进入后台,将需要更多的时间来完成某些操作。调用之后,app若进入后台,仍获取部分CPU时长,直至调用endBackgroudTask:方法。

当然,如你不调用endBackgroudTask:方法,经过一段的后台时长,IOS将调用beginBackgroundTaskWithExpirationHander中定义的Block,让你结束正在执行的代码,所以此处是调用endBackgroudTask的好地方,告知IOS已经完成相关功能。若你不理会此方法的调用,依然执行代码的话,app将会被强制终止。

if之后使timer无效(invalidates):并调用endBackgroudTask:方法告知IOS不再需要额外的后台时间。

每次调用beginBackgroundTaskWithExpirationHander时调用endBackgroudTask:是很重要的。假如调用beginBackgroundTaskWithExpirationHander两次,而只调用endBackgroudTask一次,app将仍请求有CPU时间,直到用第二次backgroundtask的值再一次调用endBackgroudTask为止。这也是需要background值的原因。

补全代码:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_18_6862322" name="code" class="objc">- (void)calculateNextNumber
  2. {
  3. NSDecimalNumber *result = [self.current decimalNumberByAdding:self.previous];
  4. if ([result compare:[NSDecimalNumber decimalNumberWithMantissa:1 exponent:40 isNegative:NO]] == NSOrderedAscending)
  5. {
  6. self.previous = self.current;
  7. self.current  = result;
  8. self.position++;
  9. }
  10. else
  11. {
  12. // This is just too much.... Let's start over.
  13. self.previous = [NSDecimalNumber one];
  14. self.current  = [NSDecimalNumber one];
  15. self.position = 1;
  16. }
  17. NSString *currentResultLabel = [NSString stringWithFormat:@"Position %d = %@", self.position, self.current];
  18. if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
  19. {
  20. self.txtResult.text = currentResultLabel;
  21. }
  22. else
  23. {
  24. NSLog(@"App is backgrounded. Next number = %@", currentResultLabel);
  25. NSLog(@"Background time remaining = %.1f seconds", [UIApplication sharedApplication].backgroundTimeRemaining);
  26. }
  27. }</pre><br><br>

此处比较有意思的是backgroundTimeRemaining的值。此代码仅当调用beginBackgroundTaskWithExpirationHander的block时才停止。

运行项目,并切换至第三个tab。

点击开始,查看app计算情况。点击home键,查看控制台输出情况,you should see the app still updating the numbers while the time remaining goes down。

在大多数情况下,这个后台时长始以600s(10分钟)到最低5s。当时长达到5s即过期时(也可能是其它时长),过期block将被调用,app停止输出信息。此时返回app,timer将重新运行。

在上面代码中仅有一个bug,由此可以解释什么是‘后台通知’。

假设将app置于后台,知道过期,app将调用过期block,并执行endBackgroudTask。结束后台时间需求。

此刻再次重回app,timer将继续fire。但再次置于后台,将不会再有后台时长。这是为什么?因为在过期后和再次进入后台时没有再次调用beginBackgroundTaskWithExpirationHander。

有多种解决方式来修正此bug,其中之一是利用app状态更改监听来更正之。

2种获取状态更改通知:

1,通过main app delegate方法。

2,监听IOS发送来的监听事件。

UIApplicationWillResignActiveNotification和applicationWillResignActive:当app将要置为inactive状态时,前者将被sent,后者将被调用。此时,app尚未进入后台--仍是前台app但不会接收任何UI事件。

UIApplicationDidEnterBackgroundNotification和applicationDidEnterBackground:app置为后台时将被sent和调用。此时,app不再active,并且这是最后运行代码的机会。这也是调用beginBackgroundTaskWithExpirationHander的执行时间,若想获得更多的CPU时长的话。

UIapplicationWillEnterForegroundNotification和applicationWillEnterForeground:app将至active状态时将被sent和调用。app此时仍在后台,但你已经可以开始你要做的代码了。也是调用endBackgroudTask的好时刻,若是在进入后台时调用beginBackgroundTaskWithExpirationHander的话。

UIApplicationDidBecomeActiveNotification和applicationDidBecomeActive:在上面的事件发生后,自后台后,将被sent和调用;或者在app被临时中断后,比如一个电话呼叫。并非真正的进入后台,将会收到UIApplicationWillResignActiveNotification监听事件。

你可以在apple文档中查看上述流程的图列:App
States and Multitasking
 。

下一部分将会介绍如何使用这些监听,解决beginBackgroundTaskWithExpirationHander 的bug将留为一个课后联系。

演示代码:BackgroundWhatever 。

杂志下载

在IOS5中,apple介绍了杂志API:允许建立杂志类报纸类的app,并且具有一些别于其它app的特性。用杂志API创建的app在安装到原生杂志app里以前它不具有app icon的。

杂志后台模式非常便于杂志类app。他提供了一个API集来使app下载大文件(一般为期刊杂志)变得容易。即使你的app不是在active状态,它也支持下载知道完成。

附:如果你想更具体了解杂志类app,IOS5 by Tutorials有相关的章节。

实例项目的xib中,在UIWebView控件上有一个载有URL的UITextField,默认的URL为:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/ProgrammingWithObjectiveC.pdf

这个一个PDF的链接文件,方便展示app进入后台后,仍然可以继续下载的效果。其他的大文件下载链接亦可。

开始在TBFourthViewController.m文件中添加2个属性:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_19_941523" name="code" class="objc">@property (nonatomic, strong) NKIssue *currentIssue;
  2. @property (nonatomic, strong) NSString *issueFilename;</pre><br><br>

完善UITextFieldDelegate方法,响应编辑好textfiled,并点击return后的操作:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_20_5336369" name="code" class="objc">#pragma mark - UITextFieldDelegate
  2. - (BOOL)textFieldShouldReturn:(UITextField *)textField
  3. {
  4. self.webView.hidden = YES;
  5. self.progress.progress = 0.0f;
  6. self.progress.hidden = NO;
  7. NKLibrary *library = [NKLibrary sharedLibrary];
  8. for (NKIssue *issue in [library.issues copy])
  9. {
  10. [library removeIssue:issue];
  11. }
  12. self.currentIssue = [library addIssueWithName:@"test" date:[NSDate date]];
  13. NSURL *downloadURL = [[NSURL alloc] initWithString:self.txtURL.text];
  14. NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
  15. NKAssetDownload *assetDownload = [self.currentIssue addAssetWithRequest:request];
  16. [assetDownload downloadWithDelegate:self];
  17. [textField resignFirstResponder];
  18. return YES;
  19. }</pre><br><br>

首先讲webView隐藏,并显示置零的progress。

NKLibrary提供一个单例来管理app中杂志/报纸的issues。利用它来创建NKIssue对象。

移除所有现存的issue以防添加issue时出现重名错误。

创建issue,命名。在真实的生产环境下,名称需是唯一,issue名称或者号码序列。

以textfield的值建立NSURL对象,并创建NSURLRequest,将其添加至NKIssue对象中,返回NKAssetDownload对象,开启下载任务,并设置self为delegate。

实现NSURLConnection的一个委托方法(可选),即用于接收下载的进度:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_21_3667929" name="code" class="objc">#pragma mark - NSURLConnectionDownloadDelegate
  2. - (void)connection:(NSURLConnection *)connection
  3. didWriteData:(long long)bytesWritten
  4. totalBytesWritten:(long long)totalBytesWritten
  5. expectedTotalBytes:(long long)expectedTotalBytes
  6. {
  7. float progress = (float)totalBytesWritten / (float)expectedTotalBytes;
  8. if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
  9. {
  10. self.progress.progress = progress;
  11. }
  12. else
  13. {
  14. NSLog(@"App is backgrounded. Progress = %.1f", progress);
  15. }
  16. }
  17. - (void)connectionDidFinishDownloading:(NSURLConnection *)connection
  18. destinationURL:(NSURL *)destinationURL
  19. {
  20. self.issueFilename = destinationURL.pathComponents.lastObject;
  21. NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
  22. [[NSFileManager defaultManager] moveItemAtURL:destinationURL
  23. toURL:fileURL
  24. error:nil];
  25. if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
  26. {
  27. self.webView.hidden = NO;
  28. self.progress.hidden = YES;
  29. NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
  30. [self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];
  31. }
  32. else
  33. {
  34. NSLog(@"App is backgrounded. Download finished");
  35. }
  36. }</pre><br><br>

第一个方法在下载数据有更新时被调用。不需要太多操作,OS进行下载数据,app只要赶住UI更新即可,当然区分app处的状态。

第二个方法是当下载完成时被调用。然后将下载的文件迁移到NewsstandAPI期望的目录下。并且当设备空间不足时允许API进行删除老的issues。

文件迁移之后,当app为active时更新UI,否则打印至控制台上。

但当app处后台时,下载完成,将如何处理?webVIew不会更新PDF。我们可以利用监听事件来时刻关注上述问题。

首先在上述方法中找到并提取出更新代码,避免重复代码(时刻铭记保持代码DRY),在本m文件的任何地方添加如下方法:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_22_4507744" name="code" class="objc">- (void)updateWebView
  2. {
  3. self.webView.hidden = NO;
  4. self.progress.hidden = YES;
  5. NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
  6. [self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];
  7. }</pre><br><br>

从connectionDidFinishDownloading:destinationURL:移除多余代码,使之为下:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_23_8063557" name="code" class="objc">if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
  2. {
  3. [self updateWebView];
  4. }
  5. else
  6. {
  7. NSLog(@"App is backgrounded. Download finished");
  8. }</pre><br><br>

添加如下方法,当app为active状态时调用

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_24_9775424" name="code" class="objc">- (void)appBecameActive
  2. {
  3. if (self.currentIssue && self.currentIssue.downloadingAssets.count == 0 && self.webView.hidden)
  4. {
  5. [self updateWebView];
  6. }
  7. }</pre><br><br>

最后,在viewDidLoad最后添加:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_25_5075342" name="code" class="objc"> [[NSNotificationCenter defaultCenter] addObserver:self
  2. selector:@selector(appBecameActive)
  3. name:UIApplicationDidBecomeActiveNotification
  4. object:nil];</pre><br><br>

无论何时添加监听,不要忘记对应的添加移除操作,在dealloc中:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_26_7202696" name="code" class="objc">- (void)dealloc
  2. {
  3. [[NSNotificationCenter defaultCenter] removeObserver:self];
  4. }</pre><br><br>

最后一步,在info.plst中添加键值。这次需要2个键值对来使app成为一个杂志类app。

在Required background modes数组下添加:

接着创建一个新的key,并选择application presents content in Newsstand,在value中改为boolean属性,并设置为YES:

运行之,效果:

ok,它在前台工作很好,但是它还未经真实的测试。

重新运行,在下载时点击HOME,将看到打印到控制台的信息,重回APP,将看到webview已经更新PDF,ok,正确。

假如网络很好的情况下,可能还没有来得及看到控制台的信息,下载过程就很快完成了。这种情况,你可以在设备上运行该app,并使用较大文件下载,或者将网络调至成较差的情况。

需要注意的是你的app并不含有一个正规的icon图片,它是在newsstandAPP中的并且有个默认的杂志icon。

演示项目:BackgroundNewsstand

5,提供VoIP服务

最后一个是一个强大的后台模式,它允许你的APP在后台时运行任意代码。这个后台模式相较‘任意时长(whatever)’的更好,因为它没有时长限制即不限时。

更重要的,app若崩溃或者重启设备,APP仍然自动在后台运行。good。

当然,使用它的前提是:你的APP必须提供给用户VoIP功能才可以,否则,apple将会拒掉。

VoIP app的创建超出本文所述,但我会阐述基本。

VoIP:Voice over IP或者网络电话。本文中,你将建立一个简单app并链接至服务器,在后台时仍保持链接,app接收讯息时回调。

打开TBFifthViewController.m文件,添加如下属性声明:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_27_3582409" name="code" class="objc">@property (nonatomic, strong) NSInputStream *inputStream;
  2. @property (nonatomic, strong) NSOutputStream *outputStream;
  3. @property (nonatomic, strong) NSMutableString *communicationLog;
  4. @property (nonatomic) BOOL sentPing;</pre><br><br>

在@implementation TBFifthViewController之前,定义字串常量,在链接中会用到:

  1. const uint8_t pingString[] = "ping\n";
  2. const uint8_t pongString[] = "pong\n";

下面是一个简便方法添加到TextView上的。用它来判断是否链接完成,是否中断或者接收到讯息。

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_29_6308815" name="code" class="objc">- (void)addEvent:(NSString *)event
  2. {
  3. [self.communicationLog appendFormat:@"%@\n", event];
  4. if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
  5. {
  6. self.txtReceivedData.text = self.communicationLog;
  7. }
  8. else
  9. {
  10. NSLog(@"App is backgrounded. New event: %@", event);
  11. }
  12. }</pre><br><br>

app是active时其追加字串并更新UI,否则处在后台模式时仅打印在控制台上。

下面方法在用户点击连接按钮时调用:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_30_4291711" name="code" class="objc">- (IBAction)didTapConnect:(id)sender
  2. {
  3. if (!self.inputStream)
  4. {
  5. // 1
  6. CFReadStreamRef readStream;
  7. CFWriteStreamRef writeStream;
  8. CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(self.txtIP.text), [self.txtPort.text intValue], &readStream, &writeStream);
  9. // 2
  10. self.sentPing = NO;
  11. self.communicationLog = [[NSMutableString alloc] init];
  12. self.inputStream = (__bridge_transfer NSInputStream *)readStream;
  13. self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;
  14. [self.inputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
  15. // 3
  16. [self.inputStream setDelegate:self];
  17. [self.outputStream setDelegate:self];
  18. [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  19. [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  20. // 4
  21. [self.inputStream open];
  22. [self.outputStream open];
  23. // 5
  24. [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{
  25. if (self.outputStream)
  26. {
  27. [self.outputStream write:pingString maxLength:strlen((char*)pingString)];
  28. [self addEvent:@"Ping sent"];
  29. }
  30. }];
  31. }
  32. }</pre><br><br>

看起来有点复杂,因为创建了2条stream。

1,应用CFStreamCreatePairWithSocketToHost方法是最简便的方式,意味着此后更多的桥接(bridging)可以应用NSInputStream和NSOutputStream类。

2,以CFStreamCreatePairWithSocketToHost创建2个stream对后,将它们转换成oc类,setProperty:forKey方法调用非常重要,由此告知OS,app在后台时,链接仍需保持。该设置只需在input
stream完成即可。

3,设置s2个tream的委托对象为self,并将runloop设置为main
runloop。OS需要确定委托方法需要在哪个runloop下被调用,本例中最适合的runloop便是和app主线程相关的。为了在接收到信息后更新UI,故需要设置在main
runloop中。

4,设置好后,开启2个stream。

5,调用setKeepAliveTimeout:handler:,app在后台时将会定期调用handler。运行在其中做任何事情,但是它应当用于发送‘ping’到服务器,来保持链接可用。上述代码设置为每隔10
min进行ping操作(文档所示的最小值)。在此唯一做的便是ping服务器,并打印输出。

添加stream委托事件来接收链接更新:

  1. <pre code_snippet_id="268909" snippet_file_name="blog_20140401_31_2623271" name="code" class="objc">#pragma mark - NSStreamDelegate
  2. - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
  3. {
  4. switch (eventCode) {
  5. case NSStreamEventNone:
  6. // do nothing.
  7. break;
  8. case NSStreamEventEndEncountered:
  9. [self addEvent:@"Connection Closed"];
  10. break;
  11. case NSStreamEventErrorOccurred:
  12. [self addEvent:[NSString stringWithFormat:@"Had error: %@", aStream.streamError]];
  13. break;
  14. case NSStreamEventHasBytesAvailable:
  15. if (aStream == self.inputStream)
  16. {
  17. uint8_t buffer[1024];
  18. NSInteger bytesRead = [self.inputStream read:buffer maxLength:1024];
  19. NSString *stringRead = [[NSString alloc] initWithBytes:buffer length:bytesRead encoding:NSUTF8StringEncoding];
  20. stringRead = [stringRead stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
  21. [self addEvent:[NSString stringWithFormat:@"Received: %@", stringRead]];
  22. if ([stringRead isEqualToString:@"notify"])
  23. {
  24. UILocalNotification *notification = [[UILocalNotification alloc] init];
  25. notification.alertBody = @"New VOIP call";
  26. notification.alertAction = @"Answer";
  27. [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
  28. }
  29. else if ([stringRead isEqualToString:@"ping"])
  30. {
  31. [self.outputStream write:pongString maxLength:strlen((char*)pongString)];
  32. }
  33. }
  34. break;
  35. case NSStreamEventHasSpaceAvailable:
  36. if (aStream == self.outputStream && !self.sentPing)
  37. {
  38. self.sentPing = YES;
  39. if (aStream == self.outputStream)
  40. {
  41. [self.outputStream write:pingString maxLength:strlen((char*)pingString)];
  42. [self addEvent:@"Ping sent"];
  43. }
  44. }
  45. break;
  46. case NSStreamEventOpenCompleted:
  47. if (aStream == self.inputStream)
  48. {
  49. [self addEvent:@"Connection Opened"];
  50. }
  51. break;
  52. default:
  53. break;
  54. }
  55. }</pre><br><br>

该方法涵盖了链接中所有可能的事件,大多数是简单的自我说明的。

NSStreamEventHasBytesAvailable:一般发生在inputstream中,但是你必须要检测。读取数据至字串中,消去换行符等,打印出来。

附:如果事件是‘notify’,创建一个本地通知。在真实的VoIP中,该行为应是对应一个接入呼叫。即使在后台,本地通知亦展示出来。

如果是‘ping’,应该回应服务器‘pong’。

NSStreamEventHasSpaceAvailable:输出的数据为空,仅当首次接收到时,发送ping。

然后打开info.plist文件,在Required background Modes数组下添加App provides Voice over IP services:

运行项目前,需要一个测试服务器。你可以应用mac已有的工具,叫做netcat。可以实现简单的基于文本的服务器。

打开终端,键入:

  1. nc -l 10000

命令开始了一个在端口10000上运行的服务器,并开启监听链接,返回Xcode,运行项目:

若在模拟器上运行,可以设置地位为127.0.0.1,若真机测试,应找出测试mac的ip地址。

点击connect,链接正常开始的话,可以看到控制台的输出:

如果要测试通知命令,需要在真机上测试。有关通知的内容目前在模拟器上无法测试。

点击HOME进入后台,在终端发送ping,你仍可在控制台输出中看到pong的回应。、

如果运行在真机上,当app是后台模式时,试着发送nitify命令给它,可以看到:

演示项目:backgroundVoIP 。

何去何从?

你可下载关于本文的所有的资源文件:full Project

若想阅读关于本文技术相关的apple文档,请移步:Background
Execution and multitasking
。文档很好的的解释了每个后台模式,每个部分均有合适的外链文档。

本文特别值得关注的是:being
a responsible background app
。在发布你的具有后台模式的app之前,有一些可行/不可行的细节。

全部项目在my GitHub,下载,fork,并修复相关bug,祝愉快。

参考资料(戳这里):

>>  http://blog.csdn.net/chowpan/article/details/22417247

iOS的后台任务的更多相关文章

  1. iOS 允许后台任务吗?

    个人整理 1,用户层: 低电量模式 App后台数据刷新 的开关会影响App后台运行 2,   10分钟时间 后台任务: 在AppDelegate中加入以下代码:不受1影响 - (void)applic ...

  2. ios实现程序切入后台,实现后台任务 (转自)

    ,项目需求,是程序home键切入后台,3分钟后退出登陆, 首先,iOS 会再持续切入后台,给我们5秒钟的时间去处理相关数据,5秒后,程序不会再执行任何代码,处于挂起状态. - (void)applic ...

  3. ios实现程序切入后台,实现后台任务

    首先,iOS 会再持续切入后台,给我们5秒钟的时间去处理相关数据,5秒后,程序不会再执行任何代码,处于挂起状态. // 项目需求,按下Home切换后台后向服务器传一些数据,废话不多说,直接上代码 /* ...

  4. iOS 后台任务

    首先开启后台任务 使用设置后台任务触发的时机 application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIn ...

  5. ios实现无限后台任务

    需求 我们的app是使用心跳机制来保持用户的登陆状态,这样才能收到服务器发来的消息和命令,但是当app进入后台以后大约3分钟或者10分钟之后app就会被系统挂起,用户就会超时下线,这样就必须保持app ...

  6. iOS开发系列--音频播放、录音、视频播放、拍照、视频录制

    --iOS多媒体 概览 随着移动互联网的发展,如今的手机早已不是打电话.发短信那么简单了,播放音乐.视频.录音.拍照等都是很常用的功能.在iOS中对于多媒体的支持是非常强大的,无论是音视频播放.录制, ...

  7. iOS多线程主题

    下面是:2个并发进程.和2个并发线程的示意图: 下面介绍三种多线程技术(Thread.Cocoa Operation.Grand Central Dispatch): 1.最轻量级Thread(需要自 ...

  8. ios常用的第三方库

    ios开发中有可能用到的第三方库进行记录一下: 注:资料信息来源于网络 自己整理  https://developer.apple.com/reference(苹果官方文档) https://gith ...

  9. 关于iOS后台问题( 一 )(ios后台刷新,后台定位,后台下载,真后台)

    关于iOS的后台,以下引用一些文段进行一下脑补,请同学们大致看一下,有个基础,原文出处 -------------------------------------------------------- ...

随机推荐

  1. Key/Value存储系统etcd的特性

    etcd 是一个高可用的Key/Value存储系统,和其他KV存储系统不同的是,它的灵感来自于 ZooKeeper 和 Doozer,主要用于分享配置和服务发现.利用 etcd 的特性,应用程序可以在 ...

  2. 关于mvc ajax (post提交)——页面传值以及后台接收

    // 前段页面,点击按钮触发Success事件 function success() { var BusiName =“公司名称”; var UserName = “用户”; var UserPhon ...

  3. css响应有media,js响应有这种

    比如,我不想在移动端执行某js特效可以参考(function(doc, win) { var screenWidth = 0, size = 'M', root = doc.documentEleme ...

  4. Scrum7.0

    Sprint回顾 让我们一次比一次做得更好. 1.回顾组织 主题:“我们怎样才能在下个sprint中做的更好?” 时间:1个小时 参与者:整个团队 场所:课室 秘书:陈程 2.回顾流程 (1)spri ...

  5. Linq的一些很方便的方法

    Aggregate Aggregate我用的最多的地方就是拼接字符串,打个比方来说,如果有数组,想要的结果是在他们之间插入一个","然后返回拼接以后的新字符串. 常规的做法是: L ...

  6. 向python文件传递参数

    1. 向python传递单个参数: import sys print sys.argv[0]  ##脚本名 print sys.argv[1]  ## 第一个参数 2. 向python传递数组: pr ...

  7. [转]Android音频底层调试-基于tinyalsa

    http://blog.csdn.net/kangear/article/details/38139669 [-] 编译tinyalsa配套工具 查看当前系统的声卡 tinymix查看混响器 使用ti ...

  8. jquery实现幻灯片

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  9. django框架的models

    在django的框架设计中采用了mtv模型,即Model,template,viewer Model相对于传统的三层或者mvc框架来说就相当对数据处理层,它主要负责与数据的交互,在使用django框架 ...

  10. linux 定时执行php脚本

    第一种方法: 1.编写shell脚本: shell文件:/home/www/shell/phpshell.php #!/bin/bash while [ true ]; do /bin/sleep 1 ...