【翻譯】Android 數據存儲

之前閑著無聊,研究了下 Android 存儲方面的知識,順便翻譯了下官方文檔(雖然有已經被翻譯過...)。這里就算是水一篇博客好了 # ̄▽ ̄#

1.1 應用數據存儲

Android為你提供了一些用來持久保存應用數據的選擇。你所選擇的解決方案依賴于你明確的需求,例如數據對于你的應用來說是否是私有的,還是能被其他應用(以及用戶)所獲取,以及你的數據需要多大的存儲空間。
你的數據存儲方式為以下幾種:

(1) Shared Preferences 共享偏好

以鍵值對的方式存儲私有的簡單的數據

(2) Internal storage 內部存儲空間

存儲私有數據在設備的內存上

(3) External storage 外部存儲空間

存儲公共數據在共享的外部存儲上

(4) SQLite Databases 數據庫

存儲結構化數據在私有的數據庫中

(5) Network Connection網絡連接

在網絡上用你自己的網絡服務器存儲數據
Android為你提供了一種為其它應用程序暴露私有數據的方式來——使用 content provider . (內容共享)。content provider 是一種可選的組件,它為那些受限于你施加限制的應用數據暴露了讀寫方法。需要更多有關使用conten providers的信息,請見content providers文檔

1.1.1 使用共享偏好

SharedPreferences 該類 提供了一種通用的框架允許你去保存和重新獲取持久性的、以鍵值對方式保存的原始數據種類。你可以使用SharedPreferences去保存任意的原始數據:布爾類型、單精度浮點數、整型、長整型、和字符串類型。這種數據會持續作用在整個用戶會話期間(即使你的應用被銷毀了)。
為了在你的應用中獲取sharedpreferences 對象,可以使用下列兩種方法之一:
· getSharePreferences() – 若果你需要獲取多種以文件名區分的偏好文件,請使用這種方法。可以用第一個參數來指定文件。
· getPreferences() – 若果你只需要一個偏好文件在你的Activity中,請使用這種方法。因為這種方法會為你的Activity提供唯一的preferences 文件,而你不需要提供文件名。
寫入值:
(1) 調用 edit() 函數來獲取一個 SharedPreferences.Editor。
(2) 使用諸如putBoolean()和putString()等方法來添加值。
(3) 使用commit()方法來提交新的值
讀取值,使用SharePreferences的方法,諸如getBoolean()和getString。
以下是一個例子,它在計算器中保存了靜音模式的偏好。

public class Calc extends Activity {
    public static final String PREFS_NAME = "MyPrefsFile";
    @Override
    protected void onCreate(Bundle state){
       super.onCreate(state);
       . . .

       //存儲偏好
       SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
       boolean silent = settings.getBoolean("silentMode", false);
       setSilent(silent);
    }

    @Override
    protected void onStop(){
       super.onStop();

      // 我們需要一個Editor對象來保存偏好的改變
      // 所有的對象都來自于android.context.Context
      SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
      SharedPreferences.Editor editor = settings.edit();
      editor.putBoolean("silentMode", mSilentMode);

      // Commit the edits!
      editor.commit();
    }
}

1.1.2 使用內部存儲

你可以直接在你的設備內部存儲中保存文件。默認情況下,保存在內部存儲中的文件對你的應用來說是私有的,其它應用不能獲取它們(即使是用戶)。當用戶卸載你的應用時,這些文件也會被刪除。
在你的內部存儲中創建和寫一份私有文件:
(1) 通過文件名和操作模式調用 openFileOutput()。它將返回一個FileOutPutStream對象。
(2) 使用Write() 方法,對文件執行寫操作
(3) 使用close() 方法關閉流
舉個例子

String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();

MODE_PRIVATE 會創建一個文件(或者取代原有的同名文件),并且使它對你的應用來說是私有的。
其它可以使用的模式有:MODE_APPEDN MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE
從內部存儲中讀取文件
(1) 調用 openFileInput() 并傳遞文件名。它將返回一個FileInputStream對象
(2) 使用read()方法讀取字節
(3) 使用close()方法關閉流
小提示:在應用中,如果你想在編譯的時候保存一個靜態的文件,那么請在工程的res/raw/目錄保存它。你可以調用openRawResource()方法來打開它,通過傳遞R.raw.<Filename>的id,這個方法返回一個InputStream對象,你可以用它來讀取文件(但是你不能用他來寫入原始文件)
? 存儲緩存文件
如果你想要臨時存儲一些數據,而不是永久性的存儲它。你應該使用getCacheDir()方法去打開一個文件。這個文件代表一個內部存儲路徑,在這你可以存儲臨時的緩存文件。
當設備的內部存儲空間較低時,Android系統可能會刪除這些緩存文件來恢復空間。然而,你不應該依賴于你的系統去為你清理這些文件。你應該始終自己去維持這些緩存文件,并且控制它的空間消耗在一個合理的范圍內,例如1MB。當用戶卸載你的應用時,這些文件將會被刪除。
? 其它有用的方法
getFilesDir()
獲取在文件系統中,你內部文件保存的絕對路徑。
getDir()
在你內部存儲空間中創建(如果存在則打開)路徑。
deleteFile()
刪除內部存儲中的文件。
fileList();
以數組的方式返回你應用中存儲的文件列表。

1.1.3使用外部存儲

任何兼容Android的設備都支持一個共享的外部存儲,你可以在這里存儲文件。外部存儲可以是一種可移除的存儲媒體(例如 SD card)或者內部的(不可移除的)儲存。當用戶接通了USB大容量存儲設備在電腦上傳輸文件時,保存在外部存儲上的文件時世界可讀的(world-readable)并且可以被用戶修改。
注意:外部存儲可能會變成不可獲得的狀態,如果用戶將外部存儲連接到電腦上,或者用戶移除了媒體。并且,由于沒有一種作用于文件的強制安全措施,所用的應用都可以讀或者寫那些存放在外部存儲的文件,用戶也可以刪除它們。
? 獲得對外部存儲的訪問
為了讀寫外部存儲上的文件,你的應用必須獲取READ_EXTERNAL_STORAGE 或者 WRITE_EXTERNAL_STORAGE 兩個系統權限。舉個例子:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

如果你需要同時獲得讀和寫文件的權限,那么你只需要請求WRITE_EXTERNAL_STORAGE 權限。因為它也間接的請求了讀的權限。
提示:從android 4.4開始,這些權限并不是必須的如果你只是去讀寫你應用的私有文件。想要知道更多信息,可以查看以下章節關于 存儲應用私用的文件。
? 查看媒介是否可獲得
在你做任何有關外部存儲的工作之前,你應該總是先調用getExternalStorageState()方法來檢查媒介是否可獲得。媒介可能連接到了電腦,未找到,只讀,或者處于其它狀態。例如,你可以使用以下一堆代碼檢查外部存儲的狀態。
/* 檢查外部存儲是否可獲得以用來進行讀和寫操作*/

public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}
/* 檢查外部存儲是否可獲得并至少能進行讀操作*/
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

這個例子檢查了外部存儲設備是否可獲得以便去進行讀寫操作。getExternalStorageState()方法返回了一些你可能想要檢查的其它狀態。例如,媒介是否能被共享(連接到電腦),是否完全未找到,還是已經被永久的移除,等等。當你的應用需要連接到媒體的時候,你可以使用這些狀態以告知用戶更多的信息。
? 存儲可以被其它應用共享的文件
通常來講,用戶可能會通過你的應用獲得新的文件,這些文件應該被保存在設備上的公有空間中。這樣其它應用就能獲取它們,并且用戶也能很容易拷貝這些文件。當你這樣做時,你應該使用其中一種共享空間的路徑,例如 Music/,Pictures/,and Rintones/
為了獲得一個代表合適的公有路徑的File對象,調用getExternalStoragePublicDirectory()方法,并傳遞一個你想要的路徑類型,例如DIRECTORY_MUSIC, DIRECTORY_PICTURES, DIRECTORY_RINGTONES,或其它。通過合理的存放你的文件在對應的媒體種類路徑下,系統的媒體掃描器就能合理的在系統中分類你的文件(例如,鈴聲會在系統設置中以鈴聲出現,而不是音樂)。
舉個例子,以下是一種在公有相冊路徑下,為新的相冊創建路徑發方法。

public File getAlbumStorageDir(String albumName) {
    // 獲取用戶公有的相冊路徑
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

? 存儲應用私有的文件
如果你正在處理一些不打算被其他應用使用的文件(例如,只會被你的應用使用的圖像紋理或者音效),你應該使用getExternalFilesDir()在外部存儲上創建一個私有存儲路徑。這個方法也需要一個類型參數來指明子路徑的類型(例如 DIRECTORY_MOVIES),如果不需要一個明確的媒體路徑,傳值null來獲得你應用私有路徑的根路徑。
從Android4.4開始,讀寫應用私有路徑中的文件不再需要 READ_EXTERNAL_STORAGE 或者WRITE_EXTERNAL_STORAGE 兩個權限。所以這兩個權限只有在maxSdkVersion的版本低于18時,才需要被聲明。

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
    ...
</manifest>

注意:當用戶卸載了你的應用時,這個路徑及其中的內容都會被刪除。系統媒體掃描器也不會去讀取這些路徑下的文件。所以這些文件在meiaStore的content provider中是獲取不到的。同理,你也不應該在這些路徑存下存放那些本質上屬于用戶的媒體,例如通過你的應用捕獲或處理過的照片,或者用戶通過你的應用購買的歌曲,這些文件看起來本應該被保存在共享路徑中的文件。
有時候,設備可能會分配一部分內部存儲作為外部存儲,當然也可能會提供SD卡的卡槽。當這種設備運行在Android4.3或者更低的版本上時,getExternalFilesDir()方法只會提供一本分內部存儲的使用權,你的應用并不能讀寫SD卡上的文件。然而,從Android 4.4開始,通過getExternalFilesDirs()方法,你即可以獲得外部存儲的使用權,也可以獲得內部存儲的使用權。它返回一個File對象的數組,該數組包含每一個位置的入口路徑。數組中所包含的第一個入口路徑,被認為是主要的外部存儲,并且你應當使用這一場所,除非它已經被占滿或者是不可獲得的。如果你想要在Android4.3或以下版本獲得兩者的路徑,可以使用支持包中的靜態方法。ContextCompat.getExternalFilesDirs(). 這個方法也返回了一個File對象的數組,但在Android4.3或以下的版本上,它通常只包含一個入口。
注意:盡管getExternalFilesDir()和getExternalFilesDirs()方法提供的路徑并不會被MediaStore的Content provider獲得。但是,其它擁有READ_EXTERNAL_STORAGE權限的應用可以獲得所有外部存儲上的文件,并包含它們。如果你需要嚴格的限制你的文件的使用權,你應該將你的文件寫在內部存儲上。
? 存儲緩存文件
想要打開一個代表著在外部存儲上存放緩存文件的路徑,你可以調用getExternalCacheDire()方法,如果用戶卸載你的應用,這些文件會自動被刪除。
類似于上述所提到的ContextCompat.getExternalFilesDirs()方法,你同樣可以獲取在第二個外部存儲上的緩存入口(如果可以獲得的話),通過調用ContextCompat.getExternalCacheDirs()方法。
小提示:為了保護你的文件存儲空間,并維持你應用的表現。在整個應用的生命周期中,細心的管理你的緩存文件,并在它們不被需要的時候移除它們這些,這是很重要的。

1.1.4使用數據庫

Android 提供了完整的SQLite數據庫的支持。任何類都能通過數據庫名稱來訪問你創建的數據庫,但是該應用之外的類側不能。
建議通過繼承SQLiteOpenHelper類來創建新的SQLite數據庫,并重寫其中的onCreate()方法,在該方法中你可以執行一條SQLite命令以便在數據庫中創建表。
舉例:

public class DictionaryOpenHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 2;
    private static final String DICTIONARY_TABLE_NAME = "dictionary";
    private static final String DICTIONARY_TABLE_CREATE =
                "CREATE TABLE " + DICTIONARY_TABLE_NAME + " (" +
                KEY_WORD + " TEXT, " +
                KEY_DEFINITION + " TEXT);";
    DictionaryOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(DICTIONARY_TABLE_CREATE);
    }
}

然后你可以通過你定義的構造器來獲得繼承SQLiteOpenHelper的類的實例。為了向數據庫中寫入和讀出數據,可以分別調用getWriteableDatabase()和getReadableDatabase()方法。它們都會返回一個SQLiteDatabase對象,它代表著一個數據庫對象并向外提供對SQLite的操作方法。
通過使用SQLiteDatabase query()方法,你可以對SQLite數據庫執行查詢操作,它可以接受不同種類的查詢參數,例如表名、投影、選擇運算、列名、分類以及其它參數。為了執行一些更復雜的查詢操作,例如那些需要列的別名的操作,你應該使用SQLiteQueryBuilder這個類,它提供了一些便捷的操作方法來構建查詢操作。
每一次SQLite的查詢操作都會返回一個游標,它指向所有查詢結果的行。游標的原理是使你通過它可以駕馭你從數據庫中查詢的結果,并讀取行和列。
想獲得一些在Android中演示如何使用SQLite數據庫的例子,可以查看Note Pad和Searchable Ditionary 這些應用。
? 數據庫調試
Android SDK 包含了一個 sqlite3 數據庫工具,它允許你瀏覽表的內容,運行SQL命令,并執行其它有用的SQLite數據庫操作。查閱Examining sqlite3 databases from a remote shell 來學習如何使用該工具。

1.1.5使用網絡連接

你可以使用網絡(當可獲得時) 在你自己的基于web的服務上,來存儲并重新獲取數據,為了獲得更多的聯網操作,你可以使用以下包中的類:
java.net
Android.net

1.2應用安裝路徑

從API版本8開始,你可以允許你的應用安裝在外部存儲上(例如,設備的SD卡)。這是一個可選項,你可以在Mainfest文件中用 android:installLocation 參數來聲明它。如果你沒有聲明這個元素,你的應用只會被裝在內部存儲上,并且不能被移動到外部存儲中。
為了允許系統在外部存儲上安裝你的應用,修改mainfest文件,在其中包含android:installLocation參數,可以使用的值有preferExternal 或者 auto
舉例:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:installLocation="preferExternal"
    ... >

如果你聲明的參數的值是preferExternal ,則你要求你的應用裝載在外存儲上 。但是系統并不保證你的應用會被安裝在外部存儲上。如果外部存儲已經滿了,系統會安裝在內部存儲上。用戶可以在外部存儲和內部存儲之間轉移你的應用。
如果你聲明的參數是 auto,則你暗示你的應用可能會被安裝在外部存儲上,但你對安裝的位置并沒有指明。系統會基于一些因素來決定在什么位置安裝你的應用。用戶同樣也可以在外部存儲和內部存儲之間轉移你的應用。
當你的應用被安裝在外部存儲上時:
(1) 只要外部存儲處于連接狀態,對于應用的表現就沒有任何影響。
(2) .apk文件會被保存在外部存儲上,但是用戶所有的私有數據,數據庫,優化后的.dex文件以及提取的本地代碼都會被保存在內部存儲上。
(3) 唯一的用來存儲你應用的容器已經被一個隨機產生的密鑰加密過。這個密鑰只會被最初安裝它的設備所加密。因此,一個被安裝在外部存儲上的應用只會為一臺設備工作。
(4) 用戶可以通過系統設置將你的應用移動到內部存儲上。
警告:當用戶通過USB大容量存儲設備與你的電腦共享文件或者通過系統設置移除了你的SD卡,使得外部存儲對于你的設備來說變得不可獲得,那么被安裝在外部存儲上的應用都會立即被結束掉。

1.2.1向后兼容性

讓應用安裝在外部存儲上的這一特征,只有設備運行的api版本在8(Android的版本2.2)或以上才有用。那些基于api版本8構建的應用總是會被安裝在內部存儲上,并且不能被轉移到外部存儲上(即使設備的api版本為8),然而當你的應用是為api版本8或以下的版本來設計的時候,你可以選擇是否支持api版本為8或者以上的版本,并通過使用api版本8或者更低來編譯。
為了讓應用安裝在外部存儲上,并對低于api 8 的版本保持兼容,你需要:
(1) 在<mainifest>元素中,包含 android:installLocation參數,使用”auto”或”preferExternal” 兩個值之一。
(2) 使你的 android:minSdkVersion 參數保持在低于api 8的版本,并確保你應用中的代碼只使用了那些兼容該版本的應用程序接口。
(3) 為了編譯你的應用,修改構建目標的api 版本為 8。這是必須的,因為在更早的Android庫中不明白 android:installLocation 這個參數,當它出現時,也不會去編譯它。
(4) 當你的應用被安裝在低于api本版8的設備上時,android:installLocation參數會被忽略,并且應用會被安裝在內部存儲上
注意:盡管在更早的版本中,xml中的標記會被忽略,但當你的minSdkVersion小于8 時你必須小心不要去使用api版本8中的應用程序接口,除非你在你的代碼中做過了必要的向后兼容的工作。

1.2.2不應該被安裝在外部存儲上的應用

當用戶連接了USB大容量存儲設備在電腦上共享文件時(或者卸載、移除了外部存儲),任何正在運行的、安裝在外部存儲上的應用都會被結束掉。系統實際上會不知道這些應用在哪,直到大容量存儲設備被重新安裝到設備上。除去結束掉應用,使應用不能被用戶獲得的后果之外,這種做法也會使得某些種類的應用發生嚴重的錯誤。為了讓你的應用一直表現的如你預期那樣,如果它使用了以下的特征,你不應該允許你的應用被安裝在外部存儲上。當外部存儲未被裝載時,可能會出現以下結果:

服務:

你正在運行的服務會被結束掉,即使外部存儲被重新裝載時也不會重新運行。但是,你依然可以注冊ACTION_EXTERNAL_APPLICATIONS_AVAILABLE的事件廣播。當被安裝在外部存儲上的應用對系統來說變得可獲得時,這個廣播會通知你的應用,此時你可以重啟你的服務。

鬧鐘服務:

你的通過AlarmManager注冊的鬧鈴會被取消,你必須手動再次注冊鬧鐘服務,當外部存儲被重新裝載時。

輸入法引擎:

你的輸入法引擎會被默認的取代。當你的外部存儲被重新裝載時,用戶可以打開系統設置重新使用你的輸入法引擎。

動態壁紙:

你正在運行的動態壁紙會被默認的動態壁紙所代替。當外部存儲被重新裝載時,用戶可以重新選擇你的動態壁紙。

應用組件:

你的應用組件會從主界面移除。當外部存儲重新裝載時,你的應用組件對于用戶來說是不可選取的,直到系統重置主界面應用(通常直到系統重啟,都不會這樣)

賬戶管理:

你通過AccountManager創建的用戶會消失,直到外部存儲重新裝載。

異步適配:

你的AbstractThreadedSyncAdapter和其它異步方法都會停止工作,直到外部存儲重新裝載。

設備管理者:

你的DeviceAdminReceiver及其所有管理功能都會不能使用,這可能會給設備功能造成不可預見的后果,即便外部存儲重新裝載后,這個問題也會持續。

監聽啟動完成的廣播接收者:

在外部存儲被裝載之前,系統會發送ACTION_BOOT_COMPLETED的廣播。如果你的應用安裝在外部存儲上,你永遠也接受不到這個廣播。

如果你的應用使用了任何以上列出的特征,你應該允許你的應用被安裝在外部存儲上。默認情況下,系統也不會允許你的應用安裝在外部存儲上,所以你也不需要擔心那些已經存在的應用。然而,如果你的確信你的應用永遠也不應該被安裝在外部存儲上,那么你可以通過聲明android:installLoaction的值為”internalOnly”。盡管這并不會改變默認行為,但這個聲明明確的指出了你的應用應該被安裝在內部存儲上,并作為一個提醒告知其它開發者。

1.2.3應該被安裝在外部存儲上的應用

簡而言之,任何沒有使用到上述特征的應用安裝在外部存儲上時,都是安全的。大型游戲一般都是應該被安裝在外部存儲上的應用類型,因為當游戲閑置時,不需要額外的服務。當外部存儲變得不可獲得時,游戲進程會被結束掉。當外部存儲重新可獲得,用戶重啟了游戲時(假設游戲在整個活動周期中合理的保存了它的狀態) 也不會有任何可見的影響。
如果你的應用需要一些兆字節的文件,你應該仔細考慮是否應該將應用安裝在外部存儲上,以便讓用戶更好的保護內部存儲上的空間。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容