美文网首页android开发专题跑步资料Android
Android 后台任务型App多进程架构演化

Android 后台任务型App多进程架构演化

作者: Dozen | 来源:发表于2016-05-19 16:45 被阅读5131次

    什么是后台任务型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.

    那么如何解决呢?
    两种方案

    性能比较
    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

    以上就是笔者在多进程开发中遇到的一些问题和解决方案,希望可以对大家有所帮助

    相关文章

      网友评论

      • heybik:@Dozen ,我也在做运动类app,需要使用gps, 你们有没有app在后台(锁屏 灭屏)等情况下 gps的locationChanged得不到回调的情况. 可能与Android 6.0+后的 doze模式相关
        heybik:@Dozen 谢谢你的方案,多进程我们的运动app目前还没做,就像你这篇文章中说的 多进程的通信的问题 我们有很多要处理的,保活只做了基本的 指导用户设置app的后台锁定等类似白名单的操作;丢失后的补全有做处理(丢失前后2个点的距离 作为补全距离),

        我这次遇到的应该是纯粹的app的service还在运行中,但是手机在锁屏的情况下得不到gps,猜测是doze模式 导致硬件不再给gps点了(非必现); 虽然官方声明前台service可以不被doze影响 但是也是Google到还有人遇到类似的问题,所以不知道你的app有没有遇到类似的问题 有没有什么有效的解法
        Dozen:@heybik 这种问题很大情况是进程保活问题,有几种解决方案:
        1.调研保活技术,提高保活能力
        2.从产品层面包装,将你们的产品添加至系统白名单。
        3.增加数据丢失后的补全方案,对用户的公里数进行补偿
      • 570e37344076:"性能比较
        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性能更好
      • 56a2e8b8a207:希望大神继续发文分享,非常喜欢你写的文章!
      • 木TT:哦,可以加 android:exported="false"
      • 木TT:"在Sqlite 上层封装一层ContentProvider" 这样应该其他应用也能访问你的Sqlite,是不是不太安全。
      • 4051eece2305:寫得不錯
        清晰好讀
      • 奋斗的Leo:貌似在不同线程下,unregisterCallback这个操作会报错。
        博主可以考虑用android提供的RemoteCallbackList,这个是专门用来删除跨进程listener的接口
        Dozen:@奋斗的Leo 恩,是的,多谢
        奋斗的Leo:@奋斗的Leo 打错,是不同进程下
      • brzhang:怎么不对比下集中进程间通讯,虽然出BC,其他都是依靠binder,但也应该有各自的优缺点吧。
      • 光源_Android:非常棒的记录,先mark,相信后面会用到。

      本文标题:Android 后台任务型App多进程架构演化

      本文链接:https://www.haomeiwen.com/subject/mbqgrttx.html