美文网首页Android 进阶之旅
Android 进阶学习(十六) SharedPreferenc

Android 进阶学习(十六) SharedPreferenc

作者: Tsm_2020 | 来源:发表于2020-12-07 17:22 被阅读0次

    SharedPreferences 应该是大家经常使用的缓存库,但是他的实现过程想必大多数人肯定是不知道的,今天我们就来看看SharedPreferences 是如何实现文件读写的,

    public interface SharedPreferences {
       public interface OnSharedPreferenceChangeListener {
           void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
       }
       public interface Editor {
           Editor putString(String key, @Nullable String value);
           Editor putStringSet(String key, @Nullable 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();
       }
       Map<String, ?> getAll();
       @Nullable
       String getString(String key, @Nullable String defValue);
       @Nullable
       Set<String> getStringSet(String key, @Nullable Set<String> defValues);
       int getInt(String key, int defValue);
       long getLong(String key, long defValue);
       float getFloat(String key, float defValue);
       boolean getBoolean(String key, boolean defValue);
       boolean contains(String key);
       Editor edit();
       void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
       void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    }
    

    看到上面的代码可以发现SharedPreferences 是一个借口,定义了各种操作, 想要看他的具体怎么实现的,需要看他的实现类SharedPreferencesImpl,不过在看他的实现类之前,先看一下context.getSharedPreferences("tsm",MODE_PRIVATE);,这个方法是怎么实现的,也就是利用context来获取SharedPreferences 是怎么实现的

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

    Activity 中的getSharedPreferences 使用的是contextWrapper的getSharedPreferences,而contextWrapper绑定了一个contextImpl ,实际创建的方法就是调用contextImpl 的getSharedPreferences 方法,说到这里大家可能非常乱,contextWrapper 与 contextImpl 他们是什么东西,又与activity 之间有什么关系,

    ContextImpl

    先来看一下contextImpl 与context 的关系

    /**
    * Common implementation of Context API, which provides the base
    * context object for Activity and other application components.
    */
    class ContextImpl extends Context {
    }
    

    ContextImpl 继承了Context ,并将它的方法一一都实现了

    ContextWrapper

    /**
    * Proxying implementation of Context that simply delegates all of its calls to
    * another Context.  Can be subclassed to modify behavior without changing
    * the original Context.
    */
    public class ContextWrapper extends Context {
       @UnsupportedAppUsage
       Context mBase;
    
       public ContextWrapper(Context base) {
           mBase = base;
       }
       /**
        * @return the base context as set by the constructor or setBaseContext
        */
       public Context getBaseContext() {
           return mBase;
       }
    
       @Override
       public AssetManager getAssets() {
           return mBase.getAssets();
       }
    
       @Override
       public Resources getResources() {
           return mBase.getResources();
       }
    
    }
    

    ContextWrapper 同样是继承了Context ,但是他是一个代理类,所有的功能实现都是通过它所代理的context 来实现的,

    Activity

    再来看一下Activity的 结构
    public class Activity extends ContextThemeWrapper{
    }
    Activity 继承了ContextThemeWrapper ,来实现了 所有ContextWrapper 的功能,也就是说,Activity 通过持有了ContextWrapper 间接的持有了ContextImpl ,而具体的操作是通过ContextImpl 来完成的,我们只需要知道 ContextImpl 是如何与Activity之间关联的就能大概了解这个过程了,
    Activity 的启动最终创建Activity 的地方是ActivityThread 的performLaunchActivity 这个方法,看一下他在这个方法中做了一些什么

    ActivityThread

     private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
          ....省略部分代码......
           ContextImpl appContext = createBaseContextForActivity(r);
           Activity activity = null;
           appContext.setOuterContext(activity);
           activity.attach(appContext, this, getInstrumentation(), r.token,
                           r.ident, app, r.intent, r.activityInfo, title, r.parent,
                           r.embeddedID, r.lastNonConfigurationInstances, config,
                           r.referrer, r.voiceInteractor, window, r.configCallback,
                           r.assistToken);
       ....省略部分代码......
       }
    

    看上面的代码,我只贴了一些关键逻辑部分,那就是先创建了一个ContextImpl ,与一个Activity, 将Activity attach 了ContextImpl ,同时将activity 通过setOuterContext 给与了ContextImpl ,让他们之间建立了关系
    看到这里已经知道了activity中getSharedPreferences到底是在哪里实现的了,接下来去一下他的实现方法,

    ContextImpl.getSharedPreferences(String name,int mode)

    private ArrayMap<String, File> mSharedPrefsPaths;
       @Override
       public SharedPreferences getSharedPreferences(String name, int mode) {
           // At least one application in the world actually passes in a null
           // name.  This happened to work because when we generated the file name
           // we would stringify it to "null.xml".  Nice.
           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,找到File,如果没有就创建file,将它放入到mSharedPrefsPaths中,

    ContextImpl.getSharedPreferences(File file,int mode)

    @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) {
               // If somebody else (some other process) changed the prefs
               // file behind our back, we reload it.  This has been the
               // historical (if undocumented) behavior.
               sp.startReloadIfChangedUnexpectedly();
           }
           return sp;
       }
    
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
       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;
       }
    
    

    这段方法看起来平平无奇,但是仔细看一下还是挺有意思的,为什么这么说,我们先来说一个问题,那就是 ActivityA 与 ActivityB 同时使用
    getSharedPreferences("tsm",MODE_PRIVATE)这个方法来获取 名字是tsm 的SharedPreferences,那么他们是否访问的是同一个文件呢,答案肯定是同一个文件,但是为什么呢,这就需要好好研究一下上面的getSharedPreferencesCacheLocked 的代码,
    在 getSharedPreferencesCacheLocked 中访问的是sSharedPrefsCache ,而sSharedPrefsCache是一个静态变量,所以他是全局共享的,也就是同一个名字只有一个,

    到这里创建SharedPreferences的过程就已经结束了,接下来继续分析他是如何将数据加载到内存的,已经在内存中的存储形式

    SharedPreferencesImpl 初始化方法

       SharedPreferencesImpl(File file, int mode) {
           mFile = file;
           mBackupFile = makeBackupFile(file);
           mMode = mode;
           mLoaded = false;
           mMap = null;
           mThrowable = null;
           startLoadFromDisk();
       }
    
       @UnsupportedAppUsage
       private void startLoadFromDisk() {
           synchronized (mLock) {
               mLoaded = false;
           }
           new Thread("SharedPreferencesImpl-load") {
               public void run() {
                   loadFromDisk();
               }
           }.start();
       }
    

    在创建SharedPreferencesImpl 的时候就会开启一个线程,在线程中 调用startLoadFromDisk这个方法

    SharedPreferencesImpl.startLoadFromDisk

    private void loadFromDisk() {
           synchronized (mLock) {
               if (mLoaded) {
                   return;
               }
               if (mBackupFile.exists()) {
                   mFile.delete();
                   mBackupFile.renameTo(mFile);
               }
           }
    
           // Debugging
           if (mFile.exists() && !mFile.canRead()) {
               Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
           }
    
           Map<String, Object> map = null;
           StructStat stat = null;
           Throwable thrown = null;
           try {
               stat = Os.stat(mFile.getPath());
               if (mFile.canRead()) {
                   BufferedInputStream str = null;
                   try {
                       str = new BufferedInputStream(
                               new FileInputStream(mFile), 16 * 1024);
                       map = (Map<String, Object>) XmlUtils.readMapXml(str);
                   } catch (Exception e) {
                       Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                   } finally {
                       IoUtils.closeQuietly(str);
                   }
               }
           } catch (ErrnoException e) {
               // An errno exception means the stat failed. Treat as empty/non-existing by
               // ignoring.
           } catch (Throwable t) {
               thrown = t;
           }
    
           synchronized (mLock) {
               mLoaded = true;
               mThrowable = thrown;
    
               // It's important that we always signal waiters, even if we'll make
               // them fail with an exception. The try-finally is pretty wide, but
               // better safe than sorry.
               try {
                   if (thrown == null) {
                       if (map != null) {
                           mMap = map;
                           mStatTimestamp = stat.st_mtim;
                           mStatSize = stat.st_size;
                       } else {
                           mMap = new HashMap<>();
                       }
                   }
                   // In case of a thrown exception, we retain the old map. That allows
                   // any open editors to commit and store updates.
               } catch (Throwable t) {
                   mThrowable = t;
               } finally {
                   mLock.notifyAll();
               }
           }
       }
    

    可以看到这里就是利用xml解析文件,最后使用notifyAll唤醒其他处于等待的线程,到这里从文件中间数据加载到内存就已经结束了,

    SharedPreferencesImpl.getString

    继续看一下get方法

       @Override
       @Nullable
       public String getString(String key, @Nullable String defValue) {
           synchronized (mLock) {
               awaitLoadedLocked();
               String v = (String)mMap.get(key);
               return v != null ? v : defValue;
           }
       }
    

    到这里可以看出来,并不是每次getString 或者其他get方法都会重新读取文件,而是从那个我们加载到内存的map中获取数据,

    再看一下edit相关的方法

    edit

       @Override
       public Editor edit() {
           // TODO: remove the need to call awaitLoadedLocked() when
           // requesting an editor.  will require some work on the
           // Editor, but then we should be able to do:
           //
           //      context.getSharedPreferences(..).edit().putString(..).apply()
           //
           // ... all without blocking.
           synchronized (mLock) {
               awaitLoadedLocked();
           }
    
           return new EditorImpl();
       }
    

    这里同样是等待数据加载到内存之后才会创建EditorImpl,继续看一下 EditorImpl 的提交方法

    SharedPreferencesImpl.commit

           @Override
           public boolean commit() {
               long startTime = 0;
    
               if (DEBUG) {
                   startTime = System.currentTimeMillis();
               }
    
               MemoryCommitResult mcr = commitToMemory();
    
               SharedPreferencesImpl.this.enqueueDiskWrite(
                   mcr, null /* sync write on this thread okay */);
               try {
                   mcr.writtenToDiskLatch.await();
               } catch (InterruptedException e) {
                   return false;
               } finally {
                   if (DEBUG) {
                       Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                               + " committed after " + (System.currentTimeMillis() - startTime)
                               + " ms");
                   }
               }
               notifyListeners(mcr);
               return mcr.writeToDiskResult;
           }
    

    这个commit 方法一共就执行了2步,第一步是组装需要提交的result,第二步是提交result,先来看一下MemoryCommitResult 的组成都有哪些

    SharedPreferencesImpl.MemoryCommitResult

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

    这里面有一个List<String> keysModified,他包含了所有改变了的key,而mapToWriteToDisk 包含了所有本次提交的以key 和value 的数据,但是这个数据时全量还是所有改变的数据需要看一下commitToMemory 方法中的实现

    SharedPreferencesImpl.commitToMemory

    private MemoryCommitResult commitToMemory() {
               long memoryStateGeneration;
               List<String> keysModified = null;
               Set<OnSharedPreferenceChangeListener> listeners = null;
               Map<String, Object> mapToWriteToDisk;
    
               synchronized (SharedPreferencesImpl.this.mLock) {
                   // We optimistically don't make a deep copy until
                   // a memory commit comes in when we're already
                   // writing to disk.
                   if (mDiskWritesInFlight > 0) {
                       // We can't modify our mMap as a currently
                       // in-flight write owns it.  Clone it before
                       // modifying it.
                       // noinspection unchecked
                       mMap = new HashMap<String, Object>(mMap);
                   }
                   mapToWriteToDisk = mMap;////全量map
                   mDiskWritesInFlight++;
    
                   boolean hasListeners = mListeners.size() > 0;
                   if (hasListeners) {
                       keysModified = new ArrayList<String>();
                       listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                   }
    
                   synchronized (mEditorLock) {
                       boolean changesMade = false;
    
                       if (mClear) {
                           if (!mapToWriteToDisk.isEmpty()) {
                               changesMade = true;
                               mapToWriteToDisk.clear();
                           }
                           mClear = false;
                       }
    
                       for (Map.Entry<String, Object> e : mModified.entrySet()) {
                           String k = e.getKey();
                           Object v = e.getValue();
                           // "this" is the magic value for a removal mutation. In addition,
                           // setting a value to "null" for a given key is specified to be
                           // equivalent to calling remove on that key.
                           if (v == this || v == null) {
                               if (!mapToWriteToDisk.containsKey(k)) {///改变的key在本次提交中没有,则跳过
                                   continue;
                               }
                               mapToWriteToDisk.remove(k);///没有value 则删除key
                           } else {
                               if (mapToWriteToDisk.containsKey(k)) {///本次提交包含key
                                   Object existingValue = mapToWriteToDisk.get(k);
                                   if (existingValue != null && existingValue.equals(v)) {///相同的value
                                       continue;
                                   }
                               }
                               mapToWriteToDisk.put(k, v);//重新put
                           }
    
                           changesMade = true;
                           if (hasListeners) {
                               keysModified.add(k);
                           }
                       }
    
                       mModified.clear();
    
                       if (changesMade) {
                           mCurrentMemoryStateGeneration++;
                       }
    
                       memoryStateGeneration = mCurrentMemoryStateGeneration;
                   }
               }
               return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                       mapToWriteToDisk);///创建需要提交的数据,全量
           }
    

    从commitToMemory 这个方法中看到每次写文件都是写全量的数据,并不是哪个改变了就修改哪个,
    从这里其实我们可以延伸出一下问题,那就是在app中肯定存在一些不常用,但是数据量比较大的数据,如果选用了SharedPreferences作为数据保存的的方式,那么我们应该尽可能的拆分多个文件,从而创建不同的SharedPreferences,这样在修改数据的过程中可以使我们在写文件时消耗较小的资源, 从网上看到一些文章说的意思就是让我们尽可能的使用同一个文件,不知道他是从哪里看出来的,虽然一个文件只需要管理一个Map,多个文件就需要管理多个Map,如果文件特别多这样确实会导致内存碎片化比较严重,但是几个文件的情况下相比较于频繁的写重复数据,只是稍微增加了一点内存还是可以接受的,总结如下

    开关类型的数据放在一个文件中,大的多的数据如果能拆分,则拆分多个文件,这样的设计比较合理

    创建完需要提交的result后,接下来就时写文件了,不过写文件等分析完apply后统一看一下

    可以看到commit 是一个同步方法,会返回本次写文件的结果,下面我们再来看看apply 异步提交方法

    SharedPreferencesImpl.apply

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

    在apply 方法中同样还是先构造一个需要提交的result, 与commit 写文件的方法不同之处就是创建了一个等待的线程,继续看看写文件的方法

    SharedPreferencesImpl.enqueueDiskWrite

       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);//加入工作队列,异步方法
       }
    

    其实在最开始的时候阅读这段代码让我进入了一个误区,那就是一个Runnable必须运行的在一个新的Thread中,其实开启线程并执行的过程是new Thread(Ruannable).start(),而上面的代码其实只是创建了Runnable,并没有启动新的线程,其实还是同步执行的,这种做法相当于将一个方法包装成一个参数,可以向下传递,想明白了这些再去看代码就非常简单了

    总结

    1.App中肯定存在一些不常用,但是数据量比较大的数据,如果选用了SharedPreferences作为数据保存的的方式,那么我们应该尽可能的拆分多个文件,从而创建不同的SharedPreferences,这样在修改数据的过程中可以使我们在写文件时消耗较小的资源, 从网上看到一些文章说的意思就是让我们尽可能的使用同一个文件,不知道他是从哪里看出来的,虽然一个文件只需要管理一个Map,多个文件就需要管理多个Map,如果文件特别多这样确实会导致内存碎片化比较严重,但是几个文件得情况下相比较于频繁的写重复数据,只是稍微增加了一点内存还是可以接受的,当然如果你只是对这些数据作读的操作,而不做写的操作,那么合并文件确实是必须的,

    2.commit 方法是同步的 而 apply 方法是异步的,如果你在提交后,并不是马上就需要这个结果,建议使用apply方法,这样消耗的资源更小,

    3.如果同时需要修改多个数据,则应该一起提交,而不是提交了一个再提交另一个,造成性能上的浪费

    相关文章

      网友评论

        本文标题:Android 进阶学习(十六) SharedPreferenc

        本文链接:https://www.haomeiwen.com/subject/lleuwktx.html