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

课程地址: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

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值