使用 promise 重构 Android 异步代码

背景

业务当中写Android异步任务一直是一项挑战,以往的回调和线程管理方式比较复杂和繁琐,造成代码难以维护和阅读。在前端领域中JavaScript其实也面临同样的问题,Promise 就是它的比较主流的一种解法。 在尝试使用Promise之前我们也针对Android现有的一些异步做了详细的对比。

文章思维导图

image.png

What:什么是Promise?

对于Android开发的同学,可能很多人不太熟悉Promise,它主要是前端的实践,所以先解析概念。
Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。

最简单例子(JavaScript)

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
}).then(function(value) {
    console.log('resolved.');
}).catch(function(error) {
    console.log('发生错误!', error);
});

实例化一个Promise对象,构造函数接受一个函数作为参数,该参数分别是resolvereject
resolve函数:将Promise 对象状态从pending 变成 resolved
reject函数:将Promise 对象状态从 pending 变成 rejected
then函数:回调 resolved状态的结果
catch函数:回调 rejected状态的结果

可以看到Promise的状态是非常简单且清晰的,这也让它在实现异步编程减少很多认知负担。

Why:为什么要考虑引入Promise

前面说的Promise 不就是 JavaScript 异步编程的一种思想吗,那这跟 Android 开发有什么关系? 虽然前端终端领域有所不同,但面临的问题其实是大同小异的,比如常见的异步回调导致回调地狱,逻辑处理不连贯等问题。
从事Android开发的同学应该对以下异步编程场景比较熟悉:

  • 单个网络请求
  • 多个网络请求竞速
  • 等待多个异步任务返回结果
  • 异步任务回调
  • 超时处理
  • 定时轮询

这里可以停顿思考一下,如果利用 Android常规的方式去实现以上场景,你会怎么做?你的脑子可能有以下解决方案:

  • 使用 Thread 创建
  • 使用 Thread + Looper + Handler
  • 使用 Android 原生 AsyncTask
  • 使用 HandlerThread
  • 使用 IntentService
  • 使用 线程池
  • 使用 RxJava 框架

以上方案都能在Android中实现异步任务处理,但或多或少存在一些问题和适用场景,我们详细剖析下各自的优缺点:
image.png
通过不同的异步实现方式的对比,可以发现每种实现方式都有适用场景,我们面对业务复杂度也是不一样的,每一种解决方案都是为了降低业务复杂度,用更低成本的方式来编码,但我们也知道代码写出来是给人看的,是需要持续迭代和维护,类似RxJava 这种框架于我们而言太复杂了,繁琐的操作符容易写出不易维护的代码,简单易理解应该是更好的追求,而不是炫技,所以我们才会探索用更轻量更简洁的编码方式来提升团队的代码一致性,就目前而言使用 Promise 来写代码将会有以下好处:

  • 解决回调地狱:Promise 可以把一层层嵌套的 callback 变成  .then().then()... ,从而使代码编写和阅读更直观
  • 易于处理错误:Promise 比 callback 在错误处理上更清晰直观
  • 非常容易编写多个异步操作的代码

How:怎么使用 Promise 重构业务代码?

这里由于我们的Java版本的Promise组件未开源,所以本部分只分析重构Case使用案例。

重构case1: 如何实现一个带超时的网络接口请求?

这是一段未重构前的获取付款码的异步代码:


可以看到以上代码存在以下问题:

  • 需要定义异步回调接口
  • 很多 if-else 判断,圈复杂度较高
  • 业务实现了一个超时类,为了不受网络库默认超时影响
  • 逻辑不够连贯,不易于维护

使用 Promise重构后:


可以看到有以下变化:

  • 消除了异步回调接口,链式调用让逻辑更连贯更清晰了
  • 通过 Promise 包装了网络请求调用,统一返回 Promise
  • 指定了 Promise 超时时间,无需额外实现繁琐的超时逻辑
  • 通过 validate 方法 替代 if - else 的判断,如果需要还可以定义校验规则
  • 统一处理异常错误,逻辑变得更加完备

重构case2:如何更优雅的实现长链接降级短链接?

重构前的做法:


代码存在以下问题:

  • 处理长链接请求超时,通过回调再处理降级逻辑
  • 使用Handler实现定时器轮询请求异步结果并处理回调
  • 处理各种逻辑判断,代码难以维护
  • 不易于模拟超时降级,代码可测试性差

使用Promise重构后:


第一个Promise处理长链接Push监听 ,设置5s超时,超时异常发生回调except方法,判断throwable 类型,如果为PromiseTimeoutException实例对象,则执行降级短链接。短链接是另外一个Promise,通过这种方式将逻辑都完全结果,代码不会割裂,逻辑更连贯。
短链接轮训查单逻辑使用Promise实现:

  • 最外层Promise,控制整体的超时,即不管轮询的结果如何,超过限定时间直接给定失败结果
  • Promise.delay(),这个比较细节,我们认定500ms轮询一定不会返回结果,则通过延迟的方式来减少一次轮询请求
  • Promise.retry(),真正重试的逻辑,限定了最多重试次数和延时逻辑,RetryStrategy定义的是重试的策略,延迟(delay)多少和满足怎样的条件(condition)才允许重试

这段代码把复杂的延时、条件判断、重试策略都通过Promise这个框架实现了,少了很多临时变量,代码量更少,逻辑更清晰。

重构case3:实现 iLink Push支付消息和短链接轮训查单竞速

后面针对降级策略重构成竞速模型,采用Promise.any很轻松得实现代码重构,代码如下图所示。

总结

本文提供一种异步编程的思路,借鉴了Promise思想来重构了Android的异步代码。通过Promise组件提供的多种并发模型能够更优雅的解决绝大部分的场景需求。

防踩坑指南

如果跟Activity或Fragment生命周期绑定,需要在生命周期结束时,取消掉promise的线程运行,否则可能会有内存泄露;这里可以采用AbortController来实现更优雅的中断 Promise。

并发模型

● 多任务并行请求
Promise.all():接受任意个Promise对象,并发执行异步任务。全部任务成功,有一个失败则视为整体失败。
Promise.allSettled(): 任务优先,所有任务必须执行完毕,永远不会进入失败状态。
Promise.any():接受任意个Promise对象,并发执行异步任务。等待其中一个成功即为成功,全部任务失败则进入错误状态,输出错误列表。
● 多任务竞速场景
Promise.race(): 接受任意个Promise对象,并发执行异步任务。时间是第一优先级,多个任务以最先返回的那个结果为准,此结果成功即为整体成功,失败则为整体失败。

扩展思考

  1. Promise 最佳实践
  1. 避免过长的链式调用:虽然Promise可以通过链式调用来避免回调地狱,但是如果Promise的链过长,代码的可读性和维护性也会变差。
  2. 及时针对Promise进行abort操作:Promise使用不当可能会造成内存泄露,比如未调用abort,页面取消未及时销毁proimse。
  3. 需要处理except异常回调,处理PromiseException.
  4. 可以使用validation来实现规则校验,减少if-else的规则判断
  1. Java Promise 组件实现原理
  1. 状态机实现(pending、fulfilled、rejected)
  2. 默认使用 ForkJoinPool 线程池,适合计算密集型任务。针对阻塞IO类型,可以使用内置ThreadPerTaskExecutor 简单线程池模型。
  1. Promise vs Kotlin协程

Promise 链式调用,代码清晰,上手成本较低;底层实现仍然是线程,通过线程池管理线程调度
Koitlin 协程,更轻量的线程,使用比较灵活,可以由开发者控制,比如挂起和恢复
刷掌业务相对比较简单,轻量的操作比较少,所以使用基本的线程池就能满足需求,如果需要频繁创建线程和切换,可以考虑使用协程来减少线程池的开销。

  1. 可测试性的思考

根据 Promise 的特点,可以通过Mock状态(resolve、reject、outTime)来实现模拟成功,拒绝、超时;
实现思路:
● 自定义注解类辅助定位Hook点
● 使用ASM字节码对Promise 进行代码插桩

附录

● Promise - JavaScript | MDN
● Promises/A+

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/125117.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【BUG解决】服务器没报警但是应用接口崩了....

最近遇到一个突发问题:服务器没报警但是应用接口崩了… 为其他业务系统提供一个接口,平时好好的,突然就嚷嚷反馈说访问不了了,吓得我赶紧跳起来! 正常情况下在系统崩溃前,我会收到很多系统报警&#xff0…

【Linux】补充:进程管理之手动控制进程,以及计划任务

目录 一、手动启动进程 1、理解前台启动与后台启动 2、如何完成前台启动后台启动的切换 3、完成并行执行多个任务 4、结束进程 1、kill 2、killall 2、pkill 二、计划任务 1、at一次性计划任务 2、实操 2、周期性计划任务 1、关于设置周期性任务的配置文件以及格式…

使用ffmpeg调用电脑自带的摄像头和扬声器录制音视频

1、打开cmd,执行chcp 65001,修改cmd的编码格式为utf8,避免乱码 2、执行指令ffmpeg -list_devices true -f dshow -i dummy,查看当前window的音频和视频名称 3、打开windows系统的"打开声音设置"–“麦克风隐私设置”–"允许应用访问你…

技术分享 | 测试平台开发-前端开发之数据展示与分析

测试平台的数据展示与分析,我们主要使用开源工具ECharts来进行数据的展示与分析。 ECharts简介与安装 ECharts是一款基于JavaScript的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表&#xff…

第七章《搞懂算法:线性回归是怎么回事》笔记

线性回归算法是机器学习算法中最简单的一类,线性回归算法主要用于连续值的预测问题。 7.1 什么是线性回归 这种刻画了不同变量之间关系的模型叫作回归模型,如果这个模型是线性的,则为线性回归模型。 线性回归主要是应用回归分析来确定两种…

EfficientNet 系列网络学习

EfficientNet V1 EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks 增加网络参数的方式有三种:深度、宽度和输入图像的分辨率。探究这三种方式对网络性能的影响,以及如何同时缩放这三种因素是 EifficentNet的主要贡献。 单独…

Centos7开放及查看端口

1、开放端口 firewall-cmd --zonepublic --add-port8888/tcp --permanent # 开放8888端口 firewall-cmd --zonepublic --remove-port8888/tcp --permanent #关闭8888端口 firewall-cmd --reload # 配置立即生效 2、查看防火墙所有开放的端口 firewall-cmd --zonepubl…

什么是数字化管理?产业园区如何进行数字化管理?

工业园区的数字化管理涉及利用技术和数据驱动的工具来优化工业园区环境中的运营、提高效率并改进决策流程。它通常包括使用各种数字技术和数据分析技术来监视、控制和增强公园运营的各个方面。 以下是工业园区数字化管理的一些关键方面以及如何实施: 1.数据收集和…

初识Java 17-4 反射

本笔记参考自: 《On Java 中文版》 接口和类型信息 interface关键字的一个重要目标就是允许程序员隔离组件,减少耦合。但我们可以通过类型信息来绕过接口的隔离,这使得接口不一定能够保证解耦。 为了演示这一实现,我们需要先创建一…

C/C++轻量级并发TCP服务器框架Zinx-游戏服务器开发005:守护进程与进程监控

文章目录 1 守护进程1.1 进程组和会话1.2 会话的相关概念1.3 守护进程的概念1.4 守护线程的特点1.5 守护进程创建的基本步骤1.6 本项目守护进程的实现 2 进程监控2.1 进程监控的实现 1 守护进程 1.1 进程组和会话 进程除了有进程的PID之外还有一个进程组,进程组是…

threejs (二) 相机

正交相机 const camera new THREE.OrthographicCamera(-aspect,aspect,aspect,-aspect,0.1, //进平面1000 //远平面); // 透视相机创建相机辅助线 const cameraHelper new THREE.CameraHelper(this.camera);创建一个透视相机观察正交相机 // 创建透视相机const watchCamera …

【算法与数据结构】39、LeetCode组合总和

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析:这道题当中数字可以多次使用,那么我们在递归语句当中不能直接找下一个candidate的元素&…

两台linux虚拟机之间实现免密登录

主要实现两台虚拟机之间的免密登录,总所周知,虚拟机之间登录使用的协议是ssh协议,端口号是 22 主机 创建对应的加密文件 [rootweb-2 ~]# ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/root/.s…

docker容器中运行jar 出现invalid or corrupt jarfile

1,背景: 在本地java开发完毕之后,想要打包成docker镜像,方便安装。由于本地没有docker环境,也懒得装了。有一台测试的linux机器可以使用,所以先在本地打包生成xxx.jar,然后拷贝到有docker环境的…

vite + electron引入itk报错

代码 import { readImageArrayBuffer } from itk-wasm console.log(readImageArrayBuffer)通过itk-wasm官网,创建新的项目vitevue(vue2或者vue3),都没问题。加入electeon后包此错。通过排查,意外找到原因,…

抵御数字威胁的铠甲——发现迅软DSE加密软件在企业保护中的关键角色

目前国内有自主知识产权和研发成果的企业,它们的电子文档大都以明文的方式存储在计算机硬盘中,电子格式存储的重要机密信息却由于传播的便利性和快捷性,对分发出去的文档无法控制,大的增加了管理的复杂程度,这部分信息…

Swift--量值与基本数据类型

系列文章目录 第一章: Swift–量值与基本数据类型 文章目录 系列文章目录前言对学习过程做一个记录 变量和常量命名规范注释 元祖类型可选类型拆包 typealias 前言 对学习过程做一个记录 提示:以下是本篇文章正文内容,下面案例可供参考 变量和常量 …

家用工作站方案:ThinkBook 14 2023 版

本篇文章聊聊今年双十一,我新购置的家用工作站设备:ThinkBook 14 2023,一台五千元价位,没有显卡的笔记本。我为什么选择它,它又能做些什么。 写在前面 2021 年年中的时候,我写过一篇《廉价的家用工作站方…

开源知识库软件xwiki在Windows下的安装

文章目录 开源知识库软件-xwiki在windows上的部署0、参考文档1、前置环境准备1.1、Windows版本及系统配置1.2、JDK11安装1.3、Tomcat9安装1.4、MySQL5.7数据库的安装 2、xwiki安装3、配置3.1、修改配置支持对文档内容进行搜索 4、问题解决4.1、附件无法上传问题4.1、附件无法下…

【309. 买卖股票的最佳时机含冷冻期】

目录 一、题目解析 二、算法原理 三、代码实现 class Solution { public:int maxProfit(vector<int>& prices) {int nprices.size();vector<vector<int>> dp(n,vector<int>(3));dp[0][0]-prices[0];dp[0][1]0;dp[0][2]0;for(int i1;i<n;i){dp…