在android開發過程中,檔案上傳非常常見。但是檔案的斷點續傳就很少見了。因為android都是通過http協議請求伺服器的,http本身不支援檔案的斷點上傳。但是http支援檔案的斷點下載,可以通過http檔案斷點下載的原理來實現檔案的斷點上傳,檔案的斷點下載比較簡單,主要步驟如下
(1)開啟服務,通過服務後台下載檔案
(2)conn.getContentLength();擷取要下載檔案的長度,建立對應大小的檔案
(3) 通過range欄位來設定我們要下載的檔案的開始位元組和結束位元組。http.setRequestProperty(“Range”, “bytes=” +
startPos + “-” + endPos) //注意格式中間加“-”
(4)通過sqllite儲存我們下載的進度,以便在重新開始任務的時候斷點下載
(5)通過廣播將下載的進度回調到主線程,更新進度條
(6)利用RandomAccessFile的seek方法,跳過已下載的位元組數來將下載的位元組寫入檔案
通過這六步就可以實現簡單的斷點續傳,當然單線程下載效能比較差。在最後的最佳化過程中還要加入多線程下載來提高我們的下載效率。ok檔案的斷線下載就說到這裡,這不是我們要談論的重點,接下來才是核心內容。
檔案的上傳對於android來說是非常耗時的操作,因此不能再主線程中進行。還是要利用服務來完成。但是上傳的時候並沒有range欄位來傳入我們要上傳的開始位元組和結束位元組。這應該怎樣做呢,這就要求對於檔案上傳,我們的伺服器要做出相應的改變了。在上傳檔案的時候,將檔案的大小的名稱和大小作為參數上傳到伺服器,這樣伺服器就記錄了要上傳的檔案大小。在上傳過程中服務端要對已上傳的位元組數進行記錄。在斷點時,移動端不用記錄檔案的已上傳的大小,而是首先去請求伺服器得到已上傳的位元組數,移動端做的只是跳過相應的位元組數來讀取檔案即可。在這裡有必要說一下android通過http請求時如何與伺服器互動的。移動端得到與伺服器響應的連結後,通過流來進行資料的傳遞,在移動端讀取檔案的時候,並沒有全部緩衝到流中,而是在android端做了緩衝,而android的記憶體有限,在上傳大檔案時就會記憶體溢出。在往流裡面寫入資料的時候,伺服器也不能及時得到流內的資料,而是當移動端提交以後伺服器才能做出response,移動端就是依據response判斷檔案上傳是否成功。這樣就會出現一個問題,android要即時提交檔案,負責記憶體溢出導致程勳崩潰。在上傳檔案時必須先對檔案校正,以免檔案重複上傳。對於重複上傳的檔案,服務端只要將以上傳的檔案與使用者關聯就可以了。檔案的校正當然是md5校正了,md5校正也是耗時操作,因此也要多執行緒。在進度條的更新上還是使用廣播來更新就可以了,當然也可以用handler來更新進度條。這個全憑個人喜好。這樣檔案的斷點上傳思路就有了。
(1)開啟服務,通過服務後台檢驗檔案和上傳檔案
(2)在檢驗檔案的response中得到檔案的已上傳位元組數
(3) 利用RandomAccessFile的seek方法,跳過已上傳的位元組數來讀取檔案
(4)多檔案上傳時通過sqllite儲存檔案的狀態,對於已上傳的檔案將狀態值設定為已上傳或者在資料庫刪除
(5)通過廣播將下載的進度回調到主線程,更新進度條
下面是檔案斷點上傳的主要代碼
@Override public void onClick(View v) { switch (v.getId()) { case R.id.tv_shangchuan: //開始上傳時將選中的檔案路徑通過intent傳給服務 list_filePath.clear(); //將檔案的路徑傳入service int file_count = map_check_file.size();// L.i("-----count"+file_count); if(file_count==0){ T.showShort(this,"空檔案夾不能上傳"); return; } loading_dialog1.show(); for(Map.Entry<String, File> entry: map_check_file.entrySet()){ File value = entry.getValue(); list_filePath.add(value.getAbsolutePath()); } Intent intent_service = new Intent(); intent_service.setAction(MainActivity.ACTION_START); intent_service.setPackage(getPackageName()); intent_service.putExtra("file_list", (Serializable) list_filePath); getApplication().startService(intent_service); break; case R.id.tv_lixian: if (map_check_file.size() != 0) { for (Map.Entry<String, File> entry : map_check_file.entrySet()) { //將未上傳的檔案加入資料庫 fileDaoImp.insertData(entry.getValue().getAbsolutePath()); } Fragment fragmentById = manager.findFragmentById(R.id.fl_director_activity); FileFragment fileFragment = (FileFragment) fragmentById; if (fileFragment != null) { sendMessageToFragment(fileFragment); } T.showShort(this, "已加入離線"); rl_director_bottom.setVisibility(View.GONE); } else { T.showShort(this,"空檔案夾不能加入離線"); } break; } }
因為在項目中有離線功能,就一併貼出來吧,離線的實現也較為簡單。就是當使用者點擊離線的時候,將離線檔案的路徑進入資料庫,然後通過廣播判斷該網路狀態,當wifi條件下,讀取資料庫中未下載檔案然後開啟服務下載。
服務的代碼如下:
public class DownLoadService extends Service implements APICallBack { private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 1: downLoadTask = new DownLoadTask(DownLoadService.this); downLoadTask.download(); break; } } }; public static final String RECEIVI = "UPDATEPROGRESS"; //下載檔案的線程 private DownLoadTask downLoadTask = null; //檔案斷點上傳的資料庫管理類 FileDaoImp fileDaoImp = new FileDaoImp(DownLoadService.this); boolean isFirst = true; List<String> list_file_path = new ArrayList<>(); @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { if (MainActivity.ACTION_START.equals(intent.getAction())) { downLoadTask.isPause = false; String loading_shangchuan = intent.getStringExtra("loading_shangchuan"); if (loading_shangchuan != null && loading_shangchuan.equals("loading_shangchuan")) { isFirst = false; new InitThread().start(); return super.onStartCommand(intent, flags, startId); } list_file_path = (List<String>) intent.getSerializableExtra("file_list"); isFirst = true; Log.i("main", "--------list---Service--------------" + list_file_path.size()); //初始化線程 new InitThread().start(); } else if (MainActivity.ACTION_STOP.equals(intent.getAction())) { if (downLoadTask != null) { downLoadTask.isPause = true; downLoadTask = null; } } else if (MainActivity.ACTION_CANCEL.equals(intent.getAction())) { downLoadTask.isPause = true; downLoadTask = null; fileDaoImp.deletDateFileTask(); fileDaoImp.deleteFileUrl(); } }// START_NO_STICKY// START_STICKY 預設調用 return super.onStartCommand(intent, flags, startId); }//初始話檔案線程 class InitThread extends Thread { @Override public void run() { if (isFirst) { for (int i = 0; i < list_file_path.size(); i++) { File file = new File(list_file_path.get(i));// L.i("-------file-------------" + file.length()); FileInfo fileInfo2 = null; try { if (!file.isDirectory()) { //將選中的檔案存入資料庫 fileInfo2 = new FileInfo(2, file.getAbsolutePath(), file.getName(), file.length(), 0, MD5Util.getFileMD5String(file)); fileDaoImp.insertFileUrl(fileInfo2.getUrl(), fileInfo2.getLength(), fileInfo2.getMd5(), fileInfo2.getFileName()); } } catch (IOException e) { e.printStackTrace(); } } } handler.obtainMessage(1).sendToTarget(); } }}
//檔案上傳線程,將檔案按照5M分區上傳。下面也給出了android如何不再本機快取的方法。
/** * Created by zhoukai on 2016/5/3. * // int fixedLength = (int) fStream.getChannel().size(); * // 已知輸出資料流的長度用setFixedLengthStreamingMode() * // 位置輸出資料流的長度用setChunkedStreamingMode() * // con.setChunkedStreamingMode(塊的大小); * // 如果沒有用到以上兩種方式,則會在本機快取後一次輸出,那麼當向輸出資料流寫入超過40M的大檔案時會導致OutOfMemory * //設定固定流的大小 * // con.setFixedLengthStreamingMode(fixedLength); * // con.setFixedLengthStreamingMode(1024 * 1024*20); */public class DownLoadTask { private Context context; private FileDaoImp fileDaoImp; public static boolean isPause = false; private long file_sum = 0; String isExistUrl = "http://123.56.15.30:8080/upload/isExistFile"; String actionUrl = "http://123.56.15.30:8080/upload/uploadFile"; private int finishedLength; public DownLoadTask(Context context) { this.context = context; fileDaoImp = new FileDaoImp(context); } public void download() { new DownThread().start(); } class DownThread extends Thread { private double load_lenth = 0; String end = "\r\n"; String twoHyphens = "--"; String boundary = "*****"; @Override public void run() { //未上傳的檔案 List<FileInfo> list = fileDaoImp.queryFileByState(); Log.i("main", "--------list--資料庫---------------" + list.size()); int sum_filelength = (int) fileDaoImp.getLengthByState(0); if (list.size() == 0) { return; } Intent intent = new Intent(); intent.setAction(DownLoadService.RECEIVI); int nSplitter_length = 1024 * 1024 * 5; for (int i = 0; i < list.size(); i++) { int file_length = (int) list.get(i).getLength(); int count = file_length / nSplitter_length + 1;// L.i("-------------------md5------------" + list.get(i).getMd5());// L.i("------------------fileName------------" + list.get(i).getFileName());//---------------------驗證檔案-------------------------------------------------- URL realurl = null; InputStream in = null; HttpURLConnection conn = null; try { realurl = new URL(isExistUrl); conn = (HttpURLConnection) realurl.openConnection(); conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); conn.setRequestMethod("POST"); conn.setChunkedStreamingMode(1024 * 1024 * 10); //無窮大逾時 conn.setReadTimeout(0); conn.setConnectTimeout(0); conn.setDoInput(true); conn.setDoOutput(true); PrintWriter pw = new PrintWriter(conn.getOutputStream()); pw.print("userId=" + AppUtils.getUserName(context) + "&md5=" + list.get(i).getMd5() + "&did=" + getDid() + "&name=" + list.get(i).getFileName() + "&size=" + list.get(i).getLength()); Log.i("main", "-------------userId---------" + AppUtils.getUserName(context));// Log.i("main", "-------------md5---------" + list.get(i).getMd5());// Log.i("main", "-------------did---------" + getDid());// Log.i("main", "-------------name---------" + list.get(i).getFileName());// Log.i("main","-------------size---------"+list.get(i).getLength()); pw.flush(); pw.close(); /* 取得Response內容 */ in = conn.getInputStream(); int ch; StringBuffer stringBuffer = new StringBuffer(); while ((ch = in.read()) != -1) { stringBuffer.append((char) ch); } String json = stringBuffer.toString(); JSONObject jsonObject = new JSONObject(json); boolean isSuccess = jsonObject.optBoolean("success"); if (isSuccess) { int lengths = jsonObject.optJSONObject("info").optJSONObject("file").optInt("length"); finishedLength = lengths; if (finishedLength == list.get(i).getLength()) { fileDaoImp.deleteFilebyMd5(list.get(i).getMd5()); fileDaoImp.deleteFilebyPath(list.get(i).getUrl()); if (i == list.size() - 1) { intent.putExtra("progress", (load_lenth * 100 / ((double) sum_filelength))); intent.putExtra("state", "success"); context.sendBroadcast(intent); } continue; } Log.i("main", "-----length_finished------" + finishedLength); } } catch (Exception eio) { Log.i("main", "-----Exception------" + eio.toString()); } //---------------------上傳檔案-------------------------------------------------- for (int j = 0; j < count; j++) { try { File file = new File(list.get(i).getUrl()); URL url = new URL(actionUrl); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setChunkedStreamingMode(1024 * 1024 * 10); //無窮大逾時 con.setReadTimeout(0); con.setConnectTimeout(0); /* 允許Input、Output,不使用Cache */ con.setDoInput(true); con.setDoOutput(true); con.setUseCaches(false); /* 設定傳送的method=POST */ con.setRequestMethod("POST"); /* setRequestProperty */ con.setRequestProperty("Connection", "Keep-Alive");//建立長串連 con.setRequestProperty("Charset", "UTF-8"); //編碼格式 con.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);//表單提交檔案 DataOutputStream ds = new DataOutputStream(con.getOutputStream()); //添加參數 StringBuffer sb = new StringBuffer(); Map<String, String> params_map = new HashMap<>(); params_map.put("nSplitter", "3"); params_map.put("md5", list.get(i).getMd5()); params_map.put("dId", getDid()); params_map.put("userId", AppUtils.getUserName(context)); params_map.put("name", file.getName()); params_map.put("from", finishedLength + ""); Log.i("main", "-------------userId----上傳-----" + AppUtils.getUserName(context)); if (finishedLength + nSplitter_length > file_length) { params_map.put("to", file_length + ""); } else { params_map.put("to", (finishedLength + nSplitter_length) + ""); } params_map.put("size", list.get(i).getLength() + ""); //添加參數 for (Map.Entry<String, String> entries : params_map.entrySet()) { sb.append(twoHyphens).append(boundary).append(end);//分界符 sb.append("Content-Disposition: form-data; name=" + entries.getKey() + end); sb