類似音樂、錄音機,需要使用者長時間在後臺使用的產品
筆者之前的專案一直在做跑步app, 使用者的場景是這樣的,使用者開啟跑步模式後,我們需要監聽Gps 訊號來統計使用者的運動資料,包括距離,配速,時間。其實是看似很「簡單”的使用者場景, 起初筆者也這麼認為,經過了一段時間的迭代完善,現在就來分享一些其中的」不簡單「。筆者會從一個跑步app開發者的角度分享這樣一個跑步App的架構演化。
筆者爲了儘快實現產品經理的需求,馬不停蹄的完成了app 的最初版,這時這個架構是這樣的
Activity + Forground Service + Sqlite+Eventbus
其中: Activity 代表UI 層, Service 代表開啟跑步模式時啟動的forground service,用以記錄運動資料,Sqlite 代表資料的儲存層, eventbus 是一個事件匯流排的library,用於模組間解耦。
最初版發出之後,收到一些使用者反饋,反應運動資料里程丟失,記錄不準,這樣的問題對於一款資料統計的運動app來說是致命的,那麼為什麼會有這樣的問題呢?很容易猜到,因為我們app的程序被回收了
主要做了UI程序與Service程序分離和一些service保活的策略,主要基於一下兩點原因
- Android程序管理機制
這裏就不得不提到Android 的對於程序管理的機制,Android 系統是通過Low Memory Killer 機制(參考)來管理程序的,對於程序分為幾個優先順序:
- native
- persistent
- forground
- visible
- cache
每個程序的優先順序取決於系統計算oom_adj 的值,那麼影響oom_adj的因素有哪些呢?主要是程序佔用記憶體的大小- 便於系統回收資源
對於跑步這類app而言,使用者場景很長時間是處於後臺執行的狀態,前臺UI只負責互動,後臺的service負責業務的處理,而且UI程序的記憶體佔遠大於Sevice的記憶體佔用,所以如果能夠在app切換到後臺的時候釋放掉所有的UI資源,那麼這個app執行時就能夠 省出大量記憶體。
基於以上兩點原因, 於是有了第二版的重構,架構變成了這樣:
UI程序 + Remote程序(service 程序)
那麼問題來了,app從單程序變成多程序會存在哪些坑呢?筆者主要遇到了三個問題
針對第一個問題,多程序通訊的方式:
1.Broadcast :
這種方式的所有通訊協議都需要放在intent裏面傳送和接受,是一種非同步的通訊方式,即呼叫後無法立刻得到返回結果。另外還需要在UI和service段都要註冊receiver才能達到他們之間的相互通訊。
2.android/os/Messenger.html" target="_blank">Messager
Messenger的使用 方法比較簡單,定義一個Messenger並指定一個handler作為通訊的介面,在onBind的時候返回Messenger的getBinder方 法,並在UI利用返回的IBinder也建立一個Messenger,他們之間就可以進行通訊了。這種呼叫方法也屬於非同步呼叫。
3.android/os/ResultReceiver.html" target="_blank">ResultReceiver 跨元件的非同步通訊,常用於請求-回撥模式.
4.重寫Binder
這種通過aidl進行通訊
我們選擇了最後一種方案:
主程序通過bindservice 調起remote 程序,並在onServiceConnection時,註冊一個remote 程序的callback 回撥,用於監聽,接收remote程序的訊息。
- 首先在AndroidManifest.xml 中宣告
<serviceandroid:name=".RemoteService" android:process=":remote" android:label="@string/app_name" />
- 宣告aidl介面
//aidl service 程序持有的物件 interface IRemoteService { void registerCallback(IRemoteCallback cb); void unregisterCallback(IRemoteCallback cb); }
//回撥更新UI程序資料的介面 interface IRemoteCallback { void onDataUpdate(double distance,double duration, double pace, double calorie, double velocity); }
- 重寫RemoteService Binder
LocalBinder mBinder = new LocalBinder(); IRemoteCallback mCallback; class LocalBinder extends IRemoteService.Stub { @Override public void registerCallback(IRemoteCallback cb) throws RemoteException { mCallback = cb; } @Override public void unregisterCallback(IRemoteCallback cb) throws RemoteException { mCallback = null; } public IBinder asBinder() { return null; } }
- 重寫UI程序的Binder
public class RemoteCallback extends IRemoteCallback.Stub { @Override public void onActivityUpdate(final double distance, final double duration, final double pace, final double calorie, final double velocity) throws RemoteException { //do something } }
- onServiceConnection 時將UI 程序的binder 註冊到remote程序
@Override public void onServiceConnected(ComponentName name, IBinder service) { try { mService = IRemoteService.Stub.asInterface(service); mService.registerCallback(mCallback); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { try { if (mService != null) { mService.unregisterCallback(mCallback); } } catch (RemoteException e) { e.printStackTrace(); } mService = null; }
第二個問題,兩個程序如何訪問資料保證一致性:ContentProvider
在Sqlite 上層封裝一層ContentProvider
於是現有的架構變成了:
UI process: Activity + eventbus
Remote process : Service + ContentProvider + Sqlite + Eventbus
還有第三個問題:
使用者需求:多個程序需要獲取跑步的狀態資訊,比如跑步中,跑步暫停還是跑步結束。
一個程序的時候使用SharePreference儲存一個持久化的狀態,分程序之後,開始使用android/content/Context.html#MODE_MULTI_PROCESS" target="_blank">MODE_MULTI_PROCESS, 而後來發現文件註釋被廢棄掉了,multi_process 模式下sharepreference工作不會可靠,同步資料不會一致,如下描述:
SharedPreference loading flag: when set, the file on disk will be checked for modification even if the shared preferences instance is already loaded in this process. This behavior is sometimes desired in cases where the application has multiple processes, all writing to the same SharedPreferences file. Generally there are better forms of communication between processes, though.
那麼如何解決呢?
兩種方案
- 1.ContentProvider+ Sqlite
Tray(https://github.com/grandcentrix/tray/)- 2.ContentProvider + SharePreference(MODE_PRIVATE)
DPreference(https://github.com/DozenWang/DPreference)
效能比較
DPreference setString
called 1000 times cost : 375 ms getString
called 1000 times cost : 186 ms
Tray setString
called 1000 times cost : 13699 ms getString
called 1000 times cost : 3496 ms
方案1還有一個缺點,如果將老的SharePreference 資料遷移到 用sqlite的方式需要全部拷貝,而方案二天然的避免了這樣的問題,並且讀寫效能更佳,於是採用了方案二。
於是架構變成了這樣:
UI process: Activity + eventbus
Remote process : Service + (ContentProvider + Sqlite)+ (ContentProvider + SharePreference) + Eventbus
本文來自開發者頭條