SP源码分析

825 阅读6分钟
先上图,由于是试用版软件,有水印了,实在抱歉了。

SP原理分析1.png

步骤说明

1、获取SharedPreferences对象

ContextImpl:
public SharedPreferences getSharedPreferences(String name, int mode) {
}
public SharedPreferences getSharedPreferences(File file, int mode) {
}

说明:

1、两个重载方法,第一个会调用第二个方法。

2、传入File的方法,可用于跨进程读写(首先此文件支持快进程读写权限),但一般现在这个模式已经不建议使用了,存在数据缺失问题,后面再提。

3、中间缓存有两个map说明下:

private ArrayMap<String, File> mSharedPrefsPaths;//通过name获取对应的文件对象
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;//通过包名获取对应应用的SharePreferencesImpl集合。再通过File获取SharePreferencesImpl

2、创建SharedPreferencesImpl对象中读取对应的xml文件

private void startLoadFromDisk() {    
    synchronized (mLock) {        
        mLoaded = false; //用于判断是否加载完成,处理多线程问题  
    }    
    new Thread("SharedPreferencesImpl-load") {
        public void run() {            
            loadFromDisk();        
        }    
    }.start();}
private void loadFromDisk() {    
    synchronized (mLock) { 
        if (mLoaded) {  //用于判断是否加载完成,处理多线程问题            
            return;        
        }        
        if (mBackupFile.exists()) {
            //多进程间创建不同SharedPrefrences对象,备份文件存在说明有进程正在写入数据,
            //或者写入失败,导致备份文件未被删除,所以在此要讲失败的文件删除,
            //将备份文件转为正式文件。       
            //所以说多进程间不安全,这是个原因。
            mFile.delete();
            mBackupFile.renameTo(mFile);        
        }    
    }   ……
        mLock.notifyAll();    
}

说明:

1、mLoaded: //用于判断是否加载完成,同步锁处理多线程问题

2、判断是否有备份文件(name.xml.bak),若存在有两个原因,一是先前写入文件异常导致备份文件未删除,另一个原因是另一个进程正在写入文件,当前进程准备写入,由于进程不同,创建的对象肯定是不同的,但文件又是同一个,所以出现了这种把文件直接删除的问题,导致新写入的问题异常。(未实践验证)

3、将xml文件中的数据加载到内存map中。并唤醒所有等待mLock锁的代码块。

3、创建EditorImpl对象

 public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();//等待mLoaded=true,即等待加载数据完成
        }
        return new EditorImpl();
 }

说明:

awaitLoadedLocked()就是轮询等待mLoaded为true,若不是则等待获取mLock锁,等加载完数据被唤醒。这里如果加载的xml文件特大,实践耗时较长,容易导致ANR。

4、给Editor添加数据

public final class EditorImpl implements Editor {
 private final Map<String, Object> mModified = Maps.newHashMap();
 public Editor putInt(String key, int value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
  }
   public Editor clear() {
            synchronized (mLock) {
                mClear = true;
                return this;
            }
    }
}

说明:

1、添加数据也加了同步锁,防止多线程数据异常

2、clear方法可以清除掉先前所有老数据,配合commitToMemory()方法,在方法中会判断clear,若true则清除原有读取的map数据,使用新的mModified的数据。

5、处理添加进入的数据

   private MemoryCommitResult commitToMemory() {
    if (mClear) {
        if (!mMap.isEmpty()) {
             changesMade = true;
              mMap.clear();//清除原来数据
         }
            mClear = false;
         }
     for (Map.Entry<String, Object> e : mModified.entrySet()) {
         …… //判断是否有key,key对应的的value是否相同,不同则替换原来的map数据        
      }
 return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk);
       //这个对象还是很有用的,类中setDiskWriteResult方法用于结束sp的写入,其中有个CountDownLatch,apply和commit结束都需要等待此闭锁。

说明:

1、mClear==true会清除原有数据

2、遍历新增加的数据,和原有数据对比,若无对应key或相应key对应的value不同,则覆盖添加。

3、MemoryCommitResult:这个对象还是很有用的,类中setDiskWriteResult方法用于结束sp的写入,其中有个CountDownLatch,apply和commit结束都需要等待此闭锁。

6、apply()异步提交数据

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

这中间用到了很多嵌套的runnable暂时没弄明白,可能和QueuedWork原理有关,后期再研究。

但是否异步的原因是取决于enqueueDiskWrite()的参数。

7、commit()同步提交数据

SharedPreferencesImpl.this.enqueueDiskWrite(    mcr, null /* sync write on this thread okay */);

直接调用enqueueDiskWrite(),并用闭锁直接等待,直到sp写入数据完成才往下走,所以是同步的,这个会返回写入的结果成功/失败。

8、enqueueDiskWrite处理同步异步提交数据方式

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);//同步提交还是异步
if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();//同步就直接运行,并返回
                return;
            }
   }
 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);//异步添加到队列中
}

说明:

1、使用参数postWriteRunnable==null来区分同步异步

2、若wasEmpty==true说明暂时还没有线程正在写文件,mDiskWritesInFlight这个数据是在步骤5中进行++的,若==1说明,只有现有的一个提交认数,所以直接执行。

3、若wasEmpty==false,即使是同步提交也需要加入到队列中,但不需要延迟,这个可以看QueuesWork原理。

9、写入文件

// Note: must hold mWritingToDiskLockprivate void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
  if (fileExists) {
            if (!needsWrite) {
            //若不需要现在写,直接中止,这是apply异步执行的结果
                mcr.setDiskWriteResult(false, true);
                return;
            }
​
            boolean backupFileExists = mBackupFile.exists();
​
            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }
​
//备份文件不存在则将原来的文件重命名为备份文件,这个可以看下面的打点的截图。若存在,那说明先前写文件时出现了问题,所以将现有文件删除,这还是有个问题--多进程之间的问题,会导致正在另一个进程正在写入中断,或者数据混乱。
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
            …………
    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    ……
    //根据mode修改文件的权限
    ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
  }

说明:

1、由于writeToFile()调用前有个写锁锁住,所以同一进程间不会有问题,但多进程间就会有问题。

2、文件已经存在的情况做出了处理,判断是否需要执行写入(needsWrite这个逻辑还是似懂非懂,后面再研究)

3、文件已经存在的情况做出了处理,判断备份文件是否存在,若不存在,则将现有文件重命名为备份文件

1630141173974.png

4、文件已经存在的情况做出了处理,判断备份文件是否存在,若存在,那说明先前写文件时出现了问题,所以将现有文件删除,这就有另外一个问题--多进程之间的问题,会导致正在另一个进程正在写的文件被删除,导致数据异常。

总结:

差不多就读到这了,后面再去看看MMVK的原理吧,看完SP,还是感觉性能确实比较差,多进程间的安全性也成问题。

建议:

1、建议使用私有模式,不要进行跨进程处理,非要跨进程可以通过进程通信的方式+sp,或者使用contentProvide。

2、建议一个xml文件不要存放太多东西,防止写入时间过长导致ANR。

3、优先考虑apply写入数据,只有对写入结果非常重要的情况下才用commit

4、apply和commit在添加完所有数据后,再调用,只执行一次,防止多此写文件。

\