SharedPreferences 源码分析

一,如何使用?

先从简单的使用示例开始

写入数据

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;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342