持久化技術(shù)
數(shù)據(jù)持久化就是講那些內(nèi)存中的瞬時數(shù)據(jù)保存到儲存設(shè)備中,保證即使在手機(jī)或電腦關(guān)機(jī)的情況下,這些數(shù)據(jù)仍然不會丟失。保存在內(nèi)存中的數(shù)據(jù)是屬于瞬時狀態(tài)的,兒保存在存儲設(shè)備中的數(shù)據(jù)上處于持久狀態(tài)的,持久化技術(shù)提供了一種可以讓數(shù)據(jù)在瞬時狀態(tài)和持久狀態(tài)之間轉(zhuǎn)換的機(jī)制。
Android系統(tǒng)中主要提供了3種方式用于簡單地實現(xiàn)數(shù)據(jù)持久化功能,即文件存儲、SharedPreference
存儲以及數(shù)據(jù)庫存儲。當(dāng)然,除了這三種之外,還可以將數(shù)據(jù)保存在手機(jī)的SD卡之中,不過使用文件、SharedPreference
存儲以及數(shù)據(jù)庫存儲會更加簡單一些,而且更加安全。
文件存儲
文件存儲是Android中最基本的一種數(shù)據(jù)存儲方式,它不對存儲的內(nèi)容進(jìn)行任何格式化處理,所有數(shù)據(jù)都是原封不動地保存到文件當(dāng)中的,因而它比較適合用于存儲一些簡單的文本數(shù)據(jù)或者二進(jìn)制數(shù)據(jù)。
如果想要使用文件存儲的方式來保存一些較為復(fù)雜的文本數(shù)據(jù),就需要定義一套自己的格式規(guī)范,這樣就可以方便之后將數(shù)據(jù)從文件中重新解析出來。
將數(shù)據(jù)存儲到文件中
Context
類中提供了一個openFileOutput
方法,可以用于將數(shù)據(jù)存儲到指定的文件中。這個方法接收兩個參數(shù),第一個參數(shù)的文件名,在文件創(chuàng)建的時候使用的就是這個名稱,注意這里指定的文件名不可以包含路徑,因為所有的文件都是默認(rèn)存儲到/data/data/<packagename>/files/
目錄下的。第二個參數(shù)是文件的操作模式,主要有兩種模式可以選:MODE_PRIVATE
和MODE_APPEND
。其中MODE_PRIVATE
是默認(rèn)的操作模式,寫入的內(nèi)容會覆蓋原文件的內(nèi)容,而MODE_APPEND
則表示如果該文件已經(jīng)存在,就往文件里面追加內(nèi)容,不存在就創(chuàng)建新文件。
openFileOutput
方法返回的是一個FileOutputStream
對象,得到了這個對象之后就可以使用Java
流的方式將數(shù)據(jù)寫入到文件中了。
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
對象,然后再借助它構(gòu)建出一個OutputStreamWriter
對象,接著再構(gòu)建一個BufferedWriter
對象,這樣就可以通過BufferedWriter
來將文本內(nèi)容寫入到文件中了。
下面創(chuàng)建一個完整的例子:
//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();
}
}
}
}
從文件中讀取數(shù)據(jù)
類似于將數(shù)據(jù)存儲到文件中,Context
類中還提供了一個openFileInput
方法,用于從文件中讀取數(shù)據(jù)。這個方法要比openFileOutput
簡單一些,只接受一個參數(shù),即要讀取的文件名,然后系統(tǒng)會自動到/data/data/<packagename>/files
目錄下去加載這個文件,并返回一個FileInputStream
對象,得到了這個對象之后再通過Java
流的方式就可以將數(shù)據(jù)讀取出來了。
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
對象,然后借助它有構(gòu)建出了一個InputStreamReader
對象,再構(gòu)造一個BufferedReader
。這樣我們就可以通過BufferedReader
進(jìn)行一行一行地讀取,把文件中所有的文本內(nèi)容全部讀取出來,并且存放在一個StringBuilder
對象中,最后將讀取到的內(nèi)容返回即可。
然后我們來完善一下上面的例子。
在onCreate
方法中加入下面的代碼:
String inputText = load();
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_LONG).show();
}
注意,上述代碼在對字符串進(jìn)行非空判斷的時候用到了TextUtils.isEmpty
方法,這是個非常好用的方法,可以一次進(jìn)行兩種空值的判斷。當(dāng)傳入的字符串為null
或者等于空字符串的時候,這個方法都會返回true
,從而使得我們不需要先單獨判斷這兩種空值在使用邏輯運算符連接起來了。
SharedPreferences存儲
不同于文件的儲存方式,SharedPreferences
是使用鍵值對的方式來存儲數(shù)據(jù)的。也就是說,當(dāng)保存一條數(shù)據(jù)的時候,需要給這條數(shù)據(jù)提供一個隊員的鍵,這樣在讀取數(shù)據(jù)的時候可以通過這個鍵把對應(yīng)的值取出來。而且SharedPreferences
還支持多種不同的數(shù)據(jù)類型存儲,如果存儲的數(shù)據(jù)類型是整型,那么讀取出來的數(shù)據(jù)也是整型。
將數(shù)據(jù)存儲到SharedPreferences中
要想使用SharedPreferences
,需要先獲取SharedPreferences
對象。
Android中主要提供了三種方法用于得到SharedPreferences
對象。
-
Context
類中的getSharedPreferences
方法
此方法接受兩個參數(shù),第一個參數(shù)用于指定SharedPreferences
文件的名稱,如果文件不存在則創(chuàng)建一個。文件都存放在/data/data/<packagename>/shared_prefs/
目錄下。第二個參數(shù)用于指定操作模式,目前只有MODE_PRIVATE
一種模式可選。它是默認(rèn)的操作模式,和直接傳入0效果是相同的,表示只有當(dāng)前的應(yīng)用程序才可以對這個SharedPreferences
文件進(jìn)行讀寫。 -
Acitvity
類中的getPreferences
方法
這個方法和上面的很相似,不過只接受一個操作模式參數(shù),它會自動使用當(dāng)前活動的類名來作為SharedPreferences
的文件名。 -
PreferenceManager
類中的getDefaultSharedPreferences
方法
這是一個靜態(tài)方法,它接收一個Context
參數(shù),并自動使用當(dāng)前應(yīng)用程序的報名作為前綴來命名SharedPreferences
文件。
得到了SharedPreferences
對象之后,就可以開始向SharedPreferences
文件中存儲數(shù)據(jù)了。
- 調(diào)用
SharedPreferences
對象的edit
方法來獲取一個SharedPreferences.Editor
對象。 - 向
SharedPreferences.Editor
對象中添加數(shù)據(jù),比如添加一個布爾型數(shù)據(jù)就使用putBoolean
方法,添加一個字符串則使用putString
方法。 - 調(diào)用
apply
方法將添加的數(shù)據(jù)提交,從而完成數(shù)據(jù)存儲操作。
實踐
這里用一個按鈕來作為演示。
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
對象。接著向這個對象添加了三條不同類型的數(shù)據(jù),最后apply
提交。
運行之后,我們能發(fā)現(xiàn),生成了一個data.xml
文件。
從SharedPreferences中讀取數(shù)據(jù)
從SharedPreferences
中存儲數(shù)據(jù)非常簡單,讀取數(shù)據(jù)也很簡單。SharedPreferences
對象提供了一系列的get
方法,用于對存儲的數(shù)據(jù)進(jìn)行讀取,每種get
方法都對應(yīng)了SharedPreferences.Editor
中的一種put
方法。
get
方法接收兩個參數(shù),第一個參數(shù)是鍵,傳入存儲數(shù)據(jù)時使用的鍵就可以得到相應(yīng)的值了;第二個參數(shù)是默認(rèn)值,即表示當(dāng)傳入的鍵找不到對于的值時會以什么樣的默認(rèn)值進(jìn)行返回。
我們還是來實踐一下。
在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數(shù)據(jù)庫存儲
創(chuàng)建數(shù)據(jù)庫
Android為了讓我們能夠更加方便地管理數(shù)據(jù)庫,專門提供了一個SQLiteOpenHelper
幫助類,借助這個類可以很簡單地對數(shù)據(jù)庫進(jìn)行創(chuàng)建和升級。
首先,SQLiteOpenHelper
是一個抽象類,這意味著我們想要使用它的話就需要創(chuàng)建一個自己的幫助類來繼承它。SQLiteOpenHelper
中有兩個抽象方法,分別是onCreate
和onUpgrade
,我們必須在自己的幫助類里重寫這兩個方法,然后分別在這兩個方法中去實現(xiàn)創(chuàng)建、升級數(shù)據(jù)庫的邏輯。
SQLiteOpenHelp
中還有兩個非常重要的實例方法:getReadableDatabase
和getWritableDatabase
。這兩個方法都可以創(chuàng)建或者打開一個現(xiàn)有的數(shù)據(jù)庫,并返回一個可對數(shù)據(jù)庫進(jìn)行讀寫的對象。不同的是,當(dāng)數(shù)據(jù)庫不可寫入的時候,getReadableDatabase
方法返回的對象會用只讀的方式打開數(shù)據(jù)庫,而getWritableDatabase
會出現(xiàn)異常。
SQLiteOpenHelper
中有兩個構(gòu)造方法可供重寫,一般使用參數(shù)少一點的那個構(gòu)造方法即可。這個構(gòu)造方法中接收4個參數(shù),第一個是Context
,第二個是數(shù)據(jù)庫名,第三個參數(shù)允許我們在查詢數(shù)據(jù)的時候返回一個自定義的Cursor
,一般都是傳入null
,第四個參數(shù)表示當(dāng)前數(shù)據(jù)庫的版本號,可以用于升級數(shù)據(jù)庫。
構(gòu)建數(shù)據(jù)庫實例后,就可以調(diào)用getReadableDatabase
和getWritableDatabase
創(chuàng)建數(shù)據(jù)庫了。數(shù)據(jù)庫文件會存放在/data/data/<packagename>/databases/
目錄下。此時,重寫的onCreate
方法也會得到執(zhí)行。所以通常會在這里處理一些創(chuàng)建表的邏輯。
接下來還是通過例子來直觀地體會一下。
我們創(chuàng)建一個名為BookStore.db
的數(shù)據(jù)庫,然后在這個數(shù)據(jù)庫中新建一張Book
表,表中有id(主鍵)
、作者、價格、頁數(shù)、書名等。
創(chuàng)建數(shù)據(jù)庫表當(dāng)然還是需要建表語句的。
create table Book(
id interger primary key autoincrement,
author text,
price real,
pages integer,
name text)
SQLite的數(shù)據(jù)類型很簡單,integer
表示整型,real
表示浮點型,text
表示文本類型,blob
表示二進(jìn)制類型。另外,上述建表語句中我們還使用了primary key
將id
設(shè)置為主鍵,并且用autocrement
關(guān)鍵字表示id
列是自增長的。
然后需要在代碼中去執(zhí)行這條SQL語句,才能完成創(chuàng)建表的操作。
新建一個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
方法中有調(diào)用了SQLiteDatabase
的execSQL
方法去執(zhí)行這條建表語句,并彈出一個Toast
提示創(chuàng)建成功,這樣就可以保證在數(shù)據(jù)庫創(chuàng)建完成的時候還能同時創(chuàng)建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();
}
});
至此,建庫的代碼就完成了。
建庫之后,如果才能證實已經(jīng)創(chuàng)建成功?如果還是使用File Explorer
,最多只能看到database
目錄下多了一個文件,Book
表是無法通過File Explorer
看到的。因此這次我們準(zhǔn)備換一種查看方式,用adb shell
來對數(shù)據(jù)庫和表的創(chuàng)建情況進(jìn)行檢查。
關(guān)于
adb shell
的內(nèi)容下次補全。
升級數(shù)據(jù)庫
之前我們重寫了一個空方法onUpgrade
方法是用于對數(shù)據(jù)庫進(jìn)行升級的,它在整個數(shù)據(jù)庫的管理工作中起著非常重要的作用。
目前DatabaseTest
項目中已經(jīng)有了一張Book
表用于存放書的各種詳細(xì)數(shù)據(jù),如果我們想要再添加一張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);
}
當(dāng)我們調(diào)用onUpgrade
方法時,如果Book
和Category
已經(jīng)存在了,就會將原先存在的表刪除,然后在onCreate
方法中創(chuàng)建一個新的表。
然后修改一下主活動中創(chuàng)建表格的語句,就可以調(diào)用onUpgrade
方法了。
dbHelper = new MyDatebaseHelper(
this, "BookStore.db", null, 2
);
添加數(shù)據(jù)
接下來學(xué)習(xí)一下如何對表中的數(shù)據(jù)進(jìn)行操作。
其實我們能對數(shù)據(jù)進(jìn)行的操作無非四種,CRUD
。即添加、查詢、更新、刪除。每種操作又對于了一種SQL命令,添加insert
,查詢select
,更新update
,刪除delete
。Android為了照顧開發(fā)者的水平,提供了一系列的輔助性方法,使得在Android中即使不編寫SQL語句,也能輕松完成所有CRUD操作。
前面我們知道,getReadableDatabase
和getWriteableDatabase
方法是可以用于創(chuàng)建和升級數(shù)據(jù)庫的,不僅如此,兩個方法還都會返回一個SQLiteDatabase
對象,借助這個對象就可以對數(shù)據(jù)進(jìn)行操作了。
首先是添加數(shù)據(jù)。
SQLiteDatabase
提供了一個insert
方法。它接收三個參數(shù),第一個是表名,第二個用于在未指定添加數(shù)據(jù)的情況下給某些可為空的列自動賦值為null,第三個是一個ContentValues
對象,它提供了一系列的put
方法重載,用于向ContentValues
中添加數(shù)據(jù),只需要將表中的每個列名已經(jīng)相應(yīng)的數(shù)據(jù)傳入即可。
我們來實踐一下。
首先我在主活動的布局中添加了一個按鈕,然后在主活動中給按鈕添加了一個監(jiān)聽器。
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)
}
});
我們賦值的時候?qū)d這列沒有賦值,因為在創(chuàng)建表的時候,已經(jīng)將主鍵設(shè)置為自增長了。
更新數(shù)據(jù)
SQLiteDatabase
中同樣提供了一個很好的updata
方法,用于對數(shù)據(jù)進(jìn)行更新,這個方法接受4個參數(shù),第一個參數(shù)同樣是表名,第二個參數(shù)是ContentValues
對象,第三個、第四個參數(shù)用于約束更新某一行或者某幾行的數(shù)據(jù),不指定的話就是默認(rèn)更新所有行。
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" });
}
});
刪除數(shù)據(jù)
依然是用SQLiteDatabase
中的delete
方法。方法接受三個參數(shù),第一個是表名,第二、三個用于約束刪除某一行的數(shù)據(jù)。
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"});
}
});
查詢數(shù)據(jù)
查詢數(shù)據(jù)是CRUD中最復(fù)雜的一種操作。
SQLiteDatabase
中有一個query
方法用于對數(shù)據(jù)進(jìn)行查詢,這個方法的參數(shù)十分復(fù)雜,最短的一個方法重載也需要傳入七個參數(shù)。
第一個參數(shù)自然還是表名,第二個參數(shù)用于指定查詢哪幾列,三四個參數(shù)用于約束查詢某一行或者某幾行的數(shù)據(jù),第五個參數(shù)用于指定需要去group by
的列,第六個參數(shù)用于對group by
之后的數(shù)據(jù)進(jìn)一步過濾,第七個參數(shù)用于指定查詢結(jié)果的排序方式。
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操作數(shù)據(jù)庫
Android提供了很多方便的API來操作數(shù)據(jù)庫,不過也提供了直接使用SQL來操作數(shù)據(jù)庫的方法。只要使用execSQL
方法,其中傳入你需要的SQL語句即可。