多线程下载 HttpURLConnection

时间:2021-12-24 22:28:14

Activity
多线程下载 HttpURLConnection
/**实际开发涉及文件上传、下载都不会自己写这些代码,一般会使用第三方库(如xUtils)或Android提供的DownloadManager下载*/
public class HttpDownloadActivity extends ListActivity {
    private TextView tv_info;
    private LinearLayout ll_pbs;
    public static final String PATH_URL_SMALL = "http://f2.market.xiaomi.com/download/AppStore/0b6c25446ea80095219649f646b8d67361b431127/com.wqk.wqk.apk";
    public static final String PATH_URL_BIG = "http://f3.market.xiaomi.com/download/AppChannel/099d2b4f6006a4c883059f459e0025a3e1f25454e/com.pokercity.bydrqp.mi.apk";
    public static final String PATH_FILE = Environment.getExternalStorageDirectory().getPath() + File.separator + "bqt_download" + File.separator;
    /**下载完毕后安装下载的APK*/
    public static final int MSG_WHAT_DOWNLOAD_OK = 1;
    /**下载过程中更新信息*/
    public static final int MSG_WHAT_DOWNLOAD_INFO = 2;
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_WHAT_DOWNLOAD_OK:
                Toast.makeText(HttpDownloadActivity.this, "下载完毕,请安装", Toast.LENGTH_SHORT).show();
                tv_info.append("\n路径为:" + (String) msg.obj);
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setDataAndType(Uri.parse("file://" + (String) msg.obj), "application/vnd.android.package-archive");
                startActivity(intent);
                break;
            case MSG_WHAT_DOWNLOAD_INFO:
                tv_info.append((String) msg.obj);
                break;
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        String[] array = { "使用HttpURLConnection单线程下载文件", "使用HttpURLConnection多线程下载文件", "使用开源框架下载文件" };
        tv_info = new TextView(this);
        tv_info.setTextColor(Color.BLUE);
        tv_info.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
        tv_info.setPadding(20, 10, 20, 10);
        getListView().addFooterView(tv_info);
        ll_pbs = new LinearLayout(this);
        ll_pbs.setOrientation(LinearLayout.VERTICAL);
        getListView().addFooterView(ll_pbs);
        setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new ArrayList<String>(Arrays.asList(array))));
        File directory = new File(PATH_FILE);
        if (!directory.exists()) directory.mkdirs();//必须有这一步
    }
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        switch (position) {
        case 0://使用HttpURLConnection单线程下载文件
            tv_info.setText("下载过程信息:");
            new Thread() {
                @Override
                public void run() {
                    HttpDownloadFilesUtils.simpleDownLoad(PATH_URL_SMALL, PATH_FILE, false, mHandler);
                }
            }.start();
            break;
        case 1://使用HttpURLConnection多线程下载文件
            tv_info.setText("下载过程信息:");
            ll_pbs.removeAllViews();//清空掉旧的进度条
            final ArrayList<ProgressBar> pbs = new ArrayList<ProgressBar>();//ProgressBar、SeekBar、ProgressDialog 这些都是可以在子线程直接更新进度的
            for (int j = 0; j < HttpDownloadFilesUtils.THREAD_COUNT; j++) {
                ProgressBar progressBar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
                ll_pbs.addView(progressBar);//添加到布局中
                pbs.add(progressBar);//添加到集合中
            }
            new Thread() {
                @Override
                public void run() {
                    HttpDownloadFilesUtils.mutileThreadDownload(PATH_URL_BIG, PATH_FILE, false, mHandler, pbs);
                }
            }.start();
            break;
        case 2:
            Toast.makeText(this, "请引用第三个库或jar包后自行测试", Toast.LENGTH_SHORT).show();
            break;
        }
    }
}

工具类
/** 下传文件工具类*/
public class HttpDownloadFilesUtils {
    /**直接使用URLConnection.openStream()打开网络输入流,然后将流写入到文件中*/
    public static void simpleDownLoad(String fileUrl, String filePath, boolean isUseUrlName, Handler mHandler) {
        String fileName;
        if (isUseUrlName) fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);//截取文件名及后缀名
        else fileName = new SimpleDateFormat("yyyy.MM.dd HH-mm-ss", Locale.CHINA).format(new Date()) + fileUrl.substring(fileUrl.lastIndexOf("."));
        fileName = fileName == null ? "bqt" : fileName;
        try {
            InputStream inputStream = new URL(fileUrl).openStream();
            OutputStream outputStream = new FileOutputStream(new File(filePath + fileName));
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, len);
            }
            inputStream.close();
            outputStream.close();
            mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_OK, filePath + fileName));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //*****************************************************************************************************************
    //                                                                                            多线程下载
    //*****************************************************************************************************************
    public static final int THREAD_COUNT = 3;// 线程的数量
    public static long blocksize;// 每个下载区块的大小
    public static int runningTHREAD_COUNT;// 正在运行的线程的数量
    /**
     * 多线程下载
     *  方式1:使用多线程分别下载文件的不同部分,最后把【合并】成一个文件(效率高)。方式2:使用java提供的【RandomAccessFile】类实现多线程的下载(简单)。
     * @param fileUrl        服务器路径
     * @param filePath    保存本地路径
     * @param isUseUrlName    是否使用服务器路径中的文件名,设为false则使用当前时间作为文件名
     * @param mHandler    通过mHandler和UI线程通讯
     * @param pbs        在子线程直接更新各线程下载进度,设为null则不考虑
     */
    public static void mutileThreadDownload(String fileUrl, String filePath, boolean isUseUrlName, Handler mHandler, ArrayList<ProgressBar> pbs) {
        try {
            HttpURLConnection conn = (HttpURLConnection) new URL(fileUrl).openConnection();//获取连接
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            if (conn.getResponseCode() == 200) {
                // 1、在本地创建一个大小跟服务器一模一样的空白文件
                long fileSize = conn.getContentLength();// 得到服务端文件的大小
                String info = "\n服务端文件的大小:" + fileSize + " ( " + fileSize / 1024 / 1024 + "M )";
                mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                String fileName;
                if (isUseUrlName) fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);//截取文件名及后缀名
                else fileName = new SimpleDateFormat("yyyy.MM.dd HH-mm-ss", Locale.CHINA).format(new Date()) + fileUrl.substring(fileUrl.lastIndexOf("."));
                fileName = fileName == null ? "bqt" : fileName;
                RandomAccessFile raf = new RandomAccessFile(filePath + fileName, "rw");//可以从指定位置开始读、写文件;模式:r、rw、rws、rwd
                raf.setLength(fileSize);//设定大小
                raf.close();
                // 2、开启若干个子线程分别去下载对应的资源
                blocksize = fileSize / THREAD_COUNT; // 每个下载区块的大小
                runningTHREAD_COUNT = THREAD_COUNT; //运行的线程数量
                for (int i = 1; i <= THREAD_COUNT; i++) {
                    long startIndex = (i - 1) * blocksize;//开始位置,从0开始
                    long endIndex = i * blocksize - 1;//结束位置
                    if (i == THREAD_COUNT) endIndex = fileSize - 1;// 最后一个线程的结束位置为 size - 1,若值比它大,也不会有异常,实际下载大小也是  size - 1
                    info = "\n开启线程 " + i + " ,下载范围:" + startIndex + "~" + endIndex;
                    mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                    //ProgressBar、SeekBar、ProgressDialog 这些都是可以在子线程直接更新进度的
                    if (pbs != null && pbs.get(i - 1) != null) pbs.get(i - 1).setMax((int) (endIndex - startIndex));//设置各个线程的进度条的最大值
                    //3、调用下面的逻辑完成多线程下载
                    new DownloadThread(fileUrl, filePath + fileName, i, startIndex, endIndex, mHandler, pbs).start();
                }
            }
            conn.disconnect();//取消连接
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ProtocolException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class DownloadThread extends Thread {//线程Thread的子类
        private String fileUrl;
        private String filePath;
        private int threadId;
        private long startIndex;
        private long endIndex;
        private Handler mHandler;
        private ArrayList<ProgressBar> pbs;
        /**定义一个记录当前线程已下载文件的大小的临时文件,若文件不存在则从头下载,否则从记录的位置继续下载;写入时则将其封装为RandomAccessFile*/
        private File positionFile;
        /***
        * @param fileUrl        服务器路径
        * @param filePath    缓存文件保存路径
        * @param threadId        线程id,请使用0、1、2、3……形式,并请按顺序命名
        * @param startIndex    当前线程开始下载的位置
        * @param endIndex    当前线程结束下载的位置
        * @param mHandler    通过mHandler和UI线程通讯
        * @param pbs        在子线程直接更新各线程下载进度,设为null则不考虑
        */
        public DownloadThread(String fileUrl, String filePath, int threadId, long startIndex, long endIndex, Handler mHandler, ArrayList<ProgressBar> pbs) {
            this.fileUrl = fileUrl;
            this.filePath = filePath;
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.mHandler = mHandler;
            this.pbs = pbs;
        }

        public void run() {
            String info;
            try {
                // 1、记录当前线程已下载的总大小
                int total = 0;// 初始值设为0,若已下载部分文件,则获取已下载部分文件的大小并重新赋值
                positionFile = new File(filePath + "-" + threadId);
                if (positionFile.exists() && positionFile.length() > 0) {
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(positionFile)));
                    String totalstr = bufferedReader.readLine();// 获取当前线程上次下载的总【大小】是多少
                    total = Integer.valueOf(totalstr);
                    info = "\n上次线程" + threadId + "下载的总大小:" + total;
                    mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                    startIndex += total;//每个线程继续下载时的开始【位置】注意startIndex的值为开始下载的【位置】,是一个索引;total的值为实际下载的文件的【大小】
                    bufferedReader.close();
                }

                //2、获取连接,设置连接的参数信息
                HttpURLConnection conn = (HttpURLConnection) new URL(fileUrl).openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                conn.setRequestProperty("RANGE", "bytes=" + startIndex + "-" + endIndex);//指定每条线程从文件的什么位置开始下载,下载到什么位置为止
                // 注意,下载服务器中某一部分内容返回码是206,可以用【code/100==2】来判断
                InputStream inputStream = conn.getInputStream();//返回服务端返回的流

                //3、将流中的数据写入文件中
                RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
                randomAccessFile.seek(startIndex);// 指定文件开始写的位置(指针偏移量)。
                info = "\n第 " + threadId + " 个线程:写文件的开始位置:" + String.valueOf(startIndex);
                mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                int len = 0;
                byte[] buffer = new byte[1024];
                while ((len = inputStream.read(buffer)) != -1) {
                    randomAccessFile.write(buffer, 0, len);//向randomAccessFile中写入读取到的流中的内容
                    //将此文件封装成为一个RandomAccessFile,并采用采用rwd模式,即使断电也不会丢失信息!
                    RandomAccessFile rf = new RandomAccessFile(positionFile, "rwd");
                    total += len;//记录下载的总大小
                    rf.write(String.valueOf(total).getBytes());
                    rf.close();
                    if (pbs != null && pbs.get(threadId - 1) != null) pbs.get(threadId - 1).setProgress(total);//设置当前进度条的进程值
                }
                inputStream.close();
                randomAccessFile.close();

            } catch (Exception e) {
            } finally {
                //4、 所有的线程都下载完毕后删除记录文件
                synchronized (HttpDownloadFilesUtils.class) {//线程安全问题
                    info = "\n线程 " + threadId + " 下载完毕了";
                    mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                    runningTHREAD_COUNT--;
                    if (runningTHREAD_COUNT < 1) {
                        info = "\n所有线程已下载完毕,删除临时文件";
                        mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                        for (int i = 1; i <= THREAD_COUNT; i++) {
                            File temFile = new File(filePath + "-" + i);
                            info = "\n删除临时文件 " + i + " ,状态: " + temFile.delete();//删除记录文件
                            mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_INFO, info));
                        }
                        mHandler.sendMessage(Message.obtain(mHandler, HttpDownloadActivity.MSG_WHAT_DOWNLOAD_OK, filePath));
                    }
                }
            }
        }
    }
}