您好,登錄后才能下訂單哦!
這篇文章主要介紹“SharedPreference引發(fā)ANR原理是什么”,在日常操作中,相信很多人在SharedPreference引發(fā)ANR原理是什么問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”SharedPreference引發(fā)ANR原理是什么”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
日常開(kāi)發(fā)中,使用過(guò)SharedPreference
的同學(xué),肯定在監(jiān)控平臺(tái)上看到過(guò)和SharedPreference
相關(guān)的ANR,而且量應(yīng)該不小。如果使用比較多或者經(jīng)常用sp存一些大數(shù)據(jù),如json等,相關(guān)的ANR經(jīng)常能排到前10。下面就從源碼的角度來(lái)看看,為什么SharedPreference
容易產(chǎn)生ANR。
SharedPreference
的用法,相信做過(guò)Android開(kāi)發(fā)的同學(xué)都會(huì),所以這里就只簡(jiǎn)單介紹一下,不詳細(xì)介紹了。
// 初始化一個(gè)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(); // 讀取一個(gè)key sharedPreferences.getBoolean("key_test", false);
SharedPreference
的相關(guān)方法,除了commit
外,一般的開(kāi)發(fā)同學(xué)都會(huì)直接在主線程調(diào)用,認(rèn)為這樣不耗時(shí)。但其實(shí),SharedPreference
的很多方法都是耗時(shí)的,直接在主線程調(diào)很可能會(huì)引起ANR的問(wèn)題。另外,雖然apply
方法的調(diào)用不耗時(shí),但是會(huì)引起生命周期相關(guān)的ANR問(wèn)題。
下面就來(lái)從源碼的角度,看一下可能引起ANR的問(wèn)題所在。
getSharedPreference(String name, int mode)
@Override public SharedPreferences getSharedPreferences(String name, int mode) { File file; // 與sp相關(guān)的操作,都使用ContextImpl的類(lèi)鎖 synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } // mSharedPrefsPaths是內(nèi)存緩存的文件路徑 file = mSharedPrefsPaths.get(name); if (file == null) { // 此處獲取SharedPreferences的文件路徑,可能存在耗時(shí) file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); }
下面看下獲取文件路徑的方法:getSharedPreferencesPath()
,這個(gè)方法可能存在耗時(shí)。
public File getSharedPreferencesPath(String name) { // 創(chuàng)建一個(gè)sp的存儲(chǔ)文件 return makeFilename(getPreferencesDir(), name + ".xml"); }
調(diào)用getPreferencesDir()
獲取sharedPrefs
的根路徑
private File getPreferencesDir() { // 所有和文件有關(guān)的操作,都會(huì)使用mSync鎖,可能出現(xiàn)與其他線程搶鎖的耗時(shí) synchronized (mSync) { if (mPreferencesDir == null) { mPreferencesDir = new File(getDataDir(), "shared_prefs"); } // 這個(gè)方法,如果目錄不存在,會(huì)創(chuàng)建目錄,可能存在耗時(shí) 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)建文件夾,會(huì)耗時(shí) Os.mkdir(path, mode); Os.chmod(path, mode); } catch (ErrnoException e) { } return file; }
再來(lái)看看getSharedPreferences
生成SharedPreferenceImpl
對(duì)象的流程。
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) { // 如果沒(méi)有cache,則創(chuàng)建一個(gè)SharedPreferencesImpl,此處可能存在耗時(shí) sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } return sp; }
先來(lái)看下cache的原理
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() { // sSharedPrefsCache是一個(gè)靜態(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; }
再來(lái)看看SharedPreferenceImpl
的構(gòu)造方法,看看SharedPreference
是怎么初始化的。
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); // 設(shè)置是否load到內(nèi)存的標(biāo)志位為false mLoaded = false; startLoadFromDisk(); }
startLoadFromDisk()
:開(kāi)啟一個(gè)子線程,將sp中的內(nèi)容讀取到內(nèi)存中
private void startLoadFromDisk() { // 改mLoaded標(biāo)志位時(shí),需要獲取mLock鎖 synchronized (mLock) { // load之前先設(shè)置mLoaded標(biāo)志位為false mLoaded = false; } // 開(kāi)啟一個(gè)線程,從文件中將sp中的內(nèi)容讀取到內(nèi)存中 new Thread("SharedPreferencesImpl-load") { public void run() { // 在子線程load loadFromDisk(); } }.start(); }
loadFromDisk
:真正讀取文件的地方
private void loadFromDisk() { synchronized (mLock) { // 如果已經(jīng)load過(guò)了,直接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) { // 設(shè)置mLoaded標(biāo)志位為true,表示已經(jīng)load完,通知所有在等待的線程 mLoaded = true; mLock.notifyAll(); } }
總結(jié):經(jīng)過(guò)上面的分析,getSharedPreferences
主要的卡頓點(diǎn)在于,獲取PreferencesDir
的時(shí)候,可能存在目錄尚未創(chuàng)建的情況。如果這個(gè)時(shí)候調(diào)用了創(chuàng)建目錄的方法,就會(huì)非常耗時(shí)。
getBoolean(String key, boolean defValue)
這個(gè)方法和所有獲取key的方法一樣,都可能存在耗時(shí)。
從SharedPreferencesImpl
的構(gòu)造方法,我們知道會(huì)開(kāi)啟一個(gè)新的線程,將內(nèi)容從文件中讀取到緩存的map里,這個(gè)步驟我們叫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; } }
主要耗時(shí)的方法,在awaitLoadedLocked里。
private void awaitLoadedLocked() { // 只有當(dāng)mLoaded為true時(shí),才能跳出死循環(huán) while (!mLoaded) { try { // 調(diào)用wait后,會(huì)釋放mLock鎖,并且進(jìn)入等待池,等待load完之后的喚醒 mLock.wait(); } catch (InterruptedException unused) { } } }
這個(gè)方法,調(diào)用了mLock.wait()
,釋放了mLock
的對(duì)象鎖,并且進(jìn)入等待池,直到load完被喚醒。
總結(jié):所以,getBoolean
等獲取key
的方法,會(huì)等待,直到sp的內(nèi)容從文件中copy到緩存map里。很可能存在耗時(shí)。
commit()
commit()
方法,會(huì)進(jìn)行同步寫(xiě),一定存在耗時(shí),不能直接在主線程調(diào)用。
public boolean commit() { // 開(kāi)始排隊(duì)寫(xiě) SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { // 等待同步寫(xiě)的結(jié)果 mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { } notifyListeners(mcr); return mcr.writeToDiskResult; }
apply()
大家都知道apply
方法是異步寫(xiě),但是也可能造成ANR的問(wèn)題。下面我們來(lái)看apply
方法的源碼。
public void apply() { // 先將更新寫(xiě)入內(nèi)存緩存 final MemoryCommitResult mcr = commitToMemory(); // 創(chuàng)建一個(gè)awaitCommit的runnable,加入到QueuedWork中 final Runnable awaitCommit = new Runnable() { @Override public void run() { try { // 等待寫(xiě)入完成 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); // 雖然還沒(méi)寫(xiě)入文件,但是內(nèi)存緩存已經(jīng)更新了,而listener通常都持有相同的sharedPreference對(duì)象,所以可以使用內(nèi)存緩存中的數(shù)據(jù) notifyListeners(mcr); }
可以看到這里確實(shí)是在子線程進(jìn)行的寫(xiě)入操作,但是為什么說(shuō)apply
也會(huì)引起ANR呢?
因?yàn)樵?code>Activity和Service
的一些生命周期方法里,都會(huì)調(diào)用QueuedWork.waitToFinish()
方法,這個(gè)方法會(huì)等待所有子線程寫(xiě)入完成,才會(huì)繼續(xù)進(jìn)行。主線程等子線程,很容易產(chǎn)生ANR問(wèn)題。
public static void waitToFinish() { Runnable toFinish; //等待所有的任務(wù)執(zhí)行完成 while ((toFinish = sPendingWorkFinishers.poll()) != null) { toFinish.run(); } }
Android 8.0 在這里做了一些優(yōu)化,但還是需要等寫(xiě)入完成,無(wú)法完成解決ANR的問(wèn)題。
到此,關(guān)于“SharedPreference引發(fā)ANR原理是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。