一、后臺(tái)優(yōu)化
Android 7.0 移除了三項(xiàng)隱式廣播 CONNECTIVITY_ACTION 、 ACTION_NEW_PICTURE 和 ACTION_NEW_PICTURE ,以幫助優(yōu)化內(nèi)存使用和電量消耗。此項(xiàng)變更很有必要,因?yàn)殡[式廣播會(huì)在后臺(tái)頻繁啟動(dòng)已注冊(cè)偵聽這些廣播的應(yīng)用。刪除這些廣播可以顯著提升設(shè)備性能和用戶體驗(yàn)。
移動(dòng)設(shè)備會(huì)經(jīng)歷頻繁的連接變更,例如在 WLAN 和移動(dòng)數(shù)據(jù)之間切換時(shí)。目前,可以通過在應(yīng)用清單中注冊(cè)一個(gè)接收器來偵聽隱式 CONNECTIVITY_ACTION 廣播,讓應(yīng)用能夠監(jiān)控這些變更。由于很多應(yīng)用會(huì)注冊(cè)接收此廣播,因此單次網(wǎng)絡(luò)切換即會(huì)導(dǎo)致所有應(yīng)用被喚醒并同時(shí)處理此廣播。
同理,在之前版本的 Android 中,應(yīng)用可以注冊(cè)接收來自其他應(yīng)用(例如相機(jī))的隱式 ACTION_NEW_PICTURE 和 ACTION_NEW_PICTURE 廣播。當(dāng)用戶使用相機(jī)應(yīng)用拍攝照片時(shí),這些應(yīng)用即會(huì)被喚醒以處理廣播。
為緩解這些問題,Android 7.0 應(yīng)用了以下優(yōu)化措施:
- 面向 Android 7.0 開發(fā)的應(yīng)用不會(huì)收到
CONNECTIVITY_ACTION廣播,即使它們已有清單條目來請(qǐng)求接受這些事件的通知。在前臺(tái)運(yùn)行的應(yīng)用如果使用 BroadcastReceiver 請(qǐng)求接收通知,則仍可以在主線程中偵聽CONNECTIVITY_CHANGE。 - 應(yīng)用無法發(fā)送或接收
ACTION_NEW_PICTURE或ACTION_NEW_VIDEO廣播。此項(xiàng)優(yōu)化會(huì)影響所有應(yīng)用,而不僅僅是面向 Android 7.0 的應(yīng)用。
針對(duì)這項(xiàng)變更,Android 框架提供了多種解決方案來緩解對(duì)這些隱式廣播的需求。例如,JobScheduler 和新的 WorkManager 提供了強(qiáng)大的機(jī)制,可在滿足指定條件時(shí)(例如連接到不按流量計(jì)費(fèi)網(wǎng)絡(luò)時(shí))調(diào)度網(wǎng)絡(luò)操作?,F(xiàn)在,您還可以使用 JobScheduler 來響應(yīng)對(duì)內(nèi)容提供程序的更改。JobInfo 對(duì)象可封裝 JobScheduler 用來調(diào)度作業(yè)的參數(shù)。當(dāng)滿足作業(yè)條件時(shí),系統(tǒng)會(huì)在應(yīng)用的 JobService 上執(zhí)行此作業(yè)。
如果以Android 7.0為目標(biāo)平臺(tái)的應(yīng)用仍然需要監(jiān)聽網(wǎng)絡(luò)更改 或 在設(shè)備連接到不按流量計(jì)費(fèi)的網(wǎng)絡(luò)時(shí)執(zhí)行批量網(wǎng)絡(luò)活動(dòng),使用隱式廣播的方案就無法滿足需求。
ConnectivityManager監(jiān)聽網(wǎng)絡(luò)更改
ConnectivityManager API 提供一個(gè)更強(qiáng)大的方法,用于僅在滿足指定的網(wǎng)絡(luò)條件時(shí)請(qǐng)求回調(diào)。首先我們獲取到系統(tǒng)的 ConnectivityManager 服務(wù), 并且使用 registerNetworkCallback 將 NetworkRequest 對(duì)象傳遞給系統(tǒng),最終系統(tǒng)會(huì)通過 ConnectivityManager.NetworkCallback 回調(diào),將網(wǎng)絡(luò)變更情況告知給應(yīng)用。
//獲取ConnectivityManager
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
//定義ConnectivityManager.NetworkCallback回調(diào)方法
callback = new ConnectivityManager.NetworkCallback() {
// 可用網(wǎng)絡(luò)接入
public void onCapabilitiesChanged(@NotNull Network network, @NotNull NetworkCapabilities networkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities);
LogUtils.d("onCapabilitiesChanged");
checkNetworkCapabilities(networkCapabilities);
}
@Override
public void onLost(@NonNull Network network) {
super.onLost(network);
if (cm != null) {
Network activeNetwork = cm.getActiveNetwork();
if (activeNetwork == null) {
//連接不到可用網(wǎng)絡(luò)
return;
}
NetworkCapabilities networkCapabilities = cm.getNetworkCapabilities(activeNetwork);
checkNetworkCapabilities(networkCapabilities);
}
}
};
NetworkRequest.Builder builder = new NetworkRequest.Builder();
if (cm != null) {
cm.registerNetworkCallback(builder.build(), callback);
}
//判斷當(dāng)前網(wǎng)絡(luò)連接情況
private void checkNetworkCapabilities(NetworkCapabilities networkCapabilities) {
if (networkCapabilities == null) {
return;
}
// 表明網(wǎng)絡(luò)連接成功
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
// 使用WI-FI
LogUtils.d("WIFI network");
} else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
// 使用蜂窩網(wǎng)絡(luò)
LogUtils.d("mobile network");
} else {
// 未知網(wǎng)絡(luò),包括藍(lán)牙、VPN、LoWPAN
LogUtils.d("unknown network");
}
} else {
//網(wǎng)絡(luò)連接失敗
}
}
JobScheduler 設(shè)備連接到不按流量計(jì)費(fèi)的網(wǎng)絡(luò)且正在充電
JobScheduler API 提供了一個(gè)穩(wěn)健可靠的機(jī)制來安排滿足指定條件(例如連入無限流量網(wǎng)絡(luò))時(shí)所執(zhí)行的網(wǎng)絡(luò)操作。
public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
JobScheduler js =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo job = new JobInfo.Builder(
MY_BACKGROUND_JOB,
new ComponentName(context, MyJobService.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.setRequiresCharging(true)
.build();
js.schedule(job);
}
當(dāng)滿足作業(yè)條件時(shí),您的應(yīng)用會(huì)收到一個(gè)回調(diào),以運(yùn)行指定的 JobService.class 中的 onStartJob() 方法。
JobScheduler 的一個(gè)新替代工具是WorkManager ,這個(gè) API 可用來調(diào)度無論應(yīng)用進(jìn)程是否存在都需要保證完成的后臺(tái)任務(wù)。WorkManager 根據(jù)設(shè)備 API 級(jí)別等因素選擇運(yùn)行工作的適當(dāng)方式(直接在應(yīng)用進(jìn)程中的線程上以及使用 JobScheduler、FirebaseJobDispatcher 或 AlarmManager)。此外,WorkManager 不需要 Play 服務(wù),并且提供多項(xiàng)高級(jí)功能,例如將任務(wù)鏈接在一起或檢查任務(wù)狀態(tài)。要了解詳情,請(qǐng)參閱 WorkManager。
二、權(quán)限更改
Android 7.0 做了一些權(quán)限更改,這些更改可能會(huì)影響到應(yīng)用的正常運(yùn)行。
系統(tǒng)權(quán)限更改
為了提高私有文件的安全性,面向 Android 7.0 或更高版本的應(yīng)用私有目錄被限制訪問 (0700)。此設(shè)置可防止私有文件的元數(shù)據(jù)泄漏,如它們的大小或存在性。此權(quán)限更改有多重副作用:
私有文件的文件權(quán)限不應(yīng)再由所有者放寬,為使用 MODE_WORLD_READABLE 和/或 MODE_WORLD_WRITEABLE 而進(jìn)行的此類嘗試將觸發(fā) SecurityException。
注:迄今為止,這種限制尚不能完全執(zhí)行。應(yīng)用仍可能使用原生 API 或 File API 來修改它們的私有目錄權(quán)限。但是,我們強(qiáng)烈反對(duì)放寬私有目錄的權(quán)限。傳遞軟件包網(wǎng)域外的 file:// URI 可能給接收器留下無法訪問的路徑。因此,嘗試傳遞 file:// URI 會(huì)觸發(fā) FileUriExposedException。分享私有文件內(nèi)容的推薦方法是使用 FileProvider。
DownloadManager 不再按文件名分享私人存儲(chǔ)的文件。舊版應(yīng)用在訪問 COLUMN_LOCAL_FILENAME 時(shí)可能出現(xiàn)無法訪問的路徑。面向 Android 7.0 或更高版本的應(yīng)用在嘗試訪問 COLUMN_LOCAL_FILENAME 時(shí)會(huì)觸發(fā) SecurityException。通過使用 DownloadManager.Request.setDestinationInExternalFilesDir() 或 DownloadManager.Request.setDestinationInExternalPublicDir() 將下載位置設(shè)置為公共位置的舊版應(yīng)用仍可以訪問 COLUMN_LOCAL_FILENAME 中的路徑,但是我們強(qiáng)烈反對(duì)使用這種方法。對(duì)于由 DownloadManager 公開的文件,首選的訪問方式是使用ContentResolver.openFileDescriptor()。參考Android使用DownloadManager下載安裝包并跳轉(zhuǎn)到安裝界面
在應(yīng)用間共享文件
對(duì)于面向 Android 7.0 的應(yīng)用,Android 框架執(zhí)行的 StrictMode API 政策禁止在您的應(yīng)用外部公開 file:// URI。如果一項(xiàng)包含文件 URI 的 intent 離開您的應(yīng)用,則應(yīng)用出現(xiàn)故障,并出現(xiàn) FileUriExposedException 異常。
要在應(yīng)用間共享文件,必須使用content:// URI格式的Uri ,并授予 URI 臨時(shí)訪問權(quán)限。具體步驟如下:
指定 FileProvider
在應(yīng)用的AndroidManifest.xml文件中添加provider條目
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.xxx.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:authorities 屬性用于指定一個(gè)或多個(gè) URI 授權(quán)方的列表,這些 URI 授權(quán)方用于標(biāo)識(shí)內(nèi)容提供程序提供的數(shù)據(jù)。列出多個(gè)授權(quán)方時(shí),用分號(hào)將其名稱分隔開來。
指定可共享的目錄
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path path="images/" name="myimages" />
</paths>
name 屬性的作用是FileProvider 將路徑段myimages 添加到 files/images/ 子目錄中文件的內(nèi)容 URI 中。
<paths> 元素可以有多個(gè)子元素,每個(gè)子元素指定一個(gè)不同的共享目錄。
- root-path:表示根目錄,『/』。
- files-path:表示 content.getFileDir() 獲取到的目錄。
- cache-path:表示 content.getCacheDir() 獲取到的目錄
- external-path:表示Environment.getExternalStorageDirectory() 指向的目錄。
- external-files-path:表示 ContextCompat.getExternalFilesDirs() 獲取到的目錄。
- external-cache-path:表示 ContextCompat.getExternalCacheDirs() 獲取到的目錄。
現(xiàn)在已經(jīng)完整地指定了 FileProvider,該提供器可用于為應(yīng)用內(nèi)部存儲(chǔ)中的 files/ 目錄中的文件或 files/ 的子目錄中的文件生成內(nèi)容 URI。當(dāng)應(yīng)用為文件生成內(nèi)容 URI 時(shí),會(huì)包含 <provider> 元素中指定的授權(quán) (com.xxx.fileprovider)、路徑 myimages/ 以及文件的名稱。
例如使用FileProvider請(qǐng)求files/images/目錄下的文件android.jpg的URI,F(xiàn)ileProvider將會(huì)返回如下URI:
content://com.xxx.fileprovider/myimages/android.jpg
獲取指定File文件的Uri:
public Uri getUri(Context context, File file) {
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//第二個(gè)參數(shù)為 com.xxx.fileprovider
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}