zoukankan      html  css  js  c++  java
  • Android开发指南中文版(四)Processes and Threads

    如果某个应用程序组件是第一次被启动,且这时应用程序也没有其他组件在运行,则Android系统会为应用程序创建一个包含单个线程的linux进程。默认情况下,同一个应用程序的所有组件都运行在同一个进程和线程里(叫做“main”主线程)。如果组件启动时,已经存在应用程序的进程了(因为应用程序的其它组件已经在运行了),则此组件会在已有的进程和线程中启动运行。不过,可以指定组件运行在其他进程里,也可以为任何进程创建额外的线程。

    进程(Processes)

    组件运行所在的进程由manifest文件所控制。组件元素<activity><service>,<receiver><provider>都有一个process 属性来指定组件应当运行于哪个进程之内。这些属性可以设置为使每个组件运行于它自己的进程之内,或一些组件共享一个进程而其余的组件不这么做。它们也可以 设置为令不同应用程序的组件在一个进程中运行——使应用程序的组成部分共享同一个Linux用户ID并赋以同样的权限。

    <application>元素也有一个process属性,以设定所有组件的默认值。

    在可用内存不足而又有一个正在为用户进行服务的进程需要更多内存的时候,Android有时候可能会关闭一个进程。而在这个进程中运行着的应用程序也因此被销毁。当再次出现需要它们进行处理的工作的时候,会为这些组件重新创建进程。

    在决定结束哪个进程的时候,Android会衡量它们对于用户的相对重要性。比如说,相对于一个仍有用户可见的activity的进程,它更有可能去关闭一个其activity已经不为用户所见的进程。也可以说,决定是否关闭一个进程主要依据在那个进程中运行的组件的状态。

    进程与生命周期(Processes and lifecycles)

    Android系统会尽可能长的延续一个应用程序进程,但在内存过低的时候,仍然会不可避免需要移除旧的进程。为决定保留或移除一个进程,Android 将每个进程都放入一个“重要性层次”中,依据则是它其中运行着的组件及其状态。重要性最低的进程首先被消灭,然后是较低的,依此类推。重要性共分五层,依据重要性列表如下:(第一类进程是最重要的,将最后一个被终止):

    1. 前台进程是用户操作所必须的。
      当满足如下任一条件时,进程被认为是处于前台的:
      • 运行着正在与用户交互的activity(Activity对象的 onResume() 方法已被调用)。
      • 一个正在与用户交互的activity使用着它提供的一个服务。
      • 一个正在运行的Service(Service对象的startForeground()方法被调用)
      • 包含着一个正在执行生命周期回调方法(onCreate()onStart()onDestroy())  的Service对象。
      • 它包含着一个正在执行 onReceive() 方法的BroadcastReceiver对象。

      任一时间下,仅有少数进程会处于前台,仅当内存实在无法供给它们维持同时运行时才会被杀死。一般来说,在这种情况下,设备已然处于使用虚拟内存的状态,必须要杀死一些前台进程以用户界面保持响应。
    2. 可视进程没有前台组件,但仍可被用户在屏幕上所见。
      当满足如下任一条件时,进程被认为是可视的:
      • 它包含着一个不在前台,但仍然为用户可见的activity(它的onPause()方法被调用)。这种情况可能出现在以下情况:比如说,前台activity是一个对话框,而之前的activity位于其下并可以看到。
      • 它包含了一个绑定至一个可视的activity的服务。 可视进程依然被视为是很重要的,非到不杀死它们便无法维持前台进程运行时,才会被杀死。
    3. 服务进程是由 startService()方法启动的服务,它不会变成上述两类。尽管服务进程不会直接为用户所见,但它们一般都在做着用户所关心的事情(比如在后台播放mp3或者从网上下载东西)。所以系统会尽量维持它们的运行,除非系统内存不足以维持前台进程和可视进程的运行需要。
    4. 背景进程包含目前不为用户所见的activity(Activity对象的 onStop()方法已被调用)。这些进程与用户体验没有直接的联系,可以在任意时间被杀死以回收内存供前台进程、可视进程以及服务进程使用。一般来说,会有很多背景进程 运行,所以它们一般存放于一个LRU(最后使用)列表中以确保最后被用户使用的activity最后被杀死。如果一个activity正确的实现了生命周 期方法,并捕获了正确的状态,则杀死它的进程对用户体验不会有任何不良影响。
    5. 空进程不包含任何活动应用程序组

    Android会依据进程中当前活跃组件的重要程度来尽可能高的估量一个进程的级别。比如说,如果一个进程中同时有一个服务和一个可视的activity,则进程会被判定为可视进程,而不是服务进程。

    此外,一个进程的级别可能会由于其它进程依赖于它而升高。一个为其它进程提供服务的进程级别永远高于使用它服务的进程。比如说,如果A进程中的内容提供者 为进程B中的客户端提供服务,或进程A中的服务为进程B中的组件所绑定,则A进程最低也会被视为与进程B拥有同样的重要性。

    运行着一个服务的进程重要级别总高于一个背景activity。所以一个activity以启动一个服务的方式启动一个长时间运行过程比简单的衍生一个线程来进行处理要好。尤其是当处理过程比activity本身存在时间要长的情况之下。我们以背景音乐播放和上传一个相机拍摄的图片至网站上为例。使用服务则不论activity发生何事,都至少可以保证操作拥有“服务进程”的权限。这也正是广播接收器使用服务,而不是使用线程来处理耗时任务的原因。

    线程(Threads) 

    应用程序启动时,系统会为它创建一个名为“main”的主线程。主线程非常重要,因为它负责把事件分发给相应的用户界面widget——包括屏幕绘图事件。它也是应用程序与Android UI组件包(来自android.widgetandroid.view包)进行交互的线程。因此主线程有时也被叫做UI线程。

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

    如果应用程序在与用户交互的同时需要执行繁重的任务,单线程模式可能会导致运行性能很低下,除非应用程序的执行时机刚好很合适。如果UI线程需要处理每一件事情,那些耗时很长的操作——诸如访问网络或查询数据库等——将会阻塞整个UI(线程)。一旦线程被阻塞,所有事件都不能被分发,包括屏幕绘图事件。从用户的角度看来,应用程序看上去像是挂起了。更糟糕的是,如果UI线程被阻塞超过一定时间(目前大约是5秒钟),用户就会被提示那个可恶的“应用程序没有响应(ANR)对话框。如果引起用户不满,他可能就会决定退出并删除这个应用程序。

    此外,Andoid的UI组件包并不是线程安全的。因此不允许从工作线程中操作UI——只能从UI线程中操作用户界面。于是,Andoid的单线程模式必须遵守两个规则:

    • 不要阻塞UI线程。
    • 不要在UI线程之外访问Andoid的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线程之外访问Andoid的UI组件包——以上例子在工作线程里而不是UI线程里修改了ImageView。这可能导致不明确、不可预见的后果,要跟踪这种情况也是很困难很耗时间的。

    为了解决以上问题,Android提供了几种途径来从其它线程中访问UI线程。下面列出了有助于解决问题的几种方法:

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

    比如说,可以使用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(); 
    }
    
    
    

    以上代码的执行现在是线程安全的了:网络相关的操作在单独的线程里完成,而ImageView是在UI线程里操纵的。

    不过,随着操作变得越来越复杂,这类代码也会变得很复杂很难维护。为了用工作线程完成更加复杂的交互处理,可以考虑在工作线程中用Handler来处理UI线程分发过来的消息。当然,最好的解决方案也许就是继承使用异步任务类AsyncTask,此类简化了一些工作线程和UI交互的操作。

    使用异步任务

    异步任务AsyncTask允许以异步的方式对用户界面进行操作。它先阻塞工作线程,再在UI线程中呈现结果,在此过程中不需要对线程和handler进行人工干预。

    要使用异步任务,必须继承AsyncTask类并实现doInBackground()回调方法,该对象将运行于一个后台线程池中。要更新UI时,须实现onPostExecute()方法来分发doInBackground()返回的结果,由于此方法运行在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> { 
        /** The system calls this to perform work in a worker thread and 
          * delivers it the parameters given to AsyncTask.execute() */ 
        protected Bitmap doInBackground(String... urls) { 
            return loadImageFromNetwork(urls[0]); 
        } 
    
        /** The system calls this to perform work in the UI thread and delivers 
          * the result from doInBackground() */ 
        protected void onPostExecute(Bitmap result) { 
            mImageView.setImageBitmap(result); 
        } 
    }

    现在UI是安全的,代码也得到简化,因为任务分解成了工作线程内完成的部分和UI线程内完成的部分。

    要全面理解这个类的使用,须阅读AsyncTask的参考文档。以下是关于其工作方式的概述:

    注意:在使用工作线程时,可能遇到的另一个问题是由于运行配置的改变(比如用户改变了屏幕方向)导致activity意外重启,这可能会销毁该工作线程。要了解如何在这种情况下维持任务执行、以及如何在activity被销毁时正确地取消任务,请参见Shelves例程的源代码。

    远程方法调用(Remote procedure calls)

             Android有一个轻量级的远程方法调用(RPC)机制:即在本地调用一个方法,但在远程(其它的进程中)进行处理,然后将结果返回调用者。这将方法调用及其附属的数据以系统可以理解的方式进行分离,并将其从本地进程和本地地址空间传送至远程过程和远程地址空间,并在那里重新装配并对调用做出反应。返回的结果将以相反的方向进行传递。Android提供了完成这些工作所需的所有的代码,以使你可以集中精力来实现RPC接口本身。

             RPC接口可以只包括方法。即便没有返回值,所有方法仍以同步的方式执行(本地方法阻塞直至远程方法结束)。

    简单的说,这套机制是这样工作的:一开始,你用简单的IDL(界面描绘语言)声明一个你想要实现的RPC接口。然后用 aidl工具为这个声明生成一个Java接口定义,这个定义必须对本地和远程进程都可见。它包含两个内部类,如下图所示:

    内部类中有管理实现了你用IDL声明的接口的远程方法调用所需要的所有代码。两个内部类均实现了 IBinder接口。一个用于系统在本地内部使用,你些的代码可以忽略它;另外一个,我们称为Stub,扩展了Binder类。除了实现了IPC调用的内部代码之外,它还包括了你声明的RPC接口中的方法的声明。你应该如上图所示的那样写一个Stub的子类来实现这些方法。

    一般情况下,远程过程是被一个服务所管理的(因为服务可以通知系统关于进程以及它连接到别的进程的信息)。它包含着 aidl工具产生的接口文件和实现了RPC方法的Stub的子类。而客户端只需要包括aidl工具产生的接口文件。

    下面将说明服务与其客户端之间的连接是如何建立的:

    • 服务的客户端(位于本地)应该实现 onServiceConnected()onServiceDisconnected() 方法。这样,当至远程服务的连接成功建立或者断开的时候,它们会收到通知。这样它们就可以调用 bindService() 来设置连接。
    • 而服务则应该实现 onBind() 方法以接受或拒绝连接。这取决于它收到的intent(intent将传递给bindService())。如果接受了连接,它会返回一个Stub的子类的实例。
    • 如果服务接受了连接,Android将会调用客户端的onServiceConnected() 方法,并传递给它一个IBinder对象,它是由服务所管理的Stub的子类的代理。通过这个代理,客户端可以对远程服务进行调用。
    线程安全的方法

    在某些场合,方法可能会从不止一个线程中被调用,因此这些方法必须是写成线程安全的。

    对于能被远程调用的方法——比如绑定服务(bound service)中的方法,这是理所当然的。如果对IBinder所实现方法的调用发起于IBinder所在进程的内部,那么这个方法是执行在调用者的线程中的。但是,如果调用发起于其他进程,那么这个方法将运行于线程池中选出的某个线程中(而不是运行于进程的UI线程中),该线程池由系统维护且位于IBinder所在的进程中。例如,即使一个服务的onBind()方法是从服务所在进程的UI线程中调用的,实现了onBind()的方法对象(比如,实现了RPC方法的一个子类)仍会从线程池中的线程被调用。因为一个服务可以有不止一个客户端,所以同时可以有多个线程池与同一个IBinder方法相关联。因此IBinder方法必须实现为线程安全的。

    类似地,内容提供者(content provider)也能接收来自其它进程的数据请求。尽管ContentResolver类、ContentProvider类隐藏了进程间通讯管理的细节,ContentProvider中响应请求的方法——query()insert()delete()update()getType()方法——是从ContentProvider所在进程的线程池中调用的,而不是进程的UI线程。因为这些方法可能会从很多线程同时调用,它们也必须实现为线程安全的。

    进程间通讯

    Android利用远程过程调用(remote procedure call,RPC)提供了一种进程间通信(IPC)机制,通过这种机制,被activity或其他应用程序组件调用的方法将(在其他进程中)被远程执行,而所有的结果将被返回给调用者。这就要求把方法调用及其数据分解到操作系统可以理解的程度,并将其从本地的进程和地址空间传输至远程的进程和地址空间,然后在远程进程中重新组装并执行这个调用。执行后的返回值将被反向传输回来。Android提供了执行IPC事务所需的全部代码,因此只要把注意力放在定义和实现RPC编程接口上即可。

    要执行IPC,应用程序必须用bindService()绑定到服务上。详情请参阅服务Services开发指南。

  • 相关阅读:
    ORA01033:ORCLE initialization or shutdown in progress问题的解决
    OA、SOA和SOAP
    [导入]循环赛如何排列问题
    [导入]Free Compilers / Interpreters
    终于知道纹理的GL_MODULATE是怎么回事了
    怎么acm.tongji.edu.cn又关机了?
    [导入]一个2002罗马尼亚赛区的ACM的题目
    netcraft at tongji.edu.cn
    [导入]对编写安全的代码这个方法有用吗?
    [导入]最简单的方法输出下列图形
  • 原文地址:https://www.cnblogs.com/blueofsky/p/2297876.html
Copyright © 2011-2022 走看看