异步任务之 线程和进程

Android 中的进程

我们知道,Android 是基于 Linux 系统的,Linux 拥有“多用户、多任务、多线程”的特性,所以 Android 也有这个特性。

Android 系统中的每一个 App 都是一个不同的用户,App 安装之后,系统会为其分配一个用户 ID,并新创建一个进程,并创建一个虚拟机来运行该应用(Android 5.0 之前是 Dalvik 虚拟机,之后是 ART),也就是说,每个 App 的运行环境都是独立的。Android 会在需要执行任何应用组件时启动该进程,然后在不再需要该进程或系统必须为其他应用恢复内存时关闭该进程。

默认情况下,我们的应用只能访问自己的工作进程内的组件,不能访问其他进程内的组件,这样也就达到了安全的目的,有以下方法可以访问其他应用或者系统服务:

  • 可以为两个不同的 App 设置相同的用户 ID,在这种情况下,它们能够相互访问彼此的文件。 为了节省系统资源,可以安排具有相同用户 ID 的应用在同一 Linux 进程中运行,并共享同一 VM(应用还必须使用相同的证书签署)。
  • 应用可以请求访问设备数据(如用户的联系人、短信、可装载存储装置 [SD 卡]、相机、蓝牙等)的权限。 用户必须明确授予这些权限。

默认情况下,我们 App 的组件都是运行在同一进程内的,但是我们可以为组件指定新的进程,,我们可以在清单文件中的 <activity><service><receiver><provider> 中直接指定 android:process 属性为该组件设置单独的进程,也可以在 <application> 标签中设置该属性,用于为所有组件设置共同的特定进程:

  • 如果这个进程名称用“:”开头,那么在需要的时候,就会给应用程序创建一个新的、私有的进程。
  • 如果进程名用小写字符开头,就会用这个名字创建一个全局的进程,这个全局的进程能够被其他应用程序共享,从而减少资源的使用。

当系统内存不足又需要为其他应用开启进程时,系统会选择性的关闭一些进程,那么进程中运行的组件也会随之销毁,销毁的顺序按照其重要程度决定,重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

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

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

通常情况下,前台进程都不会太多,只有在内存严重不足,不足到满足不了他们同时存在的情况下,才会终止他们来确保一些前台进程来确保用户界面的正常交互。

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

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

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

  • 服务进程:正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
  • 后台进程:包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。
  • 空进程:不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

Android 中的线程

应用在启动时,系统会创建一个名为“主线程”的执行线程,该线程主要主要负责界面的绘制、事件传递响应,有时候,该线程也称为“UI 线程”。

主线程之外的线程被称之为“工作线程”。

我们知道主线程用于绘制界面、事件传递响应,假如在主线程内执行耗时操作,譬如下载操作、大数据 I/O,则会阻塞 UI 线程,从而导致界面无法绘制、事件无法传递,Android 系统会弹出“ANR”提示(Application Not Responding,程序无响应)。

此外,Android UI 工具包并非线程安全工具包,所以,我们不可以通过工作线程去操作 UI,只能通过 UI 线程去操作。因此,Android 的单线程模式必须遵守两条规则:

  1. 不要阻塞 UI 线程
  2. 不要在 UI 线程之外操作 UI

那如果我们需要从网上下载图片,然后再设置到 UI 上呢?Android 提供了以下几种方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable)

示例一:

public class MainActivity extends Activity {

    private Button button;
    private ImageView image;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        image = findViewById(R.id.image);


        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    URL url = new URL("http://pic.yesky.com/uploadImages/2015/019/31/C6IQ4C17849F_200x0.png");
                    URLConnection connection = url.openConnection();
                    InputStream fis = connection.getInputStream();
                    FileOutputStream fos = openFileOutput("girl.png", Context.MODE_PRIVATE);
                    byte[] buf = new byte[1024];
                    int len = 0;
                    while ((len = fis.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        fos.flush();
                    }
                    fis.close();
                    fos.close();

                    String path = getApplicationContext().getFilesDir().getAbsolutePath() + "/girl.png";
                    Bitmap bitmap = BitmapFactory.decodeFile(path);
                    image.setImageBitmap(bitmap);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

代码很简单,点击按钮,从网上下载一张图片,然后设置给 ImageView,但是在上面例子中,点击按钮会程序崩溃抛出异常:

android.os.NetworkOnMainThreadException

顾名思义,主线程里执行了网络操作,修改一下代码:

public class MainActivity extends Activity {

    private Button button;
    private ImageView image;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        image = findViewById(R.id.image);


        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        URL url = null;
                        try {
                            url = new URL("http://pic.yesky.com/uploadImages/2015/019/31/C6IQ4C17849F_200x0.png");
                            URLConnection connection = url.openConnection();
                            InputStream fis = connection.getInputStream();
                            FileOutputStream fos = openFileOutput("girl.png", Context.MODE_PRIVATE);
                            byte[] buf = new byte[1024];
                            int len = 0;
                            while ((len = fis.read(buf)) != -1) {
                                fos.write(buf, 0, len);
                                fos.flush();
                            }
                            fis.close();
                            fos.close();

                            String path = getApplicationContext().getFilesDir().getAbsolutePath() + "/girl.png";
                            final Bitmap bitmap = BitmapFactory.decodeFile(path);
                            image.post(new Runnable() {
                                @Override
                                public void run() {
                                    image.setImageBitmap(bitmap);
                                }
                            });
                        } catch (MalformedURLException e) {
                            e.printStackTrace();
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        });
    }
}

这下就可以执行成功了~新开了线程去下载图片,然后调用 View 的 post 方法,在 UI 线程中更新设置图片。

但是这样写是不是很麻烦?Android 提供了 Handler 机制来帮助我们完成类似这种异步任务,也可以使用 AsyncTask