如何使用Retrofit下载大文件

本文介绍了使用Retrofit框架下载大文件时可能遇到的问题及解决方案,包括如何避免内存溢出和处理NetworkOnMainThreadException异常。

背景

姑且大家都知道现在网络框架多的不可开交,但是很多框架能处理很多事比如即可以请求图片,又可以请求数据接口等等,获取很多初学者认为这是一个很好的事情,但是我觉得并不是,因为功能越多,意味着很难在某个功能上超过专门做这个功能库强,并且bug和维护肯定也不会很好。其实在软件设计领域有一个原则那就是单一职责原则,跟我所说的你需要什么数据就选择专门请求数据的网络框架不谋而合,因为一个库能把一件事做好就很不错了。
在上面原则的基础上,所以目前来说单纯的网络请求库就锁定在了 Volley、OkHttp、Retrofit 三个,android-async-http 的作者已经不维护(该框架是基于HttpClient开发的,因为后来Google不建议使用了,s所以有可能就该作者不维护的原因),所以这里就不多说了,下面我们分别来说说这三个库的区别。

OkHttp

OkHttp 是 Square 公司开源的针对 Java 和 Android 程序,封装的一个高性能 http 请求库,所以它的职责跟 HttpUrlConnection 是一样的,支持 spdy、http 2.0、websocket ,支持同步、异步,而且 OkHttp 又封装了线程池,封装了数据转换,封装了参数使用、错误处理等,api 使用起来更加方便。可以把它理解成是一个封装之后的类似 HttpUrlConnection 的一个东西,但是你在使用的时候仍然需要自己再做一层封装,这样才能像使用一个框架一样更加顺手。

Volley

Volley 是 Google 官方出的一套小而巧的异步请求库,该框架封装的扩展性很强,支持 HttpClient、HttpUrlConnection,甚至支持 OkHttp,而且 Volley 里面也封装了 ImageLoader ,所以如果你愿意你甚至不需要使用图片加载框架,不过这块功能没有一些专门的图片加载框架强大,对于简单的需求可以使用,对于稍复杂点的需求还是需要用到专门的图片加载框架。
Volley的缺陷,比如不支持 post 大数据,所以不适合上传文件。不过 Volley 设计的初衷本身也就是为频繁的、数据量小的网络请求而生!

Retrofit

Retrofit 是 Square 公司出品的默认基于 OkHttp 封装的一套 RESTful 网络请求框架,不了解 RESTful 概念的不妨去搜索学习下,RESTful 可以说是目前流行的一套 api 设计的风格,并不是标准。Retrofit 的封装可以说是很强大,里面涉及到一堆的设计模式,你可以通过注解直接配置请求,你可以使用不同的 http 客户端,虽然默认是用 http ,可以使用不同 Json Converter 来序列化数据,同时提供对 RxJava 的支持,使用 Retrofit + OkHttp + RxJava + Dagger2 可以说是目前比较潮的一套框架,但是需要有比较高的门槛。

下载Retrofit下载大文件踩过的坑

OOM(out of memory)

1.这里一个情景是这样的,开始下载文件都比较小,并且项目组为了定位问题打开了log监听器,然而项目上线了,一次后台配置了一个比较大的文件然后,用户反馈崩溃,然后查看后台日志追踪,发现基本都是OOM导致。
a:定位问题:发现是把日志监听器打开了,日志监听器打开会导致每次都把整个文件加载到内存,这样这样我们就由想而知了,OOM势在必得。问题代码如下:

    OkHttpClient okHttpClient = new OkHttpClient().newBuilder()//
                .readTimeout(5, TimeUnit.SECONDS)//
                .connectTimeout(5, TimeUnit.SECONDS)//
                .addInterceptor(new DownloadProgressInterceptor())
                .addInterceptor(new HttpLoggingInterceptor()
                .setLevel(HttpLoggingInterceptor.Level.BODY))
                .build();

b:解决问题:将日志监听代码注释掉,所以这里有个建议,只有开发阶段才打开日志监听器开关

OkHttpClient okHttpClient = new OkHttpClient().newBuilder()//
                .readTimeout(5, TimeUnit.SECONDS)//
                .connectTimeout(5, TimeUnit.SECONDS)//
                .addInterceptor(new DownloadProgressInterceptor())
                .build();

2.基于上面那个情景继续讲,前面那种方式基本可以解决百分之95问题了,开始我们以为已经解决了相关的问题了,但是后来又有用户陆陆续续反馈,程序总是崩溃,然后查看后台依然有好几个是因为OOM导致的。这下就懵逼了,我们不是已经解决了吗?然后才发现前面那种方式根本没有解决实质问题,换句话说前面日志监听器打开会导致每次都把整个文件加载到内存。所以当我们关闭时,极大部分是可以解决相关问题,但是也有一种情况不能排除,就是当下载特别大的文件,也有可能出现OOM,具体是什么原因导致的,有可能是Retrofit处理的问题吧。所以针对这种情况,我这里提供一种百分之百保险的方法。那就是使用注解@Streaming。Streaming意味着立刻传递字节码,而不需要把整个文件读进内存,下面我就让我们一起看下怎么使用吧。
a.那时我们遇到的android.os.NetworkOnMainThreadException。
这里有一个很坑爹的问题就是,当我们使用上面注解时,发现总是点击了下载没反应,程序也没报错。后来查看日志终于发现原来是抛了一个这样的Exception,之所以程序没闪退是因为恰好我的程序又有捕获。而且主要我们都是异步调用啊,还有NetworkOnMainThreadException,真是坑爹啊。后来查看源码才发现,因为我们调用Call的enqueue异步调用时,回调方法默认是在主线程里面的,具体可以查看源码,我这里只给出一部分源码,该源码是Platform一部分.代码如下:

static class Android extends Platform {
    @Override public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

    @Override CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }

    static class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override public void execute(Runnable r) {
        handler.post(r);
      }
    }
  }

大家应该很明白了吧,之所以是在主线程就是使用的Handle而已。下面看下我当时写的代码:
1)回调在主线程,这时候就会抛出上面说的异常代码如下:

      Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://f5.market.xiaomi.com/download/AppStore/")
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

2)错误解决方案:相信大家最先想到的也是这种方式。但是这种方式并没什么乱用,明白了抛异常的真正原因就知道了。代码如下:

       new AsyncTask<Void, Long, Void>() {
              @Override
              protected Void doInBackground(Void... voids) {
                   Call<ResponseBody> call = downLoadService.downloadLargeAPK();
                   call.enqueue(new Callback<ResponseBody>() {
                        @Override
                       public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                            if (response.isSuccessful()) {
                                  BufferedSink sink = null;
                                  //下载文件到本地
                                  try {
                                       sink = Okio.buffer(Okio.sink(downloadFile));
                                       sink.writeAll(response.body().source());
                                    } catch (Exception e) {
                                        if (e != null) {
                                            e.printStackTrace();
                                        }
                                    } finally {
                                        try {
                                            if (sink != null) sink.close();
                                        } catch (IOException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                    Log.d("下载成功", "isSuccessful");
                                } else {

                                    Log.d("---------------------", response.code() + "");
                                }
                            }
                            @Override
                            public void onFailure(Call<ResponseBody> call, Throwable t) {
                                Log.d("下载失败", t.getMessage());
                            }
                        });
                        return null;
                    }
                }.execute();
            }
        });

3)最终解决方案:就是不让回调回调在主线程中。代码如下:

     ExecutorService executorService = Executors.newFixedThreadPool(1);
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://f5.market.xiaomi.com/download/AppStore/")
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .callbackExecutor(executorService) //默认CallBack回调在主线程进行,当设置下载大文件时需设置注解@Stream 不加这句话会报android.os.NetworkOnMainThreadException
                .build();

总结

至此,介绍使用Retrofit下载大文件的坑就介绍完毕,最后希望能帮助到大家。谢谢阅读。源代码请到该地址去下载:

源码下载

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值