分析达成目标

  • 了解基本实现
  • SharePreferences是否线程安全
  • SharePreferences的mode参数是什么
  • 了解apply与commit的区别
  • 导致ANR的原因
  • Android8.0做了什么优化

基本实现

简单使用

先从如何简单使用开始

val sp = context.getSharedPreferences("123", Context.MODE_PRIVATE)
//通过SharedPreferences读值
val myValue = sp.getInt("myKey",-1)
//通过SharedPreferences.Editor写值
sp.edit().putInt("myKey",1).apply()

SharedPreferences对象从哪里来

SharedPreferences只是一个有各种get方法的接口,结构是这样的

//SharedPreferences.java
public interface SharedPreferences {
    int getInt(String key, int defValue);
    Map<String, ?> getAll();

    public interface Editor {
        Editor putString(String key, @Nullable String value);
        Editor putInt(String key, int value);
    }
}

那么它从哪里来,我们得到context具体实现类ContextImpl里去找,以下代码都会省略不必要的部分

//ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    //可以看到返回的SharedPreferences其实就是一个SharedPreferencesImpl实例
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        //每个File都对应着一个SharedPreferencesImpl实例
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    return sp;
}

从上面可以看出

  1. SharedPreferences真正实现是SharedPreferencesImpl
  2. 对于同一个进程来说,SharedPreferencesImpl和同一个文件是一一对应的

SharedPreferencesImpl

内部存储了一个Map用于把数据缓存到内存

//SharedPreferencesImpl.java
@GuardedBy("mLock")//操作时通过mLock对象锁保证线程安全
Map<String, Object> mMap

对于同一个SharedParences.Editor来说,每个Editor也包含了一个map用来保存本次改变的数据

//SharedPreferencesImpl.java
@GuardedBy("mEditorLock")//操作时通过mEditorLock对象锁保证线程安全
Map<String, Object> mModified

getInt

//SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        //如果正在从xml文件中同步map到内存,则会阻塞等待同步完成
        awaitLoadedLocked();
        //直接从内存mMap中拿数据
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

从上面代码可以看出,SharedPreferences会优先从内存中拿数据

Editor.putInt

public Editor putInt(String key, int value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

putInt只是存入了mModified中,并没有进行其它操作

Editor.apply

//SharedPreferencesImpl.java
public void apply() {
    //1. 遍历mModified
    //2. 合并修改到mMap中
    //3. 当前memory的代数 mCurrentMemoryStateGeneration++
    //以此完成内存的实现。返回的MemoryCommitResult用于之后的xml文件写入
    final MemoryCommitResult mcr = commitToMemory();

    //这里是一个纯粹等待xml写入用的任务
    //writtenToDiskLatch只在本次Editor的修改完全写入到文件后释放
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    //把上面的任务加入到QueuedWork的finisher列表中
    //ActivityThread在调用Activity的onPause、onStop,或者Service的onStop之前都会调用QueuedWork的waitToFinish
    //waitToFinish方法则会轮流遍历运行它们的run方法,即在主线程触发await
    QueuedWork.addFinisher(awaitCommit);

    //在上一个等待任务外面再封装一层等待任务,用于在写入文件完成后从QueuedWork里移除finish
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                //若成功完成,则从QueuedWork里移除该finisher
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    //把写入磁盘的任务提交去执行,commit就不会带第二个参数,后面会说这里
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    //写入内存就直接触发回调监听
    notifyListeners(mcr);
}

Editor.commit

//SharedPreferencesImpl.java
@Override
public boolean commit() {
    //与apply相同,直接写入内存
    MemoryCommitResult mcr = commitToMemory();

    //直接提交disk任务给线程进行处理,第二个参数为空,表示自己是同步的
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    //注意这里是与apply的不同,直接自己触发await,不再放到Runnable里
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

SharedPreferencesImpl.this.enqueueDiskWrite

执行写入磁盘的任务

//SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
    //通过第二个参数来判断是apply还是commit,即是否是同步提交
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    //这个runnable就是写入磁盘的任务
    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //关键方法:写入磁盘
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //这个值在写入一次内存后+1,写入一次磁盘后-1,表示当前正在等待写入磁盘的任务个数
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //与QueuedWork的waitToFinish不同,这里是在子线程等待写入磁盘任务的完成
                    postWriteRunnable.run();
                }
            }
        };

    // 下面的条件我判断只有当前是最后一次commit任务才会到当前线程执行
    // 而commit正常情况下是同步进行的,因此只要之前的apply任务未执行完成,也会改为异步执行
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            //此次同步任务为当前所有任务的最后一次
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //直接在当前线程执行写入xml操作
            writeToDiskRunnable.run();
            return;
        }
    }

    //这里是把写入xml文件的任务放到QueuedWork的子线程去执行。
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    //8.0之前则是直接用单线程池去执行
    //QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

QueuedWork

QueuedWork更像是一个等待任务的集合,其内部含有两个列表

//写入磁盘任务会存入这个列表中,在8.0之前没有这个列表,只有一个SingleThreadExecutor线程池用来执行xml写入任务
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//等待任务会存入这个列表中
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//插入一个磁盘写入的任务,会放到QueuedWork里的一个HandlerThread去执行
public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            //如果是apply则100ms后再触发去遍历执行等待任务,commit则不延迟
            if (shouldDelay && sCanDelay) {
                //这里只需要知道是触发执行sWork里的所有任务,即写入磁盘任务
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

而等待任务列表sFinishers会在waitToFinish方法中使用到,作用是直接去执行所有磁盘任务,执行完成之后再轮流执行所有等待任务

//SharedPreferencesImpl.java
public static void waitToFinish() {
    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // 由于我们会手动执行所有的磁盘任务,所以不再需要这些触发执行任务的消息
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        // 执行此方法的过程中若插入了其它任务,都不需要再延迟了,直接去触发执行
        sCanDelay = false;
    }

    //遍历执行当前的所有等待硬盘任务的run方法
    processPendingWork();

    try {
        while (true) {
            Runnable finisher;

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

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        //所有任务执行完成之后,道路通畅了,这次waitToFinish执行通过,可以继续延迟100ms
        sCanDelay = true;
    }
}

以下是Android8.0之前的waitToFinish,只是遍历执行所有等待任务,也不会去主动写入xml,从而导致ANR出现


public static void waitToFinish() {
    Runnable toFinish;
    //只是去轮流执行所有等待任务
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}

mode权限

我们会通过context获取SharedPreferences对象时传入mode

context.getSharedPreferences("123", Context.MODE_PRIVATE)

该mode会在生成SharedPreferencesImpl实例时传入

//SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    //...
    mMode = mode;
}

在xml文件写入完成后调用

//SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    //写入文件
    FileOutputStream str = createFileOutputStream(mFile);
    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    str.close();

    //给文件加权限
    ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
}

加权限的过程就终相当于我们在串口使用chmod给权限

//ConetxtImpl.java
static void setFilePermissionsFromMode(String name, int mode,
        int extraPermissions) {
    //默认给了同一用户与同一群组的读写权限
    int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
        |FileUtils.S_IRGRP|FileUtils.S_IWGRP
        |extraPermissions;
    if ((mode&MODE_WORLD_READABLE) != 0) {
        //其它用户读权限
        perms |= FileUtils.S_IROTH;
    }
    if ((mode&MODE_WORLD_WRITEABLE) != 0) {
        //其它用户写权限
        perms |= FileUtils.S_IWOTH;
    }
    FileUtils.setPermissions(name, perms, -1, -1);
}

//FileUtils.java
public static int setPermissions(String path, int mode, int uid, int gid) {
    Os.chmod(path, mode);
    return 0;
}

总结

基本实现

SharedPreference有一个内存缓存mMap,以及一个硬盘缓存xml文件。每次通过apply或者commit提交一次editor修改,都会先合入mMap即内存中,之后再缓存到硬盘。注意提交会触发整个文件的修改,因此多个修改最好放在同一个Editor对象中。

线程安全

SharedPreferences主要通过对象锁来保证线程安全,Editor修改时用的是另一个对象锁
,写入disk时也用的是另一个对象锁。

mode是什么

mode类似于给通过chmod给xml文件不同的权限,从而实现其他应用也可以访问的效果,默认MODE_PRIVATE给的是所有者和群组的读写权限,而MODE_WORLD_READABLE与MODE_WORLD_WRITEABLE分别给了其它用户的读写权限

apply与commit

commit与apply的不同主要在于:commit直接在自己的线程等待写入硬盘任务的执行,且commit一次就写一次。而apply不会等待写入硬盘,且8.0之后会根据当前最新的内存代数来过滤掉之前的所有内存修改,只保存最后一次内存修改。

导致ANR的原因

apply提交时会生成一个等待任务放到QueuedWork的一个等待列表里,在Activity的pause、Stop,或者Service的stop执行时,会依次调用这个等待列表的任务,保证每个等待列表所等待的任务都可以执行。若未执行完毕则会导致ANR

Android8.0做了什么优化

8.0的优化方案为

  1. 若提交了多个apply,在执行时只会执行最后一次提交,减少了文件的写入次数
  2. QueuedWork优化:执行写入磁盘的任务时,不再直接放到线程池执行,而是先放入一个真实任务的List,在waitToFinish调用时,会主动执行这些真实任务,再执行所有等待任务。而8.0之前只会执行等待任务,对推动任务执行没有任何帮助

参考

SharedPreferences ANR问题分析和解决 & Android 8.0的优化

10-31 05:02