10 后台默默地劳动者--探究服务

不管怎么说,后台功能属于四大组件之一,其重要程度不言而喻

10.1 服务是什么

服务(Service)是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要求长期运行的任务。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。

不过需要注意的是,服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。

另外,也不要被服务的后台概念所迷惑,实际上服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞住的情况。

10.2.1 线程的基本用法

定义一个线程只需要新建一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时逻辑即可。

class MyThread extends Thread {
    @Override
    public void run() {
       //处理具体的逻辑
    }
}

只需要newMyThread的实例,然后调用它的start()方法,这样run()方法中的代码就会在子线程当中运行了。

new MyThread().start();

当然,使用继承的方式耦合性有点高,更多的时候我们都会选择使用实现Runnable接口的方式来定义一个线程。

class MyThread implements Runnable {
    @Override
    public void run() {
       //处理具体的逻辑
    }
}

如果使用了这种写法,启动线程的方法也需要进行相应的改变。

MyThread myThread = new MyThread();
new Thread(myThread).start();

Thread的构造函数接收一个Runnable参数,而我们new出的MyThread正是一个实现了Runnable接口的对象,所以可以直接将它传入到Thread的构造函数中。接着调用Threadstart()方法,run()方法中的代码就会在子线程当中运行了。

当然,如果你不想专门再定义一个类去实现Runnable接口,也可以使用匿名类的方式,这种写法更为常见。

new Thread(new Runnable() {
    @Override
    public void run() {
      //处理具体的逻辑
    }
}).start();

10.2.2 在子线程中更新UI

和其他的GUI库一样,AndroidUI也是线程不安全的。也就是说,如果想要更新应用程序里的UI元素,则必须在主线程中进行,否则就会出现异常。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView text;

    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text2);
        button = (Button) findViewById(R.id.change_text);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        text.setText("Nice to meet you");
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

观察logcat中的错误日志,可以看出是由于在子线程中更新UI所导致的

 Process: com.example.androidthreadtest, PID: 10616  
 
 android.view.ViewRootImpl$CalledFromWrongThreadException:
 
 Only the original thread that created a view hierarchy can touch its views.

由此证实了Android确实是不允许在子线程中进行UI操作的。

有些时候,我们必须在子线程里去执行一些耗时任务,然后根据任务的执行结果来更新相应的UI控件。对于这种情况,Android提供了一套异步消息处理机制,完美地解决了在子线程中进行UI操作的问题。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView text;

    private Button button;

    private static final int UODATE_TEXT = 1;

    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text2);
        button = (Button) findViewById(R.id.change_text);
        button.setOnClickListener(this);

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case UODATE_TEXT:
                        //在这里可以进行UI操作
                        text.setText("Nice to meet you !");
                        break;
                    default:
                        break;
                }
            }
        };

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();
                        message.what = UODATE_TEXT;
                        handler.sendMessage(message);//将Message对象发送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

我们先是定义了一个整形常量UPDATE_TEXT,用于表示更新TextView这个动作。然后新增一个Handler对象,并重写父类的handleMessage()方法,在这里对具体的Message进行处理。如果发现Messagewhat字段的值等于UPDATE_TEXT,就将TextView显示的内容改成Nice to meet you

可以看到,这次我们并没有在子线程里直接进行UI操作,而是创建了一个Message(android.os.Message)对象,并将它的what字段的值指定为UPDATE_TEXT,然后调用HandlersendMessage()方法将这条Message发送出去。很快,Handler就会收到这条Message,并在handleMessage()方法中对它进行处理。注意此时handleMessage()方法中的代码就是在主线程当中运行的了,所以我们可以放心地在这里进行UI操作。接下来对Message携带的what字段的值进行判断,如果等于UPDATE_TEXT,就将TextView显示的内容改成Nice to meet you.

10.2.3 解析异步消息处理机制

Android中的异步消息处理主要由四部分组成:MessageHandlerMessageQueueLooper

1.Message

Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。上面我们使用到了Messagewhat字段,除此之外还可以使用arg1arg2字段来携带一些整形数据,使用obj字段携带一个Object对象。

2.Handler

Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息。发送消息一般是使用HandlersendMessage()方法,而发出的消息经过一系列辗转处理后,最终会传递到HandlerhandleMessage()方法中。

3.MessageQueue

MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。

4.Looper

Looper是每个线程中的MessageQueue的管家,调用Looperloop()方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到HandlerhandleMessage()方法中。每个线程中也只会有一个Looper对象。

异步消息处理机制流程图

我们来把异步消息处理的整个流程梳理一遍。首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回HandlerhandleMessage()方法中。由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。

一条Message经过这样一个流程的辗转调用后,也就从子线程进入到了主线程,从不能更新UI变成了可以更新UI,整个异步消息处理的核心思想也就是如此。

而我们前面使用到的runOnUiThread()方法其实就是一个异步消息处理机制的接口封装,它虽然表面上看起来用法更为简单,但其实背后的实现原理和上图的描述是一样的。

10.2.4 使用AsyncTask

为了更加方便我们在子线程中对UI进行操作,Android还提供了另外一些好用的工具,比如AsyncTask。借助AsyncTask,即便你对异步消息处理机制完全不了解,也可以十分简单地从子线程切换到主线程。当然,AsyncTask背后的实现原理也是基于异步消息处理机制的,只是Android帮我们做了很好地封装而已。

AsyncTask的基本用法,由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为AsyncTask类指定3个泛型参数。

Params: 在执行AsyncTask时需要传入的参数,可用于在后台任务中执行。

Progress: 后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

Result: 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

因此,一个最简单的自定义AsyncTask就可以写成如下方式:

class DownloadTask extends AsyncTask<Void,Integer,Boolean> {
    ....
}

我们把AsyncTask的第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整形数据来做为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。

目前我们自定义的DownloadTask还是一个空任务,并不能进行任何的操作,我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。

1.onPreExecute()

这个方法会在后台任务开始之前调用,用于一些界面上的初始化操作,比如显示一个进度条对话框等。

2.doInBackground(Params...)
这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。注意:在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress...)方法来完成

3.onProgressUpdate(Progress...)
当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate(Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。

4.onPostExecute(Result)
当后台任务执行完毕通过return语句进行返回时,这个方法就很快被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI的操作,比如说提醒任务执行的结果,以及关闭掉进度对话框等。

class DownloadTask extends AsyncTask<Void,Integer,Boolean> {

        @Override
        protected void onPreExecute() {
            progressDialog.show();
        }

        @Override
        protected Boolean doInBackground(Void... params)
        {
            try
            {
                while (true)
                {
                    int downloadPercent = doDownload();//这是一个虚构的方法
                    publishProgress(downloadPercent);
                    if (downloadPercent >= 100)
                    {
                        break;
                    }
                }
            } catch (Exception e)
            {
                return false;
            }
            return true;
        }

        @Override
        protected void onProgressUpdate(Integer... values)
        {
            //在这里进行更新下载进度
            progressDialog.setMessage("Downloaded " + values[0] + "%");
        }

        @Override
        protected void onPostExecute(Boolean aBoolean)
        {
            progressDialog.dismiss();//关闭进度对话框
            //在这里提示下载结果
            if (aBoolean)
            {
                Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
            }
            else 
            {
                Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
            }
        }
        
    }

在这个DownloadTask中,我们在doInBackground()方法中去执行具体的下载任务。这个方法里的代码都是在子线程中运行的,因而不会影响到主线程的运行。注意这里虚构了一个doDownload()方法,这个方法用于计算当前的下载进度并返回,我们假设这个方法已经存在了。在得到了当前的下载进度后,下面就该考虑如何把它显示到界面上了,由于doInBackground()方法是在子线程中运行的,在这里肯定不能进行UI操作,所以我们可以调用publishProgress()方法并将当前的下载进度传进来,这样onProgressUpdate()方法就会很快被调用,在这里就可以进行UI操作了。

当下载完成后,doInBackground()方法会返回一个布尔型变量,这样onPostExecuet()方法就会很快被调用,这个方法也是在主线程中运行的。然后在这里我们会根据下载的结果来弹出相应的Toast提示,从而完成整个DownloadTask任务。

使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。

如果想要启动这个任务,只需编写以下代码:

new DownloadTask().execute()

服务的基本用法

定义一个服务

image

我们将服务命名为MyService,Exported属性表示是否允许除了当前程序以外的其他程序访问这个服务,Enabled属性表示是否启用这个服务。

public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

MyService是继承自Service类的,说明这是一个服务。目前MyService中可以算是空空如也,但有一个onBind()方法特别醒目。这个方法是Service中唯一的一个抽象方法,所以必须要在子类里实现。

我们在服务中处理一些事情,处理事情的逻辑写在哪呢?这时就可以重写Service中的另外一些方法了。

public class MyService extends Service {
    
    ```

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

}

我们又重写了onCreate(),onStartCommand()onDestroy()这3个方法,他们是每个服务中最常用到的3个方法了。其中onCreate()方法会在服务创建的时候调用,onStartCommand()方法会在每次服务启动的时候调用,onDestroy()方法会在服务销毁的时候调用。

通常情况下,如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法中。而当服务销毁时,我们又应该在onDestroy()方法中回收那些不再使用的资源。

另外需要注意的是,每一个服务都需要在AndroidManifest.xml文件中进行注册才能生效。

<service
      android:name=".MyService"
      android:enabled="true"
      android:exported="true">
</service>

10.3.2 启动和停止服务

启动服务和停止服务的方法当然你也不会陌生,主要是借助Intent来实现的。

 @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//启动服务
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);//停止服务
                break;
            default:
                break;
        }
    }

onCreate()方法中分别获取到了Start Service按钮和Stop Service按钮的实例,并给他们注册了点击事件。然后在Start Service按钮的点击事件里我们构建出了一个Intent对象,并调用startService()方法来启动MyService服务。

Stop Service按钮的点击事件里我们同样构建出了一个Intent对象,并调用stopService()方法来停止MyService服务。

startService()stopService()方法都是定义在Context类中的,所以我们在活动里可以直接调用这两个方法。注意,这里完全是由活动来决定服务何时停止的,如果没有点击Stop Service按钮,服务就会一直处于运行状态。那服务有没有什么办法让自己停止下来呢?当然可以,只需要在MyService的任何一个位置调用stopSelf()方法就能让这个服务停止下来了。

onCreate()方法是在服务第一次创建的时候调用的,而onStartCommand()方法则在每次启动服务的时候都会调用,由于刚才我们是第一次点击Start Service按钮,服务此时还未创建过所以两个方法都会执行,之后如果你再连续多点击几次Start Service按钮,你就会发现只有onStartCommand()方法可以得到执行了。

10.3.3 活动和服务进行通信

上一节中虽然服务是在活动里启动的,但在启动了服务之后,活动和服务基本就没有什么关系了。

服务会一直处于运行状态,但具体是运行的什么逻辑,活动就控制不了了。这就类似于活动通知了服务一下:“你可以启动了”,然后服务就去忙自己的事情了,但活动并不知道服务到底去做了什么事情,以及完成的如何。

创建一个专门的Binder对象来对下载功能进行管理。

public class MyService extends Service {

    private static final String TAG = "MyService";
    
    ······

    private DownloadBinder mBinder = new DownloadBinder();

    class DownloadBinder extends Binder {
        public void startDownload() {
            Log.d(TAG, "startDownload executed");
        }

        public int getProgress() {
            Log.d(TAG, "getProgress executed");
            return 0;
        }
    }

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

我们新建了一个DownloadBinder类,并让它继承自Binder,然后在它的内部提供了开始下载以及查看下载进度的方法。当然这只是两个模拟方法,并没有实现真正的功能,我们在这两个方法中分别打印了一行日志。

当一个活动和服务绑定之后,就可以调用该服务里的Binder提供的方法了。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button startService,stopService;

    private Button bindService,unbindService;

    private MyService.DownloadBinder downloadBinder;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (MyService.DownloadBinder) service;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }

        @Override
        public void onServiceDisconnected(ComponentName name)
        {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startService = (Button) findViewById(R.id.start_service);
        stopService = (Button) findViewById(R.id.stop_service);
        startService.setOnClickListener(this);
        stopService.setOnClickListener(this);

        bindService = (Button) findViewById(R.id.bind_service);
        unbindService = (Button) findViewById(R.id.unbind_service);
        bindService.setOnClickListener(this);
        unbindService.setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        switch (v.getId())
        {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//启动服务
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);//停止服务
                break;
            case R.id.bind_service:
                Intent bindIntent = new Intent(this,MyService.class);
                bindService(bindIntent,connection,BIND_AUTO_CREATE);//绑定服务
                break;
            case R.id.unbind_service:
                unbindService(connection);//解绑服务
                break;
            default:
                break;
        }
    }
}

我们首先创建了一个ServiceConnection的匿名类,在里面重写了onServiceConnected()方法和onServiceDisconnected()方法,这两个方法分别会在活动与服务成功绑定以及解除绑定的时候调用。在onServiceConnected()方法中,我们又通过向下转型得到了DownloadBinder的实例,有了这个实例,活动和服务之间的关系就变得非常紧密了。现在我们可以在活动中根据具体的场景来调用DownloadBinder中的任何public方法,即实现了指挥服务干什么服务就去干什么的功能。

我们这里仍然是构建出了一个Intent对象,然后调用bindService()方法将MainActivityMyService进行绑定。bindService()方法接收三个参数,第一个参数就是刚刚构建出的Intent对象,第二个参数是前面创建出的ServiceConnection的实例,第三个参数则是一个标志位,这里传入BIND_AUTO_CREATE表示在活动和服务进行绑定后自动创建服务。这会使得MyService中的onCreate()方法得到执行,但是onStartCommand()方法不会执行。

解除服务和活动之间的绑定,调用一下unbindService()方法就可以了。

另外需要注意,任何一个服务在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的活动进行绑定,而且在绑定完成后他们都可以获取到相同的DownloadBinder实例。

10.4 服务的生命周期

服务有自己的生命周期。

一旦在项目的任何位置调用了ContextstartService()方法,相应的服务就会启动起来,并回调onStartCommand()方法。如果这个服务之前还没有创建过,onStart()方法会先于onStartCommand()方法执行。服务启动了之后会一直保持运行状态,直到stopService()stopSelf()方法被调用。注意,虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService()stopSelf()方法,服务就会停止下来了。

另外,还可以调用ContextbindService()来获取一个服务的持久连接,这时就会回调服务中的onBind()方法。类似里,如果这个服务之前还没有创建过,onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到onBind()方法里返回的IBinder对象的实例,这样就能自由地和服务进行通信了。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。

当调用了startService()方法后,又去调用stopService()方法,这时服务中的onDestroy()方法就会执行,表示服务已经销毁了。类似地,当调用了bindService()方法后,又去调用unbindService()方法,onDestroy()方法也会执行。这两种情况都很好理解。但是需要注意,我们是完全有可能对一个服务即调用了startService()方法,又调用了bindService()方法,这种下情况该如何让服务销毁呢?根据Android系统的机制,一个服务只要被启动或者被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用stopService()unbindService()方法,onDestroy()方法才能执行。

这样你就已经把服务的生命周期完整里走了一遍。

10.5 服务的更多技巧

10.5.1 使用前台服务

服务几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是服务的系统优先级还是比较低的,当系统出现内存不足的情况时,就有可能回收掉正在后台运行的服务。如果你希望服务可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台服务。

前台服务和普通服务最大的区别就在于,它会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。当然有时候你也可能不仅仅是为了防止服务被回收掉才使用前台服务的,有些项目由于特殊的需求会要求必须使用前台服务,比如说彩云天气这款天气预报应用,它的服务在后台更新天气数据的同时,还会在系统状态栏一直显示当前的天气信息。

public class MyService extends Service {
     ······
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate executed");
        Intent intent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,0);
        Notification notification = new Notification.Builder(this)
                .setContentTitle("This is content title")
                .setContentText("This is content text")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
                .setContentIntent(pendingIntent)
                .build();
        startForeground(1,notification);
    }

    ``````
}

这次在构建出Notification对象后并没有使用NotificationManager来将通知显示出来,而是调用了startForeground()方法。这个方法接收两个参数,第一个参数是通知的id,类似于notify()方法的第一个参数,第二个参数则是构建出的Notification对象。调用startForeground()方法后就会让MyService变成一个前台服务,并在系统状态显示出来。

10.5.2 使用IntentService

服务中的代码都是默认运行在主线程当中的,如果直接在服务里去处理一些耗时的逻辑,就很容易出现ANR(Application Not Responding)的情况。

所以这个时候就需要用到Android多线程编程的技术了,我们应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑。因此,一个比较标准的服务就可以写成如下形式:

public class MyService extends Service {
    ······
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               //处理具体的逻辑
            }
        }).start();
        return super.onStartCommand(intent,flags,startId);
    }

}

但是,这个服务一旦启动之后,就会一直处于运行状态,必须调用stopService()或者stopSelf()方法才能让服务停止下来。所以,如果想要实现让一个服务在执行完毕后自动停止的功能,就可以这样写:

public class MyService extends Service {
    ······
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               //处理具体的逻辑
               stopSelf();
            }
        }).start();
        return super.onStartCommand(intent,flags,startId);
    }

}

为了可以简单里创建一个异步的,会自动停止的服务,Android专门提供了一个IntentService类。

public class MyIntentService extends IntentService {

    private static final String TAG = "MyIntentService";
    
    public MyIntentService() {
        super("MyIntentService");//调用父类的有参构造函数
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //打印当前线程的id
        Log.d(TAG, "Thread id is " +Thread.currentThread().getId());
    }

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

这里首先要提供一个无参的构造函数,并且必须在其内部调用父类的有参构造函数。然后要在子类中去实现onHandleIntent()这个抽象方法,在这个方法中可以去处理一些具体的逻辑,而且不用担心 ANR的问题,因为这个方法已经是在子线程中运行的了。这里为了证实一下,我们在onHandleIntent()方法中打印了当前线程的id。另外根据IntentService的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了onDestroy()方法,在这里也打印了一行日志,以证实服务是不是停止掉了。

 case R.id.start_intent_service:
                //打印主线程id
                Log.d(TAG, "Thread id is " +Thread.currentThread().getId());
                Intent intentService = new Intent(this,MyIntentService.class);
                startService(intentService);
                break;

在按钮的点击事件里面去启动MyIntentService这个服务,并在这里打印了一下主线程的id,稍后用于和IntentService进行对比。你会发现,其实IntentService的用法和普通的服务没什么两样。

最后不要忘记,服务都是需要在AndroidManifest.xml里注册的。

<service android:name=".MyIntentService"/>

第一部分

public class MainActivity extends AppCompatActivity implements View.OnClickListener
{

    private Button startService,pauseService,cancelService;

    private DownloadService.DownloadBinder downloadBinder;

    private ServiceConnection connection = new ServiceConnection()
    {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
        {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name)
        {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startService = (Button) findViewById(R.id.start_download);
        pauseService = (Button) findViewById(R.id.pause_download);
        cancelService = (Button) findViewById(R.id.cancel_download);

        startService.setOnClickListener(this);
        pauseService.setOnClickListener(this);
        cancelService.setOnClickListener(this);

        Intent intent = new Intent(this,DownloadService.class);
        startService(intent);//启动服务
        bindService(intent,connection,BIND_AUTO_CREATE);//绑定服务

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission
                .WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
        {
            ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission
                    .WRITE_EXTERNAL_STORAGE},1);
        }
    }

    @Override
    public void onClick(View v)
    {
        if (downloadBinder == null)
        {
            return;
        }

        switch (v.getId())
        {
            case R.id.start_download:
                String url = "https://raw.githubusercontent.com/guolindev/eclipse/" +
                        "master/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[]
            permissions, @NonNull int[] grantResults)
    {
        switch (requestCode)
        {
            case 1:
                if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED)
                {
                    Toast.makeText(MainActivity.this, "拒绝权限将无法使用程序!",
                            Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        unbindService(connection);
    }
}

这里我们首先创建一个ServiceConnection的匿名类,然后在onServiceConnected()方法中获取到DownloadBinder的实例,有了这个实例,我们就可以在活动中调用服务提供的各种方法了。

在onCreate()方法中,对按钮进行了初始化操作并设置了点击事件,然后分别调用了startService()和bindService()方法来启动和绑定服务。这一点至关重要,因为启动服务可以保证DownloadService一直在后台运行,绑定服务则可以让MainActivity和DownloadService进行通信,因此两个方法调用都必不可少。在onCreate()方法的最后,我们还进行了WRITE_EXTERNAL_STORAGE的运行时权限申请,因为下载文件是要下载到SD卡的Download目录下的,如果没有这个权限的话,我们整个程序就都无法正常工作。

在onClick()方法中我们对点击事件进行判断,如果点击了开始按钮就调用DownloadBinder的startDownload()方法,如果点击了暂停按钮就调用pauseDownload()方法,如果点击了取消按钮就调用cancelDownload()方法。startDownload()方法中你可以传入任意的下载地址。

另外还有一点需要注意,如果活动被销毁了,那么一定要记得对服务进行解绑,不然就有可能会造成内存泄露。

第二部分

public class DownloadService extends Service
{

    private DownloadTask downloadTask;

    private String downloadUrl;

    private DownloadListener listener = new DownloadListener()
    {
        @Override
        public void onProgress(int progress)
        {
            getNotificationManager().notify(1,getNotification("Downloading...",progress));
        }

        @Override
        public void onSuccess()
        {
            downloadTask = null;
            //下载成功时将前台服务通知关闭,并创建一个下载成功的通知
            stopForeground(true);
            getNotificationManager().notify(1,getNotification("Downloading Success",-1));
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onFailed()
        {
            downloadTask = null;
            //下载失败时将前台服务通知关闭,并创建一个下载失败的通知
            stopForeground(true);
            getNotificationManager().notify(1,getNotification("Download Failed",-1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPaused()
        {
            downloadTask = null;
            Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onCanceled()
        {
            downloadTask = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
        }
    };

    private DownloadBinder mBinder = new DownloadBinder();

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

    class DownloadBinder extends Binder
    {
        public void startDownload(String url)
        {
            if (downloadTask == null)
            {
                downloadUrl = url;
                downloadTask = new DownloadTask(listener);
                downloadTask.execute(downloadUrl);
                startForeground(1,getNotification("Downloading...",0));
                Toast.makeText(DownloadService.this, "Downloading...",
                        Toast.LENGTH_SHORT).show();
            }
        }

        public void pauseDownload()
        {
            if (downloadTask != null)
            {
                downloadTask.pauseDownload();
            }
        }

        public void cancelDownload()
        {
            if (downloadTask != null)
            {
                downloadTask.cancelDownload();
            }
            else
            {
                if (downloadUrl != null)
                {
                    String filename = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory(Environment
                    .DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory + filename);
                    if (file.exists())
                    {
                        file.delete();
                    }
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

    private NotificationManager getNotificationManager()
    {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        return manager;
    }

    private Notification getNotification(String title,int progress)
    {
        Intent intent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),
                R.mipmap.ic_launcher));
        builder.setContentIntent(pendingIntent);
        builder.setContentTitle(title);
        if (progress > 0)
        {
            //当progress大于或等于0时才需显示下载进度
            builder.setContentText(progress + "%");
            builder.setProgress(100,progress,false);
        }
        return builder.build();
    }
}

首先这里创建了一个DownloadListener的匿名类实例,并在匿名类中实现了onProgress(),onSuccess(),onFailed(),onPaused()和onCanceled()这5个方法。在onProgress()方法中,我们调用getNotification()构建了一个用于显示下载进度的通知,然后调用NotificationManager的notify()方法去触发这个通知,这样就可以在下拉状态栏中实时看到当前的下载进度了。在onSuccess()方法中,我们首先是将正在下载的前台通知关掉,然后创建了一个新的通知用于告诉用户下载成功了。

接下来为了让DownloadService可以和活动进行通信,我们又创建了一个DownloadBinder。DownloadBinder中提供了startDownload(),pauseDownload()和cancelDownload()这三个方法,在startDownload()方法中,我们创建了一个DownloadTask的实例,把刚才的DownloadListener作为参数传入,然后调用execute()方法开启下载,并将下载文件的URL地址传入到execute()方法中。同时为了让这个下载服务成为一个前台服务,我们还调用了startForeground()方法,这样就会在系统状态栏中创建一个持续运行的通知了。

pausedDownload()方法中的代码就非常简单了,就是简单地调用了一下DownloadTask中的pauseDownload()方法。cancelDownload()方法中的逻辑也基本类似,但是要注意,取消下载的时候我们需要将正在下载的文件删除掉,这一点和暂停下载时不同的。

DownloadService类中所有使用到的通知都是调用getNotification()方法进行构建的,这个方法中的代码我们我们之前基本都是学过的,只有一个setProgress()方法没有见过。
setProgress()方法接收3个参数,第一个参数传入通知的最大进度,第二个参数传入通知的当前进度,第三个参数表示是否使用模糊进度条,这里传入false。设置完setProgress()方法,通知上就会有进度条显示出来了。

第三部分

public class DownloadTask extends AsyncTask<String,Integer,Integer>
{
    public static final int TYPE_SUCCESS = 0;
    public static final int TYPE_FAILED = 1;
    public static final int TYPE_PAUSED = 2;
    public static final int TYPE_CANCELED = 3;

    private DownloadListener listener;

    private boolean isCanceled = false;

    private boolean isPaused = false;

    private int lastProgress;

    public DownloadTask(DownloadListener listener)
    {
        this.listener = listener;
    }

    @Override
    protected Integer doInBackground(String... params)
    {
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;

        try
        {
            long downloadLength = 0;
            String downloadUrl = params[0];
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                    .getPath();
            file = new File(directory + fileName);
            if (file.exists())
            {
                downloadLength = file.length();
            }
            long contentLength = getContentLength(downloadUrl);

            if (contentLength == 0)
            {
                return TYPE_FAILED;
            }
            else if (contentLength == downloadLength)
            {
                return TYPE_SUCCESS;
            }

            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    //断点下载,指定从哪个字节开始下载
                    .addHeader("RANGE","bytes = " + downloadLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if (response != null)
            {
                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file,"rw");
                savedFile.seek(downloadLength);//跳过已下载的字节
                byte[] b = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(b)) != -1)
                {
                    if (isCanceled)
                    {
                        return TYPE_CANCELED;
                    }
                    else if (isPaused)
                    {
                        return TYPE_PAUSED;
                    }
                    else
                    {
                        total += len;
                        savedFile.write(b,0,len);
                        int progress = (int) ((total + downloadLength) * 100 / contentLength);
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally
        {
            try
            {
                if (is != null)
                {
                    is.close();
                }
                if (savedFile != null)
                {
                    savedFile.close();
                }
                if (isCanceled && file != null)
                {
                    file.delete();
                }
            } catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    @Override
    protected void onProgressUpdate(Integer... values)
    {
        int progress = values[0];
        if (progress > lastProgress)
        {
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }

    @Override
    protected void onPostExecute(Integer integer)
    {
        switch (integer)
        {
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_CANCELED:
                listener.onCanceled();
                break;
        }
    }

    public void pauseDownload()
    {
        isPaused = true;
    }

    public void cancelDownload()
    {
        isCanceled = true;
    }

    private long getContentLength(String downloadUrl) throws IOException
    {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response = client.newCall(request).execute();
        if (response != null && response.isSuccessful())
        {
            long contentLength = response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }
}

首先看一下AsyncTask的3个泛型参数:第一个泛型参数指定为String,表示在执行AsyncTask的时候需要传入一个字符串参数给后台任务;第二个泛型参数指定为Integer,表示使用整形数据来作为进度显示单位;第三个泛型参数指定为Integer,则表示使用整形数据来反馈执行结果。

接下来我们定义了4个整形常量用于表示下载的状态,TYPE_SUCCESS表示下载成功,TYEP_FAILED表示下载失败,TYEP_PAUSED表示暂停下载,TYPE_CANCELED表示取消下载。然后在DownloadTask的构造函数中要求传入一个刚刚定义的DownloadListener参数,我们待会就会将下载的状态通过这个参数进行回调。

接着就是重写doInBackground(),onProgressUpdate()和onPostExecute()这三个方法了,

doInBackground():用于在后台执行具体的下载逻辑

onProgressUpdate():用于在界面上更新当前的下载进度

onPostExecute():通知最终的下载结果。

先来看一下doInBackground()方法,首先我们从参数中获取到了下载的URL地址,并根据URL地址解析出了下载的文件名,然后指定将文件下载到Environment.DIRECTORY_DOWNLOADS目录下,也就是SD卡的Download目录。我们还要判断一下Download 目录中是不是已经存在要下载的文件了,如果已经存在的话则读取已下载的字节数,这样就可以在后面启用断点续传的功能了。接下来先是调用getContentLength()方法来获取待下载文件的总长度,如果文件长度等于0则说明文件有问题,直接返回TYPE_FAILED,如果文件长度等于已下载文件长度,那么就说明已经下载完了,直接返回TYPE_SUCCESS即可。紧接着使用OKhttp来发送一条网络请求,需要注意的是,这里在请求中加入了一个header,用于告诉服务器我们想要从哪个字节下载,因为已下载过的部分就不要重新下载了。接下来读取服务器响应的数据,并使用Java的文件流方式,不断从网络上读取数据,不断写入到本地,一直到文件下载完成为止。在这个过程中,我们还要判断用户有没有触发暂停或者取消的操作,如果有的话则返回TYPE_PAUSED或TYPE_CANCELED来中断下载,如果没有的话则实时计算当前的下载进度,然后调用publishProgress()方法进行通知。暂停和取消操作都是使用一个布尔型的变量来进行控制的,调用pauseDownload()或cancelDownload()方法即可更改变量的值。

接下来看一下onProgressUpdate()方法,这个方法就简单的多了,它首先从参数中获取到当前的下载进度,然后和上一次的下载进度对比,如果有变化的话则调用DownloadListener的onProgress()方法来通知下载进度更新。

最后是onPostExecute()方法,也非常简单,就是根据参数中传入的下载状态来进行回调,下载成功就调用DownloadListener的onSuccess()方法,下载失败就调用onFailed()方法,暂停下载就调用onPaused()方法,取消下载就调用onCeaceled()方法。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,833评论 25 707
  • 9.1服务是什么 服务( Service)是 Android 中实现程序后台运行的解决方案,它适合用于去执行不需要...
    wyxjoker阅读 328评论 0 1
  • 《课程的逻辑》第二章内容是课程改革的文化使命。在新课程实施中首当其冲的,是直接参与课程教材设计的编审队伍的建...
    徐徐聪阅读 273评论 0 0
  • 1邱卫豪 随笔 失败是一种营养,让我们学会坚强,跌倒是一种营养,让我们在风雨中茁壮成长,挫折是一种...
    跬步堂阅读 851评论 0 0
  • 昨天是爷爷去世二十周年的日子,我写了一篇《没有告别的永别》,谨以怀念。爷爷的“得意门生”侯保方看后,情不自禁,回忆...
    雪琴吟阅读 600评论 0 3