Android上传下载文件进度监听,大文件(500M以内)上传

1.普通文件的上传下载采用的是retrofit+rxjava的形式;

1.1  依赖

    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    implementation 'io.reactivex:rxandroid:1.2.1'

1.2 retrofit接口

    @GET("{filePath}")
    Observable<ResponseBody> getFile(@Path("filePath") String filePath,
                                       @Query("token") String token);


    @Multipart
    @POST(ApiConstants.FILE_UPLOAD_URL)
    Observable<ResponseBody> uploadFile(@Part("type") RequestBody type,
                                    @PartMap Map<String, RequestBody> file);

    @Multipart
    @POST(ApiConstants.FILE_UPLOAD_URL)
    Observable<ResponseBody> uploadFile2(@Part("type") RequestBody type,
                                            @Part MultipartBody.Part file);

上传文件使用@Multiparty和@post方式,参数可以使用@PartMap(可以上传多个文件)或@Part的MultipartBody来传输

1.3 传输

1.3.1 上传多个文件

Map<String, RequestBody> fileMap = new HashMap<>();
            RequestBody photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file1);
            fileMap.put("file\";filename=\"" + file1.getName(), photoRequestBody);
RequestBody photoRequestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file2);
            fileMap.put("file\";filename=\"" + file2.getName(), photoRequestBody2);

使用RequestBody的create方法创建对象,并放入map中;

ApiClient.initService(INoticeDetailBiz.class,
                ApiConstants.FILE_SERVER, listener)
                .uploadFile(RequestBody.create(MediaType.parse("text/plain"),"nstest",
                            fileMap)
                .compose(RxSchedulerUtils.normalSchedulersTransformer())
                .subscribe(new Observer<ResponseBody>() {
                    
                });

上面代码就是rxjava的调用方式了,不再多说;

1.3.2  上传单个文件MultiPart.Part

RequestBody requestBody1 = RequestBody.create(MediaType.parse("application/octet-stream"), file1);
MultipartBody.Part partFile = MultipartBody.Part.createFormData("file", file1.getName(), requestBody1);

使用MultipartBody.Part.createFormData方法创建接口所需的类型

ApiClient.initService(INoticeDetailBiz.class,
                ApiConstants.FILE_SERVER, listener)
                .uploadFile2(RequestBody.create(MediaType.parse("text/plain"),"nstest",
                            partFile)
                .compose(RxSchedulerUtils.normalSchedulersTransformer())
                .subscribe(new Observer<ResponseBody>() {
                    
                });

同样的rxjava调用

1.4  ApiClient类

private static Converter.Factory gsonConverterFactory = GsonConverterFactory.create(new Gson());
    private static CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create();
public static <T> T initService(Class<T> clazz, String baseUrl,
                                    ProgressListener listener) {
        OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
        builder.connectTimeout(connectionTime, TimeUnit.SECONDS);
        builder.readTimeout(readTime, TimeUnit.SECONDS);
        builder.writeTimeout(writeTime, TimeUnit.SECONDS);
        builder.addNetworkInterceptor(new StethoInterceptor());
        builder.addInterceptor(new ErrorInterceptor());
        HttpLoggingInterceptor httpLoggingInterceptor;
        if (BuildConfig.DEBUG) {
            httpLoggingInterceptor = new HttpLoggingInterceptor();
        } else {
            httpLoggingInterceptor = new HttpLoggingInterceptor(
                    message -> logger.debug(message));
        }
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(httpLoggingInterceptor);
        if (null != listener) {
            if (listener.isDownload()) {
                builder.addInterceptor(new DownloadInterceptor(listener));
            } else {
                builder.addInterceptor(new UploadInterceptor(listener));
            }
        }
        Retrofit retrofit = new Retrofit.Builder()
                .client(builder.build())
                .baseUrl(baseUrl)
                .addConverterFactory(gsonConverterFactory)
                .addCallAdapterFactory(rxJavaCallAdapterFactory)
                .build();

        return retrofit.create(clazz);
    }

在这里,发现了三个新对象

ProgressListener  //监听进度的接口
DownloadInterceptor   //下载的拦截器
UploadInterceptor   //上传的拦截器

2. 上传和下载进度监听的实现

2.1 进度监听接口

public interface ProgressListener {
    void onProgress(int currentLength);//当前的进度,已经是百分数了
    boolean isDownload();//返回true表示下载,false表示上传
}

2.2  上传进度监听

上传拦截器

package com.dtrt.preventpro.myhttp.download;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class UploadInterceptor implements Interceptor {
    private ProgressListener listener;

    public UploadInterceptor(ProgressListener listener) {
        this.listener = listener;
    }
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request build = request.newBuilder().post(new ProgressRequestBody(request.body(), listener)).build();
        return chain.proceed(build);
    }
}

ProgressRequestBody.java

package com.dtrt.preventpro.myhttp.download;

import java.io.IOException;

import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.ForwardingSink;
import okio.ForwardingSource;
import okio.Okio;
import okio.Sink;
import okio.Source;

public class ProgressRequestBody extends RequestBody {
    private final RequestBody requestBody;
    private final ProgressListener progressListener;
    private BufferedSink bufferedSink;

    public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
        this.requestBody = requestBody;
        this.progressListener = progressListener;
    }

    @Override
    public MediaType contentType() {
        return requestBody.contentType();
    }


    @Override
    public long contentLength() throws IOException {
        return requestBody.contentLength();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        if (bufferedSink == null) {
            //包装
            Sink sk = sink(sink);
            bufferedSink = Okio.buffer(sk );
        }
        //写入
        requestBody.writeTo(bufferedSink);
        //必须调用flush,否则最后一部分数据可能不会被写入
        bufferedSink.flush();
    }

    private Sink sink(Sink sink) {
        return new ForwardingSink(sink) {
            //当前写入字节数
            long bytesWritten = 0L;
            //总字节长度,避免多次调用contentLength()方法
            long contentLength = 0L;

            @Override
            public void write(Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
                if (contentLength == 0) {
                    //获得contentLength的值,后续不再调用
                    contentLength = contentLength();
                }
                //增加当前写入的字节数
                bytesWritten += byteCount;
                //回调
                if (progressListener != null) {
                    progressListener.onProgress((int)(bytesWritten*1.0f/requestBody.contentLength()*100));
                }
            }
        };
    }

}

其实就是继承RequestBody,把进度监听器传进去,在writeTo()方法中对buffer进行操作;其中会把写入的字节数累加下来,然后算出百分数,通过进度接口回调回去;所以在上面代码中的rxjava调用里面传入了一个ProgressListener对象。

private ProgressListener uploadListener = new ProgressListener() {
        @Override
        public void onProgress(int currentLength) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tv_progress.setText("正在上传文件..."+currentLength+"%");
                }
            });
        }
        @Override
        public boolean isDownload() {
            return false;
        }
    };

这是页面写更新进度的地方,把这个对象传给ApiClient方法后;就会自动添加需要的拦截器;拦截器监听到进度变化后,又会回调回来更新ui;注意:这里进度是回调在子线程的,需要发到主线程更新ui。

2.3   同理,我们来看下载进度监听

下载拦截器

package com.dtrt.preventpro.myhttp.download;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class DownloadInterceptor implements Interceptor {
    private ProgressListener downloadListener;

    public DownloadInterceptor(ProgressListener downloadListener) {
        this.downloadListener = downloadListener;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        return response.newBuilder()
                .body(new ProgressResponseBody(response.body(), downloadListener))
                .build();
    }
}

ProgressResponseBody.java

package com.dtrt.preventpro.myhttp.download;

import java.io.IOException;

import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Source;

public class ProgressResponseBody extends ResponseBody {
    private final ResponseBody responseBody;
    private final ProgressListener progressListener;
    private BufferedSource bufferedSource;

    public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
        this.responseBody = responseBody;
        this.progressListener = progressListener;
    }

    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }


    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(source(responseBody.source()));
        }
        return bufferedSource;
    }

    private Source source(Source source) {
        return new ForwardingSource(source) {
            long totalBytesRead = 0L;
            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink,byteCount);
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                if (progressListener != null) {
                    progressListener.onProgress((int)(totalBytesRead*1.0f/responseBody.contentLength()*100));
                }
                return bytesRead;
            }
        };
    }
}

同理,继承ResponseBody,把进度监听器传进去,在source()方法中对buffer进行操作;其中会把读到的字节数累加下来,然后算出百分数,通过进度接口回调回去;所以在上面代码中的rxjava调用里面传入了一个ProgressListener对象。

private ProgressListener downloadListener = new ProgressListener() {
        @Override
        public void onProgress(int currentLength) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tv_progress.setText("正在下载文件..."+currentLength+"%");
                }
            });
        }
        @Override
        public boolean isDownload() {
            return true;
        }
    };

这是页面写更新进度的地方,把这个对象传给ApiClient方法后;就会自动添加需要的拦截器;拦截器监听到进度变化后,又会回调回来更新ui;注意:这里进度是回调在子线程的,需要发到主线程更新ui。里面的isDownload()方法:下载时返回true,上传时返回false。

至此,对于普通的文件上传,下载以及进度监听就写完了;但我们的项目中要支持200M以下的大文件上传;通过上面的代码测试发现,华为手机能上传70M的文件,vivo手机只能上传50M的文件;不同手机能支持的上传大小还不一样;但超过10M的都属于大文件了。下面介绍一种大文件上传方式。

3.  大文件上传

网上查到可以使用apache的httpclient来上传,就来试试吧;

3.1 添加依赖

//httpclient上传大文件功能
    implementation group: 'org.apache.httpcomponents.client5' , name: 'httpclient5' , version: '5.0'
    implementation 'org.slf4j:slf4j-android:1.7.22'

这里用的时httpclient5。

3.2 上传

private void upload(File file, ProgressListener listener){
        HttpClient client = HttpClientBuilder.create().build();
        HttpPost httpPost = new HttpPost(ApiConstants.FILE_SERVER+ApiConstants.FILE_UPLOAD_URL);
        try {
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
            String filesKey = "file";
            multipartEntityBuilder.addPart(filesKey, new MyFileBody(file, new WriteListener() {
                @Override
                public void registerWrite(long amountOfBytesWritten) {
                    System.out.println("===========>"+amountOfBytesWritten);
                    listener.onProgress((int)(amountOfBytesWritten * 1.0f / notice.getFile().length() * 100));
                }
            }));
            // 第二个文件(多个文件的话,使用同一个key就行,后端用数组或集合进行接收即可)
            //File file2 = new File("C:\\Users\\JustryDeng\\Desktop\\头像.jpg");
            // 防止服务端收到的文件名乱码。 我们这里可以先将文件名URLEncode,然后服务端拿到文件名时在URLDecode。就能避免乱码问题。
            // 文件名其实是放在请求头的Content-Disposition里面进行传输的,如其值为form-data; name="files"; filename="头像.jpg"
            //multipartEntityBuilder.addBinaryBody(filesKey, file2, ContentType.DEFAULT_BINARY, URLEncoder.encode(file2.getName(), "utf-8"));
            // 其它参数(注:自定义contentType,设置UTF-8是为了防止服务端拿到的参数出现乱码)
            ContentType contentType = ContentType.create("text/plain", Charset.forName("UTF-8"));
            multipartEntityBuilder.addTextBody("type", "nstest", contentType);
            HttpEntity httpEntity = multipartEntityBuilder.build();
            httpPost.setEntity(httpEntity);
            //这里可以配置超时时间等其它参数
            //RequestConfig requestConfig = RequestConfig
            //   .custom()
            //   .setConnectionRequestTimeout(10, TimeUnit.MINUTES)
            //   .setConnectTimeout(10, TimeUnit.MINUTES)
            //   .setResponseTimeout(10, TimeUnit.MINUTES)
            //   .build();
            //httpPost.setConfig(requestConfig);

            CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpPost);
            System.out.println("HTTPS响应状态为:" + response.getCode());
            if (response.getCode() == 200 && response.getEntity() != null) {
                System.out.println("HTTPS响应内容长度为:" + response.getEntity().getContentLength());
                // 主动设置编码,来防止响应乱码
                String responseStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
                System.out.println("HTTPS响应内容为:" + responseStr);
                if (!TextUtils.isEmpty(responseStr)) {
                    notice.setCodes(responseStr);
                    commit(notice);
                }else {
                    //上传失败
                }
            }else {
                //上传失败
            }
        } catch (Exception e) {
            e.printStackTrace();
            //上传失败
        }
    }

其实和上面的上传文件类似;就是把文件包装成httpclient里的对象;然后根据返回码判断上传是否成功。这里有篇文章写的很详细:https://blog.csdn.net/justry_deng/article/details/81042379

重点看这里:

multipartEntityBuilder.addPart(filesKey, new MyFileBody(notice.getFile(), new WriteListener() {
    @Override
    public void registerWrite(long amountOfBytesWritten) {
        System.out.println("===========>"+amountOfBytesWritten);
        listener.onProgress((int)(amountOfBytesWritten * 1.0f / notice.getFile().length() * 100));
    }
}));

当向Entity对象中天加文件时,使用了MyFileBody:

package com.dtrt.preventpro.myhttp.download;

import org.apache.hc.client5.http.entity.mime.FileBody;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

public class MyFileBody extends FileBody {
    private WriteListener listener;
    public MyFileBody(File file, WriteListener listener) {
        super(file);
        this.listener = listener;
    }

    @Override
    public void writeTo(OutputStream out) throws IOException {
        OutputStreamProgress output = new OutputStreamProgress(out, listener);
        super.writeTo(output);
    }
    public interface WriteListener {
        void registerWrite(long amountOfBytesWritten);
    }
    public class OutputStreamProgress extends OutputStream {
        private final OutputStream outstream;
        private long bytesWritten=0;
        private final WriteListener writeListener;
        public OutputStreamProgress(OutputStream outstream, WriteListener writeListener) {
            this.outstream = outstream;
            this.writeListener = writeListener;
        }
        @Override
        public void write(int b) throws IOException {
            outstream.write(b);
            bytesWritten++;
            writeListener.registerWrite(bytesWritten);
        }
        @Override
        public void write(byte[] b) throws IOException {
            outstream.write(b);
            bytesWritten += b.length;
            writeListener.registerWrite(bytesWritten);
        }
        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            outstream.write(b, off, len);
            bytesWritten += len;
            writeListener.registerWrite(bytesWritten);
        }
        @Override
        public void flush() throws IOException {
            outstream.flush();
        }
        @Override
        public void close() throws IOException {
            outstream.close();
        }
    }
}

原理:MyFileBody继承了FileBody,在writeTo()方法中源码接受输出流的时候,我们把它换成安插了监听器的输出流;此时源码调用到写操作的时候,就可以触发监听器,监听写入的进度了。

OutputStreamProgress继承自输出流OutputStream,把监听器传进去,重写每个write方法,在写入操作的时候把写入的字节累加下来,并触发监听器;这样就实现了,大文件的上传。

页面的进度更新和上面的一样。使用这种方式传输,我测试的结果是可以上传500M以下的文件,大于500M的会报一个Socket管道断裂的错误。不过已经足够了,毕竟是手机,大文件传输用得地方很少。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值