從2015年,接觸到的項目里,就會有這樣的需求:APP需要像Android那樣,在后台狀態下,執行正常的功能。到現在已經一年多了吧,一直在研究這個方面,寫下一些心得,希望與大家共同交流探討。
首先,我們要知道,蘋果對APP占用硬件資源管的很嚴,更不要說應用后台時候的資源占用了。正常情況下,使用應用時,APP從硬盤加載到內存,開始工作;當用戶按下home鍵,APP便被掛起,依然駐留在內存中,這種狀態下,不調用蘋果已開放的幾種后台方法,程序便不會運行;如果在這個時候,使程序繼續運行,則為后台狀態;如果當前內存將要不夠用時,系統會自動把之前掛起狀態下的APP請出內存。所以我們看到,有些時候打開APP時,還是上次退出時的那個頁面那些數據,有時則是重新從閃屏進入。
這樣,就知道了后台運行最大的前置條件——APP處於內存中的掛起狀態。
然后,再來看看上面說到的蘋果已開放的后台運行方法。先看這張圖
很明顯,我們項目里能用的機制就這么多,Background Audio,這是后台的音頻,這個很早之前便有,可以實現后台的聲音播放。去年的項目里用它在后台一直播放沒有聲音的文件,結果審核失敗。
在這里說一下去年做的那個項目的需求,用戶類型A可以在任何時刻查看用戶類型B的地理位置。這個功能有點像iPhone上的『查找朋友』,不知道的朋友請自行了解(想知道你的朋友在哪里嗎,想知道你的另一半在哪里嗎,對了,就用它);A想看B的時候,B需要上傳自己的當前位置給服務器;先不考慮APP在掛起狀態怎么做,先說APP在活動狀態下,服務器想和客戶端進行通信,告訴客戶端要上傳自己的位置了,這種服務器主動通信,常用到的就是socket和推送通知。我決定用推送,在APP收到來自APNS的推送時,就進行定位並上傳。
然而,按下home鍵進入掛起狀態時,程序是不會執行的,所以也獲取不到B的位置。BOSS大為惱火,Android分分鍾干完的事,你怎么就搞不定呢(腦補:再搞不出來就滾蛋)。
當時第一次接觸蘋果這些后台機制,探索之路彎彎曲曲,就不一一表述了。最后用靜默推送解決了這個問題:Remote Notification!原理非常簡單,不過蘋果的初衷不是讓我這樣用的……,說一下這個機制的應用場景:以往聊天類應用接受推送后點進去需要再收一次信息,這情況在QQ、微信等應用上最為明顯。不過擁有了這個接口后,這情況將不復存在,以后推送將能夠直接啟動后台任務,在后台就已經接收到信息,點開APP不需要去拉取。
so,在A查看B位置的時候,給B一條靜默推送,B在后台定位並上傳信息。這個喚起時間比較短,在3-5秒左右,有時候B網絡不好,沒有上傳成功就又被掛起了,就需要重復進行。這個機制添加方法和推送一樣,只有一點區別,就是委托方法不同。普通推送會執行這個回調:
1 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { 2 3 DDLogDebug(@"[普通推送]%@", userInfo); 4 }
而勾選住上面那個推送喚醒,就會回調這個方法:
1 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { 2 DDLogDebug(@"[后台推送]%@", userInfo); 3 completionHandler(UIBackgroundFetchResultNewData); 4 }
可以在這個回調里,調用定位請求之類的。
本以為解決了問題,BOSS試了,也覺得可以。就喜滋滋的等着升職加薪走上人生巔峰,咳咳,又做夢了。
"咋么回事呃,現在又定不到位了,趕緊搞好"還沒到兩個小時,BOSS氣呼呼的跑過來噴了一頓。說完之后的0.01秒,我就知道是怎么回事了,"聽我解釋啊,老板"還沒說出口,他就摔門而出,留下欲哭無淚的我。只有兩個原因,一、APP被人為上划kill掉;二、APP被系統回收了,kill 了。
就醬,明知山有虎,偏向虎山行。為了不讓系統回收APP,我非常強硬的,加上了Background Audio。結果可想而知,被拒絕的同時,收到一封英文郵件,問我為什么這樣做,如有異議,可提出。哎,總算松了一口氣,可以離職了(個人原因)。
換了公司,需求也不一樣了,APP需要每五秒和服務器進行一次數據交換;以下,用到的是VoIP。
剛開始的時候,推送喚醒機制還可以,不過林子大了什么鳥都有,一樣的型號一樣的設置,有台iPhone就是喚醒不了,只能嘗試新方法。想起QQ語音時,切到后台依然可以通話,我想,就是它了。VoIP:后台語音服務,類似Skype通話應用需要調用,可進行后台的語音通話。既然是語音通話,那么肯定是常連接,於是,有了以下代碼。
1 @implementation NSStream(StreamsToHost) 2 3 + (void)getStreamsToHostNamed:(NSString *)hostName 4 port:(UInt32)port 5 inputStream:(out __strong NSInputStream **)inputStreamPtr 6 outputStream:(out __strong NSOutputStream **)outputStreamPtr 7 { 8 CFReadStreamRef readStream; 9 CFWriteStreamRef writeStream; 10 11 assert(hostName != nil); 12 assert( (port > 0) && (port < 65536) ); 13 assert( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) ); 14 15 readStream = NULL; 16 writeStream = NULL; 17 18 CFStreamCreatePairWithSocketToHost( 19 NULL, 20 (__bridge CFStringRef) hostName, 21 port, 22 ((inputStreamPtr != NULL) ? &readStream : NULL), 23 ((outputStreamPtr != NULL) ? &writeStream : NULL) 24 ); 25 26 if (inputStreamPtr != NULL) { 27 *inputStreamPtr = CFBridgingRelease(readStream); 28 } 29 30 if (outputStreamPtr != NULL) { 31 *outputStreamPtr = CFBridgingRelease(writeStream); 32 } 33 } 34 35 @end
給NSStream加了一個類目。然后還需要一個server,我就不寫了;發起連接請求讓客戶端與server保持通信,這些代碼也太多了,就不貼了。勾選Voice over IP后,APP掛起狀態時,系統會接管socket會話句柄,當收到從server發來的數據流時,就會喚起APP進入后台執行代碼。這個喚起時間要長一些,可以在十秒多點。已經測試成功,但是還沒有提交審核,還需要給它一個外套,不然就像上次一樣被拒絕。
一直在探索,因為以上方法並不完美,而且項目對后台的要求比較苛刻,事實上,用戶在使用APP時,會有很多場景,最常見的就是弱網絡,在這個場景下,不管是推送還是socket都無法收到內容,所以像這種需要依賴外力喚起的方式,弊端還是相當明顯。
已經感覺到后面寫的比較倉促,VoIP涉及的內容還是比較多,還沒有一一吃透,還是心急了些。個人知識有限,如有錯誤,歡迎指正。