通过ContentProvider多进程共享SharedPreferences数据

通过ContentProvider多进程共享SharedPreferences数据

2017.01.19 09:34* 字数 1953 阅读 4997 评论 12 赞赏 1

转载注明出处:简书-十个雨点

开发一个多进程应用的时候,我们往往无法避免在多个进程之间共享数据。
多进程共享数据的方法有很多种,在Android中常用的有:SharedPreferences(多进程模式)、广播、Socket、ContentProvider、Messenger、AIDL等。这些方法适用于不同的使用场景,又有各自的局限性。

本文即将介绍的是通过ContentProvider,结合SharedPreferences(以下简称SP)实现的进程间共享设置项的功能。这种方式主要适用于以下场景:在一个进程中进行一些设置,而需要在另一个进程实时读取设置,并根据这些设置来执行功能。

1.SharedPreferences在多进程共享下的局限

有些同学可能会觉得奇怪:明明上面才说了SP(多进程模式)也是多进程共享数据的方法,为什么还需要通过ContentProvider来做呢。答案很简单,因为SP其实并不能保证多进程间同步,下图是关于MODE_MULTI_PROCESS的注释,我就不全文翻译了。

MODE_MULTI_PROCESS模式的注释

我们都知道,SP其实是将key-value对保存在手机的data/data/you.package.name/shared_prefs/目录下的文件中。为了减少IO造成的性能损失,SP使用了缓存的机制,会先把数据保存在内存中,在读取的时候直接从内存中读取,而写的时候才会保存到文件。也就是说普通SP是一次读取,多次写入的工作模式。所以如果多个进程中都使用了普通的SP,分别进行保存就会导致相互覆盖。而设置了MODE_MULTI_PROCESS之后,在多进程使用的时候,会在检测到文件变化的时候重新加载文件到内存中,这样虽然损失了一部分性能,但是却部分实现了多进程间同步。

为什么说是"部分"实现了多进程间同步呢?因为在频繁进行SP操作的时候,就还是会出现相互覆盖的问题。我初步估计是因为两个进程同时进行了文件的写操作,带着这个猜测我阅读了源码。

先看看对MODE_MULTI_PROCESS是如何处理的
ContextImpl的getSharedPreferences方法:

@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 = 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;
}

其中SharedPreferencesImpl的startReloadIfChangedUnexpectedly方法:

    void startReloadIfChangedUnexpectedly() {
        synchronized (this) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

可见遇到MODE_MULTI_PROCESS的时候,会强制让SP进行一次读取操作,从而保证数据是最新的。因此如果你在外部保存了一份SP的对象,反而会导致享受不到MODE_MULTI_PROCESS带来的同步效果了,而且从源码中可以看出ContextImpl中是对SP对象做了缓存的,每次重新getSharedPreferences并不会造成太大的性能损失。
从getSharedPreferences的代码中看不出会造成多进程互相覆盖的问题,那我们看看Editor的commit方法,
EditorImpl的commit和相关方法:

final class SharedPreferencesImpl implements SharedPreferences {
    ...
    public final class EditorImpl implements Editor {
        ...
        private MemoryCommitResult commitToMemory() {
            ...
        }
        public boolean commit() {
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
    }

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        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) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

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

可见每次会先保存到内存,然后再写入文件,而写文件的操作又是在子线程中依次按顺序执行的(上面代码最后一行)。
那么结论就出来了:

当多个进程同时而又高频的调用commit方法时,就会导致文件被反复覆盖写入,而并没有被及时读取,所以造成进程间数据的不同步

2.通过ContentProvider进行实现多进程共享SharedPreferences

既然SP有多进程不同步的隐患,那么我们怎么要怎么解决呢?
多进程同步的方法中,ContentProvider、Messenger、AIDL等方式都是基于Binder实现的,所以本质上并没有太大差别,而ContentProvider又是Android提倡的数据提供组件,所以我选择它来实现多进程SP操作。

具体怎么做呢?
SP本身的调用方式已经提供了较高的存取便利性,所以我们只要封装出一个SPHelper,去调用SPContentProvider,SPContentProvider用于保证跨进程的同步性,其内部再用SPHelperImpl来做真正的实现即可。

对SP的封装结构

那怎么用SPContentProvider来实现数据存取操作呢?实现ContentProvider需要实现几个方法,这些方法分别对应了ContentResolver中的同名方法,我们可以通过ContentResolver来调用这些方法,达到传递数据和进行命令解析的目的。


ContentProvider需要实现的方法

SP中的方法有sava,get,remove,clean,getAll这几种。那么我们可以用update或insert来实现save;用delete实现clean和remove,用getType或者query实现get和getAll。
所以最终的调用方式如下如所示:

SP调用getInt()的传递过程

下面看看代码中是如何处理的,还是以getInt()为例:

public class SPHelper {
    ...

    public static final String CONTENT="content://";
    public static final String AUTHORITY="com.pl.sphelper";
    public static final String SEPARATOR= "/";
    public static final String CONTENT_URI =CONTENT+AUTHORITY;
    public static final String TYPE_INT="int";
    public static final String NULL_STRING= "null";

    public static int getInt(String name, int defaultValue) {
        ContentResolver cr = context.getContentResolver();
        Uri uri = Uri.parse(CONTENT_URI + SEPARATOR + TYPE_INT + SEPARATOR + name);
        String rtn = cr.getType(uri);
        if (rtn == null || rtn.equals(NULL_STRING)) {
            return defaultValue;
        }
        return Integer.parseInt(rtn);
    }
    ...
}

public class SPContentProvider extends ContentProvider{
    ...
    public static final String SEPARATOR= "/";

    public String getType(Uri uri) {
        // 用这个来取数值
        String[] path= uri.getPath().split(SEPARATOR);
        String type=path[1];
        String key=path[2];
        return  ""+SPHelperImpl.get(getContext(),key,type);
    }
    ...
}

class SPHelperImpl {
    ...

    public static final String TYPE_INT="int";

    static String get(Context context, String name, String type) {
        if (type.equalsIgnoreCase(TYPE_STRING)) {
            return getString(context, name, null);
        } else if (type.equalsIgnoreCase(TYPE_BOOLEAN)) {
            return getBoolean(context, name, false);
        } else if (type.equalsIgnoreCase(TYPE_INT)) {
            return getInt(context, name, 0);
        } else if (type.equalsIgnoreCase(TYPE_LONG)) {
            return getLong(context, name, 0L);
        } else if (type.equalsIgnoreCase(TYPE_FLOAT)) {
            return getFloat(context, name, 0f);
        } else if (type.equalsIgnoreCase(TYPE_STRING_SET)) {
            return getString(context, name, null);
        }
        return null;
    }

    static int getInt(Context context, String name, int defaultValue) {
        SharedPreferences sp = getSP(context);
        if (sp == null) return defaultValue;
        return sp.getInt(name, defaultValue);
    }
    ...
}

其中SPContentProvider必须在AndroidManifest中声明,并设置android:authorities="com.pl.sphelper"。

3.优化性能和内存

本来以为做完上面的工作就算完了,但是在实际使用的过程中,发现一个问题,就是内存消耗比较大。tracking后发现是由于生成的SharedPreferences.Editor对象占用了大量内存,这是因为我的应用场景中,会频繁的将运行过程中的几个数据存储到SP中,所以导致大量生成Editor对象。但是实际上,很多保存的值是相同的,此时可以考虑使用缓存机制,而不用重复写入,代码如下:


private static SoftReference<Map<String, Object>> sCacheMap;

private static Object getCachedValue(String name) {
    if (sCacheMap != null) {
        Map<String, Object> map = sCacheMap.get();
        if (map != null) {
            return map.get(name);
        }
    }
    return null;
}

private static void setValueToCached(String name, Object value) {
    Map<String, Object> map;
    if (sCacheMap == null) {
        map = new HashMap<>();
        sCacheMap = new SoftReference<Map<String, Object>>(map);
    } else {
        map = sCacheMap.get();
        if (map == null) {
            map = new HashMap<>();
            sCacheMap = new SoftReference<Map<String, Object>>(map);
        }
    }
    map.put(name, value);
}
synchronized static <T> void save(Context context, String name, T t) {
    SharedPreferences sp = getSP(context);
    if (sp == null) return;

    if (t.equals(getCachedValue(name))) {
        return;
    }
    SharedPreferences.Editor editor = sp.edit();
    if (t instanceof Boolean) {
        editor.putBoolean(name, (Boolean) t);
    }
    if (t instanceof String) {
        editor.putString(name, (String) t);
    }
    if (t instanceof Integer) {
        editor.putInt(name, (Integer) t);
    }
    if (t instanceof Long) {
        editor.putLong(name, (Long) t);
    }
    if (t instanceof Float) {
        editor.putFloat(name, (Float) t);
    }
    editor.commit();
    setValueToCached(name, t);
}

实测内存使用量下降80%(这么大比例的原因有一部分是因为我一原先消耗的内存太多了),改进效果还是比较明显的。

4.还没有完,SPHelper会造成多大的性能损失呢?

既然是多进程间的交互,肯定会造成一定的性能损失,那么具体是多少呢?我用以下代码测试了一下:

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        Context appContext = InstrumentationRegistry.getTargetContext();
        SPHelper.init((Application) appContext.getApplicationContext());
        Random random = new Random();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            SPHelper.save("key" + random.nextInt(200), i);
        }
        long end = System.currentTimeMillis();
        Log.e("ExampleInstrumentedTest", "SPHelper takes " + (end - start) + "millis");

        start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            SharedPreferences sp = appContext.getSharedPreferences("text", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sp.edit();
            editor.putInt("key" + random.nextInt(200), i);
            editor.commit();
        }
        end = System.currentTimeMillis();
        Log.e("ExampleInstrumentedTest", "SharedPreferences takes " + (end - start) + "millis");
    }
}

代码中是SPHelper和系统默认的SharedPreferences ,进行了100次的SP储存操作,输出消耗的时间(单位:毫秒)。

当调用方和SPContentProvider在相同的进程中时,性能如下:

调用方式第一次第二次第三次第四次第五次
SPHelper966903944987951
SharedPreferences850836904844838
性能差距0.140.080.040.170.13

可见性能损失并不大,大概10%左右。这是因为Binder的工作方式,当在两者同一个进程中是,只是相当于函数调用,不会引起太大的消耗,具体的可以阅读Binder源码。所以这10%的差距基本上都是由于对数据的包装和解包装所产生的。

那么进程间通信的消耗到底有多大呢,下表是调用方和SPContentProvider在不同的进程中时的结果:

调用方式第一次第二次第三次第四次第五次
SPHelper13741283136612861296
SharedPreferences829850861886869
性能差距0.660.510.590.450.49

可见通过三次函数就行转发调用,再加上进程间通信,造成的性能损失还是比较可观的,达到50%以上。

需要注意的是,如果SPContentProvider所在的进程只有这个ContentProvider而没有其他组件的话,第一次调用时才会启动进程,所以会耗费大约50ms以上的时间,而且调用完以后一段时间,这个进程就会自动消耗,所以使用的时候,最好把SPContentProvider放到最常用SP的进程中,这样才能保证性能消耗最小。

5.源码和Demo

源码,demo和测试用例都在Github上:SPHelper
如果对你有帮助的话,可以给我star!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
视频名称 源码 -------------------------------------------------------------------------------- 04_开发与运行(卸载)第一个ANDROID应用.avi 所在项目:Hello 06_电话拔号器.avi 所在项目:phone 08_短信发送器.avi 所在项目:sms 09_深入了解各种布局技术.avi 所在项目:sms & FrameLayout 10_对应用进行单元测试.avi 所在项目:junitest 11_查看与输出日志信息.avi 所在项目:junitest 12_文件的保存与读取.avi 所在项目:File 13_文件的操作模式.avi 所在项目:File & other 14_把文件存放在SDCard.avi 所在项目:File 15_采用Pull解析器解析和生成XML内容.avi 所在项目:xml 16_采用SharedPreferences保存用户偏好设置参数.avi 所在项目:SharedPreferences 17_创建数据库与完成数据添删改查.avi 所在项目:db 18_在SQLite中使用事务.avi 所在项目:db 19_采用ListView实现数据列表显示.avi 所在项目:db 20_采用ContentProvider对外共享数据.avi 所在项目:db & other 21_监听ContentProvider数据的变化.avi 所在项目:db & other & Aapp 22_访问通信录中的联系人和添加联系人.avi 所在项目:contacts 23_网络通信之网络图片查看器.avi 所在项目:netimage & Web端应用:web 24_网络通信之网页源码查看器.avi 所在项目:HtmlViewer & Web端应用:web 25_网络通信之视频资讯客户端.avi 所在项目:news & Web端应用:web 26_采用JSON格式返回数据给资讯客户端.avi 所在项目:news & Web端应用:web 27_网络通信之通过GET和POST方式提交参数给web应用.avi 所在项目:newsmanage & Web端应用:web 28_网络通信之通过HTTP协议实现文件上传.avi 所在项目:newsmanage & Web端应用:web 29_发送xml数据和调用webservice.avi 所在项目:mobileAddressQuery & Web端应用:web 30_多线程下载原理.avi 所在项目:net 31_多线程断点下载器.avi 所在项目:MulThreadDownloader 32_文件断点上传器.avi 所在项目:videoUpload & javaSE应用:socket 33_为应用添加多个Activity与参数传递.avi 所在项目:MulActivity 34_Activity的启动模式.avi 所在项目:LaunchMode & openSingleInstance & singleInstance 35_Intent深入解剖.avi 所在项目:Intent 36_Activity生命周期.avi 所在项目:ActivityLife 37_采用广播接收者实现短信窃听器.avi 所在项目:SMSListener & Web端应用:web 38_采用广播接收者拦截外拔电话与其特性.avi 所在项目:SMSListener 39_采用Service实现电话窃听器.avi 所在项目:phonelistener 40_建立能与访问者进行相互通信的本地服务.avi 所在项目:studentquery 41_使用AIDL实现进程通信.avi 所在项目:remoteService & remoteServiceClient 42_服务的生命周期.avi 43_音乐播放器.avi 所在项目:audioplayer 44_在线视频播放器.avi 所在项目:videoplayer 45_拍照.avi 所在项目:takepicture 46_视频刻录.avi 所在项目:videoRecorder 47_手势识别.avi 所在项目:gesture & GestureBuilder 48_实现软件国际化.avi 所在项目:i18n 49_屏幕适配.avi 所在项目:ScreenAdapter 50_样式与主题.avi 所在项目:style 51_编码实现软件界面.avi 所在项目:codeUI 52_发送状态栏通知.avi 所在项目:Notification 53_采用网页设计软件界面.avi 所在项目:htmlUI 54_tween动画.avi 所在项目:tween 55_frame动画的实现.avi 所在项目:frameAnimation 56_activity切换动画与页面切换动画.avi 所在项目:animation 57_采用方向传感器实现指南针.avi 所在项目:sensor 58_拖拉功能与多点触摸.avi 所在项目:DragScale 59_各种图形的使用介绍.avi 所在项目:drawable 60_meta-data的使用.avi 所在项目:metadata 61_Widget.avi 所在项目:Widgets 62_自定义窗口标题.avi 所在项目:customtitle 63_PopupWindow.avi 所在项目:PopupWindow 64_ListView数据异步加载与AsyncTask.avi 所在项目:DataAsyncLoad 65_ListView数据的分批加载.avi 所在项目:datapageload 66_自定义标签页.avi 所在项目:tabhost 67_TraceView性能测试与Android应用性能优化方案.avi 所在项目:tabhost Android核心基础加强内容(暂不对外公布) 复杂UI界面设计、GPS与GoogleMap、自定义View、Ubuntu Linux下使用C语言面向底层开发、通过JNI进行底层组件调用、图形与OpenGl ES、界面特效、下载修改及编绎Android框架代码。
传智播客 Android 视频教程 课程源码 课程安排 第一天 1>搭建Android开发环境 2> 创建与启动手机模拟器 3> 学习使用ANDROID操作系统 4> 开发与运行(卸载)第一个ANDROID应用 5> 项目的目录结构 6> 项目清单文件分析 7> 分析第一个ANDROID应用的启动过程 8> 电话拔打 9> 查看手机模拟器往控制台输出的日志信息 10> 如何部署应用到真实手机 11> 短信发送 12> 布局介绍 LinearLayout (线性布局)、AbsoluteLayout(绝对布局)、RelativeLayout(相对布局)、TableLayout(表格布局)、FrameLayout(帧布局) 第二天 1> 单元测试 2> 查看与输出日志信息 3> 文件操作 4> 往SDCard读写文件 5> XML解析(SAX/DOM/PULL),写xml文件 6> SharedPreferences 第三天 1> SQLite数据库添删改查操作 A.创建数据库 B.SQLiteOpenHelper自动创建数据库的原理实现 C.数据库版本变化 D.编写代码完成添删改查操作(两种实现方法) E.事务的实现 F.采用ListView实现数据列表显示 2> 采用ContentProvider对外共享数据 第四天 1> 往通信录添加联系人,和获取联系人 2> 网络--获取数据(图片、网页、xml、Json等) 3> 如何把数据通过HTTP协议提交到网络上的Web应用(get / post ) 数据大于2k的时候 A.通过Get方式提交参数给Web应用 B.通过Post方式提交参数给Web应用 C.使用HttpClient开源项目提交参数给服务器 4> 网络--通过HTTP协议实现文件上传 第五天 1> 网络--通过HTTP协议发送XML数据,并调用webservice实现手机号归属地查询 2> 网络--通过HTTP协议实现多线程断点续传下载 3> 为应用添加新的Activity与参数传递 4> 意图 第六天 1> Activity的生命周期 2> 广播接收者(实现短信监听) 3> 服务与语音刻录(实现电话监听)、使用AIDL实现进程通信 4> 音乐播放器 5> 视频播放器 第七天 1> 拍照 2> 视频录制 3> 手势识别 4> 国际化(文字、图片)、屏幕适配、样式与主题 5> 编码实现软件界面 第八天 1> 采用HTML设计软件界面 2> 传感器的使用和拖拉功能实现 3> 软件打包与发布,生成私钥签名你的软件 4> 简历介绍 第九天以后 讲解Android手机视频客户端、来电知了、新浪微博客户端等项目 项目名称 Android手机视频客户端:本系统专为视频网站与电视媒体而开发的手机视频客户端,通过该软件,手机用户可以在线观看视频,在线播放MP3,上传视频,下载视频,视频搜索,视频共享、了解最新最热视频资讯。并且可以使用手机摄像头对事件发生的现场进行拍摄并同步上传至视频网站,网友可以实时观看现场发生的一切,使用该软件,每个手机用户都成为视频网站或电视媒体的现场记者。本软件包含手机客户端和服务器端软件,服务器端软件基于javaee技术构建,主要用于为客户端提供数据、接收客户端上传数据和管理数据。本软件可以进行二次定做,基础价为15万,如需额外功能,价格面议,欲购此软件的企业请与传智播客联系。 来电知了:该软件为共享软件,提供来电归属地信息显示、黑名单拦截、防电信诈骗、生活资讯查询等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值