持久化技術
數據持久化就是講那些內存中的瞬時數據保存到儲存設備中,保證即使在手機或電腦關機的情況下,這些數據仍然不會丟失。保存在內存中的數據是屬于瞬時狀態的,兒保存在存儲設備中的數據上處于持久狀態的,持久化技術提供了一種可以讓數據在瞬時狀態和持久狀態之間轉換的機制。
Android系統中主要提供了3種方式用于簡單地實現數據持久化功能,即文件存儲、SharedPreference
存儲以及數據庫存儲。當然,除了這三種之外,還可以將數據保存在手機的SD卡之中,不過使用文件、SharedPreference
存儲以及數據庫存儲會更加簡單一些,而且更加安全。
文件存儲
文件存儲是Android中最基本的一種數據存儲方式,它不對存儲的內容進行任何格式化處理,所有數據都是原封不動地保存到文件當中的,因而它比較適合用于存儲一些簡單的文本數據或者二進制數據。
如果想要使用文件存儲的方式來保存一些較為復雜的文本數據,就需要定義一套自己的格式規范,這樣就可以方便之后將數據從文件中重新解析出來。
將數據存儲到文件中
Context
類中提供了一個openFileOutput
方法,可以用于將數據存儲到指定的文件中。這個方法接收兩個參數,第一個參數的文件名,在文件創建的時候使用的就是這個名稱,注意這里指定的文件名不可以包含路徑,因為所有的文件都是默認存儲到/data/data/<packagename>/files/
目錄下的。第二個參數是文件的操作模式,主要有兩種模式可以選:MODE_PRIVATE
和MODE_APPEND
。其中MODE_PRIVATE
是默認的操作模式,寫入的內容會覆蓋原文件的內容,而MODE_APPEND
則表示如果該文件已經存在,就往文件里面追加內容,不存在就創建新文件。
openFileOutput
方法返回的是一個FileOutputStream
對象,得到了這個對象之后就可以使用Java
流的方式將數據寫入到文件中了。
public void save(){
String data = "Data to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (IOException e){
e.printStackTrace();
} finally {
try {
if (writer != null)
writer.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
這里通過openFileOutput
方法能夠得到一個FileOutputStream
對象,然后再借助它構建出一個OutputStreamWriter
對象,接著再構建一個BufferedWriter
對象,這樣就可以通過BufferedWriter
來將文本內容寫入到文件中了。
下面創建一個完整的例子:
//FilePersistenceTest.MainActivity
public class MainActivity extends AppCompatActivity {
private EditText edit;
private Button confirm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
confirm = (Button) findViewById(R.id.Upload);
confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String inputText = edit.getText().toString();
save(inputText);
}
});
}
public void save(String inputText){
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
} catch (IOException e){
e.printStackTrace();
} finally {
try{
if (writer != null)
writer.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}
從文件中讀取數據
類似于將數據存儲到文件中,Context
類中還提供了一個openFileInput
方法,用于從文件中讀取數據。這個方法要比openFileOutput
簡單一些,只接受一個參數,即要讀取的文件名,然后系統會自動到/data/data/<packagename>/files
目錄下去加載這個文件,并返回一個FileInputStream
對象,得到了這個對象之后再通過Java
流的方式就可以將數據讀取出來了。
public String load(){
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try{
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while((line = reader.readLine()) != null)
content.append(line);
} catch (IOException e){
e.printStackTrace();
} finally {
if (reader != null){
try {
reader.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
return content.toString();
}
在這段代碼中,首先通過openFileInput
方法獲取到了一個FileInputStream
對象,然后借助它有構建出了一個InputStreamReader
對象,再構造一個BufferedReader
。這樣我們就可以通過BufferedReader
進行一行一行地讀取,把文件中所有的文本內容全部讀取出來,并且存放在一個StringBuilder
對象中,最后將讀取到的內容返回即可。
然后我們來完善一下上面的例子。
在onCreate
方法中加入下面的代碼:
String inputText = load();
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_LONG).show();
}
注意,上述代碼在對字符串進行非空判斷的時候用到了TextUtils.isEmpty
方法,這是個非常好用的方法,可以一次進行兩種空值的判斷。當傳入的字符串為null
或者等于空字符串的時候,這個方法都會返回true
,從而使得我們不需要先單獨判斷這兩種空值在使用邏輯運算符連接起來了。
SharedPreferences存儲
不同于文件的儲存方式,SharedPreferences
是使用鍵值對的方式來存儲數據的。也就是說,當保存一條數據的時候,需要給這條數據提供一個隊員的鍵,這樣在讀取數據的時候可以通過這個鍵把對應的值取出來。而且SharedPreferences
還支持多種不同的數據類型存儲,如果存儲的數據類型是整型,那么讀取出來的數據也是整型。
將數據存儲到SharedPreferences中
要想使用SharedPreferences
,需要先獲取SharedPreferences
對象。
Android中主要提供了三種方法用于得到SharedPreferences
對象。
-
Context
類中的getSharedPreferences
方法
此方法接受兩個參數,第一個參數用于指定SharedPreferences
文件的名稱,如果文件不存在則創建一個。文件都存放在/data/data/<packagename>/shared_prefs/
目錄下。第二個參數用于指定操作模式,目前只有MODE_PRIVATE
一種模式可選。它是默認的操作模式,和直接傳入0效果是相同的,表示只有當前的應用程序才可以對這個SharedPreferences
文件進行讀寫。 -
Acitvity
類中的getPreferences
方法
這個方法和上面的很相似,不過只接受一個操作模式參數,它會自動使用當前活動的類名來作為SharedPreferences
的文件名。 -
PreferenceManager
類中的getDefaultSharedPreferences
方法
這是一個靜態方法,它接收一個Context
參數,并自動使用當前應用程序的報名作為前綴來命名SharedPreferences
文件。
得到了SharedPreferences
對象之后,就可以開始向SharedPreferences
文件中存儲數據了。
- 調用
SharedPreferences
對象的edit
方法來獲取一個SharedPreferences.Editor
對象。 - 向
SharedPreferences.Editor
對象中添加數據,比如添加一個布爾型數據就使用putBoolean
方法,添加一個字符串則使用putString
方法。 - 調用
apply
方法將添加的數據提交,從而完成數據存儲操作。
實踐
這里用一個按鈕來作為演示。
confirm = (Button) findViewById(R.id.Upload);
confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name ", "Tom");
editor.putInt("age", 28);
editor.putBoolean("married", false);
editor.apply();
}
});
點擊事件通過getSharedPreferences
方法指定SharedPreferences
的文件名為data
,并得到了SharedPreferences.Editor
對象。接著向這個對象添加了三條不同類型的數據,最后apply
提交。
運行之后,我們能發現,生成了一個data.xml
文件。
從SharedPreferences中讀取數據
從SharedPreferences
中存儲數據非常簡單,讀取數據也很簡單。SharedPreferences
對象提供了一系列的get
方法,用于對存儲的數據進行讀取,每種get
方法都對應了SharedPreferences.Editor
中的一種put
方法。
get
方法接收兩個參數,第一個參數是鍵,傳入存儲數據時使用的鍵就可以得到相應的值了;第二個參數是默認值,即表示當傳入的鍵找不到對于的值時會以什么樣的默認值進行返回。
我們還是來實踐一下。
在onCreate
方法中添加下面的代碼,效果和之前相同。
SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
String inputText = pref.getString("input", "");
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_LONG).show();
}
SQLite數據庫存儲
創建數據庫
Android為了讓我們能夠更加方便地管理數據庫,專門提供了一個SQLiteOpenHelper
幫助類,借助這個類可以很簡單地對數據庫進行創建和升級。
首先,SQLiteOpenHelper
是一個抽象類,這意味著我們想要使用它的話就需要創建一個自己的幫助類來繼承它。SQLiteOpenHelper
中有兩個抽象方法,分別是onCreate
和onUpgrade
,我們必須在自己的幫助類里重寫這兩個方法,然后分別在這兩個方法中去實現創建、升級數據庫的邏輯。
SQLiteOpenHelp
中還有兩個非常重要的實例方法:getReadableDatabase
和getWritableDatabase
。這兩個方法都可以創建或者打開一個現有的數據庫,并返回一個可對數據庫進行讀寫的對象。不同的是,當數據庫不可寫入的時候,getReadableDatabase
方法返回的對象會用只讀的方式打開數據庫,而getWritableDatabase
會出現異常。
SQLiteOpenHelper
中有兩個構造方法可供重寫,一般使用參數少一點的那個構造方法即可。這個構造方法中接收4個參數,第一個是Context
,第二個是數據庫名,第三個參數允許我們在查詢數據的時候返回一個自定義的Cursor
,一般都是傳入null
,第四個參數表示當前數據庫的版本號,可以用于升級數據庫。
構建數據庫實例后,就可以調用getReadableDatabase
和getWritableDatabase
創建數據庫了。數據庫文件會存放在/data/data/<packagename>/databases/
目錄下。此時,重寫的onCreate
方法也會得到執行。所以通常會在這里處理一些創建表的邏輯。
接下來還是通過例子來直觀地體會一下。
我們創建一個名為BookStore.db
的數據庫,然后在這個數據庫中新建一張Book
表,表中有id(主鍵)
、作者、價格、頁數、書名等。
創建數據庫表當然還是需要建表語句的。
create table Book(
id interger primary key autoincrement,
author text,
price real,
pages integer,
name text)
SQLite的數據類型很簡單,integer
表示整型,real
表示浮點型,text
表示文本類型,blob
表示二進制類型。另外,上述建表語句中我們還使用了primary key
將id
設置為主鍵,并且用autocrement
關鍵字表示id
列是自增長的。
然后需要在代碼中去執行這條SQL語句,才能完成創建表的操作。
新建一個MyDatabaseHelper
類。
public class MyDatebaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "name text)";
private Context mContext;
public MyDatebaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
super(context, name, factory, version);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL(CREATE_BOOK);
Toast.makeText(mContext, "Create succeed", Toast.LENGTH_LONG).show();
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
}
}
我們把建表語句定義成一個字符串常量,然后在onCreate
方法中有調用了SQLiteDatabase
的execSQL
方法去執行這條建表語句,并彈出一個Toast
提示創建成功,這樣就可以保證在數據庫創建完成的時候還能同時創建Book
表。
再修改一下activity_main.xml
。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_weight="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/create_database"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
添加了一個用于建庫的按鈕。
最后修改一下主活動。
dbHelper = new MyDatebaseHelper(this, "BookStore.db", null, 1);
Button button_createdb = (Button) findViewById(R.id.create_Database);
button_createdb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dbHelper.getWritableDatabase();
}
});
至此,建庫的代碼就完成了。
建庫之后,如果才能證實已經創建成功?如果還是使用File Explorer
,最多只能看到database
目錄下多了一個文件,Book
表是無法通過File Explorer
看到的。因此這次我們準備換一種查看方式,用adb shell
來對數據庫和表的創建情況進行檢查。
關于
adb shell
的內容下次補全。
升級數據庫
之前我們重寫了一個空方法onUpgrade
方法是用于對數據庫進行升級的,它在整個數據庫的管理工作中起著非常重要的作用。
目前DatabaseTest
項目中已經有了一張Book
表用于存放書的各種詳細數據,如果我們想要再添加一張Catagory
表用于記錄圖書的分類,怎么做呢?
Category
表中有id(主鍵)
、分類名和分類代碼這幾列,那么建表語句就可以寫成:
create table Category(
id integer primary key autoincrement,
category_name text,
category_code integer)
接下來我們將這條建表語句添加到MyDatabaseHelper
中,然后修改一下onUpgrade
方法。
public static final String CREATE_CATEGORY = "create table Category ("
+ "id integer primary key autoincrement, "
+ "category_name text, "
+ "category_code integer)";
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
db.execSQL("drop table if exists Book");
db.execSQL("drop teble if exists Category");
onCreate(db);
}
當我們調用onUpgrade
方法時,如果Book
和Category
已經存在了,就會將原先存在的表刪除,然后在onCreate
方法中創建一個新的表。
然后修改一下主活動中創建表格的語句,就可以調用onUpgrade
方法了。
dbHelper = new MyDatebaseHelper(
this, "BookStore.db", null, 2
);
添加數據
接下來學習一下如何對表中的數據進行操作。
其實我們能對數據進行的操作無非四種,CRUD
。即添加、查詢、更新、刪除。每種操作又對于了一種SQL命令,添加insert
,查詢select
,更新update
,刪除delete
。Android為了照顧開發者的水平,提供了一系列的輔助性方法,使得在Android中即使不編寫SQL語句,也能輕松完成所有CRUD操作。
前面我們知道,getReadableDatabase
和getWriteableDatabase
方法是可以用于創建和升級數據庫的,不僅如此,兩個方法還都會返回一個SQLiteDatabase
對象,借助這個對象就可以對數據進行操作了。
首先是添加數據。
SQLiteDatabase
提供了一個insert
方法。它接收三個參數,第一個是表名,第二個用于在未指定添加數據的情況下給某些可為空的列自動賦值為null,第三個是一個ContentValues
對象,它提供了一系列的put
方法重載,用于向ContentValues
中添加數據,只需要將表中的每個列名已經相應的數據傳入即可。
我們來實踐一下。
首先我在主活動的布局中添加了一個按鈕,然后在主活動中給按鈕添加了一個監聽器。
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values);
values.clear();
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values)
}
});
我們賦值的時候對id這列沒有賦值,因為在創建表的時候,已經將主鍵設置為自增長了。
更新數據
SQLiteDatabase
中同樣提供了一個很好的updata
方法,用于對數據進行更新,這個方法接受4個參數,第一個參數同樣是表名,第二個參數是ContentValues
對象,第三個、第四個參數用于約束更新某一行或者某幾行的數據,不指定的話就是默認更新所有行。
Button updataData = (Button) findViewById(R.id.update_data);
updataData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
db.update("Book", values, "name = ?", new String[]{ "The Da Vinci Code" });
}
});
刪除數據
依然是用SQLiteDatabase
中的delete
方法。方法接受三個參數,第一個是表名,第二、三個用于約束刪除某一行的數據。
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] {"500"});
}
});
查詢數據
查詢數據是CRUD中最復雜的一種操作。
SQLiteDatabase
中有一個query
方法用于對數據進行查詢,這個方法的參數十分復雜,最短的一個方法重載也需要傳入七個參數。
第一個參數自然還是表名,第二個參數用于指定查詢哪幾列,三四個參數用于約束查詢某一行或者某幾行的數據,第五個參數用于指定需要去group by
的列,第六個參數用于對group by
之后的數據進一步過濾,第七個參數用于指定查詢結果的排序方式。
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if(cursor.moveToFirst()){
do {
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("DatabaseTest", "book name is "+name);
Log.d("Main.Activity", "book author is"+author);
Log.d("Main Activity", "book pages is "+pages);
Log.d("Main Activity", "book price is"+price);
}while (cursor.moveToNext());
}
cursor.close();
}
});
使用SQL操作數據庫
Android提供了很多方便的API來操作數據庫,不過也提供了直接使用SQL來操作數據庫的方法。只要使用execSQL
方法,其中傳入你需要的SQL語句即可。