Android SharedPreferences源码分析.md

我们经常使用SharedPreferences保存一些简单的数据,比如Settings的数据。如果我们只是简单的使用,可能没什么问题,但是如果要用好它还是得明白它的实现方式,下面来从源码上来分析下SharedPreferences的缓存,异步读写实现,多线程,多进程访问。

SharedPreferences简介

SharedPreferences是Android提供的一种使用XML文件保存内容的机制。其内部就是通过xml写入文件的。

SharedPreferences是一个接口类,这是使用它的一个基础,我们可以通过Context的getSharedPreference来获取SharedPreferences。如下所示:


SharedPreferences sp = context.getSharedPreferences("name",Context.MODE_PRIVATE);

第一个参数表示存储的文件名,第二个表示创建文件时的模式。

它提供了getInt, getLong, getFloat,getChar, getString 来读取int, long, float, char, String类型的数据,并且提供了一个Editor接口来用于写入对应的数据类型。Android在API14时又提供了Set类型的数据写入读取。下面看一段简单实用示例:


SharedPreferences sp = context.getSharedPreferences("name",Context.MODE_PRIVATE);

int val1 = sp.getInt("val1",0);

SharedPreferences.Editor editor = sp.edit();

editor.putInt("val1", val1+1);

editor.apply();

// editor.commit(); 跟apply方法是一样的,但是apply是异步写入。

下面就针对上面这段代码流程,分析一下SharedPreferences的源码。

获取SharedPreferences

我们通过context.getSharedPreferences方法获取SharedPreferences,而Context得真正实现者是ContextImpl,所以看看ContextImpl里面的getSharedPreferences方法:


@Override

public SharedPreferences getSharedPreferences(String name, int mode) {
   SharedPreferencesImpl sp;
   synchronized (ContextImpl.class) {
       if (sSharedPrefs == null) {
           sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
       }

       final String packageName = getPackageName();
       ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
       if (packagePrefs == null) {
           packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
           sSharedPrefs.put(packageName, packagePrefs);
       }

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

       sp = packagePrefs.get(name);
       if (sp == null) {
           File prefsFile = getSharedPrefsFile(name); //根据文件名,获取存储的文件
           sp = new SharedPreferencesImpl(prefsFile, mode);
           packagePrefs.put(name, 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();       //在多进程模式或者目标sdk版本在HONEYCOMB以下版本每次读取缓存了的sp,Android会检查xml文件是否已经被重写了。
   }
   return sp;
}

这段代码首先判断sSharedPrefs是否为空,如果为空则给他初始化。sSharedPrefs是一个用来缓存SharedPreferences的ArrayMap,它的key为包名,它的value为ArrayMap,这个ArrayMap保存的键值对是SharedPreferences文件名和对应的SharedPreferencesImpl(是SharedPreferences的实现类)。如果SharedPreferencesImpl已经存在,它会直接返回已经存在的SharedPreferencesImpl。如果是在多进程模式下,或者目标版本低于HONEYCOMB的时候,会检查是否需要重新从磁盘中加载文件。但是需要说的是MODE_MULTI_PROCESS模式已经被deprecated了,官方建议使用ContentProvider来处理多进程访问,其实我们项目中就遇到这么一个问题导致了一个BUG。

在重新创建SharedPreferencesImpl的时候,getSharedPreferences会调用getSharedPrefsFile来获取存储的xml文件,这个函数对xml文件名进行了组装:


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

通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀。Android没有提供直接访问shared_prefs目录的API,getPreferencesDir是一个私有类,我们如果想要直接访问这个目录,可以通过下面这段代码访问:


String sharedPrefsDir = context.getCacheDir().getParent().getAbsolutePath()+"/shared_prefs";

SharedPreferencesImpl构造函数

从上面的代码已经知道SharedPreferences具体的实现者是SharedPreferencesImpl。我们都知道Android的SharedPreferences对XML操作是使用DOM方式解析的(一开始就把整个XML给读取出来)。在SharedpreferencesImpl源码中,它的构造函数里面它就把XML文件给读取出来了:


SharedPreferencesImpl(File file, int mode) {
   mFile = file;
   mBackupFile = makeBackupFile(file);
   mMode = mode;
   mLoaded = false;
   mMap = null;
   startLoadFromDisk();
}

它的构造函数中startLoadFromDisk就是将xml给读取出来的。下面看看startLoadFromDisk:


private void startLoadFromDisk() {
   synchronized (this) {
       mLoaded = false;
   }
   new Thread("SharedPreferencesImpl-load") {
       public void run() {
           synchronized (SharedPreferencesImpl.this) {
               loadFromDiskLocked();
           }
       }
   }.start();
}

它使用了一个异步线程来读取xml,最终实现的函数是loadFromDiskLocked(),在读取的时候它必须获取SharedPreferencesImpl.this的锁:


private void loadFromDiskLocked() {
...
   Map map = null;
   StructStat stat = null;
   try {
       stat = Os.stat(mFile.getPath());
       if (mFile.canRead()) {
           BufferedInputStream str = null;
           try {
               str = new BufferedInputStream(
                       new FileInputStream(mFile), 16*1024);
               map = XmlUtils.readMapXml(str);
...

  mLoaded = true;
  if (map != null) {
      mMap = map;
      mStatTimestamp = stat.st_mtime;
      mStatSize = stat.st_size;
  } else {
      mMap = new HashMap<String, Object>();
  }

这个函数里面省略了一些代码,想看全部的,可以直接去SharedPreferencesImpl文件看。这个函数最终调用了XmlUtils.readMapXml来调用,读取整个xml的内容,放到mMap当中。

读取key对应的值

SharedPreferencesImpl的读取是非常简单的,因为在构造函数当中就已经读取整个xml文件的内容到mMap当中了,所以再次读取的时候直接从mMap当中读取就好了,但是得注意同步的问题:


public int getInt(String key, int defValue) {
   synchronized (this) {
       awaitLoadedLocked();
       Integer v = (Integer)mMap.get(key);
       return v != null ? v : defValue;
   }
}

函数awaitLoadedLocked就是等待读取文件完成。因为如果读取具体元素的时候,读取文件线程却没有完成,那么必须等待文件读取完成,不然结果肯定会乱。

写入

SharedPreferences的写入是通过Editor来实现的,Editor接口在SharedPreferencesImpl具体实现是EditorImpl,在这看看它的源码:


public final class EditorImpl implements Editor {
   private final Map<String, Object> mModified = Maps.newHashMap();
   private boolean mClear = false;

   public Editor putInt(String key, int value) {
       synchronized (this) {
           mModified.put(key, value);
           return this;
       }
   }
   //... 省略了其他类型的value操作,和clear,remove函数。

   public void apply() {
       final MemoryCommitResult mcr = commitToMemory();
       final Runnable awaitCommit = new Runnable() {
               public void run() {
                   try {
                       mcr.writtenToDiskLatch.await();
                   } catch (InterruptedException ignored) {
                   }
               }
           };

       QueuedWork.add(awaitCommit);

       Runnable postWriteRunnable = new Runnable() {
               public void run() {
                   awaitCommit.run();
                   QueuedWork.remove(awaitCommit);
               }
           };

       SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // enqueueDiskWrite会调用异步线程执行postWriteRunnable。

       // Okay to notify the listeners before it‘s hit disk
       // because the listeners should always get the same
       // SharedPreferences instance back, which has the
       // changes reflected in memory.
       notifyListeners(mcr);
   }
....省略了commitToMemory
   public boolean commit() {
       MemoryCommitResult mcr = commitToMemory();
       SharedPreferencesImpl.this.enqueueDiskWrite(
           mcr, null /* sync write on this thread okay */); //第二个参数为null,enqueueDiskWrite会直接写入。
       try {
           mcr.writtenToDiskLatch.await();
       } catch (InterruptedException e) {
           return false;
       }
       notifyListeners(mcr);
       return mcr.writeToDiskResult;
   }

... 省略了notifyListeners

}

从源码上面可以看出,首先使用put写入的时候,只是写入到一个mModified里面,但是实际上还没写入SharedPreferencesImpl的mMap当中,更没有写入磁盘,只有当调用commit或者apply函数的时候才会开始写入。而apply是异步写入,而commit是在当前线程直接写入。commit在enqueueDiskWrite的第二个参数传入null,看看enqueueDiskWrite的实现:


private void enqueueDiskWrite(final MemoryCommitResult mcr,
                             final Runnable postWriteRunnable) {
   final Runnable writeToDiskRunnable = new Runnable() {
           public void run() {
               synchronized (mWritingToDiskLock) {
                   writeToFile(mcr);
               }
               synchronized (SharedPreferencesImpl.this) {
                   mDiskWritesInFlight--;
               }
               if (postWriteRunnable != null) {
                   postWriteRunnable.run();
               }
           }
       };

   final boolean isFromSyncCommit = (postWriteRunnable == null); //如果postWriteRunnable就同步写入

   // Typical #commit() path with fewer allocations, doing a write on
   // the current thread.
   if (isFromSyncCommit) {
       boolean wasEmpty = false;
       synchronized (SharedPreferencesImpl.this) {
           wasEmpty = mDiskWritesInFlight == 1;
       }
       if (wasEmpty) {
           writeToDiskRunnable.run();
           return;
       }
   }

   QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

但是需要指出的是,两种方式首先都会先使用commitTomemory函数将修改的内容写入到SharedPreferencesImpl当中。看看commitToMemory的实现:


private MemoryCommitResult commitToMemory() {
   MemoryCommitResult mcr = new MemoryCommitResult();
   synchronized (SharedPreferencesImpl.this) {
       // 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);
       }
       mcr.mapToWriteToDisk = mMap;
       mDiskWritesInFlight++;

       boolean hasListeners = mListeners.size() > 0;
       if (hasListeners) {
           mcr.keysModified = new ArrayList<String>();
           mcr.listeners =
                   new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
       }

       synchronized (this) {
           if (mClear) {
               if (!mMap.isEmpty()) {
                   mcr.changesMade = true;
                   mMap.clear();
               }
               mClear = false;
           }

           for (Map.Entry<String, Object> e : mModified.entrySet()) {  // 在这开始将修改的内容写入到mMap当中。
               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 (!mMap.containsKey(k)) {
                       continue;
                   }
                   mMap.remove(k);
               } else {
                   if (mMap.containsKey(k)) {
                       Object existingValue = mMap.get(k);
                       if (existingValue != null && existingValue.equals(v)) {
                           continue;
                       }
                   }
                   mMap.put(k, v);
               }

               mcr.changesMade = true;
               if (hasListeners) {
                   mcr.keysModified.add(k);
               }
           }

           mModified.clear();
       }
   }
   return mcr;
}

通知修改的变化

我们可以通过下面两个函数注册监视xml文件变化的通知,在这里我直接把函数源码给顺便贴出来了,因为比较简短:


public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
   synchronized(this) {
       mListeners.put(listener, mContent);
   }
}

public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
   synchronized(this) {
       mListeners.remove(listener);
   }
}

在前面分析了的函数commitToMemory中会返回修改的内容保存在MemoryCommitResult当中,然后使用使用notifyListener函数通知监听者。

总结

SharedPreferences从功能上面来讲就是三个部分读取(一开始异步全部读取出来,get的时候,如果没有读取完,会等待),写入,监听SharedPreferences的变化。另外Android会使用ArrayMap对SharedPreferences进行缓存,以SharedPreferences的name作为key。需要进一步理解的是关于多线程,多进程时的使用。

首先从线程方面来看,从源码上看apply是使用异步线程写入磁盘,commit是同步写入磁盘。所以我们在主线程使用的commit的时候,需要考虑是否会出现ANR问题。我们不用担心apply异步写入会出现先写入的内容,在该线程之后读取会读取不到,因为它写入内存的时候没有使用异步线程,所以在主线程最好使用apply。所有的线程读取的时候都会加SharedPreferencesImpl.this锁,editor写入内存的时候(写入SharedPreferencesImpl.this.mMap)也会加SharedPreferencesImpl.this锁,另外editor调用put,clear, remove方法的时候都会加上EditorImpl.this锁,这些是线程安全的保证,只有在commit/apply后才会写入内存(mMap, xml内容缓存的map变量)和磁盘。

另外从多进程方面来看,SharedPreferences本身提供了MODE_MULTI_PROCESS的模式,但是现在已经deprecated了,不建议使用。MODE_MULTI_PROCESS也仅仅是每次读取缓存的SharedPreferencesImpl时重写读取一次磁盘(其实效率很低,而且从源码看,并不能很好地保持同步)。所以Android建议使用ContentProvider来保持多进程的访问。有人已经实现了,可以通过google搜索multi process sharedpreferences找到,因为我没看过那些,所以自己搜吧,我是直接看的公司的。



理解,分析

时间: 2024-10-10 04:20:12

Android SharedPreferences源码分析.md的相关文章

[Android]Fragment源码分析(一) 构造

Fragment是Android3.0之后提供的api,被大家广泛所熟知的主要原因还是因为随即附带的ViewPager控件.虽然我并不喜欢用它,但是它确实是一个相对不错的控件.还是我的一贯作风,我将从源码上向大家展示什么是Fragment.我们先写一个简单的代码对Fragment有个直观的认识:(为了保证我们方便调试,我们可以直接使用V4提供的源码包) FragmentTransaction t = getSupportFragmentManager().beginTransaction();

[Android]Volley源码分析(四)

上篇中有提到NetworkDispatcher是通过mNetwork(Network类型)来进行网络访问的,现在来看一下关于Network是如何进行网络访问的. Network部分的类图: Network有一个实现类BasicNetwork,它有一个mHttpStack的属性,实际的网络请求是由这个mHttpStack来进行的,看BasicNetwork的performRequest()方法, 1 @Override 2 public NetworkResponse performRequest

android 从源码分析为什么Listview初次显示时没滚动却自动调用onScroll方法的原因

我们做Listview的分批加载时,需要为Listview调用setOnScrollListener(具体代码可见我上一篇博客) 可是,我们会发现,当运行程序时,listview明明没有滚动,那为什么系统会调用onScroll方法呢?(补充:此时onScrollStateChanged并不会调用) 我们先看setOnScrollListener源码: public void setOnScrollListener(OnScrollListener l) { mOnScrollListener =

[Android]Fragment源码分析(三) 事务

Fragment管理中,不得不谈到的就是它的事务管理,它的事务管理写的非常的出彩.我们先引入一个简单常用的Fragment事务管理代码片段: FragmentTransaction ft = this.getSupportFragmentManager().beginTransaction(); ft.add(R.id.fragmentContainer, fragment, "tag"); ft.addToBackStack("<span style="fo

[Android]Volley源码分析(二)Cache

Cache作为Volley最为核心的一部分,Volley花了重彩来实现它.本章我们顺着Volley的源码思路往下,来看下Volley对Cache的处理逻辑. 我们回想一下昨天的简单代码,我们的入口是从构造一个Request队列开始的,而我们并不直接调用new来构造,而是将控制权反转给Volley这个静态工厂来构造. com.android.volley.toolbox.Volley: public static RequestQueue newRequestQueue(Context conte

[Android]Volley源码分析(叁)Network

如果各位看官仔细看过我之前的文章,实际上Network这块的只是点小功能的补充.我们来看下NetworkDispatcher的核心处理逻辑: <span style="font-size:18px;">while (true) { try { // Take a request from the queue. request = mQueue.take(); } catch (InterruptedException e) { // We may have been int

[Android]Volley源码分析(肆)应用

通过前面的讲述,相信你已经对Volley的原理有了一定了解.本章将举一些我们能在应用中直接用到的例子,第一个例子是 NetworkImageView类,其实NetworkImageView顾名思义就是将异步的操作封装在了控件本身,这种设计可以充分保留控件的移植性和维护性.NetworkImageView通过调用setImageUrl来指定具体的url: public void setImageUrl(String url, ImageLoader imageLoader) { mUrl = ur

Android IntentService 源码分析

IntentService简介: IntentService是一个通过Context.startService(Intent)启动可以处理异步请求的Service,使用时你只需要继承IntentService和重写其中的onHandleIntent(Intent)方法接收一个Intent对象,该服务会在异步任务完成时自动停止服务. 所有的请求的处理都在IntentService内部工作线程中完成,它们会顺序执行任务(但不会阻塞主线程的执行),某一时刻只能执行一个异步请求. IntnetServi

[Android] Volley源码分析(一)体系结构

Volley:google出的一个用于异步处理的框架.由于本身的易用性和良好的api,使得它能得以广泛的应用.我还是一如既往从源码的方向上来把控它.我们先通过一段简单的代码来了解Volley RequestQueue queue = Volley.newRequestQueue(this); ImageRequest imagerequest = new ImageRequest(url, new Response.Listener<Bitmap>(){ @Override public vo