【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

美团外卖 iOS 多端复用的推动、支撑与思考

  • 2020-02-25
  • 本文字数:8403 字

    阅读完需:约 28 分钟

美团外卖iOS多端复用的推动、支撑与思考

前言

美团外卖 2013 年 11 月开始起步,随后高速发展,不断刷新多项行业记录。截止至 2018 年 5 月 19 日,日订单量峰值已超过 2000 万,是全球规模最大的外卖平台。业务的快速发展对技术支撑提出了更高的要求。为线上用户提供高稳定的服务体验,保障全链路业务和系统高可用运行的同时,要提升多入口业务的研发速度,推进 App 系统架构的合理演化,进一步提升跨部门跨地域团队之间的协作效率。而另一方面随着用户数与订单数的高速增长,美团外卖逐渐有了流量平台的特征,兄弟业务纷纷尝试接入美团外卖进行推广和发布,期望提供统一标准化服务平台。因此,基础能力标准化,推进多端复用,同时输出成熟稳定的技术服务平台,一直是我们技术团队追求的核心目标。

多端复用的端

这里的“端”有两层意思:


  • 其一是相同业务的多入口

  • 美团外卖在 iOS 下的业务入口有三个,『美团外卖』App、『美团』App 的外卖频道、『大众点评』App 的外卖频道。

  • 值得一提的是:由于用户画像与产品策略差异,『大众点评』外卖频道与『美团』外卖频道和『美团外卖』虽经历技术栈融合,但业务形态区别较大,暂不考虑上层业务的复用,故这篇文章主要介绍美团系两大入口的复用。

  • 在 2015 年外卖 C 端合并之前,美团系的两大入口由两个不同的团队研发,虽然用户感知的交互界面几乎相同,但功能实现层面的代码风格和技术栈都存在较大差异,同一需求需要在两端重复开发显然不合理。所以,我们的目标是相同功能,只需要写一次代码,做一次估时,其他端只需做少量的适配工作。

  • 其二是指平台上各个业务线

  • 外卖不同兄弟业务线都依赖外卖基础业务,包括但不限于:地图定位、登录绑定、网络通道、异常处理、工具 UI 等。考虑到标准化的范畴,这些基础能力也是需要多端复用的。


  • 图1 美团外卖的多端复用的目标


关于组件化

提到多端复用,不免与组件化产生联系,可以说组件化是多端复用的必要条件之一。大多数公司口中的“组件化”仅仅做到代码分库,使用 Cocoapods 的 Podfile 来管理,再在主工程把各个子库的版本号聚合起来。但是能设计一套合理的分层架构,理清依赖关系,并有一整套工具链支撑组件发版与集成的相对较少。否则组件化只会导致包体积增大,开发效率变慢,依赖关系复杂等副作用。

整体思路

A. 多端复用概念图


图 2 多端复用概念图


多端复用的目标形态其实很好理解,就是将原有主工程中的代码抽出独立组件(Pods),然后各自工程使用 Podfile 依赖所需的独立组件,独立组件再通过 podspec 间接依赖其他独立组件。

B. 准备工作

确认多端所依赖的基层库是一致的,这里的基层库包括开源库与公司内的技术栈。


iOS 中常用开源库(网络、图片、布局)每个功能基本都有一个库业界垄断,这一点是 iOS 相对于 Android 的优势。公司内也存在一些对开源库二次开发或自行研发的基础库,即技术栈。不同的大组之间技术栈可能存在一定差异。如需要复用的端之间存在差异,则需要重构使得技术栈统一。(这里建议重构,不建议适配,因为如果做的不够彻底,后续很大可能需要填坑。)


就美团而言,美团平台与点评平台作为公司两大 App,历史积淀厚重。自 2015 年底合并以来,为了共建和沉淀公共服务,减少重复造轮子,提升研发效率,对上层业务方提供统一标准的高稳定基础能力,两大平台的底层技术栈也在不断融合。而美团外卖作为较早实践独立 App,同时也是依托于两大平台 App 的大业务方,在外卖 C 端合并后的 1 年内,我们也做了大量底层技术栈统一的必要工作。

C. 方案选型

在演进式设计与计划式设计中的抉择。


演进式设计指随着系统的开发而做设计变更,而计划式设计是指在开发之前完全指定系统架构的设计。演进的设计,同样需要遵循架构设计的基本准则,它与计划的设计唯一的区别是设计的目标。演进的设计提倡满足客户现有的需求;而计划的设计则需要考虑未来的功能扩展。演进的设计推崇尽快地实现,追求快速确定解决方案,快速编码以及快速实现;而计划的设计则需要考虑计划的周密性,架构的完整性并保证开发过程的有条不紊。


美团外卖 iOS 客户端,在多端复用的立项初期面临着多个关键点:频道入口与独立应用的复用,外卖平台的搭建,兄弟业务的接入,点评外卖的协作,以及架构迁移不影响现有业务的开发等等,因此权衡后我们使用“演进式架构为主,计划式架构为辅”的设计方案。不强求历史代码一下达到终极完美架构,而是循序渐进一步一个脚印,满足现有需求的同时并保留一定的扩展性。

演进式架构推动复用

术语解释

  • Waimai:特指『美团外卖』App,泛指那些独立 App 形式的业务入口,一般为 project。

  • Channel:特指『美团』App 中的外卖频道,泛指那些以频道或者 Tab 形式集成在主 App 内的业务入口,一般为 Pods。

  • Special:指将 Waimai 中的业务代码与原有工程分离出来,让业务代码成为一个 Pods 的形态。

  • 下沉:即下沉到下层,这里的“下层”指架构的基层,一般为平台层或通用层。“下沉”指将不同上层库中的代码统一并移动到下层的基层库中。


在这里先贴出动态的架构演进过程,让大家有一个宏观的概念,后续再对不同节点的经历做进一步描述。



图 3 演进式架构动态图

原始复用架构

如图 4 所示,在过去一两年,因为技术栈等原因我们只能采用比较保守的代码复用方案。将独立业务或工具类代码沉淀为一个个“Kit”,也就是粒度较小的组件。此时分层的概念还比较模糊,并且以往的工程因历史包袱导致耦合严重、逻辑复杂,在将 UGC 业务剥离后发现其他的业务代码无法轻易的抽出。(此时的代码复用率只有 2.4%。)


鉴于之前的准备工作已经完成,多端基础库已经一致,于是我们不再采取保守策略,丰富了一些组件化通信、解耦与过渡的手段,在分层架构上开始发力。



图 4 原始复用架构

业务复用探索

在技术栈已统一,基础层已对齐的背景下,我们挑选外卖核心业务之一的 Store(即商家容器)开始了在业务复用上的探索。如图 5 所示,大致可以理解为“二合一,一分三”的思路,我们从代码风格和开发思路上对两边的 Store 业务进行对齐,在此过程中顺势将业务类与技术(功能)类的代码分离,一些通用 Domain 也随之分离。随着一个个组件的拆分,我们的整体复用度有明显提升,但开发效率却意外的受到了影响。多库开发在版本的发布与集成中增加了很多人工操作:依赖冲突、lock 文件冲突等问题都阻碍了我们的开发效率进一步提升,而这就是之前“关于组件化”中提到的副作用。


于是我们将自动发版与自动集成提上了日程。自动集成是将“组件开发完毕到功能合入工程主体打出测试包”之间的一系列操作自动化完成。在这之前必须完成一些前期铺垫工作——壳工程分离。



图 5 商家容器下沉时期

壳工程分离

如图 6 所示,壳工程顾名思义就是将原来的 project 中的代码全部拆出去,得到一个空壳,仅仅保留一些工程配置选项和依赖库管理文件。


为什么说壳工程是自动集成的必要条件之一?


因为自动集成涉及版本号自增,需要机器修改工程配置类文件。如果在创建二进制的过程中有新业务 PR 合入,会造成 commit 树分叉大概率产生冲突导致集成失败。抽出壳工程之后,我们的壳只关心配置选项修改(很少),与依赖版本号的变化。业务代码的正常 PR 流程转移到了各自的业务组件 git 中,以此来杜绝人工与机器的冲突。



图 6 壳工程分离


壳工程分离的意义主要有如下几点:


  • 让职能更加明确,之前的综合层身兼数职过于繁重。

  • 为自动集成铺路,避免业务 PR 与机器冲突。

  • 提升效率,后续 Pods 往 Pods 移动代码比 proj 往 Pods 移动代码更快。

  • 『美团外卖』向『美团』开发环境靠齐,降低适配成本。



图 7 壳工程分离阶段图


图 7 的第一张图到第二张图就是上文提到的壳工程分离,将“Waimai”所有的业务代码打包抽出,移动到过渡仓库 Special,让原先的“Waimai”成为壳。


第二张图到第三张图是 Pods 库的内部消化。


前一阶段相当于简单粗暴的物理代码移动,后一阶段是对 Pods 内整块代码的梳理与分库。

内部消化对齐

在前文“多端复用概念图”的部分我们提到过,所谓的复用是让多端的 project 以 Pods 的方式接入统一的代码。我们兼容考虑保留一端代码完整性,降低回接成本,决定分 Subpods 使用阶段性合入达到平滑迁移。



图 8 代码下沉方案


图 8 描述了多端相同模块内的代码具体是如何统一的。此时因为已经完成了壳工程分离,所以业务代码都在“Special”这样的过渡仓库中。


“Special”和“Channel”两端的模块统一大致可分为三步:平移 → 下沉 → 回接。(前提是此模块的业务上已经确定是完全一致。)


平移阶段是保留其中一端“Special”代码的完整性,以自上而下的平移方式将代码文件拷贝到另一端“Channel”中。此时前者不受任何影响,后者的代码因为新文件拷贝和原有代码存在重复。此时将旧文件重命名,并深度优先遍历新文件的依赖关系补齐文件,最终使得编译通过。然后将旧文件中的部分差异代码加到新文件中做好一定的差异化管理,最后删除旧文件。


下沉阶段是将“Channel”处理后的代码解耦并独立出来,移动到下层的 Pods 或下层的 SubPods。此时这里的代码是既支持“Special”也支持“Channel”的。


回接阶段是让“Special”以 Pods 依赖的形式引用之前下沉的模块,引用后删除平移前的代码文件。(如果是在版本的间隙完成固然最好,否则需要考虑平移前的代码文件在这段时间的 diff。)


实际操作中很难在有限时间内处理完一个完整的模块(例如订单模块)下沉到 Pods 再回接。于是选择将大模块分成一个个子模块,这些子模块平滑的下沉到 SubPods,然后“Special”也只引用这个统一后的 SubPods,待一个模块完全下沉完毕再拆出独立的 Pods。


再总结下大量代码下沉时如何保证风险可控:


  • 联合 PM,先进行业务梳理,特殊差异要标注出来。

  • *使用 OClint 的提前扫描依赖,做到心中有数,精准估时。

  • *以“Special”的代码风格为基准,“Channel”在对齐时仅做加法不做减法。

  • *“Channel”对齐工作不影响“Special”,并且回接时工作量很小。

  • *分迭代包,QA 资源提前协调。

中间件层级压平

经过前面的“内部消化”,Channel 和 Special 中的过渡代码逐渐被分发到合适的组件,如图 9 所示,Special 只剩下 AppOnly,Channel 也只剩下 ChannelOnly。于是 Special 消亡,Channel 变成打包工程。


AppOnly 和 ChannelOnly 与其他业务组件层级压平。上层只留下两个打包工程。



图 9 中间件层级压平

平台层建设

如图 10 所示,下层是外卖基础库,WaimaiKit 包含众多细分后的平台能力,Domain 为通用模型,XunfeiKit 为对智能语音二次开发,CTKit 为对 CoreText 渲染框架的二次开发。


针对平台适配层而言,在差异化收敛与依赖关系梳理方面发挥重要角色,这两点在下问的“衍生问题解决中”会有详细解释。


外卖基础库加上平台适配层,整体构成了我们的外卖平台层(这是逻辑结构不是物理结构),提供了 60 余项通用能力,支持无差异调用。



图 10 外卖平台层的建设

多端通用架构

此时我们把基层组件与开源组件梳理并补充上,达到多端通用架构,到这里可以说真正达到了多端复用的目标。



图 11 多端通用架构完成


由上层不同的打包工程来控制实际需要的组件。除去两个打包工程和两个 Only 组件,下面的组件都已达到多端复用。对比下“Waimai”与“Channel”的业务架构图中两个黑色圆圈的部分。



图 12 “Waimai”的业务架构



图 13 “Channel”的业务架构

衍生问题解决

差异问题

A.需求本身的差异


三种解决策略:


  • 对于文案、数值、等一两行代码的差异我们使用 运行时宏(动态获取 proj-identifier)或预编译宏(custome define)直接在方法中进行 if else 判断。

  • 对于方法实现的不同 使用 Glue(胶水层),protocol 提供相同的方法声明,用来给外部调用,在不同的载体中写不同的方法实现。

  • 对于较大差异例如两边 WebView 容器不一样,我们建多个文件采用文件级预编译,可预编译常规.m 文件或者 Category。(例如 WMWebViewManeger_wm.m&WMWebViewManeger_mt.m、UITableView+WMEstimated.m&UITableView+MTEstimated.m)


进一步优化策略:


用上述三种策略虽然完成差异化管理,但差异代码散落在不同组件内难以收敛,不便于管理。有了平台适配层之后,我们将差异化判断收敛到适配层内部,对上层提供无差异调用。组件开发者在开发中不用考虑宿主差异,直接调用用通用接口。差异的判断或者后续优化在接口内部处理外部不感知。


图 14 给出了一个平台适配层提供通用接口修改后的例子。



图 14 平台适配层接口示例


B.多端节奏差异


实际场景中除了需求的差异还有可能出现多端进版节奏的差异,这类差异问题我们使用分支管理模型解决。


前提条件既然要多端复用了,那需求的大方向还是会希望多端统一。一般较多的场景是:多端中 A 端功能最少,B 端功能基本算是是 A 端的超集。(没有绝对的超集,A 端也会有较少的差异点。)在外卖的业务中,“Channel”就是这个功能较少的一端,“Waimai”基本是“Channel”的超集。


两端的差异大致分为了这 5 大类 9 小类:


  1. 需求两端相同(1.1、提测上线时间基本相同;1.2、“Waimai”比“Channel”早 3 天提测 ;1.3、“Waimai”比“Channel”晚 3 天提测)。

  2. 需求“Waimai”先进版,“Channel”下一版进 (2.1、频道下一版就上;2.2、频道下两版本后再上)。

  3. 需求“Waimai”先进版,“Channel”不需要。

  4. 需求“Channel”先进版,“Waimai”下一版进(4.1、需要改动通用部分;4.2、只改动“ChannelOnly”的部分)。

  5. 需求“Channel”先进版,“Waimai”不需要(只改动“ChannelOnly”的部分)。



图 15 最复杂场景下的分支模型


也不用过多纠结,图 15 是最复杂的场景,实际场合中很难遇到,目前的我们的业务只遇到 1 和 2 两个大类,最多 2 条线。

编译问题

以往的开发方式初次全量编译 5 分钟左右,之后就是差量编译很快。但是抽成组件后,随着部分子库版本的切换间接的增加了 pod install 的次数,此时高频率的 3 分钟、5 分钟会让人难以接受。


于是在这个节点我们采用了全二进制依赖的方式,目标是在日常开发中直接引用编译后的产物减少编译时间。



图 16 使用二进制的依赖方式


如图所示三个.a 就是三个 subPods,分了三种 Configuration:


  1. debug/ 下是 deubg 设置编译的 x64 armv7 arm64。

  2. release/ 下是 release 设置编译的 armv7 arm64。

  3. dailybuild/ 下是 release + TEST=1 编译的 armv7 arm64。

  4. 默认(在文件夹外的.a)是 debug x64 + release armv7 + release arm64。


这里有一个问题需要解决,即引用二进制带来的弊端,显而易见的就是将编译期的问题带到了运行期。某个宏修改了,但是编译完的二进制代码不感知这种改动,并且依赖版本不匹配的话,原本的方法缺失编译错误,就会带到运行期发生崩溃。解决此类问题的方法也很简单,就是在所有的打包工程中都配置了打包自动切换源码。二进制仅仅用来在开发中获得更高的效率,一旦打提测包或者发布包都会使用全源码重新编译一遍。关于切源码与切二进制是由环境变量控制拉取不同的 podspec 源。


并且在开发中我们支持源码与二进制的混合开发模式,我们给某个 binary_pod 修饰的依赖库加上标签,或者使用.patch 文件,控制特定的库拉源码。一般情况下,开发者将与自己当前需求相关联的库拉源码便于 Debug,不关联的库拉二进制跳过编译。

依赖问题

如图 17 所示,外卖有多个业务组件,公司也有很多基础 Kit,不同业务组件或多或少会依赖几个 Kit,所以极易形成网状依赖的局面。而且依赖的版本号可能不一致,易出现依赖冲突,一旦遇到依赖冲突需要对某一组件进行修改再重新发版来解决,很影响效率。解决方式是使用平台适配层来统一维护一套依赖库版本号,上层业务组件仅仅关心平台适配层的版本。



图 17 平台适配层统一维护依赖


当然为了避免引入平台适配层而增加过多无用依赖的问题,我们将一些依赖较多且使用频度不高的 Kit 抽出 subPods,支持可选的方式引入,例如 IM 组件。


再者就是 pod install 时依赖分析慢的问题。对于壳工程而言,这是所有依赖库汇聚的地方,依赖关系写法若不科学极易在 analyzing dependency 中耗费大量时间。Cocoapods 的依赖分析用的是Molinillo算法,链接中介绍了这个算法的实现方式,是一个具有前向检察的回溯算法。这个算法本身是没有问题的,依赖层级深只要依赖写的合理也可以达到秒开。但是如果对依赖树叶子节点的版本号控制不够严密,或中间出现了循环依赖的情况,会导致回溯算法重复执行了很多压栈和出栈操作耗费时间。美团针对此类问题的做法是维护一套“去依赖的 podspec 源”,这个源中的 dependency 节点被清空了(下图中间)。实际的所需依赖的全集在壳工程 Podfile 里平铺,统一维护。这么做的好处是将之前的树状依赖(下图左)压平成一层(下图右)。



图 18 依赖数的压平

效率问题

前面我们提到了自动集成,这里展示下具体的使用方式。美团发布工程组自行研发了一套HyperLoop发版集成平台。当某个组件在创建二进制之前可自行选择集成的目标,如果多端复用了,那只需要在发版创建二进制的同时勾选多个集成的目标。发版后会自行进行一系列检查与测试,最终将代码合入主工程(修改对应壳工程的依赖版本号)。



图 19 HyperLoop 自动发版自动集成



图 20 主工程 commit message 的变化


以上是“Waimai”的 commit 对比图。第一张图是以往的开发方式,能看出工程配置的 commit 与业务的 commit 交错堆砌。第二张图是进行壳工程分离后的 commit,能看出每条 message 都是改了某个依赖库的版本号。第三张图是使用自动集成后的 commit,能看出每条 message 都是画风统一且机器串行提交的。


这里又衍生出另一个问题,当我们用壳工程引 Pods 的方式替代了 project 集中式开发之后,我们的代码修改散落到了不同的组件库内。想看下主工程 6.5.0 版本和 6.4.0 版本的 diff 时只能看到所有依赖库版本号的 diff,想看 commit 和 code diff 时必须挨个去组件库查看,在三轮提测期间这样类似的操作每天都会重复多次,很不效率。


于是我们开发了 atomic diff 的工具,主要原理是调 git stash 的接口得到版本号 diff,再通过版本号和对应的仓库地址深度遍历 commit,再深度遍历 commit 对应的文件,最后汇总,得到整体的代码 diff。



图 21 atomic diff 汇总后的 commit message

整套工具链对多端复用的支撑

上文中已经提到了一些自动化工具,这里整理下我们工具链的全景图。



图 22 整套工具链


  1. 在准备阶段,我们会用 OClint 工具对 compile_command.json 文件进行处理,对将要修改的组件提前扫描依赖。

  2. 在依赖库拉取时,我们有 binary_pod.rb 脚本里通过对源的控制达到二进制与去依赖的效果,美团发布工程组维护了一套 ios-re-sankuai.com 的源用于存储 remove dependency 的 podspec.json 文件。

  3. 在依赖同步时,会通过 sync_podfile 定时同步主工程最新 Podfile 文件,来对依赖库全集的版本号进行维护。

  4. 在开发阶段,我们使用 Podfile.patch 工具一键对二进制/源码、远端/本地代码进行切换。

  5. 在引用本地代码开发时,子库的版本号我们不太关心,只关心主工程的版本号,我们使用 beforePod 和 AfterPod 脚本进行依赖过滤以防止依赖冲突。

  6. 在代码提交时,我们使用 git squash 对多条相同 message 的 commit 进行挤压。

  7. 在创建 PR 时,以往需要一些网页端手动操作,填写大量 Reviewers,现在我们使用 MTPR 工具一键完成,或者根据个人喜好使用 Chrome 插件。

  8. 在功能合入 master 之前,会有一些 jenkins 的 job 进行检测。

  9. 在发版阶段,使用 Hyperloop 系统,一键发版操作简便。

  10. 在发版之后,可选择自动集成和联合集成的方式来打包,打包产物会自动上传到美团的“抢鲜”内测平台。

  11. 在问题跟踪时,如果需要查看主工程各个版本号间的 commit message 和 code diff,我们有 atomic diff 工具深度遍历各个仓库并汇总结果。

总结

  • 多端复用之后对 PM-RD-QA 都有较大的变化,我们代码复用率由最初的 2.4%达到了 84.1%,让更多的 PM 投入到了新需求的吞吐中,但研发效率提升增大了 QA 的工作量。一个大的尝试需要 RD 不断与 PM 和 QA 保持沟通,选择三方都能接受的最优方案。

  • 分清主次关系,技术架构等最终是为了支撑业务,如果一个架构设计的美如画天衣无缝,但是落实到自己的业务中确不能发挥理想效果,或引来抱怨一片,那这就是个失败的设计。并且在实际开发中技术类代码修改尽量选择版本间隙合入,如果与业务开发的同学产生冲突时,都要给业务同学让路,不能影响原本的版本迭代速度。

  • 时刻对 “不合理” 和 “重复劳动”保持敏感。新增一个埋点常量要去改一下平台再发个版是否成本太大?一处订单状态的需求为什么要修改首页的 Kit?实际开发中遇到别扭的地方多增加一些思考而不是硬着头皮过去,并且手动重复两次以上的操作就要思考有没有自动化的替代方案。

  • 一旦决定要做,在一些关键节点决不能手软。例如某个节点为了不 Block 别人,加班不可避免。在大量代码改动时也不用过于紧张,有提前预估,有 Case 自测,还有 QA 的三轮回归来保障,保持专注,放手去做就好。

作者简介

  • 尚先,美团资深工程师。2015 年加入美团,目前作为美团外卖 iOS 端平台化虚拟小组组长,主要负责业务架构、持续集成和工程化相关工作,致力于提升研发效率与协作效率。


2020-02-25 20:321702

评论 1 条评论

发布
用户头像
请问代码复用率是怎么计算的
2022-08-12 06:38 · 广东
回复
没有更多了
发现更多内容

【INFINI Workshop 深圳站】8 月 31 日一起动手实验玩转 Easysearch

极限实验室

深圳 INFINI Labs Workshop 极限科技

车载语音识别数据的社会影响与未来展望

来自四九城儿

技术思维和产品思维

老张

产品思维 技术思维 解决问题

代码随想录Day59 - 单调栈(二)

jjn0703

Premiere Pro 2021功能 视频编辑软件pr2021中文版下载

mac

苹果mac Windows软件 视频编辑软件 Premiere Pro Premiere Pro 2021

Zebec在Nautilus Chain 开启质押,ZBC 将极致通缩

西柚子

5分钟搞懂K8S Pod Terminating/Unknown故障排查

俞凡

Kubernetes 最佳实践 云原生

寻找注册配置中心最佳评测官,赢取丰厚奖品 | 测评开启,开发者请速速集结

阿里巴巴云原生

阿里云 微服务 云原生

Presto 设计与实现(九):SQL 词法分析

冰心的小屋

数据湖 词法分析器 presto 设计与实现

车载语音识别数据的应用与挑战

来自四九城儿

Go 输出函数

小万哥

Go 编程 程序员 后端 开发

蓝易云:什么是 sudo,为什么它如此重要?

百度搜索:蓝易云

云计算 Linux 运维 root sudo

Lightroom Classic for mac(Lrc2021照片编辑软件) 10.3中文版

mac

照片编辑软件 苹果mac Windows软件 Lightroom Classic 2021 lrc2021

代码随想录Day60 - 单调栈(三)

jjn0703

微信多开 WechatTweak for Mac(微信多开、消息防撤回工具)v3.8.2中文版

mac

微信 苹果mac Windows软件 WeChatTweak 微信多开助手

3D渲染动画制作 KeyShot 2023.2 Pro 补丁安装教程

胖墩儿不胖y

3D渲染 动画制作 Mac软件 渲染工具

打字练习软件Master of Typing 3 mac激活版下载

mac

苹果mac Windows软件 ​Master of Typing 3 打字练习软件

Mac电脑fcpx视频剪辑推荐Final Cut Pro 最新中文激活版

mac大玩家j

视频剪辑 Mac软件 视频编辑处理工具 视频处理软件

基于RUM高效治理网站用户体验入门-价值篇

Yestodorrow

可观测性

蓝易云:如何使用 Fail2ban 防止对 Linux 的暴力攻击?

百度搜索:蓝易云

云计算 Linux 运维 SSH Fail2ban

代码随想录Day61 - 完篇总结

jjn0703

ARTS 打卡 第二周

一期一会

pandas ARTS 打卡计划 职业发展 LLMs

SketchUp Pro 2023中文 for Mac(草图大师) v23.0.418

mac大玩家j

建模软件 三维建模软件 Mac软件推荐

车载语音识别数据的技术进展与前景

来自四九城儿

磁盘管理工具推荐DiskCatalogMaker 最新激活中文

胖墩儿不胖y

磁盘清理 磁盘管理 Mac软件 清理磁盘软件

美团外卖iOS多端复用的推动、支撑与思考_文化 & 方法_美团技术团队_InfoQ精选文章