如何在 Android 中下载并保存图像

2023-12-19

如何在 Android 中从给定的 URL 下载并保存图像?


编辑截至 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 错误。

壁画用途Drawees 显示图像,您可以将它们视为ImageViews:

    <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;
        }
      }
   }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 Android 中下载并保存图像 的相关文章

随机推荐

  • BufferedImage颜色饱和度

    我正在编写一个简单的扫描应用程序自由精神 https github com sjamesr jfreesane and 阿帕奇PDFBox https pdfbox apache org 这是扫描码 InetAddress address
  • 与 QB 桌面产品集成

    几年前 我编写了一个 QB 集成 它使用 Web 连接器在 QB 桌面产品中读取和写入数据 它运行良好 但我不喜欢 Web Connector 我的任务是建立另一个 QB 集成 我希望这次避免使用 Web Connector 我尝试避免使用
  • 如何显示六边形填充颜色的图例?

    我正在 ggplot2 中绘制十六进制图 出现的图例是关于六边形的填充 计数 而不是关于六边形实际热缩放的内容 预测 我如何摆脱计数的图例 但让它显示实际热缩放的内容 预测 这是我的代码和我所指内容的图片 FF1xBARHH lt ggpl
  • Google Cloud Pub/Sub 推送消息 - 空 POST

    我目前已在Google云平台中成功设置主题和订阅 并已通过Google验证了我的网站并将域添加到GCP 每当我尝试从以下位置发送测试消息时https console cloud google com cloudpubsub topics s
  • 无法让小网格线出现在 matplotlib 图中

    好的 我有下面的代码 用于实时绘制通过串行接收的嵌入式设备的一些数据 它并不是一个生产工具 而是一个内部工程工具 因此它不是非常用户友好 问题是 无论我做什么 我都无法让小网格线出现 即使这里它们被设置为True which both 我可
  • 聚合管道和索引

    From http docs mongodb org manual core indexes multikey indexes http docs mongodb org manual core indexes multikey index
  • 如何在 Java 中从原始 byte[] 创建 BMP 文件

    我有一个 C 应用程序 它与相机通信并获取原始图像数据 然后我有一个 C 中的 Byte 我想用 JNI 将其发送到 Java 但是 我需要将原始 Byte 转换为真实的文件格式 bmp 是我的第一选择 如果我使用 BITMAPFILEIN
  • 从R中的循环返回单独的txt文件

    我想返回循环中每次迭代的结果 并将其写入单独的文本文件中 但由于某种原因 它似乎不起作用 我的代码是 for i in length traject player lt subset traject i subset dt 1 test l
  • 哪种排序方法最适合并行处理?

    我现在正在查看我以前的学校作业 想找到问题的答案 哪种排序方法最适合并行处理 冒泡排序 快速排序 归并排序 选择排序 我想快速排序 或合并排序 就是答案 我对么 与合并排序一样 快速排序由于其分而治之的性质也可以轻松并行化 单独的就地分区操
  • 通过 List 发布多个文件

    我只想通过模型将多个文件发布到控制器 class myModel public List
  • Android MediaPlayer 未从prepareAsync 返回

    我在使用特定 URI 启动 MediaPlayer 的 Logcat 中得到以下信息 通常 每个 Uri 无论好坏 都会播放或返回错误 除了这个特定的 Uri I MPS PrepAsync started V MediaPlayer me
  • 如何在 F# 中编写函子在 OCaml 中执行的操作的代码?

    我有很多用 OCaml 编写的程序 其中一些使用函子 现在 我正在考虑用 F 编写和重写部分代码 以受益于 OCaml 不具备的一些优点 我担心的一件事是在 F 中编写函子在 OCaml 中执行的操作的代码 例如 我们如何模仿这个例子来自
  • 在 PHP 开关中使用 strstr

    我只是想不出代码 我有太多 if 语句 我想将其更改为 switch 语句 但我找不到逻辑 目前我有 if strstr var texttosearch echo string contains texttosearch if strst
  • org.h2.jdbc.JdbcSQLException:未找到列“ID”

    我的代码中有以下 DDL CREATE TABLE IF NOT EXISTS SOMETABLE id BIGINT AUTO INCREMENT NOT NULL FOREIGN KEY id REFERENCES OTHERTABLE
  • Javascript (MVC) 从数据库加载图像(字节数组)

    Stack 上有很多这个问题的答案 但没有一个对我有用 我需要通过对控制器的 ajax 调用检索字节数组 在 javascript 中设置图像标签的 src 属性 我必须在客户端执行此操作 因为我正在动态构建一些 html 在下面的简单示例
  • 如何在 ASP.Net 中转储响应标头

    我正在使用 VSTS 2008 C Net 3 5 来开发 ASP Net 我想转储特定 aspx 文件返回给客户端的所有响应标头 有什么想法可以轻松做到这一点吗 我知道如何使用 Response Headers 集合 但我的困惑是在哪里枚
  • 如何告诉屏幕阅读器链接已禁用?

    我有一个页面n部分 这些部分是隐藏的 只能通过单击各自的链接来显示 页面加载时 只有第一个链接处于活动状态 其余 n 1 个链接处于活动状态href 基于某种逻辑 其他链接被单独激活 如何让屏幕阅读器理解该链接是disabled or 停用
  • Handsontable:隐藏一些列而不更改数据数组/对象

    我有一个数据要在网格中显示 我正在使用 Handsontable 来显示数据 每个第三列都计算为前两列的差值 例如 第三列渲染为第一列和第二列的总和 这是由自定义渲染器通过取总和来完成的i 1 and i 2列 这是我的 差异 列的自定义渲
  • React - Axios 调用发出太多请求

    我通过制作游戏项目来学习 React 和 Redux 我想通过API获取数据 属性 但它导致太多请求 我猜它可以与直接在功能性反应组件中放置 axios 调用有关 但我不知道如何修复它 function Attributes attribu
  • 如何在 Android 中下载并保存图像

    如何在 Android 中从给定的 URL 下载并保存图像 编辑截至 2015 年 12 月 30 日 图像下载终极指南 最后一次重大更新 2016 年 3 月 31 日 TL DR 又名 别再说了 给我代码吧 跳到这篇文章的底部 复制Ba