安卓多线程下载精灵应用开发

安卓多线程下载精灵应用开发

使用 Android Studio 开发多线程下载精灵

应用功能

本文将讨论安卓中的多线程开发和异步处理的相关知识,并开发一个安卓多线程下载精灵,实现多线程下载功能,并支持断点续传。

技术原理

多线程

一个功能复杂的程序,大多是使用了多线程的。让多个线程并行,可以充分调用处理器资源,让程序的表现更为流畅。今天我们要研究的是
Android 开发中多线程的使用。

1170391c67505a75859a891fba009924.png

主线程

  • 应用启动,系统会创建一个主线程(main thread);

  • 这个主线程负责向UI组件分发事件,主线程里应用和 Android 的 UI 组件发生交互

  • 主线程也称为 UI 线程

  • UI
    线程做一些比较耗时的工作比如访问网络或者数据库查询,都会阻塞UI线程,导致事件停止分发(包括绘制事件)

  • 主线程有两个原则:不要阻塞 UI 线程;不要在 UI 线程外访问 UI 组件

异步处理

使用多线程绕不开异步处理的编程模式,在安卓开发中,多线程和异步处理通常结合使用,以实现复杂的并发操作。

异步处理概念

举一个例子简单理解一下异步处理,假设有一个程序开始有 A线程 和 B线程
在运行,某一个时刻 A线程 需要一个资源 R,于是他发出了请求。但是不巧,B线程
正在使用 R,这时如果:

  • A 老老实实等待 B 先用完,再去使用 R,那么这种方式就是同步处理。

  • A
    不做等待,而是继续干自己的事儿,等到他收到消息:“你刚才请求的资源现在空闲啦”,他就去处理要用到资源
    R 的工作,那么这种方式就是异步处理。

异步处理优势

在安卓中使用异步处理有很多好处:

  • 提供更好的用户体验:通过在后台执行耗时操作,保持 UI
    线程的响应性,用户可以继续与应用交互而不会感到卡顿或无响应的情况。

  • 避免 ANR 错误:在 Android 应用中,如果在 UI
    线程中执行耗时操作超过一定时间限制,会导致
    ANR错误。使用异步处理可以避免这种情况的发生。

  • 提高应用性能:通过将耗时操作放入后台线程中执行,可以减少 UI
    线程的负载,从而提高应用的性能和响应速度。

异步处理最佳实践

在使用异步处理时,还需要注意以下最佳实践:

  1. 根据需求选择合适的异步处理方式:根据具体的需求和场景,选择合适的异步处理方式。例如,对于简单的异步操作,可以使用Handler和Looper;对于复杂的异步操作,可以考虑使用AsyncTask、RxJava或Kotlin
    Coroutines等。

  2. 处理异常:在异步处理中,可能会出现异常情况,例如网络请求失败、文件读写错误等。因此,在异步处理中需要合理地处理异常情况,例如进行错误处理、显示错误提示等,以提高应用的稳定性和用户体验。

  3. 取消异步任务:在某些情况下,可能需要取消正在进行的异步任务,例如用户取消了操作或者应用被销毁了。因此,需要提供取消异步任务的机制,例如在AsyncTask中可以使用cancel()方法取消任务,在RxJava中可以使用Disposable进行取消操作,在Kotlin
    Coroutines中可以通过取消协程来取消异步操作。

多线程的几种实现方式

  1. Thread:Thread 是 Java 中的线程类,也可以在 Android 应用中使用。通过创建
    Thread 对象并重写 run() 方法,可以在新线程中执行耗时操作,从而避免在 UI
    线程中阻塞。

  2. HandlerThread:HandlerThread 是 Android 中的一个特殊线程类,继承自
    Thread,并内置了一个 Looper 和
    Handler。它可以方便地处理线程间的消息传递和处理,从而实现线程间的通信和协作。

  3. AsyncTask:AsyncTask 是 Android
    提供的一个简单的异步任务类,用于在后台执行耗时操作,并在操作完成后将结果返回到
    UI 线程。AsyncTask 内部使用了 Handler 和 Thread 来实现异步操作和 UI
    线程的通信。

  4. IntentService: 是 Service
    的一种特殊形式,专门用于处理异步请求。IntentService
    会创建一个工作线程来处理所有通过其 onHandleIntent 方法传递的
    Intent,一旦所有的请求都被处理完毕,IntentService
    会自动停止自己,不需要手动调用 stopSelf()或
    stopService()。稍后将进行详细介绍。

多线程的最佳实践

在进行多线程编程时,还需要注意以下最佳实践:

  1. 避免内存泄漏:在使用多线程时,需要注意内存泄漏的问题。例如,持有对Activity或Fragment的引用的线程可能导致内存泄漏。可以使用弱引用(WeakReference)来避免这种情况发生。

  2. 线程间通信:多线程之间需要进行通信和协作,例如,从后台线程向UI线程更新UI状态。可以使用Handler、Messenger、BroadcastReceiver等方式来实现线程间的通信。

  3. 线程同步:多线程访问共享资源时需要进行线程同步,以避免竞态条件(Race
    Condition)和其他线程安全问题。可以使用synchronized关键字、Lock和Condition等方式来进行线程同步。

服务(Service)

服务是安卓赖以实现多线程处理的一个重要组件,下面将进行一些必要的介绍。

概念

  • 服务(Service)是安卓中一种可以在后台长时间运行的组件,用于执行一些不需要用户界面的任务。服务不依赖于任何用户界面,即使应用程序被切换到后台或者用户打开了其他应用程序,服务仍然可以保持正常运行。服务通常不是运行在一个独立的进程中,而是依赖于创建服务时所在的应用程序进程。当应用程序进程被杀掉时,依赖于该进程的服务也会停止运行。

  • 服务并不会自动开启线程,所有的代码默认都是运行在主线程中的。因此,为了避免主线程被阻塞,需要在服务的内部手动创建子线程,并在其中执行具体的任务。这样可以确保长时间运行的任务不会影响到主线程,保持应用程序的响应性和流畅性。

服务的生命周期

服务的生命周期如下:

  1. 创建服务:onCreate() 第一次创建服务时调用,Service
    执行内容建议写于onCreate()方法中,该方法需自行重写。

  2. 启动服务:onStartCommand() 调用startService()方法启动服务时执行的方法。

  3. 绑定服务:通过 bindService() 方法绑定服务时,如果服务尚未创建,则先调用
    onCreate() 方法,然后调用 onBind() 方法返回一个 IBinder
    对象。服务会一直保持运行,直到所有客户端解除绑定。

  4. 解除绑定服务:当所有客户端解除绑定时,调用 onUnbind() 方法。如果此时没有通过
    startService() 启动服务,服务将会销毁,调用 onDestroy() 方法。

总的来说,服务的生命周期取决于服务的启动方式(startService())和绑定方式(bindService()),以及客户端与服务之间的交互状态。

后台服务

  • 后台服务是在用户不直接与之交互的情况下运行的服务。这些服务在后台默默地执行任务,而无需用户的干预。

  • 后台服务通常用于执行一些不需要用户干预的长时间运行任务,例如数据同步、定期备份等。它们不会在用户界面上显示任何通知。

前台服务

  • 在Android中,将Service设置为前台Service可以提高其优先级,确保系统不容易终止该服务。前台Service通常用于执行与用户直接交互或对用户可见的任务,例如播放音乐、进行下载等。前台服务简单来说就是为服务添加一个通知。

  • 前台服务需要静态申请权限。

  • <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

  • 前台服务是指用户可以看到并与之交互的服务。通常,前台服务与正在用户界面上显示的活动相关联,因此它们对用户来说是可见的。

  • 前台服务通常用于执行用户正在直接与之交互的任务,例如播放音乐、下载文件等。因为它们对用户可见,所以它们需要显示一个持续的通知,以告知用户服务正在运行。

服务间通信

通过调用 bindService()
方法开启服务后,服务与绑定服务的组件是可以通信的,通过组件可以控制服务并进行一些操作。

在 Android 中,服务的通信方式有两种,一种是 本地服务通信,另一种是
远程服务通信。本地服务通信是指应用程序内部的通信,远程服务通信是指两个应用程序之间的通信。使用这两种方式进行通信时必须保证服务以绑定方式开启,否则无法进行通信和数据交换。

本地服务通信

在使用服务进行本地通信时,首先需要创建一个 Service类,该类会提供一个
onBind()方法,onBind()方法的返回值是一个
IBinder对象,IBinder对象会作为参数被传递给 ServiceConnection 类中的
onServiceConnected(ComponentName name,IBinder
service)方法
,这样访问者(绑定服务的组件)就可以通过 IBinder 对象与 Service
进行通信。

远程服务通信

在 Android
中,各个应用程序都运行在自己的进程中,如果想要完成不同进程之间的通信,就需要使用远程服务通信。远程服务通信是通过
AIDL (Android.Interface DefinitionLanguage)
实现的,它是一种接口定义语言(Interface Definition
Language),其语法格式非常简单,与 Java
中定义接口类似,但是存在一些差异,具体介绍如下:

  • AIDL 定义接口的源代码必须以 .aidl 结尾。

  • AIDL 接口中用到的数据类型,除了基本数据类型 String List Map Charsequence
    之外,其它类型全部都需要导入包,即使它们在同一个包中

IntentService

刚才说到,安卓实现多线程异步任务处理可以用到 IntentService
的方式,接下来我们简要介绍一下 IntentService。

默认的 Service
是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的
AsyncTask 与 HandlerThread,我们还可以选择使用 IntentService
来实现异步操作。IntentService 继承自普通 Service 同时又在内部创建了一个
HandlerThread,在 onHandlerIntent()的回调里面处理扔到 IntentService
的任务,在执行完任务后会自动停止。所以 IntentService
就不仅仅具备了异步线程的特性,还同时保留了 Service
不受主页面生命周期影响,优先级比较高,适合执行高优先级的后台任务,不容易被杀死的特点。

IntentService对比普通Service:

ElementServiceIntentService
处理多个请求的方式可以处理多个不同的请求,并且可以同时执行多个任务只能处理一个请求,但可以按顺序依次处理多个请求
是否自动停止服务默认情况下不会自动停止,必须显式调用 stopSelf() 或 stopService() 方法来停止服务在处理完所有请求后会自动停止,无需调用 stopSelf() 或 stopService() 方法
线程安全性默认是在主线程中运行的,因此需要在 Service 中手动创建线程来执行长时间运行的任务,以避免阻塞主线程在 onCreate() 方法中创建了一个工作线程,并在工作线程中处理请求,无需手动创建线程,并且更容易保证线程安全

因此,如果需要处理多个并发请求或需要更精细地控制服务的生命周期,则应使用
Service。如果只需要处理一个请求并且希望在完成任务后自动停止服务,则应使用
IntentService。

开发步骤

Download Service类


public class DownloadService extends Service {
             public class DownloadBinder extends Binder {
              /*
              * 向activity提供下载服务的控制方法
               */
                    private String TAG = "DownloadBinder";
                    public DownloadService getService() {
                        return DownloadService.this;
                    }
                    /*
                    * 如果没有下载任务或上个任务已经结束则新建DownloadTask
                    * 如果任务还未开始则开始任务,调用execute()
                    * 如果任务还未完成则继续任务,调用resume()
                    */
                    public void start() {
                        Log.d(TAG, "start: ");
                        if (downloadTask == null) {
                            downloadTask = new DownloadTask(getService());
                        }
                        switch (downloadTask.getStatus()) {
                            case FINISHED: downloadTask = new DownloadTask(getService());
                            case PENDING: downloadTask.execute(); break;
                            case RUNNING:
                                Toast.makeText(getService(), "下载继续", Toast.LENGTH_LONG).show();
                                downloadTask.resume();
                        }           

                }
                /*
                * 调用DownloadTask内部提供的暂停方法
                */
                public void pause() {
                    Log.d(TAG, "pause: ");
                    Toast.makeText(getService(), "下载暂停", Toast.LENGTH_LONG).show();
                    downloadTask.pause();
                }
                /*
                 * 调用DownloadTask内部提供的取消方法
                 */
                public void cancel() {
                    Log.d(TAG, "cancel: ");
                    Toast.makeText(getService(), "下载取消", Toast.LENGTH_LONG).show();
                    downloadTask.cancel();
                }
            }           

            private String TAG = "DownloadService";
            private DownloadBinder downloadBinder;
            private DownloadTask downloadTask;
            @Override
            public void onCreate() {
                super.onCreate();
                Log.i(TAG, "onCreate:下载服务初始化");
                downloadBinder = new DownloadBinder();
            }           

            @Override
            public int onStartCommand(Intent intent, int flags, int startId) {
                Log.i(TAG, "onStartCommand: ");
                return super.onStartCommand(intent, flags, startId);
            }           

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

            @Nullable
            @Override
            public IBinder onBind(Intent intent) {
                Log.i(TAG, "onBind: ");
                return downloadBinder;
            }
        }

前端界面设计

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".DownloadSpirit">

    <Button
        android:id="@+id/btn_pause"
        android:layout_width="115dp"
        android:layout_height="65dp"
        android:layout_marginStart="38dp"
        android:layout_marginTop="36dp"
        android:layout_marginEnd="30dp"
        android:background="@drawable/round"
        android:text="@string/pulse"
        app:layout_constraintEnd_toStartOf="@+id/btn_cancel"
        app:layout_constraintHorizontal_bias="0.515"
        app:layout_constraintStart_toEndOf="@+id/btn_start"
        app:layout_constraintTop_toBottomOf="@+id/btn_startDownload" />

    <Button
        android:id="@+id/btn_start"
        android:layout_width="115dp"
        android:layout_height="65dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="476dp"
        android:background="@drawable/round"
        android:text="@string/start"
        app:layout_constraintEnd_toStartOf="@+id/btn_pause"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/pgb"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:layout_marginTop="36dp"
        android:indeterminateDrawable="@android:drawable/progress_indeterminate_horizontal"
        android:progressDrawable="@android:drawable/progress_horizontal"
        app:layout_constraintTop_toBottomOf="@+id/txt_progress"
        tools:layout_editor_absoluteX="-1dp" />

    <Button
        android:id="@+id/btn_startDownload"
        android:layout_width="115dp"
        android:layout_height="65dp"
        android:layout_marginStart="160dp"
        android:layout_marginTop="376dp"
        android:layout_marginEnd="157dp"
        android:background="@drawable/round"
        android:text="@string/startDownload"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.533"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txt_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="174dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="177dp"
        android:text="@string/progress"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.4"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_pause" />

    <Button
        android:id="@+id/btn_cancel"
        android:layout_width="115dp"
        android:layout_height="65dp"
        android:layout_marginStart="11dp"
        android:layout_marginTop="476dp"
        android:layout_marginEnd="16dp"
        android:background="@drawable/round"
        android:text="@string/cancel"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/btn_pause"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

DownloadService类继承于Service类,实现后台下载服务主线程,具有DownloadBinder和DownloadTask两个成员类:

运行结果:

参考:

https://blog.csdn.net/m0_57150356/article/details/134201112

https://blog.csdn.net/u011897062/article/details/130247279

https://blog.csdn.net/JifengZ9/article/details/128699389

https://blog.csdn.net/u010227042/article/details/108407135

Comment