微服务不死 — 共享变量在策略引擎项目的落地详解

01

   背景


1、共享变量的提出

前段时间,来自亚马逊 Prime Video 团队的一个案例研究在开发者社区中掀起了轩然大波。大体是这样一件事,作为一个流媒体平台,Prime Video每天都会向客户提供成千上万的直播流。为了确保客户无缝接收内容,Prime Video需要构建一个监控工具来识别客户所查看的每个流中的质量问题,这提出了极高的可扩展性要求。

对此,Prime Video团队优先考虑了微服务架构。由于微服务能够将单体应用分解为多个模块,这不仅能解决工具独立开发和部署的问题,还能为应用提供更高的可用性、可靠性以及技术多样性。最终,Prime Video的服务由三个部分组成,媒体转换器将音视频流发送到检测器的音视频缓冲区;缺陷检测器执行算法,并在发现缺陷时发送实时通知;第三个组件则提供控制服务流程的编排。

4f6334f29d067847c53c482bd9ca86a8.png

当更多流加入到服务中时,成本过高问题开始逐渐显现。由于AWS Step按照函数状态转换向用户收费,当需要处理大量流的时候,大规模运行基础设施的开销会变得非常昂贵,所有构建模块的总成本过高,导致Prime Video团队无法大规模接受最初的解决方案。最终,Prime Video团队重构了基础设施,从微服务迁移到了单体架构,据他们的数据,基础设施成本降低90%。

这件事情也让我们更加的注意到,分布式架构相比单体服务架构同样存在缺点。比如Prime Video 团队遇到问题:分布式架构无法像单体架构那样共享变量,从而导致底层服务处理更多相同的请求进而产生成本飙升的问题。这样的困境在爱奇艺海外架构中同样存在,特别明显的是在策略引擎的调用关系上。

2、爱奇艺海外策略引擎调用关系

2.1 策略引擎调用关系介绍

2096e6ebb9071155a11391c57b1bbcb3.png

其中,card是页面内的每个栏目的细分模块,通常如电视剧,电影等专栏是一个card。每个card内的数据源都不同,比如有从营销获取营销类数据,有从推荐获取的内容,还有从奇普获取节目内容等。其中这里面有存在关联关系,比如页面关联在导航下面,而card关联在页面下,card内的具体业务数据关联在card下面。

策略引擎是识别人群的匹配服务,如目前配置一条人群策略,里面内容是日本黄金会员,男性,会员到期天数小于7天,偏爱日本动漫。策略引擎服务可以识别某用户是否属于以上人群策略。

在“一切皆可策略”的技术改造之后,已经实现了导航、页面、card、card内数据的用户画像维度定制的能力。大体的实现方式如下:当客户端发起一个请求时,首先请求导航API。在导航数据配置后台,运营同学配置了不同的导航数据,每条导航数据关联了一个策略。导航API内部获取全部导航数据,然后将导航关联的策略与该用户uid和设备id作为入参请求策略引擎,策略引擎进行匹配将满足匹配的策略返回,导航API内部将满足要求的策略关联的导航数据返回,这样实现了不同用户画像看到了不同的导航数据的能力。页面、card、card内数据实现方式大体相同。

从以上的内容可以总结,在策略引擎的调用链路上存在以下特点:

(1)用户打开页面的一次操作会串联调用多次策略引擎服务。

(2)策略引擎接口性能直接影响用户体验:关联很多页面业务服务的请求。

(3)策略引擎数据要求强实时性 :用户购买会员后应该立刻关联到会员相关策略。

2.2 遇到的困境

从上节调用关系可知,策略引擎作为底层服务,承接了较多业务方的流量,而策略引擎判断人群策略是否匹配需要获取用户画像数据,这又强依赖了DMP(Data Management Platforms)服务。为了减少对DMP服务的流量,我们思考了本地缓存的方案。

9a1aa2ed6d4969279756ebdccfa8eaaa.png

但是这存在的问题很明显,即无法满足数据实时性要求。当用户购买了会员,DMP服务返回的画像数据变更时,因为本地缓存的延迟而导致用户无法看到最新的策略关联数据,这显然是无法忍受的。

同样我们思考了分布式缓存方案。如果使用用户id为key,其痛点和本地缓存一样,无法满足实时性要求。

因此,如何在满足数据实时性要求的情况下,优化对DMP服务的流量是整个策略引擎项目优化的挑战点。

02

   共享变量的曙光


1、概述

困境问题的关键在于,分布式服务无法共享变量。用户的一次页面打开行为伴随着多个后端请求,而这些多个后端请求关联的用户画像数据其实是一个,即从DMP服务获取的画像数据一定是相同的。下面我们对策略引擎的调用链路进行抽象分析,看看有哪些特性。

2、策略引擎调用链路分析

策略引擎的调用关系 在1.2.1 策略引擎调用关系介绍章节当中已经介绍,本次主要对其调用链路进行抽象分类。

2.1 串行调用场景

7641637dc6c50ce43157ed5f56dbc224.png

从上图可以看到,一个用户发起一个请求,经过多个节点服务,而节点服务间是串行关系,而每个节点都依赖了策略引擎服务,策略引擎服务都需要获取该用户的画像数据,而很显然,T1到Tn的请求都是同一个用户,DMP服务获取的数据一定是相同的,那么,如果使用共享变量的思路,可以将T1 ~ Tn次对DMP服务的请求优化为1个请求。我们把这里从DMP服务获取到的画像数据命名为分布式共享变量

2.2 并行调用场景

d36d2a2c0c43e83022d8155257712e91.png

与上面串行调用链不同,串行调用T1到Tn是有时间先后顺序,T1调用一定在T2调用前面。而并行调用没有先后时间顺序,即可能同一个用户发起一次请求,在聚合层业务同时对依赖服务发起请求,依赖服务又依赖了策略引擎,所以,对于同一个用户发起的一次请求,就会同时对策略引擎发起多次请求。那么如果将多次请求放置于一个队列,其中第一个请求去实际的请求DMP服务,其余请求在队列中等待第一个请求的数据,n个请求就可以优化为1个请求。我们把这里从DMP服务获取到的画像数据称为本地共享变量


03

   分布式共享变量介绍


1、原理概述

c8e166ebfc8423007068d2944203713b.png

到用户打开页面的时候,客户端会请求导航,然后依次获取特点页面,特定card和特点的card内数据。每个环节都会涉及到策略引擎服务。正常情况下,一次客户端请求将会引发多次策略引擎的调用,从而引起多次对DMP服务的调用。但是很显然,这都是同一个用户的一次请求,这几次对DMP服务的请求获取的用户画像数据一定是相同的。

基于以上分析,分布式共享变量的原理简单描述就是,当【导航】第一次获取到画像数据后,将其内容放在请求链路中,类似于全链路的TraceId一样向下传递。这样下游如【页面】再请求策略引擎时,则可以直接获取链路上下文TraceContext中的链路数据使用,而无需再请求DMP服务。对于CARD,和页面业务同样如此。

值得一提的是,TraceContext只能通过request向下传递。这样在存放链路数据的时候只能是在【导航】获取到策略引擎数据后,将画像数据放置到链路上下文TraceContext中。

如果导航没有关联策略数据,无需请求策略引擎,但是后面的页面、CARD等又关联了策略引擎,那该怎么处理呢?我们参考了TraceId的处理方式,在每个调用策略引擎服务的节点(不同业务如页面、CARD等)进行判断是否有链路数据,如果没有,则获取策略引擎数据后放置进去,如果有则忽略。这样就保证最前置的节点拿到画像数据后,进行向后传递,减少后续节点对于DMP服务的流量。很明显,这些逻辑有一些业务侵入性,所以我们将调用策略引擎的方式优化为SDK调用,在SDK内部做了一些统一的逻辑处理,让业务调用方无感知。

1c81a58be2d32ec5aad2cd28d2e6b051.png

2、全链路追踪 — 基于SkyWalking

skywalking 是分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器化技术(docker、K8s、Mesos)架构而设计,它是一款优秀的 APM(Application Performance Management)工具。skywalking 是观察性分析平台和应用性能管理系统。提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。对于为什么选择skywalking,除去skywalking本身的优势以外,业务上的理由是爱奇艺海外项目目前已经接入SkyWalking,开发成本最低,维护更加便利。所以,使用skywalking传递分布式共享变量只需要引入一个Maven依赖,调用其特有的方法,就可以将数据进行链路传递。

分布式共享变量的方案会增加网络传递数据的大小,增加网络开销;当链路数据足够大的时候甚至会影响服务响应性能。因此控制链路数据大小、链路数据的控制和评估链路数据对网络性能造成影响是尤为重要的。下面将详细介绍。

3、链路传输优化 — 压缩解压缩

3.1 压缩基本原理

目前用处最为广泛的压缩算法包括Gzip等大多是基于DEFLATE,而DEFLATE 是同时使用了 LZ77 算法与哈夫曼编码(Huffman Coding)的一种无损数据压缩算法。其中 LZ77 算法是先通过前向缓冲区预读取数据,然后再向滑动窗口移入(滑动窗口有一定的长度), 不断寻找能与字典中短语匹配的最长短语,然后通过标记符标记,依次来缩短字符串的长度。哈夫曼编码主要是用较短的编码代替较常用的字母,用较长的编码代替较少用的字母,从而减少了文本的总长度,其较少的编码通常使用构造二叉树来实现。

3.2 压缩选型

由于BI获取的用户画像TAG固定且个数较少,因此这里选择DMP数据作为实验对比数据。以下是不同场景下压缩大小对比数据

093832b9e0d16411f8a3160b28702afb.png

由上表分析可得

方案3得到的数据最小,因此选择方案3作为分布式共享变量的压缩方案。

4、数据大小导致的网络消耗分析和极端情况控制

4.1 背景概述

这种方案也存在一些弊端,即需要把用户画像数据通过网络传递,显然这增加了网络开销。理论上,网络数据量与传输速度成正比,但是在工程实践中,带宽肯定是有上限的,因此,对于DMP画像数据存入大小进行压测试验,以确定分布式共享变量对于网络性能的影响。

4.2 压测方案

1.测试网络,画像数据不被策略引擎使用,策略引擎依然请求DMP服务。

实验组是请求策略引擎服务的时候带入压缩后的画像数据,对照组是请求策略引擎服务的时候不带入压缩后的画像数据。调整并发值,比较在不同QPS场景下两者的接口性能。

280999cfae84f5f8d2591c686567d840.png

2.分布式共享变量的画像数据被策略引擎使用,策略引擎在有分布式共享变量画像数据的时候,不再请求DMP服务。

7a51061fbe8b4dcb55ef3fe0266083cf.png

4.3结论

  1. 网络链路上存放数据大小在2000以下,对网络性能的影响可以忽略不计。

  2. 因为分布式共享变量的存在而减少对DMP服务的请求,接口性能可以有比较大的提升。具体数值为P99从25ms提升到2.96ms。

4.4 极端情况控制

因为DMP数据与用户行为相关,比如一个用户在海外站点所有站点都有购买会员的行为,那么其DMP画像数据就会很大。为了防止这种极端情况所以在判断压缩后的用户画像数据足够大的时候,将自动舍弃,而不是放入网络当中,防止大数据对整个网路数据的性能损耗。

5、线上运行情况

5.1 性能优化

P99

P90

51a0170911e6c6fe9d25bac936507fc8.png

6d4788ec83c0c52c3df3a5791479baf3.png

P99 由之前的43ms下降到22ms。下降幅度 48.8%

P90由之前19ms下降到14ms,下降幅度26.3%

5.2 对DMP服务的流量优化

监控

结论

727d126bcc97aae462018258bfa55d95.png

分布式共享变量使用率即为对不同DMP服务优化流量。

A业务节约大约25%的流量,B业务节约约10% 的流量,

C业务节约约2%的流量

66a7a0e8e15594ea707a221399144990.png

2d671eabe8710c080bdf81c6e6ac7945.png

6、结论

分布式共享变量在满足数据实时性要求的前提下,减少了对DMP服务的流量,同时提高了策略引擎服务的接口性能,具体优化指标见上节。

04

   本地共享变量介绍


1、原理概述

在2.2.2 并行调用场景章节对本地共享变量解决的调用场景进行了阐述,主要解决的是同一个用户并发请求策略引擎带来的多次请求DMP服务问题。如何区分是同一个用户的同一次请求呢?答案是TraceId。在一个请求下,TraceId一定是相同的,如果TraceId相同,那么策略引擎则可以认为是同一个用户的一次请求。

0d3ba1d89ba8e5383c48bf8cd9ad91e9.png

如上图,如果同时多个TraceId3的请求到达策略引擎,将这些请求放入队列,只要其中一个去获取用户画像数据(此处为TraceId3''),其余的请求TraceId3和TraceId3'在队列中等待TraceId3''的结果拿来用即可。

这种思路可以很好的优化并发请求的数据,符合策略引擎调用特性。实现起来有点类似AQS,开发落地有一些难点,比如Trace3''什么时候去请求DMP服务,当拿到数据后,后面仍然有其他trace3进来该如何处理,等待多少时间?这么一思考,这个组件的实现将会耗费我们很多的开发时长,那么有没有现成的中间件可以用呢?答案是本地缓存框架。

6b5cf2f868f877b04eee2d867faab2f4.png

无论是本地缓存Caffeine 还是Guava Cache,有相同key的多个请求,只有一个key会请求下游服务,而其他请求会等待拿现成的结果。另外存放的时间可以通过配置缓存的失效时间来确定,至于失效时间的计算方法,将在下面章节会介绍。

2、网关层Hash路由方式的支持

目前,主流的服务一般都是多机房多机器部署,这样有水平扩展能力可以应对业务增长带来的流量增加的问题。但同一个用户的同一个请求,很可能到不同的服务实例,这样上一次获取到的本地缓存数据在下一次请求当中就无法获取。

bd0124978171dc1f183df994bb615bed.png

如上图,同一个用户的同一次请求,被聚合层并发请求到不同业务节点1到节点n。由于策略引擎服务是多实例部署,那么不同节点的请求可能到不同实例,那么本地共享变量的命中率就会大大降低,对DMP服务的流量节约数据就会小很多。因此,需要一个方案使得用户的多次请求能到同一个机房的同一个实例。

最终落地的方案是网关支持按照业务自定义字段Hash路由。策略引擎使用qyid进行hash路由,即同一个设备的所有请求到策略引擎服务,那么路由到的机器实例一定是同一个。这样可以很好的提升本地共享变量的命中率。这里提一下,相比轮询请求,字段Hash方式存在如流量偏移的问题,需要配合服务实例流量的监控和报警,避免某些实例流量过多而导致不可用。由于和本次主题无关,实例流量的监控和报警在这里就不做介绍。

3、本地共享变量个数和有效时间设计

和本地缓存不同,本地共享变量的最大个数和过期时间与命中率不成正比,这和具体业务指标相关。

假设策略引擎服务QPS10000,服务实例有50台,那么每台实例的QPS是200,即一台服务实例每秒的请求是200个。只需要保证,同一个TraceId的一批请求,在个数区间内不被淘汰,在时间区间内不被过期即可。我们通过网关日志查找历史上同一个traceId的请求时间戳,几乎都在100ms内。

f186d7fdbe4db7f2fbbb2f61ccc773ea.png

那么过期时间设置为1s,最大个数设置为200个就可以保证绝大多数同一个TraceId的批次请求,只有一个请求下游服务,其余从缓存获取数据。我们为此也进行了实验,设置不同的过期时间和缓存最大个数,结论和以上分析完全一致。

0061b1c61ac63dd7c1a80890a396cfb9.png

本地共享变量命中率与接口QPS和相同TraceId并发时间相关。

4、结论

本地共享变量上线后,优化数据如下

  • 对于DMP服务1,优化流量15.8%。对于DMP服务2,优化流量 16.7%。对于DMP服务3,优化流量16.2%。

  • 分布式共享变量一样,本地共享变量同样可以满足数据实时性要求,即不会存在1.2.2 遇到的困境 所遇到的缓存导致的数据实时性不够的问题

05

   总结和展望

本次优化是比较典型的技术创新项目。是先从社区看到一篇技术博客,然后想到爱奇艺海外遇到相同痛点问题的的项目,从而提出优化因为微服务导致的策略引擎对于DMP服务流量压力的目标。

在落地过程中,遇到使用本地缓存进行优化而无法克服数据实效性问题的挑战。最终沉下心分析策略引擎的调用链路,将调用链路一分为二:串行调用和并行调用,最终提出了共享变量的解决方案。因为串行调用和并行调用的特点迥异,依次针对两者进行分期优化,其中第一期通过分布式共享变量优化了串行调用DMP服务的流量,在第二期通过本地共享变量优化了并行调用DMP服务的流量。

由于作者水平有限,疏漏之处欢迎读者批评指正。


参考文章:

  • 猎户座-持续打造爱奇艺海外高扩展性的策略引擎项目

  • Scaling up the Prime Video audio/video monitoring service and reducing costs by 90%

  • 从微服务转为单体架构、成本降低 90%,亚马逊内部案例引发轰动!CTO:莫慌,要持开放心态

  • 分布式系统日志打印优化方案的探索与实践

04b81b9aeaba58de9638d57196d14883.jpeg

也许你还想看

低代码、中台化:爱奇艺号微服务工作流实践

揭秘内存暴涨:解决大模型分布式训练OOM纪实

分布式系统日志打印优化方案的探索与实践

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

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

相关文章

多人在线聊天交友工具,匿名聊天室网站源码,附带搭建教程

源码介绍 匿名聊天室(nodejs vue) 多人在线聊天交友工具,无需注册即可畅所欲言!你也可以放心讲述自己的故事,说出自己的秘密,因为谁也不知道对方是谁。 运行说明 安装依赖项:npm install 启动…

SpringBoot整合Dubbo和Zookeeper分布式服务框架使用的入门项目实例

文章目录 SpringBoot整合Dubbo和Zookeeper分布式服务框架使用的入门项目实例Dubbo定义其核心部分包含: 工作原理为什么要用dubbo各个节点角色说明:调用关系说明: dubbo为什么需要和zookeeper结合使用,zookeeper在dubbo体系中起到什么作用&…

Chatgpt+Comfyui绘图源码说明及本地部署文档

其他文档地址: ChatgptComfyui绘图源码运营文档 ChatgptComfyui绘图源码线上部署文档 一、源码说明 1、源码目录说明 app_home:app官网源码chatgpt-java:管理后台服务端源码、用户端的服务端源码chatgpt-pc:电脑网页前端源码cha…

论文阅读笔记AI篇 —— Transformer模型理论+实战 (四)

论文阅读笔记AI篇 —— Transformer模型理论实战 (四) 一、理论1.1 理论研读1.2 什么是AI Agent? 二、实战2.1 先导知识2.1.1 tensor的创建与使用2.1.2 PyTorch的模块2.1.2.1 torch.nn.Module类的继承与使用2.1.2.2 torch.nn.Linear类 2.2 Transformer代…

YOLOv5改进 | 主干篇 | 华为GhostnetV1一种移动端的专用特征提取网络

一、本文介绍 本文给大家带来的改进机制是华为移动端模型Ghostnetv1,华为GhostnetV1一种移动端的专用特征提取网络,旨在在计算资源有限的嵌入式设备上实现高性能的图像分类。GhostNet的关键思想在于通过引入Ghost模块,以较低的计算成本增加了特征图的数量,从而提高了模型的…

一、用户管理中心——前端初始化

一、Ant Design Pro初始化 1.创建空文件夹 2.打开Ant Design Pro官网 3.打开终端进行初始化 在终端输入npm i ant-design/pro-cli -g 在终端输入pro create myapp 选择umi3 选择simple 项目创建成功后,在文件夹中出现myapp 4.安装依赖 使用vscode打开项目 …

Java学习笔记(八)——Lambda表达式

文章目录 Lambda表达式Lambda表达式的省略写法Lambda练习练习1练习2 算法题算法题1 斐波那契数列算法题2 猴子吃桃子算法题3 爬楼梯 Lambda表达式 Lambda表达式是JDK8开始的一种新语法形式。 基本作用:简化函数式接口的匿名内部类的写法。 注意: Lam…

lambda

文章目录 lambda 概述lambda的演变过程lambda 表达式的基本格式案例:调用接口里面的方法几种方式 lambda省略写法案例一:抽象方法一个参数抽象方法两个参数 啦么大 使用的注意事项啦么大 与 匿名内部类 lambda 概述 函数式编程思想 面向对象思想在乎的是…

Java 面向对象02 封装 (黑马)

人画圆:画圆这个方法应该定义在园这个类里面。 人关门:是人给了门一个作用力,然后门自己关上了门,所以关门的方法是在门的类里面 封装对象的好处: 调用Java自带的方法举例实现: 在测试类中,对其…

PDshell16逆向PostgreSQL 工程显示字段comment备注

现状:当刚逆向成功的表结构是没有原来表结构中的,comment备注如下 然后pd逆向工程的sql已经返回了这个备注的含义 解决方案: 1、设置显示注释列 tools——Display Preferences…如下 勾选-按照下面得方式勾选这三个 复制这里的VBS脚本&a…

触摸屏监控双速电动机-确定地址分配

I/O地址分配 当选择了PLC之后,首先需要确定的是系统中各I/O点的绝对地址。在某些PLC 中1/O绝对地址的分配方式共有固定地址型、自动分配型、用户定义型3种。实际所使用的方式取决于所采用的PLC的CPU型号、编程软件、软件版本、编程人员的选择等因素。 本任务输入信…

51单片机原理及应用张毅刚版课后习题以及答案

AT89S51单片机内部集成了哪些外围功能部件 ①8位微处理器CPU ②数据存储器 128B RAM ③程序存储器 ④4个8位可编程并行I/O口 ⑤1个全双工的异步串行口 ⑥2个可编程的16位定时器/计数器 ⑦1个看门狗定时器WDT ⑧中断系统具有五个中断源 五个中断向量 ⑨特殊功能寄存器SFR 26个…

低代码技术杂谈

一、探讨低代码的定义 “Low-Code”是什么?身为技术人员听到这种技术名词,咱们第一反应就是翻看维基百科 或者其他相关技术论文,咱们想看维基百科的英文介绍: A low-code development platform (LCDP) provides a development env…

web蓝桥杯真题--11、蓝桥知识网

介绍 蓝桥为了帮助大家学习,开发了一个知识汇总网站,现在想设计一个简单美观的首页。本题请根据要求来完成一个首页布局。 准备 开始答题前,需要先打开本题的项目代码文件夹,目录结构如下: ├── css │ └──…

【JavaEE Spring】SpringBoot 配置文件

SpringBoot 配置文件 1. 配置文件的作用1.1 配置文件的说明1.2 SpringBoot 配置文件 2. 配置文件的格式特殊说明 3. properties 配置文件说明3.1 properties 基本语法3.2 读取配置文件3.3 properties 缺点分析 4. yml 配置文件说明4.1 yml 的基本语法4.2 yml 使⽤进阶4.2.1 yml…

大语言模型无代码构建知识图谱概述

2023年3月15日,ChatGPT4.0的横空出世,将人们对大语言模型的关注推到了风口浪尖。由于其在智能问答、翻译以及文本生成等工作任务上的卓越表现,业界一度出现了不再需要发展知识图谱相关技术的观点,知识图谱相关概念严重受挫。无可置…

【设计模式】文件目录管理是组合模式吗?

组合模式是什么? 组合模式是一种将对象组合成树形结构以表示"部分-整体"的层次结构的设计模式。它使得用户对单个对象和组合对象的使用具有一致性。 组合模式在什么情况下使用? 当你发现你需要在代码中实现树形数据结构,让整体-部…

无人机航迹规划(一)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划(提供MATLAB代码)

一、七种算法(DBO、LO、SWO、COA、LSO、KOA、GRO)简介 1、蜣螂优化算法DBO 蜣螂优化算法(Dung beetle optimizer,DBO)由Jiankai Xue和Bo Shen于2022年提出,该算法主要受蜣螂的滚球、跳舞、觅食、偷窃和繁…

Flutter 与 Android原生 相互通信:BasicMessageChannel、MethodChannel、EventChannel

前言 本文主要讲解,使用不同的 Channel 让 Flutter 和 Android原生 进行通信,由于只是讲解两端通信,所以可视化效果不好; 不过我写了一篇专门讲解 Flutter 嵌入 Android原生View的文章 Flutter 页面嵌入 Android原生 View-CSDN…

【富文本编辑器实战】02 编写编辑器配置文件

编写编辑器配置文件 目录 编写编辑器配置文件前言项目结构分析项目配置菜单项配置语言配置总体配置 总结 前言 本篇文章主要内容是项目的配置文件的编写与讲解,包括菜单项配置、语言配置、总体配置。 项目结构分析 下图是编辑器的总体结构: 编辑器大致…
最新文章