Android中自定義MultipartEntity實現檔案上傳以及使用Volley庫實現檔案上傳
最近在參加CSDN部落格之星,希望大家給投一票,謝謝啦~ 點這裡投我一票吧~
前言
在開發當中,我們常常需要實現檔案上傳,比較常見的就是圖片上傳,比如修改個頭像什麼的。但是這個功能在Android和iOS中都沒有預設的實現類,對於Android我們可以使用Apache提供的HttpClient.jar來實現這個功能,其中依賴的類就是Apache的httpmime.jar中的MultipartEntity這個類。我就是要實現一個檔案上傳功能,但是我還得下載一個jar包,而這個jar包幾十KB,這尼瑪彷彿並非人間!今天我們就來自己實現檔案上傳功能,並且弄懂它們的原理。
在上一篇文章HTTP POST請求報文格式分析與Java實現檔案上傳中我們介紹了HTTP POST報文格式,如果有對POST報文格式不瞭解的同學可以先閱讀這篇文章。
自定義實現MultipartEntity
我們知道,使用網路協議傳輸資料無非就是要遵循某個協議,我們在開發移動應用時基本上都是使用HTTP協議。HTTP協議說白了就是基於TCP的一套網路請求協議,你根據該協議規定的格式傳輸資料,然後伺服器返回給你資料。你的協議引數要是傳遞錯了,那麼伺服器只能給你返回錯誤。
這跟間諜之間對暗號有點相似,他們有一個規定的暗號,雙方見面,A說: 天王蓋地虎,B對: 寶塔鎮河妖。對上了,說事;對不上,弄死這B。HTTP也是這樣的,在HTTP請求時新增header和引數,伺服器根據引數進行解析。形如 :
POST /api/feed/ HTTP/1.1
這裡是header資料
--分隔符
引數1
--分隔符
引數2
只要根據格式來向伺服器傳送請求就萬事大吉了!下面我們就來看MultipartEntity的實現:public class MultipartEntity implements HttpEntity { private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" .toCharArray(); /** * 換行符 */ private final String NEW_LINE_STR = "\r\n"; private final String CONTENT_TYPE = "Content-Type: "; private final String CONTENT_DISPOSITION = "Content-Disposition: "; /** * 文字引數和字符集 */ private final String TYPE_TEXT_CHARSET = "text/plain; charset=UTF-8"; /** * 位元組流引數 */ private final String TYPE_OCTET_STREAM = "application/octet-stream"; /** * 二進位制引數 */ private final byte[] BINARY_ENCODING = "Content-Transfer-Encoding: binary\r\n\r\n".getBytes(); /** * 文字引數 */ private final byte[] BIT_ENCODING = "Content-Transfer-Encoding: 8bit\r\n\r\n".getBytes(); /** * 分隔符 */ private String mBoundary = null; /** * 輸出流 */ ByteArrayOutputStream mOutputStream = new ByteArrayOutputStream(); public MultipartEntity() { this.mBoundary = generateBoundary(); } /** * 生成分隔符 * * @return */ private final String generateBoundary() { final StringBuffer buf = new StringBuffer(); final Random rand = new Random(); for (int i = 0; i < 30; i++) { buf.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]); } return buf.toString(); } /** * 引數開頭的分隔符 * * @throws IOException */ private void writeFirstBoundary() throws IOException { mOutputStream.write(("--" + mBoundary + "\r\n").getBytes()); } /** * 新增文字引數 * * @param key * @param value */ public void addStringPart(final String paramName, final String value) { writeToOutputStream(paramName, value.getBytes(), TYPE_TEXT_CHARSET, BIT_ENCODING, ""); } /** * 將資料寫入到輸出流中 * * @param key * @param rawData * @param type * @param encodingBytes * @param fileName */ private void writeToOutputStream(String paramName, byte[] rawData, String type, byte[] encodingBytes, String fileName) { try { writeFirstBoundary(); mOutputStream.write((CONTENT_TYPE + type + NEW_LINE_STR).getBytes()); mOutputStream .write(getContentDispositionBytes(paramName, fileName)); mOutputStream.write(encodingBytes); mOutputStream.write(rawData); mOutputStream.write(NEW_LINE_STR.getBytes()); } catch (final IOException e) { e.printStackTrace(); } } /** * 新增二進位制引數, 例如Bitmap的位元組流引數 * * @param key * @param rawData */ public void addBinaryPart(String paramName, final byte[] rawData) { writeToOutputStream(paramName, rawData, TYPE_OCTET_STREAM, BINARY_ENCODING, "no-file"); } /** * 新增檔案引數,可以實現檔案上傳功能 * * @param key * @param file */ public void addFilePart(final String key, final File file) { InputStream fin = null; try { fin = new FileInputStream(file); writeFirstBoundary(); final String type = CONTENT_TYPE + TYPE_OCTET_STREAM + NEW_LINE_STR; mOutputStream.write(getContentDispositionBytes(key, file.getName())); mOutputStream.write(type.getBytes()); mOutputStream.write(BINARY_ENCODING); final byte[] tmp = new byte[4096]; int len = 0; while ((len = fin.read(tmp)) != -1) { mOutputStream.write(tmp, 0, len); } mOutputStream.flush(); } catch (final IOException e) { e.printStackTrace(); } finally { closeSilently(fin); } } private void closeSilently(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (final IOException e) { e.printStackTrace(); } } private byte[] getContentDispositionBytes(String paramName, String fileName) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(CONTENT_DISPOSITION + "form-data; name=\"" + paramName + "\""); // 文字引數沒有filename引數,設定為空即可 if (!TextUtils.isEmpty(fileName)) { stringBuilder.append("; filename=\"" + fileName + "\""); } return stringBuilder.append(NEW_LINE_STR).toString().getBytes(); } @Override public long getContentLength() { return mOutputStream.toByteArray().length; } @Override public Header getContentType() { return new BasicHeader("Content-Type", "multipart/form-data; boundary=" + mBoundary); } @Override public boolean isChunked() { return false; } @Override public boolean isRepeatable() { return false; } @Override public boolean isStreaming() { return false; } @Override public void writeTo(final OutputStream outstream) throws IOException { // 引數最末尾的結束符 final String endString = "--" + mBoundary + "--\r\n"; // 寫入結束符 mOutputStream.write(endString.getBytes()); // outstream.write(mOutputStream.toByteArray()); } @Override public Header getContentEncoding() { return null; } @Override public void consumeContent() throws IOException, UnsupportedOperationException { if (isStreaming()) { throw new UnsupportedOperationException( "Streaming entity does not implement #consumeContent()"); } } @Override public InputStream getContent() { return new ByteArrayInputStream(mOutputStream.toByteArray()); } }
使用者可以通過addStringPart、addBinaryPart、addFilePart來新增引數,分別表示新增字串引數、新增二進位制引數、新增檔案引數。在MultipartEntity中有一個ByteArrayOutputStream物件,先將這些引數寫到這個輸出流中,當執行網路請求時,會執行
writeTo(final OutputStream outstream)
方法將所有引數的位元組流資料寫入到與伺服器建立的TCP連線的輸出流中,這樣就將我們的引數傳遞給伺服器了。當然在此之前,我們需要按照格式來向ByteArrayOutputStream物件中寫資料。例如我要向伺服器傳送一個文字、一張bitmap圖片、一個檔案,即這個請求有三個引數。程式碼如下 :
MultipartEntity multipartEntity = new MultipartEntity();
// 文字引數
multipartEntity.addStringPart("type", "我的文字引數");
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
// 二進位制引數
multipartEntity.addBinaryPart("images", bitmapToBytes(bmp));
// 檔案引數
multipartEntity.addFilePart("images", new File("storage/emulated/0/test.jpg"));
// POST請求
HttpPost post = new HttpPost("url") ;
// 將multipartEntity設定給post
post.setEntity(multipartEntity);
// 使用http client來執行請求
HttpClient httpClient = new DefaultHttpClient() ;
httpClient.execute(post) ;
MultipartEntity的輸出格式會成為如下的格式 :
POST /api/feed/ HTTP/1.1
Content-Type: multipart/form-data; boundary=o3Fhj53z-oKToduAElfBaNU4pZhp4-
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.4; M040 Build/KTU84P)
Host: www.myhost.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 168518
--o3Fhj53z-oKToduAElfBaNU4pZhp4-
Content-Type: text/plain; charset=UTF-8
Content-Disposition: form-data; name="type"
Content-Transfer-Encoding: 8bit
This my type
--o3Fhj53z-oKToduAElfBaNU4pZhp4-
Content-Type: application/octet-stream
Content-Disposition: form-data; name="images"; filename="no-file"
Content-Transfer-Encoding: binary
這裡是bitmap的二進位制資料
--o3Fhj53z-oKToduAElfBaNU4pZhp4-
Content-Type: application/octet-stream
Content-Disposition: form-data; name="file"; filename="storage/emulated/0/test.jpg"
Content-Transfer-Encoding: binary
這裡是圖片檔案的二進位制資料
--o3Fhj53z-oKToduAElfBaNU4pZhp4---
看到很熟悉吧,這就是我們在文章開頭時提到的POST報文格式。沒錯!HttpEntity就是負責將引數構造成HTTP的報文格式,文字引數該是什麼格式、檔案該是什麼格式,什麼型別,這些格式都是固定的。構造完之後,在執行請求時會將http請求的輸出流通過writeTo(OutputStream) 函式傳遞進來,然後將這些引數資料全部輸出到http輸出流中即可。
明白了這些道理,看看程式碼也就應該明白了吧。
Volley中實現檔案上傳
Volley是Google官方推出的網路請求庫,這個庫很精簡、優秀,但是他們也沒有預設新增檔案上傳功能的支援。我們今天就來自定義一個Request實現檔案上傳功能,還是需要藉助上面的MultipartEntity類,下面看程式碼:/**
* @author mrsimple
*/
public class MultipartRequest extends Request<String> {
MultipartEntity mMultiPartEntity = new MultipartEntity();
Map<String, String> mHeaders = new HashMap<String, String>();
private final Listener<String> mListener;
/**
* Creates a new request with the given url.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
*/
public MultipartRequest(String url, Listener<String> listener) {
this(url, listener, null);
}
/**
* Creates a new POST request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public MultipartRequest(String url, Listener<String> listener, ErrorListener errorListener) {
super(Method.POST, url, errorListener);
mListener = listener;
}
/**
* @return
*/
public MultipartEntity getMultiPartEntity() {
return mMultiPartEntity;
}
@Override
public String getBodyContentType() {
return mMultiPartEntity.getContentType().getValue();
}
public void addHeader(String key, String value) {
mHeaders.put(key, value);
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return mHeaders;
}
@Override
public byte[] getBody() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
// multipart body
mMultiPartEntity.writeTo(bos);
} catch (IOException e) {
Log.e("", "IOException writing to ByteArrayOutputStream");
}
return bos.toByteArray();
}
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed = "";
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
@Override
protected void deliverResponse(String response) {
if (mListener != null) {
mListener.onResponse(response);
}
}
}
使用示例程式碼:
RequestQueue queue = Volley.newRequestQueue(this);
MultipartRequest multipartRequest = new MultipartRequest(
"http://yourhost.com", new Listener<String>() {
@Override
public void onResponse(String response) {
Log.e("", "### response : " + response);
}
});
// 新增header
multipartRequest.addHeader("header-name", "value");
// 通過MultipartEntity來設定引數
MultipartEntity multi = multipartRequest.getMultiPartEntity();
// 文字引數
multi.addStringPart("location", "模擬的地理位置");
multi.addStringPart("type", "0");
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
// 直接從上傳Bitmap
multi.addBinaryPart("images", bitmapToBytes(bitmap));
// 上傳檔案
multi.addFilePart("imgfile", new File("storage/emulated/0/test.jpg"));
// 將請求新增到佇列中
queue.add(multipartRequest);
效果圖
這是我post到我的應用的截圖 :注意,MultipartRequest並不適合大檔案上傳,如果是大檔案上傳則需要分段上傳,否則會出現OOM。最後給出 github連結。
相關推薦
Android中自定義MultipartEntity實現檔案上傳以及使用Volley庫實現檔案上傳
最近在參加CSDN部落格之星,希望大家給投一票,謝謝啦~ 點這裡投我一票吧~前言在開發當中,我們常常需要實現檔案上傳,比較常見的就是圖片上傳,比如修改個頭像什麼的。但是這個功能在Android和iOS中都沒有預設的實現類,對於And
Android中自定義drawable資源實現佈局的圓角邊框效果
佈局的圓角邊框效果圖如下所示: 如上圖紅色標註的部分就是一個圓角邊框效果的自定義搜尋框。 實現起來很簡單,讓佈局(Relativelayout或者LinearLayout)的background屬性引用自定義的drawable資源即可。 andro
android中自定義畫布Canvas的實現
一、要求:1.畫布繪製控制元件的方法,控制元件應該是一個可以自定義的;2.畫布是可以縮放,且提供一個縮放的方法供外使用;3.控制元件之間連線的方法;4.畫布縮放之後手勢滑動的識別實現; 二、在github裡面種找到了一個類似度挺高的開源專案: github中的第三方的開源專
android中自定義的dialog中的EditText無法彈出輸入法解決方案
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);//彈出輸入法,並且寫在show()方法之後。 解決Dialog 消失,輸入法不消失的問題: 參考:https://blog.csd
Android中自定義DatePicker
先看一下效果 看這個圖很顯然就是一個DatePicker和一個TimePicker組合來實現的 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.a
Android中自定義Dialog樣式
public class MyMiddleDialog extends Dialog { private Context context; public MyMiddleDialog(Context context) { sup
Android中自定義TabLayout指示器長度
效果圖: MainActivity.java檔案 public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullabl
Android中自定義滑動選中控制元件WheelView
WheelView a great functional custom WheelView with demo in dialog and bottomDialog,android 滾動選擇控制元件,滾動選擇器 ========= How to
Android中自定義ViewGroup使每行元件數量不確定,並拿到選中資料
1先看效果圖 2專案目錄 3在定義控制元件FlowTagGroup package android.zhh.com.myviewgroup; /** * Created by sky on 2017/3/10. */ import android.conten
Android中自定義ProgressBar的樣式
如果想快速獲取水平進度條顯示操作,直接進入第四步和第六步操作就可以了!! 首先可以去sdk中檢視 sdk1\platforms\android-23\data\res\values,中的sty
Android中自定義底部彈出框ButtomDialog
先看看效果和你要的是否一樣 一 、先來配置自定義控制元件需要的資源。 1.在res資料夾下建立一個anim資料夾並建立兩個slide_in_bottom.xml、slide_out_bottom.xml檔案,負責彈框進出動畫。 <?xml version="1.0" enco
Android中自定義控制元件SegmentedGroup
GitHub:https://github.com/Kaopiz/android-segmented-control 一 、新增依賴 implementation 'info.hoang8f:android-segmented:1.0.6' 二、佈局中使用 <info.hoan
Android中自定義SeekBar的樣式
有時候原生的SeekBar太醜了,已經滿足不了我們的效果,需要我們自定義樣式。 第一步:在drawable裡建立一個xml檔案 <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://
Android中自定義頂部狀態列顏色
public class StatusBarUtils { public static void setWindowStatusBarColor(Activity activity, int c
Android中自定義SeekBar背景顏色,進度條顏色,滑塊圖片
目錄 Android SeekBar常見問題 在使用Android Seekbar大家可能經常遇到下面這幾個問題: 如何設定Seekbar進度條的高度? 如何設定滑塊的樣式? 如何設定進度條的顏色和背景顏色? 接下來,針對這三個問題我會
Android中自定義ScrollView的滑動監聽事件
專案結構: 1.LazyScrollView類(自定義ScrollView) package android.zhh.com.myapplicationscrollview; /** * Created by sky on 2017/3/19. */ impor
Android中自定義樣式與View的建構函式中的第三個引數defStyle的意義
零、序 零、序 系統自帶的View可以在xml中配置屬性,對於寫的好的Custom View同樣可以在xml中配置屬性,為了使自定義的View的屬性可以在xml中配置,需要以下4個步驟: 通過<declare-styleable>為自定
Android中自定義View的研究 -- 在XML中引用自定義View
如果在一直使用SetContentView(new HellwView(this)覺得總是少了一點東西,少了什麼了,失去了Android中使用XML定義元件的便攜性,這種感覺讓人很不爽,呵呵,在這節裡我們會看到一個自定義View報錯的解決方法,讓我們來看看在XML中定義Vi
android中自定義View設定屬性
記錄一下,以方便以後用到,再次出錯 1.首先需要在values/下面新建一個attrs.xml檔案,將一些需要的一些屬性填寫進去,比如: <?xml version="1.0" encoding="utf-8"?> <resources>
Android中自定義ScrollView的滑動監聽事件,並在滑動時漸變標題欄背景顏色
效果圖 滑動前: 滑動中: 滑動到底部: 專案結構 ObservableScrollView package com.jukopro.titlebarcolor; import android.content.Context; import android.u