Android sp相关问题

1,332 阅读4分钟

SP推荐实践

1.在工作线程中写入sp时,直接调用commit就可以,不必调用apply,这种情况下,commit的开销更小

2.在主线程中写入sp时,不要调用commit,要调用apply

3.sp对应的文件尽量不要太大,按照模块名称去读写对应的sp文件,而不是一个整个应用都读写一个sp文件

4.sp的适合读写轻量的、小的配置信息,不适合保存大数据量的信息,比如长串的json字符串。

5.当有连续的调用PutXxx方法操作时(特别是循环中),当确认不需要立即读取时,最后一次调用commit或apply即可。

Android轻量级存储方案对比

源码解析

apply 方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 中,awaitCommit 中包含了一个等待锁,需要在其它地方释放。我们在上面看到的 QueuedWork.waitToFinish() 其实就是等待这个队列中的 awaitCommit 全部释放。

public void apply() {

   final MemoryCommitResult mcr = commitToMemory();

   final Runnable awaitCommit = new Runnable() {

      public void run() {

         try {

            mcr.writtenToDiskLatch.await();

         } catch (InterruptedException ignored) {

         }

      }

   };

   QueuedWork.add(awaitCommit);

   Runnable postWriteRunnable = new Runnable() {

      public void run() {
         awaitCommit.run();
         QueuedWork.remove(awaitCommit);
      }

   };

   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);

}

然后通过 SharedPreferencesImpl.this.enqueueDiskWrite 创建了一个任务来执行真正的 SP 持久化。

commit 则会同步的执行 writeToFile,apply 则会将 writeToFile 加入到一个任务队列中异步的执行

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {

    final boolean isFromSyncCommit = (postWriteRunnable == null);

    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) {
        boolean wasEmpty = false;
        
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }

        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork 中的这个等待任务移除。

  • 总结来看,SP调用 apply方法,会创建一个等待锁放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。

Android8.0对Sp的优化主要是有两个方面:

1、改变原来被动等待线程调度执行写入的方式,改为主动去调用,涉及主要方法是SharedPreferencesImpl.waitToFinish

2、 增加版本号控制的逻辑,原来是所有的提交都会执行写入磁盘一遍,现在是只执行最后、最新的提交写入磁盘,涉及的主要方法是:SharedPreferencesImpl.writeToFile

针对8.0以下优化:

mp.weixin.qq.com/s/IFgXvPdiE…


public static void tryHackActivityThreadH() {

   try {
      Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
      Method currentAtyThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
      Object activityThread = currentAtyThreadMethod.invoke(null);



      Field mHField = activityThreadClass.getDeclaredField("mH");
      mHField.setAccessible(true);
      Handler handler = (Handler) mHField.get(activityThread);


      Field mCallbackField = Handler.class.getDeclaredField("mCallback");
      mCallbackField.setAccessible(true);
      mCallbackField.set(handler,new SpCompatCallback());
      Log.d(TAG,"hook success");
   } catch (ClassNotFoundException e) {
      e.printStackTrace();
   } catch (NoSuchFieldException e) {
      e.printStackTrace();
   } catch (NoSuchMethodException e) {
      e.printStackTrace();
   } catch (IllegalAccessException e) {
      e.printStackTrace();
   } catch (InvocationTargetException e) {
      e.printStackTrace();
   } catch (Throwable e){
      e.printStackTrace();
   }
}


//自定义callbak:SpCompatCallback,在这个方法中做清理等待锁列表的操作:

public class SpCompatCallback implements Handler.Callback {

   public SpCompatCallback(){

   }



   //handleServiceArgs

   private static final int SERVICE_ARGS = 115;

   //handleStopService

   private static final int STOP_SERVICE = 116;

   //handleSleeping

   private static final int SLEEPING = 137;

   //handleStopActivity

   private static final int STOP_ACTIVITY_SHOW = 103;

   //handleStopActivity

   private static final int STOP_ACTIVITY_HIDE = 104;

   //handlePauseActivity

   private static final int PAUSE_ACTIVITY = 101;

   //handlePauseActivity

   private static final int PAUSE_ACTIVITY_FINISHING = 102;



   @Override

   public boolean handleMessage(Message msg) {

      switch (msg.what){

         case SERVICE_ARGS:

            SpHelper.beforeSpBlock("SERVICE_ARGS");

            break;

         case STOP_SERVICE:

            SpHelper.beforeSpBlock("STOP_SERVICE");

            break;

         case SLEEPING:

            SpHelper.beforeSpBlock("SLEEPING");

            break;

         case STOP_ACTIVITY_SHOW:

            SpHelper.beforeSpBlock("STOP_ACTIVITY_SHOW");

            break;

         case STOP_ACTIVITY_HIDE:

            SpHelper.beforeSpBlock("STOP_ACTIVITY_HIDE");

            break;

         case PAUSE_ACTIVITY:

            SpHelper.beforeSpBlock("PAUSE_ACTIVITY");

            break;

         case PAUSE_ACTIVITY_FINISHING:

            SpHelper.beforeSpBlock("PAUSE_ACTIVITY_FINISHING");

            break;

         default:

            break;

      }

      return false;

   }

}
//清理等待列表的操作

public class SpHelper {

   private static final String TAG = "SpHelper";

   private static boolean init = false;

   private static String CLASS_QUEUED_WORK = "android.app.QueuedWork";

   private static String FIELD_PENDING_FINISHERS = "sPendingWorkFinishers";

   private static ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;



   public static void beforeSpBlock(String tag){

      if(!init){

         getPendingWorkFinishers();

         init = true;

      }

      Log.d(TAG,"beforeSpBlock "+tag);

      if(sPendingWorkFinishers != null){

         sPendingWorkFinishers.clear();

      }

   }



   private static void getPendingWorkFinishers() {

      Log.d(TAG,"getPendingWorkFinishers");

      try {

         Class clazz = Class.forName(CLASS_QUEUED_WORK);

         Field field = clazz.getDeclaredField(FIELD_PENDING_FINISHERS);

         field.setAccessible(true);

         sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);

         Log.d(TAG,"getPendingWorkFinishers success");

      } catch (ClassNotFoundException e) {

         e.printStackTrace();

      } catch (NoSuchFieldException e) {

         e.printStackTrace();

      } catch (IllegalAccessException e) {

         e.printStackTrace();

      } catch (Throwable e){

         e.printStackTrace();

      }



   }

}

链接:www.jianshu.com/p/3f64caa56…

sp全面分析

sp线程安全相关

关联整理:

apply 与commit的对比

  • apply没有返回值, commit有返回值能知道修改是否提交成功

  • apply是将修改提交到内存,再异步提交到磁盘文件; commit是同步的提交到磁盘文件;

  • 多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而apply只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据,从一定程度上提高很多效率。

获取SP与Editor:

  • getSharedPreferences()是从ContextImpl.sSharedPrefsCache唯一的SPI对象;

  • edit()每次都是创建新的EditorImpl对象.

优化建议:

  • 强烈建议不要在sp里面存储特别大的key/value, 有助于减少卡顿/anr

  • 请不要高频地使用apply, 尽可能地批量提交;commit直接在主线程操作, 更要注意了

  • 不要使用MODE_MULTI_PROCESS;

  • 高频写操作的key与高频读操作的key可以适当地拆分文件, 由于减少同步锁竞争;

  • 不要一上来就执行getSharedPreferences().edit(), 应该分成两大步骤来做, 中间可以执行其他代码.

  • 不要连续多次edit(), 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动; 经常看到大家喜欢封装方法, 结果就导致这种情况的出现.

  • 每次commit时会把全部的数据更新的文件, 所以整个文件是不应该过大的, 影响整体性能;