解讀Android之資料儲存方案
本文翻譯自android官方文件,結合自己測試,整理如下。
Android提供了一些永久儲存資料的方法,可以根據具體的需求決定使用哪種方式儲存,例如私有資料,外部程式是否可以訪問等等。有以下幾種方法儲存:
- Shared Preferences
使用鍵值對儲存私有資料型別 - Internal Storage(或稱為檔案儲存)
使用內部儲存儲存私有資料 - External Storage
使用外部儲存儲存公共資料 - SQLite Databases
使用私有資料庫儲存結構化資料 - Network Connection
儲存在網路伺服器中
當然Android中提供了一種使用content provider可以將私有資料暴露給外部程式使用。有興趣地可以參考我之前翻譯的文章:
下面分別介紹以上四種(Network Connection不介紹)。
使用Shared Preferences
SharedPreferences類提供了一個基本的框架,能夠使我們儲存和檢索私有鍵值對,可以儲存的型別有:boolean,float,int,long,String。這些資料將會永久儲存。
為了能過獲得SharedPreferences物件,我們可以使用以下兩種方法中的任何一種:
getSharedPreferences()
如果需要使用多個通過名字識別的儲存檔案,使用該方法。該方法需要指定檔名。getPreferences()
如果在activity中只需要一個儲存檔案的話,使用該方法。由於該方法只能建立一個檔案,因此不需要提供檔名。
然後,可以通過下面的步驟完成寫資料:
- 呼叫
edit()
獲得SharedPreferences.Editor
物件; - 通過
putXXX()
方法新增資料; - 完成新增資料時呼叫
commit()
;
為了讀取資料,可以使用SharedPreferences中的getXXX()
方法。
注意:Shared Preferences方式不是嚴格意義上的儲存使用者偏好(user preference),例如儲存使用者選擇的鈴聲。若想要實現這種功能的話可以繼承PreferenceActivity類,該類是Activity框架,但是能夠自動永久儲存使用者偏好(也是使用Shared Preferences)。當然對於其他的控制元件來說,Android也提供了相應的處理辦法。例如:CheckBoxPreference, EditTextPreference, ListPreference, MultiSelectListPreference, PreferenceCategory, PreferenceScreen, SwitchPreference
使用內部儲存
我們可以直接將資料儲存在內部儲存上。預設情況下,儲存在記憶體上的資料是對程式私有的,外部程式無法獲取。該儲存方式又可稱為檔案儲存,是android中一種比較簡單的儲存方式,它不對儲存內容進行任何處理(怎麼讀的怎麼存),就是利用java中的檔案輸入輸出流來管理資料。
我們可以通過以下方法實現內部儲存:
- 呼叫Context類中的
openFileOutput()
方法,該方法返回FileOutputStream
; - 呼叫
write()
寫資料; - 呼叫
close()
關閉流。
例如:
String FILENAME = "hello_file";
String string = "hello world!";
// FILENAME可能是存在或不存在的檔名,若存在則替換現有的
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
openFileOutput()
接收兩個引數:檔名和操作模式。檔名不能包括路徑,這是因為所有的檔案都有預設的位置:/data/data//files/。操作模式有:MODE_PRIVATE
(預設情況,程式私有,若檔案存在覆蓋原有),MODE_APPEND
(若檔案存在,則在內容後面新增而不是替換,若不存在直接建立),MODE_WORLD_READABLE
(API17後已棄用),MODE_WORLD_WRITEABLE
(API17後已棄用)。
通過上面的例子可以看到和java輸入輸出流一樣。
通過下列方法可以讀取資料:
- 呼叫
openFileInput()
方法,該方法返回FileInputStream; - 呼叫
read()
方法讀取資料; - 呼叫
close()
方法關閉流。
openFileInput()
方法只接受一個檔名引數,系統會自動在/data/data//files/目錄下查詢,之後呼叫java流進行讀取資料。
注意:若想在編譯時儲存靜態檔案,該檔案儲存在專案的res/raw/目錄下。使用Resources的例項方法openRawResource()
獲取InputStream讀取資料,該方法引數為:R.raw.<filename>
。但是我們不能向該檔案中寫內容。
儲存快取檔案
若我們想快取某些資料而不是永久儲存的話可以使用Context類中的getCacheDir()
方法開啟一個檔案,該檔案代表了一個可以儲存臨時檔案的絕對路徑(即/data/data//cache)。當內部儲存空間不足時,可能會刪除這些快取,然而我們通常需要在程式中限制並清除這些快取,大小最好不要超過1MB。當用戶把我們的程式解除安裝時應該刪除這些快取。
在獲得以上目錄檔案後就可以根據java輸入輸出流對檔案進行讀寫。
其它方法
Context中還提供了一下方法方便我們處理檔案儲存:
getFilesDir()
獲得內部檔案儲存的絕對路徑。getDir()
開啟或建立一個內部儲存空間中的目錄。deleteFile()
刪除一個檔案fileList()
返回檔案列表。
使用外部儲存
每一個android相容的裝置都支援共享的外部儲存,我們可以儲存檔案資料。這些裝置可以是可拆卸的(例如SD卡)或者是內部的。儲存在外部儲存上的資料外部程式是可以獲取的,並能夠通過USB傳到電腦上進行修改。
注意:如果使用者連線到計算機或刪除外部裝置上的媒體檔案,外部儲存可能會變得不可用,並沒有安全強制執行儲存到外部儲存的檔案。所有應用程式都可以讀取和寫入存放在外部儲存上的檔案,使用者也可以移除它們。
獲取許可
想要讀取或寫入外部裝置上的檔案,我們的程式必須獲得READ_EXTERNAL_STORAGE
或WRITE_EXTERNAL_STORAGE
系統許可。例如:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
若想同時獲得讀寫許可的話, 只需要另一個許可即可(寫許可隱式包含了讀許可)。
檢查媒體檔案的可用性
在使用外部裝置時,我們應該首先呼叫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/,Ringtones/。
為了獲得一個合適的公共目錄檔案,可以呼叫getExternalStoragePublicDirectory()
,目錄型別可以有:DIRECTORY_MUSIC
,DIRECTORY_PICTURES
,DIRECTORY_RINGTONES
等。按照目錄型別建立檔案並存放相應型別的內容以便系統方便尋找。例如儲存媒體型別的檔案在相應的目錄中時,系統媒體掃描器能夠對檔案進行合適的分類(for instance, ringtones appear in system settings as ringtones, not as music)。
例如下面一個方法用於建立一個名為album資料夾用於存放圖片, 該資料夾在公共的圖片目錄下:
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
注意:為了迴避媒體掃描器的掃描,我們可以建立一個以.nomedia
為名字的空檔案。該檔案能夠禁止掃描器讀取媒體檔案。但是若我們的檔案是程式私有的,應該在私有的目錄下儲存它們。
儲存私有檔案
若想儲存程式私有檔案,則需要私有儲存目錄儲存檔案,可以呼叫getExternalFilesDir()
。該方法接收一個型別引數,能夠指定子目錄的型別(例如DIRECTORY_MOVIES
)。若不需要指定媒體目錄,可以需要傳遞null,來接收私有目錄的根目錄。
從Android4.4之後,讀寫私有目錄下的檔案不需要許可READ_EXTERNAL_STORAGE
或WRITE_EXTERNAL_STORAGE
。我們可以宣告使用許可權的最大版本號,如下:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest>
注意:當程式被解除安裝後,這些檔案目錄將都會刪除。系統媒體掃描器不能夠讀取這些目錄的檔案。因此我們不能使用這些目錄儲存屬於使用者的媒體,例如使用者下載的圖片等,這些檔案應該儲存在公共的目錄下,以防在解除安裝程式後刪除這些檔案。
有時候,一種裝置,該裝置分配一個記憶體作為外部儲存,也可以提供一個SD卡插槽。那麼該裝置執行在4.3及以下系統上時,getExternalFilesDir()
只能獲得內部儲存檔案,我們的app不能讀寫到SD卡上。4.4開始以上兩個位置都可以獲取,通過getExternalFilesDirs()
方法,該方法返回檔案陣列。若想在低版本中使用,則可以使用相容庫中的靜態方法ContextCompat.getExternalFilesDirs()
。雖然仍返回一個檔案陣列,但通常只有一個元素。
注意儘管通過getExternalFilesDir()
和getExternalFilesDirs()
獲得的目錄不能通過MediaStore content provider獲取,但是其他擁有READ_EXTERNAL_STORAGE
許可的程式能夠獲取所有外部儲存上的檔案。若要做到嚴格限制的話,需要使用內部儲存。
儲存快取檔案
通過呼叫getExternalCacheDir()
可以用於儲存快取檔案。當用戶解除安裝程式時,這些檔案自動刪除。通過呼叫ContextCompat.getExternalCacheDirs()
可以將快取檔案儲存在第二個儲存裝置上。
注意:為了充分利用檔案空間並且提高程式效能,因此管理好快取檔案非常重要,並且在不需要它們的時候移除它們。
在獲得以上目錄檔案後就可以根據java輸入輸出流對檔案進行讀寫。
使用資料庫
android完全支援SQLite資料庫,在程式內的任何類都可以訪問我們建立的資料庫,其它外部程式則不能直接訪問。
SQLite是一款輕量級的關係型資料庫,它的運算速度非常快,佔用資源非常小,通常只需要幾百KB的記憶體就能夠滿足,因此特別適合移動裝置。
建立SQLite資料庫的一個好的方法是建立一個抽象類SQLiteOpenHelper的子類,該抽象類是一個幫助類,可以方便的對資料庫進行建立和升級。在SQLiteOpenHelper類中有兩個重要的抽象方法:onCreate()
和onUpgrade()
,我們必須實現這兩個方法,前者用於建立資料庫,後者用於升級資料庫。並且這兩個方法無須我們呼叫,系統會在合適的地方呼叫(下面有講到)。
SQLiteOpenHelper類中還有兩個重要的方法:getReadableDatabase()
和getWritableDatabase()
,兩者都可以開啟(若沒有則建立)現有的資料庫,並返回一個SQLiteDabase物件,然後使用該物件就可以對資料庫進行對數的操作。。兩者的不同點在於:前者在資料庫不可寫入(如空間已滿)時,返回的物件只能以只讀的方式開啟資料庫;而後者將會丟擲異常。資料庫檔案會存放在/data/data//databases/目錄下。
由於SQLiteOpenHelper類沒有無參構造器,因此在繼承SQLiteOpenHelper類時,必須要呼叫父類構造器,而通常來說,我們可以呼叫引數較少的一個構造器。
下面我們來看一個具體的示例:
/**
* SQLiteOpenHelper練習
* SQLiteOpenHelper是一個管理SQLite資料庫的幫助抽象類
* Created by sywyg on 2015/5/20.
*/
public class MyDatabaseHelper extends SQLiteOpenHelper{
private Context mContext;
/**
* SQL語句,
* SQLite中支援的資料型別包括:null, integer整型,text文字型別,real浮點型別,
* blob二進位制型別(應該是任意輸入的數值)
* primary key 表示設定主鍵,autoincrement表示id自動增長
*/
public static final String CREATE_BOOK = "create table book(" +
"id integer primary key autoincrement," +
"author text," +
" price real," +
" state blob)";
public static final String CREATE_CATEGORY = "create table category(" +
"id integer primary key autoincrement," +
"name text," +
" state blob)";
/**
* java語法:若父類沒有無參構造器的話,則子類必須呼叫父類構造器,否則在例項化子類的時候無法呼叫父類構造器。
* 因此這個構造器(或另一個引數多的構造器)是必須的
* @param context 當前訪問資料庫的元件
* @param name 資料庫名稱
* @param factory 自定義Cursor ,一般為null
* @param version 資料庫版本號,可用於對資料庫升級操作。
*/
public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
super(context,name,factory,version);
mContext = context;
}
/**
* 新建資料庫時會執行,在這裡一般處理建立表的邏輯
* @param db
*/
@Override
public void onCreate(SQLiteDatabase db) {
//執行SQL語句,建立兩個表,可以封裝在一個方法中,方便在onUpgrade()中呼叫
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_LONG).show();
}
/**
* 用於對資料庫進行升級,當在例項化該類時傳入的version大於之前的值就會執行該方法
* @param db
* @param oldVersion
* @param newVersion
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 這種採用直接刪除的辦法會導致資料的丟失,其實是不合理的
// 可以通過switch條件判斷:若產品升級的話,現有的資料表儲存
// 而不是刪除表,且case塊中,不需要break語句。
// 這樣能夠保證無論多少次更新,資料庫總是最新的。
// 以上內容參考郭霖第一行。
db.execSQL("drop table if exists book");
db.execSQL("drop table if exists category");
onCreate(db);
}
/**
* 可以建立或開啟一個現有的資料庫(如果資料庫已經存在則開啟,若不存在則新建)
* 返回一個可對資料庫讀寫操作的物件
* 但資料庫不可寫入(如磁碟空間已滿)時,返回的物件只能以只讀的方式開啟資料庫
* @return
*/
@Override
public SQLiteDatabase getReadableDatabase() {
return super.getReadableDatabase();
}
/**
* 可以建立或開啟一個現有的資料庫(如果資料庫已經存在則開啟,若不存在則新建)
* 返回一個可對資料庫讀寫操作的物件
* 但資料庫不可寫入時,將出現異常
* @return
*/
@Override
public SQLiteDatabase getWritableDatabase() {
return super.getWritableDatabase();
}
}
為了讀寫資料,我們可以呼叫getWritableDatabase()
和getReadableDatabase()
獲取SQLiteDabase物件,然後使用該類的方法就可以對資料庫進行對數的操作。我們可以使用query()
方法查詢資料庫,若要執行更為複雜的查詢語句,則可以使用SQLiteQueryBuilder。
每一個SQLite查詢都會返回Cursor物件,使用該物件對查詢結果進行處理。
我們來看一下如何對資料庫進行處理:
/**
* SQLite資料庫練習
* @author sywyg
* @since 2015.5.20
*/
public class MainActivity extends Activity {
private MyDatabaseHelper helper;
private SQLiteDatabase sqLiteDatabase;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 建立資料庫幫助類
helper = new MyDatabaseHelper(this,"BookStore.db",null,1);
}
/**
* 按鈕點選事件
* @param view
*/
public void onButtonClick(View view) {
switch (view.getId()) {
/**
* 獲得資料庫
*/
case R.id.btn_create:
sqLiteDatabase = helper.getReadableDatabase();
break;
/**
* 插入資料(也可以直接執行SQL語句)。
* SqLiteDatabase類中的例項方法insert()方法
* insert()方法接受三個引數分別為:
* 表名,不指定列的預設值null,ContentValues物件。
* ContentValues類實現了Parcelable介面,提供一系列的put()方法。
* 用於新增資料,put()方法引數為:列名,值。
*/
case R.id.btn_add:
sqLiteDatabase = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("author","sywyg");
values.put("price", 25);
values.put("state", 0);
sqLiteDatabase.insert("book", null, values);
//清除values中的值
values.clear();
//插入第二條資料
values.put("author","sywyg2");
values.put("price", 15);
//values.put("state",1);
sqLiteDatabase.insert("book",null,values);
Toast.makeText(this, "add succeeded", Toast.LENGTH_LONG).show();
break;
/**
* 刪除資料(也可以直接執行SQL語句)
* delete()方法引數分別為:表名,第二和第三個為約束條件
*/
case R.id.btn_delete:
sqLiteDatabase = helper.getReadableDatabase();
//問號表示佔位符,由第三個引數中的字串陣列指定相應的內容
sqLiteDatabase.delete("book","author = ?",new String[]{"sywyg2"});
Toast.makeText(this, "delete succeeded", Toast.LENGTH_LONG).show();
break;
/**
* 更新資料(也可以直接執行SQL語句)
* update()方法引數分別為:表名,ContentValues物件,第三和第四個為約束條件
*/
case R.id.btn_update:
sqLiteDatabase = helper.getWritableDatabase();
ContentValues values1 = new ContentValues();
values1.put("author","wygsy");
values1.put("price", 100);
//更新author為sywyg且price為15的資料
sqLiteDatabase.update("book", values1, "author = ? and price = ?", new String[]{"sywyg", "15"});
Toast.makeText(this, "update succeeded", Toast.LENGTH_LONG).show();
break;
/**
* 查詢資料(也可以直接執行SQL語句)
* query()方法引數分別為:
*/
case R.id.btn_select:
sqLiteDatabase = helper.getReadableDatabase();
Cursor cursor = sqLiteDatabase.query("book", null, null, null, null, null, null);
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor.getColumnIndex("id"));
String author = cursor.getString(cursor.getColumnIndex("author"));
Toast.makeText(this, "id:" + id + ",author:" + author, Toast.LENGTH_LONG).show();
}
break;
}
}
}
程式碼中已經解釋的很清楚,不再多說。需要多說的是關於資料庫的增刪改查(CRUD)操作,我已在contentprovider中講的很清楚了,有興趣的可以去看看:解讀Android之ContentProvider(1)CRUD操作和解讀Android之ContentProvider(2)建立自己的Provider。
當然上面的CRUD也直接可以使用SQL語句處理,使用SQLiteDatabase物件的execSQL()
。
若要實現插入資料的唯一性可以使用insertWithOnConflict()
同時需要在建立表時指定個不允許重複的欄位設為主鍵PrimaryKey或者唯一性索引UNIQUE。
Android沒有增加任何超出SQLite語句的限制。我們推薦使用一個自動增加的主鍵,但是這個不是必須的。對於content provider來說,這個主鍵(BaseColumns._ID
)是必須的。
使用事務
這部分內容來自郭霖第一行。
SQLite資料庫是支援事務的,事務的特性是保證某一系列操作要麼都執行,要麼都不執行。那麼如何使用事務呢?
首先呼叫SQLiteDatabase物件的beginTransaction()
開啟事務,然後在一個異常捕獲塊中去執行資料庫操作,當所有的操作完成後呼叫setTransactionSuccessful()
,表示事務完成,最後在finally中呼叫endTransaction()
關閉事務。
以上操作能夠保證一次事務的執行,若執行不到setTransactionSuccessful()
,則所有資料庫操作都將無效。
資料庫除錯
Android SDK中的adb除錯工具中包括sqlite3命令,這些命令可以進行相關資料庫操作。這一部分將在Android Debug Bridge中介紹。