SharedPreferences 源碼分析

一,如何使用?

先從簡(jiǎn)單的使用示例開始

寫入數(shù)據(jù)

SharedPreferences sharedPreferences = this.getSharedPreferences("settings", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("isOpen",  true);
editor.putString("name",  "coder");
editor.putInt("number",  10);
editor.commit();

讀取數(shù)據(jù)

boolean isOpen = sharedPreferences.getBoolean("isOpen", false);
String name = sharedPreferences.getString("name", "");
int years = sharedPreferences.getInt("years", 0);

使用很簡(jiǎn)單螟够,接下來我們一句一句的來分析源碼實(shí)現(xiàn)。

二, 源碼分析

1. 獲取 SharedPreferences

/frameworks/base/core/java/android/content/ContextWrapper.java

 @Override
  public SharedPreferences getSharedPreferences(String name, int mode) {
      return mBase.getSharedPreferences(name, mode);
  }

mode可以選以下四種妓笙,:
MODE_PRIVATE 默認(rèn)模式若河,創(chuàng)建的文件只能在應(yīng)用內(nèi)訪問(或者共享相同userID的所有應(yīng)用)
MODE_WORLD_READABLE(過時(shí))允許其他應(yīng)用訪問本應(yīng)用的文件,使用此模式會(huì)拋出異常
MODE_WORLD_WRITEABLE(過時(shí))允許其他應(yīng)用寫本應(yīng)用的文件寞宫,使用此模式會(huì)拋出異常
MODE_MULTI_PROCESS(過時(shí))官方提示這種模式在某些版本無法可靠運(yùn)行萧福,并且未來也不會(huì)支持多進(jìn)程

但目前除了第一種其他的都不建議使用,并且從 Android N 開始MODE_WORLD_READABLE辈赋, MODE_WORLD_WRITEABLE會(huì)直接拋出異常鲫忍,后面我們?cè)诜治鲈创a的時(shí)候會(huì)詳細(xì)說明這塊;

上面 mBase 是 Context 類型的實(shí)例钥屈,Context是一個(gè)抽象類悟民,它有兩個(gè)直接子類,一個(gè)是ContextWrapper篷就,一個(gè)是ContextImpl射亏,ContextWrapper 是上下文功能的封裝類,而ContextImpl則是上下文功能的實(shí)現(xiàn)類竭业, Activity智润,Service, Application都是繼承自ContextWrapper的永品,因此這里的mBase其實(shí)最終指向的就是 ContextImpl做鹰;

ContextImpl 是何時(shí)創(chuàng)建的?

ContextImpl 是主線程ActivityThread 的成員變量鼎姐,ActivityThread 是管理應(yīng)用進(jìn)程的主線程的執(zhí)行钾麸,ActivityThread 是在App冷啟動(dòng)main(String[] args)中初始化的,說明ActivityThread只有一個(gè)炕桨,從而對(duì)應(yīng)一個(gè)ContextImpl 饭尝,分析這個(gè)有利于我們接下來分析SharedPreferences 的一些代碼;

public ContextImpl getSystemContext() {
    synchronized (this) {
        if (mSystemContext == null) {
            mSystemContext = ContextImpl.createSystemContext(this);
        }
        return mSystemContext;
    }
}
static ContextImpl createSystemContext(ActivityThread mainThread) {
        LoadedApk packageInfo = new LoadedApk(mainThread);
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null, null);
        context.setResources(packageInfo.getResources());
        context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
                context.mResourcesManager.getDisplayMetrics());
        return context;
    }

/frameworks/base/core/java/android/app/ContextImpl.java

 @Override
 public SharedPreferences getSharedPreferences(String name, int mode) {
     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);
 }

name 傳 null 是被允許的献宫,它會(huì)生成一個(gè)以null.xml為名的文件, 我用targetSdkVersion為29钥平,在Android 10的機(jī)器上也驗(yàn)證了確實(shí)是可以的:
/data/user/0/com.cjl.loadingview/shared_prefs/null.xml

我們知道一個(gè)App可以對(duì)SharedPreferences設(shè)置不同name,這樣最終也就對(duì)應(yīng)著不同的xml文件姊途,mSharedPrefsPaths 是一個(gè)map涉瘾,它就是用來保存不同name的文件的;如果mSharedPrefsPaths里沒有該name對(duì)應(yīng)的文件捷兰,那么就 通過getSharedPreferencesPath(name)獲取一個(gè),代碼如下:

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}

上述代碼最終會(huì)生成一個(gè)下面路徑的.xml文件:
/data/user/0/com.cjl.loadingview/shared_prefs/settings.xml
/data/user/0 是一個(gè) /data/data 的 link立叛,

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
     SharedPreferencesImpl sp;
     synchronized (ContextImpl.class) {
         final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
         sp = cache.get(file);
         if (sp == null) {
             checkMode(mode);
             if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                 if (isCredentialProtectedStorage() && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                     throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                 }
             }
             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) {
         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;
 }

sSharedPrefsCache 存儲(chǔ)不同packageName 的 SharedPreferencesImpl, 冷啟動(dòng)進(jìn)來SharedPreferencesImpl是為null的贡茅,因此回去新建一個(gè)秘蛇,在新建之前會(huì)檢查最初傳進(jìn)來的mode其做,從如下代碼可以看到Android N 后已強(qiáng)制不能在使用 MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE 赁还。
如果sp不為null妖泄,mode 是 多進(jìn)程模式 MODE_MULTI_PROCESS, 此時(shí)需要重新讀取文件艘策;

private void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
         if ((mode & MODE_WORLD_READABLE) != 0) {
             throw new SecurityException("MODE_WORLD_READABLE no longer supported");
         }
         if ((mode & MODE_WORLD_WRITEABLE) != 0) {
             throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
         }
    }
 }

我們來看看SharedPreferencesImpl 到底是什么蹈胡?

final class SharedPreferencesImpl implements SharedPreferences {
···
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
   }

SharedPreferences 是一個(gè)接口類型的,SharedPreferencesImpl 是它的實(shí)現(xiàn)類柬焕,因此SharedPreferencesImpl 才是我們分析的重點(diǎn)审残,這里面有SharedPreferences 各種操作的具體實(shí)現(xiàn):

static File makeBackupFile(File prefsFile) {
     return new File(prefsFile.getPath() + ".bak");
}

創(chuàng)建.bak備份文件,接下來會(huì)開啟一個(gè)名為 SharedPreferencesImpl-load 的子線程從磁盤讀取文件斑举,并且讀取到文件后立即將其轉(zhuǎn)換成了map文件保存在內(nèi)存中搅轿,為什么轉(zhuǎn)換成map保存在內(nèi)存中呢,這里留一個(gè)伏筆富玷,看到后面你自然會(huì)明白璧坟;

private void startLoadFromDisk() {
     synchronized (mLock) {
         mLoaded = false;
     }
     new Thread("SharedPreferencesImpl-load") {
         public void run() {
             loadFromDisk();
         }
    }.start();
}

private void loadFromDisk() {
     synchronized (mLock) {
         if (mLoaded) {
             return;
         }
         if (mBackupFile.exists()) {
             mFile.delete();
             mBackupFile.renameTo(mFile);
         }
     }

     Map<String, Object> map = null;
     StructStat stat = null;
 
      stat = Os.stat(mFile.getPath());
      if (mFile.canRead()) {
           BufferedInputStream str = null;
           str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
           map = (Map<String, Object>) XmlUtils.readMapXml(str);
      }
      
     synchronized (mLock) {
         mLoaded = true;
         if (map != null) {
              mMap = map;
              mStatTimestamp = stat.st_mtim;
              mStatSize = stat.st_size;
          } else {
              mMap = new HashMap<>();
         }  
    }
}

mLoaded 這個(gè)成員變量得留意一下,在首次讀取完磁盤文件后赎懦,下次調(diào)用getSharedPreferences就不會(huì)再從磁盤讀取了雀鹃;

2. 寫入數(shù)據(jù)

SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name",  "coder");
editor.commit();
@Override
public Editor edit() {
     synchronized (mLock) {
         awaitLoadedLocked();
     }
     return new EditorImpl();
}

private void awaitLoadedLocked() {
    ...
    while (!mLoaded) {
         try {
             mLock.wait();
         } catch (InterruptedException unused) {}
    }
    ...
}

寫入數(shù)據(jù)需要使用Editor 實(shí)例,Editor 是一個(gè)接口励两,它的實(shí)現(xiàn)類是EditorImpl黎茎,在獲取Editor 之前如果mLock鎖沒有被釋放則會(huì)處于等待狀態(tài), 等待什么呢,從上面分析我們不難看出其實(shí)是在等待getSharedPreferences時(shí)從磁盤中讀取文件当悔,如果文件都沒有讀取完成傅瞻,我們拿到Editor 去寫數(shù)據(jù)肯定是不行的,加載成功后的 notifyAll 要結(jié)合 awaitLoadedLocked 來分析盲憎。在準(zhǔn)備讀嗅骄、寫 SP 的時(shí)候,都會(huì)先調(diào)用 awaitLoadedLocked 等待 loadFromDisk loadFromDisk 饼疙,在讀取磁盤文件結(jié)束后會(huì)調(diào)用mLock.notifyAll()喚醒這些等待數(shù)據(jù)加載完成的線程溺森,接下來我們就可以去獲取EditorImpl去寫文件了

public final class EditorImpl implements Editor {
     private final Object mEditorLock = new Object();
     private final Map<String, Object> mModified = new HashMap<>();
     private boolean mClear = false;

     @Override
     public Editor putString(String key, @Nullable String value) {
          synchronized (mEditorLock) {
               mModified.put(key, value);
               return this;
          }
     }

     @Override
     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;
     }

     @Override
     public void apply() {
         final long startTime = System.currentTimeMillis();
         final MemoryCommitResult mcr = commitToMemory();
         final Runnable awaitCommit = new Runnable() {
                @Override
                 public void run() {
                     mcr.writtenToDiskLatch.await();
                 }
         };
        QueuedWork.addFinisher(awaitCommit);
        Runnable postWriteRunnable = new Runnable() {
               @Override
                public void run() {
                      awaitCommit.run();
                      QueuedWork.removeFinisher(awaitCommit);
                }
         };
         SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
         notifyListeners(mcr);
    }
     ...
}

寫入操作大致可分為兩步完成:
第一步是用EditorImpl直接更改內(nèi)存mMap的值;
第二步是將內(nèi)存mMap中的鍵值對(duì)寫入到磁盤文件中窑眯;

我們putString為例來分析下寫文件的操作屏积,在調(diào)用完putString之后我們必須要調(diào)用commit()或者apply()去保存數(shù)據(jù);
這兩個(gè)方法都會(huì)調(diào)用commitToMemory()將數(shù)據(jù)寫入內(nèi)存map磅甩,接著都會(huì)add一個(gè)寫入文件的任務(wù)炊林,等待后續(xù)系統(tǒng)執(zhí)行

commit()或者apply()不同的地方在于:
apply將文件寫入操作放到一個(gè)Runnable對(duì)象中,等待系統(tǒng)在子線程中調(diào)用更胖, 此時(shí)不會(huì)阻礙主線程;
commit 是直接在主線程中同步進(jìn)行寫入操作, 因此使用commit是會(huì)阻塞主線程的却妨,這點(diǎn)得注意饵逐;
關(guān)于上述不同點(diǎn)可以詳細(xì)追一下enqueueDiskWrite()方法, 如果是apply()方法會(huì)使用writeToDiskRunnable , commit會(huì)在主線程寫入

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    final Runnable writeToDiskRunnable = new Runnable() {
           @Override
           public void run() {
                synchronized (mWritingToDiskLock) {
                     writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                     mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                     postWriteRunnable.run();
                }
            }
     };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
          boolean wasEmpty = false;
          synchronized (mLock) {
              wasEmpty = mDiskWritesInFlight == 1;
          }
          if (wasEmpty) {
              writeToDiskRunnable.run();
              return;
          }
        }
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

3. 數(shù)據(jù)的讀取

@Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末彪标,一起剝皮案震驚了整個(gè)濱河市倍权,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捞烟,老刑警劉巖薄声,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異题画,居然都是意外死亡默辨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門苍息,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缩幸,“玉大人,你說我怎么就攤上這事竞思”硪辏” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵盖喷,是天一觀的道長(zhǎng)爆办。 經(jīng)常有香客問我,道長(zhǎng)课梳,這世上最難降的妖魔是什么距辆? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮惦界,結(jié)果婚禮上挑格,老公的妹妹穿的比我還像新娘。我一直安慰自己沾歪,他們只是感情好漂彤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著灾搏,像睡著了一般挫望。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狂窑,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天媳板,我揣著相機(jī)與錄音,去河邊找鬼泉哈。 笑死蛉幸,一個(gè)胖子當(dāng)著我的面吹牛破讨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奕纫,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼提陶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了匹层?” 一聲冷哼從身側(cè)響起隙笆,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎升筏,沒想到半個(gè)月后撑柔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡您访,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年铅忿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洋只。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辆沦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出识虚,到底是詐尸還是另有隱情肢扯,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布担锤,位于F島的核電站蔚晨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏肛循。R本人自食惡果不足惜铭腕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望多糠。 院中可真熱鬧累舷,春花似錦、人聲如沸夹孔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搭伤。三九已至只怎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怜俐,已是汗流浹背身堡。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拍鲤,地道東北人贴谎。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓汞扎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親擅这。 傳聞我的和親對(duì)象是個(gè)殘疾皇子佩捞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354