注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

装了我这 10 个 IDEA 神级插件后,同事也开始情不自禁的嘚瑟了

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。 Vuesion Theme 颜值是生产...
继续阅读 »

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。


Vuesion Theme


颜值是生产力的第一要素,IDE 整好看了,每天对着它也是神清气爽,有木有?就 Intellij IDEA 提供的暗黑和亮白主色,虽然说已经非常清爽了,但时间久了总觉得需要再来点新鲜感?


Vuesion Theme 这个主题装上后,你会感觉整个 Intellij IDEA 更高级了。



安装完插件就立马生效了,瞧这该死的漂亮,整个代码着色,以及文件的图标,都更炫酷了:



当然了,主题这事,萝卜白菜各有所爱,就像玩 dota,我就喜欢露娜。


lombok


可能提到 lombok,多多少少有些争议,但不得不说,这玩意的确是很能省代码,并且很多开源的第三方 jar 包,以及 Intellij IDEA 2020.3 以后的版本也都默认加了 lombok。



这么多注解可以选择,在写 VO、DO、DTO 的时候是真的省心省力。



如果没有 lombok 的帮助,那整个代码就要炸了呀。对比一下,是不是感受还挺明显的?



当然了,要使用 lombok,你得在 pom.xml 文件中引入 lombok 的依赖包。


xml
复制代码
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

File Expander


这个插件不仅可以反编译,还可以打开 tar.gz,zip 等压缩文件,



如果有小伙伴反驳说自己不装插件也可以打开 jar 包里的代码,那是因为你的 jar 在 classpath。如果单独打开一个 jar 包,不装插件是看不了的。



GitToolBox


如果你经常使用 Git 提交代码的话,这款插件就非常的爽。



它能直接提示你远程版本库里有多少文件更新,你有多少文件没有提交到版本库,甚至可以显示上一次提交的时间和版本更新者。



Maven Helper


这插件几乎人手一个了吧,Java 后端开发必备啊。



依赖可视化的神器,可以很清楚地知道依赖的关系图谱,假如有冲突的话,也是一目了然。



Translation


对于英文能力差的同学来说,这个翻译插件简直神了,它支持 Google 翻译、有道翻译、百度翻译、Alibaba 翻译。



刚好写这篇内容的时候,发现最新的版本是 3.3.5,趁机升级一波。有了这款翻译插件,看源码绝对是爽歪歪。以前遇到不认识的单词,真的是好烦,还要切到翻译软件那里查,现在可好,单词翻译、文档翻译、注释翻译,都有了。



arthas idea


Arthas 应该大家都很熟悉了,阿里开源的一款强大的 java 在线诊断工具。


但如果每次都要你输入一长串命令的话,相信你也会很崩溃,尤其是很多时候我还记忆模糊,很多记不住。这款插件刚好解决了我这个烦恼,极大地提高了生产力



使用起来也非常方便,直接进入你要诊断的方法和类,右键选择对应的命令,就会自动帮你生成了。



Free Mybatis plugin


Mybatis 基本上是目前最主流的 ORM 框架了,相比于 hibernate 更加灵活,性能也更好。所以我们一般在 Spring Boot 项目中都会写对应的 mapper.java 和 mapper.xml。


那有了这款插件之后,两者就可以轻松关联起来。



比如,我这里要查看 ArticleMapper 的 xml,那么编辑器的行号右侧就会有一个向右的→,直接点击就跳转过去了。



想跳转回来的话,也是同样的道理,所以有了这款产检,mapper 和 xml 之间就可以自由切换了,丝滑。


VisualGC


这里给大家推荐一个 JVM 堆栈可视化工具,可以和 Intellij IDEA 深度集成——VisualGC。



当我们需要监控一个进程的时候,直接打开 VisualGC面板,就可以查看到堆栈和垃圾收集情况,可以说是一目了然。



CheckStyle-IDEA


如果你比较追求代码规范的话,可以安装这个插件,它会提醒你注意无用导入、注释、语法错误❎、代码冗余等等。



在 CheckStyle 面板中,你可以选择 Google 代码规范或者 sun 的代码规范,跑一遍检查,就可以看到所有的修改建议了。



最后


以上这 10 款 Intellij IDEA 插件也是我平常开发中经常用到的,如果大家有更好更效率的插件,也可以评论里留言。


参考链接:juejin.cn/post/702802…


没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。


本文已收录到 GitHub 上星标 4k+ 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路😄。


Github 仓库:github.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



作者:沉默王二
链接:https://juejin.cn/post/7161603780574707720
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么选择FastAPI?

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。 一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数...
继续阅读 »

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。



一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数据库 ORM 支持,功能完善、要素齐全:自带大量常用工具和框架(比如分页,auth,权限管理), 适合快速开发企业级网站。完善的文档:经过多年的发展和完善,Django 有广泛的实践案例和完善的在线文档,可以让开发人员有好的借鉴途径。并且使用众多,能够借鉴的例子也很多。目前 NetDevOps 的平台后端,大多数都是基于 Python 的 Django 来开发的,原因是:python 的确上手快,django 支持完善,能够迅速的建立和设备的联系并生成自带的管理页面。


但是Django太全了太重了,随着开发周期越往后,开发模块越来越多,整个程序就显得异常臃肿,代码逻辑相互耦合十分严重,整个系统需要保持极高的稳定性。所以,我们希望能够有个小而精的框架,可以逐步的将Django项目中的模块慢慢做拆分,做解耦,做程序的可插拔。FastAPI这时候进入到我们的视野。



FastAPI 顾名思义,一个字,就是快。基于(并完全兼容)OPenAPI 的相关开放标准。



  • 高性能:FastAPI 采用异步编程模式,基于 Starlette 框架和 pydantic 库进行开发。其性能相比于 Flask 和 Django 均有很大提升。

  • 简单易用:FastAPI 提供了自动生成 API 文档的功能,丰富的文档可以让开发人员更快速地了解 API 的使用方法。

  • 规范化:FastAPI 默认支持 OpenAPI(前身为 Swagger)和 JSON Schema,从而规范化 API 的设计和发布。

  • 类型检查:FastAPI 强制使用类型注解,使得代码更加严谨,同时可以使用 mypy 等类型检查工具来保证代码的质量。

  • 整合多种数据库支持:FastAPI 可以无缝进行整合多种数据库的使用,比如 SQLAlchemy、Tortoise ORM 等。


使用 FastAPI 可以提升性能、简化开发、规范 API 设计、增加代码可读性和可维护性,从而促进开发效率的提升。 FastAPI 站在以下巨人的肩膀之上:


Starlette 负责 web 部分。 Pydantic 负责数据部分。


Starlette


Starlette 是一个轻量级的 ASGI 框架和工具包,特别适合用来构建高性能的 asyncio 服务.


Starlette 的主要特性:



  1. 性能表现优异

  2. WebSocket 支持.

  3. GraphQL 支持.

  4. 进程内的后台任务执行

  5. 启动和关闭服务的事件触发

  6. 测试客户端构建于 requests.

  7. 支持 CORS, GZip, Static Files, Streaming 响应.

  8. 支持会话和 Cookie

  9. 100% 测试覆盖率

  10. 100% 类型注解

  11. 无依赖


Pydantic


pydantic 库是 python 中用于数据接口定义检查与设置管理的库。


pydantic 在运行时强制执行类型提示,并在数据无效时提供友好的错误。


它具有如下优点:



  1. 与 IDE/linter 完美搭配,不需要学习新的模式,只是使用类型注解定义类的实例

  2. 多用途,BaseSettings 既可以验证请求数据,也可以从环境变量中读取系统设置

  3. 快速

  4. 可以验证复杂结构

  5. 可扩展,可以使用 validator 装饰器装饰的模型上的方法来扩展验证

  6. 数据类集成,除了 BaseModel,pydantic 还提供了一个 dataclass 装饰器,它创建带有输入数据解析和验证的普通 Python 数据类。


FastAPI 推荐使用 uvicorn 启动服务



Uvicorn 是基于 uvloop 和 httptools 构建的非常快速的 ASGI 服务器。


uvicorn 是一个基于 asyncio 开发的一个轻量级高效的 web 服务器框架


uvicorn 设计的初衷是想要实现两个目标:


使用 uvloop 和 httptools 实现一个极速的 asyncio 服务器


实现一个基于 ASGI(异步服务器网关接口)的最小应用程序接口。


它目前支持 http, websockets, Pub/Sub 广播,并且可以扩展到其他协议和消息类型。


uvloop 用于替换标准库 asyncio 中的事件循环,使用 Cython 实现,它非常快,可以使 asyncio 的速度提高 2-4 倍。asyncio 不用我介绍吧,写异步代码离不开它。


httptools 是 nodejs HTTP 解析器的 Python 实现。


综述


综上所述,我们推荐使用 FastAPI 来进行快速开发和程序构建,尤其是功能较为简单的应用,它也支持数据结构校验和异步。提高代码执行效率和开发效率。同时具备丰富的数据支持,基于 OpenAPI 的自助交互式文档,让开发过程中的调试也十分方便。快速敏捷开发是我们的追求。



  • 快速开发:提供基础框架和默认设置,使开发人员可以快速构建 API,并自动生成文档和测试样例。

  • 高性能:基于 Python 3.6+、ASGI 和 asyncio 等技术,提供高性能特性,因此在高流量和低延迟的应用场景下比其他框架有更好的响应速度和性能。

  • 文档自动生成:内置文档生成器,自动解析函数参数和返回值来生成规范化的文档,并支持 OpenAPI 和 JSON Schema 的标准格式。

  • 强类型支持:采用类型注解,提高代码的质量和可读性,减少出现运行时错误的可能性。

  • 多种数据库支持:支持多种数据库,包括 SQLAlchemy、Tortoise ORM 等,可以方便地处理数据库操作。


FastAPI 是一款快速、高性能、安全、易用、规范化的 Web 应用框架,能够极大地提高开发效率,保证应用的高性能和可靠性。


作者:一个有理想的工程师
链接:https://juejin.cn/post/7225424380468953145
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Socket学习网络基础

1.OSI七层网络模型浅析 当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了! OSI七层网络模型(从下往上) : 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传...
继续阅读 »

1.OSI七层网络模型浅析


当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了!


OSI七层网络模型(从下往上)




  • 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等!在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,这一层的单位是:bit比特

  • 数据链路层(Datalink) :可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等!另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两过程!这种建立起来的数据收发关系~该层的设备有:网卡,网桥,网路交换机,另外该层的单位为:

  • 网络层(Network) :主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了把任意两台终端设备数据链接起来的问题!简单点说就是:建立网络连接和为上层提供服务!该层的设备有:路由!该层的单位为:数据包,另外IP协议就在这一层!

  • 传输层(Transport) :向上面的应用层提供通信服务,面向通信部分的最高层,同时也是用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络层,并且保证这些数据段有效的到达对端!所以这层的单位是:数据段;而这层有两个很重要的协议就是:TCP传输控制协议UDP用户数据报协议,这也是本章节核心讲解的部分!

  • 会话层(Session) :负责在网络中的两节点之间建立、维持和终止通信。建立通信链接,保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!

  • 表示层(Presentation) :对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、数据格式转换和加密解密,压缩解压缩"等

  • 应用层(Application) :OSI参考模型的最高层,为用户的应用程序提供网络服务。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。



好的上面我们浅述了OSI七层网络模型,下面总结下:



OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊的网络功能,从网络功能角度观察:



  • 下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主

  • 第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;

  • 上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。


简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。




TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层(对应OSI的第四层)。TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这4层分别为:



  • 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。

  • 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。

  • 网络互连层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。

  • 主机到网络层:对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。


3.TCP/UDP区别讲解


好吧,前两点侃侃而谈,只是给大家普及下OSI七层模型和TCP/IP四层模型的概念,接下来要讲的是和我们Socket开发相关的一些概念名词了!


1)IP地址



2)端口



1. 用于区分不同的应用程序


2. 端口号的范围为0-65535,其中0-1023未系统的保留端口,我们的程序尽可能别使用这些端口!


3. IP地址和端口号组成了我们的Socket,Socket是网络运行程序间双向通信链路的终结点,是TCP和UDP的基础!


4. 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23




3)TCP协议与UDP协议的比较:


TCP协议流程详解:


首先TCP/IP是一个协议簇,里面包括很多协议的。UDP只是其中的一个。之所以命名为TCP/IP协议,因为TCP,IP协议是两个很重要的协议,就用他两命名了。


下面我们来讲解TCP协议和UDP协议的区别:


TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,即在收发数据钱,都需要与对面建立可靠的链接,这也是面试经常会问到的TCP的三次握手以及TCP的四次挥手三次握手:建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立,在Socket编程中,这一过程由客户端执行connect来触发,具体流程图如下:




  • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。


四次挥手:终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在Socket编程中,这一过程由客户端或服务端任一方执行close来触发,具体流程图如下:




  • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态

  • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。另外也可能是同时发起主动关闭的情况:



另外还可能有一个常见的问题就是:为什么建立连接是三次握手,而关闭连接却是四次挥手呢?答:因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。


UDP协议详解


UDP(User Datagram Protocol)用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。相比TCP就是无需建立链接,结构简单,无法保证正确性,容易丢包。


4.Java中对于网络提供的几个关键类


针对不同的网络通信层次,Java给我们提供的网络功能有四大类:



  • InetAddress:用于标识网络上的硬件资源

  • URL:统一资源定位符,通过URL可以直接读取或者写入网络上的数据

  • Socket和ServerSocket:使用TCP协议实现网络通信的Socket相关的类

  • Datagram:使用UDP协议,将数据保存在数据报中,通过网络进行通信


本节我们只介绍前两个类,Socket与Datagram到TCP和UDP的章节再讲解!


~InetAddress的使用例子


示例代码

    public static void main(String[] args) throws Exception{
//获取本机InetAddress的实例:
InetAddress address = InetAddress.getLocalHost();
System.out.println("本机名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
byte[] bytes = address.getAddress();
System.out.println("字节数组形式的IP地址:" + Arrays.toString(bytes));
System.out.println("直接输出InetAddress对象:" + address);
}

运行结果图



作者:向阳逐梦
链接:https://juejin.cn/post/7222996459833196581
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从进入内核态看内存管理

知乎上搜到一个比较有意思的话题:如何理解「进入内核态」,要回答好这个问题需要对内存管理及程序的运行机制有比较深刻的了解,比如你需要了解内存的分段,分页,中断等机制,信息量比较大,本文将会 Intel CPU 的发展历史讲起,循序渐近地帮助大家彻底掌握这一概念,...
继续阅读 »

知乎上搜到一个比较有意思的话题:如何理解「进入内核态」,要回答好这个问题需要对内存管理及程序的运行机制有比较深刻的了解,比如你需要了解内存的分段,分页,中断等机制,信息量比较大,本文将会 Intel CPU 的发展历史讲起,循序渐近地帮助大家彻底掌握这一概念,相信大家看了肯定有帮助,本文目录如下



  • CPU 运行机制

  • Intel CPU 历史发展史

    • 分段

    • 保护模式



  • 特权级

    • 系统调用

    • 中断



  • 分段内存的优缺点

  • 内存分页

  • 总结


CPU 运行机制


我们先简单地回顾一下 CPU 的工作机制,重新温习一下一些基本概念,因为我在查阅资料的过程发现一些网友对寻址,CPU 是几位的概念理解得有些模糊,理解了这些概念再去看 CPU 的发展史就不会再困惑


CPU 是如何工作的呢?它是根据一条条的机器指令来执行的,而机器指令= 操作码+操作数,操作数主要有三类:寄存器地址、内存地址或立即数(即常量)。


我们所熟悉的程序就是一堆指令和数据的集合,当打开程序时,装载器把程序中的指令和数据加载到内存中,然后 CPU 到内存中一条条地取指令,然后再译码,执行。


在内存中是以字节为基本单位来读写数据的,我们可以把内存看作是一个个的小格子(一般我们称其为内存单元),而每个小格子是一个字节,那么对于 B8 0123H 这条指令来说,它在内存中占三字节,如下,CPU 该怎么找到这些格子呢,我们需要给这些格子编号,这些编号也就是我们说的内存地址,根据内存地址就是可以定位指令所在位置,从而取出里面的数据



如图示:内存被分成了一个个的格子,每个格子一个字节,20000~20002 分别为对应格子的编号(即内存地址)


CPU 执行指令主要分为以下几个步骤




  1. 取指令,CPU 怎么知道要去取哪条指令呢,它里面有一个 IP 寄存器指向了对应要取的指令的内存地址, 然后这个内存地址会通过地址总线找到对应的格子,我们把这个过程称为寻址,不难发现寻址能力决定于地址总线的位宽,假设地址总线位数为 20 位,那么内存的可寻址空间为 2^20 * 1Byte = 1M,将格子(内存单元)里面的数据(指令)取出来后,再通过数据总线发往 CPU 中的指令缓存区(指令寄存器),那么一次能传多少数据呢,取决于数据总线的位宽,如果数据总线为 16 位,那么一次可以传 16 bit 也就是两个字节。




  2. 译码:指令缓冲区中的指令经过译码以确定该进行什么操作




  3. 执行:译码后会由控制单元向运算器发送控制指令进行操作(比如执行加减乘除等),执行是由运算器操纵数据也就是操作数进行计算,而操作数保存在存储单元(即片内的缓存和寄存器组)中,由于操作数有可能是内存地址,所以执行中可能需要到内存中获取数据(这个过程称为访存),执行后的结果保存在寄存器或写回内存中



    以指令 mov ax, 0123H 为例,它表示将数据 0123H 存到寄存器 AX 中,在此例中 AX 为 16 位寄存器,一次可以操作 16 位也就是 2 Byte 的数据,所以我们将其称为 16 位 CPU,CPU 是多少位取决于它一次执行指令的数据带宽,而数据带宽又取决于通用寄存器的位宽




  4. 更新 IP:执行完一条指令后,更新 IP 中的值,将其指向下一条指令的起始地址,然后重复步骤 1




由以上总结可知寻址能力与寄存器位数有关


接下来我们以执行四条指令为例再来仔细看下 CPU 是如何执行指令的,动图如下:



看到上面这个动图,细心地你可能会发现两个问题



  1. 前文说指令地址是根据 IP 来获取的吗,但上图显示指令地址却是由「CS 左移四位 + IP」计算而来的,与我们所阐述的指令保存在 IP 寄存器中似乎有些出入,这是怎么回事呢?

  2. 动图显示的地址是真实物理地址,这样进程之间可以互相访问/改写对方的物理地址,显然是不安全的,那如何才能做到安全访问或者说进程间内存的隔离呢


以上两点其实只要我们了解一下 CPU 的发展历史就明白解决方案了,有了以上的铺垫,在明白了寻址16/32/64 位 CPU 等术语的含义后,再去了解 CPU 的发展故事会更容易得多,话不多说,发车


Intel CPU 历史发展史


1971 年世界上第一块 4 位 CPU-4004 微处理器横空出世,1974 年 Intel 研发成功了 8 位 CPU-8080,这两款 CPU 都是使用的绝对物理地址来寻址的,指令地址只存在于 IP 寄存器中(即只使用 IP 寄存器即可确定内存地址)。由于是使用绝对物理地址寻址,也就意味着进程之间的内存数据可能会互相覆盖,很不安全,所以这两者只支持单进程


分段


1978 年英特尔又研究成功了第一款 16 位 CPU - 8086,这款 CPU 可以说是 x86 系列的鼻祖了,设计了 16 位的寄存器和 20 位的地址总线,所以内存地址可以达到 2^20 Byte 即 1M,极大地扩展了地址空间,但是问题来了,由于寄存器只有 16 位,那么 16 位的 IP 寄存器如何能寻址 20 位的地址呢,首先 Intel 工程师设计了一种分段的方法:1M 内存可以分为 16 个大小为 64 K 的段,那么内存地址就可以由「段的起始地址(也叫段基址) + 段内偏移(IP 寄存器中的值)」组成,对于进程说只需要关心 4 个段 ,代码段 数据段 堆栈段 附加段,这几个段的段基址分别保存在 CS,DS,SS,ES 这四个寄存器中



这四个寄存器也是 16 位,那怎么访问 20 位的内存地址呢,实现也很简单,将每个寄存器的值左移四位,然后再加上段内偏移即为寻址地址,CPU 都是取代码段 中的指令来执行的,我们以代码段内的寻址为例来计算内存地址,指令的地址 = CS << 4 + IP ,这种方式做到了 20 位的寻址,只要改变 CS,IP 的值,即可实现在 0 到最大地址 0xFFFFF 全部 20 位地址的寻址


举个例子:假设 CS 存的数据为 0x2000,IP 为 0x0003,那么对应的指令地址为



图示为真实的物理地址计算方式,从中可知, CS 其实保存的是真实物理地址的高 16 位


分段的初衷是为了解决寻址问题,但本质上CS:IP 计算得到的还是真实物理地址,所以它也无法支持多进程,因为使用绝对物理地址寻址意味着进程可以随意修改 CS:IP,将其指向任意地址,很可能会覆盖正在运行的其他进程的内存,造成灾难性后果。


我们把这种使用真实物理地址且未加任何限制的寻址方式称为实模式(real mode,即实际地址模式)


保护模式


实模式上的物理地址由 段寄存器中的段基址:IP 计算而来,而段基址可由用户随意指定,显然非常不安全,于是 Intel 在之后推出了 80286 中启用了保护模式,这个保护是怎么做的呢


首先段寄存器保存的不再是段基址了,而是段选择子(Selector),其结构如下



其中第 3 到 15 位保存的是描述符索引,此索引会根据 TI 的值是 0 还是 1 来选择是到 GDT(全局描述符表,一般也称为段表)还是 LDT 来找段描述符,段描述符保存的是段基址和段长度,找到段基址后再加上保存在 IP 寄存器中的段偏移量即为物理地址,段描述符的长度统一为 8 个字节,而 GDT/LDT 表的基地址保存在 gdtr/ldtr 寄存器中,以 GDT (此时 TI 值为 0)为例来看看此时 CPU 是如何寻址的



可以看到程序中的地址是由段选择子:段内偏移量组成的,也叫逻辑地址,在只有分段内存管理的情况下它也被称为虚拟内存


GDT 及段描述符的分配都是由操作系统管理的,进程也无法更新 CS 等寄存器中值,这样就避免了直接操作其他进程以及自身的物理地址,达到了保护内存的效果,从而为多进程运行提供了可能,我们把这种寻址方式称为保护模式


那么保护模式是如何实现的呢,细心的你可能发现了上图中在段选择子和段描述符中里出现了 RPLDPL 这两个新名词,这两个表示啥意思呢?这就涉及到一个概念:特权级


特权级


我们知道 CPU 是根据机器指令来执行的,但这些指令有些是非常危险的,比如清内存置时钟分配系统资源等,这些指令显然不能让普通的进程随意执行,应该始终控制在操作系统中执行,所以要把操作系统和普通的用户进程区分开来


我们把一个进程的虚拟地址划分为两个空间,用户空间内核空间,用户空间即普通进程所处空间,内核空间即操作系统所处空间



当 CPU 运行于用户空间(执行用户空间的指令)时,它处于用户态,只能执行普通的 CPU 指令 ,当 CPU 运行于内核空间(执行内核空间的指令)时,它处于内核态,可以执行清内存,置时钟,读写文件等特权指令,那怎么区分 CPU 是在用户态还是内核态呢,CPU 定义了四个特权等级,如下,从 0 到 3,特权等级依次递减,当特权级为 0 时,CPU 处于内核态,可以执行任何指令,当特权级为 3 时,CPU 处于用户态,在 Linux 中只用了 Ring 0,Ring 3 两个特权等级



那么问题来了,怎么知道 CPU 处于哪一个特权等级呢,还记得上文中我们提到的段选择子吗



其中的 RPL 表示请求特权((Requested privilege level))我们把当前保存于 CS 段寄存器的段选择子中的 RPL 称为 CPL(current priviledge level),即当前特权等级,可以看到 RPL 有两位,刚好对应着 0,1,2,3 四个特权级,而上文提到的 DPL 表示段描述符中的特权等级(Descriptor privilege level)知道了这两个概念也就知道保护模式的实现原理了,CPU 会在两个关键点上对内存进行保护




  1. 目标段选择子被加载时




  2. 当通过线性地址(在只有段式内存情况下,线性地址为物理地址)访问一个内存页时。由此可见,保护也反映在内存地址转换的过程之中,既包括分段又包括分页(后文分提到分页)




CPU 是怎么保护内存的呢,它会对 CPL,RPL,DPL 进行如下检查



只有 CPL <= DPL 且 RPL <= DPL(申请特权等级待以及当前特权等级必须比调用的目标代码段的特权级更高,以防普通程序直接调用目标代码段)时,才会加载目标代码段执行,否则会报一般保护异常 (General-protection exception)


那么特权等级(也就是 CPL)是怎么变化的呢,我们之前说了 CPU 运行于用户空间时,处于用户态,特权等级为 3,运行于内核空间时,处于内核态,特权等级为 0,所以也可以换个问法 CPU 是如何从用户空间切换到内核空间或者从内核空间切换到用户空间的,这就涉及到一个概念:系统调用


系统调用


我们知道用户进程虽然不能执行特权指令,但有时候也需要执行一些读写文件,发送网络包等操作,而这些操作又只能让操作系统来执行,那该怎么办呢,可以让操作系统提供接口,让用户进程来调用即可,我们把这种方式叫做系统调用,系统调用可以直接由应用程序调用,或者通过调用一些公用函数库或 shell(这些函数库或 shell 都封装了系统调用接口)等也可以达到间接调用系统调用的目的。通过系统调用,应用程序实现了陷入(trap)内核态的目的,这样就从用户态切换到了内核态中,如下


应用程序通过系统调用陷入内核态


那么系统调用又是怎么实现的呢,主要是靠中断实现的,接下来我们就来了解一下什么是中断


中断


陷入内核态的系统调用主要是通过一种 trap gate(陷阱门)来实现的,它其实是软件中断的一种,由 CPU 主动触发给自己一个中断向量号,然后 CPU 根据此中断向量号就可以去中断向量表找到对应的门描述符,门描述符与 GDT 中的段描述符相似,也是 8 个字节,门描述符中包含段选择子,段内偏移,DPL 等字段 ,然后再根据段选择子去 GDT(或者 LDT,下图以 GDT 为例) 中查找对应的段描述符,再找到段基地址,然后根据中断描述符表的段内偏移即可找到中断处理例程的入口点,整个中断处理流程如下



画外音:上图中门描述符和段描述符只画出了关键的几个字段,省略了其它次要字段


当然了,不是随便发一个中断向量都能被执行,只有满足一定条件的中断才允许被普通的应用程序调用,从发出软件中断再到执行中断对应的代码段会做如下的检查



一般应用程序发出软件中断对应的向量号是大家熟悉的 int 0x80(int 代表 interrupt),它的门描述符中的 DPL 为 3,所以能被所有的用户程序调用,而它对应的目标代码段描述符中的 DPL 为 0,所以当通过中断门检查后(即 CPL <= 门描述符中的 DPL 成立),CPU 就会将 CS 寄存器中的 RPL(3) 替换为目标代码段描述符的 DPL(0),替换后的 CPL 也就变成了 0,通过这种方式完成了从用户态到内核态的替换,当中断代码执行后执行 iret 指令又会切换回用户态


另外当执行中断程序时,还需要首先把当前用户进程中对应的堆栈,返回地址等信息,以便切回到用户态时能恢复现场


可以看到 int 80h 这种软件中断的执行又是检查特权级,又是从用户态切换到内核态,又是保存寄存器的值,可谓是非常的耗时,光看一下以下图示就知道像 int 0x80 这样的软件中断开销是有多大了


系统调用


所以后来又开发出了 SYSENTER/SYSCALL 这样快速系统调用的指令,它们取消了权限检查,也不需要在中断描述表(Interrupt Descriptor Table、IDT)中查找系统调用对应的执行过程,也不需要保存堆栈和返回地址等信息,而是直接进入CPL 0,并将新值加载到与代码和堆栈有关的寄存器当中(cs,eip,ss 和 esp),所以极大地提升了性能


分段内存的优缺点


使用了保护模式后,程序员就可以在代码中使用了段选择子:段偏移量的方式来寻址,这不仅让多进程运行成为了可能,而且也解放了程序员的生产力,我们完全可以认为程序拥有所有的内存空间(虚拟空间),因为段选择子是由操作系统分配的,只要操作系统保证不同进程的段的虚拟空间映射到不同的物理空间上,不要重叠即可,也就是说虽然各个程序的虚拟空间是一样的,但由于它们映射的物理地址是不同且不重叠的,所以是能正常工作的,但是为了方便映射,一般要求在物理空间中分配的段是连续的(这样只要维护映射关系的起始地址和对应的空间大小即可)


段式内存管理-虚拟空间与实际物理内存的映射


但段式内存管理缺点也很明显:内存碎片可能很大,举个例子



如上图示,连续加载了三个程序到内存中,如果把 Chrome 关闭了,此时内存中有两段 128 M的空闲内存,但如果此时要加载一个 192 M 的程序 X 却有心无力了 ,因为段式内存需要划分出一块连续的内存空间,此时你可以选择把占 256 M 的 Python 程序先 swap 到磁盘中,然后紧跟着 512 M 内存的后面划分出 256 M 内存,再给 Python 程序 swap 到这块物理内存中,这样就腾出了连续的 256 M 内存,从而可以加载程序 X 了,但这种频繁地将几十上百兆内存与硬盘进行 swap 显然会对性能造成严重的影响,毕竟谁都知道内存和硬盘的读写速度可是一个天上一个地上,如果一定要交换,能否每次 swap 得能少一点,比如只有几 K,这样就能满足我们的需求,分页内存管理就诞生了


内存分页


1985 年 intel 推出了 32 位处理器 80386,也是首款支持分页内存的 CPU


和分段这样连续分配一整段的空间给程序相比,分页是把整个物理空间切成一段段固定尺寸的大小,当然为了映射,虚拟地址也需要切成一段段固定尺寸的大小,这种固定尺寸的大小我们一般称其为页,在 LInux 中一般每页的大小为 4KB,这样虚拟地址和物理地址就通过页来映射起来了



当然了这种映射关系是需要一个映射表来记录的,这样才能把虚拟地址映射到物理内存中,给定一个虚拟地址,它最终肯定在某个物理页内,所以虚拟地址一般由「页号+页内偏移」组成,而映射表项需要包含物理内存的页号,这样只要将页号对应起来,再加上页内偏移,即可获取最终的物理内存



于是问题来了,映射表(也称页表)该怎么设计呢,我们以 32 位虚拟地址位置来看看,假设页大小为 4K(2^12),那么至少需要 2^20 也就是 100 多万个页表项才能完全覆盖所有的虚拟地址,假设每一个页表项 4 个字节,那就意味着为一个进程的虚拟地址就需要准备 2^20 * 4 B = 4 M 的页表大小,如果有 100 个进程,就意味着光是页表就要占用 400M 的空间了,这显然是非常巨大的开销,那该怎么解决这个页表空间占用巨大的问题呢


我们注意到现在的做法是一次性为进程分配了占用其所有虚拟空间的页表项,但实际上一个进程根本用不到这么巨大的虚拟空间,所以这种分配方式无疑导致很多分配的页表白白浪费了,那该怎么办,答案是分级管理,等真正需要分配物理空间的时候再分配,其实大家可以想想我们熟悉的 windows 是怎么分配的,是不是一开始只分配了 C 盘,D盘,E盘,等要存储的时候,先确定是哪个盘,再在这个盘下分配目录,然后再把文件存到这个目录下,并不会一开始就把所有盘的空间给分配完的



同样的道理,以 32 位虚拟地址为例,我们也可以对页表进行分级管理, 页表项 2^20 = 2^10 * 2^10 = 1024 * 1024,我们把一个页表分成两级页表,第一级页表 1024 项,每一项都指向一个包含有 1024 个页表项的二级页表


图片来自《图解系统》


这样只有在一级页表中的页表项被分配的时候才会分配二级页表,极大的节省了空间,我们简单算下,假设 4G 的虚拟空间进程只用了 20%(已经很大了,大部分用不到这么多),那么由于一级页表空间为 1024 *4 = 4K,总的页表空间为 4K+ 0.2 * 4M = 0.804M,相比于原来的 4M 是个巨大的提升!


那么对于分页保护模式又是如何起作用的呢,同样以 32 位为例,它的二级页表项(也称 page table entry)其实是以下结构



注意第三位(也就是 2 对应的位置)有个 U/S,它其实就是代表特权级,表示的是用户/超级用户标志。为 1 时,允许所有特权级别的程序访问;为 0 时,仅允许特权级为0、1、2(Linux 中没有 1,2)的程序(也就是内核)访问。页目录中的这个位对其所映射的所有页面起作用


既然分页这么好,那么分段是不是可以去掉了呢,理论上确实可以,但 Intel 的 CPU 严格执行了 backward compatibility(回溯兼容),也就是说最新的 CPU 永远可以运行针对早期 CPU 开发的程序,否则早期的程序就得针对新 CPU 架构重新开发了(早期程序针对的是 CPU 的段式管理进行开发),这无论对用户还是开发者都是不能接受的(别忘了安腾死亡的一大原因就是由于不兼容之前版本的指令),兼容性虽然意味着每款新的 CPU 都得兼容老的指令,所背的历史包袱越来越重,但对程序来说能运行肯定比重新开发好,所以既然早期的 CPU 支持段,那么自从 80386 开始的所有 CPU 也都得支持段,而分页反而是可选的,也就意味着这些 CPU 的内存管理都是段页式管理,逻辑地址要先经过段式管理单元转成线性地址(也称虚拟地址),然后再经过页式管理单元转成物理内存,如下


分页是可选项


在 Linux 中,虽然也是段页式内存管理,但它统一把 CS,DS,SS,ES 的段基址设置为了 0,段界限也设置为了整个虚拟内存的长度,所有段都分布在同一个地址空间,这种内存模式也叫平坦内存模型(flat memory model)


平坦内存模型


我们知道逻辑地址由段选择子:段内偏移地址组成,既然段选择子指向的段基地址为 0,那也就意味着段内偏移地址即为即为线性地址(也就是虚拟地址),由此可知 Linux 中所有程序的代码都使用了虚拟地址,通过这种方式巧妙地绕开了分段管理,分段只起到了访问控制和权限的作用(别忘了各种权限检查依赖 DPL,RPL 等特权字段,特权极转移也依赖于段选择子中的 DPL 来切换的)


总结


看完本文相信大家对实模式,保护模式,特权级转换,分段,分页等概念应该有了比较清晰的认识。


我们简单总结一下,CPU 诞生之间,使用的绝对物理内存来寻址(也就是实模式),随后随着 8086 的诞生,由于工艺的原因,虽然地址总线是 20 位,但寄存器却只有 16 位,一个难题出现了,16 位的寄存器该怎么寻址 20 位的内存地址呢,于是段的概念被提出了,段的出现虽然解决了寻址问题,但本质上 CS << 4 + IP 的寻址方式依然还是绝对物理地址,这样的话由于地址会互相覆盖,显然无法做到多进程运行,于是保护模式被提出了,保护就是为了物理内存免受非法访问,于是用户空间,内核空间,特权级也被提出来了,段寄存器里保存的不再是段基址,而是段选择子,由操作系统分配,用户也无法随意修改段选择子,必须通过中断的形式才能从用户态陷入内核态,中断执行的过程也需要经历特权级的检查,检查通过之后特权级从 3 切换到了 0,于是就可以放心合法的执行特权指令了。可以看到,通过操作系统分配段选择子+中断的方式内存得到了有效保护,但是分段可能造成内存碎片过大以倒频繁 swap 会影响性能的问题,于是分页出现了,保护模式+分页终于可以让多进程,高效调度成为了可能


参考



作者:码海
链接:https://juejin.cn/post/7107199434425335821
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

计算机网络 传输层

计算机网络 传输层 传输层服务 传输层在网络层的基础上,实现了进程到进程的服务。 传输层通过引入端口号来区分进程,在 UDP/TCP 的首部中,一个端口占用 2B, 即 16 bit。 复用与分用 一台主机上的多个进程,利用一个传输层实体,将数据发送出去,这...
继续阅读 »

计算机网络 传输层


传输层服务


传输层在网络层的基础上,实现了进程到进程的服务。


传输层通过引入端口号来区分进程,在 UDP/TCP 的首部中,一个端口占用 2B, 即 16 bit。



复用与分用


一台主机上的多个进程,利用一个传输层实体,将数据发送出去,这就叫复用。而远端主机的传输层实体,收到了很多数据,然后通过识别端口号,将数据交给具体的进程,这就叫分用(或解复用)。


UDP(无连接传输协议 / 用户数据报协议)


面向报文协议,即一个 UDP 数据报就是应用层交下来一个完整报文。


是一种尽力而为的,非有序的、无连接的一种协议,通常用于流媒体应用、事务性应用,如 DNS 等。


UDP 的报文结构



UDP 会使用校验和对数据做差错控制编码。


将数据按每16位一组相加,最高位相加产生的进位,回卷到最低位去,如此,将最后的和取反码就是最终的校验和,在接收端,按同样的方法求校验和,与接收端的校验和相加,如果结果不是 16 个 1,则肯定出错,如果是 16 个 1 ,说明大概率没有出错


可靠数据传输的基本原理


可靠的传输通常需要实现数据的不丢失、不乱序、无差错等核心功能。


我所看过的传输层相关的书,都是通过递进式的讲解实现可靠的网络传输,这里也已这种方式来记录。但我这里 rdt1.0、rdt2.0、rdt3.0跟书本上的可能不一致,纯粹是按自己理解的来,我觉得这里能够理解其逐渐完善可靠性的过程就OK了。


rdt1.0 停等协议


先假设传输层以下的实现都是可靠的,不会出现数据差错的情况,**那如何实现不乱序呢?**很简单,我们只要保证按顺序传就可以,发送端先发一个,等接收端收到了再发下一个,如此往复直到数据全部发送完。这样的协议叫做停等协议。



rdt2.1 ACK 和 NAK


这里看上去确实实现了按序到达,但这是建立在底层不会出现差错的情况下,一旦出现了一些异常情况,比如**分组在传输过程中出现了比特反转(0被识别为1,1识别为0)**该如何处理?


那需要接收端给发送端一个答复,接收端是否收到了合法的分组。如果收到了正确的分组,接收端回复一个确定(ACK),发送端识别到时ACK,就可以继续发送下一个分组,若收到了错误的分组,则回复一个否定(NAK),那接收端就需要重发当前分组。



如图这里主要有两种异常情况:




  1. 分组在传输过程中出错:


    接收端检测到错误之后,回复 NAK , 发送端收到 NAK 后,需要重传上一个分组。




  2. ACK 或 NAK 在传输过程中出错:


    发送端不能识别这到底是 ACK 还是 NAK,于是就统一按照这是 NAK 来处理,于是就重发上一个分组。而这里重发后,会导致接收端收到重复的分组,接收端需要丢弃这个分组,然后回复 ACK。




当然,如果过程中没有出错,那就一步一步的正常的将数据发送给接收端就OK了。


rdt2.2 序号


在rdt2.2中,我们开始使用序号,给分组进行编号。并且使用上一个分组的编号来替代 NAK ,什么意思呢?如果用户在收到一个分组 P3 时,发现数据出错了,那这时它就不再回复 NAK 了,而是回复 ACK = P2,表示接收端才收到 P2,于是发送端需要重传 P3,这样实现了跟 NAK 一样的效果。这么做为后面的流水协议奠定了基础。



可以看到,这里用分组的序号替代单纯的 ACK/NAK,异常情况跟 rdt 2.1 非常相似。


rdt3.0 超时重传机制


如果分组在传输过程中丢失了,接收端等待下一个分组,发送端等待 ACK,双方如果都这么默默的等待下去,那不就出现死锁了?


我们可以让发送端发送一个分组后,设置一个超时定时器,如果在一段时间后还没有收到 ACK, 触发超时重传机制,重发上一个分组,这里不论是分组丢失还是 ACK 丢失,都可以重发。如果是 ACK 丢失,接收端会收到重复的分组,跟 rdt2.0 的方案类似,将重复的分组丢弃就可以了,并且对当前的分组发送 ACK 。



由于定时器的设置,不可能百分百的确定分组或 ACK 丢失,可能在超时重传后好一段时间,对前面某个分组的 ACK 慢悠悠的过来了,这是发送端可以什么都不做,无视它就可以了,因为前面已经收到过对这个分组的确认。


超时计时器的时间需要根据RTT的时间来动态设置,如果超时时间设置得过短,会引起不必要的重传,如果设置得过长,会使重传效率变低。


到目前为止,停等协议已经实现了可靠传输,但是它的信道利用率很低


滑动窗口协议(slide window)


停等协议的信道利用率很低,是因为它一次只发一个分组,确认发送完成后才能发另一个,这中间会有很长的等待时间,如何提高信道利用率呢?显然,我们在发完一个分组后,不等 ACK 就在此下一个,看上去就能提高效率。因此,这种连续发送多个未经确认的分组的协议叫做流水线协议


在发送端会有一段缓冲区,可以存储已经发送出去但还没有收到 ack 的分组,用于后面检错重发、超时重发等。而在接收端,也有一段缓存区,主要是为了适应发送端发送速率和接收端接收速率不一致的情况,当发送端速度过快时,可以用缓冲区暂存一下。


发送端缓冲区的发送窗口长度叫做 sw , 接收端缓冲区接收窗口长度叫做 rw 。窗口是缓冲区的一个子集。



  • 当 sw = 1,rw = 1 时,这就是停等协议

  • 当 sw > 1,rw = 1 时,这就是 GBN 协议

  • 当 sw > 1,rw > 1时,这就是 SR (selective repeat)协议



通常发送窗口的后沿指针指向低位的首个已发送但没有确认的分组,前沿指针指向高位第一个已发送的分组,如果发送的分组数量比最大发送窗口长度小,那表明发送端可以继续发送分组,也就是前沿指针可以向前移动。如果低位的未确认的分组收到了确认,则后沿指针可以向前移动,移动到首个未确认的分组。这时,发送窗口被使用的长度就变小了,发送端就可以继续发送分组。


而接收端收到的分组在接收窗口范围内时,可以接收并对其作出确认,若没有比它序号更低的分组等待被确认,则接收窗口的后沿指针可以向前移动,移动到首个未确认的分组。由于窗口指针的移动,接收端又有了更多的空间来接收后续的分组。如果接收的分组超出接收窗口范围,则将其丢弃,因为接收窗口暂时没有足够空间去保存更多的分组。


GBN

GBN 协议特点:rw = 1, 顺序接收,累积确认。


双方正常的流程:发送方可以按序发送多个分组,这些分组有序的到达接收端,接收端对其一一作出确认,接收端每接收一个分组,窗口就向前移动一位,然后接收到下一个分组,再对下一个分组进行确认,然后再往前移动。


双方的异常流程:



  1. 如果接收端收到乱序的分组,比如在等待 P1 的过程中,结果收到一个 P2,那接收端会将其丢弃,并发送其上一个分组,也就是 P0 的确认。发送端收到 P0 的 ACK 后,就会将 P0 之后的所有的分组重发一遍,这也就是 go back N 的意义。总结起来就是,如果低位的分组出错(失序或超时)了,需要从低位开始重传后续所有的分组



SR

SR 协议特点:rw > 1, 可以乱序接收非累积确认,或者叫单独确认,收到哪个分组,就给哪个确认。窗口向前滑动时,滑到从低位开始第一个未确认的分组


双方的正常流程:发送方依次发送多个分组,每个分组要设置一个独立的超时计时器,由于 SR 的接收窗口长度大于 1,因此只要是在窗口范围内的分组,它都可以接收,并单独对其作出确认。发送方收到确认后,取消超时计时器,如果没有比当前更小的需要被确认的分组,那后沿指针就向前移动,发送端继续发送后续的分组。


双方的异常情况:




  1. 发送方的某个分组丢失了,那接收方就无法对其作出确认,接收窗口的后沿指针无法向前移动,一段时间后,发送端的这个分组的超时计时器会触发重传,然后重新设置超时计时器。接收方收到这个分组后,对其作出确认,然后接收窗口的前沿指针得以向前移动,从而可以接收更多的分组。而发送方收到确认后,发送窗口的后沿指针也会向前移动,发送端就可以发送后续的分组。




  2. 接收端的某个分组的ACK 丢失了,这里发送端对应的分组超时计时器会进行重发,重发后接收端收到了重复的分组,而接收端发现这个分组已经被确认过,直接将其忽略即可。如果接收端的某个分组的 ACK 是因为网络拥塞导致超时之后才到达发送端,那发送端发现自己已经重发了这个分组,正在等待确认中,那发送端直接忽略这个ACK就可以了,同理,如果某个分组已经通过超时重传后被确认了,而首次发送的 ACK 才缓缓到达,发送端同样可以忽略这次的确认,因为这个分组已经被确认过了。




GBN & SR 的差别

GBN 使用起来简单,适合在低差错率的网络中使用,使用累积确认(比如,对某个分组进行确认,则表明这个分组及以前所有的分组,都被正确接收了,而之后的分组没有被收到),一旦出错,需要重发之后所有已经发送过的分组,会有比较大的代价。


SR 实现起来复杂些,因为每个分组都有单独的超时计时器,只需要对出现差错的分组单独重传,适合在差错率较高的网络中使用。使用非累积确认。


TCP(面向连接传输协议)


在可靠传输原理中,我们为了方便,是给分组设置了编号,但实际在 TCP 中是对字节流进行编号。


由于我们到了具体的 TCP 协议,通常需要将分组描述为报文段


TCP 采用了前面可靠传输原理中的流水线协议、累积确认、单超时计时器等特点,是 SR 和 GBN 的混合,同时还支持流量控制,拥塞控制。


段结构



TCP 在载荷部分前,会加上自己的控制信息,也就是 TCP 的首部。如果不包含选项部分,首部的长度为 20个字节。


介绍几个主要的字段。


序号:由发送端设置,表示发送端发送报文段的首字节编号。


确认号:由接收端设置,当 ACK = 1时,确认号才有效,表示接收端期望收到字节编号。同时,在累积确认中表示这个编号之前的分组已经收到。


SYN:表示要建立连接。


FIN:表示要拆除连接。


可靠数据传输


TCP 的可靠传输原理大致可以理解为是 SR + 累积确认 + 快速重传。TCP 的发送端不会为每个发出去的报文段设置超时计时器,而是在发送窗口滑动到首个未确认的报文段时启动一个超时计时器。


TCP 对于乱序到达的分组,没有规定该存储还是丢弃,可以自由实现。


快速重传


低位的分组,在超时计时器触发超时之前,如果收到了连续 3 次重复的对当前报文段的确认,可以认为当前报文段已经出现了差错,发送端就可以提前重发这个报文段,而不用等到超时计时器生效。


TCP 的实际实现中,在收到某个分组后,会短暂的等待一会,等收到下一个分组后,再发送确认,这样就可以只发送 1 次确认,这也是利用了累积确认的优点。当然,它不能等待太久,并且等待的分组只能有 1 个,如果下一个分组到来,立刻发送确认。


流量控制


流量控制时为了解决发送端的发送速率和接收端的读取数据不一致的问题,通常是发送端发送的太快,超出了接收端的缓冲区,超出的这些报文段就会被丢弃,那发送端发这么多也就没有了意义。


接收方在发送确认时,可以将自己可用的缓冲区大小放在 TCP 首部的接收窗口字段中,发送端收到这个 ACK 后,就知道是否还能够继续发送数据,可以根据接收端的窗口,调节自己的窗口大小。


连接管理


TCP 是面向连接的,在正式传送数据前,需要做一些准备工作,如:协商起始序号,设置发送窗口、接收窗口等。



连接建立被称为“三次握手”,TCP 的连接建立过程如下:


连接建立前,客户端处于连接关闭状态,服务端处于监听状态。



  1. 客户端发送连接建立请求,SYN=1,同时设置一个随机的初始字节序号x,即 seq=x。

  2. 服务端收到了连接建立请求,设置 ACK=1,表示同意建立请求,同时设置请求号 ack=x+1,TCP 规定 SYN 报文段不能携带数据,但需要消耗 1 个序号,因此这里的 ack=x+1。这部分操作表示服务端同意建立请求。同时,第二次握手还有服务端作为发送方希望与客户端建立连接的请求,所以,这里有跟第一次握手相似的数据,SYN=1, seq=y,y 是服务端随机生成的序号。

  3. 客户端收到了服务端发来的 ack,那客户端作为发送方与服务端的连接就已经建立,这时客户端就已经可以向服务端发送数据了。同时也收到了服务端发来的连接建立请求,因而这次客户端需要回复 ACK=1,ack=y+1, 表示同意建立请求,然后这次不论是否传送数据 seq都是 x+1。


经过上面三次握手,客户端与服务端的双向的连接就建立起来了。


当双方通信完毕后,TCP 需要拆除连接。连接拆除的过程通常叫做“四次挥手”,过程如下:




  1. 客户端发送 FIN=1,表示希望拆除连接。

  2. 服务端收到后发送 ACK=1。(客户端收到ACK后,客户端到服务端的连接就拆除了,但服务端到客户端的连接仍然可用,服务端仍然可以向客户端发送数据)

  3. 服务端发送 FIN=1,表示希望拆除连接。

  4. 客户端收到后发送 ACK=1。服务端收到后确认后,连接即拆除。但站在客户端的角度,作为最后一个发送消息的角色,它还不知道自己发送的确认有没有准确的到达服务器,所以在发送完 ACK 后,会等待一段时间,如果服务端没有重传FIN,那客户端作为接收方到服务端的接收连接也就断开了,那到目前为止,所有的连接就都拆除了。


上面第四步客户端会有一个等待时间来防止 ack 出现差错,但这个方案也不是完美的,如果服务端发现这个ack 有差错,重发了一个连接拆除的请求,但这个请求慢悠悠过了好一会才到达客户端(时间大于客户端的等待时间),可是客户端已经关闭了连接,那服务端短时间内就没法收到拆除连接的 ack 了。


因此,作为拆除连接的最后一环,第四次挥手无论怎样都不会完美,只能尽可能的降低出现问题的概率。


为什么要设置 SequenceNum?


防止滞留在网络中的上一次连接中的一些段,被识别位本次连接中的段。SequenceNum 的长度是 32 位,可以编址 4G 的字节,这么大的范围在两次连接中很难出现相近的序号段,一旦序号段差别很大,这个“野“报文段也就很难出现接收窗口中,就会被接收方丢弃。


为什么2次握手不行?



  1. TCP 是全双工的连接,建立连接时,既要有客户端到服务端的连接,也要有服务端到客户端的连接。建立一个方向的连接需要 2 次握手,两个方向就是 4 次,但是在服务端向客户端确认连接时,可以捎带上服务端对客户端的连接建立请求,这样就合并了一次握手,所以有三次。

  2. 如果只有 2 次握手,且服务端的确认出现差错的情况下,客户端就收不到建立连接的确认,这个方向的连接就会失败。而服务端不知道自己的连接确认丢失了,导致服务端到客户端的连接建立了,这样就出现了“半连接”的情况。如果服务端有大量的这种“半连接”,会极大的消耗服务端的性能。如果上一个连接滞留在网络中的报文段到达了接收方,还有可能阴差阳错的被服务端接收。



TCP的拥塞控制


拥塞时指过多的分组被注入到网络中,超出了网络的处理能力,导致大量的分组 ”拥挤“ 在网络中间设备队列中等待转发,网络性能显著下降的现象。


拥塞的直接后果:



  1. 数据分组通过网络的延时显著增加;

  2. 由于中间网络设备的队列满导致大量的分组被丢弃。



拥塞控制就是通过合理调度、规范、调整向网络中发送数据的主机数量、发送速率或数据量,以避免拥塞或尽快消除已发生的拥塞。拥塞控制可以在不同层实现,比较典型的是在网络层和传输层进行拥塞控制。


拥塞控制的基本思想是 AIMD。拥塞窗口的调整主要分为慢启动阶段和拥塞避免阶段。在慢启动阶段,每收到 1 个确认段,拥塞窗口增加 1 个 MSS,每经过 1 个 RTT,拥塞窗口增长 1 倍;在拥塞避免阶段,每经过 1 个 RTT,拥塞窗口才增长 1 个 MSS。



如何检测网络中是否发生了拥塞?



  1. 分组发生了超时

  2. 连续收到了多个对同一分组的重复确认。

作者:Jax__
链接:https://juejin.cn/post/7144759075631267853
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 开发中必须了解的 Context

1. 什么是 context? 作为安卓开发工程师,Context是我们经常使用的一个重要概念。 从代码的角度来看,Context是一个抽象类,它代表着应用程序环境和运行时状态的信息。Context具有许多子类,包括Activity和Service等,每个子类...
继续阅读 »

1. 什么是 context?


作为安卓开发工程师,Context是我们经常使用的一个重要概念。


从代码的角度来看,Context是一个抽象类,它代表着应用程序环境和运行时状态的信息。Context具有许多子类,包括Activity和Service等,每个子类都代表着不同的应用程序环境和状态。


从设计的角度来看,Context是一个非常重要的概念,因为它允许我们在应用程序中访问系统资源,例如数据库,共享偏好设置和系统服务等。Context还允许我们在应用程序中创建新的组件,例如Activity和Service等。


实际上,Context在安卓开发中几乎无处不在。例如,我们可以使用Context来启动一个新的Activity,获取应用程序的资源,读取和写入文件,以及访问系统服务和传感器等。Context还可以帮助我们管理应用程序的生命周期,例如在应用程序销毁时释放资源。


总之,Context是安卓开发中不可或缺的概念,它允许我们访问系统资源,管理应用程序的生命周期,并与系统交互。理解Context的概念和使用方法对于成为一名优秀的安卓开发工程师至关重要。


2. context继承关系


Context
├── ContextImpl
├── ContextWrapper
│ ├── Application
│ ├── Service
│ ├── ContextThemeWrapper
│ │ ├── Activity
│ │ │ ├── FragmentActivity
│ │ │ └── ...
│ │ └── ...
│ └── ...
└── ...

Context是一个抽象类,它有多个直接或间接的子类。


ContextImpl是Context的一个实现类,真正实现了Context中的所有函数,所调用的各种Context类的方法,其实现均来自于该类。


ContextWrapper是一个包装类,它可以包装另一个Context对象,并在其基础上添加新的功能。内部包含一个真正的Context引用,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。


ContextThemeWrapper是一个特殊的包装类,它可以为应用程序的UI组件添加主题样式。主题就是指Activity元素指定的主题。只有Activity需要主题,所以Activity继承自ContextThemeWrapper,而Application和Service直接继承自ContextWrapper。


总之,Context的继承关系非常复杂,但是理解这些关系对于在安卓开发中正确地使用Context非常重要。通过继承关系,我们可以了解每个Context子类的作用和用途,并且可以选择合适的Context对象来访问应用程序的资源和系统服务。


3.Context如何创建


在安卓应用程序中,Activity是通过调用startActivity()方法来启动的。当我们启动一个Activity时,系统会通过调用Activity的生命周期方法来创建、启动和销毁Activity对象。
而其中创建Activity的方法最终是走到ActivityThread.performLaunchActivity()方法。将其中无关方法删除后:
、、、
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
//创建 ContextImpl对象
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;


    //创建Activity对象
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
//Activity初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken, r.shareableActivityToken);
//Theme设置
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

return activity;
}

、、、
可以看到Activity的创建过程十分清楚:



  1. 创建ContextImpl对象,方法最终走到静态方法ContextImpl.createActivityContext()创建。

  2. 创建Activity对象,最终instantiateActivity()通过调用Class的newInstance()方法,反射创建出来,方法注解到This method is only intended to provide a hook for instantiation. It does not provide earlier access to the Activity object. The returned object will not be initialized as a Context yet and should not be used to interact with other android APIs.,方法只创建了Activity的早期对象,并没有对它做Context的初始化,所以不能调用安卓相关api。简单来说,Activity本身继承自ContextWrapper,这个方法并没有具体实现任何Context的方法,只是将所有方法代理给了内部的baseContext,所以反射创建后,调用任何的系统的方法都是无效的。

  3. Activity初始化,调用Activity.attch(),这个方法对Activity做各种所需的初始化,Context、Thread、parent、Window、Token等等,而Context的初始化就是调用ContextWrapper.attachBaseContext()把第一步创建的ContextImpl设置到baseContext。

  4. Theme设置,前面说到Activity实现的是ContextThemeWrapper,对ContextWrapper扩展并支持了Theme的替换,调用ContextThemeWrapper.setTheme()完成Theme的初始化。


4.一些思考




  1. ContextThemeWrapper作为ContextWrapper一个扩展,它是重写了ContextImpl中的一些关于Theme的实现,也就是说ContextImpl本身也是有Theme的实现,它提供的Theme是整个APP的Theme,而这里扩展了之后,支持了Theme的替换之后,在不同的页面支持了不同的Theme设置。




  2. Context作为应用程序环境和运行时状态的信息,设计初衷上它应该是固定的,在创建成功之后就禁止改变,所以在ContextWrapper.attachBaseContext()中设置了拦截,只允许设置一次baseContext,重新设置会抛出异常。但是在一些特殊的场景中,比如跨页面使用View,或者提前创建View的时候,其实会有场景涉及替换Context。另一个坑是ContextWrapper限制baseContext只允许系统调用。不过在SDK31中,官方提供了一个特殊版本的ContextWrapper,也就是MutableContextWrapper,支持了替换baseContext。




  3. Context设计是很典型的装饰器模式,Context抽象定义了具体的接口;ContextImpl具体实现了Context定义的所有方法;ContextWrapper继承了Context接口,并包装了具体实现ContextImpl;ContextThemeWrapper继承了ContextWrapper并扩展了替换Theme的功能。




5. 附录


装饰器模式是一种结构型设计模式,它允许我们在运行时动态地为一个对象添加新的行为,而无需修改其源代码。装饰器模式通过将对象包装在一个装饰器对象中,来增加对象的功能。装饰器模式是一种非常灵活的模式,它可以在不改变原始对象的情况下,动态地添加新的行为和功能。


装饰器模式的核心思想是将对象包装在一个或多个装饰器对象中,这些装饰器对象具有与原始对象相同的接口,可以在不改变原始对象的情况下,为其添加新的行为。装饰器对象可以嵌套在一起,形成一个链式结构,从而实现更复杂的功能。


装饰器模式的结构由四个基本元素组成:




  1. 抽象组件(Component):定义了一个对象的基本接口,可以是一个抽象类或接口。




  2. 具体组件(ConcreteComponent):实现了抽象组件接口,是被装饰的对象。




  3. 抽象装饰器(Decorator):继承或实现了抽象组件接口,用于包装具体组件或其他装饰器。




  4. 具体装饰器(ConcreteDecorator):继承或实现了抽象装饰器接口,实现了具体的装饰逻辑。




装饰器模式的优点在于:




  1. 可以动态地为对象添加新的行为,无需修改其源代码。




  2. 可以嵌套多个装饰器对象,形成一个链式结构,从而实现更复杂的功能。




  3. 装饰器对象与原始对象具有相同的接口,可以完全替代原始对象。




装饰器模式的缺点在于:




  1. 可能会导致类的数量增加,增加代码的复杂度。




  2. 在装饰器链中,有些装饰器可能不被使用,但仍然需要创建和维护,浪费资源。


作者:bufan
链接:https://juejin.cn/post/7224433996137431101
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 官方项目是怎么做模块化的?快来学习下

概述 模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。 模块化的好处 模块化有以下好处: 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。 支持并行工...
继续阅读 »

概述


模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。


模块化的好处


模块化有以下好处:



  • 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。

  • 支持并行工作:模块化有助于减少代码冲突,为大型团队中的开发人员提供更高效的并行工作。

  • 所有权:一个模块可以有一个专门的 owner,负责维护代码和测试、修复错误和审查更改。

  • 封装:独立的代码更容易阅读、理解、测试和维护。

  • 减少构建时间:利用 Gradle 的并行和增量构建可以减少构建时间。

  • 动态交付:模块化是 Play 功能交付 的一项要求,它允许有条件地交付应用程序的某些功能或按需下载。

  • 可重用性:模块化为代码共享和构建多个应用程序、跨不同平台、从同一基础提供了机会。


模块化的误区


模块化也可能会被滥用,需要注意以下问题:



  • 太多模块:每个模块都有其成本,如 Gradle 配置的复杂性增加。这可能会导致 Gradle 同步及编译时间的增加,并产生持续的维护成本。此外,与单模块相比,添加更多模块会增加项目 Gradle 设置的复杂性。这可以通过使用约定插件来缓解,将可重用和可组合的构建配置提取到类型安全的 Kotlin 代码中。在 Now in Android 应用程序中,可以在 build-logic文件夹 中找到这些约定插件。

  • 没有足够的模块:相反,如果你的模块很少、很大并且紧密耦合,最终会产生另外的大模块。这将失去模块化的一些好处。如果您的模块臃肿且没有单一的、明确定义的职责,您应该考虑将其进一步拆分。

  • 太复杂了:模块化并没有灵丹妙药 -- 一种方案解决所有项目的模块化问题。事实上,模块化你的项目并不总是有意义的。这主要取决于代码库的大小和相对复杂性。如果您的项目预计不会超过某个阈值,则可扩展性和构建时间收益将不适用。


模块化策略


需要注意的是没有单一的模块化方案,可以确保其对所有项目都适用。但是,可以遵循一般准则,可以尽可能的享受其好处并规避其缺点。


这里提到的模块,是指 Android 项目中的 module,通常会包含 Gradle 构建脚本、源代码、资源等,模块可以独立构建和测试。如下:


一般来说,模块内的代码应该争取做到低耦合、高内聚。



  • 低耦合:模块应尽可能相互独立,以便对一个模块的更改对其他模块的影响为零或最小。他们不应该了解其他模块的内部工作原理。

  • 高内聚:一个模块应该包含一组充当系统的代码。它应该有明确的职责并保持在某些领域知识的范围内。例如,Now in Android 项目中的core-network模块负责发出网络请求、处理来自远程数据源的响应以及向其他模块提供数据。


Now in Android 项目中的模块类型



注:模块依赖图(如下)可以在模块化初期用于可视化各个模块之间的依赖关系。



modularization-graph.png


Now in Android 项目中有以下几种类型的模块:



  • app 模块: 包含绑定其余代码库的应用程序级和脚手架类,app例如和应用程序级受控导航。一个很好的例子是通过导航设置和底部导航栏设置。该模块依赖于所有模块和必需的模块。

  • feature- 模块: 功能特定的模块,其范围可以处理应用程序中的单一职责。这些模块可以在需要时被任何应用程序重用,包括测试或其他风格的应用程序,同时仍然保持分离和隔离。如果一个类只有一个feature模块需要,它应该保留在该模块中。如果不是,则应将其提取到适当的core模块中。一个feature模块不应依赖于其他功能模块。他们只依赖于core他们需要的模块。

  • core-模块:包含辅助代码和特定依赖项的公共库模块,需要在应用程序中的其他模块之间共享。这些模块可以依赖于其他核心模块,但它们不应依赖于功能模块或应用程序模块。

  • 其他模块 - 例如和模块syncbenchmark、 test以及 app-nia-catalog用于快速显示我们的设计系统的目录应用程序。


项目中的主要模块


基于以上模块化方案,Now in Android 应用程序包含以下模块:



































































模块名职责关键类及核心示例
app将应用程序正常运行所需的所有内容整合在一起。这包括 UI 脚手架和导航。NiaApp, MainActivity 应用级控制导航通过 NiaNavHost, NiaTopLevelNavigation
feature-1, feature-2 ...与特定功能或用户相关的功能。通常包含从其他模块读取数据的 UI 组件和 ViewModel。如:feature-author在 AuthorScreen 上显示有关作者的信息。feature-foryou它在“For You” tab 页显示用户的新闻提要和首次运行期间的入职。AuthorScreen AuthorViewModel
core-data保存多个特性模块中的数据。TopicsRepository AuthorsRepository
core-ui不同功能使用的 UI 组件、可组合项和资源,例如图标。NiaIcons NewsResourceCardExpanded
core-common模块之间共享的公共类。NiaDispatchers Result
core-network发出网络请求并处理对应的结果。RetrofitNiANetworkApi
core-testing测试依赖项、存储库和实用程序类。NiaTestRunner TestDispatcherRule
core-datastore使用 DataStore 存储持久数据。NiaPreferences UserPreferencesSerializer
core-database使用 Room 的本地数据库存储。NiADatabase DatabaseMigrations Dao classes
core-model整个应用程序中使用的模型类。Author Episode NewsResource
core-navigation导航依赖项和共享导航类。NiaNavigationDestination

Now in Android 的模块化


Now in Android 项目中的模块化方案是在综合考虑项目的 Roadmap、即将开展的工作和新功能的情况下定义的。Now in Android 项目的目标是提供一个接近生产环境的大型 App 的模块化方案,并且要让方案看起来并没有过度模块化,希望是在两者之间找到一种平衡。


这种方法与 Android 社区进行了讨论,并根据他们的反馈进行了改进。这里并没有一个绝对的正确答案。归根结底,模块化 App 有很多方法和方法,没有唯一的灵丹妙药。这就需要在模块化之前考虑清楚目标、要解决的问题已经对后续工作的影响,这些特定的情况会决定模块化的具体方案。可以绘制出模块依赖关系图,以便帮助更好地分析和规划。


这个项目就是一个示例,并不是一个需要固守不可改变固定结构,相反而是可以根据需求就行变化的。根据 Now in Android 这是我们发现最适合我们项目的一般准则,并提供了一个示例,可以在此基础上进一步修改、扩展和构建。如果您的数据层很小,则可以将其保存在单个模块中。但是一旦存储库和数据源的数量开始增长,可能值得考虑将它们拆分为单独的模块。


最后,官方对其他方式的模块化方案也是持开发态度,有更好的方案及建议也可以反馈出来。


总结


以上内容是根据 Modularization learning journey 翻译整理而得。整体上是提供了一个示例,对一些初学者有一个可以参考学习的工程,对社区中模块化开发起到的积极的作用。说实话,这部分技术在国内并不是什么新技术了。


下面讲一个我个人对这个模块化方案的理解,以下是个人观点,请批判性看待。


首先是好的点提供了通用的 Gradle 配置,简化了各个模块的配置步骤,各种方式预计会在之后的一些项目中流行开来。


不足的点就是没有明确模块化的整体策略,是应采取按照功能还是按照特性分,类似讨论还有我们平时的类文件是按照功能来分还是特性来分,如下是按照特性区分:


# DO,建议方式
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3

按照功能区分的方式大致如下:


# DO NOT,不建议方式
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data

我个人是倾向去按照特性的方式区分,而示例中看上去是偏后者,或者是一个混合体,比如有的模块是添加 feature 前缀的,但是 core-model 模块又是在统一的一个模块中集中管理。个人建议的方式应该是将各个模块中各自使用的模型放到自己的模块中,否则项目在后续进行组件化时将会遇到频繁发版的问题。当然,这种方式在模块化的阶段并没有什么大问题。


模块化之后就是组件化,组件化之后就是壳工程,每个技术阶段对应到团队发展的阶段,有机会的话后面可以展开聊聊。


作者:madroid
链接:https://juejin.cn/post/7128069998978793509
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Android】书客编辑器安卓Java版

书客编辑器是一款基于Markdown标记语言的开源的富文本编辑器,它以简易的操作界面和强大的功能深受广大开发者的喜爱。正如官方所说:现在的版本不一定是最好的版本,却是最好的开源版本。官方地址:editor.ibooker.cc。 下面针对书客编辑器安卓Java...
继续阅读 »

书客创作


书客编辑器是一款基于Markdown标记语言的开源的富文本编辑器,它以简易的操作界面和强大的功能深受广大开发者的喜爱。正如官方所说:现在的版本不一定是最好的版本,却是最好的开源版本。官方地址:editor.ibooker.cc


下面针对书客编辑器安卓Java版,进行详解说明。


效果图


在进行讲解之前,首先看一下书客编辑器安卓版的效果图:


书客编辑器安卓版效果图


一、引入资源


引入书客编辑器安卓Java版的方式有很多,这里主要提供两种方式:


1、在build.gradle文件中添加以下代码:


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

dependencies {
compile 'com.github.zrunker:IbookerEditorAndroid:v1.0.1'
}

2、在maven文件中添加以下代码:


<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>

<dependency>
<groupId>com.github.zrunker</groupId>
<artifactId>IbookerEditorAndroid</artifactId>
<version>v1.0.1</version>
</dependency>

二、使用


书客编辑器安卓版简易所在就是只需要简单引入资源之后,可以直接进行使用。因为书客编辑器安卓版不仅仅提供了功能实现,还提供了界面。所以使用过程中,连界面绘制都不用了。


界面分析


书客编辑器安卓版界面大致分为三个部分,即编辑器顶部,内容区(编辑区+预览区)和底部(工具栏)。


书客编辑器安卓-布局轮廓图


首先在布局文件中引入书客编辑器安卓版控件,如布局文件为activity_main.xml,只需要在该文件内添加以下代码即可:


<?xml version="1.0" encoding="utf-8"?>
<cc.ibooker.ibookereditorlib.IbookerEditorView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ibookereditorview"
android:layout_width="match_parent"
android:layout_height="match_parent" />

实际上IbookerEditorView继承LinearLayout,所以它具备LinearLayout的一切功能。


三、功能介绍


根据轮廓图可以看出,书客编辑器安卓版布局只有三个部分,所以关于书客编辑器安卓版功能模块也就分三个部分对外提供使用,即修改哪一个布局模块就是对于哪一个功能模块。


顶部功能模块


书客编辑器安卓版顶部实际上是采用IbookerEditorTopView控件进行呈现,所以要实现顶部相关控件功能首先要获取该控件。


书客编辑器安卓版顶部


书客编辑器安卓版顶部界面图,从左到右分别对应返回(back),撤销(undo),重做(redo),编辑模式(edit),预览模式(preview),帮助(help),关于(about)。知道每个按钮对应的功能,所以就可以去修改或完善相关实现过程。


例如修改返回按钮一些属性,可以使用一下代码:


// 设置书客编辑器顶部布局相关属性
ibookerEditorView.getIbookerEditorTopView()
.setBackImgVisibility(View.VISIBLE)
.setBackImageResource(R.mipmap.ic_launcher);

当然也可以通过IbookerEditorTopView获取相关控件,然后针对该控件进行逐一处理:


ibookerEditorView.getIbookerEditorTopView()
.getBackImg()
.setVisibility(View.VISIBLE);

这里只是使用返回按钮进行举例说,其他按钮使用规则更返回按钮一样。


中间功能模块


书客编辑器安卓版中间区域又分为两个部分,分别是编辑部分和预览部分,所以要修改相关功能就要获取到相关部分的控件。其中编辑部分由IbookerEditorEditView控件进行呈现,预览部分由IbookerEditorPreView控件进行呈现。


例如修改编辑部分相关属性,可以使用如下代码:


// 设置书客编辑器中间布局相关属性
ibookerEditorView.getIbookerEditorVpView().getEditView()
.setIbookerEdHint("书客编辑器")
.setIbookerBackgroundColor(Color.parseColor("#DDDDDD"));

编辑部分并不是只有一个控件,所以也可以获取相关控件,然后针对特定控件进行逐一操作:


ibookerEditorView.getIbookerEditorVpView()
.getEditView()
.getIbookerEd()
.setText("书客编辑器");

// 执行预览功能
ibookerEditorView.getIbookerEditorVpView()
.getPreView()
.ibookerHtmlCompile("预览内容");

底部功能模块


书客编辑器安卓版,底部为工具栏,由IbookerEditorToolView进行呈现。


工具栏一共提供了30多种功能,每一个按钮对应一个功能。各个控件分别为:


boldIBtn, italicIBtn, strikeoutIBtn, underlineIBtn, capitalsIBtn, 
uppercaseIBtn, lowercaseIBtn, h1IBtn, h2IBtn,
h3IBtn, h4IBtn, h5IBtn, h6IBtn, linkIBtn, quoteIBtn,
codeIBtn, imguIBtn, olIBtn, ulIBtn, unselectedIBtn,
selectedIBtn, tableIBtn, htmlIBtn, hrIBtn, emojiIBtn;

所以要修改底部相关属性,首先要获取到IbookerEditorToolView控件,然后对该控件进行操作。


// 设置书客编辑器底部布局相关属性
ibookerEditorView.getIbookerEditorToolView()
.setEmojiIBtnVisibility(View.GONE);

当然底部一共有30多个控件,也可以直接获取到相关控件,然后该控件进行操作,如:


ibookerEditorView.getIbookerEditorToolView().getEmojiIBtn().setVisibility(View.GONE);

补充功能:按钮点击事件监听


这里的按钮点击事件监听主要是针对顶部布局按钮和底部布局按钮。


顶部部分按钮点击事件监听,需要实现IbookerEditorTopView.OnTopClickListener接口,而每个按钮点击通过对应Tag来判断,具体代码如下:


// 顶部按钮点击事件监听
@Override
public void onTopClick(Object tag) {
if (tag.equals(IMG_BACK)) {// 返回
} else if (tag.equals(IBTN_UNDO)) {// 撤销
} else if (tag.equals(IBTN_REDO)) {// 重做
} else if (tag.equals(IBTN_EDIT)) {// 编辑
} else if (tag.equals(IBTN_PREVIEW)) {// 预览
} else if (tag.equals(IBTN_HELP)) {// 帮助
} else if (tag.equals(IBTN_ABOUT)) {// 关于
}
}

其中IMG_BACK、IBTN_UNDO等变量是由IbookerEditorEnum枚举类提供。


底部部分按钮点击事件监听,需要实现IbookerEditorToolView.OnToolClickListener接口,而每个按钮点击通过对应Tag来判断,具体代码如下:


// 工具栏按钮点击事件监听
@Override
public void onToolClick(Object tag) {
if (tag.equals(IBTN_BOLD)) {// 加粗
} else if (tag.equals(IBTN_ITALIC)) {// 斜体
} else if (tag.equals(IBTN_STRIKEOUT)) {// 删除线
} else if (tag.equals(IBTN_UNDERLINE)) {// 下划线
} else if (tag.equals(IBTN_CAPITALS)) {// 单词首字母大写
} else if (tag.equals(IBTN_UPPERCASE)) {// 字母转大写
} else if (tag.equals(IBTN_LOWERCASE)) {// 字母转小写
} else if (tag.equals(IBTN_H1)) {// 一级标题
} else if (tag.equals(IBTN_H2)) {// 二级标题
} else if (tag.equals(IBTN_H3)) {// 三级标题
} else if (tag.equals(IBTN_H4)) {// 四级标题
} else if (tag.equals(IBTN_H5)) {// 五级标题
} else if (tag.equals(IBTN_H6)) {// 六级标题
} else if (tag.equals(IBTN_LINK)) {// 超链接
} else if (tag.equals(IBTN_QUOTE)) {// 引用
} else if (tag.equals(IBTN_CODE)) {// 代码
} else if (tag.equals(IBTN_IMG_U)) {// 图片
} else if (tag.equals(IBTN_OL)) {// 数字列表
} else if (tag.equals(IBTN_UL)) {// 普通列表
} else if (tag.equals(IBTN_UNSELECTED)) {// 复选框未选中
} else if (tag.equals(IBTN_SELECTED)) {// 复选框选中
} else if (tag.equals(IBTN_TABLE)) {// 表格
} else if (tag.equals(IBTN_HTML)) {// HTML
} else if (tag.equals(IBTN_HR)) {// 分割线
}
}

其中IBTN_BOLD、IBTN_ITALIC等变量是由IbookerEditorEnum枚举类提供。


Github地址
阅读原文




微信公众号:书客创作


作者:非言
链接:https://juejin.cn/post/7110187212729221156
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

常用到的几个Kotlin开发技巧,减少对业务层代码的入侵

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。 善用@get/@set: JvmName()注解并搭配setter/getter使用 假设当前存在下面三个类代码: #Opt1 public cl...
继续阅读 »

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。



善用@get/@set: JvmName()注解并搭配setter/getter使用


假设当前存在下面三个类代码:


#Opt1


public class Opt1 {

private String mContent;

public String getRealContent() {
return mContent;
}

public void setContent(String mContent) {
this.mContent = mContent;
}
}

#Opt2


public class Opt2 {

public void opt2(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

@Opt3


public class Opt3 {

public void opt3(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

这个时候我想将Opt1类重构成kotlin,我们先看下通过AS的命令Convert Java File to Kotlin File自动转换的结果:


image.png


可以看到为了兼容Opt2Opt3的调用,直接把我的属性名给改成了realContent,kotlin会自动生成getRealContent()setRealContent()方法,这样Opt2Opt3就不用进行任何调整了,kotlin这样就显得太过于智能了。


这样看起来没啥问题,但是java重构kotlin,直接把属性名给我改了,并隐式生成了属性的set和get方法,对于java而言不使用的方法会报灰提示或者只有当前类使用AS会警告可以声明成private,但是对于kotlin生成的set、get方法是隐式的,容易忽略。


所以大家在使用Convert Java File to Kotlin File命令将java重构kotlin的结果一定不能抱有百分之百的信任,即使它很智能,但还是一定要细细的看下转换后的代码逻辑,可能还有不少的优化空间。


这个地方就得需要我们手动进行修改了,比如不想对外暴露修改这个字段的set方法,调整如下:


class Opt1 {
var realContent: String? = null
private set
}

再比如保持原有的字段名mContent,不能被改为realContent,同时又要保证兼容Opt2Opt3类的调用不能报错,且尽量避免去修改里面的代码,我们就可以做如下调整:


class Opt1 {
@get: JvmName("getRealContent")
var mContent: String? = null
private set
}

善用默认参数+@JvmOverloads减少模板代码编写


假设当前Opt1有下面的方法:


public String getSqlCmd(String table) {
return "select * from " + table;
}

且被Opt2Opt3进行了调用,这个时候如果有另一个类Opt3想要调用这个函数并只想从数据库查询指定字段,如果用java实现有两种方式:



  1. 直接在getSqlCmd()方法中添加一个查询字段参数,如果传入的值为null,就查询所有的字段,否则就查询指定字段:


public String getSqlCmd(String table, String name) {
if (TextUtils.isEmpty(name)) {
return "select * from " + table;
}
return "select " + name + " from " + table;
}

这样一来,是不是原本Opt2Opt3getSqlCmd()方法调用是不是需要改动,多传一个参数给方法,而在日常的项目开发中,有可能这个getSqlCmd()被几十个地方调用,难道你一个个的改过去?不太现实且是一种非常糟糕的实现。



  1. 直接在Opt1中新增一个getSqlCmd()的重载方法,传入指定的字段去查询:


public String getSqlCmd(String table,String name) {
return "select " + name + " from " + table;
}

这样做的好处就是不用调整Opt2Opt3getSqlCmd(String table)方法调用逻辑,但是会编写很多模板代码,尤其是getSqlCmd()这个方法体可能七八十行的情况下。


如果Opt1类代码减少即200-400行且不负责的情况下,我们可以将其重构成kotlin,借助于默认参数来实现方法功能增加又不用编写模板代码的效果(如果你的Java类上千行又很复杂,请谨慎转换成kotlin使用下面这种方式)。


@JvmOverloads
fun getSqlCmd(table: String, name: String? = null): String {
return "select ${if (name.isNullOrEmpty()) "*" else name} from $table"
}

添加默认参数name时还要添加@JvmOverloads注解,这样是为了保证java只传一个table参数也能正常调用。


通过上面这种方式,我们就能保证实现了方法功能增加,又不用改动Opt2Opt3对于getSqlCmd()方法的调用逻辑,并且还不用编写额外的模板代码,一举多得。


总结


本篇文章主要介绍了在java重构成kotlin过程中比较常用到的两个技巧,最终实现效果是减少对业务逻辑代码的入侵,希望能对你有所帮助。


作者:长安皈故里
链接:https://juejin.cn/post/7148823027608715295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用力一瞥Android渲染机制-黄油计划

一. 渲染基本概念 对于渲染来说在开始前我们先了解几个概念: CPU主要负责包括 Measure,Layout,Record,Execute 的计算操作。 GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(...
继续阅读 »

一. 渲染基本概念


对于渲染来说在开始前我们先了解几个概念:


CPU主要负责包括 MeasureLayoutRecordExecute 的计算操作。


GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(像素)以用于显示设备输出的过程,简单来说就是将我们要显示的视图,转换成用像素来表示的格式。


帧率代表了GPU在一秒内绘制操作的帧数。


刷新率代表了屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ。


二. Android黄油计划


涉及到滑动流畅,Android在谷歌4.1版本引入了黄油计划。其中有三个重要的核心元素:VSYNC、缓存区和Choreographer:


2.1 VSYNC信号


在Android4.0的时候,CPU可能会因为在忙其他的事情,导致没来得及处理UI绘制。为了解决这个问题,设计成系统在收到VSYN信号后,才会开始下一帧的渲染。也就是收到VSYN通知,CPU和GPU才开始计算然后把数据写入buffer中。


VSYN信号是由屏幕产生的,并且以60fps的固定频率发送给Android系统,在Android系统中的SurfaceFlinger接收发送的Vsync信号。当屏幕从缓存区扫描完一帧到屏幕上之后,开始扫描下一帧之前,发出的一个同步信号,该信号用来切换前缓冲区和后缓冲区。


在引入了Vsyn信号之后,绘制就变成了:


image.png


可以看到渲染的时候从第0帧开始,CPU开始准备第一帧的图形处理,好了才交给GPU进行处理,再上一帧到来之后,CPU就会开始第二帧的处理,基本上跟Vsync的信号保持同步。


有了Vsync机制,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。


2.2 三重缓存


在采用双缓冲机制的时候,也意味着有两个缓存区,分别是让绘制和显示器拥有各自的buffer,GPU使用Back Buffer进行一帧图像数据写入,显示器则是用Frame Buffer,一般来说CPU和GPU处理数据的速度视乎都能在16ms内完成,而且还有时间空余。但是一旦界面比较复杂的情况,CPU/GPU的处理时间超过了16ms,双缓冲开始失效了:


image.png


在第二个时间段内,因为GPU还是处理B帧,数据没有及时交换,导致继续系那是之前A缓存区中的内容。


在B帧完成之后,又因为缺少了Vusnc信号,只能等待一段时间。


直到下一个Vsync信号出现的时候,CPU/GPU才开始马上执行,由于执行时间仍然超过了16ms,导致下一次应该执行的缓存区交换又被推迟了,反复这种情形,就会出越来越多的jank。


为了解决这个问题,Android 4.1才引入了三缓冲机制:在双缓冲机制的基础上增加了一个Graohic Buffer缓冲区,这样就可以最大限度的利用空闲的时间。


image.png


可以看到在第二个时间段里有了区别,在第一次Vsync发生之后,CPU不用再等待了,它会使用第三个bufferC来进行下一帧的准备工作。整个过程就开始的时候卡顿了一下,后面还是很流畅的。但是GPU需要跨越两个Vsync信号才能显示,这样就还是会有一个延迟的现象。


总的来说三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了lag。


2.3 Choreographer


在了解了Vsync机制后,上层又是如何接受这个Vsync信号的?


Google为上层设计了一个Choreographer类,翻译成中文是“编舞者”,是希望通过它来控制上层的绘制(舞蹈)节奏。


可以直接从其构造函数开始看起:


private Choreographer(Looper looper, int vsyncSource) {
//创建Looper对象
mLooper = looper;
//接受处理消息
mHandler = new FrameHandler(looper);
//用来接受垂直同步脉冲,也就是Vsync信号
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
//计算下一帧的时间,Androoid手机屏幕是60Hz的刷新频率
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//初始化CallbackQueue,将在下一帧开始渲染时回调
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}

主要来看下FrameHandlerFrameDisplayEventReceiver的数据结构:


private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//开始渲染下一帧的操作
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
//请求Vsync信号
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
//请求执行Callback
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}

FrameHandler可以看到对三种消息进行了处理,对其具体实现一会分析。


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
}

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {

......
mTimestampNanos = timestampNanos;
mFrame = frame;
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

可以看出来这个类主要是用来接收底层的VSync信号开始处理UI过程。而Vsync信号是由SurfaceFlinger实现并定时发送,接收到之后就会调用onVsync方法,在里面进行处理消息发送到主线程处理,另外在run()方法里面执行了doFrame(),这也是接下来要关注的重点方法。


2.3.1 Choreographer执行过程



ViewRootImpl 中调用 Choreographer 的 postCallback 方法请求 Vsync 并传递一个任务(事件类型是 Choreographer.CALLBACK_TRAVERSAL)



最开始执行的是postCallBack发起回调,这个FrameCallback将会在下一帧渲染时执行。而其内部又调用了postCallbackDelayed方法,在其中又调用了postCallbackDelayedInternal方法:


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
......
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

在这里执行了时间的计算,如果立即就会调用scheduleFrameLocked方法,不然就会延迟发送一个MSG_DO_SCHEDULE_CALLBACK消息,并且在这里使用msg.setAsynchronous(true)讲消息设置成异步。、


而所对应的mHandle也就是之前的FrameHandler,根据消息类型MSG_DO_SCHEDULE_CALLBACK,最终会调用到doScheduleCallback方法:


void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

到了这一步看到还是会调用到scheduleFrameLocked方法。


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//开启了Vsync
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}


if (isRunningOnLooperThreadLocked()) {
//申请Vsync信号
scheduleVsyncLocked();
} else {
//最终还是会调用到scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//如果没有直接使用Vsync的话,则直接通过该消息执行doFrame
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

在这里对是否使用Vsync信号进行处理,如果没有使用则直接通过消息执行doFrame。如果使用的就会先判断是否在当前Looper线程中运行,如果在的话就会请求Vsync信号,否则发送消息到 FrameHandler。直接来看下scheduleVsyncLocked方法:


 private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

可以看到调用了FrameDisplayEventReceiverscheduleVsync方法,通过查找在其父类DisplayEventReceiver中找到了scheduleVsync方法:


public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
//申请VSYNC信号,会回调onVsunc方法
nativeScheduleVsync(mReceiverPtr);
}
}

scheduleVsync()就是使用native方法nativeScheduleVsync()去申请VSYNC信号。等下一次信号接收后会调用dispatchVsync 方法:


private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) {
onVsync(timestampNanos, physicalDisplayId, frame);
}

这个onVsync方法最终实现也就是在FrameDisplayEventReceiver里。可以知道最终还是走到了doFrame方法里。


void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
......
//设置当前frame的Vsync信号到来时间
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//时间差大于一个时钟周期,认为跳frame
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//跳frame数大于默认值,打印警告信息,默认值为30
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
//计算实际开始当前frame与时钟信号的偏差值
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}

//修正偏差值,忽略偏差,为了后续更好地同步工作
frameTimeNanos = startNanos - lastFrameOffset;
}

//若时间回溯,则不进行任何工作,等待下一个时钟信号的到来
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
//请求下一次时钟信号
scheduleVsyncLocked();
return;
}

......

//记录当前frame信息
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
//记录上一次frame开始时间,修正后的
mLastFrameTimeNanos = frameTimeNanos;
}

try {
//执行相关callBack
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

doFrame方法对当前帧的运行时间进行了一系列判断和修正,最终顺序执行了五种事件回调。



  1. CALLBACK_INPUT:输入

  2. CALLBACK_ANIMATION:动画

  3. CALLBACK_INSETS_ANIMATION:插入更新的动画

  4. CALLBACK_TRAVERSAL:遍历,执行measure、layout、draw

  5. CALLBACK_COMMIT:遍历完成的提交操作,用来修正动画启动时间


接着就会执行doCallbacks方法:


void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
......
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
//迭代执行所有队列任务
for (CallbackRecord c = callbacks; c != null; c = c.next) {
.....
//调用CallbackRecord内的run方法
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

主要是去遍历CallbackRecrd,执行所有任务:


private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;

@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}

最终actionrun方法会被执行,这里的action也就是我们在前面调用psetCallback传进来的,也就是 ViewRootImpl 发起的绘制任务mTraversalRunnable了。


然后这里又一次调用了doFrame方法,在啥时候token会是FRAME_CALLBACK_TOKEN呢? 可以发现在我们调用postFrameCallback内部会调用postCallbackDelayedInternal进行赋值:


 public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

ChoreographerpostFrameCallback()通常用来计算丢帧情况。


知道了Choreographer是上层用来接收VSync的角色之后,我们需要进一步了解VSync信号是如何控制上层的绘制的。而绘制UI的起点是View的requestLayout或者是invalidate方法被调用触发,好了时间不早了,这些就放在下一篇Android的屏幕刷新机制里解释吧。(刷新流程和同步屏障)


三. 小结


Android在黄油计划中引入了三个核心元素:VSYNCTriple BufferChoreographer


VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。


三重缓存机制(Triple Buffer) 利用 CPU/GPU 的空闲等待时间提前准备好数据,有效的提升了渲染性能。


又介绍了 Choreographer ,它实现了协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。


参考


Android 显示刷新机制、VSYNC和三重缓存机制


Android图形显示系统(一)


Android屏幕刷新机制


Android Choreographer 源码分析


“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!


作者:罗恩不带土
链接:https://juejin.cn/post/7149838503973830692
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

代码重构和架构重构:你需要了解的区别

1 代码重构 定义 对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。 目的 增加可读性、增加可维护性、可扩展性 3 关键点 不影响输出 不修正错误 不增加新的功能性 代码重构时,发现有个功能实现逻辑不合理,可直接修改吗? 当然不可! 2 架构...
继续阅读 »

1 代码重构


定义


对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。


目的


增加可读性、增加可维护性、可扩展性


3 关键点



  • 不影响输出

  • 不修正错误

  • 不增加新的功能性


代码重构时,发现有个功能实现逻辑不合理,可直接修改吗?


当然不可!


2 架构重构


定义


通过整系统结构(4R)来修复系统质量问题而不影响整体系统能力。


目的


修复质量问题(性能、可用性、可扩展......)


关键点



  • 修复质量(架构,而非代码层面的质量)问题,提升架构质量

  • 不影响整体系统功能

  • 架构本质没有发生变化


把某个子系统的实现方式从硬编码改为规则引擎,是代码重构还是架构重构?


属于架构重构,架构设计方案了,实现系统可扩展性。


3 代码重构 V.S 架构重构



4 架构重构技巧


4.0 手段



架构重构是否可以修改 4R 中的 Rank?


不能!修改 rank 就不是重构,而是演进了。拆微服务不属于改 rank。外部系统协作方式都得修改了。比如将淘宝的支付方式支付宝拆出来,成为支付宝公司了。


4.1 先局部优化后架构重构


局部优化


定义:对部分业务或者功能进行优化,不影响系统架构。


常见手段:



  • 数据库添加索引,优化索引

  • 某个数据缓存更新策略采用后台更新

  • 增加负载均衡服务数量

  • 优化代码里面并发的逻辑

  • 修改Innodb buffer pool 配置,分配更多内存

  • 服务间的某个接口增加1个参数


架构重构


定义:优化系统架构,整体提升质量,架构重构会影响架构的4R定义。


常见手段:



  • 引入消息队列(增加 Role )

  • 去掉 ZooKeeper,改为内置 Raft 算法实现(删除 Role)

  • 将 Memcached 改为 Redis( 改变 Role)

  • 按照稳定性拆分微服务( 拆分 Role )

  • 将粒度太细的微服务合并(合并 Role)

  • 将服务间的通信方式由 HTTP 改为 gRPC(修改 Relation )

  • SDK从读本地配置文件改为从管理系统读取配置(修改Rule )


4.2 有的放矢



案例




  • 开发效率很慢,P业务和M系统互相影响

  • 线上问题很多,尤其是数据类问题

  • M系统性能很低


有的放矢:



重构只解决第1个问题(开发效率很慢,P业务和M系统互相影响)。其他问题咋办,架构师你不解决了吗?架构重构后了,各个业务部门再解决各自的问题,如 P业务后台优化自己的问题,M 系统优化自己的性能问题,因为这些问题本身靠重构是解决不了的,而是要靠重构拆分之后,各自再继续优化。


4.3 合纵连横


合纵


说服业务方和老板




  1. 以数据说话


    把“可扩展性”转换为“版本开发速度很慢然后给出对应的项目数据(平时注意搜集数据)。




  2. 以案例说话(其实更有效,给人的冲击力更明显) 若没有数据,就举极端案例,如某个小功能,开发测试只要5天,但是等了1个月才上线。




连横


说服其它团队。



  1. 换位思考 思考对其它团队的好处,才能让人配合。

  2. 合作双赢 汇报和总结的时候,把其它团队也带上。


案例


合纵:告诉PM和项目经理极端案例,设计2周、开发2天、一个月才上线。


连横:P业务线上问题大大减少,P业务不会被其它业务影响


4.4 运筹帷幄


① 问题分类


将问题分类,一段时间集中处理类问题。 避免对照 Excel表格,一条条解决。


② 问题排序


分类后排序,按照优先级顺序来落地。


避免见缝插针式的安排重构任务,不要搭业务的顺风车重构:



  • 避免背锅

  • 效果不明显

  • 无法安排工作量大的重构


③ 逐一攻破


每类问题里面先易后难。


把容易的问题解决掉,增强信心。


④ 案例


Before:



  • 1个100多行的Excel问题表格,一个一个的解决

  • 专挑软柿子捏

  • 见缝插针


After:



  1. 分类:性能、组件、架构、代码

  2. 分阶段: 优化-> 架构重构 -> 架构演进

  3. 专项落地: 明确时间、目标、版本



5 架构重构FAQ


架构重构是否可以引入新技术?


可以,但尽量少,架构重构要求快准


业务不给时间重构怎么办 ?


会哭的孩了有奶吃。收集数据和案例,事实说话。


其它团队不配合怎么办 ?


学会利用上级力量。上级都不支持,说明你做的这个没意义,所以领导也不在乎。那就别做了。


业务进度很紧,人力不够怎么办 ?


收集需要重构的证据,技术汇报的时候有理有据



6 测试


6.1 判断



  1. 代码重构、架构重构、架构演进都不需要去修复问题 ×

  2. 微服务拆分既可以是架构重构的手段,也可以是架构演进的手段 √

  3. 架构重构应该搭业务版本的便车,可以避免对业务版本有影响 ×

  4. 架构重构是为修复问题,因此应该将系统遗留的问题都在架构重构的时候修复 ×

  5. 架构重构应该分门别类,按照优先级逐步落地 √


6.2 思考


架构重构的时候是否可以顺手将代码重构也做了 ? 因为反正都安排版本了。No!


局部优化不属于代码/架构重构。


作者:JavaEdge在掘金
链接:https://juejin.cn/post/7222288378459848762
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Redis 使用zset做消息队列总结

1.zset为什么可以做消息队列 zset做消息队列的特性有: 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。 元素唯一性:zset的每个元素都是独一无二的...
继续阅读 »

1.zset为什么可以做消息队列


zset做消息队列的特性有:



  1. 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。

  2. 元素唯一性:zset的每个元素都是独一无二的,这对于实现某些消息需求(比如幂等性)是非常有帮助的。

  3. 成员和分数之间的映射关系:有序集合中的每个成员都有一个分数,这样就可以将相同的数据划分到不同的 queue 中,以及为每个 queue 设置不同的延时。

  4. 高效的添加删除操作:因为zset会自动维护元素之间的顺序,所以在添加或删除元素时无需进行手动排序,从而能提升操作速度。


Redis的zset天然支持按照时间顺序的消息队列,可以利用其成员唯一性的特性来保证消息不被重复消费,在实现高吞吐率等方面也有很大的优势。


2.zset实现消息队列的步骤


Redis的zset有序集合是可以用来实现消息队列的,一般是按照时间戳作为score的值,将消息内容作为value存入有序集合中。


以下是实现步骤:



  1. 客户端将消息推送到Redis的有序集合中。

  2. 有序集合中,每个成员都有一个分数(score)。在这里,我们可以设成消息的时间戳,也就是当时的时间。

  3. 当需要从消息队列中获取消息时,客户端获取有序集合前N个元素并进行操作。一般来说,N取一个适当的数值,比如10。


需要注意的是,Redis的zset是有序集合,它的元素是有序的,并且不能有重复元素。因此,如果需要处理有重复消息的情况,需要在消息体中加入某些唯一性标识来保证不会重复。


3.使用jedis实现消息队列示例


Java可以通过Redis的Java客户端包Jedis来使用Redis,Jedis提供了丰富的API来操作Redis,下面是一段实现用Redis的zset类型实现的消息队列的代码。


import redis.clients.jedis.Jedis;
import java.util.Set;

public class RedisMessageQueue {
  private Jedis jedis; //Redis连接对象
  private String queueName; //队列名字

  /**
    * 构造函数
    * @param host Redis主机地址
    * @param port Redis端口
    * @param password Redis密码
    * @param queueName 队列名字
    */
  public RedisMessageQueue(String host, int port, String password, String queueName){
      jedis = new Jedis(host, port);
      jedis.auth(password);
      this.queueName = queueName;
  }

  /**
    * 发送消息
    * @param message 消息内容
    */
  public void sendMessage(String message){
      //获取当前时间戳
      long timestamp = System.currentTimeMillis();
      //将消息添加到有序集合中
      jedis.zadd(queueName, timestamp, message);
  }

  /**
    * 接收消息
    * @param count 一次接收的消息数量
    * @return 返回接收到的消息
    */
  public String[] receiveMessage(int count){
      //设置最大轮询时间
      long timeout = 5000;
      //获取当前时间戳
      long start = System.currentTimeMillis();

      while (true) {
          //获取可用的消息数量
          long size = jedis.zcount(queueName, "-inf", "+inf");
          if (size == 0) {
              //如果无消息,休眠50ms后继续轮询
              try {
                  Thread.sleep(50);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } else {
              //计算需要获取的消息数量count与当前可用的消息数量size的最小值
              count = (int) Math.min(count, size);
              //获取消息
              Set<String> messages = jedis.zrange(queueName, 0, count - 1);
              String[] results = messages.toArray(new String[0]);
              //移除已处理的消息
              jedis.zremrangeByRank(queueName, 0, count - 1);
              return results;
          }

          //检查是否超时
          if (System.currentTimeMillis() - start > timeout) {
              return null; //超时返回空
          }
      }
  }

  /**
    * 销毁队列
    */
  public void destroy(){
      jedis.del(queueName);
      jedis.close();
  }
}


使用示例:


public static void main(String[] args) {
  //创建消息队列
  RedisMessageQueue messageQueue = new RedisMessageQueue("localhost", 6379, "password", "my_queue");

  //生产者发送消息
  messageQueue.sendMessage("message1");
  messageQueue.sendMessage("message2");

  //消费者接收消息
  String[] messages = messageQueue.receiveMessage(10);
  System.out.println(Arrays.toString(messages)); //输出:[message1, message2]

  //销毁队列
  messageQueue.destroy();
}


在实际应用中,可以结合线程池或者消息监听器等方式,将消息接收过程放置于独立的线程中,以提高消息队列的处理效率。


4.+inf与-inf


+inf 是 Redis 中用于表示正无穷大的一种特殊值,也就是无限大。在使用 Redis 的 zset 集合时,+inf 通常用作 ZREVRANGEBYSCORE 命令的上限值,表示查找 zset 集合中最大的分数值。+inf 后面的 -inf 表示 zset 中最小的分数值。这两个值一起可以用来获取 zset 集合中的所有元素或一个特定范围内的元素。例如:


# 获取 zset 集合中所有元素
ZREVRANGE queue +inf -inf WITHSCORES

# 获取 zset 集合中第1到第10个元素(分数从大到小排列)
ZREVRANGE queue +inf -inf WITHSCORES LIMIT 0 9

# 获取 zset 集合中分数在 1581095012 到当前时间之间的元素
ZREVRANGEBYSCORE queue +inf 1581095012 WITHSCORES

在这些命令中,+inf 代表了一个最大的分数值,-inf 代表了一个最小的分数值,用于确定查询的分数值范围。


5.redis使用list与zset做消息队列有什么区别


Redis 使用 List 和 ZSET 都可以实现消息队列,但是二者有以下不同之处:



  1. 数据结构不同:List 是一个有序的字符串列表,ZSET 则是一个有序集合,它们的底层实现机制不同。

  2. 存储方式不同:List 只能存储字符串类型的数据,而 ZSET 则可以存储带有权重的元素,即除了元素值外,还可以为每个元素指定一个分数。

  3. 功能不同: List 操作在元素添加、删除等方面比较方便,而 ZSET 在处理数据排序和范围查找等方面比 List 更加高效。

  4. 应用场景不同: 对于需要精细控制排序和分值的场景可以选用 ZSET,而对于只需要简单的队列操作,例如先进先出,可以直接采用 List。


综上所述,List 和 ZSET 都可以用于消息队列的实现,但如果需要更好的性能和更高级的排序功能,建议使用 ZSET。而如果只需要简单的队列操作,则 List 更加适合。


6.redis用zset做消息队列会出现大key的情况吗


在Redis中,使用zset作为消息队列,每个消息都是一个元素,元素中有一个分数代表了该消息的时间戳。如果系统中有大量消息需要入队或者大量的不同的队列,这个key的体积会越来越大,从而可能会出现大key的情况。


当Redis存储的某个键值对的大小超过实例的最大内存限制时,会触发Redis的内存回收机制,可以根据LRU算法等策略来选择需要回收的数据,并确保最热数据保持在内存中。如果内存不足,可以使用Redis的持久化机制,将数据写入磁盘。使用Redis集群,并且将数据分片到多个节点上,也是一种可以有效解决大key问题的方法。


针对大key的问题,可以考虑对消息进行切分,将一个队列切分成多个小队列,或者对消息队列集合进行分片,将消息分布到不同的Redis实例上,从而降低单个Redis实例的内存使用,并提高系统的可扩展性。


7.redis 用zset做消息队列如何处理消息积压



  1. 改变消费者的消费能力:


可以增加消费者的数量,或者优化消费者的消费能力,使其能够更快地处理消息。同时,可以根据消息队列中消息的数量,动态地调整消费者的数量、消费速率和优先级等参数。



  1. 对过期消息进行过滤:


将过期的消息移出消息队列,以减少队列的长度,从而使消费者能够及时地消费未过期的消息。可以使用Redis提供的zremrangebyscore()方法,对过期消息进行清理。



  1. 对消息进行分片:


将消息分片,分布到不同的消息队列中,使得不同的消费者可以并行地处理消息,以提高消息处理的效率。



  1. 对消息进行持久化:


使用Redis的持久化机制,将消息写入磁盘,以防止消息的丢失。同时,也可以使用多个Redis节点进行备份,以提高Redis系统的可靠性。


总的来说,在实际应用中,需要根据实际情况,综合考虑上述方法,选择适合自己的方案,以保证Redis的消息队列在处理消息积压时,能够保持高效和稳定。


8. redis使用zset做消息队列时,有多个消费者同时消费消息怎么处理


当使用 Redis 的 zset 作为消息队列时,可以通过以下方式来处理多个消费者同时消费消息:



  1. 利用Redis事务特性:zset中的元素的score会反映该元素的优先级,多个消费者可以使用Redis事务特性,采用原子性的操作将空闲的消息数据上锁,只有在被加锁的消费者消费完当前消息时,往消息队列中发送释放锁的指令,其它消费者才能够获得该消息并进行消费。

  2. 利用Redis分布式锁:使用 Redis 实现分布式锁来实现只有一个消费者消费一条消息,可以使用redis的SETNX命令(如果键已存在,则该命令不做任何事,如果密钥不存在,它将设置并返回1可以用作锁),将创建一个新的键来表示这一消息是否已经被锁定。

  3. 防止重复消费:为了防止多个消费者消费同一条消息,可以在消息队列中添加一个消息完成的标记,在消费者处理完一条消息之后,会将该消息的完成状态通知给消息队列,标记该消息已经被消费过,其它消费者再次尝试消费该消息时,发现已经被标记为完成,则不再消费该消息。


无论采用哪种方式,都需要保证消息队列的可靠性和高效性,否则会导致消息丢失或重复消费等问题。


9.redis使用zset做消息队列如何实现一个分组的功能


Redis 中的 Zset 可以用于实现一个有序集合,其中每个元素都会关联一个分数。在消息队列中,可以使用 Zset 来存储消息的优先级(即分数),并使用消息 ID 作为 Zset 中的成员,这样可以通过 Zset 的有序性来获取下一条要处理的消息。


为了实现一个分组的功能,可以使用 Redis 的命名空间来创建多个 Zset 集合。每个分组都有一个对应的 Zset 集合,消息都被添加到对应的集合中。然后,你可以从任何一个集合中获取下一条消息,这样就可以实现分组的功能。


例如,假设你的 Redis 实例有三个 Zset 集合,分别是 group1、group2 和 group3,你可以按照如下方式将消息添加到不同的分组中:


ZADD group1 1 message1
ZADD group2 2 message2
ZADD group3 3 message3

然后,你可以通过以下方式获取下一条要处理的消息:


ZRANGE group1 0 0 WITHSCORES
ZRANGE group2 0 0 WITHSCORES
ZRANGE group3 0 0 WITHSCORES

将返回结果中的第一个元素作为下一条要处理的消息。由于每个分组都是一个独立的 Zset 集合,因此它们之间是相互独立的,不会干扰彼此。


10. redis使用zset做消息队列有哪些注意事项


Redis 使用 ZSET 做消息队列时,需要注意以下几点:



  1. 消息的唯一性:使用 ZSET 作为消息队列存储的时候需要注意消息的唯一性,避免重复消息的情况出现。可以考虑使用消息 ID 或者时间戳来作为消息的唯一标识。

  2. 消息的顺序:使用 ZSET 作为消息队列存储可以保证消息的有序性,但消息的顺序可能不是按照消息 ID 或者时间戳的顺序。可以考虑在消息中增加时间戳等信息,然后在消费时根据这些信息对消息进行排序。

  3. 已消费的消息删除:在使用 ZSET 作为消息队列的时候需要注意如何删除已经消费的消息,可以使用 ZREMRANGEBYLEX 或者 ZREMRANGEBYSCORE 命令删除已经消费的消息。

  4. 消息堆积问题:ZSET 作为一种有序存储结构,有可能出现消息堆积的情况,如果消息队列里面的消息堆积过多,会影响消息队列的处理速度,甚至可能导致 Redis 宕机等问题。这个问题可以使用 Redis 定时器来解决,定期将过期的消息从队列中删除。

  5. 客户端的能力:在消费消息的时候需要考虑客户端的能力,可以考虑增加多个客户端同时消费消息,以提高消息队列的处理能力。

  6. Redis 节点的负载均衡:使用 ZSET 作为消息队列的存储结构,需要注意 Redis 节点的负载均衡,因为节点的并发连接数可能会受到限制。必要的时候可以增加 Redis 节点数量,或者采用 Redis 集群解决这个问题。


总之,使用 ZSET 作为消息队列存储需要特别注意消息的唯一性、消息的顺序、已消费消息删除、消息堆积问题、客户端的能力和节点的负载均衡等问题。


作者:香吧香
链接:https://juejin.cn/post/7221910537432825916
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可


@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样


fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:


fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑


val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。


@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。


@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:


single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


作者:fundroid
链接:https://juejin.cn/post/7230597098847092791
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Unit 为啥还能当函数参数?面向实用的 Kotlin Unit 详解

视频先行 这是一篇视频形式的分享,如果你方便看,可以直接去看视频: 哔哩哔哩:这里 抖音:这里 YouTube:这里 下面是视频内容的脚本文案原稿分享。 视频文案原稿 很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有...
继续阅读 »

视频先行


这是一篇视频形式的分享,如果你方便看,可以直接去看视频:




下面是视频内容的脚本文案原稿分享。



视频文案原稿


很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有沿用 Java 的 void 关键字,而要引入这个叫 Unit 的新东西?


// Java
public void sayHello() {
System.out.println("Hello!");
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

不过这个问题一般也不会维持很久,因为就算你不明白,好像……也不影响写代码。


直到这两年,大家发现 Compose 的官方示例代码里竟然有把 Unit 填到函数参数里的情况:


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

我们才觉得:「啊?还能这么写?」


Unit 的本质


大家好,我是扔物线朱凯。


今天来讲一讲 Unit 这个特殊的类型。


我们在刚学 Kotlin 的时候,就知道 Java 的 void 关键字在 Kotlin 里没有了,取而代之的是一个叫做 Unit 的东西:


// Java
public void sayHello() {
System.out.println("Hello!")
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

而这个 Unit,和 Java 的 void 其实是不一样的。比如 Unit 的返回值类型,我们是可以省略掉不写的:


// Kotlin
fun sayHello() {
println("Hello!")
}

不过省略只是语法上的便利,实际上 Kotlin 还是会把它理解成 Unit


Unit 和 Java 的 void 真正的区别在于,void 是真的表示什么都不返回,而 Kotlin 的 Unit 却是一个真实存在的类型:


public object Unit {
override fun toString() = "kotlin.Unit"
}

它是一个 object,也就是 Kotlin 里的单例类型或者说单例对象。当一个函数的返回值类型是 Unit 的时候,它是需要返回一个 Unit 类型的对象的:


// Kotlin
fun sayHello() {
println("Hello!")
return Unit
}

只不过因为它是个 object ,所以唯一能返回的值就是 Unit 本身。


另外,这一行 return 我们也可以省略不写:


// Kotlin
fun sayHello() {
println("Hello!")
}

因为就像返回值类型一样,这一行 return,Kotlin 也会帮我们自动加上:


// Kotlin
fun sayHello(): Unit {
println("Hello!")
return Unit
}

这两个 Unit 是不一样的,上面的是 Unit 这个类型,下面的是 Unit 这个单例对象,它俩长得一样但是是不同的东西。注意了,这个并不是 Kotlin 给 Unit 的特权,而是 object 本来就有的语法特性。你如果有需要,也可以用同样的格式来使用别的单例对象,是不会报错的:


object Rengwuxian

fun getRengwuxian(): Rengwuxian {
return Rengwuxian
}

包括你也可以这样写:


val unit: Unit = Unit

也是一样的道理,等号左边是类型,等号右边是对象——当然这么写没什么实际作用啊,单例你就直接用就行了。


所以在结构上,Unit 并没有任何的特别之处,它就只是一个 Kotlin 的 object 而已。除了对于函数返回值类型和返回值的自动补充之外,Kotlin 对它没有再施加任何的魔法了。它的特殊之处,更多的是在于语义和用途的角度:它是个由官方规定出来的、用于「什么也不返回」的场景的返回值类型。但这只是它被规定的用法而已,而本质上它真就是个实实在在的类型。也就是在 Kotlin 里,并不存在真正没有返回值的函数,所有「没有返回值」的函数实质上的返回值类型都是 Unit,而返回值也都是 Unit 这个单例对象,这是 Unit 和 Java 的 void 在本质上的不同。


Unit 的价值所在


那么接下来的问题就是:这么做的意义在哪?


意义就在于,Unit 去掉了无返回值的函数的特殊性,消除了有返回值和无返回值的函数的本质区别,这样很多事做起来就会更简单了。


例:有返回值的函数在重写时没有返回值


比如?


比如在 Java 里面,由于 void 并不是一种真正的类型,所以任何有返回值的方法在子类里的重写方法也都必须有返回值,而不能写成 void,不管你用不用泛型都是一样的:


public abstract class Maker {
public abstract Object make();
}

public class AppleMaker extends Maker {
// 合法
@Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker {
// 非法
@Override
public void make() {
world.refresh();
}
}


public abstract class Maker<T> {
public abstract T make();
}

public class AppleMaker extends Maker<Apple> {
// 合法
Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker<void> {
// 非法
Override
public void make() {
world.refresh();
}
}


你只能去写一行 return null 来手动实现接近于「什么都不返回」的效果:


public class NewWorldMaker extends Maker {
@Override
public Object make() {
world.refresh();
return null;
}
}


而且如果你用的是泛型,可能还需要用一个专门的虚假类型来让效果达到完美:


public class NewWorldMaker extends Maker<Void> {
@Override
public Void make() {
world.refresh();
return null;
}
}


而在 Kotlin 里,Unit 是一种真实存在的类型,所以直接写就行了:


abstract class Maker {
abstract fun make(): Any
}

class AppleMaker : Maker() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker() {
override fun make() {
world.refresh()
}
}

abstract class Maker<T> {
abstract fun make(): T
}

class AppleMaker : Maker<Apple>() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker<Unit>() {
override fun make() {
world.refresh()
}
}

这就是 Unit 的去特殊性——或者说通用性——所给我们带来的便利。


例:函数类型的函数参数


同样的,这种去特殊性对于 Kotlin 的函数式编程也提供了方便。一个函数的函数类型的参数,在函数调用的时候填入的实参,只要符合声明里面的返回值类型,它是可以有返回值,也可以没有返回值的:


fun runTask(task: () -> Any) {
when (val result = task()) {
Unit -> println("result is Unit")
String -> println("result is a String: $result")
else -> println("result is an unknown type")
}
}

...

runTask { } // () -> Unit
runTask { "完成!" } // () -> String
runTask { 1 } // () -> Int

Java 不支持把方法当做对象来传递,所以我们没法跟 Java 做对比;但如果 Kotlin 不是像现在这样用了 Unit,而是照抄了 Java 的 void 关键字,我们就肯定没办法这样写。


小结:去特殊化


这就是我刚才所说的,对于无返回值的函数的「去特殊化」,是 Unit 最核心的价值。它相当于是对 Java 的 void 进行了缺陷的修复,让本来有的问题现在没有了。而对于实际开发,它的作用是属于润物细无声的,你不需要懂我说的这一大堆东西,也不影响你享受 Unit 的这些好处。


…………


那我出这期视频干嘛?


——开个玩笑。了解各种魔法背后的实质,对于我们掌握和正确地使用一门语言是很有必要的。


延伸:当做纯粹的单例对象来使用


比如,知道 Unit 是什么之后,你就能理解为什么它能作为函数的参数去被使用。


Compose 里的协程函数 LaunchedEffect() 要求我们填入至少一个 key 参数,来让协程在界面状态变化时可以自动重启:


LaunchedEffect(key) {
xxxx
xxxxxx
xxx
}

而如果我们没有自动重启的需求,就可以在参数里填上一个 Unit


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

因为 Unit 是不变的,所以把它填进参数里,这个协程就不会自动重启了。这招用着非常方便,Compose 的官方示例里也有这样的代码。不过这个和 Unit 自身的定位已经无关了,而仅仅是在使用它「单例」的性质。实际上,你在括号里把它换成任何的常量,效果都是完全一样的,比如 true、比如 false、比如 1、比如 0、比如 你好,都是可以的。所以如果你什么时候想「随便拿个对象过来」,或者「随便拿个单例对象过来」,也可以使用 Unit,它和你自己创建一个 object 然后去使用,效果是一样的。


总结


好,这就是 Kotlin 的 Unit,希望这个视频可以帮助你更好地了解和使用它。下期我会讲 Kotlin 里另一个特殊的类型:Nothing。关注我,了解更多 Android 开发的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!


作者:扔物线
链接:https://juejin.cn/post/7231345137850286138
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-EventBus修改纪实

背景 笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例: // Event.java public final class Event { private final in...
继续阅读 »

背景


笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例:


// Event.java
public final class Event {

private final int code;

public Event(int code) {
this.code = code;
}

public int getCode() {
return code;
}
}

// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

所以去查看了 EventBus 的源码,接下来我们分析下 EventBus 发送粘性事件的流程。


分析


粘性事件



以下源码基于 EventBus 3.3.1 版本



下面是发送粘性事件的源码:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

postSticky 代码比较简单,首先对 stickyEvents 进行加锁,接下来把 event 事件的 Class 对象作为 Key,event 事件本身作为 value 放进 Map 中,其中stickyEvents 是 Map 对象,实例是 ConcurrentHashMap, 其 Key 和 Value 的泛型形参分别是 ClassObject, 它的作用就是用来存储粘性事件;然后调用 post(event) 把粘性事件当作普通事件发送一下。


首先我们看下最后为什么要调用下 post(event)


虽然 post(evnet) 上面有注释,简单翻译下:"在放进 Map 后应该再发送一次,以防止订阅者想立即删除此事件",读完注释后,可能还是不太明白,这里笔者认为:在前面存储完粘性事件后,这里调用 post 把粘性事件当作普通事件发送出去,或许是因为现在已经有注册的粘性事件订阅者,此时把已经注册的粘性事件订阅者当作普通事件的订阅者,这样已经注册的粘性事件订阅者可以立即收到相应的事件,只是此时事件不再是粘性的。


postSticky 中我们并没有看到粘性事件是在哪里发送的,想一想我们使用粘性事件的目的是什么?当注册订阅者时可以收到之前发送的事件,这样来看,粘性事件的发送是在注册订阅者时,下面是注册订阅者的源码,删除了一些无关代码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

register 代码也比较简单,首先通过订阅者的 Class 对象查找订阅者所有的Event事件接收方法,然后对 EventBus 对象加锁,遍历所有的Event事件接收方法 subscriberMethods 调用 subscribe 方法,以下是 subscribe 方法源码:


// Key 为 Event Class 对象,Value 为存储 Event 的订阅者和接收 Event 方法对象的集合 
private final Map, CopyOnWriteArrayList> subscriptionsByEventType;

// Key 为订阅者对象,Value 为订阅者中的 Event Class对象集合
private final Map>> typesBySubscriber;

// Must be called in synchronized block
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// Event Class对象
Class eventType = subscriberMethod.eventType;

// 订阅者和接收 Event 方法对象
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);

// 根据 Event Class对象,获取订阅者和接收 Event 方法对象的集合
CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType);

// 判断订阅者和接收 Event 方法对象是否为空
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
// 判断是否已经包含了新的订阅者和接收 Event 方法对象,若是包含则认为是重复注册
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}

// 这里是按优先级排序插入到集合中
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}

// 这里是把 Event Class对象添加进对应订阅者的 Event Class对象集合中
List> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}

// 上面已经判断了是否重复注册,所以这里直接添加
subscribedEvents.add(eventType);

// 接下来就是粘性事件的发送逻辑了
// 判断 Event 接收方法是否可以处理粘性事件
if (subscriberMethod.sticky) {
// 这里判断是否考虑 Event 事件类的继承关系,默认为 Ture
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}

在上面的源码中,增加了不少注释有助于我们读懂源码,在源码的最后就是粘性事件的发送逻辑了,其中有两个分支,其中一个分支根据 Event 事件的继承关系发送事件,另外一个分支根据接收 Event 方法中的 Event Class 对象从 stickyEvents 中直接查找粘性事件,最后两个分支殊途同归,都调用了 checkPostStickyEventToSubscription 方法:


private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
if (stickyEvent != null) {
// If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
// --> Strange corner case, which we don't take care of here.
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}

checkPostStickyEventToSubscription 方法很简单,对粘性事件做下判空处理,继续调用 postToSubscription 方法,传入订阅者与接收 Event 方法对象,粘性事件和是否是主线程布尔值:


private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;A
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}

postToSubscription 方法比较长,但是比较好理解,就是根据接收 Event 方法上的 @Subscribe 注解中传入的线程模型进行事件的分发,具体的事件分发流程,有空再分析,本文就先不分析了,现在我们只需知道最后都会调用 invokeSubscriber(Subscription subscription, Object event) 方法即可:


void invokeSubscriber(Subscription subscription, Object event) {
try {
// 反射调用 Event 接收方法传入 Event 事件
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}

终于在 invokeSubscriber 方法中找到调用 Event 接收方法的地方了,原来 EventBus 最后是通过反射调用 Event 接收方法并传入相应 Event 事件的。


分析完 Event 事件的发送流程,好像没有发现为什么有时收不到粘性事件。


我们回过头来再看下笔者的使用示例,为了方便查看,下面贴出使用示例代码:


// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

可能细心的读者已经发现 test 方法调用了,问题应该出在 postSticky 方法中,让我们再次查看 postSticky 方法:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

根据前面分析 postSticky 方法的结果,stickyEvents 用于存储粘性事件,它是个 Map 结构,而 stickyEvents 的 Key 正是 Event 的 Class 对象,根据 Map 结构的存储原理:如果存在相同的 Key,则覆盖 Value 的值,而 stickyEvents 的 Value 正是 Event 本身。


终于真相大白,多次调用 test 方法发送粘性事件,EventBus 只会存储最后一次的粘性事件。


小结


EventBus 针对同一个粘性 Event 事件只会存储最后一次发送的粘性事件。


EventBus 的上述实现可能是因为多次发送同一个粘性事件,则认为之前的事件是过期事件应该抛弃,因此只传递最新的粘性事件。


EventBus 的这种实现无法满足笔者的业务逻辑需求,笔者希望多次发送的粘性事件,订阅者都能接收到,而不是只接收最新的粘性事件,可以理解为粘性事件必达订阅者,下面让我们修改 EventBus 的源码来满足需求吧。


修改


上一节我们分析了粘性事件的发送流程,为了满足粘性事件必达的需求,基于现有粘性事件流程,我们可以仿照粘性事件的发送来提供一个发送必达消息的方法。


Subscribe


首先我们定义 Event 接收方法可以接收粘性事件是在 @Subscribesticky = true , 所以我们可以修改 Subscribe 注解,增加粘性事件必达的方法:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING;

/**
* If true, delivers the most recent sticky event (posted with
* {@link EventBus#postSticky(Object)}) to this subscriber (if event available).
*/

boolean sticky() default false;

// 增加消息必达的方法
boolean rendezvous() default false;

/** Subscriber priority to influence the order of event delivery.
* Within the same delivery thread ({@link ThreadMode}), higher priority subscribers will receive events before
* others with a lower priority. The default priority is 0. Note: the priority does *NOT* affect the order of
* delivery among subscribers with different {@link ThreadMode}s! */

int priority() default 0;
}

rendezvous 以为约会、约定的意思,可以理解为不见不散,在这里它有两层作用,其一是标记方法可以接收粘性事件,其二是标记方法接收的事件是必达的。


findSubscriberMethods


接下来就需要解析 rendezvous 了,我们先看看 sticky 是如何解析的,在上一节我们分析了 register 方法,方便查看,下面再贴出 register 方法源码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

上一节分析中,我们没有分析查找订阅者中所有的 Event 接收方法 findSubscriberMethods ,接下来我们分析下在 findSubscriberMethods 方法是如何查找 Event 接收方法的:


List findSubscriberMethods(Class subscriberClass) {
// 先从缓存中查找
List subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}

// 是否忽略生成索引,默认为False,所以这里走else分支
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
// 查找Event接收方法
subscriberMethods = findUsingInfo(subscriberClass);
}

// 如果订阅者和订阅者父类中没有Event接收方法则抛出异常
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
// 添加进缓存中
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}

调用 findSubscriberMethods 方法需要传入订阅者 Class 对象,通过笔者在源码中增加的注释分析发现默认调用 findUsingInfo 方法查找 Event 接收方法,我们继续跟踪 findUsingInfo 方法:


private List findUsingInfo(Class subscriberClass) {
// FindState对订阅者Class对象和Event接收方法进行了一层封装
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass); // ①
while (findState.clazz != null) {

// 查找订阅者信息,包含订阅者Class对象、 订阅者父类、Event接收方法等
findState.subscriberInfo = getSubscriberInfo(findState); // ②

// 在 ① initForSubscriber中会把subscriberInfo置为null,
// 在 ② getSubscriberInfo中没有Index对象,
// 所以第一次时这里会走else分支
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod : array) {
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
// 查找Event接收方法
findUsingReflectionInSingleClass(findState);
}

// 查找父类的Event接收方法
findState.moveToSuperclass();
}

// 通过findState返回Event接收方法,并回收findState
return getMethodsAndRelease(findState);
}

根据笔者在源码中的注释分析,在 findUsingInfo 方法中使用「享元模式」对 FindState 进行回收利用,避免创建大量临时的 FindState 对象占用内存,最后再次调用 findUsingReflectionInSingleClass 方法查找 Event 接收方法,看方法名字应该是使用反射查找,findUsingReflectionInSingleClass 源码较长,删减一些不关心的代码:


private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
// 通过反射获取当前类中声明的所有方法
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// 删减不关心的代码
}

// 遍历所有方法
for (Method method : methods) {

// 获取方法的修饰符
int modifiers = method.getModifiers();

// 判断方法是否是public的;是否是抽象方法,是否是静态方法,是否是桥接方法,是否是合成方法
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {

// 获取方法的形参Class对象数组
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {

// 获取方法上的Subscribe注解
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class eventType = parameterTypes[0];

// 检测是否已经添加了相同签名的方法,考虑子类复写父类方法的情况
if (findState.checkAdd(method, eventType)) {

// 获取注解的参数
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky(),

// 这里我们添加rendezvous参数 ①
subscribeAnnotation.rendezvous()));
}
}
}
// 删减不关心的代码
}
// 删减不关心的代码
}
}

findUsingReflectionInSingleClass 方法中通过反射获取订阅者中声明的所有方法,然后遍历所有方法:



  1. 首先判断方法的修饰符是否符合,

  2. 其次判断方法是否只有一个形参,

  3. 再次判断方法是否有 Subscribe 注解,

  4. 然后检测是否已经添加了相同签名的方法,主要是考虑子类复写父类方法这种情况,

  5. 最后获取 Subscribe 注解的参数,在这里我们解析 rendezvous,封装进 SubscriberMethod 中。


SubscriberMethod 中增加 rendezvous 字段,删除不关心的代码:


public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class eventType;
final int priority;
final boolean sticky;

// 增加 `rendezvous` 字段
final boolean rendezvous;
/** Used for efficient comparison */
String methodString;

public SubscriberMethod(Method method, Class eventType, ThreadMode threadMode,
int priority, boolean sticky,

// 增加 `rendezvous` 形参
boolean rendezvous) {
this.method = method;
this.threadMode = threadMode;
this.eventType = eventType;
this.priority = priority;
this.sticky = sticky;
this.rendezvous = rendezvous;
}
}

postRendezvous


好的,rendezvous 已经解析出来了,接下来我们对外提供发送必达事件的接口:


// 选择List存储必达事件,使用Pair封装必达事件的Key和Value
private final List, Object>> rendezvousEvents;

public void postRendezvous(Object event) {
synchronized (rendezvousEvents) {
rendezvousEvents.add(Pair.create(event.getClass(), event));
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

上面的源码,我们通过仿照 postSticky 方法实现了 postRendezvous 方法,在 postSticky 方法中使用 Map 存储粘性事件,不过我们在 postRendezvous 方法中使用 List 存储必达事件,保证必达事件不会因为 Key 相同而被覆盖丢失,最后也是调用 post 方法尝试先发送一次必达事件。


register


在上一节中我们分析了粘性事件是在 register 中调用 subscribe 方法进行发送的,这里我们仿照粘性事件的发送逻辑,实现必达事件的发送逻辑,我们可以在 subscribe 方法最后增加发送必达事件的逻辑,以下源码省略了一些不关心的代码:


private final List, Object>> rendezvousEvents;

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// 省略不关心的代码

// 粘性事件发送逻辑
if (subscriberMethod.sticky) {
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}

// 新增必达事件发送逻辑
// 判断方法是否可以接收必达事件
if (subscriberMethod.rendezvous) {
if (eventInheritance) {
for (Pair, Object> next : rendezvousEvents) {
Class candidateEventType = next.first;
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = next.second;
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object rendezvousEvent = getRendezvousEvent(eventType);
if (rendezvousEvent != null) {
checkPostStickyEventToSubscription(newSubscription, rendezvousEvent);
}
}
}
}

subscribe 方法中,我们通过仿照粘性事件的发送逻辑增加了必达事件的发送:



  1. 首先判断 Event 接收方法是否可以接收必达事件

  2. 其次考虑 Event 必达事件的继承关系,

  3. 最后两个分支都调用 checkPostStickyEventToSubscription 方法发送必达事件


happy~


总结


使用第三方库时,发现问题不要慌张,带着问题去查看源码总有一番收获,这也告诫我们在使用第三库时最好先搞明白它的实现原理,遇到问题时不至于束手无策。


通过分析 EventBus 的源码,我们有以下收获:



  1. 明白了我们注册订阅者时 EventBus 做了哪些事情

  2. 知晓了我们发送粘性事件时,EventBus 是如何处理及何时发送粘性事件的

  3. 了解到 EventBus 是通过反射调用 Event 事件的接收方法

  4. 学习了 EventBus 中的一些优化点,比如对 FindState 使用「享元模式」避免创建大量临时对象占用内存

  5. 进一步了解到对并发的处理


通过以上收获,我们成功修改 EventBus 源码实现了我们必达事件的需求。


到这里我们已经完成了必达事件的发送,不过我们还剩下获取必达事件,移除必达事件没有实现,最后 EventBus 中还有单元测试 module,我们还没有针对 rendezvous 编写单元测试,读者有兴趣的话,可以自己试着实现。


希望可以帮到你~


作者:guodongAndroid
链接:https://juejin.cn/post/7104107150678917133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

入职东北国企做程序员一个月,感受如何?

工作环境 我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。 人 目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职...
继续阅读 »

工作环境


我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。



目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职责去负责自己的事情就好。


办公环境可以分为两个环境,分别是“职能部门”和“研发部门”:


* 职能部门比较正式,工位、装修以及员工着装都比较正规。
* 研发部门较为随意一些,无论是工位还是桌椅什么的,有些东拼西凑的感觉,但是整体还是可以接受。

另外可能是因为国企的原因,所有的工位都是大隔断那种,如果换成现在公司常见的大通桌,估计人数还能多做十好几个,毕竟我刚来的时候还没有正式工位坐呢。



相比于在其他公司上班,可能在这最大的体会就是不用考虑吃什么。公司有食堂,提供午饭,菜不能选,但是每天四菜一汤,加水果或酸奶。相比于每天纠结的选择外卖,我对这个很满意。


晚上如果加班的话,公司会统一订餐,大概一餐的费用也在20至30块之间吧,当然也没法选择吃什么,有啥吃啥被。


早餐为什么最后说,因为公司的早餐在早上八点之前供应,八点半上班。。。有点难受啊。


幸好公司提供简单的零食,面包、火腿肠、泡面等等,虽然偶尔会被大家抢空,但是总比没有强吧。



上家公司离我家只有1公里的距离,所以从回到哈尔滨也没有买车,每天不行上班,还挺惬意的。


现在不行了,新公司距离家里有十好几公里,当然我也暂时没有选择买车,地铁出行,快捷方便,还省心,唯一的缺点就是要走个1.5公里吧。


在晚上八点之后打车可以报销的,但是只能是网约车,可能是出租车的票,粘贴太过麻烦了吧。反正我是不打车,因为我嫌报销麻烦。


工具


啥是工具呢,对程序员来说就是电脑了,公司提供电脑,也可以自己买电脑进行报销,还是很人性化地。


公司的会议室设施还是不错的,各种投屏等等,比较先进,完全摒弃了传统的投影仪等等,这还让我对公司有种另眼相看的感觉。


还提供显示器什么的,自己申请就好了。


入职感受


我面试的岗位是java开发,常规的java框架使用起来都没有问题。面试过程还是比较简单的,主要是常用的一些组件,简单的实现原理等等,所以顺利通过了。


但是比较遗憾的公司给我砍了一些,定位的职级也不是很高。说实话我还是有点难受的,毕竟整个面试过程,和我对个人的能力认知还是比较清楚地。


但是当我入职后我明白了,这里毕竟是哈尔滨,收入和年龄还是有很大的关系的。部门内有好几位大哥,想想也就释然了,在其位谋其政吧,他们的工作确实我我接下来要做的繁琐。希望日后能够慢慢的升职加薪吧。


总体来说,东北人还是敞亮,有事直接提,工作也没啥拐弯抹角的,干就完了。我才刚来公司第一天,就给我把工作安排上了,一点不拿我当外人啊


工作感受


既然谈到工作了,就展开说说。


我第一天到公司,找了个临时工位,领导们各种git账号、禅道账号就给我创建好,一个项目扔给我,本月中期要求做完。。我当时内心的想法真的是:东北人果然是好相处啊,整的跟老同事似的。我能怎么办,干就完了啊。


项目还是很简单的,常规的springboot + mybatis + vue2的小项目,大概也没到月中期,一个礼拜就完事了。


比较让我惊喜的是部署的环节。居然使用的是devops工具KubeSphere。我只说这一句你们可能不理解,这是我在哈尔滨的第三家公司,从来没有一家公司说使用过k8s,甚至相关的devops工具。只能说是哈尔滨软件行业的云化程度还是太低了。唯一在上家公司的jenkins还是因为我想偷懒搭建的。


不过运维相关的内容都把握在运维人员手里,所以想要料及并且掌握先关的知识还是要自己私下去学习的。


项目其实都是小项目,以web端和app为主,基本都是前后端分离的单体架构。唯一我接触到的微服务架构应该就是公司的中台,提供统一的权限配置和登录认证,整体还是很不错的。


虽然公司的项目很多,工作看起来很忙碌,但实际还是比较轻松愉快的,我还能应付自如。每天晚上为了蹭一顿晚饭,通常会加班到七点半。用晚上这个时间更更文,也挺好的。


从体来说,是我比较喜欢的工作节奏。


个人分析


我是一个不太安定的人,长期干一件事会让我比较容易失去兴趣,还是挺享受刚换工作时,这段适应环境的感觉。也有可能更喜欢这种有一定挑战的感觉。


和上一家公司相比,这家公司在公司的时间明显多出很多,也没有那么悠闲了,但是我却觉得这更适合我,毕竟我是一个闲不住的人,安逸的环境让我感到格外的焦虑,忙碌的生活会让自己感到生活很充实。


记得之前的文章说过自己的身体健健的不太好,但是最近不知道是上班的路程变远,导致运动量的增加,之前不适的症状似乎都小时了。真闲出病来了




既来之,则安之,时刻提醒自己再努力点,阳光总在风雨后。


作者:我犟不过你
链接:https://juejin.cn/post/7125627005407592462
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

IDEA用上这十大插件绝对舒服

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。 1、TabNine TabNine 是一个 IntelliJ IDEA 插...
继续阅读 »

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。


1、TabNine


TabNine 是一个 IntelliJ IDEA 插件,可以为 Java 和 JavaScript 开发人员的代码提供 AI 建议。它分析来自数百万个开源项目的代码,并提供相关且准确的代码片段,以帮助开发人员更快、更准确地编写代码。

使用 TabNine 的众多优势包括:



  1. 有效的代码提示。

  2. 支持大量编程语言。

  3. 为主流编辑器和IDE提供帮助。

  4. 使用机器学习,记住你经常写的代码,并提供极其详细的提示。


地址:plugins.jetbrains.com/plugin/1279…



2、RestfulToolkit


RestfulToolkit 提供了与 RESTful API 交互的有用工具。开发人员可以使用此插件直接从 IDE 轻松测试、调试和管理 RESTful API 请求,从而提高他们的整体效率和生产力。


该插件与 HTTP Client、REST Assured 等流行工具集成,使其成为 RESTful API 开发的完整解决方案。


地址:plugins.jetbrains.com/plugin/1029…


3、MyBatisCodeHelperPro


MyBatisCodeHelperPro 在使用 MyBatis 框架时提高了开发人员的工作效率。它包括代码生成和实时模板,使编写和管理 MyBatis 代码更加容易,节省时间和精力。



此外,该插件支持数据库架构同步和 SQL 文件生成,提高开发效率。



地址:plugins.jetbrains.com/plugin/9837…
dehelperpro


4、CodeGlance


CodeGlance 为开发人员提供了代码右侧添加了简明概览,使他们更容易浏览和理解代码。

地址:plugins.jetbrains.com/plugin/7275…



可以看到在上图右侧区域有一个代码概览区域,并且可以上下滑动。


5、GenerateAllSetter


GenerateAllSetter 有助于为类中的所有属性生成 setter 方法。这可以在编写代码时节省时间和精力,同时也降低了出错的可能性。



地址:plugins.jetbrains.com/plugin/9360…



6、Lombok


Lombok:一个自动生成样板代码的 Java 库。



Project Lombok 是一个 java 库,可自动插入您的编辑器和构建工具,为您的 java 增添趣味。永远不要再写另一个 getter 或 equals 方法,通过一个注解,您的类就有一个功能齐全的构建器,自动化您的日志变量,等等。



地址:projectlombok.org/

需要注意的就是在使用了在 IDEA 中使用 Lombok 插件记得启用 Enable annotation processing


7、Rainbow Brackets


该插件为代码的方括号和圆括号着色,从而更容易区分不同级别的代码块。


地址:plugins.jetbrains.com/plugin/1008…


可以看到添加彩色方括号后,代码可读性有所提高。


8、GitToolBox


它包括许多额外的功能和快捷方式,使开发人员更容易使用 Git。使用 GitToolBox 的众多优点包括:



  1. GitToolBox 在 IntelliJ IDEA 上下文菜单中添加了几个快速操作,允许您在不离开 IDE 的情况下执行常见的 Git 任务。

  2. Git 控制台:该插件向 IntelliJ IDEA 添加了一个 Git 控制台,允许您在 IDE 中使用 Git。

  3. GitToolBox包含了几个解决合并冲突的工具,可以更容易地解决冲突并保持你的代码库是最新的。

  4. Git stash management:该插件添加了几个用于管理Git stashes的工具,使保存和重新应用代码更改变得更加容易。


地址:plugins.jetbrains.com/plugin/7499…


9、Maven Helper


Maven Helper 提供了一种更方便的方式来处理 Maven 项目。


Maven Helper 是一个帮助开发人员完成 Maven 构建过程的工具。该插件包括用于管理依赖项、插件和配置文件的功能,例如查看、分析和解决冲突以及运行和调试特定 Maven 目标的能力。


这可以通过减少花在手动配置和故障排除任务上的时间,使开发人员有时间进行编码和创新,从而提高生产力。


地址:plugins.jetbrains.com/plugin/7179…


10、Sonarlint


Sonarlint 是一个代码质量检测工具,集成了 SonarQube 以动态检测和修复代码质量问题。


Sonarlint 提供实时反馈和建议,帮助开发人员提高代码质量。它集成了 SonarQube 代码分析平台,允许开发人员直接在他们的 IDE 中查看代码质量问题。


这通过在潜在问题到达构建和测试阶段之前检测它们来节省时间并提高效率。 Sonarlint 还可以帮助开发人员遵守最佳实践和编码标准,从而生成更易于维护和更健壮的代码。


地址:plugins.jetbrains.com/plugin/7973…



作者:wayn
链接:https://juejin.cn/post/7225989135999434789
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。


4个方面


设计数据库表结构需要考虑到以下4个方面:




  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。




  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。




  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。




  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。




设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:




  1. 简单明了:表结构应该简单明了,避免过度复杂化。




  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。




  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。




  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。




  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。




  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。




最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析



  1. 可以根据红框的标签筛选视频

  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样



  • 综合是根据业务逻辑取值,并不需要入库

  • 类型、地区、年份、演员等需要入库



  1. 设计表结构时要考虑到:



  • 方便获取标签信息,方便把标签信息缓存处理

  • 方便根据标签筛选视频,方便我们写后续的业务逻辑


设计思路



  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中

  2. 类型、地区、年份、演员都设计单独的表

  3. 视频表中设计标签表的外键,方便视频列表筛选取值

  4. 标签信息写入缓存,提高接口响应速度

  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护


表结构设计


视频表































字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表























字段注释
id类型主键id
name类型名称
sort排序字段

地区表























字段注释
id类型主键id
name类型名称
sort排序字段

年份表























字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表























字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:




  1. 比较常用的就是redis缓存

  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗

  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能


列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}
复制代码

go-zero


DB缓存机制


go-zero缓存设计之持久层缓存


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:



  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。

  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。

  3. 或者将视频详情的查询结果整体进行缓存


还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:



  1. 关键问题是想解决管理后台灵活配置

  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。

  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~


总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度



本文抛砖引玉,欢迎大家留言交流。


作者:王中阳Go
链接:https://juejin.cn/post/7212828749128876092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用Snackbar代替Toast

用Snackbar代替Toast Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Sna...
继续阅读 »

用Snackbar代替Toast



Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Snackbar。



认识Snackbar


我是在看某个三方库源码的时候发现它的,在以往的开发经验中,它出现的频率并不高。我在某个三方库中发现,在两个项目中,提示的表现却不一样,代码怎么会骗人呢。于是认真的看这个三方库的源码,直到看到下面的判断。



static {

try {

Class.forName("android.support.design.widget.Snackbar");

Class.forName("android.support.design.widget.BottomSheetDialog");

HAS_DESIGN_LIB = true;

} catch (Throwable ignore) {

HAS_DESIGN_LIB = false;

}

}

protected AbsAgentWebUIController create() {

return HAS_DESIGN_LIB ? new DefaultDesignUIController() : new DefaultUIController();

}

这里的意思是说,如果项目中有SnackbarBottomSheetDialog库的引用,则使用DefaultDesignUIController的UI,也就是Snackbar的提示;没有的话,则使用DefaultUIController的UI,也就是默认的Toast提示。


实际上其实谷歌在Android 5.0的时候就推出了Snackbar,它是Material Design中的一个控件。


实践 Snackbar


下图为ToastSnackbar的展示效果


image.png



  • 简单的提示


Snackbar的基本用法和Toast类似



Snackbar.make(findViewById(R.id.root), "这是一条提示", Snackbar.LENGTH_LONG).show();


  • 带有Action的提示



Snackbar snackbar = Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE);

snackbar.setAction("取消", new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this,"点击取消",Toast.LENGTH_SHORT).show();

}

});

snackbar.show();

这里只能设置一个Action,不然旧的会被替代掉。




  1. Snackbar.LENGTH_INDEFINITE:代表无限期的显示,一直显示,点击按钮才可以隐藏




  2. Snackbar.LENGTH_LONG:长时间提示




  3. Snackbar.LENGTH_SHORT:短时间提示





  • 修改样式


更改Snackbar的背景颜色



snackbar.getView().setBackgroundColor(Color.parseColor("#0000ff"));

更改Action提示的颜色



snackbar.setActionTextColor(Color.parseColor("#ffffff"));

更改padding的距离



snackbar.getView().setPadding(50, 50, 50 , 50);

操作文字,比如添加图片、更改文字内容、更改文字颜色,更改文字大小等。


虽然Snackbar没有提供给我们直接操作文字样式的方法,但我们可以通过findViewById获取这个文字,然后就像操作TextView一样去操作它就可以了。


怎么获取TextView?



TextView textView = snackbar.getView().findViewById(R.id.snackbar_text);

snackbar_text id来自Snackbar的源码。获取ID的时候编辑器可能会报错提示,实际上是可以运行的。



@NonNull

public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {

ViewGroup parent = findSuitableParent(view);

if (parent == null) {

throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");

} else {

LayoutInflater inflater = LayoutInflater.from(parent.getContext());

SnackbarContentLayout content = (SnackbarContentLayout)inflater.inflate(hasSnackbarButtonStyleAttr(parent.getContext()) ? layout.mtrl_layout_snackbar_include : layout.design_layout_snackbar_include, parent, false);

Snackbar snackbar = new Snackbar(parent, content, content);

snackbar.setText(text);

snackbar.setDuration(duration);

return snackbar;

}

}


<view

xmlns:android="http://schemas.android.com/apk/res/android"

class="android.support.design.widget.SnackbarContentLayout"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_gravity="bottom"

android:theme="@style/ThemeOverlay.AppCompat.Dark">

<TextView

android:id="@+id/snackbar_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_weight="1"

android:layout_gravity="center_vertical|left|start"

android:paddingTop="@dimen/design_snackbar_padding_vertical"

android:paddingBottom="@dimen/design_snackbar_padding_vertical"

android:paddingLeft="@dimen/design_snackbar_padding_horizontal"

android:paddingRight="@dimen/design_snackbar_padding_horizontal"

android:ellipsize="end"

android:maxLines="@integer/design_snackbar_text_max_lines"

android:textAlignment="viewStart"

android:textAppearance="?attr/textAppearanceBody2"/>

<Button

android:id="@+id/snackbar_action"

style="?attr/snackbarButtonStyle"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_gravity="center_vertical|right|end"

android:minWidth="48dp"

android:visibility="gone"/>

</view>

作者:Android唐浮
链接:https://juejin.cn/post/7103803312910106660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

定位都得集成第三方?Android原生定位服务LocationManager不行吗?

前言 现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。 有些同学觉得不就是获取到经纬度么,An...
继续阅读 »

前言


现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。


有些同学觉得不就是获取到经纬度么,Android 自带的就有位置服务 LocationManager ,我们无需引入第三方服务,就可以很方便的实现定位逻辑。


确实 LocationManager 的使用很简单,获取经纬度很方便,我们就无需第三方的服务了吗? 或者说 LocationManager 有没有坑呢?兼容性问题怎么样?获取不到位置有没有什么兜底策略?


一、LocationManager的使用


由于是Android的系统服务,直接 getSystemService 可以获取到


LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

一般获取位置有两种方式 NetWork 与 GPS 。我们可以指定方式,也可以让系统自动提供最好的方式。


// 获取所有可用的位置提供器
List<String> providerList = locationManager.getProviders(true);
// 可以指定优先GPS,再次网络定位
if (providerList.contains(LocationManager.GPS_PROVIDER)) {
provider = LocationManager.GPS_PROVIDER;
} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {
provider = LocationManager.NETWORK_PROVIDER;
} else {
// 当没有可用的位置提供器时,弹出Toast提示用户
return;
}

当然我更推荐由系统提供,当我的设备在室内的时候就会以网络的定位提供,当设备在室外的时候就可以提供GPS定位。


 String provider = locationManager.getBestProvider(criteria, true);

我们可以实现一个定位的Service实现这个逻辑


/**
* 获取定位服务
*/
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@SuppressLint("MissingPermission")
@Override
public void onCreate() {
super.onCreate();

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

//第二个参数是间隔时间 第三个参数是间隔多少距离,这里我试过了不同的各种组合,能获取到位置就是能,不能获取就是不能
lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:"+provider +" status:"+status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}

@Override
public void onDestroy() {
super.onDestroy();
lm.removeUpdates(listener); // 停止所有的定位服务
}

}

使用:定义并动态申请权限之后即可开启服务



fun testLocation() {

extRequestPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) {

startService(Intent(mActivity, LocationService::class.java))

}

}

这样我们启动这个服务就可以获取到当前的经纬度,只是获取一次,大家如果想再后台持续定位,那么实现的方式就不同了,我们服务要设置为前台服务,并且需要额外申请后台定位权限。


话说回来,这么使用就一定能获取到经纬度吗?有没有兼容性问题


Android 5.0 Oppo



Android 6.0 Oppo海外版



Android 7.0 华为



Android 11 三星海外版



Android 12 vivo



目前测试不多,也能发现问题,特别是一些低版本,老系统的手机就可能无法获取位置,应该是系统的问题,这种服务跟网络没关系,开不开代理都是一样的。


并且随着测试系统的变高,越来越完善,提供的最好定位方式还出现混合定位 fused 的选项。


那是不是6.0的Oppo手机太老了,不支持定位了?并不是,百度定位可以获取到位置的。



既然只使用 LocationManager 有风险,有可能无法获取到位置,那怎么办?


二、混合定位


其实目前百度,高度的定位Api的服务SDK也不算大,相比地图导航等比较重的功能,定位的SDK很小了,并且目前都支持海外的定位服务。并且定位服务是免费的哦。


既然 LocationManager 有可能获取不到位置,那我们就加入第三方定位服务,比如百度定位。我们同时使用 LocationManager 和百度定位,哪个先成功就用哪一个。(如果LocationManager可用的话,它的定位比百度定位更快的)


完整代码如下:


@SuppressLint("MissingPermission")
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;
private LocationClient mBDLocationClient = null;
private MyBDLocationListener mBDLocationListener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();

createNativeLocation();

createBDLocation();
}

/**
* 第三方百度定位服务
*/
private void createBDLocation() {
mBDLocationClient = new LocationClient(UIUtils.getContext());
mBDLocationListener = new MyBDLocationListener();
//声明LocationClient类
mBDLocationClient.registerLocationListener(mBDLocationListener);
//配置百度定位的选项
LocationClientOption option = new LocationClientOption();
option.setLocationMode(LocationClientOption.LocationMode.Battery_Saving);
option.setCoorType("WGS84");
option.setScanSpan(10000);
option.setIsNeedAddress(true);
option.setOpenGps(true);
option.SetIgnoreCacheException(false);
option.setWifiCacheTimeOut(5 * 60 * 1000);
option.setEnableSimulateGps(false);
mBDLocationClient.setLocOption(option);
//开启百度定位
mBDLocationClient.start();
}

/**
* 原生的定位服务
*/
private void createNativeLocation() {

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:" + provider + " status:" + status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}


/**
* 百度定位的监听
*/
class MyBDLocationListener extends BDAbstractLocationListener {

@Override
public void onReceiveLocation(BDLocation location) {

double latitude = location.getLatitude(); //获取纬度信息
double longitude = location.getLongitude(); //获取经度信息


YYLogUtils.w("百度的监听 latitude:" + latitude);
YYLogUtils.w("百度的监听 longitude:" + longitude);

YYLogUtils.w("onBaiduLocationChanged:" + longitude + "-" + latitude);

stopSelf(); // 获取到经纬度以后,停止该service
}
}

@Override
public void onDestroy() {
super.onDestroy();
// 停止所有的定位服务
lm.removeUpdates(listener);

mBDLocationClient.stop();
mBDLocationClient.unregisterLocationListener(mBDLocationListener);
}

}

其实逻辑都是很简单的,并且省略了不少回调通信的逻辑,这里只涉及到定位的逻辑,别的逻辑我就尽量不涉及到。


百度定位服务的API申请与初始化请自行完善,这里只是简单的使用。并且坐标系统一为国际坐标,如果需要转gcj02的坐标系,可以网上找个工具类,或者看我之前的文章


获取到位置之后,如何Service与Activity通信,就由大家自由发挥了,有兴趣的可以看我之前的文章


总结


所以说Android原生定位服务 LocationManager 还是有问题啊,低版本的设备可能不行,高版本的Android系统又很行,兼容性有问题!让人又爱又恨。


很羡慕iOS的定位服务,真的好用,我们 Android 的定位服务真是拉跨,居然还有兼容性问题。


我们使用第三方定位服务和自己的 LocationManager 并发获取位置,这样可以增加容错率。是比较好用的,为什么要加上 LocationManager 呢?我直接单独用第三方的定位服务不香吗?可以是可以,但是如果设备支持 LocationManager 的话,它会更快一点,体验更好。


好了,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。


作者:newki
链接:https://juejin.cn/post/7131910145574961182
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android案例手册 - 仅一个文件的展开收缩LinearLayout

👉实践过程 Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。 前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。 仅一个文件(Java版或...
继续阅读 »

👉实践过程


Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。


前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。


仅一个文件(Java版或Kotlin版),随时复制随时用。
先看效果图


可展开的LinearLayout-仅一个文件复制即用.gif


默认展示两个子item,当点击“显示更多”的时候展开所有的子View,当点击“收起内容”的时候除了前两个其他的都隐藏。


😜使用


我们先来看看使用方式:


<cn.phototocartoonstudy.ExpandableLinearLayout
    android:id="@+id/idExpandableLinearLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="芝麻粒儿" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://juejin.cn/user/4265760844943479" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="CSDN" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://zhima.blog.csdn.net/" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="Android/Unity技术" />
</cn.phototocartoonstudy.ExpandableLinearLayout>

直接布局中用即可,或者动态代码添加:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_framelayout);
ExpandableLinearLayout idExpandableLinearLayout=findViewById(R.id.idExpandableLinearLayout);
for (int i = 0; i < 4; i++) {
TextView  txtViewTip = new TextView(this);
txtViewTip.setText("芝麻粒儿添加更多内容"+i);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
idExpandableLinearLayout.addView(txtViewTip);
}
}

😜实现


说完了使用,我们就来说说实现,前面学完LinearLayout后知道该控件使用了wrap_content,如果子View使用隐藏GONE的形式,则高度自动变化,页面布局中和该控件对其的其他控件也会自动变化。


所以,当子View的个数小于设置的默认个数,则不用添加底部,如果子View个数大于默认显示个数,则在最后动态添加一个View,当点击展开和隐藏的时候,其他多余的控件进行GONE和VISIBLE的控制即可。


我们再为控件增加点其他方法:




  1. 修改当隐藏的时候默认展示的条目




  2. 可修改展开和收起的控件文本




  3. 可修改展开和收起控件的字体大小和颜色




  4. 其他功能自己看着加吧




public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}
public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}
public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}

Java版


/**
* Created by akitaka on 2022-08-11.
*
* @author akitaka
* @filename ExpandableLinearLayout
*/

public class JavaExpandableLinearLayout extends LinearLayout implements View.OnClickListener {

private TextView txtViewTip;
/**
* 是否是展开状态,默认是隐藏
*/
private boolean isExpand = false;
private boolean boolHasBottom = false;

private int intDefaultItemCount = 2;
/**
* 待展开显示的文字
*/
private String strExpandText = "显示更多";
/**
* 待隐藏显示的文字
*/
private String strHideText = "收起内容";
private float fontTextSize;
private int intTextColor;

public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}

public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}

public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}
public JavaExpandableLinearLayout(Context context) {
this(context, null);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//设置垂直方向
setOrientation(VERTICAL);
}

@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("ExpandableLinearLayout只支持垂直布局");
}
super.setOrientation(orientation);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
justToAddBottom(childCount);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


/**
* 判断是否要添加底部
*/
private void justToAddBottom(int childCount) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true;
//要使用默认底部,并且还没有底部
LinearLayout linearLayoutBottom = new LinearLayout(getContext());
LinearLayout.LayoutParams layoutParamsBottom = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutBottom.setLayoutParams(layoutParamsBottom);
linearLayoutBottom.setGravity(Gravity.CENTER);
txtViewTip = new TextView(getContext());
txtViewTip.setText("展开更多");
txtViewTip.setTextSize(fontTextSize);
txtViewTip.setTextColor(intTextColor);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10);
linearLayoutBottom.addView(txtViewTip);
linearLayoutBottom.setOnClickListener(this);
//添加底部
addView(linearLayoutBottom);
hide();
Log.e("TAG", "justToAddBottom: zou l zhe ");
}
}

/**
* 刷新UI
*/
private void refreshView(View view) {
int childCount = getChildCount();
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount);
}
//大于默认数目的先隐藏
view.setVisibility(GONE);
}
}

/**
* 展开
*/
private void expand() {
for (int i = intDefaultItemCount; i < getChildCount(); i++) {
//从默认显示条目位置以下的都显示出来
View view = getChildAt(i);
view.setVisibility(VISIBLE);
}
}

/**
* 收起
*/
private void hide() {
int endIndex = getChildCount() - 1;
for (int i = intDefaultItemCount; i < endIndex; i++) {
//从默认显示条目位置以下的都隐藏
View view = getChildAt(i);
view.setVisibility(GONE);
}
}

@Override
public void onClick(View v) {
outUseMethodToggle();
}

/**
* 外部也可调用 展开或关闭
*/
public void outUseMethodToggle() {
if (isExpand) {
hide();
txtViewTip.setText(strExpandText);
} else {
expand();
txtViewTip.setText(strHideText);
}
isExpand = !isExpand;
}

/**
* 外部可随时添加子view
*/
public void outUseMethodAddItem(View view) {
int childCount = getChildCount();
//插在底部之前
addView(view, childCount - 1);
refreshView(view);
}
}

Kotlin版


/**
* Created by akitaka on 2022-08-11.
* @author akitaka
* @filename KotlinExpandableLinearLayout
*/
class KotlinExpandableLinearLayout :LinearLayout, View.OnClickListener {
private var txtViewTip: TextView? = null
constructor(context: Context?) :this(context,null)
constructor(context: Context?, attrs: AttributeSet?) :this(context,attrs,0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
//设置垂直方向
orientation = VERTICAL
}
/**
* 是否是展开状态,默认是隐藏
*/
private var isExpand = false

private var intDefaultItemCount = 2
private var boolHasBottom = false

/**
* 待展开显示的文字
*/
private var strExpandText = "显示更多"

/**
* 待隐藏显示的文字
*/
private var strHideText = "收起内容"
private var fontTextSize = 0f
private var intTextColor = 0

fun outUseMethodChangeDefaultItemCount(intDefaultItemCount: Int) {
this.intDefaultItemCount = intDefaultItemCount
}

fun outUseMethodChangeExpandText(strExpandText: String) {
this.strExpandText = strExpandText
}

fun outUseMethodChangeHideText(strHideText: String) {
this.strHideText = strHideText
}

fun outUseMethodChangeExpandHideTextSize(fontTextSize: Float) {
this.fontTextSize = fontTextSize
}

fun outUseMethodChangeExpandHideTextColor(@ColorInt intTextColor: Int) {
this.intTextColor = intTextColor
}

override fun setOrientation(orientation: Int) {
require(HORIZONTAL != orientation) { "ExpandableLinearLayout只支持垂直布局" }
super.setOrientation(orientation)
}


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val childCount = childCount
justToAddBottom(childCount)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}


/**
* 判断是否要添加底部
*/
private fun justToAddBottom(childCount: Int) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true
//要使用默认底部,并且还没有底部
val linearLayoutBottom = LinearLayout(context)
val layoutParamsBottom = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
linearLayoutBottom.layoutParams = layoutParamsBottom
linearLayoutBottom.gravity = Gravity.CENTER
txtViewTip = TextView(context)
txtViewTip!!.text = "展开更多"
txtViewTip!!.textSize = fontTextSize
txtViewTip!!.setTextColor(intTextColor)
val layoutParamsBottomTxt = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
txtViewTip!!.layoutParams = layoutParamsBottomTxt
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10)
linearLayoutBottom.addView(txtViewTip)
linearLayoutBottom.setOnClickListener(this)
//添加底部
addView(linearLayoutBottom)
hide()
}
}

/**
* 刷新UI
*/
private fun refreshView(view: View) {
val childCount = childCount
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount)
}
//大于默认数目的先隐藏
view.setVisibility(GONE)
}
}

/**
* 展开
*/
private fun expand() {
for (i in intDefaultItemCount until childCount) {
//从默认显示条目位置以下的都显示出来
val view: View = getChildAt(i)
view.setVisibility(VISIBLE)
}
}

/**
* 收起
*/
private fun hide() {
val endIndex = childCount - 1
for (i in intDefaultItemCount until endIndex) {
//从默认显示条目位置以下的都隐藏
val view: View = getChildAt(i)
view.setVisibility(GONE)
}
}

override fun onClick(v: View?) {
outUseMethodToggle()
}

/**
* 外部也可调用 展开或关闭
*/
fun outUseMethodToggle() {
if (isExpand) {
hide()
txtViewTip!!.text = strExpandText
} else {
expand()
txtViewTip!!.text = strHideText
}
isExpand = !isExpand
}

/**
* 外部可随时添加子view
*/
fun outUseMethodAddItem(view: View) {
val childCount = childCount
//插在底部之前
addView(view, childCount - 1)
refreshView(view)
}
}


📢作者:小空和小芝中的小空


作者:芝麻粒儿
链接:https://juejin.cn/post/7130639529123250213
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用Flutter写一个单例

在Flutter中创建单例可以使用Dart语言中的静态变量和工厂方法的组合来实现。下面是一个示例代码: class MySingleton { // 静态变量 static final MySingleton _singleton = MySingle...
继续阅读 »

在Flutter中创建单例可以使用Dart语言中的静态变量和工厂方法的组合来实现。下面是一个示例代码:


class MySingleton {
// 静态变量
static final MySingleton _singleton = MySingleton._internal();

// 工厂方法
factory MySingleton() {
return _singleton;
}

// 私有构造函数
MySingleton._internal();

// 其他方法
void doSomething() {
print("Doing something...");
}
}

在上面的代码中,MySingleton类有一个私有的构造函数,这意味着它不能直接实例化。


相反,它使用一个静态变量 _singleton 来存储唯一的实例,并使用一个工厂方法来获取该实例。因此,当您需要引用该单例时,您只需调用 MySingleton() 方法,就可以得到唯一的实例。


要使用该单例,只需调用 MySingleton() 方法,并调用其公共方法,如 doSomething()


MySingleton mySingleton = MySingleton();
mySingleton.doSomething();

Flutter单例模式可以在以下场景中使用:



  1. 网络请求:在网络请求过程中,您可能只需要一个单例来管理所有的HTTP客户端和连接。使用单例模式可以确保只有一个实例在整个应用程序中被创建和使用,这样可以节约系统资源并避免重复创建相同的实例。

  2. 数据库操作:在应用程序中,您可能需要与数据库进行交互。使用单例模式,您可以确保只需要一个单例来管理数据库连接并执行所有数据库操作。

  3. 状态管理:在Flutter中,您可以使用单例模式来管理应用程序状态。您可以创建一个具有全局作用域的单例,以存储和管理应用程序中的状态,并确保在整个应用程序中只有一个实例在使用。

  4. 全局管理:在某些情况下,您可能需要在整个应用程序中共享某些对象或数据。使用单例模式,您可以创建一个具有全局作用域的单例来存储这些对象和数据,并确保在整个应用程序中只有一个实例在使用。 在这些场景中,使用单例模式可以简化代码并提高应用程序性能,避免了创建多个重复的对象的开销。

作者:早起的年轻人
链接:https://juejin.cn/post/7230416597012463677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter list 数组排序

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。 sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。 以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序: List<...
继续阅读 »

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。
sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。
以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序:


List<int> numbers = [1, 3, 2, 5, 4];
// 升序排序
numbers.sort((a, b) => a.compareTo(b));
print(numbers); // 输出:[1, 2, 3, 4, 5]

// 降序排序
numbers.sort((a, b) => b.compareTo(a));
print(numbers); // 输出:[5, 4, 3, 2, 1]

在上述代码中,我们使用了sort()方法将数字列表按照升序和降序进行了排序。


在比较函数中,我们使用了 compareTo() 方法来比较两个数字对象。


如果想按照其他字段进行排序,只需将比较函数中的a和b替换为您想要排序的字段即可。




以下是示例代码,假设您有一个包含Person对象的列表,可以按照Person的年龄字段进行排序:


class Person {
String name;
int age;

Person({this.name, this.age});
}

List<Person> persons = [
Person(name: "John", age: 30),
Person(name: "Jane", age: 20),
Person(name: "Bob", age: 25),
];

// 按照年龄字段进行排序
persons.sort((a, b) => a.age.compareTo(b.age));

// 输出排序后的列表
print(persons);

在上述代码中,我们使用了sort()函数将Person对象列表按照年龄字段进行排序。
在该示例中,我们使用了compareTo()函数来比较Person对象的年龄字段,并按照升序排序。


作者:早起的年轻人
链接:https://juejin.cn/post/7230420475494137913
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

No Focused Window ANR是怎样产生的

ANR
之前我们讲过因为事件没有得到及时处理,引起的ANR问题。但这只是Input Dispatching Timeout中的一种情况,还有一种情况,在我们应用中出现的也很常见,就是No Focused Window ANR,这个又是在哪些情况下产生的呢? 由之前的文...
继续阅读 »

之前我们讲过因为事件没有得到及时处理,引起的ANR问题。但这只是Input Dispatching Timeout中的一种情况,还有一种情况,在我们应用中出现的也很常见,就是No Focused Window ANR,这个又是在哪些情况下产生的呢?


由之前的文章,我们知道,点击事件都是由InputDispatcher来分发的,我们直接来看InputDispatcher的源码。


No Focused Window ANR如何产生


如果是Key事件,或Motion事件,都需要找到焦点窗口取处理,都会调用到findFocusedWindowTargetsLocked()。


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
const EventEntry& entry,
std::vector<InputTarget>& inputTargets,
nsecs_t* nextWakeupTime) {
std::string reason;

int32_t displayId = getTargetDisplayId(entry);
// mFocusedWindowHandlesByDisplay在setInputWindowsLocked()里赋值
sp<InputWindowHandle> focusedWindowHandle =
getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
// mFocusedApplicationHandlesByDisplay在setFocusedApplication()里赋值
sp<InputApplicationHandle> focusedApplicationHandle =
getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);

// focusedWindowHandle和focusedApplicationHandle都为空时表示当前无窗口,该事件会被丢弃,不会执行dispatchEventLocked
// 一般出现两个都为空的场景,是在窗口切换的过程,此时不处理事件注入
if (focusedWindowHandle == nullptr && focusedApplicationHandle == nullptr) {
return INPUT_EVENT_INJECTION_FAILED;
}

// focusedWindowHandle为空但focusedApplicationHandle不为空时开始ANR检查
if (focusedWindowHandle == nullptr && focusedApplicationHandle != nullptr) {
// 默认mNoFocusedWindowTimeoutTime没有值,第一次检查ANR会走下面这个流程
if (!mNoFocusedWindowTimeoutTime.has_value()) {
// DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5s * HwTimeoutMultiplier();
// 默认input dispatch timeout时间时5s
const nsecs_t timeout = focusedApplicationHandle->getDispatchingTimeout(
DEFAULT_INPUT_DISPATCHING_TIMEOUT.count());
// 给mNoFocusedWindowTimeoutTime赋值,触发ANR时会检查这个值是否为空,不为空才触发ANR
mNoFocusedWindowTimeoutTime = currentTime + timeout;
// 把当前的focusedApplicationHandle赋值给mAwaitedFocusedApplication,触发ANR时会检查这个值是否为空,不为空才触发ANR
mAwaitedFocusedApplication = focusedApplicationHandle;
mAwaitedApplicationDisplayId = displayId;
*nextWakeupTime = *mNoFocusedWindowTimeoutTime;
// 返回INPUT_EVENT_INJECTION_PENDING表示dispatchKeyLocked()或者dispatchMotionLocked()为false
return INPUT_EVENT_INJECTION_PENDING;
} else if (currentTime > *mNoFocusedWindowTimeoutTime) {
// Already raised ANR. Drop the event
return INPUT_EVENT_INJECTION_FAILED;
} else {
// Still waiting for the focused window
return INPUT_EVENT_INJECTION_PENDING;
}
}

// 如果走到这个流程,说明没有ANR,清空mNoFocusedWindowTimeoutTime和mAwaitedFocusedApplication
resetNoFocusedWindowTimeoutLocked();
return INPUT_EVENT_INJECTION_SUCCEEDED;
}

主要逻辑:



  • 如果focusedWindowHandle和focusedApplicationHandle都为null,一般发生在窗口切换的时候,返回INPUT_EVENT_INJECTION_FAILED,直接drop事件,不做处理

  • 如果focusedWindowHandle为null,focusedApplicationHandle不为null,返回INPUT_EVENT_INJECTION_PENDING,在nextWakeupTime之后唤醒,检查是否发生ANR

    • mNoFocusedWindowTimeoutTime:记录no focused window timeout的时间

    • mAwaitedFocusedApplication:记录focusedApplicationHandle

    • nextWakeupTime: 下次唤醒pollInner的时间




接下来看看检查ANR的逻辑:


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
nsecs_t InputDispatcher::processAnrsLocked() {
const nsecs_t currentTime = now();
nsecs_t nextAnrCheck = LONG_LONG_MAX;
// 在findFocusedWindowTargetsLocked()中,如果focusedWindowHandle为空,focusedApplicationHandle不为空,以下条件就会满足
if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
// mNoFocusedWindowTimeoutTime为检查时间+5s,如果currentTime大于等于mNoFocusedWindowTimeoutTime,表示超时
if (currentTime >= *mNoFocusedWindowTimeoutTime) {
// 触发ANR流程,此处触发的ANR类型是xxx does not have a focused window
processNoFocusedWindowAnrLocked();
// 清空mAwaitedFocusedApplication,下次就不会再走ANR流程
mAwaitedFocusedApplication.clear();
mNoFocusedWindowTimeoutTime = std::nullopt;
return LONG_LONG_MIN;
} else {
// Keep waiting
const nsecs_t millisRemaining = ns2ms(*mNoFocusedWindowTimeoutTime - currentTime);
ALOGW("Still no focused window. Will drop the event in %" PRId64 "ms", millisRemaining);
// 还没有超时,更新检查时间
nextAnrCheck = *mNoFocusedWindowTimeoutTime;
}
}
....
// 如果走到这个流程,ANR类型是xxx is not responding. Waited xxx ms for xxx
// 这个地方,focusedWindowHandle和focusedApplicationHandle都是不为空的场景
onAnrLocked(*connection);
return LONG_LONG_MIN;
}

主要流程:



  • 如果mNoFocusedWindowTimeoutTime有值,且mAwaitedFocusedApplication不为空

    • 超时:调用processNoFocusedWindowAnrLocked触发ANR

    • 未超时:更新检查时间



  • 继续检查input事件是否超时,如果超时,则调用onAnrLocked触发ANR


接下来,我们看看processNoFocusedAnrLocked的流程:


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::processNoFocusedWindowAnrLocked() {
// 在触发ANR前,再获取一次当前的focusedApplication
sp<InputApplicationHandle> focusedApplication =
getValueByKey(mFocusedApplicationHandlesByDisplay, mAwaitedApplicationDisplayId);
// 检查触发ANR时的条件是focusedApplication不为空
// 如果此时focusedApplication为空,或者focusedApplication不等于前一个mAwaitedFocusedApplication表示已经切换application focus,取消触发ANR
if (focusedApplication == nullptr ||
focusedApplication->getApplicationToken() !=
mAwaitedFocusedApplication->getApplicationToken()) {
return; // The focused application has changed.
}
// 在触发ANR前,再获取一次当前的focusedWindowHandle
const sp<InputWindowHandle>& focusedWindowHandle =
getFocusedWindowHandleLocked(mAwaitedApplicationDisplayId);
// 检查触发ANR时focusedWindowHandle为空,如果此时focusedWindowHandle不为空,取消触发ANR
if (focusedWindowHandle != nullptr) {
return; // We now have a focused window. No need for ANR.
}
// 通过前面的判断,还是无法拦截,说明该ANR无可避免,最终触发ANR
// 早期代码没有前面一系列的判断,是直接触发的ANR,会在性能较差的场景下出现误判
onAnrLocked(mAwaitedFocusedApplication);
}

主要流程:



  • 在这个方法里面,再次检查focusedApplication

    • 如果当前focusedApplication为空,或者和之前记录的mAwaitedFocusedApplication不一致,则说明窗口已经切换,不需要报ANR



  • 再次检查focusedWindow是否未空

    • 如果不为空,则不需要报ANR



  • 检查都通过之后,才会调用onAnrLocked,报no Focused Window ANR


focusedApplication设置流程


// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
void InputDispatcher::setFocusedApplication(
int32_t displayId, const sp<InputApplicationHandle>& inputApplicationHandle) {
{ // acquire lock
std::scoped_lock _l(mLock);
// 获取当前的focusedApplicationHandle
sp<InputApplicationHandle> oldFocusedApplicationHandle =
getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);
// 如果当前的focusedApplicationHandle跟触发ANR是的focusedApplicationHandle是一样且
// 新的focusedApplicationHandle跟旧的不一样,说明focusedApplicationHandle有更新
// 需要重置ANR计时
if (oldFocusedApplicationHandle == mAwaitedFocusedApplication &&
inputApplicationHandle != oldFocusedApplicationHandle) {
// 重置ANR计时
resetNoFocusedWindowTimeoutLocked();
}

if (inputApplicationHandle != nullptr && inputApplicationHandle->updateInfo()) {
if (oldFocusedApplicationHandle != inputApplicationHandle) {
// 赋值新的inputApplicationHandle到mFocusedApplicationHandlesByDisplay,在findFocusedWindowTargetsLocked()时用到
mFocusedApplicationHandlesByDisplay[displayId] = inputApplicationHandle;
}
} else if (oldFocusedApplicationHandle != nullptr) {
// 如果inputApplicationHandle为空,oldFocusedApplicationHandle不为空,需要清除oldFocusedApplicationHandle
oldFocusedApplicationHandle.clear();
// 走到这个流程会出现findFocusedWindowTargetsLocked()中focusedApplicationHandle为空
mFocusedApplicationHandlesByDisplay.erase(displayId);
}
} // release lock

// Wake up poll loop since it may need to make new input dispatching choices.
mLooper->wake();
}

主要流程:



  • 如果inputApplicationHandle与oldFocusedApplication,则要重置ANR计时

  • 如果inputApplicationHandle不为空,则更新map中的值

  • 如果inputApplicationHandle为空,则清除oldFocusedApplication


这个方法,是从AMS调过来的,主要流程如下图:
image.png


focusedWindow设置流程


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// 当VSYNC信号来了之后,会调用到SurfaceFlinger的onMessageInvalidate()方法
// SurfaceFlinger::onMessageInvalidate()
// ==> SurfaceFlinger: updateInputFlinger()
// ==> SurfaceFlinger: updateInputWindowInfo()
// ==> InputManager::setInputWindows()
// ==> InputDispatcher::setInputWindows()
// ==> InputDispatcher::setInputWindowsLocked()
void InputDispatcher::setInputWindowsLocked(
const std::vector<sp<InputWindowHandle>>& inputWindowHandles, int32_t displayId) {

// ......
const std::vector<sp<InputWindowHandle>> oldWindowHandles = getWindowHandlesLocked(displayId);
// 更新mWindowHandlesByDisplay这个map,然后通过getWindowHandlesLocked()找newFocusedWindowHandle
updateWindowHandlesForDisplayLocked(inputWindowHandles, displayId);

sp<InputWindowHandle> newFocusedWindowHandle = nullptr;
bool foundHoveredWindow = false;
// 在mWindowHandlesByDisplay这个map里面找newFocusedWindowHandle
for (const sp<InputWindowHandle>& windowHandle : getWindowHandlesLocked(displayId)) {
// newFocusedWindowHandle要不为空,windowHandle具备focusable和visible属性
if (!newFocusedWindowHandle && windowHandle->getInfo()->hasFocus &&
windowHandle->getInfo()->visible) {
// 给newFocusedWindowHandle赋值,最后这个值存到mFocusedWindowHandlesByDisplay这个map
newFocusedWindowHandle = windowHandle;
}
if (windowHandle == mLastHoverWindowHandle) {
foundHoveredWindow = true;
}
}

if (!foundHoveredWindow) {
mLastHoverWindowHandle = nullptr;
}

// 在mFocusedWindowHandlesByDisplay这个map里找当前的焦点窗口
sp<InputWindowHandle> oldFocusedWindowHandle =
getValueByKey(mFocusedWindowHandlesByDisplay, displayId);

// 判断oldFocusedWindowHandle是否等于newFocusedWindowHandle,如果相等则不走focus change流程
if (!haveSameToken(oldFocusedWindowHandle, newFocusedWindowHandle)) {
// 如果当前的焦点窗口不为空,需要从mFocusedWindowHandlesByDisplay移除掉
if (oldFocusedWindowHandle != nullptr) {
sp<InputChannel> focusedInputChannel =
getInputChannelLocked(oldFocusedWindowHandle->getToken());
if (focusedInputChannel != nullptr) {
CancelationOptions options(CancelationOptions::CANCEL_NON_POINTER_EVENTS,
"focus left window");
synthesizeCancelationEventsForInputChannelLocked(focusedInputChannel, options);
// 新建一个FocusEntry加入到mInboundQueue去dispatch
enqueueFocusEventLocked(*oldFocusedWindowHandle, false /*hasFocus*/);
}
// oldFocusedWindowHandle不为空时需要移除旧的
mFocusedWindowHandlesByDisplay.erase(displayId);
}
// 走到这个流程,如果oldFocusedWindowHandle不为空,newFocusedWindowHandle为空,那么在findFocusedWindowTargetsLocked()中的focusedWindowHandle为空
// 如果newFocusedWindowHandle不为空,更新mFocusedWindowHandlesByDisplay
if (newFocusedWindowHandle != nullptr) {
// 更新mFocusedWindowHandlesByDisplay,在findFocusedWindowTargetsLocked()时用到
mFocusedWindowHandlesByDisplay[displayId] = newFocusedWindowHandle;
// 新建一个FocusEntry加入到mInboundQueue去dispatch
enqueueFocusEventLocked(*newFocusedWindowHandle, true /*hasFocus*/);
}

if (mFocusedDisplayId == displayId) {
// 添加focusChanged到mCommandQueue,在dispatchOnce时会执行
onFocusChangedLocked(oldFocusedWindowHandle, newFocusedWindowHandle);
}
}

// ......
}

这个方法,是从WMS调过来的,主要流程如下图:
image.png


ANR可能的原因



  1. 设置focusedApplication和focusedWindow中间时间差太长,在这个时间差内发生了ANR



  • 设置focusedApplication发生在resumeTopActivity,也就是am_set_resumed_activity的时候。

  • 设置focusedWindow发生在onResume结束后,也就是调用WMS的addView添加完窗口之后。


在这个过程中,很有很多的生命周期流程,包括前一个Activity的onPause,Applicaiton的attachBaseContext, onCreate, Activity的onCreate,onStart,onResume。所有方法加起来耗时不能超过5s,否则很容易发生ANR。



  1. window被设置成了no_focusable,无法响应焦点。



  • 如果误将一个window设置成no_focusable,则窗口无法成为focusedWindow,也可能导致ANR的发生。

  • 不过这种情况一般比较少出现。

作者:尹学姐
链接:https://juejin.cn/post/7224839996402122809
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android自定义控件之虚拟摇杆(遥控飞机)

前言 之前在开发项目中,有一个功能是,设计一个虚拟摇杆,操作大疆无人机飞行,在实现过程中感觉比较锻炼自定义View的能力,在此记录一下,本文中摇杆代码从项目中抽取出来重新实现,如下是程序运行图: 功能分析 本次自定义View功能需求如下: 1.摇杆绘制 自定...
继续阅读 »

前言


之前在开发项目中,有一个功能是,设计一个虚拟摇杆,操作大疆无人机飞行,在实现过程中感觉比较锻炼自定义View的能力,在此记录一下,本文中摇杆代码从项目中抽取出来重新实现,如下是程序运行图:
虚拟摇杆.gif


功能分析


本次自定义View功能需求如下:

1.摇杆绘制

自定义View绘制摇杆大小圆,手指移动时只改变小圆位置,当手指触摸点在大圆外时,小圆圆心在大圆边缘上,并且绘制一条蓝色弧线,绘制度数为小圆圆心位置向两侧延伸45度(一般UI设计的时候,会给特定的圆弧形图片,如果显示图片就需要将图片移动到小圆圆心位置,之后根据手指触摸点与大圆圆心夹角来旋转图片,目前没有找到类似的圆弧图片,后期看能不能找到类似的)。

2.摇杆移动数据返回

返回摇杆移动产生的数据,根据这些数据控制飞行图片移动。在这里我返回的是飞机图片x,y坐标应该改变的值。这个值具体如何获得,在下面代码实现中讲解。

3.飞机图片移动

飞机图片移动相对简单,只需要在接收到摇杆数据的时候,修改飞机图片绘制位置,并重绘即可,需要注意的地方是摇杆移动飞机超出View边界该怎么处理。


代码实现


摇杆绘制和摇杆移动数据返回,通过自定义的RockerView内实现,飞机图片移动,通过自定义的FlyView实现,上述功能在RockerView和FlyView代码实现里面介绍。


摇杆(RockerView)


我们可以先从摇杆如何绘制开始。


首先从RockerView开头声明一些绘制需要一些变量,比如画笔,圆心坐标,手指触摸点坐标,圆半径等变量。


在init()方法内对画笔样式,颜色,View默认宽高等数据进行设置。


在onMeasure()方法内获取View的宽高模式,该方法简单可以概况为,宽高有具体值或者为match_parent。宽高设置为MeasureSpec.getSize()方法获取的数据,之后宽高值取两者中最小值,当宽高值在xml设置为wrap_content时,宽高取默认值,之后在方法末尾通过setMeasuredDimension()设置宽高。


在onLayout()方法内,对绘制圆等图像用到的变量进行赋值,例如,大圆圆心xy值,小圆圆心xy值,大小圆半径,绘制蓝色圆弧矩形,RockerView宽高等数据。


之后是onDraw()方法,在该方法内绘制大小圆,蓝色圆弧等图案。只不过蓝色圆弧需要加上判断条件来控制是否绘制。


手指触摸时绘制小圆位置改变,则需要重写onTouchEvent()方法,当手指按下或移动时,需要更新手指触摸点坐标,并判断手指触摸点是否超出大圆,超出大圆时,需要计算小圆圆心位置,并且还需要计算手指触摸点与圆心连线和x正半轴形成的夹角。并且通过接口返回摇杆移动的数据,飞机图片根据这些数据来移动。


绘制代码简单介绍如上,下面对View内一些需要注意地方进行介绍。如果看到完整代码,里面有一个自定义方法是initAngle(),该方法代码如下:


/** 计算夹角度数,并实现小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//范围-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点距离大圆圆心距离
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//距离大于半圆半径时,固定小圆圆心在大圆边缘上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}

这个方法用在onTouchEvent()方法的手指按下与移动事件中应用,这个方法前两行代码是计算手指触摸点与圆心连线和x正半轴形成的夹角取值,夹角取值范围如下图所示。
图片.png
代码先通过Math.atan2(y,x)方法获取手指触摸点与圆心连线和x正半轴之间的弧度制,获取弧度后通过(float) (radian * (180 / Math.PI))获取对应的度数,这里特别注意下Math.atan2(y,x)方法是y值在前,x在后。

此外这个方法还计算了手指触摸点与大圆圆心距离,以及判断手指触摸点是否在大圆外,以及在大圆外时,获取在大圆边缘上的小圆圆心的xy值。


在计算小圆圆心的坐标需要了解一个地方是,view实现过程中使用的坐标系是屏幕坐标系,屏幕坐标系是以View左上角为原点,原点左边是x的正半轴,原点下面是y正半轴,屏幕坐标系和数学坐标系是不一样。小圆圆心坐标获取原理,是根据三角形的相似原理获取,小圆圆心的坐标获取原理如下图所示:


图片.png
在上图中可以看到小圆y坐标的获取,小圆x坐标获取与y获取类似。可以直接把公式套进去。关于摇杆绘制的内容,至此差不多完成了,下面来处理返回摇杆移动数据的功能。


返回摇杆移动数据是通过自定义接口实现的。在触摸事件返回摇杆移动数据的事件有手指按下与移动。我们代码可以写为下面的形式(下面代码是伪代码)。


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//返回摇杆移动数据的方法
break;
case MotionEvent.ACTION_UP:
...
break;
}
postInvalidate();
return true;
}

如果按照上面代码写法我们会发现,当我们手指按下不动的时候或者手指按下移动一会后手指不动,是不会触发ACTION_MOVE事件的,不触发这个事件,我们就无法返回摇杆移动的数据,进而无法控制飞机改变位置。效果图如下


虚拟摇杆_按下不移动的问题.gif
解决这个问题,需要使用Handler和Runnable,在Runnable的run方法内,实现接口方法,并调用自身。getFlyOffset()是传递摇杆移动数据的方法,代码如下:


private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};

之后在手指按下与点击事件里面,先判断Handler有没有开始,若isStart为true,则isStart改为false,并移除mRunnable,之后isStart改为true,延迟16ms执行mRunnable,当手指抬起时,若Handler状态为开始,则修改状态为false并移除mRunnable,这样就解决了手指按下不移动时,传递摇杆数据,相关代码如下:


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
...
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
...
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}

至此摇杆相关功能介绍完毕,RockerView完整代码如下:


public class RockerView extends View {
private final int VELOCITY = 40;//飞机速度

private Paint smallCirclePaint;//小圆画笔
private Paint bigCirclePaint;//大圆画笔
private Paint sideCirclePaint;//大圆边框画笔
private Paint arcPaint;//圆弧画布
private int smallCenterX = -1, smallCenterY = -1;//绘制小圆圆心 x,y坐标
private int bigCenterX = -1,bigCenterY = -1;//绘制大圆圆心 x,y坐标
private int touchX = -1, touchY = -1;//触摸点 x,y坐标
private float bigRadiusProportion = 69F / 110F;//大圆半径占view一半宽度的比例 用于获取大圆半径
private float smallRadiusProportion = 4F / 11F;//小圆半径占view一半宽度的比例
private float bigRadius = -1;//大圆半径
private float smallRadius = -1;//小圆半径
private double distance = -1; //手指按压点与大圆圆心的距离
private double radian = -1;//弧度
private float angle = -1;//度数 -180~180
private int viewHeight,viewWidth;
private int defaultViewHeight, defaultViewWidth;
private RectF arcRect = new RectF();//绘制蓝色圆弧用到矩形
private int drawArcAngle = 90;//圆弧绘制度数
private int arcOffsetAngle = -45;//圆弧偏移度数
private int drawTime = 16;//告诉flyView重绘的时间间隔 这里是16ms一次
private boolean isBigCircleOut = false;//触摸点在大圆外

private boolean isStart = false;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};

public RockerView(Context context) {
super(context);
init(context);
}

public RockerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

public RockerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}


private void init(Context context) {
defaultViewWidth = DensityUtil.dp2px(context,220);
defaultViewHeight = DensityUtil.dp2px(context,220);

bigCirclePaint = new Paint();
bigCirclePaint.setStyle(Paint.Style.FILL);
bigCirclePaint.setStrokeWidth(5);
bigCirclePaint.setColor(Color.parseColor("#1AFFFFFF"));
bigCirclePaint.setAntiAlias(true);

smallCirclePaint = new Paint();
smallCirclePaint.setStyle(Paint.Style.FILL);
smallCirclePaint.setStrokeWidth(5);
smallCirclePaint.setColor(Color.parseColor("#4DFFFFFF"));
smallCirclePaint.setAntiAlias(true);

sideCirclePaint = new Paint();
sideCirclePaint.setStyle(Paint.Style.STROKE);
sideCirclePaint.setStrokeWidth(DensityUtil.dp2px(context, 1));
sideCirclePaint.setColor(Color.parseColor("#33FFFFFF"));
sideCirclePaint.setAntiAlias(true);

arcPaint = new Paint();
arcPaint.setColor(Color.parseColor("#FF5DA9FF"));
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(5);
arcPaint.setAntiAlias(true);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取视图的宽高的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width,height;
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else {
width = defaultViewWidth;
}

if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else {
height = defaultViewHeight;
}
width = Math.min(width,height);
height = width;
//设置视图的宽度和高度
setMeasuredDimension(width,height);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
bigCenterX = getWidth() / 2;
bigCenterY = getHeight() / 2;
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;

bigRadius = bigRadiusProportion * Math.min(bigCenterX, bigCenterY);
smallRadius = smallRadiusProportion * Math.min(bigCenterX, bigCenterY);

arcRect.set(bigCenterX-bigRadius,bigCenterY-bigRadius,bigCenterX+bigRadius,bigCenterY+bigRadius);
viewHeight = getHeight();
viewWidth = getWidth();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, bigCirclePaint);
canvas.drawCircle(smallCenterX, smallCenterY, smallRadius, smallCirclePaint);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, sideCirclePaint);

if (isBigCircleOut) {
canvas.drawArc(arcRect,angle+arcOffsetAngle,drawArcAngle,false,arcPaint);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
touchX = (int) event.getX();
touchY = (int) event.getY();
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;
isBigCircleOut = false;
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}

/** 计算夹角度数,并实现小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//范围-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点距离大圆圆心距离
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//距离大于半圆半径时,固定小圆圆心在大圆边缘上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}

/** 获取飞行偏移量 */
private void getFlyOffset() {
float x = (smallCenterX - bigCenterX) * 1.0f / viewWidth * VELOCITY;
float y = (smallCenterY - bigCenterY) * 1.0f / viewHeight * VELOCITY;
onRockerListener.getDate(this, x, y);
}

/**
* pX,pY为手指按点坐标减view的坐标
*/
public interface OnRockerListener {
public void getDate(RockerView rocker, final float pX, final float pY);
}
private OnRockerListener onRockerListener;
public void getDate(final OnRockerListener onRockerListener) {
this.onRockerListener = onRockerListener;
}
}

飞机(FlyView)


飞机图片移动相对简单,实现原理是在自定义View里面,通过改变绘制图片方法(drawBitmap()方法)里的left,top值来模拟飞机移动。FlyView实现代码如下:


public class FlyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int viewHeight, viewWidth;
private int imgHeight, imgWidth;
private int left, top;

public FlyView(Context context) {
super(context);
init(context);
}

public FlyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

public FlyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

void init(Context context) {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.fly);
imgHeight = mBitmap.getHeight();
imgWidth = mBitmap.getWidth();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewHeight = h;
viewWidth = w;
left = w / 2 - imgHeight / 2;
top = h / 2 - imgWidth / 2;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, left, top, mPaint);
}

/** 移动图片 */
public void move(float x, float y) {
left += x;
top += y;
if (left < 0) {
left = 0;
}else if (left > viewWidth - imgWidth) {
left = viewWidth - imgWidth;
}

if (top < 0) {
top = 0;
} else if (top > viewHeight - imgHeight) {
top = viewHeight - imgHeight;
}
postInvalidate();
}
}

在Activity或者Fragment里面对View设置代码(kotlin)如下:


binding.viewRocker.getDate { _, pX, pY ->
binding.viewFly.move(pX, pY)
}

飞机图片如下:


fly.png


总结


摇杆整体实现没有太复杂的逻辑,比较容易混的地方,可能是屏幕坐标系和数学坐标系能不能转过弯来。印象中好像可以通过Matrix将坐标变换,但一时间想不起来怎么实现,后面了解下Matrix相关内容。

关于虚拟摇杆实现有很多方式,我写的这个不是最优的方式,虚拟摇杆有些需求没有接触到,在代码实现中可能比较简单,小伙伴们看到文章不足的地方,可以留言告诉我,一起学习交流下。


项目地址: GitHub


作者:卤肉拌面
链接:https://juejin.cn/post/7225079705992822842
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我受精了,Git log竟然还有这种用法!

Git
前言 当你使用Git进行版本控制时,经常需要查看提交历史记录。Git提供了git log命令来查看版本历史记录。 在本文中,我们将介绍如何使用git log命令来查看Git提交历史记录。 查看提交历史记录 通过在命令行中输入以下命令来查看提交历史记录: git...
继续阅读 »

前言


当你使用Git进行版本控制时,经常需要查看提交历史记录。Git提供了git log命令来查看版本历史记录。


在本文中,我们将介绍如何使用git log命令来查看Git提交历史记录。


查看提交历史记录


通过在命令行中输入以下命令来查看提交历史记录:


git log

这将显示最新的提交历史记录,包括提交ID、作者、提交日期、提交消息等。例如:


commit 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 22:21:48 2023 -0500

Added new feature

commit b3f3f066e75a7d3352898c9eddf23baa7265f5b5
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 17:32:16 2023 -0500

Fixed bug

commit 0498b3d96b2732e36e7d41501274c327a38188aa
Author: Zhangsan <Zhangsan@example.com>
Date: Fri Apr 23 14:01:11 2023 -0500

Updated documentation

显示分支图


如果你想查看分支的提交历史记录,你可以使用git log --graph命令来显示一个分支图。分支图可以帮助你更好地理解分支之间的关系和合并历史。


例如:


git log --graph

这将显示一个分支图,其中每个提交都显示为一个节点,不同的分支用不同的线表示。你可以在分支图中看到合并提交和分支之间的关系。


例如:


* commit da32d1d7e7f22ec59330e6b8c51def819b951aec
| Author: Zhangsan <Zhangsan@example.com>
| Date: Wed Apr 12 15:28:40 2023 +0800
|
| feat:xxx
|
* commit 8fdc0a9838d45d9e027740e7a448822bb8431969
|\ Merge: e22ce87ae d80ce707b
| | Author: Zhangsan <Zhangsan@example.com>
| | Date: Wed Apr 12 13:08:17 2023 +0800
| |
| | Merge branch 'xxx' into xxx
| |
| * commit d80ce707b72e1231c18a4843e62175a7a430e3c3
| | Author: Zhangsan <Zhangsan@example.com>
| | Date: Tue Apr 11 19:36:48 2023 +0800
| |
| | xxxx
| |

格式化输出


git log命令还支持格式化输出,你可以使用--pretty选项来指定输出的格式。例如,以下命令将以一种类似于JSON的格式输出提交记录:


git log --pretty=format:'{%n "commit": "%H",%n "author": "%an <%ae>",%n "date": "%ad",%n "message": "%f"%n},' --no-merges

这将输出每个提交的哈希值、作者、提交日期和提交消息。


例如:


{
"commit": "a8c4b34ab5e4d844dc741e105913266502d82dcd",
"author": "Zhangsan <Zhangsan@example.com>",
"date": "Sun Apr 16 16:32:20 2023 +0800",
"message": "feat-resize"
},
{
"commit": "f23b4e61633033b9db5a3c87afc5f523cf5e583e",
"author": "Zhangsan <Zhangsan@example.com>",
"date": "Sat Apr 15 15:32:25 2023 +0800",
"message": "feat"
}

你也可以使用一些预定义的格式来输出,例如--pretty=oneline将每个提交压缩成一行,只包含哈希值和提交消息。


例如:


a3fe1d136ab9587db19d9f8073fd491ead892f4a feat:xxxx
84738075dd00f1e0712f139c23c276b7559fd0d9 feat:xxxx
a8c4b34ab5e4d844dc741e105913266502d82dcd feat:xxxx
f23b4e61633033b9db5a3c87afc5f523cf5e583e feat:xxxx

查看详细信息


默认情况下,git log命令只显示每个提交的基本信息。但是,你可以通过添加--stat选项来显示每个提交所做的更改数量和文件列表。


例如:


git log --stat

这将显示每个提交所做的更改数量和文件列表


例如:


commit 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 22:21:48 2023 -0500

Added new feature

somefile.txt | 1 +
1 file changed, 1 insertion(+)

commit b3f3f066e75a7d3352898c9eddf23baa7265f5b5
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 17:32:16 2023 -0500

Fixed bug

somefile.txt | 1 -
1 file changed, 1 deletion(-)

commit 049

查看某个提交的详细信息


除了git log命令,我们还可以使用git show


如果你想查看某个提交的详细信息,可以使用git show <commit>命令。


例如:


git show 6d883ef

这将显示提交6d883ef的详细信息,包括提交消息、作者、提交日期和更改的文件。


查看某分支记录


有时候你可能只想查看某个特定分支历史记录。这可以使用git log <branch>命令。


例如,如果你只想查看main分支的历史记录,你可以输入以下命令:


git log main

显示指定文件的提交历史记录


如果你只想查看某个文件的提交历史记录,你可以使用git log <filename>命令。


例如:


git log somefile.txt

这将显示与该文件相关的所有提交历史记录。


显示指定作者的提交历史记录


如果你只想查看某个作者的提交历史记录,你可以使用git log --author=<author>命令。例如:


例如:


git log --author="Zhangsan"

这将显示所有由Zhangsan提交的历史记录。


显示指定时间段的提交记录


如果你指向查看某个时间范围内的提交历史记录、你可以使用git log --after,git log --before命令。


例如:


git log --after="2023-04-01" --before="2023-04-02"

这将显示出,2023-04-01 到 2023-04-02之间的提交记录



  • --after 会筛选出指定时间之后的提交记录

  • --before 会筛选出指定时间之前的提交记录。


还有一些快捷命令:


git log --after="yesterday" //显示昨天的记录
git log --after="today" //显示今天的
git log --before="10 day ago" // 显示10天前的提交记录
git log --after="1 week ago" //显示最近一周的提交录
git log --after="1 month ago" //显示最近一个月的提交率

限制输出的历史记录数量


例如,要查看最近的5个提交,你可以使用以下命令:


git log -5

搜索历史记录


git log命令还可以用于搜索历史记录。例如,如果你只想查看提交消息中包含关键字“bug”或“fix”的历史记录,可以使用以下命令:


git log --grep=bug --grep=fix

这将显示所有提交消息中包含关键字“bug”或“fix”的提交记录。


commit 27ad72addeba005d7194132789a22820d994b0a9
Author: Zhangsan <Zhangsan@example.com>
Date: Thu Apr 13 11:17:13 2023 +0800

fix:还原local环境配置

commit 8369c45344640b3b7215de957446d7ee13a48019
Author: Zhangsan <Zhangsan@example.com>
Date: Mon Apr 10 11:02:47 2023 +0800

fix:获取文件

显示带有内容变更的提交日志


如果你想查看带有内容变更的提交日志,可以使用git log -p,能清楚的看到每次提交的内容变化。
非常有用的一个命令;


例如:


git log -p

这将显示每个提交与它的父提交之间的差异。


diff --git a/xxxx.tsx b/xxxx.tsx
index 7f796c934..87b365426 100644
--- a/xxx.tsx
+++ b/xxx.tsx

我们也可以知道某个具体的提交的差异,可以使用git log -p <commit>命令


显示提交的差异


如果你想查看某个提交与上一个提交之间的差异,可以使用git log -p <commit>命令。例如:


git log -p 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429

这将显示提交6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429与它的父提交之间的差异。


显示当前分支的合并情况


如果你想查看当前分支的合并情况,可以使用git log --merges命令。例如:


git log --merges

commit 2f3f4c45a7be3509fff6496c9de6d13ef0964c9d
Merge: 8369c4534 4103a08bf
Author: xxx <xxx@xx.com>
Date: Mon Apr 10 11:03:55 2023 +0800

Merge branch 'dev/feature1' into dev/dev

commit 14b40421ef54c875b8f8f0cfc297bcdc3960b9be
Merge: 30e36edbb 48bb05ede
Author: xxx <xxx@xx.com>
Date: Mon Apr 10 00:34:09 2023 +0800

Merge branch 'dev/feature1' into dev/dev

总结


以上是更多关于git log命令的使用教程、示例,希望对你有所帮助。


作者:前端后腿哥
链接:https://juejin.cn/post/7225902885544165436
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

无聊的分享:点击EditText以外区域隐藏软键盘

1.前言 当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小...
继续阅读 »

1.前言


当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小小的思路。


2.如何实现


当我们在Activity单纯的添加一个EditText时,点击吊起软键盘,这个时候再点击EditText外部区域会是这个样子的:



会发现,无论我们怎么点击外部区域软键盘都不会收起。所以要达到点击外部区域收起键盘效果需要我们自己添加方法去隐藏键盘:


重写dispatchTouchEvent


override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
   ev?.let {
       if (it.action == MotionEvent.ACTION_DOWN) {
           //如果现在取得焦点的View为EditText则进入判断
           currentFocus?.let { view ->
               if (view is EditText) {
                   if (!isInSide(view, ev) && isSoftInPutDisplayed()) {
                       hideSoftInPut(view)
                  }
              }
          }
      }
  }
   return super.dispatchTouchEvent(ev)
}

在Activity 中重写dispatchTouchEvent,对ACTION_DOWN事件做处理,使用getCurrentFocus()方法拿到当前获取焦点的View,判断其是否为EditText,若为EditText,则看当前软键盘是否展示(isSoftInPutDisplayed)并且点击坐标是否在EditText的外部区域(isInSide),满足条件则隐藏软键盘(hideSoftInPut)。


判断点击坐标是否在EditText内部


//判断点击坐标是否在EditText内部
private fun isInSide(currentFocus: View, ev: MotionEvent): Boolean {
   val location = intArrayOf(0, 0)
//获取当前EditText坐标
   currentFocus.getLocationInWindow(location)
//上下左右
   val left = location[0]
   val top = location[1]
   val right = left + currentFocus.width
   val bottom = top + currentFocus.height
//点击坐标是否在其内部
   return (ev.x >= left && ev.x <= right && ev.y > top && ev.y < bottom)
}

定义一个数组location存储当前EditText坐标,计算出其边界,再用点击坐标(ev.x,ev.y)和边界做比较最终得出点击坐标是否在其内部。


来判断软键盘是否展示


private fun isSoftInPutDisplayed(): Boolean {
   return ViewCompat.getRootWindowInsets(window.decorView)
       ?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
}

使用
WindowInsetsCompat类来判断当前状态下软键盘是否展示,WindowInsetsCompat是AndroidX库中的一个类,用于处理窗口插入(WindowInsets)的辅助类,可用于帮助开发者处理设备的系统UI变化,如状态栏、导航栏、软键盘等,给ViewCompat.getRootWindowInsets传入decorView拿到其实例,利用isVisible方法判断软键盘(WindowInsetsCompat.Type.ime())是否显示。


隐藏软键盘


private fun hideSoftInPut(currentFocus: View) {
   currentFocus.let {
    //清除焦点
       it.clearFocus()
    //关闭软键盘
       val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
       imm.hideSoftInputFromWindow(it.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
  }
}

首先要清除当前EditText的焦点,防止出现键盘收起但是焦点还在的情况:



最后是获取系统Service隐藏当前的键盘。


来看看最终的效果吧:



3.结尾


以上就是关于点击EditText外部区域隐藏软键盘并且清除焦点的实现方法,当然这只是其中的一种方式,如有不足请在评论区或私信指出,如果你们有更多的实现方法也欢迎在论区或私信留言捏❤️❤️


作者:Otaku_尻男
链接:https://juejin.cn/post/7226248402798936119
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin ?: 语法糖的 一个坑

问题还原 先定义一个函数 /** * @param s 字符串 * @return 参数的 长度如果不为1 返回null */ fun testStr(s: String): String? { return if (s.length == 1...
继续阅读 »

问题还原


先定义一个函数


/**
* @param s 字符串
* @return 参数的 长度如果不为1 返回null
*/
fun testStr(s: String): String? {
return if (s.length == 1) {
"length:1"
} else {
null
}
}

然后定义一个类 里面有个函数调用了上面的方法


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
} ?: println("inner else")

} ?: println("outer else")
}
}

在main函数里面跑一下


fun main() {

val t = Test()
t.testLet()
}

看下运行结果:


image.png


到这里还是好理解的, 但是我如果稍微改一下我们的代码


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
}
} ?: println("outer else")
}
}

这个时候 你认为程序应该打印什么?


我觉得多数人可能会认为 程序只会打印一行
inner let-1


然而你运行一下你就会发现:


image.png


他竟然还多打印了 outer else


这个行为我估计很多kotlin开发者就理解不了了, 因为按照javaer 的习惯, if 和 else 总会配对出现
我们上面的代码, 内部的判断逻辑 没有写else 那就不应该有任何行为啊, 为什么 内部的else 逻辑没写的时候
自动给我走了外部的else逻辑?


我们反编译看一下


image.png


这个反编译的代码是符合 程序运行结果的,虽然 他肯定不符合 多数kotlin开发者的意愿,


我们再把代码修改回去,让我们内部的inner case 也有一个else的逻辑


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
} ?: println("inner else")
} ?: println("outer else")
}
}

再反编译看一下代码:


image.png


这个代码就很符合我们写代码时候的意愿了


到这里我们就可以下一个结论


对于 ?: 这样的语法糖来说 ,如果内部 使用了?. 的写法 而没有写?: 的逻辑,则当内部的代码真的走到else的逻辑时,外部的?: 会默认执行


另外一个有趣的问题


下面的let 为什么会报错?


image.png


这里就有点奇怪了,这个let的写法为什么会报错? 但是run的写法 却是ok的?
而且这个let的写法 在我们第一个小节的代码中也没报错啊? 这到底是咋回事


看下这2个方法的定义


image.png


image.png


最关键就在于 这个let 需要有一个receiver, 而如果这个let 刚才在main函数中的写法就不对了


因为main函数 显然不属于任何一个对象,也不属于任何一个类,let在这个执行环境下 找不到自己的receiver 自然就会编译报错了


但是对于run 来说,这个函数的定义没有receiver的概念,所以可以运行


作者:vivo高启强
链接:https://juejin.cn/post/7229606662289457211
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

轻松便捷,使用Compose的Scaffold搭建一个首页布局

节后第一篇文章,稍微轻松一点,简单介绍下Scaffold的使用。在如今各式各样的app里面,如果我们想要在里面寻找一个共同点,那么就是基本每个app首页都是以底部tab切换页面加上顶部导航栏的形式出现,所以Compose也专门为了这一点设计出自己的便捷式布局-...
继续阅读 »

节后第一篇文章,稍微轻松一点,简单介绍下Scaffold的使用。在如今各式各样的app里面,如果我们想要在里面寻找一个共同点,那么就是基本每个app首页都是以底部tab切换页面加上顶部导航栏的形式出现,所以Compose也专门为了这一点设计出自己的便捷式布局--Scaffold,我们称为脚手架布局,如同字面意思,脚手架布局就是将各种Meterial Design风格的组件组合在一起形成一个具有Meterial Design风格的布局


Scaffold的参数


学习Compose的组件第一步我们肯定是去看看这个组件都支持哪些参数,哪些必传哪些可选,当我们点到Scaffold里面去查看它的参数的时候,会发现它的参数基本囊括了我们熟知的首页所具备的一切元素


image.png

其中有几个参数是专门用来设置Composable组件的,它们分别是



  • topBar:顶部导航栏布局,推荐使用TopAppBar组件

  • bottomBar:底部导航栏,推荐使用BottomAppBar

  • snackBarHost:Snackbars组件

  • floatingActionButton:悬浮按钮

  • drawerContent:侧边栏布局

  • content: Scaffold需要呈现出来的布局


一般一个普通的app首页,使用topBar,bottomBar和content这三个参数就可以搭建出来,我们来尝试下


搭建首页


导航栏


首先创建个Composable函数,在最外层使用Scaffold布局,然后topBar就使用官方推荐的TopAppBar组件


image.png

而这个TopAppBar组件在AppBar.kt文件里面,有两个重载函数,一个是内部布局完全自定义的


image.png

一个是如同脚手架布局一样帮你已经排版好了布局,你只需要往里面添加相应组件就好


image.png

我们这边就选择第二种方式来做我们的导航栏,代码如下


image.png

navigationIcon对应着导航栏上最左边的图标,一般可以放返回键,菜单键,这边使用系统自带的菜单icon,title对应着导航栏上的标题,可以直接放一个文案,也可以放一个布局,actions是一个RowScope,放置在导航栏的右侧,在里面可以添加一些小按钮,使用的也都是系统自带的icon,效果图如下


image.png

很方便是不,想想看这样的一个布局我们用传统xml形式来写需要多少代码量,用我们的脚手架布局几行代码就搞定了,接下来做底部导航栏


底部导航栏


底部导航栏也使用官方推荐的BottomAppBar,它也是个RowScope,所以往里面添加组件就能满足水平布局的效果,而我们每一个tab都可以使用另外一个组件,BottomNavigationItem来表示,它的参数如下所示


image.png

  • selected:是否被选中

  • onClick:点击事件

  • icon:图标组件,通常是个矢量图

  • modifier:操作符

  • enabled:是否可点击

  • label:文案

  • alwaysShowLabel:默认为true,表示文案常驻在图标底下,如果是false,那么只有等item被选中时候文案才会展示出来

  • interactionSource:监听导航栏点击事件

  • selectedContentColor:选中的颜色

  • unselectedContentColor:未被选中的颜色


知道参数以后,我们就可以把底部导航栏添加到布局里面


image.png

selectedIndex记录着我们点击过的下标值,我们可以通过下标值来判断应该展示哪个页面,代码如下


image.png

一个简单的首页切换页面的效果就出来了,效果如下


0503aa1.gif

侧边栏


刚刚我们看到Scaffold的参数里面还有大量drawerxxxx相关的参数,不用猜肯定知道是用来设置侧边栏的,其中drawerContent就是拿来写侧边栏里面的视图,那么我们也简单写个侧边栏布局看看


image.png

代码很简单就是一个头像加一个用户名,运行下代码看看


0504aa1.gif

加了drawerContent以后我们在界面上轻松向右一滑就出来个侧边栏了,能够通过手势直接滑出侧边栏主要是因为Scaffold里面的drawerGesturesEnabled这个参数默认为true,我们如果说哪一天不要侧边栏了,又不想把代码删掉,可以直接把drawerGesturesEnabled设置为false就好了,但是这里有个问题,我们看到drawerxxx参数里面没有用来设置去打开侧边栏的参数,要知道现在应用当中但凡有侧边栏功能的,都会提供一个按钮提示用户去点击展示出侧边栏,还是说Compose里面的侧边栏里面没有这功能?当然不是,我们看下Scaffold里面第二个参数,也就是ScaffoldState,我们看下这个状态类里面都有啥


image.png

总共俩参数,第一个就是DrawerState,一看就知道是拿来设置侧边栏状态的,我们再去DrawerState里面看看


image.png

第一个参数DrawerValue就是拿来设置侧边栏打开还是关闭的,他有两个值,分别是ClosedOpen,第二个参数是用来监听侧边栏开启或者关闭状态的,暂时用不到先忽略,这样我们先在代码当中创建一个ScaffoldState,并且传入一个DrawerState,代码如下


image.png

到了这一步我们知道了如果要通过某个按钮来展示侧边栏,只需要改变drawerStateDrawerValue属性就好,从Closed改变到Open,那怎么变呢?我们使用DrawerState里面提供的animateTo函数


image.png

我们看到animateTo函数里面第一个参数就是目标值,也就是我们需要设置成DrawerValue.Open的地方,第二个参数是个动画参数,因为侧边栏展示出来有个滑动的动画过程,当然如果不需要动画可以使用另一个函数snapTo就可以了,这个函数只需要设置一个targetValue值,另外无论是animateTo还是snapTo,都是挂起函数,所以我们还需要为它提供一个协程作用域,这里直接使用
rememberCoroutineScope函数来创建协程作用域


image.png

现在我们就可以在界面顶部的菜单按钮上设置一个打开侧边栏的动作,代码如下


image.png

代码到这里就结束了,我们看下效果


0504aa2.gif

总结


Scaffold使用起来就相当于我们平时用过的自定义View一样,已经把一些常用的视图和逻辑已经封装在里面,我们开发时候可以使用已经被封装过的组件,也可以自己去写视图,开发起来也是很方便的.


作者:Coffeeee
链接:https://juejin.cn/post/7229150192259465274
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不是吧不是吧,fdsan 都不知道?

背景 fd 是什么 In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique id...
继续阅读 »

背景


fd 是什么



In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique identifier (handle) for a file or other input/output resource, such as a pipe or network socket.



fd 通常用作进程内或进程间通信,是进程独有的文件描述符表的索引,简单来说,就是系统内核为每个进程维护了一个 fd table,来记录进程中的fd,通常在android 系统上,每个进程所能最大读写的fd数量是有限的,如果超限,会出现fd 无法创建/读取的问题。


fdsan 是什么


fdsan 全称其实是 file descriptor sanitizer,是一种用于检测和清除进程中未关闭的文件描述符(fd)的工具。它通常用于检测程序中的内存泄漏和文件句柄泄漏等问题。文件描述符是操作系统中用于访问文件、网络套接字和其他I/O设备的机制。在程序中,打开文件或套接字会生成一个文件描述符,如果此文件描述符在使用后未关闭,就会造成文件句柄泄漏,导致程序内存的不断增加。fd sanitizer会扫描进程的文件描述符表,检测未关闭的文件描述符,并将它们关闭,以避免进程内存泄漏。


fdsan in Android


在 Android 上,fdsan(File Descriptor Sanitizer)是自 Android 11 开始引入的一项新功能。fdsan 旨在帮助开发人员诊断和修复 Android 应用程序中的文件描述符泄漏和使用错误。


fdsan 使用 Android Runtime (ART) 虚拟机中的功能来捕获应用程序的文件描述符使用情况。它会跟踪文件描述符的分配和释放,并在文件描述符泄漏或错误使用时发出警告。fdsan 还支持在应用程序崩溃时生成详细的调试信息,以帮助开发人员诊断问题的根本原因。


常见场景



void thread_one() {
int fd = open("/dev/null", O_RDONLY);
close(fd);
close(fd);
}

void thread_two() {
while (true) {
int fd = open("log", O_WRONLY | O_APPEND);
if (write(fd, "foo", 3) != 3) {
err(1, "write failed!");
}
}
}

同时运行上述两个线程,你会发现


thread one                                thread two
open("/dev/null", O_RDONLY) = 123
close(123) = 0
open("log", O_WRONLY | APPEND) = 123
close(123) = 0
write(123, "foo", 3) = -1 (EBADF)
err(1, "write failed!")

断言失败可能是这些错误中最无害的结果:也可能发生静默数据损坏或安全漏洞(例如,当第二个线程正在将用户数据保存到磁盘时,第三个线程进来并打开了一个连接到互联网的套接字)。


检测原理


fdsan 试图通过文件描述符所有权来强制检测或者预防文件描述符管理错误。与大多数内存分配可以通过std::unique_ptr等类型来处理其所有权类似,几乎所有文件描述符都可以与负责关闭它们的唯一所有者相关联。fdsan提供了将文件描述符与所有者相关联的函数;如果有人试图关闭他们不拥有的文件描述符,根据配置,会发出警告或终止进程。


实现这个的方法是提供函数在文件描述符上设置一个64位的关闭标记。标记包括一个8位的类型字节,用于标识所有者的类型(在<android/fdsan.h>中的枚举变量 android_fdsan_owner_type),以及一个56位的值。这个值理想情况下应该是能够唯一标识对象的东西(原生对象的对象地址和Java对象的System.identityHashCode),但是在难以为“所有者”推导出标识符的情况下,即使对于模块中的所有文件描述符都使用相同的值也很有用,因为它会捕捉关闭您的文件描述符的其他代码。


如果已标记标记的文件描述符使用错误的标记或没有标记关闭,我们就知道出了问题,就可以生成诊断信息或终止进程。


在Android Q(11)中,fdsan的全局默认设置为单次警告。可以通过<android/fdsan.h>中的android_fdsan_set_error_level函数在运行时使 fdsan 更加严格或宽松。


fdsan捕捉文件描述符错误的可能性与在您的进程中标记所有者的文件描述符百分比成正比。


常见问题


E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xfffddddd was expected to be unowned

通常情况下,fd 所有权的误用并不会造成闪退,但是由于国内外厂商对 framework 的魔改,目前线上高频出现对应的闪退,为了规避这类情况,我们首先要规范 fd 的使用,特别是所有权的迁移,另外,在操作涉及到 localsocketsharedmemory 时,要慎之又慎,系统会为每个进程记录一份 fd table,会记录每个fd 对应的所有权。如果长时间不释放并且又在不断分配,会出现fd 超限问题,报错提示 cannot open fd


image.png


来看看 java 侧对文件描述符操作的注释



/**
* Create a new ParcelFileDescriptor that is a dup of the existing
* FileDescriptor. This obeys standard POSIX semantics, where the
* new file descriptor shared state such as file position with the
* original file descriptor.
*/
public ParcelFileDescriptor dup() throws IOException {
if (mWrapped != null) {
return mWrapped.dup();
} else {
return dup(getFileDescriptor());
}
}


/**
* Create a new ParcelFileDescriptor from a raw native fd. The new
* ParcelFileDescriptor holds a dup of the original fd passed in here,
* so you must still close that fd as well as the new ParcelFileDescriptor.
*
* @param fd The native fd that the ParcelFileDescriptor should dup.
*
* @return Returns a new ParcelFileDescriptor holding a FileDescriptor
* for a dup of the given fd.
*/
public static ParcelFileDescriptor fromFd(int fd) throws IOException {
final FileDescriptor original = new FileDescriptor();
original.setInt$(fd);

try {
final FileDescriptor dup = new FileDescriptor();
int intfd = Os.fcntlInt(original, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0);
dup.setInt$(intfd);
return new ParcelFileDescriptor(dup);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
}


/**
* Return the native fd int for this ParcelFileDescriptor and detach it from
* the object here. You are now responsible for closing the fd in native
* code.
* <p>
* You should not detach when the original creator of the descriptor is
* expecting a reliable signal through {@link #close()} or
* {@link #closeWithError(String)}.
*
* @see #canDetectErrors()
*/
public int detachFd() {
if (mWrapped != null) {
return mWrapped.detachFd();
} else {
if (mClosed) {
throw new IllegalStateException("Already closed");
}
int fd = IoUtils.acquireRawFd(mFd);
writeCommStatusAndClose(Status.DETACHED, null);
mClosed = true;
mGuard.close();
releaseResources();
return fd;
}

Share两个闪退案例:



  1. fd 超限问题


W zygote64: ashmem_create_region failed for 'indirect ref table': Too many open files

这个时候我们去查看 系统侧对应 fd 情况,可以发现,fd table 中出现了非常多的 socket 且所有者均显示为unix domain socket,很明显是跨进程通信的 socket 未被释放的原因



  1. fd 所有权转移问题


[DEBUG] Read self maps instead! map: 0x0

[]()****#00 pc 00000000000c6144 /apex/com.android.runtime/bin/linker64 (__dl_abort+168)

[]()****#01 pc 00000000000c6114 /apex/com.android.runtime/bin/linker64 (__dl_abort+120)

这个堆栈看得人一头雾水,因为蹦在 linker 里,我们完全不知道发生了什么,但是通过观察我们发现问题日志中都存在如下报错


E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xsssssss was expected to be unowned

根据上述知识,我们有理由怀疑是代码中fd 的操作合法性存在问题,通过细致梳理,我们得出了对应这两类问题的一些action:


所以有以下对应的action:




  • local socket 要及时关闭 connection,避免 fd 超限问题。




  • sharememory 从 进程A 转移到 进程B 时,一定要 detachFd 进行 fd 所有权转移,如果需要在进程 A 内进行缓存,那么 share 给进程B 时需要对 fd 进行 dup 操作后再 detachFd




版本差异


fdsan 在 Android 10 上开始引入,在Android 10 上会持续输出检测结果,在Android 11 及以上,fdsan 检测到错误后会输出错误日志并中止检测,在 Android 9 以下,没有对应的实现。所以,如果你需要在代码中引入fdsan 来进行 fd 校验检测。请参照以下实现:


extern "C" {
void android_fdsan_exchange_owner_tag(int fd,
uint64_t expected_tag,
uint64_t new_tag)
__attribute__((__weak__));
}

void CheckOwnership(uint64_t owner, int fd) {
if (android_fdsan_exchange_owner_tag) {
android_fdsan_exchange_owner_tag(fd, 0, owner);
}
}

作者:faultaddr
链接:https://juejin.cn/post/7225511164121120829
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【直播开发】Android 端实现 WebSocket 通信

前言 在之前的文章中,我们知道了 WebSocket 是一种全双工通信协议。本文将介绍如何在 Android 端使用 WebSocket 进行双向通信。其中包括创建 WebSocket 对象、发送和接收数据等操作。 创建 WebSocket 对象 要使用 We...
继续阅读 »

前言


在之前的文章中,我们知道了 WebSocket 是一种全双工通信协议。本文将介绍如何在 Android 端使用 WebSocket 进行双向通信。其中包括创建 WebSocket 对象、发送和接收数据等操作。


创建 WebSocket 对象


要使用 WebSocket 对象,我们需要先创建一个 WebSocket 客户端对象。在 Android 中,我们可以使用 OkHttp 库来创建 WebSocket 客户端对象。在开始之前,我们先在 Gradle 文件中添加 OkHttp 库的依赖:


implementation 'com.squareup.okhttp3:okhttp:4.9.3'

在代码中创建 WebSocket 客户端对象的示例如下:


import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket

//创建一个 OkHttpClient 对象
val client = OkHttpClient()

//请求体
val request = Request.Builder()
.url("wss://example.com/websocket")
.build()

//通过上面的 client 创建 webSocket
val webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// WebSocket 连接建立成功
}

override fun onMessage(webSocket: WebSocket, text: String) {
// 收到 WebSocket 服务器发送的文本消息
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
// 收到 WebSocket 服务器发送的二进制消息
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// WebSocket 连接失败
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
// WebSocket 连接关闭
}
})

在上面的示例中,我们首先创建了一个 OkHttpClient 对象,然后使用 Request.Builder 类构建了一个 WebSocket 请求对象,并指定了 WebSocket 服务器的地址。接着,我们调用 OkHttpClient 对象的 newWebSocket() 方法,传入 WebSocket 请求对象和一个 WebSocketListener 对象,来创建一个 WebSocket 客户端对象。在 WebSocketListener 对象中,我们可以实现 WebSocket 连接建立、收到消息、连接失败、连接关闭等事件的回调函数。


发送和接收数据


WebSocket 客户端对象创建成功后,我们可以通过 send() 方法来向 WebSocket 服务器发送消息。在 WebSocketListener 对象中的 onMessage() 方法中,我们可以接收到 WebSocket 服务器发送的消息。下面是一个发送和接收文本消息的示例:


val message = "Hello, WebSocket!"

webSocket.send(message)

// 在 onMessage() 方法中接收消息
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received message: $text")
}

如果要发送二进制消息,可以使用 send() 同名的另一个重载方法:


val message = ByteString.encodeUtf8("Hello, WebSocket!")

webSocket.send(message)

// 在 onMessage() 方法中接收消息
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d(TAG, "Received message: ${bytes.utf8()}")
}

关闭 WebSocket 连接


当 WebSocket 连接不再需要时,我们可以调用 WebSocket 对象的 close() 方法来关闭连接,及时释放资源,避免引起内存泄漏。在 WebSocketListener 对象中的 onclose() 方法中,我们可以接收到 WebSocket 关闭事件,可以在该事件中执行一些清理操作。下面是一个关闭 WebSocket 连接的示例:


webSocket.close(NORMAL_CLOSURE_STATUS, "WebSocket connection closed")

// 在 onClosed() 方法中接收关闭事件
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket connection closed: $reason")
}

在上面的示例中,我们调用 WebSocket 对象的 close() 方法来关闭连接,传入一个关闭代码和关闭原因。在 WebSocketListener 对象中的 onClosed() 方法中,我们可以接收到 WebSocket 关闭事件,并处理关闭原因。


完整示例


下面是一个完整的 WebSocket 通信示例,包括创建 WebSocket 对象、发送和接收消息、关闭连接等操作:


import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.*
import okio.ByteString

class MainActivity : AppCompatActivity() {

private lateinit var webSocket: WebSocket

companion object {
private const val TAG = "WebSocketDemo"
private const val NORMAL_CLOSURE_STATUS = 1000
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val client = OkHttpClient()

val request = Request.Builder()
.url("wss://echo.websocket.org")
.build()

webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send("Hello, WebSocket!")
}

override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received message: $text")
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d(TAG, "Received message: ${bytes.utf8()}")
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "WebSocket connection failed", t)
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket connection closed: $reason")
}
})

btn_send.setOnClickListener {
val message = et_message.text.toString()
webSocket.send(message)
}

btn_close.setOnClickListener {
webSocket.close(NORMAL_CLOSURE_STATUS, "WebSocket connection closed")
}
}
}

在上面的示例中,我们在 Activity 的 onCreate() 方法中创建了 WebSocket 客户端对象,并通过按钮的点击事件来发送消息和关闭连接。我们使用了 Echo WebSocket 服务器来测试 WebSocket 通信。在实际开发中,我们可以使用自己的 WebSocket 服务器来进行通信。


总结


WebSocket 是一种全双工通信协议,可以在 Android 应用程序中使用 WebSocket 对象实现双向通信。通过 OkHttp 库,我们可以创建 WebSocket 客户端对象,使用 send() 方法发送消息,使用 WebSocketListener 回调接口处理事件。在实际应用中,我们可以使用 WebSocket 协议来实现实时交互、即时通信等功能,提升 Android 应用程序的用户体验和竞争力。


作者:Quincy_Ye
链接:https://juejin.cn/post/7224015103481200677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Java、Kotlin不香吗?为什么Flutter要选用Dart作为开发语言?

对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。 尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑: 既然同可以归...
继续阅读 »

对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。


尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑:



既然同可以归到移动开发的范畴,也同属于Google旗下的团队,为什么Flutter不能沿用既有的Java或Kotlin语言来进行开发呢?



通过阅读本文,你的疑惑将得到充分的解答,你不仅能够了解到Flutter团队在选用Dart作为开发语言时的考量,还能充分感受到使用Dart语言进行开发的魅力所在。


照例,先奉上思维导图一张,方便复习:





热重载 (Hot Reload)一直以来都是Flutter对外推广的一大卖点,这是因为,相对于现有的基于原生平台的移动开发流程来讲,热重载在开发效率上确实是一个质的飞跃。



简单讲,热重载允许你在无需重启App的情况下,快速地构建页面、添加功能或修复错误。这个功能很大程度上依赖于Dart语言的一个很突出的特性:


同时支持AOT编译与JIT编译


AOT编译与JIT编译


AOT Compilation(Ahead-of-Time Compilation, 提前编译)是指在程序执行之前,将源代码或中间代码(如Java字节码)转换为可执行的机器码的过程。这么做可以提高程序的执行效率,但也需要更长的编译时间。


JIT Compilation(Just-in-Time Compilation, 即时编译)是指在程序执行期间,将源代码或中间代码转换为可执行的机器码的过程。这么做可以提高程序的灵活性和开发效率,但也会带来一些额外的开销,例如会对程序的初始执行造成一定的延迟。


用比较贴近生活的例子来解释二者之间的区别,就是:



AOT编译就像你在上台演讲之前,把原本全是英文的演讲稿提前翻译成中文,并写在纸上,这样当你上台之后,就可以直接照着译文念出来,而不需要再在现场翻译,演讲过程能更为流畅,但就是要在前期花费更多的时间和精力来准备。




JIT编译就像你在上台演讲之前,不需要做过多的准备,等到上台之后,再在现场将演讲稿上的英文逐句翻译成中文,也可以根据实际情况灵活地调整演讲内容,但就是会增加演讲的难度,遇到语法复杂的句子可能也会有更多的停顿。



可以看到,两种编译方式的应用场景不同,各有优劣,而Dart是为数不多的同时支持这两种编译方式的主流编程语言之一。根据当前所处项目阶段的不同,Dart提供了两种不同的构建模式:开发模式与生产模式。


开发模式与发布模式


在开发模式下,会利用 Dart VM 的 JIT 编译器,在运行时将内核文件转换为机器码,以实现热重载等功能,缩短开发周期。


热重载的流程,可以简单概括为以下几步:




  1. 扫描改动:当我们保存编辑内容或点击热重载按钮时,主机会扫描自上次编译以来的任何有代码改动的文件。




  2. 增量编译:将有代码改动的文件增量编译为内核文件。




  3. 推送更新:将内核文件注入到正在运行的 Dart VM。




  4. 代码合并:使用新的字段和函数更新类。




  5. Widget重建:应用的状态会被保留,并重建 widget 树,以便快速查看更改效果。




而在发布模式下,则会利用 Dart VM 的 AOT 编译器,在运行前将源代码直接转换为机器码,以实现程序的快速启动和更流畅地运行。


这里的“更流畅地运行”指的是在运行时能够更快地响应用户的操作,提供更流畅的用户体验,而不是单指让程序运行得更“快”。


这是因为Dart代码在被转换为机器码后,是可以直接在硬件上运行的,而不需要在运行时进行解释或编译,因此可以减少运行时的开销,提高程序的执行效率。


此外,经 AOT 编译后的代码,会强制执行健全的 Dart 类型系统,并使用快速对象分配和分代垃圾收集器来更好地管理内存。


因此,根据当前所处项目阶段的不同,采用不同的构建模式,Dart语言可以实现两全其美的效果


单线程模型


现如今,几乎所有的智能终端设备都支持多核CPU,为使应用在设备上能有更好的表现,我们常常会启动多个共享内存的线程,来并发执行多个任务。


大多数支持并发运行线程的计算机语言,如我们熟知的Java、Objective-C等,都采用了“抢占”的方式在线程之间进行切换,每个线程都被分配了一个时间片以执行任务,一旦超过了分配的时间,操作系统就会中断当前正在执行的线程,将CPU分配给正在等待队列的下一个线程。


但是,如果是在更新线程共享资源(如内存)期间发生的抢占行为,则可能会引致竞态条件的产生。竞态条件会导致严重的错误,轻则数据丢失,重则应用崩溃,且难以被定位和修复。


修复竞争条件的典型做法就是加锁,但锁本身会导致卡顿,甚至引发死锁等更严重的问题。


那Dart语言又是怎么解决这个问题的呢?


Dart语言采用了名为Isolate的单线程模型,Isolate模型是以操作系统提供的进程和线程等更为底层的原语进行设计的,所以你会发现它既有进程的特征(如:不共享内存),又有线程的特征(如:可处理异步任务)。


正如Isolate这个单词的原意“隔离”一样,在一个Dart应用中,所有的Dart代码都在Isolate内运行,每个Isolate都会有自己的堆内存,从而确保Isolate之间相互隔离,无法互相访问状态。在需要进行通信的场景里,Isolate会使用消息机制。


因为不共享内存,意味着它根本不允许抢占,因此也就无须担心线程的管理以及后台线程的创建等问题。


在一般场景下,我们甚至完全无需关心Isolate,通常一个Dart应用会在主Isolate下执行完所有代码。


虽然是单线程模型,但这并不意味着我们需要以阻塞UI的方式来运行代码,相反,Dart语言提供了包括 async/await 在内的一系列异步工具,可以帮助我们处理大部分的异步任务。关于 async/await 我们后面会有一篇单独的文章讲到,这里先不展开,只需要知道它跟Kotlin的协程有点像就可以了。



如图所示,Dart代码会在readAsString()方法执行非Dart代码时暂停,并在 readAsString()方法返回值后继续执行。


Isolate内部会运行一个消息循环,按照先进先出的模式处理重绘、点击等事件,可以与Android主线程的Looper相对照。



如图所示,在main()方法执行完毕后,事件队列会依次处理每一个事件。


而如果某个同步执行的操作花费了过长的处理时间,可能会导致应用看起来像是失去了响应。



如图所示,由于某个点击事件的同步处理耗时过长,导致其超过了处理两次重绘事件的期望时间间隔,直观的呈现就是界面卡顿。


因此,当我们需要执行消耗CPU的计算密集型工作时,可以将其转移到另外一个Isolate上以避免阻塞事件循环,这样的Isolate我们称之为后台运行对象



如图所示,生成的这个Isolate会执行耗时的计算任务,在结束后退出,并把结果返回。


由于这个Isolate持有自己的内存空间,与主Isolate互相隔离,因此即使阻塞也不会对其他Isolate造成影响。


快速对象分配与分代垃圾回收


在Android中,视图 (View)是构成用户界面的基础块,表示用户可以看到并与之交互的内容。在Flutter中,与之大致对应的概念则是Widget。Widget也是通过多个对象的嵌套组合,来形成一个层次结构关系,共同构建成一棵完整的Widget树。


但两者也不能完全等同。首先,Widget并非视图本身,最终的UI树是由一个个称之为Element的节点构成的;其次,Widget也不会直接绘制任何内容,最终的绘制工作是交由RenderObject完成的。Widget只是一个不可变的临时对象,用于描述在当前状态下视图应该呈现的样子


而所谓的Widget树只是我们描述组件嵌套关系的一种说法,是一种虚拟的结构。但 Element和RenderObject是在运行时实际存在的,如图:



这就好比手机与其规格参数的关系。Widget就像是一台手机的规格参数,是对当前组装成这个手机的真正的硬件配置的描述,当手机的硬件有更新或升级时,重新生成的规格参数也会有所变化。


由于Widget是不可变的,因此,我们无法直接对其更新,而是要通过操作状态来实现。但实际上,当Widget所依赖的状态发生改变时,Flutter框架就会重新创建一棵基于当前最新状态绘制的新的Widget树,对于原先的Widget来说它的生命周期其实已经结束了。


有人可能会对这种抛弃了整棵Widget树并完全重建一棵的做法存有疑问,担心这种行为会导致Flutter频繁创建和销毁大量短暂的Widget对象,给垃圾回收带来了巨大压力,特别对于一些可能由数千个Widget组合而成的复杂页面而言。


实际上这种担心完全没有必要,Dart的快速对象分配与分代垃圾回收足以让它应对这种情况。


快速对象分配


Dart以指针碰撞(Bump Pointer)的形式来完成对象的内存分配。


指针碰撞是指在堆内存中,Dart VM使用一个指针来跟踪下一个可用的内存位置。当需要分配新的内存时,Dart VM会将指针向前移动所需内存大小的距离,从而分配出新的内存空间



这种方式可以快速地分配内存,而不需要查找可用的内存段,并且使内存增长始终保持线性


另外,前面我们提到,由于每个Isolate都有自己的堆内存,彼此隔离,无法互相访问状态,因此可以实现无锁的快速分配。


分代垃圾回收


Dart的垃圾回收器是分代的,主要分为新生代(New Generation)与老年代(Old Generation)。


新生代用于分配生命周期较短的临时对象。其所在的内存空间会被分为两半,一个处于活跃状态,另一个处于非活跃状态,并且任何时候都只使用其中的一半。



新的对象会被分配到活跃的那一半,一旦被填满,垃圾回收器就会从根对象开始,查找所有对象的引用状态。




被引用到的对象会被标记为存活状态,并从活跃的一半复制到非活跃的一半。而没有被引用到的对象会被标记为死亡状态,并在随后的垃圾回收事件中被清除。



最后,这两半内存空间会交换活跃状态,非活跃的一半会再次变成活跃的一半,并且继续重复以上过程。



当对象达到一定的生命周期后,它们会被提升为老年代。此时的垃圾回收策略会分为两个阶段:标记与清除。


首先,在标记阶段,会遍历整个对象图,标记仍在使用的对象。


随后,在清除阶段,会扫描整个内存,回收任何没有被标记的对象,然后清除所有标记。


这种形式的垃圾回收发生频率不高,但有时需要暂停Dart Runtime以支持其运行。


为了最小化地降低垃圾回收事件对于应用程序的影响,垃圾回收器为Flutter引擎提供了钩子,当引擎检测到应用程序处于空闲状态并且没有用户交互时会发出通知,使得垃圾回收器可以在不影响性能的情况下执行回收工作。


另外,同样由于每个Isolate都在自己都独立线程内运行,因此每个Isolate的垃圾回收事件不会影响到其他Isolate的性能。


综上可知,Flutter框架所采用的工作流程,很大程度上依赖于其下层的内存分配器和垃圾回收器对于小型的、短生命周期的对象高效的内存分配和回收,缺少这个机制的语言是无法有效运作的


学习成本低


对于想要转岗Flutter的Android或iOS开发者,Dart语言是很友好的,其语法与Kotlin、Swift等语言都存在一些相似之处。


例如,它们都是面向对象的语言,都支持类、接口、继承、抽象类等概念。绝大多数开发者都拥有面向对象开发的经验,因此可以以极低的学习成本学习Dart语言。


此外,Dart语言也拥有着许多与其他语言相似的优秀的语法特性,可以提高开发人员的生产力,例如:




  • 字符串插值:可以直接在字符串中嵌入变量或表达式,而不需要使用+号相连:
    var name = 'Bob'; print('Hello, $name!');




  • 初始化形式参数:可以在构造函数中直接初始化类的属性,而不需要在函数体中赋值:
    class Point { num x, y; Point(this.x, this.y); }




  • 函数式编程风格:可以利用高阶函数、匿名函数、箭头函数等特性简化代码的结构和逻辑:
    var numbers = [1, 2, 3]; var doubled = numbers.map((n) => n * 2);




Dart团队配合度高


拥有一定工作年限的Android开发者,对于早些年Oracle与Google两家科技公司的Java API版权之争可能还有些许印象。


简单讲就是,Oracle认为Google在Android系统中对Java API的复制使用侵犯了其版权和专利权,这场持续了11年的专利纠纷最终以Google的胜利结束。


相比之下,Dart语言与Flutter之间则没有那么多狗血撕逼的剧情,相反,Flutter与Dart社区展开了密切合作,Dart社区积极投入资源改进Dart语言,以便在Flutter中更易使用。


例如,Flutter在最开始采用Dart语言时,还没有用于生成原生二进制文件的AOT工具链,但在Dart团队为Flutter构建了这些工具后,这个缺失已经不复存在了。


结语


以上,就是我汇总Flutter官网资料及Flutter社区推荐博文的说法之后,总结出的Flutter选用Dart作为开发语言的几大主要原因,希望对于刚入门或想要初步了解Flutter开发的小伙伴们有所帮助。


引用



作者:星际码仔
链接:https://juejin.cn/post/7225629551602565178
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Git】代码提交到了错误的分支怎么办

Git
最近在git的代码提交中遇到了一个这样的情况,A分支要提交功能a1、a2、a3,B分支要提交功能b1、b2、b3,但是误把a2功能提交到了B分支上,一开始以为得在在A分支上重新把a2功能重写一遍,后来查阅了一番才知道有git cherry-pick 和 git...
继续阅读 »

最近在git的代码提交中遇到了一个这样的情况,A分支要提交功能a1、a2、a3B分支要提交功能b1、b2、b3,但是误把a2功能提交到了B分支上,一开始以为得在在A分支上重新把a2功能重写一遍,后来查阅了一番才知道有git cherry-pickgit rebase这种好东西。


image.png


首先要解决上面的失误,分为两步:




  • 把a2功能添加回A分支




  • 把a2功能从B分支中删除




1. 把a2功能添加回A分支(cherry-pick)


直接切到A分支,然后运行


git cherry-pick a2功能的SHA值

然后就可以了,就是这么简单就把a2挪到A分支上



2. 把a2功能从B分支中删掉(rebase)


切到B分支,然后运行


git rebase -i a2前一个功能的SHA值

运行后我们就能进入SHA值后面的提交文本记录



我们把a2对应的pick改为drop,就能删掉对应的提交,并且不影响后续b3的提交。


编辑方式:按i 进入编辑状态,按Esc:wq就能保存退出。


改成这样的就可以了



就能成功达到我们想要的效果了


image.png


平时Git Rebase 很少用到,直到这一次,不得不用,最后也顺利解决了问题,同时get到了新技能。


作者:前端小小梦
链接:https://juejin.cn/post/7224382425664159800
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 项目的 Code Reviewer 清单

查看 Android 代码库时要记住的几点: 代码审查有时是一个乏味的过程,但我相信我们需要花更多的时间在这上面。 也许这是你学习或分享一些知识的机会。 这里列出了一些在 Android 项目的 Code Review 过程中检查所必需的要点。 Memory ...
继续阅读 »

查看 Android 代码库时要记住的几点:
代码审查有时是一个乏味的过程,但我相信我们需要花更多的时间在这上面。 也许这是你学习或分享一些知识的机会。
这里列出了一些在 Android 项目的 Code Review 过程中检查所必需的要点。


Memory Leaks(内存泄漏):


想象一下以下情况:一个漂亮的应用程序,但同时速度很慢,屏幕之间的导航每次都变慢。
在代码审查期间要检查的一些要点:



  • 这段新代码是否有任何 Context 保留?

  • 有没有相关的 RxAndroid 代码? 如果是,请检查 RxCall 是否在生命周期范围(ViewModel/Fragment/Activity)结束时被释放。

  • 如果代码有 coroutines(协程),检查 Job 是否从 ViewModel 范围启动以正确释放。

  • 代码中有使用 CountDownTimerAsyncTask 或任何视频/音频播放器的实现吗? 如果是,则代码应该释放与避免泄漏相关的内存资源。

  • 是否有新的 Fragment with ViewBinding 方法,代码应该在 onDestroyView 方法处释放绑定。


private ResultProfileBinding binding;

@Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
binding = ResultProfileBinding.inflate(inflater, container, false);
View view = binding.getRoot();
return view;
}

@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}

深度布局:


有时我们有一个 ViewGroup 有另一个 ViewGroup 作为子布局。
一个好的建议是始终使用 ViewGroup ConstraintLayouts 来保持布局更平坦。 它可以避免导致性能问题的过度绘制。
Android 文档中提到了一些减少过度绘制的技巧。


资源注释:


Android 世界中的每个资源都有一个标识符,它是一个 Integer 类型。 如何确保我们的 Integer 值代表一个有效的资源 ID? 注释可以帮助我们解决这个问题。
例如:


fun showMessage(context: Context, val idRes: Int) {
Toast.makeText(context, idRes, Toast.SHORT_LENGHT).show()
}

在上面的代码中,Code Reviewer 会建议你,在这里使用注释。
例如:


fun showMessage(context: Context, @StringRes val idRes: Int) {
Toast.makeText(context, idRes, Toast.SHORT_LENGHT).show()
}

其他注释示例在这里


类似组件:


Code Review 期间投入时间非常重要。 尝试按照作者的想法思考,反映他的选择,提出问题以理解代码。
一个新的需求,需要写很多新代码。 应该和项目组成员及时沟通,比如:“此代码与我们在此组件中已有的相同”。 像这样的沟通说明是必不可少的,因为最重要的是重用我们代码库中已经存在的代码。


危险代码区:


如果你有遇到敏感的代码或危险代码区。 危险代码是 Code Reviewer 需要重点查看的,当你发现一些代码没有任何意义的时候。在这种情况下,不要评判作者。与他们交谈以了解该代码背后的原因。 这次谈话可能是一个学习新东西的机会。也有可能是你自己对这块业务考虑不周,没了解到这段代码本质的原因。如果交流后,发现还是确实有问题,就可以把这块代码区的问题给更正了。


架构违规📐:


软件架构是定义在软件各部分之间的通信协议。
Code Review 是识别架构违规等问题的非常有效的手段。 在这种情况下可以做如下沟通是比较合适的:
“你的 ViewModel 正在访问存储库。 我们在它们之间有一个用例。 请看一下 MainViewModel 文件,这是一个可以参考的例子。”
“为什么要在 ViewModel 中使用 Adapter 引用? 适配器是一个 RecyclerView 组件。 最好把它放在一个 Fragment 中,让它远离我们的 ViewModel。”


小细节决定一切:



  • 未使用 import

  • 未使用的资源,例如 drawables, strings, colors……

  • 注释代码;

  • 未格式化的代码;

  • 变量名、方法名、文件名……

  • 代码不遵循样式指南。 例如,Kotlin 定义了一个定义明确的样式指南。 它对任何开发人员在现有代码库中快速找到任何软件组件都有很大帮助。


结论


静态分析工具在代码审查过程中很有帮助,但它们并不是 100% 有效的。 如果你的团队正在寻找代码质量,关键的 Code Review 是必不可少的。


作者:JeffreyAI
链接:https://juejin.cn/post/7119414483566460935
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android串口开发与SerialPort工具封装

Android串口开发之SerialPort工具封装 一. 什么是串口 串口通讯(Serial Communication),是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通讯方式。 串口是一种接口标准,它规定了接口的电气标准,没有规定接口...
继续阅读 »

Android串口开发之SerialPort工具封装


一. 什么是串口


串口通讯(Serial Communication),是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通讯方式。


串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。


在Android开发这一块来说,串口通信应用越来越广泛,得益于物联网的发展,很多定制设置在Android系统上外挂设备,如何与外挂设备进行双方通信,就需要串口来进行。


其实串口的通信和蓝牙的通信有很多相似的地方,数据帧的处理相似,只是连接通信的方式不同了而已,蓝牙的通信可以使用Android Api的方式,而串口需要自己编译Ndk,这里把封装的工具类开源出来,希望对大家有帮助。


二. Android如何进行串口通信


在Android的Framwork中是没有相关的Api的,我们想进行串口的相关操作还是得借助于谷歌2012年的老库android-serialport-api
项目结构如下:



编译NDK为对应的so库,如何使用Java调用想要的方法,想想真令人头秃







网上有很多编译的教程了,如果觉得麻烦的同学可以往下看我的封装库

三. Android串口通信的封装


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

implementation 'com.gitee.newki123456:serial_port_tools:1.0.0'

一句话解决串口通信:


    private lateinit var mSerialPortManager: SerialPortManager
var cmd = byteArrayOf(-91, 85, 1, -5) //开启通信的指令


//具体调用就是2个回调,设置串口,开启指令
mSerialPortManager = SerialPortManager()
mSerialPortManager
.setOnOpenSerialPortListener(object : OnOpenSerialPortListener {
override fun onFail(paramFile: File?, paramStatus: OnOpenSerialPortListener.Status) {
Toast.makeText(mContext,paramStatus.toString(),Toast.LENGTH_SHORT).show()
}

override fun onSuccess(paramFile: File) {

}
})
//设置串口的数据通信回调
.setOnSerialPortDataListener(object : OnSerialPortDataListener {
override fun onDataReceived(paramAnonymousArrayOfByte: ByteArray) {
//解析返回的数据转换为摄氏度
val i = paramAnonymousArrayOfByte[3]
val f = (paramAnonymousArrayOfByte[2] + i * 255 + 20) / 100.0f
val message = Message.obtain()
message.obj = java.lang.Float.valueOf(f)
message.what = 1
mHandler.sendMessage(message)
}

override fun onDataSent(paramArrayOfByte: ByteArray?) {

}
})
.openSerialPort(File("dev/ttyS3"), 115000) //打开指定串口

mSerialPortManager.beginBytes(cmd) //开启读取

重点概念:

串口路径 波特率 输入指定的指令

只要能通信了之后,处理数据的逻辑其实就和蓝牙的数据帧处理很像啦。


项目介绍:

自定义Android设备,支持人脸识别打卡并外挂的红外温度传感器,我们再识别比对到人脸成功之后需要通过串口拿到温度计的温度展示出来。

源码在此




作者:newki
链接:https://juejin.cn/post/7096022114041348133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么智能硬件首选MQTT

前言 前面讲了Android如何通过串口通信操作硬件,但实际业务场景大多是既可以屏幕操控硬件也可以远程下发操控,这时就需要MQTT协议来完成这一工作。本文将介绍MQTT协议及其在物联网设备通信中的应用。 一、初识MQTT 1. 什么是MQTT协议 MQTT是一...
继续阅读 »


前言


前面讲了Android如何通过串口通信操作硬件,但实际业务场景大多是既可以屏幕操控硬件也可以远程下发操控,这时就需要MQTT协议来完成这一工作。本文将介绍MQTT协议及其在物联网设备通信中的应用。


一、初识MQTT


1. 什么是MQTT协议


MQTT是一种轻量级的消息传递协议,全称为Message Queuing Telemetry Transport。它是基于发布-订阅模式的协议,可以在分布式系统中实现设备之间的消息传递和通信。


2. 为什么要用MQTT


它是一种非常轻量级的二进制协议,并且由于其最小的数据包开销,与 HTTP 等协议相比,MQTT在通过网络传输数据时表现出色。该协议的另一个重要方面是 MQTT 在客户端非常容易实现。易用性是 MQTT 开发的一个关键问题,使其非常适合当今资源有限的受限设备。


3. MQTT诞生背景



  • 1999 年:Andy Stanfork-Clark (IBM) 和 Arlen Nipper 发布 MQTT 协议,用于通过卫星连接石油管道遥测系统,MQTT 中的 TT (Telemetry Transport) 就是源于这样一个遥测系统;

  • 2010 年:MQTT 协议免费发布;

  • 2014 年:MQTT 协议正式成为 OASIS 标准,经过多年的发展,MQTT 协议已经成为互联网 (IoT) 的主要协议之一。


目前,MQTT 主要分为两个大版本:



  • MQTT v3 其中 v3.1.1 是目前接入设备最多的版本;

  • MQTT v5 2018 年发布,部分设备接入。


4. MQTT核心要求



  • 简单实现

  • 服务质量数据传输

  • 轻巧且带宽高效

  • 数据不可知

  • 连续会话意识


5. MQTT 发布/订阅模式


MQTT 发布/订阅模式(也称为发布/订阅)提供了传统客户端-服务器架构的替代方案。在客户端-服务器模型中,客户端直接与端点通信。发布/订阅模型将发送消息的客户端(发布者)与接收消息的客户端(订阅者)分离。发布者和订阅者从不直接联系对方。事实上,他们甚至不知道对方的存在。它们之间的连接由第三个组件(代理)处理。代理的工作是过滤所有传入的消息并将它们正确分发给订阅者。
image.png
pub/sub 最重要的方面是消息的发布者与接收者(订阅者)的解耦。这种解耦有几个方面:



  • 空间解耦:发布者和订阅者不需要相互了解(例如,不需要交换 IP 地址和端口)。

  • 时间解耦:发布者和订阅者不需要同时运行。

  • 同步解耦:两个组件的操作在发布或接收过程中不需要中断。


总之,MQTT 发布/订阅模型消除了消息发布者与接收者/订阅者之间的直接通信。代理的过滤活动可以控制哪个客户端/订阅者接收哪个消息。解耦具有三个维度:空间、时间和同步。


6. 可拓展性


pub/sub 比传统的客户端/服务器模式有了更好的拓展,这是由于 broker 的高度并行化,并且是基于事件驱动的模式。可扩展性还体现在消息的缓存和消息的智能路由,还可以通过集群代理来实现数百万的连接,使用负载均衡器将负载分配到更多的单个服务器上,这就是 MQTT 的深度应用了。


7. 消息过滤



  • 基于主题的过滤此过滤基于属于每条消息的主题。接收客户端向代理订阅感兴趣的主题,订阅后,broker 就会确保客户端收到发布到 topic 中的消息。

  • 基于内容的过滤在基于内容的过滤中,broker 会根据特定的内容过滤消息,接受客户端会经过过滤他们感兴趣的内容。这种方法的一个显著的缺点就是必须事先知道消息的内容,不能加密或者轻易修改。

  • 基于类型的过滤当使用面向对象的语言时,基于消息(事件)的类型/类进行过滤是一种常见做法。例如,订阅者可以收听所有类型为 Exception 或任何子类型的消息。


**发布/订阅并不是每个用例的答案。在使用此模型之前,您需要考虑一些事项。**发布者和订阅者的解耦是发布/订阅的关键,它本身也带来了一些挑战。例如,您需要事先了解已发布数据的结构。对于基于主题的过滤,发布者和订阅者都需要知道要使用哪些主题。要记住的另一件事是消息传递。发布者不能假定有人正在收听所发送的消息。在某些情况下,可能没有订阅者阅读特定消息。


8. 主要特性



  • MQTT 在空间上解耦了发布者和订阅者。要发布或接收消息,发布者和订阅者只需要知道代理的主机名/IP 和端口。

  • MQTT 按时间解耦。尽管大多数 MQTT 用例近乎实时地传递消息,但如果需要,代理可以为不在线的客户端存储消息。(必须满足两个条件才能存储消息:客户端已连接到持久会话并订阅了服务质量大于 0 的主题)。

  • MQTT 异步工作。因为大多数客户端库异步工作并且基于回调或类似模型,所以任务在等待消息或发布消息时不会被阻塞。在某些用例中,同步是可取且可能的。为了等待某个消息,一些库有同步 API。但流程通常是异步的。


二、MQTT通信过程


1. 连接到MQTT服务器


使用MQTT协议建立通信的第一步是连接到MQTT服务器。MQTT客户端需要提供MQTT服务器的IP地址和端口号,以建立到服务器的TCP连接。一旦连接建立成功,客户端将发送连接请求(Connect Packet)到MQTT服务器,服务器将响应确认请求。


2. 发布主题


发布者(Publisher)将消息发布到特定的主题上。在MQTT中,主题是消息的标识符。主题可以是任何有效的UTF-8字符串,并且可以包含多个层次结构(例如,/sensors/temperature)。当发布者发送消息到主题时,服务器将转发该消息给所有订阅该主题的订阅者。


3. 订阅主题


订阅者(Subscriber)可以订阅感兴趣的主题。在MQTT中,订阅者可以订阅整个主题树,或仅订阅特定的主题。当订阅者订阅主题时,服务器将记录其订阅信息,并将其添加到订阅列表中。


4. 接收消息


一旦订阅者订阅了主题,服务器将转发所有发布到该主题的消息给订阅者。当订阅者接收到消息时,它将执行相应的操作,例如更新设备状态或向其他设备发送命令。


5. 断开连接


使用完MQTT协议后,客户端应该断开与MQTT服务器的连接。MQTT客户端将发送断开请求(Disconnect Packet)到MQTT服务器,服务器将响应确认请求并断开连接。


三、MQTT常用术语



  1. Broker:MQTT的中间件,负责接收和转发消息。

  2. Client:MQTT的客户端,可以是发布者或订阅者。

  3. Topic:MQTT的消息主题,用于识别消息的内容和目的地。格式: “myhome/livingroom/temperature”或“Germany/Munich/Octoberfest/people”

  4. QoS(Quality of Service):MQTT消息的服务质量,分为三个级别:QoS 0,QoS 1和QoS 2。

  5. Retain:MQTT消息的保留标志,表示该主题上最新的消息是否需要保留。

  6. Last Will and Testament(LWT):MQTT客户端的遗嘱消息,用于在客户端离线时通知其他客户端。

  7. Clean Session:MQTT客户端连接时的清理会话标志,用于指示中间件是否保留客户端的订阅信息。


四、总结


MQTT协议的优点



  1. 简单和轻量级:MQTT协议非常简单和轻量级,适合于小型设备和带宽受限的环境。

  2. 可靠:MQTT协议具有多种消息传递保证机制,例如QoS级别,可确保消息可靠地传递到目标设备。

  3. 灵活性:MQTT协议具有灵活的主题订阅机制,允许客户端订阅不同级别的主题。

  4. 可拓展性:MQTT协议支持多个客户端连接到单个服务器,并且可以扩展到支持数百万个设备。


MQTT协议的缺点:



  • 安全性:MQTT协议本身没有提供加密和身份验证机制。如果需要安全通信,则需要使用TLS/SSL等其他协议。

  • 可靠性受限:MQTT提供了不同的服务质量(QoS)级别,但最高的QoS级别也不能保证消息100%可靠传递。在不稳定的网络环境下,消息可能会丢失或重复传递,需要应用程序自行处理。

  • 适用场景受限:MQTT适用于传递简单的消息,但不适用于传递大量的数据或复杂的消息。在需要进行复杂消息处理和转换的场景中,可能需要使用更为复杂的消息传递技术,例如消息队列。

  • 性能受限:MQTT是一种轻量级的协议,但在高并发和大规模消息传递的环境中,可能会面临性能瓶颈。此外,在使用较高的QoS级别时,可能会导致更多的网络流量和延迟,从而影响系统的性能。

  • 可扩展性受限:MQTT本身不提供扩展性的支持,需要使用其他技术来实现分布式消息传递和扩展性。


MQTT其他协议对比:


与其他通信协议相比,MQTT具有一定的优势:



  1. 与HTTP对比:HTTP是一种请求/响应模型的通信协议,适用于网页浏览等场景。相较于HTTP,MQTT协议更轻量级,适用于低带宽、不稳定网络环境。同时,MQTT基于发布/订阅模式,支持实时数据推送,而HTTP需要轮询获取数据,实时性较差。

  2. 与WebSocket对比:WebSocket是一种双向通信协议,适用于实时Web应用。与WebSocket相比,MQTT协议在低带宽和低功耗场景下具有更高的性能优势。此外,MQTT协议内置消息队列和QoS(Quality of Service,服务质量)机制,可确保消息可靠传输。

  3. 与CoAP对比:CoAP(Constrained Application Protocol,受限应用协议)是另一种物联网通信协议,主要针对资源受限的设备。相较于CoAP,MQTT协议在低带宽环境下具有更好的性能。然而,CoAP协议基于UDP,具有较低的传输延迟。二者之间的选择取决于具体应用场景和需求。

作者:程序员昊白
链接:https://juejin.cn/post/7224794158744862776
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 带你重新认知属性动画

前言 之前写过一篇关于属性动画简单使用的文章juejin.cn/post/714417… 虽然官方直接提供的属性动画只有4个效果:透明度、位移、旋转、缩放,然后用Set实现组合,用插值器加一些效果。但其实属性动画能做的超越你的想象,他能做到anything。你...
继续阅读 »

前言


之前写过一篇关于属性动画简单使用的文章juejin.cn/post/714417…


虽然官方直接提供的属性动画只有4个效果:透明度、位移、旋转、缩放,然后用Set实现组合,用插值器加一些效果。但其实属性动画能做的超越你的想象,他能做到anything。你可以实现各种你所想象的效果,改图片形状、路径的动画、颜色的变化等(当然这得是矢量图)。而插值器,除了系统提供的那些插值器之外,你还能进行自定义实现你想要的运动效果。


实现的效果


我这里拿个形变的效果来举例。可以先看看实现的效果:


sp.gif


实现要点


要点主要有两点:(1)要去想象,到了这种程度包括更复杂的效果,没有人能教你的,只能靠自己凭借经验和想象力去规划怎么实现。 (2)要计算,一般做这种自定义的往往会涉及计算的成分,所以你要实现的效果越高端,需要计算的操作就越复杂。


思路


我做这个播放矢量图和暂停矢量图之间的形变,这个思路是这样的: 其实那个三角形是由两部分组成,左边是一个矩形(转90度的梯形),右边是一个三角形。然后把两个图形再分别变成长方形。具体计算方式是我把width分成4份,然后配合一个偏移量offset去进行调整(计算的部分没必要太纠结,都是要调整的)


步骤:



  1. 绘制圆底和两个图形

  2. 属性动画

  3. 页面退出后移除动画


1. 绘制圆底和两个图形


一共三个Paint


init {
paint = Paint()
paint2 = Paint()
paint3 = Paint()

paint?.color = context.resources.getColor(R.color.kylin_main_color)
paint?.isAntiAlias = true
paint2?.color = context.resources.getColor(R.color.kylin_white)
paint2?.style = Paint.Style.FILL
paint2?.isAntiAlias = true
paint3?.color = context.resources.getColor(R.color.kylin_white)
paint3?.isAntiAlias = true
}

绘制圆底就比较简单


paint?.let {
canvas?.drawCircle((width/2).toFloat(), (height/2).toFloat(), (width/2).toFloat(),
it
)
}

然后先看看我的一个参考距离的计算(有这个参考距离,才能让图形大小跟着宽高而定,而不是写死)


override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (baseDim == 0f){
baseDim = (0.25 * width).toFloat()
}
}

另外两个图用路径实现


if (path1 == null || path2 == null){
path1 = Path()
path2 = Path()
// 设置初始状态
startToStopAnim(0f)
}
paint2?.let { canvas?.drawPath(path1!!, it) }
paint3?.let { canvas?.drawPath(path2!!, it) }

看具体的绘制实现


private fun startToStopAnim(currentValue : Float){
val offset : Int = (baseDim * 0.25 * (1-currentValue)).toInt()

path1?.reset()
path1?.fillType = Path.FillType.WINDING
path1?.moveTo(baseDim + offset, baseDim) // 点1不变
path1?.lineTo(2 * baseDim+ offset - baseDim/3*currentValue,
baseDim + (0.5 * baseDim).toInt() * (1-currentValue))
path1?.lineTo(2 * baseDim+ offset - baseDim/3*currentValue,
2 * baseDim +(0.5 * baseDim).toInt() + (0.5 * baseDim).toInt() * currentValue)
path1?.lineTo(baseDim+ offset, 3 * baseDim) // 点4不变
path1?.close()


path2?.reset()
path2?.fillType = Path.FillType.WINDING
if (currentValue <= 0f) {
path2?.moveTo(2 * baseDim + offset, baseDim + (0.5 * baseDim).toInt())
path2?.lineTo(3 * baseDim + offset, 2 * baseDim)
path2?.lineTo(2 * baseDim + offset, 2 * baseDim + (0.5 * baseDim).toInt())
}else {
path2?.moveTo(2 * baseDim+ offset + baseDim/3*currentValue,
baseDim + (0.5 * baseDim).toInt() * (1-currentValue))
path2?.lineTo(3 * baseDim + offset, baseDim + baseDim * (1-currentValue))
path2?.lineTo(3 * baseDim + offset, 2 * baseDim + baseDim * currentValue)
path2?.lineTo(2 * baseDim+ offset + baseDim/3*currentValue,
2 * baseDim +(0.5 * baseDim).toInt() + (0.5 * baseDim).toInt() * currentValue)
}
path2?.close()
}

这个计算的过程不好解释,加偏移量就是一个调整的过程,可以去掉偏移量offset看看效果就知道为什么要加了。path1代表左边的路径,左边的路径是4个点,path2是右边的路径,右边的路径会根据情况去决定是3个点还是4个点,默认情况是3个。


2、属性动画


fun startToStopChange(){
isRecordingStart = true
if (mValueAnimator1 == null) {
mValueAnimator1 = ValueAnimator.ofFloat(0f, 1f)
mValueAnimator1?.addUpdateListener {
val currentValue: Float = it.animatedValue as Float
startToStopAnim(currentValue)
postInvalidate()
}
mValueAnimator1?.interpolator = AccelerateInterpolator()
}
mValueAnimator1?.setDuration(500)?.start()
}

float类型0到1其实就是实现一个百分比的效果。变过去能实现后,变回来就也就很方便


fun stopToStartChange(){
isRecordingStart = false
if (mValueAnimator2 == null) {
mValueAnimator2 = ValueAnimator.ofFloat(1f, 0f)
mValueAnimator2?.addUpdateListener {
val currentValue: Float = it.animatedValue as Float
startToStopAnim(currentValue)
postInvalidate()
}
mValueAnimator2?.interpolator = AccelerateInterpolator()
}
mValueAnimator2?.setDuration(500)?.start()
}

3.移除动画


view移除后要移除动画


fun close(){
try {
if (mValueAnimator1?.isStarted == true){
mValueAnimator1?.cancel()
}
if (mValueAnimator2?.isStarted == true){
mValueAnimator2?.cancel()
}
}catch (e : Exception){
e.printStackTrace()
}finally {
mValueAnimator1 = null
mValueAnimator2 = null
}
}

然后还要注意,这个动画是耗时操作,所以要做防快速点击。


总结


从代码可以看出,其实并实现起来并不难,难的在于自己要有想象力,要能想出这样的一个过程,比较花费时间的可能就是一个调整的过程,其它也基本没什么技术难度。


我这个也只是简单做了个Demo来演示,你要问能不能实现其它效果,of course,你甚至可以先把三角形变成一个正方形,再变成两个长方形等等,你甚至可以用上贝塞尔来实现带曲线的效果。属性动画就是那么的强大,对于矢量图,它能实现几乎所有的你想要的效果,只有你想不到,没有它做不到。


作者:流浪汉kylin
链接:https://juejin.cn/post/7144190782205853732
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信发送语音自定义view中的事件分发

这里通过一个自定义view的例子来看事件分发在自定义view中的使用,其实大部分的Android框架下的事件分发的也都差不多的样子,抛砖引玉,我自己做一个记录,如果能帮到有需要的人那就更上一层楼。 先来看一个微信发送语音的效果图: 关于事件分发我们其实耳熟能...
继续阅读 »

这里通过一个自定义view的例子来看事件分发在自定义view中的使用,其实大部分的Android框架下的事件分发的也都差不多的样子,抛砖引玉,我自己做一个记录,如果能帮到有需要的人那就更上一层楼。

先来看一个微信发送语音的效果图:


关于事件分发我们其实耳熟能详,可以通过一段非常有名的伪代码来大致了解:


  @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;

}

事件都是从一个DOWN开始,中间经过一堆MOVE,到一个UP结束(先抛开CANCEL的情况)。

事件流向画了半天,感觉也没有人家画的好,可以参考 这里 ,很清晰,忘记了的或者细节不清楚模糊了的可以移步去复习一下。


我们的需求是点击发送按钮后显示浮层view,相当于事件先由发送按钮处理,等浮层view显示后再交由浮层view处理,这个事件的流向很清晰,那应该怎么做呢。


那最简单的view的层级结构就是发送按钮浮层view处在同一层级,那一个问题,事件能否在parent什么都不做的情况下实现事件在同级别view之间的转移呢?

肯定是不可以或者说没有必要的,最好的方式还是通过parent来做分发,由parent的决定此时到底是需要把事件交给发送按钮还是浮层view


所以层级结构上:


<?xml version="1.0" encoding="utf-8"?>
<!-- parent, 来控制事件的分发 -->
<com.yocn.af.view.widget.WeChatParentViewGroup
android:id="@+id/wechat_root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:id="@+id/ll_option"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true">

<!-- 发送按钮view -->
<com.yocn.af.view.widget.WeChatVoiceTextView
android:id="@+id/tv_voice"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="按住 说话"/>
</LinearLayout>
<!-- 点击后需要显示的浮层view -->
<com.yocn.af.view.widget.WeChatVoiceView
android:id="@+id/voice_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/half"
android:visibility="gone" />

</com.yocn.af.view.widget.WeChatParentViewGroup>

我们梳理一下思路,需要做的就是:



  1. 什么都没做时ParentViewGrouponInterceptTouchEvent要返回false,使得事件能顺利的从ParentViewGroup传递到VoiceTextView(发送按钮)

  2. 点击到VoiceTextView(发送按钮)时,发送按钮的dispatchTouchEvent返回true,处理DOWN事件并告诉parent需要显示WeChatVoiceView(浮层view)

  3. parent接收到需要显示浮层view的命令,显示浮层view并且onInterceptTouchEvent返回true,表示事件我parent来处理,这时VoiceTextView(发送按钮)会收到一个CANCEL事件并且不会继续接受MOVE事件。

  4. parent来分发事件,在WeChatVoiceView(浮层view)显示出来之后直接将后续的MOVE事件交给WeChatVoiceView(浮层view)处理,当然浮层view的onInterceptTouchEvent需要返回true,会回调到浮层view的onTouchEvent,直接做对应的动画或者手势操作。

  5. 当然不要忘记在parent收到ACTION_UP的时候将浮层view置为不可见,因为事件是由parent分发给浮层view的,当然parent可以一直拿到事件。


至此,整个事件分发的流程就结束了。
附上代码地址WeChatSendVoice


作者:Yocn
链接:https://juejin.cn/post/7224318172831531068
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android项目cicd流程总结(使用jenkins)

没有cicd之前我们都是怎么做的 相信做安卓开发的都做过这些事 手动运行单元测试,根据报错改代码 检查代码风格,根据报错改代码 构建apk包,发给测试,有时候还得打很多个 接收测试的反馈,改bug, 重复之前的步骤 把apk放到ftp或者其他地方去发布 是...
继续阅读 »

没有cicd之前我们都是怎么做的


相信做安卓开发的都做过这些事



  1. 手动运行单元测试,根据报错改代码

  2. 检查代码风格,根据报错改代码

  3. 构建apk包,发给测试,有时候还得打很多个

  4. 接收测试的反馈,改bug,

  5. 重复之前的步骤

  6. 把apk放到ftp或者其他地方去发布


是不是想到这一套流程,头都大了,虽然每一步都不难,但是连起来都手工操作就很繁琐


像这些手动流程固定的事,我们完全就可以交给机器来做,让我们有更多时间做点别的事,没错,这就是今天要说的cicd


什么是cicd,以及在cicd过程中包含了哪些步骤


一般来说,安卓开发的CI/CD流程包括以下几个阶段:代码提交、代码检查、编译构建、单元测试集成测试、部署发布、用户反馈。



  1. 在代码提交阶段,开发者将自己的代码推送到远程仓库,例如Git或SVN,并触发CI/CD工具或平台例如Jenkins或Travis CI等

  2. 在代码检查阶段,CI/CD工具或平台会对代码进行静态分析和风格检查,例如使用SonarQube或Checkstyle等。

  3. 在编译构建阶段,CI/CD工具或平台会使用Gradle或Maven等工具对代码进行编译和打包,生成APK文件

  4. 在单元测试阶段,CI/CD工具或平台会使用JUnit或Espresso等框架对代码进行单元测试,并生成测试报告。

  5. 在集成测试阶段,CI/CD工具或平台会使用Appium或Selenium等框架对应用进行集成测试,并生成测试报告

  6. 在部署发布阶段,CI/CD工具或平台会将APK文件上传到内部服务器或外部平台,例如蒲公英或Google Play等,并通知相关人员。

  7. 在用户反馈阶段,开发者可以通过Bugly或Firebase等工具收集用户的反馈和错误信息,并根据需要进行修复和更新


通过以上几个步骤,我们可以把以前的app构建流程从手动变为自动,而且可以通过不断以非常低的成本的重复这个过程,提高我们的项目质量,这就是cicd带给我们的自信


今天我们来通过jenkins来实现上面的几个步骤


安装配置jenkins


本文讨论的主要是在windows环境下安装jenkins



  1. 从jenkins官网下载对应的安装包即可

  2. 安装过程很简单但是需要提供一个账号,就像下图显示的界面,这个账号需要有权限
    图片.png
    打开开始菜单,搜索本地安全策略,选择本地策略用户权限分配,在右侧的策略中找到作为服务登录,双击打开。点击添加用户或组,在输入框中填入你的账户的名字,单击检查名称,如果加上了下划线,则说明没有问题,如果输入的用户不存在,则会跳出来一个找不到名称的对话框。
    图片.png


这里需要注意一点,windows家庭版默认是没有本地安全策略的,需要用一些技巧把它开启,如下:


1.  在桌面上单击右键,选择“新建”->“文本文档”。

2. 将文本文档重命名为“OpenLocalSecurityPolicy.bat”。

3. 右键单击“OpenLocalSecurityPolicy.bat”,选择“编辑”。

4. 将以下命令复制并粘贴到文本编辑器中:


@echo off
pushd "%SystemRoot%\system32"
findstr /c:"[SR] Cannot repair member file" %windir%\logs\cbs\cbs.log >%userprofile%\Desktop\sfcdetails.txt
start ms-settings:windowsdefender
start ms-settings:windowsupdate
start ms-settings:windowsupdate-history
start ms-settings:windowsupdate-options
start ms-settings:storagesense
start ms-settings:storagesense-diagnostics
start ms-settings:storagesense-configurecleanup
start ms-settings:storagesense-changehowwesave
start ms-settings:storagesense-runstoragecleanupnow
start ms-settings:storagesense-storageusage
start ms-settings:storagesense-changestoragesavelocations
start ms-settings:backup
start ms-settings:backup-advancedsettings
start ms-settings:backup-addalocaldriveornetworklocation
start ms-settings:backup-managebackups
start ms-settings:backup-moreoptions
start ms-settings:dateandtime
start ms-settings:regionlanguage
start ms-settings:regionlanguage-languagepacks
start ms-settings:regionlanguage-speech
start ms-settings:regionlanguage-keyboards
start ms-settings:regionlanguage-morespeechservicesonline
start ms-settings:speech
start ms-settings:speech-microphoneprivacysettings


5. 保存并关闭文本编辑器。
6. 双击“OpenLocalSecurityPolicy.bat”文件,以打开本地安全策略。
复制代码

3. 修改默认根地址到其他盘符


默认情况下,jenkins的主目录都是在c盘,如果这样,我们使用中产生的数据都是在c盘,用过windows的都知道,数据放在c盘是很危险也是很让人不爽的一件事,我们可以通过修改jenkins的主目录方法来把数据放到其他盘


在成功安装了jenkins并解锁之后,我们可以配置环境变量JENKINS_HOME,地址就是我们想改的目录,


图片.png
然后修改jenkins.xml


    <env name="JENKINS_HOME" value="%LocalAppData%\Jenkins.jenkins"/>
复制代码

改为


    <env name="JENKINS_HOME" value="E:\jenkins"/>
复制代码


  1. 配置常用的插件
    在第一次启动jenkins的时候,会让你选择安装哪些插件,这时候直接选择推荐的插件就好,包含了一些常用插件,比如git等等,如下图


图片.png


配置针对于android的环境



  1. android sdk 见下图
    图片.png

  2. gradle -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的Gradle配置gradle路径即可

  3. jdk -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的JDK配置jdk路径即可

  4. git -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的Git installations配置git路径即可


配置Android的具体job信息


新建一个freestyle的item,在里面做以下几步:




  1. 配置git仓库地址以及构建分支




  2. 设置构建触发器(定时构建) -- 找到构建触发器,勾选build periodically,在编辑框里按照规则设置构建时间,在某天的某个时段自动构建,比如45 9-15/2 * * 1-5,虽然可以提交一次就构建一次,但是不建议这么做。构建表达式的规则见下图,可以根据自己的需要写表达式。
    图片.png




  3. 添加构建步骤,打包出apk,如下图在build step中触发
    图片.png




  4. 配置构建后步骤,自动把apk包上传到ftp或者其他地方


    在Jenkins的项目中,选择“构建”->“增加构建步骤”->“执行shell”或“执行Windows批处理命令”
    一个上传ftp的例子




ftp -n <<EOF
open http://ftp.example.com
user username password
cd /remote/directory
put /local/file
bye
EOF
复制代码

配置邮件通知


在构建完成之后,特别是失败的时候,我们希望收到一封邮件告诉我们构建失败了,快去处理,我们可以通过以下步骤来实现



  1. 在Jenkins中安装Email Extension Plugin插件,可以在插件管理中搜索并安装。

  2. 在Jenkins的系统管理中,配置邮件服务器的地址,用户名,密码和端口。如果使用的是QQ邮箱或者163邮箱,还需要获取邮箱授权码并使用授权码作为密码。

  3. 在Jenkins的项目中,选择“构建后操作”->“增加构建后操作”->“Editable Email Notification”。

  4. 在邮件通知的配置中,填写收件人,抄送人,邮件主题,邮件内容等信息。可以使用一些变量来自定义邮件内容,例如BUILDSTATUS表示构建状态,BUILDSTATUS表示构建状态, {BUILD_URL}表示构建链接等。


这里特别要注意的是,上面的配置地址和授权码需要在job的设置里面进行,在全局配置有可能发不出邮件


配置单元测试和代码检查


我们还需要在运行前执行代码lint检查和单元测试,也需要配插件,插件名字是JUnit和Warnings Next Generation



  1. 参考上面 配置Android的具体job信息 中的配置,添加lint和单元测试的任务

  2. 配置单元测试插件和lint插件,主要指定报告文件的位置,见下图


图片.png


图片.png
3. 把单元测试的结果放到邮件的附件中去,配置见下图,也可以放些别的东西


图片.png


一劳永逸,使用docker把上面的配置做到随时随地使用


上面的步骤完成之后,我们就能自动构建,上传apk什么的了,但是每次换台机器我们都得再配一次,想下就很累,这时候我们就可以用docker,创建一个容器,把上面这些操作放在容器里面,在新环境里面拉个镜像,创建容器跑起来,就ok啦,关于怎么用docker,就需要大家自己去搜索学习了


最后放张图吧,jenkins真好用啊


封面.png


作者:u3shadow
链接:https://juejin.cn/post/7224410321669554231
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

模拟点击与群控——autojs使用

写在前面 autojs是利用accessibility功能实现的一套免root自动化模拟点击框架,让开发者可以直接通过使用js脚本实现一系列的自动化操作,包括:触摸屏幕、滑动、输入文字等。autojs具有很高的灵活性和可扩展性,可以被用于各种场景,例如自动化游...
继续阅读 »

写在前面


autojs是利用accessibility功能实现的一套免root自动化模拟点击框架,让开发者可以直接通过使用js脚本实现一系列的自动化操作,包括:触摸屏幕、滑动、输入文字等。autojs具有很高的灵活性和可扩展性,可以被用于各种场景,例如自动化游戏操作、自动登录、自动化测试等。


autojs免费版本已经停止维护,官网只有Autojs Pro付费版本。然而最近(20230214)又因为一些合规问题,被强制下架已经不允许用户注册。市面上可使用代替产品autox,该项目基于原autojs4.1版本基础上进行维护开发。本文主要围绕该项目展开。


开发环境搭建


使用vscode,添加插件Auto.js-Autox.js-VSCodeExt,如下图,注意不要重复安装多个类似插件,会存在冲突问题。




按住cmd+shift+p,输入> Auto.js,选择"开启服务(Start Server)",此时右下角会提示服务正在运行提示,并显示ip地址信息,如下图,ip为:192.168.1.102




手机与电脑连接同个wifi,并打开autox app,打开"无障碍服务",并打开"连接电脑"按钮,输入ip地址,如下图,点击确认,即可与电脑同步。



连接成功后出现如下提示,此时开发环境搭建完成。



测试hello world程序,创建Autox.js文件,并输入内容toast("hello world!"),选择js文件,右键-重新运行,即可将脚本同步到手机运行,此时手机会出现hello world!的一个toast提示。



js脚本开发指导


关于autojs的API可参考官方文档,这里主要是讲解一下使用的思路。我们在开发自动化工具时,最常见的问题就是如何找到我们所需要点击的控件节点,每一个节点包含的信息包括:



  • className 类名。类名表示一个控件的类型,例如文本控件为"android.widget.TextView",图片控件为"android.widget.ImageView"等。

  • id控件节点的唯一id。

  • text节点名字,不一定有,可能为空。

  • desc节点的描述信息,不一定有,可能为空。

  • packageName 包名。包名表示控件所在的应用包名,例如 QQ 界面的控件的包名为"com.tencent.mobileqq"。

  • bounds 控件在屏幕上的范围。

  • drawingOrder 控件在父控件的绘制顺序。

  • indexInParent 控件在父控件的位置。

  • clickable 控件是否可点击。

  • longClickable 控件是否可长按。

  • checkable 控件是否可勾选。

  • checked 控件是否可以勾选。

  • scrollable 控件是否可滑动。

  • selected 控件是否已选择。

  • editable 控件是否可编辑。

  • visibleToUser 控件是否可见。

  • enabled 控件是否已启用。

  • depth 控件的布局深度。


控件id是最为常用的一个唯一性标记,我们写自动化认为时,经常使用id来对特定控件做点击操作。但是我们如何得知具体控件id信息呢?我们可以利用以下js脚本,将整个界面的控件信息进行打印输出。


toastLog("start.");

function printNode(node){
if(node){
var text = node.text();
var desc = node.desc();
let bounds = node.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
var click = node.clickable();
var id = node.id();
log(id, text, desc, click, left, right, top, bottom);
}
}

function traverse(node) {
printNode(node);
var cnt = node.childCount();
for (var i = 0; i < cnt; i++) {
traverse(node.child(i));
}
}

let windowRoot = auto.rootInActiveWindow;
if(windowRoot){
log("tracerse node.");
traverse(windowRoot);
}else{
log("window root is null.");
}

我们可以结合node.bounds()中控件的大小以及所在位置,来猜测我们所要点击的目标控件。在获得某个具体控件id后,即可使用如下js脚本进行点击操作。


target_id=""
id(target_id).findOne().click()

查看viewid脚本开发


这一节我们将利用canvas绘图将每个控件绘制出来,让我们方便地看出来我们所要操作的控件viewid。首先我们需要利用递归方式遍历当前页面上的所有控件,并存放在list变量中,如下。


function traverse(node) {
if(node != null){
viewNodes.push(node);
}
var cnt = node.childCount();
for (var i = 0; i < cnt; i++) {
traverse(node.child(i));
}
}

//x:946, y:80
let windowRoot = auto.rootInActiveWindow;

if(windowRoot){
log("tracerse node.");
traverse(windowRoot); // 开始遍历控件树并打印控件的text属性
}else{
log("window root is null.");
}

function printNode(i, node){
if(node){
var text = node.text();
var desc = node.desc();
let bounds = node.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
var click = node.clickable();
var id = node.id();
log(i, id, text, desc, click, left, right, top, bottom);
}
}
var len = viewNodes.length;
for (var i = 0; i < len; i++) {
let childViewNode = viewNodes[i];
printNode(i, childViewNode);
}

使用浮窗功能,在顶层绘制一张透明的画布,如下:


//ui布局为一块画布
var w = floaty.rawWindow(
<frame gravity="center" bg="#ffffff">
<canvas id="canvas" layout_weight="1"/>
</frame>
);

w.setSize(device.width, device.height); // 设置窗口大小
w.setTouchable(false); // 设置触摸透传

使用canvas绘图库,用绿色边框将各个控件圈出,并在每个控件上显示在list中对应的序号。


let paint = new Paint();
paint.setColor(colors.parseColor("#00ff00"));
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);

let paintText = new Paint();
paintText.setColor(colors.parseColor("#FF0000"));
paintText.setTextSize(80);
paintText.setStrokeWidth(20);

var isDraw = 1;
w.canvas.on("draw", function (canvas) {
if(isDraw < 20){
isDraw = isDraw + 1;
var len = viewNodes.length;
for (var i = 0; i < len; i++) {
let childViewNode = viewNodes[i];
let bounds = childViewNode.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
canvas.drawRect(left, top, right, bottom, paint);
// log(left, bottom, right, top)
canvas.drawText("" + i, left, bottom, paintText);
}
}
});

为了不让脚本退出,我们需要使用设置等待时间,让脚本持续运行,如下,若没有等待执行,脚本执行后立马退出,我们将无法看到绘图内容。


setTimeout(()=>{
w.close();
}, 50000);

效果图如下:



作者:风铃Cipher
链接:https://juejin.cn/post/7224063449616236605
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android通过BLE传输文件

1、遇到的问题 公司要通过Android设备给外围设备的固件进行OTA升级,最开始想到的有两种方案。 1、将当前Android设备所连接 Wifi名称,WiFi密码通过BLE发送给外围设备。 外围设备拿到当前环境的WiFi名称和密码连接热点, 然后自己去服务器...
继续阅读 »

1、遇到的问题


公司要通过Android设备给外围设备的固件进行OTA升级,最开始想到的有两种方案。


1、将当前Android设备所连接 Wifi名称,WiFi密码通过BLE发送给外围设备。 外围设备拿到当前环境的WiFi名称和密码连接热点, 然后自己去服务器下载OTA文件,进行升级

2、当前Android设备和外围设备通过经典蓝牙进行传输OTA文件, 外围设备拿到OTA文件进行升级

但是很遗憾,外围设备既没有WiFi芯片, 也没有经典蓝牙芯片, 只有一颗BLE(低功耗蓝牙)芯片。 这意味着上面的两种方案都行不通。 那我们能不能通过BLE芯片来做文章, 来传输OTA文件?


BLE设计之初就是为了传输简单的指令的, 传输一些小数据的, 每次发送的数据大小不能超过20个字节。到底靠不靠谱啊?


2、 能不能通过BLE传输文件


让我们来问问 GPT 吧


p9uZOaR.png


GPT 的回答, 是可以通过BLE传输文件的, 由于BLE 每次传输的内容最大为20个字节, 传输大文件时就需要分包传输,
同时需要确保分包传输的可靠性和稳定性。


3、 如何传输文件


让 GPT 给我们一些示例代码


p9uekdA.png


可以看出, 发送端分包批量发送数据,接收端


4、如何保证可靠性和稳定性


p9K6UHO.png


1、超时重传


蓝牙在传输过程中, 可能会存在丢包的情况。分两种情况,
1、Android设备发送的数据,外设设备没有收到。
2、Android设备发送的数据,外设设备收到了,并且发送了回复确认。 回复确认包Android设备却没有收到。


出现了这两种情况的任意一种, 则认为发生了丢包的情况。 Android 对这个包进行重发。


2、序列号


针对超时重传的第二种情况, 外设设备会收到两个相同的包。 但是外设设备不清楚是不是重装包。 这时就要给每个数据包添加序列号。 等外设设备收到两个相同序列号的数据包时, 丢弃这个数据包, 回复Android设备收到此包, 开始发送下一个数据包。


3、数据校验


BLE在传输的过程中, 如果周围环境有强蓝牙干扰,或者其他传输通道, 可能会导致数据包变更, 所以需要在数据包添加一个校验位, 这个校验位根据特定的算法,由数据包的数据计算得来。 外设设备收到数据后, 重新计算校验位, 判断数据传输过程是否出现差错, 如果计算的校验位和包传输的校验位不一致, 则要求Android设备重新发送这个包。


5、 传输速度提升 RequestMtu


为了保证传输过程中的可靠性和稳定性,我们需要在传输包中,添加序列号,数据校验等信息。 Android默认每个BLE数据包不超过20个字节,当我们加了一些其他信息时, 每次传输的有效数据可能只有15个字节左右。 导致在传输的过程中分包更多, 传输时间更长。


为了提升传输的速度, 我们来提升BLE每个数据包的传输大小限制, 使每个分包可以传输更多的数据。 系统为我们提供了 RequestMtu这个接口。 需要在gatt连接成功时调用


    private val bluetoothGattCallback = object : BluetoothGattCallback() {

override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)

if (newState == BluetoothGatt.STATE_CONNECTED) {
Log.d(TAG, "gatt 连接成功")
gatt?.requestMtu(40)
} else {
Log.d(TAG, "gatt 连接失败 status $status newstate $newState")
}

}


override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)

if (BluetoothGatt.GATT_SUCCESS == status) {
Log.d(TAG, "onMtuChanged suc : $mtu")
gatt?.discoverServices()
} else {
Log.d(TAG, "onMtuChanged fail : $status")
}
}
}

MTU改变成功后, 再去gatt.discoverServices()发现服务


作者:沉默王贰
链接:https://juejin.cn/post/7226223658775298107
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Android】Kotlin 中的 apply、let、also、run 到底有啥区别?

前言 Kotlin 作为一门现代的编程语言,为了提高代码的可读性和简洁性,引入了许多扩展函数。本文将详细介绍 Kotlin 中的四个常用扩展函数:apply、let、also、run,并比较它们的联系与区别。 apply apply 函数接收一个对象并返回该对...
继续阅读 »

前言


Kotlin 作为一门现代的编程语言,为了提高代码的可读性和简洁性,引入了许多扩展函数。本文将详细介绍 Kotlin 中的四个常用扩展函数:applyletalsorun,并比较它们的联系与区别。


apply


apply 函数接收一个对象并返回该对象本身。它允许您在对象上执行一些操作,同时仍然返回原始对象。


这个函数的语法为:


fun <T> T.apply(block: T.() -> Unit): T

其中,T 是对象的类型,block 是一个 lambda 表达式,可以在该对象上执行一些操作。在这个 lambda 中,您可以使用 this 关键字来引用当前对象。


例如:


val person = Person().apply {
name = "John"
age = 30
address = "New York"
}

在这个例子中,我们创建了一个 Person 对象并在 apply 函数中设置了该对象的属性。最终返回的是这个 Person 对象本身。


let


let 同样是一个扩展函数,它也接收一个 Lambda 表达式作为参数。不同于 apply,在 Lambda 表达式中,let 函数将接收者对象作为 Lambda 的参数,通常用 it 作为隐式名称。let 函数的返回值是 Lambda 表达式的结果。


这个函数的语法为:


kotlinCopy code
fun <T, R> T.let(block: (T) -> R): R

其中,T 是对象的类型,R 是返回值的类型,block 是一个 lambda 表达式,它对该对象进行一些操作并返回一个结果。


val person = Person("Alice", 25)
val ageAfterFiveYears = person.let {
it.age + 5
}

在这个示例中,let 函数用于计算 Person 对象五年后的年龄。


also


also 是一个扩展函数,与 apply 类似,但有一个关键区别:also 函数的返回值是接收者对象本身,而 Lambda 表达式的参数是接收者对象,通常用 it 作为隐式名称。
这个函数的语法为:


fun <T> T.also(block: (T) -> Unit): T

其中,T 是对象的类型,block 是一个 lambda 表达式,可以在该对象上执行一些操作。在这个 lambda 中,您可以使用 it 关键字来引用当前对象。


val person = Person("Alice", 25).also {
it.name = "Bob"
it.age = 30
}

在上述示例中,also 函数用于修改 Person 类的属性,最后返回修改后的对象。


run


run 是一个扩展函数,它结合了 applylet 的特点。run 函数在 Lambda 表达式中直接访问接收者对象的属性和方法,同时返回 Lambda 表达式的结果。


这个函数的语法为:


fun <T, R> T.run(block: T.() -> R): R

其中,T 是对象的类型,R 是返回值的类型,block 是一个 lambda 表达式,它对该对象进行一些操作并返回一个结果。在这个 lambda 中,您可以使用 this 关键字来引用当前对象。


val person = Person("Alice", 25)
val greeting = person.run {
"Hello, $name! You are $age years old."
}

在这个示例中,run 函数用于生成一个包含 Person 对象信息的字符串。


总结


四个函数的相同点是,它们都可以操作对象,并可以在 lambda 中引用当前对象。但是,它们的返回值和返回时机有所不同。


apply 和 also 函数的返回值是该对象本身,而 let 和 run 函数的返回值是 lambda 表达式的结果。


apply 函数在对象上执行一些操作,并返回该对象本身。它通常用于在对象创建后立即对其进行初始化。


also 函数类似于 apply 函数,但它返回原始对象的引用。它通常用于对对象进行一些副作用,例如打印日志或修改对象状态。


let 函数在 lambda 中对对象进行一些操作,并返回 lambda 表达式的结果。它通常用于在某些条件下对对象进行转换或计算。


run 函数类似于 let 函数,但它返回 lambda 表达式的结果。它通常用于对对象进行计算,并返回计算结果。


总之,这四个函数都是非常有用的函数式编程工具,可以帮助您以简洁、可读性强的方式操作对象和代码块。对于每个情况,您应该选择最合适的函数,以便以最有效的方式编写代码。


作者:Quincy_Ye
链接:https://juejin.cn/post/7225975883618140221
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不就new个对象的事,为什么要把简单的问题复杂化?为什么要使用Hilt依赖注入?看完就知道!

为什么要使用Hilt依赖注入 之前有写过一些文章 Hilt与Koin的对比,和 Android开发中Hilt的常用场景。看起来是非常简单与实用了,但是会引起一些同学的疑问? 为什么要使用依赖注入?直接new对象不香吗?为什么要把简单的问题复杂化? 你是不是在炫...
继续阅读 »

为什么要使用Hilt依赖注入


之前有写过一些文章 Hilt与Koin的对比,和 Android开发中Hilt的常用场景。看起来是非常简单与实用了,但是会引起一些同学的疑问?


为什么要使用依赖注入?直接new对象不香吗?为什么要把简单的问题复杂化?


你是不是在炫技,是不是像装13?


这还真不是,如果说我使用的Dagger2,还真是炫技,NB啊。Dagger的坑也是真的多,能在大项目中把Dagger用好,那还真是牛,但是我们现在都用Hilt了有什么可装的,使用是真的简单,都是一些场景化的东西,一些固定用法。没必要没必要。


回归正题,为什么要使用依赖注入?哪种情况下推荐使用依赖注入?就算要用依赖注入,为什么依赖注入推荐使用Hilt?


一、自动管理(灵活与解耦)


首先不是说大家写项目就一定要使用依赖注入,如果大家的项目不是大项目,总共就5、6个,10多个页面,你没必要上依赖注入框架,如果是大项目,分模块,分组件,多人协同开发的,并且可能依赖的对象很复杂,或者说套娃似的对象依赖,那么使用Hilt就非常方便。不同模块/组件的开发人员直接在他自己的组件/模块下定义好对象的提供方式,另一边直接用即可,无需关系依赖的复杂度,和实现的逻辑。


我们先看看一些复杂的嵌套依赖,比如我们来一个三层套娃的依赖:


@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

fun testUser() {
YYLogUtils.w(userDao.printUser())
toast(userDao.printUser())
}

fun getDaoContent():String{
return userDao.printUser()
}

}

@Singleton
class UserDao @Inject constructor(private val user: UserBean) {

fun printUser(): String {
return user.toString()
}

}

data class UserBean(
val name: String,
val age: Int,
val gender: Int,
val languages: List<String>
)

其他三个类都是必须了,其实也就多了一个这个类,提供UserBean对象


@Module
@InstallIn(SingletonComponent::class)
class Demo10DIModule {

@Singleton
@Provides
fun provideUser(): UserBean {
return UserBean("newki", 18, 1, listOf("中文", "英文"))
}

}

使用:


@AndroidEntryPoint
class Demo10DIActivity : BaseVMActivity() {

@Inject
lateinit var userServer: UserServer

override fun getLayoutIdRes(): Int = R.layout.activity_demo10_di

override fun init() {
findViewById<Button>(R.id.btn_01).click {
YYLogUtils.w(userServer.toString())
userServer.testUser()
}
}

如果不使用Hilt,自己new对象也能实现


class Demo10DIActivity : BaseVMActivity() {

lateinit var userServer: UserServer

override fun getLayoutIdRes(): Int = R.layout.activity_demo10_di

override fun init() {
//自己new三个对象
userServer = UserServer(UserDao(UserBean("newki", 18, 1, listOf("中文", "英文"))))

findViewById<Button>(R.id.btn_01).click {
YYLogUtils.w(userServer.toString())
userServer.testUser()
}
}

这样new出来的对象,且不说生命周期是跟随页面的,无法保持单例,我们就说如果需求变了,UseDao中需要UserBean和UserProfile2个对象了,如果你是new对象,那么就要到处修改,如果是Hilt的方式,我们就只需要修改UserDao对象的构造即可


@Singleton
class UserDao @Inject constructor(private val user: UserBean,private val profile:UserProfile) {

fun printUser(): String {
return user.toString()
}

}

以上只是举个简单例子,构建一个对象,还要构建一堆其他的对象,并且其他对象的构建同样复杂,并且必须按顺序构建,而且需要的对象的生命周期都不一样,有些生命周期可能和Activity一样,有些可能是单例,所以在构建的时候还要考虑对象声明周期,考虑对象的来源。


特别是在大型项目中很痛苦,因为项目不是一个人写的,大家协同合作开发,看别人的代码也和看天书一样,并不知道同事的对象是如何创建的,如果一个对象的构建方式发生改变,会影响整个的构建过程以及所关联的代码,牵一发而动全身。


这个时候依赖注入框架就派上用场了,我们只用专注于怎么实现功能,对象的依赖关系和生命周期,都让它来帮我们管理,一个Inject,它会按照依赖关系帮我们注入我们需要的对象,并且它会管理好每个对象的生命周期,在生命周期还没结束的情况下是不会重复new的。


所以Hilt依赖注入非常适合大项目,小项目开发者因为项目复杂度低,没遇到这些问题,所以不会理解为什么要用Hilt依赖注入,发出疑问,为什么要让简单的new对象搞这么复杂。


二、生命周期控制


这里说对象的生命周期,其实就是在一定作用域的生命周期,如果只是说单例有点太浅薄,可以说是是在一定范围内的单例。


我们直接new对象是无法控制生命周期的,除非我们使用全局单例的对象,而通过Hilt依赖注入我们可以很方便的实现对象的生命周期的控制。


比如我们普通对象的快速注入方式,直接注解Singleton就标注的是全局范围单例


@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

fun testUser() {
YYLogUtils.w(userDao.printUser())
toast(userDao.printUser())
}

fun getDaoContent():String{
return userDao.printUser()
}

}

另一种用法是我们使用Module的方式定义依赖注入,那么使用SingletonComponent + Singleton 同样是全局范围单例的意思


@Module
@InstallIn(SingletonComponent::class)
class Demo10DIModule {

@Singleton
@Provides
fun provideUser(): UserBean {
return UserBean("newki", 18, 1, listOf("中文", "英文"))
}

}

如果我们想Activity内的单例,我们使用ActivityComponent + ActivityScoped 就是Activity范围的单例。


@Module
@InstallIn(ActivityComponent::class)
class Demo10DIModule {

@ActivityScoped
@Provides
fun provideUser(): UserBean {
return UserBean("newki", 18, 1, listOf("中文", "英文"))
}

}

以上两种都是比较常用的作用域,其他的我们还能保证Fragment内单例,View内部单例等等,用法都是同样的用法。


所以对依赖对象的生命周期控制也是Hilt很方便的一个特点,使用new对象是无法做到这一点的。


三、对比其他依赖注入


目前Android主流的也就是三种依赖注入 Dagger2 Hilt Koin。


之前比较过Koin,只能在Kotlin语言环境中使用。并且性能并不会好过Hilt。错误提示也不友好。


Dagger2不是不能用,17年18年的时候特别火,但是学习成本很高,每次创建UI个依赖注入类还得mackproject,并且错误的提示也不友好,



其实我17年就已经在使用Dagger2了,然后自己做了Demo与框架封装,后来做项目中并没有使用,一个是坑太多,一个是同事不会用,学习成本太高。也就放弃使用Dagger2了。


而Hilt其实就是Daggert2的Android场景化的实现,内部对Dagger2进行了封装,在Android开发中使用Hilt更加的简便,学习成本很低,错误提示友好。并且还对ViewModel都可以注入了哦。所以说它是专为Android开发而生。


关于注入的对象内存占用是否有优化的这一点,其实并没有文章或者官方的文档指出有内存优化这一点,仅我自己的测试来说,整个页面如果有多个注入对象和直接new对象相比,感觉注入的对象占用内存稍微少一点,不知道是不是测试的波动,我不理解,如有懂的大佬还望指点一下。


总结


总结为什么要使用Hilt。



  1. 偷懒;自动管理,多对象的自动注入,万一有修改不需要到尸山中到处趴。

  2. 单例;让对象拥有生命周期,无需我们自己手动单例创建,然后去手动注销。

  3. 解耦;不需要到处引入我一些不需要的对象,特别是组件化的项目,另一个组件只管注入,在我的组件中我只管引用。


我觉得这是我使用Hilt最吸引我的三个点,


所以说目前2022年了,依赖注入我推荐Hilt。关键使用简单,在Android的常用场景下我还做了一些 Demo, 总共就那么多固定的一些用法,之前我写过Demo覆盖Android开发大部分的使用场景, 有需要直接拿走即可,可以查看我之前的文章。


顺便说一句,这是国外程序员的必备面试技能,感觉相比国内的开发者老外的Android开发者特别喜欢使用Dagger2和Hilt,不少老项目都是用Dagger2的。


好了,本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



作者:newki
链接:https://juejin.cn/post/7129673928460468254
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

理解Kotlin中的reified关键字

标题:理解Kotlin中的reified关键字 摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。 正文: 什么是reified关键字? 在Kotli...
继续阅读 »

标题:理解Kotlin中的reified关键字


摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。


正文:


什么是reified关键字?


在Kotlin中,reified是一个特殊的关键字,用于修饰内联函数中的类型参数。这使得在函数内部可以访问类型参数的具体类型。通常情况下,由于类型擦除(type erasure),在运行时是无法直接获取泛型类型参数的具体类型的。reified关键字解决了这个问题。


使用reified关键字的条件


要使用reified关键字,需要遵循以下几点:



  1. 函数必须是内联的(使用inline关键字修饰)。

  2. 类型参数前需要加上reified关键字。


示例:reified关键字的用法


下面是一个使用reified关键字的简单示例:


inline fun <reified T> checkType(value: Any) {
if (value is T) {
println("Value is of type T.")
} else {
println("Value is NOT of type T.")
}
}

fun main() {
val stringValue = "Hello, Kotlin!"
val intValue = 42

checkType<String>(stringValue) // 输出 "Value is of type T."
checkType<String>(intValue) // 输出 "Value is NOT of type T."
}

在这个示例中,我们定义了一个内联函数checkType,它接受一个reified类型参数T。然后,我们使用is关键字检查传入的value变量是否为类型T。在main函数中,我们用不同的类型参数调用checkType函数来验证它的功能。


获取类型参数的Java类


当你使用reified关键字修饰一个内联函数的类型参数时,你可以通过T::class.java获取类型参数对应的Java类。这在需要访问泛型类型参数的具体类型时非常有用,比如在反射操作中。


下面是一个简单的例子:


import kotlin.reflect.KClass

inline fun <reified T : Any> getClass(): KClass<T> {
return T::class
}

inline fun <reified T : Any> getJavaClass(): Class<T> {
return T::class.java
}

fun main() {
val stringKClass = getClass<String>()
println("KClass for String: $stringKClass") // 输出 "KClass for String: class kotlin.String"

val stringJavaClass = getJavaClass<String>()
println("Java class for String: $stringJavaClass") // 输出 "Java class for String: class java.lang.String"
}

在这个示例中,我们定义了两个内联函数,getClassgetJavaClass,它们都接受一个reified类型参数TgetClass函数返回类型参数对应的KClass对象,而getJavaClass函数返回类型参数对应的Java类。在main函数中,我们用String类型参数调用这两个函数,并输出结果。


注意事项


需要注意的是,reified关键字不能用于非内联函数,因为它们的类型参数在运行时会被擦除。此外,reified类型参数不能用于普通类和接口,只能用于内联函数。


总结


Kotlin中的reified关键字允许我们在内联函数中访问类型参数的具体类型。它在需要访问泛型类型参数的场景中非常有用,例如在反射操作中。本文通过实例介绍了如何使用reified关键字,并讨论了相关注意事项。希望这些示例能够帮助您更好地理解和应用reified关键字。


作者:就不呵呵呵
链接:https://juejin.cn/post/7225457156816355365
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

终于可以彻底告别手写正则表达式了

大家好,我是风筝 这篇文章的目的是让你能得到完美的正则表达式,而且还不用自己拼。 说到正则表达式,一直是令我头疼的问题,这家伙一般时候用不到,等用到的时候发现它的规则是一点儿也记不住,\d表示一个数字,\s表示包括下划线在内的任意单词字符,也就是 [A-Za-...
继续阅读 »

大家好,我是风筝


这篇文章的目的是让你能得到完美的正则表达式,而且还不用自己拼。


说到正则表达式,一直是令我头疼的问题,这家伙一般时候用不到,等用到的时候发现它的规则是一点儿也记不住,\d表示一个数字,\s表示包括下划线在内的任意单词字符,也就是 [A-Za-z0-9_],还有[\s\S]*可以匹配包括换行在内的任意字符串。



这你都能记住吗,如果能的话,那真的佩服,反正我是记不住,之前每次手写的时候都得跟查字典似的一个个的查,简单的还好,复杂的就很痛苦了。



过程往往是这个样子的:


1、 先打开 Google,搜索一篇正则表达式,找到一份像上图那样的字典教程,先看个几分钟,回忆回忆,还有可能回忆不起来。


2、然后就开始根据需求写一个正则表达式。


3、放到程序中执行一下。


4、诶,怎么不好用,匹配不上啊,接着修改正则。


5、继续从 3 - 4 的循环,直到运气来了,正常出结果了。


这是最早的时候,真的是全靠那点仅有的实力和运气了。


记得刚毕业不久的时候,有一次领导给安排一个任务,要在一堆 PDF 文件里把我们需要的数据摘出来。PDF 这玩意儿吧,你把它的内容读出来,它就是一大段文本,要在这一堆内容不一致的文件中准确的拿到数据,第一反应就是用正则。


当时的做法就是上面的 1-5这几步来的,加上当时候刚毕业比较菜,跌跌撞撞才把程序写好,中间有几次调试的时候,程序一跑起来,VS(Visual Studio)就特别卡。对的,就是宇宙第一强大的 IDE ,当时我还在写 C#,纵然是宇宙第一强大,也被我弄的特别卡。


当时只道是正则写的有问题,然后就一直改。


后来才知道,那是因为正则写的不合理,发生了回溯现象,越不合理,回溯越严重,加上当时的 PDF 内容很多,所以导致开发工具都卡了,这要是整到线上,那怕是混不下去了。


关于回溯的问题,可以参考下面这篇文章《失控的正则表达式:灾难性的回溯》


http://www.regular-expressions.info/catastrophi…



后来就不至于那么菜了,知道了一些关于正则表达式的在线网站,上面有一些常用的正则表达式,不用自己捣鼓了,能偷懒当然要偷懒了。可以在 Goolge 上搜索关键词「正则表达式 在线」,然后就会出来一大堆,直接在上面用那些常用的正则,例如手机号、邮箱、网址啊,基本上能解决90%的需求场景。


另外的10%呢,以前可能只能自己琢磨了,现在都2023年了,基本上99%的概率都不用亲自动手了,当然了,如果是大佬呢,就想自己写,那完全没问题。


ChatGPT 完美解决


ChatGPT 是LLM(大语言模型)的产品,最最擅长的事情就是分析语言,而正则表达式的应用场景是什么呢,其实就是在一大堆文本语言中按照我们的规则,找到我们需要的内容,总的来说,也是对于文本语言的处理,所以用 ChatGPT 解决正则表达式的问题简直太合适不过了。


比如最简单的,匹配中国的手机号,直接让 ChatGPT 把正则写出来,而且连代码都给你写好了。



至于网址、邮箱等等也不在话下了。


不仅ChatGPT 可以,连百度文心一言也可以。百度文心一言虽然这样可以,但是如果你反过来问它,它就蒙圈了。


比如我问 aaa@126.com 是不是一个合法的邮箱,ChatGPT 会告诉你这个邮箱是合法的,但是百度文心一言就不行了。


下面这个是 ChatGPT 的回答:


ChatGPT 的回答


下面这个是百度文心一言的回答:


文心一言的回答


不仅邮箱不行,你问它一个手机号是否合法,百度文心一言也不行,还会告诉你这个号码的归属地,但是这个归属地也是错误的。


这样就看出来什么是智能,什么是大数据了,明显 ChatGPT 更智能一点,希望国产的大模型能在这两年追上吧。


再举一个例子


匹配一段 HTML 中的某个部分也是正则的常用场景,做过爬虫的或多或少都用过正则吧。


比如我在一大段 HTML 中有这么一部分


<div class="time">这是一个,this is some</div>

现在要拿到这个 div 中的内容部分,当然有很多其他的方式了,比如 Java 版的 jsoup,使用 xpath、css selector 等都可以,但是如果就要用正则呢,是不是自己写的话,一般菜鸟就感觉很麻烦了。


这时候我们问问 ChatGTP ,看看它怎么搞的。


直接就这么问了:



<div> <div> <div>这是一个,this is some</div> <div>button</div> </div> </div>, 用 Java 正则表达式匹配这段 HTML 中 的这个标签的 Text 部分



image-20230418224312067


直接拿过代码跑一下,没有任何问题。


有同学说了,这么明显的标签,还用的着 ChatGPT ,直接拿过来就写了。


这里只是举个例子,如果哪位有比较复杂的匹配逻辑,也可以用ChatGPT 来试试,基本上99%都能直接解决。


还有一个网站很厉害


如果你没有办法或者不想用 ChatGPT ,也不想用百度文心一言这些,我还发现一个网站,这个网站我严重怀疑它已经接入了 ChatGPT ,它也支持通过自然语言描述,就能给出相应的正则表达式。


网站地址:wangwl.net/static/proj…


比如我跟他说:提取一段字符串中的中国手机号码部分,而且还有正则可视化。



上面的那个匹配 HTML 的例子,我也在这个网站上试过,结果也是可以的。


纯粹的好东西分享,我跟这个网站没有任何关系。


一个帮你分析正则的网站


接下来这个网站呢,如果你想对正则有比较深入的理解,或者想看看自己写好的正则或ChatGPT 帮你生成的正则表达式效果怎么样,性能好不好,都可以在这个网站进行。


网站地址:regex101.com/



网站左侧可以选择你的目标语言,也就是你的代码实现是哪种语言 Java 还是 JavaScript 等。


中间上方是正则表达式,中间下方是待匹配的内容。


右侧上方是你写的正则对待匹配内容完整的匹配分析过程,非常详细,可以通过这里清楚的看出这个正则匹配的时候经过了哪些路径。


右侧下方是最终的匹配结果。


如果你写的正则在工作的时候发生了明显的回溯,这里也会给出提示,告诉你问题,让你去优化。


总结


君子善假于物也,虽然我很菜,但是工具好用啊,我+好用的工具,等于我也很厉害了。


作者:古时的风筝
链接:https://juejin.cn/post/7226013021842948156
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

超有用的Android开发技巧:拦截界面View创建

本篇文章主要是分析如何拦截Activity中View的创建流程,实现无感知的使用自定义View替换指定的系统View,这对于换肤、埋点设计等等将是非常有帮助的一种方式。 LayoutInflater.Factory2是个啥? Activity内界面的创建是由...
继续阅读 »

本篇文章主要是分析如何拦截ActivityView的创建流程,实现无感知的使用自定义View替换指定的系统View,这对于换肤、埋点设计等等将是非常有帮助的一种方式。



LayoutInflater.Factory2是个啥?


Activity内界面的创建是由LayoutInflater负责,LayoutInflater最终会交给内部的一个类型为LayoutInflater.Factory2factory2成员变量进行创建。


这个属性值可以外部自定义传入,默认的实现类为AppCompatDelegateImpl


image.png


然后在AppCompatActivity的初始化构造方法中向LayoutInflater注入AppCompatDelegateImpl:


image.png


image.png


image.png


常见的ImageViewTextView被替换成AppcompatImageViewAppCompatTextView等就是借助AppCompatDelegateImpl进行实现的。


这里有个实现的小细节,在initDelegate()方法中,调用了addOnContextAvailableListener()方法传入一个监听事件实现的factory2注入,这个addOnContextAvailableListener()方法有什么魅力呢?


addOnContextAvailableListener()是干啥用的?


咱们先看下这个方法是干啥用的:


image.png


image.png


最终是将这个监听对象加入到了ContextAwareHelper类的内部mListeners集合中,咱们接下里看下这个监听对象集合最终是在哪里被调用的。


image.png


image.png


可以看到,这个集合最终在ComponetActivityonCreate()方法中调用,请注意,这个调用时机还是在父类的super.onCreate()方法前进行调用的。


所以我们可以得出结论,addOnContextAvailableListener()添加的监听器将在父类onCreate()方法前进行调用。


这个用处的场景还是比较多的,比如我们设置Activity的主题就必须在父类的onCreate()方法前调用,借助这个监听,可以轻松实现。


代码实战



请注意,这个factory2的设置必须在ActivityonCreate()方法前调用,所以我们可以直接借助addOnContextAvailableListener()进行实现,也可以重写onCreate()方法在指定位置实现。当然了,前者更加的灵活,这里我们还是以后者进行举例。



override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {

return if (name == "要替换的系统View名称") {
CustumeView()
} else delegate.createView(parent, name, context, attrs)
}
})
}

请注意,这里也有一个实现的小细节,如果当某个系统View不属于我们要替换的View,请继续委托给AppCompatDelegateImpl进行处理,这样就保证了实现系统组件特有功能的前提下,又能完成我们的View替换工作。


统一所有界面View的替换工作


如果要替换View的界面非常多,一个Activity一个Activity替换过去太麻烦 ,这个时候就可以使用我们经常使用到的ApplicationregisterActivityLifecycleCallbacks()监听所有Activity的创建流程,其中我们用到的方法就是onActivityPreCreated():


registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(activity.layoutInflater, object : LayoutInflater.Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {

return if (name == "要替换的系统View名称") {
CustumeView()
} else (activity as? AppCompatActivity)?.delegate?.createView(parent, name, context, attrs) ?: null
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
TODO("Not yet implemented")
}
})

}
}

不过这个Application.ActivityLifecycleCallbacks接口要重写好多无用的方法,太麻烦了,之前写过一篇关于接口优化相关的文章吗,详情可以参考:接口使用额外重写的无关方法太多?优化它


总结


之前看过很多换肤、埋点统计上报等相关文章,多多少少都介绍了向AppCompatActivity中注入factory2拦截系统View创建的思想,我们设置还可以借助此实现界面黑白化的效果,非常的好用,每个开发者都应该去了解掌握的知识点。


作者:长安皈故里
链接:https://juejin.cn/post/7137305357415612452
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Transform API 废弃了,路由插件怎么办?

前言 在 AGP 7.2 中,谷歌废弃了Android开发过程非常常用的Transform API,具体信息可以查看Android Gradle 插件 API 更新。 可以看到Transform API在 AGP 7.2 标记了废弃,且在 AGP 8.0 将...
继续阅读 »

前言


在 AGP 7.2 中,谷歌废弃了Android开发过程非常常用的Transform API,具体信息可以查看Android Gradle 插件 API 更新


image.png


可以看到Transform API在 AGP 7.2 标记了废弃,且在 AGP 8.0 将面临移除的命运。如果你将Android工程的AGP升级到7.2.+,尝试运行使用了Transform API的插件的项目时,将得到以下警告。


API 'android.registerTransform' is obsolete.
It will be removed in version 8.0 of the Android Gradle plugin.
The Transform API is removed to improve build performance. Projects that use the
Transform API force the Android Gradle plugin to use a less optimized flow for the
build that can result in large regressions in build times. It’s also difficult to
use the Transform API and combine it with other Gradle features; the replacement
APIs aim to make it easier to extend the build without introducing performance or
correctness issues.

There is no single replacement for the Transform API—there are new, targeted
APIs for each use case. All the replacement APIs are in the
`androidComponents {}` block.
For more information, see https://developer.android.com/studio/releases/gradle-plugin-api-updates#transform-api.
REASON: Called from: /Users/l3gacy/AndroidStudioProjects/Router/app/build.gradle:15
WARNING: Debugging obsolete API calls can take time during configuration. It's recommended to not keep it on at all times.

看到这种情况,相信很多人第一反应都是how old are you?。Gradle API的频繁变动相信写过插件的人都深受其害,又不是一天两天了。业界一些解决方案大都采用封装隔离来最小化Gradle API的变动。常见的如



此次 Transform API 将在 AGP 8.0 移除,这一改动对于目前一些常用的类库、插件都将面临一个适配的问题,常见的如路由、服务注册、字符串加密等插件都广泛使用了Transform API。那么究竟该怎么解决此类适配问题找到平替方案呢?本篇将探讨目前主流的一些观点是否能够满足需求以及如何真正的做到适配。


主流观点



当你尝试解决此问题时,一通检索基本会得到两种不同的见解,目前也有一些同学针对这两个API进行了探索。




那么上述提到的两种API是否真的就能解决我们的问题呢?其实行也不行!


AsmClassVisitorFactory



首先来看看AsmClassVisitorFactory



AsmClassVisitorFactory是没有办法做到像Transform一样,先扫描所有class收集结果,再执行ASM修改字节码。原因是AsmClassVisitorFactoryisInstrumentable方法中确定需要对哪些class进行ASM操作,当返回true之后,就执行了createClassVisitor方法进行字节码操作去了,这就导致可能你路由表都还没收集完成就去修改了目标class


机灵的小伙伴可能会想,那我再注册一个收集路由表的AsmClassVisitorFactory,然后在注册一个真正执行ASM操作的AsmClassVisitorFactory不就好了,那么这种做法可以吗,其实在你的插件想适配Transform Action? 可能还早了点 - 掘金这边文章里已经给出了答案。


TransformAction



既然 AsmClassVisitorFactory 不能打,那 TransformAction 能打不,我们来看下AGP中的实现。




可以看到是有相关ASM实现的。TransformAction 的应用目前较少,主要常见的有 JetifyTransformAarTransform等,主要做产物转换。但 TransformAction 操作起来比较麻烦,详细可以看Transforming dependency artifacts on resolution


平替方案



既然两种观点,一个不能打,一个嫌麻烦,那有没有简单既易用,又可少量修改即可完成适配的方案呢,答案当然是有了。不然水本篇就没有意义了。那么本篇就带大家来简单探索下 Transform API的废弃,针对路由类库的插件适配的一种平替方案。



首先我们要知道Transform在Gradle中其实也对应一个Task,只是有点特殊。我们来看下定义:


public abstract class Transform {

··· omit code ···

public void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
}

public void transform(@NonNull TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// Just delegate to old method, for code that uses the old API.
//noinspection deprecation
transform(transformInvocation.getContext(), transformInvocation.getInputs(),
transformInvocation.getReferencedInputs(),
transformInvocation.getOutputProvider(),
transformInvocation.isIncremental());
}

··· omit code ···
}

看到这里,有些同学就要疑问了。你这不扯淡吗,Transform根本没有继承 DefaultTaskAbstractTask或者实现 Task 接口。你怎么断定Transform本质上也是一个GradleTask呢?这部分完全可以由Gradle的源码里找到答案,这里不赘述了。


Plugin



回到正题。究竟该怎么去使用Task去适配呢?我们先用伪代码来简要说明下。



class RouterPlugin : Plugin<Project> {

override fun apply(project: Project) {

··· omit code ···

with(project) {

··· omit code ···

plugins.withType(AppPlugin::class.java) {
val androidComponents =
extensions.findByType(AndroidComponentsExtension::class.java)
androidComponents?.onVariants { variant ->
val name = "gather${variant.name.capitalize(Locale.ROOT)}RouteTables"
val taskProvider = tasks.register<RouterClassesTask>(name) {
group = "route"
description = "Generate route tables for ${variant.name}"
bootClasspath.set(androidComponents.sdkComponents.bootClasspath)
classpath = variant.compileClasspath
}
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProvider)
.toTransform(
ScopedArtifact.CLASSES,
RouterClassesTask::jars,
RouterClassesTask::dirs,
RouterClassesTask::output,
)
}
}
}

··· omit code ···
}
}

我们使用了onVariants API 注册了一个名为gather[Debug|Release]RouteTablesTask,返回一个TaskProvider对象,然后使用variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)来使用这个Task进行toTransform操作,可以发现我们无需手动执行该Task。来看下这个toTransform的定义。


注意ScopedArtifacts需要AGP 7.4.+以上才支持


/**
* Defines all possible operations on a [ScopedArtifact] artifact type.
*
* Depending on the scope, inputs may contain a mix of [org.gradle.api.file.FileCollection],
* [RegularFile] or [Directory] so all [Task] consuming the current value of the artifact must
* provide two input fields that will contain the list of [RegularFile] and [Directory].
*
*/
interface ScopedArtifactsOperation<T: Task> {

/**
* Append a new [FileSystemLocation] (basically, either a [Directory] or a [RegularFile]) to
* the artifact type referenced by [to]
*
* @param to the [ScopedArtifact] to add the [with] to.
* @param with lambda that returns the [Property] used by the [Task] to save the appended
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toAppend(
to: ScopedArtifact,
with: (T) -> Property<out FileSystemLocation>,
)

/**
* Set the final version of the [type] artifact to the input fields of the [Task] [T].
* Those input fields should be annotated with [org.gradle.api.tasks.InputFiles] for Gradle to
* property set the task dependency.
*
* @param type the [ScopedArtifact] to obtain the final value of.
* @param inputJars lambda that returns a [ListProperty] or [RegularFile] that will be used to
* set all incoming files for this artifact type.
* @param inputDirectories lambda that returns a [ListProperty] or [Directory] that will be used
* to set all incoming directories for this artifact type.
*/
fun toGet(
type: ScopedArtifact,
inputJars: (T) -> ListProperty<RegularFile>,
inputDirectories: (T) -> ListProperty<Directory>)

/**
* Transform the current version of the [type] artifact into a new version. The order in which
* the transforms are applied is directly set by the order of this method call. First come,
* first served, last one provides the final version of the artifacts.
*
* @param type the [ScopedArtifact] to transform.
* @param inputJars lambda that returns a [ListProperty] or [RegularFile] that will be used to
* set all incoming files for this artifact type.
* @param inputDirectories lambda that returns a [ListProperty] or [Directory] that will be used
* to set all incoming directories for this artifact type.
* @param into lambda that returns the [Property] used by the [Task] to save the transformed
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toTransform(
type: ScopedArtifact,
inputJars: (T) -> ListProperty<RegularFile>,
inputDirectories: (T) -> ListProperty<Directory>,
into: (T) -> RegularFileProperty)

/**
* Transform the current version of the [type] artifact into a new version. The order in which
* the replace [Task]s are applied is directly set by the order of this method call. Last one
* wins and none of the previously set append/transform/replace registered [Task]s will be
* invoked since this [Task] [T] replace the final version.
*
* @param type the [ScopedArtifact] to replace.
* @param into lambda that returns the [Property] used by the [Task] to save the replaced
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toReplace(
type: ScopedArtifact,
into: (T) -> RegularFileProperty
)
}

可以看到不光有toTransform,还有toAppendtoGettoReplace等操作,这部分具体用法和案例感兴趣的同学可以自行尝试。接下来来看看Task中的简要代码


Task


abstract class RouterClassesTask : DefaultTask() {

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val jars: ListProperty<RegularFile>

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val dirs: ListProperty<Directory>

@get:OutputFile
abstract val output: RegularFileProperty

@get:Classpath
abstract val bootClasspath: ListProperty<RegularFile>

@get:CompileClasspath
abstract var classpath: FileCollection

@TaskAction
fun taskAction() {
// 输入的 jar、aar、源码
val inputs = (jars.get() + dirs.get()).map { it.asFile.toPath() }
// 系统依赖
val classpaths = bootClasspath.get().map { it.asFile.toPath() }
.toSet() + classpath.files.map { it.toPath() }

··· omit code ···

JarOutputStream(BufferedOutputStream(FileOutputStream(output.get().asFile))).use { jarOutput ->

jars.get().forEach { file ->
Log.d("handling jars:" + file.asFile.absolutePath)
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.isDirectory.not() &&
// 针对需要字节码修改的class进行匹配
jarEntry.name.contains("com/xxx/xxx/xxx", true)
) {
// ASM 自己的操作
} else {
// 不处理,直接拷贝自身到输出
}
jarOutput.closeEntry()
}
jarFile.close()
}

··· omit code ···
}
}

··· omit code ···

}

看完了伪代码,相信很多同学已经知道该怎么做了。那么我们再来个简单🌰来看下我们如何适配现有的路由插件。


在开始之前,我们要知道主流的路由插件使用Transform主要是干了啥,简单概括下其实就是两大步骤:



  • 扫描依赖,收集路由表使用容器存储结果

  • 根据收集到的路由表修改字节码进行路由注册


前面的伪代码其实也是按照这两大步来做的。


示例


chenenyu/Router 为例我们来具体实现以下,至于其他类似库如:alibaba/ARouter 操作方法也类似,这部分工作就留给其他说话又好听的同学去做了。


期望结果


chenenyu/Router需要进行ASM字节码操作的类是com.chenenyu.router.AptHub,这里仅以chenenyu/RouterSample进行演示。我们先看一下使用Transform进行字节码修改后的代码是什么,可以看到通过Gradle动态的新增了一个静态代码块,里面注册了各个module的路由表、拦截器表、路由拦截器映射表等。


static {
HashMap hashMap = new HashMap();
routeTable = hashMap;
HashMap hashMap2 = new HashMap();
interceptorTable = hashMap2;
LinkedHashMap linkedHashMap = new LinkedHashMap();
targetInterceptorsTable = linkedHashMap;
new Module1RouteTable().handle(hashMap);
new Module2RouteTable().handle(hashMap);
new AppRouteTable().handle(hashMap);
new AppInterceptorTable().handle(hashMap2);
new AppTargetInterceptorsTable().handle(linkedHashMap);
}

Plugin



  1. RouterPlugin代码与伪代码基本一致


Task


RouterClassesTask大部分实现与伪代码也相同。这里我们主要以说明用法为主,相应的接口设计以及优化不做处理。通俗点说,就是代码将就看~


abstract class RouterClassesTask : DefaultTask() {

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val jars: ListProperty<RegularFile>

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val dirs: ListProperty<Directory>

@get:OutputFile
abstract val output: RegularFileProperty

@get:Classpath
abstract val bootClasspath: ListProperty<RegularFile>

@get:CompileClasspath
abstract var classpath: FileCollection

@TaskAction
fun taskAction() {
val inputs = (jars.get() + dirs.get()).map { it.asFile.toPath() }
val classpaths = bootClasspath.get().map { it.asFile.toPath() }
.toSet() + classpath.files.map { it.toPath() }
val grip: Grip = GripFactory.newInstance(Opcodes.ASM9).create(classpaths + inputs)
val query = grip select classes from inputs where interfaces { _, interfaces ->
descriptors.map(::getType).any(interfaces::contains)
}
val classes = query.execute().classes

val map = classes.groupBy({ it.interfaces.first().className.separator() },
{ it.name.separator() })

Log.v(map)

JarOutputStream(BufferedOutputStream(FileOutputStream(output.get().asFile))).use { jarOutput ->

jars.get().forEach { file ->
println("handling jars:" + file.asFile.absolutePath)
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.isDirectory.not() &&
jarEntry.name.contains("com/chenenyu/router/AptHub", true)
) {
println("Adding from jar ${jarEntry.name}")
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
val reader = ClassReader(it)
val writer = ClassWriter(reader, 0)
val visitor =
RouterClassVisitor(writer, map.mapValues { v -> v.value.toSet() })
reader.accept(visitor, 0)
jarOutput.write(writer.toByteArray())
}
} else {
kotlin.runCatching {
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
it.copyTo(jarOutput)
}
}
}
jarOutput.closeEntry()
}
jarFile.close()
}

dirs.get().forEach { directory ->
println("handling " + directory.asFile.absolutePath)
directory.asFile.walk().forEach { file ->
if (file.isFile) {
val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
println("Adding from directory ${relativePath.replace(File.separatorChar, '/')}")
jarOutput.putNextEntry(JarEntry(relativePath.replace(File.separatorChar, '/')))
file.inputStream().use { inputStream ->
inputStream.copyTo(jarOutput)
}
jarOutput.closeEntry()
}
}
}
}
}

companion object {
@Suppress("SpellCheckingInspection")
val descriptors = listOf(
"Lcom/chenenyu/router/template/RouteTable;",
"Lcom/chenenyu/router/template/InterceptorTable;",
"Lcom/chenenyu/router/template/TargetInterceptorsTable;"
)
}

}

需要额外说明一下的是,一般我们进行路由表收集的工作都是扫描所有classesjarsaars,找到匹配条件的class即可,这里我们引入了一个com.joom.grip:grip:0.9.1依赖,能够像写SQL语句一样帮助我们快速查询字节码。感兴趣的可以详细了解下grip的用法。



  1. 这里我们把所有依赖产物作为Input输入,然后创建grip对象。


val inputs = (jars.get() + dirs.get()).map { it.asFile.toPath() }
val classpaths = bootClasspath.get().map { it.asFile.toPath() }
.toSet() + classpath.files.map { it.toPath() }

val grip: Grip = GripFactory.newInstance(Opcodes.ASM9).create(classpaths + inputs)


  1. 查询所有满足特定描述符的类。


val query = grip select classes from inputs where interfaces { _, interfaces ->
descriptors.map(::getType).any(interfaces::contains)
}
val classes = query.execute().classes


  1. 对查询的结果集进行分类,组装成ASM需要处理的数据源。标注的separator扩展函数是由于字节码描述符中使用/,在ASM操作中需要处理为.


val map = classes.groupBy({ it.interfaces.first().className.separator() }, { it.name.separator() })

通过打印日志,可以看到路由表已经收集完成。




  1. 至此几行简单的代码即实现了字节码的收集工作,然后把上面的map集合直接交给ASM去处理。ASM的操作可以沿用之前的ClassVisitorMethodVisitor,甚至代码都无需改动。至于ASM的操作代码该如何编写,这个不在本篇的讨论范围。由于我们需要修改字节码的类肯定位于某个jar中,所以我们直接针对输入的jars进行编译,然后根据特定条件过滤出目标字节码进行操作。


JarOutputStream(BufferedOutputStream(FileOutputStream(output.get().asFile))).use { jarOutput ->

jars.get().forEach { file ->
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.isDirectory.not() &&
jarEntry.name.contains("com/chenenyu/router/AptHub", true)
) {
println("Adding from jar ${jarEntry.name}")
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
val reader = ClassReader(it)
val writer = ClassWriter(reader, 0)
val visitor =
RouterClassVisitor(writer, map.mapValues { v -> v.value.toSet() })
reader.accept(visitor, 0)
jarOutput.write(writer.toByteArray())
}
} else {
kotlin.runCatching {
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
it.copyTo(jarOutput)
}
}
}
jarOutput.closeEntry()
}
jarFile.close()
}

dirs.get().forEach { directory ->
println("handling " + directory.asFile.absolutePath)
directory.asFile.walk().forEach { file ->
if (file.isFile) {
val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
println("Adding from directory ${relativePath.replace(File.separatorChar, '/')}")
jarOutput.putNextEntry(JarEntry(relativePath.replace(File.separatorChar, '/')))
file.inputStream().use { inputStream ->
inputStream.copyTo(jarOutput)
}
jarOutput.closeEntry()
}
}
}
}

所有需要修改的代码已经写完了,是不是很简单。最后我们来验证下是否正常。执行编译后发现字节码修改成功,且与Transform执行结果一致。至此,基本完成了功能适配工作。



总结



  • 本篇通过一些伪代码对适配 AGP 7.4.+ 的 Transform API 进行了简单说明,并通过一个示例进行了实践。

  • 实践证明,对于 Transform API 的废弃,此方案简单可用,但此方案存在一定限制,需AGP 7.4.+。

  • 相比较于TransformAction,迁移成本小且支持增量编译、缓存


示例代码已上传至Router,有需要的请查看v1.7.6分支代码。主要代码在RouterPluginRouterClassesTask


作者:5upport
链接:https://juejin.cn/post/7222091234100330554
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

中小型项目统一处理请求重复提交

请求重复提交的危害 数据重复:例如用户重复提交表单,造成数据重复。 资源浪费:多次重复请求提交将会浪费服务器的处理资源。但这个相比数据重复的危害性较小。 不一致性:假设我们触发请求增加用户的积分500,如果多次触发这个请求,积分是累加的。这个危害性比重复的数...
继续阅读 »

请求重复提交的危害



  • 数据重复:例如用户重复提交表单,造成数据重复。

  • 资源浪费:多次重复请求提交将会浪费服务器的处理资源。但这个相比数据重复的危害性较小。

  • 不一致性:假设我们触发请求增加用户的积分500,如果多次触发这个请求,积分是累加的。这个危害性比重复的数据更大。

  • 安全性:例如我们在登录页面触发手机验证码的发送请求。频繁触发这个请求将会耗费我们的验证码成本。


防请求重复提交的方案


前端



  • 在用户第一次点击按钮后,即禁用提交按钮。

  • 限制用户提交请求间隔,在一定的时间间隔内只允许用户发起某个请求一次。

  • 在表单提交前,检查前一次请求是否提交成功,已成功的话则提示用户无需再重复提交。


后端



  • 严谨的做法

    • Token机制,在每一个请求中都添加一个Token。Token由服务端生成并发放给前端。服务端接收到请求时,根据Token进行校验。看这个Token是否已被使用。(一般基于缓存)

    • 唯一标志,比如在创建订单的时候,即生成一个唯一的订单号,并将其作为订单的唯一标识。在后续的请求中携带该订单号。当收到订单创建请求时,检查订单号是否已经存在。(一般基于数据库)



  • 非严谨的做法

    • 后端拦截请求,检查请求的用户和参数是否和上次请求相同,相同的话即为重复请求。




这种防请求重复提交的实现有基于Filter的实现,也有基于HandlerInterceptor的实现。最后考量下笔者认为利用RequestBodyAdviceAdapter类来实现代码实现更加简洁,配置更加简单。


在此笔者提供一个注解+RequestBodyAdviceAdapter配合使用的防重复提交的实现。
但是这个方案有个小弊端。仅生效于有RequestBody注解的参数,因为使用RequestBodyAdvice来实现。但是大部分我们需要做请求防重复提交的接口一般都是POST请求,且有requestBody。


完整实现在开源项目中:github.com/valarchie/A…


实现


声明注解


/**
* 自定义注解防止表单重复提交
* 仅生效于有RequestBody注解的参数 因为使用RequestBodyAdvice来实现
* @author valarchie
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {

/**
* 间隔时间(s),小于此时间视为重复提交
*/
int interval() default 5;

}

继承RequestBodyAdviceAdapter实现ResubmitInterceptor


大致的实现是。



  • 覆写了supports方法,指明我们仅处理拥有Resubmit注解的方法。

  • 生成每一个请求的签名作为Key。key的生成由generateResubmitRedisKey方法实现。格式如下:resubmit:{}:{}:{}。比如用户是userA。我们请求的类是UserService。方法名是addUser。则这个key为resubmit:userA:UserService:addUser

  • 将Key和请求的参数作为值存到redis当中去

  • 每一次请求过来时,我们检查缓存中这个请求的签名对应的参数是否相同,相同的话即为重复请求。


/**
* 重复提交拦截器 如果涉及前后端加解密的话 也可以通过继承RequestBodyAdvice来实现
*
* @author valarchie
*/
@ControllerAdvice(basePackages = "com.agileboot")
@Slf4j
@RequiredArgsConstructor
public class ResubmitInterceptor extends RequestBodyAdviceAdapter {

public static final String NO_LOGIN = "Anonymous";
public static final String RESUBMIT_REDIS_KEY = "resubmit:{}:{}:{}";

@NonNull
private RedisUtil redisUtil;

@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Resubmit.class);
}

/**
* @param body 仅获取有RequestBody注解的参数
*/
@NotNull
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 仅获取有RequestBody注解的参数
String currentRequest = JSONUtil.toJsonStr(body);

Resubmit resubmitAnno = parameter.getMethodAnnotation(Resubmit.class);
if (resubmitAnno != null) {
String redisKey = generateResubmitRedisKey(parameter.getMethod());

log.info("请求重复提交拦截,当前key:{}, 当前参数:{}", redisKey, currentRequest);

String preRequest = redisUtil.getCacheObject(redisKey);
if (preRequest != null) {
boolean isSameRequest = Objects.equals(currentRequest, preRequest);

if (isSameRequest) {
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);
}
}
redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);
}

return body;
}

public String generateResubmitRedisKey(Method method) {
String username;

try {
LoginUser loginUser = AuthenticationUtils.getLoginUser();
username = loginUser.getUsername();
} catch (Exception e) {
username = NO_LOGIN;
}

return StrUtil.format(RESUBMIT_REDIS_KEY,
method.getDeclaringClass().getName(),
method.getName(),
username);
}
}

使用


通过在Controller上打上Resubmit注解即可,interval即多久的间隔内相同参数视为重复请求。


/**
* 新增通知公告
*/
@Resubmit(interval = 60)
@PostMapping
public ResponseDTO<Void> add(@RequestBody NoticeAddCommand addCommand) {
noticeApplicationService.addNotice(addCommand);
return ResponseDTO.ok();
}

这是笔者关于中小型项目防请求重复提交的实现,如有不足欢迎大家评论指正。


作者:CoderV的进阶笔记
链接:https://juejin.cn/post/7226322524496953399
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 13 平行视界 ActivityEmbedding详解

背景 Android 13推出了一个大屏幕设备显示方案:Activity嵌入(Activity Embedding)。该功能不同于分屏模式(将多个应用同时显示在屏幕上),而是类似华为平行视界将同一个应用的多个不同Activity同时显示到屏幕上。 本文结合An...
继续阅读 »

背景


Android 13推出了一个大屏幕设备显示方案:Activity嵌入(Activity Embedding)。该功能不同于分屏模式(将多个应用同时显示在屏幕上),而是类似华为平行视界将同一个应用的多个不同Activity同时显示到屏幕上。


本文结合Android 13窗口架构,介绍Activity Embedding的创建流程。


image.png


image.png


窗口模型与Activity Embedding平行视界


Actiivty Embedding的实现流程与Android 13的窗口模型紧密相关。最简单最抽象的情况下,窗口对象之间的关系如下:


image.png


简单来说,Activity所在的Task,是以DisplayArea的形式在整个层级中组织管理的。代表Activity节点的ActivityRecord作为Task的child来管理,Task则由TaskDisplayArea来管理。


用层级结构来看非常直观。下图为常规情况(未进入Activity Embedding平行视界的情况)的层级描述:


image.png


简单解释一下这个层级,该Task位于一个DisplayArea,并以Activity栈的形式管理多个Activity,Activity管理WindowState。其中“V“图标含义是”可见“。


而进入平行视界的分屏模式时,两个Activity将同时显示,而Task本身不提供这个能力,而是由TaskFragment来实现。TaskFragment插入到Task和Activity之间,分割了Task在屏幕上的显示区域,提供给平行视界的两个Activity:


image.png


进入平行视界的层级,可见Task不再管理Actiivty栈,而被TaskFragment取代。


因此,TaskFragment是实现Activity Embedding平行视界的关键。进入平行视界实际上也就是创建TaskFragment并排布位置。


平行视界的一大特征是,Configuration有专属的WindowingMode、MaxBounds不等于Bounds。


image.png


Activity Embedding的两个Activity是独立绘制的:


image.png


Activity Embedding平行视界的流程


Activity Embedding的应用层经过封装,使用相对简单,并且可以根据配置的分屏规则自动完成分屏。


其中关键的两个技术点,自动完成分屏、执行分配操作分别依靠Instrumentation.ActivityMonitor和android.window.ITaskFragmentOrganizerController这个Binder来实现。当新的Activity启动时,ActivityMonitor.onActivityStart()被回调,判断需要启动平行视界时,自动调用ITaskFragmentOrganizerController,实现自动进入分屏。


ITaskFragmentOrganizerController


进入Activity Embedding分屏的关键接口为ITaskFragmentOrganizerController.applyTransaction(),该接口在WindowManagerService中由WindowOrganizerController实现


该方法主要处理Transaction,分为两大类类:Change、HierachyOps。对于平行视界的进入流程,为HierachyOps,简称为HOPs。


通过Transaction为左右两个Activity创建平行视界


创建平行视界的流程封装到了Transaction内,作为HOPs进行处理,保存到Transaction.hops(ArrayLists)内。


创建一个平行视界,实际上分为4步走:1. 创建左边Activity的TaskFragment,设置Configuration内的Bounds属性为左边的平行视界面积大小,并添加到Task内;2. 将左边Activity添加到TaskFragment内;3. 重复第一步,只不过创建的是右边Activity的Fragment;4. 重复第2步,只是目标是右边的Activity。


对应到源码,分别为HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT、HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT、HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT、HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS。


image.png


image.png


image.png


image.png


创建的各个参数,关键点为initialBounds属性记录的Rect,它确定了TaskFragment的大小和位置,最终确定了平行视界左右两个Activity的位置和大小。


添加完成后,会按照常规方式触发窗口的relayout和绘制,将平行视界的内容显示到屏幕上。


作者:飞起来_飞过来
链接:https://juejin.cn/post/7225914608552181821
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

5分钟速通Kotlin委托

1、什么是委托? 委托,又叫委托模式是一种常用的设计模式,它可以让一个对象在不改变自己原有的行为的前提下,将某些特定的行为委托给另一个对象来实现。它通过将对象之间的关系分离,可以降低系统的耦合度,提高代码的复用性和可维护性。 其中有三个角色,约束、委托对象和被...
继续阅读 »

1、什么是委托?


委托,又叫委托模式是一种常用的设计模式,它可以让一个对象在不改变自己原有的行为的前提下,将某些特定的行为委托给另一个对象来实现。它通过将对象之间的关系分离,可以降低系统的耦合度,提高代码的复用性和可维护性。


其中有三个角色,约束、委托对象和被委托对象。



  • 约束: 一般为接口也可以是抽象类,定义了某个行为。

  • 被委托对象: 负责执行具体的行为。

  • 委托对象: 负责将约束中定义的行为交给被委托对象。


2、Java中的委托


先来说一说委托在Java中的应用用一个简单的例子来说明:


老板在创业初期时因为只有一个人而需要负责产品的客户端UI服务器
这个时候老板负责的这些工作就可以被抽象出来形成一个约束接口:


public interface Work {
void app();
void ui();
void service();
}


public class Boss implements Work {

@Override
public void app() {
System.out.println("Boss doing app");
}

@Override
public void ui() {
System.out.println("Boss doing ui");
}

@Override
public void service() {
System.out.println("Boss doing service");
}
}

现在老板每天都在做这几件事:


public class Main {  
public static void main(String[] args) {
Boss boss = new Boss();
boss.app();
boss.ui();
boss.service();
}
}

输出:


Boss doing app
Boss doing ui
Boss doing service

运气不错,产品赚了不少钱,老板花钱雇了一个员工,将这些工作委托给他处理,自己直接脱产,只需要知道结果就可以了,于是就有了:


public class Employee implements Work{  
@Override
public void app() {
System.out.println("Employee doing app");
}

@Override
public void ui() {
System.out.println("Employee doing ui");
}

@Override
public void service() {
System.out.println("Employee doing service");
}
}


public class Boss implements Work{  
private Employee employee;

public Boss(Employee employee) {
this.employee = employee;
}

@Override
public void app() {
employee.app();
}

@Override
public void ui() {
employee.ui();
}

@Override
public void service() {
employee.service();
}
}


public class Main {  
public static void main(String[] args) {
Boss boss = new Boss(new Employee());
boss.app();
boss.ui();
boss.service();
}
}


Employee doing app
Employee doing ui
Employee doing service

这就是一个委托模式,老板委托对象)将 工作约束)委托给 员工被委托者)处理,老板并不关心每项工作具体是如何实现的,员工在完成工作后也会和老板汇报,就算这几项工作内容发生变化也只是员工需要处理。


3、Kotlin中的委托


那么针对上述的委托所描述例子在Kotlin中是如何实现的呢?


答案是使用关键字by,Kotlin专门推出了by来实现委托:
上述例子中的工作员工都不变:


interface Work {  
fun app()
fun ui()
fun service()
}


class Employee : Work {  
override fun app() {
println("Employee doing app")
}

override fun ui() {
println("Employee doing ui")
}

override fun service() {
println("Employee doing service")
}
}

老板这个类中,我们要将工作使用关键字by委托给员工


class Boss(private val employee: Employee) : Work by employee

就这么一行,实现了Java代码中老板类的效果。


fun main(args: Array<String>) {  
val boss = Boss(Employee())
boss.app()
boss.ui()
boss.service()
}

结果肯定是一样的。
那么by是如何实现Java中委托的效果的呢?通过反编译Kotlin字节码后我们看到:


public final class Boss implements Work {  
private final Employee employee;

public Boss(@NotNull Employee employee) {
Intrinsics.checkNotNullParameter(employee, "employee");
super();
this.employee = employee;
}

public void app() {
this.employee.app();
}

public void service() {
this.employee.service();
}

public void ui() {
this.employee.ui();
}
}

其实就是Java中实现委托的代码,Kotlin将它包成一个关键字by,效率大幅提升。


4、属性委托


上述说明的委托都属于类委托,而在Kotlin当中by不仅可以实现类委托,还可以实现属性委托,属性委托为Kotlin的一大特性,将对属性的访问委托给另一个对象。使用属性委托可以让我们编写更简洁、更模块化的代码,并且能够提高代码的可重用性。


4.1 如何实现属性委托?


Kotlin官方文档中给出了定义:


使用方式:val/var <属性名>: <类型> by <表达式>


在 by 后面的表达式是该 委托, 属性对应的 get() 和set()会被委托给它的 getValue() 与 setValue() 方法。 如果该属性是只读的(val)其委托只需要提供一个 getValue() 函数如果该属性是var则还需要提供 setValue()函数。例如:


   class Example {  
var str: String by Delegate()
}


    class Delegate {  
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}


fun main(args: Array<String>) {  
val p = Example()
p.str = "Hello"
println(p.str)
}

因为属性str是可变的所以在Delegate类中实现了getValue和setValue两个函数,其中一共出现了三个参数分别是



  • thisRef :读出 str 的对象

  • property :保存了对 str 自身的描述 (例如你可以取它的名字)

  • value :保存将要被赋予的值


运行结果如下:


Hello has been assigned to 'str' in Example@1ddc4ec2.
Example@1ddc4ec2, thank you for delegating 'str' to me!

我们再将Example类中的代码转为Kotlin字节码反编译得到以下代码:


 public final class Example {  
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty1(new MutablePropertyReference1Impl(Example.class, "str", "getStr()Ljava/lang/String;", 0))};
@NotNull
private final Delegate str$delegate = new Delegate();

@NotNull
public final String getStr() {
return this.str$delegate.getValue(this, $$delegatedProperties[0]);
}

public final void setStr(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.str$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}

就是创建了一个Delegate对象,再通过调用setVaule和getValue一对方法来获取和设置值的。


4.2 标准委托


在Kotlin标准库为委托提供了几种方法


4.2.1 延迟属性 Lazy


public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

首次访问属性时才进行初始化操作,lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托, 该lambda表达式将在第一次访问该属性时被调用,初始化属性并返回属性值,之后的访问将直接返回初始化后的值。


简单的例子:


fun main(args: Array<String>) {  
val str : String by lazy {
println("Hello str")
"lazy"
}
println(str)
println(str)
}

输出:


Hello str//只在第一次访问时执行
//后续访问只返回值
lazy
lazy

当我们使用 by lazy 委托实现延迟初始化时,Kotlin 编译器会生成一个私有的内部类,用于实现委托属性的懒加载逻辑,其内部包含一个名为 value 的属性,用于存储真正的属性值。同时,还会生成一个名为 isInitialized 的私有 Boolean 属性,用于标识属性是否已经初始化。


当我们首次访问被 lazy 修饰的属性时,如果它还未被初始化,就会调用 lazy 所接收的 lambda 表达式进行初始化,并将结果保存在 value 属性中。之后,每次访问该属性时,都会返回 value 中存储的属性值。


4.2.2 可观察属性 Observable


Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值执行)。它有三个参数:被赋值的属性、旧值与新值:


class User {  
var name : String by Delegates.observable("no value") {
property, oldValue, newValue ->
println("property :${property.name}, old value $oldValue -> new value $newValue")
}
}


fun main() {
val user = User()
user.name = "Alex"
user.name = "Bob"
}


property :name, old value no value -> new value Alex
property :name, old value Alex -> new value Bob

如果你想截获赋值并“否决”它们,那么使用 vetoable() 取代 observable()。 在属性被赋新值生效之前会调用传递给 vetoable 的处理程序,简单来说就是利用你设定的条件来决定设定的值是否生效,还是以上述代码为例,在User中增加一个年龄属性:


var age : Int by Delegates.vetoable(0) {  
_, oldValue, newValue ->
println("old value : $oldValue, new value : $newValue")
newValue > oldValue
}

在这里我们设定了输入的年龄大于现在的年龄才生效,运行一下看看输出什么:
0
old value : 0, new value : 20
20
old value : 20, new value : 19
20
old value : 20, new value : 25
25


0
old value : 0, new value : 20
20
old value : 20, new value : 19
20
old value : 20, new value : 25
25

4.2.3 将属性储存在映射中


映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。


class User(map: MutableMap<String, Any?>) {  
val name: String by map
val age: Int by map
}


fun main(args: Array<String>) {  
val user = User(
mutableMapOf(
"name" to "Alex",
"age" to 18
)
)
println("name : ${user.name}, age : ${user.age}")
}

输出:


name : Alex, age : 18

5、总结


委托是一种常见的软件设计模式,旨在提高代码的复用性和可维护性,在 Java 中,委托通过定义接口和实现类来实现。实现类持有接口的实例,并将接口的方法委托给实例来实现。这种方式可以实现代码的复用和解耦,但是需要手动实现接口中的方法,比较繁琐,而在 Kotlin 中,委托通过by关键字实现委托其中还包括了属性委托一大特性,Kotlin 提供了很多内置的属性委托,比如延迟属性、映射属性等。此外,Kotlin 还支持自定义属性委托。自定义属性委托需要实现 getValuesetValue 方法,用于获取和设置属性的值,与 Java 的委托相比,Kotlin 的属性委托更加方便和简洁,减少样板代码。


6、感谢



  1. 校稿:ChatGpt

  2. 文笔优化:ChatGpt


参考:Kotlin官方文档:委托 Kotlin官方文档:属性委托


作者:Otaku_尻男
链接:https://juejin.cn/post/7223258679259873317
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »