Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 一個輕量級的Android網絡請求框架

一個輕量級的Android網絡請求框架

編輯:關於Android編程

最近有空在看《App研發錄》一書,良心之作。書中第一部分第二章節講了不少關於網絡底層封裝的知識,看後覺得學到了不少干貨。

索性自己也動手完成了一個非常輕量級的網絡請求框架,從該書中獲得了不少幫助。特此記錄,回顧一下思路,整理收獲。OK,一起來看。

就如書中所言,通常我們可以通過AsyncTask來進行網絡請求的處理。而不少網絡請求框架的底層也正是基於AsyncTask來進行封裝的。

顯然AsyncTask有很多優點,使用也十分便捷。但它肯定同樣也存在缺點:即我們無法靈活控制其內部的線程池;無法取消請求等。

無法取消一個請求的情況是指:假設我們在Activity-A中有10個請求需要執行。那麼可能因為網絡條件等原因出現一種情況:

即用戶已經通過某種操作從Activity-A跳轉至B,這時B中也有網絡請求需要執行。但Activity雖然已經跳轉,而在其中發出的請求仍會繼續進行。

那麼,Activity-B中的請求就會因為等待Activity-A中的請求執行完畢而陷入阻塞。從而造成一種無限“擁堵”的情況。

我們自然需要避免之前的這些情況。所以我們的框架將采取原生 的ThreadPoolExecutor + Runnble + Handler + HttpUrlConnection實現。

我們先通過一張圖,來看一看完成後的框架其最終的結構是怎麼樣的:

 

這裡寫圖片描述

 

現在我們一次來分析一下它們的作用,由於篇幅的原因,這裡不會一一貼出源碼,該框架的github地址如下,有興趣的朋友可以對應一看:

https://github.com/unconventional1programmer/PacHttpClient

現在我們首先肯定就是用一用它,先爽一下。然後簡單的挨個分析一下框架中的每個類都扮演了什麼角色。

首先,我們要做的肯定是設置相關配置信息,並初始化我們的請求框架。就像下面這樣:
        PacHttpClientConfig config =
                new PacHttpClientConfig(getApplicationContext())
                .corePoolZie( ? )
                .maxPoolSize( ? )
                .keepAliveTime( ? )
                .timeUnit( ? )
                .blockingQueue( ? );
        PacHttpClient.init(config);

而因為我們本身在框架裡也設置過默認的線程池配置信息,所以我們也可以使用另一種更加偷懶的初始化方式:

        PacHttpClientConfig config = new PacHttpClientConfig(getApplicationContext());
        PacHttpClient.init(config);
好了,現在我們先來簡單的發起一個GET請求。來體驗一下框架的使用快感:
        /*HttpRequest request = */ PacHttpClient.invokeRequest(this, "testGet", null, new RequestCallback() {
            @Override
            public void onSuccess(String content) {
                Log.d("TestMyFrameWork", "請求成功");
            }

            @Override
            public void onFail(String errorMessage) {
                Log.d("TestMyFrameWork", "請求失敗");
            }
        });

運行程序,我們查看相關的日志打印,證明我們確實已經成功的與服務器進行了一次GET通信:
這裡寫圖片描述

好的,但與此同時,我們說過我們搭建的這個框架的一大優點之一在於我們可以中斷請求,現在我們就來看看。

要模擬中斷的情況也簡單,我們只需要在Servlet服務器通過讓線程休眠5秒來模擬實際情況中的讀取數據的過程。也就是說:
當我們該次HTTP請求與服務器建立起鏈接後,read inputstrem的過程會經過5秒的時間。我們就在這個時間內,中斷本次請求。
現在,我們在之前的代碼的基礎上,加上如下一句中斷請求的代碼:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> PacHttpClient.cancelDesignatedRequest(this,request);

我們再次運行程序,經過耐心的等待,會發現不會得到任何相關的輸出信息。因為請求的確已經被我們中斷了。

好的,現在我們來加大點力度。假設我們在一個Activity中發出10個請求,看看會發生什麼。
        for (int i = 0; i < 10; i++) {
            // PacHttpClient.invokeRequest().....
        }

再次運行程序,我們會發現如下的輸出情況:
這裡寫圖片描述
這種情況我們是可以預料得到的,因為我們在框架中為線程池設置的核心池的默認大小為5,所以每次自然只會有5個線程來執行請求。
而當有請求執行完畢後,則會從阻塞隊列中取出新的請求來執行。那麼注意了,也就是說我們在Activity發出10個請求後:
有5個請求會率先開始執行,另外5個將會進入阻塞隊列中等待。那麼,我們也就可以測試我們的框架中的另一個方法了。

現在我們來測試另一種使用情況。假設我們在當前Activity發起了10個請求,但請求並未執行完畢,我們的Activity就跳轉了。

這時,我們可能會希望用兩種方式應對。第一種就是,我們希望已經開始執行的請求繼續。但將還沒執行的請求中斷。而第二種就像我們說過的:
假設我們跳轉後的Activity也有請求需要執行,那麼受之前的界面中的請求影響,所以我們希望中斷跳轉之前的Activity中的所有請求,包括正在執行的。
那麼,這個時候就開心了。因為我們在自己的請求框架中已經對於這些情況做了封裝。所以我們能很容易就能實現這種需求。

首先,我們來看看中斷未執行的請求怎麼樣發生。我們在之前的代碼的基礎上加上如下代碼:

        PacHttpClient.cancelBlockingRequest(this);

然後,我們觀察日志信息發現,還未來得及執行的5條請求的確是被取消了:
這裡寫圖片描述

好的,現在修改如下的代碼。這樣做的目的在於:我們雖然發起了10個請求,但我們希望只要有某一個請求執行完畢,就取消剩余所有的請求(包括正在執行的)

                public void onSuccess(String content) {
                    Log.d("TestMyFrameWork", "請求成功");
                    PacHttpClient.cancelAllRequest(MainActivity.this);
                }

根據日志信息,我們可以驗證我們的確實現了我們的目的:

這裡寫圖片描述

由此,我們就可以針對於一些情況做出應對了。以我們說的跳轉Activity希望取消請求而言,我們只需要在適合的聲明周期調用對應的方法就搞定了。

最後,PacHttpClient還有提供了另外兩個公有方法:shutdown以及shutdownRightnow。顧名思義,就是關閉框架中封裝的線程池的。
例如,我們可以在退出應用的時候調用來關閉線程池釋放資源。它們的不同就在於,shutdown雖然關閉線程池但會執行完當前線程池中剩余的任務。而shutdownRightnow則還會試圖立刻結束當前剩余的線程。

簡單的爽了一下,現在來簡單分析下整個框架的構成。首先來說,當我們項目中的http-api越來越多,那麼將這些url信息存放在代碼中肯定是很不爽的。
那麼,就像《App研發錄》一書中推薦的一樣,我們可以在xml目錄下新建一個xml文件單獨來管理我們的api。我們暫時將格式設定如下:



    

現在有了存放url的xml文件。那麼,對應的我們就需要一個類來解析xml文件,獲取到相關的請求信息;並將讀取到的信息存放進一個實體類以供使用。
URLEntity.java

class URLEntity {
    private String key; //apiKey
    private long expires; //緩存時間
    private HttpRequest.RequestType netType; //請求方式(GET or POST)
    private String url; //url

    //相關的setter/getter
}

URLConfigManager.java
關於這個其實類沒什麼好說的,所做的工作就是解析xml文件,並將讀取的信息存放進URLEntity對象。唯一值得注意的一點是:
如果每次讀取url都從xml文件進行解析,肯定影響效率。所以我們在初次讀取時,一次性將所有url讀進內存中的map存放,以後就直接從map中讀取。

RequestThreadPool.java
我們說過框架將采取原生的RequestThreadPool實現,該類實際就是對線程池的一個封裝。並提供相關的操作線程池的方法。

class RequestThreadPool {
    // 封裝的線程池
    private static ThreadPoolExecutor pool;
    /**
     * 根據配置信息初始化線程池
     */
    static void init(){
        PacHttpClientConfig config = PacHttpClient.config;
        pool = new ThreadPoolExecutor(config.corePoolZie,
                config.maxPoolSize, config.keepAliveTime,
                config.timeUnit, config.blockingQueue);
    }
    /**
     * 執行任務
     * @param r
     */
    public static void execute(final Runnable r) {}
    /**
     * 清空阻塞隊列
     */
    static void removeAllTask() {}
    /**
     * 從阻塞隊列中刪除指定任務
     * @param obj
     * @return
     */
    static boolean removeTaskFromQueue(final Object obj) {}
    /**
     * 獲取阻塞隊列
     * @return
     */
    static BlockingQueue getQuene(){}
    /**
     * 關閉,並等待任務執行完成,不接受新任務
     */
    static void shutdown() {}
    /**
     * 關閉,立即關閉,並掛起所有正在執行的線程,不接受新任務
     */
    static void shutdownRightnow() {}
}

HttpRequest.java
這可以說是最關鍵的一個類了,在這個類當中,我們通過HttpUrlConncetion完成對請求的實際封裝。

public class HttpRequest implements Runnable {

    //some code...

    @Override
    public void run() {
        // 判斷請求類型
        switch (urlInfo.getNetType()) {
            case GET:
                // 類型為HTTP-GET時,將請求參數組裝到URL鏈接字符串上
                String trulyURL;
                if (params != null && !params.isEmpty()) {
                    StringBuilder urlBuilder = new StringBuilder(urlInfo.getUrl());
                    urlBuilder.append("?").append(convertParam2String());
                    trulyURL = urlBuilder.toString();
                } else {
                    trulyURL = urlInfo.getUrl();
                }
                // 正式發送GET請求到服務器
                sendHttpGetToServer(trulyURL);
                break;
            case POST:
                // 發送POST請求到服務器
                sendHttpPostToServer(urlInfo.getUrl());
                break;
            default:
                break;
        }
    }

    /**
     * 發起GET請求
     *
     * @param url
     */
    private void sendHttpGetToServer(String url) {
        try {
            mURL = new URL(url);
            mConnection = (HttpURLConnection) mURL.openConnection();
            // 連接服務器的超時時長
            mConnection.setConnectTimeout(5000);
            // 從服務器讀取數據的超時時長
            mConnection.setReadTimeout(8000);

            if (mConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                // 如果未設置請求中斷,則進行讀取數據的工作
                if (!interrupted) {
                    // read content from response..
                    final String result = readFromResponse(mConnection.getInputStream());
                    // call back
                    if (callback != null) {
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onSuccess(result);
                            }
                        });
                    }
                } else { // 中斷請求
                    return;
                }
            } else {
                handleNetworkError("網絡異常");
            }
        } catch (MalformedURLException e) {
            handleNetworkError("網絡異常");
        } catch (IOException e) {
            handleNetworkError("網絡異常");
        } finally {
            hostManager.requests.remove(this);
        }
    }

   // some code....

    /**
     * 中斷請求
     */
    void disconnect() {
        // 設置標志位
        interrupted = true;
        // 如果當前請求正處於與服務器連接狀態下,則斷開連接
        if (mConnection != null)
            mConnection.disconnect();
    }
}

我們保留了部分關鍵代碼,其實該類的核心工作從上面的代碼基本上能夠得以體現。
我們這裡關注的重點放在“取消”請求。“取消”的情況實際上大體可以分為三種:

還未執行的請求,這種情況最易處理。因為未執行代表它現在處於線程池的阻塞隊列中,我們只要把任務從阻塞隊列中清楚就搞定了。 另一種情況是該請求已經與服務器建立連接,這個時候,我們需要通過httpurlconnection.disconnect來中斷連接。注意該方法會拋出IOException。 還有另外一種可惡的狀態,即可能mConnection的一些初始化工作已經執行了。但還未來及的從服務器read inputStream,線程便切換了。
這個時候disconnect是沒用的,因為現在實際根本就還沒有與服務器建立連接。所以我們在從服務器讀取數據的代碼前加上了一個判斷,
通過標志位interrupted判斷請求是否已經取消,從而決定是繼續與服務器建立連接,read inputstream;還是直接中斷任務。

RequestManager.java
因為我們知道一個activity通常肯定不會只有一個請求需要執行。所以,我們需要一個對象來管理activity中的所有請求。

class RequestManager {

    ArrayList requests;

    public RequestManager() {
        requests = new ArrayList<>();
    }

    /**
     * 無參數調用
     */
    public HttpRequest createRequest(URLEntity url, RequestCallback requestCallback) {
        return createRequest(url, null, requestCallback);
    }

    /**
     * 有參數調用
     */
    public HttpRequest createRequest(URLEntity url, List params, RequestCallback requestCallback) {
        HttpRequest request = new HttpRequest(this, url, params, requestCallback);
        addRequest(request);
        return request;
    }

    /**
     * 添加Request到列表
     */
    public void addRequest(final HttpRequest request) {
        requests.add(request);
    }

    /**
     * 取消所有的網絡請求(包括正在執行的)
     */
    public void cancelAllRequest() {
        BlockingQueue queue = RequestThreadPool.getQuene();
        for (int i = requests.size() - 1; i >= 0; i--) {
            HttpRequest request = requests.get(i);
            if (queue.contains(request)) {
                queue.remove(request);
            } else {
                request.disconnect();
            }
        }
        requests.clear();
    }

    /**
     * 取消未執行的網絡請求
     */
    public void cancelBlockingRequest() {
        // 取交集(即取出那些在線程池的阻塞隊列中等待執行的請求)
        List intersection = (List) requests.clone();
        intersection.retainAll(RequestThreadPool.getQuene());
        // 分別刪除
        RequestThreadPool.getQuene().removeAll(intersection);
        requests.removeAll(intersection);
    }

    /**
     * 取消指定的網絡請求
     */
    public void cancelDesignatedRequest(HttpRequest request) {
        if (!RequestThreadPool.removeTaskFromQueue(request)) {
            request.disconnect();
        }
    }
}

RequestParameter.java
這個類也很簡單,就是對請求參數做一個封裝,簡單的來說就是封裝請求參數的鍵值對。

RequestCallback.java
很顯然,通常我們都會根據請求從服務器返回的數據來執行一些操作。所以,我們還需要一個回調接口:

public interface RequestCallback
{
    void onSuccess(String content);

    void onFail(String errorMessage);
}

PacHttpClient.java
實際上,現在我們已經萬事俱備了。但我們不希望框架的使用者直接接觸我們底層封裝的這些類。所以我們來提供一個共有的調用類。
這個類的工作很簡單,就是提供一些共有的方法供用戶調用,來完成發起請求,中斷請求,關閉線程池等操作。
該類中還有一個關鍵的變量managerMap。我們說了,每個activity都需要自己的RequestManager來管理自身的所有請求。
這個意義在於,調用者在Activity執行響應的請求操作時,只需要傳入自身this對象,我們就能夠找到對應的請求進行操作。

    // 存放每個Activity對應的RequestManager
    static Map managerMap;

當然,我們需要為我們的框架提供一個酷酷的名字。因為讀書的時候就是一位偉大的已故HipHop大神2pac的腦殘粉,所以就叫PacHttpClient吧。
PacHttpClientConfig.java
我們還可以支持讓用戶來自己定制關於網絡框架的一些相關信息,目前這裡主要是提供對於線程池的配置信息以及context的設置。

ImageLoader.getInstance.init(config);

上面這樣類似的代碼一定很熟悉吧,我們這裡定義的此類也是提供同樣的效果。

好了,就總結到這裡了。當然了,這個小框架肯定還有很多不足和可以改進的地方,請多多指教!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved