2019独角兽企业重金招聘Python工程师标准>>>

剖析IntentService的运作机理

(本文以Android 5.1为准)

侯 亮

1 概述

在讲述Service机制的文章里,我们曾经稍微提起过IntentService,今天再来详细剖析一下它。说起来,IntentService只是一个处理异步请求的服务基类而已。当人们通过调用startService()启动IntentService时,实质上是向其发送了一个请求。而如果有多个地方同时向同一个IntentService发送请求的话,那么这些请求会被串行化处理。所以,IntentService常常用于执行那种“一次性处理”的工作。

IntentService的运作机理相对比较简单,而且从Android2.3(我手头最早的版本)到Android5.1,IntentService的实现代码一直就没怎么变动过,可见其稳定程度。

当然,我们既然说要详细剖析IntentService,就不可能仅仅讲这么点儿东西。我们可以挖得再深一点儿,看看IntentService到底是如何实现的,又是如何保证串行处理工作的。现在我们就开始吧。

2 先简单说说IntentService的运作

2.1 onCreate()中启动一个消息泵线程

我们先看一下IntentService的onCreate()函数:
【frameworks/base/core/java/android/app/IntentService.java】

@Override
public void onCreate() {super.onCreate();HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");thread.start();mServiceLooper = thread.getLooper();mServiceHandler = new ServiceHandler(mServiceLooper);
}

可以看到,IntentService创建伊始就会启动一个消息泵线程HandlerThread,然后又创建了一个可以向这个消息泵线程传递消息的Handler(即ServiceHandler)。

ServiceHandler是IntentService的一个内嵌类,其定义如下:

private final class ServiceHandler extends Handler {public ServiceHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {onHandleIntent((Intent)msg.obj);stopSelf(msg.arg1);  // 注意,不是stopService()!}
}

所以,上面的代码的逻辑意义就很明确了,那就是构造一个消息泵线程以及一个可以向这个泵传递消息的Handler。而后,当这个消息泵线程最终处理这个消息时,其实就是在回调IntentService的onHandleIntent()函数而已,并且在该回调函数返回后,handleMessage()会进一步调用stopSelf()结束service。注意,这里说“结束service”只是一种简略说法,实际的情况会稍微复杂一点儿,这个我们后文再细说。

2.2 onStartCommand()利用ServiceHandler向消息泵打入特殊消息

在紧随onCreate()函数之后,系统会调用到onStartCommand()函数,这个大家都很熟悉了。对于startService()动作而言,系统只会在service尚不存在的情况下,创建相应的service,并回调其onCreate()函数。以后,只要这个service没有被销毁,就不会重复再调用onCreate()了。不过,每次调用startService()都会导致走到onStartCommand()函数。IntentService的onStartCommand()函数的代码如下:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {onStart(intent, startId);return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}
@Override
public void onStart(Intent intent, int startId) {Message msg = mServiceHandler.obtainMessage();msg.arg1 = startId;msg.obj = intent;mServiceHandler.sendMessage(msg);
}

看起来,每当用户调用startService()启动IntentService,就会自动向其内部消息泵线程打入一条消息,该消息的.obj域记录着用户传来的intent,这个intent在handleMessage()里会进一步传递给onHandleIntent()。另外,请大家注意那个startId参数,这里我们先打个伏笔,后文还会细说。

2.3 实现你的onHandleIntent()函数

在IntentService.java文件里,onHandleIntent()只是做了简单的声明:

protected abstract void onHandleIntent(Intent intent);

其具体行为要看我们写的IntentService派生类怎么定义这个函数啦。

消息泵里的消息队列在先天上就维护了一条串行化的执行链。消息泵线程逐个摘取消息节点,回调每个节点的onHandleIntent()函数,一切都显得那么自然。现在我们可以画出如下示意图:

3 再深挖一下

有了以上这些基础知识,现在我们要再深挖一下了。

在消息队列中,消息的确被顺序排列了,可是在处理前一个消息时,调用的stopSelf()不是把service结束了吗?那它还怎么保证继续处理后续消息呢?可见,前文我们说的“IntentService还会自动执行stopSelf()关闭自己”的说法并不准确。问题的关键在于,ServiceHandler的handleMessage()里调用的是stopSelf(),而不是stopService()!它们是不一样的。

3.1 stopSelf()和stopService()是不一样的!

stopSelft()的函数定义如下:
【frameworks/base/core/java/android/app/Service.java】

public final void stopSelf(int startId) {if (mActivityManager == null) {return;}try {mActivityManager.stopServiceToken(new ComponentName(this, mClassName), mToken, startId);} catch (RemoteException ex) {}
}

请大家注意那个startId参数,这个参数是系统通过onStartCommand()传递给service的,也就是前文我们打伏笔的地方。要了解这个startId参数,我们得看一下AMS里的相关代码。

AMS中启动service的函数是startService,其代码截选如下:
【frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java】

public ComponentName startService(IApplicationThread caller, Intent service,String resolvedType, int userId) {. . . . . .. . . . . .ComponentName res = mServices.startServiceLocked(caller, service,resolvedType, callingPid, callingUid, userId);. . . . . .. . . . . .
}

【frameworks/base/services/core/java/com/android/server/am/ActiveServices.java】

ComponentName startServiceLocked(IApplicationThread caller,Intent service, String resolvedType,int callingPid, int callingUid, int userId) {. . . . . .ServiceRecord r = res.record;. . . . . .r.lastActivity = SystemClock.uptimeMillis();r.startRequested = true;r.delayedStop = false;r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),service, neededGrants));. . . . . .. . . . . .return startServiceInnerLocked(smap, service, r, callerFg, addToStarting);
}

也就是说,每当我们调用startService()启动一个服务时,不但会在其对应的ServiceRecord节点的pendingStarts里插入一个新的StartItem节点,而且会为这个StartItem节点生成一个新的id号,这个id号就是日后的startId啦。

生成id号时,采用的办法是最简单的加1操作,代码如下:
【frameworks/base/services/core/java/com/android/server/am/ServiceRecord.java】

public int makeNextStartId() {lastStartId++;if (lastStartId < 1) {lastStartId = 1;}return lastStartId;
}

接下来,我们绘制一张调用关系图:

图中的sendServiceArgsLocked()会将r.pendingStarts列表中的StartItem节点转移到r.deliveredStarts列表中。这主要是因为启动service的动作本身是比较复杂的,有时候甚至会遇到目标service寄身的进程尚未启动的情况,此时就得先启动一个用户进程,而后再进一步启动service。要规划好这些异步的动作,我们常常需要把信息先记录进一个pending列表里,而后再在合适的时机将信息从pending列表里取出。

上图中最后调用的r.app.thread.scheduleServiceArgs()其实是向service所在的进程发出SERVICE_ARGS语义,语义中携带着刚取出的StartItem节点的id和intent信息。该语义最终导致执行到目标service的onStartCommand()。这个就和前文介绍onStartCommand()的地方契合起来了。

当onStartCommand()向消息泵线程打入消息时,startId就被记录进message里了。我们现在举个例子,假设几乎在同时有两个地方都调用startService()来启动同一个IntentService,此时很可能会形成两个StartItem和两个IntentService消息,画出示意图如下:

既然消息队列里的消息已经和AMS里的StartItem对应上了,那么在处理完消息后,调用stopSelf()时,应该也必须考虑到这种匹配关系。所以stopSelf()的参数就是startId。前文我们已经看到,stopSelf()内部是在调用:

        mActivityManager.stopServiceToken(new ComponentName(this, mClassName), mToken, startId);

stopServiceToken()的代码如下:
【frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java】

@Override
public boolean stopServiceToken(ComponentName className, IBinder token,int startId) {synchronized(this) {return mServices.stopServiceTokenLocked(className, token, startId);}
}

【frameworks/base/services/core/java/com/android/server/am/ActiveServices.java】

boolean stopServiceTokenLocked(ComponentName className, IBinder token, int startId) {. . . . . .ServiceRecord r = findServiceLocked(className, token, UserHandle.getCallingUserId());if (r != null) {if (startId >= 0) {ServiceRecord.StartItem si = r.findDeliveredStart(startId, false);if (si != null) {// 用while循环删除位于startId对应的StartItem节点以及其之前的所有节点while (r.deliveredStarts.size() > 0) {ServiceRecord.StartItem cur = r.deliveredStarts.remove(0);cur.removeUriPermissionsLocked();if (cur == si) {break;}}}if (r.getLastStartId() != startId) {return false;    // 如果不是最后一个startItem节点,则直接return false了}if (r.deliveredStarts.size() > 0) {Slog.w(TAG, "stopServiceToken startId " + startId+ " is last, but have " + r.deliveredStarts.size()+ " remaining args");}}. . . . . .// 当最后一个startItem摘掉后,才真正结束servicebringDownServiceIfNeededLocked(r, false, false);  . . . . . .return true;}return false;
}

这段代码的主要意思还是比较明确的,那就是按序从ServiceRecord的deliveredStarts列表中删除StartItem节点,直到所删除的是startId参数对应的StartItem节点,如果此时尚未抵达ServiceRecord内部记录的最后一个start Id号,则说明此次stopSelf()操作没必要进一步结束service,那么直接return false就可以了。只有在所删除的startItem节点的确是最后一个startItem节点时,才会调用bringDownServiceIfNeededLocked()去结束service。这就是为什么IntentService的ServiceHandler在处理完消息后,可以放心调用stopSelf()的原因。

那么,为什么要用一个while循环来删除位于startId对应的StartItem节点之前的所有节点呢?大家可以设想一下,如果service进程被异常kill了,那么它里面的消息队列肯定也就销毁了。可是在AMS一侧的ServiceRecord里,那些对应的StartItem节点还是存在的,就好像一个个孤儿一样。此时,如果用户再一次调用startService()启动了这个IntentService,那么系统最好能在处理完此次message后,一并将那些孤儿StartItem销毁。不过这只是我目前的一点儿猜想,实际上可能不大容易遇到这种情况。

3.2 说说setIntentRedelivery()

现在我们再来看看IntentService里其他一些细节。比如setIntentRedelivery()函数:
【frameworks/base/core/java/android/app/IntentService.java】

public void setIntentRedelivery(boolean enabled) {mRedelivery = enabled;
}

一般,我们可以在实现IntentService派生类时,在构造函数里调用这个函数。这里设置的mRedelivery成员会在onStartCommand()函数里影响最终的返回值,从而影响service的“重递送intent”的行为。

我们再列一次onStartCommand()的代码:

public int onStartCommand(Intent intent, int flags, int startId) {onStart(intent, startId);return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

当mRedelivery为true,会返回START_REDELIVER_INTENT,这个值的意思是说,如果在onStartCommand()之后的某个时刻,该service对应的进程被kill了,那么系统会自动重启该service,并向onStartCommand()重新传入当初startService()时指定的intent。另外,在极端情况下,可能已经有多个地方startService()了,那么系统在重启service之后,应该会将自己记录的多个intent逐条传递回service,也就是说,有可能会执行多次onStartCommand()了。而当mRedelivery为false时,会返回START_NOT_STICKY,它表示如果在后续某个时刻,该service对应的进程被kill了,系统是不会自动重启该service的。

4 小结

有关IntentService的知识,我们就先说这么多,有兴趣的同学不妨对比代码看看。

转载于:https://my.oschina.net/youranhongcha/blog/785387

剖析IntentService的运作机理相关推荐

  1. 了解Android 4.1,之三:黄油项目 —— 运作机理及新鲜玩意

    了解Android 4.1,之三:黄油项目 -- 运作机理及新鲜玩意 译者:biAji 原文作者: Ron Amadeo  发布:2012-07-27 13:55:39  挑错 How They Di ...

  2. (转载)了解Android 4.1,之三:黄油项目 —— 运作机理及新鲜玩意

    Welcome back to GTKA, everyone's favorite investigative series where we learn all about the newest v ...

  3. 深入剖析Win32可移植可执行文件格式

    深入剖析Win32可移植可执行文件格式 原文一:http://msdn.microsoft.com/zh-cn/magazine/bb985992(en-us).aspx 原文二http://msdn ...

  4. 【Flink】Metrics运作机制

    1.概述 转载并且补充:Flink源码剖析:Metrics运作机制 1. Metrics简介 1.1 什么是 Metrics? Flink 提供的 Metrics 可以在 Flink 内部收集一些指标 ...

  5. VB.NET开发人员必备参考10本书目

    一.程序设计 1.<<Programming Microsoft Visual Basic .NET(Core Reference)>>(Visual Basic NET技术内 ...

  6. 系统辨识理论及应用_企业战略分析的理论工具

    按照企业战略规划的3+1模型,企业战略规划分为四个步骤:战略分析.目标选择.路径选择.战略实施.今天学习第一部分:战略分析.战略分析分为两个部分:外部分析和内部分析. 图片来源于网络 一.外部分析 外 ...

  7. 时间同步——TSN(Time Sensitive Network-时间敏感网络)协议802.1AS介绍

    前言 之前的主题TSN的发展历史和协议族现状介绍了TSN技术的缘起,最近一期的主题TSN协议导读从定时与同步.延时.可靠性.资源管理四个方面,帮助大家了解TSN协议族包含哪些子协议,以及这些子协议的作 ...

  8. python3.7-初学篇-21

    Python源码剖析 <Python源码剖析>是2008年06月电子工业出版社出版的图书,作者是陈儒.   书    名 Python源码剖析--深度探索动态语言核心技术 作    者 陈 ...

  9. 《.NET 4.0面向对象编程漫谈》前言及配套资源包发布

    <.NET 4.0面向对象编程漫谈>前言及配套资源包发布 注: 今天拿到了出版社快递送来的<.NET 4.0面向对象编程漫谈>(基础篇和应用篇)的24小时样书,相信读者很快就能 ...

最新文章

  1. 新冠疫情下的AI顶会,ACL、ICLR、ICML等均改为线上会议!
  2. 什么是COM[网摘]
  3. 学习Struts--Chap02:Struts2框架各个功能模块和程序执行流程的介绍
  4. hdu 3033(分组背包)
  5. HarmonyOS之常用组件RoundProgressBar的功能和使用
  6. Github怎么删除之前的项目
  7. libgdx和Kotlin –类[2D平台原型]
  8. 前端学习(2529):搭建中央管理
  9. [机器学习笔记]Note13--异常检测
  10. 四种策略确保 RabbitMQ 消息发送可靠性!你用哪种?
  11. 小马虎想用计算机计算396乘19,2020版苏教版数学四年级下册第四单元《用计算器计算》单元测试卷C卷...
  12. linux内存管理(六)-伙伴分配器
  13. c语言的编译器还真是不好理解...
  14. ts文件合并为MP4的方法
  15. 源恒房地产税务软件土地增值税清算系统
  16. 计算机日期函数公式大全,Excel技巧: 根据日期汇总月份的计算公式
  17. ECSHOP模板堂商品最小起订量插件
  18. Lighthouse performance scoring
  19. 微博指定日期舆情数据爬虫获取—基于中文金融词典(python)
  20. 高效进行接口测试,简单易懂

热门文章

  1. 图解未来50年前沿科技趋势与22个预测
  2. 高精度定位!“天地一体”基础设施助中国北斗在商用领域“弯道超车”
  3. 你拖后腿了吗?工信部发布前 11 月软件行业经济报告
  4. 想进大厂?Dubbo 普普通通 9 问你知道吗
  5. [Leetcode] Max Area of Island 最大岛屿面积
  6. 手机密钥连接linux主机
  7. jQuery通过ajax方法获取json数据不执行success的原因及解决方法
  8. 利用 Pandas 将数据集中的某列文本拆分为多行
  9. java中String new和直接赋值的区别
  10. Linux下使用rsync最快速删除海量文件的方法