Android O 除了提供諸多新特性和功能外,還對系統(tǒng)和 API 行為做出了各種變更。本文重點(diǎn)介紹您應(yīng)該了解并在開發(fā)應(yīng)用時(shí)加以考慮的一些主要變更。
其中大部分變更會影響所有應(yīng)用,而不論應(yīng)用針對的是何種版本的 Android。不過,有幾項(xiàng)變更僅影響針對 Android O 的應(yīng)用。為清楚起見,本頁面分為兩個(gè)部分:針對所有 API 級別的應(yīng)用和針對 Android O 的應(yīng)用。
針對所有 API 級別的應(yīng)用
這些行為變更適用于在 Android O 平臺上運(yùn)行的所有應(yīng)用,無論這些應(yīng)用是針對哪個(gè) API 級別構(gòu)建。所有開發(fā)者都應(yīng)查看這些變更,并修改其應(yīng)用以正確支持這些變更(如果適用)。
網(wǎng)絡(luò)連接和 HTTP(S) 連接
Android O 對網(wǎng)絡(luò)連接和 HTTP(S) 連接行為做出了以下變更:
無正文的 OPTIONS 請求具有Content-Length: 0標(biāo)頭。之前,這些請求沒有Content-Length標(biāo)頭。
HttpURLConnection 在包含斜線的主機(jī)或頒發(fā)機(jī)構(gòu)名稱后面附加一條斜線,使包含空路徑的網(wǎng)址規(guī)范化。例如,它將http://example.com轉(zhuǎn)化為http://example.com/。
通過 ProxySelector.setDefault ( ) 設(shè)置的自定義代理選擇器僅針對所請求的網(wǎng)址(架構(gòu)、主機(jī)和端口)。因此,僅可根據(jù)這些值選擇代理。傳遞至自定義代理選擇器的網(wǎng)址不包含所請求的網(wǎng)址的路徑、查詢參數(shù)或片段。
URI 不能包含空白標(biāo)簽。
之前,平臺支持一種權(quán)宜方法,即允許主機(jī)名稱中包含空白標(biāo)簽,但這是對 URI 的非法使用。此權(quán)宜方法只是為了確保與舊版 libcore 兼容。開發(fā)者如果對 API 使用不當(dāng),將會看到一條 ADB 消息:“URI example..com 的主機(jī)名包含空白標(biāo)簽。此格式不正確,將不被未來的 Android 版本所接受。”Android O 廢除了此權(quán)宜方法;系統(tǒng)對格式錯(cuò)誤的 URI 會返回 null。
Android O 在實(shí)現(xiàn) HttpsURLConnection 時(shí)不會執(zhí)行不安全的 TLS/SSL 協(xié)議版本回退。
對隧道 HTTP(S) 連接處理進(jìn)行了如下變更:
在通過連接建立隧道 HTTP(S) 連接時(shí),系統(tǒng)會在 Host 行中正確放置端口號 (:443) 并將此信息發(fā)送至中間服務(wù)器。之前,端口號僅出現(xiàn)在 CONNECT 行中
系統(tǒng)不再將隧道連接請求中的 user-agent 和 proxy-authorization 標(biāo)頭發(fā)送至代理服務(wù)器。
在建立隧道時(shí),系統(tǒng)不再將隧道 Http(s)URLConnection 中的 proxy-authorization 標(biāo)頭發(fā)送至代理。相反,由系統(tǒng)生成 proxy-authorization 標(biāo)頭,在代理響應(yīng)初始請求發(fā)送 HTTP 407 后將其發(fā)送至此代理。
同樣地,系統(tǒng)不再將 user-agent 標(biāo)頭由隧道連接請求復(fù)制到建立隧道的代理請求。相反,庫為此請求生成 user-agent 標(biāo)頭。
如果之前執(zhí)行的 connect ( ) 函數(shù)失敗,send( java.net.DatagramPacket )函數(shù)將會引發(fā) SocketException:
如果存在內(nèi)部錯(cuò)誤,DatagramSocket.connect ( ) 會引發(fā) pendingSocketException。對于 Android O 之前的版本,即使 send ( ) 調(diào)用成功,后續(xù)的 recv ( ) 調(diào)用也會引發(fā) SocketException。為確保一致性,現(xiàn)在這兩個(gè)調(diào)用均會引發(fā) SocketException。
在回退到 TCP Echo 協(xié)議之前,InetAddress.isReachable ( ) 會嘗試執(zhí)行 ICMP:
對于某些屏蔽端口 7 (TCP Echo) 的主機(jī)(例如 google.com),如果它們接受 ICMP Echo 協(xié)議,現(xiàn)在也許能夠訪問它們。
對于確實(shí)無法訪問的主機(jī),此項(xiàng)變更意味著調(diào)用需要兩倍的時(shí)間才能返回結(jié)果。
集合的處理
現(xiàn)在,AbstractCollection.removeAll ( )和AbstractCollection.retainAll ( )始終引發(fā)NullPointerException;之前,當(dāng)集合為空時(shí)不會引發(fā)NullPointerException。此項(xiàng)變更使行為符合文檔要求。
記錄未捕獲的異常
如果某個(gè)應(yīng)用安裝的Thread.UncaughtExceptionHandler未移交給默認(rèn)的Thread.UncaughtExceptionHandler,則當(dāng)出現(xiàn)未捕獲的異常時(shí),系統(tǒng)不會終止應(yīng)用。從 Android O 開始,在此情況下系統(tǒng)將記錄異常堆棧跟蹤情況;在之前的平臺版本中,系統(tǒng)不會記錄異常堆棧跟蹤情況。
我們建議,自定義Thread.UncaughtExceptionHandler實(shí)現(xiàn)始終移交給默認(rèn)處理程序處理;遵循此建議的應(yīng)用不受 Android O 此項(xiàng)變更的影響。
輸入和導(dǎo)航
隨著 Android 應(yīng)用出現(xiàn)在 Chrome 操作系統(tǒng)和平板電腦等其他大尺寸設(shè)備上,我們看到,用戶在 Android 應(yīng)用中又重新開始使用鍵盤導(dǎo)航。在 Android O 中,我們又再次使用鍵盤作為導(dǎo)航輸入設(shè)備,從而為基于箭頭鍵和 Tab 鍵的導(dǎo)航構(gòu)建了一種更可靠并且可預(yù)測的模型。
尤其要指出的是,我們對元素焦點(diǎn)行為做出以下變更:
現(xiàn)在,如果您沒有為View對象(前景或背景圖片)定義任何焦點(diǎn)狀態(tài)顏色,框架會為View設(shè)置默認(rèn)的焦點(diǎn)突出顯示顏色。此焦點(diǎn)突出顯示標(biāo)志是基于操作組件主題背景的漣漪圖片。
如果您不希望View對象在接收焦點(diǎn)時(shí)使用此默認(rèn)突出顯示標(biāo)志,請?jiān)诎琕iew的布局 XML 文件中將android:defaultFocusHighlightEnabled屬性設(shè)置為false,或者將false傳遞至應(yīng)用界面邏輯中的setDefaultFocusHighlightEnabled ( )。
要測試鍵盤輸入對界面元素焦點(diǎn)有何影響,您可以啟用 Drawing > Show layout bounds開發(fā)者選項(xiàng)。在 Android O 中,此選項(xiàng)在當(dāng)前具有焦點(diǎn)的元素上顯示一個(gè) “X” 圖標(biāo)。
另外,Android O 中的所有工具欄元素自動組成鍵盤導(dǎo)航鍵區(qū),用戶可以更加輕松地導(dǎo)航進(jìn)入和離開每個(gè)作為一個(gè)整體的工具欄。
如需詳細(xì)了解如何在您的應(yīng)用中改善對鍵盤導(dǎo)航的支持,請閱讀以下鏈接中的支持鍵盤導(dǎo)航指南。
(https://developer.android.google.cn/training/keyboard-input/navigation.html)
安全性
Android O 包含以下與安全性有關(guān)的變更:
此平臺不再支持 SSLv3
Android O 將使用安全計(jì)算 (SECCOMP) 過濾器來過濾所有應(yīng)用。允許的系統(tǒng)調(diào)用列表僅限于通過 bionic 公開的系統(tǒng)調(diào)用。此外,還提供了其他幾個(gè)后向兼容的系統(tǒng)調(diào)用,但我們不建議使用這些系統(tǒng)調(diào)用。
在與未正確實(shí)現(xiàn) TLS 協(xié)議版本協(xié)商的服務(wù)器建立 HTTPS 連接時(shí),HttpsURLConnection不再嘗試回退到之前的 TLS 協(xié)議版本并重試的權(quán)宜方法。
現(xiàn)在,您的應(yīng)用的WebView對象將在多進(jìn)程模式下運(yùn)行。網(wǎng)頁內(nèi)容在獨(dú)立的進(jìn)程中處理,此進(jìn)程與包含應(yīng)用的進(jìn)程相隔離,以提高安全性。
您無法再假定 APK 駐留在名稱以 -1 或 -2 結(jié)尾的目錄中。應(yīng)用應(yīng)使用 sourceDir 獲取此目錄,而不能直接使用目錄格式。
有關(guān)提升應(yīng)用安全性的其他準(zhǔn)則,請參閱以下鏈接中的面向 Android 開發(fā)者的安全性。
(https://developer.android.google.cn/topic/security/index.html)
后臺執(zhí)行限制
Android O 為提高電池續(xù)航時(shí)間而引入的變更之一是,當(dāng)您的應(yīng)用進(jìn)入已緩存狀態(tài)時(shí),如果沒有活動的組件,系統(tǒng)將解除應(yīng)用具有的所有喚醒鎖。
此外,為提高設(shè)備性能,系統(tǒng)會限制未在前臺運(yùn)行的應(yīng)用的某些行為。具體而言:
現(xiàn)在,在后臺運(yùn)行的應(yīng)用對后臺服務(wù)的訪問受到限制。
應(yīng)用無法使用其清單注冊大部分隱式廣播(即,并非專門針對此應(yīng)用的廣播)。
Android O 還對特定函數(shù)做出了以下變更:
如果針對 Android O 的應(yīng)用嘗試在不允許其創(chuàng)建后臺服務(wù)的情況下使用startService ( )函數(shù),則該函數(shù)將引發(fā)一個(gè)IllegalStateException。
新的Context.startForegroundService ( )函數(shù)將啟動一個(gè)前臺服務(wù)?,F(xiàn)在,即使應(yīng)用在后臺運(yùn)行,系統(tǒng)也允許其調(diào)用Context.startForegroundService ( )。不過,應(yīng)用必須在創(chuàng)建服務(wù)后的五秒內(nèi)調(diào)用該服務(wù)的startForeground ( )函數(shù)。
如需了解詳細(xì)信息,請參閱以下鏈接中的后臺執(zhí)行限制。
(https://developer.android.google.cn/preview/features/background.html)
隱私性
Android O 對平臺做出了以下與隱私性有關(guān)的變更:
現(xiàn)在,平臺改變了標(biāo)識符的處理方式:
對于在 OTA 之前安裝到某個(gè)版本 Android O(API 級別 26)的應(yīng)用,除非在 OTA 后卸載并重新安裝,否則ANDROID_ID的值將保持不變。要在 OTA 后在卸載期間保留值,開發(fā)者可以使用密鑰/值備份關(guān)聯(lián)舊值和新值。
對于安裝在運(yùn)行 Android O 的設(shè)備上的應(yīng)用,ANDROID_ID的值現(xiàn)在將根據(jù)應(yīng)用簽署密鑰和用戶確定作用域。應(yīng)用簽署密鑰、用戶和設(shè)備的每個(gè)組合都具有唯一的ANDROID_ID值。因此,在相同設(shè)備上運(yùn)行但具有不同簽署密鑰的應(yīng)用將不會再看到相同的 Android ID(即使對于同一用戶來說,也是如此)。
只要簽署密鑰相同(并且應(yīng)用未在 OTA 之前安裝到某個(gè)版本的 O),ANDROID_ID的值在軟件包卸載或重新安裝時(shí)就不會發(fā)生變化。
即使系統(tǒng)更新導(dǎo)致軟件包簽署密鑰發(fā)生變化,ANDROID_ID的值也不會變化。
要借助一個(gè)簡單的標(biāo)準(zhǔn)系統(tǒng)實(shí)現(xiàn)應(yīng)用獲利,請使用廣告 ID。廣告 ID 是 Google Play 服務(wù)針對廣告服務(wù)提供的唯一 ID,此 ID 可由用戶重置。
查詢net.hostname系統(tǒng)屬性返回的結(jié)果為空。
針對 Android O 的應(yīng)用
這些行為變更專門應(yīng)用于針對 O 平臺或更高平臺版本的應(yīng)用。針對 Android O 或更高平臺版本進(jìn)行編譯,或?qū)argetSdkVersion設(shè)為 Android O 或更高版本的應(yīng)用開發(fā)者必須修改其應(yīng)用以正確支持這些行為(如果適用)。
內(nèi)容變更通知
Android O 更改了ContentResolver.notifyChange ( )和registerContentObserver ( Uri, boolean, ContentObserver )在針對 Android O 的應(yīng)用中的行為方式。
現(xiàn)在,這些 API 需要在所有 URI 中為頒發(fā)機(jī)構(gòu)定義一個(gè)有效的 ContentProvider。使用相關(guān)權(quán)限定義一個(gè)有效的ContentProvider可幫助您的應(yīng)用防范來自惡意應(yīng)用的內(nèi)容變更,并防止將可能的私密數(shù)據(jù)泄露給惡意應(yīng)用。
視圖焦點(diǎn)
可點(diǎn)擊的View對象現(xiàn)在默認(rèn)也可以成為焦點(diǎn)。如果您希望View對象可點(diǎn)擊但不可成為焦點(diǎn),請?jiān)诎琕iew的布局 XML 文件中將android:focusable屬性設(shè)置為false,或者將false傳遞至應(yīng)用界面邏輯中的setFocusable ( )。
權(quán)限
在 Android O 之前,如果應(yīng)用在運(yùn)行時(shí)請求權(quán)限并且被授予該權(quán)限,系統(tǒng)會錯(cuò)誤地將屬于同一權(quán)限組并且在清單中注冊的其他權(quán)限也一起授予應(yīng)用。
對于針對 Android O 的應(yīng)用,此行為已被糾正。系統(tǒng)只會授予應(yīng)用明確請求的權(quán)限。然而,一旦用戶為應(yīng)用授予某個(gè)權(quán)限,則所有后續(xù)對該權(quán)限組中權(quán)限的請求都將被自動批準(zhǔn)。
例如:
假設(shè)某個(gè)應(yīng)用在其清單中列出READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。應(yīng)用請求READ_EXTERNAL_STORAGE,并且用戶授予了該權(quán)限。
如果該應(yīng)用針對的是 API 級別 24 或更低級別,系統(tǒng)還會同時(shí)授予WRITE_EXTERNAL_STORAGE,因?yàn)樵摍?quán)限也屬于同一STORAGE權(quán)限組并且也在清單中注冊過。
如果該應(yīng)用針對的是 Android O,則系統(tǒng)此時(shí)僅會授予READ_EXTERNAL_STORAGE;不過,如果該應(yīng)用后來又請求WRITE_EXTERNAL_STORAGE,則系統(tǒng)會立即授予該權(quán)限,而不會提示用戶。
集合的處理
在 Android O 中,Collections.sort ( )是在List.sort ( )的基礎(chǔ)上實(shí)現(xiàn)的。在 Android 7.x(API 級別 24 和 25)中,則恰恰相反。在過去,List.sort ( )的默認(rèn)實(shí)現(xiàn)會調(diào)用Collections.sort ( )。
此項(xiàng)變更使Collections.sort ( )可以利用優(yōu)化的List.sort ( )實(shí)現(xiàn),但具有以下限制:
List.sort ( )的實(shí)現(xiàn)不能調(diào)用Collections.sort ( ),因?yàn)檫@會導(dǎo)致堆棧因無限遞歸而溢出。相反,如果您需要 List 實(shí)現(xiàn)的默認(rèn)行為,應(yīng)避免重寫 sort()。
如果父類以不適當(dāng)?shù)姆椒▽?shí)現(xiàn)sort ( ),通常最好使用在List.toArray ( )、Arrays.sort ( )和ListIterator.set ( )的基礎(chǔ)上構(gòu)建的實(shí)現(xiàn)重寫List.sort ( )。
例如:
@Override
publicvoidsort(Comparatorc){
Object[]elements=toArray();
Arrays.sort(elements,c);
ListIteratoriterator=(ListIterator)listIterator();
for(Objectelement:elements){
iterator.next();
iterator.set((E)element);
}
}
在大多數(shù)情況下,您也可以使用根據(jù) API 級別委托給其他默認(rèn)實(shí)現(xiàn)的實(shí)現(xiàn)重寫List.sort ( )
例如:
@Override
publicvoidsort(Comparatorcomparator){
if(Build.VERSION.SDK_INT<=25){
Collections.sort(this);
}else{
super.sort(comparator);
}
}
如果您選擇后者只是因?yàn)槟M_發(fā)一種適用于所有 API 級別的sort ( )函數(shù),可以考慮賦予其一個(gè)唯一的名稱,例如sortCompat ( ),而不是重寫sort ( )。
現(xiàn)在,Collections.sort ( )只是對調(diào)用sort ( )的 List 實(shí)現(xiàn)進(jìn)行的一項(xiàng)結(jié)構(gòu)性修改。例如,在 Android O 之前的平臺版本中,如果通過調(diào)用List.sort ( )進(jìn)行排序,則當(dāng)?shù)幚鞟rrayList以及在迭代過程中調(diào)用sort ( )時(shí),會引發(fā)ConcurrentModificationException。而Collections.sort ( )則不會引發(fā)異常。
此項(xiàng)變更使平臺行為更加一致:現(xiàn)在,兩種方法都會引發(fā)ConcurrentModificationException。
媒體
框架會執(zhí)行音頻閃避。進(jìn)行AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK時(shí),應(yīng)用不會失去焦點(diǎn)。新的 API 適用于需要暫停而不是閃避的應(yīng)用。請注意,此行為無法在 Android O Developer Preview 1 版本中實(shí)現(xiàn)。
當(dāng)用戶打電話時(shí),活動的媒體流將在通話期間靜音。
所有與音頻相關(guān)的 API 都應(yīng)使用AudioAttributes而不是音頻流類型來說明音頻播放用例。僅為音量控制繼續(xù)使用音頻流類型。流類型(例如,已棄用的AudioTrack constructor)的其他用途仍然有效,但是系統(tǒng)會將其記錄為錯(cuò)誤。
使用AudioTrack時(shí),如果應(yīng)用請求了足夠大的音頻緩沖區(qū),則框架將嘗試使用深度緩沖區(qū)輸出(如果可用)。
在 Android O 中,媒體按鈕事件的處理有所不同:
在界面操作組件中處理媒體按鈕未發(fā)生變化:前臺操作組件在處理媒體按鈕時(shí)仍然優(yōu)先。
如果前臺操作組件不處理媒體按鈕,系統(tǒng)會將媒體按鈕路由到最近在本地播放音頻的應(yīng)用。在確定哪些應(yīng)用接收媒體按鈕事件時(shí),不再考慮活動狀態(tài)、標(biāo)志和媒體會話的播放狀態(tài)。即使在應(yīng)用調(diào)用setActive( false )后,媒體會話仍然可以接收媒體按鈕事件。
如果應(yīng)用的媒體會話已經(jīng)釋放,系統(tǒng)會將媒體按鈕事件發(fā)送到應(yīng)用的MediaButtonReceiver(如果有)。
對于任何其他情況,系統(tǒng)都會舍棄媒體按鈕事件。與其開始播放錯(cuò)誤的應(yīng)用,不如不播放任何東西。
下圖匯總了新的媒體按鈕路由邏輯:
類加載行為
Android O 檢查確保類加載器在加載新類時(shí)不會違反運(yùn)行時(shí)假設(shè)條件。不論類引用自 Java(來自forName ( ))、Dalvik 字節(jié)碼還是 JNI,都會執(zhí)行這些檢查。平臺不會攔截 Java 對loadClass ( )函數(shù)的直接調(diào)用,也不會檢查此類調(diào)用的結(jié)果。此行為不應(yīng)影響運(yùn)行良好的類加載器的正常運(yùn)行。
平臺將檢查類加載器返回的類描述符是否與預(yù)期的描述符一致。如果返回的描述符與預(yù)期不符,平臺會引發(fā)NoClassDefFoundError錯(cuò)誤,并在異常日志中存儲一條注明不一致之處的詳細(xì)錯(cuò)誤消息。
平臺還檢查請求的類描述符是否有效。此檢查捕獲間接加載諸如GetFieldID ( )等類的 JNI 調(diào)用,向這些類傳遞無效的描述符。例如,找不到包含java/lang/String簽名的字段,是因?yàn)榇撕灻麩o效;它應(yīng)為Ljava/lang/String;。
這與 JNI 對FindClass ( )的調(diào)用不同,其中java/lang/String是一個(gè)有效的完全限定名稱。
Android O 不支持多個(gè)類加載器同時(shí)嘗試使用相同的 DexFile 對象來定義類。嘗試進(jìn)行此操作,會導(dǎo)致 Android 運(yùn)行時(shí)引發(fā)InternalError錯(cuò)誤,同時(shí)顯示消息 “Attempt to register dex filewith multiple class loaders” 。
DexFile API 現(xiàn)已棄用,強(qiáng)烈建議您改為使用此平臺的類加載器之一,包括PathClassLoader或BaseDexClassLoader。
注:您可以創(chuàng)建多個(gè)引用文件系統(tǒng)中同一個(gè) APK 或 JAR 文件容器的類加載器。這樣做通常不會占用大量內(nèi)存:如果存儲而不壓縮容器中的 DEX 文件,平臺可以對此類文件執(zhí)行 mmap 操作,而不直接提取它們。但是,如果平臺必須從容器中提取 DEX 文件,以這種方式引用 DEX 文件可能占用大量內(nèi)存。
在 Android 中,所有類加載器都被視為支持并行運(yùn)行。當(dāng)多個(gè)線程爭用同一個(gè)類加載器加載相同的類時(shí),第一個(gè)完成此操作的線程勝出,而操作結(jié)果將用于其他線程。無論類加載器是返回同一個(gè)類、返回不同的類還是引發(fā)異常,都將發(fā)生此行為。該平臺靜默忽略此類異常。
注意:在低于 Android O 的平臺版本中,違反這些假設(shè)條件可能導(dǎo)致多次定義同一個(gè)類、由于類混淆造成堆損壞和其他不良影響。