androidGreenHand

V1

2023/04/13阅读:20主题:全栈蓝

android SP源码

SP

  1. 轻量级储存
  2. 以键值对(key-value)的方式保存数据的xml文件,其保存在/data/data/shared_prefs目录下

项目中已迁移使用MMKV或者DataStore,为什么还热衷于SP的源码呢?

有没有可能知道我不会MMKV或者DataStore源码呢,我想大概是的
TODO MMKV或者DataStore原理

读操作

当SharedPreferences对象第一次通过Context.getSharedPreferences()进行初始化时,对xml文件进行一次读取,并将文件内所有内容(即所有的键值对)缓到内存的一个Map中

    final class SharedPreferencesImpl implements SharedPreferences {
     private final File mFile;             // 对应的xml文件
     private Map<String, Object> mMap;     // Map中缓存了xml文件中所有的键值对
}  

备注:详细的过程,ANR部分再解析

写操作

抽象出了一个Editor类,不管某次操作通过若干次调用putXXX()方法,更新了xml中的几个键值对,只有调用了commit()方法,最终才会真正写入文件

commit 调用线程写操作

**commit 源码**:
   @Override
   public boolean commit() {
       ```
       MemoryCommitResult mcr = commitToMemory();//内存保存
       SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);//第二个参数为null
        ```
       return mcr.writeToDiskResult;
   }
   

可以看到enqueueDiskWrite的第二个参数为null

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable postWriteRunnable)
 
{
            final boolean isFromSyncCommit = (postWriteRunnable == null);//此时postWriteRunnable为null,isFromSyncCommit 则为true
    
            final Runnable writeToDiskRunnable = new Runnable() {
                    @Override
                    public void run() {
                        synchronized (mWritingToDiskLock) {
                            writeToFile(mcr, isFromSyncCommit);
                        }
                        synchronized (mLock) {
                            mDiskWritesInFlight--;
                        }
                        if (postWriteRunnable != null) {
                            postWriteRunnable.run();
                        }
                    }
                };
    
            // Typical #commit() path with fewer allocations, doing a write on
            // the current thread.
            if (isFromSyncCommit) {  //当调用commit方法时,isFromSyncCommit则为true
                boolean wasEmpty = false;
                synchronized (mLock) {
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                if (wasEmpty) {
                    writeToDiskRunnable.run();//主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储
                    return;
                }
            }
    
            QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
        }
    

postWriteRunnable为null,则isFromSyncCommit为true,代码会在主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储。这部分动作直接在主线程执行,如果文件较大,则主线程也会因为IO时间长造成ANR的。

apply 异步线程写操作

apply()方法设计的初衷是为了规避主线程的I/O操作导致ANR问题的产生,那么ANR的问题真得到了有效的解决吗?

apply 源码

   public void apply() {
      ```
         final Runnable awaitCommit = new Runnable() {
                 public void run() {
                     try {
                         mcr.writtenToDiskLatch.await();
                     } catch (InterruptedException ignored) {
                     }
                     
             };
      // 将 awaitCommit 添加到队列 QueuedWork 中
         QueuedWork.addFinisher(awaitCommit);

         Runnable postWriteRunnable = new Runnable() {
                 public void run() {
                     awaitCommit.run();
                     QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
                 }
             };
         SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
      ```
   }

apply方法最后调用了SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)

enqueueDiskWrite方法会把存储文件的动作放到子线程,这个方法的第二个参数postWriteRunnable做了两件事:

  1. 让awaitCommit执行 及执行 mcr.writtenToDiskLatch.await();
  2. 执行QueuedWork.remove(awaitCommit);
CountDownLatch

writtenToDiskLatch的类型是CountDownLatch,CountDownLatch是一个同步工具类,通过一个计数器来实现的同步。当一个线程完成了自己的任务调用countDown(),则计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程await()就可以恢复执行任务。

  1. countDown(): 对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。

  2. await(): 阻塞当前线程,将当前线程加入阻塞队列。

可以看到如果postWriteRunnable方法被触发执行的话,由于 mcr.writtenToDiskLatch.await()的缘故,UI线程会被一直阻塞住,等待计数器减至0才能被唤醒。

enqueueDiskWrite源码如下所示:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable postWriteRunnable)
 
{
            final boolean isFromSyncCommit = (postWriteRunnable == null);
            final Runnable writeToDiskRunnable = new Runnable() {
                    public void run() {
                        synchronized (mWritingToDiskLock) {
                            writeToFile(mcr, isFromSyncCommit);
                        }
                        synchronized (mLock) {
                            mDiskWritesInFlight--;
                        }
                        if (postWriteRunnable != null) {
                            postWriteRunnable.run();
                        }
                    }
                };
            // Typical #commit() path with fewer allocations, doing a write on
            // the current thread.
            if (isFromSyncCommit) {
                boolean wasEmpty = false;
                synchronized (mLock) {
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                if (wasEmpty) {
                    writeToDiskRunnable.run();
                    return;
                }
            }
            QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
        }
    

很明显postWriteRunnable不为null,程序会执行QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

writeToDiskRunnable:

  1. writeToFile(): 内容存储到文件
  2. postWriteRunnable.run()

QueuedWork.queue源码:

    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);
                }
            }
        }
    
    
    private static class QueuedWorkHandler extends Handler {
          static final int MSG_RUN = 1;
         
        QueuedWorkHandler(Looper looper) {
              super(looper);
          }
      
    
          public void handleMessage(Message msg) {
              if (msg.what == MSG_RUN) {
                  processPendingWork();
              }
          }
      }

就是把writeToDiskRunnable这个任务放到sWork这个list中,并且执行handler,根据HandlerThread的知识点,我们知道handlermessage里面就是子线程了。 接下来我们继续看handleMessage里面的processPendingWork()方法:

     private static void processPendingWork() {
            long startTime = 0;
    
            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
    
            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();
                    }
    
                    if (DEBUG) {
                        Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                                +(System.currentTimeMillis() - startTime) + " ms");
                    }
                }
            }
        }

是把sWork克隆给work,然后开启循环,执行work对象的run方法,及调用writeToDiskRunnable的run方法。上面讲过了,他里面做了两件事:

  1. 内容存储到文件
  2. postWriteRunnable方法回调

执行run方法的代码:

     final Runnable writeToDiskRunnable = new Runnable() {
                   public void run() {
                       synchronized (mWritingToDiskLock) {
                           writeToFile(mcr, isFromSyncCommit);//由于handlermessage在子线程,则writeToFile也在子线程中
                       }
                       synchronized (mLock) {
                           mDiskWritesInFlight--;
                       }
                       if (postWriteRunnable != null) {
                           postWriteRunnable.run();
                       }
                   }
               };
     

writeToFile方法我们不深入去看,但是要关注,里面有个setDiskWriteResult方法,在该方法里面做了如下的事情:

      void setDiskWriteResult(boolean wasWritten, boolean result) {
                this.wasWritten = wasWritten;
                writeToDiskResult = result;
                writtenToDiskLatch.countDown();//计数器-1
            }

当调用countDown()方法时,会对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。

问题:子线程为什么要去执行postWriteRunnable呢?子线程为什么需要await? 也没有看到会出现ANR的情况

数据的更新

xml文件中的数据会缓存到内存的mMap中,每次在调用editor.putXXX()时,实际上会将新的数据存入在mMap,当调用commit()或apply()时,最终会将mMap的所有数据全量更新到xml文件里。

   @Override
   public SharedPreferences getSharedPreferences(String name, int mode) {
     // name参数就是文件名,通过不同文件名,获取指定的SharedPreferences对象
   }

线程安全 ^^3把锁^^

   final class SharedPreferencesImpl implements SharedPreferences {
     // 1、使用注释标记锁的顺序
     // Lock ordering rules:
     //  - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
     //  - acquire mWritingToDiskLock before EditorImpl.mLock
   
     // 2、通过注解标记持有的是哪把锁
     @GuardedBy("mLock")
     private Map<String, Object> mMap;
   
     @GuardedBy("mWritingToDiskLock")
     private long mDiskStateGeneration;
   
     public final class EditorImpl implements Editor {
       @GuardedBy("mEditorLock")
       private final Map<String, Object> mModified = new HashMap<>();
     }
   }

第一把锁对于简单的 读操作 而言,我们知道其原理是读取内存中mMap的值并返回,那么为了保证线程安全,只需要加一把锁保证mMap的线程安全即可:

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

保证写操作的线程安全

对于写操作而言,每次putXXX()并不能立即更新在mMap中 ,如果开发者没有调用apply()方法,那么这些数据的更新理所当然应该被抛弃掉,但是如果直接更新在mMap中,那么数据就难以恢复。
因此,Editor本身也应该持有一个mEditorMap对象,用于存储数据的更新;只有当调用apply()时,才尝试将mEditorMap与mMap进行合并,以达到数据更新的目的。
因此,这里我们还需要另外一把锁保证mEditorMap的线程安全,不和mMap公用同一把锁的原因是,在apply()被调用之前,getXXX和putXXX理应是没有冲突的。

    public final class EditorImpl implements Editor {
      @Override
      public Editor putString(String key, String value) {
          synchronized (mEditorLock) {
              mEditorMap.put(key, value);
              return this;
          }
      }

而当真正需要执行apply()进行写操作时,mEditorMap与mMap进行合并,这时必须通过2把锁保证mEditorMap与mMap的线程安全,保证mMap最终能够更新成功,最终向对应的xml文件中进行更新。

文件的更新理所当然也需要加一把锁:

    // SharedPreferencesImpl.EditorImpl.enqueueDiskWrite()
    synchronized (mWritingToDiskLock) {
        writeToFile(mcr, isFromSyncCommit);
    }

文件损坏 & 备份机制

由于不可预知的原因(比如内核崩溃或者系统突然断电),xml文件的写操作异常中止,Android系统本身的文件系统虽然有很多保护措施,但依然会有数据丢失或者文件损坏的情况。

  1. 对文件进行备份,SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件:
    // 尝试写入文件
    private void writeToFile(...) {
      if (!backupFileExists) {
          !mFile.renameTo(mBackupFile);
      }
    }
  1. 尝试对文件进行写入操作,写入成功时,则将备份文件删除:
    // 写入成功,立即删除存在的备份文件
    // Writing was successful, delete the backup file if there is one.
    mBackupFile.delete();
  1. 若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃:
   // 从磁盘初始化加载时执行
   private void loadFromDisk() {
       synchronized (mLock) {
           if (mBackupFile.exists()) {
               mFile.delete();
               mBackupFile.renameTo(mFile);
           }
       }
     }

通过文件备份机制,我们能够保证数据只会丢失最后的更新,而之前成功保存的数据依然能够有效。

进程不安全

没有使用跨进程的锁,SharedPreferences是进程不安全的,在跨进程频繁读写会有数据丢失的可能 问题:这里如何解决?

ANR

写操作时ANR

Android系统在页面切换前,将数据写入文件。在ActivityThread的handlePauseActivity和handleStopActivity时会通过waitToFinish保证这些异步任务都已经被执行完成。如果这个时候过渡使用apply方法,则可能导致onpause,onStop执行时间较长,从而导致ANR。

   private void handlePauseActivity(IBinder token, boolean finished,
               boolean userLeaving, int configChanges, boolean dontReport, int seq)
 
{
          ......
               r.activity.mConfigChangeFlags |= configChanges;
               performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");
   
               // Make sure any pending writes are now committed.
               if (r.isPreHoneycomb()) {
                   QueuedWork.waitToFinish();
               }
              ......
       }
      public static void waitToFinish() {
             Handler handler = getHandler();
             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;
             }
     
            
         }
  1. 主线程执行processPendingWork()方法,把之前未执行完的内容存储到文件的操作执行完,这部分动作直接在主线程执行,如果有未执行的文件操作并且文件较大,则主线程会因为IO时间长造成ANR。
  2. 循环取出sFinishers数组,执行他的run方法。如果这时候有多个异步线程或者异步线程时间过长,同样会造成阻塞产生ANR。

sFinishers数组是在什么时候add数据的? sFinishers的addFinisher方法是在apply()方法里面调用的,代码如下:

     @Override
     public void apply() {
         ......
           // 将 awaitCommit 添加到队列 QueuedWork 中
           QueuedWork.addFinisher(awaitCommit);
     
         Runnable postWriteRunnable = new Runnable() {
           @Override
           public void run() {
             awaitCommit.run();
             QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
           }
         };
         ......
      }

由于UI线程循环取出sFinishers数组,执行他的run方法造成阻塞产生ANR addFinisher刚刚上面提到是在apply方法中调用,入参awaitCommit,他的run方法如下:

        final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
              try {
                mcr.writtenToDiskLatch.await();//阻塞
              } catch (InterruptedException ignored) {
              }
              if (DEBUG && mcr.wasWritten) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                      + " applied after " + (System.currentTimeMillis() - startTime)
                      + " ms");
              }
            }
        };
        

不难看出,就是调用CountDownLatch对象的await方法,阻塞当前线程,将当前线程加入阻塞队列。所以如果过渡使用apply方法,则可能导致onpause,onStop执行时间较长从而导致ANR。

首次读操作时ANR

sp 文件创建以后,会单独的使用一个线程来加载解析对应的 sp 文件。但是当 UI 线程尝试访问 sp 中内容时,如果 sp 文件还未被完全加载解析到内存,此时 UI 线程会被 block,直到 SP 文件被完全加载到内存中为止

sp 被创建的时候会同时启动一个线程加载对应的 sp 文件,执行 startLoadFromDisk(); 在 startLoadFromDisk()时,标记 sp 不可使用状态,后期无论是尝试读数据或者写数据,读写线程都会被 block,直到 sp 文件被全部加载解析完毕。

  源码
   @UnsupportedAppUsage
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }

    @UnsupportedAppUsage
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

sp 文件完全加载解析到内存中,直接唤起所有在等待在当前 sp 的读写线程。

   private void loadFromDisk() {
        synchronized (mLock) {
           
        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);
                } 
              }
        } 

        synchronized (mLock) {
            mLoaded = true;
            mLock.notifyAll();
            
        }
    }

当UI线程读取SP的值时,可能会发生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;
        }
    }

   @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

分类:

移动端开发

标签:

移动端开发

作者介绍

androidGreenHand
V1