Volley源码解析

原创 2016年08月28日 19:50:44

Volley源码解析

Volley是一个网络请求的库,当然现在大家都用OkHttp或者Retrofit, 写这篇博客目的是想自定义一个线程模型处理网路请求,Volley比较简单而且采用了面向接口的设计,拿来作参考,顺便把分析做一下记录。

Volley的使用

我们需要创建一个RequestQueue queue,如果项目中的网路请求比较少的话那推荐用一个queue,在Application中对它进行初始化,如果有频繁的网络请求可以在每个Activity里面创建一个queue。在做网络请求的时候只需要这样:先创建一个RequestQueue,再创建一个Request,可以是StringRequest,也可以是JsonRequest,然后调用RequestQueue对象的add方法,把刚创建的Request对象添加到队列就可以了。
以StringRequest为例

RequestQueue queue = Volley.newRequestQueue(context);
StringRequest request = new StringRequest("http://www.baidu.com", new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
            Log.i("VolleyRequest", response);
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            Log.i("VolleyRequest", error.getMessage());
        }
    });
queue.add(request);

源码解析

下面是主要的类关系图
这里写图片描述
先来看RequestQueue,下面这行代码会创建一个RequestQueue

Volley.newRequestQueue(context);

Volley.java是一个初始化RequestQueue的类,它有两个重载的方法

public static RequestQueue newRequestQueue(Context context) {
        return newRequestQueue(context, null);
    }

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        ...
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);

        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();

        return queue;
    }

最终都是通过new RequestQueue(new DiskBasedCache(cacheDir), network)创建了一个RequestQueue,可以看到在初始化了queue之后马上调用了start方法,所以在使用的时候不需要显示调用queue的start方法。另外一点它会根据编译版本的API选择不同的网络实现类,如果API小于9就是用Apache的HttpClient,否则使用基于HttpURLConnection的HurlStack。这个Network等会再说,先看RequestQueue,它有两个带有优先级的阻塞式队列:

private final PriorityBlockingQueue<Request<?>> mCacheQueue =
        new PriorityBlockingQueue<Request<?>>();

private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
        new PriorityBlockingQueue<Request<?>>();

mCacheQueue是存放的缓存过的请求队列,mNetworkQueue是需要从网络获取数据的请求队列,这两个队列都是PriorityBlockingQueue,与普通的阻塞式队列相比它要求里面的元素必须实现Comparable接口或者在构造队列的时候需要传一个实现了 Comparator接口的比较器。它是按照待比较对象的降序排列的,也就是说在比较的时候小的会比大的优先级高。Request实现了Comparable接口,看它的compare方法:

 @Override
    public int compareTo(Request<T> other) {
        Priority left = this.getPriority();
        Priority right = other.getPriority();

        return left == right ?
                this.mSequence - other.mSequence :
                right.ordinal() - left.ordinal();
    }

Priority是一个常量类,有四个值从小到大分别是LOW,NORMAL,HIGH和IMMEDIATE。每次在调用getPriority()取值的时候会返回指定值:默认是返回NORMAL,ImageRequest是返回LOW。这样的话有一个ImageRequest imageRequest先进队列接着又进来一个StringRequest stringRequest,stringRequest跟imageRequest相比,后者getPriority()返回LOW而前者返回NORMAL取ordinary时后者小于前者,在执行stringRequest.compareTo(imageRequest)时返回负数,那么StringRequest会先出队列。如果是相同类型的Request那么就比较它们的mSequence值,mSequence是在RequestQueue的add方法里被赋值的

public <T> Request<T> add(Request<T> request) {
        ...
        // Process requests in the order they are added.
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");
        ...
    }

再看getSequenceNumber()方法:

public int getSequenceNumber() {
        return mSequenceGenerator.incrementAndGet();
    }

这个mSequenceGenerator是一个AtomicInteger,每次调incrementAndGet()它会自增1,这样每一个刚加入的Request的mSequence值都会比上一个Request高,在调用compareTo方法比较的时候越往后的Request就越大,这样越早加入的Request的优先级就越高,实际上就是维护了一个FIFO的Request队列,保证先来的请求会优先被处理。
再看一下它的构造方法,总共有三个:

public RequestQueue(Cache cache, Network network) {
        this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
    }
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }
public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }

两个重载的方法最终调用了4个参数的构造方法初始化了它的4个成员变量,其中mNetwork就是上面Volley在初始化队列时会区分API版本的参数,这4个参数分别什么含义呢

/** Cache interface for retrieving and storing responses. */
private final Cache mCache;

/** Network interface for performing requests. */
private final Network mNetwork;

/** The network dispatchers. */
private NetworkDispatcher[] mDispatchers;

/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;

四个参数分别对应了它的缓存策略,网络请求实现,网路请求调度器和执行结果的传递者。

Cache

它是一个接口,它的实现类是DiskBasedCache,它会以请求的URL作为Key(不是只把URL当做key),请求结果的byte数组为Value存储请求结果,它默认是5M的存储空间,如果不够了它会把先缓存的数据给删掉,再存新数据,同时会缓存请求的Header信息并且根据header的信息判断缓存是否过期。Cache的实现类是DiskBasedCache,它有一个内部类CacheHeader,成员跟Cache的内部类Entry基本一致,存放的是请求结果的Header信息,与Entry相比少了一个byte数组data,data是存放请求结果的,它只存Header所以不需要结果,多了一个key,用来标识缓存的位置,还多了一个size,用来标识Header的大小。DiskBasedCache还有一个LinkedHashMap类型的的成员mEntries,它以String为键以CacheHeader为值保存请求的信息,这样每次在需要对缓存做IO操作的时候取它的值来比较,避免了频繁的IO操作提升了效率。

Network

它也是一个接口,它的实现类是BasicNetwork,Volley用它来进行网路操作,但是实际执行又对它做了一次封装,具体网络实现依赖于它的成员HttpStack

public interface Network {
    public NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

public class BasicNetwork implements Network {
    @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        ...

        while (true) {
            HttpResponse httpResponse = null;
            byte[] responseContents = null;
            httpResponse = mHttpStack.performRequest(request, headers);
            return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
                        SystemClock.elapsedRealtime() - requestStart);
            }

        ...
        }
    }
}

可以看到BasicNetwork在调用performRequest时是通过调用mHttpStack.performRequest来拿到数据,这个HttpStack也是一个接口(Volley的设计给了开发者充分自定义的空间),这就表示开发者可以使用自定义的HttpStack来进行网络操作

public interface HttpStack {
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;
}

它的实现类有两个:使用了Apache的HttpClientStack和基于HttpURLConnection的HurlStack,这两个也是开发者初次接触网络请求会遇到的,至于应该用哪个可以参考使用HttpURLConnection还是HttpClient

NetworkDispatcher

这是一个线程,里面会关联一个请求队列mQueue和一个网络处理类mNetwork,用于对Volley的请求做调度,它会在run方法里面会执行一个死循环从mQueue里挨个取出Request然后调用mNetwork的performRequest方法做处理,最后把结果通过ResponseDelivery传递给主线程。默认会开启4个线程,看RequestQueue代码,DEFAULT_NETWORK_THREAD_POOL_SIZE会作为参数传递给构造方法,这个mDispatchers是一个线程数组,可以对每一线程做调度,尽管没有用ThreadPoolExecutor,但是也就相当于一个线程池,线程池的目的就是为了可以对每一个线程做调度重用线程已有线程。

/** 要启动的网络请求调度线程个数 */
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }
ResponseDelivery

毫无疑问这也是一个接口,它的实现类是ExecutorDelivery,网络操作是耗时的需要在子线程中完成,但是结果是在主线程通过Listener拿到的,那怎么办,肯定是通过Handler了,一个关联了主线程Looper的Handler

public ExecutorDelivery(final Handler handler) {
        mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };
    }

如果有请求结果需要传递给主线程就会执行一个新的Runnable,任务的执行是在主线程完成。但在使用的时候并没有要求开发者需要传递一个这样的Handler,是怎么回事,看RequestQueue的构造方法

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

也就明白了,它在初始化queue的时候就同时把这个Handler给初始化了。

下面来看它的执行过程,由主线程发起,交给子线程处理,最后把结果通过回调传递给主线程
任务的发起从RequestQueue的start方法开始

public void start() {
        stop();  // 把当前线程停止
        // 创建一个缓存调度线程并启动
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // 创建4个网络请求调度线程
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

首先会调用stop方法把线程给停掉,然后再初始化新的线程执行任务。这里要额外说一下它在初始化NetworkDispatcher的同时还初始化了一个CacheDispatcher,这个CacheDispatcher也是一个线程,Volley是自带缓存功能的,这个线程就是负责对已经缓存的请求做调度
线程开启了会做哪些事情呢,先看缓存调度线程

 public class CacheDispatcher extends Thread {

    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // 初始化缓存,会在这里创建缓存目录
        mCache.initialize();

        // 执行一个死循环轮询mCacheQueue
        while (true) { 
            try {
                // 从缓存的请求队列里取一个请求,如果没有当前线程会被阻塞直到拿到一个可以用的
                final Request<?> request = mCacheQueue.take();
                // 添加标记,过程中会添加很多标记都是为了在Logcat中显示标记信息
                request.addMarker("cache-queue-take");

                // 如果请求被取消了就不再对结果进行分发
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // Cache.Entry是用来保存请求结果的
                // 尝试从缓存中恢复
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // 如果根据URL得到的Entry为空就把这个请求放到需要从网络获取数据的mNetworkQueue里
                    mNetworkQueue.put(request);
                    continue;
                }

                // Entry不为空但是已经失效(通过验证ttl)也把这个请求放到需要从网络获取数据的mNetworkQueue里
                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");

                // 是否需要更新(通过验证softTtl)
                if (!entry.refreshNeeded()) {
                    // 如果不需要更新就把结果分发给用户
                    mDelivery.postResponse(request, response);
                } else {
                    // 需要更新
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // 添加一个标记
                    response.intermediate = true;

                    // 这时候可以把缓存数据分发给用户但还是得再加入到网络请求队列中
                    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;
            }
        }
    }
}

每一步操作都加了注释应该很清楚,另外有两点需要说明,第一点就是开始的
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
这条语句,它是设置当前线程为后台线程,否则尽管是工作线程但它会拥有跟UI线程相同的的抢夺CPU资源的能力,跟UI线程的优先级是一样的,这就可能导致UI线程获得的CPU时间较短造成界面的卡顿,而调用这条语句以后系统就会知道它是属于background group的,最多只分给它5%-10%的时间片,从而保障UI线程可以获取更多的CPU时间。第二点是它缓存的失效时间,缓存是否需要刷新是怎么控制的

/** True if the entry is expired. */
public boolean isExpired() {
    return this.ttl < System.currentTimeMillis();
}

/** True if a refresh is needed from the original data source. */
public boolean refreshNeeded() {
    return this.softTtl < System.currentTimeMillis();
}

这是Cache的两个方法,通过跟当前时间做对比,那结果就是看ttl和softTtl了,它们又是在哪被赋值的呢,是通过调用HttpHeaderParser的静态方法parseCacheHeaders解析请求结果得到的

public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
        long now = System.currentTimeMillis();

        Map<String, String> headers = response.headers;

        long serverDate = 0;
        long lastModified = 0;
        long serverExpires = 0;
        long softExpire = 0;
        long finalExpire = 0;
        long maxAge = 0;
        long staleWhileRevalidate = 0;
        boolean hasCacheControl = false;
        boolean mustRevalidate = false;

        String serverEtag = null;
        String headerValue;

        // 下面这些都是Http协议里规定的字段
        headerValue = headers.get("Date");
        if (headerValue != null) {
            serverDate = parseDateAsEpoch(headerValue);
        }

        headerValue = headers.get("Cache-Control");
        if (headerValue != null) {
            hasCacheControl = true;
            String[] tokens = headerValue.split(",");
            for (int i = 0; i < tokens.length; i++) {
                String token = tokens[i].trim();
                if (token.equals("no-cache") || token.equals("no-store")) {
                    return null;
                } else if (token.startsWith("max-age=")) {
                    try {
                        maxAge = Long.parseLong(token.substring(8));
                    } catch (Exception e) {
                    }
                } else if (token.startsWith("stale-while-revalidate=")) {
                    try {
                        staleWhileRevalidate = Long.parseLong(token.substring(23));
                    } catch (Exception e) {
                    }
                } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                    mustRevalidate = true;
                }
            }
        }

        headerValue = headers.get("Expires");
        if (headerValue != null) {
            serverExpires = parseDateAsEpoch(headerValue);
        }

        headerValue = headers.get("Last-Modified");
        if (headerValue != null) {
            lastModified = parseDateAsEpoch(headerValue);
        }

        serverEtag = headers.get("ETag");

        // Cache-Control takes precedence over an Expires header, even if both exist and Expires
        // is more restrictive.
        if (hasCacheControl) {
            softExpire = now + maxAge * 1000;
            finalExpire = mustRevalidate
                    ? softExpire
                    : softExpire + staleWhileRevalidate * 1000;
        } else if (serverDate > 0 && serverExpires >= serverDate) {
            // Default semantic for Expire header in HTTP specification is softExpire.
            softExpire = now + (serverExpires - serverDate);
            finalExpire = softExpire;
        }

        Cache.Entry entry = new Cache.Entry();
        entry.data = response.data;
        entry.etag = serverEtag;
        entry.softTtl = softExpire;
        entry.ttl = finalExpire;
        entry.serverDate = serverDate;
        entry.lastModified = lastModified;
        entry.responseHeaders = headers;

        return entry;
    }

它同时使用了Expires和max-age来计算时间,是因为Expires在HTTP/1.0中已经定义,Cache-Control:max-age在HTTP/1.1中才有定义,为了向下兼容,仅使用max-age不够。可以看到ttl值和softTtl值都是在当前时间的基础上加上了解析header里面的值,所以在缓存调度的时候缓存有效时间就是maxAge * 1000,缓存更新时间就是转换后的headers.get(“Expires”)时间。在缓存的调度里面只是取缓存那么缓存文件是在什么时候被创建的呢,这就得看它的网络调度了,下面是NetworkDispatcher

public class NetworkDispatcher extends Thread {

    @Override
    public void run() {
        // 也是先把线程设置为background group
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            Request<?> request;
            try {
                // 在缓存调度时就是把请求加到了这个队列里面,它是在RequeQueue里被初始化的
                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 (request.isCanceled()) {
                    request.finish("network-discard-cancelled");
                    continue;
                }

                addTrafficStatsTag(request);

                // 终于到了它的网络执行了
                // 这里在前面已经介绍过了,具体的网络操作是由mHttpStack负责
                NetworkResponse networkResponse = mNetwork.performRequest(request);
                request.addMarker("network-http-complete");

                // 当请求结果状态码为304时,NetworkResponse的notModified会被赋值为true
                // 当服务器返回304并且结果已经被传递过就终止这次请求
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                    request.finish("not-modified");
                    continue;
                }

                // 解析Header信息更新ttl和softTtl值
                Response<?> response = request.parseNetworkResponse(networkResponse);
                request.addMarker("network-parse-complete");

                // 就是在这里加入了缓存
                // 如果需要缓存并且该请求结果的Header信息已经被解析过
                if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

                // 将结果返回
                request.markDelivered();
                mDelivery.postResponse(request, response);
            } catch (VolleyError volleyError) {
                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                parseAndDeliverNetworkError(request, volleyError);
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
                VolleyError volleyError = new VolleyError(e);
                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                mDelivery.postError(request, volleyError);
            }
        }
    }
}

该线程会轮询mNetworkQueue挨个取出request,然后调用Network的performRequest联网取数据,结果会被封装成一个Response交由ExecutorDelivery将结果传递给主线程。NetworkDispatcher和CacheDispatcher共享了同一个mNetworkQueue减少了线程间数据传递的维护成本。这里有一点我还不是很清楚,缓存在加入时会检查缓存文件是否已到达上限,如果是的话会一直删除直到达到指定条件,下面是DiskBasedCache

private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }

        ...

        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, CacheHeader> entry = iterator.next();
            CacheHeader e = entry.getValue();
            boolean deleted = getFileForKey(e.key).delete();
            if (deleted) {
                mTotalSize -= e.size;
            } else {
               VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                       e.key, getFilenameForKey(e.key));
            }
            iterator.remove();
            prunedFiles++;

            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }

        ...
    }

删除终止的条件是((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR),就是:当前缓存空间大小 + 需要的缓存空间大小 < 缓存总空间大小 * HYSTERESIS_FATOR,这个HYSTERESIS_FATOR是在该类被初始化的时候就被初始化的private static final float HYSTERESIS_FACTOR = 0.9f; 为什么是0.9我猜是一个经验值,删的太多会导致缓存的命中率降低,删的少了这个方法就得频繁执行。(有待验证)
最后看下结果是怎么处理的,下面是ExecutorDelivery

 private class ResponseDeliveryRunnable implements Runnable {

        ...

        @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();
            }
       }

它是ExecutorDelivery的一个内部类,结果的传递都是通过调用ResponseDelivery的postResponse来完成,postResponse方法里面会在主线程执行一个Runnable,在这个Runnable里面做了什么事情呢,调用了Request的deliverResponse和deliverError方法,它们各自携带的参数最后传递给了Request里面的Listener,所以在主线程里面可以通过监听Listener来拿到网络操作的异步执行结果。好了,整个过程就分析结束了。

我是怎么用Volley的

已经介绍过了Volley的网络操作可以自己决定实现者,所以可以用OKHttp作为Volley的HttpStack
具体使用可以参考 使用OKHttp处理Volley的底层HTTP请求

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Volley源码解析

  • 2016-05-26 16:40
  • 1.34MB
  • 下载

Volley源码完全解析

上一篇看了Volley的使用方法,但是我们要知其然还要知其所以然,所以来看看源码究竟是怎么样实现的,下面是Volley的工作流程图 首先可以看到蓝色代表主线程,绿色代表缓存线程,橙色代表网络线程。...

Volley 图片加载相关源码解析

一 概述 最近在完善图片加载方面的代码,于是就看看Volley的图片加载相关源码,取取经,顺便写篇博文作为笔记记录下。 在使用Volley作为图片加载库的时候,肯定需要做以下几件事: App...

Volley框架使用与源码解析(一)

Volley是Google推出的异步网络请求和异步图片加载框架,Volley适合数据量小网络请求频繁的操作。 Volley的特点: 1,代码扩展性强,很多类都是基于接口设计的,自己可以重写 2,...

Volley 图片加载相关源码解析

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/47721631; 本文出自:【张鸿洋的博客】 一 概述在使用Voll...

Android之Volley 源码解析

原文来自:http://www.codekk.com  1. 功能介绍  1.1. Volley Volley 是 Google 推出的 Android 异步网络请求框架和图片加载框架。在 Goog...

Volley源码解析使用方式和使用场景分析

Volley是Google在2013年推出的一个网络库,用于解决复杂网络环境下网络请求问题。刚推出的时候是非常火的,现在该项目的变动已经很少了。项目库地址为 https://android.googl...

Android:Volley源码解析

简单实例Volley是一个封装HttpUrlConnection和HttpClient的网络通信框架,集AsyncHttpClient和Universal-Image-Loader的优点于了一身,既可...

Volley 源码解析(二)

图片加载 用过Volley图片加载的老司机们可能对这段代码非常的熟悉:imageLoader.get( url, ImageLoader.getImageListen...

Volley 源码解析

转载请注明作者和原文连接(@woaitqs woaitqs.github.io) 为什么需要阅读Volley的源码Volley是Google在2013年推出的一个网络库,用于解决复杂网络环境下网络请求...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)