好东西, 写的挺幽默的,转载
深入Android |
【一】 —— 序及开篇 【二】 —— 架构和学习
虽贵为富二代,但Android要是没任何可圈点的地方,开不过70迈,在玲琅满目的手机平台竞争中,充其量也就做几个俯卧撑打一桶酱油,然后被落的远远的。说到底,出来混,靠的还是技术。 架构 从SDK文档中,偷来一幅Android平台的架构图,如上。在整个架构最底层红彤彤的部分,是Linux Kernel在移动平台的一个移植,它隐藏了硬件、网络等相关的细节,为上层提供了一个相对纯洁的统一接口。除非要做的是Android到不同设备的移植工作,否则对于大部分普通开发者而言,基本上是远观而不必亵玩的。Google一直强调,Android的底层实现异常NB,可移植性超强,暂没有功夫研读,实属遗憾。 靠上一层,是一些核心的和扩展的类库,它们都是原生的C++实现。在这一层,你可以看到很多熟悉的面孔,一如SQLite、WebKit、OpenGL,开源的力量与贡献由此可见。如果,该层类库需要被上层函数调用,就必须要通过JNI的导出相应的接口函数,否则就只能在层次内部自个把玩。 也是在这一层次上,还有为上层Java程序服务的运行时。Dalvik虚拟机,是Android的Java虚拟机,之所以不采用J2ME的虚拟机,一方面是因为J2ME的设计是为了低端机器而优化,而Dalvik则是为了高端一些的机器进行优化,提供更好的性能。另一方面,从商业角度来看,必须绕开J2ME虚拟机,Android才能彻底解放,想怎么开源就怎么开源,不再需要考虑License的问题。 再往上,终于有Java出没了。首先是框架层,这里包含所有开发所用的SDK类库,另外还有一些未公开接口的类库和实现,它们是整个Android平台核心机制的体现。 而在最上面,就是应用层了,系统的一些应用和第三方开发的所有应用都是位于这个层次上,也许要纠结两者的差别,就是系统应用会用一些隐藏的类,而第三方的应用,总是基于SDK提供的东西来搞。 一般来说,Android开发,就是在SDK的基础上,吭哧吭哧用Java写应用。但自从有了NDK,一切有了写小变化。NDK的出现意味着,最上面应用层的内容,可以穿越Java部署的框架层,直接和底层暴露出来的,或者自行开发的C++库直接对话,当然在这些库中需要包含JNI的接口。 人说,这就不是Android也可以用C++开发应用么,但其实,这样的说法不够确切,纯C++应用,是无法被接受的。因为在Android中,大量的核心机制部署在框架层,它们都是用Java实现的,比如控件库,Activity的调度之类的。因此,没了界面,没了调度,还是只用C++做类库比较合适,否则一切都乱了套了。 特征 基于这样的架构,Android有很多的设计显得很有意思。纵览整个SDK和核心机制的设计,工整漂亮,是Android给人的第一感觉。为了说明这一点,找一个反面教材是很有必要的,Symbian同学毫无悬念的担当这个伟岸的角色。 写Symbian程序,感觉就像是在玩一个猜谜游戏。哪怕你是一个Symbian老手,当需要用到Symbian中某块陌生功能的时候,你可能依然束手无策。你往往需要猜并反复找寻,在这里我需要使用哪一种奇巧淫技呢,是该臆想某些事件,还是应该用一个神秘的UID寻找某个特定应用,诸如此类。 而做Android应用的时候,就像是做高考模拟试题,题看上去不一样,解答模式摸清楚,就一通百通,一了百了。监听某个系统事件,查一下SDK就好;访问某个应用的数据,看看它有没有提供Content Provider就可以。所有的一切,都是按套路出牌,只要你了解了套路,再陌生的牌也可以看得懂,出的顺。人说武林高手,都应该是无招胜有招,而一个好的应用框架,也应该做到举重若轻,可触类旁通。 而Android框架最文采飞扬的一点,就是引入了Mash-Up的思想。所谓Mash-Up,就是把写应用搞成搭积木,要出效果的时候,东家一块西家一块现场拼起来就好。这里面关键有两点,一个是模块化,另一个就是动态性。所谓模块化,就是一个应用的功能要明确的被封成一个个边界清晰的功能点,每一个功能点都像是一个黑盒,由预先定义的规则描述出其交互方式;而动态性,就是这些独立的模块能够在运行的时候,按照需求描述,连接在一起,共同完成某项更大的功能。在这两点上,Android都做得非常出色。 站在可Mash-Up构造应用这一点去看其他的一些Android中的核心功能设计,就显得很有章可循了。比如为什么要把文件私有化,为什么要让进程被托管,等等(当然也可以站在别的角度看出不同的效果,视角不同,视野自然不同...)。 在UI机制方面,Android也有很不错的表现。它采取xml格式的资源文件,描述所有界面相关的内容。资源文件不是什么新东西了,xml格式也是老调重弹,但可贵的是Android做的更为的丰富和彻底,基本把界面相关的逻辑,全部从代码中剥离到了资源文件中,和Symbian那四不像的资源文件相比,真是强大了不知多少倍了。 Android学习入门 不积跬步,无以至千里。 -- 《劝学》 说,万事开头难。想开始Android的开发,最重要的应该是先把马步扎稳,套路摸清楚,后面的事情就顺当多了。打开怀抱,拥抱Android,也许可以先做下面这些事。 开发环境 辣手摧花成性的GFW,无情的把Android开发者官网关在了墙外。不过没关系,猛击这里,同样可以殊途同归(Shit,一直在用代理,刚试了下,发现竟然也被盾了,如果不行,那就只能*了...)。 如果旅途顺利,你可以在路径sdk/index.html下找到安装说明,成功配置好Android的开发环境(【注】:在以后,如果要给开发者页面上的链接,都会给一个像sdk/index.html这样的相对路径,你可以在前面加上官网地址,或者本地SDK的doc地址拼凑成完整的路径,在一个盾牌横行的朝代,只能用这样委屈求全的方法保证能更好的使用...)。 在2.0之前,每一次版本更新,你都要自己去下个全新的SDK,然后按照说明,小心翼翼的一步步修改eclipse的设置,甚是麻烦。在2.0后,这个模式有所改善,你会先下到一个类似于下载器的插件,通过它可以来管理和升级SDK,不仅简化了整个升级模式,还使得你可以更好的在各个不同的SDK版本间游走,利国利民。 Eclipse + ADT(Android Development Tool),是正牌的Android开发环境。你可以在Windows,Linux,Mac下做开发,甚为*。比之Symbian的开发环境,ADT显得尤为强大,它对SDK提供的一堆优秀的命令行工具进行了UI上的封装,提供了图形界面(命令行控当然同样幸福,具体参见:guide/developing/tools/index.html)。通过ADT,你可以用运行和管理模拟器,使用调试器进行调试,过滤和查看Log,浏览模拟器上的文件信息,模拟拨号、短信等手机才有的事件,等等。 文档 我知道,有很多人在学习一个新平台开发的时候,都习惯去买一些《xxx 21天精通》之类的书籍。但其实,最好的入门学习资料,就是SDK文档。因为只有做平台的自己,才能最了解平台中的各个玄机,各方面的轻重缓急,从而能够更好的对症下药药到病除。 在Android的SDK中,guide/index.html是由浅入深的教学文档,reference/packages.html是标准的API文档。对于教学文档,我的意见是,一字不拉的通读一遍甚至多遍,至少做到能对Android摸着头脑,并且碰到问题的时候,能够快速想起在哪里可以找到,回来深刻阅读。 而API文档方面,Android做的算是还不错了。基本上每个类,每个接口,都有标准而详尽的说明,在一些尤为重要的类中,还具有大量的学习性的内容,不和Symbian似的,有太多的太监类,只有光秃秃的一个函数,一行文档说明都没有。整个文档结构是按照Java包来组织的,本身Java包命名的结构性和可读性很强,找起来也颇为方便。 很多人对SDK文档有抵触情绪,我想,有两方面的原因。一则是SDK文档普遍缺少文学性,麻木不仁的八股文,难以下咽。Android在这方面做得算是乏善可陈,虽然算不上文采华丽,但还是挺适合阅读的。另一则,就会是语言方面的原因了。SDK文档多为英语,偶尔像MSDN这样有中文的,也停留在机器翻译的水平上,阅读起来颇为难受。特地在网上搜了下,找到一些翻译SDK的中文文档,比如这里。虽然是基于1.5 r1版本SDK所著,稍显过时,但翻译的还是有小用心的,作为辅助,也不失为一份好资料,特代表广大看官向这些为人民福利着想的同志致敬。 Tutorials 光说不练假把式,除了读,在入门阶段,写也是一项不能少的运动。同样是在SDK中,Android提供了一组Tutorials和一些列的Samples,详见:resources/index.html。 Tutorials很简单,Hello World只是在教你如何在eclipse中,在ADT的帮助下,创建一个Android项目。相比之下,Hello Views复杂了些,它集中展示了几种标准的Android Layout 样式是如何构建的,很多时候,你都是在这些样式下扩展所需的UI。 Hello Localization,是教你如何使用资源的,做完这个,就可以了解Android的资源有多杀~。最后收官的是一个更为完整的Notepad Tutorial,它展示了很多Android的核心机制,比如基于Intent的Activity整合,Activity的生命周期等。迈过这个Tutorial,欢迎你,进入Android的大门。 当然,做完Tutorials,对于Android而言,只是管中窥豹略见一斑。在SDK中,还提供了一系列的Samples。可以根据自己的需求,挑选合适的Sample编译运行和学习。但其中,有一个是不论你做什么,都需要必看必读必熟悉的,就是API Demos。在这个Sample中,集中展示了Android重点功能的API使用,把这个Sample用熟悉,需要做什么的时候过去找一下就可以很快的入手了。 源码 到这里,很多看官一定很不屑,前面所谓的学习入门介绍,只不过是围着SDK打转。其实,事实也是如此,SDK中包含的内容是真的非常重要,我只是期望通过一些简短的介绍,激起一些初学者的重视,如是而已。 当然,SDK每一个平台都有,没什么稀罕的。但Android有另一个非常稀罕而值钱的看家法宝,就是源代码。从Android Source的主站上:http://source.android.com/,你可以获得整个平台的源码以及相关介绍。非常苦口婆心的期望大家都去down一份源码放在机器上,哪怕你不需要进行修改编译,放在机器上当百科全书也是远胜于任何一本Android教学书籍的。本系列文章后续很多内容,都是从源码中学习到的一些浅薄见识。 对于大部分开发者都有学习价值的源码,主要在源码的frameworks和packages目录下。前者包含的是平台核心的一些实现,比如你需要自定义一个控件,也许你就可以翻到一个系统控件的实现中去,看看它是怎么来做的。后者包含一些系统的应用实现,比如你想做个播放器,也许你可以先去参考参考系统自带的是具体怎么做的。这样的实现,即便不算是最华美,至少也是最标准,其价值不容小视。 另外,你也可以把它当个代码库来使,不会使用某个类,grep一把,也许就能获得一份最漂亮的Sample。当然,如果你有时候对某些系统机制表示费解,抑或有一些bug不知道源头在哪,都可以跟着源码顺藤摸瓜的搞清楚。这样的好东西,可不是每个平台都能够享用的。 其他 论坛,其实对于开发和学习都是很重要的资源。毕竟,所有的资料都是死的,只有人是活得,能够最大限度的因地制宜解决问题。 只不过,标准的官方论坛,放在Google Group上,已经惆怅的被盾了。中文论坛方面,没有特别优秀和活跃的,这一方面是由于Android的发展现状还不算很磅礴,另一方面是由于Android的开发相对于Symbian而言,奇技淫巧少了很多,没有那么多好问的。也许你可以去去csdn这样的传统论坛,或者eoe这样专门的论坛。有的时候,还是多少能获得一些帮助的。 书籍方面,真没有什么推荐,豆瓣上搜索一下,你可以看到,目前的书籍,基本上还是集中在SDK使用层面上,很少有解析的很透彻,做的很深入的。而SDK的使用,看SDK的文档就足够了,如果实在对e文不感冒,买一两本评价不太差的中文书籍,放着翻翻也还是挺好。 更进一步,也许可以读读一些经验性的文档,去Google Code上搜索一些代码回来看看。比如,SDK文档中,有个经验性文档的集合:resources/articles/index.html,就可以翻看一下。 最后,更多的一切还需要自己在工程和思考中,慢慢总结。相信,好的代码,会垂青一个勤于动手和思考的人。 【三】 —— 组件入门
组件(Component),在谈及所谓架构和重用的时候,是一个重要的事情。很多时候都会说基于组件的软件架构,指的是期望把程序做乐高似的,有一堆接口标准封装完整的组件放在哪里,想用的时候取上几个一搭配,整个程序就构建完成了。 在开篇的时候就在说,Android是一个为组件化而搭建的平台,它引入所谓Mash-Up的概念,这使得你在应用的最上层,想做的不组件化都是很困难的一件事情(底层逻辑,好吧,管不了...)。具体说来,Android有四大组件四喜丸子:Activity、Service、Broadcast Receiver、Content Provider。 Activity 做一个完整的Android程序,不想用到Activity,真的是比较困难的一件事情,除非是想做绿叶想疯了。因为Activity是Android程序与用户交互的窗口,在我看来,从这个层面的视角来看,Android的Activity特像网站的页面。 首先,一个网站,如果一张页面都没有,那...,真是一颗奇葩。而一张页面往往都有个独立的主题和功能点,比如登录页面,注册页面,管理页面,如是。 在每个页面里面,会放一些链接,已实现功能点的串联,有的链接点了,刷,跑到同一站点的另一个页面去了;有的链接点了,啾,可能跳到其他网站的页面去;还有的链接点了,恩...,这次没跑,但当前页面的样子可能有所变化了。这些模式,和Activity给人的感觉很像,只不过实现策略不同罢了,毕竟Android这套架构的核心思想,本身就来自源于Web的Mash-Up概念,视为页面的客户端化,也未尝不可。 Activity,在四大组件中,无疑是最复杂的,这年头,一样东西和界面挂上了勾,都简化不了,想一想,独立做一个应用有多少时间沦落在了界面上,就能琢磨清楚了。从视觉效果来看,一个Activity占据当前的窗口,响应所有窗口事件,具备有控件,菜单等界面元素。从内部逻辑来看,Activity需要为了保持各个界面状态,需要做很多持久化的事情,还需要妥善管理生命周期,和一些转跳逻辑。对于开发者而言,就需要派生一个Activity的子类,然后埋头苦干上述事情。对于Activity的更多细节,先可以参见:reference/android/app/Activity.html。后续,会献上更为详尽的剖析。 Service 服务,从最直白的视角来看,就是剥离了界面的Activity,它们在很多Android的概念方面比较接近,都是封装有一个完整的功能逻辑实现,只不过Service不抛头露脸,只是默默无声的做坚实的后盾。 但其实,换个角度来看,Android中的服务,和我们通常说的Windows服务,Web的后台服务又有一些相近,它们通常都是后台长时间运行,接受上层指令,完成相关事务的模块。用运行模式来看,Activity是跳,从一个跳到一个,呃...,这有点像模态对话框(或者还像web页面好了...),给一个输入(抑或没有...),然后不管不顾的让它运行,离开时返回输出(同抑或没有...)。 而Service不是,它是等,等着上层连接上它,然后产生一段持久而缠绵的通信,这就像一个用了Ajax页面,看着没啥变化,偷偷摸摸的和Service不知眉来眼去多少回了。 但和一般的Service还是有所不同,Android的Service和所有四大组件一样,其进程模型都是可以配置的,调用方和发布方都可以有权利来选择是把这个组件运行在同一个进程下,还是不同的进程下。这句话,可以拿把指甲刀刻进脑海中去,它凸显了Android的运行特征。如果一个Service,是有期望运行在于调用方不同进程的时候,就需要利用Android提供的RPC机制,为其部署一套进程间通信的策略。 Android的RPC实现,如上图所示(好吧,也是从SDK中拿来主义的...),无甚稀奇,基于代理模式的一个实现,在调用端和服务端都去生成一个代理类,做一些序列化和反序列化的事情,使得调用端和服务器端都可以像调用一个本地接口一样使用RPC接口。 Android中用来做数据序列化的类是Parcel,参见:/reference/android/os/Parcel.html,封装了序列化的细节,向外提供了足够对象化的访问接口,Android号称实现非常高效。 还有就是AIDL (Android Interface Definition Language) ,一种接口定义的语言,服务的RPC接口,可以用AIDL来描述,这样,ADT就可以帮助你自动生成一整套的代理模式需要用到的类,都是想起来很乏力写起来很苦力的那种。更多内容,可以再看看:guide/developing/tools/aidl.html,如果有兴致,可以找些其他PRC实现的资料lou几眼。 关于Service的实现,还强推参看API Demos这个Sample里面的RemoteService实现。它完整的展示了实现一个Service需要做的事情:那就是定义好需要接受的Intent,提供同步或异步的接口,在上层绑定了它后,通过这些接口(很多时候都是RPC的...)进行通信。在RPC接口中使用的数据、回调接口对象,如果不是标准的系统实现(系统可序列化的),则需要自定义aidl,所有一切,在这个Sample里都有表达,强荐。 Service从实现角度看,最特别的就是这些RPC的实现了,其他内容,都会接近于Activity的一些实现,也许不再会详述了。 Broadcast Receiver 在实际应用中,我们常需要等,等待系统抑或其他应用发出一道指令,为自己的应用擦亮明灯指明方向。而这种等待,在很多的平台上,都会需要付出不小的代价。 比如,在Symbian中,你要等待一个来电消息,显示归属地之类的,必须让自己的应用忍辱负重偷偷摸摸的开机启动,消隐图标隐藏任务项,潜伏在后台,监控着相关事件,等待转瞬即逝的出手机会。这是一件很发指的事情,不但白白耗费了系统资源,还留了个流氓软件的骂名,这真是卖力不讨好的正面典型。 在Android中,充分考虑了广泛的这类需求,于是就有了Broadcast Receiver这样的一个组件。每个Broadcast Receiver都可以接收一种或若干种Intent作为触发事件(有不知道Intent的么,后面会知道了...),当发生这样事件的时候,系统会负责唤醒或传递消息到该Broadcast Receiver,任其处置。在此之前和这以后,Broadcast Receiver是否在运行都变得不重要了,及其绿色环保。 这个实现机制,显然是基于一种注册方式的,Broadcast Receiver将其特征描述并注册在系统中,根据注册时机,可以分为两类,被我冠名为冷热插拔。所谓冷插拔,就是Broadcast Receiver的相关信息写在配置文件中(求配置文件详情?稍安,后续奉上...),系统会负责在相关事件发生的时候及时通知到该Broadcast Receiver,这种模式适合于这样的场景。某事件方式 -> 通知Broadcast -> 启动相关处理应用。比如,监听来电、邮件、短信之类的,都隶属于这种模式。而热插拔,顾名思义,插拔这样的事情,都是由应用自己来处理的,通常是在OnResume事件中通过registerReceiver进行注册,在OnPause等事件中反注册,通过这种方式使其能够在运行期间保持对相关事件的关注。比如,一款优秀的词典软件(比如,有道词典...),可能会有在运行期间关注网络状况变化的需求,使其可以在有廉价网络的时候优先使用网络查询词汇,在其他情况下,首先通过本地词库来查词,从而兼顾腰包和体验,一举两得一石二鸟一箭双雕(注,真实在有道词典中有这样的能力,但不是通过Broadcast Receiver实现的,仅以为例...)。而这样的监听,只需要在其工作状态下保持就好,不运行的时候,管你是天大的网路变化,与我何干。其模式可以归结为:启动应用 -> 监听事件 -> 发生时进行处理。 除了接受消息的一方有多种模式,发送者也有很重要的选择权。通常,发送这有两类,一个就是系统本身,我们称之为系统Broadcast消息,在reference/android/content/Intent.html的Standard Broadcast Actions,可以求到相关消息的详情。除了系统,自定义的应用可以放出Broadcast消息,通过的接口可以是Context.sendBroadcast,抑或是Context.sendOrderedBroadcast。前者发出的称为Normal broadcast,所有关注该消息的Receiver,都有机会获得并进行处理;后者放出的称作Ordered broadcasts,顾名思义,接受者需要按资排辈,排在后面的只能吃前面吃剩下的,前面的心情不好私吞了,后面的只能喝西北风了。 当Broadcast Receiver接收到相关的消息,它们通常做一些简单的处理,然后转化称为一条Notification,一次振铃,一次震动,抑或是启动一个Activity进行进一步的交互和处理。所以,虽然Broadcast整个逻辑不复杂,却是足够有用和好用,它统一了Android的事件广播模型,让很多平台都相形见绌了。更多Broadcast Receiver相关内容,参见:/reference/android/content/BroadcastReceiver.html。 Content Provider Content Provider,听着就和数据相关,没错,这就是Android提供的第三方应用数据的访问方案。在Android中,对数据的保护是很严密的,除了放在SD卡中的数据,一个应用所持有的数据库、文件、等等内容,都是不允许其他直接访问的,但有时候,沟通是必要的,不仅对第三方很重要,对应用自己也很重要。 比如,一个联系人管理的应用。如果不允许第三方的应用对其联系人数据库进行增删该查,整个应用就失去了可扩展力,必将被其他应用抛弃,然后另立门户,自个玩自个的去了。 Andorid当然不会真的把每个应用都做成一座孤岛,它为所有应用都准备了一扇窗,这就是Content Provider。应用想对外提供的数据,可以通过派生ContentProvider类,封装成一枚Content Provider,每个Content Provider都用一个uri作为独立的标识,形如:content://com.xxxxx。所有东西看着像REST的样子,但实际上,它比REST更为灵活。和REST类似,uri也可以有两种类型,一种是带id的,另一种是列表的,但实现者不需要按照这个模式来做,给你id的uri你也可以返回列表类型的数据,只要调用者明白,就无妨,不用苛求所谓的REST。 另外,Content Provider不和REST一样只有uri可用,还可以接受Projection,Selection,OrderBy等参数,这样,就可以像数据库那样进行投影,选择和排序。查询到的结果,以Cursor(参见:reference/android/database/Cursor.html )的形式进行返回,调用者可以移动Cursor来访问各列的数据。 Content Provider屏蔽了内部数据的存储细节,向外提供了上述统一的接口模型,这样的抽象层次,大大简化了上层应用的书写,也对数据的整合提供了更方便的途径。Content Provider内部,常用数据库来实现,Android提供了强大的Sqlite支持,但很多时候,你也可以封装文件或其他混合的数据。 在Android中,ContentResolver是用来发起Content Provider的定位和访问的。不过它仅提供了同步访问的Content Provider的接口。但通常,Content Provider需要访问的可能是数据库等大数据源,效率上不足够快,会导致调用线程的拥塞。因此Android提供了一个AsyncQueryHandler(参见:reference/android/content/AsyncQueryHandler.html),帮助进行异步访问Content Provider。 在各大组件中,Service和Content Provider都是那种需要持续访问的。Service如果是一个耗时的场景,往往会提供异步访问的接口,而Content Provider不论效率如何,都提供的是约定的同步访问接口。我想这遵循的就是场景导向设计的原则,因为Content Provider仅是提供数据访问的,它不能确信具体的使用场景如何,会怎样使用它的数据;而相比之下,Service包含的逻辑更复杂更完整,可以抉择大部分时候使用某接口的场景,从而确定最贴切的接口是同步还是异步,简化了上层调用的逻辑。 配置 四大组件说完了,四大组件幕后的英雄也该出场了,那就是每个应用都会有一份的配置文件,名称是AndroidManifest.xml,在工程的根目录下。在这个配置文件中,不仅会描述一些应用相关的信息,很重要的,会包含一个应用中所有组件的信息。如果你派生Activity或者Service实现了一个相关的类,这只是把它组件化的第一步,你需要把这个类的相关信息写到配置文件中,它才会作为一个组件被应用到,否则只能默默无闻的黯淡度过余生。 摆了一幅图出来,这次不是偷来的,是敝帚自珍原创,所以没有意外的画的很丑,但基本还是可以体现出一些意思。在In Others的部分,这里是一般平台应用之间通信和交互的模型,每个应用都有很强烈的应用边界(往往表现为进程边界...),App 1的还是App 2的,分得很是清楚。每个应用内部,都有自己的逻辑去切分功能组件,这样的切分通常没有什么标准,率性而为。应用间的交互逻辑也比较零散,App 1与App 2交互,往往需要明确知道对方应用的具体信息,比如进程ID,进程名称之类的,这样使得应用和应用之间的联系,变得很生硬。而上层应用和系统应用的通信,往往有很多特定的模式,这种模式,很可能是无法直接应用在普通应用之间的,换而言之,系统应用是有一定特殊性的。 重点,在图的下半部,描述的是Android的应用情形。在Android中,应用的边界,在组件这个层面,是极度模糊,什么进程、什么应用,都可以不必感知到。举个例子,App 1,实现了A和B两个组件,App 2,实现了C这个组件。A和C,都想使用B这个组件,那么它们的使用方式是完全一致的,都需要通过系统核心的组件识别和通信机制,找到和使用组件B。A,虽说和B是一个娘胎里蹦出来的,很不好意思,没有任何特殊的后面和捷径,还是要跑规矩的途径才能用到,一片和谐社会的景象油然而生。 在Android中,所有组件的识别和消息传递逻辑都必须依赖底层核心来进行(通信可以没有底层核心的参与,比如一旦Service找到了,就可以和它产生持久的通信...),没有底层核心的牵线搭桥,任何两个组件都无法产生联系。比如一个Activity,跳到另一个Activity,必须要向底层核心发起一个Intent,有底层解析并认可后,会找到另一个Activity,把相关消息和数据传给它。一个Activity想使用Content Provider中的数据,必须通过底层核心解析相关的uri,定位到这个Content Provider,把参数传递给它,然后返回Activity需要的Cursor。这样的设计,保证了底层核心对所有组件的绝对掌控权和认知权,使得搭积木似的开发变成可能。 为了,使得核心系统能够完整的掌握每个组件的信息,这就需要配置文件了。配置文件,就是将组件插到底层核心上的这个插头。只有通过这个插头插在底层核心的插座上(不要乱想,非十八禁...),组件才能够发光发热,闪耀光芒。 组件的配置信息在我看来主要包含两个方面,一部分是描述如何认知。比如,Activity、Service、Broadcast Receiver都会有名字信息,和希望能够把握的Intent信息(姑且看成消息好了...),Content Provider会有一个描述其身份的uri。当其他组件通过这样的名字或者Intent,就可以找到它。 另一部分是运行相关的信息。这个组件,期望怎么来运行,放在单独的进程,还是和调用者一个进程,还是找相关的其他组件挤在同一个进程里面,这些内容,都可以在配置的时候来决定(调用者在这个约束范围内,有进一步的选择权...)。更多配置项,请参见:guide/topics/manifest/manifest-intro.html。 通过前续内容,也许可以帮助大家对Android组件有个初略的了解。但这些了解都还停留在静态层面,程序是个动态的概念,关于各个组件具体是怎么联系在一起的,如何手拉手运行起来完成一项功能的,这便是后话了。
【四】 —— 组件调用
Intent解析 基于组件的架构体系,除了有定义良好的组件,如何把这些组件组装在一起,也是一门艺术。在Android中,Intent(貌似通常译作:意图...),就是连接各组件的桥梁。 前段时间看同事们做Symbian平台的网易掌上邮(真的是做的用心,NB的一米,热情欢迎所有163邮箱的S60v3用户,猛点击之...),有个功能是为邮件添加附件,比如你想要通过邮件发送一副图片泡mm,可能需要有个很直观的方式从本地选一副珍藏美图,抑或是拿相机来个完美自拍。在Symbian中,这样的功能,都需要你用底层的API,自己一点点写。为了让选图片体验更好,可能需要做一个类似于图片浏览器之类的东西,为了把拍照做的更为顺畅,甚至需要实现从聚焦到调节亮度之类一整套的相机功能。 而其实呢,用户的手机中可能本身就装了其他的专业图片浏览器、相机等应用,这些应用已经非常出色好用,而用户也已然能很纯属使用它们,如果能进行调用,对邮箱的开发者和用户而言,都会是个更好的选择。但在Symbian这样残败的系统里,应用和应用之间的结合能力奇弱无比,想复用,基本比登天还难,作为开发者,只能忍住一次又一次的恶心,为了用户,做这些重复造*吃力不讨好的附加工作。 还好还好,在Android中,一切变得美好多了,它将开发者从接口和对象的细节中解救出来,让我们有更多精力投入到核心功能的开发中去。在Android中,如果你需要选个图拍个片,只需要构造一个描述你此项意愿的Intent,发送出去,系统会帮你选择一个能够处理该项业务的组件来满足你的需求,而不再需要纠结在具体的接口和实现上,Perfect World,便应如此。 Intent构成 Intent被译作意图,其实还是很能传神的,Intent期望做到的,就是把实现者和调用者完全解耦,调用者专心将以意图描述清晰,发送出去,就可以梦想成真,达到目的。 当然,这么说太虚了,庖丁解牛,什么东西切开来看看,也许就清晰了。Intent(reference/android/content/Intent.html),在Android中表现成一个类,发起一个意图,你需要构造这样一个对象,并为下列几项中的一些进行赋值: Action。当日常生活中,描述一个意愿或愿望的时候,总是有一个动词在其中。比如:我想做三个俯卧撑;我要看一部x片;我要写一部血泪史,之类云云。在Intent中,Action就是描述看、做、写等动作的,当你指明了一个Action,执行者就会依照这个动作的指示,接受相关输入,表现对应行为,产生符合的输出。在Intent类中,定义了一批量的动作,比如ACTION_VIEW,ACTION_PICK,之类的,基本涵盖了常用动作,整一个降龙十八掌全集。当然,你也可以与时俱进,创造新的动作,比如lou这样的。与系统预定义的相比,这些自定义动作的流通范围很是有限,除非做了非常NB的应用,大家都需要follow你,否则通常都是应用内部流通。 Data。当然,光有动作还是不够的,还需要有更确切的对象信息。比如,同样是泡这个动作,但泡咖啡,和泡妞,就差之千里了。Data的描述,在Android中,表现成为一个URI。用在内部通信中,可能描述是Content Provider用的形如content://xxxx这样的东东,抑或是外部的一个形如tel://xxxx这样的链接。总而言之,是能够清楚准确的描述一个数据地址的uri。 Type。说了Data,就必须要提Type,很多时候,会有人误解,觉着Data和Type的差别,就犹如泡妞和泡马子之间的差别一样,微乎其微。但其实不然,Type信息,是用MIME来表示的,比如text/plain,这样的东西。说到这里,两者差别就很清晰了,Data就是门牌号,指明了具体的位置,具体问题具体分析,而type,则是强调物以类聚,解决一批量的问题。实际的例子是这样的,比如,从某个应用拨打一个电话,会发起的是action为ACTION_DIAL且data为tel:xxx这样的Intent,对应的人类语言就是拨打xxx的电话,很具象。而如果使用type,就宽泛了许多,比如浏览器收到一个未知的MIME类型的数据(比如一个视频...),就会放出这样的Intent,求系统的其他应用来帮助,表达成自然语言应该就是:查看pdf类文档,这样的。 Category。通过Action,配合Data或Type,很多时候可以准确的表达出一个完整的意图了,但也会有些时候,还需要加一些约束在里面才能够更精准。比如,如果你虽然很喜欢做俯卧撑,但一次做三个还只是在特殊的时候才会发生,那么你可能表达说:每次吃撑了的时候,我都想做三个俯卧撑。吃撑了,这就对应着Intent的Category的范畴,它给所发生的意图附加一个约束。在Android中,一个实例是,所有应用主Activity(就是单独启动时候,第一个运行的那个Activity...),都需要能够接受一个Category为CATEGORY_LAUNCHER,Action为ACTION_Main的意图。 Component。在此之前,我们企图用Action,Data/Type,Category去描述一个意图,这是Android推荐,并期望大家在大多数时候使用的,这样模式在Android中称做Implicit Intents,通过这种模式,提供一种灵活可扩展的模式,给用户和第三方应用一个选择权。比如,还是一个邮箱软件,他大部分功能都好,就是选择图片的功能做的很土,怎么办?如果它采用的是Implicit Intents,那么它就是一个开放的体系了,手机中没有其他图片选择程序的话,可以继续使用邮箱默认的,如果有,你可以任意选择来替代原有模块完整这功能,一切都自然而然。但这种模式,也不是没有成本,需要付出的是一些性能上的开销,因为毕竟有一个检索过程。于是,Android提供了另一种模式,叫做Explicit Intents,就需要Component的帮助了。Component就是类名,完整的,形如com.xxxxx.xxxx,一旦指明了,一切都清晰了,找的到这个类(当然会是一个特定的子类...),成功,反之,失败。这个好处,自然是速度,适合在你明确知道这就是一个内部模块的时候,使用它。 Extras。通过上面的这些项,识别问题,基本完美解决了,剩下一个重要的问题,就是传参。Extras是用来做这个事情的,它是一个Bundle类的对象,有一组可序列化的key/value对组成。每一个Action,都会有与之对应的key和value类型约定,发起Intent的时候,需要按照要求把Data不能表示的额外参数放入Extras中(当然,如果不需要额外附加参数,就算了...),否则执行者拿到的时候会抓狂的。 Flags。能识别,有输入,整个Intent基本就完整了,但还有一些附件的指令,需要放在Flags中带过去。顾名思义,Flags是一个整形数,有一些列的标志位构成,这些标志,是用来指明运行模式的。比如,你期望这个意图的执行者,和你运行在两个完全不同的任务中(或说进程也无妨吧...),就需要设置FLAG_ACTIVITY_NEW_TASK的标志位。 有了上述这些,一个Intent的形象就跃然纸上了,如此丰富的内容,决定了它比传统的模式,都来得强大。 Intent匹配 上次在moto dev上,听人做Android的讲座,下面有很多听客都对Intent这个概念表示出了强烈的兴趣,拿出自己熟悉领域的各类概念进行类比,比如事件、消息之类。当时我在想,Intent作为组件间的通信协定,与一般的简单的通信方式不同,首先,从前面部分可以看到,它的描述是针对需求而不是实现者来进行的。其次,它的解析是依托第三方而不是两方直接进行。 这个概念和设计模式中的中介模式(Mediator Pattern)是一脉的,即所有的外围组件,都只和系统的核心模块发生联系,通过它进行中转,组件之间不直接勾搭。 如上图所示,要想跑通整个流程,另一个很重要的东西,就是Intent Filters,它是用来描述一个Activity或Serveice等组件,期望能够响应怎么样的Intent。如果一个组件,只希望别的组件通过Explicit Intents(就是指明Component...)的方式来找到它,那么就不需要添加Intent Filters,反之,一定需要一个或若干个Intent Filters。Intent Filter的各个项,犹如Intent照镜子过来的效果,包括Action,Catagory,Data,Type等。 Intent Filters可以写到配置文件中,和那些组件的配置一起(不记得什么是配置文件了,可以看这里...),若干的实例可以在Intent介绍页面上找到(reference/android/content/Intent.html)。同样,Intent Filters可以在代码中,动态插拔,这个是和动态插拔的Broadcast Receiver是配套使用的。 系统核心的模块,会负责收集这些Intent Filters,和它们对应的组件信息。当请求者需要一个组件帮忙,并构造了描述它需求的Intent发送到系统核心,系统核心会将其与已知的各个Intent Filters进行匹配,挑选一个符合需求的组件返回。如果有多个符合的,会尝试看看有没有默认执行的,如果没有默认的,就会构造UI,让用户帮助抉择,如是,整个流程就跑通了。 Intent实现 上图,是请求一个Activity组件的简单实现流程图,算是用的最多的Intent解析实例。流程从调用Context.startActivity(Intent)开始,调用者传入构造好的Intent对象,然后流程会让实际的执行者,是Instrumentation对象来完成。它是整个应用激活的Activity管理者,集中负责该应用内所有Activity的起承转合生离死别。它有一个隐藏的方法execStartActivity方法,就是负责根据Intent启动Activity的。去掉一些细节,它做得最重要的事情,就是将此调用,通过RPC的方式,传递到ActivityManagerService。 前面一直再说,系统核心层,其实这里所谓的系统核心层,就是负责Android一些关键事务的一组服务。它们同样运行在虚拟机上,和普通的Service实现机理是一致的,只不过它们不抛头露脸只是默默的在下层服务,故谓之核心嘛。AcitivityManagerService,是负责Activity调度的服务,也许日后提及调度细节的时候还会有涉及。 在这里,AcitivityManagerService会分两个步骤完成相关操作,首先把Intent递交给另一个服务PackageManagerService,此服务掌握整个软件包及其各组件的信息,它会将传递过来的Intent,与已知的所有Intent Filters进行匹配(如果带有Component信息,就不用比了...),找到了,就把相关Component的信息通知回AcitivityManagerService,在这里,会完成启动Activity这个很多细节的事情。 由此可知,启动Activity,要经过多个服务的处理,并不是非常轻量的过程,在Android随机文档介绍性能的一节中,对此有一个评估。但这样的操作不是会放在循环里反复折磨的那种,因此整体效果与其付出的性能代价相比,觉得是物超所值的。
【五】 —— 任务和进程
关于Android中的组件和应用,之前涉及,大都是静态的概念。而当一个应用运行起来,就难免会需要关心进程、线程这样的概念。在Android中,组件的动态运行,有一个最与众不同的概念,就是Task,翻译成任务,应该还是比较顺理成章的。 Task的介入,最主要的作用,是将组件之间的连接,从进程概念的细节中剥离出来,可以以一种不同模型的东西进行配置,在很多时候,能够简化上层开发人员的理解难度,帮助大家更好的进行开发和配置。 任务 在SDK中关于Task(guide/topics/fundamentals.html#acttask),有一个很好的比方,说,Task就相当于应用(application)的概念。在开发人员眼中,开发一个Android程序,是做一个个独门独户的组件,但对于一般用户而言,它们感知到的,只是一个运行起来的整体应用,这个整体背后,就是Task。 Task,简单的说,就是一组以栈的模式聚集在一起的Activity组件集合。它们有潜在的前后驱关联,新加入的Activity组件,位于栈顶,并仅有在栈顶的Activity,才会有机会与用户进行交互。而当栈顶的Activity完成使命退出的时候,Task会将其退栈,并让下一个将跑到栈顶的Activity来于用户面对面,直至栈中再无更多Activity,Task结束。 事件 Task栈(粗体为栈顶组件) 点开Email应用,进入收件箱(Activity A) A 选中一封邮件,点击查看详情(Activity B) AB 点击回复,开始写新邮件(Activity C) ABC 写了几行字,点击选择联系人,进入选择联系人界面(Activity D) ABCD 选择好了联系人,继续写邮件 ABC 写好邮件,发送完成,回到原始邮件 AB 点击返回,回到收件箱 A 退出Email程序 null 如上表所示,是一个实例。从用户从进入邮箱开始,到回复完成,退出应用整个过程的Task栈变化。这是一个标准的栈模式,对于大部分的状况,这样的Task模型,足以应付,但是,涉及到实际的性能、开销等问题,就会变得残酷许多。比如,启动一个浏览器,在Android中是一个比较沉重的过程,它需要做很多初始化的工作,并且会有不小的内存开销。但与此同时,用浏览器打开一些内容,又是一般应用都会有的一个需求。设想一下,如果同时有十个运行着的应用(就会对应着是多个Task),都需要启动浏览器,这将是一个多么残酷的场面,十个Task栈都堆积着很雷同的浏览器Activity,是多么华丽的一种浪费啊。于是你会有这样一种设想,浏览器Activity,可不可以作为一个单独的Task而存在,不管是来自那个Task的请求,浏览器的Task,都不会归并过去。这样,虽然浏览器Activity本身需要维系的状态更多了,但整体的开销将大大的减少,这种舍小家为大家的行为,还是很值得歌颂的。 如此值得歌颂的行为,Android当然会举双手支持的。在Android中,每一个Activity的Task模式,都是可以由Activity提供方(通过配置文件...)和Activity使用方(通过Intent中的flag信息...)进行配置和选择。当然,使用方对Activity的控制力,是限定在提供方允许的范畴内进行,提供方明令禁止的模式,使用方是不能够越界使用的。 在SDK中(guide/topics/fundamentals.html#acttask),将两者实现Task模式配置的方式,写的非常清晰了,我再很絮叨挑选一些来解释一下(完整可配置项,一定要看SDK,下面只是其中常用的若干项...)。提供方对组件的配置,是通过配置文件(Manifest)<activity>项来进行的,而调用方,则是通过Intent对象的flag进行抉择的。相对于标准的Task栈的模式,配置的主要方向有两个:一则是破坏已有栈的进出规则,或样式;另一则是开辟新Task栈完成本应在同一Task栈中完成的任务。 对于应用开发人员而言,<activity>中的launchMode属性,是需要经常打交道的。它有四种模式:"standard", "singleTop", "singleTask", "singleInstance"。 standard模式, 是默认的也是标准的Task模式,在没有其他因素的影响下,使用此模式的Activity,会构造一个Activity的实例,加入到调用者的Task栈中去,对于使用频度一般开销一般什么都一般的Activity而言,standard模式无疑是最合适的,因为它逻辑简单条理清晰,所以是默认的选择。 而singleTop模式,基本上于standard一致,仅在请求的Activity正好位于栈顶时,有所区别。此时,配置成singleTop的Activity,不再会构造新的实例加入到Task栈中,而是将新来的Intent发送到栈顶Activity中,栈顶的Activity可以通过重载onNewIntent来处理新的Intent(当然,也可以无视...)。这个模式,降低了位于栈顶时的一些重复开销,更避免了一些奇异的行为(想象一下,如果在栈顶连续几个都是同样的Activity,再一级级退出的时候,这是怎么样的用户体验...),很适合一些会有更新的列表Activity展示。一个活生生的实例是,在Android默认提供的应用中,浏览器(Browser)的书签Activity(BrowserBookmarkPage),就用的是singleTop。 singleTop模式,虽然破坏了原有栈的逻辑(复用了栈顶,而没有构造新元素进栈...),但并未开辟专属的Task。而singleTask,和singleInstance,则都采取的另辟Task的蹊径。标志为singleTask的Activity,最多仅有一个实例存在,并且,位于以它为根的Task中。所有对该Activity的请求,都会跳到该Activity的Task中展开进行。singleTask,很象概念中的单件模式,所有的修改都是基于一个实例,这通常用在构造成本很大,但切换成本较小的Activity中。在Android源码提供的应用中,该模式被广泛的采用,最典型的例子,还是浏览器应用的主Activity(名为Browser...),它是展示当前tab,当前页面内容的窗口。它的构造成本大,但页面的切换还是较快的,于singleTask相配,还是挺天作之合的。 相比之下,singleInstance显得更为极端一些。在大部分时候singleInstance与singleTask完全一致,唯一的不同在于,singleInstance的Activity,是它所在栈中仅有的一个Activity,如果涉及到的其他Activity,都移交到其他Task中进行。这使得singleInstance的Activity,像一座孤岛,彻底的黑盒,它不关注请求来自何方,也不计较后续由谁执行。在Android默认的各个应用中,很少有这样的Activity,在我个人的工程实践中,曾尝试在有道词典的快速取词Activity中采用过,是因为我觉得快速取词入口足够方便(从notification中点选进入),并且会在各个场合使用,应该做得完全独立。 除了launchMode可以用来调配Task,<activity>的另一属性taskAffinity,也是常常被使用。taskAffinity,是一种物以类聚的思想,它倾向于将taskAffinity属性相同的Activity,扔进同一个Task中。不过,它的约束力,较之launchMode而言,弱了许多。只有当<activity>中的allowTaskReparen ting设置为true,抑或是调用方将Intent的flag添加FLAG_ACTIVITY_NEW_TASK属性时才会生效。如果有机会用到Android的Notification机制就能够知道,每一个由notification进行触发的Activity,都必须是一个设成FLAG_ACTIVITY_NEW_TASK的Intent来调用。这时候,开发者很可能需要妥善配置taskAffinity属性,使得调用起来的Activity,能够找到组织,在同一taskAffinity的Task中进行运行。 进程 在大多数其他平台的开发中,每个开发人员对自己应用的进程模型都有非常清晰的了解。比如,一个控制台程序,你可以想见它从main函数开始启动一个进程,到main函数结束,进程执行完成退出;在UI程序中,往往是有一个消息循环在跑,当接受到Exit消息后,退出消息循环结束进程。在该程序运行过程中,启动了什么进程,和第三方进程进行通信等等操作,每个开发者都是心如明镜一本帐算得清清楚楚。进程边界,在这里,犹如国界一般,每一次穿越都会留下深深的印迹。 在Android程序中,开发人员可以直接感知的,往往是Task而已。倍感清晰的,是组件边界,而进程边界变得难以琢磨,甚至有了进程托管一说。Android中不但剥夺了手工锻造内存权力,连手工处置进程的权责,也毫不犹豫的独占了。 当然,Android隐藏进程细节,并不是刻意为之,而是自然而然水到渠成的。如果,我们把传统的应用称为面向进程的开发,那么,在Android中,我们做得就是面向组件的开发。从前面的内容可以知道,Android组件间的跳转和通信,都是在第三方介入的前提下进行,正由于这种介入,使得两个组件一般不会直接发生联系(于Service的通信,是不需要第三方介入的,因此Android把它全部假设成为穿越进程边界,统一基于RPC来通信,这样,也是为了掩盖进程细节...),其中是否穿越进程边界也就变得不重要。因此,如果这时候,还需要开发者关注进程,就会变得很奇怪,很费解,干脆,Android将所有的进程一并托管去了,上层无须知道进程的生死和通信细节。 在Android的底层,进程构造了底部的一个运行池,不仅仅是Task中的各个Activity组件,其他三大组件Service、Content Provider、Broadcast Receiver,都是寄宿在底层某个进程中,进行运转。在这里,进程更像一个资源池(概念形如线程池,上层要用的时候取一个出来就好,而不关注具体取了哪一个...),只是为了承载各个组件的运行,而各个组件直接的逻辑关系,它们并不关心。但我们可以想象,为了保证整体性,在默认情况下,Android肯定倾向于将同一Task、同一应用的各个组件扔进同一个进程内,但是当然,出于效率考虑,Android也是允许开发者进行配置。 在Android中,整体的<application>(将影响其中各个组件...)和底下各个组件,都可以设置<process>属性,相同<process>属性的组件将扔到同一个进程中运行。最常见的使用场景,是通过配置<application>的process属性,将不同的相关应用,塞进一个进程,使得它们可以同生共死。还有就是将经常和某个Service组件进行通信的组件,放入同一个进程,因为与Service通信是个密集操作,走的是RPC,开销不小,通过配置,可以变成进程内的直接引用,消耗颇小。 除了通过<process>属性,不同的组件还有一些特殊的配置项,以Content Provider为例(通过<provider>项进行配置...)。<provider>项有一个mutiprocess的属性,默认值为false,这意味着Content Provider,仅会在提供该组件的应用所在进程构造一个实例,第三方想使用就需要经由RPC传输数据。这种模式,对于构造开销大,数据传输开销小的场合是非常适用的,并且可能提高缓存的效果。但是,如果是数据传输很大,抑或是希望在此提高传输的效率,就需要将mutiprocess设置成true,这样,Content Provider就会在每一个调用它的进程中构造一个实例,避免进程通信的开销。 既然,是Android系统帮助开发人员托管了进程,那么就需要有一整套纷繁的算法去执行回收逻辑。Android中各个进程的生死,和运行在其中的各个组件有着密切的联系,进程们依照其上组件的特点,被排入一个优先级体系,在需要回收时,从低优先级到高优先级回收。Android进程共分为五类优先级,分别是:Foreground Process, Visible Process, Service Process, Background Process, Empty Process。顾名思义不难看出,这说明,越和用户操作紧密相连的,越是正与用户交互的,优先级越高,越难被回收。具体详情,参见:guide/topics/fundamentals.html#proclife。 有了优先级,还需要有良好的回收时机。回收太早,缓存命中概率低可能引起不断的创造进程销毁进程,池的优势荡然无存;回收的太晚,整体开销大,系统运行效率降低,好端端的法拉利可能被糟蹋成一枚QQ老爷车。Android的进程回收,最重要的是考量内存开销,以及电量等其他资源状况,此外每个进程承载的组件数量、单个应用开辟的进程数量等数量指标,也是作为衡量的一个重要标识。另外,一些运行时的时间开销,也被严格监控,启动慢的进程会很被强行kill掉。Android会定时检查上述参数,也会在一些很可能发生进程回收的时间点,比如某个组件执行完成后,来做回收的尝试。 从用户体验角度来看,Android的进程机制,会有很可喜的一面,有的程序启动速度很慢,但是在资源充沛的前提下,你反复的退出再使用,则启动变得极其快速(进程没死,只是从后台弄到了前台),这就是拜进程托管所赐的。当然,可喜的另一面就是可悲了,Android的托管算法,还时不时的展现其幼稚的一面,明明用户已经明显感觉到操作系统运行速度下降了,打开任务管理器一看,一票应用还生龙活虎的跳跃着,必须要手动帮助它们终结生命找到坟墓,这使得任务管理器基本成为Android的装机必备软件。 从开发角度上来看,Android这套进程机制,解放了开发者的手脚。开发人员不需要处心积虑的构造一个后台进程偷偷默默监听某个时间,并尝试用各种各样的守护手段,把自己的进程锻造的犹如不死鸟一辉一般,进程生死的问题,已经原理了普通开发人员需要管理的范畴内。但同时,于GC和人肉内存管理的争议一样,所有开发人员都不相信算法能比自己做得效率更高更出色。但我一直坚信一点,所有效率的优势都会随着算法的不断改良硬件的不断提升而消失殆尽,只有开发模式的简洁不会随时间而有任何变化。 组件生命周期 任何架构上的变化,都会引起上层开发模式的变化,Android的进程模型,虽然使开发者不再需要密切关注进程的创建和销毁的时机,但仍然需要关注这些时间点对组件的影响。比如,你可能需要在进程销毁之前,将写到内存上的内容,持久化到硬盘上,这就需要关注进程退出前发生的一些事件。 在Android中,把握这些时间点,就必须了解组件生命周期(Components Lifecycles)。所谓组件的生命在周期,就是在组件在前后台切换、被用户创建退出、被系统回收等等事件发生的时候,会有一些事件通知到对应组件上,开发人员可以选择性的处理这些事件在对应的时间点上来完成一些附加工作。 除Content Provider,其他组件都会有生命周期的概念,都需要依照这个模型定时定点处理一些状况,全部内容参见:guide/topics/fundamentals.html#lcycles。在这里,擒贼先擒王,还是拿Activity出来作楷模。 继续偷图,来自SDK。一个自然的Activity生命旅途,从onCreate开始,到onDestroy消亡。但月有阴晴圆缺组件有祸福旦夕,在系统需要的时候且组件位于后台时,所在的进程随时可能为国捐躯被回收,这就使得知道切入后台这个事情也变得很重要。 当组件进入栈顶,与用户开始交互,会调用onResume函数,类似,当退出栈顶,会有onPause函数被呼唤。onResume和onPause可以处理很多事情,最常规的,就是做一些文件或设置项的读写工作。因为,在该组件不再前台运行的时候,可能别的组件会需要读写同样一份文件和设置,如果不再onResume做刷新工作,用的可能就是一份脏数据了(当然,具体情况,还需要具体分析,如果文件不会被多头读写,可以放到onCreate里面去做读工作)。 除了前述切入后台会被其他组件骚扰的问题,另外,死无定因也是件很可怕的事情。在Android中,组件都有两种常见的死法,一种是自然消亡,比如,栈元素ABC,变成AB了,C组件就自然消亡了。这种死发轻如鸿毛,不需要额外关心。但另一种情况,就是被系统回收,那是死的重如泰山,为国捐躯嘛。 但这种捐躯的死法,对用户来说,比较费解。想象一下,一款游戏,不能存盘,你一直玩啊玩,三天三夜没合眼,这时候你mm打来电话鼓励一下,你精神抖擞的准备再接再厉,却发现你的游戏进程,在切入后台之后,被系统回收了,一夜回到*三天努力成为一场泡影,你会不会想杀做游戏的人,会不会会不会会不会,一定会嘛。这时候,如果没有Activity生命周期这码事,游戏程序员一定是被冤死的,成了Android的替罪羊。但是,Android的组件是有生命周期的,如果真的发生这样情况,不要犹豫,去杀开发的程序员吧。 为了逃生,程序员们有一块免死金牌,那就是Android的state机制。所谓state,就是开发人员将一些当前运行的状态信息存放在一个Bundle对象里面,这是一个可序列化键值对集合。如果该Activity组件所处的进程需要回收,Android核心会将其上Activity组件的Bundle对象持久化到磁盘上,当用户回到该Activity时候,系统会重新构造该组件,并将持久化到磁盘上的Bundle对象恢复。有了这样的持久化的状态信息,开发人员可以很好的区分具体死法,并有机会的使得死而复生的Activity恢复到死前状态。开发者应该做的,是通过onSaveInstanceState函数把需要维系的状态信息(在默认的状态下,系统控件都会自己保存相关的状态信息,比如TextView,会保存当前的Text信息,这都不需要开发人员担心...),写入到Bundle对象,然后在onRestoreInstanceState函数中读取并恢复相关信息(onCreate,onStart,也都可以处理...)。 线程 读取数据,后台处理,这些猥琐的伙计,自然少不了线程的参与。在Android核心的调度层面,是不屑于考量线程的,它关注的只有进程,每一个组件的构造和处理,都是在进程的主线程上做的,这样可以保证逻辑的足够简单。多线程,往往都是开发人员需要做的。 Android的线程,也是通过派生Java的Thread对象,实现Run方法来实现的。但当用户需要跑一个具有消息循环的线程的时候,Android有更好的支持,来自于Handler和Looper。Handler做的是消息的传送和分发,派生其handleMessage函数,可以处理各种收到的消息,和win开发无异。Looper的任务,则是构造循环,等候退出或其他消息的来临。在Looper的SDK页面,有一个消息循环线程实现的标准范例,当然,更为标准的方式也许是构造一个HandlerThread线程,将它的Looper传递给Handler。 在Android中,Content Provider的使用,往往和线程挂钩,谁让它和数据相关呢。在前面提到过,Content Provider为了保持更多的灵活性,本身只提供了同步调用的接口,而由于异步对Content Provider进行增删改查是一个常做操作,Android通过AsyncQueryHandler对象,提供了异步接口。这是一个Handler的子类,开发人员可以调用startXXX方法发起操作,通过派生onXXXComplete方法,等待执行完毕后的回调,从而完成整个异步调用的流程,十分的简约明了。 实现 整个任务、进程管理的核心实现,尽在ActivityManagerService中。上一篇说到,Intent解析,就是这个ActivityManagerService来负责的,其实,它是一个很名不副实的类,因为虽然名为Activity的Manager Service,但它管辖的范围,不只是Activity,还有其他三类组件,和它们所在的进程。 在ActivityManagerService中,有两类数据结构最为醒目,一个是ArrayList,另一个是HashMap。ActivityManagerService有大量的ArrayList,每一个组件,会有多个ArrayList来分状态存放。调度工作,往往就是从一个ArrayList里面拿出来,找个方法调一调,然后扔到另一个ArrayList里面去,当这个组件没对应的ArrayList放着的时候,说明它离死不远了。HashMap,是因为有组件是需要用名字或Intent信息做定位的,比如Content Provider,它的查找,都是依据Uri,有了HashMap,一切都顺理成章了。 ActivityManagerService用一些名曰xxxRecord的数据结构,来表达各个存活的组件。于是就有了,HistoryRecord(保存Activity信息的,之所以叫History,是相对Task栈而言的...),ServiceRecord,BroadcastRecord,ContentProviderRecord,TaskRecord,ProcessRecord,等等。 值得注意的,是TaskRecord,我们一直再说,Task栈这样的概念,其实,真实的底层,并不会在TaskRecord中,维系一个Activity的栈。在ActivityManagerService中,各个任务的Activity,都以HistoryRecord的形式,集中存放在一个ArrayList中,每个HistoryRecord,会存放它所在TaskRecord的引用。当有一个Activity,执行完成,从概念上的Task栈中退出,Android是通过从当前HistoryRecord位置往前扫描同一个TaskRecord的HistoryRecord来完成的。这个设计,使得上层很多看上去逻辑很复杂的Task体系,在实现变得很统一而简明,值得称道。 ProcessRecord,是整个进程托管实现的核心,它存放有运行在这个进程上,所有组件的信息,根据这些信息,系统有一整套的算法来决议如何处置这个进程,如果对回收算法感兴趣,可以从ActivityManagerService的trimApplications函数入手来看。 对于开发者来说,去了解这部分实现,主要是可以帮助理解整个进程和任务的概念,如果觉得这块理解的清晰了,就不用去碰ActivityManagerService这个庞然大物了。
【六】 —— 界面构造
UI界面,对于每个应用而言,是它与用户进行交互的门脸。好的门脸,不只是是要亮丽可人,最好还能秀色可餐过目不忘,甚至还应该有涵养有气质,彬彬有理温柔耐心。 对于开发者来说,锻造这样的面容,不但需要高超的技艺,也需要有称手的工具和对得起党的料子。俗话说,朽木不可雕也,芙蓉不是一日炼成的,不是什么平台都能叫特能书。有套好用的UI框架,对于开发者而言,真有如沙漠中的甘露,而要是撞见了杯具的UI套件,整个界面开发就有如梦魇了。 Android的UI框架,最核心的,是资源和Layout体系,然后,通过完善的控件库,简明的接口设计,进一步帮助开发者,能够最快的搭建自己需要界面(听到这里,Symbian同学开始钻土...)。 UI控件 做UI,有时候就像搭积木,在Android中,这个最原子的积木块,就是View。所有其他的UI元素,都是派生于此类的子孙类们。 又从SDK中偷来张图,用来描述Android的UI控件结构,在每一个window下,这都是一个标准而完整的树结构。View有一个子类ViewGroup,它相当于一个容器类或者是复合控件,所有派生与ViewGroup的子类在这颗UI树中都可以承担着父节点的职责,而另一些绕过ViewGroup从View直通下来的,就只能蜷局在叶节点的范畴内了。 之所有说这是一个很标准的控件树,是因为父控件对子控件有绝对的掌控权,每个子控件的占地面积和位置,都是基于父控件来分配的,它能够接受和处理的事件,也是父控件派发下去的。这样的结构,被很多平台和框架广泛的认可,和传统的win开发和杯具的Symbian相比,虽然因为事件传播途径变长了,很多操作的效率变低了,但整个结构更有层次性,每个控件只需要多其父控件负责指挥子控件就好,职责明确,逻辑简单,利于开发和设计。 谈及任何平台的控件,都有一些不可避免的主题,比如,每个控件如何标识,如何设定大小和位置,如何接受和处理事件,如何绘制,诸如此类。 标识 在Android中,你可以为每个控件选择设定一个id,这个id的全局的唯一性不需要保证,但在某个局部的范围内具有可识别性,这样就可以通过这个id找到这个控件(如果不需要查找,就别设置了...)。 但是,在父控件中逐级的find比较,找到id匹配的控件,然后再做转型,是一个比较重量的操作,于是Android又为控件憋出另一个属性,tag。它接受任意object类型的数据,你可以把和这个控件对象相关的内容堆在里面。比如,在list中,我们常常将和每个list item相关的所有控件元素封装成一个object,扔到tag中,就不需要每次都去比较id进行寻找,更加高效快捷。 尺寸 在Android中,控件最重要的大小属性,就是width/height,开发者可以明确的指明控件的大小,可以设定成为fill_parent和wrap_content,这样的概念性的大小。丈量并设定控件的位置,是通过两步来进行的。 第一步是measure。它传入此控件的width/height信息,控件会根据自己的参数,计算出真实需要的width/height,然后调用setMeasuredDimension方法,缓存成成员变量,留作后用。 在计算出大小之后,会进行另一个步骤,layout。在这个过程中,父控件会计算其上各个子控件的位置,从而完成整个大小和位置的确定流程。整个measure和layout的流程,都是自上到下,从树顶往叶子来推进的。 当开发人员需要自定义控件的时候,可能需要关注这些内容,通过重载onMeasure和onLayout方法,可以定义自己控件的丈量方式。 事件 在Android中,所有的按键,触屏等事件,都是从顶至下进行分发的。每个ViewGroup的对象,会维系一个focused变量,它表示在这个父控件中具备focus的控件,当有按键时间发生的时候,会找到这个focused子控件,并传递给它。同理,触屏事件的分发也是类似,只不过和focus无关,父控件会遍历所有子控件,看看谁处于触碰位置,从而传递给谁。 另外还有一些事件,逻辑上并不是从顶至下发起的。比如,当你修改某个子控件的内容,使得该子控件的大小和内容都发生了变化,就需要进行控件的重排和重绘,这些操作不仅是子控件自己的事情,需要整个控件树上的所有控件都需要配合。在Android中,处理这类事情的实现策略是子控件维系一个ViewParent对象,该对象象征着整个控件树的管理者,子控件产生影响整个控件树的事件时,会通知到ViewParent,ViewParent会将其转换成一个自顶向下的事件,分发下去。 Android的事件处理逻辑,采用的是观察者模式。Android的控件提供了一些列的add/set Listener的接口,使得外部观察者,有机会处理控件事件。比如,你需要在某个button被点击时做一些事情,你就需要派生一个View.OnClickListener对象作为观察者,调用该控件的setOnClickListener接口注册进去,当button被点击,就可以获得处理点击事件的机会了。当然,有的时候,你需要处理的逻辑更为复杂,光是站在外面围观叫好不能解决问题,可能就需要派生某个控件,去重载onXXXX之类的事件处理函数,进行更完整的控制。 焦点 对于一个非触屏的机器,焦点的维系是一个极其重要的事情,而在有触屏的年代,焦点的地位虽有所下降,但依然还是需要妥善保护的。 Android中,是以控件树为单位,来管理焦点的。每个控件,可以设置上下左右四向的focus转移对象。当在一个控件上发生焦点转移事件,Android会如前述,自顶向下根据设定好的焦点转移逻辑,跳转到正确的控件上。和Symbian相比,真是,真是。。。 Layout Layout是一类特殊的ViewGroup控件,它们本身没有任何可显示内容,形如透明的玻璃盒子,存活的唯一理由,就是其中的内部结构,能够更好的摆放它的子控件们。 比如线性的Layout,LinearLayout。放入这个Layout的子控件,会按水平或垂直方向,排排坐,一个挨着一个按顺序排列下去。TableLayout,可以将子控件按照表格的形式,一枚枚放置好。而RelativeLayout则更灵活,可以设定各个控件之间的对齐和排列关系,适合定制复杂的界面。 有了Layout的存在,控件和控件之间不再割裂的存在,而是更有机的结合在了一起,设定起来也更为方便。比Symbian那样人肉维系各个控件的关系,轻松自在多了。 更多 这些问题的完整答案,参见SDK中View的页面:/reference/android/view/View.html。 实现 有了这些对Android的UI控件的认知,可以看更整体性的实现细节,那就是Activity的UI实现。 如上图所示,假设你做了个如同虚线框中结构的一个界面,通过Activity的setContentView方法,塞进了Activity中,就会形成图示的一个逻辑关系。每一个Activity,都包含一个Window对象,它表示的是一个*的一整屏幕上面的界面逻辑。在Android源码中,其实现是MidWindow,它包含了一个FrameLayout对象,呈现出来就是那种带着一个title的界面样子。自定义的一堆控件,会插进Window的界面部分,在Activity中,所有事件的处理逻辑,是Window先享用,没消费掉在交由这堆控件吃剩的。 在整个控件树的最顶端,是一个逻辑的树顶,ViewParent,在源码中的实现是ViewRoot。它是整个控件树和WindowManager之间的事件信息的翻译者。WindowManager是Android中一个重要的服务。它将用户的操作,翻译成为指令,发送给呈现在界面上的各个Window。Activity,会将*的控件注册到WindowManager中,当用户真是触碰屏幕或键盘的时候,WindowManager就会通知到,而当控件有一些请求产生,也会经由ViewParent送回到WindowManager中。从而完成整个通信流程。
【七】 —— 资源文件
作为一枚coder,做界面,很多时候都是一场梦魇。很多时候,我们会感觉对于底层逻辑实现的很有把握性,哪怕需求一直在变,也可以通过不断的重构一直跟进,一切尽在掌握。但遭遇界面,往往就不再如此,它的好坏总是和审美、体验之类的词汇扯在一起,在凤姐芙蓉出没的年头,谈审美成为一件恐怖的事情。你可能会被要求不停的改代码,就为了移动一个像素,调整一枚按钮,琐碎而无聊。 为了改变这样的状况,挽救coder们于水生活热之中,很多开发平台,都采用了类似于资源文件的解决方案。此类方案的基本思想是,将界面的实现与底层逻辑的实现完全剥离开来,用资源文件这样的东西来描述界面。资源文件的描述语言,往往是结构化很强,比如Html,Xml(及其变形体)之类的。于开发语言相比,此类语言逻辑性较弱但结构更好可读性更强更容易理解,并对自动化工具非常友好,可以于界面的拖拽配置结合的更加完美。这样的剥离,可以是的底层逻辑和上层界面独立变化,甚至不同的人员开发(这一点在web开发上表现的应该很明显...),两者之间的耦合性非常的小,coder们的负担,陡然减少(好吧,一个很挫的资源架构也会额外增加开发人员的负担,Symbian同学,请不要对号入座...)。 结构和格式 Android的资源文件,是由目录结构,Xml格式的文件,和纯数据文件构成。从格式上来看,无疑,学习门槛非常低。Xml作为coder们的瑞士军刀,哪怕使不习惯,弄得清楚并会用至少是没有问题。从配套的工具来看,Android的ADT,提供了一套可视化的配置工具,说不上特别好用,但至少是差强人意能凑合着用,比不上iPhone的,调戏Symbian还是没有问题的[强档广告首播:有道词典 for iPhone新版火热上线,增加了超强单词本功能,特有的触电式颤抖单词切换功能,让你欲罢不能,持有相关设备的童鞋不要犹豫,一拥而上吧...]。 Android的资源文件,覆盖面超级广,只要是和界面相关的,都可以用资源文件表示,比如:UI的样式,菜单,配置文件,各种描述性字符串,图片,音频视频文件,动画,颜色,尺寸,风格和样式,等等等。所有的资源文件(不考虑asset,它和讨论暂无关联...),都放在res目录下,不同类别的资源,需要放置在不同的特定名称的子文件夹中,或者是写在特定文件名的文件中(或者ms不是必须的,但,不用在这里特立独行,寻章办事也挺好...)。比如,所有作为UI背景之类的图片,都需要扔在drawable这类的文件夹中,所有字符串相关的,都会放到values目录下形如strings.xml这样的文件中(如下图所示,是一个资源文件目录结构的截图...)。 每个xml文件,都有一定的约定。比如一个字符串,会放在<string></string>这样的xml element中(如下图所示...),你可以通过eclipse的ADT插件提供的可是界面去填而不关注具体规范,也可以直接人肉打造,前者对于新手来说更为直观,后者对于老鸟而言更为迅捷。 可配置性 程序逻辑总是不变应万变的,但界面往往是需要能够72变。首先一种变化因素,就是状态。想象一下,我们往往会有这样类似的需求,一个按钮,我们需要没有按下去的时候是一种背景,按的过程中刷的变成另一副模样,当它可用的时候需要鲜鲜亮的一个样子,不可用的时候最好是灰不溜秋没人愿点的怂样,诸如此类。传统编程模型下(Symbian,哥叫你出来当模特...),我们总是需要不厌其烦的用代码控制这样的事情。监听不同的事件,见缝插针的切换背景,并祈祷上天,千万别让哥调整,否则哥和你没完。 在Android中,做这个事情,变得简单许多,通过预设的一些Xml属性,能够轻松的搞定。如上图所示,是Radio Button的背景。通过搭配不同的属性,就可以自动转换背景。比如第一个<item>,说的是当Radio Button被选中,并且具有焦点的时候,显示btn_radio_on这幅图片,而最后一个<item>,说的是前述条件都不满足,并且处于选中状态,那么显示btn_radio_on这幅图片。 另外一个更易变的因素,就是手机硬件/软件环境了,毕竟,不是家家都是苹果,一个平台搭一款手机,手机款形多样化,几乎是避免不了的问题。没有人希望自己做的软件在大屏幕手机上闪亮光鲜,换个小屏幕就惨不忍睹,竖屏看像那么回事横屏看就挤做一团。还有就是语言环境了,做为一个有国际眼光的coder,作面向世界的NB软件是咱的梦想,但我们不能因为自己的梦想逼迫大家都去学中文,做一款软件可以根据手机的语言环境选择最合适展示的语言,很多时候,是一个需要具备的功能点。 在Android中,实现这些,都是举手之劳。方法就是将和环境相关的资源,放入特定名称的文件夹中。比如,表示简体中文字符信息的资源,可以放到values-zh-rCN中去,当系统语言环境为简体中文时,就会呈现出中文的字符信息。在Android中,很多相关配置项,都可以按照这样的方式参与到资源自适应的活动中来,包括屏幕大小,屏幕朝向,屏幕分辨率,语言环境,触屏类型,SDK版本等等。系统会给所有配置项一个优先级(或者说权重,次序之类的),当用户提供了多份资源的时候,系统会根据优先级从高到底淘汰备选资源,如果淘汰仅剩了一个,那就是最符合当前系统软硬件语言环境的资源项,如果一个不剩,择启用默认项(最是形如values这样没有任何尾巴目录中的资源...)。因此,默认的资源是非常重要的,它必须是其他所有可选资源项的超集,否则在资源选择失败的情况下,应用会凄凉的崩溃。 关于资源配置,以及选择的详情,参见SDK中的:guide/topics/resources/resources-i18n.html部分。 R类 在使用资源后,界面逻辑与底层逻辑的耦合被降低了,但这不意味着,两者没有关联了。比如,需要为某个按钮增加一个点击事件,就需要定位到所需的那个按钮;再比如,你需要使用某个字符串资源,通知用户某件事情,就需要能定位到资源中放置的该字串。 最显而易见的一种方式,就是通过字符串比较,用名字信息在资源的xml描述文件中定位到所需的内容,加载并使用。这种方式,解决了查找的问题,但反复的字符串比较,势必带来严重的效率隐患。因此,在Android中,类似于Symbian的方法,引入了一个R类。 它的基本思想是,通过增加一个额外的编译器,为所有的资源项,都赋予一个32位的整形数来表示,同一个资源像的不同配置,都使用同一个id。这个整形数,就相当于这个资源项的门牌号码,能够帮助定位到对应的资源项。所有的这些整形数,都以常量的方式,整合到一个Java类中,这个类就是R类。这样,在程序中,就可以通过使用这个R类,来查找所需的资源,这就将字符串比较,简化成了一个整形数的比较,大大的节约了开销。 不得不说,这整套逻辑和Symbian中的资源文件预编译一致。但两者很不同的点在于Symbian中的整形数,代表的是一个二进制流的偏移量,资源中的内容在编译时决定了。而Android中的整形数,是一个有逻辑意义的数值,它表达了这个资源所处的资源包,类别,和脚标,它的具体内容在运行时才确定,这使得它的灵活性大大增强,付出的则是一定的效率代价。 实现 按照惯例,还是要说实现的,以一个查找流程为示例。当在Activity中需要使用字符串的,会调用它的getString方法,传入R.stirng.xxx的一个整形数,换取一个符合当前机器环境配置的字符串。 getString,追根溯源,来到AssetManager类中。Asset类,其实是一个空壳,它仅仅是提供了一些便利的接口,而将请求,通过JNI的接口,传入到了底层C++实现的类库中。 在底层的实现,主要是在C++实现的,AssetManager,ResourceTypes等等之中。其中: JNI文件在:framework/base/core/jni 头文件在:framework/base/include/utils CPP文件在:framework/base/libs/utils 具体实现,和前述的算法逻辑是一致的。每一个资源的id,32位,高8位表示资源包,低16位用于描述脚标,中间8位,用来说明类别。所有资源中的文件,都被预处理了,放入到了一系列的队列和表中,通过id,可以查到具体的位置。然后根据缓存的环境设置对象,跑一次淘汰算法,获得匹配的资源对象的对应文件和偏移量。然后将值读取出来,通过JNI接口,拷贝回去。 以上这些描述,并不能帮助了解真实的实现细节,主要是为了促使大家对读取资源的效率有一个比较直观的认知。整个资源读取的流程比较长,但是实现在C++中,可以预想,效率比Java高一些,开发人员,应该能够根据自己的需求,决定是否将内容写入资源文件中(还是写在代码中...),是不是需要自己稍微缓存一下,诸如此类。
【八】 —— Activity间数据传输
当对Android有一些了解后,不难发现,Android程序UI框架接近于Web页面的概念。每一个用于呈现页面的组件,Activity,都是彼此独立的,它们通过系统核心来调度整合,彼此之间的通过Intent机制来串联。 每一种架构都会有其利弊,Android当然也不能超然脱俗。由于Activity之间的松耦合关系,使得其复用能力特别的出色,Mash-Up方式可以有效的提高开发效率。但另一方面,由于Activity过于的独立,它们之间的数据共享,成为一个麻烦的事情。 基于消息的传输 最标准的Activity之间的数据传输,就是通过Intent的Extra对象。比如,你在A这个Activity上拿到一坨用户输入的文本信息,兴高采烈的想把它放到B这个Activity上展示并发送,一个很可行的方式,是通过Intent的putExtra接口,把用户输入的那些字符信息,按照key/value的形式放进Intent,传输到B这个Activity上。 如上图示,从A到B的传输,看上去是一个直连,但其实,Intent都是要经由系统核心层去分析调度的,这个操作,跨越了进程边界,自然而然,其中的数据,就是需要序列化和反序列化的,而不可以仅通过一个指针就倒腾过去了。 基于这样类消息的传输模式,好处不多说,直接谈问题: 首先,对于大数据,就是一场杯具,不可能一坨上M的数据,也来来回回的传来传去,慢死了谁来负责; 再则,Activity之间,维系的是一种线性关系,当我想把一份数据,从队尾一级级传到队头的话,自己历经磨难不提,会把中间所有的Activity都搭上,他们明明自己可能不需要这份数据,也得拿着搁着,为他人做嫁衣裳,不惆怅都不行; 此外,基于消息的传输,会把同一份数据生成若干个副本,有时候,这样很好,没有副作用,大家自己玩自己的不需要看别人脸色,但还有些时候,你就上杆子需要把这些数据都修改一下,同步起来那就太惨烈了; 最后,写序列化代码实在是太无聊了,稍微复杂点的代码,就要自己写个序列化接口,整个吃力不讨好的活计。 基于外部存储的传输 既然兽兽手手相传太幸苦,自然而然的想法就是找个地方,A把数据搁在那里,把地址信息告诉B,B需要的话,按图索骥,自取就好。这个搁东西的地方,可以考虑选择外部存储。 在Android中,预设了一些快捷便利类和模块,更好的支持不同类别数据的存取。如果,需要存储的是一些小数据量的配置信息,可以选择Preference,它等同于传统意义上的设置文件。Preference提供了一些基于key/value的存取接口,可以放置一些简单的基本数据或者派生了Parcelable接口的对象。一个很好的应用场景是Cookie的存放。你在登录界面获得了一份Cookie,你可以把它扔进Preference,谁想要谁去拿,再也不要来来回回的折腾了。 Preference适合于小数据、设置信息,如果大数据,你可以考虑使用数据库。在Android中,使用的是Sqlite,相关的类,堆放在android.database名字空间下,自查,无需赘述。 在Android里,数据库是私有的,如果想分享给第三方组件使用,就需要用ContentProvider来封装了。比如你用系统的录音机组件即时搞一段音频信息,它不是返回可能大到恐怖的录音数据,而是会返回给你一个Uri,它标明了这份数据在ContentProvider的地址信息,拿着这个Uri,领取数据就好。 当然当然,如果你足够淡定,也可以用赤果果的File来存储。如果这个文件存在手机私有目录下,那就内部使用,放在SD卡上,那就可以所有应用,一切分享。 基于这样外部存储的数据传输,优缺点显而易见,它解决了困扰Intent的传输路径复杂,不利于传输大批量数据的问题,但同时,它有留下了效率隐患,复杂了编程模型。因为面对外部存储,开发者必须要考虑效率问题,很多时候,多线程就会被提上议程,这样,想不麻烦,都不行鸟。 基于Service的传输 既然存在外部太慢,那么还是在内存级别解决问题好了,这时候,你可能就需要请出Android四大组件之一的Service了。Service设计的本意,就是提供一些后台的服务,数据存取,也可以归于其职责的一部分。 Service是提供了直连机制,调用的Activity,可以通过bindService方法,与目标Service建立一条数据通路,拿到IBinder。这样,通过Android提供的IPC模型,就可以进行远程方法的调用和数据的传输了。 如上,通过这种模式,可以解决一定问题,但是对于Service来说,实在是太大才小用了,Service的专长,不是在数据,还是在逻辑。对于传数据而言,Service还是重量了一点,不但是有连接耗精力,传输经由IPC,写起来也够费劲。而且作为组件,Service随时可能死掉,你还是要费劲心机的处理数据的持久化,得不偿失。 利用Application传输 好吧,如果你需要在不同页面之间共有某个内存对象,很合适的一种方式是把它们扔到Application里面。Application是Context的一个子类,它会在整个应用任何一个组件起来之前,先起来嘘嘘。它的生命周期会贯穿整个应用所有组件的生命旅途,因此,放在其中的对象,不会被处理掉。 在Activity中,可以通过getApplication接口,随时获得Application对象的引用,用于实现一些全局对象的存储,和处理,真是最合适不过的地方了。 当然,好东西也不要使用过度,可以想象,由于Application存活周期长,其上引用的对象一直缺少被释放的机会,如果你把它当成垃圾场,什么东西都往里扔,污染环境,混乱逻辑不提,单就是滥用内存资源这一项,就够罪孽深重一把了。 因此,如果数据不是真的需要全局使用,不要搁在其中,如果数据太大,不要全部load出来,合理使用数据库等外存储设备,还是必须要的。 结语 还有一些特殊情况,可以考虑用一些特殊的方式。比如子Activity之间,可以通过调用getParent获得父Activity的引用,来访问期间的对象,云云。小众情况,姑且不提。 以上这些概念,我相信所有的coder都了如指掌,如何处理这样的数据,都心如明镜。我只是给它们套上了一件Android的外衣,让初入Android的coder们,能迅速找到心仪的兵器,劈山砍石,攻城拔寨。
Drawing.png (9.33 KB)
yeaxzz 上传了这个附件:
yeaxzz 上传了这个附件:
yeaxzz 上传了这个附件:
yeaxzz 上传了这个附件:
yeaxzz 上传了这个附件:
|