您的位置:首页 > 移动开发 > Android开发

Android 的进程, 线程和任务

2015-11-08 17:38 417 查看

概述:

Android中有三个比较容易混淆的概念:进程(Process), 线程(Thread), 任务(Task). 如果说进程和线程还比较容易区分的话, 那么从字面意思来分辨任务和进程的区别, 就不那么容易了.

 

一些基础知识:

进程和线程是密切相关的, 当一个Android组件启动的时候, 该App又没有其它的组件在运行, 那么Android就会为该组件启动一个新的Linux进程, 并且进程中包含一个线程. 默认情况下, 该APP下所有的组件, 都将运行在该线程下, 该线程被称为”主线程”. 如果一个新的组件被启动的时候, 发现跟它同一个APP下已经有其它组件在运行了, 那么该组件会跟已有组件一起运行在同一个线程中. 当然我们还可以将某一组件运行在指定的进程里, 也可以在进程中创建额外的线程.

任务则是用来保存Activity队列的容器. 本文后面将详细介绍它.

进程:

默认情况下APP内部的所有组件都运行在同一个进程中, 我们也可以指定组件运行的进程, 而这样做的原因通常是因为资源不足. Android对单个进程所拥有的内存是有限制的.

在Mainifest.xml文件中, 四大组件<activity><service><receiver>和<provider>都支持android:process属性, 它可以指定所在组件应该运行在哪个进程里. 通过android:process属性可以指定同一个APP中的某些组件运行在不同的进程中, 也可以让不同的APP组件运行在同一个进程中. 另外<application>也可以支持android:process属性.

进程的生命周期:

Android会尽量保证每一个进程尽可能的存活, 然而在内存不足的情况下, 对进程的回收是不可避免的.Android对进程划分了如下的优先级, 按照优先级从低到高来回收它们. 下列排序从高到低:

1.      前台进程: 当进程中包含用户正在操作的组件的时候, 该进程被认为是一个前台进程, 它只需要满足下列任何一个条件即可:

·    该进程中运行着一个正在处于Resume状态的Activity.

·    该进程中运行着一个与处于Resume状态的Activity绑定着的Service.

·    该进程中运行着一个调用了startForeground()方法的Service.

·    该进程中运行着一个正在执行其callbacks(onCreate(), onStart()和onDestroy())的Service.

·    该进程中运行着一个正在执行onReceive()方法的BroadcastReceiver.

通常情况下只有为数不多的前台进程在运行, 只有在内存极少以至于无法响应用户操作的情况下, Android才会考虑回收这些进程. 它们拥有最高的优先级, 一般情况下都是它们在耍流氓抢别的进程的内存. 用户最大嘛.

2.      可见进程: 一个不满足前台进程中任何一条, 又包含正显示在屏幕上的组件的进程, 我们称之为可见进程. 一个可见进程应该满足下列任一条条件:

·   该进程运行着一个正处于Pause状态的Activity(刚被调用了onPause()方法).

·   该进程运行着一个绑定了[处于Pause状态Activity]的Service.

除非已经无法保证前台进程的运行了, 否则通常可见进程不会被回收.

3.      服务进程: 该进程需要满足如下条件:

·   该进程运行着一个Service,并且已经调用了startService().

·   该Service不满足前台进程和可见进程中的条目.

通常情况下这样的进程正在做的都是跟用户操作没关系但是是运行在后台比较重要的任务, 比如播放音乐或者下载数据之类的事情. 这些进程在更高优先级的进程内存不够的时候, 会被回收.

4.      后台进程: 进程中包含不可见的Activity(调用了onStop()方法), 并且不满足上面三条的, 我们称其为后台进程. Android随时都可以回收这些进程. 这些进程会被存放在一个最近使用的列表中, 按照最近用户操作的顺序排列, 最后被用户使用的进程将最后被回收.

5.      空进程: 没有运行任何Android组件的进程, 我们称其为空进程, 这些进程没有被回收是因为用户还有可能重新启动这些进程中的组件, 维持它们的运行可以减少组件启动的时间.  Android在内存不足的时候会优先回收这些进程.

 

根据上面的规则我们可以注意到, 拥有一个后台Activity的进程的优先级比拥有一个Service的优先级要低, 所以当我们需要执行一个比较耗时的操作的时候, 应该尽量使用Service代替直接在Activity中开启一个线程执行该操作. 同理, broadcastReceiver也应该运行在一个Service中.

线程:

当一个APP被加载的时候, Android会为其创建一个线程, 该线程被称为主线程. 该线程非常重要, 因为它需要负责维护用户界面, 所有的UI操作以及UI的绘制, 都在该线程中进行. 所以该线程又被成为UI线程.

Android系统不会主动为各组件指定运行的线程, 默认情况下所有的组件都运行在UI线程中, 并且响应用户UI操作或者对UI进行的操作, 都只能运行在UI线程中. 这就注定该线程是个很苦逼的角色, 如果所有的操作和后台运算都在该线程中执行(比如最为耗时的网络访问和数据库访问操作等), 那它很可能焦头烂额的运转不过来, 导致自己的本职工作(UI响应)做不好, 这时候后果就严重了, 轻则产生卡顿现象用户不爽, 重则产生那个臭名昭著的” application not responding”(ANR)错误,
导致APP被直接卸载.

然而更加不幸的是Android的UI操作不是线程安全的, 所以我们只能在UI线程操作UI, 所以我们需要遵循的规则就已经明了了, UI线程操作UI和轻量级的工作, 重量级的工作则应该另外开启工作线程来执行. 并且记住以下两个切不可越界的规则:

1.      不要阻塞UI线程. (会导致卡顿)

2.      不要从其它非UI线程直接访问UI资源. (会导致APP挂死)

 

工作线程:

因为上文提到的一些原因, 我们知道了, 复杂耗时的工作不应该在UI线程中执行, 并且非UI线程也不能访问UI资源. 这就导致我们需要Android提供两种资源: 工作线程以及工作线程与UI线程的通信. 有了它们我们就可以把繁重的任务丢给工作线程来做, 等它完成了之后通知UI线程更新UI界面. 当然Android这么牛X的系统, 肯定已经考虑到了这些并且提供了非常好用的接口给我们.

我们可以用Thread来创建工作线程, 先看以下这段代码:

public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
mImageView.setImageBitmap(b);
}
}).start();
}

在这段代码中, 通过响应一个点击事件, 创建新的工作线程, 然后在线程中访问一个URL图片资源, 并且在一个ImageView中显示它.

如果还记得上一节提到的内容, 那么应该很容易发现这段看似简单明了的代码隐藏着一个危机: 我们不能在工作线程中直接访问UI资源. mImageView.setImageBitmap(b);会使程序挂掉. 为了解决这个问题Android提供了如下方法:

·        Activity.runOnUiThread(Runnable)
·        View.post(Run
c6a3
nable)
·        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资源了.

然而上述几个方法并非实现该操作的最佳方法, 随着APP复杂性的增加, 我们需要更加简单易用的方法来解决工作线程中访问UI资源的问题, 在这里有两种比较常用的方法, 一种是Handler, 另一种是AsyncTask.AsyncTask是从Thread和Handler发展而来的工具类,
它可以帮助我们更简单的创建线程并且修改UI资源. 是目前最为推荐的创建工作线程的方法.

AsyncTask的使用:

使用AsyncTask的时候应该继承AsyncTask, 并且实现其doInBackground和onPostExecute方法, doInBackground方法在工作线程中执行, onPostExecute则用于工作线程执行完毕之后更新UI线程, 上述代码通过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);
}
}

具体的AsyncTask使用方法可以参考这里, 下面是它的用法的概述:

·    doInBackground()方法会自动在工作线程中调用

·    onPreExecute(), onPostExecute(), 和 onProgressUpdate()方法都会在UI线程中调用

·    doInBackground()的返回值将会被作为onPostExecute()方法的参数

·    可以在doInBackground()方法中调用publishProgress()方法更新任务进度,publishProgress()方法被调用之后, onProgressUpdate()也将会被调用, 以便更新UI中的任务进度.

·    我们可以在任何时间, 任何线程中断任务.

在使用工作线程执行任务的时候, 另一个无法回避的问题是当Activity被重建的时候(比如屏幕转动导致Activity被重建), 我们的线程也将被重建. 关于如何处理这样的情况, 可以参考官方例子Shelves

任务:

通常我们的APP由各种各样的Activity组成, 它是APP面向用户的窗口, 根据用户的操作, APP向用户展示不同的Activity. 比如邮件系统, 通常打开的时候我们会看到一个邮件列表, 点击条目, 会打开一个新的Activity进入邮件的详细内容. 这是典型的Activity之间的交互, 在这种常见的场景中, 我们需要灵活合理的处理Activity之间的切换. 还有比较特殊的情况是我们甚至需要调用其他APP中的Activity, 比如在发送邮件的时候我们可以通过给Intent传入一个send的参数,
以打开一个系统的发送邮件的Activity, 发送完成之后又将重新唤醒我们自己APP中的Activity, 这样看起来就好像系统发送邮件的Activity是属于我们自己的APP的. 管理这些复杂的Activity之间的切换, 需要一种合理的机制, 于是Android引入了任务(Task).

任务是保存Activity的集合. 每个任务中都维护着一个保存Activity的堆栈, Activity们遵循后进先出的原则, 在存放在堆栈中, 已经保存在堆栈中的Activity顺序不会发生改变.

通常任务的起始点是Android的桌面或者应用菜单, 当用户在桌面点击一款APP的图标, 那么他就开启了一个新的任务, 然后该APP的Main Activity将会启动, 并位于任务的堆栈中. 当前的Activity开启另一个新的Activity的时候, 新的Activity将会被压入栈顶, 并且获取焦点, 之前的Activity则位于栈底, 并且处于停止状态. 当用户点击返回键的时候, 位于栈顶的Activity将出栈并且销毁, 原来的Activity重新位于栈顶, 并Resume. 基本流程如下图: 



当所有的Activity都出栈之后, 该任务将不再存在.

默认情况下每个APP都自带一个任务, 该APP所启动的Activity都依次入栈, 如果在APP启动之后用户点击HOME键回到桌面, 那么该任务将和其堆栈一起变为”后台堆栈”, 所有已经打开的Activity也一起处于这个堆栈之中, 我们暂时称这个后台堆栈为A. 这时候如果用户再打开一个APP B的话, 那么B将创建一个新的任务和新的堆栈. B 的Activity将会进入堆栈B, 这时候我们推出APP B, 再点击APP A, 那么任务A将从后台转到前台, 重新变为一个拥有前台堆栈的任务. 之前打开的Activity依然保持原有的状态,
并且被Resume. 当后台的任务过多的时候, 系统可能会根据用户的使用顺序选择关闭最先被转移到后台的任务以腾出内存空间. 所以当Activity转移至后台的时候, 我们应该考虑保存它的数据.

这里有一个问题, 就是当一个Activity反复被打开的时候, 系统默认会一直打开多个Activity. 有时候还会产生这样的情况:



这样Home Activity就会被重复打开. 有些场景下, 这样做并不合理. 特别是同一个Activity被重复打开多次. 好在Android提供了相对的管理机制, 虽然比较复杂…这个将在后面的另一篇Blog中提到.

 

默认情况下Activity和Task的行为总结如下:

·    当Activity A启动Activity B的时候, ActivityA被停止, 并调用onPause(), onStop()方法, 此时系统会保存它的状态(比如滚动条的位置和文字输入栏里面的输入信息等), 当用户在Activity B中点击返回键的时候, Activity B被销毁, Activity A重新回到前台, 并恢复之前保存的状态.

·    当用户当前Activity按下HOME键的时候, 当前的Activity以及其所在的任务转至后台,Android会保留任务中每一个Activity的状态. 当用户稍后点击APP的图标再次启动APP的时候, 任务会再次回到前台, 并且唤醒之前在栈顶的Activity.

·    当用户通过返回键退出一个Activity的时候, 该Activity将会被销毁, 执行onDestroy()方法, 并且当一个Activity被Destroy之后, Android不再保存它的状态.

·    Activity可以被多次实例化, 甚至在不同的任务中.

虽然在Activity转移至后台的时候, Android会帮助我们保存Activity的状态, 但是我们还是很有必要自己保存Activity的状态, 以防止在后台的Activity在必要的情况下被回收, 并且不得不重建. 这时候这个Activity的状态就会丢失, 但是Android依然记得它在任务中的位置, 当用户点击返回键回到这个Activity的时候, 它会被重新创建, 但是状态已经丢失了, 这时候我们应该使用onSaveInstanceState()来保存我们的Activity的状态.

参考: http://developer.android.com/guide/components/tasks-and-back-stack.html
          http://developer.android.com/guide/components/processes-and-threads.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android 进程 线程 任务