Android App開發基礎篇—資料存儲(SP和檔案)
前言:Android中提供了多種資料存儲技術來永久性儲存應用資料,以便開發者能夠根據需求選擇合适的資料存儲方案,例如:資料是應用的私有資料,還是可供其他應用(和使用者)通路的資料,資料所占的存儲空間等。Android中的資料存儲方案主要有:共享首選項(SharedPreferences)、内部存儲(Internal Storage)、外部存儲(External Storage)、SQLite資料庫、網絡存儲等。下面來逐一了解。(文章時間比較早,存儲相關的部分内容未涉及安卓8以後的變更)
注意:本文中所有提到的目錄路徑(如/data/data/包名/files之類的目錄)都是使用模拟器進行測試時顯示的路徑,不同的手機顯示出的路徑可能不同。例如,使用getFilesDir()方法擷取内部存儲目錄時,在模拟器上顯示的是 /data/data/包名/files,而在部落客手機(米3W)上顯示的是 /data/user/0/包名/files。
一、SharedPreferences 共享首選項
1.定義
SharedPreferences類提供了一個通用架構,使開發者能夠以鍵值對的方式,永久性的儲存一些原始資料類型的資料,包括:布爾值,浮點值,整型值,長整型和字元串,被儲存的資料可以跨多個使用者會話永久保留(即使應用已經終止)。
2.使用
要使用SharedPreferences,主要步驟有:
(1)通過getSharedPreferences()或者getPreferences()方法擷取SharedPreferences對象。其中,兩個方法的差別在于:如果使用getSharedPreferences()方法,則儲存的資料是全局性的,即在整個app應用過程中都是可擷取的。當使用此方法時,系統會在/data/data/包名/shared_prefs目錄下生成一個xxx.xml檔案,檔案名根據getSharedPreferences()傳入的第一個參數而定;而使用getPreferences()方法,所儲存的資料僅對儲存資料的那個Activity有效,而在其他Activity中将無法擷取。系統會在/data/data/包名/shared_prefs目錄下生成一個Activity名.xml的檔案。
(2)通過SharedPreferences對象的edit()方法擷取一個SharedPreferences.Editor對象;
(3)使用(2)中擷取的對象調用putBoolean()(或者putString()、putInt()等,由要儲存的資料的資料類型而定)等方法添加值;
(4)使用commit()送出,必須執行這一步。
(5)在需要擷取資料的地方,使用SharedPreferences對象的getBoolean()(或者getString()、getInt()等,同樣由所儲存的資料的資料類型而定)等方法讀取值。
下面上一個簡單示例,示例中在MainActivity中添加兩個Button,一個存值,一個取值。代碼如下:
package com.test.app2;
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn1 = (Button) findViewById(R.id.btn_main);
Button btn2 = (Button) findViewById(R.id.btn2_main);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//擷取SharedPreferences對象,方法中兩個參數的意思為:第一個name
//表示檔案名,系統将會在/dada/dada/包名/shared_prefs目錄下生成
//一個以該參數命名的.xml檔案。第二個mode表示建立的模式,通過檢視
//方法注釋得知,建議以0或者MODE_PRIVATE為預設值。
SharedPreferences app2 = getSharedPreferences("app2", 0);
//擷取Editor對象
SharedPreferences.Editor edit = app2.edit();
//根據要儲存的資料的類型,調用對應的put方法,
//以鍵值對的形式添加新值。
edit.putString("data", "testdata");
//送出新值。必須執行,否則前面的操作都無效。
edit.commit();
}
});
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//根據儲存時所用的name屬性,擷取SharedPreferences對象
SharedPreferences pf = getSharedPreferences("app2", 0);
//根據資料類型,調用對應的get方法,通過鍵取得對應的值。
String string = pf.getString("data", "null");
Log.d("MainActivity", string);
}
});
}
}
二、存儲檔案類型的資料
當需要存儲檔案類型的資料時,可以将資料儲存在裝置的Internal Storage(内部存儲),或者External Storage(外部存儲)中。
1.Internal Storage 内部存儲 (檔案目錄:/data/data/)
1.1 定義
預設情況下,如果要儲存的檔案是應用的私有檔案,應當将檔案儲存在裝置的Internal Storage(内部存儲)中。這裡所說的Internal Storage(内部存儲),是指系統在自身内部存儲器上所配置設定出來的用作Internal Storage(内部存儲)的分區。當使用者解除安裝應用時,儲存在Internal Storage(内部存儲)中的檔案也會被移除。
1. 2.在内部存儲中建立和讀取私有檔案(檔案目錄:/data/data/包名/files)
1.2.1 建立私有檔案并存入内部存儲
要建立一個私有檔案并寫入内部存儲,可以通過以下幾個步驟:
(1)使用檔案名和操作模式為參數,調用openFileOutput()方法。該方法會在/data/data/包名/files目錄下建立一個檔案。此方法需要兩個參數:第一個參數表示要建立的檔案的檔案名;第二個參數表示建立檔案的模式,該參數有幾個可選項:Context.MODE_PRIVATE、Context.MODE_APPEND、Context.MODE_WORLD_READABLE、Context.MODE_WORLD_WRITEABLE。各個選項的意義為:
Context.MODE_PRIVATE:預設的檔案建立模式。使用該模式建立檔案,如果檔案目錄中已存在同名檔案,則建立的檔案會覆寫舊檔案。并且,檔案隻能由建立檔案的應用(或者與該應用共享同一user ID的應用)所通路。
Context.MODE_APPEND:使用該模式建立檔案,如果檔案目錄中已存在同名檔案,則新的内容将直接被添加到舊檔案的尾部,而不會建立一個檔案來覆寫舊檔案。
Context.MODE_WORLD_READABLE:使其他應用對檔案具有讀的權限。
Context.MODE_WORLD_WIRTEABLE:使其他應用對檔案具有寫的權限。
注意:從API Level 17以後,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已經被棄用。從Android N(即7.x)開始,使用這兩個常量會導緻SecurityException。這意味着面向Android N和更高版本的應用無法按名稱共享私有檔案,嘗試共享“file://"類型的URI将會導緻FileUriExposedException。
(2)使用(1)中傳回的FileOutputStream執行個體,調用write()方法将資料寫入檔案。
(3)調用close()方法關閉流式傳輸。
例如,下面一個簡單的例子:
建立一個應用私有檔案,并寫入資料
//檔案名 String file_name = "test_file"; //寫入檔案的内容 String string = "hello android!!"; try { //調用openFileOutput,傳回一個FileOutputStream FileOutputStream fos = openFileOutput(file_name, Context.MODE_PRIVATE); //調用write()方法寫入資料 fos.write(string.getBytes()); //調用close()方法關閉流式傳輸 fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
1.2.2 從内部存儲中讀取檔案 (檔案目錄:/data/data/包名/files)
要從内部存儲中讀取檔案,步驟如下:
(1)使用要讀取的檔案的檔案名作為參數,調用openFileInput()方法,傳回一個FileInputStream執行個體。
(2)調用read()方法讀取檔案位元組。
(3)調用close()方法關閉流式傳輸。
例如,下面例子示範了從上面例子建立的test_file檔案中讀取資料:
從内部存儲中讀取檔案
//用于存儲資料流的byte數組
byte[] b = new byte[1024];
try {
//調用openFileInput,傳回一個FileInputStream
FileInputStream fis = openFileInput(file_name);
//調用read()方法,将資料讀入byte數組
int read = fis.read(b);
//調用close()方法關閉流式傳輸
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
注意:檔案的讀寫操作是耗時操作,是以最好不要在主線程(UI線程)中執行,避免程式出現ANR(程式未響應)。并且,在每次操作結束後,要記得調用close()方法關閉對應的輸入流或者輸出流,以釋放系統資源。
1.3.内部存儲緩存檔案 (檔案目錄:/data/data/包名/cache)
當有一些檔案隻需臨時使用,不必永久儲存的時候,應該把這些檔案儲存在内部存儲的緩存目錄中。儲存在内部存儲的緩存目錄中的檔案,當裝置内部存儲空間不足時,系統可能會首先删除這些緩存檔案以釋放記憶體空間。但是,開發者不應該依賴系統來清理這些檔案,而應該始終自行維護緩存檔案,使其占用的空間保持在合理的限制範圍内(如1MB)。當應用被解除安裝時,緩存檔案也會被移除。
1.3.1 将檔案儲存為内部存儲緩存檔案
要在内部存儲緩存目錄中儲存緩存檔案,具體步驟如下:
在内部存儲緩存目錄中儲存緩存檔案
//檔案名
String filename = "test_cache_file";
//寫入檔案的内容
String string = "hello cache";
//使用getCacheDir()方法獲得内部存儲緩存目錄,
//即/data/data/包名/cache目錄
File cacheDir = getCacheDir();
//構造檔案路徑,即/data/data/包名/cache/檔案名,
//通過這種方式指明檔案要儲存在内部存儲緩存目錄中。
String cache_fle = cacheDir + "/" + filename;
try {
//通過FileOutputStream的構造方法傳入構造好
//的檔案路徑,傳回一個FileOutputStream執行個體。
//注意:這裡沒有再使用openFileOutput,是因
//為openFileOutput參數不允許包含路徑分隔符“/”,
//如果繼續使用openFileOutput方法,将會出現
//IllegalArgumentException異常
FileOutputStream fos = new FileOutputStream(cache_fle);
//調用write()方法寫入資料
fos.write(string.getBytes());
//調用close()方法關閉流式傳輸
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
1.3.2 讀取内部存儲緩存目錄下的緩存檔案
要讀取緩存檔案,具體步驟如下:
讀取内部存儲緩存目錄中的緩存檔案
//檔案名
String filename = "test_cache_file";
//一個byte數組用于儲存讀取到的資料
byte[] b = new byte[1024];
//使用getCacheDir()方法獲得内部存儲緩存目錄,
//即/data/data/包名/cache目錄
File cacheDir = getCacheDir();
//擷取要讀取的檔案的路徑
String cache_file = cacheDir + "/" + filename;
try {
//通過FileInputStream傳入檔案路徑,傳回一個FileInputStream
//注意:openFileInput參數同樣不能包含路徑分隔符
FileInputStream fis = new FileInputStream(cache_file);
//調用read()方法讀取檔案内容,并儲存在byte數組中
int read = fis.read(b);
//byte數組轉為String字元串
String s = new String(b);
//調用close()方法關閉流式傳輸
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
1.4.一些對Internal Storage(内部存儲)中檔案和目錄的操作實用的方法
(1)getFilesDir():擷取存儲内部檔案的檔案系統目錄的絕對路徑。即 /data/data/包名/files
(2)getDir(String name,int mode):在内部存儲空間中建立(或打開現有的)目錄。即在 data/data/包名 目錄下建立(或打開現有的)目錄。
(3)deleteFile(String name):删除 /data/data/包名/files目錄下name屬性指定的檔案。此方法參數不能包含路徑分隔符“/”,直接傳入要删除的檔案的檔案名即可。
(4)fileList():列出 /data/data/包名/files目錄下所有檔案的檔案名。
2.External Storage 外部存儲 (檔案目錄:/mnt/sdcard/)
2.1 定義
所有相容Android的裝置都支援一個共享的,能夠讓使用者儲存檔案的“External Storage(外部存儲)”。這個外部存儲可能是一個可移除的存儲媒體(如SD卡),或者是系統在自身内部存儲器上所配置設定出來的用作External Storage(外部存儲)的分區,包括裝置上的一些公共目錄,比如手機相冊(Pictures)、音樂(Music)等目錄。存儲在外部存儲中的檔案是全局可讀取檔案,并且當使用者啟用了USB大容量存儲媒體在PC上傳輸檔案時,使用者是可以對這些檔案進行編輯的。
注意:當使用者将外部存儲加載到PC上,或者将存儲媒體從裝置上移除時,外部存儲可能會變得不可用。此外,對于存儲在外部存儲裡的檔案是沒有權限限制性可言的。所有的應用都可以對這些檔案進行讀寫,使用者也可以随時對它們進行編輯或删除。
2.2 通路External Storage(外部存儲)
2.2.1.添加通路權限
為了使應用能夠通路外部存儲中的檔案,首先需要給應用添加 READ_EXTERNAL_STORAG和WRITE_EXTERNAL_STORAGE權限。READ_EXTERNAL_STORAG允許應用通路外部存儲中的檔案,但無法對檔案進行編輯。如果想要通路外部存儲中的檔案并對其進行編輯,則隻需配置 WRITE_EXTERNAL_STORAGE權限即可,該權限會使應用同時包含對檔案的讀和寫權限。配置權限的方法如下,在AndroidManifest.xml檔案中添加外部存儲通路權限配置
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.app2">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
....
</manifest>
2.2.2.檢查External Storage(外部存儲)的可用狀态
要通路外部存儲,首先需要先檢查外部存儲是否可用。通過使用Environment.getExternalStorageState()方法檢查外部存儲是否可用,該方法傳回目前裝置外部存儲的可用狀态,我們可以通過該狀态值,判斷外部存儲的可用性。如下面方法用于判斷外部存儲是否可用(既可讀又可寫):
/**
* 判斷裝置外部存儲是否可用(既可讀又可寫)
*
* @return true(可用) or false(不可用)
*/
public boolean isExternalStorageAvailable() {
//擷取外部存儲的可用狀态
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
2.2.3 在外部存儲儲存檔案
(1)将檔案儲存在裝置的公共目錄(手機相冊、音樂、鈴聲目錄等)
通常來說,如果使用者通過應用所建立的檔案,允許其他應用通路,并且允許使用者在裝置上友善的對其進行操作,比如複制,那麼這些檔案應該儲存在裝置的公共存儲目錄中,例如裝置的相冊、音樂、鈴聲等檔案目錄。可以通過Environment.getExternalStoragePublicDirectory(String type)方法,該方法必須傳入一個type參數,以指定所儲存的檔案的類型,例如Environment.DIRECTORY_MUSIC、Environment.DIRECTORY_PICTURES、Environment.DIRECTORY_RINGTONES等,而後系統的媒體掃描器将會對檔案進行歸類,儲存到對應類型的公共目錄中(例如:一個鈴聲類型的檔案将會被儲存在系統的鈴聲目錄中,而不會出現在系統的音樂檔案夾中)。例如,下面代碼将在裝置的相冊(Pictures)目錄下建立一個"MyPictures"檔案夾:
//擷取外存狀态
String state = Environment.getExternalStorageState();
//如果外存可用
if (Environment.MEDIA_MOUNTED.equals(state)) {
//擷取裝置的相冊目錄的路徑
File picDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
//擷取一個File
File newFolder = new File(picDirectory, "MyPictures");
//如果File指定的目錄不存在,建立目錄
if (!newFolder.exists()){
newFolder.mkdir();
}
}
(2)将檔案儲存在應用的私有目錄中 (檔案目錄:/mnt/sdcard/Android/data/包名/files)
如果應用所建立的檔案并不想對其他應用可見,那麼應該将檔案儲存在外部存儲的應用私有目錄中。系統的媒體掃描程式(MediaStore内容提供者)不會讀取該目錄下的檔案。但是,其他具有權限的應用仍然可以通路。存儲在該目錄下的檔案,在應用解除安裝時會一起被移除,是以,如果檔案最終是使用者需要長久持有的,應該把檔案儲存在裝置的公共目錄中。
要使檔案儲存在外部存儲,并處于應用私有目錄,需要使用getExternalFilesDir(String type)方法,該方法會傳回應用在外部存儲中的私有目錄的路徑。該方法同樣需要一個type參數來指定檔案的類型,以便對所儲存的檔案進行歸類。如果不需要指定類型,則可以使用null,檔案将被預設儲存到/mnt/sdcard/Android/data/包名/files目錄中。例如,下面例子将在外部存儲的私有目錄(/mnt/sdcard/Android/data/包名/files)下建立一個Music檔案夾并儲存一個名為music的檔案。
//擷取外部存儲中的應用私有目錄,同時指定要存儲的檔案是音樂類型的檔案
File externalFilesDir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
//擷取一個File
File file = new File(externalFilesDir, "music");
//如果檔案不存在,建立一個新的檔案
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
(3)儲存緩存檔案 (檔案目錄:/mnt/sdcard/Android/data/包名/cache)
如果要儲存的檔案是緩存檔案,可以通過getExternalCacheDir()方法擷取外部存儲中的cache目錄路徑。為了節省檔案空間和應用性能,對于緩存檔案應該要做适當的管理,比如限制緩存檔案所占空間在合理的範圍内(如1MB),及時移除沒用的緩存檔案等。
後記:文中所提到的Internal Storage(内部存儲)和External Storage(外部存儲),所指的實際上都是位于裝置内部存儲器上的兩個存儲分區,無論是Internal Storage還是External Storage,所存儲的檔案其實都是存儲在裝置的内部存儲媒體上,就如同PC上的硬碟,一般我們在作業系統中會看到被分成幾個分區一般。希望大家不要被名字所混淆。這裡的“内”和“外”,所表示的實際意義是:應用所建立的檔案對于其他應用(或者使用者)所開放的通路權限。對于Internal Storage裡面的檔案而言,隻有檔案所屬者的應用能夠通路;而對于External Storage裡面的檔案,隻要是具有READ_EXTERNAL_STORAGE或者WRITE_EXTERNAL_STORAGE權限的應用,都可以通路。
未完,待續!