注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

拥有自己的梦想,跟随心的召唤

平凡是福

 
 
 

日志

 
 

Android: 线程与进程  

2012-06-25 23:39:52|  分类: Android |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
        在网上找了很多有关Android的多线程设计实现方法,基本上都不全面,走了不少弯路,后无意中发现,原来官方网站上的才是最全面的教程,涉及到各个层面、各个方面都有了详细的说明。于是我就尝试将其翻译过来,翻译的过程也是深入巩固学习的过程。
英文原文:Processes and Threads  

快速查看


  • 默认情况下,每个应用程序都在自己的进程内运行 ,应用程序内的所有组件都运行在那个进程内。
  • 为了避免用户体验的降低,Activity内任何缓慢的、堵塞的操作都应该放到新的线程中执行。

本文档导航


  1. 进程
    1. 进程生命周期
  2. 线程
    1. 辅助线程
    2. 线程安全方法
  3. 进程间通讯

线程与进程

  当应用程序组件启动并且没有其它的组件运行时,Android系统就会为此应用启动一个新的Linux执行线程。默认情况下,此应用的所有组件都运行在相同的进程和线程内(称为主线程)。当启动组件时,如果应用已经存在一个进程(因为应用的另一个组件在运行),此组件就会在此进程内启动,并且在同一个线程内运行。不过你也可以为应用的不同组件安排各自独立的进程,然后为任意进程创建额外的线程。
  此文档讨论Android应用的进程和线程是如何工作的。

进程


        默认情况下同一应用程序内的所有组件运行在同一个进程内,大多数应用程序都不改变这种默认行为。然而,如果你需要控制特定的组件运行在指定的进程内,可以通过 manifest 配置文件来达到目的。
       manifest 配置文件对各种类型的组件元素(如<activity>、<service>、<receiver>和<provider>)支持通过配置 android:process 属性的值来指定组件运行在特定的进程内。你可以指定每个组件运行在它自己的进程内,或者一些组件共享一个进程而其它组件又例外。你甚至可以设置 android:process 使不同的应用程序运行在相同的进程内 ---- 如果这样做的话这些应用程序就共享同一个Linux用户ID和签署了同样的权限。
        <application> 元素的 android:process 属性可以设置指定所有组件使用一个默认的值。
        当内存较低但其它进程又需要更直接的服务于用户时,Android可能会自行决定去关闭一个进程。运行在这个进程内的应用程序组件也会因此而被销毁。当这些组件需要再次工作时系统会再次为其启动一个进程。
        Android会衡量进程相对于用户的重要性来决定哪个进程会被终止。例如,相对于可见的Activities,Android会先杀掉相对于屏幕不可见的Activities。也就是说决定是否终止一个进程,取决于进程内组件的运行状态。以下讨论进程将会被终止的一些基本原则。

进程的生命周期

        Android系统将尽可能的保持应用程序进程,但最终会删除旧的进程来回收内存以用于新的或更重要的进程。为了决定保持哪些进程和终止哪些进程,系统会基于进程内组件的状态将各种进程放进一个“重要性层次结构”中。哪些重要性最低的进程会首先被终止掉,然后就是哪些次最低重要性的进程,如此类推,这些都是系统资源回收所必须的。
        重要性层次结构”中共有5个级别。以下列表显示不同进程的重要性顺序(排在前面进程是最重要的,最后才会被终止):

1. 前台进程
       任何用户当前正在处理的交互都需要一个进程,符合下面条件的进程都是前台进程:
        通常只会有小量的前台程在运行。它们只会在特定重启情况下才会被终止掉 ---- 如内存低至无法继续维持。在这种情况下,设备已经到了内存分页状态,以致系统会终止一部分前台程来保证正常的用户交互响应。

2. 可见进程
       一个没有任何前台组件的进程依然可以影响用户的可见屏幕,符合如下条件的进程就被认为是可见进程。
  • 进程服务于一个非前台的、但依然可见的 Activity (调用 onPause()方法),如一个前台 Activity 启动一个对话框,在对话框的背后此Activity依然可见。
  • 进程服务于绑定在可见(或前台) Activity 上的一个 Service 上。
可见进程被认为是极其重要的,故除非是为了保证前台线程运行必须的,否则是不会被终止的。

3. 服务进程
        一个服务于通过 startService() 方法启动的、并且不符合上述那两个级别的 Service 的程,虽然对于用户界面来说不是直接紧密关联的,但是它们却正在做一些用户关注的事情(如在后台播放音乐或者正在通过网络下载数据),以至于系统将保持这些进程继续运行,除非系统没有足够的内存保证让它们与前台线程和可见线程一起运行。

4. 后台进程
        服务于对用户不再可见的 Activity 的进程(调用 onStop() 方法),不再影响用户体验,系统将随时终止这些进程来为前台进程、可见进程或服务进程回收可用内存。通常这些都是后台运行线程,它们存放在LRU(最近最少使用的)列表中,目的是确保那些用户最近经常见到的Activity关联的进程是最后被终止的。如果一个Activity实现了正确的生命周期方法并保存了其当前状态,终止其进程时用户不会察觉到任何影响,因为当用户重新返回到这个Activity时,这个Activity会恢复其所有的可见状态信息。参考 Activities 的开发文档可以查看如何保存和恢复状态的相关信息。

5. 空闲进程
        一个没有任何活动应用程序组件的进程,让它们依然存活的唯一原因是为了缓存目的,当一个组件需要它来运行时可以提高启动时间。系统会为了在进程缓存和内核缓存之间达到整体的资源平衡而经常终止这些进程。

        Android会基于当前活动进程内组件的重要性尽可能的将进程安排在较高的级别。例如,如果一个进程服务于一个Service和一个可见的Activity,进程将被当做一个可见进程而不是服务进程。
        此外,一个进程的级别可能会因为其它进程要依赖于它运行而得到提升 ---- 这个进程的级别永远不会低于其所服务进程的级别。例如,如果一个 Content Provider 进程A服务于一个客户端进程B,或者一个Service进程A绑定到一个组件进程B,进程A将至少被认为与进程B一样重要。
        因为服务进程的级别高于后台进程,因此如果为一个带有长耗时操作的Activity启动一个 Service 可能会比启动一个辅助线程会做得更好 ---- 特别地当这个操作更适合独立于Activity处理时。例如,上传图片到一个网站的Activity应该启动一个Service去执行上传处理,这样就算用户离开了这个Activity,上传操作依然在后台继续执行。使用Service,不管Activity发生了什么情况,都保证了这个操作至少有“服务进程”的优先级别。同样的原因,Broadcast Receivers 也应该使用 Service 服务而不是简单的将耗时操作放到一个线程里。


线程


        当应用程序启动的时候系统会为其创建一个执行线程,成为“主线程”。这个线程是至关重要的,因为其控制了用户界面组件的事件分发,包括绘图事件。同时它也是你的应用同Android UI 工具箱组(包 android.widget 和 android.view 下的组件)交互的线程。因此主线程有时也被叫做UI线程。
         系统不会为组件的每个实例创建独立的线程。所有运行在同一线程内的组件都会在UI线程中实例化,系统调用每个组件都派遣自该线程。因此,对系统回调方法(如 onKeyDown() 方法报告用户的动作或者一个生命周期的回调方法)的处理一直都在进程的UI线程内运行。
        例如,当用户在屏幕中触摸一个按钮时,你的应用程序UI线程就会分发这个触摸事件给组件,设置组件处于按下状态并提交一个组件界面需要重绘的请求到事件队列中。UI线程从队列中取出这个请求,通知组件应该重绘其自身。
        当你的应用程序需要执行繁重的工作来应对用户交互时,除非你恰当的处理好,否则这个单线程模式可能会导致糟糕的性能。特别是当所有事情都在UI线程中处理,如执行网络请求、数据库查询等耗时长的操作时,将会导致整个UI堵塞。当线程被堵塞时,事件将无法分发,包括绘图事件。从用户看来应用程序像是挂起了。更糟的是如果UI线程堵塞超过几秒钟(约5秒),用户将看到声名狼藉的“应用程序没有响应”(ANR)对话框。这种糟糕的用户体验可能导致用户退出并卸载你的应用程序。
        此外, Andoid UI 工具箱的组件不是线程安全的。所以你不能在辅助线程中操控UI组件 ---- 你必须在UI线程中才能操控。因此Android的单线程模式有两个简单的规则:
  • 不要堵塞 UI 线程
  • 不要在 UI 线程外存取 Android 的 UI 工具箱组件

辅助线程

       基于以上单线程模式的描述,不堵塞UI线程对你的应用程序界面响应能力来说是至关重要的。如果执行的操作不是即时就可以完成的,则应该将其放在独立的线程中执行(后台或辅助线程)。
        例如,下面的代码就是在点击事件中使用独立的线程下载一张图片并在 ImageView 中显示:
 public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
mImageView.setImageBitmap(b);
}
}).start();
}
        上面的代码看上去好像没问题,因为其创建一个新线程去处理网络操作。然而代码却违反了单线程模式的第二个规则:不要在 UI 线程外存取 Android 的 UI 工具箱组件 ---- 这个例子在辅助线程内修改了 ImageView 。这将导致不可预料的行为,可能会花费很长的时间也难以追踪到。
        为了解决这种问题,Android 提供了几种从其它线程存取UI线程的方法,列举如下:
        例如,通过 View.post(Runnable) 方法来修正上面的代码:
 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();
}
        现在的代码是线程安全的了:使用独立的线程完成网络操作然后在UI线程中更新 ImageView
        然而,随着操作变得越来越复杂,这种方法也会变得越来越复杂而难于维护。为了在辅助线程可以处理更复杂的交互,可以考虑在辅助线程中使用 Handler 来向UI线程传送消息。当然,最好的解决方案也许是扩展 AsyncTask 类,它可以更方便的执行需要与UI进行交互处理的辅助线程任务。

使用 AsyncTask
       AsyncTask 允许你在用户界面上执行异步工作。它在一个辅助线程中执行堵塞操作然后将结果发布到UI线程,不需要你去处理线程和(或)Handler
        要使用 AsyncTask 你必须继承 AsyncTask 类并实现 doInBackground() 方法,此方法运行在一个后台线程池中。为了更新 UI ,实现 onPostExecute() 方法,此方法传送 doInBackground() 方法的返回值到 UI 线程并在 UI 线程中执行,因此你可以在这里安全的更新 UI。你可以在UI线程中调用该子类的 execute() 方法来执行你的异步任务了。
        例如,前面的例子可以使用 AsyncTask 如下实现:
 public void onClick(View v) {
new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
/** 系统在一个辅助线程中执行此方法并将AsyncTask.execute()的参数传到这里 */
protected Bitmap doInBackground(String... urls) {
return loadImageFromNetwork(urls[0]);
}

/** 系统在UI线程中执行此方法并将 doInBackground() 方法的返回值传到这里 */
protected void onPostExecute(Bitmap result) {
mImageView.setImageBitmap(result);
}
}
        现在的代码更加简单和安全了,因为其将UI线程和辅助线程各自要处理的逻辑分离了。
        你应该阅读 AsyncTask 的参考文档以完全理解如何使用这个类,下面只是一个帮助你了解它是如何工作的一些快速指引:

警告:另一个您可能会遇到的问题是,Activity中的辅助线程可能会因为运行时的配置更改(如当用户改变屏幕的方向)而意想不到的重启,这可能会破坏你的辅助线程。你可以参考一下 Shelves 范例应用的源代码,来看看在Activity被销毁的情况下如何持久化这种情况下的任务、如何正确的取消异步任务的执行。


线程安全方法

       在某些情况下,你实现的方法可能会被多个线程调用,因此方法必须写成线程安全的。
       特别是远程方法的调用 ---- 如 Bound Service 内的方法。当在 IBinder 运行进程内调用一个 IBinder 接口方法时,此方法是在调用者的线程内执行的。然而,当在其它进程中调用时,此方法是在 IBinder 运行进程内的线程池选择一个线程来运行的(不是在进程的UI线程内执行)。例如,鉴于Service的 onBind() 方法会在 Service 进程的UI线程内调用,其返回的 IBinder 对象的方法(如子类实现的 RPC 方法)是在线程池中的某个线程内被调用。因为一个Service可能会有多个客户端,有多个线程池会在同一时间占用相同的IBinder方法。因此IBinder方法就必须实现为线程安全的。
        同样,Content Provider 也能从其它进程接收数据请求。虽然 ContentResolver 和 ContentProvider 类隐藏了进程间通讯管理的细节,ContentProvider 对请求的响应 ---- 方法query()insert()delete()update() 和 getType() ---- 是在Content Provider进程的线程池内被调用的,而不是在进程的UI线程内被调用。因为这些方法可能会被任意数量的线程在同一时间内调用,故它们也必须实现为线程安全的。

进程间通讯


        Android使用远程过程调用(RPCs)机制来提供进程间通讯(IPC),这种机制是指方法被Activity或应用程序组件调用,但在远程执行(在另一个进程中),执行结果再返回给调用者。这要求分解方法的调用和它的数据到一个系统可以理解的级别,从本地进程和地址空间传输到远程进程和地址空间,然后在远程重新组装和调用。返回值再反向传输回去。Android提供了所有执行 IPC 传输的代码,你只需将注意力集中在定义和实现 RPC 编程接口上。
        要执行 IPC 你的应用必须使用 bindService() 方法绑定到一个 Service 上。更多信息请参考 Service 的开发指南。

<PREVIOUS        NEXT>

  评论这张
 
阅读(1345)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017