老司機(jī)淡飆車技術(shù):Android7.0適配心得
Android7.0發(fā)布已經(jīng)有一個(gè)多月了,Android7.0在給用戶帶來(lái)一些新的特性的同時(shí),也給開發(fā)者帶來(lái)了新的挑戰(zhàn),這幾天我將應(yīng)用適配到Android7.0,其中也遇到了不少問(wèn)題也踩了一些坑,在這里就把我在Android7.0適配上的一些心得分享給大家,讓大家的應(yīng)用能早一天跑在Android7.0上。
權(quán)限更改
隨著Android版本越來(lái)越高,Android對(duì)隱私的保護(hù)力度也越來(lái)越大。從Android6.0引入的動(dòng)態(tài)權(quán)限控制(Runtime Permissions)到Android7.0的“私有目錄被限制訪問(wèn)”,“StrictMode API 政策”。這些更改在為用戶帶來(lái)更加安全的操作系統(tǒng)的同時(shí)也為開發(fā)者帶來(lái)了一些新的任務(wù)。如何讓你的APP能夠適應(yīng)這些改變而不是cash,是擺在每一位Android開發(fā)者身上的責(zé)任。
目錄被限制訪問(wèn)
一直以來(lái),在目錄及文件的訪問(wèn)保護(hù)方面iOS做的是很到位的,如:iOS的沙箱機(jī)制。但,Android在這方面的保護(hù)就有些偏弱了,在Android中應(yīng)用可以讀寫手機(jī)存儲(chǔ)中任何一個(gè)目錄及文件,這也帶來(lái)了很多的安全問(wèn)題?,F(xiàn)在Android也在著力解決這一問(wèn)題。
在Android7.0中為了提高私有文件的安全性,面向 Android N 或更高版本的應(yīng)用私有目錄將被限制訪問(wèn)。對(duì)于這個(gè)權(quán)限的更改開發(fā)者需要留意一下改變:
- 私有文件的文件權(quán)限不在放權(quán)給所有的應(yīng)用,使用 MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 進(jìn)行的操作將觸發(fā) SecurityException。
應(yīng)對(duì)策略:這項(xiàng)權(quán)限的變更將意味著你無(wú)法通過(guò)File API訪問(wèn)手機(jī)存儲(chǔ)上的數(shù)據(jù)了,基于File API的一些文件瀏覽器等也將受到很大的影響,看到這大家是不是驚呆了呢,不過(guò)迄今為止,這種限制尚不能完全執(zhí)行。 應(yīng)用仍可能使用原生 API 或 File API 來(lái)修改它們的私有目錄權(quán)限。 但是,Android官方強(qiáng)烈反對(duì)放寬私有目錄的權(quán)限。可以看出收起對(duì)私有文件的訪問(wèn)權(quán)限是Android將來(lái)發(fā)展的趨勢(shì)。
- 給其他應(yīng)用傳遞 file:// URI 類型的Uri,可能會(huì)導(dǎo)致接受者無(wú)法訪問(wèn)該路徑。 因此,在Android7.0中嘗試傳遞 file:// URI 會(huì)觸發(fā) FileUriExposedException。
應(yīng)對(duì)策略:大家可以通過(guò)使用FileProvider來(lái)解決這一問(wèn)題。
- DownloadManager 不再按文件名分享私人存儲(chǔ)的文件。COLUMN_LOCAL_FILENAME在Android7.0中被標(biāo)記為deprecated , 舊版應(yīng)用在訪問(wèn) COLUMN_LOCAL_FILENAME時(shí)可能出現(xiàn)無(wú)法訪問(wèn)的路徑。 面向 Android N 或更高版本的應(yīng)用在嘗試訪問(wèn) COLUMN_LOCAL_FILENAME 時(shí)會(huì)觸發(fā) SecurityException。
應(yīng)對(duì)策略:大家可以通過(guò)ContentResolver.openFileDescriptor()來(lái)訪問(wèn)由 DownloadManager 公開的文件。
應(yīng)用間共享文件
在Android7.0系統(tǒng)上,Android 框架強(qiáng)制執(zhí)行了 StrictMode API 政策禁止向你的應(yīng)用外公開 file:// URI。 如果一項(xiàng)包含文件 file:// URI類型 的 Intent 離開你的應(yīng)用,應(yīng)用失敗,并出現(xiàn) FileUriExposedException 異常,如調(diào)用系統(tǒng)相機(jī)拍照,或裁切照片。
應(yīng)對(duì)策略:若要在應(yīng)用間共享文件,可以發(fā)送 content:// URI類型的Uri,并授予 URI 臨時(shí)訪問(wèn)權(quán)限。 進(jìn)行此授權(quán)的最簡(jiǎn)單方式是使用 FileProvider類。 如需有關(guān)權(quán)限和共享文件的更多信息,請(qǐng)參閱共享文件。
在Android7.0上調(diào)用系統(tǒng)相機(jī)拍照,裁切照片
調(diào)用系統(tǒng)相機(jī)拍照
在Android7.0之前,如果你想調(diào)用系統(tǒng)相機(jī)拍照可以通過(guò)以下代碼來(lái)進(jìn)行:
- File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
- if (!file.getParentFile().exists())file.getParentFile().mkdirs();
- Uri imageUri = Uri.fromFile(file);
- Intent intent = new Intent();
- intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//設(shè)置Action為拍照
- intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//將拍取的照片保存到指定URI
- startActivityForResult(intent,1006);
在Android7.0上使用上述方式調(diào)用系統(tǒng)相拍照會(huì)拋出如下異常:
- android.os.FileUriExposedException: file:////storage/emulated/0/temp/1474956193735.jpg exposed beyond app through Intent.getData()at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)at android.net.Uri.checkFileUriExposed(Uri.java:2346)at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)at android.app.Activity.startActivityForResult(Activity.java:4223)...at android.app.Activity.startActivityForResult(Activity.java:4182)
這是由于Android7.0執(zhí)行了“StrictMode API 政策禁”的原因,不過(guò)小伙伴們不用擔(dān)心,上文講到了可以用FileProvider來(lái)解決這一問(wèn)題, 現(xiàn)在我們就來(lái)一步一步的解決這個(gè)問(wèn)題。
使用FileProvider
使用FileProvider的大致步驟如下:
第一步:在manifest清單文件中注冊(cè)provider
- <provider
- android:name="android.support.v4.content.FileProvider"
- android:authorities="com.jph.takephoto.fileprovider"
- android:grantUriPermissions="true"
- android:exported="false">
- <meta-data
- android:name="android.support.FILE_PROVIDER_PATHS"
- android:resource="@xml/file_paths" />
- </provider>
心得:exported:要求必須為false,為true則會(huì)報(bào)安全異常。grantUriPermissions:true,表示授予 URI 臨時(shí)訪問(wèn)權(quán)限。
第二步:指定共享的目錄
為了指定共享的目錄我們需要在資源(res)目錄下創(chuàng)建一個(gè)xml目錄,然后創(chuàng)建一個(gè)名為“file_paths”(名字可以隨便起,只要和在manifest注冊(cè)的provider所引用的resource保持一致即可)的資源文件,內(nèi)容如下:
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <paths>
- <external-path path="" name="camera_photos" />
- </paths>
- </resources>
- 代表的根目錄: Context.getFilesDir()
- 代表的根目錄: Environment.getExternalStorageDirectory()
- 代表的根目錄: getCacheDir()
心得:上述代碼中path="",是有特殊意義的,它代碼根目錄,也就是說(shuō)你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個(gè)文件了,如果你將path設(shè)為path="pictures", 那么它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應(yīng)用分享pictures目錄范圍之外的文件是不行的。
第三步:使用FileProvider
上述準(zhǔn)備工作做完之后,現(xiàn)在我們就可以使用FileProvider了。
還是以調(diào)用系統(tǒng)相機(jī)拍照為例,我們需要將上述拍照代碼修改為如下:
- File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
- if (!file.getParentFile().exists())file.getParentFile().mkdirs();
- Uri imageUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", file);//通過(guò)FileProvider創(chuàng)建一個(gè)content類型的Uri
- Intent intent = new Intent();
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加這一句表示對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該Uri所代表的文件
- intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//設(shè)置Action為拍照
- intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//將拍取的照片保存到指定URI
- startActivityForResult(intent,1006);
上述代碼中主要有兩處改變:
- 將之前Uri的scheme類型為file的Uri改成了有FileProvider創(chuàng)建一個(gè)content類型的Uri。
- 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);來(lái)對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該Uri所代表的文件。
心得:上述代碼通過(guò)FileProvider的Uri getUriForFile (Context context, String authority, File file) 靜態(tài)方法來(lái)獲取Uri,該方法中authority參數(shù)就是清單文件中注冊(cè)provider的android:authorities="com.jph.takephoto.fileprovider"。 對(duì)Web服務(wù)器如tomcat,IIS比較熟悉的小伙伴,都只知道為了網(wǎng)站內(nèi)容的安全和高效,Web服務(wù)器都支持為網(wǎng)站內(nèi)容設(shè)置一個(gè)虛擬目錄,其實(shí)FileProvider也有異曲同工之處。
將getUriForFile方法獲取的Uri打印出來(lái)如下:
- content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg`。
其中camera_photos就是file_paths.xml中paths的name。
因?yàn)樯鲜鲋付ǖ膒ath為path="",所以content://com.jph.takephoto.fileprovider/camera_photos/代表的真實(shí)路徑就是根目錄,即:/storage/emulated/0/。
content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg代表的真實(shí)路徑是:/storage/emulated/0/temp/1474960080319.jpg。
另外,推薦大家使用開源工具庫(kù)TakePhoto, TakePhoto是一款在Android設(shè)備上獲取照片(拍照或從相冊(cè)、文件中選擇)、裁剪圖片、壓縮圖片的開源工具庫(kù)。
裁切照片
在Android7.0之前,你可以通過(guò)如下方法來(lái)裁切照片:
- File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
- if (!file.getParentFile().exists())file.getParentFile().mkdirs();
- Uri outputUri = Uri.fromFile(file);
- Uri imageUri=Uri.fromFile(new File("/storage/emulated/0/temp/1474960080319.jpg"));
- Intent intent = new Intent("com.android.camera.action.CROP");
- intent.setDataAndType(imageUri, "image/*");
- intent.putExtra("crop", "true");
- intent.putExtra("aspectX", 1);
- intent.putExtra("aspectY", 1);
- intent.putExtra("scale", true);
- intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
- intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
- intent.putExtra("noFaceDetection", true); // no face detection
- startActivityForResult(intent,1008);
和拍照一樣,上述代碼在Android7.0上同樣會(huì)引起android.os.FileUriExposedException異常,解決辦法就是上文說(shuō)說(shuō)的使用FileProvider。
然后,將上述代碼改為如下即可:
- File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");if (!file.getParentFile().exists())file.getParentFile().mkdirs();Uri outputUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider",file);Uri imageUri=FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", new File("/storage/emulated/0/temp/1474960080319.jpg");//通過(guò)FileProvider創(chuàng)建一個(gè)content類型的UriIntent intent = new Intent("com.android.camera.action.CROP");intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(imageUri, "image/*");intent.putExtra("crop", "true");intent.putExtra("aspectX", 1);intent.putExtra("aspectY", 1);intent.putExtra("scale", true);intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());intent.putExtra("noFaceDetection", true); // no face detectionstartActivityForResult(intent,1008);
另外,裁切照片推薦大家使用開源工具庫(kù)TakePhoto, TakePhoto是一款在Android設(shè)備上獲取照片(拍照或從相冊(cè)、文件中選擇)、裁剪圖片、壓縮圖片的開源工具庫(kù)。
電池和內(nèi)存
Android 6.0(API 級(jí)別 23)引入了低電耗模式,Android7.0在電池和內(nèi)存上又做了進(jìn)一步優(yōu)化, 來(lái)減少Android應(yīng)用對(duì)電量的消耗以及對(duì)內(nèi)存的占用。這些優(yōu)化所帶來(lái)的一些規(guī)則的變更可能會(huì)影響你的應(yīng)用訪問(wèn)系統(tǒng)資源,以及你的系統(tǒng)通過(guò)特定隱式 Intent 與其他應(yīng)用互動(dòng)的方式。 所以開發(fā)人員需要特別注意這些改變。
低電耗模式
在低電耗模式下,當(dāng)用戶設(shè)備未插接電源、處于靜止?fàn)顟B(tài)且屏幕關(guān)閉時(shí),該模式會(huì)推遲 CPU 和網(wǎng)絡(luò)活動(dòng),從而延長(zhǎng)電池壽命。 Android7.0通過(guò)在設(shè)備未插接電源且屏幕關(guān)閉狀態(tài)下、但不一定要處于靜止?fàn)顟B(tài)(例如用戶外出時(shí)把手持式設(shè)備裝在口袋里)時(shí)應(yīng)用部分 CPU 和網(wǎng)絡(luò)限制,進(jìn)一步增強(qiáng)了低電耗模式。
也就是說(shuō),Android7.0會(huì)在手機(jī)屏幕關(guān)閉的狀態(tài)下,限時(shí)應(yīng)用對(duì)CPU以及網(wǎng)絡(luò)的使用。
具體規(guī)則如下:
- 當(dāng)設(shè)備處于充電狀態(tài)且屏幕已關(guān)閉一定時(shí)間后,設(shè)備會(huì)進(jìn)入低電耗模式并應(yīng)用第一部分限制: 關(guān)閉應(yīng)用網(wǎng)絡(luò)訪問(wèn)、推遲作業(yè)和同步。
- 如果進(jìn)入低電耗模式后設(shè)備處于靜止?fàn)顟B(tài)達(dá)到一定時(shí)間,系統(tǒng)則會(huì)對(duì) PowerManager.WakeLock、AlarmManager 鬧鈴、GPS 和 Wi-Fi 掃描應(yīng)用余下的低電耗模式限制。 無(wú)論是應(yīng)用部分還是全部低電耗模式限制,系統(tǒng)都會(huì)喚醒設(shè)備以提供簡(jiǎn)短的維護(hù)時(shí)間窗口,在此窗口期間,應(yīng)用程序可以訪問(wèn)網(wǎng)絡(luò)并執(zhí)行任何被推遲的作業(yè)/同步。
后臺(tái)優(yōu)化
小伙伴們都知道在Android中有一些隱式廣播,使用這些隱式廣播可以做一些特定的功能,如,當(dāng)手機(jī)網(wǎng)絡(luò)變成WiFi時(shí)自動(dòng)下載更新包等。 但,這些隱式廣播會(huì)在后臺(tái)頻繁啟動(dòng)已注冊(cè)偵聽這些廣播的應(yīng)用,從而帶來(lái)很大的電量消耗,為緩解這一問(wèn)題來(lái)提升設(shè)備性能和用戶體驗(yàn),在Android 7.0中刪除了三項(xiàng)隱式廣播,以幫助優(yōu)化內(nèi)存使用和電量消耗。
Android 7.0 應(yīng)用了以下優(yōu)化措施:
- 在 Android 7.0上 應(yīng)用不會(huì)收到 CONNECTIVITY_ACTION 廣播,即使你在manifest清單文件中設(shè)置了請(qǐng)求接受這些事件的通知。 但,在前臺(tái)運(yùn)行的應(yīng)用如果使用BroadcastReceiver 請(qǐng)求接收通知,則仍可以在主線程中偵聽 CONNECTIVITY_CHANGE。
- 在 Android 7.0上應(yīng)用無(wú)法發(fā)送或接收 ACTION_NEW_PICTURE 或ACTION_NEW_VIDEO 類型的廣播。
應(yīng)對(duì)策略:Android 框架提供多個(gè)解決方案來(lái)緩解對(duì)這些隱式廣播的需求。 例如,JobScheduler API 提供了一個(gè)穩(wěn)健可靠的機(jī)制來(lái)安排滿足指定條件(例如連入無(wú)限流量網(wǎng)絡(luò))時(shí)所執(zhí)行的網(wǎng)絡(luò)操作。 您甚至可以使用 JobScheduler API 來(lái)適應(yīng)內(nèi)容提供程序變化。
另外,大家如果想了解更多關(guān)于后臺(tái)的優(yōu)化可查閱后臺(tái)優(yōu)化。
移動(dòng)設(shè)備會(huì)經(jīng)歷頻繁的連接變更,例如在 Wi-Fi 和移動(dòng)數(shù)據(jù)之間切換時(shí)。 目前,可以通過(guò)在應(yīng)用清單中注冊(cè)一個(gè)接收器來(lái)偵聽隱式 CONNECTIVITY_ACTION 廣播, 讓應(yīng)用能夠監(jiān)控這些變更。 由于很多應(yīng)用會(huì)注冊(cè)接收此廣播,因此單次網(wǎng)絡(luò)切換即會(huì)導(dǎo)致所有應(yīng)用被喚醒并同時(shí)處理此廣播。