当一个应用程序开始运行它的第一个组件时,Android会为它启动一个Linux进程,并在其中执行一个单一的线程。默认情况下,应用程序所有的组件均在这个进程的这个线程中运行(就是我们常说的android app主线程)。然而,你也可以安排组件在其他进程中运行,而且可以为任意进程创建额外的线程。本章主要介绍android app下的线程和进程是如何工作的
默认情况下,同一应用程序的所有组件运行在同一进程中。不过,如果你需要控制某个组件属于哪个进程,也可以通过修改manifest文件来实现。manifest文件中的所有组件节点如<activity>,<service>,<receiver>,<provider>都支持android:process这个属性并可以指定一个进程,这样这些组件就会在指定的进程中运行。你可以设置这个属性使每个组件运行于其自己的进程或只是其中一些组件共享一个进程。你也可以设置android:process让不同应用中的组件可以运行在同一个进程,但这样需要这些应用共享同一个Linux用户ID并且有相同的数字证书签名。<application>元素也支持android:process属性,用于为所有的组件指定一个默认值,并应用于所有组件,如果你有这样的需求,可以省点事。Android系统可能在某些时刻决定关闭一个进程,比如内存很少并且其他进程更迫切的需要服务用户而启动的情况。进程被kill掉后,其中的组件们都被销毁。如果再次需要这些组件工作时,进程又会被重新创建出来。当系统决定关闭哪个进程时,Android系统会衡量进程与用户的相对重要性。例如,比起一个在前台可见的activity所在的进程,和那些在后台不可见的activity所在的进程相比,显然后者更容易被系统关闭。是否决定一个终止进程,取决于进程中所运行组件的状态。下面将详细讲述。
5.1.1进程的生命周期
Android系统会尽量维持一个进程的生命,直到最终需要为新的更重要的进程提供内存时,它才会移除不太重要的旧进程。为了决定哪个进程该终止,系统会根据这个进程内的组件状态把进程置于不同的重要性等级。当需要系统资源时,重要性等级越低的先被kill掉。系统分为 5个重要性等级。下面列出了不同进程类型的重要性等级(第一个进程类型是最重要的,它最后才会被kill)
1. 前台进程
用户当前正在做的事情所属的这个进程。如果满足下面的条件,一个进程就被认为是前台进程:
(1)这个进程拥有一个正在与用户交互的Activity(这个Activity的onResume() 方法被调用)。
(2)这个进程拥有一个正在与用户交互的activity之中并与之绑定的Service。
(3)这个进程拥有一个前台运行的Service(即service已经调用了startForeground()方法)
(4)这个进程拥有一个正在执行一个生命周期回调方法(onCreate(),onStart(), 或onDestroy())的Service。
(5)这个进程拥有正在执行其onReceive()方法的BroadcastReceiver。
通常,在任何时间点,只有很少的前台进程存在。它们只有在万不得已的情况下时才会被终止--如果内存太小而不能继续运行时。通常,到了这时,设备就达到了一个内存分页调度状态,所以需要终止一些前台进程来保证用户界面的响应。
2. 可见进程
一个没有任何前台的组件的进程,但是依然对用户所见。满足下列条件时,进程即为可见:
(1)这个进程拥有一个不在前台但仍可见的Activity(它的onPause()方法被调用)。例如当一个前台activity启动一个对话框形式的activity时,就出了这种情况。
(2)这个进程拥有一个绑定到前台Activity的service。
一个可见的进程是极其重要的,通常不会被终止,除非内存不够,需要释放内存来让前台进程运行。
3. Service进程
一个进程不在上述两种之内,但它运行着通过startService()方法所启动的service。尽管一个service进程可能对用户不所见,但是它们通常做一些用户关心的事情(比如播放音乐或下载数据),所以除非系统没有足够的空间运行前台进程和可见进程时才会终止一个service进程。
4. 后台进程
一个进程拥有一个当前不可见的activity(activity的onStop()方法被调用)。
这样的进程不会直接影响到用户体验,所以系统可以在任意时刻kill掉它们从而为前台、可见、以及service进程提供内存空间。通常有很多后台进程在运行。它们被保存在一个“最近最少使用”列表中来确保拥有最近刚被看到的activity的进程最后被kill。如果一个activity正确的实现了它的生命周期方法,并保存了它的当前状态,那么杀死它的进程将不会对用户的可视化体验造成影响,因为当用户返回到这个activity时,这个activity会恢复它所有的可见状态。
5. 空进程
一个没有任何activity组件的进程。保留这类进程的唯一理由是缓存,这样可以提高下一次一个组件要运行它时的启动速度。系统经常在进程缓存和底层的内核缓存之间的为了平衡整体系统资源而终止它们。
根据进程中当前activity的组件的重要性,Android会把使用进程的优先级中的最高级。例如,如果一个进程拥有一个service和一个可见的activity,进程会被定为可见进程,而不是service进程。另外,如果被其它进程所依赖,一个进程的级别可能会被提高——一个服务于其它进程的进程,其级别不可能比被服务进程低。因为拥有一个service的进程比拥有一个后台activitie的进程级别高,所以当一个activity启动一个需长时间执行的操作时,最好是启动一个service,而不是简单的创建一个工作线程。尤其是当这个操作可能比activity的生命还要长时。例如,一个向网站上传图片的activity,应该启动一个service,从而使上传操作可以在用户离开这个activity时继续在后台执行。使用一个service保证了这个操作至少是在"服务进程"级别,而不用管activity是否发生了什么情况。同样broadcast receivers 应该使用service而不是简单地使用一个线程。
当一个应用程序启动时,系统创建一个叫做"main"的执行线程。这个线程是十分重要的,因为它主管用户界面控件的分发事件,其中包含绘图事件。这个线程也用于你的应用程序与界面工具包(android.widget和 android.view包中的组件)的交互。所以网络上经常说的Android UI线程就是main线程。系统不会为每个组件的实例分别创建线程。所有运行在一个进程中的组件都在UI线程中被实例化,并且系统对每个组件的调用都在这个线程中发送消息。因此,你需要理解界面的操作都是在UI线程中进行的,如果不在UI线程中则会出异常,理解了这点你就知道handler的处理机制了。
例如,当用户触摸屏幕上的一个按钮时,你的应用程序UI线程把触摸事件发送给按钮控件,然后按钮控件设置它的按下状态并向事件队列发出一个刷新界面的请求,UI线程从队列中取出这个请求并通知它重绘。
当你的应用在集中响应大量的用户交互运算时,这种单线程的模型会带来低性能,除非你能正确的优化你的程序。如果所有事情都在UI线程中执行,比如网络连接或数据库请求这样的耗时操作,那么将会阻塞整个界面的响应(给用户通俗的感觉就是死机)。当UI线程被阻塞时,就不能分发事件了,包括绘图事件。从用户的角度看,程序反应太慢了。那么用户可能会退出并删掉你的应用。
此外,Andoid UI工具包不是线程安全的。所以你不能在一个自己开的线程中操作你的界面,你只能在UI线程中操作你的界面。所以,对于Android的单线程模型有两个简单的规则:
1. 不要阻塞UI线程
2. 不要在UI线程之外操作Android UI工具包。
5.2.1工作线程
由于上述的单线程模型,不要阻塞你的UI线程是非常重要的,那么如果你有一个长时间才能完成的任务,你应把它们放在另一个线程中执行(后台线程或工作线程)。例如,下面是的代码是监听click事件,它开一个线程下载一个图片并在一个ImageView中显示它,如代码清单5-1所示:
public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } }).start(); }
代码清单5-1
大致看来,这好像没什么问题,因为它创建了一个新线程来进行耗时的网络操作。然而它违反了单线程模型的第二条规则:不要在UI线程之外操作界面。这段代码在工作线程中修改了ImageView。这会导致一个异常发生。当然我们可以走其他路线,Android提供了很多从其它线程来操作界面的方法:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable,long)
例如,我们可以用View.post(Runnable)来解决上面的问题,如代码清单5-2所示:
public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(bitmap); } }); } }).start(); }
代码清单5-2
现在终于是线程安全的了:网络操作在另一个线程中并且ImageView 在UI线程中改变。然而,由于操作复杂性的增长,这样的代码就变得复杂并难以维护,为了处理更复杂的交互,你可能需要在工作线程中用到Handler对象来传递消息到UI线程中处理。还有个比较好的解决办法是继承AsyncTask类,这样可以使得工作线程和UI交互这一类任务变得更简化。
5.2.2使用AsyncTask
AsyncTask可以让你在UI上执行异步工作,它在一个工作线程中执行某些耗时的操作,之后将结果返回给UI线程。你可以不用管理工作线程和UI线程的交互,让你省事不少。使用AsyncTask类时,你需要继承AsyncTask类并实现doInBackground()回调方法。要更新UI界面,需要实现onPostExecute(),并从doInBackground()方法中传递结果,所以你能安全的更新你的界面,最后只需要在UI线程中调用execute()方法来执行任务。如代码清单5-3所示,有一个使用AsynTask的例子:
public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png"); } private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { /** 系统会开一个线程来处理后台操作*/ protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } /** doInBackground()方法返回的结果传递到UI线程中执行*/ protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); } }
代码清单5-3
因为AsyncTask将工作分成了两部分,一个部分在UI线程执行,另一个在工作线程执行,因此简化了我们的工作,并且让更新界面更安全。这里简单介绍下它是如何工作的:
AsyncTask定义了三种泛型类型 Params,Progress和Result。Params是启动任务执行的输入参数,比如HTTP请求的URL。Progress是后台任务执行的百分比。Result是后台执行任务最终返回的结果,比如String,Integer等。
1. doInBackground()会在工作线程中自动执行。
2. onPreExecute(), onPostExecute(), and onProgressUpdate()这三个方法都是在UI线程中调用。
3. doInBackground()方法返回的值会传递给onPostExecute()方法。
4. 你可以在doInBackground()方法中随时调用publishProgress()方法以此在UI线程中更新执行进度。并且可以随时在任何线程中取消任务。
关于它更详细的例子,可以参考google上的一个开源项目Shelves(http://code.google.com/p/shelves/)
5.2.3线程安全方法
在某些情况下,你实现的方法可能会被多个线程同时调用,因此要保证该方法是线程安全的。一些远程调用的方法——例如bound service。当我们在同一个进程中调用了某一个正在运行的IBander中的方法,这个方法会运行在调用者的线程中。然而,当不在同一个进程中调用该方法时,系统会为这个进程开启的线程池中的某一个线程来执行该方法。因为一个service可以有多个客户端,多个池线程可以在同一时间调用相同的IBinder方法。因此,实现IBinder方法必须是线程安全的。同样Content Provider类似。
Android提供了进程间通信(IPC)机制,使用远程过程调用(RPC),其中一个方法被 Activity或其他应用程序组件调用,但是在另一个进程中执行这个方法,然后结果又要返回到调用者那去。首先需要分解这个方法为调用和数据,这就需要在系统能理解它的调用和数据并从本地进程和远程进程的地址空间中来传递,然后再返回的时候重新组装。android系统能完美支持进程间通信(IPC),这样你就可以专注于制定和实现RPC编程接口。要实现进程间通信(IPC),应用程序必须使用bindService()来绑定一个service,使用bindService更详细的内容请参阅Service那一章。
FAQ:QQ群213821767