PRDownload使用与源码解读

使用

Prdownload是一个Android下载库,它可以下载任意类型的文件,支持暂停和继续,支持大文件的下载,并且可以根据下载ID检查它的下载状态。PRDownload提供进度值、开始、失败、取消等任意回调。Github

  • 引入依赖文件
compile 'com.mindorks.android:prdownloader:0.2.0'
  • 在应用的Application中进行初始化
PRDownloader.initialize(getApplicationContext());
  • 配置参数
// Enabling database for resume support even after the application is killed:
PRDownloaderConfig config = PRDownloaderConfig.newBuilder()
                .setDatabaseEnabled(true)
                .build();
PRDownloader.initialize(getApplicationContext(), config);

// Setting timeout globally for the download network requests:
PRDownloaderConfig config = PRDownloaderConfig.newBuilder()
                .setReadTimeout(30_000)
                .setConnectTimeout(30_000)
                .build();
PRDownloader.initialize(getApplicationContext(), config); 
  • 发起请求
int downloadId = PRDownloader.download(url, dirPath, fileName)
                        .build()
                        .setOnStartOrResumeListener(new OnStartOrResumeListener() {
                            @Override
                            public void onStartOrResume() {

                            }
                        })
                        .setOnPauseListener(new OnPauseListener() {
                            @Override
                            public void onPause() {

                            }
                        })
                        .setOnCancelListener(new OnCancelListener() {
                            @Override
                            public void onCancel() {

                            }
                        })
                        .setOnProgressListener(new OnProgressListener() {
                            @Override
                            public void onProgress(Progress progress) {

                            }
                        })
                        .start(new OnDownloadListener() {
                            @Override
                            public void onDownloadComplete() {

                            }
                            @Override
                            public void onError(Error error) {

                            }
                        });      
  • 暂停
PRDownloader.pause(downloadId);
  • 继续
PRDownloader.resume(downloadId);
  • 取消
// Cancel with the download id
PRDownloader.cancel(downloadId);
// The tag can be set to any request and then can be used to cancel the request
PRDownloader.cancel(TAG);
// Cancel all the requests
PRDownloader.cancelAll();
  • 状态获取
Status status = PRDownloader.getStatus(downloadId);
  • 清除数据库中的数据
// Method to clean up temporary resumed files which is older than the given day
PRDownloader.cleanUp(days);

断点续传原理

  • 总文件大小获取
URLConnection connection = new URL(request.getUrl()).openConnection();
String length = connection.getHeaderField("Content-Length");
try {
    return Long.parseLong(length);
} catch (NumberFormatException e) {
    return -1;
}
  • 网络断点设置
final String range = String.format(Locale.ENGLISH,"bytes=%d-", downloadedBytes);
connection.addRequestProperty(Constants.RANGE, range);
  • 文件断点设置
File file = new File(filePath);
RandomAccessFile randomAccess = new RandomAccessFile(file, "rw");
FileDescriptor fileDescriptor = randomAccess.getFD();
BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(fileDescriptor));

randomAccess.seek(downloadedBytes);
outputStream.write(buff, 0, byteCount);
  • 持续下载
InputStream inputStream = connection.getInputStream();

do {
    final int byteCount = inputStream.read(buff);
    if (byteCount == -1) {
        break;
    }
    outputStream.write(buff, 0, byteCount);

    //执行进度回调,更新进度值
    sendProgress(); 
    
    //每次循环中都要刷新缓冲,并且同步文件描述符
    try {
        outputStream.flush();
        fileDescriptor.sync();
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    //持久化进度值至数据库中
    ComponentHolder.getInstance().getDbHelper().updateProgress(request.getDownloadId(),request.getDownloadedBytes(),System.currentTimeMillis());

    //不断在循环中判断是否执行了取消或暂停操作,所以暂停或取消操作可以达到几乎无延时
    if (request.getStatus() == Status.CANCELLED) {
        response.setCancelled(true);
        return response;
    } else if (request.getStatus() == Status.PAUSED) {
        response.setPaused(true);
        return response;
    }
} while (true);

多任务思想

//核心类,其采用单例模式,主要是提供一个执行者提供器,用于提供各种任务的执行对象,从而使各种任务的管理内聚起来
public class Core {
    private static Core instance = null;
    //执行者提供器
    private final ExecutorSupplier executorSupplier;

    ... ...    

    //用于清除该单例
    public static void shutDown() {
        if (instance != null) {
            instance = null;
        }
    }
}
  • 执行者提供器的抽象接口 ExecutorSupplier
//它提供了三种类型的执行者,分别是下载执行者(多线程的线程池)、背景执行者(单线程的线程池)、主线程执行者(UI线程)
public interface ExecutorSupplier {
    DownloadExecutor forDownloadTasks();
    Executor forBackgroundTasks();
    Executor forMainThreadTasks();
}
  • 默认的执行者提供器 DefaultExecutorSupplier
public class DefaultExecutorSupplier implements ExecutorSupplier {
   /**
    * Runtime.getRuntime().availableProcessors() 代表当前机器的CPU数量,即多少核。
    * 如果所有的任务都是计算密集型的,则创建处理器可用的核心数那么多线程就可以了,如果任务都是IO密集型的,那么我们就需要开更多的线程来提高性能。
    *
    * 因为对于计算密集型来说,当有多个任务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换,而这种切换对程序性能损耗较大。
    * 而对于IO密集型来说,io有发送数据(output)和返回数据(input)两个过程。比如以浏览器为主体,浏览器发送请求给服务器(output),
    * 服务器再将请求结果返回给浏览器(input)。在io阻塞的情况下,会释放锁,其他线程会在当前线程等待返回值(阻塞)的情况下继续执行发送请求(output),
    * 第三个线程又会在第二个线程等待返回值(阻塞)的情况下发送请求(output),即在同一时间片段,会有一个线程在等待数据,也会有一个线程在发数据,这就减少了io传输的时间。
    *
    * 如果任务有50%的时间处于阻塞状态,则程序所需线程数为处理器可用核心数的两倍。
    * 如果任务被阻塞的时间少于50%,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应低于处理器的核心数。
    */
    private static final int DEFAULT_MAX_NUM_THREADS = 2 * Runtime.getRuntime().availableProcessors() + 1;
    private final DownloadExecutor networkExecutor;
    private final Executor backgroundExecutor;
    private final Executor mainThreadExecutor;

    DefaultExecutorSupplier() {
        //线程工厂接口,用于生产线程池中的线程,并使得所有线程的优先级都为THREAD_PRIORITY_BACKGROUND(后台线程建议设置这个优先级,值为10)
        ThreadFactory backgroundPriorityThreadFactory = new PriorityThreadFactory(Process.THREAD_PRIORITY_BACKGROUND);
        networkExecutor = new DownloadExecutor(DEFAULT_MAX_NUM_THREADS, backgroundPriorityThreadFactory);
        backgroundExecutor = Executors.newSingleThreadExecutor();
        mainThreadExecutor = new MainThreadExecutor();
    }

    @Override
    public DownloadExecutor forDownloadTasks() {  return networkExecutor;  }
    @Override
    public Executor forBackgroundTasks() {  return backgroundExecutor; }
    @Override
    public Executor forMainThreadTasks() { return mainThreadExecutor; }
}
  • 对优先级处理的线程生产者,专门用于生产下载线程 PriorityThreadFactory
//使用包装者模式对制造的线程进行包装处理,设置其优先级,设计的很漂亮(此优先级并不是多任务的优先级)
public class PriorityThreadFactory implements ThreadFactory {
    private final int mThreadPriority;
    PriorityThreadFactory(int threadPriority) {
        mThreadPriority = threadPriority;
    }

    @Override
    public Thread newThread(final Runnable runnable) { 
        Runnable wrapperRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Process.setThreadPriority(mThreadPriority);
                } catch (Throwable ignored) {}
                runnable.run();
            }
        };
        return new Thread(wrapperRunnable);
    }
}
  • 下载执行者 DownloadExecutor
//开始下载
Core.getInstance().getExecutorSupplier().forDownloadTasks().submit(new DownloadRunnable(request));
public class DownloadExecutor extends ThreadPoolExecutor {

    DownloadExecutor(int maxNumThreads, ThreadFactory threadFactory) {
        //PriorityBlockingQueue类需要其队列中的元素继承 Comparator 接口, 在插入资源时会按照自定义的排序规则来对资源数组进行排序。 
        //其中值大的排在数组后面 ,取值时从数组头开始取(先进先出)
        super(maxNumThreads, maxNumThreads, 0, TimeUnit.MILLISECONDS,
                new PriorityBlockingQueue<Runnable>(), threadFactory);
    }

    @Override
    public Future<?> submit(Runnable task) {
        //创建FutureTask
        DownloadFutureTask futureTask = new DownloadFutureTask((DownloadRunnable) task);
        execute(futureTask);
        return futureTask;
    }
}
public class DownloadFutureTask extends FutureTask<DownloadRunnable> implements Comparable<DownloadFutureTask> {
    private final DownloadRunnable runnable;
    DownloadFutureTask(DownloadRunnable downloadRunnable) {
        super(downloadRunnable, null);
        this.runnable = downloadRunnable;
    }
    
    //实现Comparable接口,对两个线程的优先级做比较处理
    @Override
    public int compareTo(DownloadFutureTask other) {
        Priority p1 = runnable.priority;
        Priority p2 = other.runnable.priority;  
        //如果优先级相等就按照加入顺序执行(先加入,值小,先执行),否则就比较其优先级(ordinal 是枚举的值,值越大优先级越高)
        //所以对于加入顺序是 this - other;对于优先级是 other - this
        return (p1 == p2 ? runnable.sequence - other.runnable.sequence : p2.ordinal() - p1.ordinal());
    }
}
public enum Priority {
    LOW,     //ordinal的值为0,后续依次加1
    MEDIUM,
    HIGH,
    IMMEDIATE
}
public class DownloadRunnable implements Runnable {
    public final Priority priority;
    public final int sequence;  //该任务的加入时序,加入越迟,数值越大
    public final DownloadRequest request;

    DownloadRunnable(DownloadRequest request) {
        this.request = request;
        this.priority = request.getPriority();
        this.sequence = request.getSequenceNumber();
    }

    @Override
    public void run() {
        //以下整体是同步执行
        request.setStatus(Status.RUNNING);
        DownloadTask downloadTask = DownloadTask.create(request);
        Response response = downloadTask.run();
        if (response.isSuccessful()) {
            request.deliverSuccess();
        } else if (response.isPaused()) {
            request.deliverPauseEvent();
        } else if (response.getError() != null) {
            request.deliverError(response.getError());
        } else if (!response.isCancelled()) {
            request.deliverError(new Error());
        }
    }
}
  • 主线程执行器 MainThreadExecutor
//巧妙结合Handler和Executor,值得借鉴
public class MainThreadExecutor implements Executor {
    private final Handler handler = new Handler(Looper.getMainLooper());
    @Override
    public void execute(Runnable runnable) { handler.post(runnable);}
}

扩展

Future接口

Future接口代表异步计算的结果,通过Future接口提供的方法可以查看异步计算是否执行完成,或者等待执行结果并获取执行结果,同时还可以取消执行。Future接口的定义如下:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • cancel():该方法用来取消异步任务的执行。如果异步任务已经完成或者已经被取消,或者由于某些原因不能取消,则会返回false。如果任务还没有被执行,则会返回true并且异步任务不会被执行。如果任务已经开始执行了但是还没有执行完成,若mayInterruptIfRunning为true,则会立即中断执行任务的线程并返回true,若mayInterruptIfRunning为false,则会返回true且不会中断任务执行线程。
  • isCanceled():判断任务是否被取消,如果任务在结束(正常执行结束或者执行异常结束)前被取消则返回true,否则返回false。
  • isDone():判断任务是否已经完成,如果完成则返回true,否则返回false。需要注意的是:任务执行过程中发生异常、任务被取消也属于任务已完成,也会返回true。
  • get():获取任务执行结果,如果任务还没完成则会阻塞等待直到任务执行完成。如果任务被取消则会抛出CancellationException异常,如果任务执行过程发生异常则会抛出ExecutionException异常,如果阻塞等待过程中被中断则会抛出InterruptedException异常。
  • get(long timeout,Timeunit unit):带超时时间的get()版本,如果阻塞等待过程中超时则会抛出TimeoutException异常。

AtomicInteger

原子操作的Integer,适合高并发情况下加减(线程安全),无需添加synchronized,性能较好

private final AtomicInteger sequenceGenerator = new AtomicInteger();
//自增长后并返回其值
int curNum = sequenceGenerator.incrementAndGet();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容