健壮且可读的安卓架构设计

    自接触Android以来,我一直在寻找一种比较健壮的开发方法。譬如避免在UI线程进行IO操作,防止重复的网络请求,对重要数据进行缓存并且准确的更新这些缓存等等。当然,代码结构也要保持尽量清晰。

    本文并不是给你提供一个权威精准的解决方案,更多的是去探讨在灵活性、可读性和健壮性之间有着很好平衡的App的一种开发方式。

    一些现有的解决方案

    在Android的初期版本,许多人处理多任务时会选择 AsyncTask 。大体上来说,AsyncTask非常难用,许多文章也提到了它的问题。后来,Honeycomb(3.0)引入了可配置性更好的 Loaders。到了2012年,基于Android Service的开源项目Robospice问世,带来了新的解决方案,这里介绍了 Robospice的工作原理。

    Robospice 比起 AsyncTask 的确好太多了,但是依然存在一些问题。比如下面这段常见代码,通过Robospice在Activity中发起一个请求的过程。你并不需要细读,只要有个大概的概念就好:

    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;
          }
    }
    

    存在的问题

    你需要为每个请求都做上述的处理,代码会显得很臃肿:

    • 对于你的每种请求你都需要继承SpiceRequest写一个特定的子类。
    • 同样的,对于每种请求你都需要实现一个RequestListener来监听。
    • 如果你的缓存过期时间很短,用户就需要花较长时间等待你的每个请求结束。
    • RequestListener持有了Activity的隐式引用,那么是不是还需要内存泄露的问题。

    综上,这并不是一个很好的解决方案。

    五步,让程序简洁而健壮

    在我开始开发Candyshop的时候,我尝试了其他的方法。我试图通过混合一些拥有有趣特性的库来构造一个简单而健壮的解决方案。这是我用到的库的列表:
    * AndroidAnnotations用来处理后台任务EBean等等……
    * Spring RestTemplate用来处理 REST(含状态传输)的网络请求,这个库和AndroidAnnotations配合的非常好。
    * SnappyDB这个库主要用来将一些 Java 对象缓存到本地文件中。
    * EventBus 通过 Event Bus 来解耦处理 App 内部组建间的通讯。

    下图就是我将要详细讲解的整体架构:

    3396ec8c7d8935f0a5e18b33ea932cde

    第一步 一个易于使用的缓存系统

    你肯定会需要一个持久化的缓存系统,保持这个系统尽可能简单。

    @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 放在同一个地方。

    @Rest(rootUrl = "http://anything.com")
    public interface CandyshopApi {
         @Get("/api/contacts/")
         ContactsWrapper fetchContacts();
    
         @Get("/api/user/")
         User fetchUser();
    }
    

    第三步 应用级的事件总线(Event Bus)

    在程序最初的时候就初始化Event bus对象,然后应用的全局都可以访问到这个对象。在Android中,Application初始化是一个很好的时机。

    public class CandyshopApplication extends Application {
          public final static EventBus BUS = new EventBus();
           ...
    }
    

    第四步 处理那些需要数据的Activity

    对于这一类的Activity,我的处理方式和Robospice非常类似,同样是基于Service解决。不同的是,我的Service并不是Android提供的那个,而是一个常规的单例对象。这个对象可以被App的各处访问到,具体的代码我们会在第五步进行讲解,在这一步,我们先看看这种处理Activity代码结构是怎么样的。因为,这一步可以看到的是我们简化效果最强烈的部分!

    @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当作一个线程:

    3fd3b0fb9f29378a5a2181e62937be30

    正如你所看到的,这是我非常喜欢的模式。大部分情况下用户不需要等待,程序的视图会立刻被缓存数据填充。然后,当抓取到了服务端的最新数据,视图数据会被新数据替代掉。与此对应的是,你需要确保你的Activity可以接受多次同样类型的数据。在构建Activity的时候记住这一点就没有任何问题啦。
    下面是一些示例代码:

    
    // 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()可以是这样的:

    @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请求、对无用操作的忽略,但是因为篇幅原因这里我就不继续讲述了。

    那么,你是否也找到了能让你享受每天工作的框架?

    转载请注明:Android开发中文站 » 健壮且可读的安卓架构设计

    本博客所有文章如无特别注明均为原创。
    复制或转载请以超链接形式注明转自Who am I ?,原文地址《健壮且可读的安卓架构设计
    标签:
    喜欢 | 0
    分享:

已经有1 条评论抢在你前面了~

  1. 沙发
    Allie 2017-04-24

    That inshgit would have saved us a lot of effort early on.