Android服务绑定失败的分析与对策

叁叁肆2018-09-19 14:16

本文来自网易云社区

作者:苏甦


对于使用多进程方式的Android应用程序,需要通过绑定服务获得绑定对象,进行进程间通信。在某些特殊情况下,会遇到绑定对象无法返回的问题。文中分析了问题发生的原因,给出了解决的思路。同时,也可以通过阅读本文,来加深对Android服务管理的认识。


一些缩写

  • AMS Activity Manager Service
  • AS Active Services AMS服务管理
  • Zygote 孵化进程


基础知识

  • Binder Android IPC基础,JAVA层面上包括:IBinder,Binder,Binder.BinderProxy
  • IInterface 所有IPC调用接口都必须继承它,提供了从接口向IBinder转换的方法
  • ADIL 编译AIDL文件,生成接口继承IInterface,并提供Proxy和Stub类。
  • Death Recipient 通过IBinder.linkToDeath注册死亡监控。
  • Zygote 系统上最早的Android进程,主要职责是监听孵化请求,通过fork产生新的android进程。


AMS监控进程

IApplicationThread

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


ActiveServices

AMS中针对服务管理是托管给ActiveServices来处理,以下简称为AS。


服务相关数据

ProcessRecord

  • ArraySet services
    进程中运行的所有服务

  • ArraySet connections
    进程中所有的服务连接记录


ServiceRecord

  • 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
    服务重启延时,这个是相对时间,这个变量控制对后续的分析很重要。


IntentBindRecord

  • ServiceRecord service
    指向服务(上层级)

  • Intent.FilterComparison intent
    指向Intent

  • ArrayMap apps
    所有绑定的客户端进程记录和应用绑定记录


AppBindRecord

  • IntentBindRecord intent
    指向Intent(上层级)

  • ProcessRecord client
    绑定的客户端进程记录

  • ArraySet connections
    所有绑定的客户端连接记录


ConnectionRecord

  • AppBindRecord binding
    指向应用绑定记录(上层级)

  • IServiceConnection conn
    客户端进程传递的接口。这里的conn.asBinder可以转换到Binder上,转换后的IBinder可以在ServiceRecord.connections中做为key来查找。


影响服务启动的几个关键

不管是通过Context.startService还是Context.bindService,以下的几个关键处理对服务是否启动很重要。


unscheduleServiceRestartLocked

在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把服务放入到重启状态。


bringUpServiceLocked

在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,那么就不往下处理了。


scheduleServiceRestartLocked

主要代码片如下

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中。

  • 如果是由调度重启而启动服务的,restartDelay不重置(留作下次调度延时的计算依据)。 

ServiceRecord.restartDelayActivieServices.mRestartingServices这两个关键因素可以影响服务的正常启动。

restartDelay != 0 && !mRestartingServices.contains(r) 条件成立,将导致服务启动中止。


AS处理进程死亡

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+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区