什么是后臺任務型app
類似音樂、錄音機,需要用戶長時間在后臺使用的產品
背景:
筆者之前的項目一直在做跑步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.進程間如何通信
- 2.兩個進程如何訪問數據保證進程安全
- 3.如何保證進程安全的操作sharepreference
針對第一個問題,多進程通信的方式:
1.Broadcast :
這種方式的所有通訊協議都需要放在intent里面發送和接受,是一種異步的通訊方式,即調用后無法立刻得到返回結果。另外還需要在UI和service段都要注冊receiver才能達到他們之間的相互通訊。
2.Messager
Messenger的使用 方法比較簡單,定義一個Messenger并指定一個handler作為通訊的接口,在onBind的時候返回Messenger的getBinder方 法,并在UI利用返回的IBinder也創建一個Messenger,他們之間就可以進行通訊了。這種調用方法也屬于異步調用。
3.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存儲一個持久化的狀態,分進程之后,開始使用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
以上就是筆者在多進程開發中遇到的一些問題和解決方案,希望可以對大家有所幫助