文章目錄
- 數據持久化
- 文件存儲
- 將數據存儲進文件
- 實例
- 從文件中讀取數據
- 實例
- SharedPreferences存儲
- 將數據存儲進文件
- 實例
- 從文件中讀取數據
- 實例
- 實現記住密碼的功能
- SQLite數據庫存儲
- 創建自己的幫助類
- 調用自己的幫助類
- 補全 onUpgrade() 方法
- 增刪查改
- 增:SQLiteDatabase.insert()
- 改:SQLiteDatabase.update()
- 刪:SQLiteDatabase.delete()
- 查:SQLiteDatabase.query()
- 通過 SQL語句 實現增刪查改
數據持久化
保存在內存中的數據是屬于瞬時狀態的,而保存在存儲設備中的數據上處于持久狀態的,持久化技術提供了一種可以讓數據在瞬時狀態和持久狀態之間轉換的機制。
Android系統中主要提供了3種常用方式用于簡單地實現數據持久化功能,即文件存儲、SharedPreference存儲以及數據庫存儲。
文件存儲
將數據存儲進文件
Context類 中提供了一個 openFileOutput
方法,用于將數據存儲到指定的文件中。這個方法接收兩個參數:
- 第一個參數是文件名:在文件創建的時候使用的就是這個名稱,文件名不可以包含路徑,因為所有的文件都是默認存儲到
/data/data/<packagename>/files/
目錄下的。 - 第二個參數是文件的操作模式:主要有兩種模式可以選,
MODE_PRIVATE
默認的操作模式,寫入的內容會覆蓋原文件的內容;MODE_APPEND
則表示如果該文件已經存在,就往文件里面追加內容,不存在就創建新文件。
該方法返回一個 FileOutputStream
對象,得到了這個對象之后就可以使用 Java流 的方式將數據寫入到文件中了。
實例
在布局文件中添加輸入框 EditText
控件:
在活動文件中,定義不同生命周期的不同行為:
onCreate
方法:獲取 EditText 實例;onDestroy
方法:獲取 EditText 中的內容,并通過自定義方法save
保存到名為data
的文件中。
public class SecondActivity extends AppCompatActivity {private EditText editText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.second_layout);editText = findViewById(R.id.edit);}@Overrideprotected void onDestroy() {super.onDestroy();String inputText = editText.getText().toString();save(inputText);}public void save(String inputText){FileOutputStream out = null; // 文件字節輸出流,繼承OutputStream類BufferedWriter writer = null; // 將文本寫入字符輸出流try {// 獲得一個 字節輸出流對象,規定數據存儲到名為data的文件中,文件的操作模式為MODE_PRIVATEout = openFileOutput("data", Context.MODE_PRIVATE);// 借助out構建OutputStreamWriter臨時對象,作為從字符流到字節流的橋接// 再通過臨時對象構建 字符輸出流對象 以便將文本內容寫入到字節流中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();}}}
}
PS:對于上述將文本存入文件的流程,一開始我理解錯了,順著代碼順尋看以為是字節流轉成字符流再寫入文件,把 inputText
當保存文本的文件了。。。
其實正確邏輯是:
運行結果:
在文本框內輸入內容:
退出程序后,在AS中通過如下操作打開文件頁面:
在下圖路徑中找到 data
文件,查看其內容:
從文件中讀取數據
Context 類中還提供了一個 openFileInput
方法,用于從文件中讀取數據。這個方法只接受一個參數:
- 要讀取的文件名:然后系統會自動到
/data/data/<packagename>/files
目錄下去加載這個文件。
該方法返回一個 FileInputStream
對象,得到了這個對象之后再通過 Java流 的方式就可以將數據讀取出來了。
實例
若 EditText 為空則將文件中的文本讀入到 EditText 中:
public class SecondActivity extends AppCompatActivity {private EditText editText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.second_layout);editText = findViewById(R.id.edit);String inputText = load();if(!TextUtils.isEmpty(inputText)){editText.setText(inputText);editText.setSelection(inputText.length());Toast.makeText(this, "loading successed", Toast.LENGTH_LONG).show();}}public String load(){FileInputStream in = null;BufferedReader reader = null;StringBuilder content = new StringBuilder();try {in = openFileInput("data"); // 文本字節輸入流// InputStreamReader作為字符流到字節流的橋接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();}
}
PS:在判空時使用了 TextUtils.isEmpty()
而非 String.isEmpty()
,這是因為:
- String 類下的
isEmpty()
返回的只是 字符串的長度是否為0,如果 字符串為null 就會直接報 空指針。源碼如下:
public boolean isEmpty() { return count == 0; }
- TextUtils.isEmpty() 會對 null 和 長度 進行判斷,所以 不會報空指針。源碼如下:
public static boolean isEmpty(CharSequence str) { if (str == null || str.length() == 0) return true; else return false;
}
此時,一打開界面即可顯示:
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對象 的
edit方法
來獲取一個SharedPreferences.Editor對象
。 - 向 SharedPreferences.Editor對象 中添加數據,比如添加一個布爾型數據就使用
putBoolean
方法,添加一個字符串則使用putString
方法。 - 調用
apply
方法將添加的數據提交,從而完成數據存儲操作。
實例
實現點擊按鈕保存數據的功能:
Button button_share = findViewById(R.id.button_share);button_share.setOnClickListener((View v)->{SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();editor.putString("name", "cmy");editor.putInt("weight", 120);editor.putBoolean("married", false);editor.apply();Toast.makeText(this, "share over", Toast.LENGTH_LONG).show();});
點擊紅框所示按鈕:
即可將數據存在如下圖所示的路徑中:
從文件中讀取數據
第一步仍是獲取對象,上文已經講過,這里不再贅述。
第二步,通過對應的 get**()方法
獲取對應類型數據,如字符串使用 getString()
方法,這些 get
方法都接受兩個參數:
- 第一個參數是鍵:也就是
KV模型
中的Key
; - 第二個參數是默認值:當傳入的鍵找不到對應值時,以默認值返回。
實例
點擊 get sharePreferences
按鈕從 SharedPreferences文件
中讀取數據:
再通過 Toast
顯示出來:
Button button_getShare = findViewById(R.id.button_getShare);button_getShare.setOnClickListener((View v)->{SharedPreferences preferences = getSharedPreferences("data", MODE_PRIVATE);String name = preferences.getString("name", "");int weight = preferences.getInt("weight", 0);boolean married = preferences.getBoolean("married", false);String res = name+" "+String.valueOf(weight)+" "+String.valueOf(married);Toast.makeText(this, res, Toast.LENGTH_LONG).show();});
實現記住密碼的功能
之前在本博客里實現過登陸界面,這里為登陸界面新加入一個記住密碼的功能。
修改布局文件,添加以下代碼,實現右側所示布局:
這里使用到了一個新控件 復選框:CheckBox ,用戶可以通過點擊來進行選中/取消,以表是否需要記住密碼。
修改 LoginActivity.java
代碼,結合 SharedPreferences
實現記住密碼的功能:
增添的內容主要是:
- 三個相關對象 CheckBox、SharedPreferences、SharedPreferences.Editor 的創建和實例化;
- 初始化布爾型對象
isRemember
作為 判斷記住密碼功能是否生效 的輔助變量;- 一開當然不存在
remember_password
這個鍵對應的值,isRemember
為默認值false
; - 成功登陸一次后,
remember_password
這個鍵對應的值就是true
了。
- 一開當然不存在
以及登陸成功后的操作:
- 調用 CheckBox 的
isChecked()
方法檢查復選框是否被選中,被選中則返回true
; - 為
true
時表示用戶希望記住密碼,此時:- 將
remember_password
對應的值設為true
; - 把
account
和password
對應的值都存入到SharedPreferences
文件中并提交。
- 將
- 為
false
表示用戶并不想記住密碼,此時要調用clean()
方法清楚掉SharedPreferences
文件中的所有數據。
運行結果:
- 輸入正確的賬戶和密碼,并選中記住密碼,點擊登錄:
- 通過強制下線跳轉回登陸界面,此時發現賬號密碼已經自動填充了。
- 如果此時取消選中復選框,再點擊登錄:
- 再次返回登陸界面就不會被填充了:
PS:這里只做示例,實際項目中不能將密碼以明文形式存儲到 SharedPreferences 文件中,因為會被輕易盜取,必須結合加密算法對密碼進行加密。
SQLite數據庫存儲
為了管理數據庫,安卓專門提供了一個 SQLiteOpenHelper 幫助類,這是個抽象類,如果要使用它,需要創建一個 自己的幫助類 去繼承它。下面列舉幾個該類中常用的方法:
- 兩個抽象方法:
onCreate
和onUpgrade
,我們必須在 自己的幫助類 里重寫這兩個方法,然后分別在這兩個方法中去實現創建、升級數據庫的邏輯。 - 兩個實例方法:
getReadableDatabase
和getWritableDatabase
。這兩個方法都可以創建或者打開一個現有的數據庫,數據庫文件存放在/data/data/<packagename>/databases/
目錄下,并返回一個可對數據庫進行讀寫的對象。不同的是,當數據庫不可寫入的時候,getReadableDatabase
方法返回的對象會用只讀的方式打開數據庫,而getWritableDatabase
會出現異常。 - 兩個構造函數:常用的一個構造方法接收4個參數:第一個是Context;第二個是數據庫名;第三個參數允許我們在查詢數據的時候返回一個自定義的Cursor,一般都是傳入null;第四個參數表示當前數據庫的版本號,可以用于升級數據庫。
創建自己的幫助類
public class MyDatabaseHelper extends SQLiteOpenHelper {public static final String CREATE_STUDENT = "create table Student ("+ "id integer primary key autoincrement,"+ "gender text,"+ "weight real,"+ "age integer,"+ "name text)";private Context context;public MyDatabaseHelper( Context context, String name,SQLiteDatabase.CursorFactory factory, int version) {super(context, name, factory, version);this.context = context;}@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL(CREATE_STUDENT);Toast.makeText(context, "create succeeded", Toast.LENGTH_LONG).show();}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}
- 把建表語句定義成一個字符串常量,
integer
表示整型,real
表示浮點型,text
表示文本類型,blob
表示二進制類型。此外,使用了primary key
將 id 設置為主鍵,并且用autocrement
關鍵字表示 id 列是自增長的。 - 在
onCreate
方法中有調用了 SQLiteDatabase 的execSQL
方法去執行這條建表語句。
調用自己的幫助類
創建一個活動 SQLiteActivity
,其布局內有一個按鈕,點擊即可創建 Student.db
數據庫:
public class SQLiteActivity extends AppCompatActivity {private MyDatabaseHelper myDatabaseHelper;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.sqlite_layout);myDatabaseHelper = new MyDatabaseHelper(this, "Student.db", null, 1);Button button_create = findViewById(R.id.button_create);button_create.setOnClickListener((View v)->{myDatabaseHelper.getWritableDatabase();Toast.makeText(this, "創建數據庫成功", Toast.LENGTH_LONG).show();});}
}
- 第一次點擊按鈕時,會檢測到當前程序并沒有
Student.db
這個數據庫,于是會創建該數據庫并調用 MyDatabaseHelper 中的onCreate
方法,得以創建Student
表。 - 之后點擊按鈕就不會再調用 MyDatabaseHelper 中的
onCreate
方法了,因為Student.db
已經存在了。
布局文件 sqlite_layout.xml
:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><Buttonandroid:id="@+id/button_create"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="create database"/>
</LinearLayout>
運行結果:
create succeeded
只會在數據庫首次創建時出現:
而 創建數據庫成功
會在每次點擊按鈕后出現:
查看數據庫和表的創建情況
在環境變量中添加好 platform-tools
后:
打開 cmd,輸入 adb shell
進入設備控制臺:
通過 su
獲取管理員權限,否則無法進入 /data/data/<packagename>/databases/
路徑:
通過 cd
命令進入數據庫文件所在目錄:
該目錄下有兩個數據庫文件,一個是我們創建的 Student.db
;一個是為了讓支持事務的臨時日志文件 Student.db-journal
。
打開數據庫:
查看數據庫中有哪些表:
PS:android_metadata
是每個數據庫自動生成的。
查看建表語句:
通過 .exit
或 .quit
退出數據庫:
補全 onUpgrade() 方法
該方法用于升級數據庫,目前項目中已經有了一張 Student
表用于存放學生的各種詳細數據,如果想再添加一張 Class
表用于記錄學生的班級,怎么做呢?
將建表語句添加到自己的幫助類 MyDatabaseHelper
中:
該如上圖所示在 onCreate
階段執行一次 Class
的建表語句嗎?
不是的,正如上一個實例中,數據庫創建完成后,我們再點擊按鈕,只會彈出 創建數據庫成功
而不會彈出 create succeeded
一樣,兩者的根本原因都是 onCreate
方法只會在創建數據庫時執行一次,創建成功后不會再次執行。
因此無法在 Student.db
存在的情況下通過 onCreate
方法添加新表,而應通過 onUpgrade
方法添加新表。 具體做法如下:
@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {// 如果數據庫中存在 Student 表或 Class表,就將他們刪除。db.execSQL("drop table if exists Student");db.execSQL("drop table if exists Class");// 然后調用 onCreate 方法重新創建onCreate(db);}
PS:之所以 不跳過刪除已有表直接調用 onCreate ,是因為 創建表時如果該表已存在會報錯。
接下來重新調用 SQLiteOpenHelper 的構造方法,使第四個參數——數據庫版本號大于之前傳入的 1 即可讓 onUpgrade
執行:
運行結果:
PS:使用 AS 時也可以通過一下流程查看數據庫及建表情況:
增刪查改
CRUD 操作當然可以通過 SQL 語言實現,但 Android 也提供了一系列輔助方法,前面提到 getReadableDatabase
和 getWriteableDatabase
方法是會返回一個 SQLiteDatabase
對象,借助這個對象就可以對數據進行操作了。
增:SQLiteDatabase.insert()
該方法有三個參數:
- 表名
- 用于在未指定添加數據的情況下給某些可為空的列自動賦值為
null
ContentValues
對象,它提供了一系列的put
方法重載,用于向ContentValues
中添加數據,只需要將表中的每個列名及待添加數據傳入即可。
在布局中添加了一個按鈕用于增加數據:
在 SQLiteActivity 的 onCreate
方法中添加以下代碼:
點擊兩次按鈕的運行結果:
兩張表各添加了兩行數據。
改:SQLiteDatabase.update()
該方法有四個參數:
- 表名;
- ContentValues 對象;
- 第三個、第四個參數用于約束更新某一行或者某幾行的數據,不指定的話默認更新所有行。
在布局中添加了一個按鈕用于更新數據:
在 SQLiteActivity 的 onCreate
方法中添加以下代碼:
Button button_update = findViewById(R.id.button_update);button_update.setOnClickListener((View v)->{SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();ContentValues values = new ContentValues();// 第一條數據values.put("weight", 90);values.put("name", "zj");db.update("Student", values, "id = ?", new String[] {"2"});// 第二條數據values.put("class_name", "電子");values.put("class_num", 183);db.update("Class", values, "id = ?", new String[] {"1"});Toast.makeText(this, "更新完成", Toast.LENGTH_LONG).show();});
以第一條數據為例:
- values 用以更新
weight
和name
兩項屬性的值; - 第三、第四個參數指定更新
id=2
的行。
點擊按鈕后的運行結果:
刪:SQLiteDatabase.delete()
該方法接受三個參數:
- 表名;
- 第二、三個用于約束刪除某幾行的數據,不指定則刪除所有行。
在 SQLiteActivity 的 onCreate
方法中添加以下代碼:
Button button_delete = findViewById(R.id.button_delete);button_delete.setOnClickListener((View v)->{SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();db.delete("Student", "weight < ?", new String[] {"100"});Toast.makeText(this, "刪除完成", Toast.LENGTH_LONG).show();});
- 刪除
weight < 100
的行。
點擊按鈕后的運行結果:
查:SQLiteDatabase.query()
該方法比前三個復雜一些,最短的一個重載方法也需要傳入七個參數:
- 表名;
- 用于指定查詢哪幾列;
- 三四個參數用于約束查詢某幾行的數據,不指定則默認查詢所有行的數據;
- 第五個參數用于指定需要去
group by
的列,不指定則不對查詢結果進行分組; - 第六個參數用于對
group by
之后的數據進一步過濾; - 第七個參數用于指定查詢結果的排序方式。
調用該方法后會返回一個 Cursor
對象,查詢到的所有數據都將從這個對象中取出。
Button button_query = findViewById(R.id.button_query);button_query.setOnClickListener((View v)->{SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();// 查詢 Class 表中所有數據Cursor cursor = db.query("Class", null, null, null,null, null, null);if(cursor.moveToFirst()){do{String res = "";res += cursor.getString(cursor.getColumnIndex("class_name")) + " ";res += cursor.getString(cursor.getColumnIndex("class_num"));Toast.makeText(this, res, Toast.LENGTH_LONG).show();}while(cursor.moveToNext());}cursor.close();});
- 將 query方法 首參數設置為
Class
,其余參數設置為null
,表示查詢 Class表 所有數據。 - 調用 moveToFirst方法 將數據指針移動到第一行的位置;
- 通過 getColumnIndex方法 獲取位置索引以遍歷每一行數據,并將之通過
Toast
打印到屏幕上; - 通過 moveToNext方法 移動數據指針遍歷下一行數據,如果指針已經到達了數據指針集的尾后位置,此方法將返回
false
。
通過 SQL語句 實現增刪查改
除了查詢語句通過 db.rawQuery()
執行之外,其他三種操作都可以通過 db.execSQL()
執行。