Android 数据持久化(一)

Posted by CoXier on March 31, 2017

一、Android 数据持久化

Android 中常使用的数据持久化,分为三类:

  • 文件:适用视频、音乐、图片等格式的数据
  • SharedPreference:适用 key-value 的形式,相比于 Sqlite 更轻量级
  • Sqlite :适用存储和应用相关的对象的各个属性

PS:个人觉得网络存储并不算作 Android 数据持久化。

二、SharedPreference 使用

mPreferences = getPreferences(MODE_PRIVATE);
mPreferences.edit().putString("Name","Coxier").apply();

这样就把 Name = Coxier 存储起来了。

取出数据: String name = mPreferences.getString("Name","Default");

三、SharedPreference 源码解析

3.1 getPreferences & getSharedPreferences

刚刚我在 Activity 中使用的 getPreferences:

public SharedPreferences getPreferences(int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}

这么来看,获取 SharedPreference 都是通过直接调用或者间接调用 getSharedPreferences。

3.2 ContextImpl#getSharedPreferences

Context 是一个抽象类,getSharedPreferences 是通过 ContextImpl 来实现的。

328    @Override
329    public SharedPreferences getSharedPreferences(String name, int mode) {
330        SharedPreferencesImpl sp;
331        synchronized (ContextImpl.class) {
332            if (sSharedPrefs == null) {
333                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
334            }
335
336            final String packageName = getPackageName();
337            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
338            if (packagePrefs == null) {
339                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
340                sSharedPrefs.put(packageName, packagePrefs);
341            }
342
               .....
352
353            sp = packagePrefs.get(name);
354            if (sp == null) {
355                File prefsFile = getSharedPrefsFile(name);
356                sp = new SharedPreferencesImpl(prefsFile, mode);
357                packagePrefs.put(name, sp);
358                return sp;
359            }
360        }
           ....
368        return sp;
369    }

省略无关代码。分析代码:

  • 一个 Application 有一个 packagePrefs,通过 packageName 产生映射关系
  • 如果 packagePrefs 没有和 name 相对应的 SharedPreferencesImpl 则新建一个 SharedPreferencesImpl ,关联后返回。

3.2 SharedPreferencesImpl

SharedPreferencesImpl 是 SharedPreferences 的实现类, Editor 是 Edit 的实现类。从 putString 看起:

309        public Editor putString(String key, @Nullable String value) {
310            synchronized (this) {
311                mModified.put(key, value);
312                return this;
313            }
314        }

按照使用步骤,下一步应该是 apply 或者 commit。

commit:

456        public boolean commit() {
457            MemoryCommitResult mcr = commitToMemory();
458            SharedPreferencesImpl.this.enqueueDiskWrite(
459                mcr, null /* sync write on this thread okay */);
460            try {
461                mcr.writtenToDiskLatch.await();
462            } catch (InterruptedException e) {
463                return false;
464            }
465            notifyListeners(mcr);
466            return mcr.writeToDiskResult;
467        }

apply :

361        public void apply() {
362            final MemoryCommitResult mcr = commitToMemory();
363            final Runnable awaitCommit = new Runnable() {
364                    public void run() {
365                        try {
366                            mcr.writtenToDiskLatch.await();
367                        } catch (InterruptedException ignored) {
368                        }
369                    }
370                };
371
372            QueuedWork.add(awaitCommit);
373
374            Runnable postWriteRunnable = new Runnable() {
375                    public void run() {
376                        awaitCommit.run();
377                        QueuedWork.remove(awaitCommit);
378                    }
379                };
380
381            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
               .....
388        }

commit 和 apply 之后都会调用 enqueueDiskWrite :

510    private void enqueueDiskWrite(final MemoryCommitResult mcr,
511                                  final Runnable postWriteRunnable) {
512        final Runnable writeToDiskRunnable = new Runnable() {
513                public void run() {
514                    synchronized (mWritingToDiskLock) {
515                        writeToFile(mcr);
516                    }
517                    synchronized (SharedPreferencesImpl.this) {
518                        mDiskWritesInFlight--;
519                    }
520                    if (postWriteRunnable != null) {
521                        postWriteRunnable.run();
522                    }
523                }
524            };
525
526        final boolean isFromSyncCommit = (postWriteRunnable == null);
527
528        // Typical #commit() path with fewer allocations, doing a write on
529        // the current thread.
530        if (isFromSyncCommit) {
531            boolean wasEmpty = false;
532            synchronized (SharedPreferencesImpl.this) {
533                wasEmpty = mDiskWritesInFlight == 1;
534            }
535            if (wasEmpty) {
536                writeToDiskRunnable.run();
537                return;
538            }
539        }
540
541        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
542    }

enqueueDiskWrite 反映了 commit 和 apply 的区别:

commit 传入的 postWriteRunnable 为 null ,故 isFromSyncCommit 为 true,因此 writeToDiskRunnable 执行完之后就直接返回了;反看 apply 通过线程池来执行,属于异步操作。

commit 对文件的操作发生在主线程,因此要注意是否会阻塞主线程

568    private void writeToFile(MemoryCommitResult mcr) {

590
591        // Attempt to write the file, delete the backup and return true as atomically as
592        // possible.  If any exception occurs, delete the new file; next time we will restore
593        // from the backup.
594        try {
595            FileOutputStream str = createFileOutputStream(mFile);
596            if (str == null) {
597                mcr.setDiskWriteResult(false);
598                return;
599            }
600            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
601            FileUtils.sync(str);
602            str.close();
603            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
604            try {
605                final StructStat stat = Os.stat(mFile.getPath());
606                synchronized (this) {
607                    mStatTimestamp = stat.st_mtime;
608                    mStatSize = stat.st_size;
609                }
610            } catch (ErrnoException e) {
611                // Do nothing
612            }
613            // Writing was successful, delete the backup file if there is one.
614            mBackupFile.delete();
615            mcr.setDiskWriteResult(true);
616            return;
617        } catch (XmlPullParserException e) {
618            Log.w(TAG, "writeToFile: Got exception:", e);
619        } catch (IOException e) {
620            Log.w(TAG, "writeToFile: Got exception:", e);
621        }
629    }

看 600 行,发现是通过把 map 写到 xml 文件中,进而保存 map。具体是怎么写的,我们就暂时不看了。

其实仔细一想,还真是挺有道理的,本来 Android 对 xml 文件(如资源文件)的解析算法已经很好了,而且 xml 文件又那么符合 key-value 的形式。

现在我们来看一下,上面的 demo 中产生的 xml 文件的内容:

adb shell
run-as <app-package-id>

ls /data/data/<app-package-id>/shared_prefs/
run-as com.hackerli.sharedpreferencesdemo
ls /data/data/com.hackerli.sharedpreferencesdemo/shared_prefs/
cat /data/data/com.hackerli.sharedpreferencesdemo/shared_prefs/MainActivity.xml

MainActivity.xml:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="Name">Coxier</string>
</map>