Android-Service系列之断点续传下载

来源:互联网 发布:web前端编程自学难吗 编辑:程序博客网 时间:2024/06/05 18:22

课程地址:http://www.imooc.com/learn/363

源码:http://download.csdn.net/download/qq_22804827/9424950

本次,将会利用Service进行一个单线程的断点续传下载的实例练习。
在开始之前呢,先进行一下简单的案例分析:
这里写图片描述

会涉及到一下几点内容:

  1. 基本UI定义
  2. 数据库的操作:在下载的同时将下载进度保存到数据库里面
  3. Service的启动
  4. Activity给Service传递参数
  5. Service中使用广播回传数据到Activity中
  6. 线程和Handler
  7. 网络操作

还有,在网络下载的时候有几个关键点需要注意一下:
1. 获得网络文件的长度(即大小)
2. 在本地创建一个文件,设置其长度(相当于一个容器,存储下载的文件)
3. 从数据库中获得上次下载的进度
4. 从上次下载的位置下载数据,同时保存进度到数据库中
5. 将下载进度回传给Activity
6. 下载完成后删除下载信息

接下来,就正式开始吧:
(因为接下来还会有多线程的断点续传,所以在这次代码中的一些类,是基于多线程的断点续传而考虑的)


1.首先定义两个实体类
FileInfo.java

/** * 文件实体类 * 将该类序列化后(即实现Serializable接口) * 就可以在intent中进行传递 * */public class FileInfo implements Serializable{    private int id;    private String url;    private String fileName;    /**     * 文件的大小     */    private int length;    /**     * 文件的下载进度     */    private int progress;    public FileInfo() {    }    public FileInfo(int id, String url, String fileName) {        this.id = id;        this.url = url;        this.fileName = fileName;    }    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    public String getUrl() {        return url;    }    public void setUrl(String url) {        this.url = url;    }    public String getFileName() {        return fileName;    }    public void setFileName(String fileName) {        this.fileName = fileName;    }    public int getLength() {        return length;    }    public void setLength(int length) {        this.length = length;    }    public int getProgress() {        return progress;    }    public void setProgress(int progress) {        this.progress = progress;    }    @Override    public String toString() {        return "FileInfo [id=" + id + ", url=" + url + ", fileName=" + fileName                + ", length=" + length + ", progress=" + progress + "]";    }}

2.ThreadInfo.java

/** * 线程信息实体类 *  */public class ThreadInfo {    private int id;    /**     * 跟下载的文件的url一致     */    private String url;    /**     * 上次保存的文件的下载进度     */    private int start;    /**     * 代表的线程中下载文件的总长度     */    private int end;    /**     * 下载的进度(即文件下载到了哪儿,以字节数为单位)     */    private int finished;    public ThreadInfo() {    }    public ThreadInfo(int id, String url, int start, int end, int finished) {        super();        this.id = id;        this.url = url;        this.start = start;        this.end = end;        this.finished = finished;    }    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    public String getUrl() {        return url;    }    public void setUrl(String url) {        this.url = url;    }    public int getStart() {        return start;    }    public void setStart(int start) {        this.start = start;    }    public int getEnd() {        return end;    }    public void setEnd(int end) {        this.end = end;    }    public int getFinished() {        return finished;    }    public void setFinished(int finished) {        this.finished = finished;    }    @Override    public String toString() {        return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start                + ", end=" + end + ", finished=" + finished + "]";    }}

然后是有关数据库操作的几个类
1.DBHelper.java

/** * 数据库帮助类 * 用来创建数据库 * */public class DBHelper extends SQLiteOpenHelper {    private static final String DB_NAME="download.db";    private static final int VERSION=1;//数据库的版本    /**     * 建表语法     */    private static final String TABLE_CREATE="create table thread_info(_id integer primary key autoincrement," +            "thread_id integer,url text,start integer,end integer,finished integer)";    /**     * 删表语法     */    private static final String TABLE_DROP="drop table if exits thread_info";    public DBHelper(Context context) {        super(context, DB_NAME, null, VERSION);    }    @Override    public void onCreate(SQLiteDatabase db) {        db.execSQL(TABLE_CREATE);    }    @Override    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {        db.execSQL(TABLE_DROP);        db.execSQL(TABLE_CREATE);    }}

2.ThreadDAO.java

/** * 数据访问接口i * */public interface ThreadDAO {    /**     * 插入线程信息     * @param threadInfo     */    public void insertThreadInfo(ThreadInfo threadInfo);    /**     * 删除线程信息     * @param url 文件的url     * @param id 线程的id     */    public void deleteThreadInfo(String url,int id);    /**     * 更新线程下载进度     */    public void updateThreadInfo(String url,int threadId,int finished);    /**     * 查询下载文件的线程信息     */    public List<ThreadInfo> getThreads(String url);    /**     * 判断指定线程信息是否已经存在数据库中     */    public boolean isExists(String url,int threadId);}

3.ThreadDAOImpl.java

/** * 线程数据访问接口实现 * */public class ThreadDAOImpl implements ThreadDAO {    private DBHelper mHelper;    public ThreadDAOImpl(Context context) {        mHelper=new DBHelper(context);    }    @Override    public void insertThreadInfo(ThreadInfo threadInfo) {        SQLiteDatabase db=mHelper.getWritableDatabase();        db.execSQL(                "insert into thread_info(thread_id,url,start,end,finished) values(?,?,?,?,?)",                new Object[] { threadInfo.getId(), threadInfo.getUrl(),                        threadInfo.getStart(), threadInfo.getEnd(),                        threadInfo.getFinished() });        db.close();    }    @Override    public void deleteThreadInfo(String url, int id) {        SQLiteDatabase db=mHelper.getWritableDatabase();        db.execSQL("delete from thread_info where url = ? and thread_id = ?",                new Object[]{url,id});        db.close();    }    @Override    public void updateThreadInfo(String url, int threadId,int finished) {        SQLiteDatabase db=mHelper.getWritableDatabase();        db.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?",                new Object[]{finished,url,threadId});        db.close();    }    @Override    public List<ThreadInfo> getThreads(String url) {        List<ThreadInfo> list=null;        SQLiteDatabase db=mHelper.getWritableDatabase();        Cursor cursor=db.rawQuery("select * from thread_info where url = ?",new String[]{url});        if (cursor != null) {            list=new ArrayList<ThreadInfo>();            while (cursor.moveToNext()) {                ThreadInfo temp = new ThreadInfo();                temp.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));                temp.setUrl(cursor.getString(cursor.getColumnIndex("url")));                temp.setStart(cursor.getInt(cursor.getColumnIndex("start")));                temp.setEnd(cursor.getInt(cursor.getColumnIndex("end")));                temp.setFinished(cursor.getInt(cursor                        .getColumnIndex("finished")));                list.add(temp);            }            cursor.close();        }        db.close();        return list;    }    @Override    public boolean isExists(String url, int threadId) {        SQLiteDatabase db=mHelper.getWritableDatabase();        Cursor cursor=db.rawQuery("select * from thread_info where url = ? and thread_id = ?",new String[]{url,""+threadId});        boolean exists=false;        if(cursor!=null) {            exists=cursor.moveToNext();        }        db.close();        return exists;    }}

接着是要用到的Service类以及用于下载的类
1.DownloadService.java

//每个服务都只会存在一个实例public class DownloadService extends Service {    public static final String ACTION_DOWNLOAD="DOWNLOAD";    public static final String ACTION_STOP="STOP";    public static final String ACTION_UPDATE="UPDATE";    /**     * 存放下载文件的文件夹路径     */    public static final String DOWNLOAD_PATH = Environment            .getExternalStorageDirectory().getAbsolutePath()            + "/DownloadsTest/";    private static final int MSG_INIT=0;//代表创建本地文件完成    private DownloadTask mTask;    /**     * 在每次服务启动的时候调用     * 如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法里     */    @Override    public int onStartCommand(Intent intent, int flags, int startId) {        //获得Activity传来的参数        if(ACTION_DOWNLOAD.equals(intent.getAction())) {            if(mTask==null||mTask.isPause||mTask.isFinished) {                //这里的判断语句要将mTask==null放在最前面,因为mTask还没有new出来                FileInfo fileInfo = (FileInfo) intent                        .getSerializableExtra("fileInfo");                Log.d("测试", "ACTION_DOWNLOAD:" + fileInfo.toString());                          // 启动初始化线程                new InitThread(fileInfo).start();            }        }         else if(ACTION_STOP.equals(intent.getAction())){            if (mTask!=null&&!mTask.isPause&&!mTask.isFinished) {                //这里的判断语句要将mTask!=null放在最前面,因为mTask还没有new出来                FileInfo fileInfo = (FileInfo) intent                        .getSerializableExtra("fileInfo");                Log.d("测试", "ACTION_STOP:" + fileInfo.toString());                if (mTask != null) {                    mTask.isPause = true;                }            }        }        return super.onStartCommand(intent, flags, startId);    }    @Override    public IBinder onBind(Intent intent) {        return null;    }    private Handler mHandler=new Handler() {        public void handleMessage(android.os.Message msg) {            switch(msg.what) {            case MSG_INIT:                FileInfo fileInfo=(FileInfo) msg.obj;                Log.d("测试", "mHandler"+fileInfo.toString());                //启动下载任务                mTask=new DownloadTask(DownloadService.this, fileInfo);                mTask.download();                break;            }        }    };    /**     * 从网上读取文件的长度然后再本地建立文件     */    private class InitThread extends Thread {        private FileInfo mFileInfo;        public InitThread(FileInfo fileInfo) {            mFileInfo=fileInfo;        }        public void run() {            Log.d("测试", "InitThread");            HttpURLConnection connection=null;            RandomAccessFile raf=null;            try {                //连接网络文件                URL url=new URL(mFileInfo.getUrl());                connection=(HttpURLConnection) url.openConnection();                connection.setConnectTimeout(3000);//设置连接超时                connection.setReadTimeout(3000);//设置读取超时                connection.setRequestMethod("GET");                int length=-1;                if(connection.getResponseCode()==HttpStatus.SC_OK) {//判断是否成功连接                    //获得文件长度                    length=connection.getContentLength();                }                if(length<=0) {                    return;                }                File dir=new File(DOWNLOAD_PATH);                if(!dir.exists()) {//判断存放下载文件的文件的文件夹是否存在                    dir.mkdir();                }                //在本地创建文件                File file=new File(dir,mFileInfo.getFileName());                raf=new RandomAccessFile(file,"rwd");//随机存取文件,用于断点续传,r-读取/w-写入/d-删除权限                //设置本地文件长度                raf.setLength(length);                mFileInfo.setLength(length);                mHandler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget();            } catch (Exception e) {            } finally {                         try {                    raf.close();                    connection.disconnect();                } catch (Exception e) {                }            }        }    }}
    Activity属于一个前台的组件,有可能会被用户关闭,或者被切到后台(这时就有可能会被安卓系统回收),如果在Activity中创建一个线程去下载,一旦Activity被回收,就无法对在其中创建的线程进行管理,会导致不必要的麻烦    而Service的优先级比较高,一般不会被系统回收

2.DownloadTask.java

/** * 下载任务类 *  */public class DownloadTask {    private Context mContext;    private FileInfo mFileInfo;    private ThreadDAO mDAO;    private int mFinished;//用于更新UI的下载进度    public boolean isPause;//判断是否正在下载    public boolean isFinished;//判断是否下载完成    public DownloadTask(Context context, FileInfo fileInfo) {        super();        this.mContext = context;        this.mFileInfo = fileInfo;        mDAO=new ThreadDAOImpl(context);        isPause=false;        isFinished=false;    }    public void download() {        Log.d("测试","download");        //读取上次的线程信息        List<ThreadInfo> list=mDAO.getThreads(mFileInfo.getUrl());        ThreadInfo threadInfo=null;        if(list.size()==0||list==null) {//有可能是第一次下载,数据库中还没有信息            threadInfo=new ThreadInfo(0,mFileInfo.getUrl(),0,mFileInfo.getLength(),0);        }         else {            //因为这里是单线程下载,所以这里直接get(0)就行了,下次会涉及多线程            threadInfo=list.get(0);        }        new DownloadThread(threadInfo).start();    }    /**     * 下载线程     *     */    class DownloadThread extends Thread {        private ThreadInfo mThreadInfo;        public DownloadThread(ThreadInfo ThreadInfo) {            mThreadInfo=ThreadInfo;        }        public void run() {            Log.d("测试","DownloadThread");            //向数据库中插入线程信息            if(!mDAO.isExists(mThreadInfo.getUrl(), mThreadInfo.getId())) {                mDAO.insertThreadInfo(mThreadInfo);            }            HttpURLConnection connection=null;            RandomAccessFile raf=null;            InputStream input=null;            try {                URL url=new URL(mThreadInfo.getUrl());                connection=(HttpURLConnection) url.openConnection();                connection.setConnectTimeout(3000);                connection.setRequestMethod("GET");                //设置下载位置                int start=mThreadInfo.getFinished();//上一次保存的下载进度即为这次要开始下载的地方                connection.setRequestProperty("Range", "bytes="+start+"-"+mThreadInfo.getEnd());                    //设置请求属性,将field参数设置为Range(范围),newValues为指定的字节数区间                //设置文件写入位置                File file=new File(DownloadService.DOWNLOAD_PATH,mFileInfo.getFileName());                raf=new RandomAccessFile(file, "rwd");                raf.seek(start);                    //在读写的时候跳过设置的字节数,从下一个字节数开始读写                    //如seek(100)则跳过100个字节从第101个字节开始读写                Intent intent=new Intent(DownloadService.ACTION_UPDATE);                mFinished=start;                //开始下载                if(connection.getResponseCode()==HttpStatus.SC_PARTIAL_CONTENT) {                        //因为前面的RequestProperty设置的Range,服务器会认为进行部分的下载,所以这里判断是否成功连接要用SC_PARTIAL_CONTENT                    //读取数据                    input=connection.getInputStream();                    byte[] buffer=new byte[1024*4];                    int len=-1;//标记每次读取的长度                    long time=System.currentTimeMillis();                    while((len=input.read(buffer))!=-1) {                        //写入文件                        raf.write(buffer,0,len);                        //把下载进度发送给Activity                        mFinished += len;                        if(System.currentTimeMillis()-time>500) {//因为该循环运行较快,所以这里减缓一下UI更新的频率                            time=System.currentTimeMillis();                            intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());                            mContext.sendBroadcast(intent);                            Log.d("测试", ""+mFinished * 100 / mFileInfo.getLength());                            mDAO.updateThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId(), mFinished);                                //如果隔断时间就更新一下数据库的内容                                //可以防止没有按下暂停就关闭程序重进后要重新开始下载的问题                                //而又不至于更新数据库太频繁,影响效率                        }                        //在下载暂停时,保存下载进度至数据库                        if(isPause) {                            mDAO.updateThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId(), mFinished);                            intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());                            mContext.sendBroadcast(intent);                            raf.close();                            input.close();                            connection.disconnect();                            return;                        }                    }                    intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());                    mContext.sendBroadcast(intent);                        //因为有可能在刚好下载完成的时候没有进入到if(isPause)中,所以进度条会停在上次更新的时候,显示的时候还有一小段没有下载,但是实际已经下载完成了                    //删除线程信息                    mDAO.deleteThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId());                    isFinished=true;                        }                           } catch (Exception e) {            } finally {                try {                    raf.close();                    input.close();                    connection.disconnect();                } catch (Exception e) {                }            }        }    }}

最后就是有关布局的代码以及相关的配置
1.activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context="com.example.downloaddemo.MainActivity" >    <TextView        android:id="@+id/id_fileName"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="文件名" />    <ProgressBar        android:id="@+id/id_progressBar"        style="?android:attr/progressBarStyleHorizontal"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_alignLeft="@+id/id_fileName"        android:layout_below="@+id/id_fileName"        android:layout_marginTop="15dp" />    <Button        android:id="@+id/id_stop"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignBaseline="@+id/id_download"        android:layout_alignBottom="@+id/id_download"        android:layout_alignRight="@+id/id_progressBar"        android:text="暂停" />    <Button        android:id="@+id/id_download"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_below="@+id/id_progressBar"        android:layout_marginTop="22dp"        android:layout_toLeftOf="@+id/id_stop"        android:text="下载" /></RelativeLayout>

2.MainActivity.java

public class MainActivity extends Activity {    private ProgressBar mProgressBar;    private Button mStop;    private Button mDownload;    private TextView mFileName;    private FileInfo fileInfo;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mFileName=(TextView) findViewById(R.id.id_fileName);        mProgressBar=(ProgressBar) findViewById(R.id.id_progressBar);        mProgressBar.setMax(100);        mStop=(Button) findViewById(R.id.id_stop);        mDownload=(Button) findViewById(R.id.id_download);        //注册广播接收器        IntentFilter filter=new IntentFilter();        filter.addAction(DownloadService.ACTION_UPDATE);        registerReceiver(mReceiver, filter);        //创建文件信息对象        String url="http://www.imooc.com/mobile/imooc.apk";        fileInfo=new FileInfo(0,url,"imooc.apk");        mDownload.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                Intent intent = new Intent(MainActivity.this,                        DownloadService.class);                intent.setAction(DownloadService.ACTION_DOWNLOAD);                intent.putExtra("fileInfo", fileInfo);                startService(intent);            }        });        mStop.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                Intent intent = new Intent(MainActivity.this,                        DownloadService.class);                intent.setAction(DownloadService.ACTION_STOP);                intent.putExtra("fileInfo", fileInfo);                startService(intent);            }        });    }    @Override    protected void onDestroy() {        super.onDestroy();        unregisterReceiver(mReceiver);    }    /**     * 更新UI广播的广播接收器     */    BroadcastReceiver mReceiver=new BroadcastReceiver() {        @Override        public void onReceive(Context context, Intent intent) {            if(intent.getAction().equals(DownloadService.ACTION_UPDATE)) {                Log.d("测试", "mReceiver");                mFileName.setText(fileInfo.getFileName());                int finished=intent.getIntExtra("finished", 0);                mProgressBar.setProgress(finished);            }        }           };}

3.在AndroidMainfest.xml中添加权限

<uses-permission android:name="android.permission.INTERNET"/>    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

注册服务

<application        android:allowBackup="true"        android:icon="@drawable/ic_launcher"        android:label="@string/app_name"        android:theme="@style/AppTheme" >        ...        <service      android:name="com.example.downloaddemo.services.DownloadService"></service>        ...    </application>

新增知识点:RandomAccessFile类的使用


本来是,是想另外用一篇博客来写开始提到的“Android-Service系列之多线程断点续传”课程的(http://www.imooc.com/learn/376),但是,在看了视频课程之后,顿时有种哔了狗的感觉,因为觉得老师讲得让我感觉有点凌乱,虽然在里面学到了几个好的思想,但是也有一些疏漏,而且让我感觉逻辑性不强。
下面先来说说几点不错的思想:
1、将DatabaseHelper修改成单例模式,防止多个实例同时对数据库进行操作
2、同时也要保证同一时刻只有一个线程能够对数据库的内容进行修改(即对数据库的增删改操作进行同步)
3、尽量把对数据库的操作放在线程外面去做,减少对数据库的锁定
4、因为这这个实例中设计的线程比较多,使得系统对线程的创建、销毁所占用的时间,以及性能的消耗是占了相当的比例的,所以在这里用到了线程池来进行优化

好了,说了我觉得不错的地方,就要来找找茬了(都怪小编的强迫症):
1、在视频的2-3节说提到的在适配器的getView方法中将一部分语句放在if语句里面,达到减少其执行次数来进行优化的做法是错误的,因为当复用item的时候,显示的文件名等会错乱
2、因为在DownloadTask的download方法中每个线程需要下载的文件长度总是由文件的总长度/线程数量决定的(相当于固定了的),所以即使在更新UI的时候把下载的进度保存了起来了,但是如果正在下载的文件没有按下暂停而关闭了程序,那么在重新打开程序后就要重头下载文件
3、因为这次的教程只有实现了同时下载几个文件,所以下显示下载任务的item都在同一屏,因此不会涉及到item的复用,但是如果多下载几个文件,要用到第二屏,就会因为item的复用出现按钮响应以及进度条显示的错乱

多线程断点续传涉及的源码(有做修改):
http://download.csdn.net/download/qq_22804827/9431070

1 0
原创粉丝点击