Android 进程与线程

一、前言

应用启动时,Android 系统会启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。但是,也可以安排应用中的组件在单独的进程中运行,以及为任何进程创建额外的线程。

二、进程

清单文件中各组件元素:<activity><service><receiver><provider> 均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。

  • 你可以设置此属性,使每个组件在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。

  • 此外,还可以设置 android:process,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。

此外,<application> 元素也支持 android:process 属性,用来设置所有组件该属性的默认值。

进程生命周期

系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入 “重要性层次结构” 中。必要时,系统会首先消除重要性最低的进程,然后是重要性略低的进程,依此类推,以回收系统资源。

重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

1. 前台进程

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

  • 托管用户正在交互的 Activity(已调用 ActivityonResume() 方法)

  • 托管某个 Service,后者绑定到用户正在交互的 Activity

  • 托管正在 “前台” 运行的 Service(服务已调用 startForeground()

  • 托管正执行一个生命周期回调的 ServiceonCreate()onStart()onDestroy()

  • 托管正执行其 onReceive() 方法的 BroadcastReceiver

只有在内存不足的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

2. 可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。如果一个进程满足以下任一条件,即视为可见进程:

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。

  • 托管绑定到可见(或前台)ActivityService

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

3. 服务进程

正在运行使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

4. 后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

5. 空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。


Android 会将进程评定为它可能达到的最高级别。例如,如果某进程托管服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进级别永远不会低于其所服务的进程。例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

由于服务进程其级别高于后台进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备 “服务进程” 优先级。 同理,广播接收器也应使用服务,而不是将耗时冗长的操作放入线程中。

三、线程

应用启动时,系统会为应用创建一个名为 “主线程” 的执行线程。 此线程非常重要,因为它负责将事件分派给控件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(android.widgetandroid.view 软件包的组件)进行交互的线程。因此,主线程也称为 UI 线程。

运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法始终在 UI 线程中运行。

如果 UI 线程执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。 一旦线程被阻塞,将无法分派任何事件,包括绘图事件。如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个 “应用无响应”(ANR) 对话框。

此外,Android UI 工具包不是线程安全工具包,所以不能通过子线程操纵 UI,而只能通过 UI 线程操纵用户界面。 因此,Android 的单线程模式必须遵守两条规则:

  • 不要阻塞 UI 线程

  • 不要在 UI 线程之外访问 Android UI 工具包

1. 工作线程

如果要执行耗时操作,则应确保它们在单独的线程中运行。

例如,以下代码演示了一个点击侦听器从单独的线程下载图像并将其显示在 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 线程。 以下列出了几种有用的方法:

  • 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();
}

现在,上述实现属于线程安全型:在单独的线程中完成网络操作,而在 UI 线程中操纵 ImageView。

也可以考虑使用 Handler 处理来自 UI 线程的消息。当然,最好的解决方案或许是扩展 AsyncTask 类。

例如,可以通过以下方式使用 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 线程内完成。

注意:使用工作线程时可能会遇到一个问题,即:运行时配置变更(例如,用户更改了屏幕方向)导致 Activity 意外重启,这可能会销毁工作线程。

2. 线程安全方法

在某些情况下,方法可能会在多个线程中调用,因此必须确保其线程安全。

这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果调用 IBinder 中的方法的进程和运行 IBinder 的进程一样,则该方法在调用方的线程中执行。但是,如果是从其他进程中调用,则该方法将在从线程池选择的某个线程中执行(而不是在进程的 UI 线程中执行),线程池由系统在与 IBinder 相同的进程中维护。

例如,即使服务的 onBind() 方法将从服务进程的 UI 线程调用,在 onBind() 返回的对象中实现的方法仍会从线程池中的线程调用。 由于一个服务可以有多个客户端,因此可能会有多个线程在同一时间使用同一 IBinder 方法。因此,IBinder 方法必须实现为线程安全方法。

同样,内容提供程序也可以接收来自其他进程的数据请求。尽管 ContentResolverContentProvider 类隐藏了如何管理进程间通信的细节,但响应这些请求的 ContentProvider 方法(query()insert()delete()update()getType() 方法)将从内容提供程序所在进程的线程池中调用,而不是从进程的 UI 线程调用。 由于这些方法可能会同时在任意数量的线程中调用,因此它们也必须实现为线程安全方法。

四、进程间通信

Android 利用远程过程调用 (RPC) 提供了一种进程间通信 (IPC) 机制,通过这种机制,由 Activity 或其他应用组件调用的方法将(在其他进程中)远程执行,而所有结果将返回给调用方。Android 提供了执行这些 IPC 事务所需的全部代码,因此我们只需定义和实现 RPC 接口即可。

要执行 IPC,必须使用 bindService() 将应用绑定到服务上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • 当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux...
    小芸论阅读 1,674评论 0 12
  • 那些年,我们奔跑在下雨天,周杰伦唱着“最美的不是下雨天,是曾与你一起躲过的屋檐”。那些年,我们固执地以为所有的事,...
    芒果小姐爱满天星阅读 358评论 0 3
  • 1.基础类型有哪些?复杂类型有哪些?有什么特征? 基本类型: (undefined、mull、Soolean、Nu...
    ahong_吴阅读 320评论 0 0
  • “你好像很喜欢赛车”“嗯”“要不要一起去看比赛”简短的对话开始了一段旅程 坐在飞驰的动车上看着车窗外,说走就走的感...
    十十七呀阅读 199评论 0 0