1:前言
在Android应用开发中,下载功能应该用的很多,网上也有很多的开源框架,用起来很方便。但我们也要自己熟悉下载技术,今天就分享一个多线程断点续传下载的demo.
2:技术要点和原理
断点续传的原理大家应该都很清楚,就是利用数据库保存各个线程的下载信息,现在前查询线程信息,现在完删除 线程信息。注意多线程的时候,数据库一定使用单例模式。
3:写好线程和文件实体类,文件要通过intent传递,所以要序列化
public class FileInfo implements Serializable { private int id; private String url; private String fileName; private int length; private int finished; public FileInfo() { } public FileInfo(int id, String url, String fileName, int length, int finished) { this.id = id; this.url = url; this.fileName = fileName; this.length = length; 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 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 getFinished() { return finished; } public void setFinished(int finished) { this.finished = finished; } @Override public String toString() { return "fileInfo: id=" + id + " url=" + url + " fileName=" + fileName + " length=" + length + " finished=" + finished; } }线程信息
public class ThreadInfo { private int id; private String url; private int start; private int ended; private long finished; public ThreadInfo() { } public ThreadInfo(int id, String url, int start, int ended, long finished) { this.id = id; this.url = url; this.start = start; this.ended = ended; 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 getEnded() { return ended; } public void setEnded(int ended) { this.ended = ended; } public long getFinished() { return finished; } public void setFinished(int finished) { this.finished = finished; } @Override public String toString() { return "threadInfo: id=" + id + " url=" + url + " start=" + start + " ended=" + ended + " finished=" + finished; } }
4:下载类(防止线程开销过大,利用java的线程池技术)
public class DownloadTask { private Context mContext; private FileInfo mFileInfo; private ThreadDAOImpl threadDAO; private long mFinished = 0; public boolean isPause = false; private int mThreadCount = 1; private List<DownloadThread> mThreadList = null; public static ExecutorService mExecutorService = Executors.newCachedThreadPool(); public DownloadTask(Context context, FileInfo fileInfo, int mThreadCount) { this.mContext = context; this.mFileInfo = fileInfo; this.threadDAO = new ThreadDAOImpl(context); this.mThreadCount = mThreadCount; } public void download() { List<ThreadInfo> threads = threadDAO.getThread(mFileInfo.getUrl()); if (threads.size() == 0) { int length = mFileInfo.getLength() / mThreadCount; for (int i = 0; i < mThreadCount; i++) { ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(), i * length, (i + 1) * length - 1, 0); if (i == mThreadCount + 1) { threadInfo.setEnded(mFileInfo.getLength()); } threads.add(threadInfo); threadDAO.insertThread(threadInfo); } } mThreadList = new ArrayList<DownloadThread>(); for (ThreadInfo thread : threads) { DownloadThread downThread = new DownloadThread(thread); DownloadTask.mExecutorService.execute(downThread); mThreadList.add(downThread); } } private synchronized void checkAllThreadsFinished() { boolean allFinished = true; for (DownloadThread thread : mThreadList) { if (!thread.isFinished) { allFinished = false; break; } } if (allFinished) { threadDAO.deleteThread(mFileInfo.getUrl()); Intent intent = new Intent(DownloadService.ACTION_FINISH); intent.putExtra("fileInfo", mFileInfo); mContext.sendBroadcast(intent); } } class DownloadThread extends Thread { private ThreadInfo mThreadInfo; public boolean isFinished = false; public DownloadThread(ThreadInfo threadInfo) { this.mThreadInfo = threadInfo; } @Override public void run() { HttpURLConnection connection = null; RandomAccessFile raf = null; InputStream inputStream = null; try { Intent intent = new Intent(DownloadService.ACTION_UPDATE); URL url = new URL(mThreadInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5 * 1000); connection.setRequestMethod("GET"); long start = mThreadInfo.getStart() + mThreadInfo.getFinished(); connection.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnded()); File file = new File(DownloadService.DOWNLOAD_PATH, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); raf.seek(start); long time = System.currentTimeMillis(); mFinished += mThreadInfo.getFinished(); if (connection.getResponseCode() == 206) { inputStream = connection.getInputStream(); byte[] buffer = new byte[1024 * 4]; int length = 0; int totalLength = mFileInfo.getLength(); while ((length = inputStream.read(buffer)) != -1) { raf.write(buffer, 0, length); mFinished += length; mThreadInfo.setFinished((int) mThreadInfo.getFinished() + length); if (System.currentTimeMillis() - time > 1000) { time = System.currentTimeMillis(); int progress = (int) (mFinished * 100 / totalLength); intent.putExtra("finished", progress); intent.putExtra("id", mFileInfo.getId()); if (progress > 0) mContext.sendBroadcast(intent); } if (isPause) { threadDAO.updateThread(mThreadInfo.getUrl(), mThreadInfo.getId(), mThreadInfo.getFinished()); return; } } } isFinished = true; checkAllThreadsFinished(); } catch (Exception e) { e.printStackTrace(); Log.e("m_tag", "下载出现异常:" + e.toString()); } finally { try { connection.disconnect(); inputStream.close(); raf.close(); } catch (Exception e) { e.printStackTrace(); } } } } }5:数据库的创建和增删改查(需要用到单利模式)
public class DBHelper extends SQLiteOpenHelper { private static DBHelper mHelper; private static final String DB_NAME = "download.db"; private static final String CREATE_TABLE = "create table thread_info(_id integer primary key autoincrement, thread_id integer, url text, start integer, ended integer, finished integer)"; private static final String DROP_TABLE = "drop table if exists thread_info"; private static final int VERSION = 1; private DBHelper(Context context) { super(context, DB_NAME, null, VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP_TABLE); db.execSQL(CREATE_TABLE); } public synchronized static DBHelper getDBHelper(Context context) { if (mHelper == null){ mHelper = new DBHelper(context); } return mHelper; } }
public class ThreadDAOImpl implements ThreadDAO { private DBHelper mDbHelper; public ThreadDAOImpl(Context context) { mDbHelper = DBHelper.getDBHelper(context); } @Override public synchronized void insertThread(ThreadInfo threadInfo) { SQLiteDatabase db = mDbHelper.getWritableDatabase(); db.execSQL("insert into thread_info(thread_id, url, start, ended, finished) values(?, ?, ?, ?, ?)", new Object[]{threadInfo.getId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnded(), threadInfo.getFinished()}); db.close(); } @Override public synchronized void deleteThread(String url) { SQLiteDatabase db = mDbHelper.getWritableDatabase(); db.execSQL("delete from thread_info where url = ?", new Object[]{url}); db.close(); } @Override public synchronized void updateThread(String url, int thread_id, long finished) { SQLiteDatabase db = mDbHelper.getWritableDatabase(); db.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?", new Object[]{finished, url, thread_id}); db.close(); } @Override public List<ThreadInfo> getThread(String url) { List<ThreadInfo> mList = new ArrayList<ThreadInfo>(); SQLiteDatabase db = mDbHelper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from thread_info where url = ?", new String[]{url}); ThreadInfo threadInfo; while (cursor.moveToNext()) { threadInfo = new ThreadInfo(); threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id"))); threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url"))); threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start"))); threadInfo.setEnded(cursor.getInt(cursor.getColumnIndex("ended"))); threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished"))); mList.add(threadInfo); } cursor.close(); db.close(); return mList; } @Override public boolean isExists(String url, int thread_id) { boolean exists = false; SQLiteDatabase db = mDbHelper.getWritableDatabase(); Cursor cursor = db.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, thread_id + ""}); if (cursor.getCount() > 0) { exists = true; } cursor.close(); db.close(); return exists; } }6:下载服务
public class DownloadService extends Service implements DownloadTask.ProgressChangeListener { public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/downloads"; public static final String ACTION_UPDATE = "action_update"; public static final String ACTION_START = "action_start"; public static final String ACTION_STOP = "action_stop"; public static final String ACTION_FINISH = "action_finish"; private static final int INIT_ACTION = 0; private WorkHandler mHandler = new WorkHandler(); private Map<Integer, DownloadTask> mTasks = new LinkedHashMap<Integer, DownloadTask>(); @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e("m_tag", "onStartCommand" + " intent=" + intent.getDataString()); String action = intent.getAction(); if (action.equals(ACTION_START)) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); // new InitThread(fileInfo).start(); DownloadTask.mExecutorService.execute(new InitThread(fileInfo)); Log.e("m_tag", "start fileInfo:" + fileInfo.toString()); } else if (action.equals(ACTION_STOP)) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); Log.e("m_tag", "stop fileInfo:" + fileInfo.toString()); DownloadTask task = mTasks.get(fileInfo.getId()); if (task != null){ task.isPause = true; } } return Service.START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void progressChange(String name, int progress) { Log.e("m_download", "name=" + name + " progress=" + progress); } class InitThread extends Thread { private FileInfo mFileInfo; public InitThread(FileInfo fileInfo) { this.mFileInfo = fileInfo; } @Override public void run() { super.run(); HttpURLConnection connection = null; RandomAccessFile raf = null; try { URL url = new URL(mFileInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(5 * 1000); int length = -1; if (connection.getResponseCode() == 200) { length = connection.getContentLength(); } if (length <= 0) { return; } File dir = new File(DOWNLOAD_PATH); if (!dir.exists()) { dir.mkdir(); Log.e("m_tag", "文件目录不存在"); } File file = new File(dir, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); raf.setLength(length); mFileInfo.setLength(length); mHandler.obtainMessage(INIT_ACTION, mFileInfo).sendToTarget(); } catch (Exception e) { Log.e("m_tag", "ex=" + e.toString()); } finally { try { if (raf != null) raf.close(); connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } } private class WorkHandler extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case INIT_ACTION: FileInfo fileInfo = (FileInfo) msg.obj; Log.e("m_tag", "init fileInfo:" + fileInfo.toString()); DownloadTask task = new DownloadTask(DownloadService.this, fileInfo, 3); task.download(); mTasks.put(fileInfo.getId(), task); break; } } } @Override public void onDestroy() { super.onDestroy(); if (mHandler != null) mHandler.removeCallbacksAndMessages(null); } }7:总结
上面只是一部分代码,用到的知识点都是android的基础知识,有写的不合理的地方欢迎指正,源码传送门;