안드로이드 파일 다운로드 (2)


이 섹션 소개:

이 섹션에서는 Android의 다중 스레드 중단점 재개에 대한 코드 분석을 제공합니다. 하하, 왜 분석이라고 부르나요? 나 때문에 글도 못쓰겠어요, ( ╯□╰ )! 먼저 중단점의 의미에 대해 이야기해 보겠습니다. 소위 중단점은 데이터베이스를 사용하여 스레드가 매일 수행하는 작업을 기록하는 것입니다. 다운로드 진행 상황! 시작할 때마다 스레드 ID에 따라 스레드의 다운로드 진행 상황을 쿼리하고 다운로드가 계속됩니다! 꽤 간단해 보이지만, 작성하려고 하면 작성하지 못할 가능성이 높습니다. 이는 정상적인 현상이므로 이 부분을 이해하지 못하더라도 사용하고 수정할 수 있으면 문제가 되지 않습니다. 그것! 자, 이 섹션을 시작하겠습니다~


Android 멀티 스레드 중단점 다운로드의 코드 프로세스 분석:

렌더링 실행:

1.gif

구현 프로세스의 전체 분석:


1단계: 생성 스레드 다운로드 정보를 기록하는 데 사용되는 테이블

데이터베이스 테이블을 생성하여 데이터베이스 관리자 클래스를 생성하고 SQLiteOpenHelper 클래스를 상속합니다. onCreate() 및 onUpgrade() 메서드를 재정의하여 생성한 테이블 필드는 다음과 같습니다.

2.jpg

DBOpenHelper.java:

package com.jay.example.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;

public class DBOpenHelper extends SQLiteOpenHelper {
  public DBOpenHelper(Context context) {
    super(context, "downs.db", null, 1);
  }
  @Override
  public void onCreate(SQLiteDatabase db) {
    //数据库的结构为:表名:filedownlog 字段:id,downpath:当前下载的资源,
    //threadid:下载的线程id,downlength:线程下载的最后位置
    db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog " +
        "(id integer primary key autoincrement," +
        " downpath varchar(100)," +
        " threadid INTEGER, downlength INTEGER)");
  }
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    //当版本号发生改变时调用该方法,这里删除数据表,在实际业务中一般是要进行数据备份的
    db.execSQL("DROP TABLE IF EXISTS filedownlog");
    onCreate(db);
  }

}

2단계: 데이터베이스 작업 클래스 생성

필요한 것 만드는 방법은 어떻습니까?

  • ①URL을 기반으로 각 스레드의 현재 다운로드 길이를 얻는 방법이 필요합니다
  • ②그런 다음 스레드가 새로 열릴 때 스레드와 관련된 매개 변수를 데이터베이스에 삽입하는 방법이 필요합니다
  • 3또한 정의합니다 다운로드한 파일의 길이를 실시간으로 업데이트할 수 있는 메소드
  • 4스레드 다운로드가 완료된 후에도 스레드 ID에 따라 해당 레코드를 삭제해야 합니다

FileService.java

package com.jay.example.db;

import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

/*
 * 该类是一个业务bean类,完成数据库的相关操作
 * */

public class FileService {
  //声明数据库管理器
  private DBOpenHelper openHelper;
  
  //在构造方法中根据上下文对象实例化数据库管理器
  public FileService(Context context) {
    openHelper = new DBOpenHelper(context);
  }
  
  /**
   * 获得指定URI的每条线程已经下载的文件长度
   * @param path
   * @return 
   * */
  public Map getData(String path)
  {
    //获得可读数据库句柄,通常内部实现返回的其实都是可写的数据库句柄
    SQLiteDatabase db = openHelper.getReadableDatabase();
    //根据下载的路径查询所有现场的下载数据,返回的Cursor指向第一条记录之前
    Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?",
        new String[]{path});
    //建立一个哈希表用于存放每条线程已下载的文件长度
    Map data = new HashMap();
    //从第一条记录开始遍历Cursor对象
    cursor.moveToFirst();
    while(cursor.moveToNext())
    {
      //把线程id与该线程已下载的长度存放到data哈希表中
      data.put(cursor.getInt(0), cursor.getInt(1));
      data.put(cursor.getInt(cursor.getColumnIndexOrThrow("threadid")),
          cursor.getInt(cursor.getColumnIndexOrThrow("downlength")));
    }
    cursor.close();//关闭cursor,释放资源;
    db.close();
    return data;
  }
  
  /**
   * 保存每条线程已经下载的文件长度
   * @param path 下载的路径
   * @param map 现在的di和已经下载的长度的集合
  */
  public void save(String path,Map map)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    //开启事务,因为此处需要插入多条数据
    db.beginTransaction();
    try{
      //使用增强for循环遍历数据集合
      for(Map.Entry entry : map.entrySet())
      {
        //插入特定下载路径特定线程ID已经下载的数据
        db.execSQL("insert into filedownlog(downpath, threadid, downlength) values(?,?,?)",
            new Object[]{path, entry.getKey(), entry.getValue()});
      }
      //设置一个事务成功的标志,如果成功就提交事务,如果没调用该方法的话那么事务回滚
      //就是上面的数据库操作撤销
      db.setTransactionSuccessful();
    }finally{
      //结束一个事务
      db.endTransaction();
    }
    db.close();
  }
  
  /**
   * 实时更新每条线程已经下载的文件长度
   * @param path
   * @param map
   */
  public void update(String path,int threadId,int pos)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    //更新特定下载路径下特定线程已下载的文件长度
    db.execSQL("update filedownlog set downlength=? where downpath=? and threadid=?",
        new Object[]{pos, path, threadId});
    db.close();
  }
  
  
  /**
   *当文件下载完成后,删除对应的下载记录
   *@param path 
   */
  public void delete(String path)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    db.execSQL("delete from filedownlog where downpath=?", new Object[]{path});
    db.close();
  }
  
}

Step 3: 파일 다운로더 클래스 생성

자, 데이터베이스 관리자와 작업 클래스가 완료되었습니다. 이제 다시 완료해야 하는 파일 다운로더 클래스를 생성할 차례입니다. 무슨 수술? 할 일이 너무 많아요:

스레드 다운로드 길이를 캐시하는 데 사용되는 스레드 풀 스레드와 동기화 컬렉션 ConcurrentHashMap을 핵심으로 하는 여러 변수를 정의합니다.
스레드 풀의 스레드 수를 가져오는 방법을 정의합니다. ;
3 다운로드 방법을 종료하는 방법,
4 현재 파일 크기를 가져오는 방법
현재 다운로드 길이를 누적하는 방법, 여기에 동기화 키워드를 추가하여 문제를 해결해야 합니다. 동시 액세스
지정된 스레드의 마지막 다운로드 업데이트 위치, 또한 동기화를 사용해야 합니다
7파일 다운로드, 스레드 열기 및 구성 방법의 기타 작업을 완료하려면
8파일 이름을 얻는 방법 : 제공된 URL의 마지막 '/' 뒤에 있는 문자열을 먼저 가로채십시오. 그렇지 않으면 헤드 필드에서 다시 검색하십시오. 찾을 수 없으면 네트워크 카드 식별 번호 + CPU 고유 번호를 사용하여 16바이트 바이너리를 파일 이름으로 생성합니다
9파일 다운로드 시작 방법
http 응답을 얻는 방법 헤더 필드
http 헤더 필드 인쇄 방법
12 로그 정보 인쇄 방법

FileDownloadered.java:

package com.jay.example.service;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.content.Context;
import android.util.Log;





import com.jay.example.db.FileService;

public class FileDownloadered {
  
  private static final String TAG = "文件下载类";  //设置一个查log时的一个标志
  private static final int RESPONSEOK = 200;    //设置响应码为200,代表访问成功
  private FileService fileService;        //获取本地数据库的业务Bean
  private boolean exited;             //停止下载的标志
  private Context context;            //程序的上下文对象
  private int downloadedSize = 0;               //已下载的文件长度
  private int fileSize = 0;           //开始的文件长度
  private DownloadThread[] threads;        //根据线程数设置下载的线程池
  private File saveFile;              //数据保存到本地的文件中
  private Map data = new ConcurrentHashMap();  //缓存个条线程的下载的长度
  private int block;                            //每条线程下载的长度
  private String downloadUrl;                   //下载的路径
  
  
  /**
   * 获取线程数
   */
  public int getThreadSize()
  {
    //return threads.length;
    return 0;
  }
  
  /**
   * 退出下载
   * */
  public void exit()
  {
    this.exited = true;    //将退出的标志设置为true;
  }
  public boolean getExited()
  {
    return this.exited;
  }
  
  /**
   * 获取文件的大小
   * */
  public int getFileSize()
  {
    return fileSize;
  }
  
  /**
   * 累计已下载的大小
   * 使用同步锁来解决并发的访问问题
   * */
  protected synchronized void append(int size)
  {
    //把实时下载的长度加入到总的下载长度中
    downloadedSize += size;
  }
  
  /**
   * 更新指定线程最后下载的位置
   * @param threadId 线程id
   * @param pos 最后下载的位置
   * */
  protected synchronized void update(int threadId,int pos)
  {
    //把指定线程id的线程赋予最新的下载长度,以前的值会被覆盖掉
    this.data.put(threadId, pos);
    //更新数据库中制定线程的下载长度
    this.fileService.update(this.downloadUrl, threadId, pos);
  }
  
  
  /**
   * 构建文件下载器
   * @param downloadUrl 下载路径
   * @param fileSaveDir 文件的保存目录
   * @param threadNum  下载线程数
   * @return 
   */
  public FileDownloadered(Context context,String downloadUrl,File fileSaveDir,int threadNum)
  {
    try {
      this.context = context;     //获取上下文对象,赋值
      this.downloadUrl = downloadUrl;  //为下载路径赋值
      fileService = new FileService(this.context);   //实例化数据库操作的业务Bean类,需要传一个context值
      URL url = new URL(this.downloadUrl);     //根据下载路径实例化URL
      if(!fileSaveDir.exists()) fileSaveDir.mkdir();  //如果文件不存在的话指定目录,这里可创建多层目录
      this.threads = new DownloadThread[threadNum];   //根据下载的线程数量创建下载的线程池
      
      
      HttpURLConnection conn = (HttpURLConnection) url.openConnection();   //创建远程连接句柄,这里并未真正连接
      conn.setConnectTimeout(5000);      //设置连接超时事件为5秒
      conn.setRequestMethod("GET");      //设置请求方式为GET
      //设置用户端可以接收的媒体类型
      conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, " +
          "image/pjpeg, application/x-shockwave-flash, application/xaml+xml, " +
          "application/vnd.ms-xpsdocument, application/x-ms-xbap," +
          " application/x-ms-application, application/vnd.ms-excel," +
          " application/vnd.ms-powerpoint, application/msword, */*");
      
      conn.setRequestProperty("Accept-Language", "zh-CN");  //设置用户语言
      conn.setRequestProperty("Referer", downloadUrl);    //设置请求的来源页面,便于服务端进行来源统计
      conn.setRequestProperty("Charset", "UTF-8");    //设置客户端编码
      //设置用户代理
      conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; " +
          "Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727;" +
          " .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
      
      conn.setRequestProperty("Connection", "Keep-Alive");  //设置connection的方式
      conn.connect();      //和远程资源建立正在的链接,但尚无返回的数据流
      printResponseHeader(conn);   //打印返回的Http的头字段集合
      //对返回的状态码进行判断,用于检查是否请求成功,返回200时执行下面的代码
      if(conn.getResponseCode() == RESPONSEOK)
      {
        this.fileSize = conn.getContentLength();  //根据响应获得文件大小
        if(this.fileSize <= 0)throw new RuntimeException("不知道文件大小");  //文件长度小于等于0时抛出运行时异常
        String filename = getFileName(conn);      //获取文件名称
        this.saveFile = new File(fileSaveDir,filename);  //根据文件保存目录和文件名保存文件
        Map logdata = fileService.getData(downloadUrl);    //获取下载记录
        //如果存在下载记录
        if(logdata.size() > 0)
        {
          //遍历集合中的数据,把每条线程已下载的数据长度放入data中
          for(Map.Entry entry : logdata.entrySet())
          {
            data.put(entry.getKey(), entry.getValue());
          }
        }
        //如果已下载的数据的线程数和现在设置的线程数相同时则计算所有现场已经下载的数据总长度
        if(this.data.size() == this.threads.length)
        {
          //遍历每条线程已下载的数据
          for(int i = 0;i 0) randOut.setLength(this.fileSize);
      randOut.close();    //关闭该文件,使设置生效
      URL url = new URL(this.downloadUrl);
      if(this.data.size() != this.threads.length){
      //如果原先未曾下载或者原先的下载线程数与现在的线程数不一致
        this.data.clear();
        //遍历线程池
        for (int i = 0; i < this.threads.length; i++) {
          this.data.put(i+1, 0);//初始化每条线程已经下载的数据长度为0
        }
        this.downloadedSize = 0;   //设置已经下载的长度为0
      }
      
      for (int i = 0; i < this.threads.length; i++) {//开启线程进行下载
        int downLength = this.data.get(i+1);   
        //通过特定的线程id获取该线程已经下载的数据长度
        //判断线程是否已经完成下载,否则继续下载 
        if(downLength < this.block && this.downloadedSize<this.fileSize){
          //初始化特定id的线程
          this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);
          //设置线程优先级,Thread.NORM_PRIORITY = 5;
          //Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,数值越大优先级越高
          this.threads[i].setPriority(7);   
          this.threads[i].start();    //启动线程
        }else{
          this.threads[i] = null;   //表明线程已完成下载任务
        }
      }
      
      fileService.delete(this.downloadUrl);           
      //如果存在下载记录,删除它们,然后重新添加
      fileService.save(this.downloadUrl, this.data);  
      //把下载的实时数据写入数据库中
      boolean notFinish = true;             
      //下载未完成
      while (notFinish) {               
      // 循环判断所有线程是否完成下载
        Thread.sleep(900);
        notFinish = false;                
        //假定全部线程下载完成
        for (int i = 0; i < this.threads.length; i++){
          if (this.threads[i] != null && !this.threads[i].isFinish()) {
          //如果发现线程未完成下载
            notFinish = true;                   
            //设置标志为下载没有完成
            if(this.threads[i].getDownLength() == -1){        
            //如果下载失败,再重新在已下载的数据长度的基础上下载
            //重新开辟下载线程,设置线程的优先级
              this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);
              this.threads[i].setPriority(7);
              this.threads[i].start();
            }
          }
        }       
        if(listener!=null) listener.onDownloadSize(this.downloadedSize);
        //通知目前已经下载完成的数据长度
      }
      if(downloadedSize == this.fileSize) fileService.delete(this.downloadUrl);
      //下载完成删除记录
    } catch (Exception e) {
      print(e.toString());
      throw new Exception("文件下载异常");
    }
    return this.downloadedSize;
  }
  
  
  /**
   * 获取Http响应头字段
   * @param http
   * @return
   */
  public static Map getHttpResponseHeader(HttpURLConnection http) {
    //使用LinkedHashMap保证写入和便利的时候的顺序相同,而且允许空值
    Map header = new LinkedHashMap();
    //此处使用无线循环,因为不知道头字段的数量
    for (int i = 0;; i++) {
      String mine = http.getHeaderField(i);  //获取第i个头字段的值
      if (mine == null) break;      //没值说明头字段已经循环完毕了,使用break跳出循环
      header.put(http.getHeaderFieldKey(i), mine); //获得第i个头字段的键
    }
    return header;
  }
  /**
   * 打印Http头字段
   * @param http
   */
  public static void printResponseHeader(HttpURLConnection http){
    //获取http响应的头字段
    Map header = getHttpResponseHeader(http);
    //使用增强for循环遍历取得头字段的值,此时遍历的循环顺序与输入树勋相同
    for(Map.Entry entry : header.entrySet()){
      //当有键的时候则获取值,如果没有则为空字符串
      String key = entry.getKey()!=null ? entry.getKey()+ ":" : "";
      print(key+ entry.getValue());      //打印键和值得组合
    }
  }
  
  /**
   * 打印信息
   * @param msg 信息字符串
   * */
  private static void print(String msg) {
    Log.i(TAG, msg);
  }
}

4단계: 다운로드 스레드 클래스 사용자 정의

이 사용자 정의 스레드 클래스는 무엇입니까?

  • ① 먼저 Thread 클래스를 상속받은 다음 Run() 메서드를 다시 작성해야 합니다.
  • ② Run() 메서드: 먼저 다운로드가 완료되었는지 확인하고, 완료되지 않은 경우: URLConnection을 엽니다. 링크, RandomAccessFile 데이터를 읽고 쓰고, 완료되면 완료 표시를 true로 설정하고, 예외가 발생하면 길이를 -1로 설정하고, 예외 정보를 인쇄합니다
  • 3로그 정보를 인쇄하는 방법
  • 4다운로드가 완료되었는지 확인하는 방법(에 따라) 완료 표시)
  • ⑤ 다운로드한 콘텐츠 크기 가져오기

DownLoadThread.java:

package com.jay.example.service;

import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

import android.util.Log;

public class DownloadThread extends Thread {
  private static final String TAG = "下载线程类";    //定义TAG,在打印log时进行标记
  private File saveFile;              //下载的数据保存到的文件
  private URL downUrl;              //下载的URL
  private int block;                //每条线程下载的大小
  private int threadId = -1;            //初始化线程id设置
  private int downLength;             //该线程已下载的数据长度
  private boolean finish = false;         //该线程是否完成下载的标志
  private FileDownloadered downloader;      //文件下载器

  public DownloadThread(FileDownloadered downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) {
    this.downUrl = downUrl;
    this.saveFile = saveFile;
    this.block = block;
    this.downloader = downloader;
    this.threadId = threadId;
    this.downLength = downLength;
  }
  
  @Override
  public void run() {
    if(downLength < block){//未下载完成
      try {
        HttpURLConnection http = (HttpURLConnection) downUrl.openConnection();
        http.setConnectTimeout(5 * 1000);
        http.setRequestMethod("GET");
        http.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
        http.setRequestProperty("Accept-Language", "zh-CN");
        http.setRequestProperty("Referer", downUrl.toString()); 
        http.setRequestProperty("Charset", "UTF-8");
        int startPos = block * (threadId - 1) + downLength;//开始位置
        int endPos = block * threadId -1;//结束位置
        http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//设置获取实体数据的范围
        http.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
        http.setRequestProperty("Connection", "Keep-Alive");
        
        InputStream inStream = http.getInputStream();     //获得远程连接的输入流
        byte[] buffer = new byte[1024];           //设置本地数据的缓存大小为1MB
        int offset = 0;                   //每次读取的数据量
        print("Thread " + this.threadId + " start download from position "+ startPos);  //打印该线程开始下载的位置
        
        RandomAccessFile threadfile = new RandomAccessFile(this.saveFile, "rwd");
        threadfile.seek(startPos);
        //用户没有要求停止下载,同时没有达到请求数据的末尾时会一直循环读取数据
        while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) {
          threadfile.write(buffer, 0, offset);          //直接把数据写入到文件中
          downLength += offset;             //把新线程已经写到文件中的数据加入到下载长度中
          downloader.update(this.threadId, downLength); //把该线程已经下载的数据长度更新到数据库和内存哈希表中
          downloader.append(offset);            //把新下载的数据长度加入到已经下载的数据总长度中
        }
        threadfile.close();
        inStream.close();
        print("Thread " + this.threadId + " download finish");
        this.finish = true;                               //设置完成标记为true,无论下载完成还是用户主动中断下载
      } catch (Exception e) {
        this.downLength = -1;               //设置该线程已经下载的长度为-1
        print("Thread "+ this.threadId+ ":"+ e);
      }
    }
  }
  private static void print(String msg){
    Log.i(TAG, msg);
  }
  /**
   * 下载是否完成
   * @return
   */
  public boolean isFinish() {
    return finish;
  }
  /**
   * 已经下载的内容大小
   * @return 如果返回值为-1,代表下载失败
   */
  public long getDownLength() {
    return downLength;
  }
}

5단계: DownloadProgressListener 인터페이스를 만들어 다운로드 진행 상황 모니터링

FileDownloader는 진행 상황 모니터링을 위해 DownloadProgressListener를 사용합니다. 따라서 여기에서 인터페이스를 생성하고 메소드의 빈 구현을 정의해야 합니다.

DownloadProgressListener.java:

package com.jay.example.service;
public interface DownloadProgressListener {
  public void onDownloadSize(int downloadedSize);
}

6단계: 레이아웃 코드 작성

또한 android:enabled="false"를 호출하여 구성 요소를 클릭할 수 있는지 여부를 설정합니다. 코드는 다음과 같습니다.

activity_main.xml:

            
      
     

7단계: MainActivity 작성

마지막은 MainActivity입니다. 구성 요소 및 관련 변수의 초기화를 완료합니다. 인터페이스 업데이트 작업을 완료하려면 핸들러를 사용하세요. 또한 시간이 많이 걸리는 작업은 기본 스레드에서 수행할 수 없습니다. 따라서 Runnable로 구현된 새 스레드를 여기서 열어야 합니다. 자세한 내용은 코드를 참조하세요

MainActivity.java:

package com.jay.example.multhreadcontinuabledemo;


import java.io.File;

import com.jay.example.service.FileDownloadered;


import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;


public class MainActivity extends Activity {

  private EditText editpath;
  private Button btndown;
  private Button btnstop;
  private TextView textresult;
  private ProgressBar progressbar;
  private static final int PROCESSING = 1;   //正在下载实时数据传输Message标志
  private static final int FAILURE = -1;     //下载失败时的Message标志
  
  
  private Handler handler = new UIHander();
    
    private final class UIHander extends Handler{
    public void handleMessage(Message msg) {
      switch (msg.what) {
      //下载时
      case PROCESSING:
        int size = msg.getData().getInt("size");     //从消息中获取已经下载的数据长度
        progressbar.setProgress(size);         //设置进度条的进度
        //计算已经下载的百分比,此处需要转换为浮点数计算
        float num = (float)progressbar.getProgress() / (float)progressbar.getMax();
        int result = (int)(num * 100);     //把获取的浮点数计算结果转换为整数
        textresult.setText(result+ "%");   //把下载的百分比显示到界面控件上
        if(progressbar.getProgress() == progressbar.getMax()){ //下载完成时提示
          Toast.makeText(getApplicationContext(), "文件下载成功", 1).show();
        }
        break;

      case FAILURE:    //下载失败时提示
        Toast.makeText(getApplicationContext(), "文件下载失败", 1).show();
        break;
      }
    }
    }
  
  
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    editpath = (EditText) findViewById(R.id.editpath);
    btndown = (Button) findViewById(R.id.btndown);
    btnstop = (Button) findViewById(R.id.btnstop);
    textresult = (TextView) findViewById(R.id.textresult);
    progressbar = (ProgressBar) findViewById(R.id.progressBar);
    ButtonClickListener listener = new ButtonClickListener();
    btndown.setOnClickListener(listener);
    btnstop.setOnClickListener(listener);
    
    
  }
  
  
  private final class ButtonClickListener implements View.OnClickListener{
    public void onClick(View v) {
      switch (v.getId()) {
      case R.id.btndown:
        String path = editpath.getText().toString();
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
          File saveDir = Environment.getExternalStorageDirectory();
          download(path, saveDir);
        }else{
          Toast.makeText(getApplicationContext(), "sd卡读取失败", 1).show();
        }
        btndown.setEnabled(false);
        btnstop.setEnabled(true);
        break;

      case R.id.btnstop:
        exit();
        btndown.setEnabled(true);
        btnstop.setEnabled(false);
        break;
      }
    }
    /*
    由于用户的输入事件(点击button, 触摸屏幕....)是由主线程负责处理的,如果主线程处于工作状态,
    此时用户产生的输入事件如果没能在5秒内得到处理,系统就会报“应用无响应”错误。
    所以在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件,
    导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。
     */
    private DownloadTask task;
    /**
     * 退出下载
     */
    public void exit(){
      if(task!=null) task.exit();
    }
    private void download(String path, File saveDir) {//运行在主线程
      task = new DownloadTask(path, saveDir);
      new Thread(task).start();
    }
    
    
    /*
     * UI控件画面的重绘(更新)是由主线程负责处理的,如果在子线程中更新UI控件的值,更新后的值不会重绘到屏幕上
     * 一定要在主线程里更新UI控件的值,这样才能在屏幕上显示出来,不能在子线程中更新UI控件的值
     */
    private final class DownloadTask implements Runnable{
      private String path;
      private File saveDir;
      private FileDownloadered loader;
      public DownloadTask(String path, File saveDir) {
        this.path = path;
        this.saveDir = saveDir;
      }
      /**
       * 退出下载
       */
      public void exit(){
        if(loader!=null) loader.exit();
      }
      
      public void run() {
        try {
          loader = new FileDownloadered(getApplicationContext(), path, saveDir, 3);
          progressbar.setMax(loader.getFileSize());//设置进度条的最大刻度
          loader.download(new com.jay.example.service.DownloadProgressListener() {
            public void onDownloadSize(int size) {
              Message msg = new Message();
              msg.what = 1;
              msg.getData().putInt("size", size);
              handler.sendMessage(msg);
            }
          });
        } catch (Exception e) {
          e.printStackTrace();
          handler.sendMessage(handler.obtainMessage(-1));
        }
      }     
    }
    }
}

8단계: AndroidManifest.xml 파일에 관련 권한을 추가하세요


참조 코드 다운로드:

more 스레드 중단점 다운로더 데모: MulThreadContinuableDemo.zip

멀티 스레드 중단점 다운로드 + 온라인 음악 플레이어: 멀티 스레드 중단점 다운로드 + 온라인 음악 플레이어.zip


이 섹션 요약:

좋아, 이 섹션은 Android 멀티 스레드 중단점 다운로드 코드에 관한 것입니다. 분석이 너무 많아서 꽤 부담스럽긴 하지만 그래도 즉, 다른 사람이 바퀴를 만들었다면 왜 직접 만들까요? 게다가, 우리는 지금도 그것을 창조할 수 있습니다. 그렇죠? 일단은 이해하고, 사용법도 알고, 바꾸는 방법도 알죠~ 뭐, 그게 다입니다~