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 语法

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:remotecom.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

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 的方式运行,其线程池的处理经历了多次调整:

  1. Android 1.5 使用单线程;
  2. Android 1.6 使用多线程;
  3. 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

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

其他的线程处理方式

除了 AsyncTaskHandlerThread,Android 也提供了一些便捷的方式处理线程问题,比如 IntentService、View 的 post()postDelayed()、Activity 的 runOnUiThread() 等。

对于 postDelayed(),需要注意的是,它与 HandlerThread 类似的是,它们的执行不会手动停止,因此,在合适的时候,我们需要手动停止。由于 View 类是可视的,所以我们一般在 onPause() 中会通过 View.removeCallbacks(this); 移除回调。

Thread Pool

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.MILLISECONDSTimeUnit.SECONDSTimeUnit.MINUTES 等。
  • workQueue:任务队列,通过线程池的 execute() 方法提交的 Runnable 对象会存储在这个参数中,可以注意到这是个阻塞型的双端队列,因此我们的任务处理是依次取出来执行的(如果是 BlockingDeque,就不保证按序执行了)。
  • threadFactory:用于创建新线程。

除此之外,`` 也存在一个构造函数,其还需要一个参数为 RejectedExecutionHandler handler。这个参数负责提供一种处理策略:当任务队列已满或者其他原因无法执行任务时,该如何进行处理。`RejectedExecutionHandler` 接口只有一个方法 `void rejectedExecution(Runnable r, ThreadPoolExecutor executor)`。`ThreadPoolExecutor` 提供了四种内置实现:`CallerRunsPolicy`、`AbortPolicy`、`DiscardPolicy`、`DiscardOldestPolicy`。默认情况下,是 `AbortPolicy`,可以看出,就是直接异常结束。

ThreadPoolExecutor 在执行任务时,大概流程是:

  1. 如果线程池中的线程数小于核心线程的数量,那么会直接启动一个核心线程来执行任务;
  2. 如果线程池中的线程数量已经达到或者超过核心线程的数量,那么新任务会被插入到任务队列中排队等待执行;
  3. 如果在步骤 2 中无法将任务插入到的任务队列中,可能是任务队列已满,这个时候如果线程数量没有达到规定的最大值,那么会立刻启动非核心线程来执行这个任务;
  4. 如果步骤 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 线程中执行。由于客户端可能有多个,它们又都有可能同时都在运行该方法,因此该方法需要实现线程安全。

同样的道理,ContentProviderquery()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 开发艺术探索》

⚠️ **GitHub.com Fallback** ⚠️