Android 后台服务简要概述

本篇文章主要讲述android servivce相关知识,其中会穿插一些其他的知识点,作为初学者的教程。老鸟绕路

本文会讲述如下内容:
- 为什么要用Service
- Service及其继承者IntentService
- 一个后台计数器的例子来讲述Service
- Service如何与UI组件通信

为什么要用Service

我们接触android的时候,大部分时候是在和activity打交道,但是有些比如网络下载、大文件读取、解析等耗时却又不需要界面对象的操作。一旦退出界面,那么可能就会变得不可控(比如界面退出后,线程通知UI显示进度,但是由于View已经被销毁导致报错,或者界面退出后下载中断,就算你写得非常完美,什么异常状态都考虑到了,还是保证不了系统由于内存紧张把你这个后台的activity给干掉,依附于于它的下载线程也中断。)

这时候Service就有它的用武之地了,不依赖界面,消耗资源少,优先级比后台activity高,不会轻易被系统干掉(就算被干掉,也有标志位设置可以让它自动重启,这也是一些流氓软件牛皮鲜的招数)、

Service及其继承者IntentService

service的生命周期

service的生命周期相对activity要简单不少。
Android 后台服务简要概述_第1张图片

可以看出service有两条生命线,一条是调用startService,一条是调用bindService
,两条生命线相互独立。本文只讲startService。

一道选择题,解释service生命周期的所有问题:

android通过startService的方式开启服务,关于service生命周期的onCreate()和onStart() 说法正确的是哪两项
A.当第一次启动的时候先后调用 onCreate()和 onStart()方法
B.当第一次启动的时候只会调用 onCreate()方法
C.如果 service 已经启动,将先后调用 onCreate()和 onStart()方法
D.如果 service 已经启动,只会执行 onStart()方法,不在执行 onCreate()方法

答案自己想下,结尾公布

IntentService

一些容易被忽略的基础知识:Service运行的代码是在主线程上的,也就是说,直接在上面运行会卡住UI,这时就Service的继承者(继承于Service的子类)IntentService就应运而生。android studio的新建里面直接就有IntentService的模板,足见其应用之广。
那么Service与IntentService的区别在哪呢?
详见这里 Android之Service与IntentService的比较

简单来说就是

  • IntentService内部有个工作线程(Worker Thread),会将startService传入的intent通过Handler-Message机制传入工作线程,开发者通过重载onHandleIntent进行服务的具体实现。
  • IntentService在跑完onHandleIntent后,如果Handler队列里没有其他消息,就会自动结束服务,有点像Thread中run函数一样,跑完run函数之后,线程就结束了。而service需要自己去停止。

一个后台计数器的例子来讲述Service

实战环节,本文通过一个计数器的例子模拟下载文件的耗时操作。

public void startService(View view){
    Intent intent = new Intent(this,BackgroundService.class);
    intent.setAction("com.example.administrator.servicestudy.action.counter");
    intent.putExtra("duration",10);
    intent.putExtra("interval",1.0f);
    startService(intent);
}

上述代码就是一个启动service的例子,action相当于做什么操作(适用于一个service处理多种请求的情况。),extra就是参数。参数中duration代表总时间10秒,interval代码每隔一秒。

private static final String ACTION_COUNTER = "com.example.administrator.servicestudy.action.counter";

@Override
protected void onHandleIntent(Intent intent) {
    if (intent != null) {
        final String action = intent.getAction();
        if (ACTION_COUNTER.equals(action)) {
            final int duration = intent.getIntExtra(EXTRA_DURATION,0);
            final float interval = intent.getFloatExtra(EXTRA_INTERVAL,0);
            handleActionCounter(duration, interval);
        }
    }
}

private void handleActionCounter(int duration, float interval) {
    for(int i=0; i<duration; i++){
        updateUI(i,duration);
        try {
            Thread.sleep((long) (interval*1000));
        } catch (InterruptedException ignored) {
        }
    }
    updateUI(duration,duration);
}

可以看到重载onHandleIntent处理事件,handleActionCounter表示具体服务。根据传入的参数决定循环时间和sleep间隔。

当然别忘了在manifest文件中声明该Service

<service  android:name=".BackgroundService" android:exported="false" />

以上就是最基本的IntentService的用法了,不过为了代码独立性更好,可以将代码写成这样。
Activity

public void startService(View view){
     BackgroundService.startCounterService(this,1,10);
}

Service

public static void startCounterService(@NonNull Context context, int interval, int duration) {
        Intent intent = new Intent(context, BackgroundService.class);
        intent.setAction(ACTION_COUNTER);
        intent.putExtra(EXTRA_DURATION, duration);
        intent.putExtra(EXTRA_INTERVAL, interval);
        context.startService(intent);
    }

在Service里写个静态方法,只将参数传入,剩余的全都在Service内实现。虽然代码写的位置变了,但是代码运行的位置没变(静态方法依然还是运行在activity端),这样做将EXTRA_DURATION、EXTRA_INTERVAL等参数也不暴露给外部。做到更好的封装性和模块化,推荐这种做法。

Service如何与UI组件通信

那么Service在后台努力干活的时候,如何将当前进度通知给用户呢,因为Service不依赖任何界面,所以自身没办法操作界面(除非用Toast)。所以Service就要与其他组件进行通信(主要就是activity和通知栏了,但不限于上述两者)。

android组件间的通信(还记得android四大组件是哪四个不?)。 大部分通过android四大组件之一的Broadcast来通信。
那么简要说下Broadcast

Broadcast

生命周期:
Android 后台服务简要概述_第2张图片
就这么简单,一旦处理完广播就被销毁,没有onCreate,也没有onDestory
最重要的一点就是receiver里不能处理耗时操作,超过5秒(好像是)系统就会报错

Service

 private void updateUI(int current,int total){
    Intent intent = new Intent(BROADCAST_UPDATE_UI);
    intent.putExtra(EXTRA_CURRENT,current);
    intent.putExtra(EXTRA_TOTAL,total);

    sendBroadcast(intent);
}

可以看到,发个广播就这么简单,把参数填入intent,自定义一个action,send!好了。

Activity

@Override
protected void onResume() {
    super.onResume();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BackgroundService.BROADCAST_UPDATE_UI);
    registerReceiver(mBackgroundServiceReceiver,intentFilter);
}

@Override
protected void onPause() {
    super.onPause();
    unregisterReceiver(mBackgroundServiceReceiver);    
}

private BroadcastReceiver mBackgroundServiceReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG,"receive:"+intent.getAction());
        if(intent.getAction() == BackgroundService.BROADCAST_UPDATE_UI){
            int current = intent.getIntExtra(BackgroundService.EXTRA_CURRENT,0);
            int total = intent.getIntExtra(BackgroundService.EXTRA_TOTAL,0);
            mHint.setText(current+"/"+total);
        }
    }
};

Activity在resume的时候注册一个广播接收器,pasue的时候注销掉。在receiver里处理更新UI的操作。就这么简单

同样的,为了代码更具有封装性。在Activity中将recevier去掉。放在Service中,看代码:

<receiver android:name=".BackgroundService$BackgroundServiceReceiver">
   <intent-filter>
       <action android:name="com.example.administrator.servicestudy.action.update_ui" />
   </intent-filter>
</receiver>
public static class BackgroundServiceReceiver extends BroadcastReceiver {
    private static List<UIHandler> mHandlers = new ArrayList<>();

    @Override
    public void onReceive(Context context, Intent intent) {
        if(intent.getAction().equals(BROADCAST_UPDATE_UI)){
            int current = intent.getIntExtra(BackgroundService.EXTRA_CURRENT,0);
            int total = intent.getIntExtra(BackgroundService.EXTRA_TOTAL,0);
            for (UIHandler handler : mHandlers) {
                handler.onUpdateUI(current,total);
            }
        }
    }
}

public interface UIHandler {
    void onUpdateUI(int current,int total);
}

public static void registerUIHandler(UIHandler handler){
    if(handler != null){
        BackgroundServiceReceiver.mHandlers.add(handler);
    }

}

public static void unregisterUIHandler(UIHandler handler){
    BackgroundServiceReceiver.mHandlers.remove(handler);
}

这里代码有点多,一点一点说,

  1. 首先在manifest里注册一个静态广播接收器,静态就是表示一直都会接收的,不需要手动register和unregister。一般的receiver都是单独一个文件,这里为了更好地封装性,写在Service里作为静态内部类。所以在manifest里的注册名字也写成了.BackgroundService$BackgroundServiceReceiver,注意中间一个美元符号,那就是表示公共静态内部类的标志。
  2. 在Service内部实现一个Receiver,具体和Activity里面的一样。
  3. 然后写一个interface,代表具体的UI处理
  4. 写一个注册函数和反注册函数,用以界面组件注册UI更新事件。
  5. 由于该Service可能不止只更新一个界面组件,所以注册的Handler是一个列表。在收到广播后,将所有注册过的组件都通知更新一遍。

然后在Activity中注册一下。替换掉注册广播的地方。

@Override
protected void onResume() {
    super.onResume();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BackgroundService.BROADCAST_UPDATE_UI);
// registerReceiver(mBackgroundServiceReceiver,intentFilter);
    BackgroundService.registerUIHandler(mServiceUIHandler);
}

@Override
protected void onPause() {
    super.onPause();
    BackgroundService.unregisterUIHandler(mServiceUIHandler);
// unregisterReceiver(mBackgroundServiceReceiver);
}

private BackgroundService.UIHandler mServiceUIHandler = new BackgroundService.UIHandler() {
    @Override
    public void onUpdateUI(int current, int total) {
        Log.d(TAG,"receive: service broadcast");
        mHint.setText(current+"/"+total);
    }
};

这样就完成了一个Service的封装,简化Activity的代码,我的思想一直都是Activity中,应该只处理和界面有关的代码。就像C语言的main函数一样,你不可能把所有代码都写在main函数里吧。或者把所有的函数写在同一个文件里吧。


这里需要注意的是,由于之前提过IntentService内部其实是一个Worker Thread,所以多按几次start,其实是多发了几次消息,导致会计数完成后,重新计数。这个自己感受下就知道了。

那么我们加一个stop Service的函数吧。

Service

public static void stopCounterService(@NonNull Context context){
    Intent intent = new Intent(context, BackgroundService.class);
    intent.setAction(ACTION_COUNTER);
    context.stopService(intent);
}

Activity

public void stopService(View view){
// Intent intent = new Intent(this,BackgroundService.class);
// intent.setAction("com.example.administrator.servicestudy.action.counter");
// stopService(intent);
    BackgroundService.stopCounterService(this);
}

IntentService是以Message为单位来停止的,也就是说,一定要等到当前消息处理完才能完全stop掉,为此我们可以加一个标志位,一旦Service停止,强制循环退出。

Service

@Override
public void onCreate() {
    super.onCreate();
    Log.d(TAG,"onCreate");
    mServiceFinished = false;
}

@Override
public void onDestroy() {
    super.onDestroy();
    Log.d(TAG,"onDestroy");
    mServiceFinished = true;
}

private void handleActionCounter(int duration, float interval) {
   for(int i=0; i<duration; i++){
       if(mServiceFinished){
           break;
       }
       updateUI(i,duration);
       try {
           Thread.sleep((long) (interval*1000));
       } catch (InterruptedException ignored) {
       }
   }
   updateUI(duration,duration);
}

Service与通知栏的通信

至此我们已经完成了Service与Activity的通信,Service与Activity之间通过广播进行通信。Service负责逻辑处理,Activity负责更新界面显示。但是到这边还没发现Service的独特之处,就是这个这些代码完全也可以写在Activity里面的,写在Service里面无非就是结构更好看点,如果你那么认为就错了。你可以在Activity中退出再进入,可以发现计数器并没有因为Activity的退出而终止或者暂停。依然跟着时间走。这点是写在Activity中完全做不到的。当然你也可以通过一些小技巧来达到同样的效果,不过我们这个例子是为了模拟后台下载用的。所以不扯这些了。

下面进入真正的后台下载。Service与通知栏的通信。
我们这样设计一个程序,当Activity退出后,通知栏继续显示计数器进度,点击通知或者再次进入Activity,通知栏取消显示进度(为了不重复显示,也为了演示代码)。

为此我们新建一个新的Service,并在Activity添加如下代码

NotificationService

public class NotificationService extends Service {
    private static final String TAG = NotificationService.class.getSimpleName();

    public NotificationService() {
    }

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

    @Override
    public void onCreate() {
        super.onCreate();
        BackgroundService.registerUIHandler(mUIHandler);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestroy");
        BackgroundService.unregisterUIHandler(mUIHandler);
    }
    ......
}

这里我们新建的是一个普通的service,而不是IntentService,因为这边我们不需要耗时操作,我们甚至连onStartCommand都没有重载,因为我们只需要在启动服务的时候注册一个UI更新的回调就可以了,然后在销毁服务的时候注销掉。

Activity

@Override
protected void onResume() {
    super.onResume();
    ...
    stopService(new Intent(this,NotificationService.class));
}

@Override
protected void onPause() {
    super.onPause();
    ...
    startService(new Intent(this,NotificationService.class));
}

我们在Activity Resume的时候关闭通知栏通知服务,在Pause的时候开启该服务,这样就能做到我们的设计初衷。

接下来就是通知栏的UI更新操作了,都是通知栏的接口,听说2.3和4.0以上的接口很不一样,我们这边用的是4.0以上的接口。

private BackgroundService.UIHandler mUIHandler = new BackgroundService.UIHandler() {
    @Override
    public void onUpdateUI(int current, int total) {
        Log.d(TAG,"Notification onUpdateUI");
        //点击通知后,启动Activity,最后的FLAG_ONE_SHOT,表示只执行一次,具体自行百度。
        PendingIntent pendingIntent = PendingIntent.getActivity(NotificationService.this,
                0,
                new Intent(NotificationService.this,MainActivity.class),
                PendingIntent.FLAG_ONE_SHOT);

        NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Notification.Builder builder = new Notification.Builder(getApplicationContext());
        Notification notification = builder.setContentTitle("Background Service")
                .setTicker("Counting...")//状态栏上滚动的字符串
                .setContentText("Ongoing")//设置通知的正文
                .setProgress(total, current, false)//设置通知栏的进度条,android真贴心,终于可以不用自定义进度条了。
                .setOngoing(true)//设置可不可以取消该通知
                .setContentIntent(pendingIntent)//点击该通知后的操作。
                .setDefaults(Notification.DEFAULT_ALL)//通知的音效、震动、呼吸灯全都随系统设置,当然你也可以自定义
                .setAutoCancel(true)//是不是点击之后自动取消,否则的话,可能你需要手动调用接口来取消
                .setOnlyAlertOnce(true)//音效震动呼吸灯是否只提醒一下,专门给进度条之类,频繁更新的通知用的,不设置这个,你可以试试,那鬼畜的音效
                .setSmallIcon(R.mipmap.ic_launcher)//这个不解释了
                .build();
        //第一个参数为ID,APP内全局唯一,相同的ID表示相同的通知,不会在通知栏新增一条通知,不同的话,则在通知栏插入一条新的通知。第二个参数就是刚才配置的通知。
        nm.notify(1234,notification);
    }
};

最后提醒一句,通知不配置PendingIntent是不会显示的哦

为了完美模拟后台下载,我们在下载完成后(服务被销毁后),发送一个结束广播,通知UI层。
Service

public interface UIHandler {
    void onUpdateUI(int current,int total);
    void onFinish();
}

新增一个结束时的回调

@Override
public void onDestroy() {
    ....
    Intent intent = new Intent(BROADCAST_FINISH);
    sendBroadcast(intent);
}

在被销毁时发送广播

@Override
public void onReceive(Context context, Intent intent) {
    if(intent.getAction().equals(BROADCAST_UPDATE_UI)){
       ....
    }else if(intent.getAction().equals(BROADCAST_FINISH)){
        for (UIHandler handler : mHandlers) {
            handler.onFinish();
        }
    }
}

在onReceive中发送onFinish的回调

<receiver android:name=".BackgroundService$BackgroundServiceReceiver">
  <intent-filter>
      <action android:name="com.example.administrator.servicestudy.action.update_ui" />
      <action android:name="com.example.administrator.servicestudy.action.finish" />
  </intent-filter>
</receiver>

最重要的是别忘了在manifest中声明这个广播,因为Service中的是静态广播接收器

而在Activity和Notification中就简单多了,只要实现相应的onFinish回调就可以了

@Override
public void onFinish() {
    Log.d(TAG,"receive: service finish");
    mHint.setText("Finished");
}
@Override
public void onFinish() {
    Log.d(TAG,"Notification onFinish");

    NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    Notification.Builder builder = new Notification.Builder(getApplicationContext());
    Notification notification = builder.setContentTitle("Background Service")
            .setContentText("Finished")
            .setOngoing(false)
            .setContentIntent(null)//这里PendingIntent设置为null,只是为了演示代码,这样这个通知点上去就不会有反应
            .setDefaults(Notification.DEFAULT_ALL)
            .setAutoCancel(true)
            .setOnlyAlertOnce(true)
            .setSmallIcon(R.mipmap.ic_launcher)
            .build();

    //设置两个不同的notification ID,为了演示两个不同通知,并且演示如何取消一个通知
    nm.notify(1232,notification);
    nm.cancel(1234);
}

教程到此结束。谢谢

最后公布,文中一道问题的答案,A和D。很简单吧
源码点这里下载

你可能感兴趣的:(android,service)