美文网首页
Android SharedPreferences

Android SharedPreferences

作者: cg1991 | 来源:发表于2019-04-30 17:19 被阅读0次

    Usage

    SharedPreferences preferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE);
    preference.edit().putInt(strKey, nValue).apply();
    //or
    preference.edit().putInt(strKey, nValue).commit();
    

    Context

    • getSharedPreferences

    abstract method and return SharedPreferences object

    ContextImpl extends Context

    • getSharedPreferences

    override method in Context

    • getSharedPreferencesPath

    create new directory for xml file

    • getPreferencesDir
    mPreferencesDir = new File(getDataDir(), "shared_prefs");
    
    • getDataDir

    get data directory,refer to /data/data/packagename/.And entire directory is /data/data/packagename/shared_prefs/

    • makeFilename

    entire arguments is:

    makeFilename(getPreferencesDir(), name + ".xml");
    

    the file and name will put into map:

    ArrayMap<String, File> mSharedPrefsPaths.put(name, file);
    
    • getSharedPreferences

    the arguments is file and mode.

    • getSharedPreferencesCacheLocked

    init ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache and ArrayMap<File, SharedPreferencesImpl> packagePrefs,and put packagePrefs in sSharedPrefsCache with packageName as key.

    • checkMode
      codes:
    private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }
    
    • SharedPreferencesImpl
      constructor method invoke
      and ArrayMap<File, SharedPreferencesImpl> cache put SharedPreferencesImpl object with file as key.

    SharedPreferencesImpl

    implements SharedPreferences interface and accomplish its methods.

    • SharedPreferencesImpl()
    • makeBackupFile

    create temp file with bak with trail.

    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    
    • startLoadFromDisk
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    
    • loadFromDisk

    load file from disk to memory in a map,in the method,we can find:

    Map<String, Object> map = (Map<String, Object>) XmlUtils.readMapXml(str);
    Map<String, Object> mMap = map;
    

    at the end of getSharedPreferences,we find that MODE_MULTI_PROCESS property is making effect to version which less than 11,or useless if you set it,and below 11,it just loads data from disk again.

    Editor

    After getSharedPreferences method,we will invoke edit() to get a object of Editor class to commit or apply.
    Editor is a inner interface in SharedPreferences class and will be implemented in SharedPreferencesImpl.

    • edit

    return Editor object,and code is blocked by a lock object mLock.

    • awaitLoadedLocked
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    

    mLoaded property is initialized in loadFromDisk method,and this means that before data is loaded from disk it will not be got Editor operator,means process is blocked.

    • EditorImpl

    invoke EditorImpl constructor to get its object.

    EditorImpl

    implements methods in Editor interface.

    • putString

    put data through put method,for instance:putInt,putLong,putFloat,putBoolean and so on.

    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
    
    • commit

    invoke commitToMemory first and enqueueDiskWrite in SharedPreferencesImpl class and notifyListeners with MemoryCommitResult argument.

    • commitToMemory
      foreach Map object of mModified and put key and value in mapToWriteToDisk object which key is String and value type is Object.
    Map<String, Object> mapToWriteToDisk = mMap;
    for (Map.Entry<String, Object> e : mModified.entrySet()) {
        String k = e.getKey();
        Object v = e.getValue();
        mapToWriteToDisk.put(k, v);
    }
    

    and then init a new object:new MemoryCommitResult.

    MemoryCommitResult

    this class blocked some arguments and returned to commitToMemory method,and changed data are set to mapToWriteToDisk in MemoryCommitResult class.

    Back to commit method,the next method is enqueueDiskWrit in commit.

    SharedPreferencesImpl

    • enqueueDiskWrite
      this method is defined in SharedPreferencesImpl class and do a favour to write data in xml file.
    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);
    }
    

    the postWriteRunnable argument will be null and isFromSyncCommit is true,what's more,mDiskWritesInFlight argument is self-added,wasEmpty is true.Naturelly,the thread writeToDiskRunnable will be run.
    The thread of writeToDiskRunnable does work for saving data with writeToFile method and self-decreased mDiskWritesInFlight argument.

    • writeToFile
      The vital codes are:
    FileOutputStream str = createFileOutputStream(mFile);
    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    
    • writeMapXml
    public static final void writeMapXml(Map val, String name, XmlSerializer out,WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {
        if (val == null) {
            out.startTag(null, "null");
            out.endTag(null, "null");
            return;
        }
    
        out.startTag(null, "map");
        if (name != null) {
            out.attribute(null, "name", name);
        }
    
        Set s = val.entrySet();
        Iterator i = s.iterator();
    
        while (i.hasNext()) {
            Map.Entry e = (Map.Entry)i.next();
            writeValueXml(e.getValue(), (String)e.getKey(), out, callback);
        }
    
        out.endTag(null, "map");
    }
    

    we can figure out that the step is getting out all content from file and then transfered to be FileOutputStream.The changed key and value pair will be written to xml file.
    The process indicates that we can not set numerous and complex data with commit method.

    After execute enqueueDiskWrite method in commit method,we will find out that a CountDownLatch object will execute await method util it is counted down to zero.

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

    And at the end of writeToFile method,it will invoke:

    mcr.setDiskWriteResult(true, true);
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }
    

    writtenToDiskLatch.countDown(); means that value has been to zero,the next step can be execute,and commit result will be returned.

    • apply
    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);
        notifyListeners(mcr);
    }
    

    the difference between commit is that postWriteRunnable argument to enqueueDiskWrite method is not null,and this runnable will be added to a QueueWork class:

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    
    • 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);
            }
        }
    }
    

    The runnable will be executed in handler with one by one,the lopper of handler is HandlerThread's looper..After writeToFile executed,the postWriteRunnable will run,this thread only invoke awaitCommit.run(),awaitCommit is awaitCommitRunnable,it contains
    mcr.writtenToDiskLatch.await();,before this method is invoked,the result may be returned.In enqueueDiskWrite method:

    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized code (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };
    

    postWriteRunnable.run();is not included in synchronized codes which can be executed firstly.

    Invoking apply method can not ensure that our data is written in a efficient way,it may return result to invoker before data is written in xml file.If apply method is invoked many times in a time,the runnable will be waited in line to be invoked,thus,we should put values in editor in a time and then apply.

    In ActivityThread,during operate onStop lifecycle,it will check QueuedWork state when sdk version more than HONEYCOMB(11):

    public void handleStopActivity(IBinder token, boolean show, int configChanges,
                PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        r.activity.mConfigChangeFlags |= configChanges;
    
        final StopInfo stopInfo = new StopInfo();
        performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
                reason);
    
        if (localLOGV) Slog.v(
            TAG, "Finishing stop of " + r + ": show=" + show
            + " win=" + r.window);
    
        updateVisibility(r, show);
    
        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
    
        stopInfo.setActivity(r);
        stopInfo.setState(r.state);
        stopInfo.setPersistentState(r.persistentState);
        pendingActions.setStopInfo(stopInfo);
        mSomeActivitiesChanged = true;
    }
    

    in QueuedWork.waitToFinish()method,it will iterate all runnable in sWork and then invoke,before all runnable completing it will not return which means onStop method can be stucked before data is written in disk.

    public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;
    
        Handler handler = getHandler();
    
        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);
    
                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }
    
            // We should not delay any work as this might delay the finishers
            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;
        }
    }
    
    • getString

    Getting data can be blocked before initialized with loading from disk,then return data from Map.

    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)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();//loadDataFromDisk completed will notifyAll
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
    

    Conclusion

    image
    Real Size Picture Address:https://i.loli.net/2019/04/26/5cc2ba881badf.jpg
    And article will be synced to wechat blog:Android部落格.

    相关文章

      网友评论

          本文标题:Android SharedPreferences

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