健壯且可讀的安卓架構(gòu)設(shè)計(jì)
自接觸Android以來,我一直在尋找一種比較健壯的開發(fā)方法。譬如避免在UI線程進(jìn)行IO操作,防止重復(fù)的網(wǎng)絡(luò)請求,對重要數(shù)據(jù)進(jìn)行緩存并且準(zhǔn)確的更新這些緩存等等。當(dāng)然,代碼結(jié)構(gòu)也要保持盡量清晰。
本文并不是給你提供一個(gè)權(quán)威精準(zhǔn)的解決方案,更多的是去探討在靈活性、可讀性和健壯性之間有著很好平衡的App的一種開發(fā)方式。
一些現(xiàn)有的解決方案
在Android的初期版本,許多人處理多任務(wù)時(shí)會(huì)選擇 AsyncTask 。大體上來說,AsyncTask非常難用,許多文章也提到了它的問題。后來,Honeycomb(3.0)引入了可配置性更好的 Loaders。到了2012年,基于Android Service的開源項(xiàng)目Robospice問世,帶來了新的解決方案,這里介紹了 Robospice的工作原理。
Robospice 比起 AsyncTask 的確好太多了,但是依然存在一些問題。比如下面這段常見代碼,通過Robospice在Activity中發(fā)起一個(gè)請求的過程。你并不需要細(xì)讀,只要有個(gè)大概的概念就好:
- 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
- }
- });
然后是請求的具體代碼:
- 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;
- }
- }
存在的問題
你需要為每個(gè)請求都做上述的處理,代碼會(huì)顯得很臃腫:
- 對于你的每種請求你都需要繼承SpiceRequest寫一個(gè)特定的子類。
- 同樣的,對于每種請求你都需要實(shí)現(xiàn)一個(gè)RequestListener來監(jiān)聽。
- 如果你的緩存過期時(shí)間很短,用戶就需要花較長時(shí)間等待你的每個(gè)請求結(jié)束。
- RequestListener持有了Activity的隱式引用,那么是不是還需要內(nèi)存泄露的問題。
綜上,這并不是一個(gè)很好的解決方案。
五步,讓程序簡潔而健壯
在我開始開發(fā)Candyshop的時(shí)候,我嘗試了其他的方法。我試圖通過混合一些擁有有趣特性的庫來構(gòu)造一個(gè)簡單而健壯的解決方案。這是我用到的庫的列表:
* AndroidAnnotations用來處理后臺(tái)任務(wù),EBean等等……
* Spring RestTemplate用來處理 REST(含狀態(tài)傳輸)的網(wǎng)絡(luò)請求,這個(gè)庫和AndroidAnnotations配合的非常好。
* SnappyDB這個(gè)庫主要用來將一些 Java 對象緩存到本地文件中。
* EventBus 通過 Event Bus 來解耦處理 App 內(nèi)部組建間的通訊。
下圖就是我將要詳細(xì)講解的整體架構(gòu):
***步 一個(gè)易于使用的緩存系統(tǒng)
你肯定會(huì)需要一個(gè)持久化的緩存系統(tǒng),保持這個(gè)系統(tǒng)盡可能簡單。
- @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) { ... }
- }
第二步 一個(gè)符合REST的Client
這里我通過下面的例子來說明。記得要確保你使用 REST API 放在同一個(gè)地方。
- @Rest(rootUrl = "http://anything.com")
- public interface CandyshopApi {
- @Get("/api/contacts/")
- ContactsWrapper fetchContacts();
- @Get("/api/user/")
- User fetchUser();
- }
第三步 應(yīng)用級的事件總線(Event Bus)
在程序最初的時(shí)候就初始化Event bus對象,然后應(yīng)用的全局都可以訪問到這個(gè)對象。在Android中, Application初始化是一個(gè)很好的時(shí)機(jī)。
- public class CandyshopApplication extends Application {
- public final static EventBus BUS = new EventBus();
- ...
- }
第四步 處理那些需要數(shù)據(jù)的Activity
對于這一類的Activity,我的處理方式和Robospice非常類似,同樣是基于Service解決。不同的是,我的Service并不是Android提供的那個(gè),而是一個(gè)常規(guī)的單例對象。這個(gè)對象可以被App的各處訪問到,具體的代碼我們會(huì)在第五步進(jìn)行講解,在這一步,我們先看看這種處理Activity代碼結(jié)構(gòu)是怎么樣的。因?yàn)椋@一步可以看到的是我們簡化效果***烈的部分!
- @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);
- }
- }
一行代碼完成對用戶數(shù)據(jù)的請求,同樣也只需要一行代碼來解析請求所返回的數(shù)據(jù)。對于通訊錄等其他數(shù)據(jù)也可以用一樣的方式來處理,聽起來不錯(cuò)吧!
第五步——單例版的后臺(tái)服務(wù)
正如我在上一步說的那樣,這里使用的Service并不是Android提供的Service類。其實(shí),一開始的時(shí)候,我考慮使用Android提供的Services,不過***還是放棄了,原因還是為了簡化。因?yàn)?Android提供的Services通常情況下是為那些在沒有Activity展示情況下但還需要處理的操作提供服務(wù)的。另一種情況,你需要提供一些功能給其他的應(yīng)用。這其實(shí)和我的需求并不完全相符,而且用單例來處理我的后臺(tái)請求可以讓我避免使用復(fù)雜的借口,譬如:ServiceConnection,Binder等等……
這一部分可以探討的地方就多了。為了方便理解,我們從架構(gòu)切入展示當(dāng)Activity調(diào)用getUser()和getContacts()的時(shí)候究竟發(fā)生了什么。
你可以把下圖中每個(gè)serial當(dāng)作一個(gè)線程:
正如你所看到的,這是我非常喜歡的模式。大部分情況下用戶不需要等待,程序的視圖會(huì)立刻被緩存數(shù)據(jù)填充。然后,當(dāng)抓取到了服務(wù)端的***數(shù)據(jù),視圖數(shù)據(jù)會(huì)被新數(shù)據(jù)替代掉。與此對應(yīng)的是,你需要確保你的Activity
可以接受多次同樣類型的數(shù)據(jù)。在構(gòu)建Activity
的時(shí)候記住這一點(diǎn)就沒有任何問題啦。
下面是一些示例代碼:
- // 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);
- }
- }
似乎每個(gè)請求之中的代碼還是有點(diǎn)多!實(shí)際上,這是我為了更好說明才進(jìn)行了展開。不難發(fā)現(xiàn),這些請求都遵守了類似的模式,所以你可以很容易的構(gòu)造一個(gè) Helper 來簡化他們。比如 getUser()可以是這樣的:
- @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是用來做什么的? 讓我們看看文檔是怎么說的:
默認(rèn)情況下,所有@Background的匿名方法都是并行執(zhí)行的。但是如果兩個(gè)方法使用了同樣名字的serial則會(huì)順序運(yùn)行在同一個(gè)線程中,一個(gè)接著一個(gè)執(zhí)行。
雖然把網(wǎng)絡(luò)請求放在一個(gè)線程中順序執(zhí)行可能會(huì)導(dǎo)致性能下降,但是這使得“先POST然后GET獲得數(shù)據(jù)”的那類事務(wù)處理起來非常容易,這是個(gè)特性值得為此犧牲一些性能。退一步講,如果你真的發(fā)現(xiàn)性能不可接受,還是可以很容易使用多個(gè)serial來解決?,F(xiàn)在版本的Candyshop中,我同時(shí)使用了四個(gè)不同的serial。
總結(jié)
這里描述的解決方案是我?guī)讉€(gè)月前想到的很初級的一個(gè)想法。今天,我已經(jīng)解決掉所有遇到的特殊情況,并且非常享受在這樣的架構(gòu)下開發(fā)。當(dāng)然,這個(gè)方案 中還有一些很棒的東西我想要和大家分享,比如:錯(cuò)誤處理、緩存超時(shí)機(jī)制、POST請求、對無用操作的忽略,但是因?yàn)槠蜻@里我就不繼續(xù)講述了。
那么,你是否也找到了能讓你享受每天工作的框架?
原文鏈接: joanzap 翻譯:zerob13