IntentService源码解析

上一篇我们分析了Android中的线程间通信HandlerThread的原理.HandlerThread充分的利用Handler的通信机制和消息队列。本篇将分析IntentService的作用和原理。

警告:本篇的源码可能过于枯燥和乏味,中间涉及到一次采坑,错误的分析。最后纠正回来了。我觉得此处是比较有意义的。读者对着源码的同时细读本篇可能更好一点。

目录

  • IntentService简单介绍
  • 源码分析
  • 续错误纠正

IntentService

IntentService继承自Service本质上就是一个服务。但它内部拥有一个完整的HandlerThread。可以这样说IntentService=Service+HandlerThread。

我们先来说下它的常见用法。将复杂耗时操作交由IntentService来处理,你可以通过Intent的方式启动它,然后实现onHandleIntent方法,在onHandleIntent的任何操作将属于内部HandlerThread的子线程。

代码如下:

继承一个IntentService

public class CustomIntentService extends IntentService {
 public CustomIntentService() {  super("CustomIntentService"); }

    @Override
    protected void onHandleIntent(Intent intent) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String msg = intent.getStringExtra("msg");
        Log.d("IntentService", "耗时操作" + msg);
    }

    @Override
    public void onDestroy() {
        Log.d("IntentService", "onDestroy" );
        super.onDestroy();
    }
}

启动它,并附带一个消息

 Intent intent = new Intent(MainActivity.this, CustomIntentService.class);
                intent.putExtra("msg","hello");
                startService(intent);

这里我们启动了CustomIntentService并附带了一个hello过去。此时onHandleIntent方法会接受到我们这个Intent,并模拟耗时后打印日志。
注意两点

  • 1.这里的Intent是间接性传递过去的,按通常的思路这个Intent是在主线程中,但是这里并不是主线程。后面我们会从源码上来分析。
  • 2.我们知道IntentService的特性会执行完任务后自动销毁。但有一种情况,如果我们快速调用两次startService会如何?下面是快速调用两次的日志。
07-20 21:10:57.490 5895-5977IntentService: 耗时操作hello
07-20 21:11:02.480 5895-5977IntentService: 耗时操作hello
07-20 21:11:02.480 5895-5895IntentService: onDestroy

显然,并不是说每次执行都会销毁掉,当第二条消息过来的时候它并没有销毁,而是做完后才销毁。但是这是为什么呢?先卖个关子,我们带着疑问去看源码吧。

源码分析:

注:此处源码删除了一些不影响阅读的注释和方法

public abstract class IntentService extends Service {
    private volatile Looper mServiceLooper;
    private volatile ServiceHandler mServiceHandler;
    private String mName;
    private boolean mRedelivery;

    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

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

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

    @Override
    public void onDestroy() {
        mServiceLooper.quit();
    }
    protected abstract void onHandleIntent(@Nullable Intent intent);
}

  • 1.IntentService 内部总共有四个成员变量,我们只需要关注mServiceLoopermServiceHandler即可。
  • 2.首先我们看onCreate方法,它创建了一个HandlerThread并向ServiceHandler传递了一个Looper对象。
  • 3.ServiceHandlerhandleMessage内逻辑很简单,它调用了onHandleIntent这个虚拟方法(抽象方法)。并从中取出msg.obj。可以看到它就是一个Intent,接着调用了stopSelf方法携带了msg.arg1(starId),来终止服务。
  • 4.onStart方法会率先接受到我们启动服务的Intent对象,他将该对象最终使用mServiceHandler发送给HandlerThread内部的Looper,交由子线程来处理这个消息,所以我们需要重写onHandleIntent来实现自己的需求。
    HandlerThread原理可参考:HandlerThread线程间通信 源码解析

自此流程就梳理完了,现在我们回到前面提到的问题,当快速两次启动IntentService时,他发生了什么。

1.由内部的 hander接受到第一条消息,在onHandleIntent里阻塞,立刻第二条消息进入。
2.第一条消息的StopSelf方法被调用。此时第二条消息还在处理中。
3.StopSelf方法携带了startId调用了。ActivityManagerstopServiceToken来停止服务,我们接着来看一下源码。

源码路径/core/android/app/ActivityManagerNative.java

   public boolean stopServiceToken(ComponentName className, IBinder token,
            int startId) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        data.writeInterfaceToken(IActivityManager.descriptor);
        ComponentName.writeToParcel(className, data);
        data.writeStrongBinder(token);
        data.writeInt(startId);
        mRemote.transact(STOP_SERVICE_TOKEN_TRANSACTION, data, reply, 0);
        reply.readException();
        boolean res = reply.readInt() != 0;
        data.recycle();
        reply.recycle();
        return res;
    }

可以看到入参里携带了ComponentName BinderStartId,前面没有讲到,这里补充一下,StartId 是每次启动服务时都会携带过来的一个标记,它用来表示该服务在终止以前被启动了多少次。而后stopServiceToken方法将startId和和Binder一并写入了Parcel对象内。很抱歉,分析到这里翻车了,我没法再根据调试跟进去,断点下了是一把红叉。如果有大神知道这里如何动态调这块,还请告诉我一声,感激不尽。(或者我弄错了这里根本不是这样调的。)

不过这里已经大致能说明通过Binder传递了消息 mRemote.transact(STOP_SERVICE_TOKEN_TRANSACTION, data, reply, 0);来暂停服务。mRemote就是ActivityManagerNative(错误)我们再顺着思路找到了onTransact方法里的case语句

    case STOP_SERVICE_TOKEN_TRANSACTION: {
            data.enforceInterface(IActivityManager.descriptor);
            ComponentName className = ComponentName.readFromParcel(data);
            IBinder token = data.readStrongBinder();
            int startId = data.readInt();
            boolean res = stopServiceToken(className, token, startId);
            reply.writeNoException();
            reply.writeInt(res ? 1 : 0);
            return true;
        }

非常有意思的是我们可以发现stopServiceToken方法是一个递归调用。此时我更加懵了。没有找到return的点,这将会是死循环。是不是思路错误了。

但是通过上层日志看出来,当IntentService里有消息存在时它不会结束掉。一定是IntentService内部消息全部被消耗后才会结束。

今天先到这里,我们来日弄明白了底层如何实现的后再战。


续错误纠正

前面的问题我们今天找到答案了。实际上面讲到的mRemote并非ActivityManagerNative,而是代理对象。以至于我误认为他们在递归调用。这是不对的。mRemote实际是ActivityManagerProxy,他是一个本地代理对象,而经过transact调用实际运行的是远端的ActivityManagerService中,这里牵扯到了AMSActivityManager通信流程,我们后续再单独分析。先把IntentService给弄明白。
源码地址(需要翻墙):ActivityManagerService

我们发现在ActivityManagerService里调用的stopServiceToken调用了mServices.stopServiceTokenLocked方法。

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

这里的mServicesActiveServices,我们跟进去看。

 ServiceLookupResult res =retrieveServiceLocked(service, resolvedType, callingPackage,
                    callingPid, callingUid, userId, true, callerFg, false);

ServiceRecord r = res.record;

 boolean stopServiceTokenLocked(ComponentName className, IBinder token,
            int startId) {
        if (r != null) {
            if (startId >= 0) {
                ServiceRecord.StartItem si = r.findDeliveredStart(startId, false);
                if (si != null) {
                    while (r.deliveredStarts.size() > 0) {
                        ServiceRecord.StartItem cur = r.deliveredStarts.remove(0);
                        cur.removeUriPermissionsLocked();
                        if (cur == si) {
                            break;
                        }
                    }
                }

                if (r.getLastStartId() != startId) {
                    return false;
                }
                if (r.deliveredStarts.size() > 0) {
                    Slog.w(TAG, "stopServiceToken startId " + startId
                            + " is last, but have " + r.deliveredStarts.size()
                            + " remaining args");
                }
            }

            synchronized (r.stats.getBatteryStats()) {
                r.stats.stopRunningLocked();
            }
            r.startRequested = false;
            if (r.tracker != null) {
                r.tracker.setStarted(false, mAm.mProcessStats.getMemFactorLocked(),
                        SystemClock.uptimeMillis());
            }
            r.callStart = false;
            final long origId = Binder.clearCallingIdentity();
            bringDownServiceIfNeededLocked(r, false, false);
            Binder.restoreCallingIdentity(origId);
            return true;
        }
        return false;
    }

我们可以看到最关键的if (r.getLastStartId() != startId)不是最后一个startId就直接return false。否则将执行 r.stats.stopRunningLocked();来终止。自此这个问题终于真相大白了。

回过头了再理一遍。我粗略的画了一张图。顺序从上往下看。

IntentService的源码本身并不复杂,读者不要被我深究stopSelf方法牵扯到AMS里给绕晕了。AMS这块后续我会单独做文章分析,这一块特别重要。

如果本篇看得比较云雾头疼,可以先去熟悉下面两篇。
HandlerThread线程间通信 源码解析
Handler消息源码流程分析(含手写笔记)

下一篇,我们将分析ThreadPoolExecutor线程池。

本文参考:


如何下次找到我?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容