什么是后臺(tái)任務(wù)型app
類似音樂、錄音機(jī),需要用戶長(zhǎng)時(shí)間在后臺(tái)使用的產(chǎn)品
背景:
筆者之前的項(xiàng)目一直在做跑步app, 用戶的場(chǎng)景是這樣的,用戶開啟跑步模式后,我們需要監(jiān)聽Gps 信號(hào)來統(tǒng)計(jì)用戶的運(yùn)動(dòng)數(shù)據(jù)嘁捷,包括距離,配速显熏,時(shí)間雄嚣。其實(shí)是看似很“簡(jiǎn)單"的用戶場(chǎng)景, 起初筆者也這么認(rèn)為喘蟆,經(jīng)過了一段時(shí)間的迭代完善缓升,現(xiàn)在就來分享一些其中的”不簡(jiǎn)單“。筆者會(huì)從一個(gè)跑步app開發(fā)者的角度分享這樣一個(gè)跑步App的架構(gòu)演化蕴轨。
最初的架構(gòu)
筆者為了盡快實(shí)現(xiàn)產(chǎn)品經(jīng)理的需求港谊,馬不停蹄的完成了app 的最初版,這時(shí)這個(gè)架構(gòu)是這樣的
Activity + Forground Service + Sqlite+Eventbus
其中: Activity 代表UI 層橙弱, Service 代表開啟跑步模式時(shí)啟動(dòng)的forground service,用以記錄運(yùn)動(dòng)數(shù)據(jù)歧寺,Sqlite 代表數(shù)據(jù)的存儲(chǔ)層, eventbus 是一個(gè)事件總線的library,用于模塊間解耦棘脐。
引來的問題
最初版發(fā)出之后斜筐,收到一些用戶反饋,反應(yīng)運(yùn)動(dòng)數(shù)據(jù)里程丟失荆残,記錄不準(zhǔn)奴艾,這樣的問題對(duì)于一款數(shù)據(jù)統(tǒng)計(jì)的運(yùn)動(dòng)app來說是致命的,那么為什么會(huì)有這樣的問題呢内斯?很容易猜到,因?yàn)槲覀僡pp的進(jìn)程被回收了
如何解決
主要做了UI進(jìn)程與Service進(jìn)程分離和一些service毕裉洌活的策略俘闯,主要基于一下兩點(diǎn)原因
- Android進(jìn)程管理機(jī)制
這里就不得不提到Android 的對(duì)于進(jìn)程管理的機(jī)制,Android 系統(tǒng)是通過Low Memory Killer 機(jī)制(參考)來管理進(jìn)程的忽冻,對(duì)于進(jìn)程分為幾個(gè)優(yōu)先級(jí):
- native
- persistent
- forground
- visible
- cache
每個(gè)進(jìn)程的優(yōu)先級(jí)取決于系統(tǒng)計(jì)算oom_adj 的值真朗,那么影響oom_adj的因素有哪些呢?主要是進(jìn)程占用內(nèi)存的大小
-
便于系統(tǒng)回收資源
對(duì)于跑步這類app而言僧诚,用戶場(chǎng)景很長(zhǎng)時(shí)間是處于后臺(tái)運(yùn)行的狀態(tài)遮婶,前臺(tái)UI只負(fù)責(zé)交互蝗碎,后臺(tái)的service負(fù)責(zé)業(yè)務(wù)的處理,而且UI進(jìn)程的內(nèi)存占遠(yuǎn)大于Sevice的內(nèi)存占用旗扑,所以如果能夠在app切換到后臺(tái)的時(shí)候釋放掉所有的UI資源蹦骑,那么這個(gè)app運(yùn)行時(shí)就能夠 省出大量?jī)?nèi)存。
第二版的修改
基于以上兩點(diǎn)原因臀防, 于是有了第二版的重構(gòu)眠菇,架構(gòu)變成了這樣:
UI進(jìn)程 + Remote進(jìn)程(service 進(jìn)程)
那么問題來了,app從單進(jìn)程變成多進(jìn)程會(huì)存在哪些坑呢袱衷?筆者主要遇到了三個(gè)問題
- 1.進(jìn)程間如何通信
- 2.兩個(gè)進(jìn)程如何訪問數(shù)據(jù)保證進(jìn)程安全
- 3.如何保證進(jìn)程安全的操作sharepreference
針對(duì)第一個(gè)問題捎废,多進(jìn)程通信的方式:
1.Broadcast :
這種方式的所有通訊協(xié)議都需要放在intent里面發(fā)送和接受,是一種異步的通訊方式致燥,即調(diào)用后無法立刻得到返回結(jié)果登疗。另外還需要在UI和service段都要注冊(cè)receiver才能達(dá)到他們之間的相互通訊。
2.Messager
Messenger的使用 方法比較簡(jiǎn)單嫌蚤,定義一個(gè)Messenger并指定一個(gè)handler作為通訊的接口辐益,在onBind的時(shí)候返回Messenger的getBinder方 法,并在UI利用返回的IBinder也創(chuàng)建一個(gè)Messenger搬葬,他們之間就可以進(jìn)行通訊了荷腊。這種調(diào)用方法也屬于異步調(diào)用。
3.ResultReceiver 跨組件的異步通訊急凰,常用于請(qǐng)求-回調(diào)模式.
4.重寫B(tài)inder
這種通過aidl進(jìn)行通信
我們選擇了最后一種方案:
主進(jìn)程通過bindservice 調(diào)起remote 進(jìn)程女仰,并在onServiceConnection時(shí),注冊(cè)一個(gè)remote 進(jìn)程的callback 回調(diào)抡锈,用于監(jiān)聽疾忍,接收remote進(jìn)程的消息。
- 首先在AndroidManifest.xml 中聲明
<serviceandroid:name=".RemoteService"
android:process=":remote"
android:label="@string/app_name" />
- 聲明aidl接口
//aidl service 進(jìn)程持有的對(duì)象
interface IRemoteService {
void registerCallback(IRemoteCallback cb);
void unregisterCallback(IRemoteCallback cb);
}
//回調(diào)更新UI進(jìn)程數(shù)據(jù)的接口
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進(jìn)程的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 時(shí)將UI 進(jìn)程的binder 注冊(cè)到remote進(jìn)程
@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;
}
第二個(gè)問題床三,兩個(gè)進(jìn)程如何訪問數(shù)據(jù)保證一致性:ContentProvider
在Sqlite 上層封裝一層ContentProvider
于是現(xiàn)有的架構(gòu)變成了:
UI process: Activity + eventbus
Remote process : Service + ContentProvider + Sqlite + Eventbus
還有第三個(gè)問題:
用戶需求:多個(gè)進(jìn)程需要獲取跑步的狀態(tài)信息一罩,比如跑步中,跑步暫停還是跑步結(jié)束撇簿。
一個(gè)進(jìn)程的時(shí)候使用SharePreference存儲(chǔ)一個(gè)持久化的狀態(tài),分進(jìn)程之后聂渊,開始使用MODE_MULTI_PROCESS, 而后來發(fā)現(xiàn)文檔注釋被廢棄掉了,multi_process 模式下sharepreference工作不會(huì)可靠四瘫,同步數(shù)據(jù)不會(huì)一致汉嗽,如下描述:
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還有一個(gè)缺點(diǎn)找蜜,如果將老的SharePreference 數(shù)據(jù)遷移到 用sqlite的方式需要全部拷貝饼暑,而方案二天然的避免了這樣的問題,并且讀寫性能更佳,于是采用了方案二
于是架構(gòu)變成了這樣:
UI process: Activity + eventbus
Remote process : Service + (ContentProvider + Sqlite)+ (ContentProvider + SharePreference) + Eventbus
以上就是筆者在多進(jìn)程開發(fā)中遇到的一些問題和解決方案弓叛,希望可以對(duì)大家有所幫助