59. 【Android教程】多线程

简介: 59. 【Android教程】多线程

多线程可以让你同时异步执行多种任务,是各种编程语言里很重要的一个概念。合理的采用多线程可以让你的 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 线程的创建

我们有两种方式进行线程的创建

  1. 继承Thread实现一个线程类:
class TestThread extends Thread {
    @Override
    public void run() {
        Log.d("Threading", "继承 Thread 的线程:"+Thread.currentThread().getName());
    }
}
  1. 实现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 的整体性能,但用之不当可能会造成一定的资源浪费,所以一定要谨记本节所提到的注意事项。然后按照步骤去创建、运行子线程,了解线程执行的生命周期,让程序更好的为用户服务。

相关文章
|
1月前
|
网络协议 Android开发 数据安全/隐私保护
Android手机上使用Socks5全局代理-教程+软件
Android手机上使用Socks5全局代理-教程+软件
554 2
|
19天前
|
Java Android开发
Android面试题经典之Glide取消加载以及线程池优化
Glide通过生命周期管理在`onStop`时暂停请求,`onDestroy`时取消请求,减少资源浪费。在`EngineJob`和`DecodeJob`中使用`cancel`方法标记任务并中断数据获取。当网络请求被取消时,`HttpUrlFetcher`的`cancel`方法设置标志,之后的数据获取会返回`null`,中断加载流程。Glide还使用定制的线程池,如AnimationExecutor、diskCacheExecutor、sourceExecutor和newUnlimitedSourceExecutor,其中某些禁止网络访问,并根据CPU核心数动态调整线程数。
36 2
|
10天前
|
Java
线程池和线程详细教程
线程池和线程详细教程
|
1月前
|
存储 编解码 Android开发
58. 【Android教程】音频录制:MediaRecord
58. 【Android教程】音频录制:MediaRecord
21 2
|
1月前
|
Android开发
杨老师课堂_安卓教程第一篇之入门
杨老师课堂_安卓教程第一篇之入门
18 0
|
1月前
|
存储 Java 调度
Android面试题之Kotlin协程到底是什么?它是线程吗?
本文探讨了协程与线程的区别,指出协程并非线程,而是轻量级的线程替代。协程轻量体现在它们共享调用栈,内存占用少,仅需几个KB。协程切换发生在用户态,避免了昂贵的内核态切换。在Kotlin中,协程通过Continuation对象实现上下文保存,允许高效并发执行,而不会像线程那样消耗大量资源。通过`runBlocking`和`launch`示例展示了协程的非阻塞挂起特性。总结来说,协程的轻量主要源于内存占用少、切换开销低和高并发能力。
20 0
|
1月前
|
API Android开发
57. 【Android教程】相机:Camera
57. 【Android教程】相机:Camera
40 0
|
1月前
|
API Android开发 UED
56. 【Android教程】媒体播放器:MediaPlayer
56. 【Android教程】媒体播放器:MediaPlayer
32 0
|
1月前
|
API Android开发
55. 【Android教程】位图:Bitmap
55. 【Android教程】位图:Bitmap
18 0