Android編程權(quán)威指南(第二版)學(xué)習(xí)筆記(二十三)—— 第23章 HTTP 與后臺任務(wù)

本章主要講了如何使用 android 系統(tǒng)的網(wǎng)絡(luò)連接太惠,并介紹了格式化 JSON 和多線程編程 AsyncTask 的使用蓝角。另外,挑戰(zhàn)練習(xí)里還結(jié)合了 Gson 庫的使用罢屈。

GitHub 地址:
完成23章但未完成挑戰(zhàn)
完成23章挑戰(zhàn)1:使用 Gson
完成23章挑戰(zhàn)2:添加分頁
完成23章挑戰(zhàn)3:動態(tài)調(diào)整網(wǎng)格列

1. 網(wǎng)絡(luò)連接基本

首先要在 Manifest 文件中請求網(wǎng)絡(luò)權(quán)限

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

然后我們建立一個網(wǎng)絡(luò)請求的函數(shù):

// FlickrFetchr.java
// 參數(shù)是 url 字符串,并且需要拋出 IO 錯誤
public byte[] getUrlBytes(String urlSpec) throws IOException {
    URL url = new URL(urlSpec);
    // 打開連接
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();

    try {
        // 建立兩個流對象
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        // 使用 getInputStream() 方法時才會真正發(fā)送 GET 請求
        // 如果要使用 POST 請求牍帚,需要調(diào)用 getOutputStream()
        InputStream in = connection.getInputStream();
        // 如果連接失敗就拋出錯誤
        if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw new IOException(connection.getResponseMessage() +
                    ": with" +
                    urlSpec);
        }
        
        // 建立一個計數(shù)器
        int bytesRead = 0;
        // 建立一個緩存 buffer
        byte[] buffer = new byte[1024];
        // 用 InputStream.read 將數(shù)據(jù)讀取到 buffer 中儡遮,
        // 然后寫到 OutputStream 中
        while ((bytesRead = in.read(buffer)) > 0) {
            out.write(buffer, 0, bytesRead);
        }
        // 之后一定要關(guān)閉 OutputStream
        out.close();
        return out.toByteArray();
    } finally {
        // 最后要關(guān)閉連接
        connection.disconnect();
    }
}

public String getUrlString(String urlSpec) throws IOException {
    // 將結(jié)果轉(zhuǎn)換成 String
    return new String(getUrlBytes(urlSpec));
}

2. 線程與主線程

網(wǎng)絡(luò)連接需要時間,Web 服務(wù)器可能需要1~2秒的時間來響應(yīng)訪問請求暗赶,文件下載則耗時更久鄙币。考慮到這個因素蹂随,Android 禁止任何主線程網(wǎng)絡(luò)連接行為十嘿。即使強(qiáng)行在主線程中進(jìn)行網(wǎng)絡(luò)連接,Android 也會拋出 NetworkOnMainThreadException 異常岳锁。

這是為什么呢?要想知道绩衷,首先要了解什么是線程,什么是主線程以及主線程的用途是什么激率。
線程是個單一執(zhí)行序列咳燕。單個線程中的代碼會逐步執(zhí)行。所有 Android 應(yīng)用的運行都是從主線程開始的乒躺。然而招盲,主線程不是線程那樣的預(yù)定執(zhí)行序列。相反嘉冒,它處于一個無限循環(huán)的運行狀態(tài)曹货,等待著用戶或系統(tǒng)觸發(fā)事件的發(fā)生。事件觸發(fā)后讳推,主線程便負(fù)責(zé)執(zhí)行代碼顶籽,以響應(yīng)這些事件。

主線程運行著所有更新 UI 的代碼银觅,其中包括響應(yīng) activity 的啟動礼饱、按鈕的點擊等不同 UI 相關(guān)事件的代碼。(由于響應(yīng)的事件基本都與用戶界面相關(guān)究驴,主線程有時也叫作 UI 線程慨仿。)
事件處理循環(huán)讓 UI 代碼得以按順序執(zhí)行。這可以保證任何事件處理都不會發(fā)生沖突纳胧,同時代碼也能夠快速響應(yīng)執(zhí)行镰吆。

而網(wǎng)絡(luò)連接相比其他任務(wù)更耗時。等待響應(yīng)期間跑慕,用戶界面毫無反應(yīng)万皿,這可能會導(dǎo)致應(yīng)用無響應(yīng)(Application Not Responding,ANR)現(xiàn)象發(fā)生摧找,也就是一個彈框,要求你關(guān)閉應(yīng)用牢硅。
怎樣使用后臺線程最容易呢蹬耘?答案就是使用 AsyncTask 類

3. AsyncTask

3.1 AsyncTask 的生命

AsyncTask 類可以重寫的方法和一個進(jìn)程的生命過程對應(yīng):

  • onPreExecute() 執(zhí)行之前
  • onProgressUpdate() 更新進(jìn)展
  • doInBackground() 在線程中真正要完成的事
  • onPostExecute() 完成之后要做的事(在 UI 線程中執(zhí)行)
  • onCancelled() 退出之后

3.2 AsyncTask 的三個參數(shù)

其中模板的三個類類型參數(shù)(不能是基礎(chǔ)類型)分別是:輸入、進(jìn)度减余、結(jié)果综苔。

3.2.1 第一個參數(shù):輸入

第一個類型參數(shù)可指定輸入?yún)?shù)的類型∥徊恚可參考以下示例使用該參數(shù):

AsyncTask<String,Void,Void> task = new AsyncTask<String,Void,Void>() {
    public Void doInBackground(String... params) { 
        for (String parameter : params) {
            Log.i(TAG, "Received parameter: " + parameter);
        }
        return null;
    }
};

輸入?yún)?shù)傳入 execute(...)方法(可接受一個或多個參數(shù)): task.execute("第一個參數(shù)", "第二個參數(shù)", "……");
然后如筛,再把這些變量參數(shù)傳遞給 doInBackground(...)方法。

3.2.2 第二個參數(shù):進(jìn)度

第二個類型參數(shù)可指定發(fā)送進(jìn)度更新需要的類型抒抬。以下為示例代碼:

final ProgressBar gestationProgressBar = /* 一個特定的進(jìn)度條 */;
gestationProgressBar.setMax(42); /* 最大的進(jìn)度 */
AsyncTask<Void,Integer,Void> haveABaby = new AsyncTask<Void,Integer,Void>() {
    public Void doInBackground(Void... params) {
        while (!babyIsBorn()) {
            Integer weeksPassed = getNumberOfWeeksPassed();
          publishProgress(weeksPassed); // 關(guān)鍵杨刨,將參數(shù)發(fā)送到 onProgressUpdate
          patientlyWaitForBaby();
        } 
    }
    
    public void onProgressUpdate(Integer... params) {
        int progress = params[0];
        gestationProgressBar.setProgress(progress);
    } 
};
/* call when you want to execute the AsyncTask */
haveABaby.execute();

進(jìn)度更新通常發(fā)生在執(zhí)行的后臺進(jìn)程中。問題是擦剑,在后臺進(jìn)程中無法完成必要的 UI 更新妖胀。因此 AsyncTask 提供了 publishProgress(...)和 onProgressUpdate(...)方法。
其工作方式是這樣的 : 在后臺線程中 , 從 doInBackground(...) 方法中調(diào)用 publishProgress(...)方法惠勒。這樣 onProgressUpdate(...)方法便能夠在 UI 線程上調(diào)用赚抡。因此,在 onProgressUpdate(...)方法中執(zhí)行 UI 更新就可行了纠屋,但必須在 doInBackground(...) 方法中使用 publishProgress(...)方法對它們進(jìn)行管控涂臣。

3.2.3 第三個參數(shù):結(jié)果

第三個類型參數(shù)是處理結(jié)果返回的類型參數(shù)。下面是本章的示例代碼

// PhotoGalleryFragment.java

private class FetchItemsTask extends AsyncTask<Integer, Void, List<GalleryItem>> {
    @Override
    protected List<GalleryItem> doInBackground(Integer... params) {
        return new FlickrFetchr().fetchItems(params[0]);
    }

    @Override
    protected void onPostExecute(List<GalleryItem> galleryItems) {
        mItems = galleryItems;
        setAdapter();
    }
}

第三個參數(shù)就是在 doInBackground 中返回的結(jié)果巾遭,我們需要從后臺請求 API 返回的 JSON 數(shù)據(jù)肉康,然后將其格式化闯估,返回的就是我們需要的數(shù)據(jù)灼舍。

4. JSON 數(shù)據(jù)解析

什么是 JSON 數(shù)據(jù)呢?JSON(JavaScript Object Notation) 是一種輕量級的數(shù)據(jù)交換格式涨薪。易于人閱讀和編寫骑素。同時也易于機(jī)器解析和生成。它基于 JavaScript 的一個子集刚夺。JSON 采用完全獨立于語言的文本格式献丑,但是也使用了類似于C語言家族的習(xí)慣(包括C, C++, C#, Java, JavaScript, Perl, Python 等)。這些特性使 JSON 成為理想的數(shù)據(jù)交換語言侠姑。

JSON 對象是一系列包含在{ }中的名值對创橄。JSON 數(shù)組是包含在[ ]中用逗號隔開的 JSON 對象列表。對象彼此嵌套形成層級關(guān)系莽红。詳細(xì)的語法可以查看JSON 官網(wǎng)妥畏。

JSON 這種數(shù)據(jù)格式在同樣基于這些結(jié)構(gòu)的編程語言之間交換十分方便邦邦,所以網(wǎng)絡(luò)服務(wù)器端越來越多地開始用 JSON 來交換數(shù)據(jù),我們在這章使用的 API 同樣如此醉蚁。

一個例子

// 為節(jié)省版面燃辖,去掉了無關(guān)的屬性
{
  "photos": {
    "page": 1,
    "pages": 10,
    "photo": [
      {
        "id": "31987348504",
        "title": "Penny",
        "url_s": "https://farm3.staticflickr.com/2915/31987348504_9a949c482d_m.jpg",
      },
      {
        "id": "31987352214",
        "title": "",
        "url_s": "https://farm1.staticflickr.com/455/31987352214_58428f3a9d_m.jpg",
      }
    ]
  },
  "stat": "ok"
}

對應(yīng)的解析代碼:

// 解析時用 try…catch,要拋出 JSONException 防止程序崩潰
// JSONObject 構(gòu)造方法解析傳入的 JSON 數(shù)據(jù)后
// 會生成與原始 JSON 數(shù)據(jù)對應(yīng)的對象樹
JSONObject jsonBody = new JSONObject(jsonString);

// 頂層 JSONObject 對應(yīng)著原始數(shù)據(jù)最外層的{ }网棍。它包含了一個叫作 photos 的嵌套 JSONObject
JSONObject photosJsonObject = jsonBody.getJSONObject("photos");

// 這個嵌套對象又包含了一個叫作 photo 的 JSONArray
JSONArray photoJsonArray = photosJsonObject.getJSONArray("photo");

// 這個嵌套數(shù)組中又包含了一組 JSONObject
// 這些 JSONObeject 就是要獲取的一張張圖片的元數(shù)據(jù)
for (int i = 0; i < photoJsonArray.length(); i++) {
    JSONObject photoJsonObject = photoJsonArray.getJSONObject(i);
    GalleryItem item = new GalleryItem();
    item.setId(photoJsonObject.getString("id"));
    item.setCaption(photoJsonObject.getString("title"));
    if (!photoJsonObject.has("url_s")) {
        continue;
    }
    item.setUrl(photoJsonObject.getString("url_s"));
    items.add(item);
}

解析完成后就可以在 AsyncTask 的 onPostExecute 中對 UI 進(jìn)行更新了黔龟。

5. 挑戰(zhàn)練習(xí)

本章的挑戰(zhàn)練習(xí)難度依次遞增,考驗了我們很多知識滥玷。

5.1 使用 Gson 庫解析 JSON 數(shù)據(jù)

Gson 是 Google 官方推薦的 JSON 解析庫氏身,使用 Gson 不用寫任何解析代碼,它能自動將 JSON 數(shù)據(jù)映射為 Java 對象罗捎。

5.1.1 添加 Gson 依賴

在 File -> Project Structure -> Dependencies 中添加 gson 依賴

5.1.2 構(gòu)建對應(yīng)的 POJO 類

由于不想更改原本的 GalleryItem 類观谦,并且想讓成員變量的命名符合 java 的命名規(guī)范,我使用了 @SerializedName() 注解桨菜,這個注解注明了 Gson 在轉(zhuǎn)換時對應(yīng)的鍵名豁状。并且構(gòu)建了一個新的類,用于匹配對應(yīng)的 API 結(jié)構(gòu):

// PhotoBean.java

public class PhotoBean {

    public static final String STATUS_OK = "ok"
            , STATUS_FAILED = "fail";

    @SerializedName("photos")
    private PhotosInfo mPhotoInfo;
    @SerializedName("stat")
    private String mStatus;
    @SerializedName("message")
    private String mMessage;

    public class PhotosInfo {
        @SerializedName("photo")
        List<GalleryItem> mPhoto;

        public List<GalleryItem> getPhoto() {
            return mPhoto;
        }
    }
    // 省略 getter 和 setter
}

5.1.3 使用 Gson

Gson 的使用再簡單不過了倒得,與上面的代碼相比有云泥之別:

PhotoBean photoBean = (PhotoBean) new Gson()
        .fromJson(jsonString, PhotoBean.class);

不過記得要拋出 JsonSyntaxException泻红。

5.2 分頁顯示

這個挑戰(zhàn)的需求是:如果我們下滑最底部,就在后面添加下一頁的內(nèi)容霞掺。
所以在 url 的生成中我們還要加入 page 這個參數(shù)谊路。我加入了一個成員變量 mNextPage 用于記錄下次要請求的頁面, 然后添加了一個常量 MAX_PAGES 用于控制最大請求頁數(shù)菩彬。

5.2.1 RecyclerView.onScrollListener

onScrollListener 有兩個可以重寫的方法缠劝,一個是 onScrollStateChanged(),還有一個是 onScrolled骗灶,對我們這個需求來說惨恭,顯然 onScrollStateChanged 比較合適,ScrollState 也有三種:

  • SCROLL_STATE_IDLE: 視圖沒有被拖動耙旦,處于靜止
  • SCROLL_STATE_DRAGGING: 視圖正在拖動中
  • SCROLL_STATE_SETTLING: 視圖在慣性滾動

這個挑戰(zhàn)最關(guān)鍵的就是如何判斷滑到最底端脱羡。首先滑動到最底端時前兩個狀態(tài)其實都可以,但是滑動到最底這個信息只有 LayoutManager 知道免都,我們可以直接看代碼分析:

private RecyclerView.OnScrollListener onButtomListener = 
        new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        // 首先獲取 LayoutManager
        GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
        // 然后可以找到最后顯示的位置锉罐,一旦滾動就會獲取該位置
        mLastPosition = layoutManager.findLastCompletelyVisibleItemPosition();
        // 如果靜止的時候最后的位置大于等于數(shù)據(jù)個數(shù)
        // 而且前一個任務(wù)完成時(防止多次重復(fù))
        if (newState == RecyclerView.SCROLL_STATE_IDLE
                && mLastPosition >= mPhotoAdapter.getItemCount() - 1) {
            if (mFetchItemsTask.getStatus() == AsyncTask.Status.FINISHED) {
                // 下一頁加一,在小于最大頁數(shù)時
                // 彈出 Toast 表示正在加載
                // 然后打開一個新任務(wù)绕娘,加載下一頁
                mNextPage++;
                if (mNextPage <= MAX_PAGES) {
                    Toast.makeText(getActivity(), "waiting to load ……", Toast.LENGTH_SHORT).show();
                    // AsyncTask 只能執(zhí)行一次脓规,所以需要新建
                    mFetchItemsTask = new FetchItemsTask();
                    mFetchItemsTask.execute(mNextPage);
                } else {
                    // 滑到最底提示已經(jīng)到頭了
                    Toast.makeText(getActivity(), "This is the end!", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
};

5.2.2 添加數(shù)據(jù)并展示

我在 Adapter 中加入了一個 addData 方法,將新的數(shù)據(jù)加入到數(shù)據(jù)集中险领,然后使用 notifyDataSetChanged 方法更新視圖侨舆。

然后修改了 setAdapter 方法:

private void setAdapter() {
    if (isAdded()) {
        if (mPhotoAdapter == null) {
            mPhotoAdapter = new PhotoAdapter(mItems);
            mPhotoRecyclerView.setAdapter(mPhotoAdapter);
            mPhotoRecyclerView.addOnScrollListener(onButtomListener);
        } else {
            mPhotoAdapter.addData(mItems);
        }
    }
}

5.3 動態(tài)調(diào)整網(wǎng)格列

使用 OnGlobalLayoutListener 即可:

mPhotoRecyclerView.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        // 計算列數(shù)升酣,以 1080p 屏幕顯示3列為基準(zhǔn)
        int columns = mPhotoRecyclerView.getWidth() / 350;
        // 重新設(shè)置 LayoutManager、Adapter 和 Listener
        mPhotoRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), columns));
        mPhotoRecyclerView.setAdapter(mPhotoAdapter);
        mPhotoRecyclerView.addOnScrollListener(onButtomListener);
        // 滾動到之前看到的位置
        mPhotoRecyclerView.getLayoutManager().scrollToPosition(mLastPosition);
        //將 GlobalLayoutListener 去掉以避免多次觸發(fā)
        mPhotoRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }
});

GitHub Page: kniost.github.io
簡書:http://www.reibang.com/u/723da691aa42

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末态罪,一起剝皮案震驚了整個濱河市噩茄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌复颈,老刑警劉巖绩聘,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耗啦,居然都是意外死亡凿菩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進(jìn)店門帜讲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衅谷,“玉大人,你說我怎么就攤上這事似将』袂” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵在验,是天一觀的道長玷氏。 經(jīng)常有香客問我,道長腋舌,這世上最難降的妖魔是什么盏触? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮块饺,結(jié)果婚禮上赞辩,老公的妹妹穿的比我還像新娘。我一直安慰自己授艰,他們只是感情好辨嗽,可當(dāng)我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著想诅,像睡著了一般召庞。 火紅的嫁衣襯著肌膚如雪岛心。 梳的紋絲不亂的頭發(fā)上来破,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機(jī)與錄音忘古,去河邊找鬼徘禁。 笑死,一個胖子當(dāng)著我的面吹牛髓堪,可吹牛的內(nèi)容都是我干的送朱。 我是一名探鬼主播娘荡,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼驶沼!你這毒婦竟也來了炮沐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤回怜,失蹤者是張志新(化名)和其女友劉穎大年,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玉雾,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡翔试,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了复旬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垦缅。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖驹碍,靈堂內(nèi)的尸體忽然破棺而出壁涎,到底是詐尸還是另有隱情,我是刑警寧澤志秃,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布粹庞,位于F島的核電站,受9級特大地震影響洽损,放射性物質(zhì)發(fā)生泄漏庞溜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一碑定、第九天 我趴在偏房一處隱蔽的房頂上張望流码。 院中可真熱鬧,春花似錦延刘、人聲如沸漫试。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驾荣。三九已至,卻和暖如春普泡,著一層夾襖步出監(jiān)牢的瞬間播掷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工撼班, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留歧匈,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓砰嘁,卻偏偏與公主長得像件炉,于是被迫代替她去往敵國和親勘究。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,647評論 2 354

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,082評論 25 707
  • 在Android中我們可以通過Thread+Handler實現(xiàn)多線程通信斟冕,一種經(jīng)典的使用場景是:在新線程中進(jìn)行耗時...
    呂侯爺閱讀 2,051評論 2 23
  • 介紹自己負(fù)責(zé)的部分口糕,如何實現(xiàn)的。 自定義view viewGroup activity的啟動流程 事件傳遞及滑動沖...
    東經(jīng)315度閱讀 1,208評論 1 4
  • 山有天堂磕蛇,下有蘇杭走净,說的是自然之美。蘇堤春曉孤里,曲院風(fēng)荷伏伯,平湖秋月,斷橋殘雪捌袜,四時之景 不同说搅,造物者之無盡藏也。杭州...
    長河落日07閱讀 359評論 0 0
  • 市面上的保險產(chǎn)品五花八門,怎么理清思路霍衫,建立基本的保險觀念候引,了解自己的需求,又如何找對顧問買對保險呢敦跌? 1. 保險...
    陽光小院閱讀 368評論 0 1