Android開(kāi)發(fā)中多進(jìn)程共享數(shù)據(jù)簡(jiǎn)析
背景 最近在工作中遇到一個(gè)需求,需要在接收到推送的時(shí)候?qū)⑼扑瞳@得的數(shù)據(jù)存起來(lái),以供app啟動(dòng)時(shí)使用。我們會(huì)認(rèn)為這不是So easy嗎?只要把數(shù)據(jù)存到SharedPreferences中,然后讓app打開(kāi)同一個(gè)SharedPreferences讀取數(shù)據(jù)就可以了。但是在實(shí)際的測(cè)試中,我們發(fā)現(xiàn)推送進(jìn)程存入的數(shù)據(jù),并不能在app進(jìn)程中獲得。所以這是為什么呢,也許聰明的讀者從我們上面的陳述中已經(jīng)發(fā)現(xiàn)了原因,因?yàn)槲覀冇袃蓚€(gè)進(jìn)程,推送進(jìn)程負(fù)責(zé)將推送數(shù)據(jù)存入,而app進(jìn)程負(fù)責(zé)讀取,但是正是由于是兩個(gè)進(jìn)程,如果它們同時(shí)存在,它們各自在內(nèi)存中保持了自己的SP對(duì)象和數(shù)據(jù),在推送進(jìn)程中的存入并不能在app進(jìn)程體現(xiàn)出來(lái),并且可能會(huì)被app進(jìn)程刷掉更改的數(shù)據(jù)。那么我們?cè)趺醋霾拍茏屵@兩邊共享數(shù)據(jù)呢?請(qǐng)看下面陳述。
一、多進(jìn)程支持的SharedPreferences(不推薦)
我們?cè)瓉?lái)的做法是使用SharedPreferences, 自然而然想到,SharedPreferences 在MODE_PRIVATE MODE_PUBLIC 之外其實(shí)還可以設(shè)置多進(jìn)程的Flag ———— MODE_MULTI_PROCESS
一旦我們?cè)O(shè)置了這個(gè)Flag,每次調(diào)用Context.getSharedPreferences 的時(shí)候系統(tǒng)會(huì)重新從SP文件中讀入數(shù)據(jù),因此我們?cè)谑褂玫臅r(shí)候每次讀取和存入都要使用Context.getSharedPreferences 重新獲取SP實(shí)例。即使是這樣,由于SP本質(zhì)上并不是多進(jìn)程安全的,所以還是無(wú)法保證數(shù)據(jù)的同步,因此該方法我們并沒(méi)有使用,我們也不推薦使用。
二、Tray
如果SP不是多進(jìn)程安全的,那么是否有多進(jìn)程安全的,又有SP功能的第三方項(xiàng)目呢。答案是有的,Tray——一個(gè)多進(jìn)程安全的SharedPreferences,我們可以在Github上找到它,如果是AndroidStudio,可以直接使用Gradle引入,可謂是十分方便,如下是使用的代碼,十分簡(jiǎn)單,沒(méi)有apply commit,看起來(lái)比SP還要簡(jiǎn)單。
// create a preference accessor. This is for global app preferences.
final AppPreferences appPreferences = new AppPreferences(getContext()); // this Preference comes for free from the library
// save a key value pair
appPreferences.put("key", "lorem ipsum");
// read the value for your key. the second parameter is a fallback (optional otherwise throws)
final String value = appPreferences.getString("key", "default");
Log.v(TAG, "value: " + value); // value: lorem ipsum
// read a key that isn't saved. returns the default (or throws without default)
final String defaultValue = appPreferences.getString("key2", "default");
Log.v(TAG, "value: " + defaultValue); // value: default
但是最終我們并沒(méi)有選擇使用它,主要的原因是它需要minSdk 為15,而我們是支持sdk14的,所以只能果斷放棄了。
三、ContentProvider
既然Tray不支持sdk15以下的,那么我們是否可以使用Tray的原理自己實(shí)現(xiàn)一個(gè)呢?在閱讀Tray的源碼時(shí)我們發(fā)現(xiàn)其實(shí)它是在ContentProvider的基礎(chǔ)上做的,而ContentProvider是Android官方支持的多進(jìn)程安全的。以下是使用ContentProvider的一個(gè)例子。
public class ArticlesProvider extends ContentProvider {
private static final String LOG_TAG = "shy.luo.providers.articles.ArticlesProvider";
private static final String DB_NAME = "Articles.db";
private static final String DB_TABLE = "ArticlesTable";
private static final int DB_VERSION = 1;
private static final String DB_CREATE = "create table " + DB_TABLE +
" (" + Articles.ID + " integer primary key autoincrement, " +
Articles.TITLE + " text not null, " +
Articles.ABSTRACT + " text not null, " +
Articles.URL + " text not null);";
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM);
uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID);
uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS);
}
private static final HashMap<String, String> articleProjectionMap;
static {
articleProjectionMap = new HashMap<String, String>();
articleProjectionMap.put(Articles.ID, Articles.ID);
articleProjectionMap.put(Articles.TITLE, Articles.TITLE);
articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT);
articleProjectionMap.put(Articles.URL, Articles.URL);
}
private DBHelper dbHelper = null;
private ContentResolver resolver = null;
@Override
public boolean onCreate() {
Context context = getContext();
resolver = context.getContentResolver();
dbHelper = new DBHelper(context, DB_NAME, null, DB_VERSION);
Log.i(LOG_TAG, "Articles Provider Create");
return true;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case Articles.ITEM:
return Articles.CONTENT_TYPE;
case Articles.ITEM_ID:
case Articles.ITEM_POS:
return Articles.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if(uriMatcher.match(uri) != Articles.ITEM) {
throw new IllegalArgumentException("Error Uri: " + uri);
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert(DB_TABLE, Articles.ID, values);
if(id < 0) {
throw new SQLiteException("Unable to insert " + values + " for " + uri);
}
Uri newUri = ContentUris.withAppendedId(uri, id);
resolver.notifyChange(newUri, null);
return newUri;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count = 0;
switch(uriMatcher.match(uri)) {
case Articles.ITEM: {
count = db.update(DB_TABLE, values, selection, selectionArgs);
break;
}
case Articles.ITEM_ID: {
String id = uri.getPathSegments().get(1);
count = db.update(DB_TABLE, values, Articles.ID + "=" + id
+ (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs);
break;
}
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
resolver.notifyChange(uri, null);
return count;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count = 0;
switch(uriMatcher.match(uri)) {
case Articles.ITEM: {
count = db.delete(DB_TABLE, selection, selectionArgs);
break;
}
case Articles.ITEM_ID: {
String id = uri.getPathSegments().get(1);
count = db.delete(DB_TABLE, Articles.ID + "=" + id
+ (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs);
break;
}
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
resolver.notifyChange(uri, null);
return count;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.i(LOG_TAG, "ArticlesProvider.query: " + uri);
SQLiteDatabase db = dbHelper.getReadableDatabase();
SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
String limit = null;
switch (uriMatcher.match(uri)) {
case Articles.ITEM: {
sqlBuilder.setTables(DB_TABLE);
sqlBuilder.setProjectionMap(articleProjectionMap);
break;
}
case Articles.ITEM_ID: {
String id = uri.getPathSegments().get(1);
sqlBuilder.setTables(DB_TABLE);
sqlBuilder.setProjectionMap(articleProjectionMap);
sqlBuilder.appendWhere(Articles.ID + "=" + id);
break;
}
case Articles.ITEM_POS: {
String pos = uri.getPathSegments().get(1);
sqlBuilder.setTables(DB_TABLE);
sqlBuilder.setProjectionMap(articleProjectionMap);
limit = pos + ", 1";
break;
}
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
Cursor cursor = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, TextUtils.isEmpty(sortOrder) ? Articles.DEFAULT_SORT_ORDER : sortOrder, limit);
cursor.setNotificationUri(resolver, uri);
return cursor;
}
@Override
public Bundle call(String method, String request, Bundle args) {
Log.i(LOG_TAG, "ArticlesProvider.call: " + method);
if(method.equals(Articles.METHOD_GET_ITEM_COUNT)) {
return getItemCount();
}
throw new IllegalArgumentException("Error method call: " + method);
}
private Bundle getItemCount() {
Log.i(LOG_TAG, "ArticlesProvider.getItemCount");
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("select count(*) from " + DB_TABLE, null);
int count = 0;
if (cursor.moveToFirst()) {
count = cursor.getInt(0);
}
Bundle bundle = new Bundle();
bundle.putInt(Articles.KEY_ITEM_COUNT, count);
cursor.close();
db.close();
return bundle;
}
private static class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DB_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
onCreate(db);
}
}
}
我們需要?jiǎng)?chuàng)建一個(gè)類(lèi)繼承自ContentProvider,并重載以下方法。 - onCreate(),用來(lái)執(zhí)行一些初始化的工作。 - query(Uri, String[], String, String[], String),用來(lái)返回?cái)?shù)據(jù)給調(diào)用者。 - insert(Uri, ContentValues),用來(lái)插入新的數(shù)據(jù)。 - update(Uri, ContentValues, String, String[]),用來(lái)更新已有的數(shù)據(jù)。 - delete(Uri, String, String[]),用來(lái)刪除數(shù)據(jù)。 - getType(Uri),用來(lái)返回?cái)?shù)據(jù)的MIME類(lèi)型。
具體使用參考 Android應(yīng)用程序組件Content Provider應(yīng)用實(shí)例這篇博客,這里不再贅述。 在以上對(duì)ContentProvider的使用過(guò)程中,我們發(fā)現(xiàn)過(guò)程比較繁瑣,如果對(duì)于比較復(fù)雜的需求可能還比較使用,但是我們這里的需求其實(shí)很簡(jiǎn)單,完全不需要搞得那么復(fù)雜,所以最后我們也沒(méi)有使用這個(gè)方法(你可以理解為本博主比較Lazy)。
#Broadcast 那么是否有更簡(jiǎn)單的方法呢?由于想到了ContentProvider,我們不由地想到另一個(gè)android組件,BroadcastReceiver。那么我們是否可以使用Broadcast 將我們收到的推送數(shù)據(jù)發(fā)送給app進(jìn)程呢。bingo,這似乎正是我們尋找的又簡(jiǎn)單又能解決問(wèn)題的方法。我們來(lái)看下代碼。
首先在推送進(jìn)程收到推送消息時(shí),我們將推送數(shù)據(jù)存入SP,如果這時(shí)候沒(méi)有app進(jìn)程,那么下次app進(jìn)程啟動(dòng)的時(shí)候該存入的數(shù)據(jù)就會(huì)被app進(jìn)程讀取到。而如果這時(shí)候app進(jìn)程存在,那么之后的代碼就會(huì)生效,它使用LocalBroadcastManager 發(fā)送一個(gè)廣播。LocalBroadcastManager發(fā)送的廣播不會(huì)被app之外接收到,通過(guò)它注冊(cè)的Receiver也不會(huì)接收到app之外的廣播,因此擁有更高的效率。
pushPref.add(push); Intent intent = new Intent(PushHandler.KEY_GET_PUSH); intent.putExtra(PushHandler.KEY_PUSH_CONTENT, d); LocalBroadcastManager.getInstance(context).sendBroadcastSync(intent);
而我們?cè)赼pp進(jìn)程則注冊(cè)了一個(gè)BroadReceiver來(lái)接收上面發(fā)出的廣播。在收到廣播之后將推送數(shù)據(jù)存入SP。
public class PushHandler {
public static String KEY_GET_PUSH = "PUSH_RECEIVED";
public static String KEY_PUSH_CONTENT = "PUSH_CONTENT";
// region 推送處理push
/**
* 當(dāng)有推送時(shí),發(fā)一次請(qǐng)求mPushReceiver
*/
private static BroadcastReceiver mPushReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Timber.i("在NoticeAction中收到廣播");
PushPref pushPref = App.DI().pushPref();
try {
String pushContent = intent.getStringExtra(KEY_PUSH_CONTENT);
PushEntity pushEntity = App.DI().gson().fromJson(pushContent, PushEntity.class);
pushPref.add(pushEntity);
} catch (Exception e){
Timber.e(e, "存儲(chǔ)推送內(nèi)容出錯(cuò)");
}
}
};
public static void startListeningToPush(){
try {
LocalBroadcastManager.getInstance(App.getContext()).registerReceiver(mPushReceiver, new IntentFilter(KEY_GET_PUSH));
} catch (Exception e) {
Timber.e(e, "wtf");
}
}
public static void stopListeningToPush(){
try {
LocalBroadcastManager.getInstance(App.getContext()).unregisterReceiver(mPushReceiver);
} catch (Exception e) {
Timber.e(e, "wtf");
}
}
// endregion
}
該方法相對(duì)于上面的方法使用簡(jiǎn)單,安全可靠,能夠比較好的實(shí)現(xiàn)我們的需求。不過(guò),在需求比較復(fù)雜的時(shí)候還是建議使用ContentProvider,因?yàn)楫吘惯@樣的方法不是堂堂正道,有種劍走偏鋒的感覺(jué)。
總結(jié)
實(shí)現(xiàn)一個(gè)需求可以有很多方法,而我們需要尋找的是又簡(jiǎn)單有可靠的方法,在寫(xiě)代碼之前不如多找找資料,多聽(tīng)聽(tīng)別人的意見(jiàn)。
- Android通過(guò)ViewModel保存數(shù)據(jù)實(shí)現(xiàn)多頁(yè)面的數(shù)據(jù)共享功能
- Android開(kāi)發(fā)之5.0activity跳轉(zhuǎn)時(shí)共享元素的使用方法
- Android數(shù)據(jù)共享 sharedPreferences 的使用方法
- android與asp.net服務(wù)端共享session的方法詳解
- 詳解Android(共享元素)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)開(kāi)發(fā)實(shí)踐
- Android 仿摩拜單車(chē)共享單車(chē)進(jìn)度條實(shí)現(xiàn)StepView效果
- Android設(shè)備間實(shí)現(xiàn)藍(lán)牙(Bluetooth)共享上網(wǎng)
- Android實(shí)現(xiàn)不同apk間共享數(shù)據(jù)的方法(2種方法)
- android編程實(shí)現(xiàn)設(shè)置、打開(kāi)wifi熱點(diǎn)共享供他人連接的方法
- Android編程實(shí)現(xiàn)兩個(gè)Activity之間共享數(shù)據(jù)及互相訪問(wèn)的方法
- android不同activity之間共享數(shù)據(jù)解決方法
- Android 7.0應(yīng)用之間如何共享文件
相關(guān)文章
android自定義View實(shí)現(xiàn)手勢(shì)解鎖
這篇文章主要為大家詳細(xì)介紹了android自定義View實(shí)現(xiàn)手勢(shì)解鎖,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06
Android string-array數(shù)據(jù)源簡(jiǎn)單使用
這篇文章主要介紹了Android string-array數(shù)據(jù)源簡(jiǎn)單使用的相關(guān)資料,需要的朋友可以參考下2016-09-09
Android 中ListView的Item點(diǎn)擊事件失效的快速解決方法
這篇文章主要介紹了Android 中ListView的Item點(diǎn)擊事件失效的快速解決方法的相關(guān)資料,需要的朋友可以參考下2016-09-09
Android自定義view實(shí)現(xiàn)列表內(nèi)左滑刪除Item
這篇文章主要介紹了微信小程序列表中item左滑刪除功能,本文分步驟給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-02-02
Android開(kāi)發(fā)之設(shè)置開(kāi)機(jī)自動(dòng)啟動(dòng)的幾種方法
這篇文章主要介紹了Android開(kāi)發(fā)之設(shè)置開(kāi)機(jī)自動(dòng)啟動(dòng)的幾種方法的相關(guān)資料,這里提供三種方法幫助大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-08-08
Android中okhttp3.4.1+retrofit2.1.0實(shí)現(xiàn)離線緩存
這篇文章主要介紹了Android中okhttp3.4.1結(jié)合retrofit2.1.0實(shí)現(xiàn)離線緩存,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
Android學(xué)習(xí)教程之日歷控件使用(7)
這篇文章主要為大家詳細(xì)介紹了Android學(xué)習(xí)教程之日歷控件操作代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Android 圖片的三級(jí)緩存機(jī)制實(shí)例分析
這篇文章主要介紹了Android 圖片的三級(jí)緩存機(jī)制實(shí)例分析的相關(guān)資料,需要的朋友可以參考下2017-05-05
android選擇視頻文件上傳到后臺(tái)服務(wù)器
這篇文章主要介紹了android選擇視頻文件上傳到后臺(tái)服務(wù)器的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
Android獲取清單文件中的meta-data,解決碰到數(shù)值為null的問(wèn)題
這篇文章主要介紹了Android獲取清單文件中的meta-data,解決碰到數(shù)值為null的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03

