美文网首页
SharedPreferences 源码分析

SharedPreferences 源码分析

作者: Luckflower | 来源:发表于2020-07-02 22:49 被阅读0次

    一,如何使用?

    先从简单的使用示例开始

    写入数据

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

    读取数据

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

    使用很简单,接下来我们一句一句的来分析源码实现。

    二, 源码分析

    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 默认模式,创建的文件只能在应用内访问(或者共享相同userID的所有应用)
    MODE_WORLD_READABLE(过时)允许其他应用访问本应用的文件,使用此模式会抛出异常
    MODE_WORLD_WRITEABLE(过时)允许其他应用写本应用的文件,使用此模式会抛出异常
    MODE_MULTI_PROCESS(过时)官方提示这种模式在某些版本无法可靠运行,并且未来也不会支持多进程

    但目前除了第一种其他的都不建议使用,并且从 Android N 开始MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE会直接抛出异常,后面我们在分析源码的时候会详细说明这块;

    上面 mBase 是 Context 类型的实例,Context是一个抽象类,它有两个直接子类,一个是ContextWrapper,一个是ContextImpl,ContextWrapper 是上下文功能的封装类,而ContextImpl则是上下文功能的实现类, Activity,Service, Application都是继承自ContextWrapper的,因此这里的mBase其实最终指向的就是 ContextImpl;

    ContextImpl 是何时创建的?

    ContextImpl 是主线程ActivityThread 的成员变量,ActivityThread 是管理应用进程的主线程的执行,ActivityThread 是在App冷启动main(String[] args)中初始化的,说明ActivityThread只有一个,从而对应一个ContextImpl ,分析这个有利于我们接下来分析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 是被允许的,它会生成一个以null.xml为名的文件, 我用targetSdkVersion为29,在Android 10的机器上也验证了确实是可以的:
    /data/user/0/com.cjl.loadingview/shared_prefs/null.xml

    我们知道一个App可以对SharedPreferences设置不同name,这样最终也就对应着不同的xml文件,mSharedPrefsPaths 是一个map,它就是用来保存不同name的文件的;如果mSharedPrefsPaths里没有该name对应的文件,那么就 通过getSharedPreferencesPath(name)获取一个,代码如下:

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

    上述代码最终会生成一个下面路径的.xml文件:
    /data/user/0/com.cjl.loadingview/shared_prefs/settings.xml
    /data/user/0 是一个 /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 存储不同packageName 的 SharedPreferencesImpl, 冷启动进来SharedPreferencesImpl是为null的,因此回去新建一个,在新建之前会检查最初传进来的mode,从如下代码可以看到Android N 后已强制不能在使用 MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE 。
    如果sp不为null,mode 是 多进程模式 MODE_MULTI_PROCESS, 此时需要重新读取文件;

    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 是一个接口类型的,SharedPreferencesImpl 是它的实现类,因此SharedPreferencesImpl 才是我们分析的重点,这里面有SharedPreferences 各种操作的具体实现:

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

    创建.bak备份文件,接下来会开启一个名为 SharedPreferencesImpl-load 的子线程从磁盘读取文件,并且读取到文件后立即将其转换成了map文件保存在内存中,为什么转换成map保存在内存中呢,这里留一个伏笔,看到后面你自然会明白;

    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 这个成员变量得留意一下,在首次读取完磁盘文件后,下次调用getSharedPreferences就不会再从磁盘读取了;

    2. 写入数据

    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) {}
        }
        ...
    }
    

    写入数据需要使用Editor 实例,Editor 是一个接口,它的实现类是EditorImpl,在获取Editor 之前如果mLock锁没有被释放则会处于等待状态, 等待什么呢,从上面分析我们不难看出其实是在等待getSharedPreferences时从磁盘中读取文件,如果文件都没有读取完成,我们拿到Editor 去写数据肯定是不行的,加载成功后的 notifyAll 要结合 awaitLoadedLocked 来分析。在准备读、写 SP 的时候,都会先调用 awaitLoadedLocked 等待 loadFromDisk loadFromDisk ,在读取磁盘文件结束后会调用mLock.notifyAll()唤醒这些等待数据加载完成的线程,接下来我们就可以去获取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直接更改内存mMap的值;
    第二步是将内存mMap中的键值对写入到磁盘文件中;

    我们putString为例来分析下写文件的操作,在调用完putString之后我们必须要调用commit()或者apply()去保存数据;
    这两个方法都会调用commitToMemory()将数据写入内存map,接着都会add一个写入文件的任务,等待后续系统执行

    commit()或者apply()不同的地方在于:
    apply将文件写入操作放到一个Runnable对象中,等待系统在子线程中调用, 此时不会阻碍主线程;
    commit 是直接在主线程中同步进行写入操作, 因此使用commit是会阻塞主线程的,这点得注意;
    关于上述不同点可以详细追一下enqueueDiskWrite()方法, 如果是apply()方法会使用writeToDiskRunnable , commit会在主线程写入

    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. 数据的读取

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

    相关文章

      网友评论

          本文标题:SharedPreferences 源码分析

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