前些天有个同学问我会不会使用Retrofit下载大文件,我就给了他我项目中使用的方法。发现有很多人还不会用Retrofit实现下载文件,即使会下载,也可能会出现问题,比如:不知道如何获取进度;一旦下载大文件就会OOM;不知道如何暂停下载,或者不知道如何实现断点续传等。今天这个demo就是实现上面的几个问题,使用Retrofit+Rxjava来实现,先看效果图:
具体的实现逻辑:
public class DownloadResponseBody extends ResponseBody {
private ResponseBody responseBody;
private DownloadProgressListener listener;
private BufferedSource bufferedSource;
public DownloadResponseBody(ResponseBody responseBody, DownloadProgressListener listener) {
this.responseBody = responseBody;
this.listener = listener;
}
@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);
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
if (null != listener) {
listener.progress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
}
return bytesRead;
}
};
}
}
public interface DownloadProgressListener {
/**
* @param read 已下载长度
* @param contentLength 总长度
* @param done 是否下载完毕
*/
void progress(long read, long contentLength, boolean done);
}
public interface DownLoadService {
/**
* @param start 从某个字节开始下载数据
* @param url 文件下载的url
* @return Observable
* @Streaming 这个注解必须添加,否则文件全部写入内存,文件过大会造成内存溢出
*/
@Streaming
@GET
Observable download(@Header("RANGE") String start, @Url String url);
}
public class DownloadInterceptor implements Interceptor {
private DownloadProgressListener listener;
public DownloadInterceptor(DownloadProgressListener listener) {
this.listener = listener;
}
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new DownloadResponseBody(originalResponse.body(), listener))
.build();
}
}
public class DownloadInfo {
/* 存储位置 */
private String savePath;
/* 文件总长度 */
private long contentLength;
/* 下载长度 */
private long readLength;
/* 下载该文件的url */
private String url;
private DownLoadService service;
}
public class DownloadManager implements DownloadProgressListener {
private DownloadInfo info;
private ProgressListener progressObserver;
private File outFile;
private Subscription subscribe;
private DownLoadService service;
private long currentRead;
private DownloadManager() {
info = new DownloadInfo();
outFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "yaoshi.apk");
info.setSavePath(outFile.getAbsolutePath());
}
public static DownloadManager getInstance() {
return Holder.manager;
}
public static class Holder {
private static DownloadManager manager = new DownloadManager();
}
@Override
public void progress(long read, final long contentLength, final boolean done) {
Log.e("progress : ", "read = " + read + "contentLength = " + contentLength);
// 该方法仍然是在子线程,如果想要调用进度回调,需要切换到主线程,否则的话,会在子线程更新UI,直接错误
// 如果断电续传,重新请求的文件大小是从断点处到最后的大小,不是整个文件的大小,info中的存储的总长度是
// 整个文件的大小,所以某一时刻总文件的大小可能会大于从某个断点处请求的文件的总大小。此时read的大小为
// 之前读取的加上现在读取的
if (info.getContentLength() > contentLength) {
read = read + (info.getContentLength() - contentLength);
} else {
info.setContentLength(contentLength);
}
info.setReadLength(read);
Observable.just(1).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1() {
@Override
public void call(Integer integer) {
if (progressObserver != null) {
progressObserver.progressChanged(info.getReadLength(), info.getContentLength(), done);
}
}
});
}
/**
* 开始下载
* @param url
*/
public void start(String url) {
info.setUrl(url);
final DownloadInterceptor interceptor = new DownloadInterceptor(this);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(8, TimeUnit.SECONDS);
builder.addInterceptor(interceptor);
Retrofit retrofit = new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.baseUrl(CommonUtils.getBasUrl(MyConstants.DOWNLOAD_URL))
.build();
if (service == null) {
service = retrofit.create(DownLoadService.class);
info.setService(service);
} else {
service = info.getService();
}
downLoad();
}
/**
* 开始下载
*/
private void downLoad() {
Log.e("下载:", info.toString());
subscribe = service.download("bytes=" + info.getReadLength() + "-", info.getUrl())
/*指定线程*/
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.retryWhen(new RetryWhenNetworkException())
/* 读取下载写入文件,并把ResponseBody转成DownInfo */
.map(new Func1() {
@Override
public DownloadInfo call(ResponseBody responseBody) {
try {
//写入文件
FileUtil.writeCache(responseBody, new File(info.getSavePath()), info);
} catch (IOException e) {
Log.e("异常:", e.toString());
}
return info;
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber() {
@Override
public void onCompleted() {
Log.e("下载", "onCompleted");
}
@Override
public void onError(Throwable e) {
Log.e("下载", "onError" + e.toString());
}
@Override
public void onNext(DownloadInfo downloadInfo) {
Log.e("下载", "onNext");
}
});
}
/**
* 暂停下载
*/
public void pause() {
if (subscribe != null)
subscribe.unsubscribe();
}
/**
* 继续下载
*/
public void reStart() {
downLoad();
}
/**
* 进度监听
*/
public interface ProgressListener {
void progressChanged(long read, long contentLength, boolean done);
}
public void setProgressListener(ProgressListener progressObserver) {
this.progressObserver = progressObserver;
}
}
public class FileUtil {
/**
* 写入文件
*
* @param file
* @param info
* @throws IOException
*/
public static void writeCache(ResponseBody responseBody, File file, DownloadInfo info) throws IOException {
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
long allLength;
if (info.getContentLength() == 0) {
allLength = responseBody.contentLength();
} else {
allLength = info.getContentLength();
}
FileChannel channelOut = null;
RandomAccessFile randomAccessFile = null;
randomAccessFile = new RandomAccessFile(file, "rwd");
channelOut = randomAccessFile.getChannel();
MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE,
info.getReadLength(), allLength - info.getReadLength());
byte[] buffer = new byte[1024 * 4];
int len;
int record = 0;
while ((len = responseBody.byteStream().read(buffer)) != -1) {
mappedBuffer.put(buffer, 0, len);
record += len;
}
responseBody.byteStream().close();
if (channelOut != null) {
channelOut.close();
}
if (randomAccessFile != null) {
randomAccessFile.close();
}
}
}
创建Retrofit需要有一个baseURL,并且结尾必须是以“/”结尾,否则会报异常
public class CommonUtils {
/**
* 读取baseurl
*
* @param url
* @return
*/
public static String getBasUrl(String url) {
String head = "";
int index = url.indexOf("://");
if (index != -1) {
head = url.substring(0, index + 3);
url = url.substring(index + 3);
}
index = url.indexOf("/");
if (index != -1) {
url = url.substring(0, index + 1);
}
return head + url;
}
}
这个url是从应用宝中随便找的一个App的下载链接,这个App比较小,如果有兴趣,可以下载一个游戏类型的App,比如穿越火线App,这个有近700M,下载过程不会OOM,并且查看消耗的内存,一般会在15M一下。
public class MyConstants {
public static final String DOWNLOAD_URL = "http://imtt.dd.qq.com/16891/89E1C87A75EB3E1221F2CDE47A60824A.apk?fsname=com.snda.wifilocating_4.2.62_3192.apk&csr=1bbd";
}
因为只是做个下载的例子,所以没有做动态的权限申请,开启下载前,请手动在设置中给App赋予读写文件的权限,否则下载失败,无法写入。
public class MainActivity extends AppCompatActivity implements DownloadManager.ProgressListener {
private ProgressBar pb_progress;
private TextView tv_progress;
private DownloadManager downloadManager;
private int i = 0;
private Button btn_pasuse;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pb_progress = (ProgressBar) findViewById(R.id.pb_progress);
tv_progress = (TextView) findViewById(R.id.tv_progress);
btn_pasuse = (Button) findViewById(R.id.btn_pasuse);
downloadManager = DownloadManager.getInstance();
downloadManager.setProgressListener(this);
}
/**
* 点击开始下载
*/
public void start(View view) {
downloadManager.start(MyConstants.DOWNLOAD_URL);
}
/**
* 点击暂停下载或继续下载
*/
public void pasuse(View view) {
if (i % 2 == 0) {
downloadManager.pause();
btn_pasuse.setText("继续下载");
} else {
downloadManager.reStart();
btn_pasuse.setText("暂停下载");
}
i++;
}
/**
* 进度回调接口
*/
@Override
public void progressChanged(long read, long contentLength, boolean done) {
final int progress = (int) (100 * read / contentLength);
pb_progress.setProgress(progress);
tv_progress.setText(progress + "%");
}
}
这个例子也是看了很多别人的demo,但是并没有找到简单点的比较全的例子,所以综合了几个人的代码,自己也研究了一些,写出来了这个例子。
源码