码迷,mamicode.com
首页 > 移动开发 > 详细

Android Volley框架完全解析

时间:2016-09-22 08:49:05      阅读:219      评论:0      收藏:0      [点我收藏+]

标签:

2013年Google I/O大会上推出了一个新的网络通信框架——Volley。Volley可是说是把AsyncHttpClient和Universal-Image-Loader的优点集于了一身,既可以像AsyncHttpClient一样非常简单地进行HTTP通信,也可以像Universal-Image-Loader一样轻松加载网络上的图片。除了简单易用之外,Volley在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。

准备工作

导入JAR包(下载地址),申请网络权限

<uses-permission android:name="android.permission.INTERNET" /> 

HTTP请求与响应

1. 使用StringRequest接收String类型的响应

一个最基本的HTTP请求与响应主要就是进行以下三步操作:

  • 创建一个RequestQueue对象。
  • 创建一个StringRequest对象(以StringRequest为例,后面还会介绍其他Request)。
  • 将StringRequest对象添加到RequestQueue里面。

(1)初始化请求队列对象——RequestQueue

RequestQueue mQueue = Volley.newRequestQueue(context);

RequestQueue是一个请求队列对象,它可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求。RequestQueue内部的设计就是非常合适高并发的,因此我们不必为每一次HTTP请求都创建一个RequestQueue对象,这是非常浪费资源的。所以这里建议用单例模式定义这个对象。当然,你可以选择在一个activity中定义一个RequestQueue对象,但这样可能会比较麻烦,而且还可能出现请求队列包含activity强引用的问题。

(2)使用StringRequest接收String类型的响应
前面定义了请求对象,那么自然就有接收响应的对象了,这个框架中有多个响应对象,像StringRequest接受到的响应就是string类型的;JsonRequest接收的响应就是Json类型对象。其实它们都是继承自Request<\T>,然后根据不同的响应数据来进行特殊的处理。
技术分享

来看StringRequest的两个构造函数

/** method:请求方法
    url:请求的地址
    listener:响应成功的监听器
    errorListener:出错时的监听器 **/
public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener)

/**不传入method,默认会调用GET方式进行请求**/
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
    this(Method.GET, url, listener, errorListener);
}

GET方式请求网络,代码如下:

StringRequest stringRequest = new StringRequest("http://www.baidu.com",
                        new Response.Listener<String>() {
                            @Override
                            public void onResponse(String response) {
                                Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
                            }
                        }, new Response.ErrorListener() {
                            @Override
                            public void onErrorResponse(VolleyError error) {
                                showlog(error.getMessage());
                            }
                        });

POST方式请求网络,一般我们的POST都是要带一些参数的,Volley没有提供附加参数的方法,所以我们必须要在StringRequest的匿名类中重写getParams()方法,代码如下所示:

StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener) {  
    @Override  
    protected Map<String, String> getParams() throws AuthFailureError {  
        Map<String, String> map = new HashMap<String, String>();  
        map.put("params1", "value1");  
        map.put("params2", "value2");  
        return map;  
    }  
};

这样就传入了value1和value2两个参数了。现在可能有人会问为啥这个框架不提供这个传参的方法,还非得让我们重写。个人觉得这个框架本身的目的就是执行频繁的网络请求,比如下载图片,解析json数据什么的,用GET就能很好的实现了,所以就没有提供传参的POST方法。

(3)发送请求
发送请求很简单,将StringRequest对象添加到RequestQueue里面即可。

mQueue.add(stringRequest);

运行一下程序,发出一条HTTP请求,把服务器返回的string用Toast展示出来:
技术分享
没错,百度返回给我们的就是这样一长串的HTML代码,虽然我们看起来会有些吃力,但是浏览器却可以轻松地对这段HTML代码进行解析,然后将百度的首页展现出来。

2. 使用JsonObjectRequest接收Json类型的响应

类似于StringRequest,JsonRequest也是继承自Request类的,不过由于JsonRequest是一个抽象类,因此我们无法直接创建它的实例,那么只能从它的子类入手了。JsonRequest有两个直接的子类,JsonObjectRequest和JsonArrayRequest,从名字上你应该能就看出它们的区别了吧?一个是用于请求一段JSON数据的,一个是用于请求一段JSON数组的。
这里看一下JsonObjectRequest的构造函数:

//jsonRequest:POST请求携带的参数,可以为空,表示不携带参数
public JsonObjectRequest(int method, String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener) {
     super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener, errorListener);
}

//如果jsonRequest为空,默认使用GET请求,否则使用POST
public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener) {
      this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest, listener, errorListener);
}

和StringRequest一样,遵循三步走原则:

RequestQueue mQueue = Volley.newRequestQueue(context);
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0", null,  
        new Response.Listener<JSONObject>() {  
            @Override  
            public void onResponse(JSONObject response) {  
                Toast.makeText(MainActivity.this, response.toString(), Toast.LENGTH_SHORT).show();
                try {
                    response = response.getJSONObject("weatherinfo");
                    showlog("city = " + response.getString("city"));
                    showlog("weather1 = " + response.getString("weather1"));
                    } catch (JSONException e) {
                    e.printStackTrace();
                    }
            }  
        }, new Response.ErrorListener() {  
            @Override  
            public void onErrorResponse(VolleyError error) {  
                showlog(error.getMessage());  
            }  
        });
mQueue.add(jsonObjectRequest);

注意JsonObjectRequest的POST方式携带参数和StringRequest有些不同,上面StringRequest的方式在这里不起作用。需要下面方式实现:

Map<String, String> params = new HashMap<String, String>();  
params.put("name1", "value1");  
params.put("name2", "value2");  
JSONObject jsonRequest= new JSONObject(params);
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Method.POST, url, jsonRequest, listener, errorListener)

上面我们请求的地址是中央天气预报的上海天气,看一下运行效果:
技术分享
可以看出,服务器返回给我们的数据确实是JSON格式的,并且onResponse()方法中携带的参数也正是一个JSONObject对象,之后只需要从JSONObject对象取出我们想要得到的那部分数据就可以了。
技术分享

3. 使用ImageRequest来请求图片

首先来看一下ImageRequest的构造函数

public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, Config decodeConfig, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        setRetryPolicy(new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
    }

默认的请求方式是GET,初始化方法需要传入:图片的url,一个响应结果监听器,图片的最大宽度,图片的最大高度,图片的颜色属性,出错响应的监听器。
第三第四个参数分别用于指定允许图片最大的宽度和高度,如果指定的网络图片的宽度或高度大于这里的最大值,则会对图片“等比例”进行压缩,指定成0的话就表示不管图片有多大,都不会进行压缩。第五个参数用于指定图片的颜色属性,Bitmap.Config下的几个常量都可以在这里使用,其中ARGB_8888可以展示最好的颜色属性,每个图片像素占据4个字节的大小,而RGB_565则表示每个图片像素占据2个字节大小。
三步走开始:

        RequestQueue mQueue = Volley.newRequestQueue(context);
        ImageRequest imageRequest = new ImageRequest(  
                "http://img.my.csdn.net/uploads/201308/31/1377949454_6367.jpg",  
                new Response.Listener<Bitmap>() {  
                    @Override  
                    public void onResponse(Bitmap response) {  
                        image.setImageBitmap(response);  
                    }  
                }, 0, 0, Config.RGB_565, new Response.ErrorListener() {  
                    @Override  
                    public void onErrorResponse(VolleyError error) {  
                        image.setImageResource(R.drawable.default_image);  
                    }  
                }); 
        mQueue.add(imageRequest);

看运行效果图:
技术分享

加载图片— ImageLoader & NetworkImageView

Volley有没有其他的,更好的方式来获取图片呢?当然有的,比如ImageLoader、NetworkImageView这样的对象,它们可以更加方便的获取图片。值得一提的是这两个对象的内部都是使用了ImageRequest进行操作的,也就是说ImageRequest是本质。

1. ImageLoader加载图片

ImageLoader也可以用于加载网络上的图片,不过ImageLoader明显要比ImageRequest更加高效,因为它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求。
由于ImageLoader已经不是继承自Request的了,所以它的用法也和我们之前学到的内容有所不同,总结起来大致可以分为以下四步:

  • 创建一个RequestQueue对象。
  • 创建一个ImageLoader对象。
  • 获取一个ImageListener对象。
  • 调用ImageLoader的get()方法加载网络上的图片。

(1)创建一个RequestQueue对象
我们前面已经写过很多遍了,不再重复介绍了

(2)创建一个ImageLoader对象
示例代码如下所示:

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

可以看到,ImageLoader的构造函数接收两个参数,第一个参数就是RequestQueue对象,第二个参数是一个ImageCache对象(不能传null!),这里的ImageCache就是为我们做内存缓存用的,我们可以定制自己的实现方式,现在主流的实现是LruCache,关于LruCache可以参考我之前写的一篇文章Android的缓存技术:LruCache和DiskLruCache

ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
//BitmapCache的实现类
public class BitmapCache implements ImageCache {
    private LruCache<String, Bitmap> mCache;

    public BitmapCache() {
        int maxSize = 10 * 1024 * 1024;
        mCache = new LruCache<String, Bitmap>(maxSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight();
            }
        };

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

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

(3)获取一个ImageListener对象

ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_image, R.drawable.fail_image);  

我们通过调用ImageLoader的getImageListener()方法能够获取到一个ImageListener对象,getImageListener()方法接收三个参数,第一个参数指定用于显示图片的ImageView控件,第二个参数指定加载图片的过程中显示的图片,第三个参数指定加载图片失败的情况下显示的图片。

(4)调用ImageLoader的get()方法加载网络上的图片

imageLoader.get("http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg", listener); 

get()方法接收两个参数,第一个参数就是图片的URL地址,第二个参数则是刚刚获取到的ImageListener对象。当然,如果你想对图片的大小进行限制,也可以使用get()方法的重载,指定图片允许的最大宽度和高度,如下所示:

imageLoader.get("http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg", listener, 600, 600); 

运行一下程序点击加载图片,你将看到ImageView会先显示一张默认的加载过程中图片,等到网络上的图片加载完成后,ImageView则会自动显示该图。如果我们用ImageLoader再次加载该图片,会很快显示出来而看不到默认的加载过程中图片,这是因为这次的图片是从缓存中取的,速度很快。效果如下图所示。
技术分享

注:上面我们只是定制了内存缓存,查看源码,可以发现ImageLoader对图片也进行了硬盘缓存,我们在执行get()方法前可以通过imageLoader.setShouldCache(false);来取消硬盘缓存,如果你不进行设置的话默认是执行硬盘缓存的。看看控制硬盘缓存的几个方法:

public final boolean shouldCache() //查看是否已经做了磁盘缓存。
void setShouldCache(boolean shouldCache)//设置是否运行磁盘缓存,此方法需要在get方法前使用
public boolean isCached(String requestUrl, int maxWidth, int maxHeight)//判断对象是否已经被缓存,传入url,还有图片的最大宽高

2. NetworkImageView加载图片

NetworkImageView继承自ImageView,你可以认为它是一个可以实现加载网络图片的imageview,十分简单好用。这个控件在被从父控件分离的时候,会自动取消网络请求的,即完全不用我们担心相关网络请求的生命周期问题。
NetworkImageView控件的用法大致可以分为以下五步:

  • 创建一个RequestQueue对象。
  • 创建一个ImageLoader对象。
  • 在布局文件中添加一个NetworkImageView控件。
  • 在代码中获取该控件的实例。
  • 设置要加载的图片地址。
<com.android.volley.toolbox.NetworkImageView
     android:id="@+id/network_image_view"
     android:layout_width="200dp"
     android:layout_height="200dp"
     android:layout_gravity="center_horizontal" />
/**创建RequestQueue以及ImageLoader对象**/
RequestQueue mQueue = Volley.newRequestQueue(context);
ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache()); 
/**获取NetworkImageView控件**/
NetworkImageView networkImageView = (NetworkImageView) findViewById(R.id.network_image_view);
/**设置加载中显示的图片**/
networkImageView.setDefaultImageResId(R.drawable.default_image);
/**加载失败时显示的图片**/
networkImageView.setErrorImageResId(R.drawable.fail_image);
/**设置目标图片的URL地址**/
networkImageView.setImageUrl("http://img.my.csdn.net/uploads/201309/01/1378037151_7904.jpg", imageLoader);

好了,就是这么简单,现在重新运行一下程序,你将看到和使用ImageLoader来加载图片一模一样的效果,这里我就不再截图了。
NetworkImageView没有提供任何设置图片宽高的方法,这是由于它是一个控件,在加载图片的时候它会自动获取自身的宽高,然后对比网络图片的宽度,再决定是否需要对图片进行压缩。也就是说,压缩过程是在内部完全自动化的,并不需要我们关心。NetworkImageView最终会始终呈现给我们一张大小比控件尺寸略大的网络图片,因为它会根据控件宽高来等比缩放原始图片,不会多占用任何一点内存,这也是NetworkImageView最简单好用的一点吧。
如果你不想对图片进行压缩的话,只需要在布局文件中把NetworkImageView的layout_width和layout_height都设置成wrap_content就可以了,这样它就会将该图片的原始大小展示出来,不会进行任何压缩。

自定义Request

Volley中提供了几个常用Request(StringRequest、JsonObjectRequest、JsonArrayRequest、ImageRequest),如果我们有自己特殊的需求,其实完全可以自定义自己的Request。
自定义Request之前,我们先来看看StringRequest的源码实现:

package com.android.volley.toolbox;

public class StringRequest extends Request<String> {
    // 建立监听器来获得响应成功时返回的结果
    private final Listener<String> mListener; 

    // 传入请求方法,url,成功时的监听器,失败时的监听器
    public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) {
        super(method, url, errorListener);
        // 初始化成功时的监听器
        mListener = listener;
    }

    /**
     * Creates a new GET request.
     * 建立一个默认的GET请求,调用了上面的构造函数
     */
    public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
        this(Method.GET, url, listener, errorListener);
    }

    @Override
    protected void deliverResponse(String response) {
        // 用监听器的方法来传递下响应的结果
        mListener.onResponse(response);
    }

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            // 调用了new String(byte[] data, String charsetName) 这个构造函数来构建String对象,将byte数组按照特定的编码方式转换为String对象,主要部分是data
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }
}

首先StringRequest是继承自Request类的,Request可以指定一个泛型类,这里指定的当然就是String了,接下来StringRequest中提供了两个有参的构造函数,参数包括请求类型,请求地址,以及响应回调等。但需要注意的是,在构造函数中一定要调用super()方法将这几个参数传给父类,因为HTTP的请求和响应都是在父类中自动处理的。
另外,由于Request类中的deliverResponse()和parseNetworkResponse()是两个抽象方法,因此StringRequest中需要对这两个方法进行实现。deliverResponse()方法中的实现很简单,仅仅是调用了mListener中的onResponse()方法,并将response内容传入即可,这样就可以将服务器响应的数据进行回调了。parseNetworkResponse()方法中则是对服务器响应的数据进行解析,其中数据是以字节的形式存放在NetworkResponse的data变量中的,这里将数据取出然后组装成一个String,并传入Response的success()方法中即可。

1. 自定义XMLRequest

了解了StringRequest的实现原理,下面我们就可以动手来尝试实现一下XMLRequest了,代码如下所示:

public class XMLRequest extends Request<XmlPullParser> {

    private final Listener<XmlPullParser> mListener;

    public XMLRequest(int method, String url, Listener<XmlPullParser> listener, ErrorListener errorListener) {
        super(method, url, errorListener);
        mListener = listener;
    }

    public XMLRequest(String url, Listener<XmlPullParser> listener, ErrorListener errorListener) {
        this(Method.GET, url, listener, errorListener);
    }

    @Override
    protected Response<XmlPullParser> parseNetworkResponse(NetworkResponse response) {
        try {
            String xmlString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlString));
            return Response.success(xmlPullParser, HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (XmlPullParserException e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(XmlPullParser response) {
        mListener.onResponse(response);
    }

}

可以看到,其实并没有什么太多的逻辑,基本都是仿照StringRequest写下来的,XMLRequest也是继承自Request类的,只不过这里指定的泛型类是XmlPullParser,说明我们准备使用Pull解析的方式来解析XML。在parseNetworkResponse()方法中,先是将服务器响应的数据解析成一个字符串,然后设置到XmlPullParser对象中,在deliverResponse()方法中则是将XmlPullParser对象进行回调。
下面我们尝试使用这个XMLRequest来请求一段XML格式的数据,http://flash.weather.com.cn/wmaps/xml/china.xml这个接口会将中国所有的省份数据以XML格式进行返回,如下所示:
技术分享

    XMLRequest xmlRequest = new XMLRequest("http://flash.weather.com.cn/wmaps/xml/china.xml",  
                new Response.Listener<XmlPullParser>() {  
                    @Override  
                    public void onResponse(XmlPullParser response) {  
                        try {  
                            int eventType = response.getEventType();  
                            while (eventType != XmlPullParser.END_DOCUMENT) {  
                                switch (eventType) {  
                                case XmlPullParser.START_TAG:  
                                    String nodeName = response.getName();  
                                    if ("city".equals(nodeName)) {  
                                        String pName = response.getAttributeValue(0);  
                                        String cName = response.getAttributeValue(2);  
                                        showlog("省份:" + pName + " 城市:" + cName);
                                    }  
                                    break;  
                                }  
                                eventType = response.next();  
                            }  
                        } catch (XmlPullParserException e) {  
                            e.printStackTrace();  
                        } catch (IOException e) {  
                            e.printStackTrace();  
                        }  
                    }  
                }, new Response.ErrorListener() {  
                    @Override  
                    public void onErrorResponse(VolleyError error) {  
                        showlog(error.getMessage());  
                    }  
                });  
    mQueue.add(xmlRequest);

技术分享

2. 自定义GsonRequest

JsonRequest的数据解析是利用Android本身自带的JSONObject和JSONArray来实现的,配合使用JSONObject和JSONArray就可以解析出任意格式的JSON数据。不过也许你会觉得使用JSONObject还是太麻烦了,还有很多方法可以让JSON数据解析变得更加简单,比如说GSON对象。遗憾的是,Volley中默认并不支持使用自家的GSON来解析数据,不过没有关系,通过上面的学习,相信你已经知道了自定义一个Request是多么的简单,那么下面我们就来举一反三一下,自定义一个GsonRequest。
首先我们需要把GSON的jar包导入到项目当中,接着定义一个GsonRequest继承自Request,代码如下所示:

public class GsonRequest<T> extends Request<T> {

    private final Listener<T> mListener;
    private Gson mGson;
    private Class<T> mClass;

    public GsonRequest(int method, String url, Class<T> clazz, Listener<T> listener, ErrorListener errorListener) {
        super(method, url, errorListener);
        mGson = new Gson();
        mClass = clazz;
        mListener = listener;
    }

    public GsonRequest(String url, Class<T> clazz, Listener<T> listener, ErrorListener errorListener) {
        this(Method.GET, url, clazz, listener, errorListener);
    }

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

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

}

GsonRequest是继承自Request类的,并且同样提供了两个构造函数。在parseNetworkResponse()方法中,先是将服务器响应的数据解析出来,然后通过调用Gson的fromJson方法将数据组装成对象。在deliverResponse方法中仍然是将最终的数据进行回调。
下面我们就来测试一下这个GsonRequest能不能够正常工作吧,同样调用http://www.weather.com.cn/data/sk/101020100.html这个接口可以得到一段JSON格式的天气数据,如下所示:

{"weatherinfo":{"city":"上海","city_en":"","cityid":101020100,"date":"","date_y":"2016年09月20日","fchh":0,"fl1":"","fl2":"","fl3":"","fl4":"","fl5":"","fl6":"","fx1":"","fx2":"","img1":"1","img10":"1","img11":"1","img12":"1","img2":"1","img3":"1","img4":"1","img5":"1","img6":"1","img7":"1","img8":"1","img9":"1","img_single":0,"img_title1":"","img_title10":"","img_title11":"","img_title12":"","img_title2":"","img_title3":"","img_title4":"","img_title5":"","img_title6":"","img_title7":"","img_title8":"","img_title9":"","img_title_single":"","index":"","index48":"","index48_d":"","index48_uv":"","index_ag":"","index_cl":"","index_co":"","index_d":"","index_ls":"","index_tr":"","index_uv":"","index_xc":"","st1":0,"st2":0,"st3":0,"st4":0,"st5":0,"st6":0,"temp1":"20℃~28℃","temp2":"20℃~26℃","temp3":"19℃~26℃","temp4":"21℃~26℃","temp5":"23℃~28℃","temp6":"22℃~27℃","tempF1":"","tempF2":"","tempF3":"","tempF4":"","tempF5":"","tempF6":"","weather1":"多云","weather2":"多云","weather3":"多云","weather4":"多云","weather5":"多云","weather6":"多云","week":"","wind1":"","wind2":"","wind3":"","wind4":"","wind5":"","wind6":""}}

我们需要使用对象的方式将这段JSON字符串表示出来。下面新建两个Bean文件:

public class Weather {
    public WeatherInfo weatherinfo;
}
public class WeatherInfo {
    public String city;
    public String cityid;
    public String date_y;
    public String temp1;
    public String weather1;
}

下面就是用GsonRequest请求json数据了

GsonRequest<Weather> gsonRequest = new GsonRequest<Weather>(
        "http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0", Weather.class,
        new Response.Listener<Weather>() {
            @Override
            public void onResponse(Weather weather) {
                WeatherInfo weatherInfo = weather.weatherinfo;
                showlog("city is " + weatherInfo.city);
                showlog("cityid is " + weatherInfo.cityid);
                showlog("date_y is " + weatherInfo.date_y);
                showlog("temp1 is " + weatherInfo.temp1);
                showlog("weather1 is " + weatherInfo.weather1);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                showlog(error.getMessage());
            }
        });
mQueue.add(gsonRequest);

这里onResponse()方法的回调中直接返回了一个Weather对象,我们通过它就可以得到WeatherInfo对象,接着就能从中取出JSON中的相关数据了。运行一下程序,打印Log如下:
技术分享

3. 自定义GsonRequestWithAuth

上面自定义的Request并没有携带参数,如果我们访问服务器时需要传参呢?譬如通过客户端访问服务器,服务器对客户端进行身份校验后,返回用户信息,客户端直接拿到对象。
先写Bean文件:

public class User {
    private String name;  
    private int age; 
}  

自定义GsonRequestWithAuth:

  public class GsonRequestWithAuth<T> extends Request<T> {  
    private final Gson gson = new Gson();  
    private final Class<T> clazz;  
    private final Listener<T> listener;  
    private Map<String, String> mHeader = new HashMap<String, String>(); 
    private String mBody;
    /** http请求编码方式 */  
    private static final String PROTOCOL_CHARSET = "utf-8"; 
    /** 设置访问自己服务器时必须传递的参数,密钥等 */  
    static  
    {  
        mHeader.put("APP-Key", "Key");  
        mHeader.put("APP-Secret", "Secret"); 
    }  

    /** 
     * @param url 
     * @param clazz 我们最终的转化类型 
     * @param listener 
     * @param appendHeader 附加头数据 
     * @param body 请求附带消息体 
     * @param errorListener 
     */  
    public GsonRequestWithAuth(String url, Class<T> clazz, Listener<T> listener, Map<String, String> appendHeader, String body, ErrorListener errorListener) {  
        super(Method.POST, url, errorListener);  
        this.clazz = clazz;  
        this.listener = listener;
        mHeader.putAll(appendHeader); 
        mBody = body; 
    }  

    @Override  
    public Map<String, String> getHeaders() throws AuthFailureError {  
        // 默认返回 return Collections.emptyMap();  
        return mHeader;
    }  

    @Override  
    public byte[] getBody() {
        try {  
            return mBody == null ? null : mBody.getBytes(PROTOCOL_CHARSET);  
        } catch (UnsupportedEncodingException uee) {  
            VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", mUserName, PROTOCOL_CHARSET);  
            return null;  
        }  
    }

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

    @Override  
    protected Response<T> parseNetworkResponse(NetworkResponse response) {  
        try  
        {  
            /** 得到返回的数据 */  
            String jsonStr = new String(response.data, HttpHeaderParser.parseCharset(response.headers));  
            /** 转化成对象 */  
            return Response.success(gson.fromJson(jsonStr, clazz), HttpHeaderParser.parseCacheHeaders(response));  
        } catch (UnsupportedEncodingException e)  
        {  
            return Response.error(new ParseError(e));  
        } catch (JsonSyntaxException e)  
        {  
            return Response.error(new ParseError(e));  
        }  
    }  
}  

服务器代码:

public class TestServlet extends HttpServlet {  
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
        this.doPost(request, response);  
    }  

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
        request.setCharacterEncoding("utf-8");  
        /**获取APP-Key和APP-Secret */   
        String appKey = request.getHeader("APP-Key");  
        String appSecret = request.getHeader("APP-Secret");  
        /**获取用户名、密码 */ 
        String username = request.getHeader("username");  
        String password = request.getHeader("password");  
        /**获取消息体 */
        int size = request.getContentLength();
        InputStream is = request.getInputStream();
        byte[] reqBodyBytes = readBytes(is, size);
        String body = new String(reqBodyBytes);

        if ("admin".equals(username) && "123".equals(password) && "getUserInfo".equals(body)) {  
            response.setContentType("text/plain;charset=utf-8");  
            PrintWriter out = response.getWriter();  
            out.print("{\"name\":\"Watson\",\"age\":28}");  
            out.flush();                
        }    
    }    
}  

使用GsonRequestWithAuth和服务器交互请求信息:

Map<String, String> appendHeader = new HashMap<String, String>();  
        appendHeader.put("username", "admin");  
        appendHeader.put("password", "123");

        String url = "http://172.27.35.1:8080/webTest/TestServlet";  
        GsonRequestWithAuth<User> userRequest = new GsonRequestWithAuth<User>(url, User.class, new Listener<User>() {  
            @Override  
            public void onResponse(User response)  
            {  
                Log.e("TAG", response.toString());  
            }  
        }, appendHeader, "getUserInfo", null);  

        mQueue.add(userRequest);

延伸:
看到没有,我们上面写服务器端代码时,有一句代码是设置服务器返回数据的字符集为UTF-8

response.setContentType("text/plain;charset=utf-8");  

大部分服务器端都会在返回数据的header中指定字符集,如果在服务器端没有指定字符集那么就会默认使用 ISO-8859-1 字符集。
ISO-8859-1的别名叫做Latin1。这个字符集支持部分是用于欧洲的语言,不支持中文,这就会导致服务器返回的中文数据乱码,很不能理解为什么将这个字符集作为默认的字符集。Volley这个框架可是要用在网络通信的环境中的。吐槽也没有用,我们来看一下如何来解决中文乱码的问题。有以下几种解决方式:

  • 在服务器的返回的数据的header的中contentType加上charset=UTF-8的声明。
  • 当你无法修改服务器程序的时候,可以定义一个新的子类。覆盖parseNetworkResponse这个方法,直接使用UTF-8对服务器的返回数据进行转码。
public class CharsetStringRequest extends StringRequest {

  public CharsetStringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
      super(url, listener, errorListener);
  }

  public CharsetStringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) {
      super(method, url, listener, errorListener);
  }

  @Override
  protected Response<String> parseNetworkResponse(NetworkResponse response) {
      String str = null;
      try {
          str = new String(response.data,"utf-8"); //在此处强制utf-8编码
      } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
      }
      return Response.success(str, HttpHeaderParser.parseCacheHeaders(response));
  }

}

使用CharsetStringRequest请求数据:

CharsetStringRequest stringRequest = new CharsetStringRequest("http://www.weather.com.cn/data/sk/101010100.html",
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                           showlog(response);
                    }
                }, new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                            showlog(error.getMessage());
                    }
                });
mQueue.add(userRequest);

Volley架构解析

1. 总体设计图

技术分享
上面是 Volley 的总体设计图,主要是通过两种Diapatch Thread不断从RequestQueue中取出请求,根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据,然后交由ResponseDelivery去做结果分发及回调处理。

2. Volley中的概念

简单介绍一些概念,在详细设计中会仔细介绍。
Volley 的调用比较简单,通过 newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue后,只需要往这个RequestQueue不断 add Request 即可。
Volley:Volley 对外暴露的 API,通过 newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue。
Request:表示一个请求的抽象类。StringRequest、JsonRequest、ImageRequest都是它的子类,表示某种类型的请求。
RequestQueue:表示请求队列,里面包含一个CacheDispatcher(用于处理走缓存请求的调度线程)、NetworkDispatcher数组(用于处理走网络请求的调度线程),一个ResponseDelivery(返回结果分发接口),通过 start() 函数启动时会启动CacheDispatcher和NetworkDispatchers。
CacheDispatcher:一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入NetworkDispatcher去调度处理。
NetworkDispatcher:一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理,并判断结果是否要进行缓存。
ResponseDelivery:返回结果分发接口,目前只有基于ExecutorDelivery的在入参 handler 对应线程内进行分发。
HttpStack:处理 Http 请求,返回请求结果。目前 Volley 中有基于 HttpURLConnection 的HurlStack和 基于 Apache HttpClient 的HttpClientStack。
Network:调用HttpStack处理请求,并将结果转换为可被ResponseDelivery处理的NetworkResponse。
Cache:缓存请求结果,Volley 默认使用的是基于 sdcard 的DiskBasedCache。NetworkDispatcher得到请求结果后判断是否需要存储在 Cache,CacheDispatcher会从 Cache 中取缓存结果。

3. 流程图

Volley 请求流程图
技术分享
其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。

4. 源码分析

使用Volley的第一步,首先要调用Volley.newRequestQueue(context)方法来获取一个RequestQueue对象,那么我们自然要从这个方法开始看起了,代码如下所示:

public static RequestQueue newRequestQueue(Context context) {  
    return newRequestQueue(context, null);  
} 
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {  
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);  
    String userAgent = "volley/0";  
    try {  
        String packageName = context.getPackageName();  
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);  
        userAgent = packageName + "/" + info.versionCode;  
    } catch (NameNotFoundException e) {  
    }  
    //如果stack是等于null的,则去创建一个HttpStack对象,手机系统版本号是大于9的,则创建一个HurlStack的实例,否则就创建一个HttpClientStack的实例,HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的
    if (stack == null) {  
        if (Build.VERSION.SDK_INT >= 9) {  
            stack = new HurlStack();  
        } else {  
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));  
        }  
    } 
    //创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的
    Network network = new BasicNetwork(stack);  
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);  
    queue.start();  
    return queue;  
}  

最终会走到RequestQueue的start()方法,然后将RequestQueue返回。去看看RequestQueue的start()方法内部到底执行了什么?

public void start() {  
    stop();  // Make sure any currently running dispatchers are stopped.  
    //先是创建了一个CacheDispatcher的实例,然后调用了它的start()方法
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);  
    mCacheDispatcher.start();  
    //for循环创建NetworkDispatcher的实例,并分别调用它们的start()方法 
    for (int i = 0; i < mDispatchers.length; i++) {  
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);  
        mDispatchers[i] = networkDispatcher;  
        networkDispatcher.start();  
    }  
}  

CacheDispatcher和NetworkDispatcher都是继承自Thread的,而默认情况下for循环会执行四次,也就是说当调用了Volley.newRequestQueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来,其中CacheDispatcher是缓存线程,NetworkDispatcher是网络请求线程。
得到了RequestQueue之后,我们只需要构建出相应的Request,然后调用RequestQueue的add()方法将Request传入就可以完成网络请求操作了,来看看add()方法吧:

public <T> Request<T> add(Request<T> request) {  
    // Tag the request as belonging to this queue and add it to the set of current requests.  
    request.setRequestQueue(this);  
    synchronized (mCurrentRequests) {  
        mCurrentRequests.add(request);  
    }  
    // Process requests in the order they are added.  
    request.setSequence(getSequenceNumber());  
    request.addMarker("add-to-queue");  
    //判断当前的请求是否可以缓存,如果不能缓存则直接将这条请求加入网络请求队列
    if (!request.shouldCache()) {  
        mNetworkQueue.add(request);  
        return request;  
    }  
    // Insert request into stage if there‘s already a request with the same cache key in flight.  
    synchronized (mWaitingRequests) {  
        String cacheKey = request.getCacheKey();  
        if (mWaitingRequests.containsKey(cacheKey)) {  
            // There is already a request in flight. Queue up.  
            Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);  
            if (stagedRequests == null) {  
                stagedRequests = new LinkedList<Request<?>>();  
            }  
            stagedRequests.add(request);  
            mWaitingRequests.put(cacheKey, stagedRequests);  
            if (VolleyLog.DEBUG) {  
                VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);  
            }  
        } else {  
            //当前的请求可以缓存的话则将这条请求加入缓存队列
            mWaitingRequests.put(cacheKey, null);  
            mCacheQueue.add(request);  
        }  
        return request;  
    }  
} 

在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一默认行为。既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们看下CacheDispatcher中的run()方法

public class CacheDispatcher extends Thread {  

    ……  

    @Override  
    public void run() {  
        if (DEBUG) VolleyLog.v("start new dispatcher");  
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
        // Make a blocking call to initialize the cache.  
        mCache.initialize();  
        while (true) {  
            try {  
                // Get a request from the cache triage queue, blocking until  
                // at least one is available.  
                final Request<?> request = mCacheQueue.take();  
                request.addMarker("cache-queue-take");  
                // If the request has been canceled, don‘t bother dispatching it.  
                if (request.isCanceled()) {  
                    request.finish("cache-discard-canceled");  
                    continue;  
                }  
                //尝试从缓存当中取出响应结果 
                Cache.Entry entry = mCache.get(request.getCacheKey());  
                if (entry == null) {  
                    request.addMarker("cache-miss");  
                   // 如何为空的话则把这条请求加入到网络请求队列中
                    mNetworkQueue.put(request);  
                    continue;  
                }  
                // 如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中
                if (entry.isExpired()) {  
                    request.addMarker("cache-hit-expired");  
                    request.setCacheEntry(entry);  
                    mNetworkQueue.put(request);  
                    continue;  
                }  
                //没有过期就认为不需要重发网络请求,直接使用缓存中的数据即可  
                request.addMarker("cache-hit"); 
                //对数据进行解析 
                Response<?> response = request.parseNetworkResponse(  
                        new NetworkResponse(entry.data, entry.responseHeaders));  
                request.addMarker("cache-hit-parsed");  
                if (!entry.refreshNeeded()) {  
                    // Completely unexpired cache hit. Just deliver the response.  
                    mDelivery.postResponse(request, response);  
                } else {  
                    // Soft-expired cache hit. We can deliver the cached response,  
                    // but we need to also send the request to the network for  
                    // refreshing.  
                    request.addMarker("cache-hit-refresh-needed");  
                    request.setCacheEntry(entry);  
                    // Mark the response as intermediate.  
                    response.intermediate = true;  
                    // Post the intermediate response back to the user and have  
                    // the delivery then forward the request along to the network.  
                    mDelivery.postResponse(request, response, new Runnable() {  
                        @Override  
                        public void run() {  
                            try {  
                                mNetworkQueue.put(request);  
                            } catch (InterruptedException e) {  
                                // Not much we can do about this.  
                            }  
                        }  
                    });  
                }  
            } catch (InterruptedException e) {  
                // We may have been interrupted because it was time to quit.  
                if (mQuit) {  
                    return;  
                }  
                continue;  
            }  
        }  
    }  
}  

来看一下NetworkDispatcher中是怎么处理网络请求队列的

public class NetworkDispatcher extends Thread {  
    ……  
    @Override  
    public void run() {  
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
        Request<?> request;  
        while (true) {  
            try {  
                // Take a request from the queue.  
                request = mQueue.take();  
            } catch (InterruptedException e) {  
                // We may have been interrupted because it was time to quit.  
                if (mQuit) {  
                    return;  
                }  
                continue;  
            }  
            try {  
                request.addMarker("network-queue-take");  
                // If the request was cancelled already, do not perform the  
                // network request.  
                if (request.isCanceled()) {  
                    request.finish("network-discard-cancelled");  
                    continue;  
                }  
                addTrafficStatsTag(request);  
                //调用Network的performRequest()方法来去发送网络请求 
                NetworkResponse networkResponse = mNetwork.performRequest(request);  
                request.addMarker("network-http-complete");  
                // If the server returned 304 AND we delivered a response already,  
                // we‘re done -- don‘t deliver a second identical response.  
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {  
                    request.finish("not-modified");  
                    continue;  
                }  
                // Parse the response here on the worker thread.  
                Response<?> response = request.parseNetworkResponse(networkResponse);  
                request.addMarker("network-parse-complete");  
                // Write to cache if applicable.  
                // TODO: Only update cache metadata instead of entire record for 304s.  
                if (request.shouldCache() && response.cacheEntry != null) {  
                    mCache.put(request.getCacheKey(), response.cacheEntry);  
                    request.addMarker("network-cache-written");  
                }  
                // Post the response back.  
                request.markDelivered();  
                mDelivery.postResponse(request, response);  
            } catch (VolleyError volleyError) {  
                parseAndDeliverNetworkError(request, volleyError);  
            } catch (Exception e) {  
                VolleyLog.e(e, "Unhandled exception %s", e.toString());  
                mDelivery.postError(request, new VolleyError(e));  
            }  
        }  
    }  
}  

调用Network的performRequest()方法来去发送网络请求 ,而Network是一个接口,这里具体的实现是BasicNetwork,我们来看下它的performRequest()方法

public class BasicNetwork implements Network {  
    ……  
    @Override  
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {  
        long requestStart = SystemClock.elapsedRealtime();  
        while (true) {  
            HttpResponse httpResponse = null;  
            byte[] responseContents = null;  
            Map<String, String> responseHeaders = new HashMap<String, String>();  
            try {  
                // Gather headers.  
                Map<String, String> headers = new HashMap<String, String>();  
                addCacheHeaders(headers, request.getCacheEntry()); 
                //调用了HttpStack的performRequest()方法,这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象 
                httpResponse = mHttpStack.performRequest(request, headers);  
                StatusLine statusLine = httpResponse.getStatusLine();  
                int statusCode = statusLine.getStatusCode();  
                responseHeaders = convertHeaders(httpResponse.getAllHeaders());  
                // Handle cache validation.  
                if (statusCode == HttpStatus.SC_NOT_MODIFIED) {  
                //将服务器返回的数据组装成一个NetworkResponse对象进行返回
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,  
                            request.getCacheEntry() == null ? null : request.getCacheEntry().data,  
                            responseHeaders, true);  
                }  
                // Some responses such as 204s do not have content.  We must check.  
                if (httpResponse.getEntity() != null) {  
                  responseContents = entityToBytes(httpResponse.getEntity());  
                } else {  
                  // Add 0 byte response as a way of honestly representing a  
                  // no-content request.  
                  responseContents = new byte[0];  
                }  
                // if the request is slow, log it.  
                long requestLifetime = SystemClock.elapsedRealtime() - requestStart;  
                logSlowRequests(requestLifetime, request, responseContents, statusLine);  
                if (statusCode < 200 || statusCode > 299) {  
                    throw new IOException();  
                }  
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false);  
            } catch (Exception e) {  
                ……  
            }  
        }  
    }  
}  

在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,以及将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。还记得自定义Request的方式吗?其中parseNetworkResponse()这个方法就是必须要重写的。
在解析完了NetworkResponse中的数据之后,又会调用ExecutorDelivery的postResponse()方法来回调解析出的数据

public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {  
    request.markDelivered();  
    request.addMarker("post-response");  
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));  
} 

在mResponsePoster的execute()方法中传入了一个ResponseDeliveryRunnable对象,就可以保证该对象中的run()方法就是在主线程当中运行的了,我们看下run()方法中的代码是什么样的:

private class ResponseDeliveryRunnable implements Runnable {  
    private final Request mRequest;  
    private final Response mResponse;  
    private final Runnable mRunnable;  

    public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {  
        mRequest = request;  
        mResponse = response;  
        mRunnable = runnable;  
    }  

    @SuppressWarnings("unchecked")  
    @Override  
    public void run() {  
        // If this request has canceled, finish it and don‘t deliver.  
        if (mRequest.isCanceled()) {  
            mRequest.finish("canceled-at-delivery");  
            return;  
        }  
        // Deliver a normal response or error, depending.  
        if (mResponse.isSuccess()) {  
            mRequest.deliverResponse(mResponse.result);  
        } else {  
            mRequest.deliverError(mResponse.error);  
        }  
        // If this is an intermediate response, add a marker, otherwise we‘re done  
        // and the request can be finished.  
        if (mResponse.intermediate) {  
            mRequest.addMarker("intermediate-response");  
        } else {  
            mRequest.finish("done");  
        }  
        // If we have been provided a post-delivery runnable, run it.  
        if (mRunnable != null) {  
            mRunnable.run();  
        }  
   }  
}  

其中在第22行调用了Request的deliverResponse()方法,有没有感觉很熟悉?没错,这个就是我们在自定义Request时需要重写的另外一个方法,每一条网络请求的响应都是回调到这个方法中,最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。

Demo下载地址

Android Volley框架完全解析

标签:

原文地址:http://blog.csdn.net/huaxun66/article/details/52589404

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!