当前位置:   article > 正文

Android基础知识-SharePreference存储相关_android sharepreference是否线程安全

android sharepreference是否线程安全
转载
  • SharedPreferences 线程安全

  • ArrayMap<File, SharedPreferencesImpl> 缓存在内存,多次调用 getSharedPreferences 不影响性能

  • SharedPreferences 开启一个线程,异步加载文件数据

  • get 操作线程安全,getXxx方法是直接操作内存,从内存的 mMap中,根据传入的key读取value
    文件加载完之前,调用 get 会阻塞在 awaitLoadedLocked() 方法,阻塞线程

  • put 操作线程安全,基于 Editor 类
    文件加载完之前,调用 put 会在 edit() 方法中调用 awaitLoadedLocked() 方法,阻塞线程
    Editor editor = preferences.edit()
    editor.put()

  • put / remove / clear 等写操作,先记录在 mModified 中,commit / apply 调用后
    将所有 mModified 同步到 mMap(成功后清空 mModified) 以及 文件

  • 如果有执行 clear()方法,会清空mMap,不会清空 mModified
    清空 mMap后,会遍历 mModified,将其中写记录同步到 mMap

    sharedPreferences.edit()
       	 .putString("key1", "value1")
         .clear()
         .commit();
    
    • 1
    • 2
    • 3
    • 4

    这种写法,会先清空 mMap,然后继续把 key1,value,写入mMap和磁盘

  • 用来存储一些 比较小的键值对集合
    最终会在手机的 /data/data/package_name/shared_prefs/目录下生成一个 xml 文件存储数据

  • 通过 ContextImpl.getSharedPreferences 方法能够获取SharedPreferences对象
    通过 getXxx/putXxx 方法能够进行读写操作

  • commit 方法(只有一个commit操作时同步写入文件,超过一个commit时,异步写入文件

  • apply 异步写文件
    把写入文件的任务,放入一个单线程的线程池的任务队列

  • 异步写入文件的任务,会被添加在一个全局队列中,
    在Activity#onStop执行之前,会阻塞onStop,等待任务执行完。
    所以异步 apply 太多,会阻塞 UI线程,导致Activity泄露

  • SP 性能与文件大小相关,不同业务类型数据,拆分到不到文件保存
    第一次 getSharedPreferences,会加载 SP 文件进内存,过大的 SP 文件会导致阻塞,甚至会导致 ANR
    每次 apply、commit,都会把全部的数据一次性写入磁盘

  • SP 跨进程,只是在 getSharedPreference() 是,判断文件是否修改,有改动则重新加载文件。
    数据不同步

9、apply太多会阻塞 Activity#onStop()
ActivityThread # handleStopActivity() 中 QueuedWork.waitToFinish(),
API 11以上,会阻塞 onStop,等待 Sp写入文件任务完成

1、ContextImpl.getSharedPreferences()获取SharedPreferences对象

class ContextImpl extends Context {
	
	@Override
	public SharedPreferences getSharedPreferences(String name, int mode) {
		if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) {
			if (name == null) {
				name = "null"; //4.4以下,默认文件名
			}
		}
		File file;
		synchronized (ContextImpl.class) {
			if (mSharedPrefsPaths == null) {
				mSharedPrefsPaths = new ArrayMap<>();
			}
			file = mSharedPrefsPaths.get(name);
			if (file == null) {
				// 创建一个对应路径 /data/data/packageName/name 的 File 对象
				file = getSharedPreferencesPath(name);
				mSharedPrefsPaths.put(name, file);
			}
		}
		// 这里调用了 getSharedPreferences(File file, int mode) 方法
		return getSharedPreferences(file, mode);
	}

	@Override
	public SharedPreferences getSharedPreferences(File file, int mode) {
		SharedPreferencesImpl sp;
		// 这里使用了 synchronized 关键字,确保了 SharedPreferences 对象的构造是线程安全的
		synchronized (ContextImpl.class) {
			// 获取SharedPreferences 对象的缓存,并复制给 cache
			final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
			// 以参数 file 作为 key,获取缓存对象
			sp = cache.get(file);
			if (sp == null) {// 如果缓存中不存在 SharedPreferences 对象
				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");
					}
				}
				// 构造一个 SharedPreferencesImpl 对象
				sp = new SharedPreferencesImpl(file, mode);
				// 放入缓存 cache 中,方便下次直接从缓存中获取
				cache.put(file, sp);
				// 返回新构造的 SharedPreferencesImpl 对象
				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.

			// 如果由其他进程修改了这个 SharedPreferences 文件,我们将会重新加载它
			sp.startReloadIfChangedUnexpectedly();
		}
		// 程序走到这里,说明命中了缓存,SharedPreferences 已经创建,直接返回
		return sp;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

1)缓存未命中, 才构造SharedPreferences对象,多次调用 getSharedPreferences方法,不影响性能
因为有缓存机制
2)SharedPreferences对象的 创建过程是线程安全的,因为使用了synchronized 关键字

3)如果命中了缓存,并且参数mode 使用 Context.MODE_MULTI_PROCESS
会调用sp.startReloadIfChangedUnexpectedly()方法。
在 startReloadIfChangedUnexpectedly 方法中,会判断是否由其他进程修改过这个文件,如果有,会重新从磁盘中读取文件加载数据。

2、构造SharedPreferencesImpl

final class SharedPreferencesImpl implements SharedPreferences {
	
	SharedPreferencesImpl(File file, int mode) {
		mFile = file;
		//创建灾备文件,命名为prefsFile.getPath() + ".bak"
		mBackupFile = makeBackupFile(file);
		mMode = mode;
		//mLoaded代表是否已经加载完数据
		mLoaded = false;
		//解析 xml 文件得到的键值对就存放在mMap中
		mMap = null;
		//顾名思义,这个方法用于加载 mFile 这个磁盘上的 xml 文件
		startLoadFromDisk();
	}

	//创建灾备文件,用于当用户写入失败的时候恢复数据
	private static File makeBackupFile(File prefsFile) {
		return new File(prefsFile.getPath() + ".bak");
	}
	
	//加载mFile
	private void startLoadFromDisk() {
		synchronized (this) {
			mLoaded = false;
		}

		//【注意:这里我们可以看出,SharedPreferences 是通过开启一个线程来异步加载数据的】
		new Thread("SharedPreferencesImpl-load") {
			public void run() {
				//这个方法才是真正负责从磁盘上读取 xml 文件数据
				loadFromDisk();
			}
		}.start();
	}

	private void loadFromDisk() {
		synchronized (SharedPreferencesImpl.this) {
			//如果正在加载数据,直接返回
			if (mLoaded) {
				return;
			}
			//如果备份文件存在,删除原文件,把备份文件重命名为原文件的名字
			//我们称这种行为叫做回滚
			if (mBackupFile.exists()) {
				//删除原来的文件
				mFile.delete();
				//把备份文件全路径改成 mFile的全路径,此时mFile读取的就是备份文件的了
				mBackupFile.renameTo(mFile);
			}
		}

		// Debugging
		if (mFile.exists() && !mFile.canRead()) {
			Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
		}

		Map map = null;
		StructStat stat = null;
		try {
			//获取文件信息,包括文件修改时间,文件大小等
			stat = Os.stat(mFile.getPath());
			if (mFile.canRead()) {
				BufferedInputStream str = null;
				try {
					//读取数据并且将数据解析为Map
					str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
					map = XmlUtils.readMapXml(str);
				} catch (XmlPullParserException | IOException e) {
					Log.w(TAG, "getSharedPreferences", e);
				} finally {
					IoUtils.closeQuietly(str);
				}
			}
		} catch (ErrnoException e) {
		}
		synchronized (SharedPreferencesImpl.this) {
			//加载数据成功,设置 mLoaded 为 true
			mLoaded = true;
			if (map != null) {
				//将解析得到的键值对数据赋值给 mMap
				mMap = map;
				//将文件的修改时间戳保存到 mStatTimestamp 中
				mStatTimestamp = stat.st_mtime;
				//将文件的大小保存到 mStatSize 中
				mStatSize = stat.st_size;
			} else {
				mMap = new HashMap<>();
			}
			//通知唤醒所有等待的线程
			notifyAll();
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93

1)将传进来的参数file以及mode分别保存在mFile以及mMode中

2)创建一个.bak 备份文件,写入失败时进行恢复工作

3)将存放键值对的mMap初始化为null

4)调用 startLoadFromDisk()方法加载数据

5)如果有备份文件,直接使用备份文件进行回滚
(删除原文件,把备份文件重命名为原文件的名字/用备份文件覆盖原文件)

6)第一次调用getSharedPreferences方法的时候,会从磁盘中加载数据,
数据的加载是通过【开启一个子线程调用loadFromDisk方法进行异步读取的

7)将解析得到的键值对数据保存在mMap中

8)将文件的修改时间戳以及大小分别保存在mStatTimestamp以及mStatSize中

(保存这两个值有什么用呢?我们在分析getSharedPreferences方法时说过,如果有其他进程修改了文件,
并且mode为MODE_MULTI_PROCESS,将会判断重新加载文件。
如何判断文件是否被其他进程修改过,没错,根据文件修改时间以及文件大小即可知道)

9)调用notifyAll()方法通知唤醒其他等待线程,数据已经加载完毕


3、通过 getXxx 方法能够进行读操作

final class SharedPreferencesImpl implements SharedPreferences {
	public String getString(String key, @Nullable String defValue) {
		//synchronized 关键字用于保证 getString 方法是线程安全的
		synchronized (this) {
			//方法 awaitLoadedLocked() 用于确保加载完数据并保存到 mMap 中才进行数据读取
			awaitLoadedLocked();
			//根据 key 从 mMap中获取 value
			String v = (String)mMap.get(key);
			//如果 value 不为 null,返回 value,如果为 null,返回默认值
			return v != null ? v : defValue;
		}
	}

	private void awaitLoadedLocked() {
		if (!mLoaded) {
			BlockGuard.getThreadPolicy().onReadFromDisk();
		}

		//前面我们说过,mLoaded 代表数据是否已经加载完毕
		while (!mLoaded) {
			try {
				//等待数据加载完成之后才返回继续执行代码
				wait();
			} catch (InterruptedException unused) {
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

1)getXxx方法是线程安全的,因为使用了synchronize关键字

2)getXxx方法是直接操作内存的,直接从内存中的mMap中根据传入的key读取value

3)getXxx方法有可能会卡在 awaitLoadedLocked() 方法,从而导致线程阻塞等待

(第一次调用getSharedPreferences方法时,会创建一个线程去异步加载和解析xml文件,
加载完成后把 mLoaded 设为true。加载完成之前调用 getXXX() 会阻塞调用线程。
异步加载xml完成时,会调用 notifyAll来唤醒所有等待线程。)

4、通过 put 方法进行写操作

public void put(String key, Object value) {
	...
	SharedPreferences.Editor editor = preferences.edit();
	
	editor.putString(key, String.valueOf(value));
	editor.putBoolean(key, Boolean.parseBoolean(String.valueOf(value)));                
	editor.putFloat(key, Float.parseFloat(String.valueOf(value)));	
	editor.putInt(key, Integer.parseInt(String.valueOf(value)));	
	editor.putLong(key, Long.parseLong(String.valueOf(value)));
	
	editor.apply();
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

put 操作,基于 Editor 类,它的实现类是 EditorImpl

final class SharedPreferencesImpl implements SharedPreferences {

	@Override
	public Editor edit() {
		synchronized (mLock) {
			//这里会阻塞线程,要等待 加载xml文件的线程完成加载后才能往下执行的
			awaitLoadedLocked();
		}
		//返回了一个 EditorImpl 对象
		return new EditorImpl();
	}
}


public final class EditorImpl implements Editor {

	/** 
	 * putXxx/remove/clear等写操作方法都不是直接操作 mMap 的,
	 * 而是将所有的写操作先记录在 mModified 中,
	 * 等到 commit/apply 方法被调用,
	 * 才会将所有写操作同步到内存中的 mMap 以及磁盘中
	 */
	private final Map<String, Object> mModified = Maps.newHashMap();
	
	private boolean mClear = false;

	public Editor putString(String key, @Nullable String value) {
		synchronized (this) {
			mModified.put(key, value);
			return this;
		}
	}
	...
	public Editor remove(String key) {
		synchronized (this) {
			mModified.put(key, this);
			return this;
		}
	}
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  1. SharedPreferences put 操作是线程安全的,使用了 synchronized 关键字
  2. 对键值对数据的增删操作,是保存再 mModified 这个Map中,不是直接操作 SharedPreferences.mMap
  3. mModified 在 commit/apply 方法中起到同步内存数据(SharedPreferences.mMap) 以及磁盘数据(xml文件)的作用

5、通过 commit 方法同步写磁盘

public final class EditorImpl implements Editor {
	
	public boolean commit() {
		
		/* 前面我们分析 put 的时候说过,写操作的记录是存放在 mModified 中的。
		 * commitToMemory() 方法就负责将 mModified 保存的写记录同步到内存中的 mMap 中
		 * 将 mModified 同步到 mMap 之后,清空 mModified 历史记录
		 * 并且返回一个 MemoryCommitResult 对象
		 */
		MemoryCommitResult mcr = commitToMemory();
		
		// enqueueDiskWrite 方法负责将数据落地到磁盘上
		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;
	}
}		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

1)调用 commitToMemory() 将 mModified 同步到 mMap 中,并清空 mModified

2)调用 enqueueDiskWrite() 将数据写入文件

3)同步等待写磁盘操作完成(commit() 方法会同步阻塞等待)

4)通知监听者(可以通过registerOnSharedPreferenceChangeListener方法注册监听)

5)返回执行结果:true / false


6、commitToMemory() 同步内存数据

public final class EditorImpl implements Editor {
	
	/**
	 * 内存数据同步
	 * 把 EditorImpl.mModified 中数据,同步到 SharedPreferences.mMap
	 */
	private MemoryCommitResult commitToMemory() {
		MemoryCommitResult mcr = new MemoryCommitResult();
		synchronized (SharedPreferencesImpl.this) {
			
			if (mDiskWritesInFlight > 0) {
				mMap = new HashMap<String, Object>(mMap);
			}

			//将 mMap 赋值给 mcr.mapToWriteToDisk,
			// mcr.mapToWriteToDisk 指向的就是最终写入磁盘的数据
			mcr.mapToWriteToDisk = mMap;

			//mDiskWritesInFlight 代表的是“此时需要将数据写入磁盘,但还未处理或未处理完成的次数”
			//将 mDiskWritesInFlight 自增1(这里是唯一会增加 mDiskWritesInFlight 的地方)
			mDiskWritesInFlight++;
			...
			synchronized (this) {
				//只有调用clear()方法,mClear才为 true
				if (mClear) {
					if (!mMap.isEmpty()) {
						mcr.changesMade = true;
						//当 mClear 为 true,清空 mMap
						mMap.clear();
					}
					mClear = false;
				}
				// 遍历 mModified
				for (Map.Entry<String, Object> e : mModified.entrySet()) {
					String k = e.getKey(); // 获取 key
					Object v = e.getValue(); // 获取 value
					
					//当 value 的值是 "this" 或者 null,将对应 key 的键值对数据从 mMap 中移除
					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;
					...
				}						
				//将 mModified 同步到 mMap 之后,清空 mModified 历史记录
				mModified.clear();
			}
		}
		return mcr;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

1)mDiskWritesInFlight自增1
mDiskWritesInFlight 表示 等待执行写入操作的次数
只有 commitToMemory() 对 mDiskWritesInFlight 进行增加, 其他地方都是减)

2)将mcr.mapToWriteToDisk 指向mMap,mcr.mapToWriteToDisk 就是最终需要写入磁盘的数据

3)判断 mClear的值,如果是true,清空mMap(调用clear()方法,会设置mClear为true)

4)同步 mModified 数据到 mMap 中,然后 清空mModified

5)最后返回一个 MemoryCommitResult 对象,这个对象的 mapToWriteToDisk 参数指向了最终需要写入磁盘的mMap

6)需要注意的是,
commitToMemory()方法中,当mClear为true,会清空mMap,但不会清空mModified
所以依然会遍历mModified,将其中保存的写记录同步到mMap中。

所以下面这种写法是错误的:
	sharedPreferences.edit()
		.putString("key1", "value1")    // key1 不会被 clear 掉,commit 之后依旧会被写入磁盘中
		.clear()//只会清空 SharedPreferences.mMap,然后继续把 mModified 同步到 清空的 mMap里
		.commit();
  • 1
  • 2
  • 3
  • 4
  • 5

7、通过 enqueueDiskWrite() 把数据写入文件

final class SharedPreferencesImpl implements SharedPreferences {
	
	private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
		
		//创建一个 Runnable 对象,该对象负责写磁盘操作
		final Runnable writeToDiskRunnable = new Runnable() {
			
			@Override
			public void run() {
				synchronized (mWritingToDiskLock) {
					//最终通过文件操作将数据写入磁盘的方法了
					writeToFile(mcr);
				}
				synchronized (SharedPreferencesImpl.this) {
					//写入磁盘后,将 mDiskWritesInFlight 自减1,代表写磁盘的需求减少一个
					mDiskWritesInFlight--;
				}
				//commit时,postWriteRunnable为null; apply时 postWriteRunnable 不为null
				if (postWriteRunnable != null) {							
					postWriteRunnable.run();
				}
			}
		};
		//如果传进的参数 postWriteRunnable 为 null,那么 isFromSyncCommit 为 true
		//温馨提示:从上面的 commit() 方法源码中,可以看出调用 commit() 方法传入的 postWriteRunnable 为 null
		final boolean isFromSyncCommit = (postWriteRunnable == null);
		//Typical #commit() path with fewer allocations, doing a write on the current thread.
		if (isFromSyncCommit) {
			boolean wasEmpty = false;
			synchronized (SharedPreferencesImpl.this) {
				//如果此时只有一个 commit 请求(注意,是 commit 请求,而不是 apply )未处理,那么 wasEmpty 为 true
				wasEmpty = mDiskWritesInFlight == 1;
			}
			
			if (wasEmpty) {
				//当只有一个 commit 请求未处理,那么无需开启线程进行处理,直接在本线程执行 writeToDiskRunnable 即可
				writeToDiskRunnable.run();
				return;
			}
		}
		
		//将 writeToDiskRunnable 方法线程池中执行
		//程序执行到这里,有两种可能:
		// 		1. 调用的是 commit() 方法,并且当前不止一个 commit 请求未处理
		//		2. 调用的是 apply() 方法
		QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
	}
	
	private void writeToFile(MemoryCommitResult mcr) {
		...
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

8、通过 apply 方法异步写磁盘

public final class EditorImpl implements Editor {
	
	public void apply() {
		//将 mModified 保存的写记录同步到内存中的 mMap 中,并且返回一个 MemoryCommitResult 对象
		final MemoryCommitResult mcr = commitToMemory();				
		final Runnable awaitCommit = new Runnable() {
			public void run() {
				try {
					mcr.writtenToDiskLatch.await();
				} catch (InterruptedException ignored) {
				}
			}
		};
		
		//这里会把 写入文件的任务添加到一个队列中
		//在Activity#onStop执行之前,会阻塞onStop,等待任务执行完
		QueuedWork.add(awaitCommit);
		
		Runnable postWriteRunnable = new Runnable() {
			public void run() {
				awaitCommit.run();
				QueuedWork.remove(awaitCommit);
			}
		};
		
		// 将数据落地到磁盘上,注意,传入的 postWriteRunnable 参数不为 null,所以在
		// enqueueDiskWrite 方法中会开启子线程异步将数据写入到磁盘中
		SharedPreferencesImpl.this.enqueueDiskWrite(mcr, 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);
	}
}		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

1)commitToMemory()方法,将 mModified 中记录的写操作同步回写到内存 SharedPreferences.mMap 中。
此时, 任何的 get 方法都可以获取到最新数据了

2)通过 enqueueDiskWrite() 方法,调用writeToFile将方法将所有数据【异步写入到文件中】

3)写入文件的任务添加到一个队列中,在Activity#onStop执行之前,会阻塞onStop,等待任务执行完

9、apply太多会阻塞 Activity#onStop()

ActivityThread # handleStopActivity() 中 QueuedWork.waitToFinish(),
API 11以上,会阻塞 onStop,等待 Sp写入文件任务完成

/**有SharedPreference频繁apply()导致卡顿的原因*/
public final class ActivityThread {
	...
	private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
        ActivityClientRecord r = mActivities.get(token);
        if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
            return;
        }
        r.activity.mConfigChangeFlags |= configChanges;
        StopInfo info = new StopInfo();
        performStopActivityInner(r, info, show, true, "handleStopActivity");
        updateVisibility(r, show);
        if (!r.isPreHoneycomb()) {
			/* 内部会等队列中所有任务执行完 
			 * SharedPreferences中apply()的任务都会添加到QueuedWork内部队列中
			 * 而处理该队列的线程只有一个HandlerThread
			 * 那么大量的apply()操作会导致这里要等待,从而导致UI卡顿
			 */
            QueuedWork.waitToFinish();
        }
        info.activity = r;
        info.state = r.state;
        info.persistentState = r.persistentState;
        mH.post(info);
        mSomeActivitiesChanged = true;
    }
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

10、总结

1)SharedPreferences是线程安全的,它的内部实现使用了大量synchronized关键字
2)SharedPreferences 不是【进程】安全的
3)第一次调用getSharedPreferences会加载磁盘 xml 文件(这个加载过程是异步的,通过new Thread来执行,构造SharedPreferences 时不会阻塞线程,但是会阻塞get/put/remove/clear等调用),

但后续调用getSharedPreferences会从内存缓存中获取。

第一次调用getSharedPreferences时还没从磁盘加载完毕,马上调用 get/put, 会阻塞线程,直到从磁盘加载数据完成后才返回
4)所有的 get 都是从内存 mMap 取的数据,数据来源于SharedPreferences.mMap
5)apply同步回写(commitToMemory())内存SharedPreferences.mMap,

再把异步回写磁盘的任务,放到一个单线程的线程池队列中等待执行
apply不需要等待写入磁盘完成,而是马上返回。

6)commit同步回写(commitToMemory())内存SharedPreferences.mMap,

mDiskWritesInFlight == 1 等待写入的次数为1,直接在调用commit的线程同步回写文件
mDiskWritesInFlight > 1 异步写入的任务放到一个单线程的线程池队列中等待执行
只有一个写入任务时,commit会阻塞调用线程,直到写入磁盘完成才返回

7)MODE_MULTI_PROCESS 每次 getSharedPreferences,检查文件上次修改时间、文件大小,所有修改则重新加载文件,不能保证多进程数据的实时同步
8)从 Android N 开始,不支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定, 直接抛异常

11、注意事项

1)不要使用SharedPreferences作为多进程通信手段。

没有使用跨进程的锁,就算使用 MODE_MULTI_PROCESS,SharedPreferences 跨进程频繁读写有可能导致数据全部丢失。
根据线上统计,SP 大约会有万分之一的损坏率

2) 每个 SP 文件不能过大。

SP 性能与文件大小相关,不同业务类型数据,拆分到不到文件保存,将频繁修改的条目单独隔离出来

3 SP 文件不能过大。第一次 getSharedPreferences,会先加载 SP 文件进内存,过大的 SP 文件会导致阻塞,甚至会导致 ANR
4) SP 文件不能过大。每次apply或者commit,都会把全部的数据一次性写入磁盘
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码维护者/article/detail/60744
推荐阅读
相关标签
  

闽ICP备14008679号