本文来自网易云社区
作者:苏甦
对于使用多进程方式的Android应用程序,需要通过绑定服务获得绑定对象,进行进程间通信。在某些特殊情况下,会遇到绑定对象无法返回的问题。文中分析了问题发生的原因,给出了解决的思路。同时,也可以通过阅读本文,来加深对Android服务管理的认识。
IApplicationThread接口包含了AMS对应用进程的各种调度方法(例如scheduleCreateService创建服务),ActivityThread.ApplicationThread做为IApplicationThread的Stub端,存在于应用程序进程中,所有接口方法的实现都通过消息投递给ActivityThread内部的Handler。
应用进程端调用栈
IActivityManager.attachApplication
ActivityThread.attach
ActivityThread.main
AMS进程端调用栈
IBinder.linkToDeath
ActivityManagerService.attachApplicationLocked
ActivityManagerService.attachApplication
应用进程端解析
Zygote进程fork出一个新的包含Android Runtime的进程后,进程进入ActivityThread.main,构造ActivityThread对象,并进入ActivityThread.attach中通过IActivityManager.attachApplication传递Binder(IApplicationThread)到AMS端。
AMS进程端解析
ActivityManagerService.attachApplicationLocked中构造ActivityManagerService.AppDeathRecipient对象并linkToDeath到Binder(IApplicationThread)。这样当一个进程无端消失(被AMS kill或者自身crash掉,再或者主动调用System.exit)后,AMS端可以探测到进程的消失。
AMS进程端调用栈
ActivityManagerService.handleAppDiedLocked
ActivityManagerService.AppDiedLocked
ActivityManagerService.AppDeathRecipient.BinderDied
AMS中针对服务管理是托管给ActiveServices来处理,以下简称为AS。
ArraySet services
进程中运行的所有服务
ArraySet connections
进程中所有的服务连接记录
ProcessRecord app
服务正在哪个进程中运行。如果为null,服务进程不存在(可能被kill)
ArrayMap bindings
服务发布过的绑定。Service.onBind根据不同Intent返回不同Binder,Intent.FilterComparison表示不同的Intent请求,IntentBindRecord表示返回的不同的Binder。
ArrayMap> connections
服务所有的绑定连接。 IBinder(IServiceConnection),IBinder可转换为IServiceConnection接口。ConnectionRecord代表一次Context.bindService。
long restartDelay
服务重启延时,这个是相对时间,这个变量控制对后续的分析很重要。
ServiceRecord service
指向服务(上层级)
Intent.FilterComparison intent
指向Intent
ArrayMap apps
所有绑定的客户端进程记录和应用绑定记录
IntentBindRecord intent
指向Intent(上层级)
ProcessRecord client
绑定的客户端进程记录
ArraySet connections
所有绑定的客户端连接记录
AppBindRecord binding
指向应用绑定记录(上层级)
IServiceConnection conn
客户端进程传递的接口。这里的conn.asBinder可以转换到Binder上,转换后的IBinder可以在ServiceRecord.connections中做为key来查找。
不管是通过Context.startService还是Context.bindService,以下的几个关键处理对服务是否启动很重要。
在ActiveServices.startServiceLocked和ActiveServices.bindServiceLocked中都需要先调用这个函数。在启动或者绑定服务的请求中,都必须先试图去掉服务的重启状态。主要代码片如下。
private final boolean unscheduleServiceRestartLocked(ServiceRecord r, int callingUid, boolean force) {
if (!force && r.restartDelay == 0) {
return false;
}
boolean removed = mRestartingServices.remove(r);
if (removed || callingUid != r.appInfo.uid) {
r.resetRestartCounter();
}
}
public void resetRestartCounter() {
restartCount = 0;
restartDelay = 0;
restartTime = 0;
}
这里force参数传递的是false,callingUid是请求应用的uid,大多数情况下,我们把服务设置为非导出,启动和绑定请求只能来自同一个包。从这里得出的结论是,如果restartDelay不是0,那么重置的条件是服务记录在在ActiveServices.mRestartingServices中存在,也就是AS把服务放入到重启状态。
在ActiveServices.startServiceInnerLocked和ActiveServices.bindServiceLocked中都需要调用这个函数。在启动或者绑定服务的请求中,经过一系列的前置检查判断后,进入到真正启动服务的入口,在bringUpServiceLocked中将找到进程调度服务或者先创建进程再调度服务。具体的启动流程这里不展开了,主要关注代码片如下。
private final String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg, boolean whileRestarting) {
if (r.app != null && r.app.thread != null) {
sendServiceArgsLocked(r, execInFg, false);
return null;
}
if (!whileRestarting && r.restartDelay > 0) {
// If waiting for a restart, then do nothing.
return null;
}
}
这里参数whileRestarting是指是否是重启服务逻辑进入的。也就是这个调用来自ActiveServices.performServiceRestartLocked。所以在启动或者绑定请求中,这个参数是false。第一段逻辑指如果服务的进程记录存在同时调度接口存在,那就调用Service.onStartCommand(这个流程不展开)。如果服务进程不存在(被kill),那么进入到第二段。如果ServiceRecord.restartDelay不为0,那么就不往下处理了。
主要代码片如下
if (r.restartDelay == 0) {
r.restartCount++;
r.restartDelay = minDuration;
} else {
if (now > (r.restartTime+resetTime)) {
r.restartCount = 1;
r.restartDelay = minDuration;
} else {
r.restartDelay *= SERVICE_RESTART_DURATION_FACTOR;
if (r.restartDelay < minDuration) {
r.restartDelay = minDuration;
}
}
}
r.nextRestartTime = now + r.restartDelay;
确定下一个重启延时,上面的minDuration会根据一些状态确定,这里不展开。从这里看出,如果第一次调度重启,或者启动过,那restartDelay为0,增加一次重启计数,同时按照最小延时时间。如果已经达到了复位的时间,那么重置重启计数,按照最小时间确定重启延时,否则,按照倍数放大重启延时。这里倍数是4倍。然后确定好重启的时间点。
if (!mRestartingServices.contains(r)) {
r.createdFromFg = false;
mRestartingServices.add(r);
}
这里把服务记录登记到重启服务里。
mAm.mHandler.removeCallbacks(r.restarter);
mAm.mHandler.postAtTime(r.restarter, r.nextRestartTime);
这里放入Handler,在r.nextRestartTime时间点将调用performServiceRestartLocked。
如果restartDelay不为0,startService或者bindService将中止。
restartDelay要重置,只有在startService或者bindService时,服务记录在ActivieServices.mRestartingServices中。
ServiceRecord.restartDelay和ActivieServices.mRestartingServices这两个关键因素可以影响服务的正常启动。
restartDelay != 0 && !mRestartingServices.contains(r) 条件成立,将导致服务启动中止。
AMS进程端调用栈
ActiveServices.KillServicesLocked
ActivityManagerService.CleanUpApplicationRecordLocked
ActivityManagerService.handleAppDiedLocked
流程分为三块,清理运行的服务,清理使用的服务,调度重启的服务
主要代码片如下
for (int i=app.services.size()-1; i>=0; i--) {
ServiceRecord sr = app.services.valueAt(i);
if (sr.app != app && sr.app != null && !sr.app.persistent) {
sr.app.services.remove(sr);
}
sr.app = null;
}
对于进程中运行的每个服务,从进程服务列表中移除,并清理服务中的进程记录。
主要代码片如下
for (int i=app.connections.size()-1; i>=0; i--) {
ConnectionRecord r = app.connections.valueAt(i);
removeConnectionLocked(r, app, null);
}
app.connections.clear();
void removeConnectionLocked(ConnectionRecord c, ProcessRecord skipApp, ActivityRecord skipAct) {
try {
bumpServiceExecutingLocked(s, false, "unbind");
s.app.thread.scheduleUnbindService(s, b.intent.intent.getIntent());
} catch (Exception e) {
serviceProcessGoneLocked(s);
}
}
}
private void serviceProcessGoneLocked(ServiceRecord r) {
serviceDoneExecutingLocked(r, true, true);
}
private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, boolean finishing) {
if (finishing) {
if (r.app != null && !r.app.persistent) {
r.app.services.remove(r);
}
r.app = null;
}
}
对于进程中使用的每个服务,移除连接记录,并清空连接记录。
移除连接记录时,调用Service.onUnbind接口。如果失败,则认为服务进程已经丢失,清除服务记录里的进程记录,移除进程记录里的服务记录。
主要代码片如下
for (int i=app.services.size()-1; i>=0; i--) {
ServiceRecord sr = app.services.valueAt(i);
boolean canceled = scheduleServiceRestartLocked(sr, true);
}
对于进程中运行的每个服务,调度重启。
假设应用程序有两个进程A和B,A调用Context.bindService绑定到服务,服务运行在B进程中。
由于某些原因(比如B进程crash,A先被回收,B接着被回收,用户通过launcher移除应用程序任务)B进程被kill。接着B被调度重启,这时B进程运行的服务的ServiceRecord.restartDelay不为0。
A进程启动,A绑定服务到B。
A,B进程同时被kill(ActivityManagerService.killPackageProcessesLocked),那么根据之前的分析,AMS会处理ActivityManagerService.handleAppDiedLocked,再处理服务ActiveServices.KillServicesLocked
如果A的死亡处理先于B,那么进入到ActiveServices.removeConnectionLocked,这时B进程已经死亡,scheduleUnbindService调用必然失败,接着进入到ActiveServices.serviceDoneExecutingLocked导致服务记录在B进程的进程记录中被移除。
当处理到B进程的死亡时,因为B进程的进程记录中已经没有服务记录了,就什么都不处理,也就是不会调度重启服务。
最后,当A进程再次启动调用Context.bindService时,服务就无法启动了。因为,ActiveServices.unscheduleServiceRestartLocked里ServiceRecord.restartDelay不为0,而服务并没有被调度重启,所以不存在ActiveServices.mRestartingServices中,导致ServiceRecord.restartDelay没有重置,之后在ActiveServices.bringUpServiceLocked中,由于ServiceRecord.restartDelay不为0,AS认为服务在等待重启,不进行后续处理。
到此,服务在AS中进入了死循环的状态。也就是,失去了调度重启的机会,却没有重置重启延时,而在启动调用中,认为在等待重启。
在调用Context.bindService时设置一个超时时间,当超时时间到达而ServiceConnection.onServiceConnected还未到达时,尝试Context.unbindService,Context.stopService,然后在Context.bindService。目的是为了清除所有的ConnectionRecord,同时清除AMS里的ServiceRecord。
从之前的分析可以看出,如果Context.bindService后,如果ConnectionRecord的存在是发生状况的导火索,那么可以让ConnectionRecord不存在,也就是在Context.bindService后ServiceConnection.onServiceConnected到达时,主动调用Context.unbindService。
但需要解决另外的两个问题。
需要自己处理Service.onBind返回的Binder的死亡监控。在ServiceConnection.onServiceConnected后,调用IBinder.linkToDeath来监控Binder死亡。这个时候,哪怕是服务状态已经停止,绑定的通道依旧是畅通的。IPC的Binder和服务之间没有必然的关系。
如果没有ConnectionRecord存在,将导致运行服务的进程容易被kill掉,那么我们可以引入另一个空的服务,来保持住连接,采用第一点中提到的避免的方式,即遇到问题再重试。
网易云免费体验馆,0成本体验20+款云产品!
更多网易研发、产品、运营经验分享请访问网易云社区。