多线程可以让你同时异步执行多种任务,是各种编程语言里很重要的一个概念。合理的采用多线程可以让你的 App 拥有更好的运行性能,但是如果使用不当可能会让你的程序非常混乱,出现很多令人费解且难以定位的问题。
1. 多线程初探
当用户打开一个 App 时,Android 系统会创建一个 Linux 进程,同时在进程中创建一个执行线程,我们称之为“主线程”,因为 Andfoid 规定只能在主线程更新 UI,所以又叫“UI线程”。
系统在创建主线程的时候帮我们创建好了一套消息处理机制,包含了第 38 节提到的 Handler、Looper、MessageQueue 等模块,主线程就利用这一套消息机制来实现 Actvity、Fragment 的生命周期回调以及和其他 App 之间的通信。所有需要在 UI 线程执行的任务都要首先被 push 到任务队列中,然后等待主线程 Looper 来轮询。如果我们将所有的任务都放到主线程的任务队列,那么可能需要等很久才能执行到,所以一个比较好的选择就是将耗时任务单独放到一个子线程中,这样就可以独享一个 MessageQuene,并且不再占用主线程的资源。
2. 多线程注意事项
- Android 规定刷新 UI 的操作必须在主线程执行;
- 网络请求、数据库或者文件 I / O 等都属于耗时操作,非常容易导致主线程的阻塞造成 App 卡顿;
- 由于主线程的 Looper 是按顺序轮询 MessageQueue 的,所以主线程的所有任务都是同步执行。这样如果有耗时操作那么会阻塞主线程,后面的任务都需要等待耗时操作的执行;
- 除了 I / O 操作外,开发人员需要自行评估任务的耗时情况,合理采用多线程避免主线程的阻塞;
- Android 提供了多种创建和管理线程的方法,当然如果有高并发的场景还有一些第三方库可以使用,但是系统的线程、线程池可以应对大部分常见场景。
- 接下来我们来看看具体怎么使用 Android 多线程。
3. 线程的使用方法
Java 虚拟机支持多线程并发编程,并发意味着同时执行多个任务。在 Android 中常见的多线程常见就是在子线程执行耗时操作,然后将结果通过线程间通信传递给主线程,主线程仅仅拿到结果进行 UI 的刷新。
3.1 线程的创建
我们有两种方式进行线程的创建
- 继承
Thread
实现一个线程类:
class TestThread extends Thread { @Override public void run() { Log.d("Threading", "继承 Thread 的线程:"+Thread.currentThread().getName()); } }
- 实现
Runnable
接口
class TestRunnable implements Runnable { @Override public void run() { Log.d("Runable", "实现 Runable 的线程>"+Thread.currentThread().getName()); } }
无论是哪种方式,都需要在类中实现一个无参的run()
方法,然后将线程的实际执行任务放在run()
方法中,在要用多线程的类中需要创建出一个Runnable
接口实例。
3.2 启动进程
对于第一种创建方式,直接创建 TestThread 实例调用start()
即可:
new TestThread().start()
而对于第二种方式,在创建 Thread 的同时传入 Runnable 接口实例,然后调用start()
:
new Thread(new TestRunnable()).start()
调用了 Thread 对象的 start()
之后,run()
方法就会在我们的子线程中执行了。
3.3 线程生命周期
和 Activity 一样,Thread 在执行过程中也有自己的生命周期,一共有 5 种状态:
- **New:**刚创建好,还未执行
- **Runnable:**已经调用了start(),等待 CPU 分配时间片
- **Running:**正在运行
- **Blocked:**由于某些原因(等待、睡眠、CPU暂时回收资源等)线程进入阻塞
- **Dead:**线程任务执行结束,或者主动关闭
- 各个生命周期的切换如下图:
4. 多线程示例
本节创建两个耗时子线程,在线程的开始和结束分别打上日志,然后观察两个线程任务是同时执行,还是需要等待其中一个线程的耗时任务执行结束才能执行第二个。
MainActivity 代码很简单,在里面创建两个线程,为了方便演示我们用“继承自 Thread”和“实现 Runnable”两种方式来创建两个线程:
package com.emercy.myapplication; import android.app.Activity; import android.content.pm.PackageManager; import android.media.MediaPlayer; import android.media.MediaRecorder; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.Toast; import androidx.annotation.Nullable; import java.io.IOException; import java.util.Random; import static android.Manifest.permission.RECORD_AUDIO; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; public class MainActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); new Thread() { @Override public void run() { Log.d("ThreadTest", "Thread1 start"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Log.d("ThreadTest", "Thread1 end"); } }.start(); new Thread(new Runnable() { @Override public void run() { Log.d("ThreadTest", "Thread2 start"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Log.d("ThreadTest", "Thread2 end"); } }).start(); } private void task() { for (int i = 0; i < 10; i++) { Log.d("Thread", Thread.currentThread().getName() + " 当前i = " + i); } } }
在两个线程中通过sleep()
来模拟 500 毫秒的耗时任务,在任务的开始和结束都打上日志,观察结果如下:
可以看到首先会同时开启两个子线程,然后分别同时执行 500 毫秒的任务,在执行结束打上结束的 Log,可以证明两个 Thread 是同时执行的。
5. 小结
本节学习了一个能让你的 App 并发高效执行任务的方式,多线程可以帮助你提升 App 的整体性能,但用之不当可能会造成一定的资源浪费,所以一定要谨记本节所提到的注意事项。然后按照步骤去创建、运行子线程,了解线程执行的生命周期,让程序更好的为用户服务。