Android文件存儲

Android文件存儲
1.getFilesDir(),
2.getCacheDir(),
3.getExternalFilesDir(null),
4.getExternalCacheDir(),
5.Environment.getExternalStorageDirectory(),
6.Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
7.面向 Android 7.0文件存儲變化

1.getFilesDir

應用內部存儲空間(數據文件私有)文件存儲到這個路徑下,不需要申請權限,當應用被卸載的時候,目錄下的文件會被刪除。
需要注意的是,這個文件的目錄和應用的存儲位置有關,
當應用被移動到外部存儲設備的時候,文件的絕對路徑也是變化的,所以建議當數據存儲到這個目錄的時候,用相對路徑
系統提供的訪問此路徑文件的方法是:context.openFileOutput(String,int);context.openFileInput(String name);

/**
 * app私有目錄,如shared preference文件,數據庫文件
 * 目錄爲data/data/< package name >/files/
 */
public static File getInternalFilePath ( Activity activity ) {
    return activity.getFilesDir();
}

2.getCacheDir();

應用內部存儲空間(數據文件私有)文件存儲到這個路徑下,不需要申請權限,當應用被卸載的時候,目錄下的文件會被刪除。
需要注意的是,這個文件的目錄和應用的存儲位置有關,
當應用被移動到外部存儲設備的時候,文件的絕對路徑也是變化的,所以建議當數據存儲到這個目錄的時候,用相對路徑。
這個目錄和getFilesDir()目錄最大的不同在於:當安卓設備的存儲空間少,或者不夠用的時候,系統會自動刪除這個目錄下的文件。
官方建議是,超過1MB的文件,建議存儲到getExternalCacheDir()目錄下

/**
 * app私有緩存目錄:刪除所有不再需要的文件,有些項目中清理緩存數據,就是這個文件中的數據
 * 如果在系統即將耗盡存儲,它會在不進行警告的情況下刪除您的緩存文件。
 * 獲取/data/data/<application package>/cache目錄
 */
public static File getInternalCachePath ( Activity activity ) {

    return activity.getCacheDir();
}

3.getExternalFilesDir(null);

應用外部存儲空間(數據文件私有,系統媒體文件無法訪問(例如存了一個MP3文件,通過系統的文件夾管理系統,無法找到)),
當應用被卸載的時候,目錄下的文件會被刪除,但是這裏和getFilesDir()還有不同之處:
只有手機系統使用的是虛擬外部存儲(虛擬SD卡)的時候,
纔可以在卸載應用的同時,自動刪除該目錄下的文件,如果是之前的物理存儲(物理SD卡)則不會自動刪除該目錄,及目錄下的文件
在使用的時候,需要判斷外部存儲的掛載狀態(getExternalStorageState(File)),還需要申請讀寫權限(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
注:當其他應用擁有SD卡讀寫權限的時候,可以訪問該目錄下的文件

/**
 * 外置files文件夾--對應 設置->應用->應用詳情裏面的”清除數據“選項
 * 這個目錄下的所有文件都會被刪除,不會留下垃圾信息
 * SDCard/Android/data/你的應用的包名/files/ 目錄,一般放一些長時間保存的數據
 */
public static File getExternalFilesDir ( Context context ) {
    if ( FileUtil.sdCardIsAvailable() ) {
        return context.getExternalFilesDir( null );
    }
    return context.getFilesDir();
}

4.getExternalCacheDir();

應用外部存儲空間(數據文件私有,系統媒體文件無法訪問(例如存了一個MP3文件,通過系統的文件夾管理系統,無法找到)),
當應用被卸載的時候,目錄下的文件會被刪除,但是這裏和getCacheDir()還有不同之處:
只有手機系統使用的是虛擬外部存儲(虛擬SD卡,現在絕大多數的手機,都不用外掛物理SD卡了)的時候,
纔可以在卸載應用的同時,自動刪除該目錄下的文件,如果是之前的物理存儲(物理SD卡)則不會自動刪除該目錄,及目錄下的文件。
在使用的時候,需要判斷外部存儲的掛載狀態(getExternalStorageState(File)),還需要申請讀寫權限(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
注:當其他應用擁有SD卡讀寫權限的時候,可以訪問該目錄下的文件

/**
 * 外置緩存文件夾 -- 對應 設置->應用->應用詳情裏面的”清除緩存“選項
 * 這個目錄下的所有文件都會被刪除,不會留下垃圾信息
 * SDCard/Android/data/你的應用包名/cache/目錄,一般存放臨時緩存數據
 */
public static File getExternalCacheDir ( Activity activity ) {
    return activity.getExternalCacheDir();
}

5.Environment.getExternalStorageDirectory();

應用外部存儲空間(數據文件非私有,可以被手機的系統程序訪問(如MP3格式的文件,會被手機系統檢索出來),同樣,該目錄下的文件,所有的APP程序也都是可以訪問的,)
注意:外部存儲空間可能處於不可訪問狀態,或者已經被移除狀態,或者存儲空間損壞無法訪問等問題。可以通過getExternalStorageState()這個方法來判斷外部存儲空間的狀態。
注:在該目錄下讀寫文件,需要獲取讀寫權限
該目錄下的文件,這個目錄是用戶進行操作的一個根目錄,進入二級目錄可以通過
getExternalFilesDirs(String), getExternalCacheDirs(), and getExternalMediaDirs().這些方法
官方建議,不要直接使用該目錄,爲了避免污染用戶的根命名空間,應用私有的數據,應該放在 Context.getExternalFilesDir目錄下
其他的可以被分享的文件,可以放在getExternalStoragePublicDirectory(String).目錄下

6.Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

應用外部存儲空間(數據文件非私有,可以被手機的系統程序訪問(如MP3格式的文件,會被手機系統檢索出來),同樣,該目錄下的文件,所有的APP程序也都是可以訪問的,)
這個目錄是用來存放各種類型的文件的目錄,在這裏用戶可以分類管理不同類型的文件(例如音樂、圖片、電影等);
類型如下:

DIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS, DIRECTORY_PICTURES, DIRECTORY_MOVIES, DIRECTORY_DOWNLOADS, DIRECTORY_DCIM, or DIRECTORY_DOCUMENTS

7.面向 Android 7.0 文件存儲變化
對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含文件 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。

通過FiveProvider來解決

1.在AndroidMinfest中

<manifest>
...
<application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider" ##androidx:android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider" ##android:authorities 根據您控制的域將屬性設置爲URI權限; 例如,如果您控制域mydomain.com,則應使用權限 com.mydomain.fileprovider。
        android:exported="false" ##android:exported屬性設置爲 false; FileProvider不需要是公共的
        android:grantUriPermissions="true"> ## android:grantUriPermissions屬性設置爲true,以允許您授予對文件的臨時訪問權限
        
        <meta-data ##<meta-data>元素作爲<provider>定義FileProvider 的元素的子元素
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" /> ##新文件中res/xml/file_paths.xml
            
    </provider>
    ...
</application>
</manifest>

FileProvider只能爲您事先指定的目錄中的文件生成內容URI。要指定目錄,請使用元素的子元素以XML格式指定其存儲區域和路徑

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="" /> ##代表設備的根目錄new File("/");
    <files-path name="files" path="" /> ##代表context.getFilesDir()
    <cache-path name="cache" path="" /> ##代表context.getCacheDir()
    <external-path name="external" path="" /> ##代表Environment.getExternalStorageDirectory()
    <external-files-path name="name" path="path" /> ##代表context.getExternalFilesDirs()
    <external-cache-path name="name" path="path" /> ##代表getExternalCacheDirs()
</paths>

通過FileProvider將file轉化爲content://uri

public void takePhotoNoCompress(View view) {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

核心代碼

FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);

生成的uri:

content://com.zhy.android7.fileprovider/external/20170601-041411.png

看到格式爲:

content://authorities/定義的name屬性/文件的相對路徑 ##即name隱藏了可存儲的文件夾路徑

低版本的系統,僅僅是把這個當成一個普通的Provider在使用,而我們沒有授權,contentprovider的export設置的也是false;導致Permission Denial

@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

exported必須是false,grantUriPermissions必須是true

唯一的辦法就是授權了~

context提供了兩個方法:

grantUriPermission(String toPackage, Uri uri,
int modeFlags)
revokeUriPermission(Uri uri, int modeFlags);

增加了授權後的代碼

public void takePhotoNoCompress(View view) {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);

        List<ResolveInfo> resInfoList = getPackageManager()
                .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }

        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

可以看到grantUriPermission需要傳遞一個包名,就是你給哪個應用授權,但是很多時候,比如分享,我們並不知道最終用戶會選擇哪個app,所以我們可以這樣:

List<ResolveInfo> resInfoList = context.getPackageManager()
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    context.grantUriPermission(packageName, uri, flag);
}

根據Intent查詢出的所以符合的應用,都給他們授權~~

恩,你可以在不需要的時候通過revokeUriPermission移除權限~

那麼增加了授權後的代碼是這樣的:

public void takePhotoNoCompress(View view) {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);

        List<ResolveInfo> resInfoList = getPackageManager()
                .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }

        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

這樣就搞定了,不過還是挺麻煩的,如果你僅僅是對舊系統做兼容,還是建議做一下版本校驗即可,也就是說不要管什麼授權了,直接這樣獲取uri

Uri fileUri = null;
if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

這樣會比較方便也避免導致一些問題。當然了,完全使用uri也有一些好處,比如你可以使用私有目錄去存儲拍攝的照片

文章最後會給出快速適配的方案~~不需要這麼麻煩~

好像,還有什麼知識點沒有提到,再看一個例子吧~
四、使用FileProvider兼容安裝apk

正常我們在編寫安裝apk的時候,是這樣的:

public void installApk(View view) {
    File file = new File(Environment.getExternalStorageDirectory(), "testandroid7-debug.apk");

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(Uri.fromFile(file),
            "application/vnd.android.package-archive");
    startActivity(intent);
}

拿個7.0的原生手機跑一下,android.os.FileUriExposedException又來了~~

android.os.FileUriExposedException: file:///storage/emulated/0/testandroid7-debug.apk exposed beyond app through Intent.getData()

好在有經驗了,簡單修改下uri的獲取方式。

if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

再跑一次,沒想到還是拋出了異常(警告,沒有Crash):

java.lang.SecurityException: Permission Denial: 
opening provider android.support.v4.content.FileProvider 
        from ProcessRecord{18570a 27107:com.google.android.packageinstaller/u0a26} (pid=27107, uid=10026) that is not exported from UID 10004

可以看到是權限問題,對於權限我們剛說了一種方式爲grantUriPermission,這種方式當然是沒問題的啦~

加上後運行即可。

其實對於權限,還提供了一種方式,即:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

我們可以在安裝包之前加上上述代碼,再次運行正常啦~

現在我有兩個非常疑惑的問題:

問題1:爲什麼剛纔拍照的時候,Android 7的設備並沒有遇到Permission Denial的問題?

恩,之所以不需要權限,主要是因爲Intent的action爲ACTION_IMAGE_CAPTURE,當我們startActivity後,會輾轉調用Instrumentation的execStartActivity方法,在該方法內部,會調用intent.migrateExtraStreamToClipData();方法。

該方法中包含:

if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
        || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
        || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
    final Uri output;
    try {
        output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
    } catch (ClassCastException e) {
        return false;
    }
    if (output != null) {
        setClipData(ClipData.newRawUri("", output));
        addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
        return true;
    }
}

可以看到將我們的EXTRA_OUTPUT,轉爲了setClipData,並直接給我們添加了WRITE和READ權限。

注:該部分邏輯應該是21之後添加的。

問題2:爲什麼剛纔拍照案例的時候,Android 4.4設備遇到權限問題,不通過addFlags這種方式解決?

因爲addFlags主要用於setData,setDataAndType以及setClipData(注意:4.4時,並沒有將ACTION_IMAGE_CAPTURE轉爲setClipData實現)這種方式。

所以addFlags方式對於ACTION_IMAGE_CAPTURE在5.0以下是無效的,所以需要使用grantUriPermission,如果是正常的通過setData分享的uri,使用addFlags是沒有問題的(可以寫個簡單的例子測試下,兩個app交互,通過content://)。
五、總結下

終於將知識點都涵蓋到了~

總結下,使用content://替代file://,主要需要FileProvider的支持,而因爲FileProvider是ContentProvider的子類,所以需要在AndroidManifest.xml中註冊;而又因爲需要對真實的filepath進行映射,所以需要編寫一個xml文檔,用於描述可使用的文件夾目錄,以及通過name去映射該文件夾目錄。

對於權限,有兩種方式:

方式一爲Intent.addFlags,該方式主要用於針對intent.setData,setDataAndType以及setClipData相關方式傳遞uri的。
方式二爲grantUriPermission來進行授權

相比來說方式二較爲麻煩,因爲需要指定目標應用包名,很多時候並不清楚,所以需要通過PackageManager進行查找到所有匹配的應用,全部進行授權。不過更爲穩妥~

方式一較爲簡單,對於intent.setData,setDataAndType正常使用即可,但是對於setClipData,由於5.0前後Intent#migrateExtraStreamToClipData,代碼發生變化,需要注意~

好了,看到現在是不是覺得適配7.0挺麻煩的,其實一點都不麻煩,下面給大家總結一種快速適配的方式。
六、快速完成適配
(1)新建一個module

創建一個library的module,在其AndroidManifest.xml中完成FileProvider的註冊,代碼編寫爲:

<application>
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.android7.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

注意一點,android:authorities不要寫死,因爲該library最終可能會讓多個項目引用,而android:authorities是不可以重複的,如果兩個app中定義了相同的,則後者無法安裝到手機中(authority conflict)。

同樣的的編寫file_paths~

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path
        name="root"
        path="" />
    <files-path
        name="files"
        path="" />

    <cache-path
        name="cache"
        path="" />

    <external-path
        name="external"
        path="" />

    <external-files-path
        name="external_file_path"
        path="" />
    <external-cache-path
        name="external_cache_path"
        path="" />

</paths>

最後再編寫一個輔助類,例如:

public class FileProvider7 {

    public static Uri getUriForFile(Context context, File file) {
        Uri fileUri = null;
        if (Build.VERSION.SDK_INT >= 24) {
            fileUri = getUriForFile24(context, file);
        } else {
            fileUri = Uri.fromFile(file);
        }
        return fileUri;
    }

    public static Uri getUriForFile24(Context context, File file) {
        Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
                context.getPackageName() + ".android7.fileprovider",
                file);
        return fileUri;
    }


    public static void setIntentDataAndType(Context context,
                                            Intent intent,
                                            String type,
                                            File file,
                                            boolean writeAble) {
        if (Build.VERSION.SDK_INT >= 24) {
            intent.setDataAndType(getUriForFile(context, file), type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (writeAble) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
        } else {
            intent.setDataAndType(Uri.fromFile(file), type);
        }
    }
}

可以根據自己的需求添加方法。

好了,這樣我們的一個小庫就寫好了~~
(2)使用

如果哪個項目需要適配7.0,那麼只需要這樣引用這個庫,然後只需要改動一行代碼即可完成適配啦,例如:

拍照

public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider7.getUriForFile(this, file);
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}   

只需要改動

 Uri fileUri = FileProvider7.getUriForFile(this, file);

即可。

安裝apk

同樣的修改setDataAndType爲:

FileProvider7.setIntentDataAndType(this,
      intent, "application/vnd.android.package-archive", file, true);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章