开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

记一次多租户项目的后端代码重构优化 1、背景 2、重新梳理设

发表于 2024-02-18

1、背景

分享一次代码优化的案例,一个多租户项目,基于数据库字段进行租户隔离,多租户之间用的同一个后端服务,使用MVC架构,在经过数次迭代之后,逐渐发现该项目已存在严重问题:租户之间业务代码耦合度太高。

  1. 虽然租户之间大多业务场景类似,但也有不同,用if-else判断租户进行不同的业务逻辑处理。
  2. 增加测试成本,某一租户的业务逻辑需要修改,同时使用到这一段代码的所有租户都需要进行测试。
  3. service臃肿,一个service 1000多行代码,包含了所有租户的业务逻辑,不利于后续维护和扩展。

2、重新梳理设计

  1. 在DDD领域驱动设计中,负责处理业务基本规则的是领域层,而不是应用层,我们可以借鉴这种思想,把各个租户的业务逻辑内聚在各自的租户下,做到各个租户之前代码的相互隔离,互不影响。
  2. 根据依赖倒置原则,抽象不应该依赖细节,细节应该依赖抽象,对于一些租户公共的业务逻辑行为,提取为抽象类,具体的细节差异由各个租户各自实现。
  3. 工厂模式,根据不同的租户拿到对应的租户对象,进行对应的业务处理逻辑。
    image.png

image.png

3、重构代码示例

controller

1
2
3
4
5
6
java复制代码@PostMapping("/infoByNamePage")
@ApiOperation(value = "分页查询", notes = "分页查询")
public R<Page<OrderInfoPageVO>> infoByNamePage(@Valid @RequestBody OrderInfoVoRequest orderInfoVoRequest) {
OrderAction orderAction = OrderFactory.getOrderAction(CurrentUserUtil.getTenantId());
return orderAction.infoByNamePage(orderInfoVoRequest);
}

其中OrderAction是顶层接口,拥有租户所有的业务行为

OrderFactory工厂实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public class OrderFactory implements ApplicationContextAware {
// 租户A
private static final String ORDER_TENANT_A = "A";
// 租户B
private static final String ORDER_TENANT_B = "B";

@Autowired
private OrderTenantConfig config;

private static Map<String, OrderAction> orderActionMap = new HashMap<>(2);

/**
* 根据租户id获取对应租户
**/
public static OrderAction getOrderAction(String tenantId) {
OrderAction orderAction = orderActionMap.get(tenantId);
if (ObjectUtil.isEmpty(orderAction)){
throw new OrderBusinessException(OrderCodeEnum.TENANT_ID_IS_NULL);
}
return orderAction;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// AOrderAction、BOrderAction 两个租户各自的实现类
orderActionMap.put(config.getMap().get(ORDER_TENANT_A), applicationContext.getBean(AOrderAction.class));
orderActionMap.put(config.getMap().get(ORDER_TENANT_B), applicationContext.getBean(BOrderAction.class));
}
}

image.png
重构之后,1000多行的service业务逻辑主要分布在了AbstractOrderAction、AOrderAction与BOrderAction三个类中,AbstractOrderAction也500多行代码,各自租户之间完成了解耦,更利于后续的维护和扩展。

4、思考与总结

这种设计并不适用于业务场景复杂且过多的场景

其中OrderAction顶层接口中包含了所有业务行为,如果业务一旦复杂过多,会导致AbstractOrderAction抽象类过度臃肿,进而又形成了一个1000多行的’service’,只能再度进行拆分重构。总之根据项目业务的体量做最合适的设计。没有最好的,只有最合适的,最合适的也就是最好的

若有错误,还望批评指正

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

遍历用for还是foreach?

发表于 2024-02-18

遍历用for还是foreach?这篇文章帮你轻松选择!

在编程的世界里,我们经常需要对数据进行循环处理,常用的两种方法就是:for循环和foreach循环。想象你站在一条装满宝贝的传送带前,你要亲手检查每一件宝贝。使用for循环就像是你亲手控制传送带的速度和方向,而使用foreach循环则是传送带自动运转,你只需专注于宝贝本身。好,下面就让我们一步步深入了解下这两种方法吧!

应用场景

for循环:好比你手握一张购物清单(索引),按照顺序逐项挑选商品。在数组、列表等数据结构中,for循环通过下标访问元素。这意味着,当你需要特定的遍历顺序,或者想要在循环中更改计数器时,for循环就是你的菜。

foreach循环: 更像是一个自动售货机,你只需站在出口等待,它会按顺序一个个送出商品。foreach适用于不需要关心索引,仅需遍历并处理每个元素的情况。尤其在处理集合类时,foreach更显简洁高效。

使用方法

我们用一个例子来感受一下for和foreach吧。假设你是个游戏玩家,你有一排宝箱需要打开。

for循环的使用:在for循环中,通常会定义一个迭代变量,并指定迭代变量的初始值、循环条件和迭代变量的更新方式,在循环体中根据索引值访问数组或列表中的元素。

1
2
3
4
5
ini复制代码let treasureChests = ['金币', '宝石', '魔法药水', '地图', '钥匙'];
// 使用for循环打开每个宝箱
for (let i = 0; i < treasureChests.length; i++) {
openChest(treasureChests[i]); // 打开宝箱
}

在这段JavaScript代码里,i就像是你手里的遥控器,从0开始按,一直按到最后一个宝箱。

foreach循环的使用:foreach简化了迭代过程,不需要显式地定义迭代变量和更新迭代变量,也就是无需手动管理索引,编译器会自动帮我们完成元素的迭代获取。

1
2
3
scss复制代码treasureChests.forEach((chest) => {
openChest(chest); // 打开宝箱
});

这里的foreach循环直接告诉你“这是个宝箱”,然后你就打开它。注意,我们这里没有使用索引,它是自动遍历数组中的每个元素。

注意在大多数现代编程语言中,foreach 循环(或其等效的遍历结构)设计的初衷是用来读取集合中的元素,而不是用于修改集合本身,因此我们无法在 foreach 循环中直接更改集合中对象的引用,但是我们可以修改对象中的属性。

C#的例子:

1
2
3
4
5
ini复制代码foreach (var item in collection)
{
item.Property = newValue; // 允许修改对象的属性
// item = new Object(); // 错误!不允许修改对象的引用
}

还需要注意如果集合中的元素是值类型或者基本数据类型,如int、double、string等,当你在foreach循环中迭代时,由于每次迭代获取的是该元素的一个副本,因此直接修改这个副本不会影响原数组中的元素。

1
2
3
4
5
6
7
ini复制代码let numbers = [1, 2, 3];

numbers.forEach(item => {
item = 4; // 这不会改变原始数组
});

console.log(numbers); // [1, 2, 3]

底层原理

for循环像是有条不紊的工厂流水线。在每次迭代中,都有一个明确的开始(初始化表达式),一个持续条件(条件表达式),和一个精确的进度控制(迭代表达式)。这个流水线会在你设定的条件下反复运转,直到任务完成。

foreach循环则更像是智能的机器人,它内置了遍历的逻辑。在像Java、C#这样的语言中,foreach循环背后是基于Iterable接口的。只要集合实现了Iterable接口,就可以用foreach来遍历。机器人(foreach循环)会自动调用集合的iterator方法,获取一个迭代器,然后通过这个迭代器遍历集合中的每个元素。

编程思想

for循环体现的是一种经典的命令式编程思想,它关注如何通过明确的步骤去解决问题。你需要告诉程序每一个要执行的动作,这种方式给予了程序员高度的控制权,但同时也增加了复杂性和出错的可能性。

foreach循环则是声明式编程的体现,更关注做什么而不是怎么做。你只需要声明你的需求(遍历集合),具体的遍历逻辑则被抽象掉了。这使得代码更简洁,也更易于阅读和维护,但牺牲了一些控制力。

执行效率

有的同学可能对性能比较关心。就执行速度而言,for 和 foreach 循环的效率差异通常是微不足道的,特别是在现代编译器和解释器优化的情况下。但是,还是有一些细微的差别:

  • for循环:在某些情况下,for 循环可能略微更快,因为它的控制结构很简单(通常是一个索引和一个结束条件的比较)。如果你在循环中需要使用索引,或者你需要逆序遍历,或者以非标准的步长遍历,使用 for 循环可以直接满足这些需求而无需额外的计算或间接的访问。
  • foreach循环:foreach 循环通常提供了对集合的简化访问,隐藏了迭代的细节。在一些语言中,foreach 循环背后可能使用了迭代器或者其他机制,这可能引入了轻微的性能开销。不过,对于只读操作或者不需要索引的情况,这个开销通常是可以忽略不计的。

在实际应用中,除非你正在编写非常性能敏感的代码,否则循环的选择应该更多地基于代码的清晰度和可维护性,而不是微小的性能差异。在大多数情况下,foreach 循环提供了更简洁、更易读的代码,尤其是当遍历集合而不需要索引时。

总结

for循环就像是多功能的瑞士军刀,适合于那些需要精确控制循环过程的场景。你可以自由地选择起点和终点,甚至可以逆向遍历或调整步长。

foreach循环则像是专一的榔头,对于简单地遍历集合来说,使用起来既快捷又高效。它让你免去了处理索引的烦恼,让你可以专注于元素本身。

编程不仅仅是关于写代码,更是关于选择合适的工具来解决问题。for和foreach就像是你工具箱里的两把锤子,它们各有所长,懂得在不同的情境下选择合适的一把,能让你的编程之路更加顺畅。

关注萤火架构,加速技术提升!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

糟糕,接口被刷了,怎么办?

发表于 2024-02-16

前言

在面试时,经常会被问一个问题:如何防止别人恶意刷接口?

这是一个非常有意思的问题,防范措施挺多的。今天这篇文章专门跟大家一起聊聊,希望对你会有所帮助。图片

1 防火墙

防火墙是网络安全中最基本的安全设备之一,主要用于防止未经授权的网络访问和攻击。

防火墙可以防止的攻击行为包括:

  • 无效数据包:防火墙可以识别和过滤掉无效的数据包,如错误的 IP 地址、伪造的数据包和无法识别的协议等。
  • DOS 和 DDOS 攻击:防火墙可以使用不同的技术来检测和阻止 DOS 和 DDOS 攻击,如阻止大量 TCP/UDP 连接、IP 地址过滤和流量限制等。
  • 病毒和蠕虫攻击:防火墙可以使用特定的病毒和蠕虫检测技术,如签名检测、行为检测、模式识别等,来防止这些恶意软件的传播。
  • 网络钓鱼和欺骗攻击:防火墙可以检测和防止网络钓鱼和欺骗攻击,如防止虚假登录页面和欺骗的网站等。
  • 恶意流量攻击:防火墙可以检测和防止恶意流量攻击,如过滤掉带有恶意载荷的数据包和防止被黑客利用的端口。
  • 网络侦察攻击:防火墙可以使用一些技术来防止网络侦察攻击,如防止扫描、端口扫描和漏洞利用等。

防火墙主要用于过滤和控制网络流量,以保护网络安全。

2 验证码

对于一些非常重要的接口,在做接口设计的时候,要考虑恶意用户刷接口的情况。

最早的用户注册接口,是需要用图形验证码校验的,比如下面这样的:图片用户只需要输入:账号名称、密码和验证码即可,完成注册。

其中账号名称作为用户的唯一标识。

但有些图形验证码比较简单,很容易被一些暴力破解工具破解。

由此,要给图形验证码增加难道,增加一些干扰项,增加暴力破解工具的难道。

但有个问题是:如果图形验证码太复杂了,会对正常用户使用造成一点的困扰,增加了用户注册的成本,让用户注册功能的效果会大打折扣。

因此,仅靠图形验证码,防止用户注册接口被刷,难道太大了。

后来,又出现了一种移动滑块形式的图形验证方式,安全性更高。图片此外,使用验证码比较多的地方是发手机短信的功能。

发手机短信的功能,一般是购买的云服务厂商的短信服务,按次收费,比如:发一条短信0.1元。

如果发送短信的接口,不做限制,被用户恶意调用,可能会产生非常昂贵的费用。

3 鉴权

对于有些查看对外的API接口,需要用户登录之后,才能访问。

这种情况就需要校验登录了。

可以从当前用户上下文中获取用户信息,校验用户是否登录。

如果用户登录了,当前用户上下文中该用户的信息不为空。

否则,如果用户没登录,则当前用户上下文中该用户的信息为空。

对于有些重要的接口,比如订单审核接口,只有拥有订单审核权限的运营账号,才有权限访问该接口。

我们需要对该接口做功能权限控制。

可以自定义一个权限注解,在注解上可以添加权限点。

在网关层有个拦截器,会根据当前请求的用户的权限,去跟请求的接口的权限做匹配,只有匹配上次允许访问该接口。

4 IP白名单

对于有些非常重要的基础性的接口,比如:会员系统的开通会员接口,业务系统可能会调用该接口开通会员。

会员系统为了安全性考虑,在设计开通会员接口的时候,可能会加一个ip白名单,对非法的服务器请求进行拦截。

这个ip白名单前期可以做成一个Apollo配置,可以动态生效。

如果后期ip数量多了的话,可以直接保存到数据库。

只有ip在白名单中的那些服务器,才允许调用开通会员接口。

这样即使开通会员接口地址和请求参数被泄露了,调用者的ip不在白名单上,请求开通会员接口会直接失败。

除非调用者登录到了某一个白名单ip的对应的服务器,这种情况极少,因为一般运维会设置对访问器访问的防火墙。

当然如果用了Fegin这种走内部域名的方式访问接口,可以不用设置ip白名单,内部域名只有在公司的内部服务器之间访问,外面的用户根本访问不了。

但对于一些第三方平台的接口,他们更多的是通过设置ip白名单的方式保证接口的安全性。

5 数据加密

以前很多接口使用的是HTTP(HyperText Transport Protocol,即超文本传输协议)协议,它用于传输客户端和服务器端的数据。

虽说HTTP使用很简单也很方便,但却存在以下3个致命问题:

使用明文通讯,内容容易被窃听。不验证通讯方的真实身份,容易遭到伪装。无法证明报文的完整性,报文很容易被篡改。为了解决HTTP协议的这些问题,出现了HTTPS协议。

HTTPS协议是在HTTP协议的基础上,添加了加密机制:

SSL:它是Secure Socket Layer的缩写, 表示安全套接层。TLS:它是Transport Layer Security的缩写,表示传输层安全。HTTPS = HTTP + 加密 + 认证 + 完整性保护。

为了安全性考虑,我们的接口如果能使用HTTPS协议,尽量少使用HTTP协议。

如果你访问过一些大厂的网站,会发现他们提供的接口,都是使用的HTTPS协议。

6 限流

之前提到的发送短信接口,只校验验证码还不够,还需要对用户请求做限流。

从页面上的验证码,只能限制当前页面的不能重复发短信,但如果用户刷新了页面,也可以重新发短信。

因此非常有必要在服务端,即:发送短信接口做限制。

我们可以增加一张短信发送表。

该表包含:id、短信类型、短信内容、手机号、发送时间等字段。图片

有用户发送短信请求过来时:

先查询该手机号最近一次发送短信的记录 如果没有发送过,则发送短信。如果该手机号已经发送过短信,但发送时间跟当前时间比超过了60秒,则重新发送一条新的短信。如果发送时间跟当前时间比没超过60秒,则直接提示用户操作太频繁,请稍后重试。这样就能非常有效的防止恶意用户刷短信的行为。

但还是有漏洞。

比如:用户知道在60秒以内,是没法重复发短信的。他有个程序,刚好每隔60秒发一条短信。

这样1个手机号在一天内可以发:60*24 = 1440 条短信。

如果他有100个手机号,那么一天也可以刷你很多条短信。

由此,还需要限制每天同一个手机号可以发的短信次数。

其实可以用redis来做。

用户发短信之后,在redis中保存一条记录,key是手机号,value是发短信的次数,过期时间是24小时。

这样在发送短信之前,要先查询一下,当天发送短信的次数是否超过10次(假设同一个手机号一天最多允许发10条短信)。

如果超过10次,则直接提示用户操作太频繁,请稍后重试。

如果没超过10次,则发送短信,并且把redis中该手机号对应的value值加1。

短信发送接口完整的校验流程如下:图片

7 监控

为了防止被别人恶意刷接口,对接口的调用情况进行监控,是非常有必要的。

我们的程序中可以将用户的请求记录,打印到相关日志中。

然后有专门的程序,统计用户接口的调用情况,如果发现有突增的流量,会自动发短信或者邮件提醒。

有了监控之后,我们可以及时发现异常的用户请求。

后面可以进行人工干预处理。

最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

图片

进群方式

添加,苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。

8 网关

为了保证我们接口的安全性,可以提供统一的API网关,它可以实现过滤、鉴权、限流等功能。

用户请求我们的API接口时,需要先经过API网关,它转发请求到具体的API接口。图片有了API网关层,可以保护API接口。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Dart 33 发布:扩展类型、JavaScript In

发表于 2024-02-16

参考链接:medium.com/dartlang/da…

跟随 Flutter 3.19 发布的还有 Dart 3.3 ,Dart 3.3 主要包含扩展类型增强,性能优化和 native 代码交互推进,例如本次改进的JavaScript Interop 模型就引入了类型安全,所以这一切都为 WebAssembly 支持铺平了道路。

在《Flutter 2024 路线规划里》 ,Web 平台上未来 CanvasKit 将成为默认渲染,所以未来 Dart 在 Web 上肯定是 Wasm Native 的路线。

扩展类型

扩展类型是一种编译时抽象,它用不同的纯静态接口来 “Wrapper” 现有类型,同时它们也是 Dart 和静态 JS 互操作的主要实现基础,因为它们可以轻松修改现有类型的接口(对于任何类型的相互调用都至关重要),而不会产生实际 Wrapper 的成本。

1
2
3
dart复制代码extension type E(int i) {
// Define set of operations.
}

扩展类型引入了类型的零成本 wrappers,使用它们来优化性能敏感的代码,尤其是在与 native 平台交互时,扩展类型提供了具有特定成员自定义类型的便利性,同时消除了典型的 wrappers 分配开销。

1
2
3
4
5
6
7
8
9
10
dart复制代码extension type Wrapper(int i) {
void showValue() {
print('my value is $i');
}
}

void main() {
final wrapper = Wrapper(42);
wrapper.showValue(); // Prints 'my value is 42'
}

上面的例子实现了一个 Wrapper 扩展类型,但将其用作普通的 Dart 类型,在实际使用里,开发者可以实例化它并调用函数。

这里的主要区别在于 Dart 将其编译为普通 Dart int 类型,扩展类型允许创建具有唯一的成员类型,而无需分配典型 wrappers 类型的间接成本,例如以下例子包装了对应的 int 类型以创建仅允许对 ID 号有意义的操作的扩展类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dart复制代码extension type IdNumber(int id) {
// Wraps the 'int' type's '<' operator:
operator <(IdNumber other) => id < other.id;
// Doesn't declare the '+' operator, for example,
// because addition does not make sense for ID numbers.
}

void main() {
// Without the discipline of an extension type,
// 'int' exposes ID numbers to unsafe operations:
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.

var safeId = IdNumber(42424242);
safeId + 10; // Compile-time error: No '+' operator.
myUnsafeId = safeId; // Compile-time error: Wrong type.
myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
}

因此,虽然 extension members 功能(Dart 2.7 开始)允许向现有类型添加函数和属性,但扩展类型功能也可以执行相同的操作,并且还允许定义隐藏底层表示的新 API。

这对于与 native code 的相互调用特别有用。可以直接使用原生类型,无需创建 Wrapper 和相关间接的成本,同时仍然提供干净的生产 Dart API。

扩展类型与 Wrapper 具有相同的用途,但不需要创建额外的运行时对象,当开发者需要包装大量对象时,Wrapper 这个行为可能会变得昂贵,由于扩展类型仅是静态的并且在运行时编译,因此它们本质上是零成本。

扩展方法(也称为“扩展”)是类似于扩展类型的静态抽象。但是扩展方法是直接向其基础类型的每个实例添加功能;扩展类型不同,扩展类型的接口仅适用于静态类型为该扩展类型的表达式。

默认情况下它们与其基础类型的接口不同。

扩展类型有两个同样有效但本质上不同的核心用例:

  • 为现有类型提供扩展接口,当扩展类型实现其表示类型时,一般可以认为它是“透明的”,因为它允许扩展类型“看到”底层类型。

透明扩展类型可以调用表示类型的所有成员(未重新声明的),以及它定义的任何辅助成员,这将为现有类型创建一个新的扩展接口,新接口可用于静态类型为扩展类型的表达式,例如如下代码里,v1.i 可以正常调用,但是 int 类似的 v2 不可以调用 v2.i:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dart复制代码extension type NumberT(int value)
implements int {
// Doesn't explicitly declare any members of 'int'.
NumberT get i => this;
}
void main () {
// All OK: Transparency allows invoking `int` members on the extension type:
var v1 = NumberT(1); // v1 type: NumberT
int v2 = NumberT(2); // v2 type: int
var v3 = v1.i - v1; // v3 type: int
var v4 = v2 + v1; // v4 type: int
var v5 = 2 + v1; // v5 type: int
// Error: Extension type interface is not available to representation type
v2.i;
}

  • 为现有类型提供不同的接口,不透明的扩展类型(不是 implement 其表示类型)被静态地视为全新类型,与其表示类型不同,所以无法将其分配给其表示类型,并且它不会公开其表示类型的成员,例如 NumberE 不能为 int ,并且 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
dart复制代码extension type NumberE(int value) {
NumberE operator +(NumberE other) =>
NumberE(value + other.value);

NumberE get next => NumberE(value + 1);
bool isValid() => !value.isNegative;
}

void testE() {
var num1 = NumberE(1);
int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.

num1.isValid(); // OK: Extension member invocation.
num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.

var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
var diff2 = num1.value - 2; // OK: Can access representation object with reference.
var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'.

List<NumberE> numbers = [
NumberE(1),
num1.next, // OK: 'i' getter returns type 'NumberE'.
1, // Error: Can't assign 'int' element to list type 'NumberE'.
];
}

另外需要注意,扩展类型是编译时包装构造,在运行时绝对没有扩展类型的踪迹,任何类型查询或类似的运行时操作都适用于表示类型,在任何情况下,扩展类型的表示类型都不是其子类型,因此在需要扩展类型的情况下表示类型不能互换使用。

JavaScript Interop

Dart 3.3 引入了一种与 JavaScript 和 Web 相互调用的新模型,它从一组用于与 JavaScript 交互的新 API 开始:dart:js_interop 。

现在,Dart 开发人员可以访问类型化 API 来与 JavaScript 交互,该 API 通过静态强制明确定义了两种语言之间的边界,在编译之前消除了许多问题。

除了用于访问 JavaScript 代码的新 API 之外,Dart 现在还包含一个新模型,用于使用扩展类型在 Dart 中表示 JavaScript 类型,如下代码就是前面拓展类型的实际使用实例:

1
2
3
4
5
6
7
8
9
dart复制代码import 'dart:js_interop';

/// Represents the `console` browser API.
extension type MyConsole(JSObject _) implements JSObject {
external void log(JSAny? value);
external void debug(JSAny? value);
external void info(JSAny? value);
external void warn(JSAny? value);
}

基于扩展类型的语法比扩展成员允许更多的表达和健全性。这简化了 Dart 中 JavaScript API 的利用,更多详细信息可以查看:dart.dev/interop/js-… 。

改进 browser libraries

从 1.0 版本开始,Dart SDK 就包含了一套全面的 browser libraries,其中包括核心 dart:html 库以及 SVG、WebGL 等库。

改进后的 JavaScript 调用模型提供了重新构想这些库的机会,未来 browser libraries 支持将集中在 package:web上,这简化了版本控制、加速了更新并与MDN资源保持一致,这一系列的改进推动了将 Dart 编译为 WebAssembly。

从今天开始,开启 WebAssembly 的未来

Dart 3.3 为WebAssembly 的 Web 应用奠定基础,虽然 Flutter Web 中的 WebAssembly 支持仍处于试验阶段,但是这对于 Dart 和 Flutter 是明显的方向。

要使用 WebAssembly 在 Web 上运行 Flutter 应用,需要使用新的 JavaScript Interop 机制和 package:web ,旧版 JavaScript 和 browser libraries 保持不变,并支持编译为 JavaScript 代码。但是,如果编译为 WebAssembly 需要迁移,例如:

1
2
3
4
5
dart复制代码import 'dart:html' as html; // Remove
import 'package:web/web.dart' as web; // Add

dependencies:
web: ^0.5.0

更多可见:dart.dev/interop/js-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 2024 路线规划,更多可期待的功能正在路上

发表于 2024-02-16

参考链接:github.com/flutter/flu…

2024 来了,Flutter 3.19 也发布了,目前 Flutter 官方团队也发布了 2024 的规划,而随着 3.19 的发布,目前 Impeller 在 Android 平台已经支持了 Android OpenGL 预览,随着 Impeller 的质量和性能的提升,Impeller 将有较大的计划变动:

  • 今年 Flutter Team 将计划删除 iOS 上的 Skia 的支持,从而完成 iOS 到 Impeller 的完全迁移;
  • 在 Android 上 Impeller 今年预计将完成 Vulkan 和 OpenGLES 支持,预计目标同样是完全抛弃使用 Skia

看来今年 Impeller 有望达到 Flutter 原本 Skia 的可用高度,另外抛弃 Skia 也可以减少生产中的问题回归,就是对于开发者来说,如果还没切换到 Impeller ,这算是一个较大的升级挑战。

另外关于 Material 3 继续支持,也是 2024 的计划之一,从 3.16 开始就是 Material 3 default (M3),从 3.16 开始 MaterialApp 里的 useMaterial3 默认会是 true,但是你是可以直接使用 useMaterial3: false 来关闭,就是未来 Material 2 相关的东西会被弃用并删除。

更多可见:juejin.cn/post/730453…

在 2023 年的时候,Flutter 发布了 Multiple Flutter Views 的支持计划,虽然目前这项支持在 PC 端还没完全落地,但是官方已经计划将这种支持扩展到 Android 和 iOS,同时继续提高 platform views 的性能和实用性,目前 3.19 上很多支持都已经切换到 THLC。

在 iOS 上,3.19 已经开始适配 Apple 官方要求的隐私清单 ,未来将继续支持 Swift Package Manager 等相关标准需要,在 Android 上将启动 Kotlin 构建脚本的支持(kts) 。

另外 Dart 与其他平台代码直接交互的支持一直是 Dart 的核心工作之一,目前 Dart 直接调用 Objective C 已经接近稳定,未来关于 Dart 直接调用 swift/java/kotlin 支持的也将继续推进其稳定性和可用性,相信随着 Native assets 相关的支持的成熟,未来 Dart 直接和原生语言交互的能力会越来越成熟。

在 Web 平台上,2024 将继续推进应用的大小优化,更好用的多线程支持,PlatformView 的支持和应用加载时间的缩减,同时 CanvasKit 将成为默认渲染,这和去年发布的规划一致,详细可以参考去年的《Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC》 ,另外改进文本输入以及研究支持选项 Flutter Web 的 SEO 也在今年的计划里。

这包涵了 Dart 编译为 WasmGC 并支持 Flutter Web 的 Wasm 编译,还有Dart 新的 JS 互操作机制,支持 JS 和 Wasm 编译 相关内容。

另外 Web 还在计划恢复支持网络热重载。

关于桌面端,因为某些众所周知的原因,虽然过去一年没什么大的进展,但是今年还是有相关的推进计划,例如:

  • 推进 macOS 和 Windows 上的 PlatformView 支持,从而实现对 webview 等内容的支持
  • 在 Linux 上的重点将是 GTK4 支持和可访问性
  • 在所有平台上将继续支持来自一个 Dart isolate 的多个视图,最终目标是支持从一个 Widget 树渲染多个窗口。

多窗口问题提了好久,去年《例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher》出来的时候,我还以为马上桌面多窗口支持就要来了,没想到这么一等就是 2024 了。

而在 Dart 语言方便, 2024 首要支持就是Dart 宏(Macros)编程,只有这样 JSON 序列化有救,预计这个能力会在 2024 年交付支持它们的第一阶段,当然,如果出现一些无法解决的架构问题,也可能会放弃这项工作宏,宏支持详细可见:juejin.cn/post/733052…

最后,官方又再次声明, Flutter 仍然不打算投资对代码推送或热更新的内置支持,对于代码推送,推荐可以关注 shorebird.dev,对于 UI 推送(也称为服务器驱动的 UI)相关支持,推荐 rfw 包的实现。

总的来看,Flutter 2024 的核心还是 Impeller 的推进落地,Web 上继续推动 WasmGC 从而实现全新的 Wasm Native 支持,PC 端还是继续填补曾经的大饼,最值得期待的就是 Dart 宏(Macros)编程未来的支持落地了。

那么,2024 的 Flutter 官方计划里,是否符合你的预期呢?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 319 发布,快来看看有什么更新吧? Ge

发表于 2024-02-16

参考链接:medium.com/flutter/wha…

新年假期的尾巴,Flutter 迎来了 3.19 更新,该版本带来了 Gemini(Google’s most capable AI ) 的 Dart SDK,更好控制动画颗粒度的 Widget ,Impeller 的性能增强和 Android 优化支持,deep links 工具支持,Android 和 iOS 上的特定平台新支持,Windows Arm64 支持等等。

普遍优化修复居多。

Gemini Dart SDK

Google 的 AI Dart SDK Gemini 目前已经发布,pub 上的 google_generative_ai 将 Gemini 的生成式 AI 功能支持到 Dart 或 Flutter 应用里,Google Generative AI SDK 可以更方便地让 Dart 开发人员在 App 里集成 LLM 的 AI 能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
dart复制代码import 'dart:io';

import 'package:google_generative_ai/google_generative_ai.dart';

void main() async {
// Access your API key as an environment variable (see first step above)
final apiKey = Platform.environment['API_KEY'];
if (apiKey == null) {
print('No \$API_KEY environment variable');
exit(1);
}
// For text-and-image input (multimodal), use the gemini-pro-vision model
final model = GenerativeModel(model: 'gemini-pro-vision', apiKey: apiKey);
final (firstImage, secondImage) = await (
File('image0.jpg').readAsBytes(),
File('image1.jpg').readAsBytes()
).wait;
final prompt = TextPart("What's different between these pictures?");
final imageParts = [
DataPart('image/jpeg', firstImage),
DataPart('image/jpeg', secondImage),
];
final response = await model.generateContent([
Content.multi([prompt, ...imageParts])
]);
print(response.text);
}

Framework

滚动优化

在 3.19 之前,使用两根手指在 Flutter 列表上进行滑动时,Flutter 的滚动速度会加快到两倍,这一直是一个饱受争议的问题,现在,从 3.19 开始,开发者可以使用 MultiTouchDragStrategy.latestPointer 来配置默认的 ScrollBehavior ,从而让滑动效果与手指数量无关。

ScrollBehavior.multitouchDragStrategy 默认情况下会防止多个手指同时与可滚动对象进行交互,从而影响滚动速度,如果之前你已经依赖老板本这个多指滑动能力,那么可以通过 MaterialApp.scrollBehavior / CupertinoApp.scrollBehavior 去恢复:

1
2
3
4
5
6
7
8
9
10
11
dart复制代码class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like multitouchDragStrategy
@override
MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.sumAllPointers;
}

// Set ScrollBehavior for an entire application.
MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
// ...
);

或者通过 ScrollConfiguration 进行局部配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like multitouchDragStrategy
@override
MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.sumAllPointers;
}

// ScrollBehavior can be set for a specific widget.
final ScrollController controller = ScrollController();
ScrollConfiguration(
behavior: MyCustomScrollBehavior(),
child: ListView.builder(
controller: controller,
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
);

详细可参考:docs.flutter.dev/release/bre…

另外,本次 3.19 还修复了 SingleChildScrollView#136871 和 ReorderableList#136828 相关的崩溃问题,同时 two_dimensional_scrollables 也修复了一些问题,比如在任一方向上正在进行滚动时出现拖动或者点击,scroll activity 将按预期停止。

最后,two_dimensional_scrollables 上的 TableView 控件也进行了多次更新,提供了需要改进,例如添加了对合并单元格的支持,并在上一个稳定版本 3.16 之后适配了更多 2D foundation。

AnimationStyle

来自社区 @TahaTesser 的贡献,现在 Flutter 开发者使用 AnimationStyle ,可以让用户快速覆盖 Widget 中的默认动画行为,就像 MaterialApp 、 ExpansionTile 和 PopupMenuButton :

1
2
3
4
dart复制代码   popUpAnimationStyle: AnimationStyle(
curve: Easing.emphasizedAccelerate,
duration: Durations.medium4,
),
1
2
dart复制代码return MaterialApp(
themeAnimationStyle: AnimationStyle.noAnimation,

SegmentedButton.styleFrom

来自社区成员 @AcarFurkan 的贡献,该静态方式就像其他按钮类型提供的方法一样。可以快速创建分段按钮的按钮样式,可以与其他分段按钮共享或用于配置应用的分段按钮主题。

Adaptive Switch

Adaptive Switch 可以让 Widget 在 macOS 和 iOS 上看起来和感觉是原生的效果,并且在其他地方具有 Material Design 的外观和感觉,它不依赖于 Cupertino 库,因此它的 API 在所有平台上都完全相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
dart复制代码import 'package:flutter/material.dart';

/// Flutter code sample for [Switch.adaptive].

void main() => runApp(const SwitchApp());

class SwitchApp extends StatefulWidget {
const SwitchApp({super.key});

@override
State<SwitchApp> createState() => _SwitchAppState();
}

class _SwitchAppState extends State<SwitchApp> {
bool isMaterial = true;
bool isCustomized = false;

@override
Widget build(BuildContext context) {
final ThemeData theme = ThemeData(
platform: isMaterial ? TargetPlatform.android : TargetPlatform.iOS,
adaptations: <Adaptation<Object>>[
if (isCustomized) const _SwitchThemeAdaptation()
]);
final ButtonStyle style = OutlinedButton.styleFrom(
fixedSize: const Size(220, 40),
);

return MaterialApp(
theme: theme,
home: Scaffold(
appBar: AppBar(title: const Text('Adaptive Switches')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
style: style,
onPressed: () {
setState(() {
isMaterial = !isMaterial;
});
},
child: isMaterial
? const Text('Show cupertino style')
: const Text('Show material style'),
),
OutlinedButton(
style: style,
onPressed: () {
setState(() {
isCustomized = !isCustomized;
});
},
child: isCustomized
? const Text('Remove customization')
: const Text('Add customization'),
),
const SizedBox(height: 20),
const SwitchWithLabel(label: 'enabled', enabled: true),
const SwitchWithLabel(label: 'disabled', enabled: false),
],
),
),
);
}
}

class SwitchWithLabel extends StatefulWidget {
const SwitchWithLabel({
super.key,
required this.enabled,
required this.label,
});

final bool enabled;
final String label;

@override
State<SwitchWithLabel> createState() => _SwitchWithLabelState();
}

class _SwitchWithLabelState extends State<SwitchWithLabel> {
bool active = true;

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 150,
padding: const EdgeInsets.only(right: 20),
child: Text(widget.label)),
Switch.adaptive(
value: active,
onChanged: !widget.enabled
? null
: (bool value) {
setState(() {
active = value;
});
},
),
],
);
}
}

class _SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
const _SwitchThemeAdaptation();

@override
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) {
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return defaultValue;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return Colors.yellow;
}
return null; // Use the default.
}),
trackColor: const MaterialStatePropertyAll<Color>(Colors.brown),
);
}
}
}

详细可见:main-api.flutter.dev/flutter/mat…

SemanticsProperties 可访问性标识符

3.19 里SemanticsProperties 添加了新的可访问性标识符,为 native 可访问性层次结构中的语义节点提供标识符。

  • 在 Android 上,它在辅助功能层次结构中显示为 “resource-id”
  • 在 iOS 上是设置里 UIAccessibilityElement.accessibilityIdentifier

MaterialStatesController

TextField 和 TextFormField 添加了 MaterialStatesController ,因为在此之前,开发者无法确定 TextFormField 当前是否处于错误状态,例如:

  • 它显示错误消息并使用了errorBorder
  • 确定它是否 foucs,但前提是提供自己的 FocusNode

而现在允许开发者提供自己的 MaterialStatesController(类似于ElevatedButton),以便开发者可以完全访问有关这些控件的状态信息。

1
2
3
4
5
6
7
dart复制代码final MaterialStatesController statesController = MaterialStatesController();
statesController.addListener(valueChanged);

TextField(
statesController: statesController,
controller: textEditingController,
)

UndoHistory stack

修复了 undo/redo 历史在日语键盘上可能消失的问题,并使其现在可以在将条目推送到 UndoHistory 堆栈之前对其进行修改。

UndoHistory 是一个提供撤消/重做功能的 Widget,它还具有绑定到特定于平台的实现的底层接口,监听了键盘事件以实现 undo/redo 操作。

从 Flutter 3.0.0 开始,可以将 UndoHistoryController 传递给 TextField它附带了 UndoHistoryValue 。

对于一个非常简单的展示,我将创建一个 UndoHistoryController 实例,将其传递给 TextField,使用 ValueListenableBuilder 监听该实例,并在构建器中的按钮上返回一行以执行撤消/重做操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
dart复制代码import 'package:flutter/material.dart';

/// Flutter code sample for [UndoHistoryController].

void main() {
runApp(const UndoHistoryControllerExampleApp());
}

class UndoHistoryControllerExampleApp extends StatelessWidget {
const UndoHistoryControllerExampleApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
final UndoHistoryController _undoController = UndoHistoryController();

TextStyle? get enabledStyle => Theme.of(context).textTheme.bodyMedium;
TextStyle? get disabledStyle =>
Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
maxLines: 4,
controller: _controller,
focusNode: _focusNode,
undoController: _undoController,
),
ValueListenableBuilder<UndoHistoryValue>(
valueListenable: _undoController,
builder: (BuildContext context, UndoHistoryValue value,
Widget? child) {
return Row(
children: <Widget>[
TextButton(
child: Text('Undo',
style: value.canUndo ? enabledStyle : disabledStyle),
onPressed: () {
_undoController.undo();
},
),
TextButton(
child: Text('Redo',
style: value.canRedo ? enabledStyle : disabledStyle),
onPressed: () {
_undoController.redo();
},
),
],
);
},
),
],
),
),
);
}
}

Engine

Impeller 进度

Android OpenGL 预览

在 3.16 稳定版本中,Flutter 官方邀请了用户在支持 Vulkan 的 Android 设备上试用 Impeller,覆盖了该领域 77% 的 Android 设备,而在过去的几个月里,Flutter 官方团队让 Impeller 的 OpenGL 达到了与 Vulkan 同等的功能,例如添加支持 MSAA。

这意味着几乎所有 Android 设备上的 Flutter 应用都有望支持 Impeller 渲染,除了少数即将推出的剩余功能除外,例如自定义着色器和对外部纹理的完全支持,目前官方团队表示在今年晚些时候 Androd 也会将 Impeller 作为默认渲染器。

另外,Impeller 的 Vulkan 在“调试”构建中启用了超出 Skia 的附加调试功能,并且这些功能会产生额外的运行时开销。因此,有关 Impeller 性能的反馈你许来自 profile 或 release 版本,并且需要包括 DevTools 的时间表以及与同一设备上的 Skia 后端的比较。

路线

在实现了渲染保真度之后,在 Impeller Android 预览期间的主要关注点是性能,另外一些更大的改进也正在进行,例如能够利用 Vulkan subpasses 大大提高高级混合模式的性能。

此外,Flutter 官方还期望渲染策略发生变化,不再总是将 CPU 上的每条路径细分为先模板后覆盖 ,这样的实现将大大降低 Android 和 iOS 上 Impeller 的 CPU 利用率。

最后,Flutter 还期望新的高斯模糊实施能匹配 Skia 实现的吞吐量,并改进 iOS 上模糊的惯用用法。

API 改进

字形信息

3.19 版本包括两个新的 dart:ui 方法:Paragraph 的 getClosestGlyphInfoForOffset 和 getGlyphInfoAt,这两个方式都会返回一个新类型的对象字形信息,包含段落内字符(或视觉上相连的字符序列)的尺寸。

  • Paragraph.getGlyphInfoAt,查找与文本中的代码单元关联的 GlyphInfo
  • Paragraph.getClosestGlyphInfoForOffset,查找屏幕上最接近给定 Offset 的字形的 GlyphInfo

GPU追踪

Metal 下的 Impeller(iOS、macOS、模拟器) 和在支持 Vulkan 的 Android 设备上,Flutter 引擎现在将在调试和 profile 构建中报告时间线中每个帧的 GPU 时间,可以在 DevTools 中的 “GPUTracer” 下检查 GPU 帧时序。

请注意,由于非 Vulkan Android 设备可能会误报其对查询 GPU 计时的支持,因此只能通过在 AndroidManifest.xml 设置标志来启用 Impeller 的 GPU 跟踪:

1
2
3
xml复制代码<meta-data
android:name="io.flutter.embedding.android.EnableOpenGLGPUTracing"
android:value="true" />

性能优化

Specialization Constants

Impeller 添加支持 Specialization Constants ,利用 Impeller 着色器中的这一功能,减少了 Flutter 引擎的未压缩二进制大小 350KB。

Backdrop Filter 加速

3.19 版本包含一些不错的性能改进,其中就包括了 Impeller 的 Backdrop Filter 和模糊优化,特别是开源贡献者 knopp noticed 注意到 Impeller 错误地请求了读取屏幕纹理的功能,删除这个功能支持,在基准测试中,根据复杂程度,将包含多个背景滤镜的场景改进了 20-70%。

同时,Impeller 在每个背景滤镜上不再无条件存储模板缓冲区,相反,任何影响操作的剪辑都会被记录下来,并在恢复背景滤镜的保存层时重播到新的模板缓冲区中。

通过这一更改,在运行具有 Vulkan 的 Impeller 的 Pixel 7 Pro 上进行动画高级混合模式基准测试时,将平均 GPU 帧时间从 55 毫秒改进到 16 毫秒,并将 90% 的光栅线程 CPU 时间从大约 110 毫秒改进到 22 毫秒。

Android

Deeplinking web 验证器

3.19 开始,Flutter 的 Deeplinking web 验证器的早期版本将被推出使用。

在该版本中,Flutter Deeplinking 验证器支持 Android 上的 Web 检查,这意味着可以验证 assetlinks.json 文件的设置。

开发者可以打开DevTools,单击 “Deep Links” 选项,然后导入包含 Deeplinking 的 Flutter 项目,Deeplinking 验证器将告诉你配置是否正确。

Flutter 希望这个工具能够成为简化的 Deeplinking ,后续将继续补全 iOS 上的 Web 检查以及 iOS 和 Android 上的应用检查的支持。

更多可以查阅 :docs.google.com/document/d/…

支持 Share.invoke

Android 平台上 Flutter 之前缺少默认 “share” 按钮,而本次 3.19 里将开始支持它,作为 Flutter 持续努力的一部分,以确保所有默认上下文菜单按钮在每个平台上都可用。

更多相关进度可见:github.com/flutter/flu…

Native assets

如果需要 Flutter 代码中与其他语言的其他函数进行互操作,现在可以在 Android 上通过执行 FFI 来处理 Native assets 。

简单来说就是,在此之前, Dart interop 一直在全面支持与 Java 和 Kotlin 和 Objective C 和 Swift 的直接调用支持,例如在 Dart 3.2 开始,Native assets 就作为实验性测试支持,一直在解决与依赖于 Native 代码的 Dart 包分发相关的许多问题,它通过提供统一的钩子来与构建 Flutter 和独立 Dart 应用所涉及的各种构建需要。

Native Assets 可以让 Dart 包更无缝依赖和使用 Native 代码,通过 flutter run/flutter build 和 dart run/dart build 构建并捆绑 Native 代码 。

备注:可通过 flutter config --enable-native-assets 和 flutter create --template=package_ffi [package name] 启用。

Demo native_add_library 就展示了相关使用,当 Flutter 项目依赖 package:native_add_library 时, 脚本会自动在 build.dart 命令上调用:

1
2
3
4
5
6
7
dart复制代码import 'package:native_add_library/native_add_library.dart';

void main() {
print('Invoking a native function to calculate 1 + 2.');
final result = add(1, 2);
print('Invocation success: 1 + 2 = $result.');
}

更多可见:github.com/flutter/flu…

纹理层混合合成 (THLC) 模式

现在使 Google 地图 SDK 和文本输入框的放大镜功能时,他们都是工作在 TLHC 模式下,这会让 App 的性能得到不错的提升。

自定义 system-wide text selection toolbar 按键

Android 应用可以添加出现在所有文本选择菜单(长按文本时出现的菜单)中的自定义文本选择菜单项, Flutter 的 TextField 选择菜单现在包含了这些项目。

在 Android 上,一般可以编写一个应用,将自定义按钮添加到系统范围的文本选择工具栏上。例如上图这里 Android 应用 AnkiDroid 在文本选择工具栏中添加了 “Anki Card ”按钮,并且它可以出现在任何应用中。

iOS

Flutter iOS 原生字体

Flutter 文本现在在 iOS 上看起来更紧凑、更像 native,因为根据苹果设计指南,iOS 上较小的字体应该更加分散,以便在移动设备上更容易阅读,而较大的字体应该更加紧凑,以免占用太多空间。

在此之前,我们在所有情况下都错误地使用了更小、间距更大的字体,现在默认情况下 Flutter 将为较大的文本使用紧凑字体。

开发工具

开发工具更新

3.19 版本的 DevTools 的一些亮点包括:

● 在 DevTools 中添加了新功能以进行验证 Deeplinking

● 在 “Enhance Tracing” 菜单中添加了一个选项,用于跟踪平台 channel activity,这对于带有插件的应用很有用

● 当没有连接的应用时,性能和 CPU 分析器现在也可以使用,可以重新加载之前从 DevTools 保存的性能数据或 CPU 配置文件

● VS Code 中的 Flutter 侧边栏现在能够在当前项目未启用的情况下启用新平台,并且侧边栏中的 DevTools 菜单现在有一个在外部浏览器窗口中使用 DevTools 的选项

桌面

Windows Arm64 支持

Windows 上的 Flutter 现在开始初步支持 Arm64 架构,目前仍处于开发阶段, 可以在GitHub #62597 上查看进度,虽然目前对于 Flutter 开发者来说可能用处不是特别明显,但是也算是一个难得的桌面增强。

生态系统

隐私清单

Flutter 现在包含 iOS 上的隐私清单以满足即将推出的 Apple 要求 ,所以看来一般情况下,这个 Flutter 3.19 非升不可。

包生态的进展

2023 年,pub package 生态增长了 26%,从 1 月份的 38,000 个 package 增加到 12 月底的 48,000 个,截至 2024 年 1 月,Pub.dev 现在每月活跃用户超过 700,000 名,

弃用和重大变更

放弃 Windows 7 和 8 支持

Dart 3.3 和 Flutter 3.19 版本停止对 Windows 7 和 8 的支持

Impeller Dithering flag

正如 3.16 发布时所说的,现在全局标志 Paint.enableDithering 已经删除。

弃用 iOS 11

Flutter 不再支持 iOS 11,由于调用某些网络 API 时会发生运行时崩溃, Flutter 3.16.6 及更高版本构建的应用将不再支持 iOS11 ,详细可见 :juejin.cn/post/732141…

最后

目前看来 Flutter 3.19 并没有什么大更新,属于季度正常迭代,主要是问题修复和性能高优化的版本,还是老规矩,坐等 3.19.6 版本。

最后,新年快乐~准备开工咯。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【HTML】交友软件上照片的遮罩是如何做的

发表于 2024-02-14

笑谈

我不知道大家有没有在夜深人静的时候感受到孤苦难耐,🐶。于是就去下了一些交友软件来排遣寂寞。可惜的是,有些交友软件真不够意思,连一些漂亮小姐姐的图片都要进行遮罩,完全不考虑兄弟们的感受,😠。所以今天,我们就一起来看看这些软件的遮罩是如何做的,🐶。

调研

市场上这些交友软件比较多,就拿一个我朋友他经常玩的一个软件来研究,叫做《XX之恋》,重申一下,我这里没有任何打广告的嫌疑,毕竟是我朋友玩的,🐶。我们接下来看这软件中遮罩的图片。

注:我实在没有在网上找到该软件这些有遮罩的图片,所以只好从自己的主页上截取了下,如果有当事人认为这是自己的话,请速与我联系,我会及时删除的。

正如上面所见,该软件的遮罩效果还是非常不错的,为什么说非常不错呢?个人认为有两个亮点,🐶保命。

  1. 这个遮罩效果让我们知道对面是女生。
  2. 这个遮罩效果也仅仅只能让我们知道对面是女生。

言归正传,这种效果在我们悠久的前端历史上,有一种专业名词 –> 毛玻璃效果

碎碎念:我看了蛮多毛玻璃的技术文章,这个技术大家说都是为了让能人阅读的时候更赏心悦目,能用来遮小姐姐也算是很不错的创新了。🐶

实现

现在我们只需要两样东西,一个是小姐姐的图片,一个是前端的小知识。我都准备好啦。首先我们先介绍知识点。

backdrop-filter属性

我们看MDN的介绍文档

可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。因为它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。

backdrop-filter有如下常用属性(带了数值,方便理解)

  • 🌟blur(2px): 对元素背后的背景应用2像素的模糊效果。
  • brightness(60%): 将元素背景的亮度调整为原始亮度的60%。
  • contrast(40%): 将元素背景的对比度调整为原始对比度的40%。
  • ……………等

我们主要使用的便是blur属性,对背景图片模糊,达到类似的效果。

我采用的小姐姐图片如下



思路:使用两个Class。第一个Class的背景图片是上面的小姐姐,第二个Class完全覆盖第一个Class,设置blur为10px即可。如下是代码。

效果图如下:


最后,希望大家多多点赞支持,兄弟们的点赞支持是我继续写文章的动力!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

IDEA中这么强大的接口调试插件,相见恨晚啊! 前言 效果图

发表于 2024-02-14

前言

在后端SpringBoo开发中,都需要一个接口测试工具,从一开始的postman,到现在的国产测试工具,数不胜数,而最方便的莫过于在IDEA中就可以调试,因为IDEA插件中有能力分析出当前项目所编写的Controller数据,可以进行统计,更方便我们发起http请求,不需要复制url到别的测试工具了,今天就推荐一款,名叫Cool Request,他纯免费、开源。

  • 插件名称: Cool Request
  • Github: github.com/houxinlin/c…
  • 文档链接: plugin.houxinlin.com
  • 作用: 简化SpringBoot后端接口调试流程。
  • 安装方式: IDEA 插件商店中搜索Cool Request

效果图

Cool Request的界面非常简单,下面是他的主界面,可以收集项目的Controller信息。

image.png

基本HTTP请求

当双击某个Controller时候,会跳转到请求发送界面,在这里可以填写参数,Cool Request和同类的插件相比,在填写Header时候,也是有提示的,而其他没有,这一点非常方便。

image.png

结果预览

Cool Request也有五种不同的响应预览,会自动根据响应头中的Content-Type跳转到不同的预览效果里,有json、text、image、html、xml。

image.png

手动触发SpringBoot中的定时器。

测试定时器时,不知道大家是怎么测试的,以前是通过单独写一个Controller,然后在内部调用一下,而用了Cool Request,居然可以支持手动触发,这样无论定时器的时间间隔是多少,都可以在Cool Request中随心所欲的调用。

image.png

反射调用

这一点可能是有点抽象,不好理解,这个功能只有在特定场景下才能体会到极其方便。

试想一下这个场景。

  1. Controller返回的信息不需要用户信息,也就是一些全局的数据
  2. 但是Controller需要用户登录后才能使用,即需要附带Token
  3. 你的项目具有拦截器,会拦截没有登录的用户

这个时候你有没有想过,我这个Controller又没有用到用户信息,能不能在调试时候绕过拦截器,以前的做法可能是关闭拦截器,或者先登录,正常拿到Token后在调用,而Cool Request的这个功能就是解决这个问题的,他可以通过反射的一系列技术,绕过拦截器,将请求直接到达Controller,并返回结果。

使用方式是在请求界面选择reflex,然后填写参数后发起请求。

image.png

但是reflex也有缺点,他同时也绕过了过滤器,并且没有办法让过滤器也执行,唯一的办法是发起正常的HTTP请求。

强大的前后置脚本

最方便的莫过于Cool Request提供了java的前后置脚本了,也算不上脚本了,习惯这么叫,而其他插件要不就是没有,要不就是提供的JS脚本,对不熟悉JS的人来说,增加了学习成本,而Cool Request就非常方便了,另外在编写代码时,是有语法提示的,例如在脚本中修改参数的api,也不需要过多的学习,几乎是0成本。在handlerRequest方法中,有两个参数,第一个是日志输出接口,可以使用他的println方法输出日志,会在右面的log窗口看到,第二个参数是HTTPRequest,他有一系列方法,使用.号时候就有提示,所有的方法我们一眼就能看出是做什么的。

image.png

更方便的是,脚本中可以调用项目的类,也支持第三方的类,比如SpringBoot内置的一些Util工具类,都可以调用,已经方便到极致了。

但是也有一个小缺点,内置的编译器是java8的,也就是说,项目使用的java版本超过8,或者是第三方库的版本超过java8,就无法调用了,比如项目使用了SpringBoot3,就无法调用,但是都可以在脚本中自己实现。

在一些动态参数时候,非常有用,比如参数的签名值,是通过一些参数计算出来的,或者是动态时间。

支持多种请求体

Cool Request支持六种请求体,也支持二进制文件上传。

image.png

快速导入cURL参数

如果有一个cURL参数(可能是从浏览器中复制过来的),那么可以快速导入的Cool Request里面,同时也支持将请求复制为cURL格式。

image.png

导出到Apifox

如果团队使用了apifox,它还可以支持导出到apifox里面,虽然apifox自己也有插件,但是apifox不提供目录选择,而Cool Request支持,当然需要配置apifox的一些token,这点可以在插件的官网找到详细的说明。

image.png

全局搜索API

可以通过一个api路径来快速找到项目中的Controller位置。

image.png

静态资源服务器

它内置了一个静态资源服务器,使用也非常简单,选择目录、选择端口、开启即可,就可以通过浏览器访问此目录下的文件了。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【最高效编码指南】也许你不会用VSCode IDEA 快

发表于 2024-02-10

快捷键

温馨提示:本文含有大量动图,请确保流量足够

快速打开指定文件

大多数前端开发,在找路由文件时可能时一级一级找,实际上在地址栏复制一下路由名称

按下ctrl + e,再输入路由名称即可打开。IDEA快捷键是双击 shfit

image.png

快捷跳转

当你在看别人代码时,是不是经常 ctrl + 鼠标左键 进去看,然后回来要滑半天?

那你看看我怎么做,是不是瞬间回来

1.gif
这个只需要设置一下快捷键即可,默认是没有的,如下图所示

image.png
变量 | 文件重命名


var.gif

大家设置一下对应的快捷键即可,我下图标红的就是。我设置的和 window 快捷键一样, IDEA 默认是shift + F6

如果你的 window 版本过高,那么shift + F6又会和微软输入法冲突

image.png

需要注意的是,变量重命名需要编辑器分析你的代码。所以如果你的代码写的很烂,那么 VSCode 也会被弄得神志不清

所以这项功能在一些 JS 项目可能没有作用

快捷复制

很多人复制当前行,可能是先选中,然后再 CV,实在是太慢了

VSCode 快捷键是 alt + shfit + ↓,IDEA 是ctrl + d

8.gif

快速移动行

alt + ↓,IDEA 为 ctrl + shift + ↓

PixPin_2024-02-11_11-17-12.gif

删除整行

快捷键是 ctrl + shift + k,这个也能提高很多效率,不用每次都手动删除一行的最后一个空格

9.gif

快速新开一行

假设你的鼠标不在行末,而在中间,如下图。那么你按下回车会破坏你的代码

此时你仅需按下 ctrl + enter 即可新开一行,IDEA 快捷键是 shift + enter

image.png

查看参数

在函数括号内,按下ctrl + shift + space,即可查看参数类型,以及当前参数的注释

注意,微软输入法可能和这个快捷键冲突,你需要手动关闭微软输入法的快捷键,或者修改VSCode

IDEA 快捷键为 ctrl + p

image.png

代码提示

这个在强类型语言上非常实用,以 Typescript 为例。按下 ctrl + i 这个字符串枚举就出来了

image.png

收起/ 打开 (控制台 | 终端)

ctrl + j

快速滑动

按住 alt 再滚动滚轮,可以加快滚动速度。这个速度默认 5 倍速,可以在设置调整。

搜索editor.fastScrollSensitivity

批量操作

假设我从某个地方复制很多变量,我需要批量修改。比如说把 my 改成 test,

此时你可能按下 ctrl + f 批量搜索,再批量修改,而这时我已经改完了

image.png

2.gif

在 VSCode 中,批量选中相同的快捷键是 ctrl + shift + l

我上面就是按下此快捷键批量修改的

批量大小写修改

不仅于此,我们再来发掘一下他的功能,比如批量修改大小写

这个需要手动配置,我用的是ctrl + shift + alt + u

image.png
3.gif

扩选范围

如果你需要复制某一段文字,你可能先按下鼠标左键,然后不松手慢慢拖动选中。

但是这种体验太差了,需要非常精确的操作,能不能根据代码自动扩选的呢?

答案是可以的,不过同样要手动设置快捷键,这里我设置的和 IDEA 一样

4.gif

image.png

双击分割

我敢说下面的双击全选,99.99% 的人的 VSCode 都做不到,而是会被 - 分开

5.gif

仅需设置一下配置即可,按下 ctrl + , 打开配置 editor.wordSeparators,把不要的分隔符删掉即可

image.png

批量移动光标到指定位置

6.gif

下面这一套丝滑小连招是怎么做的呢?

  1. 先按住 alt + shift,即可开启列选择模式
  2. 拖动鼠标选择
  3. ctrl + →,可以快速跳到分隔位置,这里没有被分隔,所以直接到最后了
  4. ctl + shift + 左,快速选中并跳转到分隔位置
  5. 最后执行操作即可

基于上述操作,可以玩出花来,大家可以开动一下脑袋

值得注意的是,ctrl + [shift] + ← 这些快捷键不是编辑器带来的,基本上哪里都能用

快速选中整行

有时候你可能需要选中很长一行,比如打包后的代码,爬到的代码,如果有手拖动那可太折磨人了

于是你可以按下 shift + end 来选中,这个就不放动图,和上面差不多。shift 就是多选的意思

不过有的人的键盘可能没有 end 按键,这时候可以查一下官网,大多数都是配合 FN 键实现

选中指定行

7.gif

按住 alt 不放,加上鼠标点击即可

而且你不必精确选中列的位置,可以用 end | home 回到开头或者结尾,再配合上面的技巧操作

批量修改正则指定内容

按下 ctrl + f,把最右边的 .* 勾选上,就是开启正则的意思

然后再输入表达式,\d 即所有数字,$1即匹配到的第一组

image.png

按下最右边的全部修改,结果就会改成下图,所有数字都会加上 test 后缀

image.png

不止于此,还能整个文件夹批量检索操作

image.png

路径操作

很多人可能苦于导入路径没有智能提示,那是因为你的工程没有配置路径别名

比如 @ = /src,那么你需要创建 tsconfig.json 或者 jsconfig.json

并写入如下配置

1
2
3
4
5
6
7
8
json复制代码{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

最好下个插件 Path Intellisense,然后在配置文件 setting.json 写入

1
2
3
json复制代码"path-intellisense.mappings": {
"@": "${workspaceFolder}/src",
},

JS 导入

当你需要导入模块时,仅需要输入前面几个字母,就有提示导入。

需要注意的是,你最好是具名导出,用默认导出它可能分析不了,因为没有名字

image.png

TS 导入

强类型语言,则多一个修复选项,VSCode 按下 ctrl + .触发,IDEA 按下 alt + enter触发

image.png

相对路径导入

很多人导入可能是手写 import xxx from '@/...'

这样不仅容易错,还很麻烦,实际上可以用快捷键复制一下,如图

image.png

如果你是 window,那么你的斜杠是反的,你需要配置一下explorer.copyRelativePathSeparator

image.png

VScode 还有大量配置,光是 setting.json,我就写了 360 行,以后想起什么再更新吧

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

理想汽车今年的年终奖。。(含算法原题)

发表于 2024-02-07

理想年终奖红包

2月5日,有微博用户发帖称,脉脉上看到,今年理想汽车的年终奖红包有点大。

对此,李想转发并评论:

不能只学华为的流程,而不学华为的利益分配。奖罚不分明,是组织低效的最大原因。有成长、有成就、有回报,三者缺一不可。

由此可见,华为的狼性文化,虽然被无数打工人所诟病,但还是被很多创业公司拿去借鉴的。

更残忍的是,像李想这样有”悟性”的老板还是少数。

大多创始人都是只抄流程,不抄分配。

更甚者,不学分配就算了,PUA 员工的时候还不忘把「像华为、学华为」这样的词挂嘴边。

…

回归主线。

来一道和理想汽车面试相关的算法题。

根据我们统计过的「理想汽车」面试算法题,都是比较简单的。

题目描述

平台:LeetCode

题号:2824

给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 target。

请你返回满足 0 <= i < j < n 且 nums[i] + nums[j] < target 的下标对 (i,j)(i, j)(i,j) 的数目。

示例 1:

1
2
3
4
5
6
7
8
9
scss复制代码输入:nums = [-1,1,2,3,1], target = 2

输出:3

解释:总共有 3 个下标对满足题目描述:
- (0, 1) ,0 < 1 且 nums[0] + nums[1] = 0 < target
- (0, 2) ,0 < 2 且 nums[0] + nums[2] = 1 < target
- (0, 4) ,0 < 4 且 nums[0] + nums[4] = 0 < target
注意 (0, 3) 不计入答案因为 nums[0] + nums[3] 不是严格小于 target 。

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码输入:nums = [-6,2,5,-2,-7,-1,3], target = -2

输出:10

解释:总共有 10 个下标对满足题目描述:
- (0, 1) ,0 < 1 且 nums[0] + nums[1] = -4 < target
- (0, 3) ,0 < 3 且 nums[0] + nums[3] = -8 < target
- (0, 4) ,0 < 4 且 nums[0] + nums[4] = -13 < target
- (0, 5) ,0 < 5 且 nums[0] + nums[5] = -7 < target
- (0, 6) ,0 < 6 且 nums[0] + nums[6] = -3 < target
- (1, 4) ,1 < 4 且 nums[1] + nums[4] = -5 < target
- (3, 4) ,3 < 4 且 nums[3] + nums[4] = -9 < target
- (3, 5) ,3 < 5 且 nums[3] + nums[5] = -3 < target
- (4, 5) ,4 < 5 且 nums[4] + nums[5] = -8 < target
- (4, 6) ,4 < 6 且 nums[4] + nums[6] = -4 < target

提示:

  • 1<=nums.length=n<=501 <= nums.length = n <= 501<=nums.length=n<=50
  • −50<=nums[i],target<=50-50 <= nums[i], target <= 50−50<=nums[i],target<=50

基本分析

为了方便,先对 nums 进行排序。

当 nums 有了有序特性后,剩下的便是「遍历右端点,在右端点左侧找最大合法左端点」或「遍历左端点,在左端点右侧找最大合法右端点」过程。

排序 + 二分

这是一种「遍历右端点,在右端点左侧找最大合法左端点」做法。

遍历右端点 i,然后在 [0,i−1][0, i - 1][0,i−1] 范围内进行二分,找到最大的满足 nums[j]+nums[i]<targetnums[j] + nums[i] < targetnums[j]+nums[i]<target 的位置 j。

若存在这样左端点 j,说明以 nums[i]nums[i]nums[i] 为右端点时,共有 j+1j + 1j+1 个(范围为 [0,j][0, j][0,j] )个合法左端点,需要被统计。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java复制代码class Solution {
public int countPairs(List<Integer> nums, int target) {
Collections.sort(nums);
int n = nums.size(), ans = 0;
for (int i = 1; i < n; i++) {
int l = 0, r = i - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums.get(mid) + nums.get(i) < target) l = mid;
else r = mid - 1;
}
if (nums.get(r) + nums.get(i) < target) ans += r + 1;
}
return ans;
}
}

C++ 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C++复制代码class Solution {
public:
int countPairs(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int n = nums.size(), ans = 0;
for (int i = 1; i < n; i++) {
int l = 0, r = i - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums[mid] + nums[i] < target) l = mid;
else r = mid - 1;
}
if (nums[r] + nums[i] < target) ans += r + 1;
}
return ans;
}
};

Python 代码:

1
2
3
4
5
6
7
8
9
10
11
12
Python复制代码class Solution:
def countPairs(self, nums: List[int], target: int) -> int:
nums.sort()
n, ans = len(nums), 0
for i in range(1, n):
l, r = 0, i - 1
while l < r:
mid = l + r + 1 >> 1
if nums[mid] + nums[i] < target: l = mid
else: r = mid - 1
if nums[r] + nums[i] < target: ans += r + 1
return ans

TypeScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TypeScript复制代码function countPairs(nums: number[], target: number): number {
nums.sort((a,b)=>a-b);
let n = nums.length, ans = 0;
for (let i = 1; i < n; i++) {
let l = 0, r = i - 1;
while (l < r) {
const mid = l + r + 1 >> 1;
if (nums[mid] + nums[i] < target) l = mid;
else r = mid - 1;
}
if (nums[r] + nums[i] < target) ans += r + 1;
}
return ans;
};
  • 时间复杂度:排序复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn);构造答案复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn)。整体复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn)
  • 空间复杂度:O(log⁡n)O(\log{n})O(logn)

排序 + 双指针

这是一种「遍历左端点,在左端点右侧找最大合法右端点」做法。

使用 l 和 r 分别指向排序好的 nums 的首尾。

若当前 nums[l]+nums[r]≥targetnums[l] + nums[r] \geq targetnums[l]+nums[r]≥target,说明此时对于 l 来说,r 并不合法,对 r 自减(左移)。

直到满足 nums[l]+nums[r]<targetnums[l] + nums[r] < targetnums[l]+nums[r]<target,此时对于 l 来说,找到了最右侧的合法右端点 r,在 [l+1,r][l + 1, r][l+1,r] 期间的数必然仍满足 nums[l]+nums[r]<targetnums[l] + nums[r] < targetnums[l]+nums[r]<target,共有 r−lr - lr−l 个(范围为 [l+1,r][l + 1, r][l+1,r] )个合法右端点,需要被统计。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
Java复制代码class Solution {
public int countPairs(List<Integer> nums, int target) {
Collections.sort(nums);
int n = nums.size(), ans = 0;
for (int l = 0, r = n - 1; l < r; l++) {
while (r >= 0 && nums.get(l) + nums.get(r) >= target) r--;
if (l < r) ans += r - l;
}
return ans;
}
}

C++ 代码:

1
2
3
4
5
6
7
8
9
10
11
12
C++复制代码class Solution {
public:
int countPairs(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int n = nums.size(), ans = 0;
for (int l = 0, r = n - 1; l < r; l++) {
while (r >= 0 && nums[l] + nums[r] >= target) r--;
if (l < r) ans += r - l;
}
return ans;
}
};

Python 代码:

1
2
3
4
5
6
7
8
9
10
Python复制代码class Solution:
def countPairs(self, nums: List[int], target: int) -> int:
nums.sort()
n, ans = len(nums), 0
l, r = 0, n - 1
while l < r:
while r >= 0 and nums[l] + nums[r] >= target: r -= 1
if l < r: ans += r - l
l += 1
return ans

TypeScript 代码:

1
2
3
4
5
6
7
8
9
TypeScript复制代码function countPairs(nums: number[], target: number): number {
nums.sort((a,b)=>a-b);
let n = nums.length, ans = 0;
for (let l = 0, r = n - 1; l < r; l++) {
while (r >= 0 && nums[l] + nums[r] >= target) r--;
if (l < r) ans += r - l;
}
return ans;
};
  • 时间复杂度:排序复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn);构造答案复杂度为 O(n)O(n)O(n)。整体复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn)
  • 空间复杂度:O(log⁡n)O(\log{n})O(logn)

我是宫水三叶,每天都会分享算法题解,并和大家聊聊近期的所见所闻。

欢迎关注,明天见。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…575859…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%