第二行代码学习笔记——第十章:后台默默的劳动者——探究服务

本章要点

Android沿用了诺基亚系统的Symbian操作系统的老习惯,从一开始就支持后台功能,这使得应用程序即使在关闭的情况下仍然可以在后台继续运行。后台功能属于四大组件之一,重要程度言不可寓。


10.1 服务是什么

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

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

默认在主线程,可能会造成主线程阻塞的问题。


10.2 Android多线程编程

当我们执行耗时操作,就必须放在子线程中运行,避免线程阻塞的问题,增强用户的体验度。

10.2.1 线程的基本用法

Android多线程与Java多线程使用语法相同。比如说,定义一个线程只需要定义一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时逻辑即可,如下:

class MyThread extends Thread {

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

启动这个线程,new出MyThread的实例,然后调用start()方法,这样run()方法就运行在子线程中了,如下:

new MyThread().start();

当然,使用继承的方式高耦合,更多的时候我们选择实现Runable接口的方式来定义一个线程,如下:

class MyThread implements Runable {

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

那么启动方式如下:

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

常见的实现方式如下:

new Thread(new Runable() {

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

以上就是线程的基本用法。在Java中创建和启动线程的方式是一样的。

10.2.2 在子线程中更新UI

更新应用程序中的UI元素,必须放在主线程。否则就会出现异常。

那我们就来验证下吧。新建AndroidThreadTest项目,修改activity_main.xml中的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn_change_tv"
        android:text="Change Text"
        android:textAllCaps="false"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/tv"
        android:layout_centerInParent="true"
        android:text="Hello World"
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

接下来我们对Hello World 进行修改,修改MainActivity中的代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btn_change_tv;
    private TextView tv;

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

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

我们在子线程中更新UI,运行程序,点击Change Text按钮,我们会发现程序崩溃。观察logcat日志,可以看到原因是由于在子线程中更新UI操作,如下:
e

由此我们证明了Android不能在子线程中更新UI操作。有时候我们会在耗时操作返回的结果需要进行相应的UI更新。那么我们通过Android提供的异步消息处理机制,完美解决在子线程中更新UI操作。

使用异步处理消息的方法。
修改MainActivity中的代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final int UPDATE_TEXT=1; //表示更新TextView的动作

    private Button btn_change_tv;
    private TextView tv;

    private Handler handler=new Handler(){
        @Override
        public void handleMessage(Message msg) { //4.主线程
            switch (msg.what){
                case UPDATE_TEXT: //5.判断是否相等,进行操作
                    //在这里进行UI操作
                    tv.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }
    };
    ...
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_change_tv:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message=new Message(); //1.创建Message对象
                        message.what=UPDATE_TEXT; //2.指定what值
                        handler.sendMessage(message); //3.将Message对象发送过去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

重新运行程序,点击Change Text按钮,Hello World就会变成Nice to meet you。如图:

changetext

使用Handler机制顺利的解决了在子线程中更新UI的问题。

10.2.3 解析异步消息处理机制

Android中的异步消息处理主要有4部分组成:Message,Handler,MessageQueue,Looper。

  1. Message
    Message是在线程之间传递的消息,它可以携带少量的信息,用于在不同线程之间交换数据。Message的what字段,arg1和arg2字段携带整型数据,obj字段携带一个Object对象。
  2. Handler
    Handler处理者,主要用于发送和处理消息。发送消息Handler的sendMessage()方法,发送的消息最终会传递到Handler的handleMessage()方法中。
  3. MessageQueue
    MessageQueue消息列队,主要用于存放所有通过Handler发送的消息。这部分消息一直会存在于消息列队中,等待被处理。每个线程中只会有一个MessageQueue对象。
  4. Looper
    Looper是每个线程中的MessageQueue的管家,调用Looper的lop方法后,就会进入到无线循环中,然后每当发送MessageQueue中存在的一条消息,就会将它取出,并传递到Handler中的handleMessage()方法中。每个线程中也只会有一个Looper对象。

异步消息处理的整个流程:首先需要在主线程中创建一个Handler对象,并重写handleMessage()方法。然后在子线程中进行需要UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息被添加在MessageQueue消息列队中等待被处理,而Looper会一直尝试者从MessageQueue中取出待处理的消息,最后发送给Handel的handleMessage方法。由于Handler是在主线程中创建的,所以此时handleMessage()方法也会在主线程中执行,就可以进行UI操作了。整个异步消息处理机制流程图:
handler

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

之前我们使用的runOnUiThread()方法就是一个异步消息处理机制的接口封装。原理跟上图描述的一样。

10.2.4 使用AsyncTask

Android为了更加方便在子线程中更新UI操作,使用AsyncTask。它的实现原理也是基于异步消息处理机制的封装。

AsyncTask的基本用法,AsyncTask是一个抽象类,我们必须创建一个类继承它。继承时可以为AsyncTask指定3个泛型参数。

  • Params。在执行AsyncTask时传入的参数,可用于在后台的使用。
  • Progress。 后台执行任务时,如果需要在界面显示当前的进度条,则使用这里指定的泛型作为进度单位。
  • Result。 任务执行完毕,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

一个完整的自定义AsyncTask如下:

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

    /**
     * 开始执行任务之前,用于初始化见界面。例如:显示一个进度条
     */
    @Override
    protected void onPreExecute() {
        progressDialog.show();//显示进度对话框
    }

    /**
     * 在子线程中执行,处理耗时操作。
     * 如果需要更新UI元素,则调用publishProgress()
     *
     * @param params
     * @return
     */
    @Override
    protected Boolean doInBackground(Void... params) {
        try {
            while (true) {
                int downloadPercent = doDownlod(); //这是一个虚方法
                publishProgress(downloadPercent);
                if (downloadPercent >= 100) {
                    break;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * 当在后台中调用了publishProgress()方法后,onProgressUpdate()很快就会调用,
     * 该方法携带的参数就是从后台传过来的。在这里执行对UI操作。
     *
     * @param values
     */
    @Override
    protected void onProgressUpdate(Integer... values) {
        //在这里执行下载进度
        progressDialog.setMessage("Download" + values[0] + "%");
    }

    /**
     * 当后台执行完毕返回进行返回时。利用返回数据更新UI操作。
     * 比如:提醒任务执行的结果,以及关闭对话框等。
     *
     * @param result
     */
    @Override
    protected void onPostExecute(Boolean result) {
        progressDialog.dismiss(); //关闭进度对话框
        //在这里提醒下载结果
        if (result) {
            Toast.makeText(content, "Download succeeded", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(content, "Download failed", Toast.LENGTH_SHORT).show();
        }
    }
}

第一个泛型参数Void,表示不需要传入参数给后台任务。
第二个泛型参数指定为Integer,表示使用整型数据类型作为进度显示单位。
第三个泛型参数指定为Boolean,表示返回回馈的结果。

在这个DownloadTask中,在doInBackground()方法执行具体的下载任务(子线程)。doDownload()方法是一个虚方法,计算当前的下载进度并返回,并让它显示到界面中。我们通过调用 publishProgress()方法并将下载进度传进来。这样onProgressUpdate()就会调用,进行UI操作。下载完成后,doInBackground()返回的是布尔值,然后通过onPostExecute()弹出相应的Toast提示。完成异步加载任务。

简单的来说,在AsyncTask中,doInBackground()进行耗时操作;onProgressUpdate()更新UI操作,onPostExecute()执行收尾工作。

启动这个任务,代码如下:

new DownloadTask().execute();

本章最佳实践,完善下载这个功能。


10.3 服务的基本用法

Android四大组件之一 —— 服务。

10.3.1 定义一个服务

新建ServiceTest项目,点击包—>New—>Service—>Service。弹出如下窗口:

service
Enabled:表示是否启动这个服务。 Exported:是否允许除了当前程序之外的程序进行访问这个服务。全部勾选,点击Finish完成创建。

MyService的代码如下:

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

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

MyService类继承自Service。onBind()方法是Service中唯一的一个抽象方法。

处理事情,定义Service其他方法,代码如下:

public class MyService extends Service {

     ...
    /**
     * 创建服务的时候调用
     */
    @Override
    public void onCreate() {
        super.onCreate();
    }

    /**
     * 每次启动的服务的时候调用
     * @param intent
     * @param flags
     * @param startId
     * @return
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 销毁服务的时候调用
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

每个服务都需要在AndroidManifest.xml注册是才能够使用,这是Android四大组件的共同点。创建服务的时候Android Studio已经帮我们智能的创建完成了。打开AndroidManifest.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hjw.servicetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"></service>
    </application>

</manifest>

这样,我们就定义好了一个服务。

10.3.2 启动和停止服务

借助Intent来启动和停止这个服务,在ServiceTest项目中来实现吧。

修改activity_main.xml中的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_start_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Service"
        android:textAllCaps="false" />

    <Button
        android:id="@+id/btn_stop_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Service"
        android:textAllCaps="false" />


</LinearLayout>

修改MainActivity中的代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btn_start_service, btn_stop_service;

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

        btn_start_service = (Button) findViewById(R.id.btn_start_service);
        btn_stop_service = (Button) findViewById(R.id.btn_stop_service);

        btn_start_service.setOnClickListener(this);
        btn_stop_service.setOnClickListener(this);
    }

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

测试服务启动或停止,我们在MyService中的方法加入日志,如下所示:

public class MyService extends Service {
    private static final String TAG = "MyService";
    ...
    /**
     * 创建服务的时候调用
     */
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: executed");
    }

    /**
     * 每次启动的服务的时候调用
     *
     * @param intent
     * @param flags
     * @param startId
     * @return
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand: executed");
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 销毁服务的时候调用
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy: executed");
    }
}

接下来我们运行程序,如下:

jm

点击Start Service按钮,观察logcat日志如下:

start
这时这个服务已经启动了,我们打开设置—>开发者选项—>正在运行的服务,如图:

run

然后我们点击Stop Service按钮,观察日志如下:

stopservice

可以看出MyService的确停止服务了。

onCreate()和onStartCommand()区别,onCreate()会在第一次创建的时候调用,而onStartCommand()则在每次创建服务的时候调用。当我们点击多次Start Service按钮,第一次会执行两个方法,而以后只会执行onStartCommand()方法。

10.3.3 活动和服务进行通信

借助onBind()方法,使我们的服务与活动关联起来。

我们希望实现MyService实现一个下载功能,在活动中决定合适开始下载,以及查看进度。那我们创建一个Binder对象对下载功能进行管理。修改MyService中的代码如下:

public class MyService extends Service {
    private static final String TAG = "MyService";

    public MyService() {
    }

    private DownloadBinder mBinder=new DownloadBinder();

    @Override
    public IBinder onBind(Intent intent) {

        return mBinder;

    }


    class DownloadBinder extends Binder{

        /**
         * 模拟方法
         */
        public void startDownload(){
            Log.d(TAG, "startDownload: executed");
        }

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

新建DownloadBinder类,并继承Binder,再里面实现下载和查看进度的模拟方法(这里我们分别打印一行日志)。

在MyService中创建DownloadBinder的实例,然后在onBind()返回实例。做完MyService工作了。

在活动中调用服务里的方法。新增两个按钮(绑定服务,解绑服务),修改activity_main中的文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    ...
    <Button
        android:id="@+id/btn_bind_service"
        android:textAllCaps="false"
        android:text="Bind Service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn_unbind_service"
        android:textAllCaps="false"
        android:text="unBind Service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


</LinearLayout>

当一个活动和服务绑定之后,就可以调用服务里的Binder提供的方法,修改MainActivity中的代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service;

    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);

        ...
        btn_bind_service= (Button) findViewById(R.id.btn_bind_service);
        btn_unbind_service= (Button) findViewById(R.id.btn_unbind_service);

        ...
        btn_bind_service.setOnClickListener(this);
        btn_unbind_service.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            ...
            case R.id.btn_bind_service:
                Intent bindIntent = new Intent(this, MyService.class);
                bindService(bindIntent,connection,BIND_AUTO_CREATE); //绑定服务
                break;
            case R.id.btn_unbind_service:
                unbindService(connection); //解绑服务
                break;
            default:
                break;
        }
    }
}

首先创建一个ServiceConnection匿名类,重写了onServiceConnected()和onServiceDisconnected()方法,与服务成功绑定和解绑的时候调用。通过向下转型得到了DownloadBinder的实例,接下来在onServiceConnected()中调用DownloadBinder的startDownload()和getProgress()方法。

绑定服务:binderService()接受三个参数,第一个参数Intent,第二个参数ServiceConnection,第三个参数BIND_AUTO_CREATE(表示在活动和服务进行绑定后自动创建服务)。这样MyService中的onCreate()会得到执行,但onStartCommand()方法不会执行。

解除绑定:unBinderService()方法。

运行程序,点击Bind Service中的按钮,观察logcat日志如下:

bindservice

这样就完成了我们的绑定服务,任何一个服务在应用范围内都是通用的(一个服务可以和任意一个活动绑定)。


10.4 服务的生命周期

服务也有自己的生命周期,官方给出两种服务的生命周期,一目了然,如图:

servicelife


10.5 服务的更多技巧

接下来我们学习服务高级使用技巧。

10.5.1 使用前台服务

希望服务一直保持运行,而不希望系统内存不足而被回收,我们可以使用前台服务。前台服务和普通服务的最大的区别就在与,它会一直有一个正在运行的图标在系统的状态栏显示,下拉菜单显示详情内容,类似于通知。

创建前台服务,修改MyService中的代码如下:

public class MyService extends Service {
     ...

    /**
     * 创建服务的时候调用
     */
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: executed");
        Intent intent=new Intent(this,MainActivity.class);
        PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
        Notification notification=new NotificationCompat.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(pi)
                .build();

        startForeground(1,notification);
    }
    ...
}

调用startForeground()方法来显示通知,就会让MyService变成一个前台服务,显示在系统的状态栏。

运行程序,并点击Start Service或Bind Service按钮,就会启动前台服务。如图:

qtservice

10.5.2 使用IntentService

服务的代码默认在主线程,如果在服务里进行一些耗时操作,很容易出现ANR(Application Not Responding)的情况。

为了解决ANR,我们必须在每个服务的每个具体方法里开启一个子线程,去处理一些耗时的操作。一个标准的服务的代码如下:

public class MyService extends Service {
     ...
    /**
     * 每次启动的服务的时候调用
     *
     * @param intent
     * @param flags
     * @param startId
     * @return
     */
    @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);
    }
    ...
}

为了解决我们忘记开启子线程,或忘记调用stopSelf()方法。Android提供了一个异步的,会自动停止服务的IntentService类。

新建MyIntentService类继承自IntentService,代码如下:

public class MyIntentService extends IntentService {

    public MyIntentService() {
        super("MyIntentService"); //调用父类的有参构造函数
    }

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

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

首先我们提供一个无参的构造函数,并且必须调用内部父类的有参构造函数。然后在子类中去实现onHandleIntent()抽象方法,在这个方法中处理具体的逻辑,这个方法在子线程中运行(解决了ARN问题)。

接下来修改activity_main.xml中的代码,加入一个启动MyiIntentStart按钮,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    ...
    <Button
        android:id="@+id/btn_start_intent_service"
        android:textAllCaps="false"
        android:text="Start IntentService"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

修改MainActivity中的代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service,btn_start_intent_service;

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

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            ...
            case R.id.btn_start_intent_service:
            Log.d("MainActivity", "Thread id is " + Thread.currentThread().getId());//打印主线程的id
                Intent intentService=new Intent(this,MyIntentService.class);
                startService(intentService);
                break;
            default:
                break;
        }
    }
}

在AndroidManifest.xml中注册服务,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hjw.servicetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...
        <service android:name=".MyIntentService"/>
    </application>

</manifest>

运行程序,点击Start IntentService按钮,观察logcat日志,如下:
intentservice

我们可以看到MyIntentService和MainActivity中的所在线程的id不一样,而且onDestory()也得到了执行。集开启线程和自动停止与一身,IntentService是我们的最爱。


10.6 服务的最佳实践 — 完整版的下载

实现服务中经常使用到的功能个——下载功能。

创建一个ServiceBestPractice项目。

添加OkHttp依赖库,如下:

compile 'com.squareup.okhttp3:okhttp:3.8.0'

接下来定义一个DownloadListener接口回调,用于对下载过程中的各种状态进行监听。如下:

public interface DownloadListener {

    void onProgress(int progress);

    void onSuccess();

    void onFailed();

    void onPaused();

    void onCanceled();
}

编写下载功能,新建DownloadTask继承自AysncTask,代码如下:

public class DownloadTask extends AsyncTask<String, Integer, Integer> {

    private static final int TYPE_SUCCESS = 0;
    private static final int TYPE_FAILED = 1;
    private static final int TYPE_PAUSED = 2;
    private 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 (request != null) {
                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");
                savedFile.seek(downloadLength); //跳过已下载的字节
                byte[] bytes = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(bytes)) != -1) {
                    if (isCanceled) {
                        return TYPE_CANCELED;
                    } else if (isPaused) {
                        return TYPE_PAUSED;
                    } else {
                        total += len;
                        savedFile.write(bytes, 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 (IOException 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 status) {
        switch (status) {
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_CANCELED:
                listener.onCanceled();
                break;
            default:
                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个整型常量表示下载的状态。然后在DownloadTask的构造函数中要求传入刚刚定义的DownloadListener参数,通过这个参数将下载的状态进行回调。

接下来我们重写doInBackground(),onProgressUpdate()和onPostExecute()这3个方法。

doInBackground()方法用于在后台执行具体下载的逻辑,首先我们从参数中获取了下载路径的URL地址,并根据URL地址解析出来的下载名,然后将文件下载到SD卡的Download路径下(Environment.DIRECTORY_DOWNLOADS)。判断目录下是否存在要下载的文件,存在则读取下载的字节数,这样就可以在后面启动断电下载传的功能。接下来先是调用了getContentLength()方法来获取待下载文件的长度,如果长度等于0,则说明文件有问题,返回TPE_FAILD,如果文件长度等于已经下载文件的长度,则说明下载完成,返回TYPE_SUCCESS。紧接着我们使用OKHttp发送网络请求,在请求中添加了header,指用于告诉服务器我们想从哪个节点开始下载,因为下载过的就不需要下载了(断点下载)。接下来读取服务器响应的数据,并使用Java的文件流的方式,不断从网络上读取数据,不断写入本地,直到文件全部下载完。如果用户在下载过程中没有执行暂停或取消的操作,则实时计算当前下载的进度,然后调用publishProgress()方法进行通知。暂停或取消都是用布尔值进行控制的,调用pauseDownload()或cancelDownload()方法即可更改变量的值。

onProgressUpdate()用于在界面中更新当前的下载进度,首先获取当前的下载进度,与上次下载的进行对比,如果有变化则调用DownloadListener的onProgress()方法来通知下载进度更新。

最后onPostExecute()用于通知最终下载的结果,根据参数中传入的下载状态的参数进行回调。

这样我们就实现了下载的功能,接下来我们为了保证DownloadTask一直运行在后台,则需要创建DownloadService(服务),修改代码如下:

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

    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("Download 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, "Pause", 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() {
        return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }

    private Notification getNotification(String title, int progress) {
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = 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(pi);
        builder.setContentTitle(title);
        if (progress > 0) {
            //当progress大于或等于0显示加载进度
            builder.setContentText(progress + "%");
            builder.setProgress(100, progress, false);
        }
        return builder.build();
    }


}

首先我们创建了DownloadListener的匿名实例,实现下载状态的5个方法。在onProgress方法在,调用getNotification()构建了一个用于显示下载进度的通知,然后调用NotificationManager的notify()方法出发通知,就可在下载状态中查看下载进度。再其他几个方法中将正在下载的前台服务通知关闭,创建一个新的通知告诉用于返回的结果。

为了让DownloadService和活动进行通信,我们创建了一个DownloadBinder。它提供了startDownload(),pauseDownload()和cancelDownload()方法分别是开始,暂停和取消下载的。

DownloadService类中的所有使用的通知都调用getNotification。

实现了下载的服务。编写activity_main.xml中的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_start_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Download"
        android:textAllCaps="false" />

    <Button
        android:id="@+id/btn_pause_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Pause Download"
        android:textAllCaps="false" />

    <Button
        android:id="@+id/btn_cancel_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Cancel Download"
        android:textAllCaps="false" />

</LinearLayout>

接下来修改MainActivity中的代码,如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    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) {

        }
    };

    private Button btn_start_download, btn_pause_download, btn_cancel_download;

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

        btn_start_download = (Button) findViewById(R.id.btn_start_download);
        btn_pause_download = (Button) findViewById(R.id.btn_pause_download);
        btn_cancel_download = (Button) findViewById(R.id.btn_cancel_download);

        btn_start_download.setOnClickListener(this);
        btn_pause_download.setOnClickListener(this);
        btn_cancel_download.setOnClickListener(this);

        Intent intent = new Intent(this, DownloadService.class);
        startService(intent); //启动服务
        bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(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.btn_start_download:
                String downloadUrl = "http://eclipse.stu.edu.tw/oomph/epp/neon/R3/eclipse-inst-win64.exe";
                downloadBinder.startDownload(downloadUrl);
                break;
            case R.id.btn_pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.btn_cancel_download:
                downloadBinder.cancelDownload();
                break;
            default:
                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(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进行通信(两个方法必不可少)。我们还进行了申请权限的操作。

在onClick()方法中对点击事件进行判断,点击开始就调用DownloadBinder中的startDowmload()方法;暂停调用pauseDownload()。取消调用cancelDownload()。

销毁活动,一定要对服务进行解绑,否则造成内存泄露。我们在onDestroy()完成解绑。

在AndroidManifest.xml中声明权限,以及DownloadService,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hjw.servicebestpractice">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

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

</manifest>

运行程序,会弹出授权对话框,如下:

premession

点击允许,然后点击Start Download按钮就开始下载了,下载过程下拉系统状态栏进行查看,如下:

downloadrun

我们还可以点击Pause Download或Cancel Download,甚至断网操作来测试这个程序的健壮性。下载完会弹出Download Success,提示用于下载完成。我们打开SD卡的Download查看,如下:

downloadsuccess

我们可以看到文件已经下载完成了。你可以任意的测试,这个下载实例的健壮性,综合性都是很强的。


10.7 小结与点评

学习了Android多线程,服务的基本用法,服务的生命周期,前台服务和IntentServiced等。

发布了18 篇原创文章 · 获赞 28 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/JiangWeiHu/article/details/72588039
今日推荐