Oasis Feng

万能的淘宝 之『卖的不是CPU,是手艺』

最近实在忍受不了Android Studio里连敲代码都要延迟一秒才能出来的卡顿,决定升级一下CPU。但这个六年前已经超频到4.2GHz的Intel E8400(LGA775)给我出了一道实实在在的升级难题。65nm的Q6600超频能力实在有限,基础TDP高达100多W的Q9300又过于考验我那瘦弱的CPU风扇……

就在各种碰壁各种绝望的时候,我在『万能的淘宝』上意外发现了一个神奇的存在。一张写有『不用护舒宝,不用切主板』几个大字的商品图吸引我好奇的点击进去,第一次见识到了LGA771硬改LGA775的别样洞天,才知道原来还有这样一条曲线升级弯道超车的捷径。卖家过硬的手艺更是把硬改的易用性发挥到了极致,让我这种懒人都可以不费吹灰之力。

在狠狠的恶补了退出玩机界落下的论坛干粮后,我在E5420与L5408之间选择了后者。低电压、高倍频,再加上超出同门兄弟CPU一大截的72℃ Tcase,超频潜力实在给人无限遐想~ 到手后果然不负厚望,默认电压轻松从2.1G直接拔上3.5G,略微提升核心电压后冲上3.8G。怎奈其中一根南亚易胜的内存条拖了后退,外频没能稳定爬上950MHz。最后退回到3.7G上稳定工作。得益于不足1V的超低电压,核心温度还不到40℃。高达75%的超频幅度也可算是空前绝后,甚至远超当年的圣器赛扬300~

感谢『万能的淘宝』,Android Studio终于又流畅无滞了,¥190就让整机性能再上一个台阶,这台6年前所攒PC终于可以重燃激情,继续挥洒余热了~

物理学、社会学、神学——意识与命运的含义

(现代)物理学研究微观尺度,社会学研究我们可以直接感知到的尺度,它们和神学有什么联系呢?

神学的研究对象——『神』,在传统科学界看来,是一个被人类按照自己的懵懂认知所『虚构』出来的超凡角色。无论哪一种『神』,都有着共通的特点:拥有主宰万物的力量,可以感知到信徒的诉求,能在某些时候展示『神迹』。但对于『神』的存在性,一直以来难以达成共识的一个最根本的原因在于,我们始终无法以可被广泛认知的形态直接感受到它的存在。从严谨的科学角度,『无法被感知』,并不妨碍我们探索和论证其存在性。牛顿,作为那个时代最严谨缜密的科学家之一,终其半生追寻神学,希望能证明上帝的存在,虽然不被世人所理解,但我们仍应尊重和理解他在神学上的科学态度。

那么假如存在『神』,为什么无法被我们广泛感知呢?

为了回答这个问题,不妨做一个更易于理解的类比。假如人类自身就是『神』,我们拥有什么样的主宰之力?可以感知到何种信众的诉求?又时常展示哪些『神迹』?看似让人瞠目的问题,其实不难回答,只要意识到我们的『信徒』在哪里。

当我们挤掉脸上的一个小痘痘时,你可曾意识到举手之间终结了多少细胞的生命?当我们因为蚊虫叮咬而伸手抓挠时,你可曾想象过有多少绝望之时向你祈祷的细胞因为你的『神迹』一现而获得拯救?没错,我们就是自己体内细胞所膜拜的那个『神』,拥有主宰其命运的力量,能随时感知他们的诉求,并在不经意间带给他们震撼的神迹。

那么,回到我们自己视角,人类所膜拜的『神』又是谁呢

同理,它显然并不是与我们在同一个(空间及时间)尺度的存在,兴许,它就是整个银河系。当我们仰望星空时,又怎知它不会正注视着我们?高速流星击毁一颗恒星,在银河系看来也许就像是白细胞在清理一个病变的组织。而在人类眼中完全由牛顿定律所左右的宇宙,又何尝不是那个在细胞眼中纯粹由牛顿定律所左右的人体呢?我们之所以很难观察到『神』的行为,极少见到神迹的展示,乃是因为人类有限的生命尺度在银河系的眼中,就像是我们体内一个眨眼间就已走完生命历程的细胞,能有幸见证神迹的概率,可能要以百年甚至千年来计了。

那么问题来了,牛顿定律的决定论(已知初始位置和速度的系统,其未来可被完全推演)让我们相信天体的运动是完全规律的,那么又如何解释人类的自我意识呢?到底是牛顿定律蒙蔽了我们对『神』的认知,还是说意识背后所谓的『自由意志』只是一个美好的谎言,命运其实早已注定?

意识

有一部电影叫做《黑客帝国》,它彻底颠覆了教科书传授给我的世界观;有一部科幻小说叫做《三体》,它重新开启了我自己的世界观探索之路。

《三体》里面所描绘的两个文明之间的战争,备战数个世纪,决战于转瞬间,残酷无情却不失客观。只因两个位于不同发展阶段的文明间的战争胜负,根本就没有任何悬念。类似的道理,两个不同尺度之间的意识,有着完全不同的认知能力和思维形态,低尺度的生命体压根无法理解高尺度的意识。就像人类可以轻松的研究清楚细胞的行为模式(而且我们从未认为那也可以算作『意识』),但却焉知彗星撞地球不是银河系的有意为之?

虽然大跨度的尺度超乎了人类的认知和理解能力,但由于尺度是连续的,我们可以运用归纳法从一系列逐渐超出或低于自身尺度的对象上大致推演出意识往高和低尺度发展的变化趋势。

从个体到一些人组成的『群体』,我们看到群体行为对个体意志的影响和抑制(比如路西法效应、人格面具);从群体到社会,我们看到了社会是如何决定人性的(比如人性与道德);从社会再到物种,我们看到了物种之间的竞争和共生现象(比如食物链与圈养法则、黑暗森林)。随着尺度的升高,越低尺度的组成部分,其个体意志越是无足轻重并受到压制,因为数量和尺度共同使得个体丧失了影响能力,只有服从于更高尺度的意志,才能获得生存的机会。

反过来,更高尺度的意识,或者说『自由意志』,却恰恰是由低尺度个体之间的微观扰动所形成的。正如人类的力量可以改变行星的命运,脑细胞的个体行为相互作用,也在左右你的意识,成为人类所自我认知的『自由意志』。因此,随着我们往更低尺度归纳,形成细胞意识的或许就是量子力学的不确定性原理所带来的扰动,这就足以让人类意识的『自由意志』不致被牛顿定律的决定论所责难了,

命运

在《黑客帝国》中,Smith讲过这样一段发人深省的台词:

There’s no escaping reason, no denying purpose, for as we both know, without purpose we would not exist. It is purpose that created us, purpose that connects us, purpose that pulls us, that guides us, that drives us; it is purpose that defines us, purpose that binds us.
凡事都有无法逃避的因由,无法抗拒的使命,我们都心知肚明,没有使命就没有我们的存在。是使命创造了我们,是使命维系着我们,是使命在牵引我们、指导我们、驱使我们;也是使命诠释着我们,纠结着我们。

当初我并未完全理解这段话,时至今日,才感受到这一席话背后深刻的含义。每一个个体的存在,都被赋予了『使命(Purpose)』,就像是我们体内的细胞,虽然生而平等(皆由『干细胞』始),但却早已注定最终会生长为不同的器官和组织。使命虽然无法决定命运,但却在深刻的影响着命运,我们人生之中所发生的很多事情,都有背后的原因(no escaping reason)。顺应使命的召唤,将会遇到更少的阻力,有更大概率获得生存;而未能认清使命,或是抗拒使命,则往往会命运多舛。当然,也有『人定胜天』的个例,但它要么像癌细胞一样同化并扩散至整个银河系,摧毁自己的主宰者;要么成为一个幸运的变异,为高尺度的生命进化作出贡献。每个人都有作出选择的权利,但认清使命,才能让你的人生道路更加平坦。

如何识别自己的使命,这对我们每个人来说都是一个超常规的挑战。很不幸,并不是每个人的使命都是崇高的,高尺度生命的构成部分,很可能绝大多数都是平凡而短暂的,像神经或心肌细胞这样重要和长寿的只是极少数。很多人在经历沧桑之后才终于明白自己此生的意义,有些人在年轻时就认清并坚定的走上完成使命的道路,还有人终其一生都在与命运抗争,到临终都无法认清自己的使命……

每个人都有自己的天赋,它就像DNA一样,是使命的最初表达;每个人都有难以改变的环境,它就像胚囊,是使命的强烈暗示;每个人都在不断书写命运的『境遇』,它们就像特化细胞,是使命的阶段启示。发掘自己的天赋、认清所处的环境、理解自己的过往,才能不断贴近自己的使命,掌握自己的命运,达成自身与诸神的共赢。

可穿戴时代的蓝牙耳机

9月5日的Motorola发布会,两款手机+两款可穿戴设备,其中除了众所周知的Moto X/G继任者及Moto 360之外,还有一款神秘的蓝牙耳机。为何蓝牙耳机也能会与另外三款明星设备相提并论?Motorola打算如何重塑人们印象中的蓝牙耳机呢?

蓝牙耳机虽然是历史最悠久的可穿戴设备之一,但却似乎在近年来的可穿戴设备大潮中被人们熟视无睹到几乎遗忘。很重要的一个原因是,无论用户还是厂商,都还没有改变蓝牙耳机就是用来『打电话』这种固化的认知。虽然最近几年蓝牙技术及Profile规范的发展衍生出了基于A2DP的立体声蓝牙耳机,这两年特别火的Apt-X codec又将蓝牙耳机的音乐音质发挥到了极致(尽管仍然很难讨好专业的耳膜)。但蓝牙耳机还是没能跳出『打电话』、『听音乐』这种完全无法挑动人们兴奋神经的用途。

智能手表的出现,让人们意识到,原来不用掏出手机就能解决很多短平快的需求。但是随着一大波智能手表已然进入了科技激进者的日常生活,他们才发现美好的愿望与现实的境况总是有那么一些不和谐的落差。在户外环境下效果一塌糊涂的语音识别让语音交互的美妙体验大打折扣,缺少Google Now式对话互动的搜索结果卡片让人抓狂于手表的袖珍屏幕,手腕上来电提示那个尴尬的『接听』选项让人恨不得全都拒接,比手机还不如的单手操控性让有车族更加无爱……

这一切不如意的源头,在于智能手表输入输出能力上的较大局限性:有限的显示和触控面积、较弱的麦克风收声能力、音频输出的缺乏。因此,即便是Android Wear在交互设计上全面向语音倾斜,但却仍然只能提供一个没有语音反馈的残缺体验。试想,当智能手表 + 蓝牙耳机,这一切将获得怎样的改观?骨传导技术可以有效保证户外吵杂环境下的语音识别,完整的对话式Google Now体验甚至让你都无须抬起手腕,无论来电还是通知都可以从容的选择接听或朗读,无论是驾车还是搭乘公交都再无顾虑…… 且慢,不知你是否意识到,上述的这些体验其实全都无需智能手表的存在!没错,与当代智能手机演进契合的蓝牙耳机,可能在很多方面远比智能手表对普通用户更加实用和便捷。

但是,为什么迄今为止都没有厂商推出这样一款『现代化』的蓝牙耳机呢?除了设计思考的惯性之外,技术上的限制和困难也成为大部分传统蓝牙耳机厂商迈不过的门槛。

首先是长时间佩戴的舒适性和美观度,传统的蓝牙耳机都是为短时佩戴而设计的,普遍无法舒适的长期佩戴。新生代的LG Tone系列、Moto Buds和三星刚刚发布的Gear Circle都尝试在不断改善持续佩戴的舒适度,但是迄今为止都没有哪一款蓝牙耳机的重量能降到项链类配饰的水平。从我个人佩戴LG HBS-730的感受来说,仍然无法完全习惯于长时间佩戴,最主要的因素就是重量。美观度也是蓝牙耳机长期佩戴的一项主要阻碍,LG Tone系列的最新一代已经设计为耳机线可完全隐藏收纳,而Gear Circle则主打磁性闭合为类似项链的效果。但即使是目前这一众看重设计的新生代蓝牙耳机,也都普遍显得太过臃肿。

其次是操控的便捷性。Google一直主打的『OK Google』开启语音交互的体验,在蓝牙耳机上还存在着较大的技术障碍,耗电是目前难以克服的问题。Moto X支持Touchless Control的大前提是它2200mAh的电池,而且当手机在封闭空间里时语音侦听是关闭的。对于电池不到200mAh的蓝牙耳机而言,这仍然是一个较大的耗电负担。如何有技巧性的解决这一瓶颈,是一个产品与技术的综合挑战。

尽管有很多困难,但我相信Motorola将带给我们一个满意的答案,拭目以待吧。

Motorola 将带给我们一款怎样的3D手机?

9月4日的Motorola发布会,吸引眼球的不光是闪耀的明星Moto 360,更有颇让人期待和好奇的Moto X后继者。日前@evleaks在Twitter上用一张Moto X+1的高清谍照作为其收山之作,想必定有其出众之处。

【谜题】

很快就有媒体指出谍照正面多出来的开孔很可能是类似Amazon Fire Phone的人眼追踪传感器。更有新的传言指出Moto X+1将使用视差障壁技术的3D显示屏。假使真如其言,我们不禁疑惑,在HTC、LG和Amazon先后试水3D视觉手机都未能取得成功的今天,Moto又将拿出什么样的杀手锏重新证明3D视觉之于手机还能取得成功呢?

纵观过去那些不太成功的3D手机,虽然各有缺憾,但最重要的一个共通点无疑是 软件支持的匮乏 。没有App和游戏支撑的3D显示,也就只能当为日常使用的一个陪衬,偶尔增加一点气氛罢了。那么,在Google接手并改造后,今时这个擅长软硬结合的Motorola将如何破解这个难题呢?

【钥匙】

我的预测是,关键的钥匙还在Google手中,它早已摆在众人眼前,却鲜有人注意到它就在那里。

Elevation in Material Design

今年Google I/O上大放异彩的Material Design,尤其强调Paper的立体感,通过赋予界面中各部分不同的 『海拔(Elevation)』 ,让其跃然于纸上。这不正是3D的效果么?柔性阴影(Soft Shadow)的使用无非是为了在缺少3D展现能力的传统2D屏幕上营造出3D的既视感,显然没有直接展现3D景深来的真切了。

Soft Shadow in Material Design

再从实现层面来看,『海拔』这个属性,被深刻的植入Android L之后UI元素的基石——『View』之中。任何View,都具有elevation值。也就是说,一个普通的App开发者,只要遵循Material Design的基本原则设计UI,自然会赋予界面的组成部分不同的海拔,他们也许压根没有想过要针对3D视觉作什么优化,但却在潜移默化中被Google代入了3D视觉设计的理念。带有阴影的视觉效果,反而只是3D UI在传统屏幕上的一种视觉近似。

【线索】

天衣无缝的计划,也有漏出马脚的时候。Google的Android开发者博客已经急不可耐的透露出了太多的信息。

这篇文章通过着重介绍Google I/O 2014 App的Material Design实践,试图让开发者理解前后两版不同设计细节的差异性。但在当时读完这篇文章后,我却感觉其推崇的改动后的新设计有一种说不出的违和感。因为我在Google I/O会议期间就频繁使用了第一版,在我的手机上,其原本设计的视觉体验非常好(尤其是上滑时透明效果的淡出过渡),但升级到新版本后,那些改动的细节却给人一种强扭的体验。当然,对于没有深刻体验过第一版的使用者来说,很难体会到这种变迁,通常会自然的接受目前的设计。

Google I/O App in 3D

当两件事情联系在一起时,就豁然开朗了。很可能在第一版Google I/O App设计时,还没有可用的3D手机,所以自然是为2D展现而设计。当后来Android设计人员拿到尚未公开的3D手机后(不一定是Moto X+1,说不定是Nexus X呢),重新针对3D展现效果进行了细节的优化。于是乎那几个在2D下看起来违和的调整,一放到3D视角中,简直就是量身定制的优化~

【新纪元】

如此看来, Android L或许在深思熟虑之后慎重的开启了整个Android生态的3D纪元。 不仅基本界面设计3D化,连很多原本平面的系统UI,也改为了3D效果,比如多任务切换UI(强3D视觉)、通知栏(景深明显的双层设计)、锁屏界面(注意左右滑动时下方图标的景深变换)。而在2D下效果平平的波纹状(Ripple)反馈动画,可能让人觉得多少有那么一点『败笔』之嫌,但换到3D下,倘若真如Google I/O Keynote中所展现的3D动感波纹,那将是何等的惊艳!

如果 Moto X+1 还不足以撬动整个硬件生态,那么下一代的Nexus手机倘若也如目前众多传闻所言,乃Motorola代工,那么就很可能是Moto X+1的姊妹作,其标志性的3D视觉也将成为下一代主流手机的指引设计了。

距离9月4日只有1周多的时间了,就让我们拭目以待Moto X+1能否开启Android生态的3D纪元吧!

对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();
}
« Older posts

Copyright © 2014 Oasis Feng

Theme by Anders NorenUp ↑