SharedPreference引發(fā)ANR原理詳解
正文
日常開發(fā)中,使用過SharedPreference的同學,肯定在監(jiān)控平臺上看到過和SharedPreference相關的ANR,而且量應該不小。如果使用比較多或者經(jīng)常用sp存一些大數(shù)據(jù),如json等,相關的ANR經(jīng)常能排到前10。下面就從源碼的角度來看看,為什么SharedPreference容易產(chǎn)生ANR。
SharedPreference的用法,相信做過Android開發(fā)的同學都會,所以這里就只簡單介紹一下,不詳細介紹了。
// 初始化一個sp
SharedPreferences sharedPreferences = context.getSharedPreferences("name_sp", MODE_PRIVATE);
// 修改key的值,有兩種方法:commit和apply
sharedPreferences.edit().putBoolean("key_test", true).commit();
sharedPreferences.edit().putBoolean("key_test", true).apply();
// 讀取一個key
sharedPreferences.getBoolean("key_test", false);
SharedPreference問題
SharedPreference的相關方法,除了commit外,一般的開發(fā)同學都會直接在主線程調(diào)用,認為這樣不耗時。但其實,SharedPreference的很多方法都是耗時的,直接在主線程調(diào)很可能會引起ANR的問題。另外,雖然apply方法的調(diào)用不耗時,但是會引起生命周期相關的ANR問題。
下面就來從源碼的角度,看一下可能引起ANR的問題所在。
getSharedPreference(String name, int mode)
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
// 與sp相關的操作,都使用ContextImpl的類鎖
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
// mSharedPrefsPaths是內(nèi)存緩存的文件路徑
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 此處獲取SharedPreferences的文件路徑,可能存在耗時
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
下面看下獲取文件路徑的方法:getSharedPreferencesPath(),這個方法可能存在耗時。
public File getSharedPreferencesPath(String name) {
// 創(chuàng)建一個sp的存儲文件
return makeFilename(getPreferencesDir(), name + ".xml");
}
調(diào)用getPreferencesDir()獲取sharedPrefs的根路徑
private File getPreferencesDir() {
// 所有和文件有關的操作,都會使用mSync鎖,可能出現(xiàn)與其他線程搶鎖的耗時
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
// 這個方法,如果目錄不存在,會創(chuàng)建目錄,可能存在耗時
return ensurePrivateDirExists(mPreferencesDir);
}
}
ensurePrivateDirExists():確保文件目錄存在
private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
if (!file.exists()) {
final String path = file.getAbsolutePath();
try {
// 創(chuàng)建文件夾,會耗時
Os.mkdir(path, mode);
Os.chmod(path, mode);
} catch (ErrnoException e) {
}
return file;
}
再來看看getSharedPreferences生成SharedPreferenceImpl對象的流程。
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// 獲取cache,先從cache中獲取SharedPreferenceImpl
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
// 如果沒有cache,則創(chuàng)建一個SharedPreferencesImpl,此處可能存在耗時
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
先來看下cache的原理
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
// sSharedPrefsCache是一個靜態(tài)變量,全局有效
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
// key:包名,value: ArrayMap<File, SharedPreferencesImpl>
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
再來看看SharedPreferenceImpl的構造方法,看看SharedPreference是怎么初始化的。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
// 設置是否load到內(nèi)存的標志位為false
mLoaded = false;
startLoadFromDisk();
}
startLoadFromDisk():開啟一個子線程,將sp中的內(nèi)容讀取到內(nèi)存中
private void startLoadFromDisk() {
// 改mLoaded標志位時,需要獲取mLock鎖
synchronized (mLock) {
// load之前先設置mLoaded標志位為false
mLoaded = false;
}
// 開啟一個線程,從文件中將sp中的內(nèi)容讀取到內(nèi)存中
new Thread("SharedPreferencesImpl-load") {
public void run() {
// 在子線程load
loadFromDisk();
}
}.start();
}
loadFromDisk:真正讀取文件的地方
private void loadFromDisk() {
synchronized (mLock) {
// 如果已經(jīng)load過了,直接return,不需要再重新load
if (mLoaded) {
return;
}
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
// 讀取xml的內(nèi)容到map中
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
synchronized (mLock) {
// 設置mLoaded標志位為true,表示已經(jīng)load完,通知所有在等待的線程
mLoaded = true;
mLock.notifyAll();
}
}
總結:經(jīng)過上面的分析,getSharedPreferences主要的卡頓點在于,獲取PreferencesDir的時候,可能存在目錄尚未創(chuàng)建的情況。如果這個時候調(diào)用了創(chuàng)建目錄的方法,就會非常耗時。
getBoolean(String key, boolean defValue)
這個方法和所有獲取key的方法一樣,都可能存在耗時。
從SharedPreferencesImpl的構造方法,我們知道會開啟一個新的線程,將內(nèi)容從文件中讀取到緩存的map里,這個步驟我們叫l(wèi)oad。
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
// 需要等待,直到load成功
awaitLoadedLocked();
// 從緩存中取value
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
主要耗時的方法,在awaitLoadedLocked里。
private void awaitLoadedLocked() {
// 只有當mLoaded為true時,才能跳出死循環(huán)
while (!mLoaded) {
try {
// 調(diào)用wait后,會釋放mLock鎖,并且進入等待池,等待load完之后的喚醒
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
這個方法,調(diào)用了mLock.wait(),釋放了mLock的對象鎖,并且進入等待池,直到load完被喚醒。
總結:所以,getBoolean等獲取key的方法,會等待,直到sp的內(nèi)容從文件中copy到緩存map里。很可能存在耗時。
commit()
commit()方法,會進行同步寫,一定存在耗時,不能直接在主線程調(diào)用。
public boolean commit() {
// 開始排隊寫
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
// 等待同步寫的結果
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
apply()
大家都知道apply方法是異步寫,但是也可能造成ANR的問題。下面我們來看apply方法的源碼。
public void apply() {
// 先將更新寫入內(nèi)存緩存
final MemoryCommitResult mcr = commitToMemory();
// 創(chuàng)建一個awaitCommit的runnable,加入到QueuedWork中
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
// 等待寫入完成
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// 將awaitCommit加入到QueuedWork中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 真正執(zhí)行sp持久化操作,異步執(zhí)行
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 雖然還沒寫入文件,但是內(nèi)存緩存已經(jīng)更新了,而listener通常都持有相同的sharedPreference對象,所以可以使用內(nèi)存緩存中的數(shù)據(jù)
notifyListeners(mcr);
}
可以看到這里確實是在子線程進行的寫入操作,但是為什么說apply也會引起ANR呢?
因為在Activity和Service的一些生命周期方法里,都會調(diào)用QueuedWork.waitToFinish()方法,這個方法會等待所有子線程寫入完成,才會繼續(xù)進行。主線程等子線程,很容易產(chǎn)生ANR問題。
public static void waitToFinish() {
Runnable toFinish;
//等待所有的任務執(zhí)行完成
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
Android 8.0 在這里做了一些優(yōu)化,但還是需要等寫入完成,無法完成解決ANR的問題。
總結
綜上所述,SharedPreference可能在以下幾種情況下產(chǎn)生卡頓,從而引起ANR:
- 創(chuàng)建
SharedPreference時,調(diào)用getPreferenceDir,可能存在創(chuàng)建目錄的行為 getBoolean等方法,會等待直到SharedPreference將文件中的鍵值對全部讀取到緩存里,才會返回commit方法直接同步寫,如果不小心在主線程調(diào)用,會引起卡頓apply方法雖然是在異步線程寫入,但是由于Activity和Service的生命周期會等待所有SharedPreference的寫入完成,所以可能引起卡頓和ANR問題
SharedPreference從設計之初,就是為了存儲少量key-value對,而存在的。其本身的設計,就存在很多缺陷。在存儲特別少量數(shù)據(jù)的時候,性能瓶頸還不顯著。但是現(xiàn)在很多開發(fā)同學在使用的時候,會往里面存一些大型的JSON字符串等,導致它的缺點被明顯暴露出來。建議在使用SharedPreference的時候,只用于存儲少量數(shù)據(jù),不要存大的字符串。
當然,我們也有一些方法來統(tǒng)一優(yōu)化SharedPreference,減少ANR的發(fā)生,下一篇我們繼續(xù)講。
以上就是SharedPreference引發(fā)ANR原理詳解的詳細內(nèi)容,更多關于SharedPreference引發(fā)ANR的資料請關注腳本之家其它相關文章!
相關文章
Android ScrollView嵌套ExpandableListView顯示不正常的問題的解決辦法
這篇文章主要介紹了Android ScrollView嵌套ExpandableListView顯示不正常的問題的解決辦法的相關資料,需要的朋友可以參考下2017-02-02
flutter自定義InheritedProvider實現(xiàn)狀態(tài)管理詳解
這篇文章主要為大家介紹了flutter自定義InheritedProvider實現(xiàn)狀態(tài)管理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11
Android使用RecycleView實現(xiàn)拖拽交換item位置
這篇文章主要為大家詳細介紹了Android使用RecycleView實現(xiàn)拖拽交換item位置,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
Android的Fragment的生命周期各狀態(tài)和回調(diào)函數(shù)使用
這篇文章主要介紹了Android的Fragments的生命周期各狀態(tài)和回調(diào)函數(shù)使用,Fragments的生命周期與Activity息息相關,需要的朋友可以參考下2016-02-02

