编辑截至 2015 年 12 月 30 日 - 图像下载终极指南
最后一次重大更新:2016 年 3 月 31 日
TL;DR 又名 别再说了,给我代码吧!
跳到这篇文章的底部,复制BasicImageDownloader
(javadoc版本here https://github.com/vad-zuev/ImageDownloader/blob/master/app/src/main/java/com/so/example/tools/BasicImageDownloader.java)
到您的项目中,实施OnImageLoaderListener
界面
你就完成了。
Note: 虽然BasicImageDownloader
处理可能的错误
并防止您的应用程序崩溃,以防万一出现问题,它不会执行
对下载的内容进行任何后处理(例如缩小尺寸)Bitmaps
.
由于这篇文章受到了相当多的关注,我决定对其进行彻底修改,以防止人们使用已弃用的技术、不良的编程实践或只是做一些愚蠢的事情 - 例如寻找“黑客”在主线程上运行网络或接受所有 SSL 证书。
我创建了一个名为“Image Downloader”的演示项目,它演示了如何使用我自己的下载器实现(Android 的内置下载器)下载(并保存)图像DownloadManager
以及一些流行的开源库。您可以查看完整源代码或下载项目在 GitHub 上 https://github.com/vad-zuev/ImageDownloader/.
Note:我还没有调整 SDK 23+ (Marshmallow) 的权限管理,因此该项目的目标是 SDK 22 (Lollipop)。
In my 结论在这篇文章的最后我将分享我的拙见关于我提到的每种特定图像下载方式的正确用例。
让我们从自己的实现开始(您可以在帖子末尾找到代码)。首先,这是一个BasicImageDownloader 就是这样。它所做的就是连接到给定的 url,读取数据并尝试将其解码为Bitmap
,触发OnImageLoaderListener
适当时接口回调。
这种方法的优点是简单,并且您可以清楚地了解正在发生的事情。如果您只需要下载/显示和保存一些图像,而您不关心维护内存/磁盘缓存,那么这是一个好方法。
注意:如果图像较大,您可能需要缩放它们
向下 http://developer.android.com/training/displaying-bitmaps/load-bitmap.html.
--
Android 下载管理器 http://developer.android.com/reference/android/app/DownloadManager.html是一种让系统为您处理下载的方法。它实际上能够下载任何类型的文件,而不仅仅是图像。您可以让下载悄无声息地进行并且对用户不可见,或者您可以使用户能够在通知区域中看到下载。您还可以注册一个BroadcastReceiver
下载完成后收到通知。设置非常简单,请参阅链接的项目以获取示例代码。
使用DownloadManager
如果您还想显示图像,通常不是一个好主意,因为您需要读取和解码保存的文件,而不仅仅是设置下载的文件Bitmap
进入一个ImageView
. The DownloadManager
也不为您的应用程序提供任何 API 来跟踪下载进度。
--
现在介绍一些很棒的东西——图书馆。它们不仅可以下载和显示图像,还可以做更多的事情,包括:创建和管理内存/磁盘缓存、调整图像大小、转换图像等等。
我将从Volley http://developer.android.com/training/volley/index.html,一个由 Google 创建的强大库,并包含在官方文档中。虽然 Volley 是一个不专门处理图像的通用网络库,但它具有非常强大的 API 来管理图像。
您将需要实施一个辛格尔顿 https://en.wikipedia.org/wiki/Singleton_pattern用于管理 Volley 请求的类,您就可以开始了。
您可能想更换您的ImageView
与凌空的NetworkImageView
,所以下载基本上就变成了一句话:
((NetworkImageView) findViewById(R.id.myNIV)).setImageUrl(url, MySingleton.getInstance(this).getImageLoader());
如果您需要更多控制,这就是创建一个ImageRequest
与凌空:
ImageRequest imgRequest = new ImageRequest(url, new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
//do stuff
}
}, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.ARGB_8888,
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
//do stuff
}
});
值得一提的是,Volley 具有出色的错误处理机制,提供了VolleyError
类可帮助您确定错误的确切原因。如果您的应用程序执行大量网络操作并且管理图像不是其主要目的,那么 Volley 非常适合您。
--
广场的Picasso http://square.github.io/picasso/是一个著名的库,它将为您完成所有图像加载工作。使用 Picasso 显示图像非常简单:
Picasso.with(myContext)
.load(url)
.into(myImageView);
默认情况下,Picasso 管理磁盘/内存缓存,因此您无需担心这一点。为了获得更多控制,您可以实施Target
接口并使用它来加载图像 - 这将提供类似于 Volley 示例的回调。检查演示项目以获取示例。
毕加索还允许您对下载的图像应用转换,甚至还有其他图书馆 https://github.com/wasabeef/picasso-transformations围绕这个扩展那些API。也能很好地工作在RecyclerView
/ListView
/GridView
.
--
通用图像加载器 https://github.com/nostra13/Android-Universal-Image-Loader是另一个非常流行的库,用于图像管理。它使用自己的ImageLoader
(一旦初始化)有一个全局实例,可用于在一行代码中下载图像:
ImageLoader.getInstance().displayImage(url, myImageView);
如果您想跟踪下载进度或访问已下载的Bitmap
:
ImageLoader.getInstance().displayImage(url, myImageView, opts,
new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
//do stuff
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
//do stuff
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
//do stuff
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
//do stuff
}
}, new ImageLoadingProgressListener() {
@Override
public void onProgressUpdate(String imageUri, View view, int current, int total) {
//do stuff
}
});
The opts
这个例子中的参数是DisplayImageOptions
目的。请参阅演示项目以了解更多信息。
与 Volley 类似,UIL 提供FailReason
类,使您能够检查下载失败时出现的问题。默认情况下,如果您没有明确告诉 UIL 不要这样做,UIL 会维护内存/磁盘缓存。
Note:作者提到,截至 2015 年 11 月 27 日,他将不再维护该项目。但由于贡献者很多,我们可以希望 Universal Image Loader 能够继续存在。
--
FacebookFresco http://frescolib.org/是最新且(IMO)最先进的库,它将图像管理提升到一个新的水平:从保持Bitmaps
脱离 java 堆(Lollipop 之前)以支持动画格式 http://frescolib.org/docs/animations.html and 渐进式 JPEG 流 http://frescolib.org/docs/progressive-jpegs.html.
要了解有关 Fresco 背后的想法和技术的更多信息,请参阅这个帖子 https://code.facebook.com/posts/366199913563917/introducing-fresco-a-new-image-library-for-android/.
基本用法非常简单。请注意,您需要致电Fresco.initialize(Context);
仅一次,最好在Application
班级。多次初始化 Fresco 可能会导致不可预测的行为和 OOM 错误。
壁画用途Drawee
s 显示图像,您可以将它们视为ImageView
s:
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/drawee"
android:layout_width="match_parent"
android:layout_height="match_parent"
fresco:fadeDuration="500"
fresco:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/placeholder_grey"
fresco:failureImage="@drawable/error_orange"
fresco:placeholderImageScaleType="fitCenter"
fresco:failureImageScaleType="centerInside"
fresco:retryImageScaleType="centerCrop"
fresco:progressBarImageScaleType="centerInside"
fresco:progressBarAutoRotateInterval="1000"
fresco:roundAsCircle="false" />
正如您所看到的,很多内容(包括转换选项)已经在 XML 中定义,因此显示图像所需要做的就是一行代码:
mDrawee.setImageURI(Uri.parse(url));
Fresco 提供了扩展的定制 API,在某些情况下,它可能相当复杂,需要用户仔细阅读文档(是的,有时你需要 RTFM).
我已将渐进式 JPEG 和动画图像的示例包含到示例项目中。
结论 - “我已经了解了很棒的东西,现在我应该使用什么?”
请注意,以下文字反映了我的个人观点,应该
不应被视为一个假设。
- 如果您只需要下载/保存/显示一些图像,则不打算在
Recycler-/Grid-/ListView
并且不需要一大堆图像即可显示,基本图像下载器应该适合您的需求。
- 如果您的应用程序由于用户或自动操作而保存图像(或其他文件),并且您不需要经常显示图像,请使用 Android下载管理器.
- 如果您的应用程序需要进行大量网络传输/接收
JSON
数据,适用于图像,但这些不是应用程序的主要目的,请使用Volley.
- 您的应用程序以图像/媒体为中心,您想对图像应用一些转换,并且不想使用复杂的 API:使用Picasso(注:不提供任何API来跟踪中间下载状态)或通用图像加载器
- 如果您的应用程序全部与图像有关,您需要显示动画格式等高级功能,并且您已准备好阅读文档,请选择Fresco.
如果您错过了,Github链接 https://github.com/vad-zuev/ImageDownloader对于演示项目。
这是BasicImageDownloader.java
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.Set;
public class BasicImageDownloader {
private OnImageLoaderListener mImageLoaderListener;
private Set<String> mUrlsInProgress = new HashSet<>();
private final String TAG = this.getClass().getSimpleName();
public BasicImageDownloader(@NonNull OnImageLoaderListener listener) {
this.mImageLoaderListener = listener;
}
public interface OnImageLoaderListener {
void onError(ImageError error);
void onProgressChange(int percent);
void onComplete(Bitmap result);
}
public void download(@NonNull final String imageUrl, final boolean displayProgress) {
if (mUrlsInProgress.contains(imageUrl)) {
Log.w(TAG, "a download for this url is already running, " +
"no further download will be started");
return;
}
new AsyncTask<Void, Integer, Bitmap>() {
private ImageError error;
@Override
protected void onPreExecute() {
mUrlsInProgress.add(imageUrl);
Log.d(TAG, "starting download");
}
@Override
protected void onCancelled() {
mUrlsInProgress.remove(imageUrl);
mImageLoaderListener.onError(error);
}
@Override
protected void onProgressUpdate(Integer... values) {
mImageLoaderListener.onProgressChange(values[0]);
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = null;
HttpURLConnection connection = null;
InputStream is = null;
ByteArrayOutputStream out = null;
try {
connection = (HttpURLConnection) new URL(imageUrl).openConnection();
if (displayProgress) {
connection.connect();
final int length = connection.getContentLength();
if (length <= 0) {
error = new ImageError("Invalid content length. The URL is probably not pointing to a file")
.setErrorCode(ImageError.ERROR_INVALID_FILE);
this.cancel(true);
}
is = new BufferedInputStream(connection.getInputStream(), 8192);
out = new ByteArrayOutputStream();
byte bytes[] = new byte[8192];
int count;
long read = 0;
while ((count = is.read(bytes)) != -1) {
read += count;
out.write(bytes, 0, count);
publishProgress((int) ((read * 100) / length));
}
bitmap = BitmapFactory.decodeByteArray(out.toByteArray(), 0, out.size());
} else {
is = connection.getInputStream();
bitmap = BitmapFactory.decodeStream(is);
}
} catch (Throwable e) {
if (!this.isCancelled()) {
error = new ImageError(e).setErrorCode(ImageError.ERROR_GENERAL_EXCEPTION);
this.cancel(true);
}
} finally {
try {
if (connection != null)
connection.disconnect();
if (out != null) {
out.flush();
out.close();
}
if (is != null)
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
if (result == null) {
Log.e(TAG, "factory returned a null result");
mImageLoaderListener.onError(new ImageError("downloaded file could not be decoded as bitmap")
.setErrorCode(ImageError.ERROR_DECODE_FAILED));
} else {
Log.d(TAG, "download complete, " + result.getByteCount() +
" bytes transferred");
mImageLoaderListener.onComplete(result);
}
mUrlsInProgress.remove(imageUrl);
System.gc();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public interface OnBitmapSaveListener {
void onBitmapSaved();
void onBitmapSaveError(ImageError error);
}
public static void writeToDisk(@NonNull final File imageFile, @NonNull final Bitmap image,
@NonNull final OnBitmapSaveListener listener,
@NonNull final Bitmap.CompressFormat format, boolean shouldOverwrite) {
if (imageFile.isDirectory()) {
listener.onBitmapSaveError(new ImageError("the specified path points to a directory, " +
"should be a file").setErrorCode(ImageError.ERROR_IS_DIRECTORY));
return;
}
if (imageFile.exists()) {
if (!shouldOverwrite) {
listener.onBitmapSaveError(new ImageError("file already exists, " +
"write operation cancelled").setErrorCode(ImageError.ERROR_FILE_EXISTS));
return;
} else if (!imageFile.delete()) {
listener.onBitmapSaveError(new ImageError("could not delete existing file, " +
"most likely the write permission was denied")
.setErrorCode(ImageError.ERROR_PERMISSION_DENIED));
return;
}
}
File parent = imageFile.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
listener.onBitmapSaveError(new ImageError("could not create parent directory")
.setErrorCode(ImageError.ERROR_PERMISSION_DENIED));
return;
}
try {
if (!imageFile.createNewFile()) {
listener.onBitmapSaveError(new ImageError("could not create file")
.setErrorCode(ImageError.ERROR_PERMISSION_DENIED));
return;
}
} catch (IOException e) {
listener.onBitmapSaveError(new ImageError(e).setErrorCode(ImageError.ERROR_GENERAL_EXCEPTION));
return;
}
new AsyncTask<Void, Void, Void>() {
private ImageError error;
@Override
protected Void doInBackground(Void... params) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(imageFile);
image.compress(format, 100, fos);
} catch (IOException e) {
error = new ImageError(e).setErrorCode(ImageError.ERROR_GENERAL_EXCEPTION);
this.cancel(true);
} finally {
if (fos != null) {
try {
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
@Override
protected void onCancelled() {
listener.onBitmapSaveError(error);
}
@Override
protected void onPostExecute(Void result) {
listener.onBitmapSaved();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public static Bitmap readFromDisk(@NonNull File imageFile) {
if (!imageFile.exists() || imageFile.isDirectory()) return null;
return BitmapFactory.decodeFile(imageFile.getAbsolutePath());
}
public interface OnImageReadListener {
void onImageRead(Bitmap bitmap);
void onReadFailed();
}
public static void readFromDiskAsync(@NonNull File imageFile, @NonNull final OnImageReadListener listener) {
new AsyncTask<String, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(String... params) {
return BitmapFactory.decodeFile(params[0]);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null)
listener.onImageRead(bitmap);
else
listener.onReadFailed();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, imageFile.getAbsolutePath());
}
public static final class ImageError extends Throwable {
private int errorCode;
public static final int ERROR_GENERAL_EXCEPTION = -1;
public static final int ERROR_INVALID_FILE = 0;
public static final int ERROR_DECODE_FAILED = 1;
public static final int ERROR_FILE_EXISTS = 2;
public static final int ERROR_PERMISSION_DENIED = 3;
public static final int ERROR_IS_DIRECTORY = 4;
public ImageError(@NonNull String message) {
super(message);
}
public ImageError(@NonNull Throwable error) {
super(error.getMessage(), error.getCause());
this.setStackTrace(error.getStackTrace());
}
public ImageError setErrorCode(int code) {
this.errorCode = code;
return this;
}
public int getErrorCode() {
return errorCode;
}
}
}