1. 程式人生 > >Android實現資料持久化的三種方法

Android實現資料持久化的三種方法

考慮下面幾種情況:

  • 使用者對應用的亮度、音量、字型顏色等進行了配置;
  • 使用者在遊戲過程中打出了新的記錄;
  • 使用者使用應用下載了一些圖片、音樂等大檔案;

在使用者使用應用時,上述情況經常會出現,此時就要求應用具有儲存這些資料並在以後讀取的能力,也就是所謂的資料持久化。
Android系統為開發人員提供了三種實現資料持久化的方式,分別是儲存到SharedPreferences、儲存到SQLite資料庫以及儲存到檔案。

儲存到SharedPreferences

使用SharedPreference是儲存較小的鍵值對的最佳方法。每個SharedPreference物件指向一個儲存鍵值對的shared preference檔案,併為讀寫不同型別的鍵值對提供了方便的方法。

獲取SharedPreferences物件

Context類提供了建立或訪問shared preferences檔案的方法:

  • getSharedPreferences(String name, int mode):當需要多個shared preferences檔案時使用。通過name指定shared preferences檔案的名稱。
  • getPreferences(int mode):當只需一個shared preferences檔案時,可以使用這個方法獲得預設的shared preferences檔案。

mode一般都設定成Context.MODE_PRIVATE,表示只有建立該檔案的應用才能訪問它。‘

向shared preferences檔案中寫入

獲得SharedPreferences物件後,就可以對其進行寫入了。一般步驟如下:
1. 呼叫SharedPreferences的例項方法edit()建立一個SharedPreferences.Editor物件;
2. 通過Editor物件的putXXX()方法傳遞鍵值對;
3. 呼叫Editor物件的apply()方法(在後臺進行資料儲存)或者commit()方法(阻塞當前執行緒並立即進行資料儲存)提交。

示例如下:

SharedPreferences preferences = getPreferences(Context.MODE
_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putInt("HighScore",100); editor.apply();

從shared preferences檔案中讀取

讀取shared preferences檔案中的鍵值對非常容易,只要先獲得SharedPreferences物件,之後呼叫各類getXXX()方法獲取鍵值對即可。示例如下:

SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
int highScore = preferences.getInt("HighScore",0);

需要注意的一點是,所有的get方法都需要傳入一個預設值,它會在找不到相應值的時候作為結果返回。

儲存到SQLite資料庫

對於較大量的、結構化的資料,通常的做法是將它們儲存到資料庫中。Android系統提供了對SQLite資料庫的支援。

建立Contract類

建立Contract類這個步驟不是絕對必須的,但它是Android類庫中組織資料庫結構的一個規範做法:每個Contract類代表一個數據庫,其中的每一個內部類代表一張表。下面是一個例子:

public static final class MyDbContract {
    private MyDbContract(){

    }

    public static final class Age implements BaseColumns {
        private Age(){

        }

        public static final String TABLE_NAME = "NameToAge";
        public static final String COLUMN_NAME_USER_NAME = "name";
        public static final String COLUMN_NAME_USER_AGE = "age";
    }
}

上面的Contract類可以這麼解讀:

  • 資料庫名稱為MyDb;
  • 有一張名為NameToAge的表,表由name列和age列組成。

注意:Age類實現了BaseColumns介面,這使得它能夠繼承到一個名為_ID的主鍵,這是讓資料庫和Android系統中的一些框架(如Cursor)相容的關鍵。

實現SQLiteOpenHelper類

SQLiteOpenHelper是Android系統提供的用於管理資料庫的建立與訪問的類。通過這個類,Android系統會將資料庫儲存到應用的私密空間,保證資料不會被其他應用訪問到。
下面是實現SQLiteOpenHelper的一個例子:

public class MyDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "MyDb.db";

    private static final String SQL_CREATE_TABLE_AGE =
            "CREATE TABLE " + MyDbContract.Age.TABLE_NAME + " ("
            + MyDbContract.Age._ID + " INTEGER PRIMARY KEY" + ","
            + MyDbContract.Age.COLUMN_NAME_USER_NAME + " TEXT" + ","
            + MyDbContract.Age.COLUMN_NAME_USER_AGE + " INTEGER"
            + ")";

    public MyDbHelper(Context context){
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_TABLE_AGE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

分別說明一下構造器以及各個回撥方法:
(1)構造器:構造器中呼叫了超類的構造器SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version),引數分別為用於建立資料庫的context物件、資料庫名、用於加工Cursor物件的CursorFactory、資料庫版本(一般從1開始)。
(2)onCreate():當首次建立資料庫時呼叫。一般會在這裡面建立各個表。在上面的例子中,通過原始SQL語句CREATE TABLE建立了一張表。
(3)onUpgrade():當資料庫版本升級時呼叫。
其他還有一些回撥方法,如onDowngrade()等,可以在需要時選擇性地實現。

獲取SQLiteDatabase例項

當使用SQLiteOpenHelper做一些與資料庫有關的操作時,系統會對那些有可能比較耗時的操作(例如建立與更新等)在真正需要的時候才去執行,而不是在app剛啟動的時候就執行。
對於開發者而言,需要做的僅僅是執行SQLiteOpenHelper的getWritableDatabase()或者getReadableDatabase()方法獲得SQLiteDatabase例項。需要注意的是,由於建立與訪問資料庫可能會很耗時,因此最好在後臺執行緒中執行這些操作。

新增資料

通過向SQLiteDatabase的insert()方法傳遞ContentValues物件實現:

private void put(SQLiteDatabase database,String name, int age){
    ContentValues values = new ContentValues();
    values.put(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_NAME,name);
    values.put(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_AGE,age);

    database.insert(
            MyDbHelper.MyDbContract.Age.TABLE_NAME,
            null,
            values
    );
}

insert()方法的第一個引數是將要插入的表的名稱,第二個引數是當values為空時需要被置為Null的列的名稱(SQLite不允許插入所有列都為空的行)。

查詢資料

通過SQLiteDatabase的query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy)方法實現:

Cursor cursor = database.query(
        MyDbHelper.MyDbContract.Age.TABLE_NAME,
        projections,
        null,
        null,
        null,
        null,
        MyDbHelper.MyDbContract.Age._ID + " ASC"
);

query()的引數和原始SQL語句的組織方式大致相同,如下:

  • table:表名,對應SQL中的FROM子句;
  • columns:需要返回的列名,對應SQL中的SELECT子句。
  • selection:選取條件,對應SQL中的WHERE子句。語句中可以使用”?”萬用字元,具體的值會用下面的selectionArgs中的元素代替。
  • selectionArgs:選取條件引數,用於填充selection中的“?”。舉例:假設selection == “name = ?”,selectionArgs == {“Bob” },那麼最後selection與selectionArgs拼成的WHERE子句為WHERE name = Bob。
  • groupBy:分組,對應SQL中的GROUP BY子句。
  • having:過濾組,對應SQL中的HAVING子句。
  • orderBy:排序,對應SQL中的ORDER BY子句。
  • limit(可選):返回行數,對應SQL中的LIMIT子句。

方法返回一個Cursor物件,關於Cursor物件的使用可以查閱其文件,這裡是一個示例:

while (cursor.moveToNext()){
    Log.i(
            cursor.getString(cursor.getColumnIndex(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_NAME)),
            String.valueOf(cursor.getInt(cursor.getColumnIndex(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_AGE))));
}

刪除資料

通過SQLiteDatabase的delete(String table, String whereClause, String[] whereArgs)方法實現。

更新資料

通過SQLiteDatabase的update(String table, ContentValues values, String whereClause, String[] whereArgs)方法實現。

儲存到檔案

對於使用者下載的圖片、音樂等,將其儲存到檔案中儲存起來是最合適的方法。

儲存到internal storage

internal storage是每個應用專屬的一塊儲存空間,相當於應用的根目錄,對於應用來說總是可用的。當用戶解除安裝應用時,internal storage中的資料會全部被清除掉。預設情況下,無論是使用者還是其他應用都無法訪問該區域。
可以通過以下兩個方法獲取指向internal storage的File物件:

  • Context的例項方法getFilesDir(),指向應用的internal目錄。
  • Context的例項方法getCacheDir(),指向應用的internal快取目錄。當系統空間不足時,會自動清除該目錄下的檔案,因此該目錄下只應當儲存較小的快取檔案。

下面嘗試列印這兩個目錄的地址:

Log.i("path",getFilesDir().toString());
Log.i("path",getCacheDir().toString());

結果為:
10-03 10:38:03.329 26289-26289/com.example.swt369.filepathtest I/path: /data/user/0/com.example.swt369.filepathtest/files
10-03 10:38:03.329 26289-26289/com.example.swt369.filepathtest I/path: /data/user/0/com.example.swt369.filepathtest/cache

此外,Context還提供了一個獲取指定internal目錄下的檔案的FileOutputStream的方法openFileOutput(String name, int mode)。mode一般為Context.MODE_PRIVATE。
想要快速建立快取檔案的話,可以利用File的靜態方法createTempFile(String prefix, String suffix, File directory)

儲存到external storage

external是手機提供的公用儲存區域,未root的手機開啟檔案管理等應用,看到的目錄就是external storage的根目錄。應用如果選擇將資料儲存在這裡面,那麼這些資料對於使用者和其他應用而言都是可見的。

檢驗external storage是否可用

external storage可能是不可用的(某些手機可能沒有內建的SD卡),因此在訪問之前需要檢驗其可用性,方法如下:

 /* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

儲存public檔案

應用為使用者下載的音樂、視訊等,即使應用被解除安裝了也應當被保留,此時就應當將它們以public的形式儲存起來。Environment類提供了訪問公用儲存區域的File物件的方法:

  • Environment.getExternalStorageDirectory():返回指向公用儲存區域根目錄的File物件。
  • getExternalStoragePublicDirectory(String type):根據type返回指向不同型別的公用儲存區域目錄的File物件。Environment提供了一組type常量,有DIRECTORY_MUSIC,DIRECTORY_DOWNLOADS,DIRECTORY_DOCUMENTS等。

下面做一個簡單測試:

Log.i("path",Environment.getExternalStorageDirectory().toString());
Log.i("path",Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString());

結果為:
10-03 11:06:41.384 7364-7364/? I/path: /storage/emulated/0
10-03 11:06:41.385 7364-7364/? I/path: /storage/emulated/0/Download

注意:訪問這塊區域是需要獲取android.permission.READ_EXTERNAL_STORAGE或者android.permission.WRITE_EXTERNAL_STORAGE許可權的。當獲取到寫許可權時,也會同時獲取到讀許可權。在Android 6.0及以上版本,這兩個許可權被視作危險許可權,只能夠在執行時動態獲取。

儲存private檔案

有些檔案雖然被儲存到了external storage中,但是它可能只會被建立它的應用使用,比如QQ、微信快取的聊天表情、小視訊等。Android系統為這樣的檔案提供了一個儲存目錄:公用儲存區域根目錄/Android/data/應用包名/files。這個目錄除了外部可訪問外,和internal storage的性質非常相似:解除安裝時會被清除、(自Android 4.4)不需要申請訪問許可權(僅針對與應用包名匹配的那個目錄,如果訪問其他應用的目錄還是需要申請許可權)
可以利用Context的例項方法getExternalFilesDir(@Nullable String type)獲取指向該目錄的File物件。type常量同樣由Environment類提供。示例如下:

Log.i("path",getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString());

結果為:
10-03 11:14:57.947 9412-9412/com.example.swt369.filepathtest I/path: /storage/emulated/0/Android/data/com.example.swt369.filepathtest/files/Download