Android SharePreferences源码解读

前言

在安卓开发中经常被用到,它是一个轻量级的存储类,通过key——value的形式用于保存一些配置参数。下面通过源码分析SharePreferences

主要类

首先介绍源码的分析过程中涉及到的一些类

  • PreferenceManager:静态方法通过传入Context获取一个SharedPreferences实例
  • ContextImpl:Context的实现类,真正获取SharedPreferences的类
  • SharedPreferences:是个接口,里面又有两个接口OnSharedPreferenceChangeListenerEditor
  • SharedPreferencesImpl:SharedPreferences的实现类,实现了SharedPreferences接口里的方法,EditorImpl实现了Editor的方法

源码

获取 SharedPreferences

PreferenceManager的方法getDefaultSharedPreferences开始获取 SharedPreferences

getDefaultSharedPreferences

    public static SharedPreferences getDefaultSharedPreferences(Context context) {
        return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
                getDefaultSharedPreferencesMode());
    }
    public static String getDefaultSharedPreferencesName(Context context) {
        return context.getPackageName() + "_preferences";
    }
    private static int getDefaultSharedPreferencesMode() {
        return Context.MODE_PRIVATE;
    }

传递了一个packageName_preferences 和MODE_PRIVATE,调用ContextImplgetSharedPreferences

getSharedPreferences

private ArrayMap<String, File> mSharedPrefsPaths;
public SharedPreferences getSharedPreferences(String name, int mode) {
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //根据传进来的文件名(用getDefaultSharedPreferences的话是[packageName]_preferences)查找文件
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }
	//创建一个文件
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            final File res = new File(base, name);
            // We report as filesystem access here to give us the best shot at
            // detecting apps that will pass the path down to native code.
            BlockGuard.getVmPolicy().onPathAccess(res.getPath());
            return res;
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }

通过上一步传进来的name查找file, mSharedPrefsPaths是一个ArrayMap 类型,如果找不到file创建一个/data/data//shared_prefs/[packageName]_preferences.xmlFile文件

getSharedPreferences

    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //通过包名获取 ArrayMap对象
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            //通过文件名获取ArrayMap对象里的对应的SharedPreferencesImpl
            sp = cache.get(file);
            if (sp == null) {
                //检查mode,当目标版本>=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对象
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        //多进程的情况下,会进行Check,如果有其他进程修改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获取SharedPreferences的实现类SharedPreferencesImpl

getSharedPreferencesCacheLocked

//以包名为 key ,以一个存储了 sp 文件及其 SharedPreferencesImp 对象的 ArrayMap 为 value
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
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;
}

通过包名获取 ArrayMap对象packagePrefs,提供给上一步用.

SharedPreferencesImpl

SharedPreferencesImpl(File file, int mode) {
    // sp 文件
    mFile = file;
    // 创建备份文件
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    // 标识 sp 文件是否已经加载到内存
    mLoaded = false;
    // 存储 sp 文件中的键值对
    mMap = null;
    mThrowable = null;
    // 将SP文件加载到内存中
    startLoadFromDisk();
}

    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

private void loadFromDisk() {
    synchronized (mLock) {
        //已经加载到内存了啥也不干
        if (mLoaded) {
            return;
        }
        // 如果BackupFile存在,实际读取的是备份文件
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

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

    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);
                //将XML以map的形式装载进内存
                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) {
                    //赋给全局变量mMap
                    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 {
            // 这个通知需要持有SharedPreferencesImpl.this执行
        	// 一般时所有的getXXX()
            mLock.notifyAll(); 
        }
    }
}

读取SP文件放入内存mMap中,异步执行,线程安全读取过程中持有锁 mLock。整个获取SharedPreferences到这里就结束了,之后会调用getXXX()方法获取数据。

读取数据getXXX()

读取过程很简单,getXXX方法有很多支持String,Set,int,long,float,boolean,还有全部读取getAll。这里以getString为例

getString

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        // 此处等待加载完毕,否则一直阻塞
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

awaitLoadedLocked

等待已经加载到内存mLoaded的锁,前面说过loadFromDisk加载完后会唤醒mLock.notifyAll();

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

存储数据putXXX()

edit

执行,putXXX之前需要先调用edit()获取EditorImpl对象。EditorSharedPreferences 一样,都是接口,它们的实现类分别是 EditorImplSharedPreferencesImpl。然后才能

public Editor edit() {
   
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

注意:每次edit()都会实例化一个新的EditorImpl对象,所以多次putXXX时只需要一次edit就够了,避免浪费内存

putString

//暂存用户设置的数据,待commit,或者apply()时进行写入
private final Map<String, Object> mModified = new HashMap<>();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

remove和clear

@Override
public Editor remove(String key) {
    synchronized (mEditorLock) {
        //value 为this,下面分析
        mModified.put(key, this);
        return this;
    }
}

@Override
public Editor clear() {
    synchronized (mEditorLock) {
        // 写入一个clear标志,在提交时会进行chec
        mClear = true;
        return this;
    }
}

commit()

    public boolean commit() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
		//将上一步mModified同步到内存
        MemoryCommitResult mcr = commitToMemory();
		//写入到SP文件
        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");
            }
        }
        //通知监听者,回调 OnSharedPreferenceChangeListener
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }

   

写入到内存

 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
            new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {

        if (mDiskWritesInFlight > 0) {
          
            mMap = new HashMap<String, Object>(mMap);
        }
        
        mapToWriteToDisk = mMap;
         // mDiskWritesInFlight这个变量用来表示还有多少待写入Disk的请求
        // 我们使用commitToMemory()修改一个值,这个变量会加1
        mDiskWritesInFlight++;

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

        synchronized (mEditorLock) {
            boolean changesMade = false;
			//处理clear
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.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.
                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) {
                     // 将所有需要回调Listener的字段加入ArrayList
                    keysModified.add(k);
                }
            }
			// 清空暂存缓存
            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

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

mapToWriteToDisk对原来的mMap和putXXX完后mModified做了一个合并。然后下一步enqueueDiskWrite会将mapToWriteToDisk写入到SP文件中去

enqueueDiskWrite

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) {
                    //写入到Sp文件
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //每次写入文件成功,会将mDiskWritesInFlight这个变量-1
                    mDiskWritesInFlight--;
                }
                // 如果在写入文件完毕后有回调,在这里执行
                // commit()时这个变量会为null
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };
	// commit() 直接在当前线程进行写入操作	.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
	//apply() 方法执行此处,由 QueuedWork.QueuedWorkHandler 处理
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

commit() 方法中调用 enqueueDiskWrite()方法传入postWriteRunnable为null:

 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);

所以commit() 写文件操作enqueueDiskWrite是直接在当前调用线程执行的,你在主线程调用该方法,就会直接在主线程进行 IO 操作。这样的话可能会造成卡顿或者ANR。一起来看看最终的写入到SP文件方法 writeToFile(mcr, isFromSyncCommit);

writeToFile

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) {
                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;
        }
        //将mapToWriteToDisk写入到mFile中
        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);
}

代码看着很长,其实核心代码没几句,原理和获取SharePreferences时的loadFromDisk方法相反。

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

和commit一样,先commitToMemory,再enqueueDiskWrite,这里传入postWriteRunnable。前面在 enqueueDiskWrite方法中讲过apply会走QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)

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

通过QueuedWork执行异步任务,当我们的Activity执行onPause()的时候,也就是ActivityThread类执行handleStopActivity方法是,看看它干了啥 它会执行 QueuedWork.waitToFinish()方法,而waitToFinish方法中有个while循环,如果我们还有没有完成的异步落盘操作时,它会调用到我们在apply方法中创建的awaitCommit,让我们主线程处于等待状态,直到所有的落盘操作完成,才会跳出循环,这也就是apply造成anr的元凶。

@Override
  public void handleStopActivity(IBinder token, boolean show, int configChanges,
          PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
      //...省略

      // Make sure any pending writes are now committed.
      if (!r.isPreHoneycomb()) {
      	// 可能因等待写入造成卡顿甚至 ANR
          QueuedWork.waitToFinish();
      }
     //...省略
  }

 public static void waitToFinish() {
      ...省略
      try {
          while (true) {
              Runnable finisher;

              synchronized (sLock) {
                  finisher = sFinishers.poll();
              }

              if (finisher == null) {
                  break;
              }
             
              finisher.run();
          }
      } finally {
          sCanDelay = true;
      }
      ...省略
  }


结论

阅读源码的过程中发现一些小问题,平时开发需要注意:

  1. 获取SharedPreferences的时候(getSharedPreferences)会将初始化SP文件并将SP文件加载到内存,可能导致后续 getXXX() 方法阻塞,提前异步初始化 SharedPreferences,也不要存放大数据在SP文件中
  2. Editor的commit方法和apply方法,每次执行时会写入磁盘,两者的区别是,commit同步写入有返回值,apply异步写入没有返回值。所以在不需要返回值的情况下,尽量使用apply方法提高性能。
  3. apply和commit方法都是将mapToWriteToDisk全部写入,所以说如果针对高频改变的配置项我们应该存储在单独的Sp文件中,提高性能
  4. edit方法每次都会新建一个EditorImpl对象,所以应该多次putXXX的情况下一次edit就够了
  5. 不支持跨进程,MODE_MULTI_PROCESS 也没用。跨进程频繁读写可能导致数据损坏或丢失。

你可能感兴趣的:(安卓开发)