Android 五種數據存儲的方式分別為:
- SharedPreferences:以Map形式存放簡單的配置參數;
- ContentProvider:將應用的私有數據提供給其他應用使用;
- 文件存儲:以IO流形式存放,可分為手機內部和手機外部(sd卡等)存儲,可存放較大數據;
- SQLite:輕量級、跨平臺數據庫,將所有數據都是存放在手機上的單一文件內,占用內存?。?/li>
- 網絡存儲 :數據存儲在服務器上,通過連接網絡獲取數據;
Sharedpreferences是Android平臺上一個輕量級的存儲類,用來保存應用程序的各種配置信息,其本質是一個以“鍵-值”對的方式保存數據的xml文件,其文件保存在/data/data/<package name>/shared_prefs目錄下。在全局變量上看,其優點是不會產生Application 、 靜態變量的OOM(out of memory)和空指針問題,其缺點是效率沒有上面的兩種方法高。
1.獲取SharedPreferences
要想使用 SharedPreferences 來存儲數據,首先需要獲取到 SharedPreferences 對象。Android中主要提供了三種方法用于得到 SharedPreferences 對象。
?1. Context 類中的 getSharedPreferences()方法:
?此方法接收兩個參數,第一個參數用于指定 SharedPreferences 文件的名稱,如果指定的文件不存在則會創建一個,第二個參數用于指定操作模式,主要有以下幾種模式可以選擇。MODE_PRIVATE 是默認的操作模式,和直接傳入 0 效果是相同的。
?MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 這兩種模式已在 Android 4.2 版本中被廢棄。
Context.MODE_PRIVATE: 指定該SharedPreferences數據只能被本應用程序讀、寫;
Context.MODE_WORLD_READABLE: 指定該SharedPreferences數據能被其他應用程序讀,但不能寫;
Context.MODE_WORLD_WRITEABLE: 指定該SharedPreferences數據能被其他應用程序讀;
Context.MODE_APPEND:該模式會檢查文件是否存在,存在就往文件追加內容,否則就創建新文件;
2. Activity 類中的 getPreferences()方法:
?這個方法和 Context 中的 getSharedPreferences()方法很相似,不過它只接收一個操作模式參數,因為使用這個方法時會自動將當前活動的類名作為 SharedPreferences 的文件名。
3. PreferenceManager 類中的 getDefaultSharedPreferences()方法:
?這是一個靜態方法,它接收一個 Context 參數,并自動使用當前應用程序的包名作為前綴來命名 SharedPreferences 文件。
2.SharedPreferences的使用
SharedPreferences對象本身只能獲取數據而不支持存儲和修改,存儲修改是通過SharedPreferences.edit()獲取的內部接口Editor對象實現。使用Preference來存取數據,用到了SharedPreferences接口和SharedPreferences的一個內部接口SharedPreferences.Editor,這兩個接口在android.content包中;
1)寫入數據:
//步驟1:創建一個SharedPreferences對象
SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
//步驟2: 實例化SharedPreferences.Editor對象
SharedPreferences.Editor editor = sharedPreferences.edit();
//步驟3:將獲取過來的值放入文件
editor.putString("name", “Tom”);
editor.putInt("age", 28);
editor.putBoolean("marrid",false);
//步驟4:提交
editor.commit();
2)讀取數據:
SharedPreferences sharedPreferences= getSharedPreferences("data", Context .MODE_PRIVATE);
String userId=sharedPreferences.getString("name","");
3)刪除指定數據
editor.remove("name");
editor.commit();
4)清空數據
editor.clear();
editor.commit();
注意:如果在 Fragment 中使用SharedPreferences 時,需要放在onAttach(Activity activity)里面進行SharedPreferences的初始化,否則會報空指針 即 getActivity()會可能返回null !
讀寫其他應用的SharedPreferences 步驟如下(未實踐):
?1. 在創建SharedPreferences時,指定MODE_WORLD_READABLE模式,表明該SharedPreferences數據可以被其他程序讀??;
?2. 創建其他應用程序對應的Context;
?3. 使用其他程序的Context獲取對應的SharedPreferences;
?4. 如果是寫入數據,使用Editor接口即可,所有其他操作均和前面一致;
try {
//這里的com.example.mpreferences 就是應用的包名
Context mcontext = createPackageContext("com.example.mpreferences", CONTEXT_IGNORE_SECURITY);
SharedPreferences msharedpreferences = mcontext.getSharedPreferences("name_preference", MODE_PRIVATE);
int count = msharedpreferences.getInt("count", 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
3. SharedPreferences 的源碼分析(API 25)
先從Context的getSharedPreferences開始:
public abstract SharedPreferences getSharedPreferences(String name, int mode);
我們知道Android中的Context類其實是使用了裝飾者模式,而被裝飾對象其實就是一個ContextImpl對象,ContextImpl的getSharedPreferences方法:
/**
* Map from preference name to generated path.
* 從preference名稱到生成路徑的映射;
*/
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;
/**
* Map from package name, to preference name, to cached preferences.
* 從包名映射到preferences,以緩存preferences,這是個靜態變量;
*/
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
從上面我們可以看出他們之間的關系:
mSharedPrefsPaths存放的是名稱與文件夾的映射(ArrayMap<String, File>),這里的名稱就是我們使用getSharedPreferences時傳入的name,如果mSharedPrefsPaths為null則初始化,如果file為null則新建一個File并將其加入mSharedPrefsPaths中;
( ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> )
sSharedPrefsCache 存放包名與ArrayMap鍵值對
初始化時會默認以包名作為鍵值對中的Key,注意這是個static變量;
(ArrayMap<File, SharedPreferencesImpl>)sSharedPrefs
packagePrefs存放文件name與SharedPreferencesImpl鍵值對
注意:
- 對于一個相同的SharedPreferences name,獲取到的都是同一個SharedPreferences對象,它其實是SharedPreferencesImpl對象。
- sSharedPrefs在程序中是靜態的,如果退出了程序但Context沒有被清掉,那么下次進入程序仍然可能取到本應被刪除掉的值。而換了另一種清除SharedPreferences的方式:使用SharedPreferences.Editor的commit方法能夠起作用,調用后不退出程序都馬上生效。
SharedPreferencesImpl對象
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 備份的File
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
notifyAll();
}
}
可以看到對于一個SharedPreferences文件name,第一次調用getSharedPreferences時會去創建一個SharedPreferencesImpl對象,它會開啟一個子線程,然后去把指定的SharedPreferences文件中的鍵值對全部讀取出來,存放在一個Map中。
調用getString時那個SharedPreferencesImpl構造方法開啟的子線程可能還沒執行完(比如文件比較大時全部讀取會比較久),這時getString當然還不能獲取到相應的值,必須阻塞到那個子線程讀取完為止,如getString方法:
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
顯然這個awaitLoadedLocked方法就是用來等this這個鎖的,在loadFromDiskLocked方法的最后我們也可以看到它調用了notifyAll方法,這時如果getString之前阻塞了就會被喚醒。那么這里會存在一個問題,我們的getString是寫在UI線程中,如果那個getString被阻塞太久了,比如60s,這時就會出現ANR,所以要根據具體情況考慮是否需要把SharedPreferences的讀寫放在子線程中。
關于mBackupFile,SharedPreferences在寫入時會先把之前的xml文件改成名成一個備份文件,然后再將要寫入的數據寫到一個新的文件中,如果這個過程執行成功的話,就會把備份文件刪除。由此可見每次即使只是添加一個鍵值對,也會重新寫入整個文件的數據,這也說明SharedPreferences只適合保存少量數據,文件太大會有性能問題。
注意:
- 在UI線程中調用getXXX可能會導致ANR。
- 我們在初始化SharedPreferencesImpl對象時會加SharedPreferencesImpl對應的xml文件中的所有數據都加載到內存中,如果xml文件很大,將會占用大量的內存,我們只想讀取xml文件中某個key的值,但我們獲取它的時候是會加載整個文件。
- 每添加一個鍵值對,都會重新寫入整個文件的數據,不是增量寫入;
綜上原因能說明Sharedpreferences只適合做輕量級的存儲。
SharedPreferences的內部類Editor
SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name", “Tom”);
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
其實拿到的是一個EditorImpl對象,它是SharedPreferencesImpl的內部類:
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
.....
}
可以看到它有一個Map對象mModified,用來保存“修改的數據”,也就是你每次put的時候其實只是把那個鍵值對放到這個mModified 中,最后調用apply或者commit才會真正把數據寫入文件中,如上面的putString方法,其它putXXX代碼基本也是一樣的。
commit方法和apply方法的不同
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
public boolean changesMade; // any keys different?
public List<String> keysModified; // may be null
public Set<OnSharedPreferenceChangeListener> listeners; // may be null
public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
兩種方式首先都會先使用commitTomemory函數將修改的內容寫入到SharedPreferencesImpl當中,再調用enqueueDiskWrite寫磁盤操作,commitToMemory就是產生一個“合適”的MemoryCommitResult對象mcr,然后調用enqueueDiskWrite時需要把這個對象傳進去,commitToMemory方法:
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
mModified.clear();
}
}
return mcr;
}
這里需要弄清楚兩個對象mMap和mModified,mMap是存放當前SharedPreferences文件中的鍵值對,而mModified是存放此時edit時put進去的鍵值對。mDiskWritesInFlight表示正在等待寫的操作數量。
可以看到這個方法中首先處理了clear標志,它調用的是mMap.clear(),然后再遍歷mModified將新的鍵值對put進mMap,也就是說在一次commit事務中,如果同時put一些鍵值對和調用clear后再commit,那么clear掉的只是之前的鍵值對,這次put進去的鍵值對還是會被寫入的。
遍歷mModified時,需要處理一個特殊情況,就是如果一個鍵值對的value是this(SharedPreferencesImpl)或者是null那么表示將此鍵值對刪除,這個在remove方法中可以看到,如果之前有同樣的key且value不同則用新的valu覆蓋舊的value,如果沒有存在同樣的key則完整寫入。需要注意的是這里使用了同步鎖住edtor對象,保證了當前數據正確存入。
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
commit接下來就是調用enqueueDiskWrite方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
定義一個Runnable任務,在Runnable中先調用writeToFile進行寫操作,寫操作需要先獲得mWritingToDiskLock,也就是寫鎖。然后執行mDiskWritesInFlight–,表示正在等待寫的操作減少1。
判斷postWriteRunnable是否為null,調用commit時它為null,而調用apply時它不為null。isFromSyncCommit為true,而且有1個寫操作需要執行,那么就調用writeToDiskRunnable.run(),注意這個調用是在當前線程中進行的。如果不是commit,那就是apply,這時調用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),這個QueuedWork類其實很簡單,里面有一個SingleThreadExecutor,用于異步執行這個writeToDiskRunnable,commit的寫操作是在調用線程中執行的,而apply內部是用一個單線程的線程池實現的,因此寫操作是在子線程中執行的。
commit和apply的總結:
- apply沒有返回值而commit返回boolean表明修改是否提交成功 ;
- commit是把內容同步提交到硬盤的,而apply先立即把修改提交到內存,然后開啟一個異步的線程提交到硬盤,并且如果提交失敗,你不會收到任何通知。
- 所有commit提交是同步過程,效率會比apply異步提交的速度慢,在不關心提交結果是否成功的情況下,優先考慮apply方法。
- apply是使用異步線程寫入磁盤,commit是同步寫入磁盤。所以我們在主線程使用的commit的時候,需要考慮是否會出現ANR問題。(不適合大量數據存儲)
4. 查看Sharedpreferencesd 保存數據的xml文件
要想查看data文件首先要獲取手機root權限,成功root后,修改data權限即可查看data里面的數據庫。由于在xml文件內可以很清楚的查看到各個鍵-值”對數據,所以用Sharedpreferencesd保存比較重要的數據的時候最好先加密再保存。成功查看如下圖所示:
data權限修改辦法:
1. 打開cmd;
2. 輸入’adb shell’;
3. 輸入su,回車 ;
4. 輸入chmod 777 /data/data/<package name>/shared_prefs 回車
該步驟設置data文件夾權限為777(drwxrwxrwx,也即administrators、power users和users組都有對該文件夾的讀、寫、運行權限) ;
當你在Linux下用命令ll 或者ls -la的時候會看到類似drwxr-xr-x這樣標識,具體代表什么意思呢?
?這段標識總長度為10位(10個‘-’),第一位表示文件類型,如該文件是文件(用-表示),如該文件是文件夾(用d表示),如該文件是連接文件(用l表示),后面9個按照三個一組分,第一組:用戶權限,第二組:組權限,第三組:其他權限。 每一組是三位,分別是讀 r ,寫 w,執行 x,這些權限都可以用數字來表示:r 4, w 2 , x 1。如果沒有其中的某個權限則用‘-’表示。例如:
?1. -rwxrwx---,第一位‘-’代表的是文件,第二位到第四位rwx代表此文件的擁有者有讀、寫、執行的權限,同組用戶也有讀、寫、及執行權限,其他用戶組沒任何權限。用數字來表示的話則是770.
?2. drwx------,第一位‘d’代表的是文件夾,第二位到第四位rwx代表此文件夾的擁有者有讀、寫、執行的權限,第五位到第七位代表的是擁有者同組用戶的權限,同組用戶沒有任何權限,第八位到第十位代表的是其他用戶的權限,其他用戶也沒有任何權限。用數字來表示的話則是700.