适配Android技术知识Android开发经验谈

异步网络下载案例(AsyncTask + 前台Service +

2019-11-19  本文已影响0人  凌川江雪

ServiceBestPractice项目(模块)GitHub地址

案例代码逻辑概述

  • interface DownloadListener 回调机制核心接口

  • class DownloadTask extends AsyncTask<String, Integer, Integer>
    描述异步网络下载逻辑(网络请求,文件线上状态处理,文件本地状态处理,文件写入本地),
    抽象调用接口对象方法;

  • class DownloadService extends Service

    • 匿名类方式具体实现回调接口的方法 而后将这个匿名类放入一个接口类实例
      (回调方法负责状态处理,方式是:Toast、对downloadTask归为、开关通知等);(Binder类定义中)
    • 实例化DownloadTask,把实现好的接口类实例传进去DownloadTask的构造器;
      为外部(如Activity)调用准备好业务Binder实例class DownloadBinder extends Binder
    • 封装NotificationManager以及NotificationgetNotificationManager() getNotification(String title, int progress)
    • Binder类定义中执行DownloadTask实例downloadTask.execute(downloadUrl);
      并完成开关通知、删除文件逻辑;
getNotificationManager();// 配置 NotificationManager!!!!!!!!
startForeground(1, getNotification("Downloading...", 0));
------------------------------------
file.delete();
...
getNotificationManager().cancel(1);
stopForeground(true);
  • class MainActivity extends AppCompatActivity implements View.OnClickListener
    • 实例化UI(主要是按钮);
    • 启动、绑定、解绑服务;startForegroundService(intent); startService(intent); unbindService(connection);
    • 运行时权限动态申请;
    • 准备监听事件,事件中通过Servicebinder对象来产生业务;



开始实战

implementation 'com.squareup.okhttp3:okhttp:4.2.2'
public interface DownloadListener {
    void onProgress(int progress);//通知当前下载进度
    void onSuccess();//通知下载成功事件
    void onFailed();//通知下载失败事件
    void onPaused();//通知下载暂停事件
    void onCanceled();//通知下载取消事件
}
/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/11/9 17:29
 *     desc   :三个泛型参数,
 *     第一个表示在执行AsyncTask时需传入一个字符串参数给后台任务,
 *     第二个使用整型数据最为进度显示单位,
 *     第三个表示使用整型数据来反馈结果执行
 * </pre>
 */
public class DownloadTask extends AsyncTask<String, Integer, Integer> {

    //定义四个整型常量分别表示下载的不同状态
    public static final int TYPE_SUCCESS = 0;//表示下载取消
    public static final int TYPE_FAILED = 1;//表示下载失败
    public static final int TYPE_PAUSE = 2;//表示下载暂停
    public static final int TYPE_CANCELED = 3;//表示下载取消

    private DownloadListener listener;

    //取消位以及暂停位
    // 由外部调用,在doInBackground()中生效
    private boolean isCanceled = false;
    private boolean isPaused = false;

    private int lastProgress;//记录上次的进度

    //构造方法
    public DownloadTask(DownloadListener listener){
        //将下载的状态通过此参数进行回调,此处负责调用,外部具体编写逻辑
        this.listener = listener;
    }

    //在后台执行具体的下载逻辑
    // String... params:可变长参数列表,必须是String类型,转化为数组处理
    @Override
    protected Integer doInBackground(String... params) {

        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;

        try{

            long downloadedLength = 0;//记录 已下载的文件 长度!!!!!!!

            String downloadUrl = params[0];//获取 下载的URL地址!!!!!!!!!

            // 根据URL地址解析出下载的文件名
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            // 指定文件下载到 Environment.DIRECTORY_DOWNLOADS 目录下,即SD卡的Download目录
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            //用以上的 文件下载路径 以及 要下载的文件名 得到 file句柄!!!!!!!!!!!!
            file = new File(directory + fileName);


            //判断是否已存在要下载的文件,
            // 存在则 读取 已下载的字节数(以 启用 断点续传 功能)
            if (file.exists()){
                downloadedLength = file.length();
            }


            //获取 待下载文件 的总长度!!!!!!
            // 判断 文件情况—— 有问题 或者 已下载完毕!!!!!
            long contentLength = getContentLength(downloadUrl);
            if (contentLength == 0){//总长度为0,说明文件有问题
                return TYPE_FAILED;

            }else if (contentLength == downloadedLength){//已下载字节和文件总字节相等,说明已经下载完成了
                return TYPE_SUCCESS;

            }


            //注意这里,断点续传 功能!!!!!!!!!!
            //使用.addHeader 往请求中添加一个Header,用于告诉服务器我们想要
            // 从哪个字节开始下载(已下载部分不需再重新下载)
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();//得到服务器响应的数据

            //使用 Java文件流方式 不断从网络上 读取数据!!
            // 不断写入到本地,
            // 直到文件全部下载完为止!!
            if (response != null){

                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");//封装本地文件句柄
                savedFile.seek(downloadedLength);//跳过已下载的字节


                byte[] b = new byte[1024];
                int total = 0; //本轮!!!下载的总长度!!
                int len;

                //使用 Java文件流方式 不断从网络上 读取数据!!
                // 不断写入到本地,直到文件全部下载完为止!!
                while ((len = is.read(b)) != -1){

                    //判断用户有没触发暂停或取消操作,如果有则返回相应值来中断下载
                    if (isCanceled){
                        return TYPE_CANCELED;

                    }else if (isPaused){
                        return TYPE_PAUSE;


                    }else {

                        //用户没有触发暂停或取消操作,继续下载
                        total += len;
                        savedFile.write(b, 0, len);

                        //计算已下载的百分比 == (本轮下载的长度 + 已经下载的长度)/ 要下载的 文件总长度
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);

                        publishProgress(progress);//抛出进度给 onProgressUpdate(),回调之!!!!
                    }
                }

                //执行到此,说明以上循环已执行完毕,文件下载完毕
                response.body().close();

                return TYPE_SUCCESS;

            }
        } catch (Exception e) {
            e.printStackTrace();

        }finally {

            //分开关闭资源!!!!!!
            try {

                if (is != null){
                    is.close();
                }

                if (savedFile != null){
                    savedFile.close();
                }

                if (isCanceled && file != null){
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //不从上面成功退出则执行至此,证明失败!!!
        return TYPE_FAILED;
    }

    /**
     * 在界面更新当前的下载进度
     *
     * doInBackground()的每一次!!!while 读 输入流 ,
     * 写入file,都会publishProgress(progress); 抛出进度
     * 此时就会回调此方法!!! 对进度进行处理!!!
     *
     * @param values
     */
    @Override
    protected void onProgressUpdate(Integer... values) {

        //获取当前下载进度,
        // 参数来自 doInBackground()中 publishProgress()抛出的进度
        int progress  = values[0];

        if (progress > lastProgress){//与上一次下载进度对比

            listener.onProgress(progress);//有变化则调用DownloadListener的onProgress()通知下载进度更新

            lastProgress = progress;//更新记录
        }
    }

    /**
     *  通知最终的下载结果
     *
     * 当任务执行完了,即doInBackground()一旦return,
     * 其return的值就会传到这里,作为参数,
     * 参数类型即定义泛型时的第三个参数
     *
     * 这里用了回调机制,listener负责抽象调用!!!
     * 外部负责具体实现!!!
     */
    @Override
    protected void onPostExecute(Integer status) {
        switch (status){//根据传入的下载状态进行回调
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;

            case TYPE_FAILED:
                listener.onFailed();
                break;

            case TYPE_PAUSE:
                listener.onPaused();
                break;

            case TYPE_CANCELED:
                listener.onCanceled();
                break;

            default:
                break;
        }
    }

    //取消位以及暂停位
    // 由外部调用,在doInBackground()中生效
    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;
    }

}

普及:关于RandomAccessFile

Java除了File类之外,还提供了专门处理文件的类,
即RandomAccessFile(随机访问文件)类。
该类是Java语言中功能最为丰富的文件访问类,
它提供了众多的文件访问方法。
RandomAccessFile类支持“随机访问”方式,
这里“随机”是指可以跳转到文件的任意位置处读写数据。
在访问一个文件的时候,不必把文件从头读到尾,
而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,
这时使用RandomAccessFile类就是最佳选择。

RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,
当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处
刚打开文件时,文件指示器指向文件的开头处,
可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。
RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,
但该类仅限于操作文件,
不能访问其他的I/O设备,如网络、内存映像等;

public class DownloadService extends Service {

    private DownloadTask downloadTask;
    private String downloadUrl;

    private String notificationId = "nyd001";
    private String notificationName = "downloadTask";

    /**
     * 创建DownloadListener 匿名内部类实例,
     * 然后赋值给其父类类型DownloadListener引用
     *
     * 这里实现的方法!!
     * 直接在DownloadTask 的 onPostExecute()中被调用
     *
     * 而onPostExecute() 中要调用那个回调方法
     *
     * 则由doInBackground() 的返回值位决定
     *
     * 而doInBackground() 的返回值 中
     * 成功位 和 失败位 是 客观判断的结果
     * 暂停位 和 取消位 可以 由人为点击置位
     */
    private DownloadListener listener = new DownloadListener() {

        /**
         * 在 DownloadTask 中的 onProgressUpdate()处调用
         * @param progress 来自对应的DownloadTask 的 doInBackground() 中的 publishProgress(progress);
         */
        @Override
        public void onProgress(int progress) {
            //getNotification()是自定义的封装方法,
            // 其中构造了一个用于显示下载进度的通知,
            //调用NotificationManager的 notify() 去触发这个通知,
            // 这样就可以在下拉状态栏中实时看到当前的下载进度了
            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, "Paused", Toast.LENGTH_SHORT).show();

        }

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

        }
    };


    /**
     * 创建DownloadBinder内部类,
     * 把需要放给外部调用的Service服务方法写好,
     * 实例化一个DownloadBinder内部类示例,在onBind()中返回,
     * 这样,
     * 当外部界面与本Service绑定,
     * 就可以在 ServiceConnection实例 的 onServiceConnected 回调方法中,
     * 获得这个 具备了 各种准备好的业务方法的 DownloadBinder(Binder、IBinder)实例了
     *
     */
    private DownloadBinder mBinder = new DownloadBinder();
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    //创建DownloadBinder内部类,
    //把需要放给外部调用的Service服务方法写好
    class DownloadBinder extends Binder {

        /**
         * 开启下载任务
         * @param url 要下载的资源地址
         */
        public void startDownload(String url){

            if (downloadTask == null){

                downloadUrl = url;

                //创建DownloadTask实例
                downloadTask = new DownloadTask(listener);

                //传入下载地址,启动下载任务!!!!
                downloadTask.execute(downloadUrl);

                //让这个下载任务服务成为一个前台服务!!!
                // 使用时在Activity处 先 startService(intent);  启动! 本服务DownloadService
                //
                // 然后 绑定本服务 bindService(intent, connection, BIND_AUTO_CREATE);!!!!
                // 再调用本方法 downloadBinder【即这里的mBinder】.startDownload(url);
                // 运行到下面的startForeground()!!
                // 从而使刚刚已经启动(start)的服务变成前台服务!!!!!
                //这样就会在 系统状态栏 中 创建一个持续运行的通知了
                // .
                // 注意这里有个id!!! 后续取消时 可以用!!

                getNotificationManager();// 配置 NotificationManager!!!!!!!!
                startForeground(1, getNotification("Downloading...", 0));
                //!!!!!!!!!!!

                Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
            }
        }
        public void pauseDownload(){
            if (downloadTask != null){
                //使下载任务downloadTask 的 暂停位 置位
                downloadTask.pauseDownload();
            }
        }
        public void cancelDownload(){
            if (downloadTask != null){

                //首先,使下载任务downloadTask 的 取消位 置位,终止下载!!!!
                downloadTask.cancelDownload();
                //调用流程:
                // downloadTask.cancelDownload();
                // --> isCanceled = true;   取消位 置位
                // .
                // -->downloadTask 的 doInBackground 中 取消位 置位生效
                // doInBackground() 中的 下载文件的while循环中
                // if (isCanceled){ return TYPE_CANCELED;} 返回取消位 并终止下载!!!
                // .
                // -->onPostExecute() 接收到 doInBackground()返回的取消位
                // (只要onPostExecute() 接收到了取消位, 便已经终止下载了!! 这时候回调接口...)
                // .
                // --> listener.onCanceled(); 回调 接口的 取消方法 ,
                // 即这里 DownloadService 实现的方法, 接着进行下一步操作...
                // .
                // --> downloadTask  = null;

            }else {
                //如果 downloadTask  = null; 则 执行到此

                //纵观 接口处几个方法 无论成功、失败、暂停、取消
                // 都会执行 downloadTask  = null;
                // .
                // 也就是说 只要 downloadTask 调用过 一次 接口方法!!!!
                // 之后再调用  downloadBinder.cancelDownload(); 的话,
                // 都会已 downloadTask  = null;
                // 即 会执行至此, 删除文件,关闭通知 !!!

                if (downloadUrl != null){

                    //取消下载时需将文件删除,并将通知关闭

                    //获取file 的过程 同DownloadTask 的 doInBackground()
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//得到文件名
                    String directroy = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directroy + fileName);
                    if (file.exists()){
                        file.delete();
                    }

                    //取消对应id 前台通知或者服务
                    getNotificationManager().cancel(1);
                    stopForeground(true);

                    Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }


    //封装 NotificationManager
    private NotificationManager getNotificationManager(){
        NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(notificationId, notificationName, NotificationManager.IMPORTANCE_HIGH);
            notificationManager.createNotificationChannel(channel);

            return notificationManager;
        } else {
            return notificationManager;
        }

    }

    /**
     * 封装进度条通知
     * 返回一个封装配置好的 Notification
     *
     * Notification
     * 遇 startForeground() 则成前台服务!!!
     * 遇 NotificationManager.notify() 则成通知!!!
     */
    private Notification getNotification(String title, int progress){

        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);

        //拿着Notification 的 建造者Builder, 去各种配置(set()),
        // 配置完毕了,调用builder.build(),返回 一个 Notification !!!
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationId);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        builder.setContentIntent(pi);
        builder.setContentTitle(title);

//        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//            builder.setChannelId(notificationId);
//        }

        if (progress > 0){
            //当progress大于或等于0时才需显示下载进度
            builder.setContentText(progress + "%");
            builder.setProgress(100, progress, false);//三个参数:通知的最大进度,通知的当前进度,是否使用模糊进度条
        }

        return builder.build();
    }
}

.

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

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

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

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

</LinearLayout>

修改MainActivity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private DownloadService.DownloadBinder downloadBinder;

    //创建了一个ServiceConnection 的 匿名内部类,
    // 重写方法后 赋值给ServiceConnection 实例
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

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

        initViews();
    }

    private void initViews() {
        //初始化 UI 按钮
        Button startDownload = (Button) findViewById(R.id.start_download);
        Button pauseDownload = (Button) findViewById(R.id.pause_download);
        Button cancelDownload = (Button) findViewById(R.id.cancel_download);
        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);

        //启动服务 以及 绑定服务 二者在这里 缺一不可
        Intent intent =new Intent(this, DownloadService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(intent);//启动服务,保证服务一直在后台运行!!!
        } else {
            startService(intent);
        }
        bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务,让MainActivity和服务进行通信!!!

        //运行时权限申请
        if (ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){

            ActivityCompat.requestPermissions(MainActivity.this, new String[]{
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,}, 1);
        }
    }

    @Override
    public void onClick(View v) {
        if (downloadBinder == null){
            return;
        }
        switch (v.getId()){
            case R.id.start_download:
                String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;

            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;

            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;

            default:
                break;
        }

    }

    //运行时权限申请结果
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:

                for (int grantResult : grantResults) {
                    if (grantResult != PackageManager.PERMISSION_GRANTED) {
                        Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
                        finish();
                    }
                }

                break;
            default:
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(connection);//解绑服务,避免内存泄漏
    }
}

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
注意Android 8.0 之后,开启前台服务需要关注一下几点!!!
  • 开启服务需要用startForegroundService(intent),
    不能用startService(intent);
    且调用完startForegroundService(intent)之后,
    五秒内需要调用startForeground()!!!
    否则app可能会ANR!

    实战如上,
    MainActivityinitViews()里边的startForegroundService(intent)

  • 需要为 NotificationManager 配置 NotificationChannel
    DownloadService里面的getNotificationManager()

  • 需要为 Notification 设置 channelId
    DownloadService里面的的getNotification()

  • 需要静态声明权限 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

参考文章:

运行测试:







参考自《第一行代码》

上一篇下一篇

猜你喜欢

热点阅读