美文网首页
SharePreference

SharePreference

作者: SunnyDay_ab5f | 来源:发表于2023-05-17 16:06 被阅读0次

    一、SharePreference介绍

    SharePreference是安卓数据持久化保存的方案之一,它以key-value的形式将数据保存在一个xml文件中。一般只保存数据量较小的数据,不适合跨进程调用。

    二、SharePreference源码分析

    SharePreference使用

    SharedPreferences sharedPreferences = getSharedPreferences("test_sp",MODE_PRIVATE);
    SharedPreferences.Editor edit = sharedPreferences.edit();
    edit.putBoolean("isOpen",true);
    edit.commit();//或者使用 edit.apply()
    

    使用getSharedPreferences方法获取SharedPreferences对象。

    ContextImpl.java

        @Override
        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) {
                //mSharedPrefsPaths保存sharepreference文件的map,如果为null则创建
                if (mSharedPrefsPaths == null) {
                    mSharedPrefsPaths = new ArrayMap<>();
                }
                //获取name对应的sharepreference文件
                file = mSharedPrefsPaths.get(name);
                if (file == null) {
                    //获取的文件为空则调用getSharedPreferencesPath方法创建文件
                    file = getSharedPreferencesPath(name);
                    //创建的文件保存在map中
                    mSharedPrefsPaths.put(name, file);
                }
            }
            return getSharedPreferences(file, mode);
        }
    

    看下getSharedPreferences的处理:

    @Override
        public SharedPreferences getSharedPreferences(File file, int mode) {
            SharedPreferencesImpl sp;
            synchronized (ContextImpl.class) {
                //获取缓存的本应用包名下的所有SharedPreference
                final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
                //根据File获取sp
                sp = cache.get(file);
                if (sp == null) {
                    //检查mode,如果targetSdkVersion >= Build.VERSION_CODES.N
                    // 且 mode == MODE_WORLD_READABLE 或者 mode == 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并保存到cache中,new SharedPreferencesImpl会去读取xml中的数据。
                    sp = new SharedPreferencesImpl(file, mode);
                    cache.put(file, sp);
                    return sp;
                }
            }
    
            //多进程使用时会重新加载xml文件,但是不推荐使用 因为一个进程更新了sp,另外的进程并不会收到通知,无法及时更新数据
            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的构造函数回去读取xml文件中的数据

        SharedPreferencesImpl(File file, int mode) {
            mFile = file;
            mBackupFile = makeBackupFile(file);
            mMode = mode;
            mLoaded = false;
            mMap = null;
            mThrowable = null;
            //从硬盘中舰载文件
            startLoadFromDisk();
        }
    

    这里开启了一个子线程去读取,并设置 mLoaded = false

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

    这个方法中就可以看到读取文件和xml文件解析

        private void loadFromDisk() {
            ...
    
            Map<String, Object> map = null;
            StructStat stat = null;
            Throwable thrown = null;
            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);
                    } 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) {
                //加载完成标记为设置为true
                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) {
                            mMap = map;
                            mStatTimestamp = stat.st_mtim;
                            mStatSize = stat.st_size;
                        } else {
                            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();
                }
            }
        }
    

    从上面的代码也可以证明SP只适合存储少量的数据,如果xml保存的数据太多肯定会由于文件加载太慢而造成ANR的风险。SP的get方法就可以验证这个问题,只有xml文件内容加载完成之后才能返回get的结果。

    @Override
        public int getInt(String key, int defValue) {
            synchronized (mLock) {
                //等待数据加载完成
                awaitLoadedLocked();
                Integer v = (Integer)mMap.get(key);
                return v != null ? v : defValue;
            }
        }
    
    
        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);
            }
        }
    

    下面看下看下commit和apply两个提交方法的区别

    commit方法

           @Override
            public boolean commit() {
                long startTime = 0;
    
                if (DEBUG) {
                    startTime = System.currentTimeMillis();
                }
                //将修改的数据保存到内存
                MemoryCommitResult mcr = commitToMemory();
                //直接在当前线程中写文件
                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;
            }
    
    
    
    
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable postWriteRunnable) {
            //commit方法中postWriteRunnable传入的时null
            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.
            //isFromSyncCommit == true
            if (isFromSyncCommit) {
                boolean wasEmpty = false;
                synchronized (mLock) {
                    //mDiskWritesInFlight  在commitToMemory中+1
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                //wasEmpty == true
                if (wasEmpty) {
                    //writeToDiskRunnable执行进行写文件
                    writeToDiskRunnable.run();
                    return;
                }
            }
    
            QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
        }
    
        private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
             ...
    
            // 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.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);
    
                if (DEBUG) {
                    Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                            + (backupExistsTime - startTime) + "/"
                            + (outputStreamCreateTime - startTime) + "/"
                            + (writeTime - startTime) + "/"
                            + (fsyncTime - startTime) + "/"
                            + (setPermTime - startTime) + "/"
                            + (fstatTime - startTime) + "/"
                            + (deleteTime - startTime));
                }
    
                long fsyncDuration = fsyncTime - writeTime;
                mSyncTimes.add((int) fsyncDuration);
                mNumSync++;
    
                if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                    mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
                }
    
                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);
        }
    

    apply方法
    可以看到apply方法先调用了commitToMemory方法然后调用了enqueueDiskWrite方法

    public void apply() {
                final long startTime = System.currentTimeMillis();
    
                final MemoryCommitResult mcr = commitToMemory();
                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");
                            }
                        }
                    };
    
                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 void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable postWriteRunnable) {
            //apply方法中postWriteRunnable传入的不等于null
            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.
            //isFromSyncCommit == false 不进入下面的方法
            if (isFromSyncCommit) {
                boolean wasEmpty = false;
                synchronized (mLock) {
                    //mDiskWritesInFlight  在commitToMemory中+1
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                //wasEmpty == true
                if (wasEmpty) {
                    //writeToDiskRunnable执行进行写文件
                    writeToDiskRunnable.run();
                    return;
                }
            }
             //直接放到队列中执行。
            QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
        }
    
    public static void queue(Runnable work, boolean shouldDelay) {
            Handler handler = getHandler();
    
            synchronized (sLock) {
                sWork.add(work);
    
                if (shouldDelay && sCanDelay) {
                    //延迟100ms发送
                    handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
                } else {
                    handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
                }
            }
        }
    
    

    看下getHandler方法,可以知道sHandler 消息处理放在了HandlerThread中执行了

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

    processPendingWork方法中取出队列中的runable然后调用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();
                }
            }
        }
    
        private static void processPendingWork() {
            long startTime = 0;
    
            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
    
            synchronized (sProcessingWork) {
                LinkedList<Runnable> work;
    
                synchronized (sLock) {
                    work = sWork;
                    sWork = new LinkedList<>();
    
                    // 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");
                    }
                }
            }
        }
    

    commit方法如果提交过大的数据因为在主线程中执行,当cpu使用率过高的时候有可能造成ANR。apply虽然在子线程中执行但是activity启动时会等待QueuedWork中的队列中没有要执行的任务时才会跳转。
    总结:

    • SharePreferences 是Android基于xml实现的一种数据持久化手段。

    • SharePreferences 不适合存储过大的数据, 因为所有持久化数据都是一次性加载到内存, 数据过大容易造成内存溢出。

    • SharePreferences 的commit与apply一个是同步一个是异步(大部分场景下),SharePreferences 的 commit 方法是直接在当前线程执行文件写入操作, 而 apply 方法是在工作线程执行文件写入, 尽可能使用apply , 因为不会阻塞当前线程。

    • SharePreferences 并不支持跨进程, 因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用。

    • SharePreferences 批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务。

    • 每个SharePreferences 存储的键值对不宜过多, 否则在加载文件数据到内存时会耗时过长, 而阻塞SharePreferences 的相关get或put方法, 造成ui卡顿。

    • commit有相应的返回值,可以知道操作是否成功,apply没有返回值。

    相关文章

      网友评论

          本文标题:SharePreference

          本文链接:https://www.haomeiwen.com/subject/wtrbsdtx.html