滴滴开源新项目Unify:聚焦Flutter与原生通信难题,助力跨端应用落地

引言

在移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。Flutter 作为近年来热门的跨端技术,以高性能、自渲染、泛跨端著称,得到广泛应用。在滴滴国际化业务中,我们大量应用 Flutter。目前已在滴滴国际化外卖、滴滴国际化出行司机端等业务中大规模落地,整体交付提效 50%+,收益显著。在大规模 Flutter 跨端场景下,存量的原生业务与增量 Flutter 业务间的双向通信成为痛点问题。

为此,滴滴国际化外卖自研 Unify 框架,旨在解决大规模跨端落地场景下,Flutter 与原生模块之间的通信问题。Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。

基于 Unify,滴滴国际化外卖成功将 16+ 个原生平台能力,26+ 个原生业务能力高效导入 Flutter,并沉淀出 UniFoundation、UniBusiness 两套业务架构模式,有效支撑外卖业务从零到一实现 Flutter 跨端落地。同时,Unify 也在滴滴国际化出行司机端中推广落地,有效支撑了兄弟业务的大规模跨端落地。

目前,Unify 已作为滴滴开源项目,正式开源,欢迎大家试用、体验、star 支持!

背景

在跨端落地过程中,通常会保留原生实现,以迭代方式逐步试水跨端,先跑通模式,再逐渐扩大跨端落地规模。

在原生代码与 Flutter 代码并存前提下,面临一系列实际问题:

1. 大量原生 SDK 如何高效导入 Flutter?

2. 大量业务功能如何高效导入 Flutter?

3. Flutter 功能模块如何导出给原生?

此类 Flutter 与原生代码间的双向通信问题,我们统称为混合通信问题

针对这一问题,Flutter 官方提供了 Channel 通信方案,但在大规模落地场景下,该方案存在一系列不足:

  1. 手动解析参数引发异常:使用 Channel 需要手动解析调用参数,极易出错。当接口发生变化时,需重新适配也极易引入 Bug。该问题在线上经常出现,并且难以根治。

  1. 大规模导出难以维护:在大规模能力导出场景下,需要编写大量分支语句和硬编码,难以维护。

  2. 代码封装繁琐:Channel API 较为底层,开发者还需二次封装,才能提供给业务方便调用,这一过程较为繁琐。

除 Channel 外,Pigeon 是一个更加强大的解决方案。Pigeon 由 Google 推出,该方案基于代码生成技术,有效提升了工程质量,降低了接入成本。但通过实际使用,我们发现在大规模模块导出场景下,Pigeon 的开发效率还有进一步提升的空间。

基于这一背景,Unify 通过批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。同时,Unify 也逐渐演化出自身特色,比如更加符合开发者习惯的多工程文件组织方式。

Unify 介绍

Unify 由滴滴出行国际化外卖团队自研,目前已经广泛应用于滴滴国际化外卖及国际化出行业务,有力支撑了业务的 Flutter 化进程。

Unify 的亮点特性包括:

  • 平台无关的模块抽象: 允许开发者使用 Dart 语言声明与平台无关的模块接口与实体。

  • 灵活的实现注入: 开发者可以灵活地选择注入原生实现(Android/iOS)或 Flutter 实现。

  • 自动代码生成: 借助强大的代码生成引擎,Unify 可以自动生成 Flutter、Android、iOS 多平台下统一调用的 SDK。

下面是一个使用 Unify 声明原生模块的示例:

@UniNativeModule()
abstract class DeviceInfoService {Future<DeviceInfoModel> getDeviceInfo();
}

通过 Unify,上面的 Dart 接口可以自动映射到 Android、iOS、Flutter 平台,开发者只需在各平台下填入具体实现即可。在 Flutter 中使用时,调用方式就像普通的 Flutter 模块一样简单、直观:

DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {print("${deviceInfoModel.encode()}");
});

Unify 的整体架构如下:

outside_default.png

Unify 核心概念

在进行混合通信开发时,典型场景包括

  • 将原生模块导出至 Flutter 调用

  • 将 Flutter 模块导出至原生调用

  • 在接口中传递复杂实体类

在 Unify 中定义了一系列核心概念,能够高效满足上述场景。以上场景分别对应于 UniNativeModule、UniFlutterModule、UniModel。

在具体使用时,开发者首先声明模块接口,接口声明使用 Dart 语言,以抽象类形式编写。接下来执行 Unify 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:

  1. 实现注入接口:用于开发者注入实现逻辑

  1. 对于原生模块导出至 Flutter 场景,使用 UniNativeModule 声明模块,Unify 会在原生侧 (Android、iOS)生成注入接口。

  2. 对于 Flutter 模块导出至原生场景,使用 UniFlutterModule 声明模块,Unify 会在 Flutter 侧生成注入接口。

  1. 三端统一调用接口

  1. Unify 会在 Android(Java)、iOS(Objective-C)、Flutter(Dart)生成三端统一调用接口。

  2. 在任意一端,都能在对应语言下,使用同样的模块接口签名,调用导出能力。

  3. 值得一提的是,Unify 支持使用 UniModel 声明可嵌套实体类,在三端下也会生成对应实体类,开发者在任意一技术栈下都可操作实体类,由 Unify 抹平底层序列化、反序列化通信,大幅提升开发体验与质量。

整体流程如下图所示:

outside_default.png

具体来说:

概念描述举例

UniNativeModule

声明一个模块,该模块的实现在原生(Android/iOS)注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

outside_default.png

UniFlutterModule

声明一个模块,该模块的实现在 Flutter 注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

outside_default.png

UniModel

Unify 提供的模板注解之一,主要作用:

  • 创建自定义实体(Model/Entity)。

跨端传输时,可以把它的对象实体作为参数,

直接跨端发送。

outside_default.png


Getting Start

前面的介绍有些抽象,在本节中,我们将通过实际案例,看是如何将原生模块是导入 Flutter中,来进行介绍的。

在本节中,假设有一个系统信息 SDK,在 Android、iOS 下分别实现。现在我们需要对两端进行封装,向 Flutter 侧提供统一能力。基于 Unify,这一任务能够快速、简单、高效、高质量完成。

注:完整代码实现可于文末点击「阅读原文」查看。

Step1:模块声明

第一步,开发者需要对模块接口进行声明。在 Flutter 工程根目录下创建一个 interface 目录,所有 Unify 的模块声明均位于该目录中。

interface 下包含两个声明文件,均以 Dart 抽象类方式编写。

device_info_service.dart

声明原生模块

// device_info_service.dart
@UniNativeModule()
abstract class DeviceInfoService {/// 获取设备信息Future<DeviceInfoModel> getDeviceInfo();
}

@UniNativeModule 注解表示该模块的实现由原生侧提供。

device_info_model.dart

声明返回值 Model

// device_info_model.dart
@UniModel()
class DeviceInfoModel {/// 系统版本String? osVersion;/// 内存信息String? memory;/// 手机型号String? plaform;
}

@UniModel 注解表示这是一个跨平台的数据模型。

值得一提的是:

  1. Unify 并不限制接口参数的数量,并且参数支持基本类型、List/Map 容器(支持范型)以及实体类。

  1. 在 Unify 中,实体类支持任意嵌套。

  2. 通过 Unify 生成器,interface  中声明的实体类(UniModel)将同时生成 Android(Java)、iOS(Objective-C)、Flutter(Dart)实现代码,在任意一端下,开发者都以同样方式使用实体类,由 Unify 实现底层序列化、反序列化及透传。

Step2:执行 Unify 生成器

接口声明完成后,执行如下命令生成跨平台代码:

flutter pub run unify api\--input=`pwd`/interface \--dart_out=`pwd`/lib \--java_out=`pwd`/android/src/main/java/com/example/uninativemodule_demo \--java_package=com.example.uninativemodule_demo \--oc_out=`pwd`/ios/Classes \--dart_null_safety=true \--uniapi_prefix=UD

在命令中,指定了 interface 接口目录,Android、iOS 输出位置等配置信息。

在 2.1 节中说到,对于 UniNativeModule,将会生成两部分代码:

  1. 实现注入接口:

  1. Android:DeviceInfoService.java、DeviceInfoServiceRegister.java

  2. iOS:DeviceInfoService.h、DeviceInfoService.m

  1. 三端统一调用接口:

  1. Flutter:main.dart

  2. Android:MainActivity.java

  3. iOS:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo

值得一提的是:

  1. 除了 Flutter 调用接口外,Unify 也会在 Android 和 iOS 工程内分别以 Java、Objective-C 生成双端调用接口。供开发者在任何一端下,都可以用同样的方法、同样的实体类进行调用。这对于跨端场景下的代码一致性来说,意义是巨大的,避免了跨端多技术栈下,模块抽象不一致的问题。

  2. 本例是将原生模块导入 Flutter,使用 UniNativeModule,在原生侧提供实现注入接口。如果是将 Flutter 模块导入原生,则使用 UniFlutterModule,将在 Flutter 侧提供注入接口。不论是 UniNativeModule 还是 UniFlutterModule,除了注入接口有区别外,上层的三端统一调用接口是完全一致的,这也体现了 Unify 平台无关的模块抽象的思想,这对于混合栈下的架构分层至关重要。

Step3:注入原生实现

有了实现注入接口,开发者根据接口分别补充 Android、iOS 端实现。关键代码如下:

Android 实现

public class DeviceInfoServiceImpl implements DeviceInfoService {@Overridepublic void getDeviceInfo(Result<DeviceInfoModel> result) {DeviceInfoModel model = new DeviceInfoModel();......result.success(model);}
}

iOS 实现

// DeviceInfoServiceVendor.h
@interface DeviceInfoServiceVendor : NSObject<DeviceInfoService>
@end// DeviceInfoServiceVendor.m
@implementation DeviceInfoServiceVendor
UNI_EXPORT(DeviceInfoServiceVendor)
......
#pragma mark - DeviceInfoService协议 实现
- (void)getDeviceInfo:(void(^)(DeviceInfoModel* result))success fail:(void(^)(FlutterError* error))fail {DeviceInfoModel *model = [DeviceInfoModel new];......success(model);
}
@end

对于完整代码,可参见文末「阅读原文」:

  • Android 平台实现:DeviceInfoServiceImpl.java

  • Android 平台注册实现:MainActivity.java

  • iOS 平台实现类:DeviceInfoServiceVendor.h、DeviceInfoServiceVendor.m

  • iOS 平台注册实现:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo

Step4:在 Flutter 中调用

一切就绪! 在 Flutter 代码中,现在可以直接调用 Unify 封装的原生模块了:

模块调用

OutlinedButton(child: const Text("获取设备信息"),onPressed: () {DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {setState(() {_platformVersion = "\n${deviceInfoModel.encode()}";});});},
),

效果截图

outside_default.png

至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!

小结

通过这个示例,我们体验了 Unify 带来的价值:

  1. 统一模块声明: 在任何平台下,统一的模块接口声明,避免实现不一致

  1. UniModel: 支持跨平台透明传输的数据模型

  1. 相比 Flutter 原生 Channel 方式:

  1. 避免手动解析参数易出错

  2. Android、iOS 双端自动对齐

  3. 大量 Channel 自动生成,易于维护

  4. 复杂实体无缝序列化,降低管理成本

我们总结了如下决策流程,方便大家根据场景需要,选择 UniNativeModule、UniFlutterModule:

outside_default.png

Unify 核心原理

Unify 之所以能提升跨端通信的开发效率,关键在于 Unify 实现了一套多语言代码生成器,通过该生成器,能够自动解析开发者声明的 Dart 抽象接口,并自动生成三端注入、调用代码,将开发者从繁重的胶水代码中解脱出来。在本节中,介绍 Unify 底层代码生成原理,并介绍与同类方案的对比。

Dart 代码静态分析

我们选择 Dart 语言作为模块接口声明语言,并基于 Dart Analyzer 库,实现对接口声明的静态分析,将 Dart 源代码转换为 Dart AST。在 Unify 中,我们基于 Dart AST 定义了 Unify AST,这是一套适用于模块导出场景的简化 AST,特色为内置了对多语言(Java、Dart、Objective-C)代码生成的映射关系,保证了后续多语言代码生成器实现的简洁。

从开发者接口声明,通过 Dart Analyzer 库静态分析,到产出 Unify AST 的整体流程如下:

outside_default.png

Unify 多语言代码生成器

基于这套 Unify AST,Unify 自研了一套多语言代码生成器,能够基于一套 AST 同时生成多端、多语言代码(Java、Dart、Objective-C),这也是 Unify 高效开发的关键。

在 Unify AST 中,我们抽象了多种抽象语法节点,每种节点中,都包含对多种语言的生成映射关系:

Unify AST

outside_default.png

Unify AST 节点多语言映射

outside_default.png

基于 Unify AST,以 UniModel 为例,开发者声明的 UniModel 将被转换为 Model AST 实例:

outside_default.png

有了 Model AST,Unify 声明了 UniModel 在多端下的生成代码模板。在 Unify 中,我们自研了一套类似于 Flutter 组件化的代码生成模板语法,相较于其它框架手动拼接字符串的方式,Unify 代码生成模板结合 Unify AST 具备更高的模版编写效率,同时代码质量和可维护性更高。以 UniModel 为例,部分模版如图:

outside_default.png

Unify 代码生成器的作用是将 UniModel 的 Model AST 与各技术栈下的生成模版相结合,从而生成 UniModel 在各平台下的多语言实现。最终的生成代码如图:

outside_default.png

同类方案对比

Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。同时,Unify 也逐渐演化出自身特色,比如参数支持任意嵌套的实体类、集合类范型,以及贴近 Flutter 开发者的纯 Dart 语言的接口声明方式。

Unify 还支持批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。外卖大规模 Flutter 落地之初,面临数10+基础能力的批量导出,如果逐个搭建 Git 库导出,维护成本和导出成本过高。基于 Unify 的批量导出能力,我们在短时间内完成了对平台能力的批量封装。

基于前文的使用介绍、原理介绍,相信大家对 Unify 有了深入的了解。在本节中,我们将 Unify 与其它同类框架对比,帮助大家选型、决策。

outside_default.png

通过对比可以看出,不同方案各有特色,适合于不同的场景。概括来说,如果业务中有大量封装导出场景,Unify 能够实现更高的批量导出效率,同时保持了较低的工程复杂度,易于维护。如果是对单模块进行封装导出,或者需要支持更多语言,尤其是 C++ 封装支持,Pigeon 则是较好的选择。

Unify 业务最佳实践

在滴滴国际化外卖业务 Flutter 大规模落地的初期,面临十余个公司平台能力 SDK 需要导出的 Flutter 侧,同时业务中存在大量混合通信,需要保证高可靠性。基于这一背景,在调研已有方案后,我们自研了 Unify,解决了大量模块的批量导出问题。并且在此过程中,我们沉淀出两套架构模式 UniFoundation 和 UniBusiness,成为业务混合通信最佳实践。

UniFoundation 是我们基于 Unify,高效完成公司16+ 个 SDK 批量导出,形成一套能够在 Android、iOS、Flutter 三端统一调用的基建能力。UniFoundation 是一套可复用基建,支撑了国际化外卖商家端、用户端、骑手端三端 Flutter 大规模落地。同时,作为通用基建,UniFoundation 成功推广到国际化出行司机端,助力兄弟业务的 Flutter 大规模落地,并实现跨团队合作共建。

在 UniFoundation 落地之后,在各端业务中,也存在大量业务模块与 Flutter 之间混合通信的场景,于是我们沿用 UniFoundation 的模式延伸出 UniBusiness。UniBusiness 是业务端内部,基于 Unify 批量抽象出的平台无关的业务模块,能够在三端,以统一的方式实现模块调用、复杂实体透传。随着 Flutter 落地规模的扩大,有越来越多业务模块由 Flutter 实现,并经过 Unify 封装,实现三端统一调用。

UniFoundation 和 UniBusiness 在业务中多端落地如图所示:

outside_default.png

落地收益

滴滴国际化外卖业务包含用户端、骑手端、商家端三端,目前均已实现 Flutter 大规模业务落地,并且 Flutter 均已覆盖各端核心主流程,实现跨端复用,整体交付提效 50%+,收益显著,并且是一项持续性提效的收益。其中,国际化外卖骑手端 90%+ 以上代码均为 Flutter 跨端实现,已线上稳定运行两年多时间。

目前,Unify 已成为滴滴 DiFlutter 技术体系的核心架构组件之一,稳定支撑着各端业务,并在业务中大量使用,解决了基础模块、业务模块的混合通信问题,彻底解决了由 Channel 通信导致的参数手动解析错误、Android/iOS 双端接口抽象不一致等问题。

滴滴国际化外卖 Flutter 部分业务落地场景展示:

outside_default.png

outside_default.png

总结与未来展望

滴滴国际化外卖在完成大规模 Flutter 跨端落地之后,我们意识到 Flutter 跨端仍然存在进一步提效空间,目前在向纯 Flutter 化方向演进。对于未来 Unify 的演进,我们希望将 Unify 打造成一套 Flutter 混合开发领域的标准化解决方案,帮助业务解决 Flutter 大规模落地过程中的痛点难点问题。

目前,Unify 已经完成混合通信能力的沉淀,未来我们将持续迭代,提供更多功能,让跨端混合通信开发更加高效、可靠。今年上半年,我们也调研了 Flutter PlatformView 嵌原生能力,目前 Unify 正在提供一套基于嵌原生的混合路由方案,解决大规模 Flutter 落地场景下的混合页面跳转问题。

新的混合路由相较于业界已有方案,更加轻量化,大幅降低复杂度。我们希望这套路由能够助力业务,向纯 Flutter 化方向演进、过渡。经过多年验证稳定后,我们也荣幸得将 Unify 作为滴滴官方开源项目,将这套实践分享给业内同行。欢迎大家试用、体验、star 支持!

国际化外卖技术团队正在招聘服务端高级开发工程师、高级数据研发工程师,感兴趣的小伙伴欢迎联系ginasun@didiglobal.com,期待你的加入!


更多项目开源信息,欢迎点击「阅读原文」了解!

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

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

相关文章

C语言-使用数组法,指针法实现将一个5X5的矩阵中最大的元素放在中心,四个角分别放四个最小的元素(顺序为从左到右,从上到下,从小到大存放),写一函数实现之。

1.题目要求&#xff1a; 将一个5X5的矩阵中最大的元素放在中心&#xff0c;四个角分别放四个最小的元素&#xff08;顺序为从左到右&#xff0c;从上到下&#xff0c;从小到大存放&#xff09;&#xff0c;写一函数实现之。 2.数组法实现 #define _CRT_SECURE_NO_WARNINGS 1…

python从入门到精通:数据容器

数据容器介绍 一种可以容纳多份数据的数据类型&#xff0c;容纳的每一份数据称之为一个元素&#xff0c;可以是任意类型的数据&#xff0c;如字符串、数字、布尔等。 数据容器根据特点的不同&#xff0c;如&#xff1a; 是否支持重复元素 是否可以修改 是否有序&#xff0…

数据库根据日期统计SQL编写记录

统计每天产生多少数据 SELECT to_char("日期字段",YYYY-MM-dd) time ,count(*) FROM "表名" GROUP BY time;统计每月产生多少数据 SELECT to_char("日期字段",YYYY-MM) time ,count(*) FROM "表名" GROUP BY time;统计每年产生多少数…

【GitLab】使用 Docker 安装 GitLab 1:配置 SSH 端口

使用 Docker 安装 GitLab 要求修改ssh端口 GitLab 使用 SSH 通过 SSH 与 Git 交互。默认情况下,GitLab 使用端口22。 要在使用 GitLab Docker 映像时使用其他端口,您可以执行以下操作之一: 更改服务器的 SSH 端口(推荐)。 更改 GitLab Shell SSH 端口。 更改服务器的 SSH …

C++(week17): C++提高:(七)workflow

文章目录 一、Http协议二、Nginx1.概念2.nginx的安装和部署(1)安装nginx(2)使用nginx服务器部署静态资源 3.HTTP服务器架构(1)基于进程、基于线程&#xff1a;APache(2)事件驱动模型&#xff1a;Nginx(3)反向代理(4)负载均衡 三、workflow异步事件引擎 (异步回调模型)1.服务器底…

Qt找不到QSound头文件,头文件QSoundEffect

目录 Qt找不到QSound头文件 CMake qmke 可能版本不同更新&#xff0c; 找不到QSound头文件 Qt找不到QSound头文件 #include <QSound> CMake 可以看一下这篇来自网上的文章 CMake中添加Qt模块的合理方法 - wjbooks - 博客园 (cnblogs.com) qmke 打开.pro文件&am…

[linux#39][线程] 详解线程的概念

线程&#xff1a;是进程内的一个执行分支。线程的执行粒度比进程要细 什么是线程&#xff1f; • 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程 是“一个进程内部的控制序列” • 一切进程至少都有一个执行线程 • …

BF算法,KMP算法

前言&#xff1a;今天我们来学习两种算法&#xff0c;BF算法和KMP算法。相信会让许多小伙伴们打开新世界的大门。 1 BF算法 实践是检验真理的唯一标准。举一个例子说明BF算法。现在我们要在一个主串中找子串的位置。那我们该如何解决这个问题呢&#xff1f;最简单的办法自然是…

Repeat方法:取模运算教材与Unity控制台输出数值不同的原因

学习该知识点的参考教材&#xff1a;Unity API解析/陈宏泉编著.——北京&#xff1a;人民邮电出版社&#xff0c;2014.9 编辑脚本的环境&#xff1a;Visual Studio 2022 在学习该本教材的第五章Mathf类的内容&#xff0c;通过跟随教材上的代码了解不同UnityAPI的具体用法时&a…

【机器学习】线性回归与逻辑回归的极致解析:从数学理论到实战案例

文章目录 1. 引言Python 代码示例 2. 线性回归2.1 线性回归的基本概念线性回归的定义数学表达式及模型假设 2.2 线性回归的工作原理最小二乘法&#xff08;Ordinary Least Squares, OLS&#xff09;梯度下降法在线性回归中的应用多元线性回归与一元线性回归的区别与联系 2.3 线…

什么是AR、VR、MR、XR?

时代背景 近年来随着计算机图形学、显示技术等的发展&#xff0c;视觉虚拟化技术得到了广泛的发展&#xff0c;并且越来越普及化&#xff0c;慢慢的也走入人们的视野。目前市场上视觉虚拟化技术的主流分为这几种 VR、AR、MR、XR。这几项技术并不是最近才出现的&#xff0c;VR的…

机器学习笔记 第十二章计算学习理论

12.1 基础知识 计算学习理论就是关于机器学习的理论基础&#xff0c;其作用就是分析学习任务的困难实质&#xff0c;通过分析结果来知道算法设计&#xff0c;并为学习算法提供理论保证。 给定样例集&#xff0c;&#xff0c;假设为二分类问题&#xff0c;一般。假定中的所有样…

GoMail发送邮件的性能优化策略有哪些方法?

GoMail发送邮件如何配置服务器&#xff1f;GoMail发信功能如何&#xff1f; GoMail是一款广受欢迎的Go语言邮件发送库&#xff0c;具备高效、易用等优点&#xff0c;但在高并发场景下&#xff0c;GoMail发送邮件的性能优化显得尤为重要。AokSend将探讨几种有效的GoMail发送邮件…

PDF Shaper Ultimate v14.4 中文授权版

如今对PDF处理的软件很多都是只是单一的功能。PDF Shaper给你完全不同的体验&#xff0c;因为PDF Shaper是一款PDF工具集合的软件。有了PDF Shaper&#xff0c;你以后再也不用下载其他处理PDF的软件了。PDF Shaper的功能有&#xff1a;合并&#xff0c;分割&#xff0c;加密和解…

贪吃蛇(C语言详解)

贪吃蛇游戏运行画面-CSDN直播 目录 贪吃蛇游戏运行画面-CSDN直播 1. 实验目标 2. Win32 API介绍 2.1 Win32 API 2.2 控制台程序&#xff08;Console&#xff09; 2.3 控制台屏幕上的坐标COORD 2.4 GetStdHandle 2.5 GetConsoleCursorlnfo 2.5.1 CONSOLE_CURSOR_INFO …

ant design pro access.ts 是如何控制多角色的权限的

ant design pro 如何去保存颜色ant design pro v6 如何做好角色管理ant design 的 tree 如何作为角色中的权限选择之一ant design 的 tree 如何作为角色中的权限选择之二 看上面的图片&#xff0c;在前端中如何控制这些权限&#xff0c;比如控制按钮的显示&#xff0c;还有菜单…

【Linux操作系统】进程控制

目录 一、进程创建1.1 认识fork1.2 写时拷贝 二、进程终止2.1 进程退出2.2 函数退出2.3 exit 三、进程等待四、程序替换 一、进程创建 1.1 认识fork fork函数是系统调用接口&#xff0c;用来创建子进程的 根据进程的pid&#xff0c;可以看出父进程fork后分为父进程和子进程…

找到你的任务管理伙伴:待办事项软件终极指南

国内外主流的10款待办事项管理软件对比&#xff1a;PingCode、WorktileTodoist、TickTick、Teambition、 Microsoft To Do、. Asana、Tower、番茄ToDo、飞书。 在面对日益复杂的工作和个人任务时&#xff0c;找到一款能够有效帮助你管理日常待办事项的软件&#xff0c;变得越来…

[RCTF2015]EasySQL1

打开题目 点进去看看 注册个admin账户&#xff0c;登陆进去 一个个点开看&#xff0c;没发现flag 我们也可以由此得出结论&#xff0c;页面存在二次注入的漏洞&#xff0c;并可以大胆猜测修改密码的源代码 resoponse为invalid string的关键字是被过滤的关键字&#xff0c;Le…

blender内置树木的UV

之前听说用这个内置插件可以创建树木 我这边的默认配置出来的树木长这样&#xff0c;不得不想一下&#xff0c;他的uv怎么展&#xff0c;看起来好多树 我尝试看了一眼默认的UV 结果如下 好像每个树枝都已经是平铺的样子了&#xff0c;那么如果需要改的话&#xff0c;就根据…