Android内部分享[5]——后台线程和多线程的使用

概述

我们前面提到过,在 Android 中有一个核心线程 UI 线程(或者叫主线程),负责处理 UI 渲染(包括去测量和绘制图形),协调用户交互和生命周期事件等。如果这个主线程中发送了太多的耗时操作和工作就会影响用户体验,我们的应用就会变得缓慢或者无响应,甚至出现 ANR(Application Not Responding),所以对我们开发者而言应该将那些比较耗时或者大段很多的操作委托给其他线程来处理,这些线程(非 UI 线程)在后台帮我们处理完成后再交给我们主线程来重新渲染界面。或者有时候我们需要做一些非用户交互的任务,比如定时去和服务器同步一些数据,像这种后台任务也应该直接交给非主线程,让它们在后台帮我们完成。

需要我们特别注意的是,我们在使用多线程来处理任务的时候要考虑到后台任务可能会消耗过多的资源,例如RAM 和电量,在 Android 系统中为了最大的优化系统性能和电池电量,当用户看不到应用在前台的时候会限制后台线程的工作,有可能会杀死这些线程。

Android 6.0 (API 23) 中引入了一个叫 Doze 的模式和应用程序待机处理, 当我们的屏幕关闭(也就是我们通常说的锁屏)且设备静止的时候, Doze 模式会限制应用程序的一些行为,例如网络不可访问, 线程执行的任务停止等。在 Android 7.0 和 8.0 之后更是进一步限制了后台行为,例如在后台获取位置将被禁止,通过 wakelocks 唤醒应用也被禁止。在 Android 9.0 之后引入了 App Standby Buckets, 应用程序对资源的请求根据应用程序使用模式进行动态优先级排序

所以我们在使用后台线程来实现我们的业务逻辑的时候要综合考虑,例如:

  1. 这项工作是否要立即执行,有些工作可能需要用户触发然后立即执行则尽量去立即执行,有的工作可能需要定时触发,就要考虑到对系统性能的影响和被系统停止的可能性。
  2. 我们执行的这项工作是否需要依赖一些特定的条件,比如需要有网了连接,需要电量充足等,或者需要在充电状态触发,或者在空闲的时候执行。
  3. 是否需要在准确的某个时间点执行,比如每天晚上12点执行某项任务,或者定时在某个日期执行一次。

最佳的后台任务实践流程

Google 官方给了我们如下建议:

WorkManager

对于一些可以延迟执行而且当设备重启后也需要执行的任务,建议我们使用 WorkManager。

WorkManager 是一个 Android 库,可以在满足工作条件(如网络可用性和功率)时优雅地运行可延迟的后台工作。WorkManager 提供向后兼容(API级别14+)API,利用 JobScheduler API(API级别23+)及更高版本来帮助优化电池寿命和批处理作业,以及在较低设备上组合 AlarmManager 和 BroadcastReceiver。

前台服务

对于需要立即运行并且必须执行完成的工作,请使用前台服务。 使用前台服务告诉系统应用程序正在执行重要操作并且不应该被杀死。 通过在通知栏显示一个不允许关闭的通知让用户可以看到前台服务。

AlarmManager

如果需要在精确时间运行作业,请使用 AlarmManager。 如有必要,AlarmManager 会启动您的应用程序,以便在您指定的时间完成工作。 但是,如果您的工作不需要在准确的时间运行,WorkManager 是一个更好的选择; WorkManager 能够更好地平衡系统资源。 例如,如果您需要每小时左右运行一个作业,但不需要在特定时间运行作业,则应使用 WorkManager 设置定期作业。

DownloadManager

如果您的应用正在执行长时间运行的 HTTP 下载,请考虑使用 DownloadManager。 客户端可以请求将 URI 下载到可能在应用程序进程之外的特定目标文件。 下载管理器将在后台进行下载,负责 HTTP 交互并在出现故障或连接更改和系统重新启动后重试下载。

运行一个线程

Java 中创建线程的方式有两种,一种是 new Thread(){ } 另一种则是实现 Runnable 接口。

/* 
* 线程的第一种创建方式 
*/  
Thread thread1 = new Thread(){  
    @Override  
    public void run() {  
        try {  
            sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName()); 
    }  
}; 

thread1.start();  
/* 
*线程的第二种创建方式  
*/  
Thread thread2 = new Thread(new Runnable() {  
        
    @Override  
    public void run() {  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName());   
    }  
});  

thread2.start();

上面的 Thread.currentThread() 方法可以获得当前线程对象,它是一个静态方法, 然后我们可根据 Thread 对象的 getName() 方法获得线程名称。

在 Android 中有一个进程类 android.os.Process 可以帮我们设置线程的优先级:

android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);

这个 setThreadPriority() 的参数是一个 int 类型的值, 例如上面的 android.os.Process.THREAD_PRIORITY_BACKGROUND:

public static final int THREAD_PRIORITY_BACKGROUND = 10;

如果我们设置 THREAD_PRIORITY_BACKGROUND 优先级则它的优先级略低于普通优先级,所以它可以减小对用户界面线程的影响。下面列出一些线程优先级, 事实上大多数线程优先级被系统所使用,我们应用程序无法使用。

线程优先级说明
THREAD_PRIORITY_AUDIO-16音频线程的标准优先级。 应用程序通常无法更改为此优先级。
THREAD_PRIORITY_BACKGROUND10优先级略低于普通优先级,所以它可以减小对用户界面线程的影响。
THREAD_PRIORITY_DEFAULT0应用程序线程的标准优先级
THREAD_PRIORITY_DISPLAY-4系统显示线程的标准优先级,涉及更新用户界面。
THREAD_PRIORITY_LOWEST19对低的线程优先级,那些发生在任何时候都可以的线程可以设置

Android 中的进程

当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。 如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。 但是,您可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

通俗点说就是我们的应用程序一般都是在一个 Linux 进程中,但是我们也可以给一个应用程序创建多个进程,每个进程中除了主线程外我们都可以额外创建其他线程。

注册表中的四大组件 <activity> , <service>, <receiver>, <provider> 都支持 android:process 属性,这个属性可以指定对应组件所在的进程,甚至我们可以为不同的应用中的组件指定同一个进程,但是有个前提条件是他们有相同的签名。 <application> 也支持 android:process 属性来设置所有组件的默认进程。

系统会将所有进程放进一个优先级队列,当系统资源不足,或者当息屏后系统为了节省系统资源和电量开销就会根据优先级来杀死一些进程,这个优先级分为 5 个层次:

  1. 前台进程

如果一个进程满足以下任一条件,即视为前台进程:

  • 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
  • 托管某个 Service,后者绑定到用户正在交互的 Activity
  • 托管正在“前台”运行的 Service(服务已调用 startForeground())
  • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
  • 托管正执行其 onReceive() 方法的 BroadcastReceiver

一般情况我们的前台进程不多,只有在内存不足或者其他资源紧缺的情况下系统才会终止一些前台进程。

  1. 可见进程

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

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

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

  1. 服务进程

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

  1. 后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。

  1. 空进程

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

我们会发现服务进程比后台进程优先级高,也就是说后台服务的级别高于后台 Activity 的级别,所以如果我们的应用特点是长时间启动 Activity 最好启动一个后台服务来做一些后台操作,这样即使用户退出此 Activity 也可以保证更好的在后台做一些操作。

线程调度

以下代码演示了一个点击事件内创建新线程来下载图像并将其显示在 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 控件。为了解决这个问题 Android 提供了几种途径来从其他线程访问 UI 线程。 以下列出了几种有用的方法:

  1. Activity.runOnUiThread(Runnable)

    public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap =
                    loadImageFromNetwork("http://example.com/image.png");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
    }
    
  2. 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();
    }
    
  3. View.postDelayed(Runnable, long)

    mImageView.postDelayed(new Runnable() {
    public void run() {
        mImageView.setImageBitmap(bitmap);
    }
    }, 2000);
    

上面的几种方式都是线程安全的,但是这种方式在有些场合非常实用,在某些复杂的场合则就有点难以维护了,比较复杂的逻辑场景我们可以使用 Handler 或者 AsyncTask 类。

Handler 使用

Handler 我们前面提到过,下面使用一个简单的例子来说明:

package com.dlc.innershare;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

/**
 * 描述:
 * 日期:2019/8/14
 * 作者:水寒
 * 邮箱:lxq_xsyu@163.com
 */
public class TestThreadCreateActivity extends BaseActivity {

    private static final int HAND_UPDATE_TIME = 0x0001; //计数更新消息
    private static final int HAND_START_THREAD = 0x0002; //线程启动消息
    private static final int HAND_STOP_THREAD = 0x0003;  //线程结束消息

    private TextView mShowTime;

    private Thread mCountTimeThread;
    private int mCurrentTime;
    private boolean isRunning;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){  //处理消息
                case HAND_UPDATE_TIME:
                    mShowTime.setText(String.format("正在计数:%d", mCurrentTime));
                    break;
                case HAND_STOP_THREAD:
                    ToastUtil.showToast("线程已结束");
                    break;
                case HAND_START_THREAD:
                    ToastUtil.showToast("线程已经启动");
                    break;
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_thread_create);
        mShowTime = findViewById(R.id.tv_show_time);
    }

    /**
     * 启动线程 按钮绑定事件回调
     * @param view
     */
    public void clickStartThread(View view) {
        startThread();
    }

    /**
     * 结束线程 按钮绑定事件回调
     * @param view
     */
    public void clickStopThread(View view) {
        stopThread();
    }

    /**
     * 通过 Thread 创建一个线程
     */
    private void createThread(){
        mCountTimeThread = new Thread(){
            @Override
            public void run() {
                mHandler.sendEmptyMessage(HAND_START_THREAD);
                while (isRunning){
                    try {
                        Thread.sleep(1000); //休息 1 秒钟
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    mCurrentTime++; //计数加 1
                    mHandler.sendEmptyMessage(HAND_UPDATE_TIME);
                }
                mHandler.sendEmptyMessage(HAND_STOP_THREAD);
            }
        };
    }

    /**
     * 启动一个线程
     */
    private void startThread(){
        stopThread();
        isRunning = true;
        createThread();
        mCurrentTime = 0;
        mHandler.sendEmptyMessage(HAND_UPDATE_TIME);
        mCountTimeThread.start();
    }

    /**
     * 结束一个线程
     */
    private void stopThread(){
        if(mCountTimeThread == null) return;
        isRunning = false;
        mCountTimeThread = null;
    }
}

上面示例中我们使用 Handler 的 sendEmptyMessage 发送了一个空消息(只携带ID的消息),我们可以发送 Message 对象,这样就可以携带一些数据。

Message message = Message.obtain();
message.what = HAND_UPDATE_TIME;
message.arg1 = mCurrentTime;
mHandler.sendMessage(message);

AsyncTask 使用

这个类非常方便,它为我们提供了 UI 线程和其他线程之间的剥离,我们只需要去它的指定方法中实现具体逻辑即可:

private class MyTask extends AsyncTask<Void, Integer, Boolean>{

    @Override
    protected Boolean doInBackground(Void... voids) {

    }
}

new MyTask().execute();

这个 doInBackground 方法是默认要实现的,这个方法是在其他线程,我们可以在里面做耗时操作,然后上面的三个泛型 <Void, Integer, Boolean> 分别指代的是 传入参数类型,publishProgress的参数类型,该方法返回的类型。

例如我们使用 AsyncTask 实现上面的计数案例:

private class MyTask extends AsyncTask<Void, Integer, Boolean>{

    private int mCurrentTime;

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mCurrentTime = 0;
        updateCount(mCurrentTime);
        startCount();
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        while(!isCancelled()){
            try {
                Thread.sleep(CountInterface.SLEEP_TIME);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mCurrentTime++;
            publishProgress(mCurrentTime);
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        updateCount(values[0]);
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
        stopCount();
    }
}