Volley源碼解讀

Volley 的中文翻譯為“齊射、并發(fā)”,是在2013年的Google大會上發(fā)布的一款A(yù)ndroid平臺網(wǎng)絡(luò)通信庫,具有網(wǎng)絡(luò)請求的處理塞关、小圖片的異步加載和緩存等功能,能夠幫助 Android APP 更方便地執(zhí)行網(wǎng)絡(luò)操作子巾,而且更快速高效帆赢。Volley可是說是把AsyncHttpClient和Universal-Image-Loader的優(yōu)點(diǎn)集于了一身,既可以像AsyncHttpClient一樣非常簡單地進(jìn)行HTTP通信线梗,也可以像Universal-Image-Loader一樣輕松加載網(wǎng)絡(luò)上的圖片椰于。除了簡單易用之外,Volley在性能方面也進(jìn)行了大幅度的調(diào)整仪搔,它的設(shè)計目標(biāo)就是非常適合去進(jìn)行數(shù)據(jù)量不大瘾婿,但通信頻繁的網(wǎng)絡(luò)操作,而對于大數(shù)據(jù)量的網(wǎng)絡(luò)操作烤咧,比如說下載文件等偏陪,Volley的表現(xiàn)就會非常糟糕。

在Google IO的演講上煮嫌,其配圖是一幅發(fā)射火弓箭的圖笛谦,有點(diǎn)類似流星。這表示昌阿,Volley特別適合數(shù)據(jù)量不大但是通信頻繁的場景饥脑。見下圖:

volley
volley

目錄

  • [Volley特點(diǎn)]

  • [Volley執(zhí)行流程]

  • [Volley初始化]

  • [創(chuàng)建RequestQueue]

  • [DiskBasedCache]

  • [CacheDispatcher & NetworkDispatcher]

  • [Request]

  • [加入RequestQueue]

  • [Request#finish自己]

  • [取消請求]

  • [緩存處理]

  • [Request請求失敗重試機(jī)制]

  • [PoolingByteArrayOutputStream & ByteArrayPool]

  • [Volley加載圖片 ]

  • [Handler]

  • [volley gson改造 ]

  • [volley okhttp改造]

Volley特點(diǎn)

  1. 自動調(diào)度網(wǎng)絡(luò)請求恳邀;

    • Volley直接new 5個線程(默認(rèn)5個),讓多個線程去搶奪網(wǎng)絡(luò)請求對象(Request)好啰,搶到就執(zhí)行轩娶,搶不到就等待儿奶,直到有網(wǎng)絡(luò)請求到來框往。
  2. 可以加載圖片;

  3. 通過標(biāo)準(zhǔn)的 HTTP cache coherence(高速緩存一致性)緩存磁盤和內(nèi)存透明的響應(yīng)闯捎;

  4. 支持指定請求的優(yōu)先級椰弊;

    • 根據(jù)優(yōu)先級去請求數(shù)據(jù)
  5. 網(wǎng)絡(luò)請求cancel機(jī)制。我們可以取消單個請求瓤鼻,或者指定取消請求隊(duì)列中的一個區(qū)域秉版;

    • 例如Activity finish后結(jié)束請求
  6. 框架容易被定制,例如茬祷,定制重試或者網(wǎng)絡(luò)請求庫清焕;

    • 例如基于Okhttp的Volley

Volley執(zhí)行流程

Alt text
Alt text

Volley初始化

創(chuàng)建RequestQueue

使用Volley時我們需要先創(chuàng)建一個RequestQueue,如下

RequestQueue queue = Volley.newRequestQueue(context);

Volley.newRequestQueue(context)最終調(diào)用了如下方法

    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
        ...
        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;
    }

HttpStack 是一個接口祭犯,主要用于請求網(wǎng)絡(luò)數(shù)據(jù)秸妥,并返回結(jié)果。默認(rèn)情況下stack是null沃粗,
當(dāng)android版本>=9時使用HurlStack粥惧,否則使用HttpClientStack
若用戶想要使用其他的網(wǎng)絡(luò)請求類庫,比如okhttp等就可以實(shí)現(xiàn)HttpStack接口最盅,并在

HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
    throws IOException, AuthFailureError

中調(diào)用okhttp進(jìn)行網(wǎng)絡(luò)請求突雪,并把請求的結(jié)果封裝成一個
HttpResponse返回即可,HttpResponse中包含了狀態(tài)碼涡贱,響應(yīng)頭咏删,body信息。

newRequestQueue中創(chuàng)建了一個BasicNetwork對象问词,BasicNetwork使用HttpStack執(zhí)行網(wǎng)絡(luò)請求督函,成功后返回一個NetworkResponse,NetworkResponse只是一個簡單的記錄狀態(tài)碼,body戏售,響應(yīng)頭侨核,服務(wù)端是否返回304并且緩存過,執(zhí)行網(wǎng)絡(luò)請求時間的類灌灾。

DiskBasedCache

newRequestQueue 中還創(chuàng)建了一個 RequestQueue搓译,RequestQueue 中持有一個 DiskBasedCache 對象,
DiskBasedCache 是把服務(wù)端返回的數(shù)據(jù)保持到本地文件中的類锋喜,默認(rèn)大小5M些己。
緩存文件是一個二進(jìn)制文件豌鸡,非文本文件,
緩存文件的開頭有個特殊的整型魔數(shù)(CACHE_MAGIC)段标,用于判斷是不是緩存文件涯冠。DiskBasedCache 初始化時會
讀取特定文件夾下的所有文件的部分?jǐn)?shù)據(jù),包括響應(yīng)頭等極少數(shù)數(shù)據(jù)逼庞,不包含body蛇更,當(dāng)調(diào)用DiskBasedCache的get(String key)方法時才讀取body部分,若文件開頭魔數(shù)不是 CACHE_MAGIC 則刪除赛糟。是的話就把讀取的數(shù)據(jù)保存到內(nèi)存中派任。代碼如下

public synchronized void initialize() {
        if (!mRootDirectory.exists()) {

            return;
        }

        File[] files = mRootDirectory.listFiles();
        if (files == null) {
            return;
        }
        for (File file : files) {
            BufferedInputStream fis = null;
            try {
                fis = new BufferedInputStream(new FileInputStream(file));
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);
            } catch (IOException e) {
                if (file != null) {
                    file.delete();
                }
            } finally {
                try {
                    if (fis != null) {
                        fis.close();
                    }
                } catch (IOException ignored) { }
            }
        }
    }

緩存文件除了可以保存 int,long 型數(shù)據(jù)璧南,還可以保存 String 字符串掌逛,Map<String,String>,保存字符串時先保存字符串的長度( long 型)司倚,然后再保存 byte[]數(shù)組豆混。
保存 Map<String,String>時,先保存 Map 大卸( int 型)皿伺,然后再保存key和value。代碼如下

    static String readString(InputStream is) throws IOException {
        int n = (int) readLong(is);
        byte[] b = streamToBytes(is, n);
        return new String(b, "UTF-8");
    }
    static Map<String, String> readStringStringMap(InputStream is) throws IOException {
        int size = readInt(is);
        Map<String, String> result = (size == 0)
        ? Collections.<String, String>emptyMap()
        : new HashMap<String, String>(size);
        for (int i = 0; i < size; i++) {
            String key = readString(is).intern();
            String value = readString(is).intern();
            result.put(key, value);
        }
        return result;
    }
    static long readLong(InputStream is) throws IOException {
        long n = 0;
        n |= ((read(is) & 0xFFL) << 0);
        n |= ((read(is) & 0xFFL) << 8);
        n |= ((read(is) & 0xFFL) << 16);
        n |= ((read(is) & 0xFFL) << 24);
        n |= ((read(is) & 0xFFL) << 32);
        n |= ((read(is) & 0xFFL) << 40);
        n |= ((read(is) & 0xFFL) << 48);
        n |= ((read(is) & 0xFFL) << 56);
        return n;
    }

緩存文件名由cache key字符串的前半段字符串的hashCode拼接上cache key(網(wǎng)絡(luò)請求url)后
半段字符串的hashCode值組成拍柒。代碼如下

private String getFilenameForKey(String key) {
        int firstHalfLength = key.length() / 2;
        String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
        localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
        return localFilename;
    }

當(dāng)緩存文件占用空間超過指定值時心傀,Volley只是簡單的刪除的部分文件,刪除代碼如下

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;
            } 
            iterator.remove();

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

可以看到刪除時只是遍歷mEntries拆讯,如果刪除成功并且剩余文件所占大小+新的緩存所需空間<mMaxCacheSizeInBytes * 0.9f脂男,
則停止刪除,否則繼續(xù)刪除种呐。

CacheDispatcher & NetworkDispatcher

RequestQueue 只是一個普通的類宰翅,沒有繼承任何類,RequestQueue 的 start 方法中創(chuàng)建了一個 CacheDispatcher爽室,和4個(默認(rèn)4個)NetworkDispatcher汁讼,
CacheDispatcher 和 NetworkDispatcher 都是 Thread 的直接子類,這5個 Thread 就是前面提到的搶奪網(wǎng)絡(luò)請求對象的 Thread阔墩。
調(diào)用start()時先調(diào)用了一下stop()嘿架,stop()中把5個線程的mQuit設(shè)為 true,然后調(diào)用interrupt()讓 Thread 的run不再執(zhí)行啸箫。
代碼如下

public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // Create network dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }


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

                ...

            }
        }
    }

 /**
     * Stops the cache and network dispatchers.
     */
    public void stop() {
        if (mCacheDispatcher != null) {
            mCacheDispatcher.quit();
        }
        for (int i = 0; i < mDispatchers.length; i++) {
            if (mDispatchers[i] != null) {
                mDispatchers[i].quit();
            }
        }
    }

CacheDispatcher啟動后先調(diào)用了一下DiskBasedCache的initialize()方法耸彪,這個方法要讀取文件,比較耗時忘苛,Volley把他放到了Cache線程中蝉娜。

CacheDispatcher和NetworkDispatcher的run方法內(nèi)部很像唱较,都是在 while (true)循環(huán)中從PriorityBlockingQueue中讀取Request,CacheDispatcher 獨(dú)享一個PriorityBlockingQueue召川,其余4各 NetworkDispatcher 共享一個PriorityBlockingQueue 南缓。PriorityBlockingQueue是一個阻塞隊(duì)列,當(dāng)隊(duì)列里沒有Request時take()方法就會阻塞荧呐,直到有Request到來汉形,PriorityBlockingQueue是線程安全的

同一個 Request 只能被1個線程獲取到。PriorityBlockingQueue 可以根據(jù)線程優(yōu)先級對隊(duì)列里的reqest進(jìn)行排序坛增。

Volley 的初始化到這就完成了获雕,下面開始執(zhí)行網(wǎng)絡(luò)請求

Request

使用 Volley 進(jìn)行網(wǎng)絡(luò)請求時我們要把請求封裝成一個 Request 對象薄腻,包括url收捣,請求參數(shù),請求成功失敗 Listener庵楷,
Volley 默認(rèn)給我們提供了 ImageRequest(獲取圖片)罢艾,
JsonObjectRequest、JsonArrayRequest(獲取json)尽纽,StringRequest(獲取 String)咐蚯。
例如:

Request<String>request=new StringRequest(Method.GET, "http://mogujie.com", new  Listener<String>(){
        @Override
        public void onResponse(String response) {
        }

    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
        }
    });
    Volley.newRequestQueue(context).add(request);

只需要把Request丟進(jìn)requestQueue中就可以。

加入RequestQueue

我們來看一下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 the request is uncacheable, skip the cache queue and go straight to the network.
        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 {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

add方法中首先設(shè)置了request所在的請求隊(duì)列弄贿,為了在用戶取消請求時能夠把request從requestQueue中移除掉春锋。
接下來設(shè)置了一下request的序列號,序列號按添加順序依次從0開始編號差凹,同一個隊(duì)列中任何兩個request的序列號都不相同期奔。序列號可以影響request的優(yōu)先級。
request.addMarker("")用于調(diào)試(打印日志等)危尿。
通過查看源碼我們可以看到request默認(rèn)是需要緩存的呐萌。

/** Whether or not responses to this request should be cached. */
    private boolean mShouldCache = true;

若request不需要緩存則直接把request丟到mNetworkQueue,然后4個 NetworkDispatcher 就可以"搶奪"request了谊娇,誰"搶"到誰就執(zhí)行網(wǎng)絡(luò)請求
如需要緩存則先判斷一下mWaitingRequests中有沒有正在請求的相同的request(根據(jù)request的url判斷)肺孤,沒有的話就把該request丟到mCacheQueue中,
這樣 CacheDispatcher 執(zhí)行完之前的請求后就可以執(zhí)行該request了济欢,若已經(jīng)有相同的request正在執(zhí)行赠堵,則只需保存一下該request,
等相同的request執(zhí)行完后直接使用其結(jié)果就可法褥。

CacheDispatcher中獲取到request后先判斷一下request后有沒有取消茫叭,有的話就finish掉自己,然后等待下一個request的到來

 if (request.isCanceled()) {
        request.finish("cache-discard-canceled");
        continue;
    }

接下來會從緩存中取緩存挖胃,沒有或者緩存已經(jīng)過期杂靶,就把request丟掉mNetworkQueue中梆惯,讓NetworkDisptcher去“搶奪”request

 // Attempt to retrieve this item from cache.
    Cache.Entry entry = mCache.get(request.getCacheKey());
    if (entry == null) {
        request.addMarker("cache-miss");
        // Cache miss; send off to the network dispatcher.
        mNetworkQueue.put(request);
        continue;
    }
      // If it is completely expired, just send it to the network.
    if (entry.isExpired()) {
        request.addMarker("cache-hit-expired");
        request.setCacheEntry(entry);
        mNetworkQueue.put(request);
        continue;
    }

若取到緩存且沒過期,則解析緩存

 Response<?> response = request.parseNetworkResponse(
       new NetworkResponse(entry.data, entry.responseHeaders));
  

若不需要刷新則把request和response丟到ui線程中吗垮,回調(diào)request中的請求成功或失敗listener垛吗,同時finish自己
若還需要刷新的話還需要把request丟到mNetworkQueue中,讓NetworkDispatcher去獲取數(shù)據(jù)烁登。
NetworkDispatcher在獲取到數(shù)據(jù)后執(zhí)行了如下代碼:

// 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");
    }

CacheDispatcher 才是處理緩存相關(guān)的怯屉,為什么 NetworkDispatcher 中還需要進(jìn)行以上的判斷呢?

Request#finish自己

前面我們提到過 CacheDispatcher 把相同的request放到了隊(duì)列中饵沧,當(dāng)獲取到數(shù)據(jù)后調(diào)用了request的finish方法锨络,該方法又調(diào)用了
mRequestQueue的finish方法。

 void finish(final String tag) {
        if (mRequestQueue != null) {
            mRequestQueue.finish(this);
        }
        ...
    }

request的finish方法如下:

<T> void finish(Request<T> request) {

        ...
        if (request.shouldCache()) {
            synchronized (mWaitingRequests) {
                String cacheKey = request.getCacheKey();
                Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
                if (waitingRequests != null) {
                    if (VolleyLog.DEBUG) {
                        VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
                            waitingRequests.size(), cacheKey);
                    }
                        // Process all queued up requests. They won't be considered as in flight, but
                        // that's not a problem as the cache has been primed by 'request'.
                    mCacheQueue.addAll(waitingRequests);
                }
            }
        }
    }
}

finish中取出了相同的request所在的隊(duì)列狼牺,然后把請求丟到了mCacheQueue中羡儿,丟到mCacheQueue后就會導(dǎo)致 CacheDispatcher 去執(zhí)行網(wǎng)絡(luò)請求,
這時由于上次的請求已經(jīng)緩存了是钥,所以可以直接使用上傳的數(shù)據(jù)了掠归,到此為止request如何finish自己就介紹完了。

取消請求

我們可以調(diào)用request.cancel取消某個請求悄泥,也可以調(diào)用requestQueue的 cancelAll(RequestFilter filter) 或cancelAll(final Object tag) 取消多個請求虏冻。

我們來看一下cancelAll(RequestFilter filter) 方法

 /**
     * Cancels all requests in this queue for which the given filter applies.
     * @param filter The filtering function to use
     */
    public void cancelAll(RequestFilter filter) {
        synchronized (mCurrentRequests) {
            for (Request<?> request : mCurrentRequests) {
                if (filter.apply(request)) {
                    request.cancel();
                }
            }
        }
    }

cancelAll需要一個RequestFilter,RequestFilter是一個接口弹囚,代碼如下

 /**
     * A simple predicate or filter interface for Requests, for use by
     * {@link RequestQueue#cancelAll(RequestFilter)}.
     */
    public interface RequestFilter {
        public boolean apply(Request<?> request);
    }

我們可以通過實(shí)現(xiàn)自己的RequestFilter來取消特定的請求厨相,比如我們可以在apply中判斷request的url是不是http://api.mogujie.org/gw/mwp.timelinemwp.homeListActionlet/1/?data=xxx,若是則返回true鸥鹉,否則返回false蛮穿,這樣就可以結(jié)束特定url的請求。

cancelAll(final Object tag)中自己實(shí)現(xiàn)了一個RequestFilter宋舷,根據(jù)tag來結(jié)束特定請求绪撵,代碼如下:

 /**
     * Cancels all requests in this queue with the given tag. Tag must be non-null
     * and equality is by identity.
     */
    public void cancelAll(final Object tag) {
        if (tag == null) {
            throw new IllegalArgumentException("Cannot cancelAll with a null tag");
        }
        cancelAll(new RequestFilter() {
            @Override
            public boolean apply(Request<?> request) {
                return request.getTag() == tag;
            }
        });
    }

根據(jù)以上代碼可以看出,最終都是調(diào)用了request的cancel方法祝蝠,cancel中只是簡單的標(biāo)記了一下該request需要結(jié)束音诈,代碼如下:

  /**
     * Mark this request as canceled.  No callback will be delivered.
     */
    public void cancel() {
        mCanceled = true;
    }

Volley會在執(zhí)行網(wǎng)絡(luò)請求前和回調(diào)監(jiān)聽前判斷一下標(biāo)記位是否已取消,若取消就結(jié)束自己绎狭,不再執(zhí)行網(wǎng)絡(luò)請求细溅,也不回調(diào),從而達(dá)到取消請求的效果儡嘶。代碼如下:

NetworkDispatcher 和 CacheDispatcher


    @Override
    public void run() {
       ...
        while (true) {
            try {
               ...
                final Request<?> request = mCacheQueue.take();
               ...
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }
           }
           
       }
   }

ExecutorDelivery中

    public void run() {
           
            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);
            }

緩存處理

HttpHeaderParser.parseCacheHeaders
處理字符串(分割等)得到響應(yīng)頭喇聊,并把響應(yīng)頭,body包裝到Cache.Entry中返回蹦狂。
當(dāng)NetworkDispatcher請求到數(shù)據(jù)后會判斷requset是否需要緩存誓篱,需要的話會調(diào)用DiskBasedCache的put(String key, Entry entry)方法朋贬,key是url,put中先調(diào)用了pruneIfNeeded窜骄,如果緩存新的數(shù)據(jù)后超過規(guī)定大小就先刪除一部分锦募。

  /**
     * Prunes the cache to fit the amount of bytes specified.
     * @param neededSpace The amount of bytes we are trying to fit into the cache.
     */
    private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }
        long before = mTotalSize;
      
        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;
            } 
            iterator.remove();
            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }
    }

put中可以看到數(shù)據(jù)被保存到了文件中。

 /**
     * Puts the entry with the specified key into the cache.
     */
    @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

Request請求失敗重試機(jī)制

Volley的請求重試機(jī)制是在Request中設(shè)置的邻遏,這樣的好處是每一個Request都可以有自己的重試機(jī)制糠亩,代碼如下

  /**
     * Creates a new request with the given method (one of the values from {@link Method}),
     * URL, and error listener.  Note that the normal response listener is not provided here as
     * delivery of responses is provided by subclasses, who have a better idea of how to deliver
     * an already-parsed response.
     */
    public Request(int method, String url, Response.ErrorListener listener) {
        mMethod = method;
        mUrl = url;
        mErrorListener = listener;
        setRetryPolicy(new DefaultRetryPolicy());

        mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
    }

Request#setRetryPolicy中只是記錄了一下RetryPolicy

/**
     * Sets the retry policy for this request.
     *
     * @return This Request object to allow for chaining.
     */
    public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
        mRetryPolicy = retryPolicy;
        return this;
    }

DefaultRetryPolicy實(shí)現(xiàn)了RetryPolicy接口,根據(jù)接口我們可以看到DefaultRetryPolicy可以提供當(dāng)前超時時間准验,當(dāng)前重試次數(shù)等


/**
 * Retry policy for a request.
 */
public interface RetryPolicy {

    /**
     * Returns the current timeout (used for logging).
     */
    public int getCurrentTimeout();

    /**
     * Returns the current retry count (used for logging).
     */
    public int getCurrentRetryCount();

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     * @param error The error code of the last attempt.
     * @throws VolleyError In the event that the retry could not be performed (for example if we
     * ran out of attempts), the passed in error is thrown.
     */
    public void retry(VolleyError error) throws VolleyError;
}

BasicNetwork中performRequest中請求失斒晗摺(SocketTimeoutException,ConnectTimeoutException等)時會再次請求一次(默認(rèn)重試一次)
若還是失敗就會拋出VolleyError異常
具體代碼如下:

public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        ...
        while (true) {
            ...
            try {
                ...
                httpResponse = mHttpStack.performRequest(request, headers);
                ...
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
                    SystemClock.elapsedRealtime() - requestStart);
            } catch (SocketTimeoutException e) {
                attemptRetryOnException("socket", request, new TimeoutError());
            } catch (ConnectTimeoutException e) {
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (MalformedURLException e) {
                throw new RuntimeException("Bad URL " + request.getUrl(), e);
            } catch (IOException e) {
                ...
            }
        }
    }
    

attemptRetryOnException代碼如下:

 private static void attemptRetryOnException(String logPrefix, Request<?> request,
            VolleyError exception) throws VolleyError {
        RetryPolicy retryPolicy = request.getRetryPolicy();
        int oldTimeout = request.getTimeoutMs();

        try {
            retryPolicy.retry(exception);
        } catch (VolleyError e) {
            request.addMarker(
                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
            throw e;
        }
        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
    }

request.getRetryPolicy()得到的是DefaultRetryPolicy類糊饱,DefaultRetryPolicy中retry方法

    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }

    //Returns true if this policy has attempts remaining, false otherwise.
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }

request.getRetryPolicy() 得到的是 DefaultRetryPolicy 對象垂寥,request重試次數(shù)超過規(guī)定的次數(shù)時
attemptRetryOnException 就會拋出 VolleyError,從而導(dǎo)致 performRequest() 方法中 while
循環(huán)終止济似,同時繼續(xù)向上拋異常矫废。

PoolingByteArrayOutputStream & ByteArrayPool

為了避免讀取服務(wù)端數(shù)據(jù)時反復(fù)的內(nèi)存申請适滓,Volley提供了PoolingByteArrayOutputStream和ByteArrayPool秫逝。

我們先看一下PoolingByteArrayOutputStream的父類ByteArrayOutputStream

/**
     * The byte array containing the bytes written.
     */
    protected byte[] buf;

    /**
     * The number of bytes written.
     */
    protected int count;

ByteArrayOutputStream中提供了兩個protected 的byte[] buf 和 int count婆咸,buf用于write時保存數(shù)據(jù),count記錄buf已使用的大小台舱,因?yàn)槎际莗rotected,所有在子類中可以對其進(jìn)行修改潭流。

我們來看一下ByteArrayOutputStream的write方法竞惋,可以看到write中調(diào)用了擴(kuò)展buf大小的expand方法,再來看一下expand的具體實(shí)現(xiàn)

  private void expand(int i) {
        /* Can the buffer handle @i more bytes, if not expand it */
        if (count + i <= buf.length) {
            return;
        }

        byte[] newbuf = new byte[(count + i) * 2];
        System.arraycopy(buf, 0, newbuf, 0, count);
        buf = newbuf;
    }

可以看到當(dāng)已使用的空間+要寫入的大小>buf大小時灰嫉,直接new 了一個兩倍大小的空間拆宛,并把原來的buf中的數(shù)據(jù)復(fù)制到了新的空間中,最后把新分配的空間賦值給了buf讼撒,原來的buf由于沒有被任何對象持有浑厚,最終會被回收掉。PoolingByteArrayOutputStream就是在重寫的expand對buf進(jìn)行了處理根盒。

 @Override
    public synchronized void write(byte[] buffer, int offset, int len) {
        Arrays.checkOffsetAndCount(buffer.length, offset, len);
        if (len == 0) {
            return;
        }
        expand(len);
        System.arraycopy(buffer, offset, buf, this.count, len);
        this.count += len;
    }

    /**
     * Writes the specified byte {@code oneByte} to the OutputStream. Only the
     * low order byte of {@code oneByte} is written.
     *
     * @param oneByte
     *            the byte to be written.
     */
    @Override
    public synchronized void write(int oneByte) {
        if (count == buf.length) {
            expand(1);
        }
        buf[count++] = (byte) oneByte;
    }

接下來我們看一下PoolingByteArrayOutputStream是怎么復(fù)用內(nèi)存空間的钳幅。

在執(zhí)行網(wǎng)絡(luò)請求的BasicNetwork我們看到new 了一個ByteArrayPool

 /**
     * @param httpStack HTTP stack to be used
     */
    public BasicNetwork(HttpStack httpStack) {
        // If a pool isn't passed in, then build a small default pool that will give us a lot of
        // benefit and not use too much memory.
        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
    }

我們看一下ByteArrayPool的構(gòu)造函數(shù)

 /** The buffer pool, arranged both by last use and by buffer size */
    private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
    private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);

 /**
     * @param sizeLimit the maximum size of the pool, in bytes
     */
    public ByteArrayPool(int sizeLimit) {
        mSizeLimit = sizeLimit;
    }

可以看到ByteArrayPool只是記錄了一下最大的內(nèi)存池空間,默認(rèn)是4096 bytes炎滞,并創(chuàng)建了兩個保存byte[]數(shù)組的List敢艰。
為什么要有兩個List<byte[]> , mBuffersBySize用于二分查找,(int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);)册赛。mBuffersByLastUse用于LRU(Least recently used钠导,最近最少使用)置換算法震嫉。

我們從BasicNetwork中看到讀取服務(wù)端數(shù)據(jù)時調(diào)用了entityToBytes方法

  /** Reads the contents of HttpEntity into a byte[]. */
    private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
        PoolingByteArrayOutputStream bytes =
                new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
        byte[] buffer = null;
        try {
            InputStream in = entity.getContent();
            if (in == null) {
                throw new ServerError();
            }
            buffer = mPool.getBuf(1024);
            int count;
            while ((count = in.read(buffer)) != -1) {
                bytes.write(buffer, 0, count);
            }
            return bytes.toByteArray();
        } finally {
            try {
                // Close the InputStream and release the resources by "consuming the content".
                entity.consumeContent();
            } catch (IOException e) {
                // This can happen if there was an exception above that left the entity in
                // an invalid state.
                VolleyLog.v("Error occured when calling consumingContent");
            }
            mPool.returnBuf(buffer);
            bytes.close();
        }
    }

entityToBytes中又new 了一個PoolingByteArrayOutputStream,PoolingByteArrayOutputStream是繼承自java.io.ByteArrayOutputStream的牡属。我們看一下PoolingByteArrayOutputStream構(gòu)造函數(shù)

 /**
     * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
     * more than {@code size} bytes are written to this instance, the underlying byte array will
     * expand.
     *
     * @param size initial size for the underlying byte array. The value will be pinned to a default
     *        minimum size.
     */
    public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
        mPool = pool;
        buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
    }

PoolingByteArrayOutputStream構(gòu)造函數(shù)中調(diào)用了mPool.getBuf并賦值給了父類的buf责掏,所以以后調(diào)用write時都是把數(shù)據(jù)寫到了mPool.getBuf得到的byte[]數(shù)組中,也就是byte[]池中湃望。getBuf代碼如下:

 /**
     * Returns a buffer from the pool if one is available in the requested size, or allocates a new
     * one if a pooled one is not available.
     *
     * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
     *        larger.
     * @return a byte[] buffer is always returned.
     */
    public synchronized byte[] getBuf(int len) {
        for (int i = 0; i < mBuffersBySize.size(); i++) {
            byte[] buf = mBuffersBySize.get(i);
            if (buf.length >= len) {
                mCurrentSize -= buf.length;
                mBuffersBySize.remove(i);
                mBuffersByLastUse.remove(buf);
                return buf;
            }
        }
        return new byte[len];
    }

由于內(nèi)存池是被多個NetworkDispatcher公用的换衬,所以getBuf前加了synchronized,getBuf就是從byte[]池中找一個滿足大小的空間返回证芭,并從List移除掉瞳浦,若沒有足夠大的則new一個。再來看一下PoolingByteArrayOutputStream的write方法

 @Override
    public synchronized void write(byte[] buffer, int offset, int len) {
        expand(len);
        super.write(buffer, offset, len);
    }

    @Override
    public synchronized void write(int oneByte) {
        expand(1);
        super.write(oneByte);
    }

可以看到write中調(diào)用了expand方法废士,這個方法不是ByteArrayOutputStream中的叫潦,而是PoolingByteArrayOutputStream重寫的,現(xiàn)在看一下expand方法

 /**
     * Ensures there is enough space in the buffer for the given number of additional bytes.
     */
    private void expand(int i) {
        /* Can the buffer handle @i more bytes, if not expand it */
        if (count + i <= buf.length) {
            return;
        }
        byte[] newbuf = mPool.getBuf((count + i) * 2);
        System.arraycopy(buf, 0, newbuf, 0, count);
        mPool.returnBuf(buf);
        buf = newbuf;
    }

expand中沒有調(diào)用父類的expand方法官硝,其與父類expand方法的卻別就是由每次的new byte[]變成了從byte[]池中獲取矗蕊。

在entityToBytes方法中我們看到用完之后又調(diào)用了mPool.returnBuf(buffer);把byte[]歸還給了byte[]池,代碼如下:

 /**
     * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
     * size.
     *
     * @param buf the buffer to return to the pool.
     */
    public synchronized void returnBuf(byte[] buf) {
        if (buf == null || buf.length > mSizeLimit) {
            return;
        }
        mBuffersByLastUse.add(buf);
        int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
        if (pos < 0) {
            pos = -pos - 1;
        }
        mBuffersBySize.add(pos, buf);
        mCurrentSize += buf.length;
        trim();
    }

    /**
     * Removes buffers from the pool until it is under its size limit.
     */
    private synchronized void trim() {
        while (mCurrentSize > mSizeLimit) {
            byte[] buf = mBuffersByLastUse.remove(0);
            mBuffersBySize.remove(buf);
            mCurrentSize -= buf.length;
        }
    }

Volley加載圖片

Volley除了可以獲取json還可以加載圖片氢架,用法如下:

ImageRequest imageRequest = new ImageRequest(url,  
        new Response.Listener<Bitmap>() {  
            @Override  
            public void onResponse(Bitmap response) {  
                imageView.setImageBitmap(response);  
            }  
        }, 0, 0, Config.RGB_565, new Response.ErrorListener() {  
            @Override  
            public void onErrorResponse(VolleyError error) {  
                imageView.setImageResource(R.drawable.default_image);  
            }  
        }); 

ImageRequest的構(gòu)造函數(shù)接收6個參數(shù)傻咖,第一個參數(shù)就是圖片的URL地址。第二個參數(shù)是圖片請求成功的回調(diào)岖研,這里我們把返回的Bitmap參數(shù)設(shè)置到ImageView中卿操。第三第四個參數(shù)分別用于指定允許圖片最大的寬度和高度,如果指定的網(wǎng)絡(luò)圖片的寬度或高度大于這里的最大值孙援,則會對圖片進(jìn)行壓縮害淤,指定成0的話就表示不管圖片有多大,都不會進(jìn)行壓縮拓售。第五個參數(shù)用于指定圖片的顏色屬性窥摄,Bitmap.Config下的幾個常量都可以在這里使用,其中ARGB_8888可以展示最好的顏色屬性础淤,每個圖片像素占據(jù)4個字節(jié)的大小崭放,而RGB_565則表示每個圖片像素占據(jù)2個字節(jié)大小。第六個參數(shù)是圖片請求失敗的回調(diào)值骇,這里我們當(dāng)請求失敗時在ImageView中顯示一張默認(rèn)圖片莹菱。

ImageRequest默認(rèn)采用GET方式獲取圖片,采用ScaleType.CENTER_INSIDE方式縮放圖片

 public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
            ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES,
                DEFAULT_IMAGE_BACKOFF_MULT));
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
        mScaleType = scaleType;
    }

    /**
     * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to
     * the normal constructor with {@code ScaleType.CENTER_INSIDE}.
     */
    @Deprecated
    public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
            Config decodeConfig, Response.ErrorListener errorListener) {
        this(url, listener, maxWidth, maxHeight,
                ScaleType.CENTER_INSIDE, decodeConfig, errorListener);
    }

我們來看一下ImageRequest具體執(zhí)行邏輯

  @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        // Serialize all decode on a global lock to reduce concurrent heap usage.
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                return Response.error(new ParseError(e));
            }
        }
    }

    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     */
    private Response<Bitmap> doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap = null;
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;
            // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
            // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
            decodeOptions.inSampleSize =
                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap =
                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap,
                        desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }

doParse中對圖片進(jìn)行了縮放處理,可以看到當(dāng)mMaxWidth == 0 && mMaxHeight == 0時吱瘩,沒有做過多的處理道伟,這種情況適合于不打的圖片,否則容易導(dǎo)致內(nèi)存溢出。else中對圖片進(jìn)行了縮放處理蜜徽,首先創(chuàng)建一個Options 對象祝懂,并設(shè)置decodeOptions.inJustDecodeBounds = true,然后使用BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);時就可以只獲取圖片的大小拘鞋,而不需要把data數(shù)組轉(zhuǎn)換成bitmap砚蓬,
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);可以直接從內(nèi)存中獲取圖片的寬高,之后就可以使用
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
來獲取圖片寬高了盆色。

獲取到圖片寬高后需要通過findBestSampleSize找到一個合適的縮放比例并賦值給decodeOptions.inSampleSize灰蛙,縮放比例一定是2的n次冪。即使不是2的冪最終也會按2的n次冪處理

 static int findBestSampleSize(
            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desiredHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }

        return (int) n;
    }
 /**
        * If set to a value > 1, requests the decoder to subsample the original
        * image, returning a smaller image to save memory. The sample size is
        * the number of pixels in either dimension that correspond to a single
        * pixel in the decoded bitmap. For example, inSampleSize == 4 returns
        * an image that is 1/4 the width/height of the original, and 1/16 the
        * number of pixels. Any value <= 1 is treated the same as 1. Note: the
        * decoder uses a final value based on powers of 2, any other value will
        * be rounded down to the nearest power of 2.
        */
       public int inSampleSize;

獲取到縮放比例后就可以通過一下代碼獲取到圖片了

decodeOptions.inJustDecodeBounds = false;
decodeOptions.inSampleSize =
               findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

Handler

Volley進(jìn)行網(wǎng)絡(luò)請求是在非ui線程中進(jìn)行的隔躲,回調(diào)是怎么跑到ui線程中執(zhí)行的呢摩梧?
Volley在創(chuàng)建RequestQueue時new 了一個 ExecutorDelivery(new Handler(Looper.getMainLooper()))

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

以前使用handler時我們都是直接new Handler(),沒有跟任何參數(shù),但不跟參數(shù)的Handler默認(rèn)使用的是該線程獨(dú)有的Looper宣旱,默認(rèn)情況下ui線程是有Looper的而其他線程是沒有Looper的仅父,在非UI線程中直接new Handler()會出錯,我們可以通過Looper.getMainLooper()得到ui線程的Looper浑吟,這樣任何線程都可以使用ui線程的Looper了笙纤,而且可以在任何線程中創(chuàng)建Handler,并且使用handler發(fā)送消息時就會跑到ui線程執(zhí)行了组力。

也就是說如果我們想在任何線程中都可以創(chuàng)建Hadler省容,并且handler發(fā)送的消息要在ui線程執(zhí)行的話,就可以采用這種方式創(chuàng)建Handler

new Handler(Looper.getMainLooper());

--> handler之java工程版

volley gson改造

在實(shí)際開發(fā)中我們會遇到對文字表情的混排處理忿项,一種做法是服務(wù)端直接返回轉(zhuǎn)意后的字符串(比如 ? 用 \:wx 代替)蓉冈,然后客戶端每次都要在ui線程中解析字符串轉(zhuǎn)換成Spanned,若是在ListView中轩触,滾動就會非常卡頓家夺。

我們可以自定義一個XJsonRequest<T>并繼承自Request<T>脱柱,同時為XJsonRequest增加一個注冊gson類型轉(zhuǎn)換的方法,并把泛型參數(shù)中圖文混排字段設(shè)置為Spanned拉馋,然后在Response<String> parseNetworkResponse(NetworkResponse response)中把圖文混拍json轉(zhuǎn)換成Spanned即可榨为,由于Response<String> parseNetworkResponse(NetworkResponse response)是在非ui線程中執(zhí)行的,所已不會導(dǎo)致ANR煌茴。

demo如下:

先看效果圖:

Alt text
Alt text

代碼如下:

String json = "{\"nickname\":\"xiaoming\",\"age\":10,\"emoj\":\"[:f001}[:f002}[:f003}hello[:f004}[:f005}\"}";  
        jsonTv.setText(json);  
          
        Gson gson = new GsonBuilder().registerTypeAdapter(Spanned.class, new String2Spanned()).create();  
  
        Person person = gson.fromJson(json, Person.class);  
  
        editText.append(person.getNickname());  
        editText.append(" ");  
        editText.append(person.getAge() + " ");  
        editText.append(person.getEmoj());  
  
        tv.append(Spanned2String.parse(editText.getText())); 
public  class Person {  
    private Spanned emoj;  
    private String nickname;  
    private int age;  
}
  
/** 
 * 字符串轉(zhuǎn)表情Spanned 表情格式 [:f000} 
 *  
 * @author wanjian 
 * 
 */  
public class String2Spanned extends TypeAdapter<Spanned> {  
  
    static Pattern pattern;  
  
    static Map<String, Integer> map = new HashMap<String, Integer>();  
  
    static ImageGetter imageGetter = new ImageGetter() {  
        @Override  
        public Drawable getDrawable(String source) {  
            int id = Integer.parseInt(source);  
            Drawable d = MyApplication.application.getResources().getDrawable(id);  
            d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());  
            return d;  
        }  
    };  
  
    static {  
  
        pattern = Pattern.compile("\\[:f\\d{3}\\}"); 
        //省略部分代碼 
        map.put("[:f000}", R.drawable.f000);  
        map.put("[:f001}", R.drawable.f001);  
        map.put("[:f002}", R.drawable.f002);  
        map.put("[:f003}", R.drawable.f003);  
        map.put("[:f004}", R.drawable.f004);  
        map.put("[:f005}", R.drawable.f005);  
    }  
  
    @Override  
    public Spanned read(JsonReader in) throws IOException {  
        if (in.peek() == JsonToken.NULL) {  
            in.nextNull();  
            return null;  
        }  
  
        String origStr = in.nextString();  
        Matcher matcher = pattern.matcher(origStr);  
  
        StringBuilder stringBuilder = new StringBuilder();  
  
        int last = 0;  
        while (matcher.find()) {  
  
            int s = matcher.start();  
            int e = matcher.end();  
            stringBuilder.append(origStr.substring(last, s));  
  
            String group = matcher.group();  
            Integer emojId = map.get(group);  
            if (emojId == null) {  
                stringBuilder.append(group);  
            } else {  
                stringBuilder.append("<img src='");  
                stringBuilder.append(emojId);  
                stringBuilder.append("'/>");  
            }  
  
            last = e;  
  
        }  
        stringBuilder.append(origStr.substring(last, origStr.length()));  
                // String ss = "[站外圖片上傳中……(4)]";  
  
        return Html.fromHtml(stringBuilder.toString(), imageGetter, null);  
    }  
  
    @Override  
    public void write(JsonWriter arg0, Spanned arg1) throws IOException {  
  
    }  
  
}  

import org.apache.commons.lang.StringEscapeUtils;  
/** 
 * 表情轉(zhuǎn) 字符串 
 * @author wanjian 
 * 
 */  
public class Spanned2String {  
  
    //  <p dir="ltr">[站外圖片上傳中……(5)][站外圖片上傳中……(6)]</p>  
    //   <p dir="ltr">hello</p>  
      
    static Pattern pattern;  
    static Map<String,String>map=new HashMap<>();  
    static{  
        pattern=Pattern.compile("<img src=\"\\d+\">");  
          //省略部分代碼
        map.put(String.valueOf(R.drawable.f000) , "[:f000}");  
        map.put(String.valueOf(R.drawable.f001) , "[:f001}");  
        map.put(String.valueOf(R.drawable.f002) , "[:f002}");  
        map.put(String.valueOf(R.drawable.f003) , "[:f003}");  
        map.put(String.valueOf(R.drawable.f004) , "[:f004}");  
    }
    public static String decode(String str) {  
      String[] tmp = str.split(";&#|&#|;");  
      StringBuilder sb = new StringBuilder("");  
      for (int i = 0; i < tmp.length; i++) {  
        if (tmp[i].matches("\\d{5}")) {  
          sb.append((char) Integer.parseInt(tmp[i]));  
        } else {  
          sb.append(tmp[i]);  
        }  
      }  
      return sb.toString();  
    }  
      
    public static String parse(Spanned spanned  ) {  
          
        try {  
              
            String origHtml= Html.toHtml(spanned);  
            //某些機(jī)型Html.toHtml得到的字符串略有區(qū)別随闺,需要處理下
            //模擬器:  <p dir="ltr">慢慢</p>  
            //小米2A <p dir=ltr>慢慢</p>  
              
            String origStr=origHtml.substring(origHtml.indexOf(">")+1, origHtml.length()-5);  
            origStr=StringEscapeUtils.unescapeHtml(origStr);//html 漢字實(shí)體編碼轉(zhuǎn)換  
            Matcher matcher=pattern.matcher(origStr);  
              
           //      [站外圖片上傳中……(7)]hello[站外圖片上傳中……(8)]  
            StringBuilder stringBuilder=new StringBuilder();  
            int last=0;  
            while (matcher.find()) {  
                int s= matcher.start();  
                int e=matcher.end();  
                  
                stringBuilder.append(origStr.substring( last,s));  
                  
                String group=matcher.group();  
                String id=group.substring(10, group.length()-2);  
                  
                String emojStr=map.get(id);  
                if (emojStr==null) {  
                    stringBuilder.append(group);  
                      
                }else{  
                    stringBuilder.append(emojStr);  
                      
                }  
                   
                last=e;  
                  
            }  
            stringBuilder.append(origStr.substring(last, origStr.length()));  
            return stringBuilder.toString().replace("<br>", "\n");  
              
        } catch (Exception e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
              
            return "";  
        }  
          
    }  
  
}  

-->完整代碼

volley okhttp改造


 
public class OkHttpStack implements HttpStack {
    private final OkHttpClient mClient;

    public OkHttpStack(OkHttpClient client) {
        this.mClient = client;
    }

    @Override
    public HttpResponse performRequest(com.android.volley.Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {
        OkHttpClient client = mClient.clone();
        int timeoutMs = request.getTimeoutMs();
        client.setConnectTimeout(timeoutMs, TimeUnit.MILLISECONDS);
        client.setReadTimeout(timeoutMs, TimeUnit.MILLISECONDS);
        client.setWriteTimeout(timeoutMs, TimeUnit.MILLISECONDS);

        com.squareup.okhttp.Request.Builder okHttpRequestBuilder = new com.squareup.okhttp.Request.Builder();
        okHttpRequestBuilder.url(request.getUrl());

        Map<String, String> headers = request.getHeaders();

        for (final String name : headers.keySet()) {
            if (name!=null&& headers.get(name)!=null) {
                okHttpRequestBuilder.addHeader(name, headers.get(name));
            }
            
        }

        for (final String name : additionalHeaders.keySet()) {
            if (name!=null&& headers.get(name)!=null) 
                okHttpRequestBuilder.addHeader(name, additionalHeaders.get(name));
        }

        setConnectionParametersForRequest(okHttpRequestBuilder, request);

        com.squareup.okhttp.Request okHttpRequest = okHttpRequestBuilder.build();
        Response okHttpResponse = client.newCall(okHttpRequest).execute();

        StatusLine responseStatus = new BasicStatusLine(parseProtocol(okHttpResponse.protocol()), okHttpResponse.code(),
                okHttpResponse.message());

        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
        response.setEntity(entityFromOkHttpResponse(okHttpResponse));

        Headers responseHeaders = okHttpResponse.headers();

        for (int i = 0, len = responseHeaders.size(); i < len; i++) {
            final String name = responseHeaders.name(i), value = responseHeaders.value(i);

            if (name != null) {
                response.addHeader(new BasicHeader(name, value));
            }
        }

        return response;
    }

    private static HttpEntity entityFromOkHttpResponse(Response r) throws IOException {
        BasicHttpEntity entity = new BasicHttpEntity();
        ResponseBody body = r.body();

        entity.setContent(body.byteStream());
        entity.setContentLength(body.contentLength());
        entity.setContentEncoding(r.header("Content-Encoding"));

        if (body.contentType() != null) {
            entity.setContentType(body.contentType().type());
        }
        return entity;
    }


    private static void setConnectionParametersForRequest(com.squareup.okhttp.Request.Builder builder,
            com.android.volley.Request<?> request) throws IOException, AuthFailureError {
        switch (request.getMethod()) {
        case com.android.volley.Request.Method.DEPRECATED_GET_OR_POST:
            // Ensure backwards compatibility.
            // Volley assumes a request with a null body is a GET.
            byte[] postBody = request.getPostBody();

            if (postBody != null) {
                builder.post(RequestBody.create(MediaType.parse(request.getPostBodyContentType()), postBody));
            }
            break;

        case com.android.volley.Request.Method.GET:
            builder.get();
            break;

        case com.android.volley.Request.Method.DELETE:
            builder.delete();
            break;

        case com.android.volley.Request.Method.POST:
            builder.post(createRequestBody(request));
            break;

        case com.android.volley.Request.Method.PUT:
            builder.put(createRequestBody(request));
            break;

        // case com.android.volley.Request.Method.HEAD:
        // builder.head();
        // break;
        //
        // case com.android.volley.Request.Method.OPTIONS:
        // builder.method("OPTIONS", null);
        // break;
        //
        // case com.android.volley.Request.Method.TRACE:
        // builder.method("TRACE", null);
        // break;
        //
        // case com.android.volley.Request.Method.PATCH:
        // builder.patch(createRequestBody(request));
        // break;

        default:
            throw new IllegalStateException("Unknown method type.");
        }
    }

    private static ProtocolVersion parseProtocol(final Protocol p) {
        switch (p) {
        case HTTP_1_0:
            return new ProtocolVersion("HTTP", 1, 0);
        case HTTP_1_1:
            return new ProtocolVersion("HTTP", 1, 1);
        case SPDY_3:
            return new ProtocolVersion("SPDY", 3, 1);
        case HTTP_2:
            return new ProtocolVersion("HTTP", 2, 0);
        }

        throw new IllegalAccessError("Unkwown protocol");
    }

    private static RequestBody createRequestBody(com.android.volley.Request<?> r) throws AuthFailureError {
        final byte[] body = r.getBody();
        if (body == null)
            return null;

        return RequestBody.create(MediaType.parse(r.getBodyContentType()), body);
    }
}

by 萬劍

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蔓腐,隨后出現(xiàn)的幾起案子矩乐,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件散罕,死亡現(xiàn)場離奇詭異分歇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)欧漱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門职抡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人误甚,你說我怎么就攤上這事缚甩。” “怎么了窑邦?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵擅威,是天一觀的道長。 經(jīng)常有香客問我奕翔,道長裕寨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任派继,我火速辦了婚禮宾袜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驾窟。我一直安慰自己庆猫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布绅络。 她就那樣靜靜地躺著月培,像睡著了一般。 火紅的嫁衣襯著肌膚如雪恩急。 梳的紋絲不亂的頭發(fā)上杉畜,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音衷恭,去河邊找鬼此叠。 笑死,一個胖子當(dāng)著我的面吹牛随珠,可吹牛的內(nèi)容都是我干的灭袁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼窗看,長吁一口氣:“原來是場噩夢啊……” “哼茸歧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起显沈,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤软瞎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铜涉,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡智玻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了芙代。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吊奢。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖纹烹,靈堂內(nèi)的尸體忽然破棺而出页滚,到底是詐尸還是另有隱情,我是刑警寧澤铺呵,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布裹驰,位于F島的核電站,受9級特大地震影響片挂,放射性物質(zhì)發(fā)生泄漏幻林。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一音念、第九天 我趴在偏房一處隱蔽的房頂上張望沪饺。 院中可真熱鬧,春花似錦闷愤、人聲如沸整葡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遭居。三九已至,卻和暖如春旬渠,著一層夾襖步出監(jiān)牢的瞬間俱萍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工告丢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鼠次,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓芋齿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親成翩。 傳聞我的和親對象是個殘疾皇子觅捆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容