Android Service使用拾遗[阿里工程师分享]
Service作为android的四大组件之一常用来帮助我们完成一些需要放在后台处理的任务,通过startService和bindService两种方式被调用。因为Service也是在主线程中运行的,所以如果处理耗时任务,一般在Service里再单独创建工作线程去执行耗时任务。使用Service的另一个用处是可以减少业务逻辑与界面的耦合,在产品演进中具备快速迭代的能力。
有的应用有服务需要一直常驻在内存中,如果UI和service在同一进程中,当按home键退到后台时,因为有常驻的service,整个进程的状态是service,即使在低内存时LMK也不会杀掉这个进程,使内存难以回收。这样就得把需要常驻内存的、(几乎)没有界面显示的业务逻辑单独拆出来放在一个进程里,有界面显示的放在另一个进程里。但这么做会占用更多的内存,只有当应用位于后台时也要处理大量任务时才应该考虑让app运行在多个进程中。
除此之外,在使用过程中还会碰到低内存时的情形,有一些service的知识平时可能接触的不多,在这里我做了一些简单的总结。
一、startService
1.1 onStartCommand的返回值
常用的返回值有3种,START_STICKY、START_NOT_STICKY和START_REDELIVER_INTENT。其中START_STICKY和START_REDELIVER_INTENT在service没有执行完就被系统杀掉后的一段时间内会被系统重启,被系统杀掉的情形可能是在系统内存不足或者某些ROM定制了管理后台任务的策略,比如锁屏一段时间后,不在白名单中的应用会被杀掉以释放内存。如果是service本身的错误导致在没有执行完就crash退出,是不会被系统重启的。
1)START_NOT_STICKY
如果onStartCommand返回START_NOT_STICKY,那即使service没有执行完,被杀掉后也不会被系统重启。如果这个service是用来为界面的Activity处理数据用的,那它不是必须一定要执行完的,大部分的情形是service所在的应用进程都已经被系统杀掉,这时没必要重启再次执行service。
2)START_STICKY
被杀掉后系统会重启service,并且onStartCommand一定会被调用,如果在重启期间没有任何启动命令被传递到service,那么参数Intent将为null。这里说的重启期间是指系统杀掉service后到系统再次启动该service的时间间隔,那么这段时间有多长呢,frameworks有专门处理重启service的代码。在ActiveServices.java的scheduleServiceRestartLocked函数里,
这个分支是处理非persistent的情况,只有系统应用才有权限把自己设为persistent,即在AndroidManifest.xml里设了android:persistent="true"。deliveredStarts中存放的是已经传递给service的启动参数,pendingStarts中存放的是还没有传递给service的启动参数。minDuration是被系统杀掉后被系统重启的时间间隔,resetTime是重置这个重启时间的间隔。如果Intent不为null,会更新这两个值,
然后重新计算service下一次被重启的时间。如果之前没被重启过,restartDelay为0,则把restartDelay设为minDuration。如果之前重启过,当前时间距上次重启的时间已经超过了resetTime,则把restartCount置为1,restartDelay设为minDuration;如果距上次重启时间还不到resetTime,则调大restartDelay。这是为了防止service被在内存不足的情况下被频繁重启,第一次内存不足时杀掉service,1s后重启该service,重启后又消耗了一部分内存造成内存再次不足再次杀掉service,这时1s后就不应该重启了,要往后推迟一段时间再尝试重启。
nextRestartTime就是下次重启service的时间了,然后postAtTime在nextRestartTime这个时间点重启service,并且更新nextRestartTime。
因为START_STICKY类型默认传入的Intent为null,所以在使用时我们要仔细考虑。如果service需要使用Intent里的参数,那很有可能被重启时并没有调用者能传入这个参数。比如,该service是在某场景下才会被本应用的其他组件所调用启动,那么有可能整个应用都被杀掉了,重启该service时,只会经过Application的onCreate和该service的onCreate、onStartCommand,没有经过调用启动的上下文。或者是收到broadcast而触发该service,则重启期间可能不会收到broadcast。只有当service是必须要完成的,并且不依赖于传入的Intent才需要把返回值设为START_STICKY。
3)START_REDELIVER_INTENT
在重启时会重传被杀时未完成的Intent。比如该startService调用了4次,第1、2次的任务已经被service处理完(比如调用了stopSelf或stopService),第3、4次还未被处理时就被杀掉了,重启时会按顺序传入第3、4个Intent。重启后调用stopSelf的顺序要注意startId的顺序。因为第3、4次任务可能会被service 交给不同线程去执行,可能4先被执行完,如果4执行完后调用stopSelf(startId4)的话,那么3会被立即停止,即使它还没被执行完。所以stopSelf的顺序要严格按照收到onStartCommand中的startId来执行。
1.2.IntentService
在这里推荐使用IntentService,它有一个工作线程和一个Handler,可以通过回调函数onHandleIntent依次处理onStartCommand收到的Intent,在onHandleIntent调完后会自己调stopSelf。
1.3. startForeground
为了防止处于后台的service在低内存时被系统杀掉,service可以调用startForeground()把自己放在前台进程中,但最好在完成任务后及时调用stopForeground把优先级调回来。
二、bindService
2.1 bindService的flag
bindService的第三个参数flags一般都会传0或BIND_AUTO_CREATE,跨进程调用bindService会在引起依赖,比如A进程的Activity中bindService调用B进程service,则B进程的service的oom_adj值依赖于A进程Activity的oom_adj值。如果activity在前台,它的oom_adj值为0,service的值为1,两者都难以被系统杀掉。但如果把flags设为一些“弱连接”类型,比如设为BIND_WAIVE_PRIORITY,则即使Activity位于前台,oom_adj为0,service的oom_adj值为15,也可以很容易被杀掉。其他一些flags还有:
BIND_ABOVE_CLIENT:调用bindService的应用的oomAdj的值比service本身的oom_adj更高,比如activity在后台时,oom_adj为10,service的oom_adj为9,调用者activity更容易被杀掉。
BIND_ADJUST_WITH_ACTIVITY:service的重要性跟调用它的activity一样。比如activity在前台时,oom_adj为0,service的oom_adj也为0。
2.2 DeadObjectException和RemoteException
Service异常终止或者被系统杀掉后会抛出DeadObjectException,binder的IPC过程中如果在server端发生异常抛出,client端这边也会有RemoteException,客户端在调用服务端的接口的过程中,在需要时要注意捕获这两个异常。捕获后一般意味着远程对象已经不可用了,died或异常无法继续运行下去,因此在catch后一般会重新启动服务,或重新再调一遍接口来保证高可用性。
2.3 利用bindService实现进程间通信
前台(Foreground)和后台(Background)进程要实现双向通信,即相互传输一些数据或命令,在Android上并无现成的拿来可用的框架。一个解决方案是利用service,在前台和后台进程中各创建一个service,它们两个之间互相bind。同时,前台和后台进程各自有一个transfer和handler,用来发送和接收数据。
流程简述如下:
1)前台进程的Application中bindService启动BackService。
2)在onServiceConnected中ForeTransfer发送一个启动后台服务的命令START。
3)后台进程的BackHandler通过BackService收到该命令后,bindService启动ForeService。这样前后台进程都有一个service bind到对方上。
4)后台进程的模块A要向前台进程的模块B发送数据,就通过BackTransferàBackService àForeService àForeHandler,被ForeHandler收到,模块B在ForeHandler中实现自己收到数据的处理函数即可。
5)前后进程的Handler中也可以注册回调函数,告知Transfer数据是否已发送处理完,这是因为binder调用是同步的,所以整个通信过程也是同步的。
- 嵌入式企鹅圈原创团队由阿里、魅族、nvidia、龙芯、炬力、拓尔思等资深工程师组成。百分百原创,每周两篇,分享嵌入式、Linux、物联网、GPU、Android、自动驾驶等技术。欢迎扫码关注微信公众号:嵌入式企鹅圈,实时推送原创文章!