編輯:關於Android編程
接著上一篇blog,這一篇是為一個下載任務同時使用多個線程去下載,而且可以同時下載多個任務。
具體實現的思路:跟上一篇差不多,只不過有些地方需要作出改進,因為是多線程下載,所以容易引發線程並發的問題,所以我們使用一個單例模式,DBHelper只能有一個對象,這樣避免數據庫的並發,然後對於數據庫操作的方法也是同樣,每一次都是只有一個對象,一個數據庫實例去訪問數據庫(前提是他們訪問的數據庫是同一個表,否則不用)。
改進之後的代碼如下:
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DBHelper extends SQLiteOpenHelper { // 表名 private static final String DB_NAME = "download.db"; // 版本號 private static final int VERSION = 1; // 建表語句 private static final String SQL_CREATE = "create table thread_info(_id integer primary key autoincrement," + "thread_id integer,url text,start integer,end integer,finished integer)"; private DBHelper(Context context) { super(context, DB_NAME, null, VERSION); } private static DBHelper sHelper = null; /** * 單例模式 * @param context * @return */ public static DBHelper getInstanceDBHelper(Context context) { //提高效率 if(sHelper == null) { //同步鎖 synchronized (DBHelper.class) { if(sHelper == null) sHelper = new DBHelper(context); } } return sHelper; } @Override public void onCreate(SQLiteDatabase db) { // 建表 db.execSQL(SQL_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }對於thread_info表的操作方法,需要把他們改為同步,假如沒有synchronized關鍵字可能出現線程的安全問題
import java.util.ArrayList; import java.util.List; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.example.myasyncktestdemo.entity.ThreadInfo; public class ThreadDBService { private DBHelper mHelper = null; public ThreadDBService(Context context) { //通過靜態方法來獲取DBhelper實例 mHelper = DBHelper.getInstanceDBHelper(context); } /** * 插入線程信息 * * @param threadInfo */ public synchronized void insertThread(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(); } /** * 刪除線程信息 * * @param url * @param thread_id */ public synchronized void deletetThread(String url) { SQLiteDatabase db = mHelper.getWritableDatabase(); db.execSQL("delete from thread_info where url=?", new Object[] { url }); db.close(); } /** * 更新線程信息 * * @param url * @param thread_id * @param finished */ public synchronized void updateThread(String url, int thread_id, int finished) { SQLiteDatabase db = mHelper.getWritableDatabase(); db.execSQL("update thread_info set finished=? where url=? and thread_id=?", new Object[] { finished, url, thread_id }); db.close(); } /** * 獲取線程信息 * * @param url * @return */ public synchronized List然後關於DownLaodTask類,他是對每一個下載任務都回去分配按照要求的線程數量去下載,下載完成之後再統一把有關該下載任務的類的數據庫信息刪除。它會啟動幾條線程去完成下載(有DownloadService傳入),然後每一條線程完成之後去判斷是否下載完成,發送廣播。getThreads(String url) { SQLiteDatabase db = mHelper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from thread_info where url=?", new String[] { url }); List list = new ArrayList (); while (cursor.moveToNext()) { ThreadInfo 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.setEnd(cursor.getInt(cursor.getColumnIndex("end"))); threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished"))); list.add(threadInfo); } cursor.close(); db.close(); return list; } /** * 查看線程信息想、是否存在 * * @param url * @param thread_id * @return */ public synchronized boolean isExists(String url, int thread_id) { SQLiteDatabase db = mHelper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from thread_info where url=? and thread_id=?", new String[] { url, thread_id + "" }); boolean exists = cursor.moveToNext(); cursor.close(); db.close(); return exists; } }
這裡有一點需要注意的是關於進度條顯示進度值的問題,假如,我們下載的數據比較大的時候,使用mFinished * 100 / mFileInfo.getLength()可能存在溢出的問題,解決辦法有兩種。
1,(((float) mFinished / mFileInfo.getLength()) * 100)),發送這樣的數據,為什麼要轉為float型?因為下載的長度永遠小於等於文件長度,假如沒有轉型則結果一直為0,所以需要轉型。
2,把進條的最大值設置為文件的長度,每一次發送都是發送已經下載的長度,這樣做最簡單。
DownloadService代碼:他有一個管理下載任務的map集合,可以對下載任務進行暫停,以及負責下載的時候獲取下載文件的總長度
public class DownloadService extends Service { // 下載任務的集合 private MapDownLoadTask類,在download方法中,首先去讀取數據庫信息,把該下載任務對於的幾個線程的下載信息讀取出來,假如集合長度為0則不存在則新建,然後插入數據庫,mTasks = new LinkedHashMap (); @Override public int onStartCommand(Intent intent, int flags, int startId) { if (Constant.ACTION_START.equals(intent.getAction())) { System.out.println("有執行開始"); FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); new InitThread(fileInfo).start(); } else if (Constant.ACTION_STOP.equals(intent.getAction())) { System.out.println("有執行暫停"); FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); DownLoadTask task = mTasks.get(fileInfo.getId()); if (task != null) { //存在該下載任務,點擊的時候就暫停 task.setPause(true); stopSelf(); } } return super.onStartCommand(intent, flags, startId); } @Override public IBinder onBind(Intent intent) { return null; } // 處理子線程的消息回傳 @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { if (msg.what == Constant.MSG_INIT) { System.out.println("有執行下載"); FileInfo fileInfo = (FileInfo) msg.obj; DownLoadTask task = new DownLoadTask(DownloadService.this, fileInfo, 3); task.download(); mTasks.put(fileInfo.getId(), task); } }; }; // 初始化的線程,獲取下載文件的長度 class InitThread extends Thread { private FileInfo mFileInfo = null; public InitThread(FileInfo mFileInfo) { super(); this.mFileInfo = mFileInfo; } @Override public void run() { HttpURLConnection conn = null; RandomAccessFile raf = null; try { URL url = new URL(mFileInfo.getUrl()); conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(3000); conn.setRequestMethod("GET"); int length = -1; if (conn.getResponseCode() == 200) { // 得到下載文件長度 length = conn.getContentLength(); } if (length <= 0) { return; } File dir = new File(Constant.DOWNLAOD_PATH); if (!dir.exists()) { dir.mkdirs(); } // 構建文件對象 File file = new File(dir, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); raf.setLength(length); mFileInfo.setLength(length); mHandler.obtainMessage(Constant.MSG_INIT, mFileInfo).sendToTarget(); } catch (Exception e) { e.printStackTrace(); } finally { conn.disconnect(); if(raf!= null) { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }
使用一個List管理每一個下載任務的線程,方便對下載任務完成時候發送廣播刪除數據庫操作,在DownloadThread中真正完成數據下載。各1秒發送廣播更新UI
import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.content.Intent; import com.example.myasyncktestdemo.constant.Constant; import com.example.myasyncktestdemo.db.ThreadDBService; import com.example.myasyncktestdemo.entity.FileInfo; import com.example.myasyncktestdemo.entity.ThreadInfo; /** * 下載任務類 * * @author Administrator * */ public class DownLoadTask { private ThreadDBService mDBService = null; private Context mContext; // 下載完成的進度,所有的該任務的線程公用,可能會導致並發 // 假如假如synchronized關鍵字,會導致性能消耗,比較嚴重,所以不加了 private FileInfo mFileInfo; private int mFinished = 0; private boolean isPause = false; private int mThreadCount = 1;// 每一個下載任務的下載線程數量 private ListmThreadList = null; // 用於管理下載線程 /** * 設置下載任務暫停,每一個下載任務的所有下載線程公用一個下載監聽, 一旦為true,所有該任務的下載線程暫停下載 * * @return */ public boolean isPause() { return isPause; } /** * @param isPause */ public void setPause(boolean isPause) { this.isPause = isPause; } /** * 下載任務的構造方法 * * @param mContext * 上下文 * @param mFileInfo * 需要下載的文件 * @param mThreadCount * 下載該文件需要啟動的線程數量 */ public DownLoadTask(Context mContext, FileInfo mFileInfo, int mThreadCount) { this.mContext = mContext; this.mFileInfo = mFileInfo; mDBService = new ThreadDBService(mContext); this.mThreadCount = mThreadCount; } /** * 下載,調用之後先去查詢數據庫,獲取數據之後啟動線程下載數據 */ public void download() { // 首次點擊下載的時候為0,之後都是從數據庫中讀取 List threads = mDBService.getThreads(mFileInfo.getUrl()); System.out.println("獲取到的單個任務下載的線程數" + threads.size()); if (threads.size() == 0) { // 首次下載,需要做的工作時獲取上級給的每一個任務的線程數 // 根據線程數給每一個下載線程分配下載長度 // 把每一個的下載線程插入到數據庫中 // 獲得每個線程下載的長度 int length = mFileInfo.getLength() / mThreadCount; for (int i = 0; i < mThreadCount; i++) { ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(), length * i, (i + 1) * length - 1, 0); mDBService.insertThread(threadInfo); if (i + 1 == mThreadCount) { threadInfo.setEnd(mFileInfo.getLength()); } // 添加到集合中 threads.add(threadInfo); } } mThreadList = new ArrayList (); // 啟動線程進行下載 for (ThreadInfo info : threads) { DownloadThread thread = new DownloadThread(info); thread.start(); System.out.println("有啟動線程" + "線程狀態" + isPause); // 添加線程到集合中 mThreadList.add(thread); } } /** * 判斷是否所有線程都執行完畢 */ private synchronized void checkAllThreadFinished() { boolean allFinished = true; // 遍歷線程集合 for (DownloadThread thread : mThreadList) { if (!thread.isFinished) { allFinished = false; break; } } // 下載完成,發送下載完成廣播,刪除數據庫關於該任務的下載進程所有信息 if (allFinished) { System.out.println("有發送下載完成廣播"); mDBService.deletetThread(mFileInfo.getUrl()); Intent intent = new Intent(Constant.ACTION_FINISH); intent.putExtra("fileInfo", mFileInfo); mContext.sendBroadcast(intent); } } /** * 真正的數據下載線程類 * * @author Administrator * */ class DownloadThread extends Thread { private ThreadInfo mThreadInfo; public boolean isFinished = false;// 標識是該線程的對於任務是否下載完成 public DownloadThread(ThreadInfo mThreadInfo) { super(); this.mThreadInfo = mThreadInfo; } @Override public void run() { HttpURLConnection conn = null; RandomAccessFile raf = null; InputStream input = null; try { conn = (HttpURLConnection) new URL(mThreadInfo.getUrl()).openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(3000); // 計算該線程的開始下載位置,從他的start位置+他已經完成的 int start = mThreadInfo.getStart() + mThreadInfo.getFinished(); conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnd()); File file = new File(Constant.DOWNLAOD_PATH, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); raf.seek(start); Intent intent = new Intent(Constant.ACTION_UPDATE); mFinished += mThreadInfo.getFinished(); // 斷點下載的是206不是200 if (206 == conn.getResponseCode()) { System.out.println("有執行123"); input = conn.getInputStream(); byte[] buffer = new byte[1024]; int len; long time = System.currentTimeMillis(); System.out.println("有執行DownloadThread方法下載"); while ((len = input.read(buffer)) != -1) { System.out.println("有執行456"); raf.write(buffer, 0, len); // 真個文件的進度 mFinished += len;// 每一個下載線程都會把他的下載進度假如到mFinished上面 // 每個線程的下載進度 mThreadInfo.setFinished(mThreadInfo.getFinished() + len);// 設置該線程的下載完成度 // 隔一段時間發送廣播更新ProgressBar if (System.currentTimeMillis() - time > 1000) { time = System.currentTimeMillis(); //設置進度條目前的下載長度 //這樣做的目的是防止溢出 intent.putExtra("finished", (int) (((float) mFinished / mFileInfo.getLength()) * 100)); intent.putExtra("id", mFileInfo.getId()); mContext.sendBroadcast(intent); } // 暫停保存數據庫,跳出循環 if (isPause) { mDBService.updateThread(mThreadInfo.getUrl(), mThreadInfo.getId(), mThreadInfo.getFinished()); System.out.println("有執行中途下載暫停"); return; } System.out.println("有執行789"); } } System.out.println("有執行ABC"); isFinished = true; System.out.println("有線程下載完成"); // 每一條線程下載完成之後檢查是否都執行完畢 checkAllThreadFinished(); } catch (Exception e) { e.printStackTrace(); } finally { conn.disconnect(); if (input != null) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } if (raf != null) { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }
適配器類,不多說
import java.util.List; import android.content.Context; import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import com.example.myasyncktestdemo.R; import com.example.myasyncktestdemo.constant.Constant; import com.example.myasyncktestdemo.entity.FileInfo; import com.example.myasyncktestdemo.services.DownloadService; public class MyListViewAdapter extends BaseAdapter { private Context mContext; private ListmDatas; public MyListViewAdapter(Context mContext, List mDatas) { this.mContext = mContext; this.mDatas = mDatas; } @Override public int getCount() { return mDatas.size(); } @Override public Object getItem(int position) { return mDatas.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.listviewitem, null); holder = new ViewHolder(); holder.tvFileName = (TextView) convertView.findViewById(R.id.mTvTip); holder.start = (Button) convertView.findViewById(R.id.start); holder.stop = (Button) convertView.findViewById(R.id.stop); holder.pbProgress = (ProgressBar) convertView.findViewById(R.id.pbProgress); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } final FileInfo mFileInfo = mDatas.get(position); holder.tvFileName.setText(mFileInfo.getFileName()); holder.pbProgress.setProgress(mFileInfo.getFinished()); holder.start.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(mContext, DownloadService.class); intent.putExtra("fileInfo", mFileInfo); intent.setAction(Constant.ACTION_START); mContext.startService(intent); } }); holder.stop.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(mContext, DownloadService.class); intent.putExtra("fileInfo", mFileInfo); intent.setAction(Constant.ACTION_STOP); mContext.startService(intent); } }); return convertView; } static class ViewHolder { TextView tvFileName; ProgressBar pbProgress; Button start, stop; } /** * 更新進度條 * @param id 哪一個item的進度條 * @param progress 目前完成的進度 */ public void setProgressbar(int id, int progress) { FileInfo mFileInfo = mDatas.get(id); mFileInfo.setFinished(progress); System.out.println("有收到長度"); notifyDataSetChanged(); } }
MainActivity類,作用也是差不多
import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.widget.ListView; import android.widget.Toast; import com.example.myasyncktestdemo.adapter.MyListViewAdapter; import com.example.myasyncktestdemo.constant.Constant; import com.example.myasyncktestdemo.entity.FileInfo; public class MainActivity extends Activity { private ListView mListView; private MyListViewAdapter mAapter; private List常量類mDatas; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) this.findViewById(R.id.mListview); mDatas = new ArrayList (); FileInfo fileInfo01 = new FileInfo("imooc.apk", 0, "http://www.imooc.com/mobile/imooc.apk", 0, 0); FileInfo fileInfo02 = new FileInfo("Activator.exe", 1, "http://www.imooc.com/download/Activator.exe", 0, 0); FileInfo fileInfo03 = new FileInfo("iTunes64Setup.exe", 2, "http://imooc.com/download/iTunes64Setup.exe", 0, 0); FileInfo fileInfo04 = new FileInfo( "kugou_V7.exe", 3, "http://dlsw.baidu.com/sw-search-sp/soft/1a/11798/kugou_V7.6.85.17344_setup.1427079848.exe", 0, 0); mDatas.add(fileInfo01); mDatas.add(fileInfo02); mDatas.add(fileInfo03); mDatas.add(fileInfo04); mAapter = new MyListViewAdapter(getApplicationContext(), mDatas); mListView.setAdapter(mAapter); IntentFilter filter = new IntentFilter(); filter.addAction(Constant.ACTION_FINISH); filter.addAction(Constant.ACTION_UPDATE); registerReceiver(mReceiver, filter);// 代碼注冊Receive } protected void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); }; BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Constant.ACTION_UPDATE)) { int finished = intent.getIntExtra("finished", 0); int id = intent.getIntExtra("id", -1); mAapter.setProgressbar(id, finished); } else if (Constant.ACTION_FINISH.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); mAapter.setProgressbar(fileInfo.getId(), 0); Toast.makeText(getApplicationContext(), fileInfo.getFileName() + "下載完成", 0).show(); } } }; }
import android.os.Environment; public class Constant { public static final String ACTION_FINISH = "ACTION_FINISH"; public static final String ACTION_UPDATE = "ACTION_UPDATE"; public static final String DOWNLAOD_PATH = Environment.getExternalStorageDirectory() .getAbsolutePath() + "/liweijie/downloads/"; public static final String ACTION_START = "ACTION_START"; public static final String ACTION_STOP = "ACTION_STOP"; public static final int MSG_INIT = 0; }
Entity類就不貼出來了,一個是ThreadInfo一個是FileInfo,然後大家記得加入對於的權限和注冊Service。
在android系統中主要提供了三種方式用於簡單的實現數據持久化功能,即文件存儲,SharePreference存儲以及數據庫存儲。當然還可以把數據保存到SD卡中。
FrameLayout布局(幀布局)就是在屏幕上開辟一個區域以填充所有的組件,但是使用FrameLayout布局會將所有的組件都放在屏幕的左上角,而且所有的組件可以層疊
技術是永無止境的,如果真的愛技術,那就勇敢的堅持下去。我很喜歡這句話,當我在遇到問題的時候、當我覺得代碼枯燥的時候,我就會問自己,到底是不是真的熱愛技術,這個時候,我心裡
android退出應用程序會調用android.os.Process.killProcess(android.os.Process.myPid())或是System.ex