栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Android文件多线程断点下载

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Android文件多线程断点下载

1. 前言

    在前面的博客中简单实现了Android单线程断点下载以及Android文件多线程下载,这篇将实现多线程断点下载。对于断点下载,我们知道主要是为了实现不重复下载上次下载过的数据文件内容,而和前面实践的区别在于我们将从单线程环境拓展到多线程环境中。下面简单整理下思路。

项目代码链接:https://github.com/baiyazi/AndroidDownloadUtils,可自取。

2. 设计思路
  • 确定多线程环境下的线程数量;
  • 确定每个线程自己所需要下载的数据范围;
  • 每个线程下载的数据,使用临时文件进行存储,最终进行文件合并。 (测试结果发现合并太耗时了,故而不考虑使用临时文件的方式。)
  • 记录每个线程下载了多少数据,即:记录下载进度;
  • 设计回调接口;
3. 实现逻辑

这一次不再像之前的一样使用一个Java文件来实现,因为这样代码耦合度太高了。这里就拆分为如下一些文件:

所有W开头的均是本次的文件。至于SingleThreadBreakpointDownloader、MultiThreadDownLoader也就是单线程断点下载和多线程文件下载的代码。

对于文件多线程断点下载,这里还是使用线程池来实现,并将它封装到了一个类中,即WMultiThreadDownloaderConfig:

public class WMultiThreadDownloaderConfig {
    private int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
    private int maximumPoolSize = Runtime.getRuntime().availableProcessors() + 1;

    public WMultiThreadDownloaderConfig(){
    }

    public WMultiThreadDownloaderConfig(int corePoolSize, int maximumPoolSize){
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
    }

    public int getCorePoolSize() {
        return corePoolSize;
    }

    public int getMaximumPoolSize() {
        return maximumPoolSize;
    }

    public ThreadFactory getmThreadFactory() {
        return mThreadFactory;
    }

    private ThreadFactory mThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "Thread#" + mCount.getAndIncrement());
        }
    };

    public Executor getExecutor(){
        return new ThreadPoolExecutor(corePoolSize,
                                    maximumPoolSize,
                                    10L,
                                    TimeUnit.SECONDS,
                                    new linkedBlockingDeque<>(),
                                    mThreadFactory);
    }
}

为了方便,这里设置最大线程和核心线程数目一样,都为可用CPU数目+1。

然后,使用SharedPreferences来存储每个线程下载了多少数据,即WDownloadSpHelper:

public class WDownloadSpHelper {
    private static final String TAG = "DownloadSpHelper";
    private static Context context = null;
    private static volatile SharedPreferences preferences = null;

    private WDownloadSpHelper(){}

    public static SharedPreferences getSharedPreferences(Context c){
        if(preferences == null){
            synchronized (WDownloadSpHelper.class){
                if(preferences == null){
                    if(null == context && null != c){
                        preferences = c.getApplicationContext().
                                getSharedPreferences("WDownload", Context.MODE_PRIVATE);
                        context = c.getApplicationContext();
                    }else{
                        preferences = context.getApplicationContext().
                                getSharedPreferences("WDownload", Context.MODE_PRIVATE);
                    }
                }
            }
        }
        return preferences;
    }

    public static void storageDownloadPosition(int index, long pos){
        SharedPreferences.Editor edit = preferences.edit();
        edit.putLong("" + index, pos);
        edit.apply();
    }

    public static long readDownloadPosition(int index){
        return preferences.getLong("" + index, 0);
    }

    public static void deleteSpFile(){
        Log.e(TAG, "正在删除Sharedpreferences文件。");
        SharedPreferences.Editor edit = preferences.edit();
        edit.clear();
        edit.apply();
    }
}

对于每个线程,我们需要确定自己所需要下载的数据范围,这里将每个线程需要下载的数据和当前现在的位置封装到WDownLoadFileInfo:

public class WDownLoadFileInfo {
    private String url;          // 文件链接
    private String cacheDir = "WCache";    // 文件缓存目录
    private WFileSuffix suffix;  // 文件后缀
    private long startPosition, endPosition; // 需要下载的起始位置和结束位置
    private long totalSize;  // 文件总大小
    private long currentPosition; // 当前下载到什么地方
    private Context context;
    private SharedPreferences preferences;
    private int index;
    private WDownLoadFileInfo(){}

    public WDownLoadFileInfo(Context context, String url,
                             WFileSuffix suffix, long startPosition,
                             long endPosition, long totalSize, int index){
        this.context = context;
        this.url = url;
        this.suffix = suffix;
        this.startPosition = startPosition;
        this.endPosition = endPosition;
        this.totalSize = totalSize;
        this.currentPosition = 0;
        preferences = WDownloadSpHelper.getSharedPreferences(context);
        this.index = index;
    }

    public void setCacheDir(String cacheDir){
        this.cacheDir = cacheDir;
    }

    public String getCacheDir(){
        return cacheDir;
    }

    public void addCurrentPosition(int index, long increment){
        // todo 存储当前的线程下载的位置
        this.currentPosition += increment;
        WDownloadSpHelper.storageDownloadPosition(index, this.currentPosition);
    }

    public long getCurrentPosition(int index){
        // todo 从sharedpreference中读取数据
        long position = WDownloadSpHelper.readDownloadPosition(index);
        this.currentPosition = position;
        return position;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public WFileSuffix getSuffix() {
        return suffix;
    }

    public void setSuffix(WFileSuffix suffix) {
        this.suffix = suffix;
    }

    public long getStartPosition() {
        return startPosition;
    }

    public void setStartPosition(long startPosition) {
        this.startPosition = startPosition;
    }

    public long getEndPosition() {
        return endPosition;
    }

    public void setEndPosition(long endPosition) {
        this.endPosition = endPosition;
    }

    public long getTotalSize() {
        return totalSize;
    }

    public void setTotalSize(long totalSize) {
        this.totalSize = totalSize;
    }

    
    public File getFile(){
        File file = buildPath(cacheDir);
        String fileName = EncoderUtils.hashKeyFromUrl(this.url) + "." + suffix.getValue();
        return new File(file, fileName);
    }
    
    
    private File buildPath(String cacheDir) {
        // 是否有SD卡
        boolean flag = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        // 如果有SD卡就存在外存,否则就位于这个应用的data/package name/cache目录下
        final String cachePath;
        if(flag) cachePath = context.getExternalCacheDir().getPath();
        else cachePath = context.getCacheDir().getPath();

        File directory = new File(cachePath + File.separator + cacheDir);
        // 目录不存在就创建
        if(!directory.exists()) directory.mkdirs();
        return directory;
    }
}

然后,定义一个回调接口和对应的一个抽象类的实现,即IWDownLoadListener和WDownLoadListenerImpl:

// IWDownLoadListener.java
public interface IWDownLoadListener {
    void onSuccess(File file); // 下载成功
    void onError(String msg); // 下载失败
    void onProgress(long currentPos, long totalLength); // 监听下载进度【外部-用户】
    void onListener(long currentPos, long totalLength); // 监听下载进度【内部-代码逻辑】
}

// WDownLoadListenerImpl.java
public abstract class WDownLoadListenerImpl implements IWDownLoadListener {
    private static final String TAG = "DownLoadListenerImpl";

    @Override
    public void onListener(long currentPos, long totalLength) {
        // todo 删除sharedpreferences文件
        if(currentPos == totalLength){
            WDownloadSpHelper.deleteSpFile();
        }
        onProgress(currentPos, totalLength);
    }
}

然后,就可以创建下载线程WDownloadThread:

public class WDownloadThread extends Thread{
    private static final String TAG = "DownloadThread";
    private long startPos, endPos, maxFileSize, currentPosition;
    private File file;
    private String url;
    private IWDownLoadListener listener;
    private int curIndex;
    private WDownLoadFileInfo fileInfo;

    private WDownloadThread(){}
    public WDownloadThread(WDownLoadFileInfo fileInfo, int index) {
        this.startPos = fileInfo.getStartPosition();
        this.endPos = fileInfo.getEndPosition();
        this.url = fileInfo.getUrl();
        this.file = fileInfo.getFile();
        this.maxFileSize = fileInfo.getTotalSize();
        // currentPosition来自Sp文件中读取的数据大小
        this.currentPosition = fileInfo.getCurrentPosition(index);
        this.curIndex = index;
        this.fileInfo = fileInfo;
    }

    public void setDownloadListener(IWDownLoadListener listener){
        this.listener = listener;
    }

    @Override
    public void run() {
        if((startPos + currentPosition) == endPos){
            return;
        }
        // 开始下载,需要重置下Pause字段
        WDownloadControl.restart();
        HttpURLConnection connection = null;
        URL url_c = null;
        InputStream inputStream = null;
        try{
            RandomAccessFile randomAccessFile = new RandomAccessFile(this.file, "rwd");
            // 设置写入文件的开始位置
            randomAccessFile.seek(this.startPos + this.currentPosition);
            url_c = new URL(url);
            connection = (HttpURLConnection) url_c.openConnection();
            connection.setConnectTimeout(5 * 1000); // 5秒钟超时
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Charset", "UTF-8");
            connection.setRequestProperty("accept", "**");
                    connection.connect();

                    // 获取文件总长度
                    totalLength = connection.getContentLength();
                    Log.e(TAG, "文件总长度: " + totalLength);

                    // todo 分为多个线程下载
                    long step = totalLength / maximumPoolSize;
                    Log.e(TAG, "每个线程下载的数据量大小为:" + step);

                    for (int i = 0; i < maximumPoolSize; i++) {
                        WDownLoadFileInfo info = null;
                        WDownloadThread downloadThread = null;
                        if(i != maximumPoolSize - 1) {
                            info = new WDownLoadFileInfo(context, url, suffix,
                                    i * step, (i + 1) * step - 1, totalLength, i);
                        }else{
                            info = new WDownLoadFileInfo(context, url, suffix,
                                    i * step, totalLength, totalLength, i);
                        }
                        // todo 更新进度条
                        WDownloadProgress.addProgressVal(info.getCurrentPosition(i));
                        if(null != listener) listener.onListener(WDownloadProgress.getProgressVal(), totalLength);
                        info.setCacheDir(cachePath);
                        downloadThread = new WDownloadThread(info, i);
                        downloadThread.setDownloadListener(listener);
                        executor.execute(downloadThread);
                    }
                }catch (IOException e){
                    Log.e(TAG, "Download bitmap failed.", e);
                    if(listener != null) listener.onError(e.getLocalizedMessage());
                    e.printStackTrace();
                }finally {
                    if(connection != null) connection.disconnect();
                }
            }
        }).start();
    }
}
4. 调用示例

布局文件:



    

    

也就是一个进度条,两个按钮。

public class ThreeActivity extends AppCompatActivity implements View.OnClickListener {
    private Button start, pause;
    private ProgressBar progressbar;
    private WMultiThreadBreakpointDownloader downloader;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_three);
        start = findViewById(R.id.start);
        pause = findViewById(R.id.pause);
        progressbar = findViewById(R.id.progressbar);
        progressbar.setMax(100);

        downloader = new WMultiThreadBreakpointDownloader.Builder(this)
                .url("http://vjs.zencdn.net/v/oceans.mp4")
                .suffix(WFileSuffix.MP4)
                .cacheDirName("MP4")
                .build();

        start.setOnClickListener(this);

        pause.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                downloader.setIsPause(true);
            }
        });
    }

    @Override
    public void onClick(View v) {
        downloader.download(new WDownLoadListenerImpl() {
            @Override
            public void onSuccess(File file) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(ThreeActivity.this, "Successful!", Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onError(String msg) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(ThreeActivity.this, "Error!", Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onProgress(long currentPos, long totalLength) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        int val = (int) (currentPos * 1.0 / totalLength * 100);
                        progressbar.setProgress(val);
                    }
                });
            }
        });
    }
}

效果:

这里就不录制动态效果的了。点击暂停会暂停,下载则会继续下载。

5. 后记

刚开始使用临时文件来进行单个文件存储。虽然逻辑简单,但是最后需要等待所有线程下载完毕后,再进行这几个文件的合并。也就是需要再次读取一次文件,然后再写入到一个总的文件中。为了完成这个逻辑,使用了CountDownLatch来实现线程等待,但是最后发现合并其实在我的案例中所用的时间更多,所以其实不适用。所以还是采用RandomAccessFile文件的特性来实现,并结合使用SharedPreferences文件来存储每个线程的下载位置即可。


References

  • Android中的三种常用数据持久化技术
  • 任务T1、T2、T3并发执行,最后执行任务T4的实现方法
  • Android单线程断点下载
  • Android文件多线程下载(二)
  • Android文件多线程下载
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/305761.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号