基于HTTP缓存轻松实现客户端应用的离线支持及网络优化

常规的客户端应用开发实践中,为了支持离线特性,往往需要引入本地数据存储并增加相应的『离线状态』逻辑分支。本地存储的大量使用对数据结构的前后向兼容设计提出了很高的要求,一旦考虑不足,往往不得不引入复杂的版本间数据升降级处理,进一步加剧开发和维护成本。而且针对『离线』与『在线』状态这两条并行的处理分支,对业务逻辑的清晰性和可维护性有一定的破坏,常常容易在后续开发中造成处理遗漏,给测试和维护带来更多的痛苦。

在此前的一个客户端开发项目中,我们另辟蹊径的借助HTTP协议层的缓存机制(Cache-Control),实现了一个简洁高效的离线支撑框架。一般HTTP缓存运用在客户端开发中大多是应对图片等静态资源的缓存,而我们更进一步将API也纳入缓存管理的模式。相比上述传统思路,它具有以下独到的优势:

  • 基本消除了离线相关的业务数据存储需求,免除了考虑数据结构前后向兼容性及版本间数据升降级处理的痛苦。
  • 大幅度减少了离线特性对现有业务实现的侵入性,只要API接口设计得当,现有特性实现只需要作微小的调整即可直接支持离线。
  • 在网络状况不佳的情况下,提供无缝的用户体验。(优先显示缓存内容,异步刷新)
  • 同时也能优化在线状态下的网络传输,减少不必要的重复网络请求。

基本的实现思路是,在API client层透明的管理所有API请求,对于占绝大部分比例的GET类(不影响业务状态的)API,根据当前网络状态智能的协调真正的网络请求与缓存的响应,实现对业务处理层基本透明的离线状态应对。这样,业务处理层的代码只需按照在线状态下的场景实现相应的业务逻辑,即可同时支持离线的场景。

下面以几个典型的业务场景为例说明这个框架的工作方式:

1. 用户信息的离线展现

假定业务后端提供了获取用户信息的API:『/api/v1/profile』,客户端启动后,会通过这个API获取用户的基本信息(如用户名、头像、积分等),并展现在主界面中。当启动主界面时,客户端发起API调用,API client首先判断本地是否已缓存了此前该API的响应,如果已有缓存则直接返回缓存的响应。这个缓存响应对业务处理层的代码而言,跟一个正常的服务端响应基本没有差别,因此只需当作在线状态处理即可。

在返回缓存响应的同时,API client还会根据当前的网络可用性及数据时效性,决定是否发起一次异步的重新请求。在这个例子中,我们可以为Profile API配置一个默认的时效期,比如『10分钟』。如果缓存的响应数据尚在时效期内,则不再发出这一额外的异步请求。这个策略有助于减少在线状态下客户端的重复请求频率,降低流量浪费。基于这一策略,我们在业务实现中完全抛弃了Profile数据的本地存储,简化了实现流程,在每次界面展现时均发起API调用,获取实际数据,让HTTP缓存同时充当业务数据存储的角色。这样就完全不必担心本地存储的数据结构前后向兼容和升降级问题了,而服务端的API接口URL协议中含有的API版本部分(如『/api/v2/profile』)确保了不同版本API响应的隔离。

时效的引入必然涉及到数据的一致性风险,因此除了根据业务场景合理为不同API设置各自的时效期外,客户端的业务逻辑中还需要在进行了显性影响此数据的操作后,使用『强制网络请求』的方式忽略缓存发起API调用。典型的场景如用户修改了头像或昵称后的个人信息刷新。

另一个应对数据时效性延滞的策略是采用服务端主动push数据变化的方式:为API设置一个较长的默认时效期(比如1小时),当数据发生变化时,服务端主动push一次响应。为了兼容API client使用的HTTP缓存机制,可以采用前端开发中比较成熟的『长连接挂起响应body』的push实现方式。

以上是最为常见且相对简单的离线需求场景,下面再以一个稍微复杂的例子说明这个框架的高级用法。

2. 可翻页清单的离线浏览

这是一个相对比较复杂的场景,『可翻页』意味着相关的清单数据具有关联延续性,比如搜索结果页、消息收件箱。支持此类数据的无缝离线体验,对API的设计会有一定的要求,才能确保URL在不同起点或页长下的一致性,使缓存能正确发挥作用。(其实这也是HTTP协议中URL结构的一个最初约束——『resource path』,只不过发展到今天,很多Web应用的URL规划早已忽视了这些基本原则)

搜索结果页和消息收件箱分别代表了两种不同的翻页需求场景,前者是起点固定,向后延续;而后者是终点固定,起点浮动。(假定清单的相对顺序在短期内不变)

(1) 搜索结果页

先说搜索结果页。确保URL一致性的最简单办法是固定页长(由服务端控制),URL中传递页码,例如:『/search/iphone+5/page/2』。这样,就可以保持离线搜索时的URL一致性。

这时需要解决的另一个关键问题是缓存的连带失效。对于没有关联延续性的单一页面,可以直接通过失效期和覆盖缓存的方式控制失效,但引入关联延续性之后,就需要连带失效多个关联页面的缓存了。比如在重新搜索相同的关键字后,原先缓存的后续几页就必须连带失效,以避免出现类似『新的第一页+老的第二页』所导致的清单内容混乱。

解决这个问题的方式有很多,在经过广泛的研究后,我们选择了使用Vary+特殊header的策略。这个header在每次刷新第一页时由客户端重新生成一个随机的token,并在连续的翻页期间保持不变。这个token的作用相当于一个session标识,借助HTTP协议的Vary header确保不同session的页面自动失效。这个实现方式可以很好的兼容HTTP协议的标识实现,而且对服务端没有特别的开发需求。

(2)信息收件箱

信息收件箱相比搜索结果页的复杂性在于,每次浏览的起点可能不固定(假定我们以常见的时间倒序方式浏览),但已缓存的清单条目具有相对不变性(那些旧消息)。如果我们仍然采用URL中传递页码的策略,那么就可能出现刷新后因新条目增加而顺延现有条目所造成的『页面错位』,倘若简单粗暴的连带失效后续页面,就太浪费实际上可缓存的不变内容了。

在这样的场景下,设计一个可充分利用缓存的API URL具有相当高的挑战。在经过多次尝试和摸索之后,我们最终选择了『等间隔区间』读取的API URL策略,形如:『/api/messages?last_id=120』这里的『last_id』是以20为间隔的最近区间终点ID,服务端返回ID在120之上的最多20条消息(比如121~126)。如果用户向后翻页,则发起的API请求为『/api/messages?last_id=100』,此时服务端返回ID从101~120的20条消息。首次请求时,可以不携带『last_id』参数,而第一次翻页时取首页ID范围内为20整倍数的ID作为这次请求的『last_id』。例如首页获取的ID范围为『117~126』,则第一次翻页时请求『last_id=120』。为了优化最终用户的体验,实际显示在UI中的消息清单仍是以最新消息开始的每20条分页,比如此例中的『126~117』、『116~107』,UI逻辑层对偏移映射进行了的包装。

3. 具体实现层面

Android和iOS下均有直接可用的成熟框架支持HTTP Cache机制。iOS的NSURLCache从2.0开始就提供给开发者,而Android的HttpResponseCache要到4.0版本才能直接使用。不过开源社区已经有其back-port项目,可以运用在Android 2.x版本中。

需要特别一提的是,Android虽然从4.0版本开始提供了HttpResponseCache,但其中有一个对IO性能影响较大的问题,直到4.2版本才得以解决。因此,建议运行在4.2之前的版本上时,仍旧使用开源社区的back-port(已包含了解决上述性能问题的补丁)。

业务框架层只需少量的工作就可以将其集成到现有的API library中,考虑到不同的API library接口设计,可能需要引入适当的调整以支持『优先缓存、异步请求』的机制。以我们Android ApiClient的部分片段为例:

	switch (cache_policy) {
	case NeverFromCache:
		connection.addRequestProperty("Cache-Control", "no-cache");
		break;
	case OnlyFromCache:
		connection.addRequestProperty("Cache-Control", "only-if-cached, max-stale=" + KMaxStale);
		break;
	case Default:	// Controlled by server response header
		break;
	}

这是直接指挥HTTP cache的部分,其中的三种策略(NeverFromCache、OnlyFromCache和Default)需要结合业务场景作出区分选择。

通常的原则,我们应当将API的缓存策略交由服务端根据业务需求确定,这时直接使用『Default』即可,减少客户端对业务变化的依赖。对于服务端而言,可以为不同的API指定不同的缓存策略,分别通过『Cache-Control』header指定:

不允许客户端缓存:『Cache-Control: no-cache』
在指定时效内缓存:『Cache-Control: max-age=3600』 (1小时内有效)

注:这里服务端指定的是『在线』时的缓存时效策略,影响的是客户端在主动失效缓存前可以不必请求新数据而直接使用缓存的时限。此处不必担心离线条件下超出时效的数据不可用,因为客户端可以通过前述的『max-stale』在『max-age』基础上延长时效性。(通常客户端可将『max-stale』设置的足够大以保证缓存的数据始终可用。

如果只是实现简单的离线支持,不考虑在线期间的缓存省流,那么服务端并不需要作任何调整,客户端的相应逻辑也很简单:

	cache_policy = is_offline ? OnlyFromCache : Default

但如果App中包含有显式展示最新状态的界面(陈旧或缓存的信息可能影响用户判断)时,则需要使用『NeverFromCache』。

除了上面提到的特殊场景外,在上层的业务代码中一般大部分的业务需求均不必涉及到cache策略的选择,往往只需为离线状态增加一些全局性的体验优化即可(如无缓存时的友好提示)。

基于Fragment的Android前台服务框架

从Android 3.0开始,Google引入了全新的Fragment UI体系,重新诠释了可复用可延展的Android UI设计理念。Android Support Library更是为任何面向低版本Android的应用开发者提供了完整的Fragment后向兼容方案(backport)。所以,如果开发一款新的Android应用,使用Fragment已无需有任何顾忌。尽早拥抱这一强大的机制设计,可以帮你省下可观的开发和维护工作量。

说起Service框架,大家可能已经比较熟悉,但将其与Fragment联系在一起,就多少有些让人觉得诧异了。我们不妨先来看看Android现有的标准Service框架,一般也称之为后台服务。官方文档中的定义是:一个可在后台执行长时间操作,不提供UI的应用组件。Service的主要特点是生命周期与应用的UI独立,不随应用退出而结束。后台服务的典型用途主要有两大类:执行不随应用切换而打断的任务(如下载、播放)或监听和响应系统事件(如来电、位置)。但在实际开发中,Service的实现复杂度并不低,一方面需要考虑并处理服务的生命周期,另一方面还要痛苦的处理服务与UI间的通信,倘若需要在服务代码中与用户交互,要么使用相当受限的Toast和Notification机制,要么实现一个复杂的UI回调……

实际上,在大部分的应用场景中,很多与UI相关的处理(即MVC中的Controller)也有类似后台服务一样的跨界面复用和共享需求,它们同时也与UI有着密切的联系,而且仅在应用打开时发挥作用,例如账户的全局状态、未读的通知消息、购物车等。这种需求我们一般称之为前台服务(Foreground Service)。过去,一部分的这种需求往往采用SharedPreference的方式在不同的界面间实现共享,这样做不仅有一些额外的开销(文件IO),同时数据类型和逻辑的受限也比较明显。而且,当状态较为复杂时,每次在状态切换(如屏幕旋转)后重建状态的性能代价也可能影响到用户体验。

其实,Fragment机制完全可以优雅的达成上述前台服务的需求,得益于Fragment本身与界面的紧密联系,可以方便的实现服务与UI的双向互通;受益于Fragment自动的生命周期管理,不必刻意提防内存泄露;借助Fragment的切换保留(retain)机制,可以在状态切换期间保持服务不中止。另外,由于Fragment的生命周期管理是由框架自动完成的,所以开发者也完全不必在Activity的生命周期事件代码中加入各种服务相关的冗赘处理,让代码更简洁清晰。唯一的限制是,Fragment不能跨Activity共享。不过按照基于Fragment的界面设计思想,相关联的UI组件都应基于Fragment实现,并置于一个共同的Activity之下,只有在生命周期可独立存在和延续的界面中才需要使用单独的Activity。因此,在严格按照Fragment设计思想开发的App中,这一限制并不是一个真正的问题。

如果你对Fragment的这种『特别用途』仍然持保留意见的话,不妨看看官方文档中的这一段表述:『Adding a fragment without UI』,它明确的暗示了这种使用方式的合理性与可行性。

接下来,就让我们一起来探索一个可行的基于Fragment的前台服务框架吧。

(1)前台服务的创建、销毁和获取

与后台服务类似,前台服务通常也是『按需创建』的,因此服务的创建和获取可以封装在一个操作中。由于无UI的Fragment不能通过界面嵌入点的资源ID来访问,因此tag通常是唯一可靠的辨识和访问方式。(以下代码省略了部分异常处理)

private static final String KServiceTagPrefix = "service:";

public static <T> T getService(Class service_class, FragmentManager fm) {
  final String service_name = KServiceTagPrefix + service_class.getCanonicalName();
  @SuppressWarnings("unchecked") T service = (T) fm.findFragmentByTag(service_name);
  if (service == null) {
    Log.i(TAG, "Starting service: " + service_class.getSimpleName());
    service = service_class.newInstance();
    FragmentTransaction transaction = fm.beginTransaction();
    transaction.add(service, service_name);
    transaction.commit();
    fm.executePendingTransactions();
  }
  return service;
}

注:executePendingTransactions()是为了确保service对象在返回给调用者之前完成基本的初始化生命周期。

在Activity或其它Fragment中需要用到前台服务时,调用上述静态方法即可,它会保证在整个Activity生命周期内只有一份服务实例,因此我们直接使用前台服务的Class本身作为其标识。调用中需要传入的另一个参数『FragmentManager』,在FragmentActivity中可以通过getSupportFragmentManager()得到;在Fragment中可以通过getFragmentManager()获得。

考虑到Fragment的被动生命周期随Activity的销毁而终止,而App的Activity生命周期通常是短暂的,因此就不必引入『引用计数』之类的复杂机制来维护前台服务的终止时机了。

(2)UI元素与前台服务之前的交互

从UI元素访问前台服务,可以简单的直接使用获取到的服务实例,调用其中的方法。服务实例的引用可以安全的保存在同源的Activity或Fragment对象中,但切忌不可保存在比父Activity生命周期更长的对象中,如静态成员中。

反过来,从前台服务访问UI元素,则稍有一些考究。对Activity的访问是最简单的,直接使用getActivity()方法即可得到所在的Activity实例,因此我们可以方便的将前台服务的处理过程借助Activity界面的『进度圆圈』(Indeterminate Progress)给用户友好的指示。对其它Fragment的访问,官方文档中提到了使用setTargetFragment()及getTargetFragment()实现,但放在前台服务的场景中,尤其是考虑到共享、解耦、并发等问题,这并不是一个好的方案。或许大部分开发者更容易联想到『回调模式』,比如在前台服务类中提供回调注册接口,这当然也不失为一个可行的方案,但个人更倾向使用灵活易用的LocalBroadcastManager实现服务往UI方向的通知。关于这个机制,这里就不引申介绍了,感兴趣的朋友可以直接看看Android Support Library的Javadoc。

(3)实现跨状态切换的服务保持

前台服务由于不直接涉及界面布局,因此完全不必在屏幕旋转等状态切换中重建,从而有效降低这一过程中的体验延迟。实现上,其实非常简单,只需要在Fragment的初始化过程中将自身设定为『可保持』:

@Override public void onCreate(final Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setRetainInstance(true);
}

(4)与Loader机制的配合使用

Loader机制也是Android 3.0中增加的一个实用的辅助机制,可以帮助开发者更好的实现异步IO与UI组件的协同。与(同进程的)Service机制一样,前台服务的入口代码也是在主线程(UI线程)中执行的,因此必须尽可能避免在其中执行IO操作。借助Loader机制,可以很好的将IO,尤其是网络访问隔离到独立的工作线程中,同时兼顾与UI组件的便捷协同,因此前台服务与Loader机制可谓是一对绝佳的搭档。但在实际搭配使用中,也有一些需要注意的细节,如果使用不当也可能造成一些很难排查的异常。

使用LoaderManager时,需要明确区分是希望使用Activity级别的LoaderManager还是本Fragment(前台服务)级别的LoaderManager,不同于FragmentManager的统一性,它们其实是两个不同的实例,有着不一样的影响。大多数情况下,Loader仅限这个前台服务使用,因此使用Fragment级别的LoaderManager是最佳的选择。如果希望在多个前台服务之间复用某些Loader(例如CursorLoader),则须使用Activity级别的LoaderManager,但同时应小心避免Loader ID的冲突。

 

以上是对前段时间基于Fragment所实现的前台服务框架初步探索的一个总结,这个机制已经在我最近开发的一个App中正常运作了一段时间,期间并未发现显著的问题或制肘。如果各位在借鉴上述机制的过程中遇到了任何疑惑和苦难,欢迎与我交流探讨。后续相关的经验和技巧,我也会在本文中补充完善。

附:ServiceFragment抽象基类的完整代码

public abstract class ServiceFragment extends Fragment {

  private static final String KServiceTagPrefix = "service:";

  @Override public void onCreate(final Bundle state) {
    super.onCreate(state);
    setRetainInstance(true);
  }

  /** @see {@link android.support.v4.content.LocalBroadcastManager#sendBroadcast(Intent)} */
  protected boolean sendLocalBroadcast(final Intent intent) {
    return LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent);
  }

  public static <T extends ServiceFragment> T getService(final Class<T> service_class, final FragmentManager fm) {
    if (fm == null) throw new IllegalArgumentException("FragmentManager is null");
    final String service_name = KServiceTagPrefix + service_class.getCanonicalName();
    @SuppressWarnings("unchecked") T service = (T) fm.findFragmentByTag(service_name);
    if (service == null) {
      Log.i(TAG, "Starting service: " + service_class.getSimpleName());
      try {
        service = service_class.newInstance();
      } catch (final java.lang.InstantiationException e) {
        throw new IllegalArgumentException(service_class + " cannot be instantiated");
      } catch (final IllegalAccessException e) {
        throw new IllegalArgumentException(service_class + " is inaccessible");
      }
      final FragmentTransaction transaction = fm.beginTransaction();
      transaction.add(service, service_name);
      transaction.commit();
      fm.executePendingTransactions();
    }
    return service;
  }

  private static final String TAG = ServiceFragment.class.getSimpleName();
}

活用通知栏,改善Android应用运行期体验

Android引以为傲的最为成功的UI设计之一,就是它灵活而强大的下拉通知栏,甚至连对UI有自己独到理解的Apple,都心甘情愿效仿这一设计。

不过大部分应用开发者对通知栏的运用理解上存在一些局限,以至于没有充分发挥出这一神器对App应有的价值。比较常见的理解是,通知栏是主要是用来展现Push通知,以及在用户关闭App期间通过后台服务推送信息给给用户。这确实是目前通知栏最常见的使用场景,但却在思维上将其局限于App运行期以外的交互方式。

为什么通知栏就不可以是App运行期间的一种交互形式呢?对于运行期的交互途经,一般开发者首先联想到的是Activity的范畴,需要通知时使用Dialog和Toast。其实通知栏作为一种应用运行期的交互方式,具有『低骚扰』、『节省界面空间』和『长留存』三个相当明显的体验优势。

下面就举几个典型的运用场景加以说明。

(1)App版本升级通知

很多App的升级通知都在启动阶段通过弹出对话框提示用户,用户确认后就开始下载并安装,在此过程中用户只能眼巴巴的等待下载完成。这是一个不太友好的用户体验,尤其是对工具类App而言。如果用户冲着一个急迫的待解需求打开App,这时候弹出升级提示,一方面很容易打断用户当前的使用意图,如果用户确认升级则迟迟无法解决当时的急迫需求;另一方面,如果用户吃一堑长一智,为了尽快解决需求,而关闭升级提示对话框,那么开发者希望以此推动用户升级App的效果就大打折扣了。

这时候活用通知栏,就可以很好的化解上述矛盾了。通过异步检测新版本,并创建一个通知栏消息,既能告知用户有新版本可升级(Ticker在顶栏的滚动显示效果),又不必阻断用户当前想要完成的操作。即使用户急于解决当时的需求而忽视了升级消息,在关闭App后,升级通知仍然滞留在通知栏中,可以有效的二次提示用户进行升级。如果非Play Store渠道安装的App,与之配合的最佳交互实践是:在用户点击该消息后,通过DownloadManager在后台下载安装包,待下载完成后创建另一条通知消息,用户点击再触发安装(升级)流程。这样引入的是一种很轻的交互,不会在低配置机型上因再次启动App界面带来的响应迟缓感。

(2)交互频繁场景下的非关键通知

这一点在游戏中体现的尤为典型,如果在用户的紧张操作过程中,需要给用户一些不急迫的非关键性消息(例如获得头衔、好友邀请),通过通知栏来递送就有不可取代的明显优势了。过去的交互设计习惯中,往往需要引入App内的『状态栏』来显示这类消息。这样做的缺点很明显,不仅增加开发成本,占用宝贵的屏幕空间,而且多条消息也难以实现堆叠,其实状态栏的职能用通知栏来取代是非常适用的。

与之相关的一个讨巧的通知栏使用技巧是:创建一条只含Ticker文字的通知,短时间后移除,就可以达到在通知栏滚动显示一些不需要保留的即时性消息,类似过往对『状态栏』的典型使用场景。

另外,活用同ID通知的『替换』,可以将多条消息合并提供给用户,节省通知栏的空间占用,给用户留下一个谦和的体验感受。而对于仅在运行期间有意义,不需要在App不活动时保留的消息,请记得在onPause()中移除,留给用户一个洁净的通知栏。

(3)『随叫随到』的信息区

很多互联网App的设计中,都不乏『个人信息(User Profile)』之类常驻界面信息区,开发者往往习惯在界面上开辟一小块区域显示这类信息,点击(头像)可以进入个人资料界面。

对于一些无需较强账户认知的App(如资讯类、工具类)而言,持续占用一块屏幕区域,哪怕是ActionBar上的一个按钮,对UI设计也是奢侈的。这时,不妨考虑利用Sticky的Notification(通过 Service.startForeground() 激活),Icon用于显示用户的头像,右侧的双排文字区合理排布用户的关键信息或状态。即使觉得默认的布局不够灵活,也可以定制自己的Layout以容纳更多的信息单元,并在有限的显示面积内支持简单的交互。如果支持Jelly Bean,还可以运用更为体验友好的大尺度和富媒体通知样式,以及可定制的交互按钮(Actions)。

Tip:别忘了在应用onPause()时移除这个Sticky通知。

当然,对于这种非常规的UI设计思路的认同,可能就仁者见仁智者见智了,不过一旦采用了这种设计,就需要给用户做好积极的引导,以免用户因习惯的原因而找不着这个入口。

(4)隐形通知

这应该是Google Now首次引入的一种通知体验设计模式。通过使用最低的Priority(Jelly Bean)或全透明的Icon(ICS及更早版本),使该通知在用户未下拉开通知展现区时呈现出一种『隐形』的效果。

这种通知一般展现一些优先级非常低,不需要用户显式关注的消息,但可以给用户形成一种暗示,『当我需要这类信息时,可以拉开通知栏试试看,它应该就在那里』,即使用户不再需要这些信息,也可以随时『挥之即去』(ICS+)。

虽然Google Now将隐形通知使用于后台服务中,但我们完全可以将其借鉴到App运行期间,发挥其无干扰的价值。比如用于展示不希望干扰用户的相关推荐、最近的非重要App事件、频繁更新的状态、游戏中的『任务指引』等。

同样记得要在onPause()时清理不需要留存的隐形通知。

 

将通知栏纳入App运行期的交互体验,是一种相对另类的设计思路,如果运用得当,不仅可以省下不少开发工作量,还能给用户一种略带惊艳的舒适体验。同时,需要保持清醒的是,通知栏也是一种相对紧张的资源,尤其是被国内大量常驻类App无条件霸占之后,如何掌握好通知的使用与滥用间的平衡点,也是需要一定设计智慧的。

【原创】提高专注的时间管理小工具(Win32)

在被RescueTime反复羞辱之后,痛定思痛,今天早上爬起来之后决定开发一个提高专注的小工具,拯救我的时间专注率!

其实,失去专注很多时候是由于无意识的『开小差』,或者查资料时『跑了题』,也包括来自其它插入型的干扰,比如IM消息。所以,我解决这个问题的思路很简单,也很直接:事先锁定一个窗口(比如IDE或者PowerPoint),当离开它一小段时间后,就开始闪烁任务栏的窗口标题。

实现上,用到了Win32的一个API:FlashWindow()。为了开发方便,使用了AutoHotKey作为平台,半个小时便开发调试完成。不过仅在Windows 7下测试过,如果其它版本下有问题,请反馈。

使用方法:

下载exe或者ahk(如果安装了AutoHotKey)文件,启动它之后,切换到需要专注的应用窗口,按热键『 Ctrl+Win+Alt+F 』,即可看到当前窗口在任务栏闪烁了一次,表明它已被专注。接下来只要你离开这个窗口超过1分钟,它就会开始在任务栏每分钟闪烁一遍。热键『 Shift+Ctrl+Win+Alt+F 』可以解除专注锁定。

Concentrate.exe (适合普通用户)

Concentrate.ahk (适合安装了AutoHotKey的用户)

对方正无视开源协议盗用FontRouter源码行为的严正声明

作为一款内置收费服务的商业软件,目前已经掌握的充分证据显示“方正字酷”大量使用了FontRouter开源项目的源码,却未履行“Apache License 2.0”的要求在软件本身的发布包及网站中附带任何对源码使用的声明和协议文本,已经构成了对开源协议的恣意践踏。

在此提出严正声明,要求“北京北大方正电子有限公司”立即停止上述侵权行为,并就已经造成的不良影响在媒体上公开道歉!如果对方拒不承担上述违反协议的责任,我将把这一事件永久性的写入FontRouter开源项目的“侵权榜(Shame List)”,作为对恣意践踏开源协议的抗议。

(本文将以电子邮件形式递交“北京北大方正电子有限公司”)

注:FontRouter是Symbian OS上广受欢迎的一款免费的字体替换工具软件,并在 2009.2 以 Apache License 2.0 协议开源

用Guice+Peaberry实现OSGi环境下的JIT注入

Guice是一个Java下非常强大的依赖注入框架,相比其它同类框架,我更喜欢Guice这种“配置亦代码”的风格。除了开发友好性之外,Guice的过人之处还体现在它灵活的JIT(Just-in-time)注入上。利用@ProvidedBy()注解可以方便的为接口绑定定制的Provider,从而实现结合了动态逻辑的Lazy注入。

当Guice和OSGi框架碰撞到一起时,就会遇到一些观念上的矛盾:OSGi的动态生命周期在Guice本身的静态绑定下无法发挥其应有的作用,而Dynamic Service也无法方便的与Guice对接。好在开源社区已经有人意识到这些问题,并为两者搭起了一座鹊桥,这个项目就是“Peaberry”。

这两天在捣腾Peaberry时,发现它的设计主要是针对静态绑定,在与Guice的JIT注入一起用时,却还差那么一两块砖,于是自己把它给砌上了,顺便分享出来与大家交流一下。

按照Peaberry的用户手册,静态绑定一个DS服务的写法是在Module.configure()中使用:(以LogService接口为例)

bind(LogService.class).toProvider(Peaberry.service(LogService.class).single());

如果转为JIT注入,则必须提供一个相应的Provider类。虽然Peaberry.service(…).single()返回的正是一个Provider,但鉴于Java注解只能用字面类(Literal Class),所以这里需要包装一下。我的办法是定义一个抽象的公共Provider,用反射去识别派生类的具体泛型类型:

public abstract class JitProvider implements Provider {

	protected JitProvider() {
		@SuppressWarnings("unchecked")
		final Class clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
		provider = Peaberry.service(clazz).single().direct();
	}

	@Override
	public T get() {
		return provider.get();
	}

	@Inject
	protected void setInjector(final Injector injector) {
		injector.injectMembers(provider);
	}

	private final Provider provider;
}

具体使用JitProvider的接口以如下形式声明:

@ProvidedBy(Foo.Provider.class)
public interface Foo {
	...
	static class Provider extends JitProvider {}
}

这样,所有使用Foo服务的Bundle都完全实现了即需即用,不必再像过去那样在每一个用到该服务的Bundle的Activator中事先进行一遍Peaberry繁琐的bind配置。经此精简优化,Peaberry的易用性得到了明显的提升,使用起来也更加直觉化了。

Google Wave终于支持非Wave用户匿名浏览

下面这个嵌入式的Wave就是Google Wave团队的官方公告Wave,现在你不用登录Wave就能看到它了。不过匿名用户还只能浏览,参与互动仍然需要登录。但这样已经让Google Wave的可用性大大增强了,可以在更多Web领域发挥它应有的价值。

结合Google Wave API的Proxying-for,我们也可以自己实现匿名式交互,或者与其它身份系统集成(比如OpenID)。有时间的话,我会尝试做一个OpenID Proxy的Sample。

开发跨UI体系的Symbian应用

一直以来,Symbian都是基于OS + UI体系分离的设计,这种分离又不同于Android,后者的不同UI只是视觉呈现的差异,对应用而言,是完全兼容的。但Symbian的不同UI体系,如S60、S80、UIQ、QT等,彼此间连UI的API都不兼容,对应用开发者来说,这真是一个噩梦。虽然也可以通过将UI API的使用限定于Uikon UI(S60、S80、UIQ等当代UI体系共同的继承源),从而实现最大程度的兼容,但这样做是以牺牲广泛的可用UI元素为代价的,对稍复杂的应用而言都不太现实。况且即将取代现有各种UI体系的QT,又是一次颠覆性的变革,不用指望任何的兼容可能了。

那么,在这样一个变革到来之前的暗夜,如何开发一款可跨UI体系的Symbian应用呢?这并非没有可能,但有着诸多的限制。如果你的应用能满足这些限制的话,那么完全可以成为真正意义上的跨UI体系的Symbian应用。

继续阅读开发跨UI体系的Symbian应用

在Google App Engine中使用泛域二级域名

Google App Engine(以下简称GAE)除了支持自有的appspot.com域名外,借助Google Apps,它还允许用户配置自己的独立域名提供服务。但之前使用过独立域名的朋友可能都遇到过一个相同的困扰:你可以用指定一个特定的二级域名访问你的应用,但却无法使用泛域二级域名(wildcard sub-domain)。对泛域支持的社区呼声一直都很强烈,Google也声称将要支持这一特性,但却未给出具体的时间表。

前两天为了解决tb.ly的泛域二级域名,折腾了很久。因为虚拟主机服务商Dreamhost不对非Private Server用户支持DNS泛域解析,所以我不得不另谋它策。在GAE上的一次没头没脑的尝试,居然意外的让我发现GAE已悄然支持了泛域二级域名。配置过程稍微有些复杂,所以在这里完整整理出来,以tb.ly的真实案例,分享给各位研究GAE的朋友。

继续阅读在Google App Engine中使用泛域二级域名

为什么二次确认也没能拯救照片?

前段时间,有一个朋友借我的相机去用。回头来还给我时,抱怨说他不小心把拍的很出彩的一张照片给误删除了。我琢磨了一下,富士这款F31fd上,删除相片也是有个二次确认的过程呀,而且二次确认的默认选项还是“停止”。难不成我这个朋友能短路到义无反顾的程度?不过当听完他道出苦水后,才意识到,原来这都是用户交互体验设计失误惹的祸。看似万无一失的“二次确认”,一样拯救不了你的照片。

事情的经过是这样的:当我的朋友在拍完那张照片后细细欣赏时,不小心按到了“上方向键”,这是删除当前照片的快捷键。而后,看到屏幕上显示出的删除确认提示,我这个朋友一阵紧张,先是连按返回键,发现取消不了又忙不迭的切换选择框到“停止”上,并匆匆按下“确认”。哪知道,却依旧眼睁睁的看着喜爱的照片香消玉殒……

继续阅读为什么二次确认也没能拯救照片?