【源码】SharedPreference

本文详细解析了Android中SharedPreferences的工作原理,包括内部数据结构、读写流程、线程安全和多进程支持。分析了commit与apply的区别,以及可能导致ANR的情况。还探讨了数据持久化过程中的灾备文件策略。最后,总结了SharedPreferences的使用注意事项,如数据类型限制、线程安全、内存占用和避免大文件存储。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基础说明

涉及到几个类: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,后续再重新看一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值