【源码】SharedPreference

基础说明

涉及到几个类:ContextImpl、SharedPreferencesImpl、QueueWork、ActivityThread,代码版本基于Android Q
假设有这么一个文件:

	//文件名
	public static final String SPNAME_USER = "userInfo";
	//保存着两个键值对
	public static final String USER_ID = "userId";
    public static final String USER_NAME = "userName";

	public SharePreferenceTest(Context context,String spName){
            sharedPreferences = context.getSharedPreferences(spName,Context.MODE_PRIVATE);
            editor = sharedPreferences.edit();
    } 

变量说明

  • 1、mFile:源数据文件,对应SPNAME_USER.xml文件
  • 2、mBackupFile:灾备文件,对应SPNAME_USER.xml.bak文件
  • 3、mMapmFile文件里面的具体键值对,对应例如:USER_ID:123、 USER_NAME:法外狂徒张三,间接保存在内存中
  • 4、CountDownLatch
    • (1)new CountDownLatch(1):创建一个值为count 的计数器
    • (2)await:阻塞调用方法的线程,如果如果当前计数为零,则此方法不执行阻塞,立即返回(很重要)
    • (3)countDown:对计数器进行递减1操作
  • 5、renameTo:重命名操作,我们可以理解为将A文件移动到B文件所在的目录,并以B的文件名存在(前提是B已经被删除),移动过去之后会将A删除
  • 6、mModifiedput方法所操作的临时哈希表,最终会在commitToMemory将数据转移到MemoryCommitResultmapToWriteToDisk,但由于是将mMap的引用赋值给mapToWriteToDisk,所以对mapToWriteToDisk赋值也就是对mMap赋值,因此将数据保存在SharedPreferencesImpl中,又因为SharedPreferencesImpl实际是被缓存在静态变量sSharedPrefsCache中,所以最终是保存在内存

源码

ContextImpl

1、sSharedPrefsCache

ContextImpl的静态变量,存储了FileSharedPreferencesImpl的关系,好处是可以不需要每次都新建SharedPreferencesImpl,但也有需要注意的点:SharedPreferencesImpl持有着mMap对象,如果数据量太大的话是会占据一定的内存空间的

    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

2、mSharedPrefsPaths

一个文件名对应一个xml文件,是getSharedPreferences(String name, int mode)用来获取File从而调用getSharedPreferences(File file, int mode)的方法。

   private ArrayMap<String, File> mSharedPrefsPaths;

3、getSharedPreferences(根据FileName获取)

通过定义的fileName去获取一个SharedPreferences,这是我们平常使用比较多的方法。
mSharedPrefsPaths是一个ArrayMap<String, File>数据结构,保存着文件名对应的文件,这里主要做了:

  • 1、判断mSharedPrefsPaths是否初始化过,没有的话进行初始化
  • 2、判断文件名对应的文件是否存在,不存在则新建
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        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);
    }

2、getSharedPreferences(根据File获取)

这里做了:

  • 1、获取保存在静态变量中的ArrayMap<File, SharedPreferencesImpl>,如果不存在则新建,注意这里会调用到SharedPreferencesImpl的构造函数,这里面开启了一个线程去获取本地文件,这个下面再展开讲
  • 2、当modeMODE_MULTI_PROCESS时,通过startReloadIfChangedUnexpectedly重新调用startLoadFromDisk去磁盘获取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;
            }
        }

        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

SharedPreferencesImpl

1、SharedPreferencesImpl

这里有几个值得关注的点:

  • 1、将file保存在mFile
  • 2、创建一个mBackupFile,这是一个灾备文件
  • 3、构造函数调用了startLoadFromDisk
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }

2、startLoadFromDisk

这里做了:

  • 1、加锁更改mLoaded变量
  • 2、新建一个线程去调用loadFromDisk
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

3、loadFromDisk

这里有几个重要的点:

  • 1、如果灾备文件存在,则使用灾备文件作为mFile,这里主要是和writeToFile相呼应:如果writeToFile写入磁盘失败的话,是会保留灾备文件mBackUpFile同时删除源文件mFile,下次初始化SharedPreferencesImpl进入这个方法时会使用灾备文件作为源数据文件
  • 2、将文件读取到一个map里面,这里解析xml的方式和LayoutInflater一样使用XmlPullParserException
  • 3、最后调用了mLock.notifyAll(),根据这行代码大概就可以知道肯定在某处有调用wait操作来等待这个方法的结束
   private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            //renameTo的作用就是:
            //假设有A、B两个文件,调用A.renameTo(B),会将A移动
            //到B所在的目录,并使用B的文件名(前提是B已经被删除)
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
		
		//将文件里的内容读到一个map里面
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        }

        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
			
            try {
            	//只有上面读文件出错了,thrown才会为null
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                mLock.notifyAll();
            }
        }
    }

4、getString

可以看到有几个值得关注的点:

  • 1、get方法都是加了synchronized锁的,所以多线程情况下的读操作性能都不会很好
  • 2、通过强转来转换要得到的目标数据类型,所以这里会有类型安全的隐患,当put进去的和get出来的类型不一致就有可能会出现闪退,这在多人协作开发时出现的概率更高
  • 3、awaitLoadedLocked等待唤醒,而这里等待的就是loadFromDisk这个方法,所以:如果一个xml不幸保存了一些很大的数据,那再第一次初始化SharedPreferencesImpl的时候立刻调用get方法,是有很大概率是会造成ui阻塞从而导致卡顿的。

关于3
如果在应用启动的时候初始化SharedPreferencesImpl,表现出来就是会白屏,因为这个消息处理的时间太长,导致后续的ui绘制消息一直得不到处理(就算是同步屏障消息也得等正在处理的消息处理完)。同时也有可能会造成ANR,因为我实验过取出一个200Mxml,通过systrace可以看到线程大概睡眠了2-3秒,假设在没有设置android:largeheap = "true"的情况下,200m已经接近oom了(不同厂商手机有不同的APP最大堆空间,这是我用华为手机得出的数据),而ANR5s内没有响应触摸事件(也就是新入队的消息没有及时得到处理),也就是说在设置android:largeheap = "true"的情况下,当xml文件足够大,大到可以让主线程阻塞5s以上,再点击屏幕时就会造成ANR

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

1、awaitLoadedLocked

可以看到在loadFromDisk还没有mLock.notifyAll()之前,这里是会一直wait阻塞的

    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
		...
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        ...
    }


1、contains

也是加锁wait

    @Override
    public boolean contains(String key) {
        synchronized (mLock) {
            awaitLoadedLocked();
            return mMap.containsKey(key);
        }
    }

1、edit

获取Editor的方法,同样是加锁wait,要注意每次调用edit都会新建一个EditorImpl对象,所以最好不要多次调用edit,而是创建一个保存在单例工具栏中

    @Override
    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

EditorImpl

1、mModified

SharedPreferencesImpl内部类EditorImpl的一个变量,代表着通过put添加或者修改的数据

		@GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

1、putString

可以看到put方法同样也是加了synchronized,这说明对于多线程的读写性能都有一定的影响,但是并不会影响到单线程,因为synchronized是有一个锁升级的过程,单线程多次获取同一个锁的话synchronized是不会升级成重量级锁的,而是保持在偏向锁的状态。

        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

1、remove

删除对应keyvalue用的是this,根据commitToMemory的注释说这是一个magic value,也就是这个类的编写者自己规定好的规则,我们可以将它理解为null

        @Override
        public Editor remove(String key) {
            synchronized (mEditorLock) {
                mModified.put(key, this);
                return this;
            }
        }

1、clear

可以看到clear方法只是将标志变量设置为true,内部并没有立刻调用commit或者apply,需要手动调用

        @Override
        public Editor clear() {
            synchronized (mEditorLock) {
                mClear = true;
                return this;
            }
        }

1、commit(重要)

commit提交有几个特点:

  • 1、在调用线程执行enqueueDiskWrite,从而执行writeToFile写入磁盘
  • 2、commit有返回值:mcr.writeToDiskResult
  • 3、commitapply的提交一样都是分两步:内存和磁盘;这里可以看到commitcommitToMemoryenqueueDiskWrite(writeToFile)都是执行在调用线程的,这也就是网上说的:commit的原子性是包括提交到内存和磁盘

注意这里SharedPreferencesImpl.this.enqueueDiskWrite和mcr.writtenToDiskLatch.await(),我一度以为CountDownLatch是以lock-unlock的调用形式进行同步的,所以一直想不通怎么会先通过countDown唤醒,再调用await锁定呢?其实这里的逻辑主要是enqueueDiskWrite调用到writeToFile执行setDiskWriteResult,从而调用countDown将计数器设置为0,然后await的时候发现计数器为0是不会阻塞的,直接跳过

        @Override
        public boolean commit() {
            long startTime = 0;

			//得到一个MemoryCommitResult对象
            MemoryCommitResult mcr = commitToMemory();

			//注意第二个参数传了null,而apply是传了具体的runnable
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null);
                
            try {
            	//在主线程调用await阻塞
                mcr.writtenToDiskLatch.await();
            }
            //回调监听
            notifyListeners(mcr);
            //返回结果值
            return mcr.writeToDiskResult;
        }

1、commitToMemory

这个方法主要做了几件事:

  • 1、只有进入了commitToMemory方法,mDiskWritesInFlight才会大于0,所以这里个人理解是为了避免单线程短时间内多次进入这个方法,导致下面调用HashMap的遍历时出现fail-fast(快速失败),所以这里直接拷贝一个map来操作
  • 2、处理所有监听器
  • 3、遍历出mModified的所有数据,除去被remove掉或者是不存在的那些,剩下的保存在mapToWriteToDisk这个Map变量中,这就相当于将mModified过滤后保存在mapToWriteToDisk
  • 3、将监听器、过滤后的数据、当前修改版本号、被修改过的值所对应的key全部存入一个MemoryCommitResult对象中

注:这个方法叫做“提交到内存”,但乍一看貌似没有对mMap做什么操作,但实际上是将mMap的引用赋值给了mapToWriteToDisk,所以修改了mapToWriteToDisk也就是修改mMap

		private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            //那些数据已经发生变化的所对应的key
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            //临时变量,用来保存mModified过滤后,需要被写入
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
            	//只有进入了这个方法,mDiskWritesInFlight才会大于0
                if (mDiskWritesInFlight > 0) {
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;
				
				//处理所有的监听器,最终都是要保存在MemoryCommitResult对象中
                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;
					
					//判断数据是否有改变
					//如果没有改变的话,提交任务的时候并不会执行,而是直接return
                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }
					
					//取出mModified中所有的k-v
					//除去被remove掉或者是不存在的那些
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        //根据注释,这个this算是一个"magic value",用来替代null
                        //所以remove那里也是置的this
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }

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

					//清空mModified,因为有用的数据已经存在mapToWriteToDisk中了
                    mModified.clear();

					//如果改变过数据,则将修改版本号+1
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

MemoryCommitResult

commitToMemory的返回值,里面保存了:

  • 1、修改的版本号memoryStateGeneration
  • 1、已过滤的数据mapToWriteToDisk
  • 2、已修改的key列表
  • 3、CountDownLatch同步工具类
  • 4、监听器OnSharedPreferenceChangeListener

setDiskWriteResult方法是用来表明写入是否成功,成功或失败后都会调用writtenToDiskLatch.countDown将计数器-1从而达到唤醒阻塞线程的作用

    private static class MemoryCommitResult {
        final long memoryStateGeneration;
        @Nullable final List<String> keysModified;
        @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
        final Map<String, Object> mapToWriteToDisk;
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

        @GuardedBy("mWritingToDiskLock")
        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
                @Nullable Set<OnSharedPreferenceChangeListener> listeners,
                Map<String, Object> mapToWriteToDisk) {
            this.memoryStateGeneration = memoryStateGeneration;
            this.keysModified = keysModified;
            this.listeners = listeners;
            this.mapToWriteToDisk = mapToWriteToDisk;
        }

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
    }

1、enqueueDiskWrite(重要)

commitapply都会调用到这个方法

对于commit

  • 1、isFromSyncCommittrue
  • 2、commit主线程调用writeToDiskRunnable.run(),从而触发writeToFile

对于apply

  • 1、isFromSyncCommitfalse
  • 2、apply将任务通过Handler发送到QueuedWork内部的HandlerThread中,也就是在子线程中处理
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //如果是apply,这个变量会是true
        //如果是commit,这个变量是false
        final boolean isFromSyncCommit = (postWriteRunnable == null);

		//创建一个writeToDiskRunnable 
		//这个runnable主要执行了writeToFile和postWriteRunnable
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

		//这是commit才会执行的逻辑
		//在当前线程执行writeToDiskRunnable.run()
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            //只有在writeToDiskRunnable里面才会执行mDiskWritesInFlight--将
            //值减为0,所以这里为true
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            //执行writeToDiskRunnable
            //也就是执行了writeToFile和postWriteRunnable
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
		
		
		//这是apply才会执行的逻辑
		//将任务加入队列,内部逻辑是将任务通过handler发送到
		//创建的子线程的MessageQueue中
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

1、writeToFile

这里主要做了:

  • 1、判断mFile是否存在,根据loadFromDisk可以知道原本的mFile已经被delete了,现在存在的mFile是原来的mBackUpFile通过renamTo顶替过去的,同时mBackUpFile已经不存在了,所以fileExiststrue
  • 2、根据版本号是否变化判断变量needsWrite,如果needsWritefalse即代表数据没有发生变化,不需要做无谓的IO操作,直接return
  • 3、判断灾备文件是否存在,如果存在,将mFile删除;如果不存在,将mFile通过renameTo移动到mBackUpFile的位置并重命名,同时将mFile删除(因为renameTo),虽然mFile一定会被删除,但是delete后并不会使mFile == null
  • 4、将MemoryCommitResult里面通过commitToMemory方法得到的数据mapToWriteToDisk写入xml,再通过FileUtils.sync(str)将数据强制写入磁盘文件(为了提升 I/O 性能,文件系统把数据写入到 Page Cache 中,然后等待合适的时机才会真正的写入磁盘),这里面分成两种情况:
    • (1)写入成功:将mBackUpFile删除
    • (2)写入失败:保留mBackUpFile,删除mFile,这里和loadFromDisk相呼应。假设这次写入因为异常原因(例如过热关机)而失败,那下次初始化SharedPreferencesImpl调用loadFromDisk时会使用mBackUpFile作为源文件
    @GuardedBy("mWritingToDiskLock")
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...

		//判断文件是否存在
        boolean fileExists = mFile.exists();


        if (fileExists) {
            boolean needsWrite = false;
			//判断版本是否已经改变,如果数据没有变化的话是不需要更新的
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
			
			//needsWrite为false
			//也就是数据没有变化,调用setDiskWriteResult通过
			//CountDownLatch唤醒被阻塞的线程
            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
			
			//对于初始化来说这里是false
            boolean backupFileExists = mBackupFile.exists();

			//如果灾备文件不存在,将mFile作为灾备文件,并删除mFile(因为renameTo)
			//如果灾备文件存在,删除mFile
			//但是要注意,delete后并不代表mFile就为null了
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }
        
		//到了这一步就已经将数据保存在灾备文件里面了(什么数据?)

		//
        try {
            FileOutputStream str = createFileOutputStream(mFile);
			...
			//通过XmlUtils工具类写入文件
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
			...
			//为了提升 I/O 性能,文件系统把数据写入到 Page Cache 中
			//,然后等待合适的时机才会真正的写入磁盘
			//而通过sync方法可以不需要等待,强制让数据写入文件
            FileUtils.sync(str);
			...
            str.close();
            ...
			
			//已经写入
			//删除灾备文件
            mBackupFile.delete();
			//更新 修改版本号
            mDiskStateGeneration = mcr.memoryStateGeneration;
			//
            mcr.setDiskWriteResult(true, true);
			...
			
			//注意这个return,如果写入成功的话,是不会执行下面的代码的
            return;
        }catch(){
			...
		}
		 
		//执行到这里说明写入失败了
		//将mFile删除 
        if (mFile.exists()) {
            if (!mFile.delete()) {
            	...
            }
        }
        mcr.setDiskWriteResult(false, false);
    }

1、apply

这里有几个点:

  • 1、获取一个MemoryCommitResult
  • 2、创建一个awaitCommitawaitCommit这个Runnable的执行内容是CountDownLatch.await(),它的add时机在本方法内,remove时机在postWriteRunnable这个Runnablerun方法,而它执行run的时机在QueuedWork.waitToFinish里面,重点来了:waitToFinish在Activity和Service的多个生命周期里面会调用,所以如果迟迟得不到remove,是有可能会造成ANR的,那调用remove方法的postWriteRunnable什么时候执行呢?是在QueueWork.processPendingWork中,这个方法会等writeToFile执行完再会执行
  • 3、将postWriteRunnable作为enqueueDiskWrite方法的参数,这会导致enqueueDiskWrite方法里面的isFromSyncCommit变量为false,从而区分commitapply
        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
			//过滤完mModified的值后,保存在MemoryCommitResult中
            final MemoryCommitResult mcr = commitToMemory();
            //创建一个awaitCommit
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                        	//CountDownLatch是一个同步工具类,允许一个或
                        	//多个线程一直等待,
                        	//直到其他线程运行完成后再执行。
                        	//这行代码运行在主线程,因此是会让主线程阻塞
                            mcr.writtenToDiskLatch.await();
                        }
                    }
                };
			
			//给QueuedWork添加一个runnable
            QueuedWork.addFinisher(awaitCommit);
		
			//创建一个postWriteRunnable,这个runnable是用来触发上面那个awaitCommit的
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
			//
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            //回调监听
            //所以apply也并不是拿不到操作结果
            notifyListeners(mcr);
        }

notifyListeners

这里要注意的是:

  • 1、onSharedPreferenceChanged执行在主线程
  • 2、注册监听的时候不要用匿名内部类,不然会注销不了,而且listener是一个WeakHashMap,很容易因为被回收而导致mcr.listeners == null
  • 3、只会回调value有变化key
		private void notifyListeners(final MemoryCommitResult mcr) {
            if (mcr.listeners == null || mcr.keysModified == null ||
                mcr.keysModified.size() == 0) {
                return;
            }
            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(() -> notifyListeners(mcr));
            }
        }

QueuedWork

1、getHandler

获取一个绑定了HandlerThread LooperHandler

    @UnsupportedAppUsage
    private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();

                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

1、waitToFinish(重要)

handleServiceArgs(Service.onStartCommand)、handleStopService(Service.onDestroy)、handlePauseActivity(Activity.onPause)、handleStopActivity(Activity.onStop)等生命周期中都有调用
但在 Android 8.0之前和之后的实现有些不同,8.0之后会在调用这个方法的时候尝试通过processPendingWork去触发任务

    public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
        	//这里
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;
            }
        }
    }

1、queue

apply入队是有100msDELAY

    @UnsupportedAppUsage
    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

1、processPendingWork

这里有几个主要的点:

  • 1、如果在100ms之内多次调用apply,是会一次性将这100ms queuemsg全部取出来一并处理的,这样就可以不需要每个消息都等待100ms(但是这里是遍历出所有的runnable执行run,并没有网络上说的只执行最后一次apply,还是说我看漏了?)
    private static void processPendingWork() {
        long startTime = 0;

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }

            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }

            }
        }
    }

ActivityThread

1、handleServiceArgs

可以看到这里会等待任务完成,而这些方法在AMS层启动的时候都会发送一个延时的ANR消息到MessageQueue的,如果这里阻塞太久而导致无法通知AMSremove掉这个消息,就会造成ANR
Android 8.0我们可以看到有所优化:主动触发任务进行

    private void handleServiceArgs(ServiceArgsData data) {
		res = s.onStartCommand(data.args, data.flags, data.startId);
		...
		QueuedWork.waitToFinish();
	}

1、handleStopService

同上

        private void handleStopService(IBinder token) {
        	s.onDestroy();
        	...
        	QueuedWork.waitToFinish();
        }

1、handlePauseActivity

同上

    @Override
    public void handlePauseActivity(){
    	performPauseActivity(r, finished, reason, pendingActions);
		...
		if (r.isPreHoneycomb()) {
             QueuedWork.waitToFinish();
        }
    }

1、handleStopActivity

同上

    @Override
    public void handleStopActivity(){
    	performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
                reason);
		...
		if (r.isPreHoneycomb()) {
             QueuedWork.waitToFinish();
        }
    }

1、handleSleeping

同上

    private void handleSleeping() {
		if (!r.stopped && !r.isPreHoneycomb()) {
                callActivityOnStop(r, true /* saveState */, "sleeping");
            }
		...
		if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
	}

总结

  • 1、支持的存储类型:String、StringSet、Int、Long、Float、Boolean
  • 2、保存着K-VmMap会通过静态变量sSharedPrefsCache被间接保存在内存中
  • 3、通过强转获取数据,有可能导致类型不安全而闪退
  • 4、方法都用了synchronized加锁,是线程安全
  • 5、MODE_MULTI_PROCESS只是重新从磁盘获取文件,并不能用于多进程通信
  • 6、调用getSharedPreferences时会阻塞直到文件被读出来,所以不适合一个文件里面放很大的数据,可以考虑分成多个小文件(但这些小文件一样会占据着内存空间)
  • 7、提交分为commitapply
    • commit
      • (1)内存写入和磁盘写入都运行在调用线程
      • (2)有写入结果的返回值
    • apply
      • (1)内存写入运行在主线程,磁盘写入运行在子线程
      • (2)无返回值
      • (3)在100ms内多次调用apply,系统会将这期间内的所有写入磁盘任务一起执行,从而避免每个任务都等待100ms
  • 8、onSharedPreferenceChanged运行在主线程
  • 9、commitapply都无法避免ANRcommitANR是因为提交过程在调用线程(假设是ui线程),如果文件太大阻塞了5s以上,这时候系统再接收到一个触摸事件,就会抛出ANR错误;applyANRActivityService在多个生命周期里面会通过QueueWork.waitToFinish等待任务完成,而ActivityService在执行这些生命周期过程中AMS也是会参与的,在服务端AMS执行的时候会发送一个ANR延时消息,假设客户端在回调完生命周期之后没有及时告知AMSremoveANR消息的话,就会触发ANR
  • 10、写入过程中意外退出是会丢失数据的,关于灾备文件的逻辑:在初始化SharedPreferencesImpl时不会创建灾备文件(只是创建了个File文件对象),在磁盘写入方法writeToFile时会判断灾备文件是否存在,如果不存在就将源数据文件mFile作为灾备文件,假如写入过程中失败,则会将源数据文件丢弃,保留灾备文件,下次重新启动应用初始化SharedPreferencesImpl检测到灾备文件存在,就会用灾备文件代替源数据文件

没懂的地方

1、writeToFile

mFile其实在renameTo后就不存在了,为什么还要删除多一次?


2、网上提到的apply非全量写入体现在哪

等有时间再扫多一遍


3、SharedPreferences接口在commit和apply方法的注释写着Note that when two editors are modifying preferences at the same time, the last one to call commit wins

这个是我看漏了吗,貌似多次调用一样是遍历调用Runnable.run,后续再重新看一下

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值