月度归档:2016年03月

Fun facts with HTTP

当输入一个网址后的事….

HTTP是一种不保存状态的协议(stateless)

HTTP协议自身不对请求和响应之间的通信协议状态进行保存。这样的设计是为了保证协议的简单高效。

Cookie

但是随着Web的发展,越来越多的需求需要记录状态,于是引入了Cookies的设计。Cookie技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。Cookie就像是某种token,客户端一份,服务器一份,下次通信时再互相验证。

HTTP管线化(PipeLine)

HTTP是基于TCP的应用嘛,所以早期的HTTP每进行一次HTTP通信,就开断一次TCP连接。我们知道TCP要经过三次握手才能通信。随着web的复杂,这样频繁开断很浪费资源。针对这个问题于是提出了HTTP keep-live方法,其特点是如果任意一端没有明确提出断开连接,则保持TCP连接状态。并且从前必须等到上一个请求响应才能发起下一个请求,而又了管线化技术可以连续请求啦。

压缩传输技术

传送数据压缩才能更快的传输啊,android 2.3以后对HttpUrlConnection进行了gzip压缩的支持

 

 

参考

《图解HTTP》作者: 【日】上野宣 出版社: 人民邮电出版社

 

Volley源码分析

引言

Volley是Google在2013I/O大会发布,如其名它简洁高效,之后几乎所有人都在用它。今天我们来分析一下它。

整体结构及其策略

RequestQueue:请求队列,所有的网络请求将在这PriorityQueue处理。

Dispatcher :负责请求的分发,一个负责发起缓存请求,一个负责发起网络请求

HttpClientStack和HurlStack :是真正发起网络请求的地方,这里是针对不同的Android版本分别对AndroidHttpClient(已废弃,适用于API 9以下)和HttpUrlConnection的封装

策略:

请求加入到RequestQueue里,然后被CacheDispatcher取出请求检查是否在Cache里,如果在取出数据,传回给主线程ok结束。如果Cache没有命中或者cache过期就把请求放入网络请求队列里,然后发起网络请求结束。

源码详细分析策略

分析源码当然要从我们平时的使用方法作为入口开始入手

java

Volley.newRequestQueue(Context context)

 

调用这个静态方法最后会掉到(省略部分代码)

 public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

      // 根据不同Android版本新建不同的网络请求处理栈
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);
        // 新建RequestQueue,并调用start方法
        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        queue.start();

        return queue;
    }

跟着上面RequestQueue的start方法

  public void start() {
        stop();  // 确保一批Dispatcher在活动
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // 根据Dispatcher池子大小来创造Dispatcher
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

可以看到Dispatcher在RequestQueue调用start方法的时候被创建,且在这个时候分别开启他们的线程。因为Dispatcher实际上是Thread,所以start后我们直接分析它的run方法

CacheDispatcher的run方法(代码有所省略):

  @Override
    public void run() {
    // 这里设置线程的优先级很重要,后面会说明
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        Request<?> request;
        while (true) {
           // ...
            try {
                // 尝试从队列里取出一个请求
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                if (mQuit) {
                    return;
                }
                continue;
            }
            try {
                // 如果Request被取消,直接结束
                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;
                }

                // Cache命中,读取缓存
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // 不需要刷新的叫完全命中,直接投递结果
                    mDelivery.postResponse(request, response);
                } else {
                    // 有缓存但是需要刷新的叫Soft-expired cache hit,我们投递结果,但是也把请求加入网络请求队列
                    // ...
                 }
        }
    }

如何传回数据到主线程呢?很简单就是有一个持有主线程Looper的Handler了就行啦。这个类是ExecutorDelivery,就不分析了。

Volley ImageLoader 分析

要分析ImageLoader先要提前说一下其中的一个数据封装类ImageContainer它持有关于网络图片的所有信息:

public class ImageContainer {

        private Bitmap mBitmap;
        // 图片请求结果的listener
        private final ImageListener mListener;

        // 图片请求缓存key
        private final String mCacheKey;

        private final String mRequestUrl;
}

这个ImageLoader设计最棒的地方,不仅在于可以读取缓存图片,还可以对重复的请求进行过滤避免发起多次重复的请求(这种重复请求的情况,在Listview有图片时上下滚动列表时尤为明显)

那么它是如何实现这一功能的呢?答案也很简单它用了一个Map来对请求作缓存。

// InFlightRequest: 这个Map用来缓存还正在请求中的请求队列,用这个队列来实现避免重复请求,当请求结束后移除请求
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
            new HashMap<String, BatchedImageRequest>();

下面我们来看ImageLoader的get方法,通常我们来获取图片的方法(代码有所省略):

    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        // 查看是否有缓存
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // 返回缓存数据
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        // 没有缓存,准备请求
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // 检查一下请求是否已经请求已经在处理
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // 如果已经在了,我们只在这个请求中添加我们的监听器
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // 请求没有正在处理,加入网络请求队列,别再InFlight里记录
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);

        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

小知识

BlockingQueue

它与一般的Queue不同的是多了对两个操作的支持,等到队列不为空的时候取元素,和等队列不为全满的时候放入元素。(中文好拗口,看英文)A Queue that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.

Android 自己使用线程时的注意事项

一个重要的知识点是,当一个线程被创建的时候,它将拥有和同一组线程里同样的优先级。

这意味着入股,你在UI thread里打开一个线程,如果不设置恰当的优先级,这个worker thread将和UI thread享受同样的优先级,这样可能导致worker thread和UI thread抢资源导致卡顿。前面你也可能注意到了,在Volley新建Dispatcher的时候是在UI thread,所以它把线程的优先级设置为了THREAD_PRIORITY_BACKGROUND。

android里设置priority是从负数到正数的,优先级递减的。如何理解记忆呢,可以想成这个priority的值表达了这个线程对他人的友好程度,友好程度越低代表它的时间越珍贵。

正常情况下我们希望是这样的

(在AsyncTask里和IntentService里默认是这样实现的)

参考

* [Google I/O 2013 - Volley: Easy, Fast Networking for Android](https://www.youtube.com/watch?v=yhv8l9F44qo)
*
* [Volley 源码解析](http://codekk.com/blogs/detail/54cfab086c4761e5001b2542)
* [BlockingQueue java doc](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html)