Android N 適配心得

七色音階發表於2016-11-01

Android7.0釋出已經有一個多月了,Android7.0在給使用者帶來一些新的特性的同時,也給開發者帶來了新的挑戰,這幾天我將應用適配到Android7.0,其中也遇到了不少問題也踩了一些坑,在這裡就把我在Android7.0適配上的一些心得分享給大家,讓大家的應用能早一天跑在Android7.0上。

許可權更改

隨著Android版本越來越高,Android對隱私的保護力度也越來越大。從Android6.0引入的動態許可權控制(Runtime Permissions)到Android7.0的“私有目錄被限制訪問”,“StrictMode API 政策”。這些更改在為使用者帶來更加安全的作業系統的同時也為開發者帶來了一些新的任務。如何讓你的APP能夠適應這些改變而不是cash,是擺在每一位Android開發者身上的責任。

目錄被限制訪問

一直以來,在目錄及檔案的訪問保護方面iOS做的是很到位的,如:iOS的沙箱機制。但,Android在這方面的保護就有些偏弱了,在Android中應用可以讀寫手機儲存中任何一個目錄及檔案,這也帶來了很多的安全問題。現在Android也在著力解決這一問題。

在Android7.0中為了提高私有檔案的安全性,面向 Android N 或更高版本的應用私有目錄將被限制訪問。對於這個許可權的更改開發者需要留意一下改變:

應對策略:這項許可權的變更將意味著你無法通過File API訪問手機儲存上的資料了,基於File API的一些檔案瀏覽器等也將受到很大的影響,看到這大家是不是驚呆了呢,不過迄今為止,這種限制尚不能完全執行。 應用仍可能使用原生 API 或 File API 來修改它們的私有目錄許可權。 但是,Android官方強烈反對放寬私有目錄的許可權。可以看出收起對私有檔案的訪問許可權是Android將來發展的趨勢。

  • 給其他應用傳遞 file:// URI 型別的Uri,可能會導致接受者無法訪問該路徑。 因此,在Android7.0中嘗試傳遞 file:// URI 會觸發 FileUriExposedException。

應對策略:大家可以通過使用FileProvider來解決這一問題。

應對策略:大家可以通過ContentResolver.openFileDescriptor()來訪問由 DownloadManager 公開的檔案。

應用間共享檔案

在Android7.0系統上,Android 框架強制執行了 StrictMode API 政策禁止向你的應用外公開 file:// URI。 如果一項包含檔案 file:// URI型別 的 Intent 離開你的應用,應用失敗,並出現 FileUriExposedException 異常,如呼叫系統相機拍照,或裁切照片

應對策略:若要在應用間共享檔案,可以傳送 content:// URI型別的Uri,並授予 URI 臨時訪問許可權。 進行此授權的最簡單方式是使用 FileProvider類。 如需有關許可權和共享檔案的更多資訊,請參閱共享檔案。

在Android7.0上呼叫系統相機拍照,裁切照片

呼叫系統相機拍照

在Android7.0之前,如果你想呼叫系統相機拍照可以通過以下程式碼來進行:

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);//設定Action為拍照
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//將拍取的照片儲存到指定URI
startActivityForResult(intent,1006);

Android7.0拍照.png

在Android7.0上使用上述方式呼叫系統相拍照會丟擲如下異常:

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拍照閃退.png

這是由於Android7.0執行了“StrictMode API 政策禁”的原因,不過小夥伴們不用擔心,上文講到了可以用FileProvider來解決這一問題,現在我們就來一步一步的解決這個問題。

使用FileProvider

使用FileProvider的大致步驟如下:
第一步:在manifest清單檔案中註冊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則會報安全異常。grantUriPermissions:true,表示授予 URI 臨時訪問許可權。

第二步:指定共享的目錄

為了指定共享的目錄我們需要在資源(res)目錄下建立一個xml目錄,然後建立一個名為“file_paths”(名字可以隨便起,只要和在manifest註冊的provider所引用的resource保持一致即可)的資原始檔,內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-path path="" name="camera_photos" />
    </paths>
</resources>
  • 代表的根目錄: Context.getFilesDir()

  • 代表的根目錄: Environment.getExternalStorageDirectory()

  • 代表的根目錄: getCacheDir()

心得:上述程式碼中path="",是有特殊意義的,它程式碼根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個檔案了,如果你將path設為path="pictures",那麼它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應用分享pictures目錄範圍之外的檔案是不行的。

第三步:使用FileProvider

上述準備工作做完之後,現在我們就可以使用FileProvider了。
還是以呼叫系統相機拍照為例,我們需要將上述拍照程式碼修改為如下:

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);//通過FileProvider建立一個content型別的Uri
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //新增這一句表示對目標應用臨時授權該Uri所代表的檔案
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//設定Action為拍照
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//將拍取的照片儲存到指定URI
startActivityForResult(intent,1006);

上述程式碼中主要有兩處改變:

  1. 將之前Uri的scheme型別為file的Uri改成了有FileProvider建立一個content型別的Uri。
  2. 新增了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);來對目標應用臨時授權該Uri所代表的檔案。

心得:上述程式碼通過FileProviderUri getUriForFile (Context context, String authority, File file) 靜態方法來獲取Uri,該方法中authority引數就是清單檔案中註冊provider的android:authorities="com.jph.takephoto.fileprovider"。對Web伺服器如tomcat,IIS比較熟悉的小夥伴,都只知道為了網站內容的安全和高效,Web伺服器都支援為網站內容設定一個虛擬目錄,其實FileProvider也有異曲同工之處。

getUriForFile方法獲取的Uri列印出來如下:

content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg`。 

其中camera_photos就是file_paths.xml中paths的name。

因為上述指定的path為path="",所以content://com.jph.takephoto.fileprovider/camera_photos/代表的真實路徑就是根目錄,即:/storage/emulated/0/content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg代表的真實路徑是:/storage/emulated/0/temp/1474960080319.jpg

另外,推薦大家使用開源工具庫TakePhotoTakePhoto是一款在Android裝置上獲取照片(拍照或從相簿、檔案中選擇)、裁剪圖片、壓縮圖片的開源工具庫。

裁切照片

在Android7.0之前,你可以通過如下方法來裁切照片:

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上同樣會引起android.os.FileUriExposedException異常,解決辦法就是上文說說的使用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");//通過FileProvider建立一個content型別的Uri
Intent 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 detection
startActivityForResult(intent,1008);

另外,裁切照片推薦大家使用開源工具庫TakePhotoTakePhoto是一款在Android裝置上獲取照片(拍照或從相簿、檔案中選擇)、裁剪圖片、壓縮圖片的開源工具庫。

電池和記憶體

Android 6.0(API 級別 23)引入了低電耗模式,Android7.0在電池和記憶體上又做了進一步優化,來減少Android應用對電量的消耗以及對記憶體的佔用。這些優化所帶來的一些規則的變更可能會影響你的應用訪問系統資源,以及你的系統通過特定隱式 Intent 與其他應用互動的方式。所以開發人員需要特別注意這些改變。

低電耗模式

在低電耗模式下,當使用者裝置未插接電源、處於靜止狀態且螢幕關閉時,該模式會推遲 CPU 和網路活動,從而延長電池壽命。Android7.0通過在裝置未插接電源且螢幕關閉狀態下、但不一定要處於靜止狀態(例如使用者外出時把手持式裝置裝在口袋裡)時應用部分 CPU 和網路限制,進一步增強了低電耗模式。

也就是說,Android7.0會在手機螢幕關閉的狀態下,限時應用對CPU以及網路的使用。

具體規則如下:

  1. 當裝置處於充電狀態且螢幕已關閉一定時間後,裝置會進入低電耗模式並應用第一部分限制: 關閉應用網路訪問、推遲作業和同步。
  2. 如果進入低電耗模式後裝置處於靜止狀態達到一定時間,系統則會對 PowerManager.WakeLockAlarmManager 鬧鈴、GPS 和 Wi-Fi 掃描應用餘下的低電耗模式限制。 無論是應用部分還是全部低電耗模式限制,系統都會喚醒裝置以提供簡短的維護時間視窗,在此視窗期間,應用程式可以訪問網路並執行任何被推遲的作業/同步。

後臺優化

小夥伴們都知道在Android中有一些隱式廣播,使用這些隱式廣播可以做一些特定的功能,如,當手機網路變成WiFi時自動下載更新包等。但,這些隱式廣播會在後臺頻繁啟動已註冊偵聽這些廣播的應用,從而帶來很大的電量消耗,為緩解這一問題來提升裝置效能和使用者體驗,在Android 7.0中刪除了三項隱式廣播,以幫助優化記憶體使用和電量消耗。

Android 7.0 應用了以下優化措施:

  • 在 Android 7.0上 應用不會收到 CONNECTIVITY_ACTION 廣播,即使你在manifest清單檔案中設定了請求接受這些事件的通知。 但,在前臺執行的應用如果使用BroadcastReceiver 請求接收通知,則仍可以在主執行緒中偵聽 CONNECTIVITY_CHANGE。
  • 在 Android 7.0上應用無法傳送或接收 ACTION_NEW_PICTUREACTION_NEW_VIDEO 型別的廣播。

應對策略:Android 框架提供多個解決方案來緩解對這些隱式廣播的需求。 例如,JobScheduler API 提供了一個穩健可靠的機制來安排滿足指定條件(例如連入無限流量網路)時所執行的網路操作。 您甚至可以使用 JobScheduler API 來適應內容提供程式變化。

另外,大家如果想了解更多關於後臺的優化可查閱後臺優化

移動裝置會經歷頻繁的連線變更,例如在 Wi-Fi 和移動資料之間切換時。 目前,可以通過在應用清單中註冊一個接收器來偵聽隱式 CONNECTIVITY_ACTION 廣播,讓應用能夠監控這些變更。 由於很多應用會註冊接收此廣播,因此單次網路切換即會導致所有應用被喚醒並同時處理此廣播。

以上是,我在Android7.0上適配上的一些心得,小夥伴們如果有遇到問題可以在下方留言。

轉自:http://www.cboy.me/2016/09/28/Android7.0%E9%80%82%E9%85%8D%E5%BF%83%E5%BE%97/

相關文章