Oasis Feng

对Android Wearable SDK的猜想

【背景】

Android团队早在去年初启动开发的4.3版本,就已经开始为可穿戴设备优化Android OS及其SDK了。Bluetooth 4.0 LE (Smart)的支持是一个毋容置疑的信号;而NotificationListenerService从AccessibilityService中的脱离,可以看作是Android为在第三方设备的通知投射扫清了障碍。Android 4.4的瘦身和内存优化更是直指512M内存级别的低配置设备,已经为嵌入可穿戴设备铺平了道路;而传感器事件的硬件级批量聚合及新的计步传感器支持更是将Android的野心袒露无疑。其它诸如Immersive Mode和Translucent System UI(榨干受限的显示面积)、Enhanced notification access(更全面的通知信息及Actions交互的支持)、Storage Access Framework(集中存储和远程访问)等等新特性中都能找到可穿戴设备的端倪。

在上周的SXSW上,Sundar Pichai宣称将会在2周内发布Android Wearable SDK,想必整个工程已经进入了最后的收官阶段。那么我不妨斗胆来预测一下几天后即将面试的Wearable SDK到底会长什么样。

【概貌】

Sundar Pichar在SXSW上也提到了他们认为的智能手机与可穿戴设备间的协同关系:『手机为中枢,穿戴皆IO』。(……smartphones became tiny computers, wearables are becoming nexuses of an array of sensors.)这说明Google不单单是希望把搭载了Android系统的可穿戴设备纳入生态,而要让『率土之滨,莫非王臣』。这也迎合了整个可穿戴生态的两条发展主线:提供富交互的完备设备 和 仅采集数据(及提供简单反馈)的哑设备。即便是未搭载Android系统的Pebble也好,Gear 2也罢,只要看作是手机的IO,就逃不出Android生态。

Google在可穿戴设备领域的处女作可谓是倾城的惊艳,但Google Glass很长时间以来只提供了非常受限的云端接入接口,让本就已经稀缺的开发人员抓狂不已,甚至于直接转向了root社区。好在Google最终在去年11月发布了Glass DevKit的早期预览版,开启了Glass本地App的大门。虽然Glass的交互迥异于目前常见的手腕类穿戴设备,但其SDK的设计思想则是非常明确而一致的,即基于目前Android SDK的更上层Addon SDK。考虑到离下一个Android大版本发布(Google I/O)至少还有3个月的这一时间点,相信这也将会是Wearable SDK第一版的基础形态。

SDK中可能会包含哪些有意思的设计呢?还是循着Sundar Pichar的线索顺藤摸瓜吧。『We want to develop a set of common protocols by which they can work together…… they need a mesh layer and they need a data layer by which they can all come together.』这里面传达了两个重要的信息:互操作性协议、数据交换标准。前者让彼此间的IO更加顺畅互通,后者可助任何数据为任何App所用。于是整个SDK的面目便可窥见一斑了。

【互操作性】

互操作性协议解决的典型场景便是Pebble这样的设备如何与Android App更方便的互通。Pebble SDK提供了一个私有的解决方案 —— Pebble端的Watch App(C语言开发)及其SDK提供的通信封装。这带来了一个Google最不希望面对的问题 —— 生态的分裂(Fragmentation)。因此,Wearable SDK需要以一个非常Android化的方式解决这个问题。除了已被广泛使用的『Notification Listener Service』外,我猜想中新的答案可能会是『Widget』和『Remote Sensor』

『Widget』是从Android诞生早期就支持的唯一一个天然适合于可穿戴设备的前瞻设计,基于预定义受限面积的周期或事件驱动的渲染,然后将渲染好的位图传递给另一个负责展现widget的画布主体,后者可以接收简单的点击和手势交互,并将其反馈给提供widget的应用,触发新一轮的重绘。原先App与Launcher间的互操作性,在Android 4.2开始已经拓展到了锁屏界面(Lock Screen Widget),如今又可以无缝的过渡为App与穿戴设备间的桥梁。更重要的是,目前数不尽的带有Widget的App就可以摇身一变成为『可穿戴设备友好』的App了。Wearable SDK需要做的只是搭建起这样一个延伸性的透传协议。至于Android 4.2开始支持的『Secondary Display』多屏联动机制,也许不会出现在早期的Wearable SDK中,但有望成为未来面向具有大尺寸显示界面和高速无线连接能力(如Bluetooth 3.0 HS)的穿戴设备更灵活的媒体显示解决方案。

『Remote Sensor』,顾名思义,就是不在当前设备上的传感器。由于大量可穿戴传感单元的涌现,弥补了智能手机本身传感器的可触达边界,毕竟穿戴在身上的设备才能更准确的采集心跳、血压等生理指标,而各类借助现代传感技术的奇特探头才能满足人们日益多元化的对身周环境的感知需求。但持续传输的能耗问题是拦在Remote Sensor发展道路上的主要障碍,毕竟Android 4.4提供的Sensor Batch机制在降低耗电的同时是以牺牲实时响应能力为代价的。真正的救星是近几年方兴未艾的SensorHub技术,通过一个低功耗设计的可编程嵌入式芯片,先行采集和缓存传感器数据,并进行相对有限的实时分析,当预置条件满足时才激活主CPU进行处理。例如Moto X引以为傲的X8体系。再看可穿戴领域的传感器单元设计,只需将SensorHub前移至传感器单元内,单元与手机之间维持Bluetooth LE连接,SensorHub只在必要的时候通过这个连接通知手机和传输数据,而手机则可以在有需求时向传感器单元主动请求数据回传。得益于Android良好的传感器框架设计,以上Remote Sensor机制只需在现有Android框架下通过Sensor Agent over Bluetooth LE以虚拟传感器的形式提供,在上层App看来和手机本身的传感器并无二致。

【数据交换】

互操作性的分裂问题得到解决,并不意味着广大开发者就可以轻松的开发支持可穿戴传感器单元的App了。眼下的局面是,Kickstarter和Indiegogo上大量涌现的智能传感器众筹项目都是各自为阵,这些团队都不得不投入大量精力自己为其产品开发智能手机App,结果还往往不尽如人意;另一方面,传统App开发者似乎都只能隔山观火,既下不了场,也捞不到汤。这种维度的分裂正是由于移动OS平台上传感器数据规范化缺失和领域技术与应用层面间的断层所造成的。幸运的是,衔接开发者,正是Google在Android中所一贯擅长的。

Wearable SDK正应担起这付扁担,一方面定义更广泛和通用的原始传感器数据协议,另一方面提供高阶抽象的虚拟传感器框架,将这种基于数据整合和领域算法的抽象能力开放给社区和学术界,让更多拥有领域经验的专家和开发者进来衔接『专业数据』与『高阶应用』两个位面,培育出众多高质量的虚拟传感器。如此一来,才能让生态的两端更融洽的衔接,让更多的生活类和生产力App也能与可穿戴设备的蓬勃发展相互促进。

【结语】

YY了这么多,其实都是作为一个Android资深开发者兼可穿戴设备控的一些美好愿望。不过相信在汲取了Android发展历程中的坎坷之后,Google不会在这个新的领域让我们失望。就让整个社区一起迎接即将到来的Wearable SDK吧。

【题外话】

补充一个身为Geek的不切实际的畅想,Android Accessibility框架所蕴含的抽象展现和交互代理能力其实有非常大的潜力成为衔接传统App与可穿戴设备异化交互的玄铁重剑。但亟需提升Android整体体验的Google,想必是不会在Wearable SDK中祭出这件难以驾驭的武器了。好在Android生态的开放性并不阻碍Geek社区朝着这条道路挺进,也许在不久之后,我们就能看到一个可以在智能手表上操控手机端任意Android App的利器了。


CyanogenMod的商业化或源于对Motorola的恐惧

这是去年底我在Google+上写的一篇短文


Moto X推出已逾半载,CM却几乎无法打入其用户群,即使XDA论坛上的Moto X开发板块也门可罗雀。因为Moto X近乎原生态的Android体验和超迅速的Android版本升级跟进,大大降低了超级用户对CM的渴望,而这个群体正是CM的主打力量。

我也是其中的一员,至今仍在使用Moto X的原厂4.4 ROM,除了上述原因之外,还有几个无法抗拒的理由:

1. Moto X主打的三个实用特性:Touchless Control、Active Display和Twist to Camera,全部依赖Moto自有的专利架构——X8体系,再加上CSR提供的蓝牙音质增强技术Apt-X。CM作为开源社区,几乎不可能取得商业授权,也无法以合法的理由集成这些闭源特性实现。其它厂商的机型则尚未形成如此强烈的特性壁垒,三星、HTC、索尼的超级用户大多并不太在意失去原厂的特性。

2. Moto X在底层优化上比CM走的更远。包括Bionic和Dalvik虚拟机的性能优化,采用了高通的私有优化,据称比AOSP的Dalvik性能表现更佳。(XDA上对此有一些深入的讨论)。同样,这些也是CM作为非盈利性开源社区所无法吸纳的。

3. Xposed框架,加上最近很火的GravityBox模块,大大加速了CM社区Moto X用户的叛逃,因为CM的很多人性化UI优化都可以在原厂ROM + GravityBox下实现。(Moto X贴近AOSP的ROM实现对GravityBox的兼容性更佳)

相信正是CyanogenMod的社区领袖们看到了Moto所带来的巨大威胁,以及可能由此引领的未来其它Android厂商发展思路的转变。才不得不采取商业化并加速自身品牌的建设,甚至直接参与设备打造,向更广泛用户群体的渗透。只有这样,才能有机会抗衡Moto及幕后的Google。

这不能不说是开源社区所面对的最大不幸,它把社区ROM的最大软肋暴露的体无完肤,也显现出Google未来Android战略的重要转变。


微信收费事件背后被广泛忽略的技术细节

作为一个横跨通信与互联网两大行业的从业者,前四年的核心网经验和后五年的互联网经验让我不得不感慨一个非常遗憾的现实:通信与互联网两大行业本来可以有珠联璧合的技术协同,为移动互联网提供近乎零耗电零流量的PUSH机制,但由于两个行业之间长期以来的价值观隔阂和互防心态,导致如今的手机PUSH技术不仅为用户增加了显著的电量消耗,还对移动运营商的基础设施造成了完全不必要的信令压力。微信与运营商的纷争正是这种冲突集中爆发的结果。

看到不少来自两个行业的专业分析,通信行业的专家谴责微信过于频繁的心跳和短包导致“信令风暴”,而互联网人士则往往站在用户与道德的制高点上对移动运营商挖苦讥讽,双方都很少探究这个问题的深层次技术和利益矛盾。这里我不妨提一提有些大家没有真正重视的技术细节。

为什么使用同样PUSH技术的Apple和Google等巨头,没有被运营商卯上,唯独单单拿微信下手?大家也许会认为这是运营商欺软怕硬,拿Apple和Google没办法。其实从实际数据上来看(下面将提到),微信确确实实产生了远超Apple和Google的信令需求。难道是因为腾讯技不如人,被逮着了尾巴?其实不然,我们曾经也在PUSH技术上投入了较多的分析研究,其中一项发现或许可以解释各种原委。根据分析,一般当基带空闲超过一定时间后,运营商的IP网关会自动释放(关闭)连接。目前各家所使用PUSH通道的实现原理虽然同为『长连接慢心跳』,但这个『慢』字却有很大的文章。Google在Android系统中使用蜂窝(2G/3G)网络连接GCM的PUSH通道时,默认采用的心跳周期是28分钟,这才是所谓“慢”的含义 —— 尽可能降低心跳的频度,从而达到尽量省电的目的。但这个放诸全球绝大部分地区借行得通的规则,到了中国大陆,就出现了问题。以中移动的2.5G网络为例,经过粗略测试,大约5分钟左右的基带空闲,连接就会被释放,这就是为什么微信Android版本选择以『5分钟』为周期发送连接心跳。可能有人会有疑问了,『那Google以28分钟发送心跳,岂不是在中移动的2.5G网络下无法保持PUSH长连接?』事实上,确实如此,这也是为什么Google的PUSH通道经常『迟到』。当我们活跃使用手机时,由于基带往往并不会闲置,所以部分掩盖了问题的本质。另外,当连接到Wi-Fi时,宽带的网关一般没有空闲释放机制,所以长连接会得到保持,这也进一步减少了我们平时遭遇的PUSH迟到。

『5分钟』的心跳周期到底是什么概念?可以理解为,每部安装了微信的Android设备每天发送近300条短信(其实占用的信令资源还远超这个数量);还意味着每天你的手机将被从待机省电状态唤醒近300次,每次相当于打一个几秒钟的电话。粗略测算,一般的Android手机每天有超过15-20%的电量被消耗在发送过度频繁的心跳上。其实,这都还远不是最糟糕的事情。由于众所周知的原因,大陆行货渠道发售的Android手机都无法使用Google的PUSH通道,原本每个手机中只需要建立的唯一共享的PUSH通道,被人为分裂,以至于每一个声称为用户提供实时通知的国内App,基本都在重复上面微信所做的行为。当你的手机中同时安装了多个这类App时,无论手机的耗电,还是运营商的信令负担,都要数倍于上述情形。

虽然我向来不惮以最坏的恶意揣测国内的垄断巨头,但在这个事情上,中移动或许确有它的苦衷。正如很多技术文章中所言,2G网络的基础结构和协议并未针对IP传输优化,其服务IP链路的信令承载能力相对较弱,而TD-SCDMA又长期得不到真正的发展,导致中移动的2.5G网络承受了超龄超载的负荷。刻意缩短空闲连接的释放超时,可能原本是期望能起到节省信道资源的目的,没想到聪明反被聪明误,这一限制性的举措让互联网应用不得不以远高于正常的频率发送心跳以维持PUSH长连接,结果大大加重的信令负担,给本就脆弱的2.5G网络雪上加霜,而且更给用户的手机造成了远超常规PUSH技术的电量消耗,造成了如今这一『三输』的格局。

其实,不光是微信,整个移动互联网行业都在努力解决PUSH机制目前所面对的各方面问题。包括Google、Apple这样在整个行业举足轻重的巨头,都仅仅在OSI通信协议的4层以上作各种努力,目前几乎所有的PUSH机制都基于『TCP长连接慢心跳』方式实现。虽然『慢心跳』如果得以正常工作,可以在一定程度上降低手机基带模块的工作频度,但无论互联网行业在技术上再如何标榜“PUSH”相比“PULL”的流量优势,但在OSI的下三层来看,基带模块所承受的负担和“PULL”仍然没有本质差别。这就决定了耗电问题不可能从互联网技术层面彻底解决。事实上,在移动通信网络中,信令是一种天然的最佳PUSH载体,它不需要任何IP层的收发包(也就不需要TCP连接)就能实现秒级的实时性,最重要的是它没有任何额外的电量负担,手机完全只需处于正常的待机状态。可惜移动运营商只会将其运用在一本万利的SMS(及WAP PUSH)服务,压根不可能无偿提供给互联网产业使用。结果,互联网行业选择了虽然不用付费,但却代价高昂的『TCP长连接』,只为让用户享受到免费的通知服务。这种两大行业置用户体验于不顾的分庭抗礼,已经相持近10年,而当互联网终究开始以免费服务反噬移动运营商的SMS甚至语音业务时,运营商再也坐不住了…… 但与其饱受信令风暴的折磨,不如主动免费开放信令通道作为更高效的PUSH通道给互联网产业使用,再以『免费增值』的思路构建有QoS保障的VIP PUSH服务。不仅可以大幅度节约信令资源,更能以用户体验的提升打造核心竞争优势和增值空间。能否走出这样一条转折的道路,就看运营商是否愿意转变思维了。

注:由于iOS系统的相对封闭性,暂时未能测定Apple的Push通道APNS在2.5G网络下的长连接心跳周期。欢迎了解的朋友补充测试数据。


UPDATE:更正微信Android版在中移动2.5G网络下的心跳周期为5分钟(此前测定的2.5分钟存在偏差)


基于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策略的选择,往往只需为离线状态增加一些全局性的体验优化即可(如无缓存时的友好提示)。


用Chrome更高效的网购

疯狂淘宝的网购达人,都有一个共同的烦恼 —— 打开了太多浏览器标签,以至于标签栏拥挤得只剩图标,混乱到快要抓狂。

以前,我一般会建议用『多窗口』的方式对标签进行分割,在开始一段将用到大量标签的浏览之旅前,开启一个新窗口将会让接下来的浏览脉络更为清晰。因为在『多标签』成为浏览器的标配后,大家往往忽视了窗口的价值。

但我逐渐发现,除了对事物有很强梳理习惯的人士外,普通用户大多很难养成这个习惯,因为『事前规划』在很多人看来比预测地震还要困难,往往都是在标签泛滥成灾之后,才会意识到苦痛已然铸成……

不过现在有一个更易行的技巧让没有事前规划习惯的人,也能轻松驾驭,而且无论是深陷标签的泥潭,还是初涉痛苦的沼泽,都能随时全身而退!前提是用Chrome浏览器。

例如当一个网购达人在不自不觉中打开了20多个淘宝窗口(画外迷音:淘宝前端最喜target=”_blank”……)已然完全分不清哪些窗口是『牛仔裤』,哪些窗口是『爱疯舞』,还有哪些只是路过的搜索和店铺…… 这个时候,要在Chrome中把这几十个窗口理清楚,其实相当容易!如果你之前搜索了『牛仔裤』并从结果页打开了很多件商品,那么随便在任何一个商品页或者搜索页的Tab标签上点击右键,点选『按打开者选择 (Select by opener)』,这时你会发现整个标签栏会变暗,而精确地留下了所有牛仔裤相关的商品页和搜索页的标签被点亮。接下来只要简单的将它们轻轻往下拖拽,就把这组标签划到了一个新窗口中。亦或不需要这些页面了?直接关掉整个新窗口~

按打开者选择

是不是很神奇?别激动,还有更智能的!假如你从搜索到商品页后,又进入到这个卖家的店铺去逛了一圈,打开了店铺中的很多商品浏览,这时候,同样是用『按打开者选择』(无论是对店铺页还是其中的商品页),这次被点亮的就是这个店铺和店里的商品了~

除了『按打开者选择』外,还有另一个『按域选择 (Select by domain)』。它适合当你在多个B2C网站之间穿梭浏览时,轻松的把他们按网站区分开,或是想关掉所有的商品页,抽出打开的搜索页…… 慢慢去体会和发现更多的适用场景吧~

这就是Chrome,把复杂的技术融入简单的交互之中,激发出使用者无尽的形象力~

注:上述特性在本文发表时仅限Windows版Chrome中提供。

UPDATE: 如果你的Chrome中没有发现上述标签页菜单,可以尝试在『about:flags』中打开『向标签页右键菜单中添加分组选项』这个功能开关。


基于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无条件霸占之后,如何掌握好通知的使用与滥用间的平衡点,也是需要一定设计智慧的。


阿里云主机试用体验

内部受邀试用了一下阿里云主机,让我此前对阿里云的印象有所改观。在云主机产品上,感觉阿里云还是比较能沉下心来客观面对国内的中低端市场的,并没有摆出阳春白雪的姿态来。

尽管内部试用邀请是针对『标准A型』的规格,但我坚持换成了『经济A型』,因为最低端的型号往往更能看出一家主机公司的态度和良心。

规格

阿里云主机目前的规格搭配,感觉不是特别合理,可能影响到成本及性价比。比如经济A型,单核、512M内存,60G空间,1Mbps的带宽。在这个CPU及内存规格下,空间显得太充裕,而带宽则较局促。相比之下,盛大的云主机似乎更为人性化,类似的规格『超微』,也是512M内存,存储是7.5G,带宽可独立选配(这一点是大爱),更贴合小型创业团队的需求。(也许阿里云更多面向的是电商建站,规格需求略有不同)

从阿里云的产品线布局来看,存储方案应会主推OSS和RDS。或许限制主机带宽是推广OSS的一个市场策略,但大容量的主机存储却感觉与这个目的背道而驰。

价格

因为阿里云主机的规格是绑定了带宽的,所以跟盛大云对比,同规格的价格差不多各有胜场。但是考虑到盛大云的规格搭配对小型互联网初创产品更为合理,而且带宽规格可以分开购买,就显得更有性价比了。

注:最近阿里云的高调团购促销,让价格无形中增添了相当的吸引力。预付一年省两个月,实际享受最高16个月(视团购成交量),还可以升级到1G内存和2Mbps带宽(对应『经济A型』)。相当于¥66/月的价钱购买接近『经济B型』的规格,已经比大多数国内VPS服务商的促销给力很多了。

配置

基于Xen PV虚拟化技术,目前提供的OS安装选项有点少,尤其是32bit系统,只有CentOS 5.3一个选择,对于选择小规格主机的用户有些不便。毕竟小规格主机,尤其是在内存有限的情况下,更倾向于使用32bit系统以节省内存。

后台因为上线不久的缘故,还显得略微粗糙了些,小问题不少,不过都影响不大,但其中有一点需要特别小心:管理控制太上面的停止和重启操作,是没有任何确认过程的。这样一个下拉选择框执行关键的维护动作,稍有不慎就误操作了……

性能(测试篇)

用hosting社区标准的性能测试工具unix-bench作了一个简单的横向测试(测试条件有限,数据仅供参考)。作为对比的几款主机除了大家所熟知的Linode 512之外,还有Lightwave的Xen VPS,阿里云的老双开集群Linux主机。

Aliyun Eco.A Lightwave PV.Tiny Linode 512 Aliyun Old
Specifications Virt 1 core Virt 8 cores Virt 4 cores Virt 2 cores
Dhrystone 2 using register variables 2127.5 1929.0 3024.8 2170.6
Double-Precision Whetstone 523.7 2572.6 1375.3 925.6
Execl Throughput 836.0 596.7 564.0 1673.0
File Copy 1024 bufsize 2000 maxblocks 2190.0 556.3 783.7 280.1
File Copy 256 bufsize 500 maxblocks 1495.3 441.2 489.7 183.4
File Copy 4096 bufsize 8000 maxblocks 2939.5 826.4 1573.5 461.2
Pipe Throughput 1436.6 1985.2 1242.4 1527.8
Pipe-based Context Switching 776.4 716.8 440.3 1429.7
Process Creation 966.8 532.4 407.8 1822.5
Shell Scripts (1 concurrent) 1036.6 1471.0 1355.7 2396.1
Shell Scripts (8 concurrent) 989.5 1347.6 1275.7 2516.0
System Call Overhead 2147.2 1049.8 838.5 895.1
System Benchmarks Index Score 1290.3 995.4 937.0 1045.8

可以看出,阿里云主机的优势在于相当强劲的IO性能(可能受益于现阶段母机整体IO负载较低),而在整数运算方面,Linode的优势更为明显;Lightwave的浮点性能强劲得益于它的AMD Opteron CPU。遗憾的是,在系统调用的开销上,阿里云主机似乎显得不太正常的高,可能与采用的虚拟机架构及参数调优有关。整体得分,仍然是阿里云主机胜出,显示出了阿里云主机目前在规格方面相当的诚意。

对于大部分Web或Mobile应用来说,主要关注整数和IO性能两方面,其次关注系统调用开销。JVM型后端可以不用太过关注进程创建和pipe方面的性能。

性能(感受篇)

为了有一个更为直观的感受,我在阿里云的主机上测试了一下Android源码的编译(CyanogenMod 10),首次编译大约耗时8小时,增量编译在2小时多一点。整体来说感觉比较满意。

操作过程中,shell的整体响应很稳定,没有出现国外低端VPS常见的卡顿(非网络原因)。

网络性能由于暂时没条件作国内云主机间的对比测试,就简单说一下直观感受吧。测试100M单线程下载,下行速度稳定在标称的带宽限额上,而上行带宽则没有作带宽限制,测试过程中曾经稳定达到了8M/s,这一点非常适合上行带宽需求不小的应用类型。

总结

整体上,阿里云主机给人的感觉是中规中矩,仍有较多值得改善的方面。但在一些细处,仍显示出特殊的吸引力,例如强悍的IO性能、无限制的上行带宽、高质量的双线阿里机房。作为一家国内一线的主机服务商,售后服务和技术实力方面还是有所保障的。鉴于阿里云敢于作出『100倍故障时长赔偿』的承诺,看来对于自己的可靠性技术相当自信。

无论是国内的小规模初创团队,还是Geek们希望跨越电信/联通的互通鸿沟,都值得趁阿里云此次难得力度的促销优惠,低价入手这个还处在培育期的云主机,至少可以少受小型主机服务商普遍的超售困扰。


时代的弥思(3)——后信息时代的变革

『后信息时代』,从上世纪90年代至今,曾有过各种不同阶段和角度的诠释,但今时今日,无论你相信与否,作为信息时代标志性分水岭的『信息大爆炸』已经开始加速并深刻的影响我们的世界了。

互联网的诞生将全球信息扁平化,Web 2.0又大大降低了UGC(用户产生内容)的成本,使得信息的发布从过往的媒体主导演变为全民参与,而微博客和SNS进一步降低了UGC的门槛并充分激发了UGC的冲动,近几年移动社交App的兴起更是为用户产生富媒体内容创造了无与伦比的便捷性。所有的这一切,都在迅速的燃尽信息大爆炸的引线,不断加速最终变革的到来。

当然,你不会听到那『Bang』的一声大爆炸的巨响,也不会被爆炸的气浪掀起几层楼高,不过你会快速的感受到某些变化。尽管质变的时刻尚未完全到来,但我们已经可以很清晰的预测和探讨大爆炸后信息时代的一些可预见的影响了。

最根本的质变是 信息供求关系的逆转,越来越多的信息产品将从此由卖方市场走向买方市场。与商品市场供求关系的此消彼长不同,信息时代的这一逆转趋势将是永恒而无法回头的,因为信息不仅借助于扁平的互联网进行传播,还具备了的一个显著有别于商品的特征——近乎零成本的可复制性。这个关键特征也将决定接下来所要讨论的若干内容,请谨记这一点。

信息供求关系的逆转,给整个社会所带来的深远影响中,首当其冲的就是 信息的价值分配机制

Read the rest of this entry »


【原创】提高专注的时间管理小工具(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的用户)