博主這幾個星期都比較忙,剛入職新公司,有太多的東西需要去熟悉,比如公司的代碼,新的開發工具,博主已經棄用eclipse轉用AS了。不過發現AS這個玩意確實比eclipse好用,而且現在github上的一些項目已經是AS結構,所以建議同學們也開始轉用AS吧。
這篇文章是博主最近在學習一個新的開源框架snappyDB的時候發現的,覺得寫得很不錯,網上已經有了原文的翻譯,但這只是第一部分,原作者在去年十二月的時候續寫了第二部分,但是第二部分沒有翻譯,所以博主就先轉載第一部分在翻譯第二部分吧。這篇文章裏面介紹了一些很有用的開源框架。有興趣的同學可以去研究研究。博主最近都在研究這幾個東西,主要是snappyDB和AndroidAnnotations。轉載博主就不多做處理,不貼出原文地址了。
本文由 伯樂在線 - zerob13 翻譯自 joanzap。歡迎加入Android小組。轉載請參見文章末尾處的要求。
自接觸Android以來,我一直在尋找一種比較健壯的開發方法。譬如避免在UI線程進行IO操作,防止重複的網絡請求,對重要數據進行緩存並且準確的更新這些緩存等等。當然,代碼結構也要保持儘量清晰。
本文並不是給你提供一個權威精準的解決方案,更多的是去探討在靈活性、可讀性和健壯性之間有着很好平衡的App的一種開發方式。
一些現有的解決方案
在Android的初期版本,許多人處理多任務時會選擇 AsyncTask 。大體上來說,AsyncTask非常難用,許多文章也提到了它的問題。後來,Honeycomb(3.0)引入了可配置性更好的 Loaders。到了2012年,基於Android Service的開源項目Robospice問世,帶來了新的解決方案,這裏介紹了 Robospice的工作原理。
Robospice 比起 AsyncTask 的確好太多了,但是依然存在一些問題。比如下面這段常見代碼,通過Robospice在Activity
中發起一個請求的過程。你並不需要細讀,只要有個大概的概念就好:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
FollowersRequest
request = new FollowersRequest(user);
lastRequestCacheKey
= request.createCacheKey(); spiceManager.execute(request,
lastRequestCacheKey, DurationInMillis.ONE_MINUTE, new RequestListener<FollowerList>
{ @Override public void
onRequestFailure(SpiceException e) { //
On success } @Override public void
onRequestSuccess(FollowerList listFollowers) { //
On failure } }); |
然後是請求的具體代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class
FollowersRequest extends SpringAndroidSpiceRequest<FollowerList>
{ private String
user; public FollowersRequest(String
user) { super (FollowerList. class ); this .user
= user; } @Override public FollowerList
loadDataFromNetwork() throws Exception
{ String
url = format( "https://api.github.com/users/%s/followers" ,
user); return getRestTemplate().getForObject(url,
FollowerList. class ); } public String
createCacheKey() { return "followers."
+ user; } } |
存在的問題
- 你需要爲每個請求都做上述的處理,代碼會顯得很臃腫:
- 對於你的每種請求你都需要繼承SpiceRequest
寫一個特定的子類。
- 同樣的,對於每種請求你都需要實現一個RequestListener
來監聽。
- 如果你的緩存過期時間很短,用戶就需要花較長時間等待你的每個請求結束。
- RequestListener
持有了Activity
的隱式引用,那麼是不是還需要內存泄露的問題。
綜上,這並不是一個很好的解決方案。
五步,讓程序簡潔而健壯
在我開始開發Candyshop的時候,我嘗試了其他的方法。我試圖通過混合一些擁有有趣特性的庫來構造一個簡單而健壯的解決方案。這是我用到的庫的列表:
* AndroidAnnotations用來處理後臺任務,EBean等等……
* Spring RestTemplate用來處理 REST(含狀態傳輸)的網絡請求,這個庫和AndroidAnnotations配合的非常好。
* SnappyDB這個庫主要用來將一些 Java 對象緩存到本地文件中。
* EventBus 通過 Event Bus 來解耦處理 App 內部組建間的通訊。
下圖就是我將要詳細講解的整體架構:
第一步 一個易於使用的緩存系統
你肯定會需要一個持久化的緩存系統,保持這個系統儘可能簡單。
1
2
3
4
5
6
7
|
@EBean public class
Cache { public static
enum
CacheKey { USER, CONTACTS, ... } public <T>
T get(CacheKey key, Class<T> returnType) { ... } public void
put(CacheKey key, Object value) { ... } } |
第二步 一個符合REST的Client
這裏我通過下面的例子來說明。記得要確保你使用 REST API 放在同一個地方。
1
2
3
4
5
6
7
8
9
10
|
@Rest (rootUrl
= "http://anything.com" ) public interface
CandyshopApi { @Get ( "/api/contacts/" ) ContactsWrapper
fetchContacts(); @Get ( "/api/user/" ) User
fetchUser(); } |
第三步 應用級的事件總線(Event Bus)
在程序最初的時候就初始化Event bus對象,然後應用的全局都可以訪問到這個對象。在Android中,Application
初始化是一個很好的時機。
1
2
3
4
|
public class
CandyshopApplication extends Application
{ public final
static
EventBus BUS = new EventBus(); ... } |
第四步 處理那些需要數據的Activity
對於這一類的Activity
,我的處理方式和Robospice非常類似,同樣是基於Service
解決。不同的是,我的Service
並不是Android提供的那個,而是一個常規的單例對象。這個對象可以被App的各處訪問到,具體的代碼我們會在第五步進行講解,在這一步,我們先看看這種處理Activity
代碼結構是怎麼樣的。因爲,這一步可以看到的是我們簡化效果最強烈的部分!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
@EActivity (R.layout.activity_main) public class
MainActivity extends Activity
{ //
Inject the service @Bean protected
AppService appService; //
Once everything is loaded… @AfterViews public
void
afterViews() { //
… request the user and his contacts (returns immediately) appService.getUser(); appService.getContacts(); } /* The
result of the previous calls will come
as events through the EventBus. We'll
probably update the UI, so we need
to use @UiThread. */ @UiThread public
void
onEvent(UserFetchedEvent e) { ... } @UiThread public
void
onEvent(ContactsFetchedEvent e) { ... } //
Register the activity in the event bus when it starts @Override protected
void
onStart() { super .onStart(); BUS.register( this ); } //
Unregister it when it stops @Override protected
void
onStop() { super .onStop(); BUS.unregister( this ); } } |
一行代碼完成對用戶數據的請求,同樣也只需要一行代碼來解析請求所返回的數據。對於通訊錄等其他數據也可以用一樣的方式來處理,聽起來不錯吧!
第五步——單例版的後臺服務
正如我在上一步說的那樣,這裏使用的Service
並不是Android提供的Service類。其實,一開始的時候,我考慮使用Android提供的Services
,不過最後還是放棄了,原因還是爲了簡化。因爲
Android提供的Services
通常情況下是爲那些在沒有Activity
展示情況下但還需要處理的操作提供服務的。另一種情況,你需要提供一些功能給其他的應用。這其實和我的需求並不完全相符,而且用單例來處理我的後臺請求可以讓我避免使用複雜的藉口,譬如:ServiceConnection,Binder等等……
這一部分可以探討的地方就多了。爲了方便理解,我們從架構切入展示當Activity
調用getUser()
和getContacts()
的時候究竟發生了什麼。
你可以把下圖中每個serial
當作一個線程:
正如你所看到的,這是我非常喜歡的模式。大部分情況下用戶不需要等待,程序的視圖會立刻被緩存數據填充。然後,當抓取到了服務端的最新數據,視圖數據會被新數據替代掉。與此對應的是,你需要確保你的Activity
可以接受多次同樣類型的數據。在構建Activity
的時候記住這一點就沒有任何問題啦。
下面是一些示例代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
//
As I said, a simple class, with a singleton scope @EBean (scope
= EBean.Scope.Singleton) public class
AppService { //
(Explained later) public static
final
String NETWORK = "NETWORK" ; public static
final
String CACHE = "CACHE" ; //
Inject the cache (step 1) @Bean protected
Cache cache; //
Inject the rest client (step 2) @RestService protected
CandyshopApi candyshopApi; //
This is what the activity calls, it's public @Background (serial
= CACHE) public void
getContacts() { //
Try to load the existing cache ContactsFetchedEvent
cachedResult = cache.get(KEY_CONTACTS,
ContactsFetchedEvent. class ); //
If there's something in cache, send the event if (cachedResult
!= null )
BUS.post(cachedResult); //
Then load from server, asynchronously getContactsAsync(); } @Background (serial
= NETWORK) private void
getContactsAsync() { //
Fetch the contacts (network access) ContactsWrapper
contacts = candyshopApi.fetchContacts(); //
Create the resulting event ContactsFetchedEvent
event = new ContactsFetchedEvent(contacts); //
Store the event in cache (replace existing if any) cache.put(KEY_CONTACTS,
event); //
Post the event BUS.post(event); } } |
似乎每個請求之中的代碼還是有點多!實際上,這是我爲了更好說明才進行了展開。不難發現,這些請求都遵守了類似的模式,所以你可以很容易的構造一個 Helper 來簡化他們。比如 getUser()可以是這樣的:
1
2
3
4
5
6
7
8
9
10
|
@Background (serial
= CACHE) public void
getUser() { postIfPresent(KEY_USER,
UserFetchedEvent. class ); getUserAsync(); } @Background (serial
= NETWORK) private void
getUserAsync() { cacheThenPost(KEY_USER, new UserFetchedEvent(candyshopApi.fetchUser())); } |
那麼serial
是用來做什麼的? 讓我們看看文檔是怎麼說的:
> 默認情況下,所有@Background
的匿名方法都是並行執行的。但是如果兩個方法使用了同樣名字的serial
則會順序運行在同一個線程中,一個接着一個執行。
雖然把網絡請求放在一個線程中順序執行可能會導致性能下降,但是這使得“先POST然後GET獲得數據”的那類事務處理起來非常容易,這是個特性值得爲此犧牲一些性能。退一步講,如果你真的發現性能不可接受,還是可以很容易使用多個serial
來解決。現在版本的Candyshop中,我同時使用了四個不同的serial
。
總結
這裏描述的解決方案是我幾個月前想到的很初級的一個想法。今天,我已經解決掉所有遇到的特殊情況,並且非常享受在這樣的架構下開發。當然,這個方案中還有一些很棒的東西我想要和大家分享,比如:錯誤處理、緩存超時機制、POST請求、對無用操作的忽略,但是因爲篇幅原因這裏我就不繼續講述了。