<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
日常開發中,使用過SharedPreference
的同學,肯定在監控平臺上看到過和SharedPreference
相關的ANR,而且量應該不小。如果使用比較多或者經常用sp存一些巨量資料,如json等,相關的ANR經常能排到前10。下面就從原始碼的角度來看看,為什麼SharedPreference
容易產生ANR。
SharedPreference
的用法,相信做過Android開發的同學都會,所以這裡就只簡單介紹一下,不詳細介紹了。
// 初始化一個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
的相關方法,除了commit
外,一般的開發同學都會直接在主執行緒呼叫,認為這樣不耗時。但其實,SharedPreference
的很多方法都是耗時的,直接在主執行緒調很可能會引起ANR的問題。另外,雖然apply
方法的呼叫不耗時,但是會引起生命週期相關的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是記憶體快取的檔案路徑 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) { // 建立一個sp的儲存檔案 return makeFilename(getPreferencesDir(), name + ".xml"); }
呼叫getPreferencesDir()
獲取sharedPrefs
的根路徑
private File getPreferencesDir() { // 所有和檔案有關的操作,都會使用mSync鎖,可能出現與其他執行緒搶鎖的耗時 synchronized (mSync) { if (mPreferencesDir == null) { mPreferencesDir = new File(getDataDir(), "shared_prefs"); } // 這個方法,如果目錄不存在,會建立目錄,可能存在耗時 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 { // 建立資料夾,會耗時 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,則建立一個SharedPreferencesImpl,此處可能存在耗時 sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } return sp; }
先來看下cache的原理
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() { // sSharedPrefsCache是一個靜態變數,全域性有效 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到記憶體的標誌位為false mLoaded = false; startLoadFromDisk(); }
startLoadFromDisk()
:開啟一個子執行緒,將sp中的內容讀取到記憶體中
private void startLoadFromDisk() { // 改mLoaded標誌位時,需要獲取mLock鎖 synchronized (mLock) { // load之前先設定mLoaded標誌位為false mLoaded = false; } // 開啟一個執行緒,從檔案中將sp中的內容讀取到記憶體中 new Thread("SharedPreferencesImpl-load") { public void run() { // 在子執行緒load loadFromDisk(); } }.start(); }
loadFromDisk
:真正讀取檔案的地方
private void loadFromDisk() { synchronized (mLock) { // 如果已經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的內容到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,表示已經load完,通知所有在等待的執行緒 mLoaded = true; mLock.notifyAll(); } }
總結:經過上面的分析,getSharedPreferences
主要的卡頓點在於,獲取PreferencesDir
的時候,可能存在目錄尚未建立的情況。如果這個時候呼叫了建立目錄的方法,就會非常耗時。
getBoolean(String key, boolean defValue)
這個方法和所有獲取key的方法一樣,都可能存在耗時。
從SharedPreferencesImpl
的構造方法,我們知道會開啟一個新的執行緒,將內容從檔案中讀取到快取的map裡,這個步驟我們叫load。
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時,才能跳出死迴圈 while (!mLoaded) { try { // 呼叫wait後,會釋放mLock鎖,並且進入等待池,等待load完之後的喚醒 mLock.wait(); } catch (InterruptedException unused) { } } }
這個方法,呼叫了mLock.wait()
,釋放了mLock
的物件鎖,並且進入等待池,直到load完被喚醒。
總結:所以,getBoolean
等獲取key
的方法,會等待,直到sp的內容從檔案中copy到快取map裡。很可能存在耗時。
commit()
commit()
方法,會進行同步寫,一定存在耗時,不能直接在主執行緒呼叫。
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() { // 先將更新寫入記憶體快取 final MemoryCommitResult mcr = commitToMemory(); // 建立一個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); } }; // 真正執行sp持久化操作,非同步執行 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 雖然還沒寫入檔案,但是記憶體快取已經更新了,而listener通常都持有相同的sharedPreference物件,所以可以使用記憶體快取中的資料 notifyListeners(mcr); }
可以看到這裡確實是在子執行緒進行的寫入操作,但是為什麼說apply
也會引起ANR呢?
因為在Activity
和Service
的一些生命週期方法裡,都會呼叫QueuedWork.waitToFinish()
方法,這個方法會等待所有子執行緒寫入完成,才會繼續進行。主執行緒等子執行緒,很容易產生ANR問題。
public static void waitToFinish() { Runnable toFinish; //等待所有的任務執行完成 while ((toFinish = sPendingWorkFinishers.poll()) != null) { toFinish.run(); } }
Android 8.0 在這裡做了一些優化,但還是需要等寫入完成,無法完成解決ANR的問題。
綜上所述,SharedPreference
可能在以下幾種情況下產生卡頓,從而引起ANR:
SharedPreference
時,呼叫getPreferenceDir
,可能存在建立目錄的行為getBoolean
等方法,會等待直到SharedPreference
將檔案中的鍵值對全部讀取到快取裡,才會返回commit
方法直接同步寫,如果不小心在主執行緒呼叫,會引起卡頓apply
方法雖然是在非同步執行緒寫入,但是由於Activity
和Service
的生命週期會等待所有SharedPreference
的寫入完成,所以可能引起卡頓和ANR問題SharedPreference
從設計之初,就是為了儲存少量key-value
對,而存在的。其本身的設計,就存在很多缺陷。在儲存特別少量資料的時候,效能瓶頸還不顯著。但是現在很多開發同學在使用的時候,會往裡面存一些大型的JSON
字串等,導致它的缺點被明顯暴露出來。建議在使用SharedPreference
的時候,只用於儲存少量資料,不要存大的字串。
當然,我們也有一些方法來統一優化SharedPreference
,減少ANR的發生,下一篇我們繼續講。
以上就是SharedPreference引發ANR原理詳解的詳細內容,更多關於SharedPreference引發ANR的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45