android上实现multi-part上传
2015-09-08 00:11
465 查看
最近做http的图片上传,约定好用http的multipart实现。遇到的一些坑,网上查不到不到解决方案,自己解决了,在这儿记录下来。
android自带的http实现有两种,一个是java的HttpUrlConnection,一个是apache的HttpClient,在项目里用的是后者,因为感觉它的封装程度使用起来更方便些。
之前使用的是一个国人写的基于HttpClient的框架,用着各种坑。最近接入七牛的sdk时发现人家用的是android-async-http,于是去github找到了这个,看起来挺不错的,决定换之。幸好之前觉得国人那个框架不靠谱,自己封装了个中间层,换库代价没那么大。
大坑仍然是multi-part上传图片,因为上传前有时需要压缩图片,所以传的参数是InputStream而不是File。虽然也可以把InputStream转为File或bytes,但感觉那样太蛋疼了,还是想直接传,于是问题来了。
发现,这个库上传图片的InputStream时,进度马上走到100%,像是被直接读进缓存了。这个当然不行,进度条完全就一摆设,上网查到了说法:
https://github.com/loopj/android-async-http/issues/118
原来是这个库的缺陷,有个家伙把文件上传进度失效的问题搞了,但是InputStream的没搞,虽然自己搞完后感觉人家那种搞法并没什么卵用。求人不如求己,决定自己改下这个库。
multipart的实现主要在SimpleMultipartEntity里面,isChunked()直接返回的false,十分怀疑这个能否实现文件的上传进度。学人家解决文件上传的方式处理了一下InputStream,结果进度还是直接到100%,所以这种方法并没用。七牛能实现进度,是因为他们自己把文件分成了多块,用多个请求分别上传,对于我并不适用。
本以为这样万事大吉了,没想到9个上传请求发出时进度条都很快走完,然后卡在100%,最后成功几个,挂掉几个。
查log发现,挂掉的都是SocetTimeout。联想进度条瞬间走完的表现,可以推测出,这些请求并没有按预期实现chunk上传。而是都先读进缓存,最后9个几百k的数据抢着上传,抢慢的就超时了。android-async-http默认的socket超时是10秒,改成30秒后果然好了很多。
在grepcode上找到了4.1.3的MultipartEntity的源码,发现其实只要传进了InputStreamPart,他是会使用chunk上传的。带着疑惑用fiddler抓包看了下,这个multipart上传确实是chunk编码的。
难道chunk编码的chunk size太大了?上网查了下,默认是2k,排除这个因素。
看AsynHttpClient代码,HttpConnectionsParams已经setTcpNoDelay为true,setSocketBufferSize为8192,应该也不存在数据留在缓冲区不发出去的问题。
决定看下writeTo的那个OutputStream的情况,断点调试发现是一个ChunkedOutputStream,去grepcode看源码,发现包了个SocketOutputBuffer,层层递进,最后能找到SocketImpl!
有了这个,就能直接确定socket的状况了。于是,通过这个OutputStream层层反射拿到了SocketImpl实例。断点调试一查看,send buffer的大小竟然有1m多。
妹的,HttpConnectionsParams的setSocketBufferSize方法根本不是我以为的效果啊。不过也没那么多精力再去细究了,反正httpclient的库应该是比较固定的,决定hack一下。
android自带的http实现有两种,一个是java的HttpUrlConnection,一个是apache的HttpClient,在项目里用的是后者,因为感觉它的封装程度使用起来更方便些。
之前使用的是一个国人写的基于HttpClient的框架,用着各种坑。最近接入七牛的sdk时发现人家用的是android-async-http,于是去github找到了这个,看起来挺不错的,决定换之。幸好之前觉得国人那个框架不靠谱,自己封装了个中间层,换库代价没那么大。
大坑仍然是multi-part上传图片,因为上传前有时需要压缩图片,所以传的参数是InputStream而不是File。虽然也可以把InputStream转为File或bytes,但感觉那样太蛋疼了,还是想直接传,于是问题来了。
发现,这个库上传图片的InputStream时,进度马上走到100%,像是被直接读进缓存了。这个当然不行,进度条完全就一摆设,上网查到了说法:
https://github.com/loopj/android-async-http/issues/118
原来是这个库的缺陷,有个家伙把文件上传进度失效的问题搞了,但是InputStream的没搞,虽然自己搞完后感觉人家那种搞法并没什么卵用。求人不如求己,决定自己改下这个库。
multipart的实现主要在SimpleMultipartEntity里面,isChunked()直接返回的false,十分怀疑这个能否实现文件的上传进度。学人家解决文件上传的方式处理了一下InputStream,结果进度还是直接到100%,所以这种方法并没用。七牛能实现进度,是因为他们自己把文件分成了多块,用多个请求分别上传,对于我并不适用。
RequestParams: public HttpEntity getEntity(ResponseHandlerInterface progressHandler) throws IOException { if (useJsonStreamer) { return createJsonStreamerEntity(progressHandler); } else if (!forceMultipartEntity && streamParams.isEmpty() && fileParams.isEmpty() && fileArrayParams.isEmpty()) { return createFormEntity(); } else { return createMultipartEntity(progressHandler); } }找到RequestParams,发现其实他的成员变量都是protected,不过createMultipartEntity是private的,只能改为protected再重写。
public class IhpRequestParams extends RequestParams { private static final Charset UTF_8 = Charset.forName("UTF-8"); @Override protected HttpEntity createMultipartEntity(final ResponseHandlerInterface progressHandler) throws IOException { MultipartEntity multipartEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, UTF_8) { @Override public void writeTo(OutputStream outstream) throws IOException { super.writeTo(new CountingOutputStream(outstream, getTotalLength(), progressHandler)); } }; for (BasicNameValuePair nameValuePair : getParamsList()) { multipartEntity.addPart(new FormBodyPart(nameValuePair.getName(), new StringBody(nameValuePair.getValue(), UTF_8))); } for (ConcurrentHashMap.Entry<String, StreamWrapper> entry : streamParams.entrySet()) { multipartEntity.addPart(entry.getKey(), new InputStreamBody(entry.getValue().inputStream, entry.getValue().contentType, entry.getValue().name)); } return multipartEntity; } private int getTotalLength() { int length = 0; for (ConcurrentHashMap.Entry<String, StreamWrapper> entry : streamParams.entrySet()) { try { length += entry.getValue().inputStream.available(); } catch (IOException e) { e.printStackTrace(); } } return length; } public class CountingOutputStream extends FilterOutputStream { private long transferred; private long totalLength; private long progress; private ResponseHandlerInterface responseHandler; public CountingOutputStream(final OutputStream out, long totalLength, ResponseHandlerInterface responseHandler) { super(out); this.totalLength = totalLength; this.transferred = 0; this.responseHandler = responseHandler; } @Override public void write(int oneByte) throws IOException { super.write(oneByte); if (null == responseHandler) { return; } this.transferred ++; long progressNew = transferred * 100 / totalLength; if (progressNew > progress || progressNew >= 100) { progress = progressNew; responseHandler.sendProgressMessage(transferred, totalLength); } } } }引用了httpmime-4.1.3的MultipartEntity, 其实比较新的httpmime库已经推荐用MultipartEntityBuilder了,但没怎么去了解。反正用这个有问题还可能通过继承解决,用builder如果有问题的话继承都没得搞了。进度条的问题通过包装OutputStream解决。
本以为这样万事大吉了,没想到9个上传请求发出时进度条都很快走完,然后卡在100%,最后成功几个,挂掉几个。
查log发现,挂掉的都是SocetTimeout。联想进度条瞬间走完的表现,可以推测出,这些请求并没有按预期实现chunk上传。而是都先读进缓存,最后9个几百k的数据抢着上传,抢慢的就超时了。android-async-http默认的socket超时是10秒,改成30秒后果然好了很多。
在grepcode上找到了4.1.3的MultipartEntity的源码,发现其实只要传进了InputStreamPart,他是会使用chunk上传的。带着疑惑用fiddler抓包看了下,这个multipart上传确实是chunk编码的。
难道chunk编码的chunk size太大了?上网查了下,默认是2k,排除这个因素。
看AsynHttpClient代码,HttpConnectionsParams已经setTcpNoDelay为true,setSocketBufferSize为8192,应该也不存在数据留在缓冲区不发出去的问题。
决定看下writeTo的那个OutputStream的情况,断点调试发现是一个ChunkedOutputStream,去grepcode看源码,发现包了个SocketOutputBuffer,层层递进,最后能找到SocketImpl!
有了这个,就能直接确定socket的状况了。于是,通过这个OutputStream层层反射拿到了SocketImpl实例。断点调试一查看,send buffer的大小竟然有1m多。
妹的,HttpConnectionsParams的setSocketBufferSize方法根本不是我以为的效果啊。不过也没那么多精力再去细究了,反正httpclient的库应该是比较固定的,决定hack一下。
public static void setChunkedSocketSendBufferSize(ChunkedOutputStream out, int size) { SocketOutputBuffer socketOutputBuffer = getOutputBuffer(out); if (null == socketOutputBuffer) { return; } SocketImpl socketImpl = getSocket(socketOutputBuffer); if (null != socketImpl) { try { socketImpl.setOption(SocketOptions.SO_SNDBUF, size); } catch (SocketException e) { e.printStackTrace(); } 4000 } } public static SocketOutputBuffer getOutputBuffer(ChunkedOutputStream os) { Class c = ChunkedOutputStream.class; try { Field field = c.getDeclaredField("out"); field.setAccessible(true); return (SocketOutputBuffer) field.get(os); } catch (Exception e) { e.printStackTrace(); return null; } } public static SocketImpl getSocket(SocketOutputBuffer socketOutputBuffer) { try { OutputStream outputStream = getSocketOutputStream(socketOutputBuffer); Class c = outputStream.getClass(); Field field = c.getDeclaredField("socketImpl"); field.setAccessible(true); return (SocketImpl) field.get(outputStream); } catch (Exception e) { return null; } } public static OutputStream getSocketOutputStream(SocketOutputBuffer socketOutputBuffer) { try { Class c = AbstractSessionOutputBuffer.class; Field field = c.getDeclaredField("outstream"); field.setAccessible(true); return (OutputStream) field.get(socketOutputBuffer); } catch (Exception e) { return null; } }通过各种反射把socket的send buffer size改为8k, 终于成功实现了正常的上传进度,把自己都感动了。。。。。
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories