編輯:關於Android編程
背景 最近在工作中遇到一個需求,需要在接收到推送的時候將推送獲得的數據存起來,以供app啟動時使用。我們會認為這不是So easy嗎?只要把數據存到SharedPreferences中,然後讓app打開同一個SharedPreferences讀取數據就可以了。但是在實際的測試中,我們發現推送進程存入的數據,並不能在app進程中獲得。所以這是為什麼呢,也許聰明的讀者從我們上面的陳述中已經發現了原因,因為我們有兩個進程,推送進程負責將推送數據存入,而app進程負責讀取,但是正是由於是兩個進程,如果它們同時存在,它們各自在內存中保持了自己的SP對象和數據,在推送進程中的存入並不能在app進程體現出來,並且可能會被app進程刷掉更改的數據。那麼我們怎麼做才能讓這兩邊共享數據呢?請看下面陳述。
一、多進程支持的SharedPreferences(不推薦)
我們原來的做法是使用SharedPreferences, 自然而然想到,SharedPreferences 在MODE_PRIVATE MODE_PUBLIC 之外其實還可以設置多進程的Flag ———— MODE_MULTI_PROCESS
復制代碼 代碼如下:SharedPreferences myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);
一旦我們設置了這個Flag,每次調用Context.getSharedPreferences 的時候系統會重新從SP文件中讀入數據,因此我們在使用的時候每次讀取和存入都要使用Context.getSharedPreferences 重新獲取SP實例。即使是這樣,由於SP本質上並不是多進程安全的,所以還是無法保證數據的同步,因此該方法我們並沒有使用,我們也不推薦使用。
二、Tray
如果SP不是多進程安全的,那麼是否有多進程安全的,又有SP功能的第三方項目呢。答案是有的,Tray——一個多進程安全的SharedPreferences,我們可以在Github上找到它,如果是AndroidStudio,可以直接使用Gradle引入,可謂是十分方便,如下是使用的代碼,十分簡單,沒有apply commit,看起來比SP還要簡單。
// 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
但是最終我們並沒有選擇使用它,主要的原因是它需要minSdk 為15,而我們是支持sdk14的,所以只能果斷放棄了。
三、ContentProvider
既然Tray不支持sdk15以下的,那麼我們是否可以使用Tray的原理自己實現一個呢?在閱讀Tray的源碼時我們發現其實它是在ContentProvider的基礎上做的,而ContentProvider是Android官方支持的多進程安全的。以下是使用ContentProvider的一個例子。
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); } } }
我們需要創建一個類繼承自ContentProvider,並重載以下方法。 - onCreate(),用來執行一些初始化的工作。 - query(Uri, String[], String, String[], String),用來返回數據給調用者。 - insert(Uri, ContentValues),用來插入新的數據。 - update(Uri, ContentValues, String, String[]),用來更新已有的數據。 - delete(Uri, String, String[]),用來刪除數據。 - getType(Uri),用來返回數據的MIME類型。
具體使用參考 Android應用程序組件Content Provider應用實例這篇博客,這裡不再贅述。 在以上對ContentProvider的使用過程中,我們發現過程比較繁瑣,如果對於比較復雜的需求可能還比較使用,但是我們這裡的需求其實很簡單,完全不需要搞得那麼復雜,所以最後我們也沒有使用這個方法(你可以理解為本博主比較Lazy)。
#Broadcast 那麼是否有更簡單的方法呢?由於想到了ContentProvider,我們不由地想到另一個android組件,BroadcastReceiver。那麼我們是否可以使用Broadcast 將我們收到的推送數據發送給app進程呢。bingo,這似乎正是我們尋找的又簡單又能解決問題的方法。我們來看下代碼。
首先在推送進程收到推送消息時,我們將推送數據存入SP,如果這時候沒有app進程,那麼下次app進程啟動的時候該存入的數據就會被app進程讀取到。而如果這時候app進程存在,那麼之後的代碼就會生效,它使用LocalBroadcastManager 發送一個廣播。LocalBroadcastManager發送的廣播不會被app之外接收到,通過它注冊的Receiver也不會接收到app之外的廣播,因此擁有更高的效率。
pushPref.add(push); Intent intent = new Intent(PushHandler.KEY_GET_PUSH); intent.putExtra(PushHandler.KEY_PUSH_CONTENT, d); LocalBroadcastManager.getInstance(context).sendBroadcastSync(intent);
而我們在app進程則注冊了一個BroadReceiver來接收上面發出的廣播。在收到廣播之後將推送數據存入SP。
public class PushHandler { public static String KEY_GET_PUSH = "PUSH_RECEIVED"; public static String KEY_PUSH_CONTENT = "PUSH_CONTENT"; // region 推送處理push /** * 當有推送時,發一次請求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, "存儲推送內容出錯"); } } }; 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 }
該方法相對於上面的方法使用簡單,安全可靠,能夠比較好的實現我們的需求。不過,在需求比較復雜的時候還是建議使用ContentProvider,因為畢竟這樣的方法不是堂堂正道,有種劍走偏鋒的感覺。
總結
實現一個需求可以有很多方法,而我們需要尋找的是又簡單有可靠的方法,在寫代碼之前不如多找找資料,多聽聽別人的意見。
接上篇Android 開發第五彈:簡易時鐘(鬧鐘) ,這次是一個時鐘類應用,目前依舊是主要的功能,長得還是很挫。當然了,核心功能是有的……時鐘
筆者發現在很多應用中,都有自動獲取驗證碼的功能:點擊獲取驗證碼按鈕,收到短信,當前應用不需要退出程序就可以獲取到短信中的驗證碼,並自動填充。覺得這種用戶體驗很贊,無須用戶
思路分析:1、自定義View實現字母導航欄2、ListView實現聯系人列表3、字母導航欄滑動事件處理4、字母導航欄與中間字母的聯動5、字母導航欄與ListView的聯動
API IntroductionContent providers are one of the primary building blocks(構件) of Andro