SharedPreferences源碼分析

分析達成目標

  • 瞭解基本實現
  • SharePreferences是否線程安全
  • SharePreferences的mode參數是什麼
  • 瞭解apply與commit的區別
  • 致使ANR的緣由
  • Android8.0作了什麼優化

基本實現

簡單使用

先從如何簡單使用開始java

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方法的接口,結構是這樣的android

//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用於把數據緩存到內存app

//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會優先從內存中拿數據ide

Editor.putInt

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

putInt只是存入了mModified中,並無進行其它操做post

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更像是一個等待任務的集合,其內部含有兩個列表ui

//寫入磁盤任務會存入這個列表中,在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的優化

相關文章
相關標籤/搜索