Android小技巧:利用动态代理自动切换线程

日常开发中,多线程编程是个难以避免的话题,开发者可以小心翼翼、谨慎地、严谨地编程来编写出高效的、安全的多线程程序,但是在长时间的维护中,难免因为其中某个人的某个疏忽而导致出现预料之外的并发问题,比如下面这个简单的类:

class TestActivity extends Activity{public void refreshView(){//...}
}

非常常见的一个类,在Android中只有在主线程可以更新界面,可能一开始写的时候并没有考虑其他线程调用refreshView方法。然而随着项目推进,终于有一天,某段其他线程的代码出于方便直接调用了这个方法,恰好编写调用的开发者没有来检查这个方法是否线程敏感。

解决这类问题很简单,发现问题的时候补上线程检查或线程切换的代码就好了,但这样没法避免下次犯同样的错误。建立良好的代码规范、谨慎编写并及时review可以减少这类问题的出现,但现实开发中有时候会比较仓促,需要一种比较强制性的、便利的、安全的做法来处理这类隐患。

以前在学习Actor多线程模型的时候,了解到将Actor和线程相关联是一个简单且安全的方案,但如果像Actor模型中那样Actor之间利用消息通信在处理这个问题时就显得有些麻烦,差不多需要重构模块间通信方式了。

那么有没有一种简单粗暴的方法可以像Actor模型那样使得“某个类的方法只在某个线程中执行”并且在“进入这个类的方法时自动切换到对应线程”呢?。本文就介绍一种利用动态代理来完成这个任务。

示例场景

Android开发中有两种非常常见的线程敏感场景:1. 只有主线程可以更新界面;2. 不能在主线程进行IO操作或复杂长时间计算。

第一种场景比如下面这段(这里的showSum是一个线程敏感方法,必须在主线程调用):

public interface ViewActor {void showSum(int sum);
}class ViewActorImpl implements ViewActor {TextView tvSum;@Overridepublic void showSum(int sum) {tvSum.setText("sum:" + sum);}
}

第二种场景就像下面这段(这里的readFileContent需要在子线程中执行):

public interface WorkerActor {void readFileContent(String path, Consumer<String> consumer);
}class WorkerActorImpl implements WorkerActor {@Overridepublic void readFileContent(String path, Consumer<String> consumer) {String content = FileIOUtils.readFile2String(path);consumer.accept(content);}
}

然而当我们把这些方法标记为public后,这些方法就可能在任意线程执行。

利用代理来确保在正确的线程调用

上面程序都按照“面向接口编程”来写的,这样我们就很容易创建出一个静态代理提供出去,而不是将实现类提供出去,比如:

class ViewActorProxy implements ViewActor {private final ViewActor impl;private final Handler mainHandler;public ViewActorProxy(ViewActor impl) {this.impl = impl;mainHandler = new Handler(Looper.getMainLooper());}@Overridepublic void showSum(int sum) {mainHandler.post(() -> impl.showSum(sum));}
}

但如果这个类有10个需要在主线程执行的方法呢?总不能一个个写mainHandler.post吧,正好Java提供了动态代理,原有程序上,我们可以这样写:

public interface ViewActor {static ViewActor newInstance() {Handler mainHandler = new Handler(Looper.getMainLooper());ViewActorImpl impl = new ViewActorImpl();return (ViewActor) Proxy.newProxyInstance(ViewActor.class.getClassLoader(), new Class[]{ViewActor.class}, (proxy, method, args) -> {mainHandler.post(() -> {try {method.invoke(impl);} catch (IllegalAccessException | InvocationTargetException e) {throw new RuntimeException(e);}});return null;//记得处理equals hashCode等Object方法的调用});}void showSum(int sum);
}class ViewActorImpl implements ViewActor {TextView tvSum;@Overridepublic void showSum(int sum) {tvSum.setText("sum:" + sum);}
}

外部都通过newInstance方法来获取ViewActor实例,这样对ViewActorImpl的调用将被全部切换到主线程中。

注意:实际开发中,更建议使用依赖注入的方式来创建代理,而不是这种直接使用静态方法,这里仅为了方便展示才这么写。

同理,前面的WorkActor的示例就可以改为:

public interface WorkerActor {static WorkerActor getInstance() {ExecutorService executorService = Executors.newCachedThreadPool();WorkerActorImpl impl = new WorkerActorImpl();return (WorkerActor) Proxy.newProxyInstance(WorkerActor.class.getClassLoader(), new Class[]{WorkerActor.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {executorService.submit(() -> method.invoke(impl, args));return null;}});}void readFileContent(String path, Consumer<String> consumer);
}class WorkerActorImpl implements WorkerActor {@Overridepublic void readFileContent(String path, Consumer<String> consumer) {String content = FileIOUtils.readFile2String(path);consumer.accept(content);}
}

虽说上述示例中将创建代理的代码放在对应接口中,但其实可以把这些代码提取出来封装成工具,例如创建主线程对象的代理可以是:

public class MainThreadProxy {public static <T> T newProxyFor(Class<T> clazz, T impl) {Handler mainHandler = new Handler(Looper.getMainLooper());return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {mainHandler.post(() -> {try {method.invoke(impl);} catch (IllegalAccessException | InvocationTargetException e) {throw new RuntimeException(e);}});return null;//记得处理equals hashCode等Object方法的调用});}
}

这样在其他地方就可以通过一行代码直接得到主线程的代理对象:

ViewActor viewActor = MainThreadProxy.newProxyFor(ViewActor.class, new ViewActorImpl());viewActor.showSum(1);//这行代码无论在哪个线程调用,最终都在主线程执行

写在最后

Java的动态代理还是有些局限,比如它只能针对接口来创建代理,为了使用代理,有时候我们需要额外定义一个接口。

第二个局限是动态代理对应的接口方法在线程切换时不能带返回值,如果要传递返回值,需要通过CPS/回调的方式,比如:

public interface WorkerActor {void readFileContent(String path, Consumer<String> consumer);
}

另外,这个方式切换线程默认是以对象为单位的,如果要精确到方法,就需要注解的辅助了,比如:

@ThreadType(ThreadType.IO)
public interface WorkerActor {void readFileContent(String path, Consumer<String> consumer);@AnyThreadvoid enableLog();
}

相应注解的处理实现另外再写文章阐述。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【XCharts插件】5-1、从Json中读取数据并更新图表案例(v3.0)

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址QQ群:398291828大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 XCharts插件是一款基于UGUI的功能强大、易用、参数可配置的数据可视化图表插件。 【Unity3D…

区块链技术与数字货币

1.起源 ➢中本聪(Satoshi Nakamoto), 2008 ➢比特币:一种点对点的电子现金系统 2.分布式账本技术原理 1.两个核心技术&#xff1a; ➢以链式区块组织账本数据实现账本数据的不可篡改 ➢分布式的可信记账机制 2.共识机制&#xff1a;由谁记账 ➢目的&#xff1a; ⚫ 解…

【数据结构(邓俊辉)学习笔记】二叉搜索树03——平衡

文章目录 1. 极端退化2. 平均高度3. 理想 适度4. 歧义 等价5. 等价变换 1. 极端退化 二叉搜索树为我们同时实现对数据集高效的静态操作以及动态操作打开了一扇新的大门。 正如我们所看到的&#xff0c;从策略上&#xff0c;BST可以视作是试图将此前的向量结构以及列表结构优…

SpringBoot整合MongoDB JPA使用

一、整合MongoDB SpringDataMongoDB是 SpringData家族成员之一&#xff0c;MongoDB的持久层框架&#xff0c;底层封装了 mongodb-driver。mongodb-driver 是 MongoDB官方推出的 Java连接 MongoDB的驱动包&#xff0c;相当于JDBC驱动。 SpringBoot整合 MongoDB&#xff0c;引入…

jetson 安装 Rustdesk失败

报错: rustdesk depends on gstreamer1.0-pipewire; however: Package gstreamer1.0-pipewire is not installed. 原因&#xff1a; 对于rustdesk&#xff0c;其1.2.3 版需要gstreamer1.0-pipewire软件包&#xff0c;但是此软件包仅适用于 Ubuntu 22.04、22.10、23.04 和 2…

Python数据分析入门:探索数据集

在数据科学领域&#xff0c;Python以其简洁的语法和强大的库支持&#xff0c;成为最受欢迎的编程语言之一。无论是数据清洗、探索性数据分析还是复杂的机器学习任务&#xff0c;Python都能提供相应的工具。本文将引导你使用Python进行简单的数据分析&#xff0c;以一个公开的数…

C语言 用下面的scanf函数输入数据,使a=3,b=7,x=8.5,y=71.82,c1=‘A’,c2=‘a’。在键盘上应该如何输入?

用下面的scanf函数输入数据&#xff0c;使a3&#xff0c;b7&#xff0c;x8.5&#xff0c;y71.82&#xff0c;c1‘A’,c2‘a’。在键盘上应该如何输入&#xff1f; #include<stdio.h> int main() { int a&#xff0c;b&#xff1b; float x,y; char c1,c2; scanf(“…

k8s_如何修改k8s使用docker或者container作为容器运行时

如果 kubelet.conf 没有明确的容器运行时相关设置&#xff0c;并且你希望配置 Kubernetes 使用 Docker 或 containerd 作为容器运行时&#xff0c;可以通过以下步骤进行配置。具体的配置步骤如下&#xff1a; 配置 Kubernetes 使用 Docker 作为容器运行时 确保 Docker 已安装并…

js中的浅拷贝和深拷贝

浅拷贝Shallow Copy 浅拷贝只复制对象的顶层属性及其引用&#xff0c;而不复制这些引用所指向的对象。如果原始对象中的某个属性是一个对象或数组&#xff0c;那么浅拷贝后的对象将包含对这个内部对象或数组的引用&#xff0c;而不是这个对象或数组的一个新副本。 let obj1 …

【Mac】XnViewMP for Mac(图片浏览查看器)及同类型软件介绍

软件介绍 XnViewMP 是一款多功能、跨平台的图像查看和管理软件&#xff0c;适用于 macOS、Windows 和 Linux 系统。它是经典 XnView 软件的增强版本&#xff0c;更加现代化且功能更强大。XnViewMP 支持数百种图像格式&#xff0c;并提供多种图像处理工具&#xff0c;使其成为摄…

【摄像头标定】使用kalibr进行双目摄像头标定(ros1、ros2)

使用kalibr进行双目摄像头标定 前言标定板标定①板端准备和录制②上位机准备和标定 前言 本文不是纯用ros1进行标定&#xff0c;需要ros1和ros2通信。给使用ros2进行开发&#xff0c;但又想用kalibr标定双目摄像头的小伙伴一个教程。本文双目摄像头的数据发布使用ros2&#xf…

认识Unity中的音效

一、Audio Clip&#xff1a;音频片段 一个AudioClip对象存储了一段声音&#xff0c;可用于播放音效、背景音乐和语音对白等 ambisonic参数指示该音频片段是否是立体混响声 二、Audio Source&#xff1a;音源 用于在场景中播放AudioClip ——相当于发出声音的物体或设备。…

网络安全实战,潜伏与Python反向连接

注意:本文的下载教程,与以下文章的思路有相同点,也有不同点,最终目标只是让读者从多维度去熟练掌握本知识点。 下载教程: Python网络安全项目开发实战_潜伏与Python反向连接_编程案例解析实例详解课程教程.pdf 在网络安全领域,潜伏与反向连接技术常被黑客用于绕过防火墙和…

收银系统源码-千呼新零售2.0【线上营销】

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货等连锁店使用。 详细介绍请查看&a…

Js逆向爬虫基础篇

这里写自定义目录标题 逆向技巧断点一 、请求入口定位1. 关键字搜索2. 请求堆栈3. hook4. JSON.stringify 二、响应入口定位&#xff1a;1. 关键字搜索2. hook3. JSON.parse 逆向技巧 断点 普通断点 条件断点 日志断点 XHR断点 一 、请求入口定位 1. 关键字搜索 key关…

办公软件的答案?ONLYOFFICE 桌面应用编辑器会是最好用的 Office 软件?ONLYOFFICE 桌面编辑器使用初体验

文章目录 &#x1f4cb;前言&#x1f3af;什么是 ONLYOFFICE&#x1f3af; 主要功能介绍及 8.1 新功能体验&#x1f3af; 在线体验&#x1f4dd;最后 &#x1f4cb;前言 提到办公软件&#xff0c;大家最常用的可能就是微软的 Microsoft Office 和国产的 WPS Office。这两款软件…

jenkins环境搭建--关于jenkins在Ubuntu下的安装篇(一)

在ubuntu下使用命令进行下载安装包&#xff1a; 关于jenkins的安装有多种&#xff0c;可以借助docker容器进行安装&#xff0c;也可以通过传统方法手动一步步的进行安装&#xff0c;以下介绍手动一步步的安装方法&#xff0c;后续我们将解释关于jenkins的相关配置以及实战使用…

【系统架构师】-论文-微服务设计

1、摘要: 2017年10月&#xff0c;我被任命为系统架构师参与了XXX 运营商AOP 系统架构升级项目&#xff0c;负责架构设计工作&#xff0c;该系统是运营商面向互联网销售产品的系统&#xff0c;自从年中上线流量包订购业务以来&#xff0c;系统订单量飞速上涨&#xff0c;月末订单…

Pytorch-ResNet-50 网络表情识别项目(深度学习)

ResNet-50 网络表情识别 1. 导入依赖库2. 加载中文字体文件3. 设置图像尺寸和训练参数4. 数据增强和预处理5. 加载数据集6. 检查数据维度7. 定义ResNet50模型8. 初始化模型、损失函数和优化器9. 训练和测试函数10. 训练和测试模型11. 保存模型12. 评估数据保存和可视化 原码 本…

欧盟指控苹果应用商店规则非法压制竞争,面临巨额罚款风险

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…