Android入门(16)| 服务


概念

服务(Service)用于执行无需和用户交互需要长期运行的任务,其不是独立进程,而是依赖于创建服务时所在的应用程序进程。应用程序进程死亡时,所有依赖于该进程的服务也都将停止运行。

Android 多线程

服务不会自动开启线程,因此为了防止主线程被阻塞,应该在服务内部手动创建子线程。

通常有三种线程的使用方式:

继承 Thread

新建一个类继承 Thread ,然后重写 run() 方法:

public class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        // 处理耗时逻辑
    }
}

启动线程:

// new出实例,然后调用start方法
// 这样run()方法中代码就会在子线程中运行了
new MyThread().start();

继承 Runable 接口

使用继承的方式耦合性有点高(如父类添加新方法所有子类都要跟着添加),更多时候使用 Runnable接口 定义线程来降低耦合:

public class MyThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        // 处理耗时逻辑
    }
}

启动线程:

MyThread myThread = new MyThread();
// 使用接收一个Runnable参数的 Thread() 构造方法来 new 一个匿名类
// 接着调用start方法,run()方法中代码就会在子线程中运行了
new Thread(myThread).start();

匿名类

无需专门定义一个类实现 Runnable接口,而是在代码中需要用到的地方创建匿名类,直接启动子线程执行耗时操作:

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

异步消息处理

这一点在上一篇博客中有深刻体会,不使用 runOnUiThread 跳回主线程,而是在子线程中直接操作 UI 的话会报错:Only the original thread that created a view hierarchy can touch its views

runOnUiThread() 方法其实就是一个异步消息处理机制的接口封装异步消息处理主要由四部分组成:

Message

线程间传递的消息,可以携带少量信息。通过字段来携带数据,如:

  • waht: 用户自定义的消息代码,每个 handler 各自包含自己的消息代码,所以不用担心自定义的消息跟其他 handler 有冲突。
  • arg1、arg2: 如果只需要存储几个整型数据,arg1、arg2 是 setData() 的低成本替代品。
  • obj: Object对象。当使用 Message对象 在线程间传递消息时,如果它包含一个 Parcelable 的结构类(不是由应用程序实现的类),此字段必须为非空(non-null)。其他的数据传输则使用 setData(Bundle) 方法。

Handler

用于 发送(使用 sendMessage() 方法)处理(使用handleMessage() 方法) 消息。

MessageQueue

消息队列,存放所有通过 Handler 发送的消息。每个线程中只会有一个 MessageQueue 对象

Looper

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

异步消息处理机制流程如图:
在这里插入图片描述

  1. 主线程中创建一个 Handler对象,并重写 handleMessage() 方法。
	// 隐式的Looper会导致操作丢失、程序崩溃和紊乱情况、Handler非期望等问题
	// 因此安卓11不允许使用无参数的Handler构造方法
	// 如果非得用隐式,用Looper.myLooper()作为参数
	// 否则可以使用Looper.getMainLooper()作为参数
    private Handler handler = new Handler(Looper.getMainLooper()){
    
    
        @Override
        public void handleMessage(@NonNull Message msg) {
    
    
            switch (msg.what){
    
    
            	// WHAT_CODE是自定义的what字段值
                case WHAT_CODE:
                    // 执行UI操作
                    break;
            }
        }
    }

关于ANDROID 11推荐使用的 Handler 构造方法详见

  1. 子线程需要进行 UI操作 时,创建一个 Message对象,并通过 Handler.sendMessage() 方法发送出去。
Message message = new Message();
message.what = WHAT_CODE;
handler.sendMessage(message);
  1. 该条消息会被添加到 MessageQueue 队列中等待被 Looper 取出并分发回 Handler.handleMessage() 方法中。
  2. 由于 Handler对象 是在主线程中创建的,因此 Handler.handleMessage() 方法中的 UI操作 也是在主线程中运行的。

AsyncTask

其原理也是基于异步消息处理机制,只是 Android 做好了封装。AsyncTask 是抽象类,继承时可以为其指定三个泛型参数:

  1. Params: 可在后台任务中使用。
  2. Progress: 后台任务执行时,如果需要在界面上显示当前进度,使用该参数指定的泛型作为进度单位。
  3. Result: 任务执行完毕后,若需要对结果进行返回,则使用该参数指定的泛型作为返回值类型。

举个例子:

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

上述自定义的 DownloadTask 三个参数的意义分别是:

  1. Void: 执行时无需将传入参数给后台任务。
  2. Integer: 使用整型数据作为进度显示单位。
  3. Boolean: 使用布尔型数据来反馈执行结果

自定义类继承 AsyncTask 时,常需要被重写的方法有:

class DownloadTask extends AsyncTask<Void, Integer, Boolean>{
    
    
    @Override
    protected void onPreExecute() {
    
    
        progressDialog.show(); // 显示进度对话框
    }

	// 执行具体耗时任务
    @Override
    protected Boolean doInBackground(Void... voids) {
    
    
        try{
    
    
            while (true){
    
    
                // 假设doDownload方法已实现,该方法用于计算下载速度并返回
                int downloadPercent = doDownload();
                // 当后台计算仍在运行时,可以从doInBackground调用此方法在Ul线程上发布更新;
                // 对该方法的每次调用都将触发UI线程上onProgressUpdate的执行;
                // 如果任务已取消,则不会调用onProgressUpdate。
                publishProgress(downloadPercent);
                if(downloadPercent >= 100){
    
    
                    break;
                }
            }
        } catch (Exception e){
    
    
            return false;
        }
        // 下载完成后返回布尔型变量,调用onPostExecute方法
        return true;
    }

	// 进行UI操作
    @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_LONG).show();
        }
        else{
    
    
            Toast.makeText(context, "download failed", Toast.LENGTH_LONG).show();
        }
    }
}
  1. onPreExecute: 在后台任务开始执行之前调用,用于界面上的初始化操作,如显示一个进度条对话框。
  2. doInBackground: 该方法中所有代码都在子线程中运行,可在此处理所有的耗时任务。任务一旦完成可以通过 return 语句将结果返回(如果 AsyncTask 第三个泛型参数指定的是 Void,则可以不返回执行结果)。该方法不可以进行 UI操作,如果要更新 UI元素,可以调用 publishProgress(Progress... values) 方法来完成。
  3. onProgressUpdate: 每次调用 publishProgress(Progress... values) 方法都会触发该方法执行,该方法的参数是后台任务中传递过来的,在这里可以进行UI操作,利用参数对界面元素进行更新
  4. onPostExecute:doInBackground 方法执行 return 语句后调用,可以利用返回的数据执行UI操作

启动 DownloadTask 任务只需编写以下代码:

new DownloadTask().execute();

使用服务

框架

创建服务,框架如下:

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

    // Service中唯一的抽象方法,必须在子类中实现
    @Override
    public IBinder onBind(Intent intent) {
    
    
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    // 服务创建时调用
    @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();
    }
}

每一个服务都需要在 AndroidManifest.xml 中注册才能生效:
在这里插入图片描述


启动/停止服务

启动服务的目的是让服务一直在后台运行。

活动布局文件:

在这里插入图片描述
一个按钮用来启动服务,另一个用来终止服务。

活动文件:

public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {
    
    

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

        Button button_start_service = findViewById(R.id.button_start_service);
        button_start_service.setOnClickListener(this);

        Button button_stop_service = findViewById(R.id.button_stop_service);
        button_stop_service.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
    
    
        switch (v.getId()){
    
    
            case R.id.button_start_service:
                Intent startIntent = new Intent(this, MyService.class);
                startService(startIntent);
                break;
            case R.id.button_stop_service:
                Intent stopIntent = new Intent(this, MyService.class);
                stopService(stopIntent);
                break;
        }
    }
}

除了通过 startService()stopService()启动/停止 服务,还可以在 MyService 中调用 stopSelf() 方法让服务停止。


绑定/解绑服务

绑定服务的目的是让服务和活动可以进行通信。

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

	// 用来和活动进行通信
    private DownloadBinder mBinder = new DownloadBinder();
    
    // Service中唯一的抽象方法,必须在子类中实现
    @Override
    public IBinder onBind(Intent intent) {
    
    
        return mBinder;
    }
    
    public class DownloadBinder extends Binder{
    
    

        public void startDownload(){
    
    
            Log.e(TAG, "startDownload: ");
        }

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

serviceonCreate/onStartCommand/onStart 生命周期相关的方法总是在 主线程 上执行的,如果 bindService 在主线程上阻塞的话。service 就无法执行上述生命周期相关的方法,完成初始化工作。因为 绑定服务要在子线程上执行,因此绑定完成后必须通过 ServiceConnection 来回调到主线程。

public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {
    
    
    private MyService.DownloadBinder downloadBinder;

    // 匿名类
    private ServiceConnection connection = new ServiceConnection(){
    
    
        // 成功绑定时调用
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
    
    
            // 向下转型生成实例
            downloadBinder = (MyService.DownloadBinder) service;
            // 此时可以调用DownloadBinder的任何public方法
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }

        // 解绑时调用
        @Override
        public void onServiceDisconnected(ComponentName name) {
    
    

        }
    };

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

        Button button_bind = findViewById(R.id.button_bind);
        button_bind.setOnClickListener(this);

        Button button_unbind = findViewById(R.id.button_unbind);
        button_unbind.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
    
    
        switch (v.getId()){
    
    
            case R.id.button_bind:
                Intent bindIntent = new Intent(this,  MyService.class);
                bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务
                break;
            case R.id.button_unbind:
                unbindService(connection); // 解绑服务
                break;
        }
    }
}
  • bindService: 绑定活动与服务,该方法接受三个参数:
    • Intent 对象
    • ServiceConnection 实例
    • 标志位: BIND_AUTO_CREATE 表示绑定后自动创建服务。此时 MyServiceonCreate() 方法会执行,onStartCommand() 方法不会执行。
  • unbindService: 该方法解绑活动与服务,接受一个参数:ServiceConnection 实例

PS:任何一个服务在整个应用程序范围内都是通用的,意味着可以和多个活动绑定(绑定服务是异步的),绑定后都获得相同的 DownloadBinder 实例。


服务的生命周期

  • ContextstartService() 方法结束后会立刻回调 服务onStartCommand() 方法。(如果此前服务还未创建过,会先调用 服务onCreate() 方法)。
  • ContextstopService() 方法 或 服务stopSelf() 方法 可以停止服务。值得一提的是:
    • 服务onStartCommond() 方法 里面调用 stopSelf() 方法 时,服务不会马上停止,而是在 onStartCommond() 方法 执行结束才会停止。
    • 调用 stopSelf() 方法 之后,服务会执行 onDestory() 方法。
    • 如果 onStartCommond() 方法 中启动一个线程,调用 stopSelf() 方法,线程也不会被杀死。
  • ContextbindService() 方法可以获取一个服务的持久连接,结束后回调 服务 的 onBind() 方法。调用方通过 onBind() 返回的 IBinder 实例和 服务 进行通信。

PS:一个服务只要被启动(startService)或被绑定(bindService),就会一直处于运行状态,想要停止运行时,服务必须处于 停止(stopService) + 解绑(unbindService) 的状态,服务才能被销毁。

在这里插入图片描述


前台服务

系统内存不足时,可能会回收正在运行的后台服务;而前台服务可以一直保持运行,避免被回收。前台服务和普通服务的最大区别是,会一直在系统状态栏显示一个正在运行的图标,下拉状态栏可以显示详细信息。这其实就用到了之前通知一文的知识。

    // 服务创建时调用
    @Override
    public void onCreate() {
    
    
        super.onCreate();
        Log.e(TAG, "onCreate executed");
        Intent intent = new Intent(this, ServiceActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        String id = "1";

        NotificationManager manager = (NotificationManager)
                getSystemService(Context.NOTIFICATION_SERVICE);

        if(Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
    
    
            String name = getString(R.string.app_name);
            // 创建通知通道
            // 第一个参数要和NotificationCompat.Builder的channelId一样
            // 第三个参数是通知的重要程度
            NotificationChannel notificationChannel = new NotificationChannel(id, name,
                    NotificationManager.IMPORTANCE_HIGH);
            manager.createNotificationChannel(notificationChannel);
        }
        Notification notification = new NotificationCompat.Builder(this, id)
                .setContentTitle("天气")
                .setContentText("天气内容")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.cloud)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.cloud))
                // 点击通知后执行的意图
                .setContentIntent(pi)
                .build();
        // 不使用NotificationManager.notify()显示
        // 而使用startForeground显示
        startForeground(1, notification);
    }

PS:实现通知的代码都是之前介绍过的,唯一不同的就是显示通知是通过 startForeground() 方法,而非 NotificationManager.notify() 方法。


IntentService

通常在 onStartCommand() 方法中开启子线程来执行耗时逻辑,并在子线程中逻辑处理完毕后调用 stopSelf() 方法来自动结束服务:

	// 每次服务启动时调用
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    
    
        Log.e(TAG, "onStartCommand executed");
        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";

    //  name用于命名工作线程,仅对调试很重要
    public MyIntentService() {
    
    
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
    
    
        Log.e(TAG, "onHandleIntent: Thread id is" + Thread.currentThread().getId());
    }

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

在活动中通过按钮调用它:
在这里插入图片描述
点击按钮后的输出结果:
在这里插入图片描述


完整版下载示例

下载过程的回调接口:DownloadListener

// 回调接口,监听下载过程中的各种状态
public interface DownloadListener {
    
    
    void onProgress(int progress); // 当前下载进度
    void onSuccess(); // 下载成功
    void onFailed(); // 失败
    void onPaused(); // 暂停
    void onCanceled(); // 取消下载
}

继承 AsyncTask 实现下载功能:DownloadTask

DownloadTask 实现了具体的下载功能。

// 自实现的下载任务的异步消息处理机制
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
    
    
    private static final String TAG = "DownloadTask";

    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 final DownloadListener listener;
    Context context;

    private boolean isCanceled = false;
    private boolean isPaused = false;
    private int lastProgress; // 上一次的下载进度

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

    // 执行具体耗时任务——下载逻辑
    @Override
    protected Integer doInBackground(String... strings) {
    
    
        Log.e(TAG, "doInBackground: 下载开始");
        Log.e(TAG, "子线程 id is " + Thread.currentThread().getId());
        InputStream inputStream = null;
        // RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传
        RandomAccessFile savedFile = null;
        File file = null;

        try{
    
    
            long downloadedLength = 0; // 已下载的文件长度
            String downloadUrl = strings[0]; // 从传入的参数中得到欲下载资源的URL
            // lastIndexOf返回downloadUrl最后一次出现“/”的索引位置,截取该“/”到结尾的部分作为fileName
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            // 下载目录为SD卡的Download目录
            /*String directory = Environment.getExternalStoragePublicDirectory
                    (Environment.DIRECTORY_DOWNLOADS).getPath();*/
            // /Android/data/com.example.activitytest/files/Documents
            String directory = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getPath();
            file = new File(directory + fileName);
            Log.e(TAG, "doInBackground file: " + file);

            // 文件存在则说明上次的下载行为被中断了
            // 此时需要用downloadedLength记录已下载的字节数,辅助完成断点续传功能
            if(file.exists()){
    
    
                downloadedLength = file.length();
                Log.e(TAG, "doInBackground: file exists, downloadedLength: " + downloadedLength);
            }
            else {
    
    
                Log.e(TAG, "doInBackground: file not exists, downloadedLength: " + downloadedLength);
            }

            long contentLength = getContentLength(downloadUrl); // 待下载文件总长度
            if(contentLength == 0){
    
     // 长度为0说明文件有问题
                Log.e(TAG, "doInBackground: contentLength == 0");
                return TYPE_FAILED;
            }
            else if (contentLength == downloadedLength){
    
    
                // 已下载字节和文件总字节相等,说明下载已完成
                Log.e(TAG, "doInBackground: 下载过了");
                return TYPE_SUCCESS;
            }

            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    // 断点下载,指定从哪个字节开始下载
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if(response != null){
    
    
                Log.e(TAG, "服务器的确认报文");
                inputStream = response.body().byteStream(); // 字节输入流
                Log.e(TAG, "doInBackground inputStream: " + inputStream);
                savedFile = new RandomAccessFile(file, "rw");
                Log.e(TAG, "doInBackground saveFile: " + savedFile);
                savedFile.seek(downloadedLength); // 跳过已下载字节
                Log.e(TAG, "断点重续 over");

                byte[] bArray = new byte[1024];
                int total = 0; // 已读字节
                int len; // 读入缓冲区的字节总数

                // 不断将网络数据写入本地
                while ((len = inputStream.read(bArray)) != -1){
    
    
                    Log.e(TAG, "doInBackground: 不断将网络数据写入本地");
                    if (isCanceled) {
    
    
                        return TYPE_CANCELED;
                    } else if (isPaused) {
    
    
                        return  TYPE_PAUSED;
                    } else {
    
    
                        total += len;
                        savedFile.write(bArray, 0, len);
                        Log.e(TAG, "doInBackground: total: " + total + " len: " + len
                        + " contentLength: " + contentLength);
                        // 计算已下载的百分比
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                        // 当后台计算仍在运行时,可以从doInBackground调用此方法在Ul线程上发布更新;
                        // 对该方法的每次调用都将触发UI线程上onProgressUpdate的执行;
                        // 如果任务已取消,则不会调用onProgressUpdate。
                        publishProgress(progress);
                        int tmp = 100;
                        Log.e(TAG, "doInBackground: 计算下载百分比已完成,progress:"
                                + progress + " " + tmp);
                    }
                }
                response.body().close();
                Log.e(TAG, "doInBackground: 下载已完成");
                return  TYPE_SUCCESS;
            }
        } catch (Exception e){
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                if (inputStream != null) {
    
    
                    inputStream.close();
                }
                if (savedFile != null) {
    
    
                    savedFile.close();
                }
                if(isCanceled && file != null){
    
    
                    boolean res = file.delete();
                    Log.e(TAG, "doInBackground: file.delete() is res: " + res);
                }
            } catch (Exception e){
    
    
                e.printStackTrace();
            }
        }
        return  TYPE_FAILED;
    }

    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();
            Log.e(TAG, "getContentLength: contentLength: " + contentLength);
            return contentLength;
        }
        return 0;
    }

    // 进行UI操作——更新下载进度
    @Override
    protected void onProgressUpdate(Integer... values) {
    
    
        // 更新下载速度
        int progress = values[0];
        if (progress > lastProgress){
    
    
            // 调用DownloadListener的onProgress通知下载进度的更新
            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;
        }
    }

    // 暂停下载,修改 isPaused 标记
    public void pauseDownload(){
    
    
        isPaused = true;
    }

    // 取消下载,修改 isCanceled 标记
    public void cancelDownload(){
    
    
        isCanceled = true;
    }
}

doInBackground

  • doInBackground参数 stringsAsyncTask 模板的 第一个参数,从 strings[0] 中我们可得到传入的下载资源的 url
  • 解析 url得到了 待下载文件 的 文件名,然后将文件下载到 Environment.DIRECTORY_DOWNLOADS(也就是 /storage/emulated/0/Android/data/com.example.activitytest/files/Download/<文件名>) 目录下。
  • 下载过程中用到了断点续传功能,HTTPHeader 中的 RANGE 参数就是为标识断点续传功能而存在的。而 RandomAccessFile 类型的一个重要使用场景就是网络请求中的多线程下载及断点续传。
    关于RandomAccessFile详见本文
    关于HTTP断点续传详见本文
  • 通过文件流不断从网络读取数据写到本地,在此期间还需判断用户有无触发暂停或取消操作。

onProgressUpdate

和上一次下载进度相比,有变化则回调 DownloadListener.onProgress() 方法 通知下载进度更新。

onPostExecute

根据 AsyncTask 模板的第三个参数 Integer 对应的状态参数来进行回调。


服务:DownloadService

DownloadService 保证 DownloadTask 能够一直在后台运行。

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

    private DownloadTask downloadTask;
    private String downloadUrl;

	// 匿名类实例
    private final 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_LONG).show();
        }

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

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

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

    // 和活动通信
    private final DownloadBinder mBinder = new DownloadBinder();

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

    public class DownloadBinder extends Binder {
    
    

        public void startDownload(String url){
    
    
            if (downloadTask == null) {
    
    
                downloadUrl = url;
                downloadTask = new DownloadTask(listener, DownloadService.this);
                downloadTask.execute(downloadUrl); // execute通过url开启下载
                Log.e(TAG, "startDownload: downloadTask 已执行");
                // 前台显示
                startForeground(1, getNotification("Downloading...", 0));
                Toast.makeText(DownloadService.this, "Downloading...",
                        Toast.LENGTH_LONG).show();
                Log.e(TAG, "startDownload: 通知已显示");
            }
            Log.e(TAG, "startDownload: over");
        }

        public void pauseDownload(){
    
    
            if(downloadTask != null){
    
    
                downloadTask.pauseDownload();
            }
            Log.e(TAG, "getProgress: pauseDownload over");
        }

        public void cancelDownload(){
    
    
            if(downloadTask != null){
    
    
                downloadTask.cancelDownload();
            }
            else {
    
    
                if (downloadUrl != null) {
    
    
                    // 取消下载时需删除文件
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = DownloadService.this.getExternalFilesDir
                            (Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory + fileName);
                    if (file.exists()){
    
    
                        boolean res = file.delete();
                        Log.e(TAG, "cancelDownload: file.delete() is res: " + res);
                    }
                    // 并关闭通知
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "Canceled",
                            Toast.LENGTH_LONG).show();
                }
            }
            Log.e(TAG, "cancelDownload: cancelDownload over");
        }
    }

    private NotificationManager getNotificationManager() {
    
    
        Log.e(TAG, "getNotificationManager: 生成通知管理器已完成");
        return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }

    private Notification getNotification(String title, int progress) {
    
    
        String id = "1"; // NotificationCompat.Builder 和 NotificationChannel 的 id 参数
        if(Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
    
    
            String name = getString(R.string.app_name);

            NotificationChannel notificationChannel = new NotificationChannel(id, name,
                    NotificationManager.IMPORTANCE_HIGH);
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.setShowBadge(true);
            notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
            // 通知更新时声音关掉,避免每次更新进度都会弹出提示音
            notificationChannel.setSound(null, null);

            getNotificationManager().createNotificationChannel(notificationChannel);
        }
        Intent intent = new Intent(this, ServiceActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, id);
        builder.setSmallIcon(R.mipmap.download);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.download));
        builder.setContentIntent(pi);
        builder.setContentTitle(title);

        if (progress > 0) {
    
     // >0时才有显示下载进度的需求
            builder.setContentText(progress + "%");
            // 第三个参数表述是否适用模糊进度条
            builder.setProgress(100, progress, false);
            Log.e(TAG, "getNotification: 显示下载进度");
        }
        Log.e(TAG, "getNotification: 生成通知已完成");
        return builder.build();
    }
}
  • 实现了下载过程的回调接口 DownloadTask 的匿名类实例
  • 通过 DownloadBinderDownloadService活动 通信,活动中通过点击按钮来调用这里的函数(startDownload()pauseDownload()cancelDownload())。其实例 mBinder 通过 onBind() 方法返回,onBind() 方法 在 bindService() 方法 调用后被回调。
  • 安卓 8.0 版本以上使用 Notification 时要添加 NotificationChannel
  • NotificationCompat.Builder.setProgress(100, progress, false); 第一个参数:传入通知的最大进度;第二个参数:传入通知的当前进度;第三个参数:是否使用模糊进度条。

不使用模糊进度条:
在这里插入图片描述
使用模糊进度条:
在这里插入图片描述


活动:ServiceActivity

public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {
    
    
    private static final String TAG = "ServiceActivity";

    private DownloadService.DownloadBinder downloadBinder;

	// 作为 bindService 的第二个参数
    private final 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.service_layout);

        Button button_start_download = findViewById(R.id.button_start_download);
        button_start_download.setOnClickListener(this);

        Button button_pause_download = findViewById(R.id.button_pause_download);
        button_pause_download.setOnClickListener(this);

        Button button_cancel_download = findViewById(R.id.button_cancel_download);
        button_cancel_download.setOnClickListener(this);

        Intent intent = new Intent(this, DownloadService.class);
        startService(intent); // 启动服务
        bindService(intent, connection, BIND_AUTO_CREATE); // 绑定服务
        if (ContextCompat.checkSelfPermission(ServiceActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
    
    
            ActivityCompat.requestPermissions(ServiceActivity.this, new String[]
                    {
    
     Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
        }
        else {
    
    
            Log.e(TAG, "拥有权限,无需授权");
        }
    }

    @Override
    public void onClick(View v) {
    
    
        if (downloadBinder == null) {
    
    
            return;
        }
        switch (v.getId()){
    
    
            case R.id.button_start_download:
                Log.e(TAG, "主线程 id is " + Thread.currentThread().getId());
                String url = "https://dl.hdslb.com/mobile/latest/android64/iBiliPlayer-bili.apk?t=1647227157000";
                downloadBinder.startDownload(url);
                break;
            case R.id.button_pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.button_cancel_download:
                downloadBinder.cancelDownload();
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    
    
        switch (requestCode){
    
    
            case 1:
                if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){
    
    
                    Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_LONG)
                            .show();
                    finish();
                }
                Log.e(TAG, "已完成申请授权");
                break;
        }
    }

    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        unbindService(connection); // 解绑服务,防止内存泄漏
    }
}
  • 在绑定成功时在 ServiceConnection.onServiceConnected() 方法中生成 DownloadService.DownloadBinder 的实例,以便于活动和服务之间进行通信。
  • 启动服务保证 DownloadTask 能够一直在后台运行,绑定服务让 ServiceActivityDownloadTask 能够进行通信。
  • 活动销毁时注意解绑服务,以避免内存泄漏。

AndroidManifest.xml 权限声明

在这里插入图片描述

  • WRITE_EXTERNAL_STORAGE: 允许写入外部存储目录。
  • INTERNET: 网络访问权限。
  • FOREGROUND_SERVICE: 前台服务权限。

猜你喜欢

转载自blog.csdn.net/Jormungand_V/article/details/123356783