本章介紹Android網(wǎng)絡(luò)底層的封裝。很多公司、很多團隊都只是把網(wǎng)絡(luò)底層封裝成一個好用的方法印衔,而我接下來要介紹的內(nèi)容將覆蓋的范圍很廣:
- 拋棄AsyncTask,自定義一套網(wǎng)絡(luò)底層的封裝框架姥敛。
- 設(shè)計一套App緩存策略。
- 設(shè)計一套MockService的機制瞎暑,在沒有MobileAPI的時候彤敛,也能假裝獲取到了網(wǎng)絡(luò)返回的數(shù)據(jù)。
- 封裝了用戶Cookie的邏輯了赌。
2.1 使用原生的ThreadPoolExecutor+Runnable+Handler
先說說AsyncTask的致命缺點:
那就是不能靈活控制其內(nèi)部的線程池墨榄,線程池里面的每個線程存放的都是MobileAPI的調(diào)用請求,而AsyncTask中又沒有暴露出取消這些請求的方法勿她,也就是我們熟知的CancelRequest方法袄秩,所以,一旦從A頁面跳轉(zhuǎn)到B頁面逢并,那么在A頁面發(fā)起的MobileAPI請求之剧,如果還沒有返回,并不會被取消砍聊。
圖中只列出了8個背稼,還有1個RemoteService類,位于YoungHeart項目的engine包中玻蝌。下面分別介紹蟹肘。
2.1.1 網(wǎng)絡(luò)請求的格式
1. Request格式
- GET:http://www.xxx.com/aaaa.api?k1=va&k2=v2
- POST:對于POST,我們將key-value這樣的鍵值對存放在Form表單中俯树,進行提交帘腹。
2. Response格式
JSON數(shù)據(jù)格式1:
{ "code" : 1, "message" : "網(wǎng)絡(luò)異常", "result" : ""}
可以定義code為0為成功,其他為異常情況许饿。
3. UrlConfigManager和URLData
我們把App所要調(diào)用的所有MobileAPI接口的信息都放在url.xml文件中阳欲,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<url>
<Node
Key="getWeatherInfo"
Expires="300"
NetType="get"
Url="http://www.weather.com.cn/data/sk/101010100.html" />
<Node
Key="login"
Expires="0"
NetType="post"
Url="http://www.weather.com.cn/data/login.api" />
</url>
在使用上,通過UrlConfigManager的findURL方法,在上述xml文件中找到當(dāng)前MobileAPI調(diào)用的節(jié)點胸完,其中每一個MobileAPI接口都對應(yīng)一個URLData實體书释,如下所示:
public class URLData {
private String key;
private long expires;
private String netType;
private String url;
}
4. RemoteService和RequestCallback、RequestParameter
- RequestCallback是回調(diào)赊窥,目前有onSuccess和onFail兩種爆惧。
- RequestParameter是用來傳遞調(diào)用MobileAPI接口所需參數(shù)的鍵值對的。我們原本可以使用HashMap<String,String>這樣的數(shù)據(jù)結(jié)構(gòu)锨能,但是HashMap比較耗費內(nèi)存扯再,雖然它的查找速度是o(1),而對于MobileAPI接口的參數(shù)而言址遇,數(shù)據(jù)一般不會太多熄阻,查找速度快體現(xiàn)不出優(yōu)勢來,所以我們使用ArrayList<RequestParameter>這樣的數(shù)據(jù)結(jié)構(gòu)倔约。
- RemoteService這個單例是用來發(fā)起請求的秃殉,它會創(chuàng)建一個request,并將其添加到RequestManager中浸剩,然后放到DefaultThreadPool的一個線程中去執(zhí)行這個request钾军。
5. RequestManager
RequestManager這個集合類是用于取消請求(cancelRe-quest)的。因為每次發(fā)起請求绢要,都會把為此創(chuàng)建的request添加到RequestManager中吏恭,所以RequestManager保存了全部re-quest。
6. DefaultThreadPool
DefaultThreadPool只是對ThreadPoolExecutor和Array-BlockingQueue的簡單封裝重罪。我們可以認為它就是一個線程池樱哼,每發(fā)起一次請求(runnable),就由線程池分配一個新的線程來執(zhí)行該請求剿配。
7. HttpRequest
HttpRequest是發(fā)起Http請求的地方搅幅,它實現(xiàn)了Runnable,從而讓DefaultThreadPool可以分配新的線程來執(zhí)行它惨篱,所以盏筐,所有的請求邏輯都在Runnable接口的run方法中,其中:
- 對于get形式的MobileAPI接口砸讳,它會把從上層傳遞進來的ArrayList<RequestParameter>琢融,解析為urlk1=v1&k2=v2這樣的形式。
- 對于post格式的MobileAPI接口簿寂,它會把從上層傳遞進來的ArrayList<RequestParameter>漾抬,轉(zhuǎn)為BasicNameVal-uePair的形式,放到表單中進行提交常遂。
2.1.2 網(wǎng)絡(luò)底層的一些優(yōu)化工作
我們的網(wǎng)絡(luò)底層越來越強大了纳令,是否有意猶未盡的感受?接下來將完善這個框架,修復(fù)其中的一些瑕疵平绩,如onFail的統(tǒng)一處理機制圈匆、UrlConfigManager的優(yōu)化、ProgressBar的處理等捏雌。
1. onFail的統(tǒng)一處理機制
統(tǒng)一處理異常跃赚,Toast提示或者對話框
2. UrlConfigManager的優(yōu)化
在App啟動時,一次性將url.xml文件都讀取到內(nèi)存性湿,把所有的UrlData實體保存在一個集合中纬傲,然后每次調(diào)用MobileAPI接口,直接從內(nèi)存的這個集合中查找肤频√纠ǎ考慮到內(nèi)存中的數(shù)據(jù)會被回收,所以上述這個集合一旦為空宵荒,我們要從url.xml中再次讀取汁雷。
3. 不是每個請求都需要回調(diào)的
2.2 App數(shù)據(jù)緩存設(shè)計
- 對于App而言,它是感受不到取的是緩存數(shù)據(jù)還是調(diào)用MobileAPI报咳。具體工作由網(wǎng)絡(luò)底層完成摔竿。
- 在url.xml中為每一個MobileAPI接口配置緩存時間Ex-pired。對于post少孝,一律設(shè)置為0,因為post不需要緩存熬苍。
- 在HttpRequest類中的run方法中稍走,改動3個地方:
- 寫一個排序算法sortKeys,對URL中的key進行排序柴底。
- 將newUrl作為key婿脸,檢查緩存中是否有數(shù)據(jù),有則直接返回柄驻;否則狐树,繼續(xù)調(diào)用MobileAPI接口。
- MobileAPI接口返回數(shù)據(jù)后鸿脓,將數(shù)據(jù)存入緩存抑钟。
- CacheManager用于操作讀寫緩存數(shù)據(jù),并判斷緩存數(shù)據(jù)是否過期野哭。緩存中存放的實體就是CacheItem在塔。
- 在App項目中,創(chuàng)建YoungHeartApplication這個Ap-plication級別的類拨黔,在程序啟動時蛔溃,初始化緩存的目錄,如果不存在則創(chuàng)建之。
2.3 強制更新
- 如果對于某個接口的數(shù)據(jù)贺待,MobileAPI緩存了5分鐘徽曲,App緩存了3分鐘,那么最極端的情況是麸塞,用戶在8分鐘內(nèi)是看不到數(shù)據(jù)更新的秃臣。因此,我們需要在頁面上提供一個強制更新的按鈕喘垂。
- 我們可以讓RemoteService多暴露一個boolean類型的參數(shù)甜刻,用于判斷是否要遵守App端緩存策略,如果是正勒,則在從url.xml中取出UrlData實體后得院,將其expired強制設(shè)置為0,這樣就不會執(zhí)行緩存策略了章贞。
- 數(shù)據(jù)緩存是一把雙刃劍祥绞,設(shè)置時間長了,數(shù)據(jù)長期不更新鸭限,用戶體驗就會不好蜕径。因此我們需要為那些強迫癥類型的用戶提供一個強制刷新的按鈕,點擊按鈕后败京,頁面會重新調(diào)用MobileAPI加載數(shù)據(jù)兜喻,無論緩存是否到期。
2.4 MockService
在App團隊與MobileAPI團隊協(xié)同開發(fā)的過程中赡麦,經(jīng)常會遇到因為MobileAPI接口還沒好而App又急等著用的情況朴皆。
設(shè)計App端MockService包括如下幾個關(guān)鍵點:
- 對需要Mock數(shù)據(jù)的MobileAPI接口,通過在url.xml中配置Node節(jié)點MockClass屬性泛粹,來指定要使用那個Mock子類生成的數(shù)據(jù):
<Node
Key="getWeatherInfo"
Expires="300"
NetType="get"
MockClass="com.youngheart.mockdata.MockWeatherInfo"
Url="http://www.weather.com.cn/data/sk/101010100.html" />
這里將使用com.mockdata.mockdata包下的MockWeath-erInfo子類來解析遂铡。
- 我使用了反射工廠來設(shè)計MockService。MockService類是基類晶姊,它有一個抽象方法getJsonData扒接,用于返回手動生成的Mock數(shù)據(jù)。
- 接下來介紹如何實現(xiàn)反射機制们衙。
- 主要的改造工作在RemoteService類的invoke方法中钾怔,根據(jù)是否在url.xml中指定了MockClass值來決定,是調(diào)用線上MobileAPI還是從本地MockService直接取假數(shù)據(jù)砍艾。
- 如果MockClass有值蒂教,就把這個值反射為一個具體的類,比如MockWeatherInfo脆荷,然后調(diào)用它的getJsonData方法凝垛。
- 有了MockService這個利器懊悯,對于作者來說,本書接下來的內(nèi)容將會輕松很多梦皮,因為不需要搭建自己的服務(wù)器炭分,全都用MockService在本地編寫假數(shù)據(jù)即可。
2.5 用戶登錄
首先剑肯,貫穿App的捧毛,應(yīng)該有一個User全局變量,在每次登錄成功后让网,會將其isLogin屬性設(shè)置為true呀忧,在退出登錄后,則將該屬性設(shè)置為false溃睹。這個User全局變量要支持序列化到本地的功能而账,這樣數(shù)據(jù)才不會因內(nèi)存回收而丟失。
其次因篇,登錄分為3種情形:
- 點擊登錄按鈕泞辐,進入登錄頁面LoginActivity,登錄成功后竞滓,直接進入個人中心PersonCenterActivity咐吼。這種情況最直截了當(dāng),一路執(zhí)行startActivity(intent)就能達到目的商佑。
- 在頁面A锯茄,想要跳轉(zhuǎn)到頁面B,并攜帶一些參數(shù)茶没,卻發(fā)現(xiàn)沒有登錄撇吞,于是先跳轉(zhuǎn)到登錄頁,登錄成功后礁叔,再跳轉(zhuǎn)到B頁面,同時仍然帶著那些參數(shù)迄薄。
- 在頁面A琅关,執(zhí)行某個操作,卻發(fā)現(xiàn)沒有登錄讥蔽,于是跳轉(zhuǎn)到登錄頁涣易,登錄成功后,再回到頁面A冶伞,繼續(xù)執(zhí)行該操作新症。
這里,我的操作是在父類寫一個startActivityForLogin响禽,入?yún)閕ntent
2.6 自動登錄
- 我們將cookie取出來徒爹,不用關(guān)心它是什么荚醒,只要把它存放在本地文件中即可。每次發(fā)起MobileAPI請求時隆嗅,都要把本地保存的Cookie取出來界阁,放到HttpRequest的header中。
- SD卡以及內(nèi)存中一份用戶信息
判斷用戶是否過期需要服務(wù)器返回標(biāo)識
2.7 HTTP頭中的奧妙
對于HTTP頭胖喳,我們并不陌生泡躯。我們在上一節(jié)中成功運用到了HTTP頭中的Cookie屬性。接下來丽焊,我們將繼續(xù)發(fā)揮它的威力较剃,看看它還能為我們做些什么。我們先學(xué)習(xí)一下HTTP請求的定義技健。
2.7.1 HTTP請求
HTTP請求分為HTTPRequest和HTTPResponse兩種写穴。但無論哪種請求,都由header和body兩部分組成凫乖。
- HTTP Body:Body部分就是存放數(shù)據(jù)的地方
- HTTP Header:它由很多鍵值對(key-value)組成确垫,其中有些key是標(biāo)準(zhǔn)的,兼容于各大瀏覽器帽芽,比如:
- accept
- accept-language
- referrer
- user-agent
- accept-encoding
我們還可以在MobileAPI端自定義一些鍵值對删掀,然后要求App在調(diào)用MobileAPI時把這些信息傳遞過來。比如MobileAPI可以定義一個check-value這樣的key导街,然后要求App將AppId(同一公司的不同App編號)披泪、ClientType(Android還是iPhone、iPad)這些值拼接在一起經(jīng)過MD5加密后搬瑰,作為這個key的值傳遞給MobileAPI款票,然后由MobileAPI再去分析這些數(shù)據(jù)。
2.7.2 時間校準(zhǔn)
- 對于手機系統(tǒng)時間不準(zhǔn)的問題泽论,本文給出了比較好的解決方案艾少,即通過每次調(diào)用MobileAPI來計算時間差,然后每次本地獲取時間就加上這個時間差翼悴。
- 對于用戶身處不同時區(qū)的問題缚够,App仍然返回同一個時間,只是要在App上注明這些時間都是北京時間鹦赎,而不能是北京用戶顯示飛機9點起飛而日本用戶顯示10點起飛谍椅。另一方面,這兩個時區(qū)的用戶在一起聊天是個麻煩的事情古话,即使有人在日本時區(qū)10點說句話雏吭,對于北京用戶而言,看到的也應(yīng)該是9點發(fā)的消息陪踩,反之亦然杖们。而服務(wù)器則要使用格林威治一套時間悉抵,具體怎么顯示,那是App的事情胀莹。有些App就存在這樣的bug基跑,出國旅游收不到即時聊天消息,到了晚上會莫名其妙冒出來幾百條消息描焰,就是因為這個時區(qū)問題沒有處理好導(dǎo)致的媳否。
2.7.3 開啟gzip壓縮
接下來要介紹的內(nèi)容和gzip有關(guān)。HTTP協(xié)議上的gzip編碼是一種用來改進Web應(yīng)用程序性能的技術(shù)荆秦。大流量的Web站點常常使用gzip壓縮技術(shù)來減少傳輸量的大小篱竭,減少傳輸量大小有兩個明顯的好處,一是可以減少存儲空間步绸,二是通過網(wǎng)絡(luò)傳輸時掺逼,可以減少傳輸?shù)臅r間。
2.8 本章小結(jié)
本章介紹如何對網(wǎng)絡(luò)底層進行封裝瓤介,其中包括:新寫了一個網(wǎng)絡(luò)調(diào)用框架用以代替AsyncTask吕喘;設(shè)計了App的緩存機制;設(shè)計了MockService的機制刑桑,以后即使沒有MobileAPI接口也能開發(fā)新功能了氯质。介紹用戶Cookie的設(shè)計方法;巧妙運用Http頭中的數(shù)據(jù)等祠斧。