SharedPreferences源码解析

前言

文章中部分地方SharedPreferences会简写成SP,先抛出几个问题:

  • SP存储的是什么文件,存储在哪个位置?
  • SP是线程安全的吗?
  • SP是如何保证数据安全的?
  • 使用SP有哪些问题?
  • SP会把数据加载到内存中吗?
  • 首次使用SP和第二次使用SP,关于加载数据这块会有哪些不同?
  • 使用SP存储json会有问题吗?
  • SP有没有备份机制?
  • SP可以跨进程吗?

上面问题有笔者亲身经历过的面试题,也有网上找的,确实如果没有看过SP源码的话,第一次面对这些问题真的会一脸懵逼。下面我们结合源码看下SP,顺便也找找这些问题的答案。

关于SP一些基础的使用和问题可以看我以前的一篇文章:Android数据存储之SharedPreferences详细总结

基本使用

写入:

SharedPreferences settings = getSharedPreferences("test", MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putString("name", "张飞");
editor.putInt("age", 18);
//editor.commit();
editor.apply();

读取:

SharedPreferences settings = getSharedPreferences("test", MODE_PRIVATE);
String name = settings.getString("name","");
int age = settings.getInt("age",0);

源码分析

使用入口

首先我们去看SharedPreferences.java代码,SharedPreferences仅仅是一个接口,并没有实现相关的代码。那具体的实现在哪里呢?我们去看getSharedPreferences(“test”, MODE_PRIVATE)方法是怎么获取到SharedPreferences的

//ContextWrapper.java
public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}

ContextWrapper交给了mBase调用getSharedPreferences,对Context体系比较熟悉的同学应该知道这个mBase是一个ContextImpl,我们直接去看ContextImpl

public SharedPreferences getSharedPreferences(String name, int mode) {
    // At least one application in the world actually passes in a null
    // name.  This happened to work because when we generated the file name
    // we would stringify it to "null.xml".  Nice.
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //在SharedPreferences详细总结文章中有说SharedPreferences存储的是文件
        //这里就是查找或者生成那个文件,mSharedPrefsPaths是一个ArrayMap
        //用于缓存文件路径和文件
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //没有找到,根据路径生成文件并存储在mSharedPrefsPaths中
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

下面接着去看getSharedPreferences方法,传递进去了文件和使用SharedPreferences的模式

public SharedPreferences getSharedPreferences(File file, int mode) {
    //SharedPreferences真正的实现
    SharedPreferencesImpl sp;
    //线程安全的获取
    synchronized (ContextImpl.class) {
        //还是从缓存中取,文件为键,SharedPreferencesImpl为值
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            //检查mode,Android N及以上MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已经不支持
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            //为空的话创建并写入缓存
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    //Android 11及以上不支持使用MODE_MULTI_PROCESS
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

可见核心逻辑都在SharedPreferencesImpl,下面接着去看SharedPreferencesImpl的代码

SharedPreferencesImpl

SharedPreferencesImpl构造函数

SharedPreferencesImpl(File file, int mode) {
    //传入的对应路径的file赋值给mFile
    mFile = file;
    //看名字是一个备份用的文件
    mBackupFile = makeBackupFile(file);
    //记录模式
    mMode = mode;
    mLoaded = false;
    //mMap设置为空
    mMap = null;
    mThrowable = null;
    //从存储中加载
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            //new 了一个线程去加载文件
            loadFromDisk();
        }
    }.start();
}
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    ...

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //根据文件生成BufferedInputStream流,用于读取
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                //XmlUtils从流中读取内容并写入map,会生成一个HashMap
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                //关闭流
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    //在synchronized代码块中把上面加载好的map赋值给mMap
                    //至此数据就从xml文件中读取到内存中了
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    //大概是读取失败,赋值一个空的HashMap
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

总结:

  1. 关于XML解析这块我们只需要知道,XML解析方式有Dom,Sax,还有Pull解析。这里使用了Pull解析。
  2. xml读取是耗时的,如果我们在SP中存储大量的数据必然会导致耗时增加
  3. 首次会加载到内存中,第二次就不会了
  4. 在内存中存储格式为Map<String, Object>数据形式

edit()

SharedPreferences并不允许直接putInt,putString必须先通过edit()获取一个Editor,然后调用Editor的各种putInt,putString等方法,然后在调用apply或者commit方法执行存入操作。下面我们分析下edit()方法

public Editor edit() {
    // TODO: remove the need to call awaitLoadedLocked() when
    // requesting an editor.  will require some work on the
    // Editor, but then we should be able to do:
    //
    //      context.getSharedPreferences(..).edit().putString(..).apply()
    //
    // ... all without blocking.
    synchronized (mLock) {
        //可以看下面代码,就是等待加载完成,也就是等待loadFromDisk执行完成
        awaitLoadedLocked();
    }
    //返回一个新建的EditorImpl
    return new EditorImpl();
}

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

EditorImpl

Editor真正的实现是EditorImpl

public final class EditorImpl implements Editor {
    //Editor的锁
    private final Object mEditorLock = new Object();

    //修改的数据
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    @Override
    public Editor putString(String key, @Nullable String value) {
        //put 方法加锁
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }

    //各种put方法,所有的put方法都类似都加锁
    ...
    

    @Override
    public Editor remove(String key) {
        //根据键移除一个,加锁
        synchronized (mEditorLock) {
            mModified.put(key, this);
            return this;
        }
    }

    @Override
    public Editor clear() {
        //清空,加锁
        synchronized (mEditorLock) {
            mClear = true;
            return this;
        }
    }
    
    @Override
    public void apply() {...}
    
    @Override
    public void commit() {...}
}

apply

我们都知道Editor有两个写入数据的方法,一个是apply异步写入非阻塞,一个是commit是阻塞的。但是现实情况真的是这样吗,我们来看看。

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();
    //执行commitToMemory,并返回一个MemoryCommitResult包括写入内存的结果
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    //MemoryCommitResult有一个writtenToDiskLatch
                    //只有setDiskWriteResult方法执行的时候才会countDown
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(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);
}

//看方法名,顾名思义是先提交到内存中
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;
    //以当前SharedPreferencesImpl对象的mLock为锁
    synchronized (SharedPreferencesImpl.this.mLock) {
        // We optimistically don't make a deep copy until
        // a memory commit comes in when we're already
        // writing to disk.
        //是否已经有写操作在执行,是的话复制一份,操作自己复制的这一份
        if (mDiskWritesInFlight > 0) {
            // We can't modify our mMap as a currently
            // in-flight write owns it.  Clone it before
            // modifying it.
            // noinspection unchecked
            //写入前先复制一份,并赋值给mMap
            mMap = new HashMap<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        //在执行的写操作数+1
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                //是清空的话,这里对mapToWriteToDisk执行清空操作
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                keysCleared = true;
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    //值为null,清空这个key
                    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清空,以备下次使用
            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    //返回一个MemoryCommitResult,包裹结果,包括监听者和写入后的map
    return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}

MemoryCommitResult

private static class MemoryCommitResult {
    final long memoryStateGeneration;
    final boolean keysCleared;
    @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, boolean keysCleared,
            @Nullable List<String> keysModified,
            @Nullable Set<OnSharedPreferenceChangeListener> listeners,
            Map<String, Object> mapToWriteToDisk) {
        this.memoryStateGeneration = memoryStateGeneration;
        this.keysCleared = keysCleared;
        this.keysModified = keysModified;
        this.listeners = listeners;
        this.mapToWriteToDisk = mapToWriteToDisk;
    }

    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //isFromSyncCommit是否来自同步的commit,很明显不是,这里postWriteRunnable
    //不为空
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //写入xml文件
                    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) {
            //如果是来自commit的话,看mDiskWritesInFlight是否是1
            //为1也就表示没有其他的commit或者apply在执行,只有当前
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //直接执行writeToDiskRunnable
            writeToDiskRunnable.run();
            return;
        }
    }
    //这里是apply,所以来到这里通过QueuedWork添加这个writeToDiskRunnable
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

关于QueuedWork还有一个隐藏的问题,等会在其他里面介绍。

//QueuedWork.java
public static void queue(Runnable work, boolean shouldDelay) {
    //新建一个HandlerThread,其实就是放在子线程中执行
    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 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. apply会放在子线程执行写入存储
  2. Editor写入,读取内存,读取存储,等有三把锁保证线程安全。所以SP是线程安全的。

commit

看完了apply的分析,其实commit剩余的已经没啥东西了

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
    //和apply一样先写入内存
    MemoryCommitResult mcr = commitToMemory();

    //执行enqueueDiskWrite,注意后面参数传入的是null表示直接执行
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        //阻塞等待执行完成
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

总结:
commit和apply都会先写入内存,然后再写入存储,区别是写入存储commit是在当前线程,apply是在子线程执行。

writeToFile

前面分析了apply和commit并没有深入看文件的写入文件这块,继续看下

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    long startTime = 0;
    long existsTime = 0;
    long backupExistsTime = 0;
    long outputStreamCreateTime = 0;
    long writeTime = 0;
    long fsyncTime = 0;
    long setPermTime = 0;
    long fstatTime = 0;
    long deleteTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
    //文件是否存在
    boolean fileExists = mFile.exists();

    if (DEBUG) {
        existsTime = System.currentTimeMillis();

        // Might not be set, hence init them to a default value
        backupExistsTime = existsTime;
    }

    // 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) {
                //来自commit的话直接设置为true
                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) {
            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();
        }
    }

    // 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 (DEBUG) {
            outputStreamCreateTime = System.currentTimeMillis();
        }

        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        //通过XmlUtils写入xml文件
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();
        //关闭流
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

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

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

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

        // Writing was successful, delete the backup file if there is one.
        //写入成功,备份文件已经没有存在的必要了,下次需要的时候读取新的或者直接重命名当前文件
        mBackupFile.delete();

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

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

    // Clean up an unsuccessfully written file
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false, false);
}

其他

SP备份机制

其实看完writeToFile这个方法我们应该已经知道了SP的备份机制,每次读取SP的时候都会创建一个被备份文件,每次写入文件前,先看下这个备份文件是否存在,不存在将当前指向xml的文件改名为备份文件,正常SharedPreferencesImpl构造方法里面已经根据这个xml文件创建了备份文件。
writeToFile方法执行成功会把备份文件删除,这个时候xml文件已经是新的了。异常情况写入文件失败,比如掉电,强制退出等。写入新的SP会失败那么下次loadFromDisk会把备份文件设置为mFile,也就是读取旧的内容,保证不至于失败导致数据被污染。

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    ...

}

QueuedWork隐藏问题

刚才说apply还隐藏了一个刺客在哪里呢,在QueuedWork里面,这里参考了字节的文章今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待

public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
        int configChanges, PendingTransactionActions pendingActions, String reason) {
    ...
    // Make sure any pending writes are now committed.
    if (r.isPreHoneycomb()) {
        //Android 11以前的版本,等待waitToFinish执行完成,所以这里会导致Activity一直等待
        //QueuedWork把任务执行完成,也就是隐藏会导致卡顿或者ANR
        QueuedWork.waitToFinish();
    }
    ...
}

存储json

参考请不要滥用SharedPreference

还有一些童鞋,他在sp里面存json或者HTML;这么做不是不可以,但是,如果这个json相对较大,那么也会引起sp读取速度的急剧下降。
JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多&这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。而JSON本来就是可以用来做配置文件的,你干嘛又把它放在sp里面呢?多此一举。下面我写个demo验证一下。

参考链接

https://juejin.cn/post/6961961476047568932?searchId=20240504152606909D2A140930E1DACD5E
https://juejin.cn/post/6884505736836022280?searchId=20240503211729C6AE6135B0EDD86DCF0C
https://weishu.me/2016/10/13/sharedpreference-advices/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/592571.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker部署nginx并实现https

文章目录 docker部署nginx并实现https1、服务器环境2、安装docker3、准备证书4、准备nginx配置文件和dockerfile文件5、创建nginx镜像与容器6、验证访问 docker部署nginx并实现https 1、服务器环境 [rootliuyanfen12 ~]#systemctl stop firewalld [rootliuyanfen12 ~]#setenf…

Flutter笔记:美工设计.导出视频到RIVE

Flutter笔记 美工设计.导出视频到RIVE - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_28…

RabbitMQ之顺序消费

什么是顺序消费 例如&#xff1a;业务上产生者发送三条消息&#xff0c; 分别是对同一条数据的增加、修改、删除操作&#xff0c; 如果没有保证顺序消费&#xff0c;执行顺序可能变成删除、修改、增加&#xff0c;这就乱了。 如何保证顺序性 一般我们讨论如何保证消息的顺序性&…

【Linux】进程的隔离和控制:namespace 隔离、cgroup 控制

文章目录 五、namespace 隔离dd -- 读取、转换并输出数据mkfs -- 格式化文件系统df -- 显示文件系统磁盘使用情况mount -- 加载文件系统到指定的加载点unshare -- 创建子进程&#xff0c;同时与父程序不共享namespace一个 demo 六、cgroup(Control Group) 相关命令pidstat -- 监…

【LeetCode刷题记录】230. 二叉搜索树中第K小的元素

230 二叉搜索树中第K小的元素 给定一个二叉搜索树的根节点 root &#xff0c;和一个整数 k &#xff0c;请你设计一个算法查找其中第 k 个最小元素&#xff08;从 1 开始计数&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,1,4,null,2], k 1 输出&#xff1…

2024年深圳杯东三省联赛数模竞赛A题代码改进-更加合理的结果

4月下旬深圳杯开赛后第二天就推出了完整版的论文&#xff0c;经过长达半个月大家再售后群的讨论分析&#xff0c;我们又重新对之前思路下写的代码进行了改进。本次改进的结果&#xff0c;我们特地参考了网上一些常见的火箭及其相关的级别分离高度&#xff1a;&#xff08;我们的…

c语言刷题——输出图案

1.输出用“*”组成的X形图案 题目&#xff1a;请打印用“*”组成的X形图案 描述&#xff1a; 多组输入&#xff0c;一个整数&#xff08;2~20&#xff09;&#xff0c;表示输出的行数&#xff0c;也表示组成“X”的反斜线和正斜线的长度。 输出描述&#xff1a; 针对每行输…

【C语言】指针篇- 深度解析Sizeof和Strlen:热门面试题探究(5/5)

&#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&#xff1a; C笔记 &#x1f308;喜欢的诗句:无人扶我青云志 我自踏雪至山巅 文章目录 一、简单介绍Sizeof和Strlen1.1 Sizeof1.2 Strlen函数1.3 Sie…

零基础学习数据库SQL语句之查询表中数据的DQL语句

是用来查询数据库表的记录的语句 在SQL语句中占有90%以上 也是最为复杂的操作 最为繁琐的操作 DQL语句很重要很重要 初始化数据库和表 USE dduo;create table tb_emp(id int unsigned primary key auto_increment comment ID,username varchar(20) not null unique comment…

大语言模型中的第一性原理:Scaling laws

大语言模型的尺度定律在大语言模型的训练过程中起到了非常重要的作用。即使读者不参与大语言模型的训练过程&#xff0c;但了解大语言模型的尺度定律仍然是很重要的&#xff0c;因为它能帮助我们更好的理解未来大语言模型的发展路径。 1. 什么是尺度定律 尺度定律&#xff08…

stamps做sbas-insar,时序沉降图怎么画?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

【跟我学RISC-V】(二)RISC-V的基础知识学习与汇编练习

写在前面&#xff1a; 这篇文章是跟我学RISC-V的第二期&#xff0c;是第一期的延续&#xff0c;第一期主要是带大家了解一下什么是RISC-V,是比较大体、宽泛的概念。这一期主要是讲一些基础知识&#xff0c;然后进行RISC-V汇编语言与c语言的编程。在第一期里我们搭建了好几个环…

WPF之绑定验证(错误模板使用)

1&#xff0c;前言&#xff1a; 默认情况下&#xff0c;WPF XAML 中使用的绑定并未开启绑定验证&#xff0c;这样导致用户在UI上对绑定的属性进行赋值时即使因不符合规范内部已抛出异常&#xff08;此情况仅限WPF中的数据绑定操作&#xff09;&#xff0c;也被程序默认忽略&…

4.【Orangepi Zero2】Linux定时器(signal、setitimer),软件PWM驱动舵机(SG90)

Linux定时器&#xff08;signal、setitimer&#xff09;&#xff0c;软件PWM驱动舵机&#xff08;SG90&#xff09; signalsetitimer示例 软件PWM驱动舵机&#xff08;SG90&#xff09; signal 详情请看Linux 3.进程间通信&#xff08;shmget shmat shmdt shmctl 共享内存、si…

【计算机网络】循环冗余校验:Cyclic Redundancy Check

1. 任务目标 利用循环冗余校验&#xff08;CRC&#xff09;检测错误。 循环冗余校验&#xff08;英语&#xff1a;Cyclic redundancy check&#xff0c;通称 CRC&#xff09;是一种根据网上数据包或计算机文件等数据产生简短固定位数校验码的一种散列函数&#xff0c;主要用来…

智慧文旅展现文化新风貌,科技助力旅行品质升级:借助智慧技术,文旅产业焕发新生机,为旅行者带来更高品质的文化体验之旅

一、引言 在数字化、智能化的浪潮下&#xff0c;文旅产业正迎来前所未有的发展机遇。智慧文旅作为文旅产业与信息技术深度融合的产物&#xff0c;不仅为旅行者带来了全新的文化体验&#xff0c;也为文旅产业注入了新的活力。本文旨在探讨智慧文旅如何借助智慧技术展现文化新风…

C++:二叉搜索树的底层模拟实现

概念&#xff1a; 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树&#xff1a; 搜索二叉树的操作&#xff1a; int a[] {8, 3, 1, 10, 6, 4, 7, 14, 13};二叉搜索树需要满足左子树比根小&#xff0c;右子树比根大&#xff0c;…

Leetcode—1235. 规划兼职工作【困难】(upper_bound、自定义排序规则)

2024每日刷题&#xff08;125&#xff09; Leetcode—1235. 规划兼职工作 算法思想 实现代码 class Solution { public:int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {int n startTime.size();vec…

循环神经网络完整实现(Pytorch 13)

一 循环神经网络的从零开始实现 从头开始基于循环神经网络实现字符级语言模型。 %matplotlib inline import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 train_iter, vocab …

(六)SQL系列练习题(下)#CDA学习打卡

目录 三. 查询信息 16&#xff09;检索"1"课程分数小于60&#xff0c;按分数降序排列的学生信息​ 17&#xff09;*按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩 18&#xff09;*查询各科成绩最高分、最低分和平均分 19&#xff09;*按各科成绩…