Android面试一天一题(1 Day):service

面试题:知道Service吗,它有几种启动方式?

Service是一个专门在后台处理长时间任务的Android组件,它没有UI。它有两种启动方式,startService和bindService。

你猜得没错,Bely紧接着问我:这两种启动方式的区别。

startService只是启动Service,启动它的组件(如Activity)和Service并没有关联,只有当Service调用stopSelf或者其他组件调用stopService服务才会终止。

bindService方法启动Service,其他组件可以通过回调获取Service的代理对象和Service交互,而这两方也进行了绑定,当启动方销毁时,Service也会自动进行unBind操作,当发现所有绑定都进行了unBind时才会销毁Service。

这应该是比较关键的区别了,
当然还有其他的区别,如两种调用对Service生命周期函数影响,面试官也可以就这个问题展开一下。


当我遇到面试者知道怎么使用Service,也如多年前的我可以自如的答出startService和bindService的区别时,我一般会多问一句:
Service的onCreate回调函数可以做耗时的操作吗?

很多人都会说:可以。
原形毕露,他前面的回答只是在面试前预习了一下面试题而已。如果知道Service的onCreate是在主线程(ActivityThread)中调用的,耗时操作会阻塞UI,我一般再接着问:
如果需要做耗时的操作,你会怎么做?

问题便这样展开了,一个人是否真正懂得原理会灵活运用,一下子便能看出来。 当面试者回答到线程和Handler方式时,我会再问一下对方:
是否知道IntentService,在什么场景下使用IntentService?

这也是面试官要看的点,真正的项目需要一个开发人员对某个问题有一定的深度,也需要对整个Android的知识点有一定的广度。深度代表这个人对问题认真对待有钻研的精神,广度代表这个人在面对同一个问题时,会更容易从多种可行的方案中选出最合适的一种。
Service的实际项目中一直被很多人忽略,为什么我一再强调Service很重要,我们来看看,如果对Service完全无知会在工作中遇到什么问题。
场景:如果一个应用要从网络上下载MP3文件,并在Activity上展示进度条,这个Activity要求是可以转屏的。那么在转屏时Actvitiy会重启,如何保证下载的进度条能正确展示进度呢?

没有Service概念的人,一般想出来的方案如下:
在转屏前将进度缓存,转屏后再读出来。
使用android:configChanges设置,让转屏时Activity不销毁和重建。

针对第1个方案,我会继续问他将进度值存在哪里? 转屏的过程中,我们知道Activity的重建算是比较耗时的,会可能会有几百毫秒以上,那么这时候下载线程仍然在工作,进度肯定和保存时的进度不一致了,如何处理这个问题呢?
第2个方案,大家可以自己展开思考,实际的项目中可能会需要额外做一些事情来处理ContentView的横竖布局的问题。
如果使用Service来解决这个问题,看似是比较完美的,不过就会涉及Activity(UI)和Service的交互问题,这个我们以后再讨论。
小结
当我们知道了Service的用途,心中有一个Service相关的概念时,针对实际的场景还是要做具体的分析再决定是否使用Service。因为Service仍然是在主线程中调用,还是要开线程才能处理长时间的工作,Service和UI的交互也让这个方式变得不那么简便。如果你只需要在当前界面去做一些耗时操作,界面退出或改变时,工作也要停止,那么这时直接使用Thread(或者AsyncTask, ThreadHandler)会更合适你。

IntentService 示例与详解

IntentService 是比较少使用的,如果你没听过也不意外,就像 HandlerThread 很多开发者没用过或没听过,不过我也仅仅是在demo中使用。Google 为方便开发者使用,提高开发效率,封装了 IntentService 和 HandlerThread。HandlerThread 继承自 Thread,内部封装了 Looper。那 IntentService 呢?
IntentService 是继承自 Service 并处理异步请求的一个类,在 IntentService 内有一个工作线程来处理耗时操作,当任务执行完后,IntentService 会自动停止,不需要我们去手动结束。如果启动 IntentService 多次,那么每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中执行,依次去执行,执行完自动结束。
示例代码
实现的功能
示例代码的功能是模仿下载在服务线程中每隔50ms计数一次直到计数为100停止,发送服务和线程运行状态的广播,Fragment中接收广播进行UI更新,如此。
MyIntentService 全部代码

public class MyIntentService extends IntentService {

    private boolean isRunning = true;
    private int count = 0;
    private LocalBroadcastManager mLocalBroadcastManager;

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
        sendServiceStatus("服务启动");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        try {
            sendThreadStatus("线程启动", count);
            Thread.sleep(1_000);
            sendServiceStatus("服务运行中...");

            isRunning = true;
            count = 0;
            while (isRunning) {
                count++;
                if (count >= 100) {
                    isRunning = false;
                }
                Thread.sleep(50);
                sendThreadStatus("线程运行中...", count);
            }
            sendThreadStatus("线程结束", count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        sendServiceStatus("服务结束");
    }

    // 发送服务状态信息
    private void sendServiceStatus(String status) {
        Intent intent = new Intent(IntentServiceFragment.ACTION_TYPE_SERVICE);
        intent.putExtra("status", status);
        mLocalBroadcastManager.sendBroadcast(intent);
    }

    // 发送线程状态信息
    private void sendThreadStatus(String status, int progress) {
        Intent intent = new Intent(IntentServiceFragment.ACTION_TYPE_THREAD);
        intent.putExtra("status", status);
        intent.putExtra("progress", progress);
        mLocalBroadcastManager.sendBroadcast(intent);
    }
}

Fragment 中的广播相关代码

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        switch (intent.getAction()) {
            case ACTION_TYPE_SERVICE:
                tvServiceStatus.setText("服务状态:" + intent.getStringExtra("status"));
                break;
            case ACTION_TYPE_THREAD:
                int progress = intent.getIntExtra("progress", 0);
                tvThreadStatus.setText("线程状态:" + intent.getStringExtra("status"));
                progressBar.setProgress(progress);
                tvProgress.setText(progress + "%");
                break;
        }
    }
}

注册服务启动服务

// 在 Manifest 中注册服务
<service android:name=".service.MyIntentService"/>

// 像启动 Service 那样启动 IntentService
Intent startIntent = new Intent(getActivity(), MyIntentService.class);
getActivity().startService(startIntent);

具体参考GitHub上项目
Gif图演示
1、点击[启动 IntentService],服务运行,线程计数完成后,服务和线程都结束。

图一

2、点击[启动 IntentService],服务运行,线程计数完成前,点击[停止 IntentService],服务结束,线程计数完成后线程结束。


图二

3、点击两次[启动 IntentService],服务运行,第一次线程计数完成后,进行第二次线程计数,两次完成后,服务和线程都结束。


图三

4、点击两次[启动 IntentService],服务运行,在第一次线程计数完成前,点击[停止 IntentService],服务结束,第一次线程计数结束后不进行第二次计数。


图四

查看源码
下面,从这几个功能点查看下源码:
1、启动 IntentService 为什么不需要新建线程?

// IntentService 源码中的 onCreate() 方法
@Override
public void onCreate() {
    super.onCreate();
    // HandlerThread 继承自 Thread,内部封装了 Looper,在这里新建线程并启动,所以启动 IntentService 不需要新建线程。
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();

    // 获得工作线程的 Looper,并维护自己的工作队列。
    mServiceLooper = thread.getLooper();
    // mServiceHandler 是属于工作线程的。
    mServiceHandler = new ServiceHandler(mServiceLooper);
}

private volatile ServiceHandler mServiceHandler;

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        // onHandleIntent 方法在工作线程中执行,执行完调用 stopSelf() 结束服务。
        onHandleIntent((Intent)msg.obj);
        stopSelf(msg.arg1);
    }
}

@WorkerThread
protected abstract void onHandleIntent(Intent intent);

2、为什么不建议通过 bindService() 启动 IntentService?

@Override
public IBinder onBind(Intent intent) {
    return null;
}

IntentService 源码中的 onBind() 默认返回 null;不适合 bindService() 启动服务,如果你执意要 bindService() 来启动 IntentService,可能因为你想通过 Binder 或 Messenger 使得 IntentService 和 Activity 可以通信,这样那么 onHandleIntent() 不会被回调,相当于在你使用 Service 而不是 IntentService。
3、为什么多次启动 IntentService 会顺序执行事件,停止服务后,后续的事件得不到执行?
IntentService 中使用的 Handler、Looper、MessageQueue 机制把消息发送到线程中去执行的,所以多次启动 IntentService 不会重新创建新的服务和新的线程,只是把消息加入消息队列中等待执行,而如果服务停止,会清除消息队列中的消息,后续的事件得不到执行。

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

@Override
public void onDestroy() {
    mServiceLooper.quit();
}

具体参考GitHub上项目
文/孙福生微博(简书作者)原文链接:http://www.jianshu.com/p/332b6daf91f0著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

文/goeasyway(简书作者)原文链接:http://www.jianshu.com/p/7a7db9f8692d著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

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

推荐阅读更多精彩内容