SharedPreferences

Android提供了SharedPreferences用于保存应用少量,且格式简单(key-value)的数据,如配置信息等。

SharedPreferences与Editor简介

SharedPreferences接口主要负责读取应用程序的Preferences数据,主要方法如下:

  • getXxx(String key, xxx defValue):获取SharedPreferences数据里制定key对应的value,如果该key不存在,则返回默认值defValue。其中Xxx可以是boolean、float、int、long、String等基本类型的值;
  • boolean contains(String key):判断SharedPreference中是否包含特定key的数据;
  • Map<String, ?> getAll():获取SharedPreferences数据里的全部key-value对;
    但SharedPreferences接口并未提供写入数据的能力,通过SharedPreferences.Editor才允许写入,SharedPreferences通过 edit() 方法即可获取对应的 Editor 对象,其中包含以下写入方法:
  • Editor putXxx(String key, @Nullable xxx value):向SharedPreferences中存入指定key对应的数据value,其中Xxx可以是boolean、float、int、long、String等基本类型;
  • Editor remove(String key):删除指定key对应的数据项;
  • Editor clear():清空SharedPreference中的所有数据;
  • boolean commit():当前线程提交修改;
  • void apply():后台提交修改;

SharedPreferences本身是一个接口,应用无法直接创建SharedPreferences实例,只能通过Context的 getSharedPreferences(String name, @PreferencesMode int mode) 方法来获取SharedPreferences实例,其中第二个参数一般为 Context.MODE_PRIVATE:默认模式,仅支持本应用或是同属userID的所有应用读写,其他 MODE_WORLD_WRITEABLE/ MODE_WORLD_READABLE 已注释 @Deprecated,这两种模式允许其他应用写/读本应用创建的数据,易导致安全漏洞。

SharedPreferences使用

普遍性的使用方法如下所示,在MainActivity创建时提示当前已使用多少次,流程如下:获取SharedPreferences对象->获取该SharedPreferences的Editor对象->通过editor来取出/存放值->提交修改。

public class MainActivity extends AppCompatActivity {

    private SharedPreferences preferences;
    private SharedPreferences.Editor editor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        preferences = getSharedPreferences("monster", Context.MODE_PRIVATE);
        editor = preferences.edit();
        int count = preferences.getInt("count", 0);
        editor.putInt("count", ++count);
        Toast.makeText(this,"当前应用已经被使用"+count+"次!",Toast.LENGTH_SHORT).show();
        editor.apply();
    }

SharedPreferences数据保存在当前应用的shared_prefs目录下,文件内容如下格式:根元素为<map…/>,该元素下每一个子元素代表一个key-value对,当value为Integer时,使用<int…/>子元素,以此类推。
SP键值对

原理概览

整个框架较为简单,主要分为如下几个部分:

  1. APP尝试获取SP时,会从cache加载,首次则会新建SharedPreferencesImpl对象,所有的XML键值队会加载到本地变量mMap中;
  2. APP尝试获取value时,会直接从mMap中获取value;
  3. APP尝试改变value时,会将修改提交到EditorImpl的mModified中;
  4. APP尝试提交修改时,会经由EditorImpl去做进一步的修改,后文详述逻辑;
    SP原理框架

此处值得注意的是getXxx()和putXxx()操作的对象不一样,因此信息来源/去向也不一样,分别是SharedPreferencesImpl和EditorImpl对象下的成员变量,因此即使已经修改了value值,在真正的提交修改请求前,拿到的都会是初始值。Demo如下所示:

private void onClick(View view) {
        switch (view.getId()){
            case R.id.get:
                Log.d("MONSTER_DEBUG","Now try to get Value");
                Log.d("MONSTER_DEBUG", String.valueOf(preferences.getInt("test", 0)));
                break;
                //put并未真正的提交
            case R.id.put:
                Log.d("MONSTER_DEBUG","Now try to put Value");
                int value = preferences.getInt("test", 0);
                Log.d("MONSTER_DEBUG", "previous calue = " + value + " and now set to " + (value+1));
                editor.putInt("test",value+1);
                break;
            case R.id.commit:
                Log.d("MONSTER_DEBUG","Now try to commit Value");
                editor.commit();
                break;
            default:
                break;
        }
    }

在这里插入图片描述

commit()与apply()

commit()和apply()均为提交修改的方法,二者在提交时机存在一定的差异,但流程大体一致:

  1. 先调用 commitToMemory(), 将数据同步到 SharedPreferencesImpl 的 mMap, 并保存到 MemoryCommitResult 的 mapToWriteToDisk,
  2. 再调用 enqueueDiskWrite(), 写入到磁盘文件; 先之前把原有数据保存到 .bak 为后缀的文件,用于在写磁盘的过程出现任何异常可恢复数据;
    commitToMemory()
    方法的主要功能: 把 EditorImpl数据更新到 SharedPreferencesImpl
    ● 将 mMap 信息赋值给 mapToWriteToDisk, 并 mDiskWritesInFlight 加 1;
    ● 当 mClear 为 true, 则直接清空 mMap;
    ● 当 value 值为 this 或 null, 则移除相应的 key;
    ● 当 value 值发生改变, 则会更新到 mMap;
    ● 只要有 key/value 发生改变(新增, 删除), 则设置 mcr.changesMade = true. 最后会清空EditorImpl 中的 mModified数据
private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            boolean keysCleared = false;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                if (mDiskWritesInFlight > 0) {
                    //mMap是SharedPreferencesImpl的成员变量HashMap,记录了XML文件的键值对
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

                //是否有监听key改变的监听者
                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;
//mClear是EditorImpl下的一个成员变量,当且仅当调用clear()方法时才会设置为true,此处当mClear为true时,直接清空mapToWriteToDisk并重新将mClear置为false
                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        keysCleared = true;
                        mClear = false;
                    }

                    //mModified是EditorImpl下的一个成员变量,所有的putXxx动作都会将键值对暂时保存在此处
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        //在remove()方法中,实际为mModified.put(key, this),当v为this或为null时,表示需要移除该key
                        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);
                        }
                    }
                    
                    //目前提交的所有改变已经同步到map3ToWriteToDisk中,清空mModified
                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

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

enqueueDiskWrite()

enqueueDiskWrite()方法则封装了一个Runnable对象,如果是commit请求则直接调用run()方法在当前线程执行,反之如果是apply请求则添加到QueueWork的队列中延后执行。

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

        //apply和commit的差异点,对于commit为true
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                //mDiskWritesInFlight在commitToMemory会自增
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                //正常情况下commit直接在此当前线程执行
                writeToDiskRunnable.run();
                return;
            }
        }
        //apply或commit特殊情况下会添加到Queue Work队列
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
writeToFile()
//代码量较大,删除了局部变量及debug相关
@GuardedBy("mWritingToDiskLock")
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        //mFile是SharedPreferencesImpl的成员变量,初始化为getSharedPreferences的第一个入参即文件名
        boolean fileExists = mFile.exists();

        // Rename the current file so it may be used as a backup during the next read
        if (fileExists) {
            boolean needsWrite = false;

            // Only need to write if the disk state is older than this commit
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) {
                //wasWritten及writeToDiskResult
                mcr.setDiskWriteResult(false, true);
                return;
            }
            //mBackupFile是初始化SharedPreferencesImpl对象时创建的一个以文件名开头的bak文件
            boolean backupFileExists = mBackupFile.exists();
            //备份文件不存在时,mFile重命名为备份文件
            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();
            }
        }

        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            
            //将mapToWriteToDisk写下并sync
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

            writeTime = System.currentTimeMillis();

            FileUtils.sync(str);

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

            // 成功写入,删除backup文件
            mBackupFile.delete();

            mDiskStateGeneration = mcr.memoryStateGeneration;

            mcr.setDiskWriteResult(true, true);

            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;
            //正常路径,写入并返回
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // 发生异常
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }
}
QueuedWork.queue()

所有添加到QueuedWork的任务都是交由一个名为“queued-work-looper”的handler线程完成。

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            //添加到sWork队列
            sWork.add(work);
            //并向handler发送RUN信息,sCanDelay默认为true,仅waitToFinish会临时设置为false
            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) {
                //只有一个活儿,拿到RUN信息去处理work
                processPendingWork();
            }
        }
    }
processPendingWork()
private static void processPendingWork() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = sWork;
                sWork = new LinkedList<>();

                //因为处理的是sWork的所有任务,移除其他的RUN MSG
                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");
                }
            }
        }
    }
WaitToFinish()

在handleServiceArgs()/handleStopService()/ handlePauseActivity()/ handleStopActivity()时,会在应用主线程调用WaitToFinish()方法阻塞等待所有SharedPreferences同步完成。

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

        Handler handler = getHandler();

        synchronized (sLock) {
            //前面说过每添加一个work时,会(延迟)添加一个MSG_RUN,既然此处主线程执行,移除所有的MSG
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

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

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }

由上,主线程的WaitToFinish()方法和SharedPreferences的正常逻辑存在交叉processPendingWork(),且该方法是持锁的,因此在IO压力较大的情况下,可能存在等锁导致的ANR问题。

SP同步导致的ANR

如下所示,主线程在stop Activity时需等待SP同步完成,而processPendingWork()方法是持锁的,因此若SP在Handler线程同步耗时,会导致主线程等锁导致input类型的ANR。同理也可能出现processPendingWork()耗时导致input类型的ANR或是Service类型的ANR。

04-22 19:50:13.190  1000  2265 32284 I am_anr  : [0,16321,com.miui.home,819576389,Input dispatching timed out (5f69703 GestureStubHome (server) is not responding. Waited 5000ms for MotionEvent(deviceId=8, eventTime=9850953060000, source=0x00001002, displayId=0, action=DOWN, actionButton=0x00000000, flags=0x00100000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=10.0, yPrecision=10.0, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (600.5, 2398.5)]), policyFlags=0x62000000)]
04-22 19:50:16.556  1000  2265 32284 I am_kill : [0,16321,com.miui.home,100,bg anr]
04-22 19:50:16.086 10085 16321 16321 I dvm_lock_sample: [com.miui.home,1,main,8774,QueuedWork.java,264,void android.app.QueuedWork.processPendingWork(),-,285,void android.app.QueuedWork.processPendingWork(),16427]

"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 ucsCount=0 flags=1 obj=0x7286eb68 self=0xb400007db49ddc00
  | sysTid=16321 nice=0 cgrp=foreground sched=0/0 handle=0x7db60164f8
  | state=S schedstat=( 20469752064 6186697285 46289 ) utm=1497 stm=549 core=4 HZ=100
  | stack=0x7fe5458000-0x7fe545a000 stackSize=8188KB
  | held mutexes=
  at android.app.QueuedWork.processPendingWork(QueuedWork.java:264)
  - waiting to lock <0x07860975> (a java.lang.Object) held by thread 19
  at android.app.QueuedWork.waitToFinish(QueuedWork.java:186)
  at android.app.ActivityThread.handleStopActivity(ActivityThread.java:5325)
  at android.app.servertransaction.StopActivityItem.execute(StopActivityItem.java:43)
  at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
  at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
  at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2260)
  at android.os.Handler.dispatchMessage(Handler.java:106)
  at android.os.Looper.loopOnce(Looper.java:210)
  at android.os.Looper.loop(Looper.java:299)
  at android.app.ActivityThread.main(ActivityThread.java:8108)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)


"queued-work-looper" prio=5 tid=19 Native
  | group="main" sCount=1 ucsCount=0 flags=1 obj=0x13000808 self=0xb400007ce35e3400
  | sysTid=16427 nice=-2 cgrp=foreground sched=0/0 handle=0x7c87bd5cb0
  | state=D schedstat=( 109318025 445833169 722 ) utm=3 stm=7 core=5 HZ=100
  | stack=0x7c87ad2000-0x7c87ad4000 stackSize=1039KB
  | held mutexes=
  native: (backtrace::Unwind failed for thread 16427: Thread has not responded to signal in time)
  at java.io.FileDescriptor.sync(Native method)
  at android.os.FileUtils.sync(FileUtils.java:263)
  at android.app.SharedPreferencesImpl.writeToFile(SharedPreferencesImpl.java:807)
  at android.app.SharedPreferencesImpl.access$900(SharedPreferencesImpl.java:59)
  at android.app.SharedPreferencesImpl$2.run(SharedPreferencesImpl.java:672)
  - locked <0x03bac7f1> (a java.lang.Object)
  at android.app.QueuedWork.processPendingWork(QueuedWork.java:277)
  - locked <0x07860975> (a java.lang.Object)
  at android.app.QueuedWork.access$000(QueuedWork.java:56)
  at android.app.QueuedWork$QueuedWorkHandler.handleMessage(QueuedWork.java:297)
  at android.os.Handler.dispatchMessage(Handler.java:106)
  at android.os.Looper.loopOnce(Looper.java:210)
  at android.os.Looper.loop(Looper.java:299)
  at android.os.HandlerThread.run(HandlerThread.java:67)

借用今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待中的示意图(Android版本较低,但原理基本一致):造成这个问题的根源就是太多 pending 的 apply 行为没有写入到文件,主线程在执行到指定消息的时候会有等待行为,等待时间过长就会出现 ANR。

因此,对于APP开发者而言,务必仅使用SharedPreferences保存轻量级的信息,而对于系统开发者而言,在IO压力大sync耗时时,可考虑直接skip掉waitToFinish()方法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值