前面我们讲了如何通过 volley 实现表单的提交,而这篇文章跟上一篇衔接很大,如果没有看上一篇 blog 的朋友,建议先去看看 Android Volley解析(二)之表单提交篇 因为文件上传实质就是表单的提交,只不过它提交的数据包含文件类型,接下来还是按照表单提交的套路来分析。
数据格式
这里我们通过图片上传的案例来分析,其他文件也是同样的实现方式;以下是我在传图网传图时,上传的数据格式,先来分析一下
POST http://chuantu.biz/upload.php HTTP/1.1 Host: chuantu.biz Connection: keep-alive Content-Length: 4459 Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Origin: http://chuantu.biz User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryS4nmHw9nb2Eeusll Referer: http://chuantu.biz/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8 Cookie: __cfduid=d9215d649e6e648e0eac7688b406a3d911425089350 ------WebKitFormBoundaryS4nmHw9nb2Eeusll Content-Disposition: form-data; name="uploadimg"; filename="spark_bg.png" Content-Type: image/png JFIFC %# , #&')*)-0-(0%()(C (((((((((((((((((((((((((((((((((((((((((((((((((((" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?PNG ------WebKitFormBoundaryS4nmHw9nb2Eeusll--
不难发现,这种格式跟表单提交的格式非常接近,不过还是有所差别,这里仔细看还是能看出来总共有加上结尾行,有五行,因为乱码部分,其实就是图片的二进制数,整个算一行;下面来分析下: 1、第一行:"--" + boundary + "\r\n"
; 前面也说了文件上传,其实就是表单提交,所以在提交数据的开始标志不变; 2、第二行:Content-Disposition: form-data; name="参数的名称"; filename="上传的文件名" + "\r\n"
这里比普通的表单多了一个filename=”上传的文件名”; 3、第三行:Content-Type: 文件的 mime 类型 + "\r\n"
这一行是文件上传必须要的,而普通的文字提交可有可无,mime 类型需要根据文档查询; 4、第四行:"\r\n"
5、第五行文件的二进制数据 + "\r\n"
: 这里跟普通表单提交一样; 结尾行:"--" + boundary + "--" + "\r\n"
可以看到,文件上传的诗句格式跟我们上一篇博文中讲到的表单提交只有两个地方不同,1、第二行的时候增加了一个文件名变量,2、增加了一行Content-Type: 文件的 mime 类型 + "\r\n"
; 文件也可以同时上传多个文件,上传多个文件的时候重复1、2、3、4、5步,在最后的一个文件的末尾加上统一的结束行。
文件实体类
这里是对图片操作所以我建了一个FormImg.java
/** * Created by moon.zhong on 2015/3/3. */ public class FormImage { //参数的名称 private String mName ; //文件名 private String mFileName ; //文件的 mime,需要根据文档查询 private String mMime ; //需要上传的图片资源,因为这里测试为了方便起见,直接把 bigmap 传进来,真正在项目中一般不会这般做,而是把图片的路径传过来,在这里对图片进行二进制转换 private Bitmap mBitmap ; public FormImage(Bitmap mBitmap) { this.mBitmap = mBitmap; } public String getName() { // return mName; //测试,把参数名称写死 return "uploadimg" ; } public String getFileName() { //测试,直接写死文件的名字 return "test.png"; } //对图片进行二进制转换 public byte[] getValue() { ByteArrayOutputStream bos = new ByteArrayOutputStream() ; mBitmap.compress(Bitmap.CompressFormat.JPEG,80,bos) ; return bos.toByteArray(); } //因为我知道是 png 文件,所以直接根据文档查的 public String getMime() { return "image/png"; } }
Volley 对文件数据的封装
/** * Created by gyzhong on 15/3/1. */ public class PostUploadRequest extends Request<String> { /** * 正确数据的时候回掉用 */ private ResponseListener mListener ; /*请求 数据通过参数的形式传入*/ private List<FormImage> mListItem ; private String BOUNDARY = "--------------520-13-14"; //数据分隔线 private String MULTIPART_FORM_DATA = "multipart/form-data"; public PostUploadRequest(String url, List<FormImage> listItem, ResponseListener listener) { super(Method.POST, url, listener); this.mListener = listener ; setShouldCache(false); mListItem = listItem ; //设置请求的响应事件,因为文件上传需要较长的时间,所以在这里加大了,设为5秒 setRetryPolicy(new DefaultRetryPolicy(5000,DefaultRetryPolicy.DEFAULT_MAX_RETRIES,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); } /** * 这里开始解析数据 * @param response Response from the network * @return */ @Override protected Response<String> parseNetworkResponse(NetworkResponse response) { try { String mString = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); Log.v("zgy", "====mString===" + mString); return Response.success(mString, HttpHeaderParser.parseCacheHeaders(response)); } catch (UnsupportedEncodingException e) { return Response.error(new ParseError(e)); } } /** * 回调正确的数据 * @param response The parsed response returned by */ @Override protected void deliverResponse(String response) { mListener.onResponse(response); } @Override public byte[] getBody() throws AuthFailureError { if (mListItem == null||mListItem.size() == 0){ return super.getBody() ; } ByteArrayOutputStream bos = new ByteArrayOutputStream() ; int N = mListItem.size() ; FormImage formImage ; for (int i = 0; i < N ;i++){ formImage = mListItem.get(i) ; StringBuffer sb= new StringBuffer() ; /*第一行*/ //`"--" + BOUNDARY + "\r\n"` sb.append("--"+BOUNDARY); sb.append("\r\n") ; /*第二行*/ //Content-Disposition: form-data; name="参数的名称"; filename="上传的文件名" + "\r\n" sb.append("Content-Disposition: form-data;"); sb.append(" name=\""); sb.append(formImage.getName()) ; sb.append("\"") ; sb.append("; filename=\"") ; sb.append(formImage.getFileName()) ; sb.append("\""); sb.append("\r\n") ; /*第三行*/ //Content-Type: 文件的 mime 类型 + "\r\n" sb.append("Content-Type: "); sb.append(formImage.getMime()) ; sb.append("\r\n") ; /*第四行*/ //"\r\n" sb.append("\r\n") ; try { bos.write(sb.toString().getBytes("utf-8")); /*第五行*/ //文件的二进制数据 + "\r\n" bos.write(formImage.getValue()); bos.write("\r\n".getBytes("utf-8")); } catch (IOException e) { e.printStackTrace(); } } /*结尾行*/ //`"--" + BOUNDARY + "--" + "\r\n"` String endLine = "--" + BOUNDARY + "--" + "\r\n" ; try { bos.write(endLine.toString().getBytes("utf-8")); } catch (IOException e) { e.printStackTrace(); } Log.v("zgy","=====formImage====\n"+bos.toString()) ; return bos.toByteArray(); } //Content-Type: multipart/form-data; boundary=----------8888888888888 @Override public String getBodyContentType() { return MULTIPART_FORM_DATA+"; boundary="+BOUNDARY; } }
因为代码中注解写的比较详细,加上很多东西在前面几篇 blog 已经讲过了,所以这里直接上代码。
文件上传接口
/** * Created by moon.zhong on 2015/3/3. */ public class UploadApi { /** * 上传图片接口 * @param bitmap 需要上传的图片 * @param listener 请求回调 */ public static void uploadImg(Bitmap bitmap,ResponseListener listener){ List<FormImage> imageList = new ArrayList<FormImage>() ; imageList.add(new FormImage(bitmap)) ; Request request = new PostUploadRequest(Constant.UploadHost,imageList,listener) ; VolleyUtil.getRequestQueue().add(request) ; } }
图片上传验证
上传类PostUploadActivity.java
/** * Created by moon.zhong on 2015/3/2. */ public class PostUploadActivity extends ActionBarActivity { private TextView mShowResponse ; private ImageView mImageView ; private ProgressDialog mDialog ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_upload_img); mShowResponse = (TextView) findViewById(R.id.id_show_response) ; mImageView = (ImageView) findViewById(R.id.id_show_img) ; mDialog = new ProgressDialog(this) ; mDialog.setCanceledOnTouchOutside(false); } public void uploadImg(View view){ mDialog.setMessage("图片上传中..."); mDialog.show(); Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.logo) ; UploadApi.uploadImg(bitmap,new ResponseListener<String>() { @Override public void onErrorResponse(VolleyError error) { Log.v("zgy","===========VolleyError========="+error) ; mShowResponse.setText("ErrorResponse\n"+error.getMessage()); Toast.makeText(PostUploadActivity.this,"上传失败",Toast.LENGTH_SHORT).show() ; mDialog.dismiss(); } @Override public void onResponse(String response) { response = response.substring(response.indexOf("img src=")); response = response.substring(8,response.indexOf("/>")) ; Log.v("zgy","===========onResponse========="+response) ; mShowResponse.setText("图片地址:\n"+response); mDialog.dismiss(); Toast.makeText(PostUploadActivity.this,"上传成功",Toast.LENGTH_SHORT).show(); } }) ; } }
测试结果如下: 上传图片页面:
图片上传中
图片上传成功,地址为http://www.chuantu.biz/t/67/1425474351x-1376440163.png
通过网页请求
可以看到,volley 实现文件上传的操作还是很方便的,不过,不知道大家看到这里有没有觉得哪里有问题呢?其实 volley 实现文件上传是有一个很大的问题,什么问题呢,大家自己先想想,我将会在后续的文章中讲到这个问题,并提供解决方案(是后续,不是下一篇)。volley 讲到这里为止,对于它的功能也讲了一大部分,不过还有一个非常有用的知识点没有讲到,那就是volley缓存机制,下一节,将开启 volley 的缓存之旅,敬请期待!