欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SharedPreferences源码分析

程序员文章站 2022-05-15 22:53:21
...

今天偶然看见一篇讲解SharedPreferences(以下简称SP)原理的文章,于是怀着前人种树后人乘凉的心情进去看了,发现写得不是很清楚,很多细节一笔带过,于是为了巩固一下基础就去百度了一下,发现百度上也没有让自己满意的文章。虽然SP很简单,但是为了锻炼一下写博客的技巧,还是决定看看源码记录一下分析结果。此处分析的SDK版本为android-27。

1.SP的使用

我们都知道,SP是以XML文件方式存储的,我们先来看看SP的基本使用方法。如下所示

SharedPreferences mSp = getSharedPreferences("filename", Context.MODE_PRIVATE);
SharedPreferences mSp2 = getPreferences(Context.MODE_PRIVATE);

// 监听
SharedPreferences.OnSharedPreferenceChangeListener listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
   @Override
   public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // 监听值变化的key
   }
};
mSp.registerOnSharedPreferenceChangeListener(listener);
mSp.unregisterOnSharedPreferenceChangeListener(listener);

// 取
mSp.getBoolean("key1", false);
mSp.getInt("key2", 0);
mSp.getFloat("key3", 0);
mSp.getLong("key4", 0);
mSp.getString("key5", "");
mSp.getStringSet("key6", null);
Map<String, ?> datas = mSp.getAll();

// 存
SharedPreferences.Editor editor = mSp.edit();
editor.putBoolean("key1", true);
editor.putInt("key2", 0);
editor.putFloat("key3", 0);
editor.putLong("key4", 0);
editor.putString("key5", "");
editor.putStringSet("key6", null);

// boolean isSuccess = editor.commit();
// editor.apply();

根据以上代码,我们就产生了以下两个问题:
(1) getSharedPreferences(String filename, int mode)和getPreferences(int mode)有什么区别。
(2) Editor的apply()和commit()有什么区别。

2.源码分析

1.SP实例的获取

首先我们来看SP的获取方式:
Activity.java

/**
  * Retrieve a {@link SharedPreferences} object for accessing preferences
  * that are private to this activity.  This simply calls the underlying
  * {@link #getSharedPreferences(String, int)} method by passing in this activity's
  * class name as the preferences name.
  *
  * @param mode Operating mode.  Use {@link #MODE_PRIVATE} for the default
  *             operation.
  *
  * @return Returns the single SharedPreferences instance that can be used
  *         to retrieve and modify the preference values.
  */
  public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
      return getSharedPreferences(getLocalClassName(), mode);
  }

ContextWrapper.java

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

mBase为ContextImp的实例,是在由Activity创建时传入的,而ContextImp的源文件在…/android-sdk/sources/android-27/android/app目录下,我们来看看其代码实现:
ContextImp.java

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
   /*  
   省略不相关代码 
   */
   File file;
   file = getSharedPreferencesPath(name);
   return getSharedPreferences(file, mode);
}

其中getSharedPreferencesPath()方法为获取对应的xml文件,具体实现如下

@Override
public File getSharedPreferencesPath(String name) {
   return makeFilename(getPreferencesDir(), name + ".xml");
}
@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);
           /* 省略部分代码 */
           sp = new SharedPreferencesImpl(file, mode);
           cache.put(file, sp);
           return sp;
        }
   }
   /*
   省略部分代码
   */
   return sp;
}

代码逻辑很清晰,就是从缓存里找到SP对象的实例再返回,一个文件对应一个SP实例。这里的checkMode()方法主要是Android N以后不再支持MODE_WORLD_WRITEABLE和MODE_WORLD_READABLE两种mode。我们也可以看出SP的唯一实现为SharedPreferencesImpl,稍后将解析。我们先来看看getSharedPreferencesCacheLocked()这个方法。

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是一个ArrayMap<String,ArrayMap<File, SharedPreferencesImpl>>类型的变量,并且sSharedPrefsCache的key总是应用包名,这就使应用间不能共享SP文件。以上就是关于SP获取的全部内容,下面我们来看看SP的实现SharedPreferencesImpl和SP.Editor的实现EditorImp,来分析一下数据时怎样被记录的。

2.SharedPreferencesImpl和EditorImp

实现文件…/android-sdk/sources/android-27/android/app/SharedPreferencesImpl.java
首先来看看SP的主要代码,首先mMap是对XML文件里的数据的缓存,File即对应的XML文件,mMode对应为文件的读写权限。而mListener对象是监听key值变化的回调的集合,其Object值没有任何实际意义。

final class SharedPreferencesImpl implements SharedPreferences {
    private static final String TAG = "SharedPreferencesImpl";
    @GuardedBy("mLock")
    private Map<String, Object> mMap;
    private final File mFile;
    private final int mMode;
    @GuardedBy("mLock")
    private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
            new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        // 创建时将XML文件里的数据加载到mMap里,掉用getXXX方法时,获得的是缓存值
        startLoadFromDisk();
    }
    
    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(mLock) {
            mListeners.put(listener, CONTENT);
        }
    }

    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(mLock) {
            mListeners.remove(listener);
        }
    }
    public Editor edit() {
        // 省略部分代码
        return new EditorImpl();
    }

	/*
	省略getXXXX()部分代码
	*/

	private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
		/* 写入文件部分省略 */
	}

}

好了我们再来看看EditorImp的主要代码,EditorImpSP的内部类,EditorImp就比较简单了,主要是对编辑过的key-value数据做保存并封装成MemoryCommitResult(以下简称mcr)对象,再最终掉用到外部类一系列方法将mcr写入到文件。

public final class EditorImpl implements Editor {
    @GuardedBy("mLock")
    private final Map<String, Object> mModified = Maps.newHashMap();
    @GuardedBy("mLock")
    private boolean mClear = false;
	
	/* 省略一系列putXXX方法 */
	public void apply() { ... }
	private MemoryCommitResult commitToMemory() { ... }
	public boolean commit() { ... }
	private void notifyListeners(final MemoryCommitResult mcr) { ... }
}

EditorImp主要就以上四个方法,其中commitToMemory()方法是将数据保存到SP的缓存对象mMap中,并封装成相应的结果mcr,这里我们并不关心其具体实现,我们最关心的是apply()和commit()两个方法到底做了什么。
先来看看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;
}

commit()方法首先将数据写入到缓存,再将封装后的mcr传给外部类写入内存,最后再返回一个boolean值,这个值代表是否成功写入到文件。那么我们看看apply()方法又有什么不同呢

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

我们发现,apply()和commit()的区别除了返回值以外,就是掉用外部类SharedPreferencesImpl.this.enqueueDiskWrite(…)传的值不一样,关于mcr.writtenToDiskLatch这是一个CountDownLatch对象的实例,主要作用是控制线程同步,保证文件读写安全。SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)具体代码如下:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit);
            }
        }
        ....
    };
    if (isFromSyncCommit) {
        ....
        writeToDiskRunnable.run();
        return;
    }
	QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

这里很显然,如果是commit方式,就直接同步执行外部类的writeToFile(…)方法并return,如果是apply()方式,就将writeToDiskRunnable传入QueuedWork.queue()方法继续执行,关于QueuedWork.queue()我们已经没有必要再跟踪了,后面发生的事情基本就是把writeToDiskRunnable放到一个HandlerThread中去执行,也就是异步执行。到此我们就是分析完了SP操作数据的整个过程,此处还应该注意的是notifyListeners(mcr),代码如下:

private void notifyListeners(final MemoryCommitResult mcr) {
    ....
    if (Looper.myLooper() == Looper.getMainLooper()) {
        for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
            final String key = mcr.keysModified.get(i);
            for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                if (listener != null) {
                     listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
                }
            }
        }
    } else {
        // Run this function on the main thread.
        ActivityThread.sMainThreadHandler.post(new Runnable() {
             public void run() {
                 notifyListeners(mcr);
             }
        });
    }
}

显然,监听的回调是在主线程中完成的。

3.总结

SP的整个过程就是,再创建时加载XML文件中的数据并缓存到一个Map对象中,读取数据时直接在mMap中读取,当写入数据时,先将数据写入mMap缓存中,写入成功后再将数据更新到文件。由以上的源码分析,我们可以得出以下结论:

  • (1) SP是通过XML文件方式进行存储的。
  • (2) 应用间不能共享SP。
  • (3) getPreferences(…)本质是调用的getSharedPreferences(…),并且只能在调用的Activity内共享数据。
  • (4) Editor负责将数据保存到缓存,写入文件操作由SP完成。
  • (5) Editor的commit()和apply()的本质区别是,commit()为同步操作并返回是否成功写入文件的结果,而apply()是异步执行写入文件操作没有返回值,而写入缓存的操作两者没有区别。
  • (6) 监听事件在主线程中回调。

以上,就是SP的基本内容。第一次写分析源码的博文,有错误欢迎指出。

相关标签: Android存储方式