3.1 进程和线程 - TomeOkin/Learning-Notes GitHub Wiki
默认情况下,当启动一个应用组件的时候,如果当前没有其他正在运行的应用组件,那么 Android 系统会启动一个新的 Linux 进程,并在主线程中运行该应用组件。
如果需要在其他进程中运行应用组件,可以在 manifest 文件中进行声明。<activity>
、<service>
、<receiver>
和 <provider>
标签都支持通过 android:process
设置在特定的进程中运行;也可以对 <application>
进行配置,则默认应用到子组件中。通过 android:process
配置的各个组件会共享相同的 Linux user ID,并且使用相同的证书进行签名。
每一个新启动的进程,都是由 Android 系统对 zygote 进行 fork 操作产生的,因此,每一个进程都包括了:
- 一个复制的 VM(Dalvik 或者 ART),不同的进程间通过 Linux 的 copy-on-write 进行内存共享。
- 一份复制的 Android framework classes,比如:Activity、Button。同样以 copy-on-write 进行内存共享。
- 一份通过 apk 加载的 classes。
- 由 framework 或者 apk classes 创建的实例。
VM 提供的 heap size 可能是 16M,对于目前大部分手机设备,也有可能是 32 ~ 48MB。
Android 系统会对各种进程进行分级,以决定在内存不足的时候,哪些进程会被优先结束。按照分级结果,最不重要的进程会被优先结束。以下按重要性递减依次描述(划分也依次过滤):
(1)前台进程:一般在某个时刻只有少量进程属于前台进程。只有在当设备已经处于内存分页状态,只有清除一些前台进程才能响应界面操作的时候才会结束前台进程。有几种类型可以被归结为前台进程:
- 至少包含一个执行了
onResume
之后,可以进行交互的Activity
。 - 至少包含一个 Service,该 Service 绑定了一个用户可以交互的
Activity
。 - 至少包含一个前台服务(用
startForeground()
启动的服务)。 - 至少包含一个正在执行生命周期回调(
onCreate()
、onStart()
或者onDestroy()
)的 Service。 - 至少包含一个
BroadcastReceiver
,并且正在执行onReceive()
方法。
这些组件所处的生命周期,位于 onResume()
之后,onPause()
之前。
(2)可视进程:这些进程不包含前台组件,但可以让用户获得视觉感知。有几种类型:
- 至少包含一个非前台的 Activity(执行了
onPause()
),但用户可以看到。比如启动了非全屏对话框的 Activity。 - 至少包含一个 Service,该 Service 绑定了一个可视或者前台 Activity。
这些组件所处的生命周期,位于 onStart()
之后,onStop()
之前,但不包含 onResume()
之后,onPause()
之前的。
只有在为了保持所有前台进程都能运行的时候,可视进程才会被结束。
(3)Service 进程:至少包含一个 Service,是使用 startService() 方式启动,但不包含在前两类里的进程。这类 Service 可能是比如播放音乐或者执行下载任务的。只有当为了保全所有前两类进程的时候才会被结束。
(4)后台进程:该进程包含生命周期位于 onStop()
之后的 Activity,这些进程都会以 LRU 方式进行管理。
(5)空进程:这些进程的存在仅仅是为了在下次启动相关组件时加快启动速度。这些进程常常会在平衡系统资源时被结束。
进程的优先级并不完全有这些决定。如果有某个进程为其他进程提供服务(比如 ContentProvider
),那么这个进程重要性至少与获取服务的其他进程具有相同的重要性。
基于这些规则,一般情况下,如果有需要执行较长时间的任务,最好在 Service 中进行;同样,如果广播接收器有任务要执行,最好也是启动服务来完成。
android:process
有两种写法:
- “:” 开头:为简写写法,前面省略了应用包名。这种进程属于应用的私有进程,其他应用的组件不能在该进程内运行。
- 没有以 “:” 开头:这种进程属于全局进程,其他应用可以运行在该进程内。当两个应用的组件运行在同一个进程内时,它们可以访问彼此的 data 目录、组件信息等。
<activity
android:name=".SecondActivity"
android:label="@string/app_name"
android:process=":remote" />
<activity
android:name=".ThirdActivity"
android:label="@string/app_name"
android:process="com.ryg.chapter_2.remote" />
可以通过 Android Device Monitor 的 DDMS 或者 adb shell ps
来查看应用的进程信息。如上,进程名分别为 com.ryg.chapter_2:remote
和 com.ryg.chapter_2.remote
。
主线程又称 UI 线程,在 Android 中,UI 相关的事件都在主线程上进行分发。以故,所有系统回调,比如 onKeyDown()
等也都在主线程被调用。
当我们点击一个按钮的时候,UI 线程会分发触摸事件给按钮,按钮就设置自身状态为 pressed 状态,并发送一个 invalidate 请求到事件队列,接着 UI 线程会依次取出消息并通知按钮进行重绘。因此,当在主线程上执行过多的操作时,就会影响界面更新,导致用户体验变差。当界面响应时间超过 5 秒的时候,系统就会弹出 application not responding(ANR) 对话框,提示应用无响应。对于当需要显示下一帧,但由于下一帧的数据还没准备好,只能保持当前帧的现象,在 Android 中称为 “Jank”。在 Android 4.1 中,提出了 Project Butter
,对执行时间与渲染界面相关的内容,进行了描述。当我们一帧的时间花费超过 16ms 的时候,就会发生掉帧现象,也即出现 “Jank”。为了不发生掉帧,提供一个良好的用户体验,我们需要将一些任务放到工作者线程中执行。
由于 Android 的 UI toolkit(View、ViewGroup 等)不是线程安全的,因此,我们必须在主线程上操作 UI toolkit。
主线程以外的其他线程,我们使用其来进行一些后台的工作或者任务,因此将其称为后台线程或者工作者线程。除了 java 中的 Thread 以及 java.util.concurrent
包里的工具外,Android 还提供了几种封装好的工具专门用于进行线程处理:AsyncTask、HandlerThread。
AsyncTask<Params, Progress, Result>
,有三个泛型参数,分别表示执行任务需要的描述信息,任务的进度信息,执行的结果。
public class AsyncFragment extends Fragment {
private DownloadFilesTask task = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
task = new DownloadFilesTask();
try {
task.execute(new URL("http://www.google.com"), new URL("http://www.renyugang.cn"));
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
@Override
public void onDestroy() {
if (task != null) {
task.cancel(false);
}
super.onDestroy();
}
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
// totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
protected void onProgressUpdate(Integer... progress) {
// setProgressPercent(progress[0]);
}
protected void onPostExecute(Long result) {
// showDialog("Downloaded " + result + " bytes");
}
}
}
(1)AsyncTask
有几个常用的根据需要进行重载的方法:
-
onPreExecute()
:在任务执行前被调用。 -
doInBackground()
:进行后台任务处理。 -
onProgressUpdate()
:用于进行进度更新,通过执行 publishProgress() 来触发该函数;一般在 doInBackground() 发布进度信息。 -
onPostExecute()
:任务执行完成后触发,一般用于更新界面之类的操作,如果任务取消了,则不会执行该方法。
对于以上几个方法,doInBackground()
在工作者线程中被调用,其余都在主线程中被调用。
(2)AsyncTask
是由 Handler 和 Thread Pool 封装而成的。在使用 AsyncTask
时,我们通过 AsyncTask.execute
方法来执行。为了保证 onPreExecute()
、onProgressUpdate()
、onPostExecute
等在主线程上被回调,AsyncTask
实例的创建和 execute()
都必须在主线程上执行。另外,execute()
只能使用 1 次。
(3)AsyncTask 内部使用 Thread Pool 进行线程管理,以 Process.THREAD_PRIORITY_BACKGROUND
的方式运行,其线程池的处理经历了多次调整:
- Android 1.5 使用单线程;
- Android 1.6 使用多线程;
- Android 3.2 (API 13)使用单线程,我们启动的任务会被加入队列中,依次执行。
一般情况下,使用单线程是一种较为合理的做法,如果希望使用多线程,在 API 13+ 可以通过 executeOnExecutor(Executor, ...)
实现。一般线程池(Executor)设置为 AsyncTask.THREAD_POOL_EXECUTOR
,这是默认的线程池,线程数为 CPU 核心数 * 2 + 1
。
(3)当销毁 Activity 时,如果任务还在运行,一般会使用 AsyncTask.cancel(boolean)
来取消未完成的任务。对此,如果参数为 false,那么只会将 AsyncTask 内部的 AtomicBoolean mCancelled
的值设置为 true,而不会强制结束任务,这样,我们可以在 doInBackground()
、onProgressUpdate()
中通过 isCancelled()
检查任务是否取消来结束;如果参数为 false,那么 AsyncTask
除了会将 mCancelled
的值设置为 true 外,还会强制中断任务的执行(AsyncTask
内部使用 FutureTask
执行任务,具备强制结束任务的功能)。
(4)当发生配置更改,比如旋转屏幕时,Activity 会被重建,这时 AsyncTask
也会被销毁。在 Fragment 中,我们可以通过在 onCreate() 中配置 setRetainInstance(true);
来避免 Fragment 被销毁,从而防止 AsyncTask
被销毁。除此之外,还有一种方法,就是使用 AsyncTaskLoader
来解决这个问题。
在这种情况下,还需要注意的是,如果 AsyncTask
是在 Fragment
中使用,则在 doInBackground()
中不用使用 getActivity()
,因为有些时候任务还在执行,但 getActivity()
的结果不一定有效,比如正发生配置更改的时候;在其他回调方法中,则可以使用。
(5)如果一个任务需要执行的时间不确定或者需要较长的执行时间,是不应该使用 AsyncTask 的。
HandlerThread
继承自 Thread,内部使用了 Looper 来创建消息队列和驱动任务执行。HandlerThread
默认使用了 Process.THREAD_PRIORITY_DEFAULT
级别的线程优先级。在使用 HandlerThread
时,会通过 handler 发送任务,并通过消息队列取出任务进行执行。以下是一个使用 HandlerThread
实现的图片下载 demo:
public class ThumbnailDownloader<T> extends HandlerThread {
private static final String TAG = "ThumbnailDownloader";
private static final int PASSAGE_DOWNLOAD = 0;
private Handler mResponseHandler;
private ThumbnailDownloadListener<T> mThumbnailDownloadListener;
private Handler mRequestHandler;
private ConcurrentMap<T, String> mRequestMap = new ConcurrentHashMap<>();
private LruCache<String, Bitmap> mBitmapCache;
public ThumbnailDownloader(Handler responseHandler, LruCache<String, Bitmap> bitmapCache) {
super(TAG);
mResponseHandler = responseHandler;
mBitmapCache = bitmapCache;
}
public interface ThumbnailDownloadListener<T> {
void onThumbnailDownloaded(T target, Bitmap bitmap);
}
public void setThumbnailDownloadListener(ThumbnailDownloadListener<T> listener) {
mThumbnailDownloadListener = listener;
}
@Override
protected void onLooperPrepared() {
mRequestHandler = new Handler() {
@SuppressWarnings("unchecked")
@Override
public void handleMessage(Message msg) {
if (msg.what == PASSAGE_DOWNLOAD) {
T target = (T) msg.obj;
Log.i(TAG, "Got a request for URL: " + mRequestMap.get(target));
handleRequest(target);
}
}
};
}
private void handleRequest(final T target) {
try {
final String url = mRequestMap.get(target);
if (url == null) {
return;
}
final Bitmap bitmap;
if (mBitmapCache.get(url) != null) {
bitmap = mBitmapCache.get(url);
} else {
byte[] bitmapBytes = getUrlBytes(url);
bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
if (bitmap == null) {
return;
}
mBitmapCache.put(url, bitmap);
}
Log.i(TAG, "Bitmap created");
mResponseHandler.post(new Runnable() {
@Override
public void run() {
if (mRequestMap.get(target) != url) {
return;
}
mRequestMap.remove(target);
mThumbnailDownloadListener.onThumbnailDownloaded(target, bitmap);
}
});
} catch (IOException ioe) {
Log.e(TAG, "Error downloading image", ioe);
}
}
public void queueThumbnail(T target, String url) {
Log.i(TAG, "Got a URL: " + url);
if (url == null) {
mRequestMap.remove(target);
} else {
mRequestMap.put(target, url);
mRequestHandler.obtainMessage(PASSAGE_DOWNLOAD, target)
.sendToTarget();
}
}
public void clearQueue() {
mRequestHandler.removeMessages(PASSAGE_DOWNLOAD);
}
public byte[] getUrlBytes(String urlSpec) throws IOException {
URL url = new URL(urlSpec);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
try {
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(connection.getResponseMessage() + ": with " + urlSpec);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream in = connection.getInputStream();
int bytesRead = 0;
byte[] buffer = new byte[1024];
while ((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
out.close();
return out.toByteArray();
} finally {
connection.disconnect();
}
}
}
以上代码比较简单,对于 mRequestHandler
为什么是在 onLooperPrepared()
中进行创建,进行一点说明。
HandlerThread
中,对 run()
进行了重写,如下,可以看到,在 onLooperPrepared()
之前,才刚刚创建 Looper,而之后已经开始在当前线程中处理消息队列。因此在此处创建 mRequestHandler
是相对比较合理的。
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
对于以上的实现,创建和使用的方法如下:
mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler, mBitmapCache);
mThumbnailDownloader.setThumbnailDownloadListener(mListener);
mThumbnailDownloader.start();
mThumbnailDownloader.getLooper();
mThumbnailDownloader.queueThumbnail(photoHolder, url);
同样,也有一点需要注意的地方:HandlerThread
除了像 Thread 一般,使用 start() 启动外,最好再执行一次 getLooper()
,以保证后续发送消息时,消息队列已经准备完毕。
在 Activity 销毁时,需要对 HandlerThread
进行销毁,可以通过 clearQueue()
清除消息队列,通过 quit()
或者 quitSafely()
销毁 HandlerThread
(前者可能在消息分发完成前结束 HandlerThread
)。
@Override
public void onDestroyView() {
super.onDestroyView();
mThumbnailDownloader.clearQueue();
}
@Override
public void onDestroy() {
super.onDestroy();
mThumbnailDownloader.quit();
}
除了 AsyncTask
和 HandlerThread
,Android 也提供了一些便捷的方式处理线程问题,比如 IntentService
、View 的 post()
和 postDelayed()
、Activity 的 runOnUiThread()
等。
对于 postDelayed()
,需要注意的是,它与 HandlerThread
类似的是,它们的执行不会手动停止,因此,在合适的时候,我们需要手动停止。由于 View 类是可视的,所以我们一般在 onPause()
中会通过 View.removeCallbacks(this);
移除回调。
AsyncTask
的线程池使用的是 Executor
来实现的,构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
其中:
-
corePoolSize
:决定了核心线程数,默认情况下,核心线程会一直存活。Java 1.6 起可以通过allowCoreThreadTimeOut(boolean value)
设置核心线程在 idle 超过keepAliveTime
时也会被回收。 -
maximumPoolSize
:决定线程池最多可以容纳的线程数。包括核心线程和非核心线程。 -
keepAliveTime
:idle 超时时间,默认情况下,非核心线程在超过该时间任处于 idle 状态时,会被回收。 -
unit
:用于指定keepAliveTime
的时间单位。常用的有TimeUnit.MILLISECONDS
、TimeUnit.SECONDS
、TimeUnit.MINUTES
等。 -
workQueue
:任务队列,通过线程池的execute()
方法提交的Runnable
对象会存储在这个参数中,可以注意到这是个阻塞型的双端队列,因此我们的任务处理是依次取出来执行的(如果是BlockingDeque
,就不保证按序执行了)。 -
threadFactory
:用于创建新线程。
除此之外,`` 也存在一个构造函数,其还需要一个参数为 RejectedExecutionHandler handler
。这个参数负责提供一种处理策略:当任务队列已满或者其他原因无法执行任务时,该如何进行处理。`RejectedExecutionHandler` 接口只有一个方法 `void rejectedExecution(Runnable r, ThreadPoolExecutor executor)`。`ThreadPoolExecutor` 提供了四种内置实现:`CallerRunsPolicy`、`AbortPolicy`、`DiscardPolicy`、`DiscardOldestPolicy`。默认情况下,是 `AbortPolicy`,可以看出,就是直接异常结束。
ThreadPoolExecutor
在执行任务时,大概流程是:
- 如果线程池中的线程数小于核心线程的数量,那么会直接启动一个核心线程来执行任务;
- 如果线程池中的线程数量已经达到或者超过核心线程的数量,那么新任务会被插入到任务队列中排队等待执行;
- 如果在步骤 2 中无法将任务插入到的任务队列中,可能是任务队列已满,这个时候如果线程数量没有达到规定的最大值,那么会立刻启动非核心线程来执行这个任务;
- 如果步骤 3 中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务,
ThreadPoolExecutor
会调用RejectedExecutionHandler.rejectedExecution()
处理该情况。
Executors
类提供了四种内置的线程池实现,由对应方法进行创建:
-
Executors.newFixedThreadPool()
:只含有固定数量的核心线程数,没有 idle 超时限制。 -
Executors.newCachedThreadPool()
:只含有不限数量的非核心线程数,idle 超时时间为 60 秒。 -
Executors.newScheduledThreadPool(4)
:核心线程数固定,非核心线程数不限,当不允许非核心线程 idle。 -
Executors.newSingleThreadExecutor()
:只含有 1 个核心线程,无非核心线程,无 idle 超时限制。
Runnable command = new Runnable() {
@Override
public void run() {
SystemClock.sleep(2000);
}
};
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
fixedThreadPool.execute(command);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool.execute(command);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
// 2000ms后执行command
scheduledThreadPool.schedule(command, 2000, TimeUnit.MILLISECONDS);
// 延迟10ms后,每隔1000ms执行一次command
scheduledThreadPool.scheduleAtFixedRate(command, 10, 1000, TimeUnit.MILLISECONDS);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.execute(command);
当一个方法可能在多个线程中执行的时候,就需要考虑线程安全的问题。大部分情况下,我们在 Bound Service 中才会遇到这个问题。比如:在 IBinder 运行的时候,这时如果调用该方法的进程与实现 IBinder 的进程是同一个,那么该方法就是在调用者的进程中运行(同个进程内拿到的是原始的对象);然而如果不是在同一个进程中,那么该方法则是在系统所维护的 IBinder 线程池的某个线程里被调用,这时该方法就不是在 UI 线程中执行。由于客户端可能有多个,它们又都有可能同时都在运行该方法,因此该方法需要实现线程安全。
同样的道理,ContentProvider
的 query()
、insert()
、delete()
、update()
和 getType()
这些方法也都是在 ContentProvider
的线程池中被调用的,它们也需要实现线程安全。
一般情况下,只需要保证一个方法所操作的所在类的成员变量都是线程安全,那么该方法就是线程安全的。
[Getting To Know Android 4.1, Part 3: Project Butter - How It Works And What It Added] Know_Android_4.1_Part_3_Project_Butter 这个系列有三篇文章,都是很不错的,强烈推荐
processes and threads 官方文档
《The Busy Coders Guide to Android Development》
《Android Programming:The Big Nerd Ranch Guide》
《Android 开发艺术探索》