android8.0开始对于service的使用又收紧了权限,8.0之前startservice可以在后台运行很长一段时间不会被杀死,但在8.0之后如果不显式启动前台service,可能会出现启动崩溃的异常。关于8.0后的service启动兼容性处理网上应该能找到不少文章,但关于8.0service启动源码分析结合8.0之后的启动前台service似乎没找到特别完整的分析,索性根据以往对于service源码的理解结合8.0源码整理出了这篇文章,温故而知新,刷新下对于service的掌握。
service启动方式
service启动方式有两种,一种就是比较常见的startService,另一种就是bindService。startService个人理解主要是用来处理同一个app内的操作,而bindservice更加偏向于进程间通信的形式,虽然同一个app同进程中也可以使用bindservice,但是有种杀鸡用牛刀的感觉。8.0之后对于service的影响是针对startservice的启动过程,bindservice的启动没有受到影响。所以文章主要分析startservice的启动。通过一个完整的启动流程从源码的角度理解为什么8.0之后需要使用前台service。
测试demo
比较简单准备了两个apk,一个apk作为service端,另一个apk作为客户端发起service请求,区分两个apk的目的仅仅是为了让service进程和客户端进程处在不同的进程中(实际上使用一个apk,设置service的processName属性应该也可以,未测)。service声明如下
<service
android:name="mandy.com.services.MyService"
android:exported="true" />
MyService代码如下
public class MyService extends Service {
@Override
public void onCreate() {
super.onCreate();
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel mChannel;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
mChannel = new NotificationChannel("100100", "渠道名", NotificationManager.IMPORTANCE_HIGH);
if (notificationManager != null) {
notificationManager.createNotificationChannel(mChannel);
Notification notification = new Notification.Builder(getApplicationContext(), "100100").build();
startForeground(1, notification);
}
}
Log.e("mandy", "onCreate");
}
}
上述代码是针对8.0进行兼容性处理的代码,startForeground就是为了标明启动的service是一个前台service。8.0之后对于通知栏也进行了整改,增加了通知渠道,通知组的概念,所以notification的生成就是兼容8.0通知栏进行的适配,这块不太了解的可以去查看下相关文章。service.apk中的代码就这点东西,client.apk也很简单一个按钮,触发启动service,如下
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
以上代码就处理完了8.0之后的service,当然这不是文章重点,我们接下来是要从源码的角度来分析为什么需要这么处理。
startForegroundService
8.0之后通过startForegroundService来启动前台service,跟踪对比下和startservice的区别,源码在contextimpl当中
@Override
public ComponentName startService(Intent service) {
warnIfCallingFromSystemProcess();
return startServiceCommon(service, false, mUser);
}
@Override
public ComponentName startForegroundService(Intent service) {
warnIfCallingFromSystemProcess();
return startServiceCommon(service, true, mUser);
}
很明显两者的区别就在于requireForeground参数一个为false,一个为true。实际上这个参数最终会传递到ams,ams正是通过该参数确定要启动的service是前台的还是后台的。来看下startServiceCommon源码
private ComponentName startServiceCommon(Intent service, boolean requireForeground,
UserHandle user) {
try {
validateServiceIntent(service);
service.prepareToLeaveProcess(this);
ComponentName cn = ActivityManager.getService().startService(
mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
getContentResolver()), requireForeground,
getOpPackageName(), user.getIdentifier());
if (cn != null) {
if (cn.getPackageName().equals("!")) {
throw new SecurityException(
"Not allowed to start service " + service
+ " without permission " + cn.getClassName());
} else if (cn.getPackageName().equals("!!")) {
throw new SecurityException(
"Unable to start service " + service
+ ": " + cn.getClassName());
} else if (cn.getPackageName().equals("?")) {
throw new IllegalStateException(
"Not allowed to start service " + service + ": " + cn.getClassName());
}
}
return cn;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
很明显通过ActivityManager.getService().startService来调用ams当中的服务,这里能看到会把requireForeground给传递给ams。还有一个地方需要注意的就是当返回的cn!=null时的各种检查,8.0手机上如果没有进行兼容性处理的启动崩溃就是在这里发出的,举个简单的例子,当安装好我前面提到的两个apk之后,杀死service.apk所在进程的情况下去调用clientt.apk中的启动service,你就会发现程序崩溃了!,崩溃信息大致如下:
java.lang.IllegalStateException: Not allowed to start service Intent { cmp=mandy.com.uiviews/mandy.com.services.MyService }: app is in background uid null
对照下上述源码是不是找到了崩溃的出处。这里有几个问题需要明确下:
(1)按照我上述操作,不一定会在真机上出现崩溃,国内手机厂商对于framework层源码都有改动存在一定出入,实测在vivo x20手机上不会出现启动service崩溃,但是点击启动没有任何反应。
(2)必须是在service所在进程未启动的情况下才有导致上述问题,这点可以在源码中找到答案。
(3)未做兼容处理,也就是使用startService,使用startForegroundService不会出现崩溃
跟踪到ams的startservice当中来看下具体操作
synchronized(this) {
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
ComponentName res;
try {
res = mServices.startServiceLocked(caller, service,
resolvedType, callingPid, callingUid,
requireForeground, callingPackage, userId);
} finally {
Binder.restoreCallingIdentity(origId);
}
return res;
}
主要代码见上,调用mServices.startServiceLocked来进一步处理,mServices是ActiveServices实例,requireForeground就是client进程中传递过来的参数。来看下startServiceLocked中代码,代码比较多,直接删除了很多无关代码
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
throws TransactionTooLargeException {
......
ServiceLookupResult res =
retrieveServiceLocked(service, resolvedType, callingPackage,
callingPid, callingUid, userId, true, callerFg, false);
...
ServiceRecord r = res.record;
...
if (!r.startRequested && !fgRequired) {
// Before going further -- if this app is not allowed to start services in the
// background, then at this point we aren't going to let it period.
final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
r.appInfo.targetSdkVersion, callingPid, false, false);
if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
Slog.w(TAG, "Background start not allowed: service "
+ service + " to " + r.name.flattenToShortString()
+ " from pid=" + callingPid + " uid=" + callingUid
+ " pkg=" + callingPackage);
if (allowed == ActivityManager.APP_START_MODE_DELAYED) {
// In this case we are silently disabling the app, to disrupt as
// little as possible existing apps.
return null;
}
// This app knows it is in the new model where this operation is not
// allowed, so tell it what has happened.
UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
return new ComponentName("?", "app is in background uid " + uidRec);
}
}
......
r.startRequested = true;
r.delayedStop = false;
r.fgRequired = fgRequired;
r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
service, neededGrants, callingUid));
......
ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting);
return cmp;
}
从上到下进行简单分析,retrieveServiceLocked是一个查询service信息的方法,手机上各个apk中的service通过pms都可以查询到,retrieveServiceLocked内部实际上就是通过pms进行查询,得到相关service最终保存到ServiceLookupResult返回,并将得到的ServiceRecord赋值给一个r变量,ServiceRecord是一个很重要的类,里面保存了和service相关的很多重要变量,包括和该service相关的进程名,包名,启动的intent等。
接下来代码就来到了一个关键if分支,通过startRequested和fgRequired来确定是否进行到分支内部。startRequested用来确定一个service是否已经被启动,该变量在service启动的时候会置为true,在service被停止或者被杀死的时候重置为false。fgRequired即为上述文章提到client传递过来的参数。
前台service流程
先来看一下正常情况下即fgRequired为true的逻辑,不进入if分支往下走,可以看到将startRequested设置为了true,并在pendingStarts中添加了一个StartItem,可以理解为每次启动service都会添加一个StartItem,来标记此次的service,后面会用到。最终调用进入了startServiceInnerLocked。
ComponentName startServiceInnerLocked(ServiceMap smap, Intent service, ServiceRecord r,
boolean callerFg, boolean addToStarting) throws TransactionTooLargeException {
...
String error = bringUpServiceLocked(r, service.getFlags(), callerFg, false, false);
...
}
调用bringUpServiceLocked,比较重要的一个方法,同样保留关键代码
private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting, boolean permissionsReviewRequired)
throws TransactionTooLargeException {
...
if (r.app != null && r.app.thread != null) {
sendServiceArgsLocked(r, execInFg, false);
return null;
}
...
ProcessRecord app;
if (!isolated) {
app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
if (app != null && app.thread != null) {
try {
app.addPackage(r.appInfo.packageName, r.appInfo.versionCode, mAm.mProcessStats);
realStartServiceLocked(r, app, execInFg);
return null;
} catch (TransactionTooLargeException e) {
throw e;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting service " + r.shortName, e);
}
// If a dead object exception was thrown -- fall through to
// restart the application.
}
}
}
// Not running -- get it started, and enqueue this service record
// to be executed when the app comes up.
if (app == null && !permissionsReviewRequired) {
if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
hostingType, r.name, false, isolated, false)) == null) {
String msg = "Unable to launch app "
+ r.appInfo.packageName + "/"
+ r.appInfo.uid + " for service "
+ r.intent.getIntent() + ": process is bad";
Slog.w(TAG, msg);
bringDownServiceLocked(r);
return msg;
}
if (isolated) {
r.isolatedProc = app;
}
}
...
if (!mPendingServices.contains(r)) {
mPendingServices.add(r);
}
...
return null;
}
这个方法里面有几点需要注意的地方,首先会判断下r.app != null && r.app.thread != null,app是一个ProcessRecord用来保存和该service相关的进程信息,在service进程没启动或者service进程已经启动但是还没启动service的情况下app都为null,所以不会进入到if分支内部。继续往下走会调用getProcessRecordLocked获取和service进程相关ProcessRecord,同样会得到null。所以接下来要处理的就是先去启动service所在的进程,调用startProcessLocked。startProcessLocked内部就不展开了,概括起来就是会调用newProcessRecordLocked去创建一个ProcessRecord,然后又会调用另一个startProcessLocked方法去启动service所在进程。明白startProcessLocked作用后回到bringUpServiceLocked当中,由于此时service所在进程还没启动,会将serviceRecord先保存到mPendingServices。到此关于startForegroundService相关源码就分析完了。
看完会不会有点懵,service的后继操作oncreate,onStartCommand都哪里去了,实际上service的这些生命周期回调都是ams和service所在进程进行交互了,和client关系已经不大了,这里也可以解答之前自己的一个疑惑为什么在service的oncreate方法中进行耗时操作(仅测试,实际开发不推荐),client不会被阻塞在启动service处直到oncreate执行完毕。
那么service的生命周期是在哪里被调用到的,实际上是在service所在进程启动后,会主动去通知ams,ams在得知进程启动后会从mPendingServices中取出要启动的serviceRecord,然后去调用相关的service生命周期方法。可以看出除了startservice是client发起的,后继的生命周期交互工作都是ams和service进程之间进行的,client发出一个启动命令后就可以去干自己的事情了,这就是一种典型的异步调用方式。bindservice的方式和startservice有些类似,也是异步调用,只不过bindservice会将一个回调参数serviceconn传递给ams,等ams和service进程交互完毕后会把binder通过serviceconn回调给client。
上述整个流程就是调用startForegroundService的情况,8.0以下的startservice启动也大致是这个流程,也是在service进程不存在的情况下先拉起,如果进程已经存在但还没启动service则调用realStartServiceLocked。
fgRequired为false
以上分析的都是在fgRequired为true的情况下,现在分析下在fgRequire为false,也就是8.0之后直接调用startservice会发生什么,把之前的代码再添一下
if (!r.startRequested && !fgRequired) {
// Before going further -- if this app is not allowed to start services in the
// background, then at this point we aren't going to let it period.
final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
r.appInfo.targetSdkVersion, callingPid, false, false);
if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
Slog.w(TAG, "Background start not allowed: service "
+ service + " to " + r.name.flattenToShortString()
+ " from pid=" + callingPid + " uid=" + callingUid
+ " pkg=" + callingPackage);
if (allowed == ActivityManager.APP_START_MODE_DELAYED) {
// In this case we are silently disabling the app, to disrupt as
// little as possible existing apps.
return null;
}
// This app knows it is in the new model where this operation is not
// allowed, so tell it what has happened.
UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
return new ComponentName("?", "app is in background uid " + uidRec);
}
}
进入if分支内部调用getAppStartModeLocked
int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
UidRecord uidRec = mActiveUids.get(uid);
if (uidRec == null || alwaysRestrict || uidRec.idle) {
boolean ephemeral;
...
if (ephemeral) {
// We are hard-core about ephemeral apps not running in the background.
return ActivityManager.APP_START_MODE_DISABLED;
} else {
if (disabledOnly) {
// The caller is only interested in whether app starts are completely
// disabled for the given package (that is, it is an instant app). So
// we don't need to go further, which is all just seeing if we should
// apply a "delayed" mode for a regular app.
return ActivityManager.APP_START_MODE_NORMAL;
}
final int startMode = (alwaysRestrict)
? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
: appServicesRestrictedInBackgroundLocked(uid, packageName,
packageTargetSdk);
}
...
return startMode;
}
}
return ActivityManager.APP_START_MODE_NORMAL;
}
UidRecord uidRec = mActiveUids.get(uid);见名知意通过service所在的uid来查询UidRecord,在service所在进程没启动的情况下返回null,否则返回具体值。返回具体值的情况下会直接跳过if分支然后返回ActivityManager.APP_START_MODE_NORMAL,接下来的流程就和startForegroundService保持一致了。重点看下返回null的情况,ephemeral最终值为false,会进入到else分支内部。
disabledOnly和alwaysRestrict的值均为false,这两值是直接从参数传递进来的,所以startMode的值最终由appServicesRestrictedInBackgroundLocked来决定。
int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
// Persistent app?
if (mPackageManagerInt.isPackagePersistent(packageName)) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "App " + uid + "/" + packageName
+ " is persistent; not restricted in background");
}
return ActivityManager.APP_START_MODE_NORMAL;
}
// Non-persistent but background whitelisted?
if (uidOnBackgroundWhitelist(uid)) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "App " + uid + "/" + packageName
+ " on background whitelist; not restricted in background");
}
return ActivityManager.APP_START_MODE_NORMAL;
}
// Is this app on the battery whitelist?
if (isOnDeviceIdleWhitelistLocked(uid)) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "App " + uid + "/" + packageName
+ " on idle whitelist; not restricted in background");
}
return ActivityManager.APP_START_MODE_NORMAL;
}
// None of the service-policy criteria apply, so we apply the common criteria
return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
}
源码内部有3个if判断,只要满足其中一个就是返回APP_START_MODE_NORMAL,否则会进入到appRestrictedInBackgroundLocked内部。这3个if判断源码已经给出了解释,正常情况下app是不会进入到if内部,除非你的app是一个特殊app,关于省电白名单我特地在vivo真机的设置中查找了下,并未找到相关可以将app添加到省电白名单中的操作,所以最终会调用到appRestrictedInBackgroundLocked。
int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
// Apps that target O+ are always subject to background check
if (packageTargetSdk >= Build.VERSION_CODES.O) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
}
return ActivityManager.APP_START_MODE_DELAYED_RIGID;
}
...
}
}
答案已经很明显了,在8.0及以上直接就返回APP_START_MODE_DELAYED_RIGID,最终又回到方法中
if (!r.startRequested && !fgRequired) {
// Before going further -- if this app is not allowed to start services in the
// background, then at this point we aren't going to let it period.
final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
r.appInfo.targetSdkVersion, callingPid, false, false);
if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
Slog.w(TAG, "Background start not allowed: service "
+ service + " to " + r.name.flattenToShortString()
+ " from pid=" + callingPid + " uid=" + callingUid
+ " pkg=" + callingPackage);
if (allowed == ActivityManager.APP_START_MODE_DELAYED) {
// In this case we are silently disabling the app, to disrupt as
// little as possible existing apps.
return null;
}
// This app knows it is in the new model where this operation is not
// allowed, so tell it what has happened.
UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
return new ComponentName("?", "app is in background uid " + uidRec);
}
}
返回一个ComponentName("?", "app is in background uid " + uidRec)给client,回过头再去看contextimpl中的这段判断逻辑就比较清楚了
if (cn != null) {
if (cn.getPackageName().equals("!")) {
throw new SecurityException(
"Not allowed to start service " + service
+ " without permission " + cn.getClassName());
} else if (cn.getPackageName().equals("!!")) {
throw new SecurityException(
"Unable to start service " + service
+ ": " + cn.getClassName());
} else if (cn.getPackageName().equals("?")) {
throw new IllegalStateException(
"Not allowed to start service " + service + ": " + cn.getClassName());
}
}
cn就是ams返回的ComponentName,对照着看下就能知道会抛出一个IllegalStateException异常,到此应该就能明白为什么8.0直接使用startservice去启动一个不存在进程的service时为什么会抛出这个异常了。
service ANR问题分析
回头看下测试demo中的MyService就能知道,启动的Service必须要在5秒内调用startForeground,否则会抛出一个anr的异常,又或者当在service的oncreate方法中去处理耗时操作,如果不能在一定时间内完成也会抛出anr异常,抛异常的原理比较简单就是通过handler发送一个延迟处理的message,如果能在规定时间内执行完成就remove掉该message,否则就执行该message抛出一个异常。知道原理后剩下的就是在源码中找到何时发起这个message,又是何时去remove掉该message。关键代码在bumpServiceExecutingLocked这个方法内
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, ">>> EXECUTING "
+ why + " of " + r + " in app " + r.app);
else if (DEBUG_SERVICE_EXECUTING) Slog.v(TAG_SERVICE_EXECUTING, ">>> EXECUTING "
+ why + " of " + r.shortName);
long now = SystemClock.uptimeMillis();
if (r.executeNesting == 0) {
...
if (r.app != null) {
r.app.executingServices.add(r);
r.app.execServicesFg |= fg;
if (r.app.executingServices.size() == 1) {
scheduleServiceTimeoutLocked(r.app);
}
}
} else if (r.app != null && fg && !r.app.execServicesFg) {
r.app.execServicesFg = true;
scheduleServiceTimeoutLocked(r.app);
}
...
}
调用scheduleServiceTimeoutLocked发起一个延迟message,内部代码很容易理解
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
if (proc.executingServices.size() == 0 || proc.thread == null) {
return;
}
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
mAm.mHandler.sendMessageDelayed(msg,
proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}
可以看出会通过service是前台还是后台来决定延迟执行的时间,如果在规定时间内service还没处理完毕则最终会执行到
if (anrMessage != null) {
mAm.mAppErrors.appNotResponding(proc, null, null, false, anrMessage);
}
即抛出一个anr,那么剩下的问题还有两个(1)bumpServiceExecutingLocked在哪里被调用(2)又是在哪里remove掉message。
第一个问题可以直接在源码中就能搜到调用处,比较常见的调用方法是realStartServiceLocked和sendServiceArgsLocked
如果对于service启动流程比较熟悉应该能明白这两个方法的调用。realStartServiceLocked最终会触发client中的oncreate,而
sendServiceArgsLocked会最终触发client的onStartCommand被调用。
第一个问题搞明白后剩下的就是在哪里remove掉message,实际上removemessage的发起时机在client端,当执行完毕oncreate或者onStartCommand后都会调用ams中的serviceDoneExecuting方法,该方法最终就会调用到removemesage方法
移除掉抛anr异常的message。
8.0中的serviceANR
上述分析的anr都是早已存在的service中的anr,8.0之后启动service后必须在5秒内调用startForeground,是在原有代码的基础上额外添加的anr逻辑,原理其实都是类似的,在sendServiceArgsLocked中可以找到对应的代码块
if (r.fgRequired && !r.fgWaiting) {
if (!r.isForeground) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "Launched service must call startForeground() within timeout: " + r);
}
scheduleServiceForegroundTransitionTimeoutLocked(r);
} else {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "Service already foreground; no new timeout: " + r);
}
r.fgRequired = false;
}
}
可以看出如果是一个前台service最终会调用到一个巨长名字的方法,内部很简单就是发起一个延迟message
void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) {
if (r.app.executingServices.size() == 0 || r.app.thread == null) {
return;
}
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG);
msg.obj = r;
r.fgWaiting = true;
mAm.mHandler.sendMessageDelayed(msg, SERVICE_START_FOREGROUND_TIMEOUT);
}
而SERVICE_START_FOREGROUND_TIMEOUT的值正是5秒,正也就是为什么必须要在5秒内调用startForeground的原因了,startForeground内部做的事相信都能猜到就是remove掉这个message。
总结
到此关于8.0service启动前台service的相关代码就分析完毕了,对于整个service启动流程梳理了一遍,对于service anr的原理也进行了说明。当然上述的所有理解都是基于自己对于service的理解,所以文中难免有些理解不到位的地方,如果有什么误导的地方还需多担待。分析过程中源码中还有很多的细节问题在和分析原理没有太大联系的基础上都一一忽略了,毕竟太细节的东西容易遗忘,除非你是一个framework层开发,需要对源码就是二次开发,作为一个应用层开发阅读framework层源码点到为止,明白整体原理即可。