1. 程式人生 > >Android輕量級儲存原始碼分析

Android輕量級儲存原始碼分析

Android提供了輕量級的資料儲存方式,那就是SharePreference資料儲存。其實質也就是檔案儲存,只不過是符合XML標準的檔案儲存而已,是Android中比較常用的簡易型資料儲存解決方案。下面簡單分析下原始碼實現,原始碼都是一些獨立的東西,實現也比較符合大家的編碼習慣,相比其他模組原始碼比較簡單一些

留個小問題:
1,這樣儲存基本型別資料有問題嗎?

SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
sp.edit().putString("name", "小張");
sp.edit().putInt("age", 11);
sp.edit().commit();

2,這樣儲存基本基本型別資料有什麼不好的地方?

SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.editor();
editor.putString("name", "小張");
editor.commit();
//SharedPreferences.Editor editor = sp.editor();
editor.putInt("age", 11);
editor.commit();

先從獲取SharedPreference得入口getSharedPreferences(String name, int mode)開始

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
(mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //欄位儲存在xml檔案中,而xml檔案是放在ArrayMap<String, File>中
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

原來我們的sp物件放在一個ArrayMap< String, File> mSharedPrefsPaths,看來name--File進行了關聯,看下這個getSharedPreferencesPath(String name)到底幹了啥


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

原來這貨就是建立xml檔案,結合getSharedPreferences(String name, int mode),那麼可以理解在第一次儲存資料放在某個xml時候,如果沒有xml檔案,去建立這個名稱的xml檔案,那麼我們繼續往下看如何獲取SharedPreference物件

   @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            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;
    }

要看sp物件盡然還要進一層,為啥這麼艱難,nm

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

原來,這傢伙是把File物件和SharedPreferencesImpl關聯到一塊了,存在一個ArrayMap中,那麼我們小結一下,我們命名一個fileName的xml檔案,那麼會生成一個File,所以有了fileName-File物件關聯在一塊,緊接著,File--SharedPreferencesImpl關聯在一塊,那麼我猜測最終所有的儲存邏輯是在SharedPreferencesImpl這個實現類中實現,請繼續看

  public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

這個是Editor是SharedPreferencesImpl的內部類,是一個介面,0.001s看下暴露的方法

//Editor編輯介面提供的方法概覽
  public interface Editor {
        Editor putString(String key, String value);
        Editor putStringSet(String key, Set<String> values);
        Editor putInt(String key, int value);
        Editor putLong(String key, long value);
        Editor putFloat(String key, float value);
        Editor putBoolean(String key, boolean value);
        Editor remove(String key);
        Editor clear();
        boolean commit();
        void apply();
    }
   public final class EditorImpl implements Editor {
        //mModified對的是一個String--Object的Map,其實就是儲存所有put過來的key--value,放在記憶體中,記憶體中,記憶體中(重要事情說三遍)
        //突然間我發現getXXX時候可以從mModified裡面取,不用讀取檔案了,那麼每一回儲存檔案都會呼叫sp.editor(),進而每次都會new EditorImpl(),所以有很多個 mModified這樣的map,有這麼一瞬間我覺得很坑爹,這裡問題留在這
        private final Map<String, Object> mModified = Maps.newHashMap();

        private boolean mClear = false;
        
        //舉例分析儲存boolean型別,其他的意淫一下
        public Editor putBoolean(String key, boolean value) {
            synchronized (mLock) {
            //來一個我就存一個,來倆我存一對,存的越多,賺的越多
                mModified.put(key, value);
                return this;
            }
        }
        
        public Editor remove(String key) {
            synchronized (mLock) {
                mModified.put(key, this);
                return this;
            }
        }
        
        public Editor clear() {
            synchronized (mLock) {
                mClear = true;
                return this;
            }
        }
        
        //你們要的commit在這
        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;
            } finally {
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
        
        //寫記憶體的這貨在這,看他幹啥了
        private MemoryCommitResult commitToMemory() {
            //從這貨名字可以猜處它最後應該是要寫到記憶體裡
            Map<String, Object> mapToWriteToDisk;
          //這又出來個mMap,好亂,猜一下m開頭,成員變數,沒錯,這個就是你以後每次getXXX(String key)時候要從記憶體中操作的map,你app所有儲存的東西都在這個成員變數裡
            mapToWriteToDisk = mMap;//結果賦給這個馬甲map,這個馬甲map待會肯定要去變成xml檔案內容存到手機裡去
                synchronized (mLock) {
                    boolean changesMade = false;
                    //mClear好眼熟,成員變數,還記得clear()方法裡對mClear進行賦值嗎?.....原來clear的操作在這終結
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            changesMade = true;
                            mMap.clear();
                        }
                        mClear = false;
                    }
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        //map.remove應該是你呼叫了editor.remove()方法以後要進行最終結果map的remove,這裡為啥是value == this就給remove了呢?
                        if (v == this || v == null) {
                            if (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                            if (mMap.containsKey(k)) {
                                Object existingValue = mMap.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            //大胸弟我終於看到我曾經跨過山河大海put的值,你現在終於放進這個mMap裡了
                            mMap.put(k, v);
                        }
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }
                    //本次commit臨時儲存用的map可以皈依佛門了
                    mModified.clear();
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            //我想要的東西都很貴,我想要去的地方都很遠,不造一個包裝資料結構的類怎麼把他們打包帶走
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }
        
        //小姐姐長啥樣,一睹芳容
    private static class MemoryCommitResult {
        final long memoryStateGeneration;
        @Nullable final List<String> keysModified;
        @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
        final Map<String, Object> mapToWriteToDisk;
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

        @GuardedBy("mWritingToDiskLock")
        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
                @Nullable Set<OnSharedPreferenceChangeListener> listeners,
                Map<String, Object> mapToWriteToDisk) {
            this.memoryStateGeneration = memoryStateGeneration;
            this.keysModified = keysModified;
            this.listeners = listeners;
            this.mapToWriteToDisk = mapToWriteToDisk;
        }

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
    }
    
    //原來就放了幾個成員變數,盛放下打包的物品,太醜陋了,這不是去小姐姐家的路
    
    //存完記憶體中的map以後,俺們看看如何存到disk裡
     private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
     //原來這貨根據runnabl == null就是comit,不是就是apply模式
        final boolean isFromSyncCommit=(postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                    //最終寫檔案到disk的操作在這
                    writeToFile(mcr,isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
       
       //確認過眼神,這是commit模式
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
            //commit模式下,當前只有一個寫入任務,直接在當前執行緒write
                writeToDiskRunnable.run();
                return;
            }
        }
        //apply模式 || 當前是commit模式,但是有很多個寫入任務,原來這麼坑爹commit在任務多的時候也會apply,為啥嘞?要是胡寫亂呼叫commit,阻塞了UI執行緒那不就掛了(這是我猜測)
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    
    public void apply() {
    //和commit一樣,三七二十一先給它在記憶體裡的mMap操作完再說,再處理打包回來的馬甲map
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
 //這個包裝的資料結構的writtenToDiskLatch到底是啥勒,他是一個標誌位,表示當前mac這個檔案是否寫入到disk完成,如果完成在繼續下一個mc資料結構寫入,那麼這貨到底是在什麼時候賦值的呢?                         mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

     //這個上面commit已經分析過,runnable !=null,新執行緒寫檔案
     SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }
        
 到這裡我們知道了為啥apply和commit一個非同步一個同步了,以及我們getXX是從mMap這個成員變數中獲取的了,那麼萬水千山還有一步,說了半天到底怎麼從把mMap的馬甲Map這個mWriteToDiskMap,所在的包裝類MemoryCommitResult,這個破玩意如何變成xml,再把xml寫入到disk裡呢?最後一個機關來了
 
 //我靠好長啊
  private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        //mFile是個成員變數,是當初傳過來的File檔案,判空是因為有地方會有mFile.delete()操作
        boolean fileExists = mFile.exists();
        if (fileExists) {
            boolean needsWrite = false;
            //mDiskStateGeneration是一個標誌位,在構造資料包裝結構MemoryCommitResult的時候賦值過
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                      if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
            boolean backupFileExists = mBackupFile.exists();
            if (!backupFileExists) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            //把mMap的馬甲Map結果集寫入到mFile檔案,mFile是啥,waht?,當初你在ContextImpl裡getSharedPreference(String name,Mode mode)傳入一個name,生成一個mFile,然後new SharedPreferenceImpl(File file,Mode mode),這個mFile就來了,如今將結果map寫入到mFile裡,木毛病
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
                //將xml檔案最終寫入到disk裡
            FileUtils.sync(str);
              str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
                try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtime;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
            mBackupFile.delete();
            mDiskStateGeneration = mcr.memoryStateGeneration;
            mcr.setDiskWriteResult(true, true);            mSyncTimes.add(Long.valueOf(fsyncDuration).intValue());
            mNumSync++;
            return;
        } catch (XmlPullParserException e) { 
        } catch (IOException e) {
        }

        if (mFile.exists()) {
            if (!mFile.delete()) {
            }
        }
        mcr.setDiskWriteResult(false, false);
    }



小結

1,SharedPreference儲存資料的形式是xml檔案,並且建立時不同的name對應不同的xml檔案,本質是檔案讀寫。

2,在SharedPreferences的Editor中如果用commit()方法提交資料,其過程是先把資料更新到記憶體,然後在當前執行緒中寫檔案操作,如果用的是apply()方法提交資料,首先也是寫到記憶體,接著在一個新執行緒中非同步寫檔案,注意:操作commit時有鎖操作,所以效率很低一些,如果當我們一次有多個修改寫操作時等都批量put完了再一次提交確認,這樣可以提高效率

3,SharedPreferences在例項化時首先會從disk非同步讀檔案,然後快取在記憶體中,接下來的讀操作都是記憶體快取操作而不是檔案操作。

4,由於鍵值對在記憶體中儲存了一份,放在mMap中,用記憶體換取getXXX(String key)時候速度上的提高,所以如果儲存大量資料在裡面,那麼這個map將會很大,記憶體上存在問題,所以回到題目上來,輕量級儲存,不適合大而複雜的資料儲存