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

来源:互联网 发布:krpano全景漫游软件 编辑:程序博客网 时间:2024/04/30 01:28

本章要点

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等。

阅读全文
1 1
原创粉丝点击