什么是后台任务型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
以上就是笔者在多进程开发中遇到的一些问题和解决方案,希望可以对大家有所帮助
网友评论
我这次遇到的应该是纯粹的app的service还在运行中,但是手机在锁屏的情况下得不到gps,猜测是doze模式 导致硬件不再给gps点了(非必现); 虽然官方声明前台service可以不被doze影响 但是也是Google到还有人遇到类似的问题,所以不知道你的app有没有遇到类似的问题 有没有什么有效的解法
1.调研保活技术,提高保活能力
2.从产品层面包装,将你们的产品添加至系统白名单。
3.增加数据丢失后的补全方案,对用户的公里数进行补偿
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的方式需要全部拷贝,而方案二天然的避免了这样的问题,并且读写性能更佳,于是采用了方案二。
于是架构变成了这样"
到底选用了Tray 还是Dpref,总觉得前后对不上,说反了,看数据说Dpref性能更好
清晰好讀
博主可以考虑用android提供的RemoteCallbackList,这个是专门用来删除跨进程listener的接口