使用Volley执行网络数据传输

Volley 是一个 HTTP 库,它能够帮助 Android app 更方便地执行网络操作,最重要的是,它更快速高效。我们可以通过开源的 AOSP 仓库获取到 Volley 。

Volley 有如下的优点:

  • 自动调度网络请求。

  • 高并发网络连接。

  • 通过标准的 HTTP cache coherence(高速缓存一致性)缓存磁盘和内存透明的响应。

  • 支持指定请求的优先级。

  • 撤销请求 API。我们可以取消单个请求,或者指定取消请求队列中的一个区域。

  • 框架容易被定制,例如,定制重试或者回退功能。

  • 强大的指令(Strong ordering)可以使得异步加载网络数据并正确地显示到 UI 的操作更加简单。

  • 包含了调试与追踪工具。

Volley 擅长执行用来显示 UI 的 RPC 类型操作,例如获取搜索结果的数据。它轻松的整合了任何协议,并输出操作结果的数据,可以是原始的字符串,也可以是图片,或者是 JSON。通过提供内置的我们可能使用到的功能,Volley 可以使得我们免去重复编写样板代码,使我们可以把关注点放在 app 的功能逻辑上。

Volley 不适合用来下载大的数据文件。因为 Volley 会保持在解析的过程中所有的响应。对于下载大量的数据操作,请考虑使用 DownloadManager

Volley 框架的核心代码是托管在 AOSP 仓库的 frameworks/volley 中,相关的工具放在 toolbox 下。把 Volley 添加到项目中最简便的方法是 Clone 仓库,然后把它设置为一个 library project:

  • 通过下面的命令来Clone仓库:
 git clone https://android.googlesource.com/platform/frameworks/volley
  • 以一个 Android library project 的方式导入下载的源代码到你的项目中。(如果你使用 Eclipse,请参考 Managing Projects from Eclipse with ADT,或者编译成一个 .jar 文件。

发送简单的网络请求

使用 Volley 的方式是,创建一个 RequestQueue 并传递 Request 对象给它。RequestQueue 管理用来执行网络操作的工作线程,从缓存中读取数据,写数据到缓存,并解析 Http 的响应内容。请求解析原始的响应数据,Volley 会把解析完的响应数据分发给主线程。

这节课会介绍如何使用 Volley.newRequestQueue 这个便捷的方法(建立一个请求队列 RequestQueue)来发送一个请求。在下一节课建立一个 RequestQueue中,会介绍如何自己建立一个 RequestQueue。

这节课也会介绍如何添加一个请求到 RequesutQueue 以及如何取消一个请求。

1)Add the INTERNET Permission

为了使用Volley,你必须添加android.permission.INTERNET权限到你的manifest文件中。没有这个权限,你的app将无法访问网络。

2)Use newRequestQueue

Volley提供了一个简便的方法:Volley.newRequestQueue用来为你建立一个RequestQueue,使用默认值,并启动这个队列。例如:

final TextView mTextView = (TextView) findViewById(R.id.text);
...

// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";

// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
            new Response.Listener() {
    @Override
    public void onResponse(String response) {
        // Display the first 500 characters of the response string.
        mTextView.setText("Response is: "+ response.substring(0,500));
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        mTextView.setText("That didn't work!");
    }
});
// Add the request to the RequestQueue.
queue.add(stringRequest);

Volley总是将解析后的数据返回至主线程中。在主线程中更加合适使用接收到的数据用来操作UI控件,这样你可以在响应的handler中轻松的修改UI,但是对于库提供的一些其他方法是有些特殊的,例如与取消有关的。

关于如何创建你自己的请求队列,而不是使用Volley.newRequestQueue方法,请查看建立一个请求队列Setting Up a RequestQueue。

3)Send a Request

为了发送一个请求,你只需要构造一个请求并通过add()方法添加到RequestQueue中。一旦你添加了这个请求,它会通过队列,得到处理,然后得到原始的响应数据并返回。

当你执行add()方法时,Volley触发执行一个缓存处理线程以及一系列网络处理线程。当你添加一个请求到队列中,它将被缓存线程所捕获并触发:如果这个请求可以被缓存处理,那么会在缓存线程中执行响应数据的解析并返回到主线程。如果请求不能被缓存所处理,它会被放到网络队列中。网络线程池中的第一个可用的网络线程会从队列中获取到这个请求并执行HTTP操作,解析工作线程的响应数据,把数据写到缓存中并把解析之后的数据返回到主线程。

请注意那些比较耗时的操作,例如I/O与解析parsing/decoding都是执行在工作线程。你可以在任何线程中添加一个请求,但是响应结果都是返回到主线程的。

下图1,演示了一个请求的生命周期:

4)Cancel a Request

对请求Request对象调用cancel()方法取消一个请求。一旦取消,Volley会确保你的响应Handler不会被执行。这意味着在实际操作中你可以在activity的onStop()方法中取消所有pending在队列中的请求。你不需要通过检测getActivity() == null来丢弃你的响应handler,其他类似onSaveInstanceState()等保护性的方法里面也都不需要检测。

为了利用这种优势,你应该跟踪所有已经发送的请求,以便在需要的时候可以取消他们。有一个简便的方法:你可以为每一个请求对象都绑定一个tag对象。然后你可以使用这个tag来提供取消的范围。例如,你可以为你的所有请求都绑定到执行的Activity上,然后你可以在onStop()方法执行requestQueue.cancelAll(this) 。同样的,你可以为ViewPager中的所有请求缩略图Request对象分别打上对应Tab的tag。并在滑动时取消这些请求,用来确保新生成的tab不会被前面tab的请求任务所卡到。

下面一个使用String来打Tag的例子:

  • 定义你的tag并添加到你的请求任务中。
public static final String TAG = "MyTag";
StringRequest stringRequest; // Assume this exists.
RequestQueue mRequestQueue;  // Assume this exists.

// Set the tag on the request.
stringRequest.setTag(TAG);

// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);
  • 在activity的onStop()方法里面,取消所有的包含这个tag的请求任务。
@Override
protected void onStop () {
    super.onStop();
    if (mRequestQueue != null) {
        mRequestQueue.cancelAll(TAG);
    }
}

当取消请求时请注意:如果你依赖你的响应handler来标记状态或者触发另外一个进程,你需要对此进行考虑。再说一次,response handler是不会被执行的。

建立请求队列(RequestQueue)

前一节课演示了如何使用 Volley.newRequestQueue 这一简便的方法来建立一个RequestQueue,这是利用了 Volley 默认行为的优势。这节课会介绍如何显式地建立一个 RequestQueue,以便满足我们自定义的需求。

这节课同样会介绍一种推荐的实现方式:创建一个单例的 RequestQueue,这使得 RequestQueue 能够持续保持在我们 app 的生命周期中。

建立网络和缓存

一个 RequestQueue 需要两部分来支持它的工作:一部分是网络操作,用来传输请求,另外一个是用来处理缓存操作的 Cache。在 Volley 的工具箱中包含了标准的实现方式:DiskBasedCache 提供了每个文件与对应响应数据一一映射的缓存实现。 BasicNetwork 提供了一个基于 AndroidHttpClient 或者HttpURLConnection 的网络传输。

BasicNetwork 是 Volley 默认的网络操作实现方式。一个 BasicNetwork 必须使用我们的 app 用于连接网络的 HTTP Client 进行初始化。这个 Client 通常是AndroidHttpClient 或者 HttpURLConnection:

  • 对于 app target API level 低于 API 9(Gingerbread)的使用 AndroidHttpClient。在 Gingerbread 之前,HttpURLConnection 是不可靠的。对于这个的细节,请参考 Android's HTTP Clients。

  • 对于 API Level 9 以及以上的,使用 HttpURLConnection。

我们可以通过检查系统版本选择合适的 HTTP Client,从而创建一个能够运行在所有 Android 版本上的应用。例如:

HttpStack stack;
...
// If the device is running a version >= Gingerbread...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
    // ...use HttpURLConnection for stack.
} else {
    // ...use AndroidHttpClient for stack.
}
Network network = new BasicNetwork(stack);

下面的代码片段演示了如何一步步建立一个 RequestQueue:

RequestQueue mRequestQueue;

// Instantiate the cache
Cache cache = new DiskBasedCache(getCacheDir(), 1024 * 1024); // 1MB cap

// Set up the network to use HttpURLConnection as the HTTP client.
Network network = new BasicNetwork(new HurlStack());

// Instantiate the RequestQueue with the cache and network.
mRequestQueue = new RequestQueue(cache, network);

// Start the queue
mRequestQueue.start();

String url ="http://www.myurl.com";

// Formulate the request and handle the response.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
        new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        // Do something with the response
    }
},
    new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            // Handle error
    }
});

// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);
...

如果我们仅仅是想做一个单次的请求并且不想要线程池一直保留,我们可以通过使用在前面一课:发送一个简单的请求(Sending a Simple Request)文章中提到的 Volley.newRequestQueue() 方法,在任何需要的时刻创建 RequestQueue,然后在我们的响应回调里面执行 stop() 方法来停止操作。但是更通常的做法是创建一个 RequestQueue 并设置为一个单例。下面部分将演示这种做法。

使用单例模式

如果我们的应用需要持续地使用网络,更加高效的方式应该是建立一个 RequestQueue 的单例,这样它能够持续保持在整个 app 的生命周期中。我们可以通过多种方式来实现这个单例。推荐的方式是实现一个单例类,里面封装了 RequestQueue 对象与其它的 Volley 功能。另外一个方法是继承 Application 类,并在 Application.OnCreate() 方法里面建立 RequestQueue。但是我们并不推荐这个方法,因为一个 static 的单例能够以一种更加模块化的方式提供同样的功能。

一个关键的概念是 RequestQueue 必须使用 Application context 来实例化,而不是 Activity context。这确保了 RequestQueue 在我们 app 的生命周期中一直存活,而不会因为 activity 的重新创建而被重新创建(例如,当用户旋转设备时)。

下面是一个单例类,提供了 RequestQueue 与 ImageLoader 功能:

public class MySingleton {
    private static MySingleton mInstance;
    private RequestQueue mRequestQueue;
    private ImageLoader mImageLoader;
    private static Context mCtx;

    private MySingleton(Context context) {
        mCtx = context;
        mRequestQueue = getRequestQueue();

        mImageLoader = new ImageLoader(mRequestQueue,
                new ImageLoader.ImageCache() {
            private final LruCache<String, Bitmap>
                    cache = new LruCache<String, Bitmap>(20);

            @Override
            public Bitmap getBitmap(String url) {
                return cache.get(url);
            }

            @Override
            public void putBitmap(String url, Bitmap bitmap) {
                cache.put(url, bitmap);
            }
        });
    }

    public static synchronized MySingleton getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new MySingleton(context);
        }
        return mInstance;
    }

    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            // getApplicationContext() is key, it keeps you from leaking the
            // Activity or BroadcastReceiver if someone passes one in.
            mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
        }
        return mRequestQueue;
    }

    public <T> void addToRequestQueue(Request<T> req) {
        getRequestQueue().add(req);
    }

    public ImageLoader getImageLoader() {
        return mImageLoader;
    }
}

下面演示了利用单例类来执行 RequestQueue 的操作:

// Get a RequestQueue
RequestQueue queue = MySingleton.getInstance(this.getApplicationContext()).
    getRequestQueue();
...

// Add a request (in this example, called stringRequest) to your RequestQueue.
MySingleton.getInstance(this).addToRequestQueue(stringRequest);

创建标准的网络请求

这一课会介绍如何使用 Volley 支持的常用请求类型:

  • StringRequest。指定一个 URL 并在响应回调中接收一个原始的字符串数据。请参考前一课的示例。

  • ImageRequest。指定一个 URL 并在响应回调中接收一个图片。

  • JsonObjectRequest 与 JsonArrayRequest(均为 JsonRequest 的子类)。指定一个 URL 并在响应回调中获取到一个 JSON 对象或者 JSON 数组。

如果我们需要的是上面演示的请求类型,那么我们很可能不需要实现一个自定义的请求。这节课会演示如何使用那些标准的请求类型。关于如何实现自定义的请求,请看下一课:实现自定义的请求。

请求一张图片

Volley 为请求图片提供了如下的类。这些类依次有着依赖关系,用来支持在不同的层级进行图片处理:

  • ImageRequest —— 一个封装好的,用来处理 URL 请求图片并且返回一张解完码的位图(bitmap)。它同样提供了一些简便的接口方法,例如指定一个大小进行重新裁剪。它的主要好处是 Volley 会确保类似 decode,resize 等耗时的操作在工作线程中执行。

  • ImageLoader —— 一个用来处理加载与缓存从网络上获取到的图片的帮助类。ImageLoader 是大量 ImageRequest 的协调器。例如,在 ListView 中需要显示大量缩略图的时候。ImageLoader 为通常的 Volley cache 提供了更加前瞻的内存缓存,这个缓存对于防止图片抖动非常有用。这还使得在不阻塞或者延迟主线程的前提下实现缓存命中(这对于使用磁盘 I/O 是无法实现的)。ImageLoader 还能够实现响应联合(response coalescing),避免几乎每一个响应回调里面都设置 bitmap 到 view 上面。响应联合使得能够同时提交多个响应,这提升了性能。

  • NetworkImageView —— 在 ImageLoader 的基础上建立,并且在通过网络 URL 取回的图片的情况下,有效地替换 ImageView。如果 view 从层次结构中分离,NetworkImageView 也可以管理取消挂起请求。

使用 ImageRequest

下面是一个使用 ImageRequest 的示例。它会获取 URL 上指定的图片并显示到 app 上。注意到,里面演示的 RequestQueue 是通过上一课提到的单例类实现的:

ImageView mImageView;
String url = "http://i.imgur.com/7spzG.png";
mImageView = (ImageView) findViewById(R.id.myImage);
...

// Retrieves an image specified by the URL, displays it in the UI.
ImageRequest request = new ImageRequest(url,
    new Response.Listener() {
        @Override
        public void onResponse(Bitmap bitmap) {
            mImageView.setImageBitmap(bitmap);
        }
    }, 0, 0, null,
    new Response.ErrorListener() {
        public void onErrorResponse(VolleyError error) {
            mImageView.setImageResource(R.drawable.image_load_error);
        }
    });
// Access the RequestQueue through your singleton class.
MySingleton.getInstance(this).addToRequestQueue(request);

使用 ImageLoader 和 NetworkImageView

我们可以使用 ImageLoader 与 NetworkImageView 来有效地管理类似 ListView 等显示多张图片的情况。在 layout XML 文件中,我们以与使用 ImageView 差不多的方法使用 NetworkImageView,例如:

<com.android.volley.toolbox.NetworkImageView
        android:id="@+id/networkImageView"
        android:layout_width="150dp"
        android:layout_height="170dp"
        android:layout_centerHorizontal="true" />

我们可以使用 ImageLoader 自身来显示一张图片,例如:

ImageLoader mImageLoader;
ImageView mImageView;
// The URL for the image that is being loaded.
private static final String IMAGE_URL =
    "http://developer.android.com/images/training/system-ui.png";
...
mImageView = (ImageView) findViewById(R.id.regularImageView);

// Get the ImageLoader through your singleton class.
mImageLoader = MySingleton.getInstance(this).getImageLoader();
mImageLoader.get(IMAGE_URL, ImageLoader.getImageListener(mImageView,
         R.drawable.def_image, R.drawable.err_image));

然而,如果我们要做的是为 ImageView 进行图片设置,那么我们可以使用 NetworkImageView 来实现,例如:

ImageLoader mImageLoader;
NetworkImageView mNetworkImageView;
private static final String IMAGE_URL =
    "http://developer.android.com/images/training/system-ui.png";
...

// Get the NetworkImageView that will display the image.
mNetworkImageView = (NetworkImageView) findViewById(R.id.networkImageView);

// Get the ImageLoader through your singleton class.
mImageLoader = MySingleton.getInstance(this).getImageLoader();

// Set the URL of the image that should be loaded into this view, and
// specify the ImageLoader that will be used to make the request.
mNetworkImageView.setImageUrl(IMAGE_URL, mImageLoader);

上面的代码是通过通过前一节课讲到的单例类来访问 RequestQueue 与 ImageLoader。这种方法保证了我们的 app 创建这些类的单例会持续存在于 app 的生命周期。这对于 ImageLoader(一个用来处理加载与缓存图片的帮助类)很重要的原因是:内存缓存的主要功能是允许非抖动旋转。使用单例模式可以使得 bitmap 的缓存比 activity 存在的时间长。如果我们在 activity 中创建 ImageLoader,这个 ImageLoader 有可能会在每次旋转设备的时候都被重新创建。这可能会导致抖动。

举一个 LRU cache 的例子

Volley 工具箱中提供了一种通过 DiskBasedCache 类实现的标准缓存。这个类能够缓存文件到磁盘的指定目录。但是为了使用 ImageLoader,我们应该提供一个自定义的内存 LRC bitmap 缓存,这个缓存实现了 ImageLoader.ImageCache 接口。我们可能想把缓存设置成一个单例。关于更多的有关内容,请参考建立请求队列.

下面是一个内存 LruBitmapCache 类的实现示例。它继承 LruCache 类并实现了 ImageLoader.ImageCache 接口:

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import android.util.DisplayMetrics;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap>
        implements ImageCache {

    public LruBitmapCache(int maxSize) {
        super(maxSize);
    }

    public LruBitmapCache(Context ctx) {
        this(getCacheSize(ctx));
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }

    // Returns a cache size equal to approximately three screens worth of images.
    public static int getCacheSize(Context ctx) {
        final DisplayMetrics displayMetrics = ctx.getResources().
                getDisplayMetrics();
        final int screenWidth = displayMetrics.widthPixels;
        final int screenHeight = displayMetrics.heightPixels;
        // 4 bytes per pixel
        final int screenBytes = screenWidth * screenHeight * 4;

        return screenBytes * 3;
    }
}

下面是如何实例化一个 ImageLoader 来使用这个 cache:

RequestQueue mRequestQueue; // assume this exists.
ImageLoader mImageLoader = new ImageLoader(mRequestQueue, new LruBitmapCache(LruBitmapCache.getCacheSize()));

请求 JSON

Volley 提供了以下的类用来执行 JSON 请求:

  • JsonArrayRequest —— 一个为了获取给定 URL 的 JSONArray 响应正文的请求。

  • JsonObjectRequest —— 一个为了获取给定 URL 的 JSONObject 响应正文的请求。允许传进一个可选的 JSONObject 作为请求正文的一部分。

这两个类都是基于一个公共基类 JsonRequest。我们遵循我们在其它请求类型使用的同样的基本模式来使用这些类。如下演示了如果获取一个 JSON feed 并显示到 UI 上:

TextView mTxtDisplay;
ImageView mImageView;
mTxtDisplay = (TextView) findViewById(R.id.txtDisplay);
String url = "http://my-json-feed";

JsonObjectRequest jsObjRequest = new JsonObjectRequest
        (Request.Method.GET, url, null, new Response.Listener() {

    @Override
    public void onResponse(JSONObject response) {
        mTxtDisplay.setText("Response: " + response.toString());
    }
}, new Response.ErrorListener() {

    @Override
    public void onErrorResponse(VolleyError error) {
        // TODO Auto-generated method stub

    }
});

// Access the RequestQueue through your singleton class.
MySingleton.getInstance(this).addToRequestQueue(jsObjRequest);

关于基于 Gson 实现一个自定义的 JSON 请求对象,请参考下一节课:实现一个自定义的请求。

实现自定义的网络请求

编写一个自定义请求

大多数的请求类型都已经包含在 Volley 的工具箱里面。如果我们的请求返回数值是一个 string,image 或者 JSON,那么是不需要自己去实现请求类的。

对于那些需要自定义的请求类型,我们需要执行以下操作:

  • 继承 Request<T> 类,<T> 表示解析过的响应请求预期的数据类型。因此如果我们需要解析的响应类型是一个 String,可以通过继承 Request<String> 来创建自定义的请求。请参考 Volley 工具类中的 StringRequest 与 ImageRequest 来学习如何继承 Request<T>

  • 实现抽象方法 parseNetworkResponse() 与 deliverResponse(),下面会详细介绍。

parseNetworkResponse

一个 Response 封装了用于发送的给定类型(例如,string、image、JSON等)解析过的响应。下面会演示如何实现 parseNetworkResponse():

@Override
protected Response<T> parseNetworkResponse(
        NetworkResponse response) {
    try {
        String json = new String(response.data,
        HttpHeaderParser.parseCharset(response.headers));
    return Response.success(gson.fromJson(json, clazz),
    HttpHeaderParser.parseCacheHeaders(response));
    }
    // handle errors
...
}

请注意:

  • parseNetworkResponse() 的参数是类型是 NetworkResponse,这种参数以 byte[]、HTTP status code 以及 response headers 的形式包含响应负载。

  • 我们实现的方法必须返回一个 Response<T>,它包含了我们指定类型的响应对象与缓存 metadata 或者是一个错误。

如果我们的协议没有标准的缓存机制,那么我们可以自己建立一个 Cache.Entry, 但是大多数请求都可以用下面的方式来处理:

return Response.success(myDecodedObject,
        HttpHeaderParser.parseCacheHeaders(response));

Volley 在工作线程中执行 parseNetworkResponse() 方法。这确保了耗时的解析操作,例如 decode 一张 JPEG 图片成 bitmap,不会阻塞 UI 线程。

deliverResponse

Volley 会把 parseNetworkResponse() 方法返回的数据带到主线程的回调中。如下所示:

protected void deliverResponse(T response) {
        listener.onResponse(response);

Example: GsonRequest

Gson 是一个使用映射支持 JSON 与 Java 对象之间相互转换的库文件。我们可以定义与 JSON keys 相对应名称的 Java 对象。把对象传递给 Gson,然后 Gson 会帮我们为对象填充字段值。下面是一个完整的示例:演示了使用 Gson 解析 Volley 数据:

public class GsonRequest<T> extends Request<T> {
    private final Gson gson = new Gson();
    private final Class<T> clazz;
    private final Map<String, String> headers;
    private final Listener<T> listener;

    /**
     * Make a GET request and return a parsed object from JSON.
     *
     * @param url URL of the request to make
     * @param clazz Relevant class object, for Gson's reflection
     * @param headers Map of request headers
     */
    public GsonRequest(String url, Class<T> clazz, Map<String, String> headers,
            Listener<T> listener, ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        this.clazz = clazz;
        this.headers = headers;
        this.listener = listener;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return headers != null ? headers : super.getHeaders();
    }

    @Override
    protected void deliverResponse(T response) {
        listener.onResponse(response);
    }

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String json = new String(
                    response.data,
                    HttpHeaderParser.parseCharset(response.headers));
            return Response.success(
                    gson.fromJson(json, clazz),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }
}

如果你愿意使用的话,Volley 提供了现成的 JsonArrayRequest 与 JsonArrayObject类。参考上一课创建标准的网络请求。

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1