Android多任務多線程斷點續(xù)傳下載

下載這個需求時常遇到,以前是版本更新下載單個apk包苦始,用okhttp+service或者系統(tǒng)提供的DownloadManager實現(xiàn)即可寞钥,方便快捷,不涉及多線程多任務下載陌选,特別是DownloadManager提供了完善的斷點理郑、狀態(tài)保存蹄溉、網(wǎng)絡判斷等功能,非常適合單一任務的下載情況您炉,但遇到批量下載(類似迅雷的下載)以上的方案就略顯不足了柒爵。如果全部自己來實現(xiàn)多任務、多線程赚爵、斷點續(xù)傳棉胀、暫停等功能,那工作量還是很大的冀膝,除非所開發(fā)的項目是專業(yè)下載的app唁奢,不然還是別造這個輪子了,就像我現(xiàn)在做的項目窝剖,批量下載只是app中一個小小的功能而已麻掸,所以我選擇用第三方庫。我采用的方案是Aria赐纱,也看過流利說的開源方案脊奋,但是那個庫很久沒維護,且使用復雜疙描,就選擇了aria诚隙。目前來看符合項目的需求,下載效果如下:

在這里插入圖片描述

我采用service+notification對aria進行了封裝起胰,簡化外部調(diào)用最楷,直接看代碼(項目引入請看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>
 * 添加下載任務直接調(diào)用靜態(tài)方法{@link #download(Context c, String u, String e)}
 * 在任務列表需要顯示進度的頁面bindService火俄,通過#OnUpdateStatusListener更新數(shù)據(jù)
 * <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 展示列表需要保存到數(shù)據(jù)庫的額外數(shù)據(jù)
     */
    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)//自定義數(shù)據(jù)
                        .setFilePath(file.getAbsolutePath())//設置文件保存的完整路徑
                        .create();//啟動下載
                if (taskId > 0) {
                    //如果任務創(chuàng)建成功,則開啟前臺service讲冠,開始下載
                    startForeground();
                } else {
                    ToastUtil.showShort(R.string.task_create_fail);
                }
            }

            /*
             * 用startForegroundService啟動后5s內(nèi)還沒有startForeground表示沒有下載任務瓜客,則自動銷毀service(否則O及以上的系統(tǒng)會anr)
             * 該操作對用戶不可見(startForeground后立馬stop了),代價就是創(chuàng)建了一個空service竿开,好處就是外部調(diào)用便利谱仪。
             *
             * 以下情況可以移除該操作:
             * 1、不在service內(nèi)做下載查重工作
             * 2否彩、不采用aria下載(aria需要傳入this,不靈活疯攒。但目前沒發(fā)現(xiàn)其他更好的方案,無奈列荔。敬尺、枚尼、)
             */
            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 {
            //中間狀態(tài)的通知設置為靜默
            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框架回調(diào)-------------
    @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中有一些基礎的工具類沒有貼出盯质,替換成你自己的即可。

外部只需調(diào)用內(nèi)部的download方法即可(最好自己先處理一下文件讀寫權限)概而,需要注意的是DownloadItem 是顯示用的額外實體類呼巷,傳入后aria會把它與下載任務關聯(lián)并以string的形式保存到數(shù)據(jù)庫:

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) {
            //創(chuàng)建通知渠道
            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);//創(chuàng)建通知渠道
        } 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)
                //鈴聲、閃光到腥、震動均系統(tǒng)默認
                .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(發(fā)現(xiàn)的朋友請留言告訴我

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末霸饲,一起剝皮案震驚了整個濱河市为朋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌厚脉,老刑警劉巖习寸,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異傻工,居然都是意外死亡霞溪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門中捆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸯匹,“玉大人,你說我怎么就攤上這事泄伪∨古睿” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵臂容,是天一觀的道長科雳。 經(jīng)常有香客問我根蟹,道長,這世上最難降的妖魔是什么糟秘? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任简逮,我火速辦了婚禮,結果婚禮上尿赚,老公的妹妹穿的比我還像新娘散庶。我一直安慰自己,他們只是感情好凌净,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布悲龟。 她就那樣靜靜地躺著,像睡著了一般冰寻。 火紅的嫁衣襯著肌膚如雪须教。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天斩芭,我揣著相機與錄音轻腺,去河邊找鬼。 笑死划乖,一個胖子當著我的面吹牛贬养,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琴庵,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼误算,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了迷殿?” 一聲冷哼從身側響起儿礼,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎庆寺,沒想到半個月后蜘犁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡止邮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奏窑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片导披。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖埃唯,靈堂內(nèi)的尸體忽然破棺而出撩匕,到底是詐尸還是另有隱情,我是刑警寧澤墨叛,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布止毕,位于F島的核電站模蜡,受9級特大地震影響,放射性物質發(fā)生泄漏扁凛。R本人自食惡果不足惜忍疾,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谨朝。 院中可真熱鬧卤妒,春花似錦、人聲如沸字币。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洗出。三九已至士复,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間翩活,已是汗流浹背阱洪。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留隅茎,地道東北人澄峰。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像辟犀,于是被迫代替她去往敵國和親俏竞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容