最近我一直在回答学生的以及StackOverflow上的问题,比如Activity里面应该写什么、如何在需要Context的时候获得它、如何在UI线程上进行异步任务以及为什么要用Fragment。这些问题归根结底都是在问:“我如何完成这些被Android搞得很麻烦的事情?”
不可避免的,大多数答案所提供的方法都是***黑客色彩的,虽然技术上是可行的,但不应当遵从。这些问题表现出的是对Android框架中某些类的根本误解,正确的回答应该是:你误解了这个类的用途,本来这个类就不是干这个事儿的!
我甚至还看到过相关的开源框架,在解决某个因为框架误用才产生的问题。以Infograph for Robospice为例,Robospice的基础假设是:“AsyncTask有一个大问题:它和Activity的生命周期结合不够紧密.”这个假设无疑是正确的,但这本来就不是个问题,这是AsyncTask自身的特点。任何异步的任务都不应该绑定在某个Activity或其生命周期上。这个框架的存在会导致开发者误用Activity类。
我一直想对这些误用问题给出一个统一的回答,可是想不出来,直到这周我重新阅读了Robert Martin的《代码整洁之道》中有关命名规范的章节。当我读到“添加有意义的语境”部分时,我意识到,我们通过每个Android组件的名字就可以清晰地理解其功能。下面让我们探讨有关process(进程)、thread(线程)、application(应用)、activity(活动)、task(任务)、Fragment(碎片)与context(上下文)的命名。读完之后你将对这些组件的职责有更准确的理解,并能发现我们为什么在误用这些组件。
PROCESS
在书中“使用解决方案领域名称”部分里,Martin写到:
记住,只有程序员才会读你的代码。所以,尽管用那些计算机科学术语、算法名、模式名、数学术语吧。 |
Process(进程)的名字就是这么来的。在Android系统中,Process只不过是一个普通的计算进程,这是计算机所关心的东西,使用者自然不会关心它,事实上开发者也很少使用到这个概念。当我们谈论进程、多进程实现与进程间通信时,你会想到这些在计算机内是如何完成的。
在Android中,每个Application(应用)会在自己的Linux线程中开启,当系统需要回收内存时,最终会杀掉一个进程。进程的五个状态包括:forground(前台)、visible(课件)、service(服务)、background(后台)和empty(空)。所以简单来说,一个进程包括指令集和内存空间,而且是可被系统杀死的单元。
TASK
在书中“使用源自所涉问题领域的名称”部分中,Martin写到:
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了……与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。 |
Android设备所解决的最基本的问题是什么?它们能帮助人们完成任务。这看起来异常简单,但想想过去在智能手机刚被发明的时候人们是如何憧憬的。谁也不知道未来人们能用手机完成什么事情,但是开发者所开发的工具根本上都是为了帮助人们完成任务的,也就是task(任务)。任务是以人为中心的术语,它包含未知数目的步骤,但有一个核心的主题。在日常生活中有哪些任务实例呢?打扫房间、开车上班、逛超市,这些都是任务。人们在生活中可能要完成许多任务,但在同一时刻只能进行一项任务。人可以开始或停止做任务,也可以在任务间切换。
Android中也是这样。使用者在点击应用图标时开始一个task,可以在最近任务界面中看到最近的task,可以将task暂停或重新开始,甚至可以通过在最近任务界面中移除某task来完全销毁掉这个task。最重要的是,使用者只能同时与一个task进行交互。
ACTIVITY
就像task一样,activity是问题领域中的术语。每个task包含了一个或多个activity。当一个人在进行任务时,他需要完成许多活动。比如打扫房间时,一个人可能正在叠衣服,当他叠完衣服时,他要开始清理浴缸。在完成一件活动前,他可以暂停并跳转到当前任务下的另一个活动中。
当一个人在任务间切换时,他需要先暂停***个任务中的一个活动,然后开始第二个任务中的某个活动。比如当一个人从打扫房间转到开车上班时,他需要先停止叠衣服的活动,并开始走向他的车。
虽然人可以在多个活动间切换,他同时只能进行一个活动。没有人可以同时叠衣服和擦马桶,如果有人尝试那么画面一定颇为娱乐。就算他在活动之间来回跳,也要线性地进行任务。
Android中Activity也类似,Activity是一个面向使用者的概念,它描述了使用者正在做的事情。一个Task包含了一个或多个Activity,但使用者只能同时与一个Activity交互,这个Activity通过占据整个屏幕来获得使用者所有的注意力。使用者在进行任务切换的同时会停止与启动Activity,每个Task都会记住用户正在使用哪个Activity,这样通过回退键回退时就会回到正确的Activity。
Activity的概念是基于使用者的,所以它负责向使用者展示数据并响应输入,也正因此,它不应该进行任何幕后操作,比如数据库读写、网络请求与大量计算。这些代码模块都应该与Activity解耦,并离开用户的视野。
只有当对使用者产生直接影响的变化发生时,计算机才会操作Activity生命周期,这些变化叫做配置信息改变,比如设备旋转。
这也是为什么我们不应该在设备旋转时让AsyncTask存货。Robospice看起来帮助了想在Activity中启动AsyncTask的开发者。其实,这个类库本不应这么做,因为Android中有更简洁的架构模式来处理这些情况。
Fragment
Activity大到一定程度后就需要分割成更小的组件,现在暂时我们还没有用于表示这个小组件的术语。这些Activity的部件是面向用户的,所以我们不能去找解决方案领域的名称。我们可能会想到“Sub Activity(子活动)”、“Component(组件)”、“Part(部分)”或是“Partial Activity(部分活动)”这些名称,但是这些不免有误导信息。Martin在“避免误导”部分中讲:
程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。 |
“sub Activity(子活动)”可能会被误解为Activity的子类。”Component(组件)”和”Part(部分)”都太模糊。“Partial Activity(部分活动)”隐含意是一个”部分活动”是不够的,我们需要多个这种组件来构建一个Activity。说实话,我猜当内部人员在设计框架时,对于这个组件的命名的争论最为激烈。开发者最终选择“Fragment(碎片)”这个词,我也想不出更好的词了。
就像Activity一样,Fragment的目的一样是完成任务,但是尺度则小了一些。比如“擦浴缸”与“打扫厨房”这两件活动,都需要以“准备清洁用具”这个碎片为开始。所以“准备清洁用具”这个碎片是可以在多个活动中使用的,而且不清楚整体的活动到底是擦浴缸还是打扫厨房。但比如“给花浇水”这类很小的活动,它本身就不需要再分碎片。
有些碎片很小很小,它们可以同时进行。比如“擦浴室瓷砖“和”擦浴室玻璃“是可以同时完成的,虽然它们是不同的碎片。
当使用者停止了一个任务或是任务中的一个活动时,自动就会停止一个碎片。一个人可以在多个碎片间切换,而不会停止活动。例如,一个人可以停止“准备清洁用具”并开始“往桶中灌水”,但他一直在进行”拖地”这项活动。
Android中的Fragment也是这个道理。一个Activity可以包含零个或多个Fragment,两个Activity可以包含同一个Fragment的不同实例。如果一个Fragment足够小,而且和另外的Fragment紧密相关,它们可以同时被显示,比如master/detail design pattern(主-从视图)。
使用者可以以任何顺序在Fragment间切换。开发者甚至可以让用户通过回退键在多个Fragment间回退。当使用者停止或暂停一个Activity时,Activity也会停止或暂停它的Fragment。
THREAD
Thread(线程),就像Process(进程)一样,也是解决方案领域的名称。就像Process一样,thread也只是一个计算机科学概念的基础实现。Android没有把事情搞得太复杂,直接使用了Java的thread。Android中的多数Thread都是Java Thread。Java Thread表现很好,Android只需要继承Thread一次就能实现HandlerThread。
就像Process一样,Thread是一个面向计算机的概念。使用Thread是为了同时完成多件事。就像计算机科学中线程的概念一样,多个Thread实例可以存在于一个Process中。
使用者永远不关心每个线程上正在发生什么,或是到底有多少个线程在运行。开发者使用Thread是为了告诉计算机干的再快一点。因为开发者是与计算机交谈,需要像计算机一样思考。所以涉及线程的问题一般都更难理解与debug,开发者需要考虑计算机的时间,而不是人类的时间,开发者需要思考有关内存访问的问题,以及要不要限制某些组件只能在规定时间访问规定内存空间。Thread之一,main thread又称UI Thread,负责监听使用者输入并与使用者交互。这个线程不应该进行任何后台操作。所有的Activity都存活在这个线程中,并完成其所有工作。每个应用开启时都只有一个线程,至于是否需要多个线程则是开发者所要考虑的问题。HandlerThread是进行后台任务的一个很好的选项。
APPLICATION
就像Process和Thread一样,Application(应用)也是解决方案领域的名称,也是一个基本的计算机科学概念。一个Application是一个帮助使用者完成(多个)任务的软件。但是,Application已经成为使用者与开发者所共同熟知的概念,所以有时它会带来一些面向使用者的问题。
银行和超市是物理意义上的Application(应用),它们被创建出来的目的就是帮助人们完成任务。超市的某些部分是直接与人交互的,但有些部分是在后台帮助超市实现功能,比如仓库和会计室。
许多应用可以组合成一个任务来帮助使用者完成更多的事情,即使这些应用是不同的人写的。如果使用者正在进行烤蛋糕的任务,那么他可以去超市来进行“购买原材料”的活动,然后回家继续烤蛋糕。只要有人在与一个应用交互,这个应用实例就存在。
开启一个应用可以说是开启一项任务的一部分。比如我们要开始做打扫房间这项任务,隐含意就是房间已经存在。当一个应用不再被使用时,它会被关闭掉。当所有的人都完成了超市中要完成的任务,超市就会关门。当一个人完成了要在家做的所有任务时,他就会把所有东西都关掉。
Android中的Application也是这个道理。启动一个Task,进而启动一个Activity的一部分就是启动一个Application。一般来说,开发者只需要创建一个Application实例就可以服务所有的Activity,当用户完成了所有的Activity,Android可能就会销毁Application。
在进行一项任务时,使用者可以转换到另一个Application来帮助他完成附加的活动,比如通过相机应用,使用者可以启动邮箱应用来发送有图片附件的邮箱。这两个应用是可以由不同的开发者开发的,但是都属于一个任务。
如果使用者在邮箱应用中强制停止当前任务,系统会停止邮箱应用中的Activity。如果使用者只是暂停并继续当前任务,系统会立即重新启动邮箱应用中的Activity。
如果使用者直接启动邮箱应用,系统会开启一个新的任务。相机任务中属于邮箱应用的Activity不受新创建的邮箱任务影响。也就是说,同一个Activity的不同实例可以在不同的任务中生存。
即使使用者没有与任何Activity进行交互,一个Application还是可以存在,比如当一个Application只在进行后台任务,没有Activity被展示,Application一样可以生存。
CONTEXT
有一篇来自Dave Smith的博客很好地解释了不同种类的Context(上下文)以及它们的功能。但什么是Context呢?很简单。Context就是Context(上下文)。
在我们的Android基础系列课程中,我将Context比作一个深入操作系统的钩子。因为Activity需要系统来启动,你需要使用Context来要求系统启动一个Activity)。因为系统可以填充布局,你需要通过Context来要求系统填充布局。因为系统可以发送广播,你可以通过Context来发送广播或是注册BroadcastReceiver(广播接收器)。因为系统提供系统服务,你可以通过Context来访问系统服务。
但是这些事情并不能随处完成。比如在Service中,要求系统展示对话框或是启动Activity是毫无意义的。再比如在Application中,系统甚至都不知道当前Activity的theme是什么,所以让系统填充布局是毫无意义的。所以当你要求系统来完成任务时,你需要告诉系统你是在哪种Context(上下文)里想要做这些事情。当系统认定你想要做的事情对于当前上下文是合理的。Context的名字就是这么来的。就像Task和Activity一样,Context属于问题领域的名称。
Context有许多子类,但我们主要会用到Application、Activity和Service。这几个组件都描述了不同的上下文,可以做不同的事情。Dave Smith的文章详细讲述了它们都能干什么。
Context是暂时的,随着时间变化会发生改变。当处理有关Context的问题时,我总是告诉自己:这个Context可以用于操作这个类,可是不一定能操作其他类。内存泄漏的一大来源就是到处传递Context并让其他对象持有其引用。如果这里的Context是一个Activity,那么Activity在生命周期结束后也不会被回收,所以到处传递Context并不是一个好主意,我们不应该这么做。
当你的确需要传递Context对象时,要确保这个Context对象适用范围越大越好,一般来说我们应该传递一个Application Context。当你传递Context时,不要假设传递过来的人考虑过你的需求,也不要假设传过来的就是正确的Context。
- public void doSomething(Context context) {
- // 不要假设传过来的就是Application
- mContext = context;
- // 永远要手动获取Application Context
- mContext = context.getApplicationContext();
- ...
- }
所以呢?
明白了这些名字是哪里来的又有什么用呢?我经常看到学生的问题是如下格式:我怎么让X完成Y?他们其实应该先问问自己:X应该完成Y吗?为了回答这个问题,他们需要问:X的单一职责是什么?对于所有的Android框架中的组件,其职责就体现在命名中。这个名称可以映射到问题领域或是解决方案领域的名臣很高。解决方案领域的名称,如Process、Thread和Application,是开发者所理解与熟知的。问题领域的术语如task、activity、context和fragment则只当我们在描述现实生活中的问题时才可以被理解。
所以当你下次想问如何在Activity中完成异步任务,或是如何通过Context对象获取系统组件,或是Process(进程)、Thread(线程)与Task(任务)之间的区别是什么时,看看名字就知道答案了。
这篇文章将在Android N推出时被推翻,因为多个Activity可以同时存在于一块屏幕上。
这么说好像很傻。