android KV存储三部曲之SharedPreferences

130 阅读4分钟

Android SharedPreferences

这个可能是我们接触android用到的第一个用来存储类似键值对数据的工具。经典的不能在经典了。 这里就不讨论她的使用了。毕竟例子太多了。主要说下,被时代所抛弃的原因。

ANR问题!!!!!!

主要来说引起ANR的问题有三个地方:

  • SP getValue时
  • SP commit提交数据时
  • SP apply后,生命周期方法调用时

一、getValue引起的ANR

首先调用SP方法时调用到了contextImpl类中最终调用到了SharedPreferencesImpl。 从构造函数我们可以看出,代码开了线程从文件中读取xml转换成map对象。这本身就是一个耗时操作。

    //构造函数
    @UnsupportedAppUsage
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        //从文件加载
        startLoadFromDisk();
    }

    @UnsupportedAppUsage
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        //开启新线程执行
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

此时,当我们进行getValue操作。我们看下代码

 @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            //等待加载锁
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
       @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                //wait等待。直到调用notify,才会唤醒。
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

所以在我们进行取值的时候,首先要对file文件加载、执行,转换成map对象,然后调用notify唤醒其他等待读取value的线程。

当存储的数据特别大,例如是一个json数据的时候。 那耗时就会非常严重,我们在主线程获取sp的值,可能就ANR了。

二、commit提交数据引起的ANR

@Override
        public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }

            MemoryCommitResult mcr = commitToMemory();

            //进行一个写文件的任务
            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");
                }
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
        
         private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        //构建了一个runnable任务
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        //是否是同步提交
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            //当wasEmpty为true,执行方法
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        //将任务添加到了队列中,通过handler发送消息调用
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

看了commit方法,可以知道commit方法中的操作时在调用线程执行的,假如我们在主线程调用的话,可能就会引起ANR

三、Apply引起的ANR

image.png 今日头条技术团队,有一篇文章对此进行了分析,再此引用一下图片。 总的来说,看代码。

  @Override
    public void handleStopActivity(IBinder token, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            //调用方法
            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;
        }

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }

当我们的生命周期stop的时候,调用waittofish。去等待执行完毕。等待什么? 等待我们之前QueueWork.queue提交的任务。就是addFinisher添加的Runnable,如果任务都已经完成了,那么就大家都开心。如果任务没有完成,那么就会阻塞 这个方法实在activity、service生命周期的主线程里调用的。所以也会间接的导致ANR。

one more thing

SP还有其他的缺点:

  • 比如占用内存,加载后的SP对象,不会进行数据的移除或者释放操作
  • 比如不支持局部更新,每次都会去全局覆盖更新。比如十个属性,我只想修改一个,但却十个都要覆盖。
  • 比如不支持多进程,SP中的mode是指用来做数据同步的。类似于votile机制。而且会导致数据丢失和覆盖的问题。

本篇基本上说了SP的一些缺点,当然,我们是可以通过做缓存,hook的方式来弥补这些问题。

但是这里就不多叙述了,个人觉得没必要,有兴趣的可以搜索下,有了更好的工具我们为什么不去用呢。例如mmkv,google的Datastore。

SP就像前女友,总该是要被遗忘的,也许是时候了。