編寫:kesenhoo - 原文:http://developer.android.com/training/efficient-downloads/efficient-network-access.html
使用無(wú)線電波(wireless radio)進(jìn)行傳輸數(shù)據(jù)很可能是我們 app 最耗電的來(lái)源之一。為了最小化網(wǎng)絡(luò)連接對(duì)電量的消耗,懂得連接模式(connectivity model)會(huì)如何影響底層的無(wú)線電硬件設(shè)備是至關(guān)重要的。
這節(jié)課介紹了無(wú)線電波狀態(tài)機(jī)(wireless radio state machine),并解釋了 app 的連接模式是如何與狀態(tài)機(jī)進(jìn)行交互的。然后會(huì)提出建議的方法來(lái)最小化我們的數(shù)據(jù)連接,使用預(yù)?。╬refetching)與捆綁(bundle)的方式進(jìn)行數(shù)據(jù)的傳輸,這些操作都是為了最小化電量的消耗。
一個(gè)處于完全工作狀態(tài)的無(wú)線電會(huì)大量消耗電量,因此需要學(xué)習(xí)如何在不同能量狀態(tài)下進(jìn)行過(guò)渡,當(dāng)無(wú)線電沒有工作時(shí),節(jié)省電量,當(dāng)需要時(shí)嘗試最小化與無(wú)線電波供電有關(guān)的延遲。
典型的 3G 無(wú)線電網(wǎng)絡(luò)有三種能量狀態(tài):
在低功耗和空閑的狀態(tài)下,電量消耗會(huì)顯著減少。這里也會(huì)介紹重要的網(wǎng)絡(luò)請(qǐng)求延遲。從 low power 能量狀態(tài)返回到 full power 大概需要花費(fèi)1.5秒,從空閑能量狀態(tài)返回到 full power 狀態(tài)需要花費(fèi)2秒。
為了最小化延遲,狀態(tài)機(jī)使用了一種后滯過(guò)渡到更低能量狀態(tài)的機(jī)制。下圖是一個(gè)典型的 3G 無(wú)線電波狀態(tài)機(jī)的圖示(AT&T電信的一種制式)。
http://wiki.jikexueyuan.com/project/android-training-geek/images/mobile_radio_state_machine.png" alt="mobile_radio_state_machine.png" title="Figure 1. Typical 3G wireless radio state machine." />
Figure 1. 典型的 3G 無(wú)線電狀態(tài)機(jī)
在每一臺(tái)設(shè)備上的無(wú)線狀態(tài)機(jī),特別是相關(guān)的傳輸延遲(“拖尾時(shí)間”)和啟動(dòng)延遲,都會(huì)根據(jù)無(wú)線電波的制式(2G、3G、LTE等)不同而改變,并且由設(shè)備正在所使用的網(wǎng)絡(luò)進(jìn)行定義與配置。
這一課描述了一種典型的 3G 無(wú)線電波狀態(tài)機(jī),數(shù)據(jù)來(lái)源于 AT&T。無(wú)論如何,這些原理和最佳實(shí)踐結(jié)果是具有通用性的,在其他的無(wú)線電波上同樣適用。
這種方法在典型的網(wǎng)頁(yè)瀏覽操作上是特別有效的,因?yàn)樗梢宰柚褂脩粼跒g覽網(wǎng)頁(yè)時(shí)的一些不受歡迎的延遲。相對(duì)較短的拖尾時(shí)間也保證了當(dāng)一個(gè)網(wǎng)頁(yè)瀏覽會(huì)話結(jié)束的時(shí)候,無(wú)線電波可以轉(zhuǎn)移到相對(duì)較低的能量狀態(tài)。
不幸的是,這個(gè)方法會(huì)導(dǎo)致在現(xiàn)代的智能機(jī)系統(tǒng)例如 Android 上的 app 效率低下。因?yàn)? Android 上的 app 不僅僅可以在前臺(tái)運(yùn)行(重點(diǎn)關(guān)注延遲),也可以在后臺(tái)運(yùn)行(優(yōu)先處理耗電量)。(無(wú)線電波的狀態(tài)改變會(huì)影響到本來(lái)的設(shè)計(jì),有些想在前臺(tái)運(yùn)行的可能會(huì)因?yàn)榍袚Q到低能量狀態(tài)而影響程序效率。坊間說(shuō)手機(jī)在電量低的狀態(tài)下無(wú)線電波的強(qiáng)度會(huì)增大好幾倍來(lái)保證信號(hào),可能與這個(gè)有關(guān)。)
每次創(chuàng)建一個(gè)新的網(wǎng)絡(luò)連接,無(wú)線電波就切換到 full power 狀態(tài)。在上面典型的 3G 無(wú)線電波狀態(tài)機(jī)情況下,無(wú)線電波會(huì)在傳輸數(shù)據(jù)時(shí)保持在 full power 的狀態(tài),加上一個(gè)附加的5秒拖尾時(shí)間,再之后會(huì)經(jīng)過(guò)12秒進(jìn)入到 low power 能量狀態(tài)。因此對(duì)于典型的 3G 設(shè)備,每一次數(shù)據(jù)傳輸?shù)臅?huì)話都會(huì)導(dǎo)致無(wú)線電波消耗大概20秒時(shí)間來(lái)提取電能。
實(shí)際上,這意味著一個(gè)每18秒傳輸1秒非捆綁數(shù)據(jù)(unbundled data)的 app,會(huì)一直保持激活狀態(tài)(18 = 1秒的傳輸數(shù)據(jù) + 5秒過(guò)渡時(shí)間回到 low power + 12秒過(guò)渡時(shí)間回到standby)。因此,每分鐘會(huì)消耗18秒 high power 的電量,42秒 low power 的電量。
通過(guò)比較,同一個(gè) app,每分鐘傳輸持續(xù)3秒的捆綁數(shù)據(jù)(bundle data),會(huì)使得無(wú)線電波持續(xù)在 high power 狀態(tài)僅僅8秒,在 low power 狀態(tài)僅僅12秒鐘。
上面第二種傳輸捆綁數(shù)據(jù)(bundle data)的例子,可以看到減少了大量的電量消耗。圖示如下:
http://wiki.jikexueyuan.com/project/android-training-geek/images/graphs.png" alt="graphs.png" title="Figure 2. Relative wireless radio power use for bundled versus unbundled transfers." />
Figure 2. 無(wú)線電波使用捆綁數(shù)據(jù) vs 無(wú)線電波使用非捆綁數(shù)據(jù)
預(yù)取數(shù)據(jù)是一種減少獨(dú)立數(shù)據(jù)傳輸會(huì)話數(shù)量的有效方法。預(yù)取技術(shù)指的是在一定時(shí)間內(nèi),單次連接操作,以最大的下載能力來(lái)下載所有用戶可能需要的數(shù)據(jù)。
通過(guò)前面的傳輸數(shù)據(jù)的技術(shù),減少了大量下載數(shù)據(jù)所需的無(wú)線電波激活時(shí)間。這樣不僅節(jié)省了電量,也改善了延遲,降低了帶寬,減少了下載時(shí)間。
預(yù)取技術(shù)通過(guò)減少應(yīng)用里由于在執(zhí)行一個(gè)動(dòng)作或者查看數(shù)據(jù)之前等待下載完成造成的延遲,來(lái)提高用戶體驗(yàn)。
然而,過(guò)于頻繁地使用預(yù)取技術(shù),不僅僅會(huì)導(dǎo)致電量消耗快速增長(zhǎng),還有可能預(yù)取到一些并不需要的數(shù)據(jù),導(dǎo)致增加帶寬的使用和下載配額。另外,需要確保預(yù)取不會(huì)因?yàn)?app 等待預(yù)取全部完成而延遲應(yīng)用的啟動(dòng)。從實(shí)踐的角度,那意味著需要逐步處理數(shù)據(jù),或者按照優(yōu)先級(jí)順序開始進(jìn)行持續(xù)的數(shù)據(jù)傳遞,這樣會(huì)首先下載和處理應(yīng)用啟動(dòng)時(shí)需要的數(shù)據(jù)。
根據(jù)正在下載的數(shù)據(jù)大小與可能被用到的數(shù)據(jù)量來(lái)決定預(yù)取的頻率。作一個(gè)粗略的估計(jì),根據(jù)上面介紹的狀態(tài)機(jī),對(duì)于有50%的機(jī)會(huì)被當(dāng)前的用戶會(huì)話用到的數(shù)據(jù),我們可以預(yù)取大約6秒(大約1-2Mb),這大概使得潛在可能要用的數(shù)據(jù)量與可能已經(jīng)下載好的數(shù)據(jù)量相一致。
通常來(lái)說(shuō),預(yù)取1-5Mb會(huì)比較好,這種情況下,我們僅僅只需要每隔2-5分鐘開始另一段下載。
根據(jù)這個(gè)原理,大數(shù)據(jù)的下載,比如視頻文件,應(yīng)該每隔2-5分鐘開始另一段下載,這樣能有效的預(yù)取到下面幾分鐘內(nèi)的數(shù)據(jù)進(jìn)行預(yù)覽。
值得注意的是,更進(jìn)一步的下載應(yīng)該是是捆綁的(bundled),下一小節(jié)將會(huì)講到,批量處理傳送和連接,而且上面那些大概的數(shù)據(jù)與時(shí)間可能會(huì)根據(jù)網(wǎng)絡(luò)連接的類型與速度有所變化,這將在根據(jù)網(wǎng)絡(luò)連接類型來(lái)調(diào)整下載模式講到。
讓我們來(lái)看一些例子:
一個(gè)音樂播放器
我們可以選擇預(yù)取整個(gè)專輯,然而這樣在第一首歌曲之后用戶會(huì)停止聽歌,那么就浪費(fèi)了大量的帶寬和電量。
一個(gè)比較好的方法是維護(hù)正在播放的那首歌曲的緩沖區(qū)。對(duì)于流媒體音樂,不應(yīng)該去維護(hù)一段連續(xù)的數(shù)據(jù)流,因?yàn)檫@樣會(huì)使得無(wú)線電波一直保持激活狀態(tài),而應(yīng)該考慮用 HTTP 流直播來(lái)集中傳輸音頻流,就像上面描述的預(yù)取技術(shù)一樣(下載好2Mb,然后開始一次取出,再去下載下面的2Mb)。
一個(gè)新聞閱讀器
許多新聞 app 嘗試通過(guò)只下載新聞標(biāo)題來(lái)減少帶寬,完整的文章僅在用戶想要讀取的時(shí)候再去讀取,而且文章也會(huì)因?yàn)樘L(zhǎng)而剛開始只顯示部分信息,等用戶下滑時(shí)再去讀取完整信息。
使用這個(gè)方法,無(wú)線電波僅僅會(huì)在用戶點(diǎn)擊更多信息的時(shí)候才會(huì)被激活。但是,在切換文章分類預(yù)閱讀文章的時(shí)候仍然會(huì)造成大量潛在的消耗。
一個(gè)比較好的方法是在啟動(dòng)的時(shí)候預(yù)取一個(gè)合理數(shù)量的數(shù)據(jù),比如在啟動(dòng)的時(shí)候預(yù)取第一條新聞的標(biāo)題與縮略圖信息,確保較短的啟動(dòng)時(shí)間。之后繼續(xù)獲取剩余新聞的標(biāo)題和縮略圖信息。同時(shí)獲取至少在主要標(biāo)題列表中可用的每篇文章的文本。
另一個(gè)方法是預(yù)取所有的標(biāo)題,縮略信息,文章文字,甚至是所有文章的圖片——根據(jù)既設(shè)的后臺(tái)程序進(jìn)行逐一獲取。這樣做的風(fēng)險(xiǎn)是花費(fèi)了大量的帶寬與電量去下載一些不會(huì)閱讀到的內(nèi)容,因此應(yīng)該謹(jǐn)慎使用這種方法。
其中的一個(gè)解決方案是,僅當(dāng)在連接至Wi-Fi或者設(shè)備正在充電時(shí),調(diào)度到 Full power 狀態(tài)進(jìn)行下載。關(guān)于這個(gè)細(xì)節(jié)的實(shí)現(xiàn),我們將在后面的根據(jù)網(wǎng)絡(luò)連接類型來(lái)調(diào)整下載模式課程中介紹。
每次發(fā)起一個(gè)連接——不論相關(guān)傳送數(shù)據(jù)的大小——當(dāng)使用典型的 3G 無(wú)線網(wǎng)絡(luò)時(shí),可能會(huì)導(dǎo)致無(wú)線電波消耗大約20秒的電量。
一個(gè) app 每20秒 ping 一次服務(wù)器,僅僅是為了確認(rèn) app 正在運(yùn)行和對(duì)用戶可見,那么無(wú)線電波會(huì)無(wú)限期地處于開啟狀態(tài),導(dǎo)致即使在沒有實(shí)際數(shù)據(jù)傳輸?shù)那闆r下,仍會(huì)消耗大量電量。
因此,對(duì)傳送的數(shù)據(jù)進(jìn)行捆綁操作和創(chuàng)建一個(gè)等待傳輸隊(duì)列就顯得非常重要。操作正確的話,可以使得大量的數(shù)據(jù)集中進(jìn)行發(fā)送,這樣使得無(wú)線電波的激活時(shí)間盡可能的少,同時(shí)減少大部分電量的花費(fèi)。
這樣做的潛在好處是盡可能在每次傳輸數(shù)據(jù)的會(huì)話中盡可能多的傳輸數(shù)據(jù)而且減少了會(huì)話的次數(shù)。
那就意味著我們應(yīng)該通過(guò)隊(duì)列延遲容忍傳送來(lái)批量處理我們的傳輸數(shù)據(jù),和搶占調(diào)度更新和預(yù)取,使得當(dāng)要求時(shí)間敏感傳輸時(shí),數(shù)據(jù)會(huì)被全部執(zhí)行。同樣地,我們的計(jì)劃更新和定期的預(yù)取應(yīng)該開啟等待傳輸隊(duì)列的執(zhí)行工作。
預(yù)取數(shù)據(jù)部分有一個(gè)實(shí)際的例子。
以上述使用定期預(yù)取的新聞應(yīng)用為例。新聞閱讀器收集分析用戶的信息來(lái)了解用戶的閱讀模式,并按照新聞報(bào)道的受歡迎程度對(duì)新聞進(jìn)行排序。為了保證新聞最新,應(yīng)用每個(gè)小時(shí)會(huì)檢查更新一次。為了節(jié)省帶寬,預(yù)取縮略圖信息和當(dāng)用戶選擇某個(gè)新聞時(shí)下載全部圖片,而不去下載每篇文章的所有圖片。
在這個(gè)例子中,所有在 app 中收集到的分析信息應(yīng)該捆綁在一起并放入下載隊(duì)列,而不是一收集到信息就傳輸。當(dāng)下載完一張全尺寸的圖片或者執(zhí)行每小時(shí)一次更新時(shí),應(yīng)該傳輸捆綁好的數(shù)據(jù)。
任何時(shí)間敏感或者按需的傳輸——例如下載全尺寸圖片——應(yīng)該搶占定期更新。計(jì)劃好的更新應(yīng)該與按需傳送在同一時(shí)間執(zhí)行。這個(gè)方法減小了執(zhí)行一個(gè)定期更新的開銷,該定期更新通過(guò)下載必要的時(shí)間敏感圖片的背負(fù)式傳輸實(shí)現(xiàn)。
通常來(lái)說(shuō),重用已經(jīng)存在的網(wǎng)絡(luò)連接比起重新建立一個(gè)新的連接更有效率。重用網(wǎng)絡(luò)連接同樣可以使得在擁擠不堪的網(wǎng)絡(luò)環(huán)境中進(jìn)行更加智能地作出反應(yīng)。
當(dāng)可以捆綁所有請(qǐng)求在一個(gè) GET 里面的時(shí)候,不要同時(shí)創(chuàng)建多個(gè)網(wǎng)絡(luò)連接或者把多個(gè) GET 請(qǐng)求進(jìn)行串聯(lián)。
例如,可以一起請(qǐng)求所有文章的情況下,不要根據(jù)多個(gè)新聞會(huì)話進(jìn)行多次請(qǐng)求。為傳輸與服務(wù)端和客戶端 timeout 相關(guān)的終止 / 終止確認(rèn)數(shù)據(jù)包,無(wú)線電波會(huì)保持激活狀態(tài),所以如果不需要使用連接時(shí),請(qǐng)立即關(guān)閉,而不是等待他們 timeout。
之前說(shuō)道,如果過(guò)早對(duì)一個(gè)連接執(zhí)行關(guān)閉操作,會(huì)導(dǎo)致需要額外的開銷來(lái)建立一個(gè)新的連接。一個(gè)有用的妥協(xié)是不要立即關(guān)閉連接,而是在固定期間的 timeout 之前關(guān)閉(即稍微晚點(diǎn)卻又不至于到 timeout)。
Android DDMS (Dalvik Debug Monitor Server) 包含了一個(gè)查看網(wǎng)絡(luò)使用詳情的欄目來(lái)允許跟蹤 app 的網(wǎng)絡(luò)請(qǐng)求。使用這個(gè)工具,可以監(jiān)測(cè) app 是在何時(shí),如何傳輸數(shù)據(jù)的,從而進(jìn)行代碼的優(yōu)化。
Figure 3 顯示了傳輸少量數(shù)據(jù)的網(wǎng)絡(luò)模型,可以看到每次差不多相隔15秒,這意味著可以通過(guò)預(yù)取技術(shù)或者批量上傳來(lái)大幅提高效率。
http://wiki.jikexueyuan.com/project/android-training-geek/images/DDMS.png" alt="DDMS.png" title="Figure 3. Tracking network usage with DDMS." />
Figure 3. 使用 DDMS 檢測(cè)網(wǎng)絡(luò)使用情況
通過(guò)監(jiān)測(cè)數(shù)據(jù)傳輸?shù)念l率與每次傳輸?shù)臄?shù)據(jù)量,可以查看出哪些位置應(yīng)該進(jìn)行優(yōu)化。通常的,我們會(huì)尋找類似短穗狀的地方,這些位置可以延遲,或者應(yīng)該導(dǎo)致一個(gè)后來(lái)的傳輸被搶占。
為了更好的檢測(cè)出問題所在,Traffic Status API 允許我們使用 TrafficStats.setThreadStatsTag() 方法標(biāo)記數(shù)據(jù)傳輸發(fā)生在某個(gè)Thread里面,然后可以手動(dòng)地使用 tagSocket() 進(jìn)行標(biāo)記或者使用 untagSocket()` 來(lái)取消標(biāo)記,例如:
TrafficStats.setThreadStatsTag(0xF00D);
TrafficStats.tagSocket(outputSocket);
// Transfer data using socket
TrafficStats.untagSocket(outputSocket);
Apache 的 HttpClient 與 URLConnection 庫(kù)可以根據(jù)當(dāng)前的 getThreadStatusTag() 值自動(dòng)給 sockets 加上標(biāo)記。那些庫(kù)在通過(guò) keep-alive pools 循環(huán)的時(shí)候也會(huì)為 sockets 加上或者取消標(biāo)簽。
TrafficStats.setThreadStatsTag(0xF00D);
try {
// Make network request using HttpClient.execute()
} finally {
TrafficStats.clearThreadStatsTag();
}
給 Socket 加上標(biāo)簽(Socket tagging)是在 Android 4.0 上才被支持的, 但是實(shí)際情況是僅僅會(huì)在運(yùn)行Android 4.0.3 或者更高版本的設(shè)備上才會(huì)顯示。