SharedPreferences源码分析

初始化操作
@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) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            //对文件路径和file做一个缓存
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            //初始化sp
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return 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;
}

/**
 * 通过包名缓存所有file与sp的映射关系到一个静态map中
 * @return
 */
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

先从缓存读取,没有缓存建立缓存。
1、对name与file做一次缓存到mSharedPrefsPaths中;
2、通过包名缓存所有file与sp的映射关系到一个静态sSharedPrefsCache中

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            //异步读取
            loadFromDisk();
        }
    }.start();
}

private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        if (mLoaded) {
            return;
        }
        //该逻辑出现初始化sp时
        //备份文件已经存在,说明上次写入磁盘没有成功,保留了备份文件(这种情况源文件没有内容),那么本次使用备份文件
        if (mBackupFile.exists()) {
            //删除源文件
            mFile.delete();
            //将备份文件重命名为xml文件(使用备份文件内容,使用原来的名字)
            //此时mFile有内容,mBackupFile无内容
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map map = null;
    StructStat stat = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        //操作成功后,唤醒等待线程
        notifyAll();
    }
}

1、loadFromDisk,如果存在备份文件,说明上次写入失败,源文件无效,使用备份文件。并将所有的数据读取到mMap中。
2、因为loadFromDisk为异步操作,读取完成将mLoaded置为true,并唤醒等待线程。

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

    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.
        //现开启StrickMode监控读取磁盘操作
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            //在没有加载完sp文件之前等待notifyAll唤醒
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

调用edit方法试图创建Editor;
初始化sp读取文件操作是异步,所以访问edit方法需要加锁等待,等待loadFromDisk完成后notifyAll被唤醒;
所以建议先初始化sp,后续在执行获取edit操作。以免由于等待异步操作当前线程长时间wait。

分析EditorImpl
putXXX
public Editor putBoolean(String key, boolean value) {
            synchronized (this) {
                mModified.put(key, value);
                return this;
            }
        }

        public Editor remove(String key) {
            synchronized (this) {
                mModified.put(key, this);
                return this;
            }
        }

        public Editor clear() {
            synchronized (this) {
                mClear = true;
                return this;
            }
        }
}

临时保存数据到mModified中

commitToMemory & enqueueDiskWrite

执行appy或者commit之前会先后执行commitToMemory和equeueDiskWrite

private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {
        // 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);
        }
        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        ...
        synchronized (this) {
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.clear();
                }
                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.

                //调用了remove,或者存入了null,删除对应的kv。
                if (v == this || v == null) {
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        //如果mMap中已经存在对应的kv,不再存入
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }
               ...
            }

            mModified.clear();
        }
    }
    return mcr;
}

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //1、执行写入操作
                    writeToFile(mcr);
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //2、写入操作后,执行run
                    postWriteRunnable.run();
                }
            }
        };

    final boolean isFromSyncCommit = (postWriteRunnable == null);

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            //初次调用commit,只有一个写入操作。
            wasEmpty = mDiskWritesInFlight == 1;
            //否则,多次调用commit或者apply的情况,会造成mDiskWritesInFlight>1,即使为commit,写入操作也是异步执行。
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

commitToMemory():将mModified中的数据转移到mMap的过程
mDiskWritesInFlight: 这个参数在调用commitToMemory 后会++。写入文件完成后–。代表当前剩余的待写入操作个数。
mDiskWritesInFlight作用:1、决定了commit提交的任务是否异步执行;2、决定在commitToMemory时是否需要重新clone一份mMap,防止当前的数据受影响。

apply()
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() {
                //3、执行完写入操作调用awaitCommit.run会执行writtenToDiskLatch.wait()
                //4、释放锁后才会被从QueuedWork移除
                awaitCommit.run();
                //所以,只有写入操作完成后,才会从等待队列中移除等待任务。
                QueuedWork.remove(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    ...
}

/**
     * Finishes or waits for async operations to complete.
     * (e.g. SharedPreferences$Editor#startCommit writes)
     *
     * Is called from the Activity base class's onPause(), after
     * BroadcastReceiver's onReceive, after Service command handling,
     * etc.  (so async work is never lost)
     */
    public static void waitToFinish() {
        Runnable toFinish;
        while ((toFinish = sPendingWorkFinishers.poll()) != null) {
            toFinish.run();
        }
    }

apply():创建awaitCommit,并将该Runnable加入等待队列,在equeueDiskWrite方法中,执行完writeToDisk操作后会执行mcr.writtenToDiskLatch.await();并将该Runnable从QueuedWork中移除。
目的:这里在执行写入操作的时候latch.countDown,执行完任务执行await操作。看waitToFinish方法注释,该方法会在Activty的onPause或者receiver的onReceiver之后执行,如果等待队列不为空,代表写入操作没有执行完毕,会取出继续执行等待,待到写入操作完成后放行,保证异步任务在某些生命周期回调时不丢失。

可能引发的问题:因为写入操作需要持有一把对象锁(mWritingToDiskLock)频繁调用apply可能造成写入等待,也就造成QueueWork队列过长,主线程某些方法回调waitToFinish时可能会造成阻塞导致ANR。
参考头条技术文章https://mp.weixin.qq.com/s/IFgXvPdiEYDs5cDriApkxQ,可以将等待队列QueueWork,在调用waitToFinish之前将等待队列清空,因为本身就是异步操作,对实时性没有要求。

commit()
public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
    try {
        //在静态锁控制的mDiskWritesInFlight下,可能造成commit的写入操作也是在子线程完成的,所以这里的latch.await执行当前线程等待。
        //比如在真正执行写入文件之前,多次调用commit或者apply,会造成mDiskWritesInFlight>1,使写入操作异步执行。
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    ...
    return mcr.writeToDiskResult;
}

commit方法会将执行结果返回,整体是同步状态,但是真正执行写入文件时,要看mDiskWritesInFlight,如果是多次commit或者apply会造成mDiskWritesInFlight>1,使写操作为异步操作,此时latch.await就起作用了,让当前线程等待写操作完成,同步返回写入结果。

写文件操作WriteToFile
private void writeToFile(MemoryCommitResult mcr) {
    // Rename the current file so it may be used as a backup during the next read
    if (mFile.exists()) {
        if (!mcr.changesMade) {
            // If the file already exists, but no changes were
            // made to the underlying map, it's wasteful to
            // re-write the file.  Return as if we wrote it
            // out.
            //没有进行数据更改,代表写入操作执行成功
            mcr.setDiskWriteResult(true);
            return;
        }
        if (!mBackupFile.exists()) {
            //renameTo:文件内容移动,path保留,所以这里相当于源文件xml内容移动到了备份文件bak中。

            //备份文件不存在,将源文件xml命名为备份文件bak

            //这里做备份的原因:
            // 如果写入失败,下次loadFromDisk时使用备份文件
            // 如果写入成功,备份文件会随之删除,下次loadFromDisk直接使用源文件。
            if (!mFile.renameTo(mBackupFile)) {
                //备份失败,代表写入操作执行失败
                Log.e(TAG, "Couldn't rename file " + mFile
                      + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(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);
            return;
        }
        //把map数据写入文件
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (this) {
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
        // Writing was successful, delete the backup file if there is one.
        //写入成功后,删除备份文件
        mBackupFile.delete();
        mcr.setDiskWriteResult(true);
        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);
}

执行写文件之前会进行备份
如果写文件成功,会将备份删除,下次loadFromDisk时,会使用源文件。
如果写文件失败,会保留备份文件,下次loadFromDisk时,使用备份文件。

取数据getXXX
@Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

如果需要(sp创建时读取文件未完成),执行等待
从内存缓存mMap中获取

SharedPreferences可能造成的性能问题

(参考https://mp.weixin.qq.com/s/IFgXvPdiEYDs5cDriApkxQ)

1、不要存放大的key和value!我就不重复三遍了,会引起界面卡、频繁GC、占用内存等等,好自为之!
2、毫不相关的配置项就不要丢在一起了!文件越大读取越慢,不知不觉就被猪队友给坑了;蓝后,放进defalut的那个简直就是愚蠢行为!
3、读取频繁的key和不易变动的key尽量不要放在一起,影响速度。(如果整个文件很小,那么忽略吧,为了这点性能添加维护成本得不偿失)
4、不要乱edit和apply,尽量批量修改一次提交!
5、尽量不要存放JSON和HTML,这种场景请直接使用json!
6、不要指望用这货进行跨进程通信!!!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值