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

标签: Android 开发 安卓开发 架构设计 | 发表时间:2014-04-30 00:00 | 作者:zerob13
出处:http://blog.jobbole.com

自接触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;
  }
}

存在的问题

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

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

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

五步,让程序简洁而健壮

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

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

article1_global_schema--2-

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

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

@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();
    ...
}
<pre class="brush: java; gutter: true; first-line: 1; highlight: []; html-script: false">

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

<pre class="brush: java; gutter: true; first-line: 1; highlight: []; html-script: false">
@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当作一个线程:

article1_serials--3-

正如你所看到的,这是我非常喜欢的模式。大部分情况下用户不需要等待,程序的视图会立刻被缓存数据填充。然后,当抓取到了服务端的最新数据,视图数据会被新数据替代掉。与此对应的是,你需要确保你的 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以来,我一直在寻找一种比较健壮的开发方法. 譬如避免在UI线程进行IO操作,防止重复的网络请求,对重要数据进行缓存并且准确的更新这些缓存等等. 当然,代码结构也要保持尽量清晰. 本文并不是给你提供一个权威精准的解决方案,更多的是去探讨在灵活性、可读性和健壮性之间有着很好平衡的App的一种开发方式.

软件架构设计

- - 企业架构 - ITeye博客
软件架构设计尚没有万灵的方法论支持,还是个非常新兴的行业,给出个人理解的行业软件架构设计过程,受个人水平有限,仅供参考:. 1.业务分析:针对目标行业的业务战略、蓝图、业务功能及流程进行分析,提出其中部分功能可以使用信息化进行处理,通过分析可以得出信息化要解决的问题. 2.解决方案设计:根据业务战略,形成行业信息化解决方案.

架构设计-逻辑层

- - 人月神话的BLOG
知乎看到一个问题,也是当前在软件设计开发中普遍存在的一个问题,如下:. 现在要开发一个业务逻辑比较复杂的项目,但是在网上看了设计模式的思想后感觉自己以前写的东西扩展性都不好,接口定义也不合适,都是一个实体类一个接口,项目施工也感觉不合理,感觉项目施工中应该先集中定义好接口,并完成业务逻辑,然后在具体实现接口,不知道这样想是不是正确.

秒杀架构设计

- - IT瘾-dev
最近在部门内部分享了原来在电商业务做秒杀活动的整体思路,大家对这次分享反馈还不错,所以我就简单整理了一下,分享给大家参考参考. 通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动. 比如说京东秒杀,就是一种定时定量秒杀,在规定的时间内,无论商品是否秒杀完毕,该场次的秒杀活动都会结束. 这种秒杀,对时间不是特别严格,只要下手快点,秒中的概率还是比较大的.

架构设计和概要设计

- - 人月神话的BLOG
初步再来探讨下架构设计和概要设计的区别和边界问题. 架构设计包括了功能性架构和技术架构设计两个部分的内容,功能性架构解决业务流程和功能问题,而技术架构解决非功能性需求等问题. 两种架构都包括了动态和静态两个方面的内容,对于功能性架构中动态部分为业务流程驱动全局用例,用例驱动的用例实现等;对于技术架构中动态部分为架构运行机制,而静态部分为框架,分层等方面的内容.

安卓用户体验团队制定的设计原则

- - 互联网的一些事-关注互联网产品管理,交流产品设计、用户体验心得
  这些设计准则由安卓用户体验团队提出以便于将用户兴趣铭记于心. 在实现创意及设计理念时应当考虑这些原则,除非另有目的,不应偏离.   漂亮的界面,精心设置的动画或者及时的音效是一种愉悦的体验. 微妙的效果使用户感觉轻松及拥有掌控感.    真实的对象比按钮和菜单更有趣.   允许用户直接触碰及操纵应用中的对象.

社区讨论:Android的架构设计

- - InfoQ cn
最近,开发者在知乎社区中就Android的架构设计展开了 讨论. 有人问“Android 架构设计的思想与原则是什么. 最近工作中遇到了Android中的权限问题,发现Android确实是开源的,但并不开放,比如权限控管就相当严格,限制做很多事情,这一点得益于Linux内核. 这也勾起来对其架构研究的兴趣,不知到哪位能够深度剖析下Android架构设计的思想与原则.

分层架构设计原则

- - 博客园_首页
通常一个软件系统都包含不同部分互相交互耦合,我们希望设计能够将系统划分为有意义的各个部件,各个部件能够独立的开发、演进、部署. 这时整体性的设计已经无法满足这些挑战,这就需要我们对系统进行合理清晰的划分. 通常我们为待开发的系统定义多个层次,每一层完成独立的功能. 1:系统分为多层,每层完成独立的功能,层内部继续细分子模块,每层能够独立演进、部署.

CDN架构设计及注意事项

- - ITeye博客
内容传输网络或内容分发网络(CDN)是一个包含数据副本的缓存系统,存在于网络中不同的节点以便可以最大化的利用网络来传输数据至客户端. 一个客户端访问离它最近节点的数据副本,而不是所有的客户端访问相同的中心服务器,因此避免了服务器瓶颈问题. CDN所缓存的内容类型包括web对象、可下载的对象(媒体文件、软件、文档)、应用程序和实时媒体流.

Solr与HBase架构设计 - aitanjupt

- - 博客园_首页
摘要:本篇是本人在做一个大数据项目. ,对于系统架构总结的一点想法,如何在保证存储量的情况下,又能保证数据的检索速度. 前提:      Solr、SolrCloud提供了一整套的数据检索方案,HBase提供了完善的大数据存储机制. 需求:      1、对于添加到HBase中的结构化数据,能够检索出来.