将数据与AdapterView绑定

AdapterView是ViewGroup的一个子类,其子类View由一个Adapter来决定与何种类型的数据相绑定。AdapterView在需要在布局中显示储存的数据(与资源字符串或是可绘制内容相对)时是非常有用的。

Gallery、ListView和Spinner是可以以特定方式用来与特殊类型数据绑定显示的AdapterView子类的几个例子。

AdapterView对象有两项主要任务:

  • 用数据填充布局
  • 处理用户选择

用数据填充布局

通过把AdapterView类与一个Adapter绑定可以将数据插入布局之中,Adapter会从外部资源获取数据(比如在代码中提供的一个列表或是来自设备的数据库中的一个查询结果)。

下面的范例代码的功能是:

  1. 借助于一个已有的View创建一个Spinner并将其绑定至一个新的ArrarAdapter来从本地资源中读取一列色彩。
  2. 在一个View中创建另一个Spinner并将其绑定至一个新的SimpleCursorAdapter来从设备通讯录中读取联系人姓名(参见Contacts.People)。

注意必须让People._ID栏通过CursorAdapter投影,不然会收到一个异常。

如果在程序的生命周期内改变了Adapter所读取的数据,那就该调用notifyDataSetChanged()。这将告诉被绑定的View数据已被改变以刷新显示。

处理用户选择

可以通过将类的AdapterView.OnItemClickListener成员与一个监听器(listener)相结合并捕获选择动作来处理用户选择。

关于如何创建不同类型的AdapterView的更多内容,请阅读下面的教程:Hello Spinner、Hello ListView以及Hello GridView。

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

Android中如何自动弹出软键盘

代码与范例:

说明:同样是在写Techo Lite时遇到的问题。希望程序可以自动弹出软键盘,可是尝试了各种办法都没有效果。问题的原因似乎是必须等待UI绘制完成后弹出软键盘的代码才会有效。于是就采用了上面的方法。需要注意的是,在此之前必须让edittext获取焦点,不然也是无效的。

Android中EditText如何定位光标位置

代码:edittext.setSelection(int);

范例:

说明:在写Techo Lite时遇到的问题。找了一下发现EditText控件提供了这样一个函数以设置光标所在位置。

进程和线程

在一个程序的仅有的一个应用程序组件启动时(除此之外没有其他组件在运行),Android系统会为该程序开启一个包含一个单独执行线程的新的Linux进程。默认来说,同一个程序的所有组件都运行于同一个进程的同一个线程中(被称为“主”线程(main thread))。如果一个组件在启动时该程序已经有一个进程存在的话(这是因为该程序存在有其他的组件),那么组件将运行于这个进程中且共用线程。不过,可以安排程序中的不同组件运行于不同的线程,以及为任何进程创建新的线程。

本文档将讨论Android应用程序中的进程和线程是如何工作的。

进程

默认地,同一个应用程序的所有组件都运行于同一个进程,大部分的程序不应该改变这一点。不过,如果认为有必要控制某个特定的组件所属的进程,可以在manifest文件中进行设定。

每一种类型的组件元素——<activity>、<service>、<receiver>和<provider>的manifest条目都支持android:process属性来指定组件应当运行于哪一个进程之中。可以设置该属性以使组件运行于其自有的进程或让一些组件共用进程的同时另一些使用独立进程。还可以通过设置android:process来事不同程序的组件运行于统一进程——假设这些程序共享同样的Linux用户ID并被相同的权限标记。

<application>元素也有android:process属性,用来给所有的组件设定默认值。

Android在某些时候(比如内存不足而用户又马上需要更多的内存来运行其他的进程)会做出关闭某个进程的决定。运行于该进程的应用程序组件也就相应地会被销毁。当再次需要它们时将会重启一个进程。

在决定终止哪一个进程时,Android会权衡它们对于用户的重要性。比如,系统更有可能关闭一个活动在屏幕上不可见的进程而不是一个活动仍然可见的进程。因此,是否终止一个进程取决于进程中组件的运行状态。具体决定终止哪一个进程的规则将在之后讨论。

进程生命周期

Android系统总是试图尽可能长时间地保持一个程序进程,不过旧的进程最终将被移除来为更为重要的进程提供内存。为了决定保留哪个进程,杀除哪个进程,系统将每一个进程置于一个基于进程内运行的组件及其状态的“重要性层级”之中。最低重要性的进程将被最先去除,之后是次低重要性的,以此类推直至能够提供足够的系统资源。

重要性层级中有五个等级。下面根据重要性顺序列出了不同类型的进程(第一种进程是最为重要的,将被最后杀除):

1. 前台进程

一个用户当前正在使用的进程。如果以下任一条件被满足,进程就被划归为该类型:

  • 它包含一个用户正在进行交互操作的活动(该活动的onResume()方法已被返回)。
  • 它包含一个与用户正在进行交互操作的的活动相绑定的服务。
  • 它包含一个“在前台运行的”服务——服务调用了startForeground()。
  • 它包含了一个正在执行其生命周期回馈方法之一(onCreate(),onStart()或onDestroy())的服务。
  • 它包含了一个正在执行其onReceive()方法的广播接收者。

通常,在某一时刻只会存在少数的前台进程。它们将被最后杀除——在内存极端不足的情况下。通常此时设备处于内存分页状态,需要杀除前台进程来保持用户界面响应。

2. 可见进程

这类进程没有任何前台组件,但是可以影响用户在屏幕上看到的内容。如果以下任一条件被满足,进程就被划归为可见:

  • 它包含一个不在前台但是对用户可见的活动(其onPause()方法已被调用)。比如,前台活动是一个对话框,而前一个活动仍可以被看到存在于其后方的情况。
  • 它包含一个与可见活动(或前台活动)相绑定的服务。

3. 服务进程

这是一类运行着由startService()方法启动的服务但不属于前两种进程的进程。尽管服务进程不与任何用户直接看到的内容绑定,它们确实在执行用户所希望的工作(比如背景播放mp3或是从网络下载数据),因此系统只有在亟需内存维持所有的前台和可见进程时才会杀除他们。

4. 后台进程

这是一类包含一个当前不对用户可见的活动(活动的onStop()方法已被调用)。这些进程对用户体验没有直接影响,当前台、可见或服务进程需要内存时它们会被随时杀除。通常有着大量的后台进程在运行,它们被保存在一个LRU(Least recently used)表中以确保最后看到的活动所在的进程会被最后杀除。如果一个活动正确地执行其生命周期方法并保存其状态,杀死它的进程不会影响用户体验。这是因为当用户返回活动时,所有的可见状态将被恢复。参见“活动(Activities)”文档以了解关于保存与恢复状态的更多信息。

5. 空进程

这是一类没有任何活动的(active)应用程序组件的进程。保持这样一个进程的唯一理由是作为缓存来提高下一次有组件要在其内运行时的启动速度。系统为了在进程缓存和底层核心缓存间平衡整体系统资源而常常杀除这些空进程。

Android根据进程中当前活动着的组件的重要性来尽可能高地给一个进程定级别。比如,如果一个进程包含一个服务和一个可见活动,那么进程会被分级为可见进程而不是服务进程。

另外,进程的层级可能会因为其他进程依赖于它而被提升。服务于另一个进程的进程不可能比那个进程的层级低。比如,进程A中的一个内容提供者服务于进程B中的一个客户端,又或是进程A中的一个服务与进程B中的一个组件绑定,那么进程A被认为至少要和进程B同样重要。

由于运行服务的进程比运行后台活动的进程层级高,一个执行长时间操作的活动应该开启一个服务来处理该工作而不是简单地创建一个工作线程——特别是该操作可能比活动本身持续更久时。这样的例子有个后台播放音乐或是上传一张拍摄的照片到网上。使用服务至少可以确保该操作有服务进程的优先级,而不必在意活动本身怎样。正如之前在广播接收者生命周期一节中提到的,这也是为什么广播接收者应当用服务而不是用线程来执行花费时间的工作的原因。

线程

当一个程序被启动后,系统将创建一个被称为“主线程(main)”线程来执行该程序。这个线程非常重要,它负责正确地向UI控件分发事件,包括绘图事件。它同时还与Android UI toolkit中的组件(即android.widget和android.view包中的组件)进行交互。因此,主线程有时也被称作为UI线程。

系统不会为每一个组件的实例创建新的线程。所有运行于同一进程的组件都在UI线程中被实例化,系统对它们的调用也都是从该线程中分发。因此,要响应系统回馈的方法(比如回馈用户操作的onKeyDown()或是生命周期回馈方法)总是要运行于进程的UI线程中。

例如,当用户点击屏幕上的一个按钮,程序的UI线程将分发触摸事件至控件,控件将设置其点击状态并向事件队列发送invalidate请求。UI线程会将此请求出列并通知控件进行重绘。

当程序在响应用户交互操作中需要处理密集型工作时,这样的单线程模式可能会在没有很好的设计程序的情况下降低程序表现。特别是如果所有的任务都由UI线程执行,网络连接或是数据请求等长时间操作将会阻塞整个UI。当线程被阻塞,事件就无法被分发,包括绘图事件。从用户的角度来看,程序似乎就停止响应了。更糟糕的情况下,如果UI线程被阻塞的时间过长(目前来说大约是5秒)用户就会看到糟糕的“程序没有相应”(application not responding,ANR)对话框。用户之后就会考虑退出程序,甚至不满地卸载它。

另外,Android UI toolkit不是线程安全的。因此,不能在工作线程中进行UI处理——必须将所有用户交互操作在UI线程中实行。于是,对于Android的单线程模式有两条规则:

  1. 不要阻塞UI线程
  2. 不要在UI线程外读取Android UI toolkit

工作线程

由上面所描述的单线程模式可知,不阻塞UI线程对于程序UI的响应性是很重要的。如果不得不执行无法立即完成的工作,就必须确保它们处于不同的线程之中(“后台”或“工作”线程)。

例如,下面是一个点击监听器(click listener)的代码,它将从另一个线程中下载一张图片并将其显示在一个ImageView中:

最初这看起来能很好的运行,因为它创建了一个新的线程来处理网络操作。不过,它违反了单线程模式的第二条规则:不要在UI线程外读取Android UI toolkit——这个范例在工作线程中修改了ImageView而不是在UI线程中。这可能导致未定义的不被希望的行为,并且可能要花费很长时间来解决这个问题。

要解决这个问题,Android提供了几种不同的方法来从其他线程中接入UI线程。这里列出了有用的一些方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

例如,可以用View.post(Runnable)方法来解决上面的问题:

现在代码是线程安全的了:网络操作在另一个线程中完成,同时ImageView是由UI线程处理的。

不过,当操作的复杂性提升时,这样的代码会变得很复杂而难以维护。要处理更为复杂的与工作线程的交互,应该考虑在工作线程中使用Handler,来处理接收自UI线程的消息。不过,最好的解决方案或许是继承一个AsyncTask类,来简化工作线程与UI间交互任务的执行。

使用AsyncTask

AsyncTask允许在用户界面中执行异步工作。它在一个工作线程中执行阻塞性操作并把结果发布于UI线程而不需要处理线程关系。

要使用它,必须继承AsyncTask并使用doInBackground()回馈方法(它运行于后台线程池中)。要更新UI,需要使用onPostExecute()以传递来自doInBackground()的结果来安全地更新UI。之后可以通过在调用execute()来运行UI线程中的任务。

例如,可以像这样使用AsyncTask来实现上一个范例:

现在不但UI是安全的,代码也变得更简单了,因为工作被分成了需要在工作线程中完成的部分和应当在UI线程中完成的部分。

应该阅读AsyncTask的参考文档来了解关于如何使用这个类的完整信息,不过这里有关于它工作方式的简介:

  • 可以使用generic来指定参数类型,进度值(progress value)和任务的最终值
  • doInBackground()方法自动在工作线程中执行
  • onPreExecute()、onPostExecute()和onProgressUpdate()都在UI线程中
  • onPreExecute()所返回的值被发送至onPostExecute()
  • 可以在onInBackground()执行的任意时刻调用publishProgress()以在UI线程中执行onProgressUpdate()
  • 可以在任一时刻,在任意线程中,取消任务

注意:在使用工作线程时可能遇到的另一个问题是由于运行配置的改变而活动自动重启(比如用户改变了屏幕方向),这可能会销毁该工作线程。要防止任务在这种情况下重启,并在活动被销毁时正确地取消任务,请参见Shelves范例程序的源代码。

线程安全方法(Thread-safe methods)

在某些情况下,所使用的方法可能会被不止一个线程调用,因此这些方法必须是线程安全的。

对于可以被远程调用的方法——比如在绑定的服务中的那些方法——来说,这是非常必要的。当在一个IBinder所运行的进程中调用一个在该IBinder中使用的方法时,这个方法会在调用者的线程中被执行。然而,如果这个调用是在另一个进程中的,那么这个方法将在从线程池中选择的一个系统保留的与IBinder处于同一进程的线程中执行(并不是在进程的UI线程中被执行)。例如,尽管一个服务的onBind()方法会被在服务的进程的UI线程中调用,对象中onBind()所返回的方法(比如,使用了RPC方法的一个子类)会在线程池中的线程中被调用。因为一个服务有不止一个客户端,所以多个池线程可以同时和同一个IBinder方法相关联。因此IBinder()方法必须是线程安全的。

线程间通信(Interprocess Communication,IPC)

Android提供了一种使用远程过程调用(remote procedure call,RPC)的进程间通信(IPC)机制,使得一个被某一活动或其他应用程序组件调用的方法将被(于另一进程中)远程执行,而所有的结果将被返回给调用者。这将会分解一个方法的调用及其数据至操作系统可以理解的级别,将其从本地进程及地址空间传输至远程进程及地址空间,然后在远程进程中重新组装激活这个调用。之后返回值将被反向传输。Android提供了所有执行这样的IPC事务所需的代码,因此只需关注如何定义和使用RPC编程接口即可。

要进行IPC,程序必须通过bindService()和一个服务相绑定。更多信息请参阅“服务开发者指南”。

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

Android用记事本程序 Techo Lite

虽然标题中用的是“记事本”,不过更确切的说,应该是一个简单的便筏程序。

Android平台上各类记事本程序已经数不胜数了,自己写这样一个程序的动机纯粹是满足自己的需要。和其他同类程序相比,Techo Lite的特点在于:只能保存一条记录,因而进入程序后直接可以编辑。也就是说,比其他类似程序要少一个选择要编辑的文件的步骤。

这一点对我来说很重要。我平时常常会需要用手机记录一些简单的文本,因此希望程序可以尽可能快地进入编辑模式。此外我一般只是用作短时记忆用,所以也并不需要能保存多条记录。万一遇到要记录几条不同的内容,将它们都记录在一条中也不会有太大的问题。之前也用过各种各样的程序。Color Note的问题在于进入程序后仍然要点击两次才能进入编辑模式,而且两次点击还需要移动手指位置,不是特别人性化。而另一款程序AK Notepad在进入程序后只需要再点击一次,选择要编辑的文件后即可开始编辑(如果使用桌面插件也可以实现一次点击即进入编辑)。可惜最新升级的版本不知怎么在自己的手机上经常会崩溃,于是只好暂时放弃使用。

由于以上原因,自己决定动手写一个简单的便筏程序供自己使用。程序本身相当简单。进入程序后即可编辑文本内容,退出程序后内容就会被自动保存。另外还有清空已输入内容和更换背景颜色这两个功能,除此之外就没有其他多余的功能了。事实上,设计程序的图标和考虑中日英的程序简介文本所花的时间比程序本身所花的时间更长。程序名称中的Lite有两个意义。其一是指明这个程序的功能将会相当简单,其二是因为我另外还有编写一个全功能记事本的计划,会命名为Techo。它将无缝集成文本和涂鸦两种输入模式并提供文件输出等一系列功能。不过这个计划的优先级不是很高……

总结来说,相比现有的记事本程序,Techo Lite具有以下特点:

  • 操作快捷。由简洁的设计理念,使得Techo Lite不但程序结构简单,没有冗余的操作步骤,而且事实上的反应速度也相当迅速。
  • 功能精简。完全没有同步等文本编辑之外的多余功能。没有繁琐的设置选项。这在一定程度上也保证了Techo Lite基本不存在Bug,且与任何终端都保持了高度的兼容性。
  • 可更换背景色。并不是所有同类程序都可以更换背景色。Techo Lite虽然简单,但是仍提供了多种背景色可供选择,以满足不同的人的喜好。

目前Techo Lite可以在Android Market的效率类应用程序中找到,搜索Techo或是pub:”BREEZE”即可。如果正在寻找一款将“减法法则”发挥到极致的记事本程序,不妨试试看Techo Lite吧。

Android Market页面:

https://market.android.com/details?id=org.breezesoft.techolite

最后是几张Techo Lite的截图: