面试:SharedPreference原理及多进程

1、SharedPreference跨进程的使用方式:

在API Level>=11即Android 3.0可以通过Context.MODE_MULTI_PROCESS属性来实现SharedPreferences多进程共享,具体使用方式如下:

public class PreferencesUtils {
	public static String PREFERENCE_NAME = "SharedPreferencesDemo";
	
	private PreferencesUtils(){

	}

	public static boolean putString(Context context, String key, String value) {
		SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_MULTI_PROCESS);
		SharedPreferences.Editor editor = settings.edit();
		editor.putString(key, value);
		return editor.commit();
	}

	public static String getString(Context context, String key, String defaultValue) {
		SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_MULTI_PROCESS);
		return settings.getString(key, defaultValue);
	}
}

Andorid 7.0及以上会抛出异常,Sharepreferences不再支持多进程模式。多进程共享文件会出现问题的本质在于,因为不同进程,所以线程同步会失效。要解决这个问题,可尝试跨进程解决方案,如ContentProvider、AIDL、Service。腾讯微信团队的MMKV采用内存映射的方式,解决SharedPreferences的各种问题。

2.1、Sharedpreference多进程间通信的问题。

由于进程间是不能内存共享的,每个进程操作的SharedPreferences都是一个单独的实例,上述的问题并不能通过锁来解决,这导致了多进程间通过SharedPreferences来共享数据是不安全的,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。

  1. 使用MODE_MULTI_PROCESS时,不要保存SharedPreference变量,必须每次都从context.getSharedPreferences 获取。如果你图方便使用变量存了下来,那么无法触发reload,有可能两个进程数据不同步。
  2. 前面提到过,load数据是耗时的,并且其他操作会等待该锁。这意味着很多时候获取SharedPreference数据都不得不从文件再读一遍,大大降低了内存缓存的作用。文件读写耗时也影响了性能。
  3. 修改数据时得用commit,保证修改时写入了文件,这样其他进程才能通过文件大小或修改时间感知到。

综上,无论怎么说,MODE_MULTI_PROCESS都很糟糕,避免使用就对了。

2.2、SharePreference多进程的问题

其实在sp创建的时候可以指定的加载模式中有个MODE_MULTI_PROCESS,它是Google提供的一个在多线程模式。但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的

3、SharedPreference的加载的主要过程:

1、找到对应name的文件
2、加载对应文件到内存中SharedPreference
3、一个xml文件对应一个SharedPreferences单例

4、apply和commit

1、commit()方法和apply()方法的区别:commit()方法是同步的有返回结果,同步保证使用Countdownlatch,即使同步但不保证往磁盘的写入是发生在当前线程的。apply()方法是异步的具体发生在QueuedWork中,里面维护了一个单线程去执行磁盘写入操作。
2、commit()和apply()方法其实都是Block主线程。commit()只要在主线程调用就会堵塞主线程;apply()方法磁盘写入操作虽然是异步的,但是当组件(Activity Service BroadCastReceiver)这些系统组件特定状态转换的时候,会把QueuedWork中未完成的那些磁盘写入操作放在主线程执行,且如果比较耗时会产生ANR,手动可怕。
3、跨进程操作,需要借助Android平台常规的IPC手段(如,AIDL ContentProvider等)来完成。

5、SPMode值说明

  • MODE_PRIVATE

文件是私有数据,只能被应用本身访问,在该模式下写入的内容会覆盖原文件的内容

  • MODE_WORLD_READABLE

当前文件可以被其他应用读取,但是不可以进行写入

  • MODE_WORLD_WRITEABLE

当前文件可以被其他应用写入 ps: 如果需要被其他应用写入和读取可以直接写MODE_WORLD_WRITEABLE + MODE_WORLD_READABLE

  • MODE_APPEND

该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新的文件

6、SharePreferences运行机制

  • 首先,我们看下SharePreferences的简单使用,代码如下:
    mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = mSharedPreferences.edit();
    editor.putString(key, value);
    editor.apply();
  • context.getSharedPreferences其实就是简单的调用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);
        }
        sp = packagePrefs.get(name);
        if (sp == null) {
        <!--读取文件-->
            File prefsFile = getSharedPrefsFile(name);
            sp = new SharedPreferencesImpl(prefsFile, mode);
            <!--缓存sp对象-->
            packagePrefs.put(name, sp);
            return sp;
        }
    }
    <!--跨进程同步问题-->
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}
  • 这里面数据的加载的地方需要看下,比如,SharePreferences数据的加载是同步还是异步?数据加载是new SharedPreferencesImpl对象时候开始的
 SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}
  • startLoadFromDisk很简单,就是读取xml配置,如果其他线程想要在读取之前就是用的话,就会被阻塞,一直wait等待,直到数据读取完成。
    private void loadFromDiskLocked() {
   ...
    Map map = null;
    StructStat stat = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
    <!--读取xml中配置-->
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            }...
    mLoaded = true;
    ...
    <!--唤起其他等待线程-->
    notifyAll();
}

可以看到其实就是直接使用xml解析工具XmlUtils,直接在当前线程读取xml文件,所以,如果xml文件稍大,尽量不要在主线程读取,读取完成之后,xml中的配置项都会被加载到内存,再次访问的时候,其实访问的是内存缓存。

Guess you like

Origin blog.csdn.net/cpcpcp123/article/details/121787716