OkHttp文件上传与下载的进度监听

说在前面


       要实现进度的监听,需要使用到OkHttp的依赖包Okio里的两个类,一个是Source,一个是Sink,至于Okio的东西,这里也不多说,其实OKHttp底层的实现就是基于Socket,大家都知道Socket网络编程,差不多就是操作Stream,所以OKHttp所有的操作都是依赖OKio这个IO操作库来做的,那么我们要实现一个下载监听,就应该对连接中的Stream监听,在OKHttp中网络请求基本都是RequestBody和ResponseBody这两个类封装了Stream的操作,所以下文就基于这两个类来介绍了。
       首先我们实现文件下载的进度监听。OkHttp给我们的只是一个回调,里面有Response返回结果,我们需要继承一个类,对结果进行监听,这个类就是ResponseBody,但是如何将它设置到OkHttp中去呢,答案是拦截器。拦截器的部分后面再叙述,这里先实现ResponseBody的子类ProgressResponseBody。
要监听进度,我们必然需要一个监听器,也就是一个接口,在其实现类中完成回调内容的处理,该接口声明如下:

/**   
* 包装的响体,处理进度
 */
 public interface ProgressResponseListener {
  void   onResponseProgress(long bytesRead, long contentLength,boolean done);
}

然后会使用到该接口:

 /**
  * 包装的响体,处理进度
 */
public class ProgressResponseBody extends ResponseBody {
//实际的待包装响应体
private final ResponseBody responseBody;
//进度回调接口
private final ProgressResponseListener progressListener;
//包装完成的BufferedSource
private BufferedSource bufferedSource;

/**
 * 构造函数,赋值
 * @param responseBody 待包装的响应体
 * @param progressListener 回调接口
 */
public ProgressResponseBody(ResponseBody responseBody, ProgressResponseListener progressListener) {
    this.responseBody = responseBody;
    this.progressListener = progressListener;
}


/**
 * 重写调用实际的响应体的contentType
 * @return MediaType
 */
@Override public MediaType contentType() {
    return responseBody.contentType();
}

/**
 * 重写调用实际的响应体的contentLength
 * @return contentLength
 * @throws IOException 异常
 */
@Override public long contentLength() throws IOException {
    return responseBody.contentLength();
}

/**
 * 重写进行包装source
 * @return BufferedSource
 * @throws IOException 异常
 */
@Override public BufferedSource source() throws IOException {
    if (bufferedSource == null) {
        //包装
        Source source = source(responseBody.source());
        bufferedSource = Okio.buffer();
    }
    return bufferedSource;
}

/**
 * 读取,回调进度接口
 * @param source Source
 * @return Source
 */
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);
            //增加当前读取的字节数,如果读取完成了bytesRead会返回-1
            totalBytesRead += bytesRead != -1 ? bytesRead : 0;
            //回调,如果contentLength()不知道长度,会返回-1
            progressListener.onResponseProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
            return bytesRead;
        }
    };
}
}

       类似装饰器,我们对原始的ResponseBody 进行了一层包装。并在其读取数据的时候设置了回调,回调的接口由构造函数传入,此外构造函数还传入了原始的ResponseBody,当系统内部调用了ResponseBody 的source方法的时候,返回的便是我们包装后的Source。然后我们还重写了几个方法调用原始的ResponseBody对应的函数返回结果。

       同理既然下载是这样,那么上传也应该是这样,我们乘热打铁完成上传的部分,下载是继承ResponseBody ,上传就是继承RequestBody,同时也应该还有一个监听器。

/**
 * 请求体进度回调接口,比如用于文件上传中
*/
public interface ProgressRequestListener {
    void onRequestProgress(long bytesWritten, long contentLength,     boolean done); 
}

RequestBody的子类实现类比ResponseBody ,基本上复制一下稍加修改即可使用:

 /**
   * 包装的请求体,处理进度
 */
public  class ProgressRequestBody extends RequestBody {
//实际的待包装请求体
private final RequestBody requestBody;
//进度回调接口
private final ProgressRequestListener progressListener;
//包装完成的BufferedSink
private BufferedSink bufferedSink;

/**
 * 构造函数,赋值
 * @param requestBody 待包装的请求体
 * @param progressListener 回调接口
 */
public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {
    this.requestBody = requestBody;
    this.progressListener = progressListener;
}

/**
 * 重写调用实际的响应体的contentType
 * @return MediaType
 */
@Override
public MediaType contentType() {
    return requestBody.contentType();
}

/**
 * 重写调用实际的响应体的contentLength
 * @return contentLength
 * @throws IOException 异常
 */
@Override
public long contentLength() throws IOException {
    return requestBody.contentLength();
}

/**
 * 重写进行写入
 * @param sink BufferedSink
 * @throws IOException 异常
 */
@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();

}

/**
 * 写入,回调进度接口
 * @param sink Sink
 * @return Sink
 */
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;
            //回调
            progressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
        }
    };
}}

       内部维护了一个原始的RequestBody 以及一个监听器,同样的也是由构造函数传入。当然也是要重写几个函数调用原始的RequestBody 对应的函数,文件的下载是read函数中进行监听的设置,毫无疑问文件的上传就是write函数了,我们在write函数中进行了类似的操作,并回调了接口中的函数。当系统内部调用了RequestBody 的writeTo函数时,我们对BufferedSink 进行了一层包装,即设置了进度监听,并返回了我们包装的BufferedSink 。于是乎,上传于下载的进度监听就完成了。

       我们还需要一个Helper类,对上传或者下载进行监听设置。文件的上传其实很简单,将我们的原始RequestBody和监听器 传入,返回我们的包装的ProgressRequestBody ,使用包装后的ProgressRequestBody 进行请求即可,但是文件的下载呢,OkHttp给我们返回的是Response,我们如何将我们包装的ProgressResponseBody设置进去呢,答案之前已经说过了,就是拦截器,具体见代码吧。

/**
   * 进度回调辅助类
   */
public class ProgressHelper {
/**
 * 包装OkHttpClient,用于下载文件的回调
 * @param client 待包装的OkHttpClient
 * @param progressListener 进度回调接口
 * @return 包装后的OkHttpClient,使用clone方法返回
 */
public static OkHttpClient addProgressResponseListener(OkHttpClient client,final ProgressResponseListener progressListener){
    //克隆
    OkHttpClient clone = client.clone();
    //增加拦截器
    clone.networkInterceptors().add(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            //拦截
            Response originalResponse = chain.proceed(chain.request());
            //包装响应体并返回
            return originalResponse.newBuilder()
                    .body(new ProgressResponseBody(originalResponse.body(), progressListener))
                    .build();
        }
    });
    return clone;
}

/**
 * 包装请求体用于上传文件的回调
 * @param requestBody 请求体RequestBody
 * @param progressRequestListener 进度回调接口
 * @return 包装后的进度回调请求体
 */
public static ProgressRequestBody addProgressRequestListener(RequestBody requestBody,ProgressRequestListener progressRequestListener){
    //包装请求体
    return new ProgressRequestBody(requestBody,progressRequestListener);
}}

       对于文件下载的监听器我们为了不影响原来的OkHttpClient 实例,我们调用clone方法进行了克隆,之后对克隆的方法设置了响应拦截,并返回该克隆的实例。而文件的上传则十分简单,直接包装后返回即可。

       但是你别忘记了,我们的目的是在UI层进行回调,而OkHttp的所有请求都不在UI层。于是我们还要实现我们写的接口,进行UI操作的回调。由于涉及到消息机制,我们对之前的两个接口回调传的参数进行封装,封装为一个实体类便于传递。

/**
 * UI进度回调实体类
 */
public class ProgressModel implements Serializable {
//当前读取字节长度
private long currentBytes;
//总字节长度
private long contentLength;
//是否读取完成
private boolean done;

public ProgressModel(long currentBytes, long contentLength, boolean done) {
    this.currentBytes = currentBytes;
    this.contentLength = contentLength;
    this.done = done;
}

public long getCurrentBytes() {
    return currentBytes;
}

public void setCurrentBytes(long currentBytes) {
    this.currentBytes = currentBytes;
}

public long getContentLength() {
    return contentLength;
}

public void setContentLength(long contentLength) {
    this.contentLength = contentLength;
}

public boolean isDone() {
    return done;
}

public void setDone(boolean done) {
    this.done = done;
}

@Override
public String toString() {
    return "ProgressModel{" +
            "currentBytes=" + currentBytes +
            ", contentLength=" + contentLength +
            ", done=" + done +
            '}';
}}

       再实现我们的UI回调接口,对于文件的上传,我们需要实现的是ProgressRequestListener接口,文件的下载需要实现的是ProgressResponseListener接口,但是内部的逻辑处理是完全一样的。我们使用抽象类,提供一个抽象方法,该抽象方法用于UI层回调的处理,由具体开发去实现。涉及到消息机制就涉及到Handler类,在Handler的子类中维护一个弱引用指向外部类(用到了static防止内存泄露,但是需要调用外部类的一个非静态函数,所以将外部类引用直接由构造函数传入,在内部通过调用该引用的方法去实现),然后将主线程的Looper传入,调用父类构造函数。在onRequestProgress中发送进度更新的消息,在handleMessage函数中回调我们的抽象方法。我们只需要实现抽象方法,编写对应的UI更新代码即可。具体代码如下。

/**
* 请求体回调实现类,用于UI层回调
*/
public abstract class UIProgressRequestListener implements ProgressRequestListener {
private static final int REQUEST_UPDATE = 0x01;

//处理UI层的Handler子类
private static class UIHandler extends Handler {
    //弱引用
    private final WeakReference<UIProgressRequestListener> mUIProgressRequestListenerWeakReference;

    public UIHandler(Looper looper, UIProgressRequestListener uiProgressRequestListener) {
        super(looper);
        mUIProgressRequestListenerWeakReference = new WeakReference<UIProgressRequestListener>(uiProgressRequestListener);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case REQUEST_UPDATE:
                UIProgressRequestListener uiProgressRequestListener = mUIProgressRequestListenerWeakReference.get();
                if (uiProgressRequestListener != null) {
                    //获得进度实体类
                    ProgressModel progressModel = (ProgressModel) msg.obj;
                    //回调抽象方法
                    uiProgressRequestListener.onUIRequestProgress(progressModel.getCurrentBytes(), progressModel.getContentLength(), progressModel.isDone());
                }
                break;
            default:
                super.handleMessage(msg);
                break;
        }
    }
}
//主线程Handler
private final Handler mHandler = new UIHandler(Looper.getMainLooper(), this);

@Override
public void onRequestProgress(long bytesRead, long contentLength, boolean done) {
    //通过Handler发送进度消息
    Message message = Message.obtain();
    message.obj = new ProgressModel(bytesRead, contentLength, done);
    message.what = REQUEST_UPDATE;
    mHandler.sendMessage(message);
}

/**
 * UI层回调抽象方法
 * @param bytesWrite 当前写入的字节长度
 * @param contentLength 总字节长度
 * @param done 是否写入完成
 */
public abstract void onUIRequestProgress(long bytesWrite, long contentLength, boolean done);}

另一个实现类代码雷同,不做叙述。

/**
* 请求体回调实现类,用于UI层回调
 */
public abstract class UIProgressResponseListener implements ProgressResponseListener {
private static final int RESPONSE_UPDATE = 0x02;
//处理UI层的Handler子类
private static class UIHandler extends Handler {
    //弱引用
    private final WeakReference<UIProgressResponseListener> mUIProgressResponseListenerWeakReference;

    public UIHandler(Looper looper, UIProgressResponseListener uiProgressResponseListener) {
        super(looper);
        mUIProgressResponseListenerWeakReference = new WeakReference<UIProgressResponseListener>(uiProgressResponseListener);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case RESPONSE_UPDATE:
                UIProgressResponseListener uiProgressResponseListener = mUIProgressResponseListenerWeakReference.get();
                if (uiProgressResponseListener != null) {
                    //获得进度实体类
                    ProgressModel progressModel = (ProgressModel) msg.obj;
                    //回调抽象方法
                    uiProgressResponseListener.onUIResponseProgress(progressModel.getCurrentBytes(), progressModel.getContentLength(), progressModel.isDone());
                }
                break;
            default:
                super.handleMessage(msg);
                break;
        }
    }
}
//主线程Handler
private final Handler mHandler = new UIHandler(Looper.getMainLooper(), this);

@Override
public void onResponseProgress(long bytesRead, long contentLength, boolean done) {
    //通过Handler发送进度消息
    Message message = Message.obtain();
    message.obj = new ProgressModel(bytesRead, contentLength, done);
    message.what = RESPONSE_UPDATE;
    mHandler.sendMessage(message);
}

/**
 * UI层回调抽象方法
 * @param bytesRead 当前读取响应体字节长度
 * @param contentLength 总字节长度
 * @param done 是否读取完成
 */
public abstract void onUIResponseProgress(long bytesRead, long contentLength, boolean done);}

       一个上传操作,一个下载操作,分别提供了UI层与非UI层回调的示例。最终代码中使用的监听器都是UI层的,因为我们要更新进度条。

private void download() {
    //这个是非ui线程回调,不可直接操作UI
    final ProgressResponseListener progressResponseListener = new ProgressResponseListener() {
        @Override
        public void onResponseProgress(long bytesRead, long contentLength, boolean done) {
            Log.e("TAG", "bytesRead:" + bytesRead);
            Log.e("TAG", "contentLength:" + contentLength);
            Log.e("TAG", "done:" + done);
            if (contentLength != -1) {
                //长度未知的情况下回返回-1
                Log.e("TAG", (100 * bytesRead) / contentLength + "% done");
            }
            Log.e("TAG", "================================");
        }
    };


    //这个是ui线程回调,可直接操作UI
    final UIProgressResponseListener uiProgressResponseListener = new UIProgressResponseListener() {
        @Override
        public void onUIResponseProgress(long bytesRead, long contentLength, boolean done) {
            Log.e("TAG", "bytesRead:" + bytesRead);
            Log.e("TAG", "contentLength:" + contentLength);
            Log.e("TAG", "done:" + done);
            if (contentLength != -1) {
                //长度未知的情况下回返回-1
                Log.e("TAG", (100 * bytesRead) / contentLength + "% done");
            }
            Log.e("TAG", "================================");
            //ui层回调
            downloadProgeress.setProgress((int) ((100 * bytesRead) / contentLength));
            //Toast.makeText(getApplicationContext(), bytesRead + " " + contentLength + " " + done, Toast.LENGTH_LONG).show();
        }
    };

    //构造请求
    final Request request1 = new Request.Builder()
            .url("http://121.41.119.107:81/test/1.doc")
            .build();

    //包装Response使其支持进度回调
    ProgressHelper.addProgressResponseListener(client, uiProgressResponseListener).newCall(request1).enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
            Log.e("TAG", "error ", e);
        }

        @Override
        public void onResponse(Response response) throws IOException {
            Log.e("TAG", response.body().string());
        }
    });
}

private void upload() {
    File file = new File("/sdcard/1.doc");
    //此文件必须在手机上存在,实际情况下请自行修改,这个目录下的文件只是在我手机中存在。


    //这个是非ui线程回调,不可直接操作UI
    final ProgressRequestListener progressListener = new ProgressRequestListener() {
        @Override
        public void onRequestProgress(long bytesWrite, long contentLength, boolean done) {
            Log.e("TAG", "bytesWrite:" + bytesWrite);
            Log.e("TAG", "contentLength" + contentLength);
            Log.e("TAG", (100 * bytesWrite) / contentLength + " % done ");
            Log.e("TAG", "done:" + done);
            Log.e("TAG", "================================");
        }
    };


    //这个是ui线程回调,可直接操作UI
    final UIProgressRequestListener uiProgressRequestListener = new UIProgressRequestListener() {
        @Override
        public void onUIRequestProgress(long bytesWrite, long contentLength, boolean done) {
            Log.e("TAG", "bytesWrite:" + bytesWrite);
            Log.e("TAG", "contentLength" + contentLength);
            Log.e("TAG", (100 * bytesWrite) / contentLength + " % done ");
            Log.e("TAG", "done:" + done);
            Log.e("TAG", "================================");
            //ui层回调
            uploadProgress.setProgress((int) ((100 * bytesWrite) / contentLength));
            //Toast.makeText(getApplicationContext(), bytesWrite + " " + contentLength + " " + done, Toast.LENGTH_LONG).show();
        }
    };

    //构造上传请求,类似web表单
    RequestBody requestBody = new MultipartBuilder().type(MultipartBuilder.FORM)
            .addFormDataPart("hello", "android")
            .addFormDataPart("photo", file.getName(), RequestBody.create(null, file))
            .addPart(Headers.of("Content-Disposition", "form-data; name=\"another\";filename=\"another.dex\""), RequestBody.create(MediaType.parse("application/octet-stream"), file))
            .build();

    //进行包装,使其支持进度回调
    final Request request = new Request.Builder().url("").post(ProgressHelper.addProgressRequestListener(requestBody, uiProgressRequestListener)).build();
    //开始请求
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
            Log.e("TAG", "error ", e);
        }

        @Override
        public void onResponse(Response response) throws IOException {
            Log.e("TAG", response.body().string());
        }
    });

}

       还有一个细节需要注意就是连接的超时,读取与写入数据的超时时间的设置,在读取或者写入一些大文件的时候如果不设置这个参数可能会报异常,这里就随便设置了一下值,设得有点大,实际情况按需设置。

 //设置超时,不设置可能会报异常
private void initClient() {
    client.setConnectTimeout(1000, TimeUnit.MINUTES);
    client.setReadTimeout(1000, TimeUnit.MINUTES);
    client.setWriteTimeout(1000, TimeUnit.MINUTES);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容