SharedPreferences原始碼分析

王買山發表於2020-10-30

分析達成目標

  • 瞭解基本實現
  • SharePreferences是否執行緒安全
  • SharePreferences的mode引數是什麼
  • 瞭解apply與commit的區別
  • 導致ANR的原因
  • Android8.0做了什麼優化

基本實現

簡單使用

先從如何簡單使用開始

val sp = context.getSharedPreferences("123", Context.MODE_PRIVATE)
//通過SharedPreferences讀值
val myValue = sp.getInt("myKey",-1)
//通過SharedPreferences.Editor寫值
sp.edit().putInt("myKey",1).apply()

SharedPreferences物件從哪裡來

SharedPreferences只是一個有各種get方法的介面,結構是這樣的

//SharedPreferences.java
public interface SharedPreferences {
    int getInt(String key, int defValue);
    Map<String, ?> getAll();
    
    public interface Editor {
        Editor putString(String key, @Nullable String value);
        Editor putInt(String key, int value);
    }
}

那麼它從哪裡來,我們得到context具體實現類ContextImpl裡去找,以下程式碼都會省略不必要的部分

//ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    //可以看到返回的SharedPreferences其實就是一個SharedPreferencesImpl例項
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        //每個File都對應著一個SharedPreferencesImpl例項
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    return sp;
}

從上面可以看出

  1. SharedPreferences真正實現是SharedPreferencesImpl
  2. 對於同一個程式來說,SharedPreferencesImpl和同一個檔案是一一對應的

SharedPreferencesImpl

內部儲存了一個Map用於把資料快取到記憶體

//SharedPreferencesImpl.java
@GuardedBy("mLock")//操作時通過mLock物件鎖保證執行緒安全
Map<String, Object> mMap

對於同一個SharedParences.Editor來說,每個Editor也包含了一個map用來儲存本次改變的資料

//SharedPreferencesImpl.java
@GuardedBy("mEditorLock")//操作時通過mEditorLock物件鎖保證執行緒安全
Map<String, Object> mModified

getInt

//SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        //如果正在從xml檔案中同步map到記憶體,則會阻塞等待同步完成
        awaitLoadedLocked();
        //直接從記憶體mMap中拿資料
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

從上面程式碼可以看出,SharedPreferences會優先從記憶體中拿資料

Editor.putInt

public Editor putInt(String key, int value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

putInt只是存入了mModified中,並沒有進行其它操作

Editor.apply

//SharedPreferencesImpl.java
public void apply() {
    //1. 遍歷mModified
    //2. 合併修改到mMap中 
    //3. 當前memory的代數 mCurrentMemoryStateGeneration++
    //以此完成記憶體的實現。返回的MemoryCommitResult用於之後的xml檔案寫入
    final MemoryCommitResult mcr = commitToMemory();
    
    //這裡是一個純粹等待xml寫入用的任務
    //writtenToDiskLatch只在本次Editor的修改完全寫入到檔案後釋放
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    //把上面的任務加入到QueuedWork的finisher列表中
    //ActivityThread在呼叫Activity的onPause、onStop,或者Service的onStop之前都會呼叫QueuedWork的waitToFinish
    //waitToFinish方法則會輪流遍歷執行它們的run方法,即在主執行緒觸發await
    QueuedWork.addFinisher(awaitCommit);

    //在上一個等待任務外面再封裝一層等待任務,用於在寫入檔案完成後從QueuedWork裡移除finish
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                //若成功完成,則從QueuedWork裡移除該finisher
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    //把寫入磁碟的任務提交去執行,commit就不會帶第二個引數,後面會說這裡
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    //寫入記憶體就直接觸發回撥監聽
    notifyListeners(mcr);
}

Editor.commit

//SharedPreferencesImpl.java
@Override
public boolean commit() {
    //與apply相同,直接寫入記憶體
    MemoryCommitResult mcr = commitToMemory();

    //直接提交disk任務給執行緒進行處理,第二個引數為空,表示自己是同步的
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    //注意這裡是與apply的不同,直接自己觸發await,不再放到Runnable裡
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

SharedPreferencesImpl.this.enqueueDiskWrite

執行寫入磁碟的任務

//SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
    //通過第二個引數來判斷是apply還是commit,即是否是同步提交
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    //這個runnable就是寫入磁碟的任務
    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //關鍵方法:寫入磁碟
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //這個值在寫入一次記憶體後+1,寫入一次磁碟後-1,表示當前正在等待寫入磁碟的任務個數
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //與QueuedWork的waitToFinish不同,這裡是在子執行緒等待寫入磁碟任務的完成
                    postWriteRunnable.run();
                }
            }
        };

    // 下面的條件我判斷只有當前是最後一次commit任務才會到當前執行緒執行
    // 而commit正常情況下是同步進行的,因此只要之前的apply任務未執行完成,也會改為非同步執行
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            //此次同步任務為當前所有任務的最後一次
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //直接在當前執行緒執行寫入xml操作
            writeToDiskRunnable.run();
            return;
        }
    }

    //這裡是把寫入xml檔案的任務放到QueuedWork的子執行緒去執行。
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    //8.0之前則是直接用單執行緒池去執行
    //QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

QueuedWork

QueuedWork更像是一個等待任務的集合,其內部含有兩個列表

//寫入磁碟任務會存入這個列表中,在8.0之前沒有這個列表,只有一個SingleThreadExecutor執行緒池用來執行xml寫入任務
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//等待任務會存入這個列表中
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//插入一個磁碟寫入的任務,會放到QueuedWork裡的一個HandlerThread去執行
public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            //如果是apply則100ms後再觸發去遍歷執行等待任務,commit則不延遲
            if (shouldDelay && sCanDelay) {
                //這裡只需要知道是觸發執行sWork裡的所有任務,即寫入磁碟任務
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

而等待任務列表sFinishers會在waitToFinish方法中使用到,作用是直接去執行所有磁碟任務,執行完成之後再輪流執行所有等待任務

//SharedPreferencesImpl.java
public static void waitToFinish() {
    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // 由於我們會手動執行所有的磁碟任務,所以不再需要這些觸發執行任務的訊息
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        // 執行此方法的過程中若插入了其它任務,都不需要再延遲了,直接去觸發執行
        sCanDelay = false;
    }
    
    //遍歷執行當前的所有等待硬碟任務的run方法
    processPendingWork();

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        //所有任務執行完成之後,道路通暢了,這次waitToFinish執行通過,可以繼續延遲100ms
        sCanDelay = true;
    }
}

以下是Android8.0之前的waitToFinish,只是遍歷執行所有等待任務,也不會去主動寫入xml,從而導致ANR出現


public static void waitToFinish() {
    Runnable toFinish;
    //只是去輪流執行所有等待任務
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}

mode許可權

我們會通過context獲取SharedPreferences物件時傳入mode

context.getSharedPreferences("123", Context.MODE_PRIVATE)

該mode會在生成SharedPreferencesImpl例項時傳入

//SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    //...
    mMode = mode;
}

在xml檔案寫入完成後呼叫

//SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    //寫入檔案
    FileOutputStream str = createFileOutputStream(mFile);
    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    str.close();
    
    //給檔案加許可權
    ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
}

加許可權的過程就終相當於我們在串列埠使用chmod給許可權

//ConetxtImpl.java
static void setFilePermissionsFromMode(String name, int mode,
        int extraPermissions) {
    //預設給了同一使用者與同一群組的讀寫許可權
    int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
        |FileUtils.S_IRGRP|FileUtils.S_IWGRP
        |extraPermissions;
    if ((mode&MODE_WORLD_READABLE) != 0) {
        //其它使用者讀許可權
        perms |= FileUtils.S_IROTH;
    }
    if ((mode&MODE_WORLD_WRITEABLE) != 0) {
        //其它使用者寫許可權
        perms |= FileUtils.S_IWOTH;
    }
    FileUtils.setPermissions(name, perms, -1, -1);
}

//FileUtils.java
public static int setPermissions(String path, int mode, int uid, int gid) {
    Os.chmod(path, mode);
    return 0;
}

總結

基本實現

SharedPreference有一個記憶體快取mMap,以及一個硬碟快取xml檔案。每次通過apply或者commit提交一次editor修改,都會先合入mMap即記憶體中,之後再快取到硬碟。注意提交會觸發整個檔案的修改,因此多個修改最好放在同一個Editor物件中。

執行緒安全

SharedPreferences主要通過物件鎖來保證執行緒安全,Editor修改時用的是另一個物件鎖
,寫入disk時也用的是另一個物件鎖。

mode是什麼

mode類似於給通過chmod給xml檔案不同的許可權,從而實現其他應用也可以訪問的效果,預設MODE_PRIVATE給的是所有者和群組的讀寫許可權,而MODE_WORLD_READABLE與MODE_WORLD_WRITEABLE分別給了其它使用者的讀寫許可權

apply與commit

commit與apply的不同主要在於:commit直接在自己的執行緒等待寫入硬碟任務的執行,且commit一次就寫一次。而apply不會等待寫入硬碟,且8.0之後會根據當前最新的記憶體代數來過濾掉之前的所有記憶體修改,只儲存最後一次記憶體修改。

導致ANR的原因

apply提交時會生成一個等待任務放到QueuedWork的一個等待列表裡,在Activity的pause、Stop,或者Service的stop執行時,會依次呼叫這個等待列表的任務,保證每個等待列表所等待的任務都可以執行。若未執行完畢則會導致ANR

Android8.0做了什麼優化

8.0的優化方案為

  1. 若提交了多個apply,在執行時只會執行最後一次提交,減少了檔案的寫入次數
  2. QueuedWork優化:執行寫入磁碟的任務時,不再直接放到執行緒池執行,而是先放入一個真實任務的List,在waitToFinish呼叫時,會主動執行這些真實任務,再執行所有等待任務。而8.0之前只會執行等待任務,對推動任務執行沒有任何幫助

參考

SharedPreferences ANR問題分析和解決 & Android 8.0的優化

相關文章