注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

面试官: forEach怎么停止

web
介绍 在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的...
继续阅读 »

介绍


在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的解释和实际的代码示例来消除这一概念的神秘感。


在深入探讨之前,请在我的个人网站上探索更多关于 Web 开发的深度文章:


了解 JavaScript 中的 forEach 🤔


JavaScript 的 forEach 方法是迭代数组的流行工具。它为每个数组元素执行一次提供的函数。然而,与传统的 forwhile 循环不同,forEach 旨在为每个元素执行函数,没有内置机制来提前停止或中断循环。


const fruits = ["apple", "banana", "cherry"];
fruits.forEach(function(fruit) {
console.log(fruit);
});

这段代码将输出:


apple
banana
cherry

forEach 的局限性 🚫


1. forEach 中的 break


forEach 的一个关键限制是无法使用传统的控制语句比如 breakreturn 来停止或中断循环。如果您试图在 forEach 内使用 break,将遇到语法错误,因为 break 不适用于回调函数中。


尝试中断 forEach


通常,break 语句用于在满足某个条件时提前退出循环。


const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
if (number > 3) {
break; // 语法错误:非法 break 语句
}
console.log(number);
});

当您试图在 forEach 循环中使用 break 时,JavaScript 抛出一个语法错误。这是因为 break 被设计为在传统循环(如 forwhiledo...while)中使用,在 forEach 的回调函数中不被识别。


2. forEach 中的 return


在其他循环或函数中,return 语句退出循环或函数,如果指定的话返回一个值。


forEach 的上下文中,return 不会跳出循环。相反,它仅仅退出回调函数的当前迭代,并继续下一个数组元素。


尝试返回 forEach


const numbers = [1, 2, 3, 4, 5]; 
numbers.forEach(number => {
if (number === 3) {
return; // 仅退出当前迭代
}
console.log(number);
});

输出


1
2
4
5

在这个例子中,return 跳过了打印 3,但是循环继续剩余的元素。


使用异常中断 forEach 循环 🆕


尽管不建议常规使用,但从技术上来说,通过抛出异常可以停止 forEach 循环。尽管这种方法非正统,一般不建议使用,因为它影响代码的可读性和错误处理,但它可以有效地停止循环。


const numbers = [1, 2, 3, 4, 5];
try {
numbers.forEach(number => {
if (number > 3) {
throw new Error('Loop stopped');
}
console.log(number);
});
} catch (e) {
console.log('Loop was stopped due to an exception.');
}
// 输出: 1, 2, 3, 循环由于异常而停止。

在这个例子中,当满足条件时,抛出一个异常,提前退出 forEach 循环。但是,重要的是要正确处理这些异常,以避免意外的副作用。


用于中断循环的 forEach 替代方法 💡


使用 for...of 循环


for...of 循环是在 ES6(ECMAScript 2015)中引入的,它提供了一种现代的、简洁的和可读的方式来迭代类似数组、字符串、映射、集合等可迭代对象。与 forEach 相比,它的关键优势在于它与 breakcontinue 等控制语句兼容,在循环控制方面提供了更大的灵活性。


for...of 的优点:



  • 灵活性:允许使用 breakcontinuereturn 语句。

  • 可读性:提供清晰简洁的语法,使代码更易读和理解。

  • 通用性:能够迭代各种可迭代对象,不仅仅是数组。


for...of 的实际示例


考虑以下场景,我们需要处理数组的元素,直到满足某个条件:


const numbers = [1, 2, 3, 4, 5];  

for (const number of numbers) {
if (number > 3) {
break; // 成功中断循环
}
console.log(number);
}

输出:


1
2
3

在这个例子中,循环迭代 numbers 数组中的每个元素。一旦遇到大于 3 的数字,它利用 break 语句退出循环。这在 forEach 中是不可能的。


其他方法



  • Array.prototype.some():可以使用它来通过返回 true 来模拟中断循环。

  • Array.prototype.every():当返回 false 值时,此方法停止迭代。


结论 🎓


尽管 JavaScript 中的 forEach 方法提供了直接的数组迭代方式,但它缺乏在循环中段中断或停止的灵活性。理解这个限制对开发人员来说至关重要。幸运的是,像 for...of 循环以及 some()every() 等方法提供了必要的控制来处理更复杂的场景。掌握这些概念不仅可以增强你的 JavaScript 技能,还可以让你为艰巨的面试问题和实际编程任务做好准备。


作者:今天正在MK代码
来源:juejin.cn/post/7324384460136611850
收起阅读 »

html中的lang起到什么作用?

web
今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果…… 在chrome上是这样的 再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……...
继续阅读 »

今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果……


在chrome上是这样的


image.png


再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……


image.png


记录一下,避坑~


C3C69257283D24A892D56FA7AD82A2B1.png


代码贴在下面,感兴趣的可以去试一下


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Document</title>
</head>

<body>
<p class="MsoNormal" style="width: 300px;background: yellow;">这一党章内容增写入宪法第一条第二款。<span lang="EN-US"></span>中国特色</p>
</body>

</html>

作者:lwlcode
来源:juejin.cn/post/7324750286329282597
收起阅读 »

原神UID300000000诞生,有人以高价购买!那么UID是怎么生成的?

原神UID有人要高价购买? 在原神的广袤世界中,每位冒险者都被赋予一个独特的身份标识——UID(User ID)。这个数字串既是你在游戏中独一无二的身-份-证明,也承载着无数冒险的记忆。然而,有一个UID格外引人注目——300000000,最近它在原神的世界中...
继续阅读 »

原神UID有人要高价购买?


在原神的广袤世界中,每位冒险者都被赋予一个独特的身份标识——UID(User ID)。这个数字串既是你在游戏中独一无二的身-份-证明,也承载着无数冒险的记忆。然而,有一个UID格外引人注目——300000000,最近它在原神的世界中诞生,成为了众人瞩目的焦点,因为有人要以高价购买。


查阅资料我们知道,UID不同开头代表不同的含义。


UID服务
uid1、2开头官服
uid5开头B服、小米服等,国内渠道服都是5开头
uid6开头美服
uid7开头欧服
uid8开头亚服
uid9开头港澳服

首先UID是固定的9位数,也就是100000000这样的,前面的1是固定的,所有玩家开头都是这个1,然后剩下的8位数才是注册顺序。比如:100000001,这个就是开服第一位玩家,100000013,这个就是第13位注册玩家。


300000000说明官服已经有2亿用户了!!!


我们先看下UID的生成的策略吧。


系统中UID需要怎么设计呢?


什么是UID?


UID是一个系统内用户的唯一标识(Unique Identifier),唯一标识成为了数字世界中不可或缺的一部分。无论是在数据库中管理记录,还是在分布式系统中追踪实体,唯一标识都是保障数据一致性和可追溯性的关键。为了满足各种需求,各种唯一标识生成方法应运而生。


UID如何设计


UUID模式


UUID (Universally Unique Identifier),通用唯一识别码的缩写。目前UUID的产生方式有5种版本,每个版本的算法不同,应用范围也不同。其中最常见的是基于时间戳的版本(Version 1)和基于随机数的版本(Version 4)。版本1的UUID包含了时间戳和节点信息,而版本4的UUID则是纯粹的随机数生成。


•基于时间的UUID:这个一般是通过当前时间,随机数,和本地Mac地址来计算出来,可以通过 org.apache.logging.log4j.core.util包中的 UuidUtil.getTimeBasedUuid()来使用或者其他包中工具。由于使用了MAC地址,因此能够确保唯一性,但是同时也暴露了MAC地址,私密性不够好。•基于随机数UUID :这种UUID产生重复的概率是可以计算出来的,但是重复的可能性可以忽略不计,因此该版本也是被经常使用的版本。JDK中使用的就是这个版本。


Java中可通过UUID uuid = UUID.randomUUID();生成。


虽然 UUID 生成方便,本地生成没有网络消耗,但是使用起来也有一些缺点,不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置。对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。


表ID自增


将user表的id设置为auto_increment,插入会自动生成ID,将表的主键ID作为UID.


这种方式的优势在于简单易实现,不需要引入额外的中心化服务。但也存在一些潜在的问题,比如数据库的性能瓶颈、数据量大需要分库分表等。


使用redis实现


Redis实现分布式唯一ID主要是通过提供像 INCR 和 INCRBY 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。


但是单机存在性能瓶颈,无法满足高并发的业务需求,所以可以采用集群的方式来实现。集群的方式又会涉及到和数据库集群同样的问题,所以也需要设置分段和步长来实现。


为了避免长期自增后数字过大可以通过与当前时间戳组合起来使用,另外为了保证并发和业务多线程的问题可以采用 Redis + Lua的方式进行编码,保证安全。Redis 实现分布式全局唯一ID,它的性能比较高,生成的数据是有序的,对排序业务有利,但是同样它依赖于redis,需要系统引进redis组件,增加了系统的配置复杂性。当然现在Redis的使用性很普遍,所以如果其他业务已经引进了Redis集群,则可以考虑使用Redis来实现。


号段模式


号段模式是一种常见的分布式ID生成策略,也被称为Segment模式。该模式通过预先分配一段连续的ID范围(号段),并在每个节点上使用这个号段,以减少对全局资源的竞争,提高生成ID的性能。以下是一个简单的号段模式生成分布式ID的步骤:


1.预分配号段: 一个中心化的服务(通常是一个分布式协调服务,比如Zookeeper或etcd)负责为每个节点预分配一段连续的ID号段。这个号段可以是一段整数范围,如[1, 1000],[1001, 2000]等。2.本地取ID: 每个节点在本地维护一个当前可用的ID范围(号段)。节点在需要生成ID时,首先使用本地的号段,而不是向中心化的服务请求。这可以减少对中心化服务的压力和延迟。3.号段用尽时重新申请: 当本地的号段用尽时,节点会向中心化服务请求一个新的号段。中心化服务会为节点分配一个新的号段,并通知节点更新本地的号段范围。4.处理节点故障: 在节点发生故障或失效时,中心化服务会将未使用的号段重新分配给其他正常运行的节点,以确保所有的ID都被充分利用。5.定期刷新: 节点可能定期地或在某个条件下触发,向中心化服务查询是否有新的号段可用。这有助于节点及时获取新的号段,避免在用尽号段时的阻塞。


这种号段模式的优点在于降低了对中心化服务的依赖,减少了因为频繁请求中心化服务而产生的性能瓶颈。同时,由于每个节点都在本地维护一个号段,生成ID的效率相对较高。


需要注意的是,号段模式并不保证全局的递增性或绝对的唯一性,但在实际应用中,通过合理设置号段的大小和定期刷新机制,可以在性能和唯一性之间找到一个平衡点。


Snowflake模式


Snowflake是一个经典的号段生成算法,同时市面上存在大量的XXXflake算法.一般用作订单号。主要讲一下Snowflake的原理。


arch-z-id-3.png



  • 第1位占用1bit,其值始终是0,可看做是符号位不使用。

  • 第2位开始的41位是时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么雪花算法可用的时间年限是(1L<<41)/(1000L360024*365)=69 年的时间。

  • 中间的10-bit位可表示机器数,即2^10 = 1024台机器,但是一般情况下我们不会部署这么台机器。如果我们对IDC(互联网数据中心)有需求,还可以将 10-bit 分 5-bit 给 IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,具体的划分可以根据自身需求定义。

  • 最后12-bit位是自增序列,可表示2^12 = 4096个数。


不过Snowflake需要依赖于时钟,可能受到时钟回拨的影响。同时,如果并发生成ID的速度过快,可能导致序列号用尽。


总结


在选择UID生成方法时,需要根据具体的应用场景和需求权衡其优缺点。不同的场景可能需要不同的解决方案,以满足系统的唯一性要求和性能需求。那么你觉得原神的UID是如何生成的呢?如果是你该如何设计呢?


作者:半亩方塘立身
来源:juejin.cn/post/7324633501244063782
收起阅读 »

MyBatis实战指南(三):相关注解及使用

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。一、什么是注解(Annotation)首先,我们需要明...
继续阅读 »

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。

一、什么是注解(Annotation)

首先,我们需要明白什么是注解。注解 Annotation 是从JDK1.5开始引入的新技术。

Description

在Java中,注解是一种用于描述代码的元数据,它可以被编译器、库和其他工具读取和使用。MyBatis的注解就是用来简化XML配置的,它们可以让你的代码更加简洁、易读。

注解的作用:

  • 不是程序本身,对程序作出解释
  • 可以被其他程序读取到

Annotation格式:

注解是以@注解名的方式在代码中实现的,可以添加一些参数值

如:@SuppressWarnings(value=“unchecked”)

注解使用的位置:

package、class、method、field 等上面,相当于给他们添加了额外的辅助信息。

注解的分类:

1.元注解:

  • @Target:用于描述注解的使用范围

  • @Retention:用于描述注解的生命周期

  • @Documented:说明该注解将被包含在javadoc 中

  • @Inherited:说明子类可以继承父类中的该注解

  • @Repeatable:可重复注解

2.内置注解:

  • @Override: 重写检查

  • @Deprecated:过时

  • @SuppressWarnings: 压制警告

  • @FunctionalInterface: 函数式接口

3.自定义注解:

  • public @interface MyAnno{}

二、Mybatis常用注解

首先介绍一下Mybatis注解的使用方法:

第一步,在全局配置文件里的配置映射



    


第二步,在mapper接口的方法的上面添加注解

@Select("select * from user where uid = #{uid}")

    public User findUserById(int uid);

第三步,创建会话调用此方法。

接下来,我们来看看MyBatis中最常用的几个注解:

(1)@Select

作用:标记查询语句。

@Select用于标记查询语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Select注解时,需要在注解中指定SQL语句。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

User getUserById(@Param("id") Long id);

(2)@Insert

作用:标记插入语句。

@Insert用于标记插入语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Insert注解时,需要在注解中指定SQL语句。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

int addUser(User user);

(3)@Update

作用:标记更新语句。

@Update用于标记更新语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Update注解时,需要在注解中指定SQL语句。

示例:

@Update("UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}")

int updateUser(User user);

(4)@Delete

作用:标记删除语句。

@Delete用于标记删除语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Delete注解时,需要在注解中指定SQL语句。

示例:

@Delete("DELETE FROM users WHERE id = #{id}")

int deleteUserById(@Param("id") Long id);

(5)@Results

作用:用于指定多个@Result注解。

@Results用于标记结果集映射,该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@Results注解时,需要指定映射规则。

示例:


@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(6)@Result

作用:用于指定查询结果集的映射关系。

@Result用于标记单个属性与结果集中的列之间的映射关系。该注解可以用于接口方法或XML文件中,通常与@Results注解一起使用。使用@Result注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(7)@ResultMap

作用:用于指定查询结果集的映射关系。

@ResultMap用于标记结果集映射规则。该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@ResultMap注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@ResultMap("userResultMap")

User getUserById(@Param("id") Long id);

(8)@Options

作用:用于指定插入语句的选项。

@Options用于指定一些可选的配置项。该注解可以用于接口方法或XML文件中,通常与@Insert、@Update、@Delete等注解一起使用。使用@Options注解时,可以指定一些可选的配置项。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@Options(useGeneratedKeys = true, keyProperty = "id")

int insertUser(User user);

(9)@SelectKey

作用:用于指定查询语句的主键生成方式。

@SelectKey用于在执行INSERT语句后获取自动生成的主键值。该注解可以用于接口方法或XML文件中,通常与@Insert注解一起使用。使用@SelectKey注解时,需要指定生成主键的SQL语句和将主键值赋给Java对象的哪个属性。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)

int insertUser(User user);

(10)@Param

作用:用于指定方法参数名称。

@Param用于为SQL语句中的参数指定参数名称。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Param注解时,需要指定参数名称。

示例:

@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")

List getUsersByNameAndAge(@Param("name") String name, @Param("age") Integer age);

(11)@One

作用:用于指定一对一关联关系。

@One用于在一对一关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@One注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。



  

  

  

  







  

  

  

  


上述代码中,@One注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对一关联查询。在departmentResultMap中,使用@One注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为manager,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@One注解之外,还可以使用@Many注解来指定一对多关联查询的映射方式。

总之,@One注解是MyBatis中用于在一对一关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对一关联查询的结果映射。

(12)@Many

作用:用于指定一对多关联关系。

@Many用于在一对多关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@Many注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。

示例:



  

  

  

  







  

  

  


上述代码中,@Many注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对多关联查询。在departmentResultMap中,使用@Many注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为members,ofType参数指定了集合中元素的类型为User,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@Many注解之外,还可以使用@One注解来指定一对一关联查询的映射方式。

总之,@Many注解是MyBatis中用于在一对多关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对多关联查询的结果映射。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

(13)@ResultType

作用:用于指定查询结果集的类型。

@ResultType用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

@ResultType(User.class)

User getUserById(Long id);

(14)@TypeDiscriminator

作用:用于指定类型鉴别器,用于根据查询结果集的不同类型映射到不同的Java对象。

@TypeDiscriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@TypeDiscriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:



  

  

  

  

    

    

    

  








  

  







  








  


上述代码中,@TypeDiscriminator注解用于指定不同子类型的映射方式。在vehicleResultMap中,使用@TypeDiscriminator注解指定了类型列的名称为type,javaType参数指定了类型列的Java类型为String,标签中的value属性分别对应不同的子类型(car、truck、bus),resultMap属性用于指定不同子类型的结果集映射规则。

除了使用@TypeDiscriminator注解之外,还可以使用标签来指定不同子类型的映射方式。

总之,@TypeDiscriminator注解是MyBatis中用于在自动映射时指定不同子类型的映射方式的注解之一,可以方便地实现自动映射不同子类型的结果集映射规则。

(15)@ConstructorArgs

作用:用于指定Java对象的构造方法参数。

@ConstructorArgs用于指定查询结果映射到Java对象时使用的构造函数和构造函数参数。该注解可以用于XML文件中,通常与标签一起使用。使用@ConstructorArgs注解时,需要指定构造函数参数的映射关系。

示例:



  

  

    

    

  



(16)@Arg

作用:用于指定Java对象的构造方法参数。

@Arg用于指定查询结果映射到Java对象时构造函数或工厂方法的参数映射关系。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Arg注解时,需要指定参数的映射关系。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

User getUserById(@Arg("name") String name, @Arg("age") int age);

(17)@Discriminator

作用:用于指定类型鉴别器的查询结果。

@Discriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Discriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:


@Select("SELECT * FROM vehicle WHERE type = #{type}")

@Discriminator(column = "type", javaType = String.class, cases = {

  @Case(value = "car", type = Car.class),

  @Case(value = "truck", type = Truck.class),

  @Case(value = "bus", type = Bus.class)

})


List getVehiclesByType(String type);

(18)@CacheNamespace

作用:用于指定缓存的命名空间。

@CacheNamespace用于指定Mapper接口中的查询结果是否进行缓存。该注解可以用于Mapper接口上,用于指定Mapper接口中所有方法默认的缓存配置。使用@CacheNamespace注解时,需要指定缓存配置的属性。

示例:

@CacheNamespace(

  implementation = MyBatisRedisCache.class,

  eviction = MyBatisRedisCache.Eviction.LRU,

  flushInterval = 60000,

  size = 10000,

  readWrite = true,

  blocking = true

)


public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(19)@Flush

作用:用于在插入、更新或删除操作之后自动清空缓存。

@Flush是用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Flush(flushCache = FetchType.AFTER)

User getUserById(Long id);

(20)@MappedJdbcTypes

作用:用于指定Java对象属性与数据库列的映射关系。

@MappedJdbcTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedJdbcTypes注解时,需要指定Java类型和对应的JDBC类型。

示例:

public class User {

  private Long id;

  @MappedJdbcTypes(JdbcType.VARCHAR)

  private String name;

  private Integer age;

  // ...

}

(21)@MappedTypes

作用:用于指定Java对象与数据库类型的映射关系。

@MappedTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedTypes注解时,需要指定Java类型。

示例:

@MappedTypes(User.class)

public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(22)@SelectProvider

作用:用于指定动态生成SQL语句的提供者。

@SelectProvider是用于在Mapper接口中动态生成查询SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@SelectProvider注解时,需要指定Provider类和Provider方法。

示例:

@SelectProvider(type = UserSqlProvider.class, method = "getUserByIdSql")

User getUserById(Long id);

(23)@InsertProvider

作用:用于指定动态生成SQL语句的提供者。

@InsertProvider用于在Mapper接口中动态生成插入SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@InsertProvider注解时,需要指定Provider类和Provider方法。

示例:

@InsertProvider(type = UserSqlProvider.class, method = "insertUserSql")

int insertUser(User user);

(24)@UpdateProvider

作用:用于指定动态生成SQL语句的提供者。

@UpdateProvider用于在Mapper接口中动态生成更新SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@UpdateProvider注解时,需要指定Provider类和Provider方法。

示例:

@UpdateProvider(type = UserSqlProvider.class, method = "updateUserSql")

int updateUser(User user);

(25)@DeleteProvider

作用:用于指定动态生成SQL语句的提供者。

@DeleteProvider用于在Mapper接口中动态生成删除SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@DeleteProvider注解时,需要指定Provider类和Provider方法。

示例:

@DeleteProvider(type = UserSqlProvider.class, method = "deleteUserSql")

int deleteUser(Long id);

以上就是MyBatis的相关注解及使用示例了,实际开发中不一定每个都能用到,但是可以收藏起来,有备无患嘛!

总的来说,MyBatis的注解是一个非常强大的工具,它可以帮助你减少XML配置的工作量,让你的代码更加简洁、易读。但是,它也有一定的学习成本,你需要花一些时间去理解和掌握它。希望这篇文章能帮助你更好地理解和使用MyBatis的注解。

收起阅读 »

300块成本从零开始搭建自己的家庭版NAS还可以自动备份,懂点代码有手就行!

前言 300块成本从零开始搭建自己的家庭版NAS,还可以手机上文件照片音乐自动备份,完全实现了自己的网盘效果,可以设置用户权限分配,目录上传、断点续传、并行上传、拖拽文件上传等日常操作。 为什么要搭建NAS? 现在的手机性能比以前强多了,所以每次换手机的...
继续阅读 »

前言



300块成本从零开始搭建自己的家庭版NAS,还可以手机上文件照片音乐自动备份,完全实现了自己的网盘效果,可以设置用户权限分配,目录上传、断点续传、并行上传、拖拽文件上传等日常操作。



PixPin_2024-01-14_21-24-12.png


为什么要搭建NAS?


现在的手机性能比以前强多了,所以每次换手机的原因居然是存储空间满了,不得不更换一个存储空间更大的手机,加上手机拍照,摄影,工作,生活,有娃的视频等,数据越来越多,我们需要一个性价比高的安全的存储介质。


目前市场上可选的方式很多,在线网盘,移动硬盘,U盘,私人NAS等。这些优缺点很明显,在线网盘,优点是最方便,下载个app完事,但缺点更多,大家懂的,空间大小要充值会员,下载速度要充值会员,一旦数据放上去了将会被收割个不停,更惨的是,完全没有个人隐私,想想都可怕,别人用你的数据去训练AI,你还在给他充值会员。移动硬盘和U盘,用起来最不方便,最后只能是选择NAS。


市面上的NAS分析


某宝一搜,市面上的NAS琳琅满目,经过我花了一个星期仔细筛查,主要分3种,群晖NAS(黑群晖),网络盒子,第三方公司销售的NAS云盘。大致如下:


image.png


(非广告,打码处理)



  • 群晖NAS,专业级别的NAS,性能高,效果好,价格也很感人,非公司级别也用不着,大炮打鸟的感觉

  • 网络盒子,看起来价格低廉,充值会员,流量,账号,空间,全都会卡着你

  • 第三方NAS云盘,经过研究,其所谓的外网链接都必须走他们公司的服务器转发,这意味着,你所有的数据都被别人看光光,这种还要看公司运营,还会有小公司倒闭等风险


我的私人NAS实现方式




  1. 购买一台微型服务器,接入到家庭路由

  2. 买几块硬盘挂载到服务器

  3. 部署开源的网盘系统,经过多种实验和研究,作者推荐Cloudreve社区开源网盘

  4. 通过内网穿透方式,把服务暴露出去

  5. 通过安装配置WebDAV协议访问的第三方文件管理器管理手机,通过web服务管理网盘所有数据



image.png


image.png


详细实现步骤


第一步:


购买一个微服务器,这里仅展示作者买的微服务器,不做广告和推荐,个人根据实际情况购买(如有需要可以和作者私下沟通)。大概100多即可购买一台,配置不同价格不同。买回来让商家预先安装了centos操作系统,买回来后插上路由器,连上家里的内网,在电脑上通过ssh连接上去。
PS:初始化系统相关信息可以问商家要。



image.png


第二步:


买一块硬盘通过USB接口接上去,这个完全有个人喜好,推荐机械硬盘,买个可插入多个盘位的硬盘外接盒子,安全又高效,这里可以参考之前的图,有示例,作者就买了个便宜货先用着。大约1个T,临时够用。



第三步:


部署开源网盘,我这里选择的是Cloudreve,原因如下:



  1. 开源系统,截止今日Star20.1K

  2. 中文支持的好,国产,Go语言架构,效率还行

  3. 支持WebDAV协议,可以用第三方app对接,研究了ES文件管理器,可以自动备份资料到服务器上去,IOS有专用app

  4. 前端UI做的不错,基础功能齐全

  5. 可以多用户权限管理,存储管理



image.png


部署文档参见官网,下期将会描述技术细节


第四步:


内网穿透,这里用的FRP,这个配置也折腾了我好久,要求我们要有一个服务器和域名,这个作者之前有几台非常便宜的服务器和域名在手,顺便做个部署即可,一般用户可以购买下各个云服务商的优惠版本,几百块1年非常便宜。



  • 第一个是要配置好服务端即我们的云服务器,开通ssh隧道,一个是开通转接http和https的接口,私人用无需https

  • 第二个是要配置客户端我们要放开的服务,即ssh和Cloudreve部署地址。



FRP部署技术将新开一个专题介绍


第五步:


WebDAV配置手机,我们先配置一个内网版本的网盘,然后根据内网穿透映射到外面的地址再配置一个外网的网盘,这样在家的时候我们通过连上路由器,用内网访问,速度快,建议备份都在内网时候传输,平时不在家的时候用外网来查看。



image.png


基于这个服务打通,我们可以干更多事情了,建个网站如何?



内外网打通,服务器有了,我们甚至可以做更多事情,建个网站,把家里的设备全部用服务器来管理,如果你家有视频监控,也可以备份到服务器!



更多部署软件部分细节,将在下期分享,



  • Cloudreve部署

  • FRP部署

  • WebDAV配置

  • 等等...

作者:天问cc
来源:juejin.cn/post/7323599971214802956
收起阅读 »

2023:情若能自控,要心有何用。。。。

情若能自控,要心有何用。。。。 一、开篇   岁末将至,人心渐老,百般滋味涌上心头,话到嘴边不值得一提。词穷不是沉默,而是一言难尽。该接受的不该接受的,都接受了,没啥不公平的,习惯了。看错人,不是瞎,是心软;信错人,不是傻,是重情义;爱错人,不是愚蠢,而是你的...
继续阅读 »

情若能自控,要心有何用。。。。


一、开篇


  岁末将至,人心渐老,百般滋味涌上心头,话到嘴边不值得一提。词穷不是沉默,而是一言难尽。该接受的不该接受的,都接受了,没啥不公平的,习惯了。看错人,不是瞎,是心软;信错人,不是傻,是重情义;爱错人,不是愚蠢,而是你的劫。什么事情都要自身找原因,不要苟且他人。鞋子脏了,是因为你走的路不干净。该反省的是自己的眼光和见识,永远不要怀疑自己的真诚和善良……好了,时间到了,该走了……



  • 我本两袖一清风,赤心可抵岁月长。

  • 孤身何惧人生苦,独行敢试不平路。

  • 红尘本是无情道,偏偏痴心博君笑。

  • 红杏枝头春意闹,不过岁月风中萧。

  • 梦逆光阴初到时,强船何惧风浪涌。

  • 奈何竟遇遭人欺,一人把这悲凉谱。

  • 惊鸿一瞥忘不了,只见得炊烟袅袅。

  • 此生恐难再相逢,坠落片片葬夙梦。

  • 无人问津又何妨,逍遥自在人心好。

  • 浮云千载悠悠过,何曾片缕下中州。


二、我与职场


2.1 追风赶月莫停留


  我依然在北京这座城市漂泊,我没有勇气或者说没有足够的能力与底气回到老家扎根,我依然过着普普通通的周中上班周末摆烂的人生,两点一线的在舒适圈中挣扎,不愿逃脱。在我将近九年多的工作经历中,共经历了4家公司。在工作经验不断积累的过程中,公司各种乱象或不公,几乎都经历过。也因为长期的隐忍最终爆发,开始排斥人在公司,还要平衡工作情绪+奇葩管理,但不排斥工作。我更倾向于居家办公,你给我钱,我给你成果,不需要乌烟瘴气的办公氛围,不需要能者多劳的pua,更不需要尔虞我诈的利用。

经历了公司大规模裁员,同事有被迫离职的,也有自己跳槽走人的,导致对自己的职业生涯产生了迷茫,跳槽 or 副业,一时不知道该如何选择。对于我来说,这一年的工作情况可以用四个字来形容,那就是"平平无奇",工资也是"纹丝未动"。我好似一只大蛤蟆,公司则如一锅正在加热的温水。这一年唯一的收获就是工作越来越顺手了,然后工作也变得一成不变,接需求、分析、设计、开发、测试、上线,每天好像在坐牢一样,没有一点技术含量。感觉如果继续呆下去,再过几年我就可以回家烤红薯了。


2.2 平芜尽处是春山


  说真的,今年可能是个人技术能力提升最小的一年,我竟然没有任何值得拿出手的东西,我的时间就这样白白流逝了,好像已经很努力了,但是依然很普通,导致想跳槽都没信心。一方面是因为其他事情耽搁了,另一方面的确是有点懈怠了,在工作中用不到的新技术就很少像以前那样去学习了,对已掌握的知识点也缺少动力去继续深挖了。这点的确不太好,只要还在这个行业,就如逆水行舟,不进则退。

这一年的我,可以说是从迷茫到醒悟。现在的技术层出不穷,似乎大家都在卷各种技术,例如 Flutter、Framework、Docker等等。或许大家都有跟我一样的感受,面对不断涌现的新技术,难免会让人感到迷茫,不知所措,应该躺平呢?还是盲目跟风卷呢?我真的能选择躺平吗?拼爹不行,拼存款没有,夹杂着公司裁员、经济形势不好的情况下,我决心改变自己。虽然在工作中不能提升技术,但是自己不能放弃自己,不然辞职就等于失业。首先要改变手机占用我的时间,虽然这很困难,但我不能倒在刷剧、刷短视频的魔爪之下,以学习、编写技术文章为重要事项,逼迫自己学习。为防止自己因为太难而打退堂鼓,前期制定些简单任务:一周一个核心的技术知识点,两周一篇技术文章。随着学的东西越来越多,写的文章也被更多人阅读和关注时,任务适当加大难度。所以今年在闲暇时间学习了很多东西,如 Vue 组件、Docker容器等,立志成为一名全栈工程师。截止年底,不知不觉中竟然写了160多篇随记、40多篇技术文章。当然有的是没有发表在博客上,至于为啥就不用说了,懂得都懂。

虽然我不知道 35 岁后(如果我能活到那个时候)程序员何去何从,在中国35岁是一个比较尴尬的年龄,35岁嫌老、65嫌年轻。如果一旦失业,很有可能会受到其余公司HR的歧视。做技术的学的技术一定要顺应时代的发展,社会需要什么黑科技,就要花时间去钻研。我知道现在不努力积累自己的专业知识,未来只会如逆水行舟,一步步将我推回起点。疫情三年真的是大浪淘沙,淘汰只会是那些不脚踏实地学习和工作的人,出来混迟早要还的。只有现在奋力前行,未来才有更多的选择机会。


2.3 人生苦短,帮我倒满


  这一年我遇到让我心动的那人,其实,我现在也没想好该怎么描述这段不太好的经历,怎么说呢,那种感觉就好像开局就被针对了一样,完全发育不起来!
  这段我写下她身上我喜欢的点吧,淡妆、穿着很朴素、自然,不做作。再一个就是我很喜欢她努力学习的样子,真的安静的像一道风景,我总会在旁边偷偷的看她,一边看一边傻笑。有的时候,她还有些小小的笨拙,让我觉得很喜欢,这个姑娘我不是凑合,是真的喜欢。

虽然在一起的时间不到一个月就分开了,之后那段时间我整个人精神恍惚,开始就剧烈的呕吐,整晚头疼的睡不着,去了趟医院,诊断结果是脑内伤,可能伴有中度抑郁,情况有些麻烦,给我开了一堆又一堆的治疗抑郁的药,又建议一个月再复诊。这一点也是吓到了我,也让我意识到生活和工作应该分开的道理,工作是我们赚钱的工具,不应该成为毁坏我们身体的元凶。趁着十一放假我跟一个小伙伴一起去了拉萨(遗憾的是我手机落在了小伙伴车上),这期间我遇到一个喇嘛,姑且算是算命吧,他说木性温暖,火伏其中,钻灼而出,故木生火。而我乃火命,故而需要木属性之物常伴身旁,恰好我带着一对儿核桃,此物可助我驱祸免灾。虽然我不怎么相信这些,但为了心安勉强接受。医院复诊的结果还是不出意外的坏,又恰逢不到半年间两位友人的离世,时日不多的我不得不把这些年来开发项目(纯属个人)卖掉,再加上我工作以来积攒的钱,一部分用来做父母的养老之用,一部分给父母买了养老保险,剩下的留作我半年出行之用,毕竟有些地方我一直想去,但总因种种原因不得行,这次终于可以出动了。

人生中,多的是身不由己的时刻,得也好,失也罢,都要坦然面对。喝酒不问度数,酒后不问去处。人生苦短,帮我倒满倒满……


be0b6d6e703610cffce19cc234066e56.jpeg


三、关于个人


3.1 漫天神佛不识君,幽冥可曾有知心


  2023 是疫情恢复的第一年,褪去口罩的滤镜,我们更真切的看懂了这个世界,大家都活明白了,房子不买可以租,车子能开就行。所谓的财富,在生命和健康面前微不足道;个人的努力,在时代面前微不足道,你不涨工资、买不起眉笔,也不是能力不行,降低欲望、降低消费也可以过的很好。做饭的尽头是大铁锅,衣服的尽头是保暖舒适,消费主义的尽头是断舍离,万事的尽头是尽人事知天命,幸福的尽头是平安、健康。我们是失去了很多人,但就算公交车上空无一人,司机师傅还是会把车开到终点站。战乱也让我们明白,原来生在一个和平的国家,是那么的幸福。很多人的生日愿望,也从财富、爱情变成了希望世界和平。

2023年是割裂的一年,朋友圈好像所有的人都在旅游,但携程用户从2600万跌到了600万;外卖员、网约车司机变多了,可滴滴用户却从4500万跌到了1000万;摆地摊的越来越多了,但怪兽充电宝却从300万跌到了100万;2023年失信被执行人数突破了800万,上半年有46万多家公司倒闭,boss直聘月活用户却突破了一个亿;考公的人越来越多,创业的越来越少;药店越来越多,孩子越来越少;房子越盖越多,股民越炒越少……口罩是我们最后的遮羞布,以后再赚不到钱就没有借口了。今年甚至连除夕回家都成了奢望,这一年世界也很混乱,很多生命都定格在了2023,我最好的朋友也留在了这一年,我经常梦见他……我觉得人生就像下面这张画一样:虽前路依旧光明、未来就在彼岸,可我却深处黑暗独帆前行……


20231209201408.png


3.2 但饮孟婆解千忧,余后共赴忘川流


  日月蹉跎,人已将老而功业未建。我这等人,真的能成大业吗?我没有变,只是心情变了。我还是我,只是面对现实,多了点无奈、多了点沉默。我曾享受过一天晚上花几千,也体验100块钱都借不到。人嘛,享受过不该拥有的风光,就要承受随之而来的报应。所有的事情都抵不过时间和现实,让人成熟的从来不是年龄,而是经历。我觉得今年本该幸福的,可这烂透了的生活,却耗尽我所有的精力。样样都不顺心,事事都不如意,都说先苦后甜,可是我连最基本的快乐都给不了自己,却又无能为力!好像有迹可循,又好像无路可走。我原本以为今年我会很幸福的,可是记不清了,只记得今年心态崩多少次!我真的不喜欢今年,今年让我太难过了。我讨厌现在的自己,一边压抑着自己的情绪,一边装作什么都没事的样子,一到深夜,就彻底崩溃,天亮后还要微笑的去面对一切。

曾有人问过我,2023年我们到底收获了什么?也许,还活着,算是我今年最大的收获吧!好好活下去,朋友们,哪怕凑合的活下去。的确,不是所有的坚持都会有收获,但总有一些坚持,能在一寸冰封的土地里培育出香甜的果实……不是有所成就才算活着,梦想也不是多么了不起的东西,只喜欢看天走路、吃烧烤的人生,也很好!


9af4a8316e3a15fd71219e90a0f05b82.jpeg



日落归山海,山海藏深意,没有人能不遗憾!



四、小结



把今天最好的表现当作明天最新的起点..~



  投身于天地这熔炉,一个人可以被毁灭,但绝不会被打败!一旦决定了心中所想,便绝无动摇。迈向光明之路,注定荆棘丛生,自己选择的路,即使再荒谬、再艰难,跪着也要走下去!放弃,曾令人想要逃离,但绝境重生方为宿命。若结果并非所愿,那就在尘埃落定前奋力一搏!


划重点.gif


作者:独泪了无痕
来源:juejin.cn/post/7324165965205225522
收起阅读 »

原来我们是这样对工作失去兴趣的

大家好,我是「云舒编程」,今天我们来聊聊那些让我们对工作失去兴趣的原因。 一、前言    相信很多人有过接手别人的系统,也有将自己负责的系统交接给别人的经历。既有交接出去不用在费力治理维护技术债务的喜悦,也有接手对方系统面对一系列维护问题的愁容满面。    但...
继续阅读 »

大家好,我是「云舒编程」,今天我们来聊聊那些让我们对工作失去兴趣的原因。


一、前言


   相信很多人有过接手别人的系统,也有将自己负责的系统交接给别人的经历。既有交接出去不用在费力治理维护技术债务的喜悦,也有接手对方系统面对一系列维护问题的愁容满面。

   但是被人嫌弃的系统曾经也是「创建人」心中的白月光啊,是什么导致了「白月光」变成了「牛夫人」呢?是996,工期倒排、先上再优化,还是随时变动的需求?

   让我们来复盘系统是怎么一步一步腐化的,让我们丢失了最初的兴趣,同时总结一些经验教训以及破局之策。


二、白月光到牛夫人的经历


一般当我们设计一个系统时,总是会抱着要把该项目打造为「干净整洁」的项目的想法,


图片


但是随着时间的推移,最后总是不可避免的变成了这样:


图片


2.1、从0到1


   我们发现大多数人对于创建新项目总是会抱有极大的激情兴趣,会充分的考虑架构设计。但是对于接手的项目就会缺乏耐心。

   这种心理在《人月神话》一书中被说为编程职业的乐趣:

“首先,这种快乐是一种创建事物的纯粹快乐。如同小孩在玩泥巴时 感到快乐一样,成年人喜欢创建事物,特别是自己进行设计 。我想这种 快乐是上帝创造世界的折射,一种呈现在每片独特的、崭新的树叶和雪 花上的喜悦。”

“第四,这种快乐是持续学习的快乐,它来自于这项工作的非重复特性。人们所面临的问题总有这样那样的不同,因而解决问题的人可以从 中学习新的事物,有时是实践上的,有时是理论上的,或者兼而有之。”

图片

正是由于这样的心理,人们在面对新系统时,可以实践自身所学,主动思考如何避开曾经遇到的坑。满足了内心深处的对于创造渴望。
   当一个项目是从0到1开始设计的,并且前期是由少数「高手」成员主导开发的话,一般不会有债务体现。当然明面上没有债务,不代表没有埋下债务的种子。


2.2、抢占市场、快速迭代


   系统投入市场得到验证后,如果顺利,短期会收获大量用户。伴随着用户指数增长的同时,各种产品需求也会随着而来。一般在这个阶段将会是「工期倒排、先上再优化,需求随时变动」的高发期。

   同时由于需求的爆发,为了提高团队的交付率,在这个阶段会引入大量的“新人”。随着带来的就是新老思想的碰撞,新的同学不一定认同之前的架构设计。

   在这个阶段,如果团队存在主心骨,可以“游说”多方势力,平衡技术产品、新老开发之间的矛盾,那么在这个阶段引入的债务将会还好。但是如果团队缺乏这样的角色,就会导致公说公有理婆说婆有理,最后的结果就是架构会朝着多个方向发展,一个项目里充斥着多种不同思路的设计。有些甚至是矛盾的。如果还有【又不是不能用】的想法出现,那将是灭顶之灾。

图片

但是在这个阶段对于参与者又是幸福的,一份【有市场、有用户、有技术、有价值】的项目,无论是对未来的晋升还是跳槽都是极大的谈资。


2.3、维护治理


   褪去了前期“曾经沧海难为水,除却巫山不是云”的热恋后,剩下的就是生活的柴米油盐。系统的最终结局也是“维护治理”。

   在这个阶段,需求的数量将大大减少,但是由于前期的“快速建设”,一个小小的需求,我们可能需要耗费数周的时间去迭代。而且系统越来越复杂,迭代越来越困难。
   
同时每天需要花费大量的时间处理客诉、定位bug,精力被完全分散。系统的设计和技术慢慢的变得僵化。并且由于用户量巨大,每次改动都要承担很大的线上风险。这样的情况对于程序员的精力、体力都是一场内耗,并且如果领导的重心也不在此项目时,更会加重这种内耗。于是该系统就会显得“人老色衰”,曾经的「白月光」也就变成了「牛夫人」。

图片


三、牛夫人不好吗?


3.1、缺乏成就感


《人月神话》中关于程序员职业的苦恼曾说过以下几点:



  1. 对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而这些程序往往设计得并不合理、实现拙劣、发布不完整(没有源代码或测试用例)或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可靠的、完整的。

  2. 下一个苦恼---概念性设计是有趣的,但寻找琐碎的bug却是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的 劳动。程序编制工作也不例外。

  3. 最后一个苦恼 ,有时也是一种无奈—当投入了大量辛苦的劳动 ,***产品在即将完成或者终于完成的时候,却已显得陈旧过时。***可能是同事或竞争对手已在追逐新的、更好的构思; 也许替代方案不仅仅是在构思 ,而且己经在安排了。


随着业务趋于稳定,能够发挥创造性的地方越来越少,剩下的更多是沉闷、枯燥的维护工作。并且公司的资源会更聚焦在新业务、新方向,旧系统获得的关注更少,自然而然就缺乏成就感。也就形成了【只见新人笑,不见旧人哭


3.2、旧系统复杂、难以维护


《A Philosophy of Software Design》一书中对复杂性进行了如下定义:“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.”,即任何使得软件难于理解和修改的因素都是复杂性。

作者John 教授又分别从三个角度进行了解释复杂性的来源:


3.2.1、变更放大


   复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。对于复杂的系统来说,如果代码没有聚敛,那么一次小小的需求可能会导致多处修改。同时为了保证不出故障,需要对涉及的修改点、功能点进行完整的覆盖测试。这种隐藏在背后的工作量是巨大的。


3.2.2、认知负荷


   复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。当一个系统经过多年的迭代开发,其复杂度将是指数级别的。并且会充斥这很多只有“当事人”才能理解的“离谱”功能。这就对维护者提出了极高的要求,需要掌握很多“冰山下”的知识才能做到手到擒来。而这对维护者的耐心、能力又是一次挑战。

图片


3.2.3、未知的未知


   未知的未知是指必须修改哪些代码才能完成任务,或者说开发人员必须获得哪些信息才能成功地执行任务。这一项也是John Ousterhout教授认为复杂性中最糟糕的一个表现形式。
     这句话看起来比较抽象,如果映射到我们日常的工作中就是“我也不知道为啥这么改就好了”、“在我这是好的呀”、“刚刚还能运行啊,不知道为啥现在突然不行了”。

   这种情况就是我们不知道改动的这行代码是否能让程序正常运转,也不知道这行代码的改动是否又会引发新的问题。这时候我们发现,那些“上帝类”真的就只有上帝能拯救了。

   我曾经维护一个老系统,源码是通过文件相互传递的,没有仓库,里面的框架是自己撸的轮子,没有任何说明文档。服务部署是先在本地编译成二进制文件,然后在上传到服务器启动。每次改动上线都是就是一次生死劫,幸好没过多久,这个系统就被放弃了。


四、为何变成了牛夫人


4.1、伪敏捷


   “敏捷”已经成为了国内公司的银弹了。
   需求不做市场分析、不考虑用户体验、不做设计分析、不考虑前因后果,美其名曰“敏捷”。
   工期倒排、先上再说、明天能不能上、这个问题上了再优化,美其名曰“敏捷”。

   我曾经参与过一个项目,最开始是给了三个月的时间完成产品规划+开发,但是项目立项后领导层迟迟无法达成统一,一直持续进行了两个月的讨论后,终于确定了产品模型,进入开发。到这里留给开发的时间还剩一个月,咬咬牙也能搞定。但是开发真正进场,上报了需要开发的周期后,上面觉得太慢了,要求3周搞定,过了一天还是觉得太慢,要求2周,最后变成了要求4天搞定。遇到这种情况能怎么办,只能加人,只能怎么快怎么来。

   之前阿里程序员也开玩笑式的说出了类似的场景:“2月13号上午省领导问逍遥子全省的健康码今天上线行不行,逍遥子说可以。等消息传达到产研团队的时候已经是中午了,然后团队在下午写了第一行代码。”


4.2、人的认知局限


   《人月神话》一书中提到了一种组建团队的方式「外科手术团队」:“十个人,其中七个专业人士在解决问题,而系统是一个人或者最多两个人思考的产物,因此客观上达到了概念的一致性。”
   也就是团队只需要一个或者两个掌舵人,负责规划团队的方向和系统架构,其余人配合他完成任务。目前我所待过的团队也基本是按照这个模式组成的,领导会负责重要事情的决策和团队分歧时的拍板,其余人则相互配合完成目标任务。但是这样的模式也导致了一个问题:掌舵人的认知上限就决定了团队的上限,而这种认知上限天然就会导致系统架构设计存在局限性,而这种局限性又会随着“伪敏捷”放大


4.3、人员流动


   经历过这种离职交接、活水交接的打工人应该深有体会,很多项目一旦步入这个阶段,大多数负责人就会开始放飞自我,怎么快怎么来,只想快点结束这段工作,快速奔赴下一段旅程。
   从人性的角度是很难评价这种情况的,毕竟打工人和老板天然就不是一个战线的,甚至可能是对立面的。而我们大多数人都不可能是圣人,从自身角度从发这种行为是无可厚非的。


五、如何保持白月光


   这里想首先抛个结论,系统变腐化是不可避免的。就类似人一样,随着时间的流逝,也会从以前一个连续熬夜打游戏看小说第二天依旧生龙活虎的青年变为一个在工位坐半小时都腰酸背痛,快走几步都喘的中年人。而这都来源于生活的压力、家庭的压力、工作的压力。同样的,面对业务的压力、抢占市场的压力、盈利的压力,系统也不可避免会变成“中年人”。
   就像人一样会使用护肤品、健身等手段延缓自己的衰老一样,我们也可以使用一些手段延缓系统“衰老”。
   在网上,已经有无数的文章教怎么避免代码腐化了,例如“DDD领域驱动设计”、“业务建模”、“重构”等等。
   今天我想从别的角度聊聊怎么延缓代码腐化。


5.1、避免通用


   软件领域有个特点,那就是复用。程序员们总是在思考怎么样写一段到处通用的代码,以不变应万变。特别是当国内提出中台战略后,这种情况就如脱缰的野马一般,不可阻挡。要是你做的业务、架构不带上xx中台,赋能xx,你都觉得你低人一等。
   但是其实我们大部分人做的都是业务系统,本身就是面向某块特定市场的、特定用户的。这就天然决定了其局限性。
   很多时候你会发现你用了100%的力气,设计了一个80%你认为有用的通用中台,最后只有20%产生了作用,剩下60%要么再也没有动过,要么就是被后来参与者喷的体无完肤。
   当然这里也不说,设计时就按照当前产品提出的需求设计就行,一点扩展的余地都不留,而是在「通用」与「业务需求」之间取一个平衡。而这种平衡就取决于经验了。如果你没有这方面经验,那你就去找产品要「抄袭」的是哪个产品,看看他们有哪些功能,可以预留这些功能的设计点。


5.2、Clean Code


说实话,国内的业务系统80%都没有到需要谈论架构设计的地步。能够做到以下几点已经赢麻了:



  1. 良好的代码注释和相关文档存档【重中之重】

  2. 避免过长参数

  3. 避免过长方法和类

  4. 少量的设计模式

  5. 清晰的命名

  6. 有效的Code Review【不是那种帮我CR下,对方1秒后回复你一个done】


5.3、学会拒绝


   自从国内开始掀起敏捷开发的浪潮后,在项目管理方面就出现了一个莫名其妙的指标:每次迭代的需求数都有会有一个数值,而且还不能比上一次迭代的少。
   这种情况出现的原因是需求提出者无法确定这个需求可以带来多大的收益,这个收益是否满足老板的要求。那么他只能一股脑上一堆,这样即使最后效果不及预期,也可以怪罪于用户不买账。不是他不努力。
   在这种时候,就需要开发学会识别哪些是真需求,哪些是伪需求,对于伪需求要学会说不。当然说不,不是让你上来就是开喷,而是你可以提出更加合理的理由,甚至你可以提出其他需求代替伪需求。这一般需要你对这块业务有非常深入的研究,同时对系统有上帝视角。
   基本上在每个公司的迭代周期都是有时间要求的,比如我们就是两周一迭代,如果需求是你可控的,那么你就有更多的时间和心思在维护系统上,延缓他的衰老。


结尾


       分享一些我摸鱼时喜欢看的书,除了本文总是提到的《人月神话》《A Philosophy of Software Design》外,还有《黑客与画家》、《演进式架构》。有需要的可以关注 公众号「云舒编程」,回复"书籍"即可免费获取:跳转地址


图片


作者:云舒编程
来源:juejin.cn/post/7312724606605918249
收起阅读 »

Camera2 同时预览多个摄像头,CameraX不行?

本来是想通过CameraX实现同时预览多个摄像头,通过官网文档介绍,在CameraX 1.3 后通过ConcurrentCamera运行多个摄像头,但实际在小米10(Android 13)运行,报错当前设备不支持ConcurrentCamera,代码Camer...
继续阅读 »

本来是想通过CameraX实现同时预览多个摄像头,通过官网文档介绍,在CameraX 1.3 后通过ConcurrentCamera运行多个摄像头,但实际在小米10(Android 13)运行,报错当前设备不支持ConcurrentCamera,代码CameraProvider.availableConcurrentCameraInfos查询也是返回数量0,表示设备不支持。


请教ChatGPT回答,来进行编写,回答可以通过代码创建多个previewrequireLensFacing,但是实际运行时不可行的。程序会报下面代码问题,选择摄像头设备异常。


val cameraSelector =builder
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()

因此个人下定义是在cameraX 1.3.0-alpha07前应该是不支持预览多摄像头的。如果有小伙伴验证OK,希望可以告知,多谢。


故采用Camera2来实现多摄像头同时预览。


Camera2 同时预览摄像头


记得先申请权限,以及动态申请!!


    <uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

记得先申请权限,以及动态申请!!


1、判断设备是否支持摄像头


fun isSupportCamera(): Boolean {
initCameraManager()
return cameraManager!!.cameraIdList.isNotEmpty()
}

initCameraManager主要是初始化CameraManager对象cameraManager。我们通过cameraIdList列表是否空来判断是否有摄像头。


private fun initCameraManager() {
if (cameraManager == null) {
cameraManager = getApplication<Application>().getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
}
}

2、获取摄像头列表


我们遍历第1步获取到的摄像头ID列表,然后通过getCameraCharacteristics查询该摄像头相关的数据,封装到NCameraInfo对象中。这里我们只查询几个简单的信息。


fun getCameraListInfo() {

initCameraManager()

if (cameraManager.cameraIdList.isNotEmpty()) {
for (cameraId in cameraManager.cameraIdList) {
val cameInfo = NCameraInfo()
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)

cameInfo.id = cameraId
cameInfo.face ="${ getFaceStr(facing)},CameraId:${cameraId}"
cameraMap[cameraId] = cameInfo
}
cameraInfo.value = cameraMap.values.toList()
}
}

3、打开摄像头


打开摄像头非常简单,只需要调用openCamera函数即可,主要是stateCallback函数的实现。其中handler,是用来切换到主线程var handler = Handler(Looper.getMainLooper())


fun openCamera(cameraId: String) {
initCameraManager()
cameraManager?.openCamera(cameraId, stateCallback, handler)
}

我们一起看看stateCallback函数的实现。也就是当我们打开摄像头,摄像头相关状态会通过下面三个函数进行回调,因为这里采用ViewModel方式,所以会多一份回调到Activity。不用着急,最后有完整代码。


   private val stateCallback=object : StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraMap[camera.id]?.apply {
cameraDevice = camera
state = 1
cameraCallback?.onCameraOpen(this)
}

}

override fun onDisconnected(camera: CameraDevice) {
cameraMap[camera.id]?.apply {
cameraDevice = camera
state = 0
cameraCallback?.onCameraClose(this)
}
}

override fun onError(camera: CameraDevice, error: Int) {
Log.e(TAG, "camera ${camera.id} error code:${error}")
cameraMap[camera.id]?.apply {
cameraDevice = camera
state = 3
cameraCallback?.onCameraError(this,error)
}
}
}

我们查看Activity中的实现。onCameraOpen函数主要动态创建TextureView对象,添加到界面中,用于预览摄像头内容。


	 override fun onCameraOpen(camera: NCameraInfo) {
adapter.notifyItemChanged(adapter.items.indexOf(camera))

//创建TextureView
val textureView = TextureView(this)
textureView.id = View.generateViewId()
camera.previewId=textureView.id
val layoutParams = LinearLayout.LayoutParams(previewWidth, LayoutParams.MATCH_PARENT)
viewBinding.llCameraPreview.addView(textureView, layoutParams)

//textureview 与摄像头绑定
textureView.surfaceTextureListener=object:SurfaceTextureListener{
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
//创建Surface并用于摄像头渲染
val surface = Surface(textureView.surfaceTexture)
val builder = camera.cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)!!
builder.addTarget(surface)

camera.cameraDevice?.createCaptureSession(listOf(surface), object : StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
session.setRepeatingRequest(builder.build(),null,model.handler)
}

override fun onConfigureFailed(session: CameraCaptureSession) {

}
}, model.handler)
}

override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Log.d(TAG,"onSurfaceTextureSizeChanged")
}

override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.d(TAG,"onSurfaceTextureDestroyed")
return true
}

override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
//Log.d(TAG,"onSurfaceTextureUpdated")
}
}


}

override fun onCameraClose(camera: NCameraInfo) {
Log.d(TAG,"onCameraClose:${camera}")
adapter.notifyItemChanged(adapter.items.indexOf(camera))
camera.cameraDevice?.close()
val view=viewBinding.llCameraPreview.findViewById<TextureView>(camera.previewId)
viewBinding.llCameraPreview.removeView(view)
}

override fun onCameraError(camera: NCameraInfo, error: Int) {
Log.e(TAG,"onCameraError:${camera}${error}")
adapter.notifyItemChanged(adapter.items.indexOf(camera))
camera.cameraDevice?.close()
}

4、效果


image-20230615220353385


5、小坑



  • 实测在小米10手机,先开启后摄,再开启前摄,前摄无法打开=》异常。先开前摄,再开后摄正常。

  • 小米11、诺基亚x7实测正常。


项目地址,点我跳战,关键类:Camera2Activity


作者:新小梦
来源:juejin.cn/post/7244783947821236285
收起阅读 »

总是听说 Vue3 选择 Proxy 的原因是性能更好,不如直接上代码对比对比

web
逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.definePropert...
继续阅读 »

逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.defineProperty 性能更好,因此自己创建了一个小 demo 来对比二者在不同场景下的性能。



以下测试仅在 谷歌浏览器 中进行,不同浏览器内核不同,结果可能有差异。可以访问此 在线地址 测试其他环境下的性能。



封装响应式


本文不会详细解析基于 Object.definePropertyProxy 的封装代码,这些内容在多数文章中已有介绍。Vue3 对嵌套对象的响应式处理进行了优化,采用了一种惰性添加的方式,仅在对象被访问时才添加响应式。相比之下,Vue2 采用了一次性递归处理整个对象的方式添加响应式。为了确保比较的公平性,本文下面的 Object.defineProperty 代码也采用了相同的惰性添加策略。


Object.defineProperty


/** Object.defineProperty 深度监听 */
export function deepDefObserve(obj, week) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let value = obj[key]

Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
if (
typeof value === "object" &&
value !== null &&
week &&
!week.has(value)
) {
week.set(value, true)
deepDefObserve(value)
}
return value
},
set(newValue) {
value = newValue
},
})
}
return obj
}

Proxy


/** Proxy 深度监听 */
export function deepProxy(obj, proxyWeek) {
const myProxy = new Proxy(obj, {
get(target, property) {
let res = Reflect.get(target, property)
if (
typeof res === "object" &&
res !== null &&
proxyWeek &&
!proxyWeek.has(res)
) {
proxyWeek.set(res, true)
return deepProxy(res)
}
return res
},
set(target, property, value) {
return Reflect.set(target, property, value)
},
})
return myProxy
}

测试性能


测试场景有五个:



  1. 使用两个 API 创建响应式对象的耗时,即 const obj = reactive({}) 的耗时

  2. 测量对已创建的响应式对象的属性进行访问的速度,即 obj.a 的读取时间。

  3. 测量修改响应式对象属性值的耗时,即执行 obj.a = 1 所需的时间。

  4. 创建多个响应式对象,并模拟访问和修改它们属性的操作,以评估在多对象场景下的性能表现。

  5. 针对嵌套对象进行响应式性能测试,以评估在复杂数据结构下的性能表现。


初始化性能


const _0_calling = {
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(data, keys[i], {
get() {},
set() {},
})
}
},
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = new Proxy(data, {
get() {},
set() {},
})
},
}

image.png


很明显,Proxy 的性能优于 Object.defineProperty


读取性能


const readDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const readProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _1_read = {
useObjectDefineProperty() {
readDefData.a
readDefData.b
readDefData.e
},
useProxy() {
readProxyData.a
readProxyData.b
readProxyData.e
},
}

image.png


Object.defineProperty 明显优于 Proxy


写入性能


const writeDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const writeProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _2_write = {
count: 2,
useObjectDefineProperty() {
writeDefData.a = _2_write.count++
writeDefData.b = _2_write.count++
},
useProxy() {
writeProxyData.a = _2_write.count++
writeProxyData.b = _2_write.count++
},
}

image.png


Object.defineProperty 优于 Proxy,不过差距不大。


多次创建及读写


export const _4_create_read_write = {
count: 2,
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
deepDefObserve(data)
data.a = _4_create_read_write.count++
data.b = _4_create_read_write.count++
data.a
data.c
},
proxyWeek: new WeakMap(),
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = deepProxy(data, _4_create_read_write.proxyWeek)
proxy.a = _4_create_read_write.count++
proxy.b = _4_create_read_write.count++
proxy.a
proxy.c
},
}

image.png


Proxy 优势更大,但这个场景并不多见,很少会出现一次性创建大量响应式对象的情况,对属性的读写场景更多。


对嵌套对象的性能


对内部的每个属性都进行读或写操作


const deepProxyWeek = new WeakMap()
const defWeek = new WeakMap()
export const _5_deep_read_write = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
defWeek
),
useObjectDefineProperty() {
_5_deep_read_write.defData.res.code = _5_deep_read_write.count++
_5_deep_read_write.defData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.defData.res.message.error
_5_deep_read_write.defData.res.data[0].id
_5_deep_read_write.defData.res.data[0].name
_5_deep_read_write.defData.res.data[1].id
_5_deep_read_write.defData.res.data[1].name
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
deepProxyWeek
),
useProxy() {
_5_deep_read_write.proxyData.res.code = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.message.error
_5_deep_read_write.proxyData.res.data[0].id
_5_deep_read_write.proxyData.res.data[0].name
_5_deep_read_write.proxyData.res.data[1].id
_5_deep_read_write.proxyData.res.data[1].name
},
}

image.png


Object.defineProperty 会稍好一些,但两者的差距不大。


只读取修改嵌套对象的浅层属性


const _6_deepProxyWeek = new WeakMap()
const _6_defWeek = new WeakMap()
export const _6_update_top_level = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_deepProxyWeek
),
useObjectDefineProperty() {
_6_update_top_level.defData.res.code = _6_update_top_level.count++
_6_update_top_level.defData.res.message.error
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_defWeek
),
useProxy() {
_6_update_top_level.proxyData.res.code = _6_update_top_level.count++
_6_update_top_level.proxyData.res.message.error
},
}

image.png


这个场景 Proxy 略优于 Object.defineProperty


总结


Proxy 在对象创建时的性能明显优于Object.defineProperty。而在浅层对象的读写性能方面,Object.defineProperty 表现更好。但是当对象的嵌套深度增加时,Object.defineProperty 的优势会逐渐减弱。尽管在性能测试中,Object.defineProperty 的读写优势可能更适合实际开发场景,但在 谷歌浏览器 中,Proxy 的性能与 Object.defineProperty 并没有拉开太大差距。因此,Vue3 选择 Proxy 不仅仅基于性能考量,还因为 Proxy 提供了更为友好、现代且强大的 API ,使得操作更加灵活。


作者:clench
来源:juejin.cn/post/7324141201802821672
收起阅读 »

封装v-loading指令 从此释放双手

web
封装v-loading指令 从此释放双手 前言 ​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以...
继续阅读 »

封装v-loading指令 从此释放双手


前言


​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以拍个照片发朋友圈装逼.


​ 但是想象很美好, 坐着地铁到了自习室, 发现大家伙都是在非常安静, 唯一能出声音的就是翻书或者写字的声音. 为了不影响其他人故意挑一个靠窗户的位置,接着对着电脑开始疯狂进攻. 我的键盘声很快传遍了整见屋子. 虽然别人没有说什么, 但是自己觉得好像是故意来捣乱的, 今天的键盘声音显得格外的大声. 没多久我知趣的溜溜球. 美团体验卷直接gg, 哈哈哈, 问题不大~


​ 言归正传, 今天给大家分享的是利用 VUE3 实现v-loading的加载效果 , 先看一下实现效果吧~


2023-09-24-00-48-48.gif


这一类效果在使用组件库, 例如饿了么中出现的频率很高, 使用方法也很简单, 给对应的结构添加上


v-loading="布尔值"即可, 是不是很好奇是怎么实现的? 那么就和旋风冲锋小瑜开始冲!


实现思路



  • loading肯定也是一个组件, 其中包含加载效果还有提示文字, 并且使用的时候可以去修改文字以及开启或者关闭加载动画

  • 实现的周期是在 异步开始前, 开启loading, 在异步处理[数据加载]完成后 关闭loading

  • 既然是在模版中通过 v-xxx来实现的, 那么肯定就是一个自定义指令, Vue提供指令, 也就是去操作DOM[组件实例]


那么按照以上的实现思路, 一步一步去完成, 首先搭设一个Demo的模版结构和样式


搭设基本模版


利用Vue3搭设demo架子, 头部tab栏, 切换路由 , main区域的显示内容


App.vue


<script setup lang="ts"></script>

<template>
<div class="container">
// Tab栏
<Tabs></Tabs>
// 一级路由出口
<router-view></router-view>
</div>
</template>


<style lang="scss">
.container {
width: 100vw;
height: 100vh;
background-color: #1e1e1e;
}
</style>


router路由


 routes: [
{
path: '/',
redirect: '/huawei'
},
{
path: '/huawei',
component: () => import('@/views/Huawei/index.vue'),
meta: {
title: '华为'
}
},
{
path: '/rongyao',
component: () => import('@/views/Rongyao/index.vue'),
meta: {
title: '荣耀'
}
},
{
path: '/xiaomi',
component: () => import('@/views/Xiaomi/index.vue'),
meta: {
title: '小米'
}
},
{
path: '/oppo',
component: () => import('@/views/Oppo/index.vue'),
meta: {
title: 'oppo'
}
}
]

Tabs组件


<script setup lang="ts">
import { ref } from 'vue'
const tabList = ref([
{ id: 1, text: '华为', path: '/huawei' },
{ id: 2, text: '荣耀', path: '/rongyao' },
{ id: 3, text: '小米', path: '/xiaomi' },
{ id: 4, text: 'oppo', path: '/oppo' }
])
const activeIndex = ref(0)
</script>

<template>
<div class="tabs-box">
<router-link
class="tab-item"
:to="item.path"
v-for="(item, index) in tabList"
:key="item.id"
>

<span
class="tab-link"
:class="{ active: activeIndex === index }"
@click="activeIndex = index"
>

{{ item.text }}
</span>
</router-link>
</div>
</template>


<style lang="scss" scoped>
.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
.tab-item {
padding: 10px;
&.router-link-active {
.tab-link {
transition: border 0.3s;
color: gold;
padding-bottom: 5px;
border-bottom: 2px solid gold;
&.active {
border-bottom: 2px solid gold;
color: gold;
}
}
}
}
}
</style>


按照路由去创建4个文件夹,这里按照huawei做举例


<script setup lang="ts">
const src = ref('')
</script>

<template>
<div class="box" >
<div class="img-box">
<img :src="src" alt="" />
</div>
</div>
</template>


创建Loading组件


首先按照最直接的方式, 利用 v-if 以及组件通讯, 实现组件方式的实现


注意 这里是通过 position: absolute; 通过定位的方式进行垂直水平居中, 先埋下伏笔


<script setup lang="ts">

defineProps({
title: {
type: String,
default: '正在加载中...'
}
})

</script>

<template>
<div class="loading-box">
<div class="loading-content">
// loading 动图
<img src="./loading.gif" />
// 底部提示文字
<p class="desc">{{ title }}</p>
</div>
</div>
</template>


<style lang="scss" scoped>
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
img {
width: 35px !important;
height: 35px !important;
}
.desc {
line-height: 20px;
font-size: 12px;
color: #fff;
position: relative;
}
}
}
</style>


在对应组件中使用Loading组件, 利用延时器模拟异步操作


<script>
const src = ref('')
const title = ref('华为加载中...')
const showLoading = ref(true) // 控制loading的显示和隐藏

onMounted(() => {
showLoading.value = true
// 模拟异步请求
window.setTimeout(() => {
src.value =
'https://ms.bdimg.com/pacific/0/pic/-1284887113_-1109246585.jpg?x=0&y=0&h=340&w=510&vh=340.00&vw=510.00&oh=340.00&ow=510.00'
showLoading.value = false
}, 1000)
})
</script>

<template>
<div class="box">
<div class="img-box" v-if="!showLoading">
<img :src="src" alt="" />
</div>
</div>
<Loading v-if="showLoading"></Loading>
</template>


效果一样可以出来, 接下来就利用指令的方式来优化


011.png


V-loading 指令实现


思路:



  • 在dom挂载完成后, 创建Loading实例, 需要挂载到写在具体指令结构上

  • loading需要知道传递的显示文字, 这里通过指令动态的参数传递

  • 当loading组件参数更新后卸载, 关闭loading


1. 使用自定义指令的参数


和内置指令类似,自定义指令的参数也可以是动态的, 下面是Vue官网的截图


动态指令参数.png


在模版中使用 v-loading:[title]="showLoading"


const title = ref('华为加载中...')

<template>
<div class="box" v-loading:[title]="showLoading">
...
</div>

<!-- <Loading v-if="showLoading"></Loading> -->
</template>

2. 利用插件注册指令


在Loading文件下创建js文件


import Loading from './index.vue' // 导入.vue文件
const loadingDirective = {
}
export default loadingDirective

components下创建index.js文件


import loading from '@/components/Loading/index'
export default {
install: (app: App) => {
app.directive('loading', loading)
}

入口文件中注册插件


import MyLoading from '@/components/index'
app.use(MyLoading)

3. 指令 - 节点都挂载完成后调用



  • createApp 作用: 创建一个应用实例 - 创建loading

  • app.mount 作用: 将应用实例挂载在一个容器元素中

  • mounted 参数 el=>获取dom

  • mounted 参数 binding.value => 控制开启和关闭loding 也就是 showLoading

  • mounted 参数 binding.arg => loading显示的文字 [例如华为加载中...]


const loadingDirective = {
/* 节点都挂载完成后调用 */
mounted(el: any, binding: DirectiveBinding) {
/*
value 控制开启和关闭loding
arg loading显示的文字
*/

const { value, arg } = binding
/* 创建loading实例,并挂载 */
const app = createApp(Loading)
// 这一步 instance === loading.vue
// 此时就可以视同loading.vue 也就是组件实例的方法和属性
const instance = app.mount(document.createElement('div'))
/* 为了让elAppend获取到创建的div元素 */
el.instance = instance
/* 如果传入了自定义的文字就添加title */
if (arg) {
instance.setTitle(arg)
}
/* 如果showLoading为true将loading实例挂载到指令元素中 */
if (value) {
// 添加方法方法, 看下文
handleAppend(el)
}
},
}

可以从控制台查看binding中的title以及showLoading的值


02.png


instance.setTitle(arg) 这里既然使用到了组件实例的setTitle方法, 就需要在loading中对应的方法


注意: 在vue3中需要利用defineExpose抛出事件, 让外界可以访问或使用


Loading.vue


const title = ref('')
const setTitle = (val: string) => {
title.value = val
}
// defineProps({ 组件通讯就使用不到了, 注释即可
// title: {
// type: String,
// default: '正在加载中...'
// }
// })

defineExpose({
setTitle,
title
})

<template>
<div class="loading-box">
<div class="loading-content">
<img src="./loading.gif" />
<p class="desc">{{ title }}</p>
</div>
</div>

</template>

4. 指令 - handleAppend(el)方法实现


/* 将loading添加到指令所在DOM */
const handleAppend = (el: any) => {
console.log(el.instance.$el, 'el.instance.$el')
el.appendChild(el.instance.$el)
}

04.png


5. 指令 - updated() 更新后挂载还是消除的逻辑


在第四步中, loading已经可以通过指令显示了, 此时还需要让showLoading为false的时候, 或者这么理解: 当新的值不等于老值的是够关闭loading


此时就可以利用指令中updated钩子去执行这一段关闭的逻辑, 一下是官网的说明


05.png


  /* 更新后调用 */
updated(el: any, binding: DirectiveBinding) {
const { value, oldValue, arg } = binding
if (value !== oldValue) {
/* 更新标题 */
if (arg) {
el.instance.setTitle(arg)
}
// 是显示吗? 如果是就添加 : 如果不是就删除
value ? handleAppend(el) : c(el)
}
}

6. 指令 - handleRemove()方法实现


/* 将loading在DOM中移除 */
const handleRemove = (el: any) => {
removeClass(el, relative as any)
el.removeChild(el.instance.$el)
}

此时基本已经完成了需求, 但是上文我提到了坑点, 原因是loading是通过绝对定位的方式进行水平居中, 那么比如我要在图片中显示loading呢? 我们来实现下这个坑点


7. 坑点的说明


<template>
<div class="box" //关闭 v-loading:[title]="showLoading">
<div class="img-box" v-loading:[title]="showLoading">
<img :src="src" alt="" />
</div>
</div>

<!-- <Loading v-if="true"></Loading> -->
</template>

06.png


很明显发现, 执行现在图片这个盒子上, 并没有水平居中, 审查元素其实也很明显, css样式中是根据子绝父相, 但是此时大盒子并没有提供相对定位, 自然就无法水平居中


那么如何修改呢? 其实只要给绑定指令的盒子添加position: relative;属性即可, 当然absolute或者fixed效果一样可以居中


问题已找到了, 那么在appendChild时判断当前是否存在relative | absolute | fixed 的其中一个, 如果没有就需要classList.add进行添加, 同时在removeChild删除添加的relative | absolute | fixed 即可


8. 完善坑点, 实现水平居中


getComputedStyle() 在MDN上的说明:


方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。


/* 将loading添加到指令所在DOM */
const relative = 'relative'
const handleAppend = (el: any) => {
const style = getComputedStyle(el)
if (!['absolute', 'relative', 'fixed'].includes(style.position)) {
addClass(el, relative as any)
}

el.appendChild(el.instance.$el)
}
// 添加relative
const addClass = (el: any, className: string) => {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
// 删除relative
const removeClass = (el: any, className: string) => {
el.classList.remove(className)
}

结尾


夜深人静又是卷到凌晨1点, 只管努力, 其他交给天意~ 旋风冲锋手动撒花 ✿✿ヽ(°▽°)ノ✿


demo地址: gitee.com/tcwty123/v-…


作者:不知名小瑜
来源:juejin.cn/post/7281825352530296843
收起阅读 »

今年的年终奖开了个寂寞

大家好啊,我是董董灿。 年底了,又到了一些公司开年终奖的时候了,往年这个时候,网上都是争相"炫富"的声音。 还记得去年某公司,在春节前一下子开出了十几个月的年终奖,让我羡慕了好长时间。 可是今年的形势好像不太乐观,我最近一直在关注年终奖的消息,但怎么感觉,今年...
继续阅读 »

大家好啊,我是董董灿。


年底了,又到了一些公司开年终奖的时候了,往年这个时候,网上都是争相"炫富"的声音。


还记得去年某公司,在春节前一下子开出了十几个月的年终奖,让我羡慕了好长时间。


可是今年的形势好像不太乐观,我最近一直在关注年终奖的消息,但怎么感觉,今年的年终奖开了个寂寞呢?



我也是一个在职场上摸爬滚打的打工人,遇到这种情况,总是会感慨一下。


今年就想和各位小伙伴来聊一个每个打工人可能都会遇到的问题,那就是关于年终奖和工资的那些事儿


1、年终奖本来就是不确定的


我们经常在网上看到一些说法,发了 offer 后给的包有多大,这里说的包指的是薪资总包,先看下什么是总包?


总包一般说的是年薪,它包括基础月薪绩效奖金年终奖金福利待遇以及股票期权等。


基础月薪雷打不动,只要你在公司工作,每个月就会给发,而且是受法律保护的工资。


但绩效奖金或年终奖却不是这样的,这种奖金公司拥有最终解释权。


如果公司效益不好,年终奖可能会打折,甚至直接不发。



当然也有例外。


如果在入职签合同时,合同上明确写了会发 xx 个月的年终奖,但公司又以某种理由不发。


那么这就属于是违反约定,可以通过一些法律途径来解决。


说到这你可能就明白了,只要不是付诸到纸面上的数字,尤其是年终奖,都有不发的风险,而且对公司而言,是有正当理由的。


绩效奖金和年终奖,本来就不是确定的,能拿到多少一方面看自己的能力,另一方面还要看公司的心情。


所以,如果有两个公司提供以下两种薪资待遇,你会选择哪个呢?



  • 月薪 4 万 * (12 + 3) = 60 w, 其中 3 个月工资为绩效浮动奖金

  • 月薪 4.5 万 * (12 + 1) = 58.5 w, 其中 1个月工资为绩效浮动奖金


2、 选现金还是选股票?


除了上面的例子,还有一种比较常见的 offer 选择: 你是要现金还是要股票?


不少人在入职新公司的时候,都会遇到选择薪资方案的情况。


一般情况下,公司会提供两种薪资方案让你选择:高股票方案和高现金方案。


这两种薪资包的总包一般都是一样的,不同的就是在总包中,到底是股票占的多一些还是现金占的多一些。


比如 100 w 的总包,有以下两种方案来选择:



  • 基础月薪 4 万 * (12 + 3) + 40 万股票或期权

  • 基础月薪 5 万 * (12 + 3) + 25 万股票或期权


股票还好一些,如果公司已经上市了,那么股票价值就可以直接根据股价来确定。


那如果公司没有上市,给的是期权,那么就会按照公司估值来计算期权价值。


假设给你价值 30万 的期权,每股估值 1 元,那么共计就是 30 万股。


天知道这家公司的估值到底值不值每股 1 元,可能实际估值只有 0.1 元,那么你的 30万股,可能实际只值 3 万。


而且在公司上市或者可以买卖前,就是一张废纸,没办法变现。


所以,如果你看中了公司的长远价值,并且坚信公司未来会有很好的发展,有信心可以将期权持有到公司上市。


那么就什么都不要想,All in 期权,有多少选多少。


但如果你没有信心,那还是老老实实选择现金比较靠谱,毕竟在现在的环境下,落袋为安才是王道。


我有一个朋友,最近就陷入了这样的两难选择,他前几天来找我随便聊了聊。



我想说的是,我们普通人其实很少有那种眼界,可以看到一个公司可以走多远,未来是否真的可以上市。


选择高期权方案,大部分还是抱着赌一赌的态度,而如果你是稳健型投资选手,我还是建议选择高现金方案。


少年不知现金好,却把期权当成宝。


不知道各位小伙伴怎么看待这个问题的呢,可以在评论区留言讨论。


作者:董董灿是个攻城狮
来源:juejin.cn/post/7324351711659982875
收起阅读 »

前端对接电子秤、扫码枪设备serialPort 串口使用教程

web
因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。 Serialport 官网地址:serialport.io/ Github:g...
继续阅读 »

因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。



Serialport


官网地址:serialport.io/


Github:github.com/serialport/…


官方描述:使用 JavaScript 访问串行端口。Linux、OSX 和 Windows。



SerialPort是什么?



SerialPort 是一个用于在 Node.js 环境中进行串口通信的库。它允许开发者通过 JavaScript 或 TypeScript 代码与计算机上的串口设备进行交互。SerialPort 库提供了丰富的 API,使得在串口通信中能够方便地进行设置、监听和发送数据。



一般我们的设备(电子秤/扫码枪)会有一根线插入到电脑的USB口或者其他口,电脑上的这些插口就是叫串口。设备上的数据会通过这根线传输到电脑里面,比如电子秤传到电脑里的就是重量数值。那么我们前端怎么接收解析到这些数据的呢?SerialPort的作用就是用来帮我们接收设备传输过来的数据,也可以向设备发送数据。


简单概括一下:SerialPort就是我们前端和设备之间的翻译官,可以接收设备传输过来的数据,也可以向设备发送数据。


SerialPort怎么用?


SerialPort可以在Node项目中使用,也可以在Electron项目中使用,我们一般都是用在Electron项目中,接下来讲一下在Electron项目中SerialPort怎么下载和引入


1、创建Electron项目


mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron

网上有很多Electron教程,这里不再详细说了


在package.json中看一下自己的Electron的版本,下一步会用到


2、下载SerialPort


这里先看一下自己使用的Electron对应的Node版本是什么,打开下面electron官网看表格中的Node那一列


Electron发行时间表:http://www.electronjs.org/zh/docs/lat…


image-20240113215800193.png


如果你Electron对应的Node版本高于v12.0.0,直接下载就行


npm install serialport

如果你Electron对应的Node版本低于或等于v12.0.0,请用对应的Node版本对应下面的serialport版本下载



serialport.io/docs/next/g…




  • 对于 Node.js 版本0.100.12,最后一个正常运行的版本是serialport@4

  • 对于 Node.js 版本4.0,最后一个正常运行的版本是serialport@6.

  • 对于 Node.js 版本8.0,最后一个正常运行的版本是serialport@8.

  • 对于 Node.js 版本10.0,最后一个正常运行的版本是serialport@9.

  • 对于 Node.js 版本12.0,最后一个正常运行的版本是serialport@10.



我项目的Electron版本是11.5.0,对应的Node版本号是12.0,对应的serialport版本号是serialport@10.0.0



3、编译Serialport



  • 安装node-gyp 用于调用其他语言编写的程序(如果已安装过请忽略这一步)


    npm install -g node-gyp




  • 进入@serialport目录


    cd ./node_modules/@serialport/bindings


  • 进行编译,target后面换成当前Electron的版本号


    node-gyp rebuild --target=11.5.0



如果编译的时候报错了就将自己电脑的Node版本切换成当前Electron对应的版本号再编译一次


查看Electron对应Node版本号:http://www.electronjs.org/zh/docs/lat…


编译成功以后就可以在代码里使用Serialport了


4、使用Serialport



serialport官网使用教程:serialport.io/docs/next/g…



4.1、引入Serialport


const { SerialPort } = require('serialport')
// or
import { SerialPort } from 'serialport'

4.2、创建串口(重点!)


创建串口有两种写法,新版本是这样写法new SerialPort(params, callback)


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

旧版本是下面这样的写法new Serialport(path, params, callback),我用的是serialport@10.0.0版本就是这样的写法


const port = new Serialport('COM1', {
 baudRate: 9600,
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

创建串口的时候需要传入两个重要的参数是path和baudRate,path是串口号,baudRate是波特率。最后一个参数是回调函数



不知道怎么查看串口号和波特率看这篇文章


如何查看串口号和波特率?



4.3、手动打开串口


如果autoOpen参数是false,需要使用port.open()方法手动打开


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: false,  // 是否自动打开端口, 默认true
})
// autoOpen参数是false,需要使用port.open()方法手动打开
port.open(function (err) {
 if (err) {
   return console.log('打开失败', err.message)
}
 console.log('打开成功')
})

4.4、接收数据(重点!)


接收到的data是一个Buffer,需要转换为字符串进行查看


port.on('data', function (data) {
 // 接收到的data是一个Buffer,需要转换为字符串进行查看
 console.log('Data:', data.toString('utf-8'))
})

接收过来的data就是设备传输过来的数据,转换后的字符串就是我们需要的数据,字符串里面可能有多个数据,我们把自己需要的数据截取出来就可以了


假设通过电子秤设备获取到的数据就是"205 000 000",中间是四个空格分割的,第一个数字205就是获取的重量,需要把这个重量截取出来。下面是我的示例代码


port.on('data', function (data) {
 try {
     // 获取的data是一个Buffer
     // 1.将 Buffer 转换为字符串 dataString.toString('utf-8')
     let weight = data.toString('utf-8')
     // 2.将字符串分割转换成数组,取数组的第一个值.split('   ')[0]
     weight = weight.split('   ')[0]
     // 3.将取的值 去掉前后空格
     weight = weight.trim()
     // 4.最后转换成数字,获取到的数字就是重量
     weight = Number(weight)
     console.log('获取到重量:'+ weight);
} catch (err) {
   console.error(`
     重量获取报错:${err}
     获取到的Buffer: ${data}
     Buffer转换后的值:${data.toString('utf-8')}
   `
);
}
})

4.5、写入数据


port.write('Hi Mom!')
port.write(Buffer.from('Hi Mom!'))

4.6、实时获取(监听)所有串口


const { SerialPort } = require('serialport')

SerialPort.list().then((ports, err) => {
   // 串口列表
   console.log('获取所有串口列表', ports);
})

更多内容


serialport官网教程:serialport.io/docs/next/g…


作者:Yaoqi
来源:juejin.cn/post/7323464381172301860
收起阅读 »

从framework角度看app保活问题

问题背景 最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论 不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发...
继续阅读 »

问题背景


最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论


保活


不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发者,根本无能为力,可真的是这样的吗?


保活方案


首先,我整理了从古到今,app开发者所使用过的以及当前还在使用的保活方式,主要思路有两个:保活和复活


保活的方案有:


  • 1像素惨案

  • 后台无声音乐

  • 前台service

  • 心跳机制

  • socket长连接

  • 无障碍服务

  • ......


复活的方案有:


  • 双进程守护(java层和native层)

  • JobScheduler定时任务

  • 推送/相互唤醒

  • ......


不难看出,app开发者为了能让自己的应用多存活一会儿,可谓是绞尽脑汁,但即使这样,随着Android系统升级,尤其是进入8.0之后,系统对应用的限制越来越高,传统的保活方式已经不生效,这让Android开发者手足无措,于是乎,出现了一种比较和谐的保活方式:



  • 引导用户开启手机白名单


这也是目前绝大多数应用所采用的的方式,相对于传统黑科技而言,此方式显得不那么流氓,比较容易被用户所接受。


但跟微信这样的国民级应用比起来,保活效果还是差了一大截,那么微信是怎么实现保活的呢?或者回到我们开头的问题,应用的生死真的只能靠系统调度吗?开发者能否干预控制呢?


进程调度原则


解开这个疑问之前,我们需要了解一下Android系统进程调度原则,主要介绍framework中承载四大组件的进程是如何根据组件状态而动态调节自身状态的。进程有两个比较重要的状态值:



  • oom_adj,定义在frameworks/base/services/core/java/com/android/server/am/ProcessList.java当中

  • procState,定义在frameworks/base/core/java/android/app/ActivityManager.java当中


OOM_ADJ

以Android10的源码为例,oom_adj划分为20级,取值范围[-10000,1001],Android6.0以前的取值范围是[-17,16]



  • oom_adj值越大,优先级越低

  • oom_adj<0的进程都是系统进程。


public final class ProcessList {
static final String TAG = TAG_WITH_CLASS_NAME ? "ProcessList" : TAG_AM;

// The minimum time we allow between crashes, for us to consider this
// application to be bad and stop and its services and reject broadcasts.
static final int MIN_CRASH_INTERVAL = 60 * 1000;

// OOM adjustments for processes in various states:

// Uninitialized value for any major or minor adj fields
static final int INVALID_ADJ = -10000;

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 1001;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;

// This is the oom_adj level that we allow to die first. This cannot be equal to
// CACHED_APP_MAX_ADJ unless processes are actively being assigned an oom_score_adj of
// CACHED_APP_MAX_ADJ.
static final int CACHED_APP_LMK_FIRST_ADJ = 950;

// Number of levels we have available for different service connection group importance
// levels.
static final int CACHED_APP_IMPORTANCE_LEVELS = 5;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 800;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app. This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 700;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 600;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 500;

// This is a process with a heavy-weight application. It is in the
// background, but we want to try to avoid killing it. Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 400;

// This is a process currently hosting a backup operation. Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 300;

// This is a process bound by the system (or other app) that's more important than services but
// not so perceptible that it affects the user immediately if killed.
static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 200;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 100;
static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

// This is a process that was recently TOP and moved to FGS. Continue to treat it almost
// like a foreground app for a while.
// @see TOP_TO_FGS_GRACE_PERIOD
static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

// This is the process running the current foreground app. We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -700;

// This is a system persistent process, such as telephony. Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -800;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -900;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -1000;

// Memory pages are 4K.
static final int PAGE_SIZE = 4 * 1024;

//省略部分代码
}

ADJ级别取值说明(可参考源码注释)
INVALID_ADJ-10000未初始化adj字段时的默认值
UNKNOWN_ADJ1001缓存进程,无法获取具体值
CACHED_APP_MAX_ADJ999不可见activity进程的最大值
CACHED_APP_MIN_ADJ900不可见activity进程的最小值
CACHED_APP_LMK_FIRST_ADJ950lowmemorykiller优先杀死的级别值
SERVICE_B_ADJ800旧的service的
PREVIOUS_APP_ADJ700上一个应用,常见于应用切换场景
HOME_APP_ADJ600home进程
SERVICE_ADJ500创建了service的进程
HEAVY_WEIGHT_APP_ADJ400后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ300备份进程
PERCEPTIBLE_LOW_APP_ADJ250受其他进程约束的进程
PERCEPTIBLE_APP_ADJ200可感知组件的进程,比如背景音乐播放
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ50最近运行的后台进程
FOREGROUND_APP_ADJ0前台进程,正在与用户交互
PERSISTENT_SERVICE_ADJ-700系统持久化进程已绑定的进程
PERSISTENT_PROC_ADJ-800系统持久化进程,比如telephony
SYSTEM_ADJ-900系统进程
NATIVE_ADJ-1000native进程,不受系统管理

可以通过cat /proc/进程id/oom_score_adj查看目标进程的oom_adj值,例如我们查看电话的adj


dialer_oom_adj


值为935,处于不可见进程的范围内,当我启动电话app,再次查看


dialer_oom_adj_open


此时adj值为0,也就是正在与用户交互的进程


ProcessState

process_state划分为23类,取值范围为[-1,21]


@SystemService(Context.ACTIVITY_SERVICE)
public class ActivityManager {
//省略部分代码
/** @hide Not a real process state. */
public static final int PROCESS_STATE_UNKNOWN = -1;

/** @hide Process is a persistent system process. */
public static final int PROCESS_STATE_PERSISTENT = 0;

/** @hide Process is a persistent system process and is doing UI. */
public static final int PROCESS_STATE_PERSISTENT_UI = 1;

/** @hide Process is hosting the current top activities. Note that this covers
* all activities that are visible to the user. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_TOP = 2;

/** @hide Process is hosting a foreground service with location type. */
public static final int PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3;

/** @hide Process is bound to a TOP app. This is ranked below SERVICE_LOCATION so that
* it doesn't get the capability of location access while-in-use. */

public static final int PROCESS_STATE_BOUND_TOP = 4;

/** @hide Process is hosting a foreground service. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 5;

/** @hide Process is hosting a foreground service due to a system binding. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 6;

/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 7;

/** @hide Process is important to the user, but not something they are aware of. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 8;

/** @hide Process is in the background transient so we will try to keep running. */
public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 9;

/** @hide Process is in the background running a backup/restore operation. */
public static final int PROCESS_STATE_BACKUP = 10;

/** @hide Process is in the background running a service. Unlike oom_adj, this level
* is used for both the normal running in background state and the executing
* operations state. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_SERVICE = 11;

/** @hide Process is in the background running a receiver. Note that from the
* perspective of oom_adj, receivers run at a higher foreground level, but for our
* prioritization here that is not necessary and putting them below services means
* many fewer changes in some process states as they receive broadcasts. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_RECEIVER = 12;

/** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
public static final int PROCESS_STATE_TOP_SLEEPING = 13;

/** @hide Process is in the background, but it can't restore its state so we want
* to try to avoid killing it. */

public static final int PROCESS_STATE_HEAVY_WEIGHT = 14;

/** @hide Process is in the background but hosts the home activity. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_HOME = 15;

/** @hide Process is in the background but hosts the last shown activity. */
public static final int PROCESS_STATE_LAST_ACTIVITY = 16;

/** @hide Process is being cached for later use and contains activities. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_CACHED_ACTIVITY = 17;

/** @hide Process is being cached for later use and is a client of another cached
* process that contains activities. */

public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 18;

/** @hide Process is being cached for later use and has an activity that corresponds
* to an existing recent task. */

public static final int PROCESS_STATE_CACHED_RECENT = 19;

/** @hide Process is being cached for later use and is empty. */
public static final int PROCESS_STATE_CACHED_EMPTY = 20;

/** @hide Process does not exist. */
public static final int PROCESS_STATE_NONEXISTENT = 21;
//省略部分代码
}

state级别取值说明(可参考源码注释)
PROCESS_STATE_UNKNOWN-1不是真正的进程状态
PROCESS_STATE_PERSISTENT0持久化的系统进程
PROCESS_STATE_PERSISTENT_UI1持久化的系统进程,并且正在操作UI
PROCESS_STATE_TOP2处于栈顶Activity的进程
PROCESS_STATE_FOREGROUND_SERVICE_LOCATION3运行前台位置服务的进程
PROCESS_STATE_BOUND_TOP4绑定到top应用的进程
PROCESS_STATE_FOREGROUND_SERVICE5运行前台服务的进程
PROCESS_STATE_BOUND_FOREGROUND_SERVICE6绑定前台服务的进程
PROCESS_STATE_IMPORTANT_FOREGROUND7对用户很重要的前台进程
PROCESS_STATE_IMPORTANT_BACKGROUND8对用户很重要的后台进程
PROCESS_STATE_TRANSIENT_BACKGROUND9临时处于后台运行的进程
PROCESS_STATE_BACKUP10备份进程
PROCESS_STATE_SERVICE11运行后台服务的进程
PROCESS_STATE_RECEIVER12运动广播的后台进程
PROCESS_STATE_TOP_SLEEPING13处于休眠状态的进程
PROCESS_STATE_HEAVY_WEIGHT14后台进程,但不能恢复自身状态
PROCESS_STATE_HOME15后台进程,在运行home activity
PROCESS_STATE_LAST_ACTIVITY16后台进程,在运行最后一次显示的activity
PROCESS_STATE_CACHED_ACTIVITY17缓存进程,包含activity
PROCESS_STATE_CACHED_ACTIVITY_CLIENT18缓存进程,且该进程是另一个包含activity进程的客户端
PROCESS_STATE_CACHED_RECENT19缓存进程,且有一个activity是最近任务里的activity
PROCESS_STATE_CACHED_EMPTY20空的缓存进程,备用
PROCESS_STATE_NONEXISTENT21不存在的进程

进程调度算法

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java中,有三个核心方法用于计算和更新进程的oom_adj值



  • updateOomAdjLocked():更新adj,当目标进程为空,或者被杀则返回false,否则返回true。

  • computeOomAdjLocked():计算adj,计算成功返回true,否则返回false。

  • applyOomAdjLocked():应用adj,当需要杀掉目标进程则返回false,否则返回true。


adj更新时机

也就是updateOomAdjLocked()被调用的时机。通俗的说,只要四大组件被创建或者状态发生变化,或者当前进程绑定了其他进程,都会触发adj更新,具体可在源码中查看此方法被调用的地方,比较多,这里就不列举了


adj的计算过程

computeOomAdjLocked()计算过程相当复杂,将近1000行代码,这里就不贴了,有兴趣可自行查看,总体思路就是根据当前进程的状态,设置对应的adj值,因为状态值很多,所以会有很多个if来判断每个状态是否符合,最终计算出当前进程属于哪种状态。


adj的应用

计算得出的adj值将发送给lowmemorykiller(简称lmk),由lmk来决定进程的生死,不同的厂商,lmk的算法略有不同,下面是源码中对lmk的介绍


/* drivers/misc/lowmemorykiller.c
*
* The lowmemorykiller driver lets user-space specify a set of memory thresholds
* where processes with a range of oom_score_adj values will get killed. Specify
* the minimum oom_score_adj values in
* /sys/module/lowmemorykiller/parameters/adj and the number of free pages in
* /sys/module/lowmemorykiller/parameters/minfree. Both files take a comma
* separated list of numbers in ascending order.
*
* For example, write "0,8" to /sys/module/lowmemorykiller/parameters/adj and
* "1024,4096" to /sys/module/lowmemorykiller/parameters/minfree to kill
* processes with a oom_score_adj value of 8 or higher when the free memory
* drops below 4096 pages and kill processes with a oom_score_adj value of 0 or
* higher when the free memory drops below 1024 pages.
*
* The driver considers memory used for caches to be free, but if a large
* percentage of the cached memory is locked this can be very inaccurate
* and processes may not get killed until the normal oom killer is triggered.
*
* Copyright (C) 2007-2008 Google, Inc.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/


保活核心思路


根据上面的Android进程调度原则得知,我们需要尽可能降低app进程的adj值,从而减少被lmk杀掉的可能性,而我们传统的保活方式最终目的也是降低adj值。而根据adj等级分类可以看出,通过应用层的方式最多能将adj降到100~200之间,我分别测试了微信、支付宝、酷狗音乐,启动后返回桌面并息屏,测试结果如下


微信测试结果:


weixin_oom_adj


微信创建了两个进程,查看这两个进程的adj值均为100,对应为adj等级表中的VISIBLE_APP_ADJ,此结果为测试机上微信未登录状态测试结果,当换成我的小米8测试后发现,登录状态下的微信有三个进程在运行


weixin_login_oom_adj


后查阅资料得知,进程名为com.tencent.soter.soterserver的进程是微信指纹支付,此进程的adj值居然为-800,上面我们说过,adj小于0的进程为系统进程,那么微信是如何做到创建一个系统进程的,我和我的小伙伴都惊呆了o.o,为此,我对比了一下支付宝的测试结果


支付宝测试结果:


alipay_oom_adj


支付宝创建了六个进程,查看这六个进程的adj值,除了一个为915,其余均为0,怎么肥事,0就意味着正在与用户交互的前台进程啊,我的世界要崩塌了,只有一种可能,支付宝通过未知的黑科技降低了adj值。


酷狗测试结果:


kugou_oom_adj.png


酷狗创建了两个进程,查看这两个进程的adj值分别为700、200,对应为adj等级表中的PREVIOUS_APP_ADJPERCEPTIBLE_APP_ADJ,还好,这个在意料之中。


测试思考


通过上面三个app的测试结果可以看出,微信和支付宝一定是使用了某种保活手段,让自身的adj降到最低,尤其是微信,居然可以创建系统进程,简直太逆天了,这是应用层绝对做不到的,一定是在native层完成的,但具体什么黑科技就不得而知了,毕竟反编译技术不是我的强项。


正当我郁郁寡欢之时,我想起了前两天看过的一篇文章《当 App 有了系统权限,真的可以为所欲为?》,文章讲述了第三方App如何利用CVE漏洞获取到系统权限,然后神不知鬼不觉的干一些匪夷所思的事儿,这让我茅塞顿开,或许这些大厂的app就是利用了系统漏洞来保活的,不然真的就说不通了,既然都能获取到系统权限了,那创建个系统进程不是分分钟的事儿吗,还需要啥厂商白名单。


总结


进程保活是一把双刃剑,增加app存活时间的同时牺牲的是用户手机的电量,内存,cpu等资源,甚至还有用户的忍耐度,作为开发者一定要合理取舍,不要为了保活而保活,即使需要保活,也尽量采用白色保活手段,别让用户手机变板砖,然后再来哭爹骂娘。


参考资料:


探讨Android6.0及以上系统APP常驻内存(保活)实现-争宠篇


探讨Android6.0及以上系统APP常驻内存(保活)实现-复活篇


探讨一种新型的双进程守护应用保活


史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术


当 App 有了系统权限,真的可以为所欲为?


「 深蓝洞察 」2022 年度最“不可赦”漏洞


作者:小迪vs同学
来源:juejin.cn/post/7210375037114138680
收起阅读 »

flex布局之美,以后就靠它来布局了

web
写在前面 在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的align 和valign可以实现水平垂直居中等 再后来,由于CSS 不断完善,便演变出了:标准文档流、浮动布局和定位布局 3种布局 来实现水平垂直居中等各种布局...
继续阅读 »

写在前面


在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的alignvalign可以实现水平垂直居中等


再后来,由于CSS 不断完善,便演变出了:标准文档流浮动布局定位布局 3种布局 来实现水平垂直居中等各种布局需求。


下面我们来看看实现如下效果,各种布局是怎么完成的


image-20240114134424060


实现这样的布局方式很多,为了方便演示效果,我们在html代码种添加一个父元素,一个子元素,css样式种添加一个公共样式来设置盒子大小,背景颜色


<div class="parent">
  <div class="child">我是子元素</div>
</div>

/* css公共样式代码 */
.parent{
   background-color: orange;
   width: 300px;
   height: 300px;
}
.child{
   background-color: lightcoral;
   width: 100px;
   height: 100px;
}

①absolute + 负margin 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;;
   top: 50%;
   left: 50%;
   margin-left: -50px;
   margin-top: -50px;
}

②absolute + transform 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
}

③ flex实现


.parent {
   display: flex;
   justify-content: center;
   align-items: center;
}

通过上面三种实现来看,我们应该可以发现flex 布局是最简单了吧。


对于一个后端开发人员来说,flex布局算是最友好的了,因为它操作简单方便


一、flex 布局简介



flex 全称是flexible Box,意为弹性布局 ,用来为盒状模型提供布局,任何容器都可以指定为flex布局。


通过给父盒子添加flex属性即可开启弹性布局,来控制子盒子的位置和排列方式。


父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准



flex布局


二、flex基本概念



flex的核心概念是 容器,容器包括外层的 父容器 和内层的 子容器,轴包括 主轴辅轴



<div class="parent">
   <div class="child">我是子元素</div>
</div>

2.1 轴



  • 在 flex 布局中,是分为主轴和侧轴两个方向,同样的叫法有 : 行和列、x 轴和y 轴,主轴和交叉轴

  • 默认主轴方向就是 x 轴方向,水平向右

  • 默认侧轴方向就是 y 轴方向,水平向下


    主轴和侧轴



注:主轴和侧轴是会变化的,就看 flex-direction 设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的


--flex-direction 值--含义
row默认值,表示主轴从左到右
row-reverse表示主轴从右到左
column表示主轴从上到下
column-reverse表示主轴从下到上

2.2 容器



容器的属性可以作用于父容器(container)或者子容器(item)上



①父容器(container)-->属性添加在父容器上



  • flex-direction 设置主轴的方向

  • justify-content 设置主轴上的子元素排列方式

  • flex-wrap 设置是否换行

  • align-items 设置侧轴上的子元素排列方式(单行 )

  • align-content 设置侧轴上的子元素的排列方式(多行)


②子容器(item)-->属性添加在子容器上



  • flex 属性 定义子项目分配剩余空间,用flex来表示占多少份数

  • align-self控制子项自己在侧轴上的排列方式

  • order 属性定义项目的排列顺序


三、主轴侧轴设置


3.1 flex-direction: row



flex-direction: row 为默认属性,主轴沿着水平方向向右,元素从左向右排列。



row


3.2 flex-direction: row-reverse



主轴沿着水平方向向左,子元素从右向左排列



row-reverse


3.3 flex-direction: column



主轴垂直向下,元素从上向下排列



column


3.4 flex-direction: column-reverse



主轴垂直向下,元素从下向上排列



column-reverse


四、父容器常见属性设置


4.1 主轴上子元素排列方式


4.1.1 justify-content


justify-content 属性用于定义主轴上子元素排列方式


justify-content: flex-start|flex-end|center|space-between|space-around



flex-start:起始端对齐


flex-start


flex-end:末尾段对齐


flex-end


center:居中对齐


center


space-around:子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。


space-around


space-between:子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。


space-between


4.2 侧轴上子元素排列方式


4.2.1 align-items 单行子元素排列


这里我们就以默认的x轴作为主轴



align-items:flex-start:起始端对齐


flex-start


align-items:flex-end:末尾段对齐


flex-end


align-items:center:居中对齐


center


align-items:stretch 侧轴拉伸对齐



如果设置子元素大小后不生效



stretch


4.2.2 align-content 多行子元素排列


设置子项在侧轴上的排列方式 并且只能用于子项出现 换行 的情况(多行),在单行下是没有效果的


我们需要在父容器中添加 flex-wrap: wrap;


flex-wrap: wrap; 是啥意思了,具体会在下一小节中细说,就是当所有子容器的宽度超过父元素时,换行显示



align-content: flex-start 起始端对齐


 /* 父容器添加如下代码 */
display: flex;
align-content: flex-start;
flex-wrap: wrap;

align-content: flex-start


align-content: flex-end :末端对齐


/* 父容器添加如下代码 */
display: flex;
align-content: flex-end;
flex-wrap: wrap;

align-content: flex-end


align-content: center: 中间对齐


/* 父容器添加如下代码 */
display: flex;
align-content: center;
flex-wrap: wrap;

align-content: center


align-content: space-around: 子容器沿侧轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半


/* 父容器添加如下代码 */
display: flex;
align-content: space-around;
flex-wrap: wrap;

align-content: space-around


align-content: space-between:子容器沿侧轴均匀分布,位于首尾两端的子容器与父容器相切。


/* 父容器添加如下代码 */
display: flex;
align-content: space-between;
flex-wrap: wrap;

image-20240114171606954


align-content: stretch: 子容器高度平分父容器高度


/* 父容器添加如下代码 */
display: flex;
align-content: stretch;
flex-wrap: wrap;

align-content: stretch


4.3 设置是否换行



默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,flex布局中默认是不换行的。



flex-wrap: nowrap :不换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: nowrap;

flex-wrap: nowrap


flex-wrap: wrap: 换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: wrap;

flex-wrap: wrap


4.4 align-content 和align-items区别



  • align-items 适用于单行情况下, 只有上对齐、下对齐、居中和 拉伸

  • align-content适应于换行(多行)的情况下(单行情况下无效), 可以设置 上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。

  • 总结就是单行找align-items 多行找 align-content


五、子容器常见属性设置



  • flex子项目占的份数

  • align-self控制子项自己在侧轴的排列方式

  • order属性定义子项的排列顺序(前后顺序)


5.1 flex 属性



flex 属性定义子项目分配剩余空间,用flex来表示占多少份数。



① 语法


.item {
   flex: <number>; /* 默认值 0 */
}

②将1号、3号子元素宽度设置成80px,其余空间分给2号子元素


flex:1


5.2 align-self 属性



align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。


默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。



align-self: flex-start 起始端对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-start;

align-self: flex-start


align-self: flex-end 末尾段对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-end;

align-self: flex-end


align-self: center 居中对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素*/
align-self: center;

align-self: center


align-self: stretch 拉伸对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素 未指定高度才生效*/
align-self: stretch;

align-self: stretch


5.3 order 属性



数值越小,排列越靠前,默认为0。



① 语法:


.item {
   order: <number>;
}

② 既然默认是0,那我们将第二个子容器order:-1,那第二个元素就跑到最前面了


/* 父容器添加如下代码 */
display: flex;
/*第二个子元素*/
order: -1;

order


六、小案例


最后我们用flex布局实现下面常见的商品列表布局


商品列表


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>简单商品布局</title>
   <style>
       .goods{
           display: flex;
           justify-content: center;
      }
       p{
           text-align: center;
      }
       span{
           margin: 0;
           color: red;
           font-weight: bold;
      }
       .goods001{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods002{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods003{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods004{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }

   
</style>
</head>
<body>

   <div class="goods">
       <div class="goods001">
           <img src="./imgs/goods001.jpg" >
           <p>松下(Panasonic)洗衣机滚筒</p>
           <span>¥3899.00</span>
       </div>
       <div class="goods002">
           <img src="./imgs/goods002.jpg" >
           <p>官方原装浴霸灯泡</p>
           <span>¥17.00</span>
       </div>
       <div class="goods003">
           <img src="./imgs/goods003.jpg" >
           <p>全自动变频滚筒超薄洗衣机</p>
           <span>¥1099.00</span>
       </div>
       <div class="goods004">
           <img src="./imgs/goods004.jpg" >
           <p>绿联 车载充电器</p>
           <span>¥28.90</span>
       </div>
   </div>
</body>
</html>

以上就是本期内容的全部,希望对你有所帮助。我们下期再见 (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7323539673346375719
收起阅读 »

如何选择 Android 唯一标识符

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。 标识符 IMEI 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。


标识符


IMEI



  • 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10+ 开始官方取消了获取 IMEI 的 API,无法获取到 IMEI 了


fun getIMEI(context: Context): String {
val telephonyManager = context
.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
return telephonyManager.deviceId
}

Android ID(SSAID)



  • 无需任何权限

  • 卸载安装不会改变,除非刷机或重置系统

  • Android 8.0 之后签名不同的 APP 获取的 Android ID 是不一样的

  • 部分设备由于制造商错误实现,导致多台设备会返回相同的 Android ID

  • 可能为空


fun getAndroidID(context: Context): String {
return Settings.System.getString(context.contentResolver,Settings.Secure.ANDROID_ID)
}

MAC 地址



  • 需要申请权限,Android 12 之后 BluetoothAdapter.getDefaultAdapter().getAddress()需要动态申请 android.permission.BLUETOOTH_CONNECT 权限

  • MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化

  • 搭载 Android 10+ 的设备会报告不是设备所有者应用的所有应用的随机化 MAC 地址

  • 在 Android 6.0 到 Android 9 中,本地设备 MAC 地址(如 WLAN 和蓝牙)无法通过第三方 API 使用 会返回 02:00:00:00:00:00,且需要 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限


Widevine ID



  • DRM 数字版权管理 ID ,访问此 ID 无需任何权限

  • 对于搭载 Android 8.0 的设备,Widevine 客户端 ID 将为每个应用软件包名称和网络源(对于网络浏览器)返回一个不同的值

  • 可能为空


fun getWidevineID(): String {
try {
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
val mediaDrm = MediaDrm(WIDEVINE_UUID)
val widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
val sb = StringBuilder();
for (byte in widevineId) {
sb.append(String.format("x", byte))
}
return sb.toString();
} catch (e: Exception) {
} catch (e: Error) {
}
return ""
}

AAID



  • 无需任何权限

  • Google 推出的广告 ID ,可由用户重置的标识符,适用于广告用例

  • 系统需要自带 Google Play Services 才支持,且用户可以在系统设置中重置



重置后,在未获得用户明确许可的情况下,新的广告标识符不得与先前的广告标识符或由先前的广告标识符所衍生的数据相关联。




还要注意,Google Play 开发者内容政策要求广告 ID“不得与个人身份信息或任何永久性设备标识符(例如:SSAID、MAC 地址、IMEI 等)相关联。”




在支持多个用户(包括访客用户在内)的 Android 设备上,您的应用可能会在同一设备上获得不同的广告 ID。这些不同的 ID 对应于登录该设备的不同用户。



OAID



  • 无需任何权限

  • 国内移动安全联盟出台的“拯救”国内移动广告的广告跟踪标识符

  • 基本上是国内知名厂商 Android 10+ 才支持,且用户可以在系统设置中重置


UUID



  • 生成之后本地持久化保存

  • 卸载后重新安装、清除应用缓存 会改变


如何选择


同个开发商需要追踪对比旗下应用各用户的行为



  • 可以采用 Android ID(SSAID),并且不同应用需使用同一签名

  • 如果获得的 Android ID(SSAID)为空,可以用 UUID 代替【 OAID / AAID 代替也可,但需要引入第三方库】

  • 在 Android 8.0+ 中, Android ID(SSAID)提供了一个在由同一开发者签名密钥签名的应用之间通用的标识符


希望限制应用内的免费内容(如文章)



  • 可以采用 UUID ,作用域是应用范围,用户要想规避内容限制就必须重新安装应用


用户群体主要是大陆



  • 可以采用 OAID ,低版本配合采用 Android ID(SSAID)/ UUID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等


用户群体在海外



  • 可以采用 AAID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等

作者:未央歌
来源:juejin.cn/post/7262558218169008188
收起阅读 »

Android 逆向从入门到入“狱”

免责声明 本次技术分享仅用于逆向技术的交流与学习,请勿用于其他非法用途;技术是把双刃剑,请善用它。 逆向是什么、可以做什么、怎么做 简单讲,就是将别人打包好的 apk 进行反编译,得到源码并分析代码逻辑,最终达成自己的目的。 可以做的事: 修改 smali...
继续阅读 »

免责声明


本次技术分享仅用于逆向技术的交流与学习,请勿用于其他非法用途;技术是把双刃剑,请善用它。


逆向是什么、可以做什么、怎么做



  • 简单讲,就是将别人打包好的 apk 进行反编译,得到源码并分析代码逻辑,最终达成自己的目的。

  • 可以做的事:



    • 修改 smali 文件,使程序达到自己想要的效果,重新编译签名安装,如去广告、自动化操作、电商薅羊毛、单机游戏修改数值、破解付费内容、汉化、抓包等

    • 阅读源码,借鉴别人写好的技术实践

    • 破解:小组件盒子:http://www.coolapk.com/apk/io.ifte…



  • 怎么做:



    • 这是门庞杂的技术活,需要知识的广度、经验、深度

    • 需要具体问题,具体分析,有针对性的学习与探索

    • 了解打包原理、ARM、Smali汇编语言

    • 加固、脱壳

    • Xposed、Substrate、Fridad等框架

    • 加解密

    • 使用好工具## 今日分享涉及工具



  • apktool:反编译工具



    • 反编译:apktool d <apkPath> o <outputPath>

    • 重新打包:apktool b <fileDirPath> -o <apkPath>

    • 安装:brew install apktool



  • jadx:支持命令行和图形界面,支持apk、dex、jar、aar等格式的文件查看




  • apksigner:签名工具




  • Charles:抓包工具



    • http://www.charlesproxy.com/

    • Android 7 以上抓包 HTTPS ,需要手机 Root 后将证书安装到系统中

    • Android 7 以下 HTTPS 直接抓




正题




  • 正向编译



    • java -> class -> dex -> apk



  • 反向编译



    • apk -> dex -> smali -> java



  • Smali 是 Android 的 Dalvik 虚拟机所使用的一种 dex 格式的中间语言

  • 官方文档source.android.com/devices/tec…

  • code.flyleft.cn/posts/ac692…

  • 正题开始,以反编译某瓣App为例:



    • jadx 查看 Java 源码,找到想修改的代码

    • 反编译得到 smali 源码:apktool d douban.apk -o doubancode --only-main-classes

    • 修改:找到 debug 界面入口并打开

    • 将修改后的 smali 源码正向编译成 apk:apktool b doubancode -o douban_mock1.apk

    • 重签名:jarsigner -verbose -keystore keys.jks test.apk key0

    • 此时的包不能正常访问接口,因为豆瓣 API 做了签名校验,而我们的新 apk 是用了新的签名,看接口抓包

    • 怎么办呢?

    • 继续分析代码,修改网络请求中的 apikey

    • 来看看新的 apk



  • 也可以做爬虫等


启发与防范



  • 混淆

  • 加固

  • 加密

  • 运行环境监测

  • 不写敏感信息或操作到客户端

  • App 运行签名验证

  • Api 接口签名验证


One More Thing



作者:Sinyu1012
来源:juejin.cn/post/7202573260659163195
收起阅读 »

前任开发在代码里下毒了,支付下单居然没加幂等

分享是最有效的学习方式。 故事 又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。 不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。 小猫回忆了一下“不对啊,...
继续阅读 »

分享是最有效的学习方式。



故事


又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。


不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。


小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台的老六直接找我要个支付接口,我就给他了的,以前的代码,我都没有动过的......”。


于是小猫一边疑惑一边翻看着以前的代码,越看脸色越差......


42175B273A64E95B1B5B66D392256552.jpg


小猫做的是一个标准的积分兑换商城,以前和客户合作的时候,客户直接用的是小猫单位自己定制的h5页面。这次合作了一家公司有点特殊,由于公司想要定制化自己个性化的H5,加上本身A公司自己有开发能力,所以经过讨论就以接口的方式直接将相关接口给出去,A客户H5开发完成之后自己来对接。


慢慢地,原因也水落石出,之前好好的业务一直没有问题是因为商城的本身H5页面做了防重复提交,由于量小,并且一般对接方式用的都是纯H5,所以都没有什么问题,然后这次是直接将接口给出去了,完了接口居然没有加幂等......


小猫躺枪,数据订正当然是少不了了,事故报告当然也少不了了。


正所谓前人挖坑,后人遭殃,前人锅后人背。


聊聊幂等


接口幂等梗概


这个案例其实就是一个典型的接口幂等案例。那么老猫就和大家从以下几个方面好好剖析一下接口幂等吧。


interfacemd.png


什么是接口幂等


比较专业的术语:其任意多次执行所产生的影响均与第一次执行的影响相同。
大白话:多次调用的情况下,接口最终得到的结果是一致的。


那么为什么需要幂等呢?



  1. 用户进行提交动作的时候,由于网络波动等原因导致后端同步响应不及时,这样用户就会一直点点点,这样机会发生重复提交的情况。

  2. 分布式系统之间调用的情况下,例如RPC调用,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

  3. 分布式系统经常会用到消息中间件,当由于网络原因,mq没有收到ack的情况下,就会导致消息的重复投递,从而就会导致重复提交行为。

  4. 还有就是恶意攻击了,有些业务接口做的比较粗糙,黑客找到漏洞之后会发起重复提交,这样就会导致业务出现问题。打个比方,老猫曾经干过,邻居小孩报名了一个画画比赛,估计是机构培训发起的,功能做的也差,需要靠投票赢得某些礼品,然后老猫抓到接口信息之后就模拟投票进行重复刷了投票。


那么哪些接口需要做幂等呢?


首先我们说是不是所有的接口都需要幂等?是不是加了幂等就好呢?显然不是。
因为接口幂等的实现某种意义上是要消耗系统性能的,我们没有必要针对所有业务接口都加上幂等。


这个其实并不能做一个完全的定义说哪个就不用幂等,因为很多时候其实还是得结合业务逻辑一起看。但是其中也是有规律可循的。


既然我们说幂等就是多次调用,接口最终得到结果一致,那么很显然,查询接口肯定是不要加幂等的,另外一些简单删除数据的接口,无论是逻辑删除还是物理删除,看场景的情况下其实也不用加幂等。


但是大部分涉及到多表更新行为的接口,咱们最好还是得加上幂等。


接口幂等实战方案


前端防抖处理


前端防抖主要可以有两种方案,一种是技术层面的,一种是产品层面的:



  1. 技术层面:例如提交控制在100ms内,同一个用户最多只能做一次订单提交的操作。

  2. 产品层面:当然用户点击提交之后,按钮直接置灰。


基于数据库唯一索引



  1. 利用数据库唯一索引。我们具体来看一下流程,咱们就用小猫遇到的例子。如下:


unique-key.png


过程描述:



  • 建立一张去重表,其中某个字段需要建立唯一索引,例如小猫这个场景中,咱们就可以将订单提交流水单号作为唯一索引存储到我们的数据库中,就模型上而言,可以将其定义为支付请求流水表。

  • 客户端携带相关流水信息到后端,如果发现编号重复,那么此时就会插入失败,报主键冲突的错误,此时我们针对该错误做一下业务报错的二次封装给到客户另一个友好的提示即可。


数据库乐观锁实现


什么是乐观锁,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。
说得直白一点乐观锁就是一个马大哈。总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。


例如提交订单的进行支付扣款的时候,本来可能更新账户金额扣款的动作是这样的:


update Account set balance = balance-#{payAmount} where accountCode = #{accountCode}

加上版本号之后,咱们的代码就是这样的。


update Account set balance = balance-#{payAmount},version=version +1 where accountCode = #{accountCode} and version = #{currVersion}

这种情况下其实就要求客户端每次在请求支付下单的时候都需要上层客户端指定好当前的版本信息。
不过这种幂等的处理方式,老猫用的比较少。


数据库悲观锁实现


悲观锁的话具有强烈的独占和排他特性。大白话谁都不信的主。所以我们就用select ... for update这样的语法进行行锁,当然老猫觉得单纯的select ... for update只能解决同一时刻大并发的幂等,所以要保证单号重试这样非并发的幂等请求还是得去校验当前数据的状态才行。就拿当前的小猫遇到的场景来说,流程如下:


pessimistic.png


begin;  # 1.开始事务
select * from order where order_code='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_code='666' # 更新完成
update stock set num = num - 1 where spu='xxx' # 库存更新
commit; # 5.提交事务

这里老猫一再想要强调的是在校验的时候还是得带上本身的业务状态去做校验,select ... for update并非万能幂等。


后端生成token


这个方案的本质其实是引入了令牌桶的机制,当提交订单的时候,前端优先会调用后端接口获取一个token,token是由后端发放的。当然token的生成方式有很多种,例如定时刷新令牌桶,或者定时生成令牌并放到令牌池中,当然目的只有一个就是保住token的唯一性即可。


生成token之后将token放到redis中,当然需要给token设置一个失效时间,超时的token也会被删除。


当后端接收到订单提交的请求的时候,会先判断token在缓存中是否存在,第一次请求的时候,token一定存在,也会正常返回结果,但是第二次携带同一个token的时候被拒绝了。


流程如下:


token.png


有个注意点大家可以思考一下:
如果用户用程序恶意刷单,同一个token发起了多次请求怎么办?
想要实现这个功能,就需要借助分布式锁以及Lua脚本了,分布式锁可以保证同一个token不能有多个请求同时过来访问,lua脚本保证从redis中获取令牌->比对令牌->生成单号->删除令牌这一系列行为的原子性。


分布式锁+状态机(订单状态)


现在很多的业务服务都是分布式系统,所以就拿分布式锁来说,关于分布式锁,老猫在此不做赘述,之前老猫写过redis的分布式锁和实现,还有zk锁和实现,具体可见链接:



当然和上述的数据库悲观锁类似,咱们的分布式锁也只能保证同一个订单在同一时间的处理。其次也是要去校订单的状态,防止其重复支付的,也就是说,只要支付的订单进入后端,都要将原先的订单修改为支付中,防止后续支付中断之后的重复支付。


在上述小猫的流程中还没有涉及到现金补充,如果涉及到现金补充的话,例如对接了微信或者支付宝的情况,还需要根据最终的支付回调结果来最终将订单状态进行流转成支付完成或者是支付失败。


总结


在我们日常的开发中,一些重要的接口还是需要大家谨慎对待,即使是前任开发留下的接口,没有任何改动,当有人咨询的时候,其实就要好好去了解一下里面的实现,看看方案有没有问题,看看技术实现有没有问题,这应该也是每一个程序员的基本素养。


另外的,在一些重要的接口上,尤其是资金相关的接口上,幂等真的是相当的重要。小伙伴们,你们觉得呢?如果大家还有好的解决方案,或者有其他思考或者意见也欢迎大家的留言。


作者:程序员老猫
来源:juejin.cn/post/7324186292297482290
收起阅读 »

Java 中为什么要设计 throws 关键词,是故意的还是不小心

我们平时在写代码的时候经常会遇到这样的一种情况 提示说没有处理xxx异常 然后解决办法可以在外面加上try-catch,就像这样 所以我之前经常这样处理 //重新抛出 RuntimeException public class ThrowsDemo { ...
继续阅读 »

我们平时在写代码的时候经常会遇到这样的一种情况


throws.png


提示说没有处理xxx异常


然后解决办法可以在外面加上try-catch,就像这样


trycatch.png


所以我之前经常这样处理


//重新抛出 RuntimeException
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

//打印日志
@Slf4j
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
log.error("sample4throws", e);
}
}
}

//继续往外抛,但是需要每个方法都添加 throws
public class ThrowsDemo {

public void demo4throws() throws IOException {
new ThrowsSample().sample4throws();
}
}

但是我一直不明白


这个方法为什么不直接帮我做


反而要让我很多余的加上一步


我处理和它处理有什么区别吗?


而且变的好不美观


本来缩进就多,现在加个try-catch更是火上浇油


public class ThrowsDemo {

public void demo4throws() {
try {
if (xxx) {
try {
if (yyy) {

} else {

}
} catch (Throwable e) {
}
} else {

}
} catch (IOException e) {

}
}
}

上面的代码,就算里面没有业务,看起来也已经比较乱了,分不清哪个括号和哪个括号是一对


还有就是对Lambda很不友好


lambda.png


没有办法直接用::来优化代码,所以就变成了下面这样


lambdatry.png


本来看起来很简单很舒服的Lambda,现在又变得又臭又长


为什么会强制 try-catch


为什么我们平时写的方法不需要强制try-catch,而很多jdk中的方法却要呢


那是因为那些方法在方法的定义上添加了throws关键字,并且后面跟的异常不是RuntimeException


一旦你显式的添加了这个关键字在方法上,同时后面跟的异常不是RuntimeException,那么使用这个方法的时候就必须要显示的处理


比如使用try-catch或者是给调用这个方法的方法也添加throws以及对应的异常


throws 是用来干什么的


那么为什么要给方法添加throws关键字呢?


给方法添加throws关键字是为了表明这个方法可能会抛出哪些异常


就像一个风险告知


这样你在看到这个方法的定义的时候就一目了然了:这个方法可能会出现什么异常


为什么 RuntimeException 不强制 try-catch


那为什么RuntimeException不强制try-catch呢?


因为很多的RuntimeException都是因为程序的BUG而产生的


比如我们调用Integer.parseInt("A")会抛出NumberFormatException


当我们的代码中出现了这个异常,那么我们就需要修复这个异常


当我们修复了这个异常之后,就不会再抛出这个异常了,所以try-catch就没有必要了


当然像下面这种代码除外


public boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}

这是我们利用这个异常来达成我们的需求,是有意为之的


而另外一些异常是属于没办法用代码解决的异常,比如IOException


我们在进行网络请求的时候就有可能抛出这类异常


因为网络可能会出现不稳定的情况,而我们对这个情况是无法干预的


所以我们需要提前考虑各种突发情况


强制try-catch相当于间接的保证了程序的健壮性


毕竟我们平时写代码,如果IDE没有提示异常处理,我们完全不会认为这个方法会抛出异常


我的代码怎么可能有问题.gif


我的代码怎么可能有问题!


不可能绝对不可能.gif


看来Java之父完全预判到了程序员的脑回路


throws 和 throw 的区别


java中还有一个关键词throw,和throws只有一个s的差别


throw是用来主动抛出一个异常


public class ThrowsDemo {

public void demo4throws() throws RuntimeException {
throw new RuntimeException();
}
}

两者完全是不同的功能,大家不要弄错了


什么场景用 throws


我们可以发现我们平时写代码的时候其实很少使用throws


因为当我们在开发业务的时候,所有的分支都已经确定了


比如网络请求出现异常的时候,我们常用的方式可能是打印日志,或是进行重试,把异常往外抛等等


所以我们没有那么有必要去使用throws这个关键字来说明异常信息


但是当我们没有办法确定异常要怎么处理的时候呢?


比如我在GitHub上维护了一个功能库,本身没有什么业务属性,主要就是对于一些复杂的功能做了相应的封装,提供给自己或别人使用(如果有兴趣可以看看我的库,顺便给Star,嘿嘿


对我来说,当我的方法中出现异常时,我是不清楚调用这个方法的人是想要怎么处理的


可能有的想要重试,有的想要打印日志,那么我干脆就往外抛,让调用方法的人自己去考虑,自己去处理


所以简单来说,如果方法主要是给别人用的最好用throws把异常往外抛,反之就是可加可不加


结束


很多时候你的不理解只是因为你还不够了解


作者:不够优雅
来源:juejin.cn/post/7204594495996100664
收起阅读 »

面试官:你之前的工作发布过npm包吗?

web
背景🌟 我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~ 01、步骤一注册 打开npm官网,如果没有账号就注册账号...
继续阅读 »

背景🌟


我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~


01、步骤一注册



打开npm官网,如果没有账号就注册账号,如果有就登陆。



02、步骤二创建文件夹



按需求创建一个文件夹,本文以test为例。



03、步骤三初始化package.json文件



进入test文件夹里面,使用cmd打开命令行窗口,在命令行窗口里面输入npm init初始化package.json文件。也可以在Visual Studio Coode的终端里面使用npm init命令初始化。



04、步骤四初始化package.json文件的过程



创建package.json的步骤


01、package name: 设置包名,也就是下载时所使用的的命令,设置需谨慎。


02、version: 设置版本号,如果不设置那就默认版本号。


03、description: 包描述,就是对这个包的概括。


04、entry point: 设置入口文件,如果不设置会默认为index.js文件。


05、test command: 设置测试指令,默认值就是一句不能执行的话,可不设置。


06、git repository: 设置或创建git管理库。


07、keywords: 设置关键字,也可以不设置。


08、author: 设置作者名称,可不设置。


09、license: 备案号,可以不设置。


10、回车即可生成package.json文件,然后还有一行需要输入yes命令就推出窗口。


11、测试package.json文件是否创建成功的命令npm install -g。



05、步骤五创建index.js文件



test文件夹根目录下创建index.js文件,接着就是编写index.js文件了,此处不作详细叙述。



06、步骤六初始化package-lock.json文件



test根目录下使用npm link命令创建package-lock.json文件。



07、步骤七登录npm账号



使用npm login链接npm官网账号,此过程需要输入Username、Password和Email,需要提前准备好。连接成功会输出Logged in as [Username] on registry.npmjs.org/ 这句话,账号不同,输出会有不同。



08、步骤八发布包到npm服务器



执行npm publish命令发布包即可。



09、步骤九下载安装



下载安装使用包,此例的下载命令是npm install mj-calculation --save



10、步骤十更新包



更新包的命npm version patch,更新成功会输出版本号,版本号会自动加一,此更新只针对本地而言。



11、步骤十一发布包到npm服务器



更新包至npm服务器的命令npm publish,成功会输出版本,npm服务器的版本也会更新。



12、步骤十二删除指定版本



删除指定版本npm unpublish mj-calculation@1.0.2,成功会输出删除的版本号,对应服务器也会删除。



13、步骤十三删除包



撤销已发布的包npm unpublish mj-calculation使用的命令。



14、步骤十四强制删除包



强制撤销已发布的包npm unpublish mj-calculation --force使用的命令。



作者:泽南Zn
来源:juejin.cn/post/7287425222365364259
收起阅读 »

面试被问到一个css属性,我却只会向面试官输出js解决方案。。。

web
事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一...
继续阅读 »

事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一些操作按钮,但是按钮在该区域下方,不能显示出来,怎么能够点击到按钮呢?


听到问题我一开始没反应过来,不是还在疯狂问css吗,怎么突然跳跃到canvas了?于是我就回:不好意思,canvas我不是特别熟。。。,面试官就说不关canvas的事,这样吧,你就把canvas想象成一张普通的图片,怎么能点击到图片下方的按钮呢?


思索片刻,难道面试官在考察我的JS基础了?我就很自信的回答:首先把按钮所在标签放到图片所在标签之下,再把图片的层级(z-index)设置比按钮高一点,这样按钮就被图片挡着不会显示出来,再给按钮和图片所在标签都加上点击事件,此时就可以通过事件冒泡处理按钮的事件了。说完我就非常有把握的看向面试官,以为稳了。但是面试官好像不太满意,于是添加条件说,那如果按钮和图片所在标签不是父子节点关系呢,没有这层关系,你就不能使用事件冒泡了,此时怎么处理?答曰:不知道。。。


回来后赶紧查资料,原来一个css属性就搞定了:pointer-events: none; 这才是面试官想要的答案。


pointer-events是一个CSS属性,它定义了在何种情况下元素可以成为鼠标事件(或触摸事件)的目标。这个属性可以控制元素是否可以被点击、是否可以触发鼠标事件,或者是否应该忽略鼠标事件,让事件传递给下面的元素


使用场景


pointer-events属性主要用于以下几种场景:



  • : 元素不会成为鼠标事件的目标。例如,如果想让一个元素透明对用户的点击,可以将其pointer-events设置为none

  • Auto: 默认值。元素正常响应鼠标事件。

  • VisiblePainted: 元素仅在可见部分响应鼠标事件。

  • 其他值: 还有一些其他值用于SVG元素,如visibleFillvisibleStrokepainted, 等。


示例




以下例子和上述试题很像,把mask当做一张图片,为了方便展示,为其设置了透明度,这样能看到具体按钮位置和展示层级关系。正常情况下点击按钮是不会触发click事件的,因为mask的层级更高,完全遮住了按钮,鼠标只会点击到mask,但若此时为其加上pointer-events: none属性,点击事件会“穿透”该元素并可触发下面元素的事件,即按钮点击事件就可以被触发了!!!


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.outer{
position: relative;
width: 200px;
height: 200px;
}
.mask{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, .7);
pointer-events: none; /* 重要 */
}
</style>
<body>
<div class="outer">
<div class="mask"></div>
<button id="btn">click</button>
</div>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', function(e){
console.log('click');
})
</script>
</body>
</html>

image.png


作者:自驱
来源:juejin.cn/post/7320169906221826048
收起阅读 »

我是如何找到老婆的

本文不聊技术,聊聊我跟我老婆从认识到现在的过程。(现在已经领证了) 我们是2022年过年的时候在网上通过soul认识的,当时是大年初一,我爷爷跟我说,现在过年了,大家回家父母都在催找对象,你也去网上找。 听到这话,我懵了。说的轻巧,网购一个吗,但是我还是打开手...
继续阅读 »

本文不聊技术,聊聊我跟我老婆从认识到现在的过程。(现在已经领证了


我们是2022年过年的时候在网上通过soul认识的,当时是大年初一,我爷爷跟我说,现在过年了,大家回家父母都在催找对象,你也去网上找。


听到这话,我懵了。说的轻巧,网购一个吗,但是我还是打开手机,下载了软件。开始在里面看别人发的帖子,太多了,我也发个帖子,没人理我,哈哈。然后我就加了个湖北的群,我进去做了自我介绍,还是没人理我,我发现群里30多个人,只有几个女的。


好尴尬啊,我兴致勃勃发的一段自我介绍,赫然就出现在群里,就像一件华丽的衣服上面的一个补丁,那么显眼。算了,不管了,我去玩儿了。


过了好一会,我收到了一条消息,是一个小姑娘发来的。看到这里我是有点小意外的,也很惊喜,于是我就收起我在家里的粗犷,很有礼貌地跟她互相自我介绍。通过了解我们才知道,大家都是湖北的,我是十堰市,它是鄂州市,大家都在上海工作,不过因为疫情原因,她今年没有回家。这之后几天我们也互发消息,面对过年满桌的美食,我完全没有大快朵颐的心情,我只想等她的消息,我彷佛感觉她也殷切期盼我的消息。


就这样你一言我一语,殊不知一段姻缘悄咪咪的从这里就开始了,彷佛幂幂之中一切自有定数。。。


年后,我也要回上海工作了,去的第一件事就是去跟心里的这个姑娘见面。我那天特意穿了干净的衣服鞋子,洗了头发,整个人从头到脚都好好捯饬了一番。我们约的是中山公园站,那里是个大商场,下地铁后,我发现地铁口好多,这是个大站,约的是她在一个大的花门那里,其实是商场的入口,我对这里也不熟,急切而激动的我,不知所措,到处乱跑,诺大的地铁站我来回跑了两遍,哈哈。跑了个遍,总算找到了,我远远就看到她了。


大概一米七的个子,她穿着一件白色羽绒服,长长的头发乌黑浓密,像海草一样轻盈,又如瀑布一般美丽。


随着距离靠近,她也看到我了,向我走来,莲步轻移。。。


她的双眸清澈而明亮,宛如两泓清泉,楚楚动人,她没有化妆,却有着白皙透亮的肌肤,就像刚刚剥皮的鸡蛋一样,闪烁着,她没有涂口红,花瓣一样的嘴唇却呈现出粉嫩的淡红色,是那种很自然的颜色。


我们就这样看着,对视着,然后都笑了。


她拉起我的手,我说我们一起去吃饭吧。选的是外婆家,我记得点了个糖醋里脊,还有2个菜,我们边吃边聊,很是愉快。


饭后,我提出一起去坐了摩天轮,门票是200块,但我一点都不觉得贵,反而觉得跟她一起是最浪漫的事。在摩天轮缓缓升起到最高点的时候,我拉着她的手说:“我喜欢你”,她说:“我也喜欢你”,然后我们轻吻了彼此,此刻时间彷佛都静止了,我们觉得整个世界只剩下我们两个。


然后下午我们一起去看了电影,我小心翼翼地征求了她的意见,看的是一个爱情片,此刻的我们想看的就是这种类型。


微信图片_20240111223021


看完电影已经晚上了,她说自己晚上一般不吃饭,出于绅士,我主顶提出送她回家,她也没有拒绝。


接下来几天,我们上班都是一边工作,一边互发消息,我觉得心情愉悦,连空气都是甜的。


然后就到情人节了,我晚上下班直接去她的地方找她,我特意买了玫瑰花,第一次见面没有买,这让我觉得很亏欠。为了方便,我直接在美团上面定的,送到离她最近的一个地铁站,我直接坐地铁去那个地铁站,然后她直接来这里找我,外卖好慢,她来了我们俩等了会,外卖才把玫瑰花送来,是个大妈送的,不过我此时一点没有想责怪她,反而觉得好事多磨,美景常在。然后我们就一起去吃了寿喜烧。


image-20240111230008598


后来上海封城,我们就每天视频聊天,因为她有时候没有菜,我就每天早晨5点起来抢菜,这段时光真的令人难以忘怀。因为我住的是自如的合租房,厨房都满了,我也就懒得做饭,那几天本来按照官方的说法只屯了一个星期的粮食,后来延长封闭期限,我也就弹尽粮绝,每天靠点外卖度日,有时候外卖都点不到。她就花费高价从很远地方点外卖给我吃,我当时真的很感动,以后一定要对她好点,我心里这样想着。


后来上海解封,我们又重回每周约会的日子。中秋节我去她家见了父母,然后国庆节我带我爸去了他家,双方都聊的挺好的。


时间流逝,但我们的点点滴滴,都弥足珍贵。。。。


今年我们决定结婚了,1.5号我们领了结婚证,国庆节准备办婚礼,一切都在往好的地方发展。


世间繁华,唯有你我,相知相守,情深似海。


作者:大数据技术派
来源:juejin.cn/post/7322811509536194594
收起阅读 »

日志脱敏之后,无法根据信息快速定位怎么办?

日志脱敏之殇 小明同学在一家金融公司上班,为了满足安全监管要求,最近天天忙着做日志脱敏。 无意间看到了一篇文章金融用户敏感数据如何优雅地实现脱敏? 感觉写的不错,用起来也很方便。 不过日志脱敏之后,新的问题就诞生了:日志脱敏之后,很多问题无法定位。 比如身-份...
继续阅读 »

日志脱敏之殇


小明同学在一家金融公司上班,为了满足安全监管要求,最近天天忙着做日志脱敏。


无意间看到了一篇文章金融用户敏感数据如何优雅地实现脱敏? 感觉写的不错,用起来也很方便。


不过日志脱敏之后,新的问题就诞生了:日志脱敏之后,很多问题无法定位。


比如身-份-证号日志中看到的是 3****************8,业务方给一个身-份-证号也没法查日志。这可怎么办?


在这里插入图片描述


安全与数据唯一性


类似于数据库中敏感信息的存储,一般都会有一个哈希值,用来定位数据信息,同时保障安全。


那么日志中是否也可以使用类似的方式呢?


说干就干,小明在开源项目 sensitive 基础上,添加了对应的哈希实现。


使用入门


开源地址



github.com/houbb/sensi…



使用方式


1)maven 引入


<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-core</artifactId>
<version>1.1.0</version>
</dependency>

2)引导类指定


SensitiveBs.newInstance()
.hash(Hashes.md5())

将哈希策略指定为 md5


3)功能测试


final SensitiveBs sensitiveBs = SensitiveBs.newInstance()
.hash(Hashes.md5());

User sensitiveUser = sensitiveBs.desCopy(user);
String sensitiveJson = sensitiveBs.desJson(user);

Assert.assertEquals(sensitiveStr, sensitiveUser.toString());
Assert.assertEquals(originalStr, user.toString());
Assert.assertEquals(expectJson, sensitiveJson);

可以把如下的对象


User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}

直接脱敏为:


User{username='脱**|00871641C1724BB717DD01E7E5F7D98A', idCard='123456**********34|1421E4C0F5BF57D3CC557CFC3D667C4E', password='null', email='12******.com|6EAA6A25C8D832B63429C1BEF149109C', phone='1888****888|5425DE6EC14A0722EC09A6C2E72AAE18'}

这样就可以通过明文,获取对应的哈希值,然后搜索日志了。


新的问题


不过小明还是觉得不是很满意,因为有很多系统是已经存在的。


如果全部用注解的方式实现,就会很麻烦,也很难推动。


应该怎么实现呢?


小伙伴们有什么好的思路?欢迎评论区留言


作者:老马啸西风
来源:juejin.cn/post/7239647672460705829
收起阅读 »

尊嘟假嘟?三行代码提升接口性能600倍

一、背景   业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!   然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问...
继续阅读 »

一、背景


  业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!
  然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问题导致的,接来下就把重点放到排查接口超时的问题上了。


二、问题排查


遇到生产问题先查日志是基本操作,登录阿里云的日志平台,可以查到接口耗时竟然高达469245毫秒


这个结算单关联的账单数量也就800多条,所以可以肯定这个接口存在性能问题。


image


但是日志除了接口耗时,并没有其他报错信息或异常信息,看不出哪里导致了接口慢。


接口慢一般是由如下几个原因导致:



  1. 依赖的外部系统慢,比如同步调用外部系统的接口耗时比较久

  2. 处理的数据过多导致

  3. sql性能有问题,存在慢sql

  4. 有大循环存在循环处理的逻辑,如循环读取exel并处理

  5. 网络问题或者依赖的中间件比较慢

  6. 如果使用了锁,也可能由于长时间获取不到锁导致接口超时


当然也可以使用arthas的trace命令分析哪一块比较耗时。


由于安装arthas有点麻烦,就先猜测可能慢sql导致的,然后就登录阿里云RDS查看了慢sql监控日志。
image
好家伙一看吓一跳,sql耗时竟然高达66秒,而且执行次数还挺多!


我赶紧把sql语句放到数据库用explain命令看下执行计划,分析这条sql为啥这么慢。


EXPLAIN SELECT DISTINCT(bill_code) FROM `t_bill_detail_2023_4` WHERE  
(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001');

分析结果如下:


image


如果不知道explain结果每个字段的含义,可以看看这篇文章《长达1.7万字的explain关键字指南!》。


可以看到扫描行数达到了250多万行,ref已经是最高效的const,但是看最后的Extra列
Using temporary 表明这个sql用到了临时表,顿时心里清楚什么原因了。


因为sql有个去重关键字DISTINCT,所以mysql在需要建临时表来完成查询结果集的去重操作,如果结果集数据量比较小没有超过buffer,就可以直接在内存中去重,这种效率也是比较高的。


但是如果结果集数据量很大,buffer存不下,那就需要借助磁盘完成去重了,我们都知道操作磁盘相比内存是非常慢的,时间差几个数量级


虽然这个表里的settlement_order_code字段是有索引的,但是线上也有很多settlement_order_code为null的数据,这就导致查出来的结果集非常大,然后又用到临时表,所以sql耗时才这么久!


同时,这里也解释了为什么测试环境没有发现这个问题,因为测试环境的数据不多,直接在内存就完成去重了。


三、问题解决


知道了问题原因就很好解决了,首先根据SQL和接口地址很快就找到出现问题的代码是下图红框圈出来的地方


image


可以看到代码前面有个判断,只有当isThreeOrderQuery=true时才会执行这个查询,判断方法代码如下


image


然后因为这是个编辑场景,前端会把当前结算单号(usedSettlementOrderCode字段)传给后端,所以这个方法就返回了true。


同理,拼接出来的sql就带了条件(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001')。
image


解决起来也很简单,把isThreeOrderQuery()方法圈出来的代码去掉就行了,这样就不会执行那个查询,同时也不会影响原有的代码逻辑,因为后面会根据筛选条件再查一次t_bill_detail表。


改代码发布后,再编辑结算单,优化后的效果如下图:


image


只改了三行代码,接口耗时就立马从469245ms缩短到700ms,性能提升了600多倍


四、总结


感觉压测环境还是有必要的,有些问题数据量小了或者请求并发不够都没法暴露出来,同时以后写代码可以提前把sql在数据库explain下看看性能如何,毕竟能跑就行不是我们的追求😏。


作者:2YSP
来源:juejin.cn/post/7322156759443144713
收起阅读 »

不要再滥用可选链运算符(?.)啦!

web
前言 之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。 可选链运算符(?.),大家都很熟悉了,直接看个例子: const result = obj?.a?...
继续阅读 »

前言


之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。


可选链运算符(?.),大家都很熟悉了,直接看个例子:


const result = obj?.a?.b?.c?.d

很简单例子,上面代码?前面的属性如果是空值(null或undefined),则result值是undefined,反之如果都不是空值,则会返回最后一个d属性值。


本文不是讲解这种语法的用法,主要是想分析下日常开发中,这种语法 滥用、乱用 的问题。


滥用、乱用


最近在code review一个公司项目代码,发现代码里用到的可选链运算符,很多滥用,用的很无脑,经常遇到这种代码:


const userName = data?.items?.[0]?.user?.name

↑ 不管对象以及属性有没有可能是空值,无脑加上?.就完了。


// react class component
const name = this.state?.name

// react hooks
const [items, setItems] = useState([])
items?.map(...)
setItems?.([]) // 真有这么写的

↑ React框架下,this.state 值不可能是空值,初始化以及set的值都是数组,都无脑加上?.


const item1 = obj?.item1
console.log(item1.name)

↑ 第一行代码说明obj或item1可能是空值,但第二行也明显说明不可能是空值,否则依然会抛错,第一行的?.也就没意义了。


if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2
const name = obj?.item1?.item2?.name
}

↑ if 里已经判断了非空了,内部就没必要判断非空了。


问题、缺点


如果不考虑 ?. 使用的必要性,无脑滥用其实也没问题,不会影响功能,优点也很多:



  1. 不用考虑是不是非空,每个变量或属性后面加 ?. 就完了。

  2. 由于不用思考,开发效率高。

  3. 不会有空引用错误,不会有页面点点就没反应或弹错问题。


但是问题和缺点也很明显,而且也会很严重。分两点分析下:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


1. 可读性、维护性


可读性和维护性其实是一回事,都是指不是源代码作者的开发维护人员,在捋这块代码逻辑、修改bug等情况时,处理问题的效率,代码写的好处理就快,写的烂就处理慢,很简单道理。


const onClick = () => {
const user = props.data?.items?.[0]?.user
if (user) {
// use user to do something
}
}

已这行代码为例,有个bug现象是点击按钮没反应,维护开发看到这块代码,就会想这一串链式属性里,是不是有可能有空值,所以导致了user是空值,没走进if里导致没反应。然后就继续分析上层组件props传输代码,看data值从哪儿传来的,看是不是哪块代码导致data或items空值了。。。


其实呢?从外部传过来的这一串属性里不会有空值的情况,导致bug问题根本不在这儿。


const user = props.data.items[0].user

那把?.都去掉呢?维护开发追踪问题看到这行代码,data items 这些属性肯定不能是空值,不然console就抛错了,但是bug现象里并没有抛错,所以只需要检查user能不能是空值就行了,很容易就排除了很多情况。


总结就是:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。


2. 隐式过滤了异常


api.get(...).then(result => {
const id = result?.id
// use id to do something
})

比如有个需求,从后台api获取数据时,需要把结果里id属性获取到,然后进行数据处理,从业务流程上看,这个api返回的result以及id必须有值,如果没值的话后续的流程就会走不通。


然后后台逻辑由于写的有问题,导致个别情况返回的 result=null,但是由于前端这里加了?.,导致页面没有任何反应,js不抛错,console也没有log,后续流程出错了,这时候如果想找原因就会很困难,对代码熟悉还行,如果不是自己写的就只能看代码捋逻辑,如果是生产环境压缩混淆了就更难排查了。


api.get(...).then(result => {
const id = result.id
// use id to do something
})

?.去掉呢?如果api返回值有问题,这里会立即抛错,后面的流程也就不能进行下去了,无论开发还是生产环境都能在console里快速定位问题,即使是压缩混淆的也能从error看出一二,或者在一些前端监控程序里也能监听到。


其实这种现象跟 try catch 里不加 throw 类似,把隐式异常错误完全给过滤掉了,比如下面例子:


// 这个try本意是处理api请求异常
try {
const data = getSaveData() // 这段js逻辑也在try里,所以如果这个方法内部抛错了,页面上就没任何反应,很难追踪问题
const result = await api.post(url, data)
// result 逻辑处理
} catch (e) {
// 好点的给弹个框,打个log,甚至有的啥都不处理
}

总结就是:把异常给隐式过滤掉了,导致不能快速定位问题。


3. 编译后代码冗余


如果代码是ts,并且编译目标是ES2016,编译后代码会很长。可以看下 http://www.typescriptlang.org/play 效果。


image.png


Babel在个别stage下,编译效果一样。


image.png


但并不是说一点都不用,意思是尽量减少滥用,这样使用的频率会少很多,这种编译代码沉余也会少不少。


应该怎么用?


说了这么多,.? 应该怎么用呢?意思是不用吗?当然不是不能用,这个特性对于开发肯定好处很多的,但是得合理用,不能滥用。



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。


其实说白了就是:什么时候需要判断一个变量或属性非空,什么时候不需要。首先在使用的时候得想下,问号前面的变量或属性值,有没有可能是空值:



  1. 很明显不可能是空值,比如 React类组件里的 this.state this.props,不要用;

  2. 自己定义的变量或属性,而且没有赋值为空值情况,不要用;

  3. 某些方法或者组件里,参数和属性不允许是空值,那方法和组件里就不需要判断非空。(对于比较common的,推荐写断言,或者判断空值情况throw error)

  4. 后台api请求结果里,要求result或其内部属性必须有值,那这些值就不需要判断非空。

  5. 按正常流程走,某个数据不会有空值情况,如果是空值说明前面的流程出问题了,这种情况就不需要在逻辑里判断非空。


const userName = data?.items?.[0]?.user?.name // 不要滥用,如果某个属性有可能是空值,则需要?.
const userName = data.items[0].user?.name // 比如data.items数组肯定不是空数组

const items2 = items1.filter(item => item.checked)
if (items2?.length) { } // 不需要?.

// react class component
const name = this.state?.name // 不需要?.

// react hooks
const [items, setItems] = useState([])
items?.map(...) // 如果setItems没有赋值空值情况,则不需要?.
setItems?.([]) // 不需要?.

const item1 = obj?.item1 // 不需要?.
console.log(item1.name)

const id = obj?.id // 下面代码已经说明不能是空值了,不需要?.
const name = obj.name

if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2 // 不需要?.
const name = obj?.item1?.item2?.name // 不需要?.
}

const id = obj?.item?.id // 不需要?.
api.get(id).then(...) // 这个api如果id是空值,则api会抛错

当然,写代码时还得多想一下属性是否可能是空值,会一定程度的影响开发效率,也一定有开发会觉得很烦,不理解,无脑写?.多容易啊,但是我从另外两个角度分析下:



  1. 我觉得一个合格的开发应该对自己的代码逻辑很熟悉,应该有责任知道哪些值可能是空值,哪些不可能是空值(并不是说所有,也有大部分了),否则就是对自己的代码了解很少,觉得代码能跑就行,代码质量自然就低。

  2. 想想在这个新特性出来之前大家是怎么写的,会对每个变量和属性都加if非空判断或者用逻辑与(&&)吗?不会吧。


总结


本文以一个 code reviewer 角度,分析了 可选链运算符(?.) 特性的滥用情况,以及“正确使用方式”,只是代表我本人的看法,欢迎大佬参与讨论,无条件接受任何反驳。


滥用的缺点:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


“正确用法”:



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。




后记(09月25日更新)


从评论上看,对于可选链的看法,大多声音是能加就加,多加总比少加好,原因就是不想背锅,不想上线后JS动不动就崩了,无论根本原因是不是前端开发没加判断导致的,第一责任人就会找到你,有的甚至会被上级追责,问题就更严重了,而且很难解释清楚;另一方面就是为了赶工期,可选链的其中一个优点就是简单,提高开发效率。


我再从几个方面浅浅的扩展下我的看法,欢迎参与讨论


总之。。。对对对,你们说的都对!


作者:Mark大熊
来源:juejin.cn/post/7280747572707999799
收起阅读 »

对于Android开发,Jetpack Compose真的要开始学起来了?

Jetpack Compose 是个啥?为啥要学它? 谷歌对 Jetpack Compose 的定义: Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工...
继续阅读 »

Jetpack Compose 是个啥?为啥要学它?


谷歌对 Jetpack Compose 的定义:



Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。



提取关键词:界面开发新工具包、简化并加快界面开发、Kotlin API


对于大部分Android项目来说,如果基础库(如网络库、hybird、图片加载、热修复库等)已经搭好,那么平时大部分时间就是跟 UI界面、需求逻辑 打交道了,而谷歌提供的 Jetpack Compose 正好是加快界面开发的工具包
对比



就跟魂斗罗里的子弹类型似的,使用普通子弹(XML方式)也可以通关,但是相比之下耗时更长;而换成超级子弹(Jetpack Compose)体验就不一样了,耗时更少,而且游戏体验更爽!



命令式UI vs 声明式UI


长期以来,Android 视图层次结构一直可以表示为界面 widget 树。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态,这种手动更新UI的方式即是命令式UI


在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 即是一个声明式UI框架。


Jetpack Compose要学起来了?


很遗憾,Jetpack Compose 确实要学起来了(快起来,你还能学!哈哈...),随着Jetpack Compose 版本的不断迭代,API 逐渐稳定了,性能也越来越好了。


优点



  • 更少的代码:编写代码只需要采用Kotlin,而不用拆分成 Kotlin + XML方式了。

  • 直观:只需描述界面,Compose会负责处理剩余工作。应用状态变化时,界面自动更新。

  • 加速开发View 与 Compose 之间可以相互调用,兼容现有的所有代码。借助AS可以实时预览界面,轻松执行界面检查。

  • 功能强大:直接访问Android API,内置对Material Design、主题、动画等的支持。


Jetpack Compose vs Flutter




  • Jetpack Compose的目的是为了提高 Android 原生的 UI 开发效率!声明式UI已经成为主流的开发方式了,就像当初谷歌将Kotlin定为Android主流语言时我们学习Kotlin一样,未来Jetpack Compose 一定会是Android UI开发的主流方式。

  • Flutter 的定位是多平台 UI 框架,优势在于跨平台。



大家很喜欢把Jetpack Compose 和 Flutter作对比,不知道该学哪一个?的确,某些场景下它们确实挺像的,而且还都是谷歌在推的。


个人理解是:如果你未来的主攻方向还是Android,那么无脑选择Jetpack Compose,虽然Compose目前也能实现跨端,但跨端目前看并不是它的主要工作;而如果你的方向是多平台开发,那么学习Flutter是首选吧


另外,与其一直纠结学哪一个,不如直接上手亲身感受下它们的不同,正所谓 “纸上得来终觉浅,绝知此事要躬行”。


Jetpack Compose入门


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello World!")
}
}
}

其中,setContent()传入一个@Composable作用域,其作用跟之前的setContentView()一样用来设置界面。Text()用来描述一个UI元素,里面有各种参数,这里我们只把文案填上去,执行结果:


hello world


一个最简单的功能就完成了。


1、@Composable 可组合函数


还是上述展示一个文本的功能,我们换一种写法:


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Android")
}
}
}

@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}

执行效果跟上面一样。唯一的区别就是把文本展示单独抽离到一个方法中了,并且该方法上面加了@Composable 注解



@Composable注解用于标记一个函数为可组合函数。可组合函数是一种特殊的函数,不需要返回任何UI元素,因为可组合函数描述的是所需的屏幕状态,而不是构造界面widget;而如果按我们以前的XML编程方式,必须在方法中返回UI元素才能使用它(如返回View类型)。



@Composable注解的函数之间可以相互调用,因为这样Compose框架才能正确处理依赖关系。另外,@Composable函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable函数。 这里可以类比下kotlin中suspend挂起函数的用法,其用法是相似的


几个定义:



  • 组合:对 Jetpack Compose 在执行可组合项时所构建界面的描述。

  • 初始组合:通过首次运行可组合项创建组合。

  • 重组在数据发生变化时重新运行可组合项以更新组合


可组合函数的特点:



  • 使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对 random() 的调用。

  • 此函数描述界面而没有任何副作用,如修改属性或全局变量、点击事件的处理等。当需要执行附带效应时,应通过回调触发。如:


@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

可见通过回调,点击事件这个附带效应是在调用方触发的。


在编译时,Jetpack Compose会将标记为@Composable的函数编译成字节码,并生成一个专门的ComposeNode类来管理其状态和属性。这个类会自动处理依赖关系,并在需要时计算UI元素。这样,开发者就可以专注于编写UI逻辑,而不用担心状态管理和UI更新的细节


这里引出一个问题,Compose 是如何做 UI 更新的呢?总不能每次有一小部分数据的变化,整个UI都要跟着刷新一次吧,那性能肯定差的要死。其实,当有数据变化时,Compose实现的是增量更新,只会重新绘制数据有改动的UI(该过程称为重组),数据没有改动的则不会重新绘制了


2、布局基础知识


布局
Compose 通过元素组合、布局、绘制之后可以将状态转换为UI元素。


组合

在 Compose 中,可以通过从可组合函数中调用其他可组合函数来构建界面层次结构。
基础布局
如图所示:



  • Column :可以将多个项垂直地放置在屏幕上;

  • Row :可以将多个项水平地放置在屏幕上;

  • Box :可将元素放在其他元素上,还支持为其包含的元素配置特定的对齐方式。


排列及对齐方式:


/**
* @param verticalArrangement 竖直排列方式
* @param horizontalAlignment 水平对齐方式
*/

inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)
{...}

/**
* @param horizontalArrangement 水平排列方式
* @param verticalAlignment 竖直对齐方式
*/

@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
{...}

/**
* @param contentAlignment 内容对齐方式
*/

@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
)
{...}

verticalArrangement、horizontalArrangement 排列方式及效果:
排列方式


布局

界面树布局通过单次传递即可完成。父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整
Layout流程
当界面树较深时,Compose 可以通过只测量一次子项来实现高性能。


3、Modifier修饰符


可以通过Modifier修饰符更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击等。如:


  Image(
painter = painterResource(id = R.mipmap.icon_water_melon),
contentDescription = "",
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.border(width = 2.dp, Color.Red, CircleShape)
)

执行结果:Modifier,原图本身是长方形的,通过Modifier修饰符修饰后,很容易变成圆角图片。想一下如果用XML方式来写,是不是要写好多代码呢。


4、存储状态


可组合函数中可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项),这也是上面所说的重组。如:


@Composable
fun MessageCard(msg: Message) {
// We keep track if the message is expanded or not in this variable
var isExpanded by remember { mutableStateOf(false) }
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {...}
}

当点击Column 元素时,每次都会重新执行MessageCard()可组合函数进行刷新,而通过remember和mutableStateOf可以保存了上次的isExpanded状态;如果不使用它们,重新执行 MessageCard() 时 isExpanded 也会重新初始化。


除了remember之外,还有rememberSaveable、savedStateHandle.saveable等。。。


总结


这篇文章主要讲了Compose是什么以及我们要开始学习它的必要性。作为Compose 第一篇介绍文章,本文旨在初步感受一下 Compose的能力,后续再详细研究 Compose 的精彩用法!


资料


【1】谷歌Jetpack Compose 教程
https://developer.android.com/jetpack/compose/tutorial?hl=zh-cn


作者:_小马快跑_
来源:juejin.cn/post/7271832299340202036
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

用脚本来写函数式弹窗,更快更爽

web
前言 在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方...
继续阅读 »

前言


在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。


iShot_2023-08-15_10.13.24.gif


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


传统vue编写弹窗


通过变量来直接控制弹窗的开启和关闭。


<template>
<n-button @click="showModal = true">
来吧
</n-button>

<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>

</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>


痛点



  • 深层次的传props让人有很大的心理负担,污染组件props

  • 要关注弹窗show变量的true,false


函数式弹窗


在主页面用Provider包裹一下


// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>


在页面内的某个子组件中,直接通过oepn方法打开弹窗


// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


优势



  • 对于使用者来说简单,没有控制show的心理负担

  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构

  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗


劣势



  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿

  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。


如何使用这种函数式的弹窗


原理


通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。


步骤1(❌):编写Modal


这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。


// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>

...你的弹窗内容
</n-modal>

</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
testValue: props.testValue,
})

const callBackData = computed(() => {
return {
formData,
}
})

watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)

const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态


在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。


// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/

type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}

/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/

type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};

/**
* @param param 通过函数来更新props
*/

function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}

async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}

export function injectTestModal() {
return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider


在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal


// ModalProvider.vue
<template>
<slot />

<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>

<!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>


步骤4(❌):通过函数打开弹窗


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。


步骤1(✅):初始化Provider


通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider


iShot_2023-08-15_15.16.14.gif


步骤2(✅):生成弹窗模板


通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。


iShot_2023-08-15_15.17.33.gif


步骤3(✅):修改弹窗内容


上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。


步骤4(✅):调用弹窗


我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。


Demo


预览
Demo地址
里面有完整的demo代码


iShot_2023-08-15_18.32.39.gif


脚本工具


仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme


总结


本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。


作者:恐怖屋
来源:juejin.cn/post/7267418473401057321
收起阅读 »

离职后,前领导突然找你回去帮忙写代码解决问题,该怎么办?

题目中的这个问题,我相信有遇到过这种情况的同学的第一反应是:"诶,是要白嫖我还是说解决完问题给钱呀",且听我接下来慢慢分析。 首先要说的是,这种没头没尾的突发情况,一般大部分人都是很难遇到的。 原因也很简单,老板大部分也都是打过工,当过员工的,也是一路从职场老...
继续阅读 »

题目中的这个问题,我相信有遇到过这种情况的同学的第一反应是:"诶,是要白嫖我还是说解决完问题给钱呀",且听我接下来慢慢分析。


首先要说的是,这种没头没尾的突发情况,一般大部分人都是很难遇到的。


原因也很简单,老板大部分也都是打过工,当过员工的,也是一路从职场老油子混成的老板,很多人情世故,员工的小心思,老板其实都门儿清,甚至比很多员工都更熟。


如果公司里的一些工作是交接时不太能完全搞定的,可能还需要离职的员工继续帮忙的,一般在员工离职前的时候,就各种协商好了。


而像这种“突发情况”,大部分老板在联系离职的员工回去帮忙前,一般也都会把员工会想到的那些事儿,早就想了很多遍了,基本上相关问题都会在联系员工的时候说明白。


比如很多人都提到的报酬问题,这个基本上都是作为老板不可能回避,也不可能不知道的。


如果老板在联系员工的时候什么都提了,就是没聊这个。


那肯定是老板不想给报酬,还在做着让员工回来白干活的美梦。


不可否认,现实中确实有挺多这样的老板


所以,我的经验就是,如果老板在主动联系离职员工回来帮忙的时候,都没提报酬的事儿,那基本上就是不打算给,基本上你问了也是白问。


当然,大部分人都会遇到的情况是,本来跟老板领导关系也不错,老板领导也知道这一点,所以才会跟已经离职的员工开这个口。


这种时候,大部分人看在老板领导人还不错的份儿上,还是愿意回去帮忙的。


至于会和现在的工作造成的一些冲突,比如时间上走不开,现在住得离公司远,这些也都是可以直接明说的事儿,说了后,要么老板可以帮你解决,要么老板心里会知道你回来帮这次忙的成本有多高。


我以前工作过的公司,别说离职走的同事了,有一次是碰到了一个实习生经手的项目,上面很多东西没按照公司规范写,后来看到这些资料的员工整不明白是怎么回事。


但是,部门领导在知道了这件事后,在知道了这个实习生的同学就在本部门工作的情况下,并没有说让这位同学去搞定这个问题。


而是让这位同学联系好那位实习生后,领导亲自开车带着这位同学和要用到这个资料的人,专门在下班时间守在这位实习生的工作单位门口,接着他去一家还不错的餐厅,边吃饭边解决了这个问题。


至于很多人提的,跟老板没啥交情,甚至关系还不怎么好的,那还纠结什么,直接不理或拒绝就行了,但也没必要把话说得太绝。


毕竟,如果老板真的意识到你这边不好搞,同时也只有找你来帮忙是最划算的选择后,一般都会开出更高的加码,如果加码合适,你还是可以考虑一下的。


但是一定要就是论事,划定要解决问题的范围,要不然赖上你了有问题就找你可还行,同时也要注意不要留下太多痕迹。比如你回来帮忙,是不是属于违规行为,再比如请你回来帮忙的时候,装作无意间打听你现在公司的一些事儿,这个事儿很可能属于工作机密,毕竟大家都是同行,这些一定要注意。


综上我觉得,解决这个问题的公式是:上来先拖字诀、加各种不容易各种不行,这种能挡掉99%的需求,毕竟这么大个公司离了我这个小兵还不能转了咋地;实在不行了在谈什么样的条件你才能去帮忙解决问题,而且记住是单次解决问题的条件。


作者:kevinyan
来源:juejin.cn/post/7322344486159826996
收起阅读 »

环信服务端下载消息文件---菜鸟教程

前言在服务端,下载消息文件是一个重要的功能。它允许您从服务器端获取并保存聊天消息、文件等数据,以便在本地进行进一步的处理和分析。本指南将指导您完成环信服务端下载消息文件的步骤。环信服务端下载消息文件是指在环信服务端上,通过调用相应的API接口,从服务器端下载聊...
继续阅读 »

前言

在服务端,下载消息文件是一个重要的功能。它允许您从服务器端获取并保存聊天消息、文件等数据,以便在本地进行进一步的处理和分析。本指南将指导您完成环信服务端下载消息文件的步骤。
环信服务端下载消息文件是指在环信服务端上,通过调用相应的API接口,从服务器端下载聊天消息、文件等数据的过程。因环信服务端保存的消息漫游是有时间限制,有用户需要漫游全部的消息或者自己服务端做所有消息记录的备份。可以从环信服务端下载消息文件来进行解压,读取消息文件内容进行存储到自己的服务端。

前提条件

一、下载消息文件

以下将介绍如何通过环信接口获取到的URL来进行下载文件,解压文件,读取文件。
注:
time参数: 历史消息记录查询的起始时间。UTC 时间,使用 ISO8601 标准,格式为 yyyyMMddHH。例如 time 为 2018112717,则表示查询 2018 年 11 月 27 日 17 时至 2018 年 11 月 27 日 18 时期间的历史消息。若海外集群为 UTC 时区,需要根据自己所在的时区进行时间转换。

上图是环信官方文档中给出的获取历史消息记录响应示例。从示例中可以看出我们请求以后可以得到一个URL,这个URL为消息文件的下载URL。

1、下载消息文件环信rest 接口请求代码如下:

String url = "https://{{RestApi}}/{{org_name}}/{{app_name}}/chatmessages/2023122010";
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type","application/json");
headers.add("Authorization","Bearer Authorization");
Map<String, String> body = new HashMap<>();
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response;
try {
response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);

System.out.print("消息文件下载成功---"+response.toString());
} catch (Exception e) {
System.out.print("消息文件下载失败---"+e.toString());
}

2、消息文件下载,通过请求环信下载历史消息文件接口获取到的URL 进行下载。

示例代码:

String url = "";
String targetUrl = "";
download(url,targetUrl);
/**
* 根据url下载文件,保存到filepath中
*
* @param url 文件的url
* @param diskUrl 本地存储路径
* @return
*/

public static String download(String url, String diskUrl) {
String filepath = "";
String filename = "";
try {
HttpClient client = HttpClients.createDefault();
HttpGet httpget = new HttpGet(url);
// 加入Referer,防止防盗链 httpget.setHeader("Referer", url);
HttpResponse response = client.execute(httpget);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
if (StringUtils.isBlank(filepath)){
Map<String,String> map = getFilePath(response,url,diskUrl);
filepath = map.get("filepath");
filename = map.get("filename");
}
File file = new File(filepath);
file.getParentFile().mkdirs();
FileOutputStream fileout = new FileOutputStream(file);
byte[] buffer = new byte[cache];
int ch = 0;
while ((ch = is.read(buffer)) != -1) {
fileout.write(buffer, 0, ch);
}
is.close();
fileout.flush();
fileout.close();

} catch (Exception e) {
e.printStackTrace();
}
return filename;
}


/**
* 获取response要下载的文件的默认路径
*
* @param response
* @return
*/

public static Map<String,String> getFilePath(HttpResponse response, String url, String diskUrl) {
Map<String,String> map = new HashMap<>();
String filepath = diskUrl;
String filename = getFileName(response, url);
String contentType = response.getEntity().getContentType().getValue();
if(StringUtils.isNotEmpty(contentType)){
// 获取后缀 String regEx = ".+(.+)$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(filename);
if (!m.find()) {
// 如果正则匹配后没有后缀,则需要通过response中的ContentType的值进行匹配 filename = filename +".gz";

}else{
if(filename.length()>20){
filename = getRandomFileName() + ".gz";
}
}
}
if (filename != null) {
filepath += filename;
} else {
filepath += getRandomFileName();
}
map.put("filename", filename);
map.put("filepath", filepath);
return map;
}



/**
* 获取response header中Content-Disposition中的filename值
* @param response
* @param url
* @return
*/

public static String getFileName(HttpResponse response,String url) {
Header contentHeader = response.getFirstHeader("Content-Disposition");
String filename = null;
if (contentHeader != null) {
// 如果contentHeader存在 HeaderElement[] values = contentHeader.getElements();
if (values.length == 1) {
NameValuePair param = values[0].getParameterByName("filename");
if (param != null) {
try {
filename = param.getValue();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}else{
// 正则匹配后缀 filename = getSuffix(url);
}

return filename;
}

/**
* 获取随机文件名
*
* @return
*/

public static String getRandomFileName() {
return String.valueOf(System.currentTimeMillis());
}

/**
* 获取文件名后缀
* @param url
* @return
*/

public static String getSuffix(String url) {
// 正则表达式“.+/(.+)$”的含义就是:被匹配的字符串以任意字符序列开始,后边紧跟着字符“/”, // 最后以任意字符序列结尾,“()”代表分组操作,这里就是把文件名做为分组,匹配完毕我们就可以通过Matcher // 类的group方法取到我们所定义的分组了。需要注意的这里的分组的索引值是从1开始的,所以取第一个分组的方法是m.group(1)而不是m.group(0)。 String regEx = ".+/(.+)$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(url);
if (!m.find()) {
// 格式错误,则随机生成个文件名 return String.valueOf(System.currentTimeMillis());
}
return m.group(1);

}
  • url为第一步中从环信下载历史消息文件接口中请求返回的url(消息文件下载地址)
  • targetUrl 为下载的本地存储路径

下载以后从对应的路径下就可以看到所下载的文件。

3、消息文件解压,下载完的文件是以.gz结尾的压缩文件,需要对压缩文件进行解压

 public static void unGzipFile(String gzFilePath,String directoryPath) {
String ouputfile = "";
try {
//建立gzip压缩文件输入流 FileInputStream fin = new FileInputStream(gzFilePath);
//建立gzip解压工作流 GZIPInputStream gzin = new GZIPInputStream(fin);
//建立解压文件输出流// ouputfile = sourcedir.substring(0,sourcedir.lastIndexOf('.'));// ouputfile = ouputfile.substring(0,ouputfile.lastIndexOf('.')); FileOutputStream fout = new FileOutputStream(directoryPath);
int num;
byte[] buf=new byte[1024];
while ((num = gzin.read(buf,0,buf.length)) != -1) {
fout.write(buf,0,num);
}
gzin.close();
fout.close();
fin.close();
} catch (Exception ex){
System.err.println(ex.toString());
}
return;
}

gzFilePath:压缩文件路径
directoryPath:加压到的文件目录路径
解压后的文件如下图所示:

4、文件读取,将解压后的文件读取出来

FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("/Users/liupeng/Downloads/download/1234567890");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str = null;
long i = 0;
while(true){
try {
if (!((str = bufferedReader.readLine()) != null)) break;
} catch (IOException e) {
e.printStackTrace();
}
JSONObject jo = JSONObject.parseObject(str);
System.out.println("==========================================" + i);
System.out.println("消息id:" + jo.get("msg_id"));
System.out.println("发送id:" + jo.get("from"));
System.out.println("接收id:" + jo.get("to"));
System.out.println("服务器时间戳:" + jo.get("timestamp"));
System.out.println("会话类型:" + jo.get("chat_type"));
System.out.println("消息扩展:" + jo.getJSONObject("payload").get("ext"));
System.out.println("消息体:" + jo.getJSONObject("payload").getJSONArray("bodies").get(0));
i ++;
if (i > 100) break;
}
//close try {
inputStream.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}

} catch (FileNotFoundException e) {
e.printStackTrace();
}

解析完以后日志打印如下:

至此,解析完以后可以将解析的数据进行存储。

相关文档:

注册环信即时通讯IM:https://console.easemob.com/user/register

IMGeek社区支持:https://www.imgeek.net/

收起阅读 »

漫画:成年人的社交潜台词

原则上可以=不可以原则上不可以=可以再说吧=没戏……花:学到了,看我活学活用!——————————————————————————————甜狗:hi花:?甜狗:最近过的怎么样花:还好吧甜狗:一起出去玩呀花:有空一定去甜狗:改天请你吃饭花:我比较相信缘分甜狗:我...
继续阅读 »




原则上可以=不可以
原则上不可以=可以
再说吧=没戏……


花:学到了,看我活学活用!
——————————————————————————————
甜狗:hi
花:?
甜狗:最近过的怎么样
花:还好吧
甜狗:一起出去玩呀
花:有空一定去
甜狗:改天请你吃饭
花:我比较相信缘分
甜狗:我们分手吧
花:我在考虑考虑
甜狗:考虑啥?你还爱我吗
花:哎呀、我不是这个意思
甜狗:晚安
——————————————————————————————
花:(朋友圈)最后还是自己默默承受

作者:灼见
来源:mp.weixin.qq.com/s/nvXTNj-GwNDW4zsvbBNTng

收起阅读 »

未来三年,请主动给生活降级

任正非曾在华为内部论坛发言时说:接下来3年,华为要将“活下去”作为主要纲领。企业寒意阵阵,其中的个人也在顶着寒气的侵袭,努力与生活周旋。未来的几年,普通人最好的应对方法,是主动给自己减负,给生活降级。01消费降级前阵子,话题#一件事说明你消费降级#登上微博热搜...
继续阅读 »

任正非曾在华为内部论坛发言时说:接下来3年,华为要将“活下去”作为主要纲领。


企业寒意阵阵,其中的个人也在顶着寒气的侵袭,努力与生活周旋。


未来的几年,普通人最好的应对方法,是主动给自己减负,给生活降级。


01

消费降级


前阵子,话题#一件事说明你消费降级#登上微博热搜。


以前人们把炫富当潮流,现在流行的是各种花式“抠门”。


深夜蹲在便利店等一份三折便当,作为第二天的早餐;


自己在家理发,全身上下没有超过100块的衣服;


洗面奶牙膏挤不动了,用剪刀剪开,接着再用个三四次……


谁也不想抠抠搜搜过日子,但感受过失业危机,承受过意外侵袭的我们,开始活得无比清醒。


相比起买买买的畅快,握在手里的存款,才是我们最大的安全感。


未来几年,各种考验依然在等着我们。


狠狠省钱,努力存钱,就是在不确定的未来,给自己攒一份确定性。


《极简生活》一书提供了一个购物标准,买东西前问自己3个问题:


我是否真的需要它?我使用它的频率是多少?我现有的物品中,是否有东西可以替代它?


想清楚这几个问题,就能帮你省下许多不必要的开支。


畅销书作家哈维·艾克说:你管理金钱的习惯,比你拥有的钱财数目更重要。


管理金钱的第一步,就是要养成记账的习惯。


你可以下载专门的记账APP,来记录你一天的开支。


然后每周或者每月进行复盘,看看什么地方该花,什么地方不该花。


当你清楚知道每一分钱的去处,自然能堵住出口,守住自己的钱包。


巴尔扎克说过:


对于浪费的人,金钱是圆的,可是对于节俭的人,金钱是扁的,是可以一块块堆积起来的。


学会省钱,你省的是风险;学会存钱,你存的是保障。


推荐几个我亲测有效的存钱方法:


百分比存钱法:每月把收入的10%存起来,强制储蓄,雷打不动;


365存钱法:画一个表格,每天挑1-365中的一个数字来存钱,一年后,能轻轻松松攒下66795元;



每周累计存钱法:一年52周,第一周存10块,第二周存20……


以此类推,一年下来,也能存住一万多。


消费有度,存钱上瘾,晴备雨伞,饱存饥粮,才是未来3年最好的金钱观。


02

投资降级


最近有个网络热词“中产不要命三件套”:投资商铺、辞职创业、全职炒股。


许多人对经济形势的判断太过乐观,盲目投资,最终连原本安稳的生活也赔了进去。


说两个我朋友的故事。


一位朋友是深圳一家贸易公司的高管,年薪百万。


妻子在家做全职太太,两人育有一双儿女。


去年,考虑到老大要上学了,他们想把手里的小三房卖了,置换一套好点的学区房。


可房子迟迟卖不出去,夫妻俩着急孩子上学的事,就和朋友借了150万,又贷款200万买下了学区房。


不曾想他们刚买完房子,朋友就被裁员了,此后投了上百份简历都杳无音讯。


可他还有一大家子要养活,每月还有近2万的房贷要还,离职的赔偿也很快被花光。


曾经的中产精英,只得选择断供,四处借钱度日。


另外一个朋友,是大型银行的技术专员,每月一万多的工资。


但某天他听一个朋友建议,投资了一个连锁餐饮项目。


他花光自己40多万的积蓄,还向3家银行借了贷。


但后来疫情来袭,朋友的投资也打了水漂,亏光所有本金不说,还欠了银行一屁股债。


作家连岳说:


投资的标准是,你要有本事先安置好家人的生活,此后还有闲钱,才能用来投资;


投资失败后,还要保证家人丰衣足食,不能遵循这个标准,就会被投资害死。


未来几年,各种不确定性依然存在,我建议你:


1. 清空负债,减少信用卡的使用频率;


2. 不要盲目投资,尽量选择稳健的投资策略;


3. 做任何事情都不要全押,卡上至少要有家庭储蓄一两年的生活资金。


03

就业降级


《凉子访谈录》中有位35岁的受访者,被大公司裁员后,收到一家资历尚浅的公司offer。


他直言自己还有一些行业自尊心,还是想去更大的平台,就拒绝了。


他认为凭自己的资历,找个跟之前差不多的公司不在话下,可现实却是他投的简历回应者寥寥无几。


大家应该都感觉到了,这几年工作越来越难找。


台湾劳动部《劳工失业后再就业情形》就有调查数据显示:


45岁以上职场人一旦失业,想要找到新工作平均得花6个月,还有33.6%的人找不到新工作或放弃不找了。


我身边有失业的朋友,找工作时也是一再降薪,不求岗位对口,只求尽快入职,因为房贷不等人。


未来3年,就业形势会更加残酷,你需要遵循以下三个法则。


1. 先活下去再说


一朝失业,才懂什么叫焦头烂额。


没有收入的日子,车贷房贷、孩子的教育支出、日常生活开支,样样都成了难题。


诚如俞敏洪所讲:当一个人面临生存问题,先活下去再说。


“只要这份工作不玷污你的人格,你再劳累再不喜欢,只要可以给你带来一份收入,你可以先做。”


世道艰难,暂时苟着,并不丢人。


2. 珍惜现在的单位


去年1月,Google突然宣布裁员12000人,紧接着,IBM加入了裁员大军,裁员3900人,3月底,微软也宣布裁员1万名员工……


在这个瞬息万变的时代,如果你还有班可上,其实就已经跑赢了大多数人。


所以,要珍惜现有的工作,善待你所在的单位。


一份按时到来的工资,能让你维持生计,一份不错的工作,可以为你遮风挡雨。


寒意尚未褪去之际,和现有单位一起抗住压力,和同事抱团取暖,才不至于被冻僵。


3. 保持归零心态


在找工作的心态上,你要抛弃走到今天为止你所有的成就、地位和光环。


大家总有“35岁焦虑”,是因为大家总认为自己应该越挣越多。


但是,年龄与成就并非线性关系。


前面跑得快,后面跑得慢,你会被后面的人超越,这是必然会发生的。


未来3年,什么样的人会活得很好?


热爱变化,主动拥抱不确定性,勇于走出舒适圈的人。


愿你在反复归零的状态下,依然能充满勇气,义无反顾迎接下一个变化。


04

社交降级


英国作家普利斯特利说:


社交性聚会就是去不去你都会感到后悔的一种活动,不去也没人注意你的缺席,去了就是参加一种虚情假意的游戏。


一场疫情,更是让许多人在自我隔离中,逐渐发现了社交非必要性。


事实上,过度的社交不仅无法排解情感上的孤独,无法带来所谓的人脉,反而是一种自我消耗。


以后,请给自己的社交降级。


1. 不去无意义的饭局


当我们参加热热闹闹的饭局,以为在吃喝玩乐中就把人脉搞定了。


却不明白,酒桌上的交情无法延伸到酒桌外。


热衷于这种聚会,浪费精力不说,还会有一种“朋友遍天下”的错觉。


等到现实的一个浪头打过来,你才会明白,逢场作戏的友谊,根本不堪一击。


2. 走出虚假的名利场


苏芒在《芭莎》杂志任主编时,每次与一众名媛合照都站在C位。


可当她在时尚圈地位不保之后,别人发合照都会把她裁掉。


经历过动荡起伏的人,更能懂什么叫人走茶凉。


成年人的世界向来现实,自己没有价值,所有的社交都是浮云。


与其在名利场上费心攀关系,不如好好修炼自己的本事。


3. 远离低层次圈子


周国平曾讲:


“为了尽兴而聚在一起的人,要么债台高筑,要么百病缠身,最终往往不能尽兴;


反倒是那些聚在一起吃苦的人,身体和心灵都得到锻炼,最终过得幸福圆满。”


低层次的圈子,会不断消耗你、腐蚀你,直到你沉沦其中;


融入更优秀的群体,才是成长的最佳路径。



诗人里尔克说:哪有什么胜利可言,挺住意味着一切。


生活的海域从不平静,你要以稳健的姿态迎接风浪,挺过风浪。


未来3年,请捂紧钱包,低配欲望。


请相信,你对生活的每一次低头,都是为了以后更好地昂首。


你当下的每一份积累,终将换来命运的厚待。


作者:每晚安娜贝苏
来源:每晚一卷书(JYXZ89896)

收起阅读 »

Java切换到Kotlin,Crash率上升了?

前言 最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。 通过本篇文章...
继续阅读 »

前言


最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。

通过本篇文章,你将了解到:




  1. NPE(空指针 NullPointerException)的本质

  2. Java 如何预防NPE?

  3. Kotlin NPE检测

  4. Java/Kotlin 混合调用

  5. 常见的Java/Kotlin互调场景



1. NPE(空指针 NullPointerException)的本质


变量的本质


    val name: String = "fish"

name是什么?

对此问题你可能嗤之以鼻:



不就是变量吗?更进一步说如果是在对象里声明,那就是成员变量(属性),如果在方法(函数)里声明,那就是局部变量,如果是静态声明的,那就是全局变量。



回答没问题很稳当。

那再问为什么通过变量就能找到对应的值呢?



答案:变量就是地址,通过该地址即可寻址到内存里真正的值



无法访问的地址



在这里插入图片描述


如上图,若是name="fish",表示的是name所指向的内存地址里存放着"fish"的字符串。

若是name=null,则说明name没有指向任何地址,当然无法通过它访问任何有用的信息了。


无论C/C++亦或是Java/Kotlin,如果一个引用=null,那么这个引用将毫无意义,无法通过它访问任何内存信息,因此这些语言在设计的过程中会将通过null访问变量/方法的行为都会显式(抛出异常)提醒开发者。


2. Java 如何预防NPE?


运行时规避


先看Demo:


public class TestJava {
public static void main(String args[]) {
(new TestJava()).test();
}

void test() {
String str = getString();
System.out.println(str.length());
}

String getString() {
return null;
}
}

执行上述代码将会抛出异常,导致程序Crash:



在这里插入图片描述


我们有两种解决方式:




  1. try...catch

  2. 对象判空



try...catch 方式


public class TestJava {
public static void main(String args[]) {
(new TestJava()).testTryCatch();
}

void testTryCatch() {
try {
String str = getString();
System.out.println(str.length());
} catch (Exception e) {
}
}

String getString() {
return null;
}
}

NPE被捕获,程序没有Crash。


对象判空


public class TestJava {
public static void main(String args[]) {
(new TestJava()).testJudgeNull();
}

void testJudgeNull() {
String str = getString();
if (str != null) {
System.out.println(str.length());
}
}

String getString() {
return null;
}
}

因为提前判空,所以程序没有Crash。


编译时检测


在运行时再去做判断的缺点:



无法提前发现NPE问题,想要覆盖大部分的场景需要随时try...catch或是判空
总有忘记遗漏的时候,发布到线上就是个生产事故



那能否在编译时进行检测呢?

答案是使用注解。


public class TestJava {
public static void main(String args[]) {
(new TestJava()).test();
}

void test() {
String str = getString();
System.out.println(str.length());
}

@Nullable String getString() {
return null;
}
}

在编写getString()方法时发现其可能为空,于是给方法加上一个"可能为空"的注解:@Nullable


当调用getString()方法时,编译器给出如下提示:



在这里插入图片描述


意思是访问的getString()可能为空,最后访问String.length()时可能会抛出NPE。

看到编译器的提示我们就知道此处有NPE的隐患,因此可以针对性的进行处理(try...catch或是判空)。


当然此处的注解仅仅只是个"弱提示",你即使没有进行针对性的处理也能编译通过,只是问题最后都流转到运行时更难挽回了。


有"可空"的注解,当然也有"非空"的注解:



在这里插入图片描述


@Nonnull 注解修饰了方法后,若是检测到方法返回null,则会提示修改,当然也是"弱提示"。


3. Kotlin NPE检测


编译时检测


Kotlin 核心优势之一:



空安全检测,变量分为可空型/非空型,能够在编译期检测潜在的NPE,并强制开发者确保类型一致,将问题在编译期暴露并解决



先看非空类型的变量声明:


class TestKotlin {

fun test() {
val str = getString()
println("${str.length}")
}

private fun getString():String {
return "fish"
}
}

fun main() {
TestKotlin().test()
}


此种场景下,我们能确保getString()函数的返回一定非空,因此在调用该函数时无需进行判空也无需try...catch。


你可能会说,你这里写死了"fish",那我写成null如何?



在这里插入图片描述


编译期直接提示不能这么写,因为我们声明getString()的返回值为String,是非空的String类型,既然声明了非空,那么就需要言行一致,返回的也是非空的。


有非空场景,那也得有空的场景啊:


class TestKotlin {

fun test() {
val str = getString()
println("${str.length}")
}

private fun getString():String? {
return null
}
}

fun main() {
TestKotlin().test()
}

此时将getString()声明为非空,因此可以在函数里返回null。

然而调用之处就无法编译通过了:



在这里插入图片描述


意思是既然getString()可能返回null,那么就不能直接通过String.length访问,需要改为可空方式的访问:


class TestKotlin {

fun test() {
val str = getString()
println("${str?.length}")
}

private fun getString():String? {
return null
}
}

str?.length 意思是:如果str==null,就不去访问其成员变量/函数,若不为空则可以访问,于是就避免了NPE问题。


由此可以看出:



Kotlin 通过检测声明与实现,确保了函数一定要言行一致(声明与实现),也确保了调用者与被调用者的言行一致



因此,若是用Kotlin编写代码,我们无需花太多时间去预防和排查NPE问题,在编译期都会有强提示。


4. Java/Kotlin 混合调用


回到最初的问题:既然Kotlin都能在编译期避免了NPE,那为啥使用Kotlin重构后的代码反而导致Crash率上升呢?


原因是:项目里同时存在了Java和Kotlin代码,由上可知两者在NPE的检测上有所差异导致了一些兼容问题。


Kotlin 调用 Java


调用无返回值的函数


Kotlin虽然有空安全检测,但是Java并没有,因此对于Java方法来说,不论你传入空还是非空,在编译期我都没法检测出来。


public class TestJava {
void invokeFromKotlin(String str) {
System.out.println(str.length());
}
}

class TestKotlin {

fun test() {
TestJava().invokeFromKotlin(null)
}
}

fun main() {
TestKotlin().test()
}

如上无论是Kotlin调用Java还是Java之间互调,都没法确保空安全,只能由被调用者(Java)自己处理可能的异常情况。


调用有返回值的函数


public class TestJava {
public String getStr() {
return null;
}
}

class TestKotlin {
fun testReturn() {
println(TestJava().str.length)
}
}

fun main() {
TestKotlin().testReturn()
}

如上,Kotlin调用Java的方法获取返回值,由于在编译期Kotlin无法确定返回值,因此默认把它当做非空处理,若是Java返回了null,那么将会Crash。


Java 调用 Kotlin


调用无返回值的函数


先定义Kotlin类:


class TestKotlin {

fun testWithoutNull(str: String) {
println("len:${str.length}")
}

fun testWithNull(str: String?) {
println("len:${str?.length}")
}
}

有两个函数,分别接收可空/非空参数。


在Java里调用,先调用可空函数:


public class TestJava {
public static void main(String args[]) {
(new TestKotlin()).testWithNull(null);
}
}

因为被调用方是Kotlin的可空函数,因此即使Java传入了null,也不会有Crash。


再换个方式,在Java里调用非空函数:


public class TestJava {
public static void main(String args[]) {
(new TestKotlin()).testWithoutNull(null);
}
}

却发现Crash了!



在这里插入图片描述


为什么会Crash呢?反编译查看Kotlin代码:


public final class TestKotlin {
public final void testWithoutNull(@NotNull String str) {
Intrinsics.checkNotNullParameter(str, "str");
String var2 = "len:" + str.length();
System.out.println(var2);
}

public final void testWithNull(@Nullable String str) {
String var2 = "len:" + (str != null ? str.length() : null);
System.out.println(var2);
}
}

对于非空的函数来说,会有检测代码:

Intrinsics.checkNotNullParameter(str, "str"):


    public static void checkNotNullParameter(Object value, String paramName) {
if (value == null) {
throwParameterIsNullNPE(paramName);
}
}
private static void throwParameterIsNullNPE(String paramName) {
throw sanitizeStackTrace(new NullPointerException(createParameterIsNullExceptionMessage(paramName)));
}

可以看出:




  1. Kotlin对于非空的函数参数,先判断其是否为空,若是为空则直接抛出NPE

  2. Kotlin对于可空的函数参数,没有强制检测是否为空



调用有返回值的函数


Java 本身就没有空安全,只能在运行时进行处理。


小结


很容看出来:




  1. Java 调用Kotlin的非空函数有Crash的风险,编译器无法检查到传入的参数是否为空

  2. Java 调用Kotlin的可空函数没有Crash风险,Kotlin编译期检查空安全

  3. Kotlin 调用Java的函数有Crash风险,由Java代码规避风险

  4. Kotlin 调用Java有返回值的函数有Crash风险,编译器无法检查到返回值是否为空



回到文章的标题,我们已经大致知道了Java切换到Kotlin,为啥Crash就升上了的原因了,接下来再详细分析。


5. 常见的Java/Kotlin互调场景


Android里的Java代码分布



在这里插入图片描述


在Kotlin出现之前,Java就是Android开发的唯一语言,Android Framework、Androidx很多是Java代码编写的,因此现在依然有很多API是Java编写的。


而不少的第三方SDK因为稳定性、迁移代价的考虑依然使用的是Java代码。


我们自身项目里也因为一些历史原因存在Java代码。


以下讨论的前提是假设现有Java代码我们都无法更改。


Kotlin 调用Java获取返回值


由于编译期无法判定Java返回的值是空还是非空,因此若是确认Java函数可能返回空,则可以通过在Kotlin里使用可空的变量接收Java的返回值。


class TestKotlin {
fun testReturn() {
val str: String? = TestJava().str
println(str?.length)
}
}

fun main() {
TestKotlin().testReturn()
}

Java 调用Kotlin函数


LiveData Crash的原因与预防


之前已经假设过我们无法改动Java代码,那么Java调用Kotlin函数的场景只有一个了:回调。

上面的有返回值场景还是比较容易防备,回调的场景就比较难发现,尤其是层层封装之后的代码。

这也是特别常见的场景,典型的例子如LiveData。


Crash原因


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData: MutableLiveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
println(it.length)
}
}

init {
testLiveData()
}
}

如上,使用Kotlin声明LiveData,其类型是非空的,并监听LiveData的变化。


在另一个地方给LiveData赋值:


TestKotlin(this@MainActivity).liveData.value = null

虽然LiveData的监听和赋值的都是使用Kotlin编写的,但不幸的是还是Crash了。

发送和接收都是用Kotlin编写的,为啥还会Crash呢?

看看打印:



在这里插入图片描述


意思是接收到的字符串是空值(null),看看编译器提示:



在这里插入图片描述


原来此处的回调传入的值被认为是非空的,因此当使用it.length访问的时候编译器不会有空安全提示。


再看看调用的地方:



在这里插入图片描述


可以看出,这回调是Java触发的。


Crash 预防


第一种方式:

我们看到LiveData的数据类型是泛型,因此可以考虑在声明数据的时候定为非空:


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
println(it?.length)
}
}

init {
testLiveData()
}
}

如此一来,当访问it.length时编译器就会提示可空调用。


第二种方式:

不修改数据类型,但在接收的地方使用可空类型接收:


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
val dataStr:String? = it
println(dataStr?.length)
}
}

init {
testLiveData()
}
}

第三种方式:

使用Flow替换LiveData。


LiveData 修改建议:




  1. 若是新写的API,建议使用第三种方式

  2. 若是修改老的代码,建议使用第一种方式,因为可能有多个地方监听LiveData值的变化,如果第一种方式的话需要写好几个地方。



其它场景的Crash预防:


与后端交互的数据结构
比如与后端交互声明的类,后端有可能返回null,此时在客户端接收时若是使用了非空类型的字段去接收,那么会发生Crash。

通常来说,我们会使用网络框架(如retrofit)接收数据,数据的转换并不是由我们控制,因此无法使用针对LivedData的第二种方式。

有两种方式解决:




  1. 与后端约定,不能返回null(等于白说)

  2. 客户端声明的类的字段声明为可空(类似针对LivedData的第一种方式)



Json序列化/反序列化

Json字符串转换为对象时,有些字段可能为空,也需要声明为可空字段。


小结



在这里插入图片描述


作者:小鱼人爱编程
来源:juejin.cn/post/7274163003158511616
收起阅读 »

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:


  • LruCache

  • 持久化(sqlite、file等)

  • 匿名共享内存


使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:


public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.


LruCache<String, Object> mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:


@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:


@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。



今日分享到此结束,对你有帮助的话,点个赞再走呗,每日一个面试小技巧




关注公众号:Android老皮

解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版



内容如下



1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路



作者:派大星不吃蟹
来源:juejin.cn/post/7264503091116965940
收起阅读 »

如何让 Android 网络请求像诗一样优雅

在 Android 应用开发中,网络请求必不可少,如何去封装才能使自己的请求代码显得更加简洁优雅,更加方便于以后的开发呢?这里利用 Kotlin 的函数式编程和 Retrofit 来从零开始封装一个网络请求框架,下面就一起来瞧瞧吧! 首先,引入网络请求框架的依...
继续阅读 »

在 Android 应用开发中,网络请求必不可少,如何去封装才能使自己的请求代码显得更加简洁优雅,更加方便于以后的开发呢?这里利用 Kotlin 的函数式编程和 Retrofit 来从零开始封装一个网络请求框架,下面就一起来瞧瞧吧!


首先,引入网络请求框架的依赖。


implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

定义拦截器


我们可以先自定义一些拦截器,对一些公共提交的字段做封装,比如 token。在服务器注册成功或者登录成功之后获取 token,过期之后便无法正常请求接口,所以需要在请求接口时判断 token 是否过期,由于接口众多,不可能每个接口都进行判断,所以需要全局设置一个拦截器判断 token。


class TokenInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
// 当前拦截器中收到的请求对象
val request = chain.request()
// 执行请求
var response = chain.proceed(request)
if (response.body == null) {
return response
}
val mediaType = response.body!!.contentType() ?: return response
val type = mediaType.toString()
if (!type.contains("application/json")) {
return response
}
val result = response.body!!.string()
var code = ""
try {
val jsonObject = JSONObject(result)
code = jsonObject.getString("code")
} catch (e: Exception) {
e.printStackTrace()
}
// 重新构建 response
response = response.newBuilder().body(result.toResponseBody(null)).code(200).build()
if (isTokenExpired(code)) {
// token 过期,需要获取新的 token
val newToken = getNewToken() ?: return response
// 重新构建新的 token 请求
val builder = request.url.newBuilder().setEncodedQueryParameter("token", newToken)
val newRequest = request.newBuilder().method(request.method, request.body)
.url(builder.build()).build()
return chain.proceed(newRequest)
}
return response
}

// 判断 token 是否过期
private fun isTokenExpired(code: String) =
TextUtils.equals(code, "401") || TextUtils.equals(code, "402")

// 刷新 token
private fun getNewToken() = ServiceManager.instance.refreshToken()

}

这里是 token 过期之后直接重新请求接口获取新的 token,这需要根据具体业务需求来,有些可能是过期之后跳转到登录页面,让用户重新登录等等。


我们还可以再定义一个拦截器,全局添加 token。


class TokenHeaderInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val headers = request.headers
var token = headers["token"]
if (TextUtils.isEmpty(token)) {
token = ServiceManager.instance.getToken()
request = request.newBuilder().addHeader("token", token).build()
}
return chain.proceed(request)
}

}

创建 retrofit


class RetrofitUtil {

companion object {

private const val TIME_OUT = 20L

private fun createRetrofit(): Retrofit {

// OkHttp 提供的一个拦截器,用于记录和查看网络请求和响应的日志信息。
val interceptor = HttpLoggingInterceptor()
// 打印请求和响应的所有内容,响应状态码和执行时间等等。
interceptor.level = HttpLoggingInterceptor.Level.BODY

val okHttpClient = OkHttpClient().newBuilder().apply {
addInterceptor(interceptor)
addInterceptor(TokenInterceptor())
addInterceptor(TokenHeaderInterceptor())
retryOnConnectionFailure(true)
connectTimeout(TIME_OUT, TimeUnit.SECONDS)
writeTimeout(TIME_OUT, TimeUnit.SECONDS)
readTimeout(TIME_OUT, TimeUnit.SECONDS)
}.build()

return Retrofit.Builder().apply {
addConverterFactory(GsonConverterFactory.create())
baseUrl(ServiceManager.instance.baseHttpUrl)
client(okHttpClient)
}.build()

}

fun <T> getAPI(clazz: Class<T>): T {
return createRetrofit().create(clazz)
}

}
}

网络请求封装


定义通用基础请求返回的数据结构


private const val SERVER_SUCCESS = "200"

data class BaseResp<T>(val code: String, val message: String, val data: T)

fun <T> BaseResp<T>?.isSuccess() = this?.code == SERVER_SUCCESS

请求状态流程封装,可以根据具体业务流程实现方法。


class RequestAction<T> {

// 开始请求
var start: (() -> Unit)? = null
private set

// 发起请求
var request: (suspend () -> BaseResp<T>)? = null
private set

// 请求成功
var success: ((T?) -> Unit)? = null
private set

// 请求失败
var error: ((String) -> Unit)? = null
private set

// 请求结束
var finish: (() -> Unit)? = null
private set

fun request(block: suspend () -> BaseResp<T>) {
request = block
}

fun start(block: () -> Unit) {
start = block
}

fun success(block: (T?) -> Unit) {
success = block
}

fun error(block: (String) -> Unit) {
error = block
}

fun finish(block: () -> Unit) {
finish = block
}

}

因为网络请求都是在 ViewModel 中进行的,我们可以定义一个 ViewModel 的扩展函数,用来处理网络请求。


fun <T> ViewModel.netRequest(block: RequestAction<T>.() -> Unit) {

val action = RequestAction<T>().apply(block)

viewModelScope.launch {
try {
action.start?.invoke()
val result = action.request?.invoke()
if (result.isSuccess()) {
action.success?.invoke(result!!.data)
} else {
action.error?.invoke(result!!.message)
}
} catch (ex: Exception) {
// 可以做一些定制化的返回错误提示
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}

}

private const val SERVER_ERROR = "HTTP 500 Internal Server Error"
private const val HTTP_ERROR_TIP = "服务器或者网络连接错误"

fun getErrorTipContent(ex: Throwable) = if (ex is ConnectException || ex is UnknownHostException
|| ex is SocketTimeoutException || SERVER_ERROR == ex.message.toString()
) HTTP_ERROR_TIP else ex.message.toString()

使用案例


定义网络请求接口


interface HttpApi {

@GET("/exampleA/exampleP/exampleI/exampleApi/getNetData")
suspend fun getNetData(@QueryMap params: HashMap<String, String>): BaseResp<NetDataBean>

@GET("/exampleA/exampleP/exampleI/exampleApi/getTestData")
suspend fun getTestData(
@Query("param1") param1: String,
@Query("param2") param2: String
)
: BaseResp<NetDataBean>

@GET("/exampleA/exampleP/exampleI/exampleApi/{id}")
fun getNetTask(
@Path("id") id: String,
@QueryMap params: HashMap<String, String>,
)
: Call<BaseResp<TaskBean>>

@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/confirm")
suspend fun confirm(@Field("id") id: String, @Field("token") token: String): BaseResp<String>

@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/upload")
suspend fun upload(@FieldMap params: Map<String, String>): BaseResp<String>

}

我们可以写一个网络请求帮助类,用于请求的创建。


class RequestHelper {

private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}

suspend fun getNetData(params: HashMap<String, String>) = httpApi.getNetData(params)

suspend fun getTestData(branchCode: String, token: String) =
httpApi.getTestData(branchCode, token)

suspend fun getNetTask(id: String, params: HashMap<String, String>) =
httpApi.getNetTask(id, params)

suspend fun confirm(id: String, token: String) = httpApi.confirm(id, token)

suspend fun upload(params: HashMap<String, String>) = httpApi.upload(params)

}

定义用户的意图和 UI 状态


// 定义用户意图
sealed class MainIntent {
object FetchData : MainIntent()
}

// 定义 UI 状态
sealed class MainUIState {
object Loading : MainUIState()
data class NetData(val data: NetDataBean?) : MainUIState()
data class Error(val error: String?) : MainUIState()
}

ViewModel 中做意图的处理和 UI 状态的变更,根据网络请求结果传递不同的状态,使用定义的扩展方法去执行网络请求,封装过后的网络请求就很简洁方便了,下面演示下具体使用。


class MainViewModel : ViewModel() {

val mainIntent = Channel<MainIntent>(Channel.UNLIMITED)

private val _mainUIState = MutableStateFlow<MainUIState>(MainUIState.Loading)
val mainUIState: StateFlow<MainUIState>
get() = _mainUIState

init {
viewModelScope.launch {
mainIntent.consumeAsFlow().collect {
if (it is MainIntent.FetchData) {
getNetDataResult()
}
}
}
}
// 使用
private fun getNetDataResult() = netRequest {
start { _mainUIState.value = MainUIState.Loading }
request {
val paramMap = hashMapOf<String, String>()
paramMap["param1"] = "param1"
paramMap["param2"] = "param2"
RequestHelper.instance.getNetData(paramMap)
}
success { _mainUIState.value = MainUIState.NetData(it) }
error { _mainUIState.value = MainUIState.Error(it) }
}

}

这样是不是看起来很简洁呢?接下来,Activity 负责发送意图和接收 UI 状态进行相关的处理就行啦!


class MainActivity : AppCompatActivity() {

private val viewModel by viewModels<MainViewModel>()
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
initData()
observeViewModel()
}

private fun initData() {
lifecycleScope.launch {
// 发送意图
viewModel.mainIntent.send(MainIntent.FetchData)
}
}

private fun observeViewModel() {
lifecycleScope.launch {
viewModel.mainUIState.collect {
when (it) {
is MainUIState.Loading -> showLoading()
// 这里拿到网络请求返回的数据,根据业务自行操作,这里只做简单的显示。
is MainUIState.NetData -> showText(it.data.toString())
is MainUIState.Error -> showText(it.error)
}
}
}
}

private fun showLoading() {
binding.progressBar.visibility = View.VISIBLE
binding.netText.visibility = View.GONE
}

private fun showText(result: String?) {
binding.progressBar.visibility = View.GONE
binding.netText.visibility = View.VISIBLE
binding.netText.text = result
}

}

文件的上传与下载


如果是文件的上传和下载呢?其实文件还不太一样,这涉及到上传进度,文件的处理等方面,所以,为了方便开发使用,我们可以针对文件单独再做一下封装。


定义文件上传对象


data class UpLoadFileBean(val file: File, val fileKey: String)

自定义 RequestBody,从中获取上传进度。


class ProgressRequestBody(
private var requestBody: RequestBody,
var onProgress: ((Int) -> Unit)?,
) : RequestBody() {

private var bufferedSink: BufferedSink? = null

override fun contentType(): MediaType? = requestBody.contentType()

override fun contentLength(): Long {
return requestBody.contentLength()
}

override fun writeTo(sink: BufferedSink) {
if (bufferedSink == null) bufferedSink = createSink(sink).buffer()
bufferedSink?.let {
requestBody.writeTo(it)
it.flush()
}
}

private fun createSink(sink: Sink): Sink = object : ForwardingSink(sink) {
// 当前写入字节数
var bytesWritten = 0L

// 总字节长度
var contentLength = 0L

override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)

if (contentLength == 0L) {
contentLength = contentLength()
}

// 增加当前写入的字节数
bytesWritten += byteCount

CoroutineScope(Dispatchers.Main).launch {
// 进度回调
onProgress?.invoke((bytesWritten * 100 / contentLength).toInt())
}
}
}

}

创建 MultipartBody.Part


fun <T> createPartList(action: UpLoadFileAction<T>): List<MultipartBody.Part> =
MultipartBody.Builder().apply {
// 公共参数 token
addFormDataPart("token", ServiceManager.instance.getToken())

// 其他基本参数
action.params?.forEach {
if (it.key.isNotBlank() && it.value.isNotBlank()) {
addFormDataPart(it.key, it.value)
}
}

// 文件校验
action.fileData?.let {
addFormDataPart(
it.fileKey, it.file.name, ProgressRequestBody(
requestBody = it.file
.asRequestBody("application/octet-stream".toMediaTypeOrNull()),
onProgress = action.progress
)
)
}
}.build().parts

定义文件上传行为


class UpLoadFileAction<T> {

// 请求体
lateinit var request: (suspend () -> BaseResp<T>)
private set

lateinit var parts: List<MultipartBody.Part>

// 其他普通参数
var params: HashMap<String, String>? = null
private set

// 文件参数
var fileData: UpLoadFileBean? = null
private set

// 初始化参数
fun init(params: HashMap<String, String>?, fileData: UpLoadFileBean?) {
this.params = params
this.fileData = fileData
parts = createPartList(this)
}

var start: (() -> Unit)? = null
private set

var success: (() -> Unit)? = null
private set

var error: ((String) -> Unit)? = null
private set

var progress: ((Int) -> Unit)? = null
private set

var finish: (() -> Unit)? = null
private set

fun start(block: () -> Unit) {
start = block
}

fun success(block: () -> Unit) {
success = block
}

fun error(block: (String) -> Unit) {
error = block
}

fun progress(block: (Int) -> Unit) {
progress = block
}

fun finish(block: () -> Unit) {
finish = block
}

fun request(block: suspend () -> BaseResp<T>) {
request = block
}

}

同样,定义 ViewModel 的扩展函数,用来执行文件上传。


fun <T> ViewModel.upLoadFile(
block: UpLoadFileAction<T>.() -> Unit,
params: HashMap<String, String>?,
fileData: UpLoadFileBean?,
)
= viewModelScope.launch {
val action = UpLoadFileAction<T>().apply(block)
try {
action.init(params, fileData)
action.start?.invoke()
val result = action.request.invoke()
if (result.isSuccess()) {
action.success?.invoke()
} else {
action.error?.invoke(result.message)
}
} catch (ex: Exception) {
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}

定义文件上传接口


interface HttpApi {
//...

@Multipart
@POST("/exampleA/exampleP/exampleI/exampleApi/uploadFile")
suspend fun uploadFile(@Part partLis: List<MultipartBody.Part>): BaseResp<String>

}

在 RequestHelper 中定义上传文件方法


class RequestHelper {

private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}

//...

suspend fun uploadFile(partList: List<MultipartBody.Part>) = httpApi.uploadFile(partList)

}

封装过后的文件上传就很简洁方便了,下面演示下具体使用。


private fun uploadMyFile() = upLoadFile(
params = hashMapOf("param1" to "param1", "param2" to "param2"),
fileData = UpLoadFileBean(File(absoluteFilePath), "file"),
) {
start {
// TODO: 开始上传,此处可以显示加载动画
}
request { RequestHelper.instance.uploadFile(parts) }
success {
// TODO: 上传成功
}
error {
// TODO: 上传失败
}
finish {
// TODO: 上传结束,此处可以关闭加载动画
}
}

既然上传文件都有了,那怎么少得了下载呢?其实,下载比上传更简单,下面就来写一下,同样利用了 kotlin 的函数式编程,我们添加 ViewModel 的扩展函数,需要注意的是,由于这边是直接使用 OkHttp 的同步请求,所以把这部分代码放在了 IO 线程中。


fun ViewModel.downLoadFile(
downLoadUrl: String,
dirPath: String,
fileName: String,
progress: ((Int) -> Unit)?,
success: (File) -> Unit,
failed: (String) -> Unit,
)
= viewModelScope.launch(Dispatchers.IO) {
try {
val fileDir = File(dirPath)
if (!fileDir.exists()) {
fileDir.mkdirs()
}
val downLoadFile = File(fileDir, fileName)
val request = Request.Builder().url(downLoadUrl).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
if (response.isSuccessful) {
response.body?.let {
val totalLength = it.contentLength().toDouble()
val stream = it.byteStream()
stream.copyTo(downLoadFile.outputStream()) { currentLength ->
// 当前下载进度
val process = currentLength / totalLength * 100
progress?.invoke(process.toInt())
}
success.invoke(downLoadFile)
} ?: failed.invoke("response body is null")
} else failed.invoke("download failed:$response")
} catch (ex: Exception) {
failed.invoke("download failed:${getErrorTipContent(ex)}")
}
}


// InputStream 添加扩展函数,实现字节拷贝。
private fun InputStream.copyTo(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
progress: (Long) -> Unit,
)
: Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
progress(bytesCopied)
}
return bytesCopied
}

然后,使用就会变得很简洁了,如下所示:


fun downloadMyFile(downLoadUrl: String, dirPath: String, fileName: String) =
downLoadFile(
downLoadUrl = downLoadUrl,
dirPath = dirPath,
fileName = fileName,
progress = {
// TODO: 这里可以拿到进度
},
success = {
// TODO: 下载成功,拿到下载的文件对象 File
},
failed = {
// TODO: 下载失败,返回原因
}

)

作者:阿健君
来源:juejin.cn/post/7266768708139434045
收起阅读 »

Service 层异常抛到 Controller 层处理还是直接处理?

0 前言 一般初学者学习编码和[错误处理]时,先知道[编程语言]有一种处理错误的形式或约定(如Java就抛异常),然后就开始用这些工具。但却忽视这问题本质:处理错误是为了写正确程序。可是 1 啥叫“正确”? 由解决的问题决定的。问题不同,解决方案不同。 如一个...
继续阅读 »

0 前言


一般初学者学习编码和[错误处理]时,先知道[编程语言]有一种处理错误的形式或约定(如Java就抛异常),然后就开始用这些工具。但却忽视这问题本质:处理错误是为了写正确程序。可是


1 啥叫“正确”?


由解决的问题决定的。问题不同,解决方案不同。


如一个web接口接受用户请求,参数age,也许业务要求字段是0~150之间整数。如输入字符串或负数就肯定不接受。一般在后端某地做输入合法性检查,不过就抛异常。


但归根到底这问题“正确”解决方法总是要以某种形式提示用户。而提示用户是某种前端工作,就要看界面是app,H5+AJAX还是类似于[jsp]的服务器产生界面。不管啥,你要根据需求去”设计一个修复错误“的流程。如一个常见的流程要后端抛异常,然后一路到某个集中处理错误的代码,将其转换为某个HTTP的错误(业务错误码)提供给前端,前端再映射做”提示“。如用户输入非法请求,从逻辑上后端都没法自己修复,这是个“正确”的策略。


2 报500了嘞!


如用户上传一个头像,后端将图片发给[云存储],结果云存储报500,咋办?你可能想重试,因为也许仅是[网络抖动],重试就能正常执行。但若重试多次无效,若设计了某种热备方案,可能改为发到另一个服务器。“重试”和“使用备份的依赖”都是“立刻处理“。


但若重试无效,所有的[备份服务]也无效,也许就能像上面那样把错误抛给前端,提示用户“服务器开小差”。从这方案易看出,你想把错误抛到哪里是因为那个catch的地方是处理问题最方便的地方。一个问题的解决方案可能要几个不同的错误处理组合起来才能办到。


3 NPE了!


你的程序抛个NPE。这一般就是程序员的bug:



  • 要不就是程序员想表达一个东西”没有“,结果在后续处理中忘判断是否为null

  • 要不就是在写代码时觉得100%不可能为null的地方出现了一个null


不管哪种,这错误用户总会看到一个很含糊的报错信息,这远远不够。“正确”办法是程序员自己能尽快发现它,并尽快修复。要做到这点,需要[监控系统]不断爬log,把问题报警出来。而非等用户找客服投诉。


4 OOM了!


比如你的[后端程序]突然OOM挂了。挂的程序没法恢复自己。要做到“正确”,须在服务之外的容器考虑这问题。


如你的服务跑在[k8s],他们会监控你程序状态,然后重启新的服务实例弥补挂掉的服务,还得调整流量,把去往宕机服务的流量切换到新实例。这的恢复因为跨系统所以不能仅用异常实现,但道理一样。


但光靠重启就“正确”了?若服务是完全无状态,问题不大。但若有状态,部分用户数据可能被执行一半的请求搞乱。因此重启要留意先“恢复数据到合法状态”。这又回到你要知道咋样才是“正确”的做法。只依靠简单的语法功能不能无脑解决这事。


5 提升维度



  • 一个工作线程的“外部容器“是管理工作线程的“master”

  • 一个网络请求的“外部容器”是一个Web Server

  • 一个用户进程的“外部容器”是[操作系统]

  • Erlang把这种supervisor-worker的机制融入到语言的设计


Web程序很大程度能把异常抛给顶层,是因为:



  • 请求来自前端,对因为用户请求有误(数据合法性、权限、用户上下文状态)造成的问题,最终基本只能告诉用户。因此抛异常到一个集中处理错误的地方,把异常转换为某个业务错误码的方法,合理

  • 后端服务一般无状态。这也是软件系统设计的一般原则。无状态才意味着可随时随地安心重启。用户数据不会因为因为下一条而会出问题

  • 后端对数据的修改依赖DB的事务。因此一个改一半的、没提交的事务不会造成副作用。


但这3条件并非总成立。总能遇到:



  • 一些处理逻辑并非无状态

  • 也并非所有的数据修改都能用一个事务保护


尤其要注意对[微服务]的调用,对内存状态的修改是没有事务保护的,一不留神就会搞乱用户数据。比如下面代码段


6 难以排查的代码段


 try {
int res1 = doStep1();
this.status1 += res1;
int res2 = doStep2();
this.status2 += res2;
// 抛个异常
int res3 = doStep3();
this.status3 = status1 + status2 + res3;
} catch ( ...) {
// ...
}

先假设status1、status2、status3之间需维护某种不变的约束(invariant)。然后执行这段代码时,如在doStep3抛异常,下面对status3的赋值就不会执行。这时如不能将status1、status2的修改rollback,就会造成数据违反约束的问题。


而程序员很难发现这个数据被改坏了。坏数据还可能导致其他依赖这数据的代码逻辑出错(如原本应该给积分的,却没给)。而这种错误一般很难排查,从大量数据里找到不正确的那一小段何其困难。


7 更难搞定的代码段


// controller
void controllerMethod(/* 参数 */) {
try {
return svc.doWorkAndGetResult(/* 参数 */);
} catch (Exception e) {
return ErrorJsonObject.of(e);
}
}

// svc
void doWorkAndGetResult(/* some params*/) {
int res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
return SomeResult.of(this.status1, this.status2, this.status3);
}

难搞在于你写的时候可能以为doStep1~3这种东西即使抛异常也能被Controller里的catch。


在svc这层是不用处理任何异常,因此不写[try……catch]天经地义。但实际上doStep1、doStep2、doStep3任何一个抛异常都会造成svc的数据状态不一致。甚至你一开始都可以通过文档或其他沟通确定doStep1、doStep2、doStep3一开始都是必然可成功,不会抛错的,因此你写的代码一开始是对的。


但你可能无法控制他们的实现(如他们是另外一个团队开发的[jar]提供的),而他们的实现可能会改成抛错。你的代码可能在完全不自知情况下从“不会出问题”变成“可能出问题”…… 更可怕的类似代码不能正确工作:


void doWorkAndGetResult(/* some params*/) {
try {
int res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
return SomeResult.of(this.status1, this.status2, this.status3);
} catch (Exception e) {
// do rollback
}
}

你以为这样就会处理好数据rollback,甚至觉得这种代码优雅。但实际上doStep1~3每一个地方抛错,rollback的代码都不一样。


得这么写


void doWorkAndGetResult(/* some params*/) {
int res1, res2, res3;
try {
res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
} catch (Exception e) {
throw e;
}

try {
res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
} catch (Exception e) {
// rollback status1
this.status1 -= res1;
throw e;
}

try {
res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
} catch (Exception e) {
// rollback status1 & status2
this.status1 -= res1;
this.status2 -= res2;
throw e;
}
}

这才是得到正确结果的代码,在任何地方出错都能维护数据一致性。优雅吗?


看起来很丑。比go的if err != nil还丑。但要在正确性和优雅性取舍,肯定毫不犹豫选前者。作为程序员不能直接认为抛异常可解决任何问题,须学会写出有正确逻辑的程序,哪怕很难且看起来丑。


为达成高正确性,你不能总将自己大部分注意力放在“一切都OK的流程“,而把错误看作是可随便应付了事的工作或简单的相信exception可自动搞定一切。


8 总结


对错误处理要有敬畏之心:



  • Java因为Checked Exception设计问题不得不避免使用

  • 而Uncaughted Exception实在弱鸡,不能给程序员提供更好帮助


因此,程序员在每次抛错或者处理错误的时候都要三省吾身:



  • 这个错误的处理是正确吗?

  • 会让用户看到啥?

  • 会不会搞乱数据?


不要以为自己抛个异常就完事了。在[编译器]不能帮上太多忙时,好好写UT来保护代码可怜的正确性。


请多写正确的代码


作者:JavaEdge在掘金
来源:juejin.cn/post/7280050832949968954
收起阅读 »

当别人因为React、Vue吵起来时,我们应该做什么

web
大家好,我卡颂。 最近尤大的一个推文引起了不小热议,大概经过是: 有人在推上夸React文档写的好,把可能的坑点都列出来 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户 尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关...
继续阅读 »

大家好,我卡颂。


最近尤大的一个推文引起了不小热议,大概经过是:



  1. 有人在推上夸React文档写的好,把可能的坑点都列出来

  2. 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户



尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关注度自然不低。


再加上国内前端自媒体的一波引导发酵,比如知乎下这个话题相关的问题中的措辞是怒喷,懂得都懂。



在这样氛围与二手信源的影响下,会给人一种大佬都亲手下场撕了的感觉,自然会引来ReactVue各自拥趸的一番激烈讨论。


年年都是一样的套路,毫无新意......


面对这样的争吵,我们应该做什么呢?


首先,回到源头本身,尤大diss的有道理么?有。


React的心智负担重么?确实重。比如useEffec这个API,你能想象文档中一个章节居然有6篇文章是教你如何正确使用useEffec的么?



造成这一现象的原因有很多,比如:



  1. Hooks的实现原理使得必须显式声明依赖

  2. 显式声明依赖无法覆盖useEffect所有场景,为此专门提出一个叫Effect Event的概念,以及一个对应的新hook —— useEffectEvent

  3. useEffect承载了太多功能,比如未来Offscreen的显隐回调(类似Vue中的Keep-Alive)也是通过useEffect实现


当我们继续往前回溯,Hooks必须显式声明依赖React更新机制决定的,而React更新机制又是React实现原理的核心。


本质来说,还是React既往的成功、庞大的社区生态让他积重难返,无法从底层重写。


这是历史必然的进程,如果Vue所有新特性都在Vue2基础上迭代(而不是完全重写的Vue3),我相信也是同样的局面。


所以,当前React的迭代方向是 —— 支持上层框架(比如Next.jsRemix),寄希望于靠这些框架的封装能力弥补React自身心智负担重的缺点。这个策略显然也是成功的。


回到这次争吵本身,尤大不知道React文档为什么要花大篇幅帮开发者避坑(以及背后反映的积重难返)么?他显然是知道的。


他如此回复是因为他所处的位置是框架作者React是他的竞争对手。设想一下,如果你的竞争对手在一些方面确实不如你,但他的用户对此的反应不是“太难用了,我要换个好用的”,而是“一定是我用的姿势不对,你快出个文档好好教教我”


面对这样的用户,换谁都得有一肚子牢骚吧~



让我们再把视角转到React的用户(也就是我们这些普通开发者)上。我们为什么选择React呢?


可能有些人是处于喜好。但大部分开发者之所以用React,完全是因为公司要求用React


React的公司多,招React的岗位多,自然选择React的开发者就多了。


那么为什么用React的公司多呢?这显然是多年前React在先发优势、社区生态两场战役取胜后得到的结果。


总结


所以,我们需要尊重两个事实:



  1. React心智负担重是事实

  2. React的公司多也是事实


两者并不矛盾,他们都是历史进程的产物。


VueReact之间的讨论,即使是从技术层面出发,最后也容易陷入“React心智负担这么重,你们还甘之如饴,你们React党是不是傻”这样的争吵中。


这显然就是忽略了历史的进程。


正确的应对方式是多关心关心自己未来的发展:



  • 如果我的重心在海外,那应该给Next.js更多关注。海外远程团队不是Next就是Nest

  • 如果我的重心在国内,国内流量都被小程序分割了。一个长远的增长点应该是鸿蒙


作者:魔术师卡颂
来源:juejin.cn/post/7321589055883427855
收起阅读 »

线程数突增!领导说再这么写就gc掉我

线程数突增!领导说再这么写就gc掉我 前言 大家好,我是魔性的茶叶,今天给大家分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。 今天上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现我们的一个应用,应用内线程数超过900条,接...
继续阅读 »

线程数突增!领导说再这么写就gc掉我


前言


大家好,我是魔性的茶叶,今天给大家分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。


今天上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现我们的一个应用,应用内线程数超过900条,接近1000条,但是cpu并没有高涨,内存也不算高峰。但是敏锐的我还是立刻意识到这个应用有不妥,因为线程数太多了,不符合我们一个正常健康的应用数量。熟练的打出cpu dump观察,首先看线程组名的概览。


image-20230112200957387


从线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程池导致的问题。我们先排查为何这个线程池中会有600+的线程处于waiting状态并且无法释放,记接下来我们找几条线程的堆栈观察具体堆栈:


image-20230112201456234


这个堆栈看上去很合理,线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了waiting状态,等待着有任务后被唤醒。


看上去不只一个线程池,并且这些线程池的名字居然是一样的,我大胆的猜测一下,是不断的创建同样的线程池,但是线程池无法被回收导致的线程数,所以接下来我们要分析两个问题,首先这个线程池在代码里是哪个线程池,第二这个线程池是怎么被创建的?为啥释放不了?


我在idea搜索new ThreadPoolExecutor()得到的结果是这样的:


image-20230112202915219


于是我陷入懵逼的状态,难道还有其他骚操作?


正在这时,一位不知名的郑网友发来一张截图:


image-20230112203527173


好家伙!竟然是用new FixedTreadPool()整出来的。难怪我完全搜不到,因为用的new FixedTreadPool(),所以线程池中的线程名是默认的pool(又多了一个不使用Executors来创建线程池的理由)。


然后我迫不及die的打开代码,试图找到罪魁祸首,结果发现作者居然是我自己。这是另一个惊喜,惊吓的惊。


冷静下来后我梳理一遍代码,这个接口是我两年前写的,主要是功能是统计用户的钱包每个月的流水,因为担心统计比较慢,所以使用了线程池,做了批量的处理,没想到居然导致了线程数过高,虽然没有导致事故,但是确实是潜在的隐患,现在没出事不代表以后不会出事。


去掉多余业务逻辑,我简单的还原一个代码给大家看,还原现场:


private static void threadDontGcDemo(){
      ExecutorService executorService = Executors.newFixedThreadPool(10);
      executorService.submit(() -> {
           System.out.println("111");
       });
   }

那么为啥线程池里面的线程和线程池都没释放呢


难道是因为没有调用shutdown?我大概能理解我两年前当时为啥不调用shutdown,是因为当初我觉得接口跑完,方法走到结束,理论上栈帧出栈,局部变量应该都销毁了,按理说executorService这个变量应该直接GG了,那么按理说我是不用调用shutdown方法的。


我简单的跑了个demo,循环的去new线程池,不调用shutdown方法,看看线程池能不能被回收


image-20230113142322106


打开java visual vm查看实时线程:


image-20230113142304644


可以看到线程数和线程池都一直在增加,但是一直没有被回收,确实符合发生的问题状况,那么假如我在方法结束前调用shutdown方法呢,会不会回收线程池和线程呢?


简单写个demo结合jvisualvm验证下:


image-20230113142902514


image-20230113142915722


结果是线程和线程池都被回收了。也就是说,执行了shutdown的线程池最后会回收线程池和线程对象


我们知道,一个对象能不能回收,是看它到gc root之间有没有可达路径,线程池不能回收说明到达线程池的gc root还是有可达路径的。这里讲个冷知识,这里的线程池的gc root是线程,具体的gc路径是thread->workers->线程池。线程对象是线程池的gc root,假如线程对象能被gc,那么线程池对象肯定也能被gc掉(因为线程池对象已经没有到gc root的可达路径了)。


那么现在问题就转为线程对象是在什么时候gc


郑网友给了一个粗浅但是合理的解释,线程对象肯定不是在运行中的时候被回收的,因为jvm肯定不可能去回收一条在运行中的线程,至少runnalbe状态的线程jvm不可能去回收。


在stackoverflow上我找到了更准确的答案:stackoverflow.com/questions/2…


image-20230113152802164


A running thread is considered a so called garbage collection root and is one of those things keeping stuff from being garbage collected。


这句话的意思是,一条正在运行的线程是gc root,注意,是正在运行,这个正在运行我先透露下,即使是waiting状态,也算正在运行。这个回答的整体的意思是,运行的线程是gc root,但是非运行的线程不是gc root(可以被回收)。


现在比较清楚了,线程池和线程被回收的关键就在于线程能不能被回收,那么回到原来的起点,为何调用线程池的shutdown方法能够导致线程和线程池被回收呢?难道是shutdown方法把线程变成了非运行状态吗


talk is cheap,show me the code


我们直接看看线程池的shutdown方法的源码


public void shutdown() {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           checkShutdownAccess();
           advanceRunState(SHUTDOWN);
           interruptIdleWorkers();
           onShutdown(); // hook for ScheduledThreadPoolExecutor
      } finally {
           mainLock.unlock();
      }
       tryTerminate();
}

private void interruptIdleWorkers() {
       interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           for (Worker w : workers) {
               Thread t = w.thread;
               if (!t.isInterrupted() && w.tryLock()) {
                   try {
                       t.interrupt();
                  } catch (SecurityException ignore) {
                  } finally {
                       w.unlock();
                  }
              }
               if (onlyOne)
                   break;
          }
      } finally {
           mainLock.unlock();
      }
}

我们从interruptIdleWorkers方法入手,这方法看上去最可疑,看到interruptIdleWorkers方法,这个方法里面主要就做了一件事,遍历当前线程池中的线程,并且调用线程的interrupt()方法,通知线程中断,也就是说shutdown方法只是去遍历所有线程池中的线程,然后通知线程中断。所以我们需要了解线程池里的线程是怎么处理中断的通知的。


我们点开worker对象,这个worker对象是线程池中实际运行的线程,所以我们直接看worker的run方法,中断通知肯定是在里面被处理了


//WOrker的run方法里面直接调用的是这个方法
final void runWorker(Worker w) {
       Thread wt = Thread.currentThread();
       Runnable task = w.firstTask;
       w.firstTask = null;
       w.unlock(); // allow interrupts
       boolean completedAbruptly = true;
       try {
           while (task != null || (task = getTask()) != null) {
               w.lock();
               // If pool is stopping, ensure thread is interrupted;
               // if not, ensure thread is not interrupted. This
               // requires a recheck in second case to deal with
               // shutdownNow race while clearing interrupt
               if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                     runStateAtLeast(ctl.get(), STOP))) &&
                   !wt.isInterrupted())
                   wt.interrupt();
               try {
                   beforeExecute(wt, task);
                   Throwable thrown = null;
                   try {
                       task.run();
                  } catch (RuntimeException x) {
                       thrown = x; throw x;
                  } catch (Error x) {
                       thrown = x; throw x;
                  } catch (Throwable x) {
                       thrown = x; throw new Error(x);
                  } finally {
                       afterExecute(task, thrown);
                  }
              } finally {
                   task = null;
                   w.completedTasks++;
                   w.unlock();
              }
          }
           completedAbruptly = false;
      } finally {
           processWorkerExit(w, completedAbruptly);
      }
}



这个runwoker属于是线程池的核心方法了,相当的有意思,线程池能不断运作的原理就是这里,我们一点点看。


首先最外层用一个while循环套住,然后不断的调用gettask()方法不断从队列中取任务,假如拿不到任务或者任务执行发生异常(抛出异常了)那就属于异常情况,直接将completedAbruptly 设置为true,并且进入异常的processWorkerExit流程。


我们看看gettask()方法,了解下啥时候可能会抛出异常:


private Runnable getTask() {
       boolean timedOut = false; // Did the last poll() time out?

       for (;;) {
           int c = ctl.get();
           int rs = runStateOf(c);

           // Check if queue empty only if necessary.
           if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
               decrementWorkerCount();
               return null;
          }

           int wc = workerCountOf(c);

           // Are workers subject to culling?
           boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

           if ((wc > maximumPoolSize || (timed && timedOut))
               && (wc > 1 || workQueue.isEmpty())) {
               if (compareAndDecrementWorkerCount(c))
                   return null;
               continue;
          }

           try {
               Runnable r = timed ?
                   workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                   workQueue.take();
               if (r != null)
                   return r;
               timedOut = true;
          } catch (InterruptedException retry) {
               timedOut = false;
          }
      }
  }

这样很清楚了,抛去前面的大部分代码不看,这句代码解释了gettask的作用:


Runnable r = timed ?
  workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  workQueue.take()

gettask就是从工作队列中取任务,但是前面还有个timed,这个timed的语义是这样的:如果allowCoreThreadTimeOut参数为true(一般为false)或者当前工作线程数超过核心线程数,那么使用队列的poll方法取任务,反之使用take方法。这两个方法不是重点,重点是poll方法和take方法都会让当前线程进入time_waiting或者waiting状态。而当线程处于在等待状态的时候,我们调用线程的interrupt方法,毫无疑问会使线程当场抛出异常


也就是说线程池的shutdownnow方法调用interruptIdleWorkers去对线程对象interrupt是为了让处于waiting或者是time_waiting的线程抛出异常


那么线程池是在哪里处理这个异常的呢?我们看runwoker中的调用的processWorkerExit方法,说实话这个方法看着就像处理抛出异常的方法:


private void processWorkerExit(Worker w, boolean completedAbruptly) {
       if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
           decrementWorkerCount();

       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           completedTaskCount += w.completedTasks;
           workers.remove(w);
      } finally {
           mainLock.unlock();
      }

       tryTerminate();

       int c = ctl.get();
       if (runStateLessThan(c, STOP)) {
           if (!completedAbruptly) {
               int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
               if (min == 0 && ! workQueue.isEmpty())
                   min = 1;
               if (workerCountOf(c) >= min)
                   return; // replacement not needed
          }
           addWorker(null, false);
      }
}

我们可以看到,在这个方法里有一个很明显的 workers.remove(w)方法,也就是在这里,这个w的变量,被移出了workers这个集合,导致worker对象不能到达gc root,于是workder对象顺理成章的变成了一个垃圾对象,被回收掉了。然后等到worker中所有的worker都被移出works后,并且当前请求线程也完成后,线程池对象也成为了一个孤儿对象,没办法到达gc root,于是线程池对象也被gc掉了。


写了挺长的篇幅,我小结一下:



  1. 线程池调用shutdownnow方法是为了调用worker对象的interrupt方法,来打断那些沉睡中的线程(waiting或者time_waiting状态),使其抛出异常

  2. 线程池会把抛出异常的worker对象从workers集合中移除引用,此时被移除的worker对象因为没有到达gc root的路径已经可以被gc掉了

  3. 等到workers对象空了,并且当前tomcat线程也结束,此时线程池对象也可以被gc掉,整个线程池对象成功释放


最后总结:


如果只是在局部方法中使用线程池,线程池对象不是bean的情况时,记得要合理的使用shutdown或者shutdownnow方法来释放线程和线程池对象,如果不使用,会造成线程池和线程对象的堆积。


作者:魔性的茶叶
来源:juejin.cn/post/7197424371991855159
收起阅读 »

面试官:手写一个“发布-订阅模式”

web
发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。 DOM事件 document.body.addEventListener...
继续阅读 »

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。


DOM事件


document.body.addEventListener('click',function(){

alert(绑定1);

},false);

document.body.click(); //模拟点击

document.body.addEventListener('click',function(){

alert(绑定2);

},false);

document.body.addEventListener('click',function(){

alert(绑定3);

},false);

document.body.click(); //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。


自定义事件


① 确定发布者。(例如售票处)


② 添加缓存列表,便于通知订阅者。(预订车票列表)


③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。


另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) { //增加订阅者
this.clientList.push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
for(var i = 0, fn; fn = this.clientList[i++];){
fn.apply(this, arguments); //arguments 是发布消息时带上的参数
}
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){ //小刚订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});


ticketOffice.on(function(time, path){ //小强订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。


// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){ //小强订阅消息
console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。


小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

ticketOffice.remove = function (key, fn) {
let fns = this.clientList[key]
if (!fns) return false

if (!fn) {
fns && (fns.length = 0)
} else {
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1)
}
})
}
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式


但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = new Set();
}
this.clientList[key].add(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料


《JavaScript 设计模式与开发实践》


作者:dudulala
来源:juejin.cn/post/7320075000702533671
收起阅读 »

思辨:移动开发的未来在哪?

前段时间在知乎看到关于移动开发未来的问题,就尝试回答了一下,也触发了我对移动开发未来的思考。移动开发未来怎么样? - 知乎 http://www.zhihu.com/question/61…什么是移动开发?我们口中说的移动开发是什么,从广义和狭义的角...
继续阅读 »

前段时间在知乎看到关于移动开发未来的问题,就尝试回答了一下,也触发了我对移动开发未来的思考。

image.png

移动开发未来怎么样? - 知乎 http://www.zhihu.com/question/61…

什么是移动开发?

我们口中说的移动开发是什么,从广义和狭义的角度分别来看下:

从广义角度来看,移动开发是指为移动设备(如智能手机、平板电脑等)创建软件、应用程序和服务的过程。这包括了为各种移动操作系统(如 iOS、Android 和 Windows Phone)设计、开发、测试和发布应用程序。移动开发旨在为用户提供高质量的、功能丰富的移动体验,以满足其日常需求和娱乐需求。广义上的移动开发可以包括原生应用程序开发、跨平台应用程序开发、移动网页应用程序开发,以及相关的后端服务和API开发等。

从狭义角度来看,移动开发通常指开发针对特定移动操作系统的应用程序,如 iOS 和 Android。这些应用程序通常使用特定于平台的编程语言(如 Swift 或 Kotlin)开发,并利用该平台的特性和功能。狭义的移动开发关注于为特定平台提供最佳的性能、用户体验和原生功能集成。这种开发方法需要对目标平台的技术细节和设计原则有深入了解,以便充分发挥其潜力。

这段内容是我问GPT4的生成的,针对移动开发的定义基本准确。移动开发涉及的的细分领域有非常多,比如:

  • 混合开发和跨平台框架
  • Framework和Kernel
  • 逆向安全
  • 音视频
  • 移动Web
  • 嵌入式

大家可以对照着自己的岗位要求,给自己所涉及的技术领域归个类,分析下市场的需求如何。

简单回顾一下

移动开发辉煌的十年也是移动互联网快速发展的十年,我还记得2015当年o2o百“团”大战的时候,各种创业公司,各行各业,只要你懂点移动开发就能找到不错的开发工作,那时候移动开发的培训机构也如雨后春笋一般诞生,培训个几个月可能就能获得offer。现在的滴滴、美团都是当年烧钱大户,通过庞大的资本,持续打补贴战,最后才活下来,也是寥寥无几的几家独角兽创业公司。通过烧钱的方式毕竟是不可持续的,经营一家公司必须有足够竞争力的产品和可持续的商业模式,当年的泡沫被刺破之后,你才知道什么公司在裸奔,回过头想想现在还有多少家公司能幸存至今呢。

回到今年2023年,疫情三年让整个中国经济都是千疮百孔,不知道大家是否发现这些年基本没有什么新的独角兽出现了,基本上10年前的成立的公司,跑出来成为新的大厂的我们手指头能数得过来,比如我们熟知字节跳动,因为抖音短视频,直接在短视频领域突围成为了打破了老牌大厂腾讯在社交垄断下的新的巨头,成为新的BAT中的B。

另外附上一张2022年中国互联网综合实力企业排名:

image.png

大家是否发现自己手机上常用的App基本集中在这些我们耳熟能详的企业里面。其他的App要么访问量很少,要么永远消失在你的应用列表当中,可叹可惜。所以App的消亡带来的就是移动端的夕阳西下,除了大厂和中厂还有移动客户端的需求,但也是一坑难求,对求职者的要求基本上是要中高级别的,初级的刚毕业的基本上很难拿到offer。

这里我从自己的理解分析了移动开发目前的情况,从历史进程和供需关系,我们可以看到移动开发的求职环境已经大不如前,所以如果还想进入互联网从事移动开发就要结合自身情况去考虑,或许你需要积累得更多才能在残酷的求职环境中脱颖而出拿到心仪的offer。

个人的一些思考

先说说我个人的情况,自从14年毕业之后一直从事移动开发,岗位是Android工程师,基本也算是赶上了移动互联网发展的快车道,求职路上基本上也没遇到什么坎坷,当时也算是比较幸运毕业一年半左右,以社招的身份面试进入到了腾讯,然后就一直待到现在。期间做过研发工具,比如Bugly Crash上报,应用更新和热更新;做过教育产品,比如腾讯课堂;目前投身于金融科技领域,做创新硬件上层应用相关的开发。主要的技术栈还是Android、Java/Kotlin,目前因为业务的需要,技术栈就开始涉足Linux嵌入式和C/C++。其实我个人也一直求变,不管是业务方向还是技术,危机感也在驱使着我去在专业领域获得更多的成长。作为技术人只能保持饥饿感,不停的更新自己的知识体系。

针对移动开发的未来,我个人还是保持谨慎乐观的态度的,虽然当下的求职环境发生了变化,但存量市场需求依然有很多机会,以下是我认为值得我们去关注的技术方向,但不作为任何求职建议:

  1. AIGC+移动端

2023年的AIGC的火热空前绝后,它带来的影响是非常深远的,甚至能够变革整个互联网行业,很多产品可能将会以新的思路去重构和延伸,这里面就会产生相应的在移动端和AIGC结合相关产品和业务,公司层面也会有相应的投入意愿,这也许会给我们带来新的机会。

  1. 元宇宙:VR/AR/XR

元宇宙虽然被炒概念,一直不温不火的,但这里面涉及的技术是比较前沿的,在游戏领域跟元宇宙的结合,如果能找到愿意投入企业,未尝不是一个不错的方向。

  1. IoT物联网

万物互联方向,比如智能家居,智能创新硬件产品,类似小米IoT相关的产品,智能手环、扫地机器人等等。这里面也有庞大的市场需求,另外软硬件结合对开发人员要求更高,更接近底层。

  1. 新能源车载系统

新能源车的其中一个核心就是智能中控,比如特斯拉的中控系统是Linux,比亚迪还有蔚小理和大多数造车新势力用的是Android系统,这里面也有很多车载系统应用的需求,也是很多人都求职热门方向。

  1. 音视频技术领域

当下流行的短视频,涉及到的核心就是音视频技术,有这方面的技术积累的同学应该也能获得不错的发展机会,而且这方面的人才相对而言比较稀缺。

  1. 跨平台技术

从企业降本的角度,未来可能会更倾向招聘懂跨平台开发的,希望能统一技术栈能够实现多端发布的能力。比如Flutter、React Native、UniApp等。

  1. 鸿蒙OS应用开发

国产替代是个很深远的话题,卡脖子问题现在越演越烈,从软件产业我们跟漂亮国还存在很多差距,我们能够正视这些差距并且迎头突围是一个非常值得敬佩和骄傲的事情。鸿蒙OS有望成为第一个完全去Android化的操作系统,Mate60系列手机产品我认为是一个标志性里程碑,我们不谈什么遥遥领先,我相信华为一定会越来越好,鸿蒙OS应用开发也是我觉得有较好前景的方向。

当然还有很多其他技术方向无法一一列举,我个人觉得一专多能可能是未来我们更应追求的目标,仅靠写几个UI页面就能打天下的时代已经不再适用了,想让自己有足够的竞争力,就必须要多涉猎各种技术,打通任督二脉,很多时候单一视角很难获得创新,只有多维度思考才有可能让自己突围。

最后

作为互联网从业人员,保持一定的危机感是必要的,另外多扩展自己的视野,除了专注于本身的专业领域,也要多关注技术趋势的变化,很多时候技术的价值是需要匹配业务的。移动开发有没有未来这个问题可以转化为:我们自己当前要做哪些选择,才能让自己拥有更多的未来。最后跟大家分享一句话作为结尾:

个人努力固然重要,也要考虑历史进程。


作者:巫山老妖
来源:juejin.cn/post/7292347319431790607
收起阅读 »

勇闯体制内00后:丢自己的脸,要领导的命

最近刷到00后在体制内上班,差点没给我人笑没。别的00后忙着整顿职场,而体制内的00后都在出尽洋相。众所周知,体制内工作跟普通的职场不大一样。工作内容比较接地气,工作能力比较看交际。这批进了体制内的00后,看着是端起了铁饭碗,实际上端碗的手,没有一天不在抖。三...
继续阅读 »


最近刷到00后在体制内上班,差点没给我人笑没。


别的00后忙着整顿职场,而体制内的00后都在出尽洋相。


众所周知,体制内工作跟普通的职场不大一样。


工作内容比较接地气,工作能力比较看交际。


这批进了体制内的00后,看着是端起了铁饭碗,实际上端碗的手,没有一天不在抖。


三天一大错两天一小错,每天都在勇闯体制的边缘嘚瑟。

00后前脚上岸,后脚怀疑自己该不会是个原装的傻子。


基本特征是“沉默寡言、体弱多病、孤僻内向且不善交际。”


干活主打的就是一个迷茫,说话前不着村后不着店儿。


领导上一秒说完,他下一秒就忘。



开会的时候他人五人六,把小本摆出来咔咔往上写;


完事说看看你整的会议纪要,他开始阿巴阿巴不敢吱声。



表面奋笔疾书,实际上00后的小本打开是这样的:



还有这样的:



主打一个领导说前门楼子,他在那扯胯骨轴子。


初进了体制内的00后,觉得自己像个傻子,又不只是个傻子;


还有点像腼腆的哑巴,和想努力但就是做不好的笨蛋。



周一上班碰到领导,迎面忘了领导叫啥;


轻则直接摆摆手一个“嗨”,重则四目相对毫无反应,原地飘过去了。


隔天见面敢打招呼,但又记错了领导的职称,张嘴直接给人降了半个级别。


刚进单位时头像还是这种不被重用的傻大姐人设:



后经领导点拨,改成静待花开风格,就算是当笨蛋,不如当个看起来沉稳点的笨蛋:



但头像的玄学作用,在体制内明显受限。


由于太没有眼力价,还不会跟人打交道,部分00后的愚蠢人设还是焊死在身上了。



典型的就是让众多00后显眼包,又爱又恨的酒局修罗场。


爱的是可以把酒局当搂席,恨的是自己酒量真不咋地。


凡开席必须把好吃的端自己跟前,不爱吃的放在领导前面。


前不久有个新闻,某国企会议结束有个晚宴,领导让新来的00后安排;


00后风风火火把晚宴安排到了自己爱的重庆火锅饭店,领导的心情跟怨种特效完美搭配上。


说到酒局,体制里的00后,酒量也不是不行,而是压根就没有,吃饭基本就得坐小孩那桌。



周围人花式敬酒,他低头扒拉米饭。


周围人换上白酒,他拿白水伪装白酒,还拿的热水,满桌子就他一个酒杯里冒热气。


不会喝酒也没事,问题是打进门他一屁股坐到主位上;


别人敬酒寒暄讨好领导,中间还隔着个他这个怨种。



还有的朋友更离谱,领导敬酒他端起饮料,领导低头他把酒往领导鞋上倒。


领导端酒杯致辞,他端个空杯还来回晃荡。


老员工以为这莫不是传说中的00后来整顿职场了?


00后听完把心一沉,想着自己哪有那个心眼子,不过是没有眼力价罢了。


喝酒他不行,但干饭他第一名;


别人吃完半天了,起身前还拍拍他问问吃饱了吗?不行咱就打包。



有的饭局结束了当事人还纳闷,为啥整个晚上自己的饮料杯从来没空过?


后来破案了,副局长全程给他倒了五次豆奶,同事直呼还得是00后牛逼。


不论e人还是i人,进了体制内一律按i人处理。


00后睡前都在给自己洗脑,告诉自己明天会更好;



隔天见了人还是想躲,结果不是去食堂碰到主任,就是去洗澡碰到书记,命运就是如此眷顾,想逃都逃不掉。


为了避免跟领导有眼神接触,有的路过局长办公室,浑身僵硬眼神失焦不敢歪头;


有的在乡镇工作,地方不大还研究躲避路线,真是外向不了一点。


还有的被点名参加合唱比赛,主任问她“你想参加吗?”


她直接用问题回答问题,打了主任一个措手不及“我想参加吗?”


心里其实想的是让我登台献艺,比杀了我还难受。


你永远想象不到00后在体制内是怎么活下来的,毕竟他们这新脑子完全不够用。



不是走廊里走路给老领导一杵子的,就是抬手倒水把领导茶杯盖给碰掉。


职场的打工人上班如上坟,最多就是钱难挣屎难吃;


而体制内的00后,月薪1800拿命往里搭;


每天都觉得脑子有点痒痒的,期待着赶紧长个新脑子吧。

为啥体制内的00后总担心自己闯大祸?


上岸来之不易,两眼一睁,担心竞争。


在某书上搜索体制内的00后,个个都像热锅上的蚂蚁,整天琢磨如何快速适应工作环境。


有人担心单位不让染发,也不能美甲;


有人上网寻求穿搭秘籍,准备放弃穿衣自由,走向局里局气;



有人担心听不明白话,转而研究领导语言习惯和工作中的花式暗语;


也有人按时按点写自己的闯祸日记,有的按天写,有的是周记;

重点记录每日上班遭遇,研究今天丢脸有没有比昨天少那么一点。



偶尔发现隔壁同事姐姐也会把茶水浇到副书记身上,顿时就变得很安心了,看到大家都和自己一样呆呆的,真好啊。


从小科员要掌握的办公字体,到对付老油条停止自我内耗。


再到上传下达“文经我手必熟悉”,硬着头皮记住各种可以提高效率的铁律。


日常给自己加油打气,隔天出了问题立马又泄气。



想起白天犯蠢想到失眠,打开手机又不小心点开高情商问题:


“和领导打羽毛球你赢了,领导说,我老了不中用了,你如何回答。”


看到坐标山东的网友油腻且不失风趣的回答,00后默默赞叹不愧是命里带编,下一秒赶紧把模版熟记于心。


这届进了体制内的00后,一边担心闯祸丢脸,一边又害怕自己过于被边缘;


上班前还以为体制内工作会很清闲,做好了泡壶茶水坐一天的心理预期,结果真上了班发现并不是这么简单。


基层工作跑断腿,总结汇报想流泪,更别说复杂的人际关系,直接让人身心俱疲。


甭管是体制内还是职场里,对刚工作的新人来说,总是最胆战心惊的那个。


不过话又说回来,涉世未深才有资格闯祸;


也只有清澈愚蠢的年轻人,对待人生第一份工作还肯花心思,瞎琢磨。


总有一天,爱闯祸的笨蛋,会变成真正的“大人”。


把头发梳成帅气的模样,在各种场合里游刃有余。


做着曾经最不擅长的事儿,也是最不喜欢的事儿。


或早或晚,都会长大。


眼下不如放轻松,“无论你多早迎接这清晨,在路上,都会有人在。”



作者:英才校园招聘
来源:mp.weixin.qq.com/s/UIKucQDDD5CAglfuTIzqlA

收起阅读 »

史上最全的2024罗振宇跨年演讲思维导图

作者:PMO前沿来源:mp.weixin.qq.com/s/hgB7g_F6ArrPgnAkg0tuyA















作者:PMO前沿
来源:mp.weixin.qq.com/s/hgB7g_F6ArrPgnAkg0tuyA

普通的文本输入框无法实现文字高亮?试试这个highlightInput吧!

web
背景 前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到...
继续阅读 »

背景


前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到:没办法,这是老板提的需求,你下去研究研究吧。行吧,老板发话说啥也没用,开干吧!


实现思路


实现标红就需要给文字加上html标签和样式,但是输入框会将html都转为字符串,既然输入框无法实现,那么我们换一种思路,通过div代替输入框来显示输入的文本,那我们是不是就可以实现文本标红了?话不多说,直接上代码(文章结尾会附上demo):


<div class="main">
<div id="shadowInput" class="highlight-shadow-input"></div>
<textarea
id="textarea"
cols="30"
rows="10"
class="highlight-input"
>
</textarea>
</div>

.main {
position: relative;
}
.highlight-shadow-input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 8px;
border: 1px;
box-sizing: border-box;
font-size: 12px;
font-family: monospace;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
}
.highlight-input {
position: relative;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
background: rgba(0, 0, 0, 0);
-webkit-text-fill-color: transparent;
z-index: 999;
word-break: break-all;
}

实现这个功能的精髓就在于将输入框的背景和输入的文字设置为透明,然后将其层级设置在div之上,这样用户既可以在输入框中输入,而输入的文字又不会展示出来,然后将输入的文本处理后渲染到div上。


const textarea = document.getElementById("textarea");
const shadowInput = document.getElementById("shadowInput");
const sensitive = ["敏感词", "禁用词"];
textarea.oninput = (e) => {
let value = e.target.value;
sensitive.forEach((word) => {
value = value.replaceAll(
word,
`<span style="color:#e52e2e">${word}</span>`
).replaceAll("\n", "<br>");;
});
shadowInput.innerHTML = value;
};

监听输入框oninput事件,用replaceAll匹配到敏感词并转为html后渲染到shadowInput上。此外,我们还需要对输入框的滚动进行监听,因为shadowInput是固定高度的,如果用户输入的文本出现滚动条,则需要让shadowInput也滚动到对应的位置


<div><div id="shadowInput" class="highlight-shadow-input"></div></div>

textarea.onscroll = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};
// 此处输入时也需要同步是因为输入触底换行时,div的高度不会自动滚动
textarea.onkeydown = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};

最终实现效果:


至此一个简单的文本输入框实现文字高亮的功能就完成了,上述代码只是简单示例,在实际业务场景中还需要考虑xss注入、特殊字符处理、特殊字符高亮等等复杂问题。


总结


这篇文章主要给遇到有类似业务需求的同学一个参考,以及激发大家的灵感,用这种方法是不是还可以实现一些简单的富文本功能呢?例如文字加删除线、文字斜体加粗等等。有想法或有问题的小伙伴可以在评论区留言一起探讨哦!


demo



作者:宇智波一打七
来源:juejin.cn/post/7295169886177918985
收起阅读 »

多行标签超出展开折叠功能

web
前言  记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。  今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,...
继续阅读 »

前言


 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。


出处


 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:


多行标签案列图(pdd/txxt)


其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。


功能实现


我们先看实现的效果图,然后再分析如何实现,效果图如下:



【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)


 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。


第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行


具体实现上代码:


<div class="list-con list-con-1">
<div class="label">人工智能div>
<div class="label">人工智能与应用div>
<div class="label">行业分析与市场数据div>
<div class="label">标签标签标签标签标签标签标签标签div>
<div class="label">标签div>
<div class="label">啊啊啊div>
<div class="label">宝宝贝贝div>
<div class="label">微信div>
<div class="label">吧啊啊div>
<div class="label">哦哦哦哦哦哦哦哦div>
div>
<div class="expand expand-1">展开 ∨div>



解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。


第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。


具体上代码:




解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。


【样式二】:展开隐藏按钮和标签同级(效果图样式二)


 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。


方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。


具体实现上代码:


<div id="app3">
<div class="list-con list-con-3" :class="{'list-expand': isExpand}">
<div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}div>
<div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}div>
div>
div>


<script>
const { createApp, nextTick } = Vue
createApp({
props: {
maxLine: {
type: Number,
default: 2
}
},
data () {
return {
labelArr: [],
isExpand: false,
showExpandBtn: false,
labelLength: 0,
hideLength: 0
}
},
mounted () {
const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']

this.labelArr = labels
this.labelLength = labels.length
nextTick(() => {
this.init()
})
},
methods: {
init () {
const listCon = document.querySelector('.list-con-3')
const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
const expandBtn = listCon.querySelector('.expand-btn')

let labelIndex = 0 // 渲染到第几个
const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
for(let i = 0; i < labels.length; i++) {
const _top = labels[i].getBoundingClientRect().top
if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
this.showExpandBtn = true
console.log('第几个索引标签停止', i)
labelIndex = i
break
} else {
this.showExpandBtn = false
}
}
if (!this.showExpandBtn) {
return
}
nextTick(() => {
const listConRect = listCon.getBoundingClientRect()
const expandBtn = listCon.querySelector('.expand-btn')
const expandBtnWidth = expandBtn.getBoundingClientRect().width
const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
for (let i = labelIndex -1; i >= 0; i--) {
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
this.hideLength = i + 1
this.labelLength = this.hideLength
break
}
}
})
},
changeExpand () {
this.isExpand = !this.isExpand
console.log(this.labelLength)
if (this.isExpand) {
this.labelLength = this.labelArr.length
} else {
this.labelLength = this.hideLength
}
}
}
}).mount('#app3')
script>


解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。


方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。


直接上代码:




这里也无需多做解释了,直接看代码即可。


结尾


上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库。




作者:Liben
来源:juejin.cn/post/7251394142683742269
收起阅读 »

村超,淄博烧烤,哈尔滨,本质都在做情绪价值这门生意

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。 还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。 如果叫南方小豆角,南方小冬瓜,我相信大多数人就会急了,因为...
继续阅读 »

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。


还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。


如果叫南方小豆角南方小冬瓜,我相信大多数人就会急了,因为这两个词听起来都不可爱,还会有地域歧视的意思。


虽然都是蔬菜,但是南方小土豆就不一样,虽然大家都知道里面有南方人矮的意思,但是大多数人不反感,因为现在很多家长都会叫自己的孩子小土豆,养一个宠物也有叫土豆,所以说土豆,其实带有可爱的意思。


所以就南方小土豆这个称呼,就带动了很大的流量,就打破了这么多年来对东北的刻板映像。


但是就因为一个称呼就能带动那么多的人赶往东北吗?就为了去听一句南方小土豆吗?


我想显然不是,也并不是谁都愿意听的,也有一些游客去以后说反感这个称呼。


那么究其原因,其实本质还是在做情绪价值这门生意。


而情绪价值的背后是什么?


是长久积攒的急需释放的情绪和看透生活后的无能为力。


怎么理解呢?


我发现一个现象,包括我本人也是这样。


在疫情之前,我在很多地方看到有人卖唱,下面的人基本都在听,跟着唱的人不多。


但是近年来,只要有卖唱歌手的地方,基本上大家都会蜂拥上去吼上几句,有甚者直接流着眼泪大声歌唱。


因为大家都从之前的内卷中失望了,生活很大程度上并不会因为努力而发生变化,就不太和自己较真了,从而将重心移到了生活中来。


而市井,热闹就是生活的最真实写照,不需要花多少钱就能释放情绪,收获快乐。


贵州村超淄博烧烤,再到哈尔滨,都能得到很好的体现。


我们还发现一个问题,这几个城市都是比较落后的,其实并没有什么吸引人的地方,景区,经济,文化其实都没有什么突出的地方。


但是有一个特点,那就是消费便宜


你想一下,如果要在香港,澳门,上海这些城市打造这样的活动,做这样的城市IP,现实吗?


我想不现实,因为消费太高,大多数人承受不起。


你想,开一个好一点的酒店都要不少钱,还有吃和也是很贵,加上处于经济高速发展的地区,本地人比较少。


所以情感并不浓,消费并不低。


可能会像村超那样直接免费接游客去自己家里住宿,游客离开后还深情相拥吗?


可能会像哈尔滨这样一到位就一口一个南方小土豆,然后排着队接送吗?


我想基本上不会。


因为多数人的消费能力是有限的,肯定会选择热闹,便宜且好玩的地方。


所以这样的火热IP,大概只会出现在消费相对来说比较低的城市。


所以,现在的生意大多都围绕着提供情绪价值这个方向出发。


前段时间火爆全网的海底捞科目三,虽然海底捞的价格高了一点,但是在你累了,失落了的时候,突然在你面前响起了生日快乐歌,随后又跳起了科目三。


在冰冷的建筑下瞬间热泪盈眶,脑海中蹦出一句:人间值得


要知道,在外面花几百块钱是买不到这种服务的。


而这些服务本质就是提供情绪价值。


特别是在今天这样的现状下,大家兜里都没几个子,生活也都不太如意,所以这时候情绪价值对于一个人来说尤为重要。


所以以后这样火爆的城市IP还会持续出现,这是毋庸置疑的!


作者:苏格拉的底牌
来源:juejin.cn/post/7321943946309124136
收起阅读 »

IT外传:老郑和老钱

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。 他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。 老郑刚做了一个智能识别的AI项目。这个项目快...
继续阅读 »

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。


他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。


老郑刚做了一个智能识别的AI项目。这个项目快提交测试时,老郑还在别的项目组干活。这个智能识别干了没几周,又被调走干别的事情了。


即便如此,老郑开发的智能识别项目,在识别率和识别能力上,在整个业内也是领先。这得益于他长久以来的经验积累以及巧妙的算法设计。


但是,大家却不这么看。即便业内识别率只能到50%,老郑做出了85%,但是大家也会盯着那无法识别的15%。


刚刚测试工程师就在整理那15%,他们要把这15%里的100%全部汇报给领导:你看,这些一塌糊涂,这种情况能不能用?请领导定夺。


言外之意:上线后有问题跟我们无关,风险已经全部抛出来了。


其实……老郑也习惯了。


上次另一个识别项目,老郑把准确率从刚提交测试时的50%提高到97%。而测试工程师在给领导汇报时,开头说识别率很差,只有50%。领导很忙,听完这个结论就走了。剩下一些中层,又听了40分钟他是如何通过围追堵截的测试方法一步步将识别率提高的。


大家都没错,也都很辛苦,这些老郑并不关心。老郑的心情很差,因为有一个同事离职了。


老郑的这个同事,技术能力很强,强到一个人可以顶一个团队。


在体力劳动上,一个人顶一队人可能很难。比如普通的劳工一次扛3袋水泥,有个大力士可以一次扛30袋,而且速度还很快。这很罕见。


但是在科技或者软件行业,这种情况很普遍,但是很少有人被认可。


老郑的这个同事老钱,就是这样一个人。


老钱设计的代码,简洁纯净,他擅长使用中间件和编程语言的特性,代替大量的代码逻辑。其代码格式规范、文档注释清晰。也正是得益于简洁和巧妙,他的效率还很高。同样的功能,其他同事需要3个人写两周,老钱1个人一周就能搞定。


对于速度,这顶多算是多扛几袋水泥,在软件行业,这点贡献算不了什么巨大改善。


关键是老钱写的代码很少出问题。程序员写的代码出了问题(bug),会引发后续一堆人的投入。测试同事会测试,前后端要排查、修改,产品经理要做决策,市场要应对用户的投诉。这bug就像是喂鸽子时的粮食,往东边撒一把儿,一群鸽子蜂拥而至,往西边撒一把儿,西边又密密麻麻。


老钱设计的代码,很少有bug,这一点就避免了三四个部门、10多个人白忙活几周的情况。这个隐形成本的节省是巨大的。


除了代码的设计,老钱还有个优势,那就是有远见和守原则。


项目开发中,会面临很多的技术选型和方案选定。大到使用什么框架,小到一个参数选用何种数据类型。


很多的时候,在进行技术讨论时,老钱会对其他人的方案提出建议。比如一个参数不要传来传去,就要以一方为准,否则会出问题。


其他人一般会有自己的理由,比如,传来传去不用给数据库增加额外字段。但是,往往过不了多久,问题就出现了,传着传着就传乱了。于是大家又聚到一起调试:你传给我啥,我收到啥,又传给了他啥……在广场的空地撒了一把粮食,远处的鸽群放弃了旧粮,急忙朝这里飞奔而来。


老郑和老钱也合作过一个项目。老钱曾经建议老郑不要那么搞,否则会出问题。老郑没听,结果后面确实走不通了,最终老郑还是改了回去,那一个星期白干了。


然而,老钱却离职了。


他的离职半含被迫,半含自愿。首先,经济形势不好,导致公司出现了拖延工资的情况。


其次,在拖延工资的背景下,不同员工的发放情况参差不齐。有的人拖延5个月,有的人拖延3个月,有的人正常发放。而老钱的工资,拖得最久,向领导反馈也没有结果。


领导说,公司现在的回款出现延迟,前年该给的钱,去年才刚刚给。不过,每个月也都是有回款的。这点回款,首先要保证公司的日常运作,其次保证新员工,再次保证有贡献的员工。其他人,只能克服一下了。


好像意图比较明显。老钱也是个智商和情商都在线的人。


老钱提出了离职,领导立马批准,限两周内办好手续。老钱说,我原本打算能有1个月缓冲期的。


其实,老钱和老郑早就被投诉多次了。


甚至连人事都看不惯他们:凭什么这俩人工资比我们高,还不拼命加班?我不平衡……不是,他们没有大局意识!


而同为技术人员,兄弟部门的意见就更多了:再复制一份接口,随便改个字段都不配合!群里半夜@你的消息,为什么没有及时回复?我们换个对接人问你问题,你不培训他,让他看接口文档是什么意思!


老钱和老郑有个观念:用工作时间的高效率工作,换取下班后的安心休息。但是,似乎大家并不都想这样,往往是白天静悄悄,只要一下班,工作群里立刻变得活跃起来。


老郑和老钱有时候就讨论,你说领导是否知道一个员工的真实水平或者价值。


比如,A员工干的活能顶B、C、D,3个人。或者,他手下有个员工的水平在整个行业中处于上层还是下层。


“好像不知道!”


老钱说,交接工作期间,有个问题找来,领导还问他:你也参与这个项目了?


其实就上个月,老钱还在这个项目上干了半个月,日报、周报、早会、周会地定期汇报。显然领导没有关注过,因为没有发生过大的问题。一贯零失误的工作,让老钱变成了一个小透明,而且还是经常提意见的那种问题员工。


一线的员工常常辗转于项目代码之中。领导们则开会,看书,制定考核KPI指标。长期脱离一线阵地,会让领导从业务管理上浮到任务管理(从如何带领人解决问题,变成安排人去解决问题)。


软件其实是一门工程学,而非玄学。


软件工程的最佳的实践是多进行工程管理,而非思想管理。


现实很多情况都是反过来的,大家都很重视思想管理。


如果把完成一个软件项目比作攻下一座城池。那么,策略要比士气更重要。


讲策略的将军会规划好完整的攻城计划。首先,他会盘点自己有多少人员和器械,会分析对方城池有几个薄弱点。然后,部署几个分队:哪个队伍扛着云梯往城墙上驾,哪个队伍推着木车从东门撞击。其实,队伍主力要从北门水路强攻。等到把敌方守卫都引到东门时,以山坡黑烟为号,北门发起进攻。最终全面进攻,一举取得胜利。


类比到项目开发中,其实就是各个工种的配合,结构的定义和数据的流转。安排好整个项目每个端口,从上游拿到什么数据,做怎样的处理,然后给下游如何提供。最终,定好流程和时间节点,一气呵成。肯定没法想得完全周到,不过即便有问题,也都是局部问题,整体还是丝滑的。


4cb98f20-a6d4-4872-98a5-51113d85858a.jpeg


讲士气的将军则不然,他不考虑每个环节,或者技术更新太快,他已经不擅长每个环节了。他的主要精力是给士兵做思想工作。他告诫士兵们,我们又要攻打一座城池了,大家要有大局意识,不想当将军的士兵不是好士兵,士兵就是要解决问题的,不是提出问题的。他不关注粮草,不关注器械,不关注目标城池的特征,主要强调大家一定要攻下城池,这是所有人的目标和责任。然后,一声令下,众士兵蜂拥上前,去哪儿的都有。


最后没有攻下城池。将军要求手下将领做复盘,开会讨论为什么没有成功。然后,再次鼓舞大家要有建功立业的雄心。而手下的将领回去也纷纷效仿,告诉士兵们,一定要有建功立业的雄心壮志,遇到问题要解决问题而非吐槽问题,人人都是主人翁,没有粮草你要想办法去搞些粮草。第二次,士兵们又向敌方发起总攻……


这不仅仅体现在软件行业,其他行业也一样,正如一些专家、教授频频发出雷人的言论。


在国内,大家都有上级崇拜。针对如上言论,一般会有人怼你:能当领导的人,必然有过人之处,否则为什么不是你当领导?


其实这句话也没错,还真不是一个把产品做得越好就能生存得越好的环境。


老郑不知道老钱以后会不会改变,正如他不知道自己还能坚持多久。


老钱离职前,曾经问过老郑:老郑,你说那帮“埋头苦干”的年轻人,是以前的我们呢?还是我们的以后呢?


作者:TF男孩
来源:juejin.cn/post/7322356470253731859
收起阅读 »

MyBatis实战指南(二):工作原理与基础使用详解

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。一、MyBatis的工作原理1.1 MyBatis的工作原理工作原理图示:1、读取...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。

一、MyBatis的工作原理

1.1 MyBatis的工作原理

工作原理图示:
Description

1、读取MyBatis配置文件

mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。

2、加载映射文件(SQL映射文件,一般是XXXMapper.xml)

该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。

XXXMapper.xml可以在mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂

通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

4、创建会话对象

由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

5、Executor执行器

MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

6、MappedStatement对象

在 Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

7、输入参数映射

输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

8、输出结果映射

输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

1.2 MyBatis架构

Description

API接口层

提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。

1)使用传统的MyBatis提供的API

这是传统的传递Statement Id和查询参数给SqlSession对象,使用SqlSession对象完成和数据库的交互;
Description
MyBatis提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis自身配置信息的维护操作。
示例:

SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);

上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。

2)使用Mapper接口

MyBatis将配置文件中的每一个<mapper>节点抽象为一个Mapper接口,而这个接口中声明的方法和跟<mapper>节点中的<select|update|delete|insert>节点项对应,

即<select|update|delete|insert>节点的id值为Mapper接口中的方法名称,parameterType值表示Mapper对应方法的入参类型,而resultMap值则对应了Mapper接口表示的返回值类型或者返回结果集的元素类型。

示例:

SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}

根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。
Description

使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject)或者SqlSession.update(“statementId”,parameterObject)等等来实现对数据库的操作。

MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。

数据处理层

负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

1)参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用OGNL表达式来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
Description
参数映射指的是对于Java数据类型和JDBC数据类型之间的转换,这里包括两个过程:

  • 查询阶段,我们要将java类型的数据,转换成JDBC类型的数据,通过preparedStatement.setXXX()来设值;

  • 另一个就是对ResultSet查询结果集的JdbcType 数据转换成Java数据类型。

2)SQL语句的执行以及封装查询结果集成List< E>

动态SQL语句生成之后,MyBatis将执行SQL语句并将可能返回的结果集转换成List<E> 。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

基础支撑层

负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis层次结构

Description

1.3 Executor执行器

Executor的类别

Mybatis有三种基本的Executor执行器:SimpleExecutor、ReuseExecutor和BatchExecutor。

Description

1、SimpleExecutor

每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor

执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor

执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Executor的配置

指定Executor方式有两种:

1、在配置文件中指定

<settings>
<setting name="defaultExecutorType" value="BATCH" />
</settings>

2、在代码中指定

在获取SqlSession时设置,需要注意的时是,如果选择的是批量执行器时,需要手工提交事务(默认不传参就是SimpleExecutor)。

示例:

// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();

1.4 Mybatis是否支持延迟加载

延迟加载是什么

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

假如Clazz 类中有子对象HeadTeacher。两者的关系:

public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}

是否查出关联对象的示例:

@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}

延迟加载的设置

在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false

1)延迟加载的全局设置

延迟加载默认是关闭的。如果需要打开,需要在mybatis-config.xml中修改:

<settings>

<setting name="lazyLoadingEnabled" value="true" />

<setting name="aggressiveLazyLoading" value="false"/>
</settings>

比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。

  • 侵入式延迟加载:指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。

  • 深度延迟加载: 指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。

2)延迟加载的局部设置

如果设置了全局加载,但是希望在某一个sql语句查询的时候不适用延时策略,可以配置局部的加载策略。

示例:

 <association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>

etchType值有2种,

  • eager:立即加载;

  • lazy:延迟加载。

由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。

延迟加载的原理

MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。

如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。

二、MyBatis基础使用示例

1、添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

2、创建实体类

假设我们有一个用户表(user),我们可以创建一个对应的实体类User:

public class User {
private int id;
private String name;
private int age;
// getter和setter方法省略
}

3、创建映射文件UserMapper.xml

在MyBatis的映射文件中,我们需要定义一个与实体类对应的接口。例如,我们可以创建一个名为UserMapper的接口:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

4、创建接口UserMapper.java

接下来,我们需要创建一个与映射文件对应的接口。例如,我们可以创建一个名为UserMapper的接口:

package com.example;

import com.example.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
}

5、使用MyBatis进行数据库操作

最后,我们可以在业务代码中使用MyBatis进行数据库操作。例如,我们可以在一个名为UserService的类中调用UserMapper接口的方法:

public class UserService {
public User getUserById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(id);
sqlSession.close();
return user;
}
}

总结:

MyBatis是一个非常强大的持久层框架,它可以帮助我们简化数据库操作,提高开发效率。在实际开发中,我们还可以使用MyBatis进行更复杂的数据库操作,如插入、更新、删除等。希望这篇文章能帮助你更好地理解和使用MyBatis。

收起阅读 »

2024律师课程推荐:iCourt律师执行实务集训营(赠《执行实务大礼包》)

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?从数据来看,执行业务一定是其中之一。在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;根据...
继续阅读 »

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。


如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?


从数据来看,执行业务一定是其中之一。


在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;


根据中国执行信息公开网数据,截止到 12 月 28 日,公布的失信被执行人高达 8,618,870 位;


根据最高法公布的数据显示,得益于全国法院持续推进执行难综合治理、源头治理,仅 2023 年上半年,我国执行到位金额高达 1.2 万亿元,同比增长 23.03% ......



在业内,执行案件是公认的数量多、案件标的大、个案收费高。同时,作为诉讼案件的“最后一公里”,执行能够将司法裁判真正的“落到实处”,维护客户合法利益,实现律师职业价值。因此,一直以来,都有大量律师对执行业务“跃跃欲试”。


但,为什么现实中并没有那么多律师真正从事执行业务?


蓝海是真,“执行难”也是真:线索难找、到位率低、执行周期长、执行程序复杂多变......执行背后的难题让人望而却步。


啃下执行这块“硬骨头”,就能开拓业务范围,拓宽职业路径,在广阔的蓝海中寻找发展新机遇。


基于此,3 月 2 日 - 3 月 3 日,「执行实务集训营 07 期」将在古都西安与各位校友相见。2 天一夜的课程,将对执行实务的疑难问题进行思路点拨和深入解析,以“两个拳头” + “四大利刃”全方位切入执行案件,系统梳理相关法规案例,详细剖析执行专业知识和实务要点。


老赖隐匿财产,当事人无计可施,有哪些途径可以挖掘被执行人财产线索?

处置财产,如何形成更专业化、标准化、流程化的执行案件办理流程?

“借名买房”、“股权代持”等疑难案件该如何拆解破局、一击即中?


经过了 6 期课程的升级迭代和精心打磨,「执行实务集训营 07 期」将直击痛点,打通诉讼最后一公里。


同时,该集训营也将结合刚刚审议通过、即将在 2024 年 7 月 1 日起施行的最新修订的《公司法》内容,对执行相关的新变化、新趋势、新动向进行深入解读和思路点拨。


课程大纲


(一)查找执行财产的三大视角与方法


本次课程将会详细剖析十多种常见与非常见财产类型,并从执行法院、执行律师和当事人三个视角出发,分享查询被执行人财产的途径和方法。


(二)处置被执行人财产的标准化流程


本次课程将通过细致讲解,帮助大家形成自己、团队的执行案件办理流程,以便更好地展现代理执行业务的专业化、标准化、流程化。


(三)执行异议之诉案件的破局之道


本次课程会对案外人执行异议之诉、申请执行人执行异议之诉、追加股东为被执行人异议之诉、执行分配方案异议之诉四个板块进行逐一详细解读。此外,本次课程还将对“借名买房”能否排除强制执行、“股权代持”的隐名股东能否排除强制执行等关联问题进行拆解式分享。


(四)破解执行难的“两个拳头”与“四大利刃”


推进执行案件,常规做法是“两个拳头”:一拳打向的是被执行人财产;另一拳则打向的是被执行人。对于执行案件,除了以上两种常规打法,本次课程还提炼出了破解执行难的“四大利刃”。这些“利刃”并非会用在每一个执行案件中,但却可以成为某个具体执行案件中的“大杀器”,在关键时刻真正做到执行无阻,使命必达。



课程安排




课程讲师




往期现场:


(课程现场)


在两天一夜的课程中,除了干货满满、深入浅出的内容讲解外,校友们还将通过紧张刺激的小组比赛将课程内容串联起来,达到“融会贯通”的效果。


为了团队的荣誉,各个小组直接“卷起来了”,最终的作业展示环节简直“神仙打架”、惊喜连连。经过系统的学习和模拟实践,校友们也对破解执行难题,办理执行案件有了全新的思路和理解。


(比赛环节现场)


北京、武汉、广州...... 2023 年,校友们与「执行实务集训营」共同度过了 6 期的时光。每一期校友们都满载而归、直言不虚此行。该课程也是 2023 年 iCourt 线下集训营参与人数最多、最火爆的课程之一。

(学员合影)


课程资料


除精彩内容外,现在报名集训营,更有「执行实务干货大礼包」全部送送送!


• 1 节线上课程,业务品牌双管齐下

校友报名后均可获得《执行律师如何打造个人品牌》录播课程,方便大家随时随地进行学习提升。课程围绕写作与讲课,这两个法律人必备的技能,结合实务案例,帮助执行律师打造专业品牌,赋能业务开拓。


• 2 套项目模板,标准化办案新思路

随课程赠送《执行与执行异议》、《执行》两套项目模板,涵盖丰富的任务与任务附件,帮助校友规范办案流程,建立团队知识宝库。


• 4 本实务指引,配套学习事半功倍

《执行实务操作指引》( 4.0 版)、《执行实务 108 问》、《执行一本通法律法规汇编》( 2023 版)、《律师代理执行案件 168 个执行步骤》。4 本执行办案实务操作指引,从法律规范、常见问题出发,帮助校友解决服务中的文本困扰。


• 多份文书模板,高效应对执行难题

我们汇总了办案过程中常用的变更申请执行人、查封申请书、到期债务通知书模板等等,希望能够帮助校友提升执行办案效率,高效破解执行难题。

执行虽难,但抽丝剥茧,便能破解难题,实现职业突破。在「执行实务集训营」07 期,拥有丰富执行实务经验的韩锦超老师将带领大家探讨疑难案例,学习方法,启发思路。在前六期的基础上,iCourt 课程中心也对课程内容、课程环节、课程资料、课程体验等环节进行了全面系统的升级,结合最新法律动态,带给大家焕然一新的体验。

机遇往往与挑战并存,机遇也往往留给做足了准备的人。

眼前,是一片可待征服的蓝海。跨过这座大山,是属于我们的广阔的征途。2024 年春节“节后第一课”,「执行实务集训营」将带领大家破解执行难题,挑战业务蓝海。

突破执业困境,把握发展机遇。3 月 2 日 - 3 月 3 日,我们西安见~

了解课程详情:iCourt集训营

原文链接:https://www.icourt.cc/prac-article/728.html

收起阅读 »

探索发展,融合共生|惠州OpenHarmony城市大会圆满举行

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简...
继续阅读 »

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简称“OpenHarmony”)超高清专委会承办,惠州市电子信息产业协会协办。

(图片:惠州OpenHarmony城市大会现场)

广东省政务数据管理局局领导、一级调研员姚进,广东省工业和信息化厅信息化与软件服务业处副处长陈古典,惠州市政府副秘书长程坤,惠州市工业和信息化局局长廖巍,惠州市政务服务数据管理局局长杨伟斌,仲恺高新区管委会副主任、潼湖生态智慧区党工委副书记、管委会主任汤俊,广东九联科技股份有限公司董事长詹启军,OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见,鸿蒙生态服务有限公司总经理杜金彪,中国信通院泰尔终端实验室副主任果敢,开放原子开源基金会教育培训部部长王岩广,京东方高级副总裁荆林峰,OpenHarmony生态伙伴企业代表,惠州市OpenHarmony潜在政企客户,超高清领域企业代表,专家学者,高校代表及研究机构代表等300余人出席了本次大会。大会旨在分享及探索OpenHarmony生态发展路径,着力搭建地方产业及OpenHarmony生态领域的交流合作平台,助推OpenHarmony技术创新和生态繁荣。

广东省政务服务数据管理局局领导姚进为大会在开场致辞中强调,广东作为OpenHarmony的发源地,相关工作起步早、优势大,其在开源软件贡献、人才培养、生态应用和政策支持等方面的OpenHarmony生态体系建设上已经位居全国前列。特别指出,惠州在OpenHarmony建设方面走在全省前列,本次大会在惠州市的召开,不仅为广东省乃至全国的OpenHarmony系统发展提供了宝贵的交流平台,也为其他城市发展OpenHarmony提供了典范。未来,广东省政务服务数据管理局将继续通过成立产业协会、制定标准规范以及开展应用示范等措施,全力推动OpenHarmony产业的繁荣壮大。

(图:广东省政务服务数据管理局局领导姚进)

惠州市人民政府副秘书长程坤在致辞中强调,OpenHarmony作为构建智能终端操作系统的重要基础能力平台和安全底座,对于打造自主可控的国产操作系统和构建新的智能终端产业生态具有深远意义。惠州凭借其坚实的电子信息产业基础,积极把握OpenHarmony发展的战略机遇,并已取得了一系列实践成果。例如,支持和指导华为终端、九联科技等企业牵头成立了“OpenHarmony超高清专委会”;印发实施了加快OpenHarmony生态产业发展行动计划,成效初显。程坤表示,惠州将继续强化对OpenHarmony产业的宣传引导、主体培育、示范应用、环境优化和政策谋划等工作,为OpenHarmony生态在惠落地发展提供有力支持。

(图:惠州市人民政府副秘书长程坤)

OpenHarmony打造下一代智能终端操作系统根社区

OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见分享了OpenHarmony项目及生态进展。目前已有超过220家伙伴加入OpenHarmony生态共建,累计落地超过460款软硬件产品通过OpenHarmony兼容性测评,覆盖金融、教育、交通、医疗、公共安全、智慧城市等多个行业。随着行业标准规范的推进,OpenHarmony已成为各行各业的优选。

同时,深圳、福州、惠州、北京、重庆、南京等城市率先出台相关产业政策支持OpenHarmony发展,从供给侧和需求侧推动生态建设。为培育与产业发展契合的创新型人才,壮大OpenHarmony生态新兴力量,生态伙伴联合高校共同打造人才培养闭环生态链。柳晓见表示,希望OpenHarmony生态伙伴们聚力前行,期待更多伙伴加入OpenHarmony共建中,共筑下一代智能终端操作系统根社区。

(图:OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见)

OpenHarmony生态服务助力生态商业成功

鸿蒙生态服务公司总经理杜金彪分享了在各地政府大力支持下出台的相关OpenHarmony产业扶持政策,并介绍鸿蒙生态服务公司围绕“行业集采、政府集采、政府奖补、联盟标准”四大商业机遇,开展测评认证、市场拓展、商机对接、活动承办等服务,协助生态伙伴实现降本增效。通过搭建相关联盟平台推动产业标准建设,促进良性竞争。同时也期待更多伙伴加入,共同加速OpenHarmony生态产业健康发展。

(图:鸿蒙生态服务公司总经理杜金彪)

两位特邀嘉宾专题分享完毕后,在参会嘉宾的共同见证下,举办了OpenHarmony超高清专委会揭牌仪式、OpenHarmony人才培养战略行动启动仪式。本地政府、院校、企业等多方力量表示,将全力以赴支持OpenHarmony的发展,共同推动其在更多领域的应用和普及。此次相关仪式的成功举行,为惠州市OpenHarmony生态的发展开启了新的篇章。

OpenHarmony超高清专委会揭牌仪式

2022年,在开放原子开源基金会的指导下,在惠州市政府、惠州仲恺高新区管委会的支持下,由华为终端有限公司、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司牵头推动成立了“OpenHarmony超高清专委会”(OpenHarmony生态委员会下属负责推进OpenHarmony在超高清领域生态发展的唯一主体)。专委会将面向超高清全行业,推动 OpenHarmony的研发、装机、应用等工作,打造OpenHarmony超高清生态圈。

(图:OpenHarmony超高清专委会揭牌仪式合影)

OpenHarmony人才培养战略行动启动仪式

由开放原子开源基金会、惠州学院、惠州市技师学院、惠州城市职业学院、惠州经济职业技术学院、惠州工程职业学院、深圳技术大学、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司(OpenHarmony超高清专委会秘书处单位)共同开启OpenHarmony人才培养战略行动启动仪式,承诺合力开展OpenHarmony新型操作系统的产业及技术培训等相应的活动,为企业培养更多的实用性、复合型的软件人才。仪式结束后,大会邀请了OpenHarmony生态专家及伙伴进行主题演讲。

(图:OpenHarmony人才培养战略行动启动仪式合影)

OpenHarmony的基本设计理念和关键技术最新进展

OpenHarmony项目管理委员会(PMC)主席任革林分享了OpenHarmony的基本设计理念和关键技术最新进展,并介绍了基于OpenHarmony使能的金融、智慧教室、智能化公路建设等一系列行业场景创新解决方案。他表示,OpenHarmony技术底座能力越来越成熟,一个面向全场景、全连接、全智能时代OS的阶段目标已经达成,既可以满足生态伙伴开发丰富多彩的创新设备,也可以满足应用开发者开发复杂大型应用和极致高性能应用。

(图:OpenHarmony项目管理委员会主席任革林)

深开鸿基于OpenHarmony高校人才培养实践

深圳开鸿数字产业发展有限公司(简称“深开鸿”)OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会(PMC)成员巴延兴分享了深开鸿基于OpenHarmony高校人才培养实践。作为一家立足于OpenHarmony生态,为行业数字化、智慧化提供基础软件的生态平台型企业,深开鸿积极响应国家切实推动“深化产教融合、校企人才共育”的号召,与北京理工大学、哈尔滨工业大学、东南大学、深圳信息职业技术学院、深圳技术大学等众多高校开展合作,计划在未来几年内培养大量优秀的OpenHarmony技术人才。

(图:深开鸿OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会成员巴延兴)

基于OpenHarmony的全场景解决方案实践

江苏润开鸿数字科技有限公司(简称“润开鸿”)生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安分享了润开鸿基于OpenHarmony面向行业的HiHopeOS发行版使能千行百业的场景创新解决方案及商业落地实践,并重点介绍了基于龙芯+OpenHarmony的适配进展及工业场景探索。

(图:润开鸿生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安)

新功能,新形态,新场景

京东方科技集团股份有限公司视觉艺术事业部总经理吴坚围绕京东方集团业务新功能、新形态、新场景展开讨论,探讨京东方屏之物联与OpenHarmony结合所带来的创新与变革。

(图:京东方视觉艺术事业部总经理吴坚)

基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索

广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀分享了基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索。围绕OpenHarmony和星闪的特性在智慧空间场景中应用以及场景中特性闭环,低时延、近场联接联动让分布式空间场景无处不在。

(图:广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀)

在盛大的OpenHarmony城市大会上,我们见证了科技与生活的深度融合,感受到了开源文化与创新精神的激情碰撞。通过深入的探讨与交流,我们更加坚信,OpenHarmony所倡导的开放、共享、互联的理念,将引领我们迈向一个更加智能、高效、和谐的美好未来,共同开创一个万物智联、万物互融的新时代。让我们携手并进,以“探索发展,融合共生”的精神,为开源生态的繁荣与辉煌而努力!

(图:惠州OpenHarmony城市大会现场)

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony开源三年多来,社区快速成长,版本已迭代到OpenHarmony 4.1beta1 Release,超过 6700 名共建者、70家共建单位,贡献代码行数超过1亿行。截至2023年底,OpenHarmony开源社区已有250多家生态伙伴加入,OpenHarmony项目捐赠人达35家,通过OpenHarmony兼容性评测的伙伴达170余个,累计落地230余款商用设备,涵盖金融、教育、智能家居、交通、数字政府业、医疗等各个领域,OpenHarmony已成为下一代智能终端操作系统根社区。

收起阅读 »

2024年,现在去开发一款App需要投入多少资金?

前言 本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~ 选择大于努力 原生开发的现状 先来看下目前原生开发存在的问题以及国内的现状。 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开...
继续阅读 »

1.jpg


前言


本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~


选择大于努力


原生开发的现状


先来看下目前原生开发存在的问题以及国内的现状。



  1. 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开发人员。这也是导致很多企业不愿意选择原生开发的重要原因之一(Android、iOS)。

  2. 原生应用开发成本高,开发周期慢,如果不招人(提高用人成本)较难跟上市场节奏。

  3. 原生应用推广成本也高(与小程序相对比)。

  4. 对于我们开发人员来说,需要掌握一种语言Java或者kotlin,ios开发需要oc或者swift,难度相对于跨平台学习成本较高。


企业对于技术上的选择,目前需要的就是能节省成本、同时开发效率高的,跨平台已经是大势所趋


国内特有的小程序


小程序的优势很大,自从小程序出来后,蚕食了很大一部分手机应用的市场份额。


小程序相较于原生应用具有显著的优势,其中最大的优势在于成本的降低。相比于开发原生应用,小程序的开发成本更低,同时也更加省时省力。此外,小程序还能够充分利用微信等大型平台的庞大用户流量入口,从而降低企业在推广方面的成本。这种降低成本的好处不仅体现在企业推广方面,也使得用户在使用小程序时所需投入的成本降低(不用去下载app,也不用再走一遍注册流程)。


正因为如此,许多中小企业不愿再开发原生应用,或者说,“没能力”开发原生应用。更倾向于选择小程序。小程序的低成本开发和推广,使得中小企业能够以更少的投入获得更大的回报。此外,小程序还可以借助微信等平台的用户基础,更容易吸引和留住用户。


如何计算一款App开发的成本


本文选择跨平台技术作为开发成本的参考。那在跨平台中,从Statista(一家全球领先的统计数据平台和市场研究公司)收集的数据来看,很明显 Flutter 继续脱颖而出,成为跨平台框架中的首选。截止2023年6月,Flutter占跨平台份额的46%,在跨平台中占比第一,React Native占32%,居第二。



长话短说,开发 Flutter 应用程序的相关费用基本在 10,000 到 450,000 人民币之间,甚至更高。在这本文中,我们将分解各种成本因素,去计算 Flutter 应用程序开发成本。


那如何去计算一款应用的开发成本呢,开发一款应用一共分一下几个阶段,每个阶段都会影响总成本。



  • 第一阶段:需求分析与规划

  • 第二阶段:原型设计(UI/UX)

  • 第三阶段:正式编码(此时应用已经基本成型)

  • 第四阶段:测试

  • 第五阶段:部署与维护


Flutter技术在国内多用于外包项目,所以通常三四五(有些项目会包含二)的几个阶段都由开发者全权负责完成,应用的总体成本通常是通过将总工时乘以开发者的小时费来估算的。


影响一款应用开发成本的因素


不同的应用开发的成本可能会因多种因素而有很大差异,每个因素都会直接影响项目的预算和时间表。最终价格可能受到一系列因素的影响,例如应用程序的复杂程度、要纳入的功能总数、开发人员的每小时费以及许多其他方面。


主要的因素也是对应到开发阶段中,主要是以下这些:



  • 在需求分析时,应用程序的范围和复杂性

  • 在UI设计时,UI的动画、复杂的布局、对设计风格的要求

  • 在开发时,选择的开发方式(1.外包给自由职业者。2.外包给专业软件公司。3.自己招人干)。选择外包开发者(开发商)的地理位置,假设你在美国,找一个中国开发者,成本就会降低许多

  • 在测试时,跨平台的设备成本,功能测试的范围

  • 部署维护时,服务器的成本,bug的修复,添加新的功能


那让我们再来详细聊聊每个阶段具体要花多少费用。


需求分析设计阶段


项目的需求和范围是开发成本的主要决定因素,例如,开发一个基本的笔记应用程序比开发一个功能齐全的电商平台便宜得多。因此,在App开发的初始阶段定义项目需求和应用程序复杂性对于估算总体成本至关重要。App在刚开始需要舍弃掉一些不重要的功能。


UI设计阶段


如果有一个高质量的 UI/UX 设计,那对于App的成功是很有帮助的。但它也会影响成本,一款简单、简约的设计比具有独特图形、复杂动画动画的定制成本更低。如果需要高度定制的设计或想要实现特定的品牌元素,这将极大增加的应用程序开发成本。根据应用程序的复杂程度,设计一款完整的App平均需要 40 到 90 多个小时。设计一款App的UI,价格平均在5000-25000左右,让我们对应到每项工作中去。



  1. 前期的需求交流和沟通。此阶段涉及创建草图和线框图。所需的时间和成本取决于设计的复杂程度。创建草图和线框图可能需要 200 至 1000 的预算分配

  2. UI/UX 设计视觉效果的创建。 此阶段为整个App的内容设计,例如登录界面、注册界面等。同样,实际所需时间取决于App的复杂性。此阶段的预算范围从 5,000 到 15,000或更多

  3. logo设计。在这个阶段阶段,设计师根据之前设计的App内容和、我们的品牌配色和其他设计元素。这项共工作需要相当大的预算,大约需要 5,000 到 10,000 的预算甚至更多。当然,为了节省成本也可以放弃这一阶段,由我们自己设计


代码开发阶段


选择不同的开发人员或开发团队也会影响成本。如果选择经验丰富的专业人员团队会花费更多的前期成本,但可以带来更高的效率和更高质量的产品。如果,雇用经验不足的开发人员刚开始可能会省钱,但可能会导致开发时间更长或日后出现潜在问题。目前主流的方式为以下三种:


自由职业者(外包给程序员做私活)


这种方式可以很好的降低成本,身边也有很多朋友会接私活,确实是一个很不错的选择。但是,这种方式可能会遇到许多不确定性,例如没法按时交付。此外,如果这个项目后期需要进行维护、更新,那这个方案可能就不是最可靠选择了,因为他们可能会转移到其他项目(或者跑路),从而使持续协作变得具有挑战性。如果选择这个方案,建议是朋友推荐,或者是网上具有一定知名度的开发者。在国内,跨平台应用开发者(Flutter开发)的时薪通常在每小时150到350人民币不等。如果选择这个方式,开发成本在10000到50000之间。


外包公司


这种方法是节省开发资金而又不影响产品质量的绝佳方法,通常开发成本在50000到150000之间。如果项目需要后期的维护,迭代,那么可以优先选择这样的方式。(现在的外包公司也比较卷)


自己组团队


如果是想要真的以一种创业的方式,那么开发成本的范围是0到无上限。如果自身就是一个技术人员,那么只需要一台笔记本就可以完成对应用的开发,所花的只是时间成本。如果要招人组团队,那成本就不可估计了。


测试阶段


这部分在大多数App开发过程中,已经由开发者自己测试解决的。稍微正规些的应用可以将测试的工作外包给测试公司。成本在0~20000人民币之间。


维护与迭代


开发一款App不是短跑,而更像是一场马拉松。即使在App第一版上线后,这个旅程仍在继续。定期更新、bug修复和UI修改只是维护App的冰山一角。最好预留总成本的 15-20% 的额外费用,用来进行维护。


其他因素


——每个项目都是独特的,具体要求将决定最终成本。因此,在规划App开发预算时,必须彻底了解这些因素并加以考虑。


第三方API集成


如果项目中需要集成即时通讯等功能模块,那么第三方API集成的这部分的花销也是不可忽略。


软著申请、应用商店发布


软著申请是免费的,自行准备材料申请即可,但是通常会有2~3个月的时间,才能申请成功。如果想快速申请,可以找专门的三方申请机构,价格在500-2000左右。如果App需要上架Google Play和App Store,那么,开通Google Play 开发者账户一次性收取 25 美元费用,Apple Store 个人开发者账号每年收取 99 美元费用。此外,还会从应用内购买或订阅中扣除部分费用。申请软著和App上架的材料准备工作,通常需要10-20小时的工作。按每小时50元,此部分工作需要500-1000元的费用。


后端开发和服务器的费用


如果App只会进行一些本地操作,那么这部分的费用基本为0。如果需要后端提供服务,则需要在拿出一大笔钱进行后端的开发和服务器的购买费用。


如何降低开发成本


外包项目


这种模式允许利用全球人才库,通常以比雇用本地人才更具竞争力的价格获得服务。这点如果你在美国等发达国家可以考虑。如果在大陆,可以看看三哥他们。此外,这种方式还减少了对办公空间和设备的需求,并减少了与员工福利和津贴相关的管理费用。


明确项目要求


还是那句话,最后的成本一定与开始的需求有着很大关联。所以一定要精简需求,明确App到底要做什么。


专注于敏捷方法


如果你是个人开发者或者要带领团队开发,那一定要注重敏捷开发,确定任务优先级、经常重新评估和调整项目目标。


结论 — 关于开发一款App的成本


关于开发一款App的成本,为了让大家能更直观的感受,让我们具体数字来说明这一点。(采用Flutter跨平台)



  1. 对于简单功能的App(例如提供膳食计划App、日记App、记账App等),估计开发成本约为 10,000 — 50,000人民币之间,根据项目的复杂度来决定。

  2. 对于中等复杂度的App(例如具有即时通讯、语音通话等功能)预计成本约为 50,000 — 150,000人民币之间。

  3. 对于开发高复杂度的应用,例如抖音(简化版,真抖音现在哪个团队能从0开始做一个...),起价基本在150,000,上不封顶。


那这就是当前开发一款App的成本,以及对应的工作。


免责声明:本文中提供的数字是大致的、调研来的,可能会根据具体项目要求而有所不同!!!


作者:编程的平行世界
来源:juejin.cn/post/7312353213348347916
收起阅读 »

ThreadLocal:你不知道的优化技巧,Android开发者都在用

引言 在Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。 本文将深入探讨Andr...
继续阅读 »

引言


Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。


本文将深入探讨Android中的ThreadLocal原理及其使用技巧, 帮助你更好的理解和使用ThreadLocal


ThreadLocal的原理


public class Thread implements Runnable {

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal的原理是基于每个线程都有一个独立的ThreadLocalMap对象。ThreadLocalMap对象是一个Map,它的键是ThreadLocal对象,值是ThreadLocal对象保存的值。


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

当我们调用ThreadLocalset()方法时,会将值存储到当前线程的ThreadLocalMap对象中。当我们调用ThreadLocalget()方法时,会从当前线程的ThreadLocalMap对象中获取值。


ThreadLocal的使用


使用ThreadLocal非常简单,首先需要创建一个ThreadLocal对象,然后通过setget方法来设置和获取线程的局部变量。以下是一个简单的例子:


val threadLocal = ThreadLocal<String>()

fun setThreadName(name: String) {
threadLocal.set(name)
}

fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}

Android开发中,ThreadLocal的使用场景非常多,比如:



  • Activity中存储Fragment的状态

  • Handler中存储消息的上下文

  • RecyclerView中存储滚动位置


实际应用场景


// 在 Activity 中存储 Fragment 的状态
class MyActivity : AppCompatActivity() {

private val mFragmentState = ThreadLocal<FragmentState>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

// 获取 Fragment 的状态
val fragmentState = mFragmentState.get()
if (fragmentState == null) {
// 初始化 Fragment 的状态
fragmentState = FragmentState()
}

// 设置 Fragment 的状态
mFragmentState.set(fragmentState)

// 创建 Fragment
val fragment = MyFragment()
fragment.arguments = fragmentState.toBundle()
supportFragmentManager.beginTransaction().add(R.id.container, fragment).commit()
}

}

class FragmentState {

var name: String? = null
var age: Int? = null

fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString("name", name)
bundle.putInt("age", age)
return bundle
}

}

这段代码在Activity中使用ThreadLocal来存储Fragment的状态。当Activity第一次启动时,会初始化Fragment的状态。当Activity重新启动时,会从ThreadLocal中获取Fragment的状态,并将其传递给Fragment


注意事项



  • 内存泄漏风险:


ThreadLocal变量的生命周期与线程的生命周期是一致的。这意味着,如果一个线程一直不结束,那么它所持有的ThreadLocal变量也不会被释放。这可能会导致内存泄漏。


为了避免内存泄漏,我们应该在不再需要ThreadLocal变量时,显式地将其移除。


threadLocal.remove()


  • 不适合全局变量: ThreadLocal适用于需要在线程间传递的局部变量,但不适合作为全局变量的替代品。


优化技巧



  • 合理使用默认值: 在获取ThreadLocal值时,可以通过提供默认值来避免返回null,确保代码的健壮性。


fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}


  • 懒加载初始化: 避免在声明ThreadLocal时就初始化,可以使用initialValue方法进行懒加载,提高性能。


val threadLocal = object : ThreadLocal<String>() {
override fun initialValue(): String {
return "DefaultValue"
}
}


  • 尽量避免在ThreadLocal中保存大对象


结论


在本文中,我们介绍了ThreadLocal的原理和使用技巧,希望这些知识能够帮助你更好地理解和使用它。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7317859658285858842
收起阅读 »