Android多任务多线程断点续传下载

下载这个需求时常遇到,以前是版本更新下载单个apk包,用okhttp+service或者系统提供的DownloadManager实现即可,方便快捷,不涉及多线程多任务下载,特别是DownloadManager提供了完善的断点、状态保存、网络判断等功能,非常适合单一任务的下载情况,但遇到批量下载(类似迅雷的下载)以上的方案就略显不足了。如果全部自己来实现多任务、多线程、断点续传、暂停等功能,那工作量还是很大的,除非所开发的项目是专业下载的app,不然还是别造这个轮子了,就像我现在做的项目,批量下载只是app中一个小小的功能而已,所以我选择用第三方库。我采用的方案是Aria,也看过流利说的开源方案,但是那个库很久没维护,且使用复杂,就选择了aria。目前来看符合项目的需求,下载效果如下:
在这里插入图片描述

我采用service+notification对aria进行了封装,简化外部调用,直接看代码(项目引入请看aria文档):

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.CountDownTimer;
import android.os.IBinder;
import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.arialyy.annotations.Download;
import com.arialyy.aria.core.Aria;
import com.arialyy.aria.core.download.DownloadEntity;
import com.arialyy.aria.core.download.DownloadReceiver;
import com.arialyy.aria.core.task.DownloadTask;
import com.orhanobut.logger.Logger;

import java.io.File;
import java.util.List;

/**
 * 集成aria框架的下载service,主要功能:
 * 1、提供前台通知及任务进度通知展示
 * 2、下载任务查重
 * 3、向外部提供进度更新接口
 * <p>
 * 添加下载任务直接调用静态方法{@link #download(Context c, String u, String e)}
 * 在任务列表需要显示进度的页面bindService,通过#OnUpdateStatusListener更新数据
 * <p>
 * aria文档:https://aria.laoyuyu.me/aria_doc/start/start.html
 * Created by ly on 2021/7/19 14:09
 */
public class AriaService extends Service {

    private static final String URL = "url";
    private static final String EXTRA = "extra";
    private static final int FOREGROUND_NOTIFY_ID = 1;
    private static final int PROGRESS_NOTIFY_ID = 2;
    private NotificationUtils notificationUtils;
    private static volatile boolean isForegroundSuc;
    private boolean timerFlag;
    private OnUpdateStatusListener onUpdateStatusListener;

    /**
     * 添加下载任务
     *
     * @param url   下载链接
     * @param extra 展示列表需要保存到数据库的额外数据
     */
    public static void download(@NonNull Context context, @NonNull String url, String extra) {
        if (!TextUtils.isEmpty(url)) {
            Intent intent = new Intent(context, AriaService.class);
            intent.putExtra(URL, url);
            intent.putExtra(EXTRA, extra);

            if (!isForegroundSuc) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    //android8.0以上通过startForegroundService启动service
                    context.startForegroundService(intent);
                } else {
                    context.startService(intent);
                }
            } else {
                context.startService(intent);
            }
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Logger.d("onBind>>>");
        return new MBinder();
    }

    public class MBinder extends Binder {
        public AriaService getService() {
            return AriaService.this;
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Logger.d("onStartCommand>>>");
        //只处理start启动并且传入下载url的情况
        if (intent != null && intent.hasExtra(URL)) {
            String url = intent.getStringExtra(URL);
            String extra = intent.getStringExtra(EXTRA);

            if (!isNeedlessDownload(url)) {
                File file = FUtils.createFileIsNotExist(new File(Constants.PATH_DOWNLOAD + System.currentTimeMillis()));
                long taskId = Aria()
                        .load(url)
                        .setExtendField(extra)//自定义数据
                        .setFilePath(file.getAbsolutePath())//设置文件保存的完整路径
                        .create();//启动下载
                if (taskId > 0) {
                    //如果任务创建成功,则开启前台service,开始下载
                    startForeground();
                } else {
                    ToastUtil.showShort(R.string.task_create_fail);
                }
            }

            /*
             * 用startForegroundService启动后5s内还没有startForeground表示没有下载任务,则自动销毁service(否则O及以上的系统会anr)
             * 该操作对用户不可见(startForeground后立马stop了),代价就是创建了一个空service,好处就是外部调用便利。
             *
             * 以下情况可以移除该操作:
             * 1、不在service内做下载查重工作
             * 2、不采用aria下载(aria需要传入this,不灵活。但目前没发现其他更好的方案,无奈。、、)
             */
            if (!timerFlag) {
                timerFlag = true;
                new CountDownTimer(4500, 4500) {

                    public void onTick(long millisUntilFinished) {
                    }

                    public void onFinish() {
                        if (!isForegroundSuc) {
                            startForeground();
                            stopSelf();
                        }
                    }
                }.start();
            }
        }

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Logger.d("onCreate>>>" + this);
        Aria.get(this).getDownloadConfig()
                .setUseBlock(true)
                .setMaxTaskNum(2)
                .setConvertSpeed(false)
                .setUpdateInterval(500);

        Aria().register();

        notificationUtils = new NotificationUtils(this)
                .setNotifyId(PROGRESS_NOTIFY_ID)
                .setTitle(R.string.downloading)
                .setPendingClassName("com.xxx.DownloadTaskActivity");

        notificationUtils.getBuilder()
                .setOngoing(true)//设置通知不可被取消
                .setOnlyAlertOnce(true)
                .setTicker(getString(R.string.downloading));

    }

    private void startForeground() {
        if (!isForegroundSuc) {
            startForeground(FOREGROUND_NOTIFY_ID, notificationUtils.build());
            isForegroundSuc = true;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Logger.w("onDestroy>>>");
        isForegroundSuc = false;
        timerFlag = false;
        Aria().unRegister();
    }

    /**
     * 判断本地是否存在,避免重复添加到列表
     */
    private boolean isNeedlessDownload(String url) {
        boolean isExist = Aria().taskExists(url);
        if (isExist) {
            DownloadEntity entity = Aria().getFirstDownloadEntity(url);

            isExist = new File(entity.getFilePath()).exists();
            if (!isExist) {//文件不存在了,则移除记录
                Aria().load(entity.getId()).removeRecord();
            } else {
                Logger.w("该任务已存在:" + url);
            }
        }
        return isExist;
    }

    private void update(DownloadTask task) {
        if (onUpdateStatusListener != null)
            onUpdateStatusListener.update(task);
    }

    private void notification(DownloadTask task, boolean isCompleted) {
        if (isCompleted) {
            //全部下载完成后,重新newBuilder、用户可选择移除通知
            notificationUtils.newBuilder().setTitle(R.string.all_download_completed).setContent("");
            notificationUtils.getBuilder().setTicker(getString(R.string.all_download_completed));

        } else {
            //中间状态的通知设置为静默
            notificationUtils.getBuilder().setNotificationSilent();

            List<DownloadEntity> allTaskList = Aria().getTaskList();
            List<DownloadEntity> completedList = Aria().getAllCompleteTask();
            int taskNum = allTaskList == null ? 0 : allTaskList.size();
            int completeNum = completedList == null ? 0 : completedList.size();

            notificationUtils.setTitle(getString(R.string.download_progress) + completeNum + "/" + taskNum)
                    .setContent(getString(R.string.cur_download_task) + task.getTaskName());
        }

        notificationUtils.send();
    }

    public void setOnUpdateStatusListener(OnUpdateStatusListener onUpdateStatusListener) {
        this.onUpdateStatusListener = onUpdateStatusListener;
    }

    public interface OnUpdateStatusListener {
        void update(DownloadTask task);
    }

    public DownloadReceiver Aria() {
        return Aria.download(this);
    }

    public void cancelNotification() {
        notificationUtils.cancel(FOREGROUND_NOTIFY_ID);
        notificationUtils.cancel();
    }


    //-----------aria框架回调-------------
    @Download.onTaskPre
    public void onTaskPre(DownloadTask task) {
        update(task);
    }

    @Download.onTaskStart
    public void onTaskStart(DownloadTask task) {
        update(task);
        notification(task, false);
    }

    @Download.onTaskStop
    public void onTaskStop(DownloadTask task) {
        update(task);
    }

    @Download.onTaskResume
    public void onTaskResume(DownloadTask task) {
        update(task);
        notification(task, false);
    }

    @Download.onTaskCancel
    public void onTaskCancel(DownloadTask task) {
        update(task);
    }

    @Download.onTaskFail
    public void onTaskFail(DownloadTask task) {
        update(task);
    }

    @Download.onTaskComplete
    public void onTaskComplete(DownloadTask task) {
        Logger.i("onTaskComplete>>>>" + task.getTaskName());
        update(task);

        List<DownloadEntity> list = Aria().getAllNotCompleteTask();
        List<DownloadEntity> completedList = Aria().getAllCompleteTask();

        int unCompleteNum = list == null ? 0 : list.size();
        if (unCompleteNum == 0 && completedList != null && !completedList.isEmpty()) {
            notification(task, true);
            ToastUtil.showShort(R.string.all_download_completed);
            //移除前台通知
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                stopForeground(true);
                isForegroundSuc = false;
            }
            //全部完成,结束service
            stopSelf();
        } else {
            notification(task, false);
        }
    }

    @Download.onTaskRunning
    public void onTaskRunning(DownloadTask task) {
        update(task);
    }
}

service中有一些基础的工具类没有贴出,替换成你自己的即可。

外部只需调用内部的download方法即可(最好自己先处理一下文件读写权限),需要注意的是DownloadItem 是显示用的额外实体类,传入后aria会把它与下载任务关联并以string的形式保存到数据库:

DownloadItem downloadIte = new DownloadItem();
downloadIte.taskNameDesc = "test";
downloadIte.coverPic = "https://t7.baidu.com/it/u=2621658848,3952322712&fm=193&f=GIF.jpg";
AriaService.download(CloudMineActivity.this, url, new Gson().toJson(downloadIte));

通知工具类还是贴一下吧:

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import java.util.Random;


/**
 * Created by ly on 2021/7/19 17:04
 */
public class NotificationUtils {

    private static final int MAX = 100;
    private int notifyId;
    private String channelId;
    private NotificationCompat.Builder builder;
    private final NotificationManager notificationManager;
    private NotificationManagerCompat notificationManagerCompat;

    private String title, content;
    private int progress;
    private PendingIntent pendingIntent;
    private final Context mContext;

    public NotificationUtils(@NonNull Context context) {
        this.mContext = context;

        notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        newBuilder();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //创建通知渠道
            CharSequence name = "default";
            String description = "default";
            int importance = NotificationManager.IMPORTANCE_HIGH;//重要性级别 这里用默认的
            NotificationChannel mChannel = new NotificationChannel(getChannelId(), name, importance);

            mChannel.setDescription(description);//渠道描述
            mChannel.enableLights(true);//是否显示通知指示灯
            mChannel.enableVibration(true);//是否振动

            notificationManager.createNotificationChannel(mChannel);//创建通知渠道
        } else {
            notificationManagerCompat = NotificationManagerCompat.from(mContext);
        }
    }

    public NotificationUtils newBuilder() {
        builder = new NotificationCompat.Builder(mContext, getChannelId());
        return this;
    }

    public NotificationUtils send() {
        return send(notifyId);
    }

    public NotificationUtils send(int notifyId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.notify(notifyId, build());
        } else {
            notificationManagerCompat.notify(notifyId, build());
        }
        return this;
    }

    public Notification build() {
        builder.setSmallIcon(R.mipmap.ic_launcher)
                .setPriority(NotificationCompat.PRIORITY_MAX)
                //铃声、闪光、震动均系统默认
                .setDefaults(Notification.DEFAULT_ALL)
                .setAutoCancel(true)
                .setContentTitle(title)
                .setContentText(content);

        if (progress > 0 && progress < MAX) {
            builder.setProgress(MAX, progress, false);
        } else {
            builder.setProgress(0, 0, false);
        }
        if (pendingIntent != null) {
            builder.setContentIntent(pendingIntent).setAutoCancel(true);
            builder.setFullScreenIntent(pendingIntent, true);
        }

        return builder.build();
    }

    public void cancel() {
        cancel(notifyId);
    }

    public void cancel(int notifyId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.cancel(notifyId);
        } else {
            notificationManagerCompat.cancel(notifyId);
        }
    }

    public NotificationUtils setTitle(@StringRes int title) {
        this.title = mContext.getString(title);
        return this;
    }

    public NotificationUtils setContent(@StringRes int content) {
        this.content = mContext.getString(content);
        return this;
    }

    public NotificationUtils setTitle(String title) {
        this.title = title;
        return this;
    }

    public NotificationUtils setContent(String content) {
        this.content = content;
        return this;
    }


    public NotificationUtils setProgress(@IntRange(from = 0, to = MAX) int progress) {
        this.progress = progress;
        return this;
    }

    public NotificationUtils setPendingClass(Class<?> cls) {
        Intent intent = new Intent(mContext, cls);
        pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
        return this;
    }

    public NotificationUtils setPendingClassName(String cls) {
        Intent intent = new Intent();
        intent.setClassName(mContext, cls);
        pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
        return this;
    }

    public NotificationUtils setNotifyId(int notifyId) {
        this.notifyId = notifyId;
        return this;
    }

    public NotificationUtils setChannelId(String channelId) {
        this.channelId = channelId;
        return this;
    }

    public int getNotifyId() {
        if (notifyId == 0)
            this.notifyId = new Random().nextInt() + 1;
        return notifyId;
    }

    public String getChannelId() {
        if (TextUtils.isEmpty(channelId))
            this.channelId = String.valueOf(new Random().nextInt() + 1);
        return channelId;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }

    public int getProgress() {
        return progress;
    }

    public NotificationCompat.Builder getBuilder() {
        return builder;
    }
}

到此,下载的全部代码都分享完毕,说一下我对aria的一些看法:
1、框架335kb,挺大的了,里面包含了http、下载上传等功能,不精简。作者如果能把http、上传等功能分离抽出来做成可选依赖会更好。
2、目前没找到批量添加下载任务的api(发现的朋友请留言告诉我

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java多线程断点续传下载可以使用Java中的线程池和RandomAccessFile类实现。具体步骤如下: 1. 创建一个线程池,线程数量可以根据需要调整。 ``` ExecutorService executorService = Executors.newFixedThreadPool(threadCount); ``` 2. 获取文件大小和已经下载的字节数,计算出每个线程需要下载的字节数。 ``` URL url = new URL(fileUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); long fileSize = connection.getContentLength(); long threadSize = fileSize / threadCount; ``` 3. 创建一个RandomAccessFile对象,指定文件名和读写模式。 ``` RandomAccessFile randomAccessFile = new RandomAccessFile(fileName, "rw"); ``` 4. 为每个线程创建一个DownloadThread对象,指定线程编号、起始位置和结束位置。 ``` for (int i = 0; i < threadCount; i++) { long start = i * threadSize; long end = (i == threadCount - 1) ? fileSize - 1 : start + threadSize - 1; DownloadThread thread = new DownloadThread(fileUrl, randomAccessFile, start, end); executorService.execute(thread); } ``` 5. 在DownloadThread中使用HttpURLConnection下载文件,使用RandomAccessFile写入文件。 ``` URL url = new URL(fileUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("Range", "bytes=" + start + "-" + end); InputStream inputStream = connection.getInputStream(); byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) != -1) { randomAccessFile.write(buffer, 0, length); } inputStream.close(); ``` 6. 在程序关闭时,关闭RandomAccessFile和线程池。 ``` randomAccessFile.close(); executorService.shutdown(); ``` 这样就可以实现Java多线程断点续传下载了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值