Android NDK开发详解后台任务之使用 Java 线程异步工作

异步后台处理

除了持久性工作之外,异步工作是后台工作的第二个组成部分。虽然持久性工作和异步工作均在后台进行,但它们最终截然不同。

异步工作是指:

就在当下。
在应用重启或设备重启后,无需保留。
发生在主线程以外,或阻塞主线程。

这与持久性工作相反,后者可以为将来执行,并且仍然通过应用重启和设备重新启动来调度。例如,异步工作可能在主线程之外发送 HTTP 请求,仅在请求到达时才返回其结果。

Java 和 Kotlin

处理异步工作的方式取决于您遵循的整体应用架构。如果您使用的是 Java 编程语言应用,则您的需求与使用 Kotlin 时有所不同。
在这里插入图片描述

使用 Java 线程异步工作

所有 Android 应用都使用主线程来处理界面操作。从此主线程调用长时间运行的操作可能会导致卡顿和无响应。例如,如果您的应用从主线程发出网络请求,则应用的界面会冻结,直到收到网络响应。如果您使用 Java,则可以创建额外的后台线程来处理长时间运行的操作,同时主线程继续处理界面更新。

本指南介绍了使用 Java 编程语言的开发者如何使用线程池在 Android 应用中设置和使用多个线程。本指南还介绍了如何定义要在线程上运行的代码,以及如何在其中一个线程与主线程之间进行通信。
重要提示 :如果您使用 Kotlin 编写应用,我们建议您将协程用作异步后台工作的轻量级解决方案。协程包括结构化并发、内置取消支持、Jetpack 集成等功能。

并发库

请务必了解线程及其底层机制的基础知识。不过,有许多热门库提供对这些概念的更高级别抽象,以及用于在线程之间传递数据的现成实用程序。这些库包括面向 Java 编程语言用户的 Guava 和 RxJava,以及我们建议 Kotlin 用户的协程。

在实践中,尽管线程规则保持不变,但您还是应该选择最适合您的应用和开发团队的 ViewModel。

示例概览

根据应用架构指南,本主题中的示例会发出网络请求并将结果返回到主线程,然后应用可能会在主线程上显示该结果。

具体而言,ViewModel 会在主线程上调用数据层,以触发网络请求。数据层负责将网络请求的执行移出主线程,并使用回调将结果发布回主线程。

为了将网络请求的执行任务移出主线程,我们需要在应用中创建其他线程。

创建多个线程

线程池是从队列并行运行任务的托管线程集合。当现有线程变为空闲状态时,新任务会在这些线程上执行。如需将任务发送到线程池,请使用 ExecutorService 接口。请注意,ExecutorService 与 Service(Android 应用组件)无关。

创建线程的成本很高,因此您应该仅在应用初始化时创建一次线程池。请务必将 ExecutorService 的实例保存在 Application 类或依赖项注入容器中。以下示例创建了一个包含四个线程的线程池,我们可以使用它来运行后台任务。

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

您还可以通过其他方式配置线程池,具体取决于预期的工作负载。如需了解详情,请参阅配置线程池。

在后台线程中执行

在主线程上发出网络请求会导致该线程处于等待或阻塞状态,直到收到响应。由于线程处于阻塞状态,因此操作系统无法调用 onDraw(),并且应用会冻结,这可能会导致出现“应用无响应”(ANR) 对话框。我们改为在后台线程上运行此操作。

发出请求

首先,我们来了解一下 LoginRepository 类,看看它是如何发出网络请求的:


// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

makeLoginRequest() 是同步的,并且会阻塞发起调用的线程。为了对网络请求的响应建模,我们创建了自己的 Result 类。

触发请求

ViewModel 会在用户点按(例如点按某个按钮)时触发网络请求:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

使用上述代码,LoginViewModel 会在发出网络请求时阻塞主线程。我们可以使用已实例化的线程池将执行移至后台线程。

处理依赖项注入

首先,遵循依赖项注入的原则,LoginRepository 接受 Executor(而不是 ExecutorService)的实例,因为它会执行代码而不是管理线程:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

执行器的 execute() 方法接受 Runnable。Runnable 是一个单一抽象方法 (SAM) 接口,带有调用时在线程中执行的 run() 方法。

在后台执行

我们再创建一个名为 makeLoginRequest() 的函数,该函数会将执行任务移至后台线程,并暂时忽略响应:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

在 execute() 方法内,我们使用要在后台线程(在本例中为同步网络请求方法)执行的代码块创建一个新的 Runnable。在内部,ExecutorService 管理 Runnable,并在可用线程中执行它。
注意 :在 Kotlin 中,您可以使用 lambda 表达式创建实现 SAM 接口的匿名类。

注意事项

应用中的任何线程都可以与其他线程(包括主线程)并行运行,因此您应确保代码具有线程安全性。请注意,在我们的示例中,我们避免写入线程之间共享的变量,而是传递不可变数据。这是一种很好的做法,因为每个线程都处理自己的数据实例,并且可以避免同步的复杂性。

如果您需要在线程之间共享状态,则必须小心使用同步机制(如锁)管理线程的访问。这不在本指南的讨论范围内。一般来说,您应尽可能避免在线程之间共享可变状态。

与主线程通信

在上一步中,我们忽略了网络请求响应。为了在屏幕上显示结果,LoginViewModel 需要知道结果。我们可以通过使用回调来实现此目的。

函数 makeLoginRequest() 应将回调作为参数,以便可以异步返回值。每当网络请求完成或发生失败时,系统都会调用包含结果的回调。在 Kotlin 中,我们可以使用高阶函数。不过,在 Java 中,我们必须创建一个新的回调接口,以实现相同的功能:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

ViewModel 现在需要实现回调。它可以根据结果执行不同的逻辑:

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

在此示例中,回调在发起调用的线程中执行,该线程是一个后台线程。这意味着,在切换回主线程之前,您无法直接与界面层进行修改或通信。
注意 :如需与 ViewModel 层中的 View 通信,请按照应用架构指南中的建议,使用 LiveData。如果代码在后台线程上执行,您可以调用 MutableLiveData.postValue() 与界面层进行通信。

使用处理程序

您可以使用 Handler 将要在其他线程上执行的操作加入队列。如需指定要在哪个线程上运行操作,请使用该线程的 Looper 构造 Handler。Looper 是为关联的线程运行消息循环的对象。创建 Handler 后,您可以使用 post(Runnable) 方法在相应的线程中运行代码块。

Looper 包含一个辅助函数 getMainLooper(),该函数可以检索主线程的 Looper。您可以使用此 Looper 创建 Handler,以在主线程中运行代码。由于这是您可能经常执行的操作,因此您也可以将 Handler 的实例保存在保存 ExecutorService 的同一位置:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

将处理程序注入代码库是一种很好的做法,因为这样可为您提供更高的灵活性。例如,将来您可能需要传入不同的 Handler,以在单独的线程上调度任务。如果您始终与同一线程通信,则可以将 Handler 传入代码库构造函数,如以下示例所示。

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

或者,如果您希望提高灵活性,则可以向每个函数传入 Handler:

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

在此示例中,传递到代码库的 makeLoginRequest 调用的回调在主线程上执行。这意味着,您可以直接通过回调修改界面,也可以使用 LiveData.setValue() 与界面进行通信。

配置线程池

您可以使用某个具有预定义设置的 Executor 辅助函数来创建线程池,如前面的示例代码所示。或者,如果要自定义线程池的详细信息,可以直接使用 ThreadPoolExecutor 创建实例。您可以配置以下详细信息:

初始池大小和最大池大小。
保持活跃的时间和时间单位。保持活跃时间是指线程在关闭之前可以保持空闲状态的最长时间。
包含 Runnable 任务的输入队列。此队列必须实现 BlockingQueue 接口。为了符合应用的要求,您可以从可用的队列实现中进行选择。如需了解详情,请参阅 ThreadPoolExecutor 的类概览。

下面这个示例根据处理器核心总数指定了线程池大小,并指定了保持活动状态 1 秒的线程数以及输入队列。

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2023-10-31。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值