如何在通过 Picasso 下载图像之前验证 Content-Length 和 Content-Type 的响应 headers?

How to validate response headers by Content-Length and Content-Type before downloading an image via Picasso?

在实际决定是否通过 Picasso 下载图像到 ImageView 之前,我想验证 HTTP 响应 headers。具体原因是我没有其他方法可以在不下载的情况下判断图像格式和大小是否正确,只是意识到我不得不把它扔掉,因为它不符合设定的标准。

流程是这样的:

// 1. Ask a specific Profiles Service for a given user-id
// 2. Process the response and get the avatar URI from it
// 3. Call the avatar URI to validate the headers
if (validate(response.headers)) {
    // 4. Download avatar image using Picasso into given ImageView
} else {
    // 5. Don't download anything but show a placeholder image.
}

首先,我知道 Picasso 内置了显示占位符图像的功能,但 URI 并不是 return 任何图像 - 它是,但对我们来说不是一个正确的。因此,该功能对我们没有帮助。

其次,我知道这表明 API 中的设计失败,我们必须绕过这样做才能根据我们的指南了解头像图像响应是否有效,但配置文件服务并不特定于我们的解决方案但更广泛,因此这种 "hack-y" 方式是 目前 .

的方式

有什么想法可以绕过使用 Picasso 或任何其他 URI 库验证 headers 以避免无目的下载图像并且不浪费带宽吗?

如果您想跳到答案,请转到下面列表中的第 3 点!


经过一段时间的研究,我尝试了几种方法并找到了一种可以完成上面列出的所有事情的方法,尽管它不是最干净的方法:

1. Tried adding OkHttp3 interceptors to Picasso to modify response before passed on

我的第一个想法是使用我们常用的OkHttp中的拦截器modify/add headers到requests发送出去,而不是修改响应 - 如果可能,您不应该这样做。

拦截器如下所示:

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
    @Override public okhttp3.Response intercept(Chain chain) throws IOException {
        okhttp3.Response originalResponse = chain.proceed(chain.request());

        if (validate(originalResponse.headers()) {
            return originalResponse.newBuilder()
                   .body(null) //remove the body, stupid
                   .build();
        }
        return originalResponse;
    }
};

然后我尝试使用 Jake Wharton 的 com.jakewharton.picasso:picasso2-okhttp3-downloader 库将此拦截器添加到 Picasso 实例,该库将 OkHttp3 拦截器支持添加到 Picasso 2.5.2 和 Retrofit 2.0.2。

问题是拦截器从未被调用,我怀疑(在研究之后)可能是因为 Picasso 中使用的 OkHttp 版本与我所依赖的不同。从来没有想过为什么它没有被调用(甚至其他拦截器),但我很快意识到这种方法背后的概念被打破了,我转向了。

2. Tried making a HEAD request using Retrofit 2 to verify the headers before making a call using Picasso

决定我必须进行 2 个单独的调用才能实现此目的: 1. HEAD 请求在步骤 2 之前单独验证响应 headers。 2. GET 请求下载包含图像的整个响应 body(如果第 1 步 return 有效)

在我以前的方法中,我想将这两个步骤捆绑在一起,这在概念上是错误的,因为 body 本来可以通过任何一种方式下载,但只有在满足条件时才会被取消,因此无论哪种方式都会浪费带宽。

我计划使用 Retrofit 2 实现步骤 1,使用 Picasso 2.5 实现步骤 2。

我很快就放弃了这一点,因为为这样一个简单的调用实现 Retrofit rest 接口会带来很多开销。

3. Settled with doing a HEAD call using OkHttp3 to verify the headers, then calling Picasso with the URI

类似于方法 #2。但是在使用 Picasso 下载图像到给定的 ImageView 之前,我没有使用 Retrofit 对给定的 URI 进行 head() 调用 examine/validate headers,而是决定使用 OkHttp3(这是顺便说一下,Picasso 和 Retrofit 的基础。

不久,我实现了一个 AsyncTask,它将 运行 一个简单的 OkHttp head() 调用给定的 link,这将 return 我一个包含枚举的项目输入头像和关联的 link。此调用将发生在不同的线程上。

初始化另一个调用的线程,一旦它收到来自 OkHttp 调用的反馈(使用一个简单的模式通知 UI 线程),它将处理信息并启动一个如有需要请致电 Picasso。

下面是代码:

package com.example;

import android.net.Uri;

public class Avatar {
    AvatarType _avatarType;
    Uri _uri;

    public Avatar(AvatarType type, Uri uri) {
        _avatarType = type;
        _uri = uri;
    }

    public AvatarType getType() {
        return _avatarType;
    }

    public Uri getUri() {
        return _uri;
    }
}

package com.example;

public enum AvatarType {
    TYPE_X,
    TYPE_Y
}

package com.example;

import android.net.Uri;
import android.os.AsyncTask;

import java.io.IOException;

import okhttp3.OkHttpClient;
import okhttp3.Request;

public class AvatarDownloaderTask extends AsyncTask<String, String, Avatar> {

    private OnTaskCompleted _listener;

    public AvatarDownloaderTask(OnTaskCompleted listener){
        _listener = listener;
    }


    @Override
    protected Avatar doInBackground(String... params) {
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url(params[0])
                .head()
                .build();

        try {
            okhttp3.Response response = client.newCall(request).execute();

            if (Integer.parseInt(response.header("Content-Length")) > 100) {
                return new Avatar(AvatarType.TYPE_Y, Uri.parse(params[0]));
            }
        } catch (IOException e) {
            cancel(true);
        }
        return new Avatar(AvatarType.TYPE_X, Uri.parse(params[0]));
    }

    @Override
    protected void onPostExecute(Avatar s) {
        super.onPostExecute(s);
        _listener.onTaskCompleted(s);
    }

    @Override
    protected void onCancelled(Avatar s) {
        super.onCancelled(s);
        _listener.onTaskCompleted(s);
    }

    public interface OnTaskCompleted{
        void onTaskCompleted(Avatar avatar);
    }
}

public class ProfileFragment extends Fragment implements AvatarDownloaderTask.OnTaskCompleted{

    /* -stripped out code- */

    // Use this call in your activity or fragment that also implements the OnTaskCompleted interface
    private void loadAvatar(String avatarUrl) {
        new AvatarDownloaderTask(this).execute(avatarUrl);
    }

    @Override
    public void onTaskCompleted(Avatar avatar) {
        switch (avatar.getType()) {
            case TYPE_X:
                _avatarProgressBar.setVisibility(View.GONE);
                _avatarImageView.setVisibility(View.GONE);
                _avatarImageViewPlaceholder.setVisibility(View.VISIBLE);
                break;
            case TYPE_Y:
                // Just a helper class with a shared Picasso instance
                ImageHandler.sharedHandler(getContext()).bypassImageResizer(avatar.getUri(), _avatarImageView, new com.squareup.picasso.Callback() {
                    @Override
                    public void onSuccess() {
                        _avatarProgressBar.setVisibility(View.GONE);
                        _avatarImageViewPlaceholder.setVisibility(View.GONE);
                        _avatarImageView.setVisibility(View.VISIBLE);
                    }

                    @Override
                    public void onError() {
                        _avatarProgressBar.setVisibility(View.GONE);
                        _avatarImageView.setVisibility(View.GONE);
                        _avatarImageViewPlaceholder.setVisibility(View.VISIBLE);
                    }
                });
                break;
        }
    }
}

这绝对不是一个干净的方法,迫使我这样做的 API 设计从一开始就很糟糕。我很想听听其他人的反馈以及如何以其他方式改进 this/fix 的建议。 AsyncTask 的任何其他轻量级方法?将数据传回 main/UI 线程怎么样 - 那可能是 fixed/improved/eliminated?

干杯!