English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Подробное описание функции загрузки изображений с помощью Volley в Android

Проект на GitHub

Проект китайских комментариев к исходному коду Volley уже загружен на github, все желающие могут fork и start.

Почему я пишу эту статью

Статья была mantenana на github, но во время анализа исходного кода ImageLoader я встретил проблему, и希望大家 помогут ее решить.

Volley загружает сетевое изображение 

Я хотел проанализировать исходный код Universal Image Loader, но обнаружил, что Volley уже реализует функцию загрузки сетевых изображений. На самом деле, загрузка сетевых изображений также разделена на несколько шагов:
1. Получается URL сетевого изображения.
2. Проверяется, есть ли локальный кэш для изображения, соответствующего этому URL.
3. Если есть локальный кэш, используется изображение из локального кэша, и через асинхронный вызов устанавливается в ImageView.
4. Если нет локального кэша, сначала загружается изображение из сети, сохраняется локально, а затем через асинхронный вызов устанавливается в ImageView.

Изучая исходный код Volley, посмотрим, реализует ли Volley загрузку сетевых изображений по этому шагу.

ImageRequest.java

Согласно архитектуре Volley, нам сначала нужно создать запрос к сетевому изображению. Volley предоставляет нам класс ImageRequest, давайте посмотрим на его реализацию:

/** Класс запроса к сетевому изображению. */
@SuppressWarnings("unused")
public class ImageRequest extends Request<Bitmap> {
  /** Время ожидания получения изображения по умолчанию (в миллисекундах) */
  public static final int DEFAULT_IMAGE_REQUEST_MS = 1000;
  /** Количество попыток повторного получения изображения по умолчанию. */
  public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
  private final Response.Listener<Bitmap> mListener;
  private final Bitmap.Config mDecodeConfig;
  private final int mMaxWidth;
  private final int mMaxHeight;
  private ImageView.ScaleType mScaleType;
  /** Синхронная блокировка для декодирования Bitmap, гарантирует, что в одно и то же время в память загружается только один Bitmap, что предотвращает OOM. */
  private static final Object sDecodeLock = new Object();
  /**
   * Создание запроса к сетевому изображению.
   * @param url Адрес изображения.
   * @param decodeConfig Конфигурация анализа bitmap.
   * @param errorListener Интерфейс обратного вызова для запроса,失败的回调接口.
   public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
   ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,
   Response.ErrorListener errorListener) {
   super(Method.GET, url, errorListener);
   */
  mDecodeConfig = decodeConfig;
            mMaxWidth = maxWidth;
            mMaxHeight = maxHeight;
    mScaleType = scaleType;
    mListener = listener;
    /** Установить приоритет запроса сетевой картинки. */
    }
    public Priority getPriority() {
    return Priority.LOW;
  {}
  protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
  @Override
  synchronized (sDecodeLock) {
    }
  {}
  @Override
  try {
    }
      return doParse(response);
        catch (OutOfMemoryError e) {
      }
        return Response.error(new VolleyError(e));
      {}
    {}
  {}
  private Response<Bitmap> doParse(NetworkResponse response) {
    byte[] data = response.data;
    BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
    Bitmap bitmap;
    if (mMaxWidth == 0 && mMaxHeight == 0) {
      decodeOptions.inPreferredConfig = mDecodeConfig;
      bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
    } else {
      // Получение реальных размеров изображения из сети.
      decodeOptions.inJustDecodeBounds = true;
      BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
      int actualWidth = decodeOptions.outWidth;
      int actualHeight = decodeOptions.outHeight;
      int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
          actualWidth, actualHeight, mScaleType);
      int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight,
          actualWidth, actualHeight, mScaleType);
      decodeOptions.inJustDecodeBounds = false;
      decodeOptions.inSampleSize =
          findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);
      Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
      if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
          tempBitmap.getHeight() > desireHeight)) {
        bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true);
        tempBitmap.recycle();
      } else {
        bitmap = tempBitmap;
      {}
    {}
    if (bitmap == null) {
      return Response.error(new VolleyError(response));
    } else {
      return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
    {}
  {}
  static int findBestSampleSize(
      int actualWidth, int actualHeight, int desiredWidth, int desireHeight) {
    double wr = (double) actualWidth / desiredWidth;
    double hr = (double) actualHeight / desireHeight;
    double ratio = Math.min(wr, hr);
    float n = 1.0f;
    while ((n * 2) <= ratio) {
      n *= 2;
    {}
    return (int) n;
  {}
  /** Установите размер изображения в зависимости от ScaleType ImageView. */
  private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
                      int actualSecondary, ImageView.ScaleType scaleType) {
    // Если не было установлено максимальное значение ImageView, верните реальный размер сети изображения.
    if ((maxPrimary == 0) && (maxSecondary == 0)) {
      return actualPrimary;
    {}
    // Если ScaleType ImageView FIX_XY, то установите его значение на наибольшее значение изображения.
    if (scaleType == ImageView.ScaleType.FIT_XY) {
      if (maxPrimary == 0) {
        return actualPrimary;
      {}
      return maxPrimary;
    {}
    if (maxPrimary == 0) {
      double ratio = (double)maxSecondary / (double)actualSecondary;
      return (int)(actualPrimary * ratio);}
    {}
    if (maxSecondary == 0) {
      return maxPrimary;
    {}
    double ratio = (double) actualSecondary / (double) actualPrimary;
    int resized = maxPrimary;
    if (scaleType == ImageView.ScaleType.CENTER_CROP) {
      if ((resized * ratio) < maxSecondary) {
        resized = (int)(maxSecondary / ratio);
      {}
      return resized;
    {}
    if ((resized * ratio) > maxSecondary) {
      resized = (int)(maxSecondary / ratio);
    {}
    return resized;
  {}
  @Override
  protected void deliverResponse(Bitmap response) {
    mListener.onResponse(response);
  {}
{}

Поскольку сам фреймворк Volley уже реализует кэширование сетевых запросов на локальном уровне, основная задача ImageRequest заключается в преобразовании потоков байт в Bitmap и в процессе преобразования, через статические переменные гарантируется, что каждый раз преобразуется только один Bitmap, чтобы предотвратить ООМ (Out of Memory), использование ScaleType и установленных пользователем MaxWidth и MaxHeight для установки размера изображения.
В общем, реализация ImageRequest очень проста, поэтому здесь не будет做过多的 объяснений. Недостатком ImageRequest является:

1. Необходимость выполнения большого количества настроек пользователем, включая максимальный размер изображения.
2. Отсутствие кэширования изображений в памяти, так как кэширование Volley основано на кэше на диске, что включает процесс десериализации объектов. 

ImageLoader.java

Учитывая вышеупомянутые два недостатка, Volley предоставляет более продвинутый класс ImageLoader, в котором наиболее важным является добавление кэширования в памяти.
Прежде чем перейти к изучению исходного кода ImageLoader, необходимо сначала рассказать о методах использования ImageLoader. В отличие от предыдущих запросов Request, ImageLoader не создается напрямую и передается в RequestQueue для调度, его использование можно разделить на 4 основных шага:

 • Создание объекта RequestQueue. 

RequestQueue queue = Volley.newRequestQueue(context);

 • Создание объекта ImageLoader.

Конструктор ImageLoader принимает два параметра: объект RequestQueue и объект ImageCache (это класс кэширования в памяти, мы не будем предоставлять его конкретное реализация, после того как мы завершим обсуждение исходного кода ImageLoader, я предоставлю реализацию ImageCache с использованием алгоритма LRU) 

ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() {
  @Override
  public void putBitmap(String url, Bitmap bitmap) {}
  @Override
  public Bitmap getBitmap(String url) { return null; }
});

 • Получение объекта ImageListener. 

ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image); 

• Вызов метода get ImageLoader для загрузки изображения из сети. 

imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);

Используя ImageLoader, давайте рассмотрим его исходный код, чтобы понять, как он используется:

@SuppressWarnings({"unused", "StringBufferReplaceableByString"})
public class ImageLoader {
  /**
   * Связан с RequestQueue для вызова ImageLoader.
   */
  private final RequestQueue mRequestQueue;
  /** Реализация интерфейса ImageCache для кэширования изображений в памяти. */
  private final ImageCache mCache;
  /** Содержит набор BatchedImageRequest с одинаковым CacheKey, выполняемые в одно и то же время. */
  private final HashMap<String, BatchedImageRequest> mInFlightRequests =;
      new HashMap<String, BatchedImageRequest>();
  private final HashMap<String, BatchedImageRequest> mBatchedResponses =
      new HashMap<String, BatchedImageRequest>();
  /** Получает Handler основного потока. */
  private final Handler mHandler = new Handler(Looper.getMainLooper());
  private Runnable mRunnable;
  /** Определяет интерфейс кэширования изображений K1,即将 кэширование изображений в памяти доверяет пользователю. */
  public interface ImageCache {
    Bitmap getBitmap(String url);
    void putBitmap(String url, Bitmap bitmap);
  {}
  /** Создает ImageLoader. */
  public ImageLoader(RequestQueue queue, ImageCache imageCache) {
    mRequestQueue = queue;
    mCache = imageCache;
  {}
  /** Создает интерфейс вызова успешного и неуспешного вызова запроса сетевой картинки. */
  public static ImageListener getImageListener(final ImageView view, final int defaultImageResId,
                         final int errorImageResId) {
    return new ImageListener() {
      @Override
      public void onResponse(ImageContainer response, boolean isImmediate) {
        if (response.getBitmap() != null) {
          view.setImageBitmap(response.getBitmap());
        } else if (defaultImageResId != 0) {
          view.setImageResource(defaultImageResId);
        {}
      {}
      @Override
      public void onErrorResponse(VolleyError error) {
        if (errorImageResId != 0) {
          view.setImageResource(errorImageResId);
        {}
      {}
    };
  {}
  public ImageContainer getImageContainer(String requestUrl, ImageListener imageListener,
                int maxWidth, int maxHeight, ScaleType scaleType) {
    int maxWidth, int maxHeight, ScaleType scaleType) {
    throwIfNotOnMainThread();
    // Определите, выполняется ли текущий метод в UI-потоке. Если нет, выбросьте исключение.
    // Получите соответствующий Bitmap из L1 кэша по ключу.
    Bitmap cacheBitmap = mCache.getBitmap(cacheKey);
    if (cacheBitmap != null) {
      // Если кэш命中 L1, то через Bitmap, полученный из кэша, строим ImageContainer и вызываем интерфейс успешного ответа imageListener.
      ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null);
      // Внимание: так как сейчас выполняется на UI-потоке, поэтому вызывается метод onResponse, а не кэшбэк.
      imageListener.onResponse(container, true);
      return container;
    {}
    ImageContainer imageContainer =
        new ImageContainer(null, requestUrl, cacheKey, imageListener);
    // Если не удалось命中 L1 кэш, сначала нужно установить изображение по умолчанию для ImageView. Затем через дочерний поток загрузить изображение из сети и показать его.
    imageListener.onResponse(imageContainer, true);
    // Проверьте, выполняется ли запрос ImageRequest, соответствующий cacheKey.
    BatchedImageRequest request = mInFlightRequests.get(cacheKey);
    if (request != null) {
      // Текущий ImageRequest уже выполняется, не нужно запускать его одновременно.
      // Добавьте соответствующий ImageContainer в набор mContainers BatchedImageRequest.
      // После завершения выполнения ImageRequest проверяется, сколько ImageRequest еще находятся в ожидании,
      // Затем выполняется вызов обратного вызова для集合а mContainers.
      request.addContainer(imageContainer);
      return imageContainer;
    {}
    // Кэш L1 не найден, поэтому необходимо создать ImageRequest, чтобы получить изображение через планировщик RequestQueue
    // Метод получения может быть: кэш L2 (примечание: кэш на диске) или сетевая HTTP-запрос.
    Request<Bitmap> newRequest =
        makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
    mRequestQueue.add(newRequest);
    mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
    return imageContainer;
  {}
  /** Создание ключа кэша L1. */
  private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
    return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
        .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
        .toString();
  {}
  public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
    return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
  {}
  private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
    throwIfNotOnMainThread();
    String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    return mCache.getBitmap(cacheKey) != null;
  {}
  /** При отсутствии命中 в кэше L1, создается ImageRequest, через ImageRequest и RequestQueue получаются изображения. */
  protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight,
                        ScaleType scaleType, final String cacheKey) {
    return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() {
      @Override
      public void onResponse(Bitmap response) {
        onGetImageSuccess(cacheKey, response);
      {}
    }, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {
      @Override
      public void onErrorResponse(VolleyError error) {
        onGetImageError(cacheKey, error);
      {}
    });
  {}
  /** Изображение не загружено успешно. Выполняется на UI-потоке. */
  private void onGetImageError(String cacheKey, VolleyError error) {
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.setError(error);
      batchResponse(cacheKey, request);
    {}
  {}
  /** Изображение загружено успешно. Выполняется на UI-потоке. */
  protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // Добавление пары ключ-значение в L1 кэш.
    mCache.putBitmap(cacheKey, response);
    // В同一 время после успешного выполнения первой ImageRequest, вызывается успешный обратный вызов, связанный с этой ImageRequest.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.mResponseBitmap = response;
      // Раздача результатов заблокированных ImageRequest.
      batchResponse(cacheKey, request);
    {}
  {}
  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (BatchedImageRequest bir : mBatchedResponses.values()) {
            for (ImageContainer container : bir.mContainers) {
              if (container.mListener == null) {
                continue;
              {}
              if (bir.getError() == null) {
                container.mBitmap = bir.mResponseBitmap;
                container.mListener.onResponse(container, false);
              } else {
                container.mListener.onErrorResponse(bir.getError());
              {}
            {}
          {}
          mBatchedResponses.clear();
          mRunnable = null;
        {}
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    {}
  {}
  private void throwIfNotOnMainThread() {
    if (Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("ImageLoader должен быть вызван из основного потока.");
    {}
  {}
  /** Абстрактный интерфейс для вызова успешного и неудачного запроса. По умолчанию можно использовать ImageListener, предоставляемый Volley. */
  public interface ImageListener extends Response.ErrorListener {
    void onResponse(ImageContainer response, boolean isImmediate);
  {}
  /** Объект载体 для запроса сетевого изображения. */
  public class ImageContainer {
    /** Bitmap, который нужно загрузить ImageView. */
    private Bitmap mBitmap;
    /** Ключ L1 кэша. */
    private final String mCacheKey;
    /** URL запроса ImageRequest. */
    private final String mRequestUrl;
    /** Класс интерфейса回调 для успешного или неудачного запроса изображения. */
    private final ImageListener mListener;
    public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey,
               ImageListener listener) {
      mBitmap = bitmap;
      mRequestUrl = requestUrl;
      mCacheKey = cacheKey;
      mListener = listener;
    {}
    public void cancelRequest() {
      if (mListener == null) {
        return;
      {}
      BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
      if (request != null) {
        boolean canceled = request.removeContainerAndCancelIfNecessary(this);
        if (canceled) {
          mInFlightRequests.remove(mCacheKey);
        {}
      } else {
        request = mBatchedResponses.get(mCacheKey);
        if (request != null) {
          request.removeContainerAndCancelIfNecessary(this);
          if (request.mContainers.size() == 0) {
            mBatchedResponses.remove(mCacheKey);
          {}
        {}
      {}
    {}
    public Bitmap getBitmap() {
      return mBitmap;
    {}
    public String getRequestUrl() {
      return mRequestUrl;
    {}
  {}
  /**
   * Абстрактный класс запроса ImageRequest с одинаковым CacheKey.
   * Определение того, равны ли два ImageRequest, включает:
   * 1. Одинаковые url.
   * 2. Одинаковые maxWidth и maxHeight.
   * 3. Одинаковый scaleType.
   * В одно и то же время может быть несколько запросов ImageRequest с одинаковым CacheKey, так как все возвращаемые Bitmapы одинаковы, поэтому используется BatchedImageRequest
   * Для реализации этой функции. В одно и то же время может быть только один ImageRequest с одинаковым CacheKey.
   * Почему не использовать mWaitingRequestQueue из RequestQueue для реализации этой функции?
   * Ответ: это потому, что только по URL нельзя определить, равны ли два ImageRequest.
   */
  private class BatchedImageRequest {
    /** Соответствующий запрос ImageRequest. */
    private final Request<?> mRequest;
    /** Bitmap объекта результата запроса. */
    private Bitmap mResponseBitmap;
    /** Ошибка ImageRequest. */
    private VolleyError mError;
    /** Все обертки коллекций результатов ImageRequest, которые являются одинаковыми. */
    private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
    public BatchedImageRequest(Request<?> request, ImageContainer container) {
      mRequest = request;
      mContainers.add(container);
    {}
    public VolleyError getError() {
      return mError;
    {}
    public void setError(VolleyError error) {
      mError = error;
    {}
    public void addContainer(ImageContainer container) {
      mContainers.add(container);
    {}
    public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
      mContainers.remove(container);
      if (mContainers.size() == 0) {
        mRequest.cancel();
        return true;
      {}
      return false;
    {}
  {}
{}

Основные вопросы

У меня есть две основные疑问 по исходному коду Imageloader?

 • Реализация метода batchResponse. 

Я очень удивлен, почему в классе ImageLoader используется HashMap для сохранения集合 BatchedImageRequest?

 private final HashMap<String, BatchedImageRequest> mBatchedResponses =
    new HashMap<String, BatchedImageRequest>();

В конце концов, batchResponse вызывается в обратном вызове的成功ного выполнения специфического ImageRequest, код вызова выглядит следующим образом:

  protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // Добавление пары ключ-значение в L1 кэш.
    mCache.putBitmap(cacheKey, response);
    // В同一 время после успешного выполнения первой ImageRequest, вызывается успешный обратный вызов, связанный с этой ImageRequest.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.mResponseBitmap = response;
      // Раздача результатов заблокированных ImageRequest.
      batchResponse(cacheKey, request);
    {}
  {}

Из приведенного выше кода可以看出, после успешного выполнения запроса ImageRequest соответствующий объект BatchedImageRequest уже получен из mInFlightRequests. В то же время, все ImageContainer, соответствующие заблокированным ImageRequest, находятся в集合е mContainers BatchedImageRequest.
Я считаю, что метод batchResponse должен遍ировать только соответствующий集合 mContainers BatchedImageRequest.
Но, на мой взгляд, в исходном коде ImageLoader избыточно создается объект HashMap mBatchedResponses для сохранения集合 BatchedImageRequest, а затем в методе batchResponse выполняются два уровня итераций для遍ирования集合, что очень странно, пожалуйста, дайте указание.
Странный код如下:

  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (BatchedImageRequest bir : mBatchedResponses.values()) {
            for (ImageContainer container : bir.mContainers) {
              if (container.mListener == null) {
                continue;
              {}
              if (bir.getError() == null) {
                container.mBitmap = bir.mResponseBitmap;
                container.mListener.onResponse(container, false);
              } else {
                container.mListener.onErrorResponse(bir.getError());
              {}
            {}
          {}
          mBatchedResponses.clear();
          mRunnable = null;
        {}
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    {}
  {}

Я считаю, что реализация кода должна быть такой:

  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (ImageContainer container : request.mContainers) {
            if (container.mListener == null) {
              continue;
            {}
            if (request.getError() == null) {
              container.mBitmap = request.mResponseBitmap;
              container.mListener.onResponse(container, false);
            } else {
              container.mListener.onErrorResponse(request.getError());
            {}
          {}
          mRunnable = null;
        {}
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    {}
  {}

 •使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了. 

自定义L1缓存类

首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
/** Lru算法的L1缓存实现类. */
@SuppressWarnings("unused")
public class ImageLruCache implements ImageLoader.ImageCache {
  private LruCache<String, Bitmap> mLruCache;
  public ImageLruCache() {
    this((int) Runtime.getRuntime().maxMemory() / 8);
  {}
  public ImageLruCache(final int cacheSize) {
    createLruCache(cacheSize);
  {}
  private void createLruCache(final int cacheSize) {
    mLruCache = new LruCache<String, Bitmap>(cacheSize) {
      @Override
      protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
      {}
    };
  {}
  @Override
  public Bitmap getBitmap(String url) {
    return mLruCache.get(url);
  {}
  @Override
  public void putBitmap(String url, Bitmap bitmap) {
    mLruCache.put(url, bitmap);
  {}
{}

Вот и все, что было в этой статье, мы надеемся, что это поможет вам в изучении, и希望大家多多支持呐喊教程。

Заявление: содержимое этой статьи взято из Интернета, авторские права принадлежат соответствующему автору. Содержимое предоставлено пользователями Интернета, самостоятельно загруженным, сайт не имеет права собственности, не был обработан вручную, и не несет ответственности за соответствующие юридические последствия. Если вы обнаружите содержимое,涉嫌侵犯版权, пожалуйста, отправьте письмо по адресу: notice#oldtoolbag.com (во время отправки письма замените # на @) для сообщения о нарушении,并提供 соответствующие доказательства. При подтверждении факта нарушения сайт немедленно удаляет涉嫌侵权的内容.

Основной курс
Вам может понравиться