注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

HarmonyOS 鸿蒙下载三方依赖 ohpm环境搭建

前言ohpm(One Hundred Percent Mermaid )是一个集成了Mermaid的命令工具,可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。本期教大家如何搭建ophm环境:一、在DevEco Studi...
继续阅读 »

前言

ohpm(One Hundred Percent Mermaid )是一个集成了Mermaid的命令工具,可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。

本期教大家如何搭建ophm环境:

一、在DevEco Studio中,将依赖放到指定的 oh-package-json5 的 dependencies 内


二、打开 Terminal 执行 :ohpm install

(1)成功会提示

(2)失败会提示
ohpm not found ! 大概意思就是找不到这个ohpm

三、解决小标题(2)的失败提示:

1、查阅我们的ohpm地址
一定要记住这个地址
Mac端找到该位置路径 点击DevEco Studio ->Preferences

2、打开终端命令行 输入:echo $SHELL 输入后 单击回车

注:如果不需要以下图文引导 请向下拉 下方有无图文引导方式


3、提示 /bin/zsh

(1)执行: vi ~/.zshrc 后点击回车


(2)进入到该页面 输入 i

(3)拷贝:export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm export PATH=${PATH}:${OHPM_HOME}/bin

中间的 xxx 输入 在标题三 1 图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可。


(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入:wq!单击回车保存


(5)输入: source ~/.zshrc;


以下是无图文引导方式:

(1)执行: vi ~/.zshrc
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm export PATH=${PATH}:${OHPM_HOME}/bin

中间的 xxx 输入 在 标题三的(1)图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可。

(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入:wq!,单击回车保存
(5)输入: source ~/.zshrc;

4、提示/bin/base

(1)执行: vi ~/.bash_profile
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm export PATH=${PATH}:${OHPM_HOME}/bin

中间的 xxx 输入 在 标题3的(1)图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存
(5)输入: source ~/.bash_profile

四、检验 ohpm环境是否配置成功

命令行输入 export 查验是否有 ohpm

第二种检验方式 输入 ohpm -v 会显示你的版本


相关文档:

收起阅读 »

Android:实现带边框的输入框

如今市面上APP的输入框可以说是千奇百怪,不搞点花样出来貌似代表格局没打开。还在使用系统自带的输入框的兄弟可以停下脚步,哥带你实现一个简易的带边框输入框。 话不多说,直接上图: 要实现这个效果,不得不再回顾下自定义View的流程,感兴趣的童鞋可以自行网上搜...
继续阅读 »

如今市面上APP的输入框可以说是千奇百怪,不搞点花样出来貌似代表格局没打开。还在使用系统自带的输入框的兄弟可以停下脚步,哥带你实现一个简易的带边框输入框。



话不多说,直接上图:
1.gif


要实现这个效果,不得不再回顾下自定义View的流程,感兴趣的童鞋可以自行网上搜索,这里只提及该效果涉及到的内容。总体实现大致流程:



  • 继承AppCompatEditText

  • 配置可定义的资源属性

  • onDraw() 方法的重写


首先还得分析:效果图中最多只能输入6个数字,需要计算出每个文字的宽高和间隙,再分别绘制文字背景和文字本身。从中我们需要提取背景颜色、高度、边距等私有属性,通过新建attrs.xml文件进行配置:


<declare-styleable name="RoundRectEditText">
<attr name="count" format="integer"/>
<attr name="itemPading" format="dimension"/>
<attr name="strokeHight" format="dimension"/>
<attr name="strokeColor" format="color"/>/>
</declare-styleable>

这样在初始化的时候即可给你默认值:


val typedArray =context.obtainStyledAttributes(it, R.styleable.RoundRectEditText)
count = typedArray.getInt(R.styleable.RoundRectEditText_count, count)
itemPading = typedArray.getDimension(R.styleable.RoundRectEditText_itemPading,0f)
strokeHight = typedArray.getDimension(R.styleable.RoundRectEditText_strokeHight,0f)
strokeColor = typedArray.getColor(R.styleable.RoundRectEditText_strokeColor,strokeColor)
typedArray.recycle()

接下来便是重头戏,如何绘制文字和背景色。思路其实很简单,通过for循环去遍历绘制每一个数字。关键点还在于去计算每个文字的位置及宽高,只要得到了位置和宽高,绘制背景和绘制文字易如反掌。


获取每个文字宽度:


strokeWith =(width.toFloat() - paddingLeft.toFloat() - paddingRight.toFloat() - (count - 1) * itemPading) / count

文字居中需要计算出对应Y值:


val fontMetrics = paint.fontMetrics
val textHeight = fontMetrics.bottom - fontMetrics.top
val distance = textHeight / 2 - fontMetrics.bottom
val baseline = height / 2f + distance

文字的X值则根据当前index和文字宽度以及各边距得出:


private fun getIndexOfX(index: Int): Float {
return paddingLeft.toFloat() + index * (itemPading + strokeWith) + 0.5f * strokeWith
}

得到了位置,宽高接下来的步骤再简单不过了。使用drawText 绘制文字,使用drawRoundRect 绘制背景。这里有一个细节一定要注意,绘制背景一定要在绘制文字之前,否则背景会把文字给覆盖。


另外,还需要注意一点。如果onDraw方法中不注释掉超类方法,底部会多出一段输入的数字。其实很好理解,这是AppCompatEditText 自身绘制的数字,所以我们把它注释即可,包括光标也是一样。如果想要光标则需要自己在onDraw方法中绘制即可。


//隐藏自带光标
super.setCursorVisible(false)

override fun onDraw(canvas: Canvas) {
//不注释掉会显示在最底部
// super.onDraw(canvas)
......
}

以上便是实现带边框的输入框的全部类型,希望对大家有所帮助!


作者:似曾相识2022
来源:juejin.cn/post/7271056651129995322
收起阅读 »

状态🐔到底如何优雅的实现

状态机的组成 状态机是一种抽象的数学模型,描述了对象或系统在特定时间点可能处于的各种状态以及状态之间的转换规则。它由一组状态、事件、转移和动作组成,用于模拟对象在不同条件下的行为和状态变化。 状态机包括以下基本组成部分: 状态(State):表示对象或系统...
继续阅读 »

状态机的组成


状态机是一种抽象的数学模型,描述了对象或系统在特定时间点可能处于的各种状态以及状态之间的转换规则。它由一组状态、事件、转移和动作组成,用于模拟对象在不同条件下的行为和状态变化。


image-20240423094124100

状态机包括以下基本组成部分:



  • 状态(State):表示对象或系统当前的状态,例如开、关、就绪等。

  • 事件(Event):触发状态转换的动作或条件,例如按钮点击、消息到达等。

  • 转移(Transition):定义了从一个状态到另一个状态的转换规则,通常与特定事件相关联。

  • 动作(Action):在状态转换过程中执行的操作或行为,例如更新状态、记录日志等。


状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。例如,根据自动门的运行规则,我们可以抽象出下面这么一个图。


image-20240423095540911


简单实现


在计算机中,状态机通常用编程语言来实现。在 C、C++、Java、Python 等编程语言中,可以通过使用 switch-case 语句、if-else 语句、状态转移表等来实现状态机。在下面还有更加优雅的方式,使用 Spring 状态机 来实现。


if-else 实现状态机


在上面的示例中,我们使用 if-else 结构根据当前活动来控制音乐的播放状态,并执行相应的行为。代码如下:


public class BasketballMusicStateMachineUsingIfElse {
private boolean isPlayingMusic;

public BasketballMusicStateMachineUsingIfElse() {
this.isPlayingMusic = false; // 初始状态为音乐未播放
}

public void playMusic() {
if (!isPlayingMusic) {
System.out.println("Music starts playing...");
isPlayingMusic = true;
}
}

public void stopMusic() {
if (isPlayingMusic) {
System.out.println("Music stops playing...");
isPlayingMusic = false;
}
}

public void performActivity(String activity) {
if ("basketball".equals(activity)) {
System.out.println("Music~");
playMusic(); // 打篮球时播放音乐
} else if ("sing_rap".equals(activity)) {
System.out.println("哎哟你干嘛!");
stopMusic(); // 唱跳Rap时停止音乐
} else {
System.out.println("Invalid activity!");
}
}

public static void main(String[] args) {
BasketballMusicStateMachineUsingIfElse stateMachine = new BasketballMusicStateMachineUsingIfElse();

// 测试状态机
stateMachine.performActivity("basketball"); // 打篮球,音乐开始播放
stateMachine.performActivity("sing_rap"); // 唱跳Rap,音乐停止播放
stateMachine.performActivity("basketball"); // 再次打篮球,音乐重新开始播放
}
}

switch-case 实现状态机


在这个示例中,我们使用 switch-case 结构根据不同的活动来控制音乐的播放状态,并执行相应的行为。代码如下:


public class BasketballMusicStateMachineUsingSwitchCase {
private boolean isPlayingMusic;

public BasketballMusicStateMachineUsingSwitchCase() {
this.isPlayingMusic = false; // 初始状态为音乐未播放
}

public void playMusic() {
if (!isPlayingMusic) {
System.out.println("Music starts playing...");
isPlayingMusic = true;
}
}

public void stopMusic() {
if (isPlayingMusic) {
System.out.println("Music stops playing...");
isPlayingMusic = false;
}
}

public void performActivity(String activity) {
switch (activity) {
case "basketball":
System.out.println("Music ~");
playMusic(); // 打篮球时播放音乐
break;
case "sing_rap":
System.out.println("哎哟 你干嘛 ~");
stopMusic(); // 唱跳Rap时停止音乐
break;
default:
System.out.println("Invalid activity!");
}
}

public static void main(String[] args) {
BasketballMusicStateMachineUsingSwitchCase stateMachine = new BasketballMusicStateMachineUsingSwitchCase();

// 测试状态机
stateMachine.performActivity("basketball"); // 打篮球,音乐开始播放
stateMachine.performActivity("sing_rap"); // 唱跳Rap,音乐停止播放
stateMachine.performActivity("basketball"); // 再次打篮球,音乐重新开始播放
}
}


是不是感觉状态机其实经常在我们的日常使用中捏~,接下来带大家使用更优雅的状态机 Spring 状态机。


image-20240423100302874

使用 Spring 状态机


1)引入依赖


<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

2)定义状态和事件的枚举


代码如下:


public enum States {
IDLE, // 空闲状态
PLAYING_BB, // 打篮球状态
SINGING // 唱跳Rap状态
}
public enum Event {
START_BB_MUSIC, // 开始播放篮球音乐事件
STOP_BB_MUSIC // 停止篮球音乐事件
}

3)配置状态机


代码如下:


@Configuration
@EnableStateMachine
public class BasketballMusicStateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Event> {

@Autowired
private BasketballMusicStateMachineEventListener eventListener;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Event> config) throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(eventListener); // 设置状态机事件监听器
}

@Override
public void configure(StateMachineStateConfigurer<States, Event> states) throws Exception {
states
.withStates()
.initial(States.IDLE)
.states(EnumSet.allOf(States.class));
}

@Override
public void configure(StateMachineTransitionConfigurer<States, Event> transitions) throws Exception {
transitions
.withExternal()
.source(States.IDLE).target(States.PLAYING_BB).event(Event.START_BB_MUSIC)
.and()
.withExternal()
.source(States.PLAYING_BB).target(States.SINGING).event(Event.STOP_BB_MUSIC)
.and()
.withExternal()
.source(States.SINGING).target(States.PLAYING_BB).event(Event.START_BB_MUSIC);
}
}

4)定义状态机事件监听器


代码如下:


@Component
public class BasketballMusicStateMachineEventListener extends StateMachineListenerAdapter<States, Event> {

@Override
public void stateChanged(State<States, Event> from, State<States, Event> to) {
if (from.getId() == States.IDLE && to.getId() == States.PLAYING_BB) {
System.out.println("开始打篮球,music 起");
} else if (from.getId() == States.PLAYING_BB && to.getId() == States.SINGING) {
System.out.println("唱跳,你干嘛");
} else if (from.getId() == States.SINGING && to.getId() == States.PLAYING_BB) {
System.out.println("继续打篮球,music 继续");
}
}
}

5)编写单元测试


@SpringBootTest
class ChatApplicationTests {
@Resource
private StateMachine<States, Event> stateMachine;

@Test
void contextLoads() {
//开始打球,music 起
stateMachine.sendEvent(Event.START_BB_MUSIC);
//开始唱跳,你干嘛
stateMachine.sendEvent(Event.STOP_BB_MUSIC);
//继续打球,music 继续
stateMachine.sendEvent(Event.START_BB_MUSIC);

}
}

效果如下:


image-20240423103523546


在上面的示例中,我们定义了一个状态机,用于控制在打篮球时音乐的播放和唱跳 Rap 的行为。通过触发事件来执行状态转移,并通过事件监听器监听状态变化并执行相应的操作。


image-20240423103604502


作者:cong_
来源:juejin.cn/post/7360647839448088613
收起阅读 »

数据连接已满,导致新连接无法成功

个人项目中经常会把每个项目的平台部署成开发、测试环境,而数据库就有可能是多个平台共用一个了,现在基本上都是用的微服务架构,那么数据库连接就不够用了。 我们用的是MySQL数据库,最近遇到了这个尴尬的问题,本地修改了代码启动的时候经常会连不上数据库既然连接太多,...
继续阅读 »

个人项目中经常会把每个项目的平台部署成开发、测试环境,而数据库就有可能是多个平台共用一个了,现在基本上都是用的微服务架构,那么数据库连接就不够用了。


我们用的是MySQL数据库,最近遇到了这个尴尬的问题,本地修改了代码启动的时候经常会连不上数据库既然连接太多,要么减少连接,要么扩大最大可连接数,


经过查询,MySQL 数据库 的默认最大连接数是经常设置在151的默认值,而最大连接数可以达到16384个


对应报错信息一般为:


com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure


too many connections



  1. 查看数据库当前连接信息,可以看到连接数据库的进程id,ip,用户名,连接的数据库,连接状态,连接时长等如果发现有大量的sleep状态的连接进程,则说明该参数设置的过大,可以进行适当的调整小些。




    1. SHOW FULL processlist;




    2. 在MySQL中,使用 SHOW FULL PROCESSLIST; 命令可以显示系统中所有当前运行的线程,包括每个线程的状态、是否锁定等信息。这个命令输出的表格中包含的字段具有以下含义:



      1. Id: 线程的唯一标识符。这个ID可以用来引用特定的线程,例如,在需要终止一个特定的线程时可以使用 KILL 命令。

      2. User: 启动线程的MySQL用户。

      3. Host: 用户连接到MySQL服务器的主机名和端口号,显示格式通常是 host_name:port

      4. db: 当前线程所在的数据库。如果线程没有使用任何数据库,这一列可能显示为NULL。

      5. Command: 线程正在执行的命令类型,例如 QuerySleepConnect 等。

      6. Time: 命令执行的时间,以秒为单位。对于 Sleep 状态,这表示线程处于空闲状态的时长。

      7. State: 线程的当前状态,提供了正在执行的命令的额外信息。这可以是 Sending datasorting resultLocked 等。

      8. Info: 如果线程正在执行查询,则这一列显示具体的SQL语句。对于其他类型的命令,这一列可能为空或显示为NULL。


      Command 列显示为 Sleep 时,这意味着该线程当前没有执行任何查询,只是在连接池中等待下一个查询命令。通常,应用程序执行完一个查询后,连接可能会保持打开状态而不是立即关闭,以便可以重用该连接执行后续的查询。在这种状态下,线程不会使用服务器资源来处理任何数据,但仍占用一个连接槽。如果看到很多线程处于 Sleep 状态且持续时间较长,这可能是一个优化点,例如,通过调整应用逻辑或连接池设置来减少空闲连接的数量。






  2. 查询当前最大连接数和超时时间




    1. # 查看最大连接数
      show variables like '%max_connections%';
      # 查看非交互式超时时间 单位秒
      show variables like 'wait_timeout';
      # 查看交互式叫号时间 单位秒
      show variables like 'interactive_timeout';





      1. max_connections:



        1. max_connections 参数定义了数据库服务器能够同时接受的最大客户端连接数。当达到这个限制时,任何新的尝试连接的客户端将会收到一个错误,通常是“Too many connections”。

        2. 默认值通常基于系统的能力和配置,但经常设置在151的默认值。这个值可以根据服务器的硬件资源(如CPU和内存)和负载要求进行调整。



      2. mysqlx_max_connections:



        1. mysqlx_max_connections 参数是专门为MySQL的X协议(一种扩展的协议,支持更复杂的操作,如CRUD操作和实时通知)设定的最大连接数。X协议使得开发者能够使用NoSQL风格的接口与数据库交互。

        2. 默认值通常较小,因为X协议的使用还不如传统SQL协议普遍。这个参数允许你独立于max_connections控制通过X协议可能的连接数。



      3. wait_timeout:



        1. wait_timeout 设置的是非交互式(非控制台)客户端连接在变成非活动状态后,在被自动关闭之前等待的秒数。非交互式连接通常指的是通过网络或API等进行的数据库连接,如应用程序服务器到数据库的连接。

        2. 默认值通常较长,如8小时(28800秒),但这可以根据需要进行调整,特别是在连接数资源受限的环境中。



      4. interactive_timeout:



        1. interactive_timeout 适用于MySQL服务器与客户端进行交互式会话时的连接超时设置。交互式会话通常是指用户通过MySQL命令行客户端或类似工具直接连接并操作数据库。

        2. 这个超时值只会在MySQL服务器识别连接为交互式时应用。它的默认值也通常是8小时。








  3. 修改最大连接数和超时时间



    1. SQL直接改




      1. # 重启后失效 这里直接设置1000
        SET GLOBAL max_connections = 1000;
        # 设置全局 非交互连接 超时时间 单位秒
        SET GLOBAL wait_timeout = 300;




    2. 配置文件改



      1.     MySQL的配置文件通常是 my.cnf(在Linux系统中)或 my.ini(在Windows系统中)。你应该在 [mysqld] 部分中设置这些参数

      2.     在Linux系统上,MySQL的配置文件一般位于以下几个路径之一:

      3. /etc/my.cnf

      4. /etc/mysql/my.cnf

      5. /var/lib/mysql/my.cnf

      6.     具体位置可能会根据不同的Linux发行版和MySQL安装方式有所不同。你可以使用 find 命令来搜索这个文件,例如:


      7. sudo find / -name my.cnf




    3.   找到到文件后,将这些值修改为下列的值 这里直接设置1000


    4. [mysqld]
      max_connections = 1000

      wait_timeout = 300


    5. docker情况



      1. 使用Docker命令行参数

      2.     你可以在运行MySQL容器时通过Docker命令行直接设置配置参数,例如:


      3. docker run -d \
        -p 3306:3306 \
        --name mysql \
        -e MYSQL_ROOT_PASSWORD=my-secret-pw \
        -e MYSQL_DATABASE=mydatabase \
        mysql:tag --max-connections=1000 --wait-timeout=300


      4.     在这个例子中,--max-connections=1000 是作为命令行参数传递给MySQL服务器的。

      5. 修改配置文件并挂载

      6.     如果你需要修改多个配置项或者希望使用配置文件来管理设置,可以创建一个自定义的 my.cnf 文件,然后在启动容器时将它挂载到容器中适当的位置。例如:


      7. docker run -d \
        -p 3306:3306 \
        --name mysql \
        -e MYSQL_ROOT_PASSWORD=my-secret-pw \
        -v /path/to/your/custom/my.cnf:/etc/mysql/my.cnf \
        mysql:tag






  4. 修改完后再查一遍看看有没有改成功




    1. # 查看最大连接数
      show variables like '%max_connections%';
      # 查看非交互式超时时间
      SHOW GLOBAL VARIABLES LIKE 'wait_timeout';




  5. 拓展



    1. wait_timeout 变量分为全局级别和会话级别



      •     执行 SET GLOBAL wait_timeout = 300;后,

      •     使用执行 show variables like 'wait_timeout'; 发现并没有改变

      •     是因为在MySQL中,当你执行 SET GLOBAL wait_timeout = 300; 这条命令时,理论上应该是会设置全局的 wait_timeout 值为300秒。在查询 wait_timeout 时,没有指定是查询全局变量,可能会返回会话级的值。会话级的 wait_timeout 并没有被改变。尝试使用以下命令来查看全局设置:


      • # 查看全局变量
        SHOW GLOBAL VARIABLES LIKE 'wait_timeout';
        # 与之对应的,查看会话级别的变量可以使用:
        SHOW SESSION VARIABLES LIKE 'wait_timeout';




    2. 全局变量和会话变量的区别



      • 全局变量:



        1. 全局变量对服务器上所有当前会话和未来会话都有效。

        2. 当你设置一个全局变量时,它的值会影响所有新建的连接。然而,对于已经存在的连接,全局变量的更改通常不会影响它们,除非这些连接被重新启动或者明确地重新读取全局设置。

        3. 通过 SET GLOBAL 命令修改全局变量,或者在服务器的配置文件中设置并重新启动服务器。



      • 会话变量:



        1. 会话变量只对当前连接的会话有效,并且当会话结束时,会话变量的设置就会失效。

        2. 修改会话变量的命令是 SET SESSION 或者简单的 SET,它不会影响其他会话或连接。

        3. 每个新的会话都会从当前的全局设置中继承变量的值,但在会话中对这些变量的修改不会影响到其他会话。



      • 关于是否改变全局变量,这取决于你试图解决的具体问题:



        • 如果你需要修改的设置应该对所有新的连接生效,例如,修改 wait_timeout 来减少空闲连接超时,那么修改全局变量是合适的。这样,所有新建立的连接都会采用新的超时设置。

        • 然而,如果你需要立即影响当前活动的会话,你必须在每个会话中单独设置会话变量。这在某些操作中可能是必需的,比如调整当前事务的隔离级别或者调试中动态改变某些性能调优参数。

        • 因此,如果改变是为了长期或持久的配置调整,修改全局变量通常是正确的做法。但如果需要对当前会话立即生效的改变,应该使用会话变量。








作者:不惊夜
来源:juejin.cn/post/7361056871673446437
收起阅读 »

基于EdgeEffect实现RecyclerView列表阻尼滑动效果

探索EdgeEffect的花样玩法 1、EdgeEffect是什么 当用户在一个可滑动的控件内(如RecyclerView),滑动内容已经超过了内容边界时,RecyclerView通过EdgeEffect绘制一个边界图形来提醒用户,滑动已经到边界了,不要再滑动...
继续阅读 »

探索EdgeEffect的花样玩法


1、EdgeEffect是什么


当用户在一个可滑动的控件内(如RecyclerView),滑动内容已经超过了内容边界时,RecyclerView通过EdgeEffect绘制一个边界图形来提醒用户,滑动已经到边界了,不要再滑动啦。


简言之:就是通过边界图形来提醒用户,没啥内容了,别滑了。


2、EdgeEffect在RecyclerView的现象是什么


1、到达边界后的阴影效果


在RecyclerView列表中,滑动到边界还继续滑动或者快速滑动到边界,则现象如下图中的到达边界后产生的阴影效果。


滑动到边界阴影效果

2、如何去掉阴影效果


在布局中,可以设置overScrollMode的属性值为never即可。


或者在代码中设置,即可取消


recyclerView?.overScrollMode = View.OVER_SCROLL_NEVER

3、EdgeEffect在RecyclerView的实现原理是什么


1、onMove事件对应EdgeEffect的onPull


EdgeEffect在RecyclerView中大致流程可以参考下面这个图,以onMove事件举例


EdgeEffect与RecyclerView交互图

通过上面这个图,并结合下面的源码,就能对这个流程有个大致的理解。


@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
// (1) move事件
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e, TYPE_TOUCH)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
...
}
}
break;
}
}


boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
...
// (2)判断是否设置了过度滑动,所以通过布局设置overScrollMode的属性值为never就走不进了分支逻辑中了
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
...

if (!awakenScrollBars()) {
// 刷新当前界面
invalidate();
}
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
boolean invalidate = false;
...
// 顶部边界
if (overscrollY < 0) {
// 构建顶部边界的EdgeEffect对象
ensureTopGlow();
// 调用EdgeEffect的onPull方法 设置些属性
EdgeEffectCompat.onPull(mTopGlow, -overscrollY / getHeight(), x / getWidth());
invalidate = true;
}
...

if (invalidate || overscrollX != 0 || overscrollY != 0) {
// 刷新界面
ViewCompat.postInvalidateOnAnimation(this);
}
}

void ensureTopGlow() {
...
mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
// 设置边界图形的大小
if (mClipToPadding) {
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
} else {
mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
}

}

// RecyclerView的绘制
@Override
public void draw(Canvas c) {
super.draw(c);
...
if (mTopGlow != null && !mTopGlow.isFinished()) {
final int restore = c.save();
if (mClipToPadding) {
c.translate(getPaddingLeft(), getPaddingTop());
}
// 调用 EdgeEffect的draw方法
needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
c.restoreToCount(restore);
}
...
}

// EdgeEffect的draw方法
public boolean draw(Canvas canvas) {
...
update();
final int count = canvas.save();
final float centerX = mBounds.centerX();
final float centerY = mBounds.height() - mRadius;

canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);

final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
float translateX = mBounds.width() * displacement / 2;

canvas.clipRect(mBounds);
canvas.translate(translateX, 0);
mPaint.setAlpha((int) (0xff * mGlowAlpha));
// 绘制扇弧
canvas.drawCircle(centerX, centerY, mRadius, mPaint);
canvas.restoreToCount(count);
...

同理:RecyclerView的 up 及Cancel事件对应调用EdgeEffect的onRelease;fling过度滑动对应EdgeEffect的onAbsorb方法


2、EdgeEffect的onPull、onRelease、onAbsorb方法


(1)onPull


对于RecyclerView列表而言,内容已经在顶部到达边界了,此时用户仍向下滑动时,会调用onPull方法及后续流畅,来更新当前视图,提示用户已经到边界了。


(2)onRelease


对于(1)的情况,用户松开了,不向下滑动了,此时释放拉动的距离,并刷新界面消失当前的图形界面。


(3)onAbsorb


用户过度滑动时,RecyclerView调用Fling方法,把内容到达边界后消耗不掉的距离传递给onAbsorb方法,让其显示图形界面提示用户已到达内容边界。


4、使用EdgeEffect在RecyclerView中实现列表阻尼滑动等效果


(1)先看下效果


EdgeEffect的录屏

上述gif图中展示了两个效果:RecyclerView的阻尼下拉 及 复位,这就是使用上面的EdgeEffect的三个方法可以实现。


上述的gif图中,使用MultiTypeAdapter实现RecyclerView的多类型页面(ViewModel、json数据源),可以参考这篇文章快速写个RecyclerView的多类型页面


下面主要展示如何构建一个EdgeEffect,充分地使用onPull、onRelease及onAbsorb能力


(2)代码示意


// 构建一个自定义的EdgeEffectFactory 并设置给RecyclerView
recyclerView?.edgeEffectFactory = SpringEdgeEffect()

// SpringEdgeEffect
class SpringEdgeEffect : RecyclerView\.EdgeEffectFactory() {

override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {

return object : EdgeEffect(recyclerView.context) {

override fun onPull(deltaDistance: Float) {
super.onPull(deltaDistance)
handlePull(deltaDistance)
}

override fun onPull(deltaDistance: Float, displacement: Float) {
super.onPull(deltaDistance, displacement)
handlePull(deltaDistance)
}

private fun handlePull(deltaDistance: Float) {
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
val translationYDelta =
sign * recyclerView.width * deltaDistance * 0.8f
Log.d("qlli1234-pull", "deltDistance: " + translationYDelta)
recyclerView.forEach {
if (it.isVisible) {
// 设置每个RecyclerView的子item的translationY属性
recyclerView.getChildViewHolder(it).itemView.translationY += translationYDelta
}
}
}

override fun onRelease() {
super.onRelease()
Log.d("qlli1234-onRelease", "onRelease")
recyclerView.forEach {
//复位
val animator = ValueAnimator.ofFloat(recyclerView.getChildViewHolder(it).itemView.translationY, 0f).setDuration(500)
animator.interpolator = DecelerateInterpolator(2.0f)
animator.addUpdateListener { valueAnimator ->
recyclerView.getChildViewHolder(it).itemView.translationY = valueAnimator.animatedValue as Float
}
animator.start()
}
}

override fun onAbsorb(velocity: Int) {
super.onAbsorb(velocity)
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
Log.d("qlli1234-onAbsorb", "onAbsorb")
val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
recyclerView.forEach {
if (it.isVisible) {
// 在这个可以做动画
}
}
}

override fun draw(canvas: Canvas?): Boolean {
// 设置大小之后,就不会有绘画阴影效果
setSize(0, 0)
val result = super.draw(canvas)
return result
}
}
}

这里有一个小细节,如何在使用onPull等方法时,去掉绘制的阴影部分:其实,可以重写draw方法,重置大小为0即可,如上述代码中的这一小块内容:


override fun draw(canvas: Canvas?): Boolean {
// 设置大小之后,就不会有绘画阴影效果
setSize(0, 0)
val result = super.draw(canvas)
return result
}

5、参考


1、google的motion示例中的ChessAdapter内容


2、仿QQ的recyclerview效果实现


作者:李暖光
来源:juejin.cn/post/7235463575300046903
收起阅读 »

底部弹出菜单原来这么简单

底部弹出菜单是什么 底部弹出菜单,即从app界面底部弹出的一个菜单列表,这种UI形式被众多app所采用,是一种主流的布局方式。 思路分析 我们先分析一下,这样一种UI应该由哪些布局组成?首先在原界面上以一小块区域显示界面的这种形式,很明显就是对话框Dial...
继续阅读 »

底部弹出菜单是什么


底部弹出菜单,即从app界面底部弹出的一个菜单列表,这种UI形式被众多app所采用,是一种主流的布局方式。


截屏2023-09-28 14.36.51.png


截屏2023-09-28 14.37.29.png


思路分析


我们先分析一下,这样一种UI应该由哪些布局组成?首先在原界面上以一小块区域显示界面的这种形式,很明显就是对话框Dialog做的事情吧!最底部是一个取消菜单,上面的功能菜单可以是一个,也可以是两个、三个甚至更多。所以,我们可以使用RecyclerView实现。需要注意一点的是,最上面那个菜单的样式稍微有点不一样,因为它上面是圆滑的,有圆角,这样的界面显示更加和谐。我们主要考虑的就是弹出对话框的动画样式,另外注意一点就是可以多支持几个语种,让框架更加专业,这里只需要翻译“取消”文字。


开始看代码


package dora.widget

import android.app.Activity
import android.app.Dialog
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.listener.OnItemChildClickListener
import dora.widget.bean.BottomMenu
import dora.widget.bottomdialog.R

class DoraBottomMenuDialog : View.OnClickListener, OnItemChildClickListener {

private var bottomDialog: Dialog? = null
private var listener: OnMenuClickListener? = null

interface OnMenuClickListener {
fun onMenuClick(position: Int, menu: String)
}

fun setOnMenuClickListener(listener: OnMenuClickListener) : DoraBottomMenuDialog {
this.listener = listener
return this
}

fun show(activity: Activity, menus: Array<String>): DoraBottomMenuDialog {
if (bottomDialog == null && !activity.isFinishing) {
bottomDialog = Dialog(activity, R.style.DoraView_AlertDialog)
val contentView =
LayoutInflater.from(activity).inflate(R.layout.dview_dialog_content, null)
initView(contentView, menus)
bottomDialog!!.setContentView(contentView)
bottomDialog!!.setCanceledOnTouchOutside(true)
bottomDialog!!.setCancelable(true)
bottomDialog!!.window!!.setGravity(Gravity.BOTTOM)
bottomDialog!!.window!!.setWindowAnimations(R.style.DoraView_BottomDialog_Animation)
bottomDialog!!.show()
val window = bottomDialog!!.window
window!!.decorView.setPadding(0, 0, 0, 0)
val lp = window.attributes
lp.width = WindowManager.LayoutParams.MATCH_PARENT
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
window.attributes = lp
} else {
bottomDialog!!.show()
}
return this
}

private fun initView(contentView: View, menus: Array<String>) {
val recyclerView = contentView.findViewById<RecyclerView>(R.id.dview_recycler_view)
val adapter = MenuAdapter()
val list = mutableListOf<BottomMenu>()
menus.forEachIndexed { index, s ->
when (index) {
0 -> {
list.add(BottomMenu(s, BottomMenu.TOP_MENU))
}
else -> {
list.add(BottomMenu(s, BottomMenu.NORMAL_MENU))
}
}
}
adapter.setList(list)
recyclerView.adapter = adapter
val decoration = DividerItemDecoration(contentView.context, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(decoration)
adapter.addChildClickViewIds(R.id.tv_menu)
adapter.setOnItemChildClickListener(this)
val tvCancel = contentView.findViewById<TextView>(R.id.tv_cancel)
tvCancel.setOnClickListener(this)
}

private fun dismiss() {
bottomDialog?.dismiss()
bottomDialog = null
}

override fun onClick(v: View) {
when (v.id) {
R.id.tv_cancel -> dismiss()
}
}

override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
listener?.onMenuClick(position, adapter.getItem(position) as String)
dismiss()
}
}

类的结构不仅可以继承,还可以使用聚合和组合的方式,我们这里就不直接继承Dialog了,使用一种更接近代理的一种方式。条条大路通罗马,能抓到老鼠的就是好猫。这里的设计是通过调用show方法,传入一个菜单列表的数组来显示菜单,调用dismiss方法来关闭菜单。最后添加一个菜单点击的事件,把点击item的内容和位置暴露给调用方。


package dora.widget

import com.chad.library.adapter.base.BaseMultiItemQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import dora.widget.bean.BottomMenu
import dora.widget.bottomdialog.R

class MenuAdapter : BaseMultiItemQuickAdapter<BottomMenu, BaseViewHolder>() {

init {
addItemType(BottomMenu.NORMAL_MENU, R.layout.dview_item_menu)
addItemType(BottomMenu.TOP_MENU, R.layout.dview_item_menu_top)
}

override fun convert(holder: BaseViewHolder, item: BottomMenu) {
holder.setText(R.id.tv_menu, item.menu)
}
}

多类型的列表布局我们采用BRVAH,


implementation("io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.10")

来区分有圆角和没圆角的item条目。


<?xml version="1.0" encoding="utf-8"?>

<resources>
<style name="DoraView.AlertDialog" parent="@android:style/Theme.Dialog">
<!-- 是否启用标题栏 -->
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>

<!-- 是否使用背景半透明 -->
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
</style>

<style name="DoraView.BottomDialog.Animation" parent="Animation.AppCompat.Dialog">
<item name="android:windowEnterAnimation">@anim/translate_dialog_in</item>
<item name="android:windowExitAnimation">@anim/translate_dialog_out</item>
</style>
</resources>

以上是对话框的样式。我们再来看一下进入和退出对话框的动画。


translate_dialog_in.xml


<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:duration="300"
android:fromXDelta="0"
android:fromYDelta="100%"
android:toXDelta="0"
android:toYDelta="0">

</translate>

translate_dialog_out.xml


<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:duration="300"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="100%">

</translate>

最后给你们证明一下我是做了语言国际化的。
截屏2023-09-28 15.08.20.png


使用方式


// 打开底部弹窗
val dialog = DoraBottomMenuDialog()
dialog.setOnMenuClickListener(object : DoraBottomMenuDialog.OnMenuClickListener {
override fun onMenuClick(position: Int, menu: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
startActivity(intent)
}
})
dialog.show(this, arrayOf("外部浏览器打开"))

开源项目


github.com/dora4/dview…


作者:dora
来源:juejin.cn/post/7283516197487214611
收起阅读 »

Android:LayoutAnimation的神奇效果

大家好,我是时曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 今天给大家讲讲酷炫的动画成员——LayoutAnimation。话不多说,直接上一个简单的效果图: 怎么样,和往常自己写的没有动画效果的页面比起来是不是更加酷炫。效果图只展示了从右到...
继续阅读 »

大家好,我是时曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。


今天给大家讲讲酷炫的动画成员——LayoutAnimation。话不多说,直接上一个简单的效果图:


Screenrecorder-2023-09-10-10-29-52-627.gif


怎么样,和往常自己写的没有动画效果的页面比起来是不是更加酷炫。效果图只展示了从右到左叠加渐变的效果,只要脑洞够大,LayoutAnimation是可以帮你实现各类动画的。接下来就让我们看看LayoutAnimation如何实现这样的效果。


首先,新建一个XML动画文件slide_from_right.xml:


<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="600">
<translate
android:fromXDelta="100%p"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0" />

<alpha
android:fromAlpha="0.5"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="1" />
</set>

set标签下由translate(移动)和alpha(渐变)动画组成。


其中translate(移动)动画由100%p移动到0。这里需要注意使用的是100%p,其中加这个p是指按父容器的宽度进行百分比计算。插值器就根据自己想要的效果设置,这里使用了一个decelerate_interpolator(减速)插值器。


第二个动画是alpha(渐变)动画,由半透明到不透明,其中插值器是先加速后减速的效果。


接着我们还需要创建一个layoutAnimation,其实也是一个XML文件layout_slid_from_right.xml:


<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/slide_from_right"
android:animationOrder="normal"
android:delay="15%"/>

其中animation指定的就是我们创建的第一个xml文件。animationOrder是指动画执行的顺序模式,包含normal, reverse 和random。normal就是从上到下依次进行,reverse根据名字就知道是反序,random那当然是随机了,我们就使用mormal即可。delay则是每个子视图执行动画的延迟比例,这里需要注意的是这是相对于上个子视图执行动画延时比例。


最后我们只需要在咱们的ViewGr0up中设置layoutAnimation属性即可:


android:layoutAnimation="@anim/layout_slid_from_right"

当然也可在代码中手动设置:


val lin = findViewById<LinearLayout>(R.id.linParent)
val resId = R.anim.layout_slid_from_right
lin.layoutAnimation = AnimationUtils.loadLayoutAnimation(lin.context, resId)

总结:



  • layoutAnimation可以使用在任何一个ViewGr0up上

  • 在使用set标签做动画叠加的时候一定要注意,set标签内需要添加duration属性,也就是动画时间。如果不加动画是没有效果的。

  • 使用移动动画时,在百分比后面添加p的意思是基于父容器宽度进行百分比计算


以上便是LayoutAnimation的简单使用,只要你脑洞大开,各种各样的效果都能玩出来。实现起来也很简单,赶紧在项目中使用起来吧。


作者:似曾相识2022
来源:juejin.cn/post/7276630249547513895
收起阅读 »

和后端吵架后,我写了个库,让整个前端团队更加规范!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 本文源码地址:github.com/sanxin-lin/… 背景 在平时的开发中,表格数据->(增加/编辑/查看)行->(增加/编辑)提交,这...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



本文源码地址:github.com/sanxin-lin/…



背景


在平时的开发中,表格数据->(增加/编辑/查看)行->(增加/编辑)提交,这是很常见且简单的业务,但是就是这些业务,我也发现一些问题



首先我们来理性一下这些业务的逻辑



  • 第一步:请求回表格的数据

  • 第二步:点开(增加/编辑/查看)弹窗,如果是(编辑/查看),则需要将表格行的数据传到弹窗中回显

  • 第三部:如果是(编辑)弹窗,则需要把表单数据提交请求接口


我用一个图来概括大概就是:



问题所在


我不知道其他公司怎么样,但是就拿我自身来举例子,公司的后端跟前端的命名规则是不同的



  • 后端命名: 请求方法+字段类型+字段含义+下划线命名(比如 in_name、os_user_id)

  • 前端命名: 字段含义+驼峰命名(比如 name、userId)


回到刚刚的业务逻辑,还是那张图,假如我们前端不去管命名的话,那么数据的传输是这样的,发现了很多人都懒得去转换后端返回的字段名,直接拿着后端的字段名去当做前端的表单字段名,但这是不符合前端规范的



理想应该是表单要用前端的命名,比如这样



但是很多前端就是懒得去转换,原因有多个:



  • 开发者自身比较懒,或者没有规范意识

  • 回显时要转一次,提交时还要再转一次,每次总是得写一遍


解决方案


所以能不能写一个工具,解放开发者的压力又能达到期望的效果呢?比如我开发一个工具,然后像下面这样在弹窗里用



  • state: 响应式表单数据,可以用在弹窗表单中

  • resetState: 重置表单

  • inputState: 将表格行数据转成表单数据

  • outputState: 将表单数据转成提交请求的数据


配置的含义如下:



  • default: 表单字段默认值

  • input: 转入的字段名

  • output: 转出的字段名

  • inputStrategy: 转入的转换策略,可以选择内置的,也可以自定义策略函数

  • outputStrategy: 转出的转换策略,可以选择内置的,也可以自定义策略函数



转入和转出策略,内置了一些,你也可以自定义,内置的有如下



下面是自定义策略函数的例子,必须要在策略函数中返回一个转换值



这样的话,当我们执行对应的转换函数之后,会得到我们想要的结果



use-dsp


所以我开发了一个工具



源码地址:github.com/sanxin-lin/…



其实 dsp 意思就是



  • data

  • state

  • parameter


npm i use-dsp
yarn i use-dsp
pnpm i use-dsp

import useDSP from 'use-dsp'

为啥不从一开始就转?


有人会问,为啥不从一开始请求表格数据回来的时候,就把数据转成前端的命名规范?


其实这个问题我也想过,但是设想一下,有一些表格如果只是单纯做展示作用,那么就没必要去转字段名了,毕竟不涉及任何的数据传递。


但是需要编辑或者查看弹窗的表格,就涉及到了行数据的传递,那么就需要转字段名




作者:Sunshine_Lin
来源:juejin.cn/post/7360892717545799689
收起阅读 »

特斯拉违约24届全部应届生,如何评价?

4月23日有消息称:特斯拉(上海)撤回了所有24应届生的offer,统一给了一个月底薪作为补偿。 目前这个话题的讨论热度在脉脉上排第二。 先不管是真是假,至少看到有一部分同学确实是被裁了,这个时间点被裁,基本是废了。金三银四都差不多过完了,虽说还是应届生身...
继续阅读 »

4月23日有消息称:特斯拉(上海)撤回了所有24应届生的offer,统一给了一个月底薪作为补偿。



目前这个话题的讨论热度在脉脉上排第二。


脉脉热榜


先不管是真是假,至少看到有一部分同学确实是被裁了,这个时间点被裁,基本是废了。金三银四都差不多过完了,虽说还是应届生身份,但马上就要毕业了,这个时间点工作确实不好找了。


4月15日马斯克宣布特斯拉裁员10%,按照特斯拉去年公布的数据显示,特斯拉在全球共有14万名员工,算下来得裁1.4万人呐。之前出裁员消息的时候就上过一波热搜,没想到这事还没结束,现在又把所有应届生都毁约了,特斯拉这波是有点狠的。




注:信息来源于网络,官方目前并未公开发出毁约所有24应届生的消息。



看看网友怎么说


网友一


思路打开点,反正工作找不到了,考个研缓三年,还是应届生[狗头]。



网友二


能进特斯拉的肯定都是大佬级别,趁机宣传一波公司招人,这确实是个好时机。



网友三


至少赔钱了,一般的企业你看理你吗?(确实)



网友四


看了下特斯拉股票,“广进计划” 诚不欺我😅。



我怎么看


裁员我是可以理解的,哪家公司都会有亏损的时候,进行一波裁员也很正常,谁又能保证公司能一帆风顺向上发展?


但是集体毁约应届生这个事,我是不太能理解的。每年亏损的公司这么多,集体毁约应届生的一般也就两三家(老员工也可以裁呀)。既然集体毁约,是否能够根据当前的就业环境,拿出足够的诚意?如果你是十月十一月集体毁约,那我觉得赔一个月工资差不多了。但是在四月底进行毁约,我觉得得赔3个月才够诚意。


当然啦,这些只是我站在道德制高点的一些看法,真要说起来,特斯拉这个毁约条件,比起大多数企业确实还是算好的了,就像网友说的 “赔钱了良心,国内都不理你”。


最终倒霉的还是这些拿了特斯拉offer开开心心准备去上班的同学了,用“天崩开局”来形容都不为过。



你怎么看


欢迎在评论区留言,发表的你看法。


作者:阿杆
来源:juejin.cn/post/7361023886852849718
收起阅读 »

JavaScript精粹:26个关键字深度解析,编写高质量代码的秘诀!

JavaScript关键字是一种特殊的标识符,它们在语言中有固定的含义,不能用作变量名或函数名。这些关键字是JavaScript的基础,理解它们是掌握JavaScript的关键。今天,我们将一起探索JavaScript中的26个关键字,了解这些关键字各自独特的...
继续阅读 »

JavaScript关键字是一种特殊的标识符,它们在语言中有固定的含义,不能用作变量名或函数名。这些关键字是JavaScript的基础,理解它们是掌握JavaScript的关键。

今天,我们将一起探索JavaScript中的26个关键字,了解这些关键字各自独特的含义、特性和使用方法。

一、JavaScript关键字是什么

Javascript关键字(Keyword)是指在Javascript语言中有特定含义,成为Javascript语法中一部分的那些字,是 JavaScript 语言内部使用的一组名字(或称为命令)。

Description


Javascript关键字是不能作为变量名和函数名使用的。使用Javascript关键字作为变量名或函数名,会使Javascript在载入过程中出现编译错误。

Java中的关键字可用于表示控制语句的开始或结束,或者用于执行特定操作等。按照规则,关键字也是语言保留的,不能用作标识符。

下面我们来详细介绍一下JavaScript关键字的作用和使用方法。

二、JavaScript的26个关键字

JavaScript是一种广泛使用的编程语言,它具有丰富的关键字,这些关键字在JavaScript语言中发挥着重要的作用,JavaScript一共提供了26个关键字:


break, case, catch, continue, debugger, default, delete, do, else, finally, for, function, if, in, instanceof, new, return, switch, this, throw, try, typeof, var, void, while, with
其中,debugger在ECMAScript 5 新增的。

1、break:跳出 循环

break用于跳出循环结构。循环结构是一种重复执行某个代码块的结构,break关键字可以用于循环结构中的条件语句中,用于跳出循环。例如:

for (var i = 0; i < 10; i++) {
if (i == 5) {
break; // 当i等于5时跳出循环
}
console.log(i);
}

2、case:捕捉

它用于在switch语句中定义一个分支。switch语句是一种根据表达式的值执行不同代码块的结构,case关键字可以用于switch语句中,用于定义不同的分支。例如:

switch (n) {

case 1:
console.log('n等于1');
break;
case 2:
console.log('n等于2');
break;
default:
console.log('n不等于1或2');
break;
}

3、catch:配合try进行错误判断

catch用于捕获异常。异常是一种程序运行时出现的错误,catch关键字可以用于try-catch语句中,用于捕获并处理异常。例如:

try {
// 代码
} catch (e) {
console.log('发生异常:' + e.message);
}

4、continue:继续

continue用于跳过当前循环中的某个迭代。循环结构是一种重复执行某个代码块的结构,continue关键字可以用于循环结构中的条件语句中,用于跳过当前迭代。例如:

for (var i = 0; i < 10; i++) {
if (i == 5) {
continue; // 当i等于5时跳过当前迭代
}
console.log(i);
}

5、debugger:设置断点

它用于在代码中设置断点,方便调试代码。调试是一种在代码运行时发现和解决问题的过程,debugger关键字可以用于代码中,用于设置断点。例如:

function foo() {

var x = 10;

debugger; // 在这里设置断点
console.log(x);

}

6、default:配合switch,当条件不存在时使用该项

default用于在switch语句中定义一个默认分支。switch语句是一种根据表达式的值执行不同代码块的结构,default关键字可以用于switch语句中,用于定义默认分支。例如:


switch (n) {
case 1:
console.log('n等于1');
break;
case 2:
console.log('n等于2');
break;
default:
console.log('n不等于1或2');
break;
}

7、delete:删除了一个属性

delete用于删除对象的属性或数组中的元素。对象是JavaScript中的一种数据类型,它由一组属性组成,delete关键字可以用于对象的属性中,用于删除属性。例如:

var obj = {a: 1, b: 2, c: 3};
delete obj.b; // 删除对象obj的属性b
console.log(obj); // 输出{a: 1, c: 3}

8、do:声明一个循环

do用于定义一个do-while循环结构。循环结构是一种重复执行某个代码块的结构,do关键字可以用于do-while循环中,用于定义循环体。例如:

var i = 0;
do {
console.log(i);
i++;
} while (i < 10);

9、else:否则//配合if条件判断,用于条件选择的跳转

else用于在if语句中定义一个分支。if语句是一种根据条件执行不同代码块的结构,else关键字可以用于if语句中,用于定义另一个分支。例如:

if (n == 1) {
console.log('n等于1');
} else {
console.log('n不等于1');
}

10、finally:预防出现异常时用的

finally用于定义一个try-catch-finally语句中的finally块。try-catch-finally语句是一种用于处理异常的结构,finally关键字可以用于finally块中,用于定义一些必须执行的代码。例如:

try {
// 可能会抛出异常的代码
} catch (e) {
// 处理异常的代码
} finally {
// 必须执行的代码
}

11、for:循环语句

for用于定义一个for循环结构。循环结构是一种重复执行某个代码块的结构,for关键字可以用于for循环中,用于定义循环条件。例如:

for (var i = 0; i < 10; i++) {
console.log(i);
}

12、function:定义函数的关键字

function用于定义一个函数。函数是一种封装了一段代码的结构,它可以接受参数并返回结果。function关键字可以用于函数定义中,用于定义函数名和参数列表。例如:

function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 输出3

13、if:定义一个if语句

if用于定义一个if语句。if语句是一种根据条件执行不同代码块的结构,if关键字可以用于if语句中,用于定义条件。例如:


if (n == 1) {
console.log('n等于1');
} else {
console.log('n不等于1');
}

14、in:判断某个属性属于某个对象

in用于判断一个对象是否包含某个属性。对象是JavaScript中的一种数据类型,它由一组属性组成,in关键字可以用于对象中,用于判断对象是否包含某个属性。例如:


var obj = {a: 1, b: 2, c: 3};
if ('a' in obj) {
console.log('obj包含属性a');
} else {
console.log('obj不包含属性a');
}

15、instanceof:某个对象是不是另一个对象的实例

instanceof用于判断一个对象是否是某个类的实例。类是JavaScript中的一种数据类型,它由一组属性和方法组成,instanceof关键字可以用于类中,用于判断对象是否是该类的实例。例如:

function Person(name) {
this.name = name;
}
var p = new Person('张三');
if (p instanceof Person) {
console.log('p是Person类的实例');
} else {
console.log('p不是Person类的实例');
}

16、new:创建一个新对象

new用于创建一个对象。对象是JavaScript中的一种数据类型,它由一组属性和方法组成,new关键字可以用于类中,用于创建该类的实例。例如:

function Person(name) {
this.name = name;
}
var p = new Person('张三');
console.log(p.name); // 输出张三

17、return:返回

return用于从函数中返回一个值。函数是JavaScript中的一种数据类型,它由一段代码块组成,return关键字可以用于函数中,用于返回函数的执行结果。例如:

function add(a, b) {
return a + b;
}

console.log(add(1, 2)); // 输出3

18、switch:弥补if的多重判断语句

switch用于根据不同的条件执行不同的代码块。switch语句是一种根据条件执行不同代码块的结构,switch关键字可以用于switch语句中,用于定义条件。例如:

var day = 3;
switch (day) {
case 1:
console.log('星期一');
break;
case 2:
console.log('星期二');
break;
case 3:
console.log('星期三');
break;
default:
console.log('不是星期一、二、三');
}

19、this:总是指向调用该方法的对象

this用于引用当前对象。对象是JavaScript中的一种数据类型,它由一组属性和方法组成,this关键字可以用于对象中,用于引用当前对象的属性和方法。例如:

var obj = {
name: '张三',
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // 输出张三

20、throw:抛出异常

throw用于抛出一个异常。异常是JavaScript中的一种错误类型,它可以用于在程序运行过程中发现错误并停止程序的执行。throw关键字可以用于函数中,用于抛出异常。例如:

function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');

}
return a / b;
}
console.log(divide(10, 0)); // 抛出异常

21、try:接受异常并做出判断

try用于捕获异常。异常是JavaScript中的一种错误类型,它可以用于在程序运行过程中发现错误并停止程序的执行。try语句是一种捕获异常的结构,try关键字可以用于try语句中,用于捕获异常。例如:

function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');
}
return a / b;
}
try {
console.log(divide(10, 0)); // 抛出异常
} catch (e) {
console.log(e.message); // 输出除数不能为0
}

22、typeof:检测变量的数据类型

typeof用于获取变量的类型。变量是JavaScript中的一种数据类型,它可以是数字、字符串、布尔值等。typeof关键字可以用于变量中,用于获取变量的类型。例如:

var a = 10;
console.log(typeof a); // 输出number

23、var:声明变量

var用于声明变量。变量是JavaScript中的一种数据类型,它可以用于存储数据。var关键字可以用于变量中,用于声明变量。例如:

var a = 10;
console.log(a); // 输出10

24、void:空/ 声明没有返回值

void它用于执行一个表达式并返回undefined。undefined是JavaScript中的一种特殊值,它表示一个未定义的值。void关键字可以用于表达式中,用于执行表达式并返回undefined。例如:

function doSomething() {
console.log('执行了doSomething函数');
}
var result = void doSomething();
console.log(result); // 输出undefined

25、while

while用于创建一个循环结构。循环是JavaScript中的一种控制结构,它可以用于重复执行一段代码。while关键字可以用于循环中,用于创建一个基于条件的循环。例如:

var i = 0;
while (i < 10) {
console.log(i);
i++;
}

26、with

with用于创建一个作用域。作用域是JavaScript中的一种机制,它可以用于控制变量的作用范围。with关键字可以用于代码块中,用于创建一个作用域。例如:


var obj = {
name: '张三',
age: 20
};
with (obj) {
console.log(name); // 输出张三
console.log(age); // 输出20
}


三、JS关键字注意事项

在开发过程中使用关键字我们需要注意以下几点:

  • 区分大小写: JavaScript是区分大小写的,因此关键字的大小写必须正确。

  • 不能用作变量名: 关键字不能被用作变量名,函数名等等,会出现问题

  • 不需要硬记关键字: 关键字不用去硬记,我们在编写代码时根据系统的提示去规避就可以了

  • 保留字: JavaScript有一些保留字,不能用作变量名、函数名或属性名。

  • 不要使用全局变量: 尽量避免使用全局变量,以免与其他脚本或库发生冲突。可以使用立即执行函数表达式(IIFE)或模块模式来避免全局变量污染。

  • 使用严格模式: 在代码中添加"use strict"指令,以启用严格模式。这将有助于避免一些常见的错误,例如未声明的变量、隐式类型转换等。

  • 避免使用eval()函数: eval()函数用于动态执行字符串中的JavaScript代码,但可能导致性能问题和安全风险。尽量避免使用eval(),寻找其他替代方案。

  • 不要使用with语句: with语句会改变代码的作用域链,可能导致意外的错误。尽量避免使用with语句,改用局部变量或对象属性访问。

  • 避免使用重复的标识符: 确保变量名、函数名和属性名在同一作用域内是唯一的,以避免命名冲突。

  • 遵循编码规范: 遵循一致的命名约定、缩进风格和代码结构,以提高代码的可读性和可维护性。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


四、关于保留字的了解

除了关键字还有个“保留字”的概念,所谓保留字,实际上就是预留的“关键字”。意思是现在虽然还不是关键字,但是未来可能会成为关键字,同样不能使用它们当充当变量名、函数名等标识符。

下面是JavaScript中保留字的含义,大家同样不用记,了解一下就行了。

Description

以上就是关于JavaScript关键字的相关内容了,通过了解这26个JavaScript关键字的含义、特性和使用方法,你已经迈出了成为编程高手的重要一步。

记住,实践是学习的关键,不断地编写代码并应用这些关键字,你将逐渐掌握JavaScript的精髓。

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

Android自定义一个省份简称键盘

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新And...
继续阅读 »

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新Android当中的技术点,回头一想,还是穿插着来吧,再系统的规划,难免也有变化,想到啥就写啥吧,能够坚持输出就行。


今天的这个知识点,是一个自定义View,一个省份的简称键盘,主要用到的地方,比如车牌输入等地方,相对来说还是比较的简单,我们先看下最终的实现效果:



实现方式呢有很多种,我相信大家也有自己的一套实现机制,这里,我采用的是组合View,用的是LinearLayout的方式。


今天的内容大致如下:


1、分析UI,如何布局


2、设置属性和方法,制定可扩展效果


3、部分源码剖析


4、开源地址及实用总结


一、分析UI,如何布局


拿到UI效果图后,其实也没什么好分析的,无非就是两块,顶部的完成按钮和底部的省份简称格子,一开始,打算用RecyclerView网格布局来实现,但是最后的删除按钮如何摆放就成了问题,直接悬浮在网格上边,动态计算位置,显然不太合适,也没有这样去搞的,索性直接抛弃这个方案,多布局的想法也实验过,但最终还是选择了最简单的LinearLayout组合View形式。


所谓简单,就是在省份简称数组的遍历中,不断的给LinearLayout进行追加子View,需要注意的是,本身的View,也就是我们自定义View,继承LinearLayout后,默认的是垂直方向的,往本身View追加的是横向属性的LinearLayout,这也是换行的效果,也就是,一行一个横向的LinearLayout,记住,横向属性的LinearLayout,才是最终添加View的直接父类。



换行的条件就是基于UI效果,当模于设置length等于0时,我们就重新创建一个水平的LinearLayout,这就可以了,是不是非常的简单。


至于最后的删除按钮,使其靠右,占据两个格子的权重设置即可。


二、设置属性和方法,制定可扩展效果


当我们绘制完这个身份简称键盘后,肯定是要给他人用的,基于灵活多变的需求,那么相对应的我们也需要动态的进行配置,比如背景颜色,文字的颜色,大小,还有边距,以及点击效果等等,这些都是需要外露,让使用者选择性使用的,目前所有的属性如下,大家在使用的时候,也可以对照设置。


设置属性


属性类型概述
lp_backgroundcolor整体的背景颜色
lp_rect_spacingdimension格子的边距
lp_rect_heightdimension格子的高度
lp_rect_margin_topdimension格子的距离上边
lp_margin_left_rightdimension左右距离
lp_margin_topdimension上边距离
lp_margin_bottomdimension下边距离
lp_rect_backgroundreference格子的背景
lp_rect_select_backgroundreference格子选择后的背景
lp_rect_text_sizedimension格子的文字大小
lp_rect_text_colorcolor格子的文字颜色
lp_rect_select_text_colorcolor格子的文字选中颜色
lp_is_show_completeboolean是否显示完成按钮
lp_complete_text_sizedimension完成按钮文字大小
lp_complete_text_colorcolor完成按钮文字颜色
lp_complete_textstring完成按钮文字内容
lp_complete_margin_topdimension完成按钮距离上边
lp_complete_margin_bottomdimension完成按钮距离下边
lp_complete_margin_rightdimension完成按钮距离右边
lp_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失

定义方法


方法参数概述
keyboardContent回调函数获取点击的省份简称简称信息
keyboardDelete函数删除省份简称简称信息
keyboardComplete回调函数键盘点击完成
openProhibit函数打开禁止(使领学港澳),使其可以点击

三、关键源码剖析


这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。


定义身份简称数组


    //省份简称数据
private val mLicensePlateList = arrayListOf(
"京", "津", "渝", "沪", "冀", "晋", "辽", "吉", "黑", "苏",
"浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "琼",
"川", "贵", "云", "陕", "甘", "青", "蒙", "桂", "宁", "新",
"藏", "使", "领", "学", "港", "澳",
)

遍历省份简称


mLength为一行展示多少个,当取模为0时,就需要换行,也就是再次创建一个水平的LinearLayout,添加至外层的垂直LinearLayout中,每个水平的LinearLayout中,则是一个一个的TextView。


  //每行对应的省份简称
var layout: LinearLayout? = null
//遍历车牌号
mLicensePlateList.forEachIndexed { index, s ->
if (index % mLength == 0) {
//重新创建,并添加View
layout = createLinearLayout()
layout?.weightSum = 1f
addView(layout)
val params = layout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
layout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//最后五个是否禁止
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
setTextColor(mNumProhibitColor)
mTempTextViewList.add(this)
} else {
setTextColor(mRectTextColor)
}

setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
return@setOnClickListener
}
//每个格子的点击事件
changeTextViewState(this)
}
}

addRectView(textView, layout, 0.1f)
}

追加最后一个View


由于最后一个视图是一个图片,占据了两个格子的大小,所以需要特殊处理,需要做的就是,单独设置权重weight和单独设置宽度width,如下所示:


  /**
* AUTHOR:AbnerMing
* INTRODUCE:追加最后一个View
*/

private fun addEndView(layout: LinearLayout?) {
val endViewLayout = LinearLayout(context)
endViewLayout.gravity = Gravity.RIGHT
//删除按钮
val endView = RelativeLayout(context)
//添加删除按钮
val deleteImage = ImageView(context)
deleteImage.setImageResource(R.drawable.view_ic_key_delete)
endView.addView(deleteImage)

val imageParams = deleteImage.layoutParams as RelativeLayout.LayoutParams
imageParams.addRule(RelativeLayout.CENTER_IN_PARENT)
deleteImage.layoutParams = imageParams
endView.setOnClickListener {
//删除
mKeyboardDelete?.invoke()
invalidate()
}
endView.setBackgroundResource(mRectBackGround)
endViewLayout.addView(endView)
val params = endView.layoutParams as LayoutParams
params.width = (getScreenWidth() / mLength) * 2 - mMarginLeftRight.toInt()
params.height = LayoutParams.MATCH_PARENT

endView.layoutParams = params

layout?.addView(endViewLayout)
val endParams = endViewLayout.layoutParams as LayoutParams
endParams.apply {
width = (mSpacing * 3).toInt()
height = LayoutParams.MATCH_PARENT
weight = 0.4f
rightMargin = mSpacing.toInt()
endViewLayout.layoutParams = this
}


}

四、开源地址及使用总结


开源地址:github.com/AbnerMing88…


关于使用,其实就是一个类,大家可以下载源码,直接复制即可使用,还可以进行修改里面的代码,非常的方便,如果懒得下载源码,没关系,我也上传到了远程Maven,大家可以按照下面的方式进行使用。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。


 allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


 dependencies {
implementation 'com.vip:plate:1.0.0'
}

代码使用


   <com.vip.plate.LicensePlateView
android:id="@+id/lp_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:lp_complete_text_size="14sp"
app:lp_margin_left_right="10dp"
app:lp_rect_spacing="6dp"
app:lp_rect_text_size="19sp"
app:lp_text_click_effect="false" />


总结


大家在使用的时候,一定对照属性表进行选择性使用;关于这个省份简称自定义View,实现方式有很多种,我目前的这种也不是最优的实现方式,只是自己的一个实现方案,给大家一个作为参考的依据,好了,铁子们,本篇文章就先到这里,希望可以帮助到大家。


作者:程序员一鸣
来源:juejin.cn/post/7235484890019659834
收起阅读 »

文本美学:text-image打造视觉吸引力

web
当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。 项目介绍 话不多说,我们先看下作者的demo效果: _202...
继续阅读 »

当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。


项目介绍


话不多说,我们先看下作者的demo效果:


微信截图_20240420194201.png


_20240420194201.jpg


text-image可以将文字、图片、视频进行「文本化」


只需要通过简单的配置即可使用。


虽然这个项目star数很少,但确实是一个很有意思的项目,使用起来很简单的项目。


_20240420194537.jpg


_20240420194537.jpg


github地址:https://github.com/Sunny-117/text-image


我也是使用这个项目做了一个简单的web页面,感兴趣的家人可以使用看下效果:


web地址:http://h5.xiuji.mynatapp.cc/text-image/


_20240420211509.jpg


_20240420211509.jpg


项目使用


这个项目使用起来相对简单,只需按作者的文档使用即可,虽然我前端属于小白的水平,但还是在ai的帮助下做了一个简单的html页面,如果有家人需要的话可以私信我,我发下文件。下边我们就介绍下:



  • 文字「文本化」


先看效果:


_20240420195701.jpg


_20240420195701.jpg


我们在这儿是将配置的一些参数在页面上做了一个可配置的表单,方便我们配置。


家人们想自己尝试的话可以试下以下这个demo。


demo.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
replaceText: '123',
source: {
text: '修己xj',
},
});
</script>
</body>
</html>


  • 图片「文本化」


_20240420200651.jpg


_20240420200651.jpg


demo.html


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

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

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 7,
isGray: true,
source: {
img: './assets/1.png',
},
});
</script>
</body>

</html>


  • 视频「文本化」


动画1.gif


1.gif


demo.html


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

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

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>

<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 8,
isGray: true,
source: {
video: './assets/1.mp4',
height: 700,
},
});
</script>
</body>

</html>

需要注意的是:作者在项目中提供的视频的demo这个属性值有错误,我们需要改正后方可正常显示:


_20240420211124.jpg


_20240420211124.jpg


总结


text-image 是一个强大的前端工具,可以帮助用户快速、轻松地将文本、图片、视频转换成文本化的图片,增强文本内容的表现力和吸引力。


作者:修己xj
来源:juejin.cn/post/7359510120248786971
收起阅读 »

安卓开发中如何实现一个定时任务

定时任务方式优点缺点使用场景所用的API普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)Timer定时器简单易用,可以设置固定周期或者...
继续阅读 »

定时任务方式优点缺点使用场景所用的API
普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)
Timer定时器简单易用,可以设置固定周期或者延迟执行的任务不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Timer.schedule(TimerTask,long)
ScheduledExecutorService灵活强大,可以设置固定周期或者延迟执行的任务,并支持多线程并发不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要多线程并发的定时任务Executors.newScheduledThreadPool(int).schedule(Runnable,long,TimeUnit)
Handler中的postDelayed方法简单易用,可以设置延迟执行的任务,并与UI线程交互不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要与UI线程交互的定时任务Handler.postDelayed(Runnable,long)
Service + AlarmManger + BroadcastReceiver可靠稳定,可以设置精确或者不精确的闹钟,并在后台长期运行需要声明相关权限,并受系统时间影响需要在App外部执行长期且对时间敏感的定时任务AlarmManager.set(int,PendingIntent), BroadcastReceiver.onReceive(Context,Intent), Service.onStartCommand(Intent,int,int)
WorkManager可靠稳定,不受系统时间影响,并可以设置多种约束条件来执行任务需要添加依赖,并不能保证准时执行需要在App外部执行长期且对时间不敏感且需要满足特定条件才能执行的定时任务WorkManager.enqueue(WorkRequest), Worker.doWork()
RxJava简洁、灵活、支持多线程、支持背压、支持链式操作学习曲线较高、内存占用较大需要处理复杂的异步逻辑或数据流io.reactivex:rxjava:2.2.21
CountDownTimer简单易用、不需要额外的线程或handler不支持取消或重置倒计时、精度受系统时间影响需要实现简单的倒计时功能android.os.CountDownTimer
协程+Flow语法简洁、支持协程作用域管理生命周期、支持流式操作和背压需要引入额外的依赖库、需要熟悉协程和Flow的概念和用法需要处理异步数据流或响应式编程kotlinx-coroutines-core:1.5.0
使用downTo关键字和Flow实现一个定时任务1、可以使用简洁的语法创建一个倒数的范围 2 、可以使用Flow异步地发射和收集倒数的值3、可以使用onEach等操作符对倒数的值进行处理或转换1、需要注意倒数的范围是否包含0,否则可能会出现偏差 2、需要注意倒数的间隔是否与delay函数的参数一致,否则可能会出现不准确 3、需要注意取消或停止Flow的时机,否则可能会出现内存泄漏或资源浪费1、适合于需要实现简单的倒计时功能,例如显示剩余时间或进度 2、适合于需要在倒计时过程中执行一些额外的操作,例如播放声音或更新UI 3、适合于需要在倒计时结束后执行一些额外的操作,例如跳转页面或弹出对话框implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
Kotlin 内联函数的协程和 Flow 实现很容易离开主线程,样板代码最少,协程完全活用了 Kotlin 语言的能力,包括 suspend 方法。可以处理大量的异步数据,而不会阻塞主线程。可能会导致内存泄漏和性能问题。处理 I/O 阻塞型操作,而不是计算密集型操作。kotlinx.coroutines 和 kotlinx.coroutines.flow

安卓开发中如何实现一个定时任务


在安卓开发中,我们经常会遇到需要定时执行某些任务的需求,比如轮询服务器数据、更新UI界面、发送通知等等。那么,我们该如何实现一个定时任务呢?本文将介绍安卓开发中实现定时任务的五种方式,并比较它们的优缺点,以及适用场景。


1. 普通线程sleep的方式


这种方式是最简单也最直观的一种实现方法,就是在一个普通线程中使用sleep方法来延迟执行某个任务。例如:


// 创建一个普通线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 循环执行
while (true) {
// 执行某个任务
doSomething();
// 延迟10秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 启动线程
thread.start();


这种方式的优点是简单易懂,不需要借助其他类或组件。但是它也有很多缺点:



  • sleep方法会阻塞当前线程,导致资源浪费和性能下降。

  • sleep方法不准确,它只能保证在指定时间后醒来,但不能保证立即执行。

  • sleep方法受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • sleep方法不可靠,如果线程被异常终止或者进入休眠状态,会导致计时中断。


因此,这种方式只适合一般的轮询Polling场景。


2. Timer定时器


这种方式是使用Java API里提供的Timer类来实现定时任务。Timer类可以创建一个后台线程,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Timer对象
Timer timer = new Timer();
// 创建一个TimerTask对象
TimerTask task = new TimerTask() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
timer.schedule(task, 5000, 10000);


这种方式相比第一种方式有以下优点:



  • Timer类内部使用wait和notify方法来控制线程的执行和休眠,不会浪费资源和性能。

  • Timer类可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • Timer类可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • Timer类只创建了一个后台线程来执行所有的任务,如果其中一个任务耗时过长或者出现异常,则会影响其他任务的执行。

  • Timer类受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • Timer类不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些不太重要的定时任务。


3. ScheduledExecutorService


这种方式是使用Java并发包里提供的ScheduledExecutorService接口来实现定时任务。ScheduledExecutorService接口可以创建一个线程池,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个ScheduledExecutorService对象
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
service.scheduleAtFixedRate(task, 5, 10, TimeUnit.SECONDS);


这种方式相比第二种方式有以下优点:



  • ScheduledExecutorService接口可以创建多个线程来执行多个任务,避免了单线程的弊端。

  • ScheduledExecutorService接口可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • ScheduledExecutorService接口可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • ScheduledExecutorService接口受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • ScheduledExecutorService接口不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些需要多线程并发执行的定时任务。


4. Handler中的postDelayed方法


这种方式是使用Android API里提供的Handler类来实现定时任务。Handler类可以在主线程或者子线程中发送和处理消息,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Handler对象
Handler handler = new Handler();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
// 延迟10秒后再次执行该任务
handler.postDelayed(this, 10000);
}
};
// 延迟5秒后开始执行该任务
handler.postDelayed(task, 5000);


这种方式相比第三种方式有以下优点:



  • Handler类不受系统时间影响,它使用系统启动时间作为参考。

  • Handler类可以在主线程中更新UI界面,避免了线程间通信的问题。


但是这种方式也有以下缺点:



  • Handler类只能在当前进程中使用,如果进程被杀死或者进入休眠状态,会导致计时中断。

  • Handler类需要手动循环调用postDelayed方法来实现周期性地执行任务。


因此,这种方式适合一些需要在主线程中更新UI界面的定时任务.


5. Service + AlarmManager + BroadcastReceiver


这种方式是使用Android API里提供的三个组件来实现定时任务. Service组件可以在后台运行某个长期的服务;AlarmManager组件可以设置一个闹钟,在指定的时间发送一个



  • Intent,用于指定要启动的Service组件和传递一些参数。

  • AlarmManager组件可以设置一个闹钟,在指定的时间发送一个Intent给BroadcastReceiver组件。

  • BroadcastReceiver组件可以接收AlarmManager发送的Intent,并启动Service组件来执行任务。


这种方式相比第四种方式有以下优点:



  • Service组件可以在后台运行,即使进程被杀死或者进入休眠状态,也不会影响计时。

  • AlarmManager组件可以设置精确或者不精确的闹钟,根据不同的需求节省电量。

  • BroadcastReceiver组件可以在系统开机后自动注册,实现开机自启动。


但是这种方式也有以下缺点:



  • Service组件需要在AndroidManifest.xml文件中声明,并申请相关的权限。

  • AlarmManager组件受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • BroadcastReceiver组件需要在代码中动态注册和注销,避免内存泄漏。


因此,这种方式适合一些需要长期在后台执行的定时任务。


6. WorkManager


这种方式是使用Android Jetpack里提供的WorkManager库来实现定时任务. WorkManager库是一个用于管理后台任务的框架,它可以在满足一定条件下执行某个任务,并保证任务一定会被执行。例如:


// 创建一个PeriodicWorkRequest对象
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
// 获取一个WorkManager对象
WorkManager workManager = WorkManager.getInstance(this);
// 将PeriodicWorkRequest对象加入到队列中
workManager.enqueue(request);


这种方式相比第五种方式有以下优点:



  • WorkManager库不受系统时间影响,它使用系统启动时间作为参考。

  • WorkManager库可以设置多种约束条件来执行任务,例如网络状态、电量状态、设备空闲状态等。

  • WorkManager库可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • WorkManager库需要添加依赖并配置相关的权限。

  • WorkManager库不能保证任务准时执行,它会根据系统资源和约束条件来调度任务。


因此,这种方式适合一些对时间不敏感且需要满足特定条件才能执行的定时任务


您好,这是必应。我根据您的要求,从网络上搜集了一些关于如何使用RXJAVA、CountDownTimer、协程安卓实现一个定时任务的资料,并整理成了一篇文章。文章的结构如下:


7.如何使用RXJAVA实现定时任务


RXJAVA是一个基于观察者模式和函数式编程的库,它可以用来处理异步事件和数据流。RXJAVA提供了一些操作符,可以用来创建和处理定时任务。其中,interval操作符可以用来创建一个按照固定时间间隔发射整数序列的Observable,可以用来实现周期性的定时任务。例如,以下代码可以实现每隔1秒发射一个整数,从0开始:


//创建一个Observable,每隔1秒发射一个整数,从0开始
Observable timer = Observable.interval(1, TimeUnit.SECONDS);
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


如果要实现倒计时的效果,可以使用map操作符,将发射的整数转换为倒计时的秒数。例如,以下代码可以实现一个10秒的倒计时:


//创建一个Observable,每隔1秒发射一个整数,从0开始,共发射10个
Observable timer = Observable.interval(1, TimeUnit.SECONDS).take(10);
//使用map操作符,将发射的整数转换为倒计时的秒数
timer = timer.map(new Function() {
@Override
public Long apply(Long aLong) throws Exception {
//将发射的整数转换为倒计时的秒数,例如0转换为10,1转换为9,以此类推
return 10 - aLong;
}
});
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


RXJAVA的优点是可以方便地处理异步事件和数据流,可以使用各种操作符来实现复杂的逻辑,可以避免内存泄漏和线程安全


8.如何使用CountDownTimer实现定时任务


CountDownTimer是Android中提供的一个倒计时器类,它可以用来实现一个在一定时间内递减的倒计时。CountDownTimer的构造方法接受两个参数:总时间和间隔时间。例如,以下代码可以创建一个10秒的倒计时,每隔1秒更新一次:


//创建一个10秒的倒计时,每隔1秒更新一次
CountDownTimer timer = new CountDownTimer(10000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
//每隔一秒调用一次,可以用来更新UI或者执行其他逻辑
Log.d("CountDownTimer", "onTick: " + millisUntilFinished / 1000);
}

@Override
public void onFinish() {
//倒计时结束时调用,可以用来释放资源或者执行其他逻辑
Log.d("CountDownTimer", "onFinish");
}
};
//开始倒计时
timer.start();
//取消倒计时
timer.cancel();


CountDownTimer的优点是使用简单,可以直接在UI线程中更新UI,不需要额外的线程或者Handler。CountDownTimer的缺点是只能实现倒计时的效果,不能实现周期性的定时任务,而且精度受系统时间的影响,可能不够准确。


9.如何使用协程实现定时任务


协程可以用来简化异步编程和线程管理。协程是一种轻量级的线程,它可以在不阻塞线程的情况下挂起和恢复执行。协程安卓提供了一些扩展函数,可以用来创建和处理定时任务。其中,delay函数可以用来暂停协程的执行一段时间,可以用来实现倒计时或者周期性的定时任务。例如,以下代码可以实现一个10秒的倒计时,每隔1秒更新一次:


//创建一个协程作用域,可以用来管理协程的生命周期
val scope = CoroutineScope(Dispatchers.Main)
//在协程作用域中启动一个协程,可以用来执行异步任务
scope.launch {
//创建一个变量,表示倒计时的秒数
var seconds = 10
//循环执行,直到秒数为0
while (seconds > 0) {
//打印秒数,可以用来更新UI或者执行其他逻辑
Log.d("Coroutine", "seconds: $seconds")
//暂停协程的执行1秒,不阻塞线程
delay(1000)
//秒数减一
seconds--
}
//倒计时结束,打印日志,可以用来释放资源或者执行其他逻辑
Log.d("Coroutine", "finish")
}
//取消协程作用域,可以用来取消所有的协程
scope.cancel()


协程安卓的优点是可以方便地处理异步任务和线程切换,可以使用简洁的语法来实现复杂的逻辑,可以避免内存泄漏和回调。协程的缺点是需要引入额外的依赖,而且需要一定的学习成本,不太适合初学者。


10.使用kotlin关键字 ‘downTo’ 搭配Flow


// 创建一个倒计时器,从10秒开始,每秒减一
val timer = object: CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// 在每个间隔,发射剩余的秒数
emitSeconds(millisUntilFinished / 1000)
}

override fun onFinish() {
// 在倒计时结束时,发射0
emitSeconds(0)
}
}

// 创建一个Flow,用于发射倒数的秒数
fun emitSeconds(seconds: Long): Flow = flow {
// 使用downTo关键字创建一个倒数的范围
for (i in seconds downTo 0) {
// 发射当前的秒数
emit(i.toInt())
}
}


11.kotlin内联函数的协程和 Flow 实现


fun FragmentActivity.timerFlow(
time: Int = 60,
onStart: (suspend () -> Unit)? = null,
onEach: (suspend (Int) -> Unit)? =
null,
onCompletion: (suspend () -> Unit)? =
null
): Job {
return (time downTo 0)
.asFlow()
.cancellable()
.flowOn(Dispatchers.Default)
.onStart { onStart?.invoke() }
.onEach {
onEach?.invoke(it)
delay(
1000L)
}.onCompletion { onCompletion?.invoke() }
.launchIn(lifecycleScope)
}


//在activity中使用
val job = timerFlow(
time = 60,
onStart = { Log.d("Timer", "Starting timer...") },
onEach = { Log.d("Timer", "Seconds remaining: $it") },
onCompletion = { Log.d("Timer", "Timer completed.") }
)

//取消计时
job.cancel()

作者:淘淘养乐多
来源:juejin.cn/post/7270173192789737487
收起阅读 »

各平台移动开发技术对比

针对原生开发面临的问题,业界一直都在努力寻找好的解决方案,而时至今日,已经有很多跨平台框架(注意,本书中所指的“跨平台”若无特殊说明,即特指 Android 和 iOS 两个平台),根据其原理,主要分为三类: hybrid :H5 + 原生(Cordova、I...
继续阅读 »

针对原生开发面临的问题,业界一直都在努力寻找好的解决方案,而时至今日,已经有很多跨平台框架(注意,本书中所指的“跨平台”若无特殊说明,即特指 Android 和 iOS 两个平台),根据其原理,主要分为三类:


hybrid :H5 + 原生(Cordova、Ionic、微信小程序)

JavaScript 开发 + 原生渲染 (React Native、Weex)

自绘UI + 原生 (Qt for mobile、Flutter)


1、Hybrid :H5 + 原生


主要原理:
将 App 中需要动态变动的内容通过HTML5(简称 H5)来实现,
通过原生的网页加载控件WebView (Android)或 WKWebView(iOS)来加载。
WebView 中 JavaScript 与原生 API 之间就需要一个通信的桥梁,JsBridge。


**优点是:**动态内容可以用 H5开发,而H5是Web 技术栈,Web技术栈生态开放且社区资源丰富,整体开发效率高。


缺点是:

1.性能体验不佳,对于复杂用户界面或动画,WebView 有时会不堪重任。

2.其 JavaScript 依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。所以,对于 H5 不能实现的功能,就需要原生去做了。


2、JavaScript开发 + 原生渲染 (React Native、Weex)


2.1、React Native


1.React Native (简称 RN )是 Facebook 开源的跨平台移动应用开发框架。
目前支持 iOS 和 Android 两个平台。
2.React Native 基于 JavaScript,开发者可以利用已有的前端开发经验快速上手
3.开发者编写的js代码,通过 react native 的中间层转化为原生控件和操作
4.react native 运行在JavaCore中,所以不存在浏览器兼容的问题
最终,JS代码会被打包成一个 bundle 文件,自动添加到 App 的资源目录下。



JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:



  1. 为 JavaScript 提供运行环境。

  2. 是 JavaScript 与原生应用之间通信的桥梁,作用和 JsBridge 一样,事实上,在 iOS 中,很多 JsBridge 的实现都是基于 JavaScriptCore 。




而 RN 中将虚拟 DOM 映射为原生控件的过程主要分两步:



  1. 布局消息传递; 将虚拟 DOM 布局信息传递给原生;

  2. 原生根据布局信息通过对应的原生控件渲染;



RN 和 React 原理相通,React 是一个响应式的 Web 框架。



  • 开发者只需关注状态转移(数据),当状态发生变化,React 框架会自动根据新的状态重新构建UI。

  • React 框架在接收到用户状态改变通知后,会根据当前渲染树,结合最新的状态改变,通过 Diff 算法,计算出树中变化的部分,然后只更新变化的部分(DOM操作),从而避免整棵树重构,提高性能


2.2、Weex


1.Weex 是阿里的跨平台移动端开发框架,思想及原理和 React Native 类似
底层都是通过原生渲染的
2.不同是应用层开发语法 (即 DSL,Domain Specific Language):Weex 支持 Vue 语法和 Rax 语法
3.Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造
4.但相对于 React Native,它对前端开发者的要求较低
5、一定程度减少了JS Bundle的体积,使得 bundle 里面只保留业务代码。


JavaScript 开发 + 原生渲染 的方式主要优点如下



  1. 采用 Web 开发技术栈,社区庞大、有前端基础的话上手快、开发成本相对较低。

  2. 原生渲染,性能相比 H5 提高很多。

  3. 动态化较好,支持热更新。


不足:



  1. 渲染时需要 JavaScript 和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。

  2. JavaScript 为脚本语言,执行时需要解释执行 (这种执行方式通常称为 JIT,即 Just In Time,指在执行时实时生成机器码),执行效率和编译类语言(编译类语言的执行方式为 AOT ,即 Ahead Of Time,指在代码执行前已经将源码进行了预处理,这种预处理通常情况下是将源码编译为机器码或某种中间码)仍有差距。

  3. 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;

    除此之外,其控件系统也会受到原生UI系统限制,例如,在 Android 中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手。这就会导致,如果需要自定义原生渲染组件时,开发和维护成本过高。


3、自绘UI + 原生


自绘UI + 原生这种技术的思路是:
通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,
所以可以做到不同平台UI的一致性


注意,自绘引擎解决的是 UI 的跨平台问题,如果涉及其他系统能力调用,依然要涉及原生开发。这种平台技术的优点如下:



  1. 性能高;由于自绘引擎是直接调用系统API来绘制UI,所以性能和原生控件接近。

  2. 灵活、组件库易维护、UI外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到高保真和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。


不足:



  1. 动态性不足;为了保证UI绘制性能,自绘UI系统一般都会采用 AOT 模式编译其发布包,所以应用发布后,不能像 Hybrid 和 RN 那些使用 JavaScript(JIT)作为开发语言的框架那样动态下发代码。

  2. 应用开发效率低:Qt 使用 C++ 作为其开发语言,而编程效率是直接会影响 App 开发效率的,C++ 作为一门静态语言,在 UI 开发方面灵活性不及 JavaScript 这样的动态语言,另外,C++需要开发者手动去管理内存分配,没有 JavaScript 及Java中垃圾回收(GC)的机制。


Flutter 就属于这一类跨平台技术,没错,Flutter 正是实现一套自绘引擎,并拥有一套自己的 UI 布局系统,且同时在开发效率上有了很大突破。


3.1、Qt


Qt 是一个1991年由 Qt Company 开发的跨平台 C++ 图形用户界面应用程序开发框架。


在近几年,虽然偶尔能听到 Qt 的声音,但一直很弱,无论 Qt 本身技术如何、设计思想如何,但事实上终究是败了,究其原因,笔者认为主要有四:


第一:Qt 移动开发社区太小,学习资料不足,生态不好。

第二:官方推广不利,支持不够。

第三:移动端发力较晚,市场已被其他动态化框架占领( Hybrid 和 RN )。

第四:在移动开发中,C++ 开发和Web开发栈相比有着先天的劣势,直接结果就是 Qt 开发效率太低。


3.2、Flutter


Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。
Flutter 实现了一个自绘引擎,使用自身的布局、绘制系统。


2021年8月底,已经有 127K  Star,Star 数量 Github 上排名前 20 
Flutter 生态系统得以快速增长,国内外有非常多基于 Flutter 的成功案例。



1.Flutter 采用自己的渲染引擎 Skia,将 UI 渲染到画布上,具有良好的性能表现

2.如果对性能要求较高,特别是需要处理复杂动画和大量图形渲染的场景,建议选择 Flutter。

3.Flutter 则采用 Dart 语言,需要开发人员掌握新的语法和概念。

4.支持iOS、Android、Windows/MAC/Linux等多个平台,且能达到原生性能。(移动端、Web端和PC端)



Flutter和Gt对比:



  1. 生态:Flutter 生态系统发展迅速,社区非常活跃,无论是开发者数量还是第三方组件都已经非常可观。

  2. 技术支持:现在 Google 正在大力推广Flutter,Flutter 的作者中很多人都是来自Chromium团队,并且 Github上活跃度很高。另一个角度,从 Flutter 诞生到现在,频繁的版本发布也可以看出 Google 对 Flutter的投入的资源不小,所以在官方技术支持这方面,大可不必担心。

  3. 开发效率:一套代码,多端运行;并且在开发过程中 Flutter 的热重载可帮助开发者快速地进行测试、构建UI、添加功能并更快地修复错误。在 iOS 和 Android 模拟器或真机上可以实现毫秒级热重载,并且不会丢失状态。这真的很棒,相信我,如果你是一名原生开发者,体验了Flutter开发流后,很可能就不想重新回去做原生了,毕竟很少有人不吐槽原生开发的编译速度。


4、react-native、weex、flutter对比:



三种跨平台技术



react-native、weex、flutter对比


React Native:宣布放弃使用 React Native,回归使用原生技术。主要还是集中于项目庞大之后的维护困难,第三方库的良莠不齐,兼容上需要耗费更多的精力导致放弃。


hybrid:


大家都知道hybrid即为web+native的混合开发模式



优点:就是拥有了web开发的服务端发布即可更新的便捷性,Android和iOS两端可以共用代码,并且web技术已经非常成熟,开发效率也会很高。




缺点:就是众所周知的性能相比native有很大的不足,且不同机型和系统版本下的兼容性较差。



React Native、Weex 和 Flutter 是目前最为热门的混合开发框架,它们各自有着优势和特点:


1、React Native



1.React Native 是由 Facebook 推出的开源框架,拥有庞大而活跃的社区,有大量的第三方组件和库可供使用。

2.React Native 基于 JavaScript,开发者可以利用已有的前端开发经验快速上手

3.开发者编写的js代码,通过 react native 的中间层转化为原生控件和操作

4.react native 运行在JavaCore中,所以不存在浏览器兼容的问题



  1. 最终,JS代码会被打包成一个 bundle 文件,自动添加到 App 的资源目录下。



2、Weex



  • Weex 是阿里巴巴推出的开源项目,也有一个较为活跃的社区,但相对于 React Native 来说,生态系统规模稍小。



1.React Native 和 Weex 使用了 WebView 或类似的机制来渲染应用界面,性能相对较低。

2.Weex 同样基于 JavaScript,但相对于 React Native,它对前端开发者的要求较低

3.开发者可以使用Vue.js和Rax两个前端框架来进行WEEX页面开发

4.和 react native一样,weex 所有的标签也不是真实控件,JS 代码中所生成存的 dom,最后都是由 Native 端解析,再得到对应的Native控件渲染



  1. weex:一定程度减少了JS Bundle的体积,使得 bundle 里面只保留业务代码。



3、Flutter



  • Flutter 是由 Google 开发的开源框架,虽然相对较新,但也有一个迅速增长的社区和生态系统。



1.Flutter 采用自己的渲染引擎 Skia,将 UI 渲染到画布上,具有良好的性能表现

2.如果对性能要求较高,特别是需要处理复杂动画和大量图形渲染的场景,建议选择 Flutter。

3.Flutter 则采用 Dart 语言,需要开发人员掌握新的语法和概念。

4.支持iOS、Android、Windows/MAC/Linux等多个平台,且能达到原生性能。(移动端、Web端和PC端)



4、react-native、weex、flutter对比:



react-native、weex、flutter对比


React Native:宣布放弃使用 React Native,回归使用原生技术。主要还是集中于项目庞大之后的维护困难,第三方库的良莠不齐,兼容上需要耗费更多的精力导致放弃。


作者:码农君
来源:juejin.cn/post/7360586351816638501
收起阅读 »

vue反编译dist包到源码

web
最近由于公司老项目上的问题,由于项目很老,之前交接的源码包中缺少了很大一部分模块,但是现在线上的环境和dist包是正常运行的,领导希望能够手动将这部分补全,由于前期项目的不规范,缺少接口文档以及原型图,因此无法知道到底该如何补全,因此,我想着能不能通过dist...
继续阅读 »

最近由于公司老项目上的问题,由于项目很老,之前交接的源码包中缺少了很大一部分模块,但是现在线上的环境和dist包是正常运行的,领导希望能够手动将这部分补全,由于前期项目的不规范,缺少接口文档以及原型图,因此无法知道到底该如何补全,因此,我想着能不能通过dist包去反编译源码包呢,经过多方面探索发现是可行的,但是只能编译出vue文件,但是也满足基本需要了。


1.如何反编译


1.首先需要在管理员模式下打开cmd


2.找到需要编译的dist/static/js的目录下
执行完成后在该目录会看到目录下存在下面的文件名:0.7ab7d1434ffcc747c1ca.js.map,这里以0.7ab7d1434ffcc747c1ca.js.map为例,如下图:


image.png


3.全局安装reverse-sourcemap资源



npm install --global reverse-sourcemap



4.反编译
执行:reverse-sourcemap --output-dir source 0.7ab7d1434ffcc747c1ca.js.map


2.脚本反编译


上面的方式执行完毕,确实在source中会出现源码,那么有没有可能用脚本去执行呢,通过node的child_process模块中的exec方式便可以执行reverse-sourcemap --output-dir source这个命令,那么只需要拿到当前文件夹中包含.map文件即可,那么可以借助node中fs模块,递归读取文件名,并使用正则将所有.map的文件提取出来放在一个集合或数组中,在对数组进行递归循环执行reverse-sourcemap --output-dir source这个命令


2.1根据child_process模块编写执行函数



function executeReverseSourceMap(outputDir) {
// 构建 reverse-sourcemap 命令
const command = `reverse-sourcemap --output-dir source ${outputDir}`;

// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`执行命令时出错:${error.message}`);
return;
}
if (stderr) {
console.error(`命令输出错误:${stderr}`);
return;
}
console.log(`命令输出结果:${stdout}`);
});
}

2.2读取文件并匹配文件


// // 读取文件夹中的文件
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('读取文件夹时出错:', err);
return;
}
// 遍历文件
files.forEach(file => {
// 使用正则表达式匹配特定格式的文件名
const match = /^(\d+)\..+\.js\.map$/.exec(file);
if (match) {
// 如果匹配成功,将文件名存入数组
targetFiles.push(match[0]);
}
});

// 输出目标文件名数组
targetFiles.forEach(file=>{
executeReverseSourceMap(file)
})
});

2.3完整的执行代码


const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
// 文件夹路径
const folderPath = '../js';

// 存放目标文件名的数组
const targetFiles = [];
function executeReverseSourceMap(outputDir) {
// 构建 reverse-sourcemap 命令
const command = `reverse-sourcemap --output-dir source ${outputDir}`;

// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`执行命令时出错:${error.message}`);
return;
}
if (stderr) {
console.error(`命令输出错误:${stderr}`);
return;
}
console.log(`命令输出结果:${stdout}`);
});
}
// // 读取文件夹中的文件
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('读取文件夹时出错:', err);
return;
}
// 遍历文件
files.forEach(file => {
// 使用正则表达式匹配特定格式的文件名
const match = /^(\d+)\..+\.js\.map$/.exec(file);
if (match) {
// 如果匹配成功,将文件名存入数组
targetFiles.push(match[0]);
}
});

// 输出目标文件名数组
targetFiles.forEach(file=>{
executeReverseSourceMap(file)
})
});

image.png


3最终结果展示图


image.png


作者:ws_qy
来源:juejin.cn/post/7359893196439207972
收起阅读 »

Zed,有望打败 VS Code 吗?

大家好,我是楷鹏。 先说结论,不行。 Zed,又一款新起的文本代码编辑器 👉 zed.dev 今年一月二十四号正式开源,短短不到三个月,GitHub 上已经冲上 3 万 star 正如 Zed 的口号所说「Code at the speed of th...
继续阅读 »

大家好,我是楷鹏。


先说结论,不行


Zed,又一款新起的文本代码编辑器




👉 zed.dev



今年一月二十四号正式开源,短短不到三个月,GitHub 上已经冲上 3 万 star



正如 Zed 的口号所说「Code at the speed of thought 以思考的速度编码


实际体验下来,Zed 确实会比 VS Code 丝滑


⬇️ Zed



⬇️ VS Code



官网也给出了打字输入性能对比:



输入字母 z 并显示到屏幕,Zed 仅需 58 毫秒,而 VS Code 需要 97 毫秒


Zed 比 VS Code 快了 1.4 倍


在输入性能方面,Zed 胜出


其次就是 Zed 主打的另一个核心功能,多用户协同编程



额说实话,这个功能暂时想不到很好的落地使用场景。




到目前为止,Zed 仅仅是一个不错的文本编辑器。


甚至可以说,Zed 实质上并没有重大的突破,属于自嗨产品。


Zed 宣传的高性能,并没有质的飞跃,很难打到用户的马屁上。



「58毫秒」和「97毫秒」两个差距并不大


实际开发都知道,编程的瓶颈并不在于输入速度


另外是多用户协同,目前看这个场景不友好



如果是文档协同,国内的飞书文档、腾讯文档等哪一个不是佼佼者,按着 Zed 锤。


如果是代码协同,显然 Git 才是主流。




Zed 太年轻,目前很基础的 markdown 预览都没有实现


VS Code 珠玉在前,用开源、插件化形成的护城河,一开放拥有大批拥趸


而 Zed 虽然同样有插件机制,但是能指望多少人贡献呢?



《重来》一书讲到,第一次创业失败的人,第二次创业失败概率一样大


Zed 的团队原先做过 Atom 编辑器,而现在 Atom 名存实亡



团队做 Atom 失败过,而卷土重来的 Zed,还不行


Zed 大概率能够圈住一部分用户,但不会成为领域的成功。




Zed 如何能破局呢?最重要的还是要顺势而为


想想 VS Code 当时,互联网的繁荣,带动开源领域的发展,Eclipses 老旧、Jetbrains 高昂收费,前端分工细化,急需轻量的编辑器,这些都是 VS Code 的势头。


而目前 Zed 最好的势头,显而易见,就是 AI 方向


而 Zed 目前显然支持不足,仅有 Copilot 代码不足和 Chat 能力



而这些 VS Code 不仅有,而且功能更加完善。


Zed 团队应该思考下了,要做一款怎么样的编辑器,适应目前的 AI 潮流,开创新的赛道。


如果继续安于微不足道的性能提升、垂直的协同,继续在垂直赛道内卷,那我祝你成功。



作者:吴楷鹏
来源:juejin.cn/post/7359469421742473225
收起阅读 »

为什么不建议在 Vue <style> 中使用 scoped?

web
前言 标签下编写样式。不知你是否留意,在 标签下有一个属性经常出现 - scoped。你知道它起到什么作用吗?原理是怎样的?有没有什么弊端呢?今天我们就来聊聊它。 1. 什么是 scoped? scoped 顾名思义,与作用域有关,因为是设计组件样式的,所以...
继续阅读 »

前言

亲爱的小伙伴,你好!我是 嘟老板。我们使用 Vue 开发页面时,经常需要在 

收起阅读 »

是的,失业快一个月了

离职快一个月了,在这一个月里,也会在闲暇的时间记录一下思考。突然对于工作、对于求职、对于健康、对于生活有了新的认识。 3月30日 是的,我离职了,离开了我工作接近两年的公司。没有拍很多的照片,也没有习惯性的剪辑一个vlog。走的很平静,甚至工位上的东西,用一个...
继续阅读 »

离职快一个月了,在这一个月里,也会在闲暇的时间记录一下思考。突然对于工作、对于求职、对于健康、对于生活有了新的认识。


3月30日


是的,我离职了,离开了我工作接近两年的公司。没有拍很多的照片,也没有习惯性的剪辑一个vlog。走的很平静,甚至工位上的东西,用一个装零食的塑料袋就可以全部的拿走。


那个下午,我回忆了这两年的很多的事情。从一线回来,先租房子,然后等着毕业后的7月办理入职;随后研究起AI,做了很多的demo。后来搬到了楼上,写着用古老技术写的项目,楼下部分伙伴也相继离开了。勉强熬过了23年的冬天,年后我也得走了。


经过了漫长的离职流程,一切就绪,就差一个离职证明。


正好在那天晚上回来刷着抖音的时候,听到了飞书要裁员的消息,大约1000多人要面临着失业。


在还没毕业的大学时光里,我就满怀着焦虑。考研爹妈不支持,生活费没有着落,就业压力大。实在是没了办法,四处的海投,后来去了上海,再后来去了一线。再后来,在一线面临着裁员,又一下子回到了武汉。


回武汉-到现在的住所路上


想着过去的三年,真的像是一场梦。熬过了疫情,熬过了“二阳”,任何时候,互联网上都充斥着“互联网寒冬“的声音。而我,也在将近两年的摆烂之后,不得不面对这个残酷、血淋淋的现实。


人很容易陷入自我怀疑和沮丧的境地,在激情和麻木中徘徊。我常常在一些加班到很晚的晚上,看着车窗外的街道问自己:这一切的努力为的是什么?为了梦想、为了走出农村,为了体面的活着,仿佛内心燃起了激情。然而,第二天又得从床上爬起来,继续按部就班的上班,挤着电梯,做着乏味的工作。晚上下班了,又回来坐在电脑桌前。周而复始。


年与时驰,意与日去,遂成枯落,多不接世,悲守穷庐,将复何及!终究将逃不过30岁!我也时常遥望自己的30岁,30岁是什么样子的?是有一个爱的人,有了自己的孩子,有了自己幸福的家;还是一个人在异乡狭小的出租屋等下,听着伤感的音乐,喝着咖啡抽着烟。


这次的离职给了我很多的思考时间,对于这个行业、对于接下来的选择。


四月上旬


正式开始了失业,一直呆在家里,偶尔会在晚上出去跑步。互联网行情依旧是差得很,很多的招聘平台都是已读不回,当然还有很多奇葩的HR。加上AI大模型越来越多,甚至说阿里都要用AI来代替20%的人工工作,需要传统的程序员的岗位将越来越少。于是,我开始分析各种调查报告、行业报告。最后锁定在了这几个行业:新能源领域 新材料 AIGC 化工 电驱动工艺。虽然这段时间我也没有找到合适的行业的工作,但是我总结了一个找工作的铁律:跟着资本选行业


2023Q3国内小赛道股权投资势头:来源IT桔子


投资机构基本上都是嗅觉到最新的商机的,他们的资本注入将会给新行业、新领域或者企业带来新的活力。


四月中旬


在武汉跑了马拉松之后,我想回到家乡去也跑一下。这次我全程vlog记录了:2024十堰马拉松健康跑纪实。来回两天,时间的安排上也妥妥的。


回来之后的第二天,就要去体检了。不幸的是:我的胆囊出现了息肉,而且以不可控的速度在增长。因为这几年我对于我的身体都密切的关切,每年都会体检,息肉出现在我回武汉的这两年里。


拿着最近三年的检查结果,就往我最近的军医医院去。门诊的医生建议我直接做手术将它切除了。一下子慌了神,但是又在担心它的癌变,担心每次饭后的胸闷胸胀。一番纠结,最终我入院接受手术了。


我的病房和病床


第一次手术室和重症监护室,你是什么感觉?如果问我,我会说我很害怕,害怕到浑身颤抖,害怕到忍不住哭了。


换上病号才穿的衣服,我进入了手术室,躺在了手术台上,随着手术室的大门关上,看着手术室穿着手术服的医生,我整个身体忍不住的颤抖。我不能想象锋利的手术刀割开我的肚皮,然后找到我的胆,割下来,再缝合好。上高中的时候,我就经历了不打麻药直接用针线缝补伤口(当然,那是医疗条件不是很方便的时候)。解开衣服,闭上眼睛,不知道麻药怎么注入我的身体的。我在迷迷糊糊中没了知觉。


当我再次被叫醒的时候,是我的手术已经完成了。医生告诉我:“xxx,手术做完了,手术做的很顺利。”我被几个人抱上了监护室的病床上,我的伙伴也在我身边,我才知道手术了两个多小时。


在监护室哭了两小时,这是这么多年来第一次哭,而且哭的这么久。还好诺大的监护室只有我和一个监护的护士,不至于丢掉我男子汉大丈夫的面子。她耐心的帮我多次擦干泪水,说我的眼睛都红了。


后来的两天就都是输液了。不敢咳嗽,还好也有雾化的设备。没有了痛觉,因为脖子上的止痛泵帮我麻痹了神经。


四月下旬


最后也如期的出院了,每天除了按时吃清水面条+喝药+换药外,好像也没什么事情可以做的。屏蔽了朋友圈,很少看消息,坐累了就躺着休息。一次手术,几乎花光了我三年来的积蓄,再一次是那么的怕死惜命。想着被资本家誉为996福报、单休、PUA,觉得人命在资本家面前看起来是那么的卑贱。如果资本比不上别人,那就请你一定要健康。


闲暇的时间也会去看看纪录片,楼下散步走走。


《大秦赋》-始皇嬴政


看了《大秦赋》,当然我追剧是倒着看,想起了贾谊的《六国论》,有时间的话,会出一期文章。


还有大美的天山雪山、有趣的万物生灵。


雪山


黑颈鹤



觉得地球是那么的奇妙,生命是那么的奇妙,这么多的奇妙才是神秘、才是美好、才是丰富的存在。



这个月的经历,算得上是跌宕起伏。总之:希望每一个职场人或者是同行,一定要爱惜自己的身体,按时吃饭、早睡早起。不要畏惧失业,不为各种被贩卖的焦虑而焦虑,更多的是爱护自己,珍惜眼前,享受生活。


作者:shigen01
来源:juejin.cn/post/7360711693359988786
收起阅读 »

完了,安卓项目代码被误删了......

写在前面 这是一个朋友的经历记录了下来。朋友开发完了一个公司的app,过了一段时间,在清理电脑空间的时候把该app的项目目录给删了,突然公司针对该app提出了新的需求,这不完了?幸好有之前打包好的apk,所以可以通过逆向去弥补..... Apk文件结构 apk...
继续阅读 »

写在前面


这是一个朋友的经历记录了下来。朋友开发完了一个公司的app,过了一段时间,在清理电脑空间的时候把该app的项目目录给删了,突然公司针对该app提出了新的需求,这不完了?幸好有之前打包好的apk,所以可以通过逆向去弥补.....


Apk文件结构


apk的本质是压缩包,apk解压后会生成下列所示文件夹




  • Assets:存放的是不会被编译处理的文件。

  • Lib:存放的是一些so库,native库文件

  • META-INF:存放的是签名信息,用来保证apk的完整性和系统安全。防止被重新修改打包。

  • res:存放的资源文件,图片、字符串、颜色信息等

  • AndroidManifest.xml:是Android程序的配置文件,权限和配置信息

  • Classes.dex:Android平台下的字节码文件。

  • Resources.arcs:编译后的二进制资源文件,用来记录资源文件和资源ID的关系


逆向


这里用了逆向神器——jdax。支持命令行和图形化界面,地址如下:


github.com/skylot/jadx…


下载好之后,直接解压后打开exe,将apk文件拖入进去就可以,图形化界面,更方便搜索查看,可以看到下列文件夹



先看资源文件,asset存放的是静态资源文件,一般不会被压缩,但是会占用更多的安装包空间,res文件是由Android目录下的res进行压缩得到的,所以里面的文件直接解压打开会乱码,在这个工具里打开是正常的。



话不多说直接找回我的代码,找到我写的一个类,拷贝回去,补齐里面缺失的资源文件和一些新增的接口,跟着自己之前开发的流程,一步一步的找回去,发现其中局部变量在编译的时候都被进行了优化,以便缩小体积



找到我写的最核心的代码,发现被混淆了,我在代码里没有进行代码混淆配置,还是被一些工具给我进行了混淆,只能凭借着记忆去还原了。



终于进行了不到一天多的时间,把所有的代码还原了,然后自测通过。


代码混淆


现在其实也可以看到自己的程序是非常危险的,任何人拿到我的apk进行一个逆向就可以看到大概的逻辑。所以要在Android中进行代码混淆的配置。


项目中如果含有多个module时,在主app中设置了混淆其他module都会混淆,在build.gradle中配置下列代码 proguardFiles getDefaultProguardFile


android {
...
buildTypes {
release {
minifyEnabled true // 开启代码混淆
zipAlignEnabled true // 开启Zip压缩优化
shrinkResources true // 移除未被使用的资源
//混淆文件列表,混淆规则配置
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
...
}

这里代表的是混淆文件,我们在项目里找到proguard-rules.pro,这里就是混淆规则,规定了哪些代码进行混淆,哪些不进行混淆。混淆规则一般有以下几点:



  • 混淆规则,等级、预校验、混淆算法等

  • 第三方库

  • 自定义类、控件

  • 本地的R类

  • 泛型 注解 枚举类等


示例配置如下:



#压缩等级,一般选择中间级别5
-optimizationpasses 5
#包名不混合大小写
-dontusemixedcaseclassnames
#不去忽略非公共的库类
-dontskipnonpubliclibraryclasses
#优化 不优化输入的类文件
-dontoptimize
#预校验
-dontpreverify
#混淆时采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#保护注解
-keepattributes *Annotation*
#保持下面的类不被混淆(没有用到的可以删除掉,比如没有用到service则可以把service行删除)
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.preference.Preference
-keep public class * extends android.support.v4.app.FragmentActivity
-keep public class * extends android.support.** { *;}
#如果引用了v4或者v7包
-dontwarn android.support.*
#忽略警告(开始应该注释掉,让他报错误解决,最后再打开,警告要尽量少)
-ignorewarnings
#####################记录生成的日志数据,gradle build时在本项目根目录输出################
#混淆时是否记录日志
-verbose
#apk 包内所有class 的内部结构
-dump class_files.txt
#为混淆的类和成员
-printseeds seeds.txt
#列粗从 apk 中删除的代码
-printusage unused.txt
#混淆前后的映射
-printmapping mapping.txt
#####################记录生成的日志数据,gradle build时在本项目根目录输出结束################

#本地的R类不要被混淆,不然就找不到相应的资源
-keep class **.R$*{ public static final int *; }

#保持内部类,异常类
-keepattributes Exceptions, InnerClasses
#保持泛型、注解、源代码之类的不被混淆
-keepattributes Signature, Deprecated, SourceFile
-keepattributes LineNumberTable, *Annotation*, EnclosingMethod

#保持自定义控件不被混淆(没有就不需要)
-keepclasseswithmembers class * extends android.app.Activity{
public void *(android.view.View);
}
-keepclasseswithmembers class * extends android.supprot.v4.app.Fragment{
public void *(android.view.View);
}
#保持 Parcelable 不被混淆(没有就不需要)
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#保持 Serializable 不被混淆(没有就不需要)
-keepnames class * implements java.io.Serializable

-keepclassmembers class * {
public void *ButtonClicked(android.view.View);
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

再次打包,然后打开apk后就会发现包名类名变量名都变得很奇怪。




这样代码混淆就完成了。


作者:银空飞羽
来源:juejin.cn/post/7360903734853730356
收起阅读 »

JavaScript注释:单行注释和多行注释详解

为了提高代码的可读性,JS与CSS一样,也提供了注释功能。JS中的注释主要有两种,分别是单行注释和多行注释。在编程的世界里,注释是那些默默无闻的英雄,它们静静地站在代码的背后,为后来的维护者、为未来的自己,甚至是为那些偶然间翻阅你代码的开发者提供着不可或缺的信...
继续阅读 »

为了提高代码的可读性,JS与CSS一样,也提供了注释功能。JS中的注释主要有两种,分别是单行注释和多行注释。

在编程的世界里,注释是那些默默无闻的英雄,它们静静地站在代码的背后,为后来的维护者、为未来的自己,甚至是为那些偶然间翻阅你代码的开发者提供着不可或缺的信息。

Description


今天,我们就来深入探讨JavaScript中的注释,让我们的代码不仅能够运行,还能够“说话”。

一、什么是JavaScript注释

JavaScript注释是用来解释代码的,不会被浏览器执行。它们可以帮助其他开发者理解代码的功能和目的。

注释就像是给代码穿上了一件华丽的外衣,让我们的代码更加优雅、易读。如下图中的例子所示:


Description


在JavaScript中,有两种类型的注释:单行注释和多行注释。下面分别讲解这两种注释的含义和使用。


二、JavaScript注释的种类

1、单行注释

单行注释: 使用两个斜杠(//)开头,后面的内容直到该行结束都被视为注释。例如:

// 这是一个单行注释
console.log("Hello, World!"); // 这也是一个单行注释

它适用于简短的注释,比如对某一行代码的快速说明。

2、多行注释

多行注释: 使用斜杠星号(/)开头,星号斜杠(/)结尾,中间的内容都被视为注释。

例如:

/*
这是一个多行注释
可以跨越多行
*/

console.log("Hello, World!");

这种注释可以跨越多行,适合用于函数描述、复杂的算法解释或者临时屏蔽代码块。

注意: 在HTML文件中,如果需要将JavaScript代码嵌入到<script>标签中,可以使用以下方法来添加多行注释:

<script>
<!--
这是一个多行注释
可以跨越多行
-->

console.log("Hello, World!");
</script>


三、JavaScript注释的作用

1、解释代码功能:

通过注释,我们可以解释代码的功能和作用,让其他程序员更容易理解我们的代码。

// 这是一个求和函数
function sum(a, b) {
return a + b;
}

2、 标记代码状态:

我们可以使用注释来标记代码的状态,例如TODO、FIXME等,提醒自己或其他程序员注意这些问题。

// TODO: 优化这个函数的性能
function slowFunction() {
// ...
}

3、临时禁用代码:

当我们需要暂时禁用某段代码时,可以使用注释将其包裹起来,而不是直接删除。

// function oldFunction() {
// // ...
// }


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


四、如何写好注释

注释虽好,但过多或不当的注释反而会成为阅读代码的障碍。我们在写注释时也要注意以下几点:

  • 简洁明了: 注释应该简单明了,能够快速传达信息。

  • 适当使用: 不要过度使用注释,只有在必要的时候才添加。

  • 保持一致: 在团队开发中,要遵循统一的注释规范,以便于团队成员之间的沟通。

  • 适时更新: 随着代码的变更,记得更新相关的注释。

JavaScript注释是我们编程过程中的得力助手,它们不仅能够帮助我们更好地理解代码,还能提高代码的可读性和可维护性。让我们一起学会使用注释,让我们的代码更加精彩!

收起阅读 »

记一次划线需求的实现方式

web
1 背景 1.1 需求背景 前半年接了一个划线需求,一直没空总结下。微信公众号和微信读书等读书类应用都有此功能,但前者只对部分用户开放了,并且没有长期运营。 这次只谈下划线技术实现本身。 1.2 功能详叙 用户可以对文章句子进行长按选区,过程中弹出面板,且面...
继续阅读 »

1 背景


1.1 需求背景


前半年接了一个划线需求,一直没空总结下。微信公众号和微信读书等读书类应用都有此功能,但前者只对部分用户开放了,并且没有长期运营。


这次只谈下划线技术实现本身。


1.2 功能详叙



  1. 用户可以对文章句子进行长按选区,过程中弹出面板,且面板位置动态变化,点击点赞按钮后生成划线;

  2. 点击划线句子默认选中,并弹出面板,显示所点句子点赞量;

  3. 划线句子可以合并,规则是选取句子和已赞过的句子有交叉时合并为一条新的首尾更长的句子,选取的句子被包含在已赞过的句子中时显示点赞量,选取句子包含了已赞句子则删掉已赞句子并对新句子点赞量加一;

  4. 点赞量超过3的句子才外显;

  5. 他人的划线句子用虚线展示,自己的划线用实线展示;

  6. 小流量,用户量由小至大,过程中可以对外显策略微调;


1.3 竞品


可以看到,微信公众号划线过程会弹出一个灰色面板,面板上有划线(这次需求改为了点赞)按钮:


image.png
image.png


2 关键逻辑


这个需求乍看可能会觉得没那么复杂,但细细分析后会发现有较长的交互流程和逻辑链:


image.png


其中有几个会影响整体逻辑的关键点需要关注:



  1. 渲染划线的方式:插入 dom 标签还是绝对定位或其他方式;

  2. 监听划线选取的交互事件选择 selectionchange 还是 touchend;

  3. 整个交互过程分为哪些部分;

  4. 怎么判断新划线和其他划线的位置关系,怎么合并或删除;

  5. 数据结构怎么设计;

  6. 怎么将划线序列化;

  7. 怎么将数据反序列化成划线;


3 详细设计


3.1获取划线


window 上提供了 Selection 对象,它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。调用 Selection.toString() 方法会返回被选中区域中的纯文本。


var selObj = window.getSelection();
var range = selObj.getRangeAt(0);

Selection 对象所对应的是用户所选择的 ranges(区域)


var selObj = window.getSelection();
var range = selObj.getRangeAt(0);

3.2 划线渲染方式


渲染划线有两种方式:


1 在划线range对象的首尾dom的位置,插入线段的dom标签;
优点:划线的点击不需要计算点击位置,直接在插入dom上绑定事件即可;
缺点:对原页面结构有入侵,改变了dom结构,可能引发其他问题;


2 绝对定位,相对于整篇文章;
优点:完全增量,对原页面没有入侵;
缺点:需要计算点击位置;


我选择的第二种,原因是为了不影响原有页面逻辑,这样项目风险也是最小的。那么具体怎么实现呢?


range对象提供了一个 getClientRects 方法,表示 range 在屏幕上所占的区域。这个列表相当于汇集了范围中所有元素调用 Element.getClientRects() 方法所得的结果。用拿到的位置信息进行绝对定位即可。


rectList = range.getClientRects()

我们把用户所有划线range对象和其产生的位置信息都存入到一个list中。


pageRangeList.push({
range,
rectInfo
})

3.3 交互过程


我们分析下整个交互过程:
有两个主要的交互事件,一是点击划线,二是滑动选区。


3.3.1 点击事件


处理点击事件,我们拿到点击事件的位置,和存放的 pageRangeList 进行位置比较,得出用户点击的是哪个range对象。


// 点击事件
const {pageX, pageY} = event;
const lineHeight = 23;

const {range} = rectInfo.some(rect => {
const {left, right, realY} = rect;
return pageX < left && pageX > left && pageY > realY
})

this.selection.removeAllRanges();
this.selection.addRange();

3.3.2 选区事件


选区事件我选择的是 selectionchange,需要加防抖和节流处理。


如果你选的是 touchend 安卓系统会点问题。


3.3.3 比较位置关系


如第2点核心逻辑中所说,在滑词过程中,需要比较位置关系,我们直接使用Range.compareBoundaryPoints方法即可。返回值 0 、-1 、1 分别代表不同的位置关系。


const compare = range.compareBoundaryPoints(Range.START_TO_END, sourceRange);

3.4 序列化与反序列化


序列化是整个需求的重点,序列化是指将交互产生的划线转化成某种数据结构能存储在服务器上,反序列化是指如何将server下发的序列化数据转化成非序列化的划线。


两者是两个相反的过程,当我们确定了序列化方案,其实也就知道了反序列化了。


3.4.1 序列化


方案一,识别段落


刚开始我观察文章都会拆分段落,如按P标签或某一个class类名来划分段落,于是计划用段落信息,告诉 server 划线在第几段的第几个字。


interface data {
startParagraph: 1,
startIndex: 22,
endParagraph: 2,
endIndex: 15
}

但后来发现有一些抓取的文章根本内容很混乱,且没有特定的段落,强行识别复杂度极高。(如下图)所以此方案不可行。


image.png


方案二,全文第几个字


前面的方案不可能的原因是,识别段落信息复杂度不可控,那么我们可以绕过段落信息,去识别全文第几个字。


interface data {
startCharacters: 122,
endCharacters: 166
}

具体方式是用Range,圈选文章开头到当前dom,形成一个新Range,再调用range.toString查看字数即可。


const range = new Range();
range.setStart(pageContainer, 0);
range.setEnd(curEndContainer, endOffset);
const str = range.toString();

3.4.2 反序列化


这里注意,由于 Javascript 在大多宿主环境下没有递归的尾调用优化,所以我采用了手动创建栈来进行 dfs:


    dfs({
node = this.content,
}) {
const stack = [];
if (!node) {
return;
}

stack.push(node);

while (stack.length) {
const item = stack.pop();

const children = item.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
stack.push( [i]);
}
}
}

作者:雨默默下了一整夜
来源:juejin.cn/post/7344993022075813938
收起阅读 »

⚡聊天框 - 微信加载历史数据的效果原来这样实现的

web
前言 我记得2021年的时候做过聊天功能,那时业务也只限微信小程序 那时候的心路历程是: 卧槽,让我写一个聊天功能这么高大上?? 嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧 然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,...
继续阅读 »

前言


我记得2021年的时候做过聊天功能,那时业务也只限微信小程序


那时候的心路历程是:



卧槽,让我写一个聊天功能这么高大上??


嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧


然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,真的能做到微信的那种效果吗



然后一堆调研加测试,总算在小程序中查看历史记录没那么鬼畜了,但是总是感觉不是最佳解决方案。



那时打出的子弹,一直等到现在击中了我



最近又回想到了这个痛点,于是网上想看看有没有大佬发解决方案,结果还真被我找到了。


image.png


正文开始


1,效果展示


上才艺~~~


222.gif


2,聊天页面


2.1,查看历史聊天记录的坑


常规写法加载历史记录拼接到聊天主体的顶部后,滚动条会回到顶部、不在原聊天页面


直接上图


111.gif


而我们以往的解决方案也只是各种利用缓存scroll的滚动定位把回到顶部的滚动条重新拉回加载历史记录前的位置,好让我们可以继续在原聊天页面。


但即使我们做了很多优化,也会有安卓和苹果部分机型适配问题,还是不自然,可能会出现页面闪动


其实吧,解决方案只有两行css代码~~~


2.2,解决方案:flex神功


想优雅顺滑的在聊天框里查看历史记录,这两行css代码就是flex的这个翻转属性


dispaly:flex;
flex-direction: column-reverse

灵感来源~~~


333.gif


小伙伴可以看到,在加载更多数据时



滚动条位置没变、加载数据后还是原聊天页面的位置



这不就是我们之前的痛点吗~~~


所以,我们只需要翻转位置,用这个就可以优雅流畅的实现微信的加载历史记录啦


flex-direction: column-reverse


官方的意思:指定Flex容器中子元素的排列方向为列(从上到下),并且将其顺序反转(从底部到顶部)


如果感觉还是抽象,不好理解的话,那就直接上图,不加column-reverse的样子


image.png


加了column-reverse的样子


image.png


至此,我们用column-reverse再搭配data数据的位置处理就完美解决加载历史记录的历史性问题啦


代码放最后啦~~~


2.3,其他问题


2.3.1,数据过少时第一屏展示


因为用了翻转,数据少的时候会出现上图的问题


只需要.mainArea加上height:100%


然后额外写个适配盒子就行


flex-grow: 1; 
flex-shrink: 1;

image.png


2.3.2,用了scroll-view导致的问题


这一part是因为我用了uniappscroll-view组件导致的坑以及解决方案,小伙伴们没用这个组件的可忽略~~~


如下图,.mainArea使用了height:100%后,继承了父级高度后scroll-view滚动条消失了。


image.png


.mainArea去掉height:100%后scroll-view滚动条出现,但是第一屏数据过多时不会滚动到底部展示最新信息


image.png


解决方案:第一屏手动进行滚动条置顶


scrollBottom() {
if (this.firstLoad) return;
// 第一屏后不触发
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select("#mainArea")
.boundingClientRect((data) => {
console.log(data);
if (data.height > +this.chatHeight) {
this.scrollTop = data.height; // 填写个较大的数
this.firstLoad = true;
}
})
.exec();
});
},

3,服务端


使用koa自己搭一个websocket服务端


3.1 服务端项目目录


image.png


package.json


{
"name": "websocketapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.14.2",
"koa-router": "^12.0.1",
"koa-websocket": "^7.0.0"
}
}


koa-tcp.js


const koa = require('koa')
const Router = require('koa-router')
const ws = require('koa-websocket')

const app = ws(new koa())
const router = new Router()

/**
* 服务端给客户端的聊天信息格式
* {
id: lastid,
showTime: 是否展示时间,
time: nowDate,
type: type,
userinfo: {
uid: this.myuid,
username: this.username,
face: this.avatar,
},
content: {
url:'',
text:'',
w:'',
h:''
},
}
消息数据队列的队头为最新消息,以次往下为老消息
客户端展示需要reverse(): 客户端聊天窗口最下面需要为最新消息,所以队列尾部为最新消息,以此往上为老消息
*/



router.all('/websocket/:id', async (ctx) => {
// const query = ctx.query
console.log(JSON.stringify(ctx.params))
ctx.websocket.send('我是小服,告诉你连接成功啦')
ctx.websocket.on('message', (res) => {
console.log(`服务端收到消息, ${res}`)
let data = JSON.parse(res)
if (data.type === 'chat') {
ctx.websocket.send(`我也会说${data.text}`)
}
})
ctx.websocket.on('close', () => {
console.log('服务端关闭')
})
})

// 将路由中间件添加到Koa应用中
app.ws.use(router.routes()).use(router.allowedMethods())

app.listen(9001, () => {
console.log('socket is connect')
})



切到server目录yarn


然后执行nodemon koa-tcp.js


没有nodemon的小伙伴要装一下


image.png


代码区


完整项目Github传送门


聊天页面的核心代码如下(包含data数据的位置处理和与服务端联动)



完结


这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。


到这里,想给小伙伴分享两句话



现在搞不清楚的事,不妨可以先慢下来,不要让自己钻到牛角尖了


一些你现在觉得解决不了的事,可能需要换个角度



欢迎转载,但请注明来源。


最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。


image.png


作者:尘落笔记
来源:juejin.cn/post/7337114587123335180
收起阅读 »

基于装饰器——我劝你不要在业务代码上装逼!!!

web
基于装饰器——我劝你不要在业务代码上装逼!!! 装饰器模式的定义 在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。 而装饰器模式的出现改变的这...
继续阅读 »

基于装饰器——我劝你不要在业务代码上装逼!!!


装饰器模式的定义



  • 在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。

  • 而装饰器模式的出现改变的这种方式,装饰器模式可在不改变现有对象解构的基础上,动态地为对象添加功能


传统的 JavaScript 装饰器


var plane = {
fire: function () {
console.log("普通子弹");
},
};

var missleDecorator = function () {
console.log("发射导弹");
};

var atomDecorator = function () {
console.log("发射原子弹");
};

var fire1 = plane.fire;
plane.fire = function () {
fire1();
missleDecorator();
};

var fire2 = plane.fire;
plane.fire = function () {
fire2();
atomDecorator();
};

plane.fire();
/**
普通子弹
发射导弹
发射原子弹
*/



装饰函数



  • 在 JavaScript 中,几乎一切都是对象,其中函数也被成为对象,在平时的开发中,我们都在和函数打交道。在给对象扩展属性和方法时,很难在不改动原功能函数的情况下,给函数添加一些额外的功能,最直接的粗暴方式就是直接改写函数,但这是最差的方式,这违反了开放——封闭原则。

  • 如下:


function a(){
console.log(1);
}

// 改写:
function a(){
console.log(1);

// 新功能
console.log(2);
}



  • 很多时候,我们都不想去触碰之前的一些代码,但需要添加功能,所以如果需要在不改变原功能函数的情况下,给函数添加功能。可使用以下方式:

  • 要想完美的给函数添加功能,可使用 AOP 来装饰函数

    • AOP:一种编程规范,通过将关注点从主业务逻辑中剥离出来并单独处理,以此来提高代码的可读性和重用性。



  • 如下:


Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}

// before 和 after 函数都接收一个函数作为参数,这个函数也就是新添加的函数(里面也就是要添加的新功能逻辑)。
// 而before 和 after 函数区别在于在是原函数之前执行还是之后执行。



  • AOP 函数的使用


Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}

var o1 = function(){
console.log('1');
}
var o2 = function(){
console.log('2');
}
var o3 = function(){
console.log('3');
}

var desctor = o1.after(o2);
desctor = desctor.after(o3);
desctor(); // 1 2 3
/**
var desctor = o1.after(o2);
desctor = desctor.after(o3);
desctor();
1
2
3

var desctor = o1.before(o2);
desctor = desctor.before(o3);
desctor();
3
2
1

var desctor = o1.after(o2);
desctor = desctor.before(o3);
desctor();
3
1
2


var desctor = o1.before(o2);
desctor = desctor.after(o3);
desctor();
2
1
3
*/



AOP的应用


1.数据上报



  • 在程序开发中,当业务代码开发完后,在结尾时需要加很多的日志上报的代码,普遍我们会去改已经之前封装好的功能函数。其实这并不是一个好的方式,那如何在不直接修改之前函数的基础上添加日志上报功能呢?

  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP日志上报</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<button class="btn" @click="handler">Button</button>
<p id="tt">{{message}}</p>
</div>
</body>
</html>
<script type="text/javascript">
// log report
const { reactive, ref, createApp } = Vue;
const app = createApp({
setup() {
const message = ref("未点击");
const count = ref(0);

Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function handler() {
message.value = `已点击${++count.value}`;
}

handler = handler.after(log);

function log() {
message.value = message.value + "-----> log reported";
console.log("log report");
}

return {
message,
handler,
};
},
});
app.mount("#app");
</script>


2.动态参数



  • 在日常开发中,我们需要向后台接口发送请求来获取信息,例如传参如下。业务在后续时需要添加新参数,每个接口需要把 token 值也一并传过去, 普遍我们会去改封装的请求方法,把 token 参数添加进去。但我们直接修改封装好的请求方法不是好的行为,那我们可使用上面说过的 AOP 方式来改进。


{
name: 'xxxx',
password: 'xxxx',
}



  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP动态参数</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">{{message}}</div>
</body>
</html>

<script type="text/javascript">
const { reactive, ref, createApp } = Vue;
const app = createApp({
setup() {
const message = ref("empty params");
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function ajax(type, url, params){
message.value = `${type} ----> ${url} -----> ${JSON.stringify(params)}`;
}

function getToken(){
// do something
return 'token';
}

ajax = ajax.before(function(type, url, params){
params.token = getToken();
})

ajax('get', 'https://www.baidu.com/userinfo', {name: 'se', password: 'xsdsd'});
return {
message,
};
},
});
app.mount("#app");
</script>


3.表单校验



  • 在日常开发中,我们经常要去做校验表单数据,通常的方式是在功能函数中进行判断处理或将判断逻辑提取为一个函数的方式。但这种方式其实是与功能性函数相混合,且校验逻辑与功能性函数有耦合关系。那我们可使用 AOP 方式来改进。

  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP表单验证</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<label>
姓名:
<input
type="text"
v-model="data.name"
placeholder="请输入姓名"
/>

</label>
<label>
密码:
<input
type="text"
v-model="data.pass"
placeholder="请输入密码"
/>

</label>
<p v-if="data.name || data.pass">{{data.name + '/' + data.pass}} ----after-----> {{data.message}}</p>
<hr>
<button @click="submitBtn">submit</button>
</div>
</body>
</html>

<script type="text/javascript">
const { reactive, ref, createApp, watchEffect } = Vue;
const app = createApp({
setup() {
const data = reactive({
name: "",
pass: "",
message: "",
});

Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
if (beforeFn.apply(this, arguments) === false) return;
return _self.apply(this, arguments);
};
};

function valid() {
if (!data.name || !data.pass) {
alert("用户名或密码不能为空");
return false;
}
}

function formSubmit() {
console.log("data ------>", data);
data.message = `${data.name} ------- ${data.pass}`;
}

formSubmit = formSubmit.before(valid);

function submitBtn() {
formSubmit();
}
return {
data,
submitBtn,
};
},
});
app.mount("#app");
</script>


装饰器模式的优缺点



  • 优点:

    1. 扩展性强:装饰器模式允许在不修改现有代码的情况下,动态地添加新功能或修改现有功能。通过使用装饰器,可以在运行时按需组合和堆叠装饰器对象,实现各种组合方式,从而实现更多的功能扩展。

    2. 遵循开闭原则:装饰器模式通过添加装饰器类来扩展功能,而不是修改现有的代码。这样可以保持原有代码的稳定性,符合开闭原则,即对扩展开放,对修改关闭。

    3. 分离关注点:装饰器模式将功能的扩展和核心功能分离开来,每个装饰器类只关注单一的额外功能。这样可以使代码更加清晰、可读性更高,并且容易维护和测试。



  • 缺点:

    1. 增加复杂性:使用装饰器模式会增加额外的类和对象,引入了更多的复杂性和层次结构。这可能使代码变得更加复杂,理解和调试起来可能更加困难。

    2. 潜在的性能影响:由于装饰器模式涉及多个对象的组合和堆叠,可能会引入额外的运行时开销,对性能产生一定的影响。尤其是当装饰器链较长时,可能会导致性能下降。




装饰器模式的适用场景



  1. 动态地扩展对象功能:当需要在运行时动态地为对象添加额外的功能或责任时,装饰器模式是一个很好的选择

  2. 遵循开闭原则:如果你希望在不修改现有代码的情况下扩展功能,而且要保持代码的稳定性,装饰器模式是一个合适的解决方案。

  3. 分离关注点:当你希望将不同的功能分离开来,使每个功能都有自己独立的装饰器类时,装饰器模式是有用的。每个装饰器只关注单一的额外功能,这样可以使代码更加清晰、可读性更高,并且容易维护和测试。

  4. 多层次的功能组合:如果你需要实现多个功能的组合,而且每个功能都可以灵活选择是否添加,装饰器模式可以很好地满足这个需求。通过堆叠多个装饰器对象,可以按照特定的顺序组合功能,实现各种组合方式。

  5. 继承关系的替代方案:当你面临类似于创建大量子类的情况时,装饰器模式可以作为继承关系的替代方案。通过使用装饰器模式,可以避免创建过多的子类,而是通过组合不同的装饰器来实现不同的功能组合。


Tip: 文章部分内容参考于曾探大佬的《JavaScript 设计模式与开发实践》。文章仅做个人学习总结和知识汇总

作者:南囝coding
来源:juejin.cn/post/7272869799960559679
收起阅读 »

还在封装 xxxForm,xxxTable 残害你的同事?试试这个工具

web
之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。 多图预警。。。 以管理后台一个列表页为例 选择对应的模板 截图查询区域,使用 OCR 初始...
继续阅读 »

之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。


多图预警。。。


以管理后台一个列表页为例



选择对应的模板



截图查询区域,使用 OCR 初始化查询表单的配置



截图表头,使用 OCR 初始化 table 的配置



使用 ChatGPT 翻译中文字段



生成代码



效果


目前我们没有写一行代码,就已经达到了如下的效果



下面是一部分生成的代码


import { reactive, ref } from 'vue'

import { IFetchTableListResult } from './api'

interface ITableListItem {
/**
* 决算单状态
*/

settlementStatus: string
/**
* 主合同编号
*/

mainContractNumber: string
/**
* 客户名称
*/

customerName: string
/**
* 客户手机号
*/

customerPhone: string
/**
* 房屋地址
*/

houseAddress: string
/**
* 工程管理
*/

projectManagement: string
/**
* 接口返回的数据,新增字段不需要改 ITableListItem 直接从这里取
*/

apiResult: IFetchTableListResult['result']['records'][0]
}

interface IFormData {
/**
* 决算单状态
*/

settlementStatus?: string
/**
* 主合同编号
*/

mainContractNumber?: string
/**
* 客户名称
*/

customerName?: string
/**
* 客户手机号
*/

customerPhone?: string
/**
* 工程管理
*/

projectManagement?: string
}

interface IOptionItem {
label: string
value: string
}

interface IOptions {
settlementStatus: IOptionItem[]
}

const defaultOptions: IOptions = {
settlementStatus: [],
}

export const defaultFormData: IFormData = {
settlementStatus: undefined,
mainContractNumber: undefined,
customerName: undefined,
customerPhone: undefined,
projectManagement: undefined,
}

export const useModel = () => {
const filterForm = reactive<IFormData>({ ...defaultFormData })

const options = reactive<IOptions>({ ...defaultOptions })

const tableList = ref<(ITableListItem & { _?: unknown })[]>([])

const pagination = reactive<{
page: number
pageSize: number
total: number
}>({
page: 1,
pageSize: 10,
total: 0,
})

const loading = reactive<{ list: boolean }>({
list: false,
})

return {
filterForm,
options,
tableList,
pagination,
loading,
}
}

export type Model = ReturnType<typeof useModel>


这就是用模板生成的好处,有规范,随时可以改,而封装 xxxForm,xxxTable 就是一个黑盒。


原理


下面大致说一下原理



首先是写好一个个模版,vscode 插件读取指定目录下模版显示到界面上



每个模版下可能包含如下内容:



选择模版后,进入动态表单配置界面



动态表单是读取 config/schema.json 里的内容进行动态渲染的,目前支持 amis、form-render、formily



配置表单是为了生成 JSON 数据,然后根据 JSON 数据生成代码。所以最终还是无法避免的使用私有的 DSL ,但是生成后的代码是没有私有 DSL 的痕迹的。生成代码本质是 JSON + EJS 模版引擎编译 src 目录下的 ejs 文件。


为了加快表单的配置,可以自定义脚本进行操作



这部分内容是读取 config/preview.json 内容进行显示的



选择对应的脚本方法后,插件会动态加载 script/index.js 脚本,并执行里面对应的方法



以 initColumnsFromImage 方法为例,这个方法是读取剪贴板里的图片,然后使用百度 OCR 解析出文本,再使用文本初始化表单


initColumnsFromImage: async (lowcodeContext) => {
context.lowcodeContext = lowcodeContext;
const res = await main.handleInitColumnsFromImage();
return res;
},

export async function handleInitColumnsFromImage() {
const { lowcodeContext } = context;
if (!lowcodeContext?.clipboardImage) {
window.showInformationMessage('剪贴板里没有截图');
return lowcodeContext?.model;
}
const ocrRes = await generalBasic({ image: lowcodeContext!.clipboardImage! });
env.clipboard.writeText(ocrRes.words_result.map((s) => s.words).join('\r\n'));
window.showInformationMessage('内容已经复制到剪贴板');
const columns = ocrRes.words_result.map((s) => ({
slot: false,
title: s.words,
dataIndex: s.words,
key: s.words,
}));
return { ...lowcodeContext.model, columns };
}

反正就是可以根据自己的需求定义各种各样的脚本。比如使用 ChatGPT 翻译 JSON 里的指定字段,可以看我的上一篇文章 TypeChat、JSONSchemaChat实战 - 让ChatGPT更听你的话


再比如要实现把中文翻译成英文,然后英文使用驼峰语法,这样就可以将中文转成英文代码变量,下面是实现的效果



选择对应的命令菜单后 vscode 插件会加载对应模版里的脚本,然后执行里面的 onSelect 方法。



main.ts 代码如下


import { env, window, Range } from 'vscode';
import { context } from './context';

export async function bootstrap() {
const clipboardText = await env.clipboard.readText();
const { selection, document } = window.activeTextEditor!;
const selectText = document.getText(selection).trim();
let content = await context.lowcodeContext!.createChatCompletion({
messages: [
{
role: 'system',
content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
},
{
role: 'user',
content: selectText || clipboardText,
},
],
});
content = content.charAt(0).toLowerCase() + content.slice(1);
window.activeTextEditor?.edit((editBuilder) => {
if (window.activeTextEditor?.selection.isEmpty) {
editBuilder.insert(window.activeTextEditor.selection.start, content);
} else {
editBuilder.replace(
new Range(
window.activeTextEditor!.selection.start,
window.activeTextEditor!.selection.end,
),
content,
);
}
});
}


使用了 ChatGPT。


再来看看,之前生成管理后台 CURD 页面的时候,连 mock 也一起生成了,主要逻辑放在了 complete 方法里,这是插件的一个生命周期函数。



因为 mock 服务在另一个项目里,所以需要跨目录去生成代码,这里我在 mock 服务里加了个接口返回 mock 项目所在的目录


.get(`/mockProjectPath`, async (ctx, next) => {
ctx.body = {
status: 200,
msg: '',
result: __dirname,
};
})

生成代码的时候请求这个接口,就知道往哪个目录生成代码了


const mockProjectPathRes = await axios
.get('http://localhost:3001/mockProjectPath', { timeout: 1000 })
.catch(() => {
window.showInformationMessage(
'获取 mock 项目路径失败,跳过更新 mock 服务',
);
});
if (mockProjectPathRes?.data.result) {
const projectName = workspace.rootPath
?.replace(/\\/g, '/')
.split('/')
.pop();
const mockRouteFile = path.join(
mockProjectPathRes.data.result,
`${projectName}.js`,
);
let mockFileContent = `
import KoaRouter from 'koa-router';
import proxy from '../middleware/Proxy';
import { delay } from '../lib/util';

const Mock = require('mockjs');

const { Random } = Mock;

const router = new KoaRouter();
router{{mockScript}}
module.exports = router;
`
;

if (fs.existsSync(mockRouteFile)) {
mockFileContent = fs.readFileSync(mockRouteFile).toString().toString();
const index = mockFileContent.lastIndexOf(')') + 1;
mockFileContent = `${mockFileContent.substring(
0,
index,
)}
{{mockScript}}\n${mockFileContent.substring(index)}`
;
}
mockFileContent = mockFileContent.replace(/{{mockScript}}/g, mockScript);
fs.writeFileSync(mockRouteFile, mockFileContent);
try {
execa.sync('node', [
path.join(
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'/node_modules/eslint/bin/eslint.js',
),
mockRouteFile,
'--resolve-plugins-relative-to',
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'--fix',
]);
} catch (err) {
console.log(err);
}

mock 项目也可以通过 vscode 插件快速创建和使用



上面展示的模版都放在了 github.com/lowcode-sca… 仓库里,照着 README 步骤做就可以使用了。


作者:若邪
来源:juejin.cn/post/7315242945454735414
收起阅读 »

精美绝伦:小程序日历组件推荐

web
前言众所周知,小程序的渲染性能一直被广大开发者诟病,2023年中旬,小程序团队正式发布了 skyline 渲染引擎,Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验...
继续阅读 »

前言

众所周知,小程序的渲染性能一直被广大开发者诟病,2023年中旬,小程序团队正式发布了 skyline 渲染引擎,Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验。

非常好,那么就是说我们可以在小程序上体验类原生的特性啦!这下谁敢再说小程序是屎?

尝试

在用了一段时间,主要尝试了canvas、手势组件动画等功能,惊奇的发现,小程序做的这个 skyline 渲染引擎,是一陀超大的屎。

噢!腾讯,你小子好啊,研究了这么长时间,跑我这排宿便了是吧?

image.png

image.png

自己写的样式和iconfont样式给我报了很多坨警告,能用吗?能用,但是我是屎我需要恶心你,我必须得给你点警告。

除了控制台脏了之外,还有各种各样数不清的 bug。比如,地图的bindregionchange失效,而你去论坛发,他们只会说:"未复现"、"写片段"、"你试试",发文时实测依然没有修复😅。

爱莲说

铺垫了这么多,实属无奈,我也不想说这么多,只是这口屎憋在嘴里,臭的难受。我本以为出淤泥而不染已经很难得了,没想到在这屎坑里还有大佬栽培了一朵精美绝伦的白莲花,它就是 lspriv/wx-calendar ,github链接:github.com/lspriv/wx-c…

看到这么牛逼的组件,只有区区一百来个 star。

牛逼不牛逼,直接看效果:

QQ2024422-123019.webp

它还同时支持 skyline 和 webview 下渲染。

image.png

每个场景都是丝滑过渡的,元素到元素的联合动画。看的出来,这个日历是有很重的 MIUI 风格的,如果不是右上角的小程序胶囊,我甚至以为是某手机的自带日历。

QQ2024422-14944.webp

依赖 skyline 的 worklet 动画,组件做到了跟手、丝滑,且符合直觉的动画。

lspriv/wx-calendar 使用

lspriv/wx-calendar 需要使用npm下载并构建,然后引入组件使用。

npm i @lspriv/wx-calendar

然后需要使用微信开发者工具构建 npm

{
   "usingComponents": {
       "calendar": "@lspriv/wx-calendar"
  }
}
id="calendar" bindload="handleLoad" />

生态

作者十分聪明,给 lspriv/wx-calendar 预留了插件接口,开发者可以根据自身需求,写扩展功能。

源码中 src>plugins>lunar.ts 是一个内置插件,实现了农历、节气、闰年等功能。

目前为止,还没有看到有第二个人为作者贡献插件。

展望

目前还有很多基础功能还没有开发,比如

  1. 日期标注,日期标注有是有,但是作者将几种标记方式写死了,只能用内置的日程、角标和节假日标记,开发中肯定是期望可以传入组件或自定义样式的。
  2. 选择区间,一个很常见的场景,需要选择日期区间,跨月、跨年选择,这些在不了解源码的情况下,去手写插件也是比较困难的。
  3. 自定义样式,作者将自己的样式隔离了,开发者只能通过传入指定的style字符串修改样式,这个用起来不是很方便。

结语

总的来说,这是一款不可多得的组件,即使在PC端,也是不常见的。在小程序的层层阻挠下能开发出如此的组件,实属不易。

ce2898a24a9846c59a058e07eaeea24c_tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.gif


作者:德莱厄斯
来源:juejin.cn/post/7360237771637489679

收起阅读 »

一个小小的批量插入,被面试官追问了6次

嗨,你好呀,我是哪吒。 面试经常被问到“MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?”。 首先需要明确一点,优先使用批量插入,而不是在Java中通过循环单条插入。 很多小伙伴都知道这个结论,但是,为啥?很少有人...
继续阅读 »

嗨,你好呀,我是哪吒。


面试经常被问到“MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?”。


首先需要明确一点,优先使用批量插入,而不是在Java中通过循环单条插入。


很多小伙伴都知道这个结论,但是,为啥?很少有人能说出个所以然来。


就算我不知道,你也不能反反复复问我“同一个问题”吧?


1、MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?


批量入库时,如果通过Java循环语句一条一条入库,每一条SQL都需要涉及到一次数据库的操作,包括网络IO以及磁盘IO,可想而知,这个效率是非常低下的。


xml中使用foreach的方式会一次性发送给数据库执行,只需要进行一次网络IO,提高了效率。


但是,xml中的foreach可能会导致内存溢出OOM问题,因为它会一次性将所有数据加载到内存中。而java中的foreach可以有效避免这个问题,因为它会分批次处理数据,每次只处理一部分数据,从而减少内存的使用。


如果操作比较复杂,例如需要进行复杂的计算或者转换,那么使用java中的foreach可能会更快,因为它可以直接利用java的强大功能,而不需要通过xml进行转换。


孰重孰轻,就需要面试官自己拿捏了~


2、在MyBatis中,对于<foreach>标签的使用,通常有几种常见的优化方法?


比如避免一次性传递过大的数据集合到foreach中,可以通过分批次处理数据或者在业务层先进行数据过滤和筛选。


预编译SQL语句、优化SQL语句,减少foreach编译的工作量。


对于重复执行的SQL语句,可以利用mybatis的缓存机制来减少数据库的访问次数。


对于关联查询,可以考虑使用mybatis的懒加载特性,延迟加载关联数据,减少一次性加载的数据量。


3、MyBatis foreach批量插入会有什么问题?


foreach在处理大量数据时会消耗大量内存。因为foreach需要将所有要插入的数据加载到内存中,如果数据量过大,可能会导致内存溢出。


有些数据库对单条SQL语句中可以插入的数据量有限制。如果超过这个限制,foreach生成的批量插入语句将无法执行。


使用foreach进行批量插入时,需要注意事务的管理。如果部分插入失败,可能需要进行回滚操作。


foreach会使SQL语句变得复杂,可能影响代码的可读性和可维护性。


4、当使用foreach进行批量插入时,如何处理可能出现的事务问题?内存不足怎么办?


本质上这两个是一个问题,就是SQL执行慢,一次性执行SQL数量大的问题。


大多数数据库都提供了事务管理功能,可以确保一组操作要么全部成功,要么全部失败。在执行批量插入操作前,开始一个数据库事务,如果所有插入操作都成功,则提交事务;如果有任何一条插入操作失败,则回滚事务。


如果一次插入大量数据,可以考虑分批插入。这样,即使某一批插入失败,也不会影响到其他批次的插入。


优化foreach生成的SQL语句,避免因SQL语句过长或过于复杂而导致的问题。


比如MySQL的INSERT INTO ... VALUES语法 通常比使用foreach进行批量插入更高效,也更可靠。


5、MyBati foreach批量插入时如何处理死锁问题?


当使用MyBatis的foreach进行批量插入时,可能会遇到死锁问题。这主要是因为多个事务同时尝试获取相同的资源(如数据库的行或表),并且每个事务都在等待其他事务释放资源,从而导致了死锁。


(1)优化SQL语句


确保SQL语句尽可能高效,避免不必要的全表扫描或复杂的联接操作,这可以减少事务持有锁的时间,从而降低死锁的可能性。


不管遇到什么问题,你就回答优化SQL,基本上都没毛病。


(2)设置锁超时


为事务设置一个合理的锁超时时间,这样即使发生死锁,也不会导致系统长时间无响应。


(3)使用乐观锁


乐观锁是一种非阻塞性锁,它假设多个事务在同一时间不会冲突,因此不会像悲观锁那样在每次访问数据时都加锁。乐观锁通常用于读取频繁、写入较少的场景。


(4)分批插入


如果一次插入大量数据,可以考虑分批插入。这样,即使某一批插入失败,也不会影响到其他批次的插入。


(5)调整事务隔离级别


较低的隔离级别(如READ UNCOMMITTED)可能会减少死锁的发生,但可能会导致其他问题,如脏读或不可重复读。


6、mybatis foreach批量插入时如果数据库连接池耗尽,如何处理?


(1)增加最大连接数


数据库连接池耗尽了,增加最大连接数,这个回答,没毛病。


(2)优化SQL语句


减少每个连接的使用时间,从而减少连接池耗尽的可能性。


万变不离其宗,优化SQL,没毛病。


(3)分批插入


避免一次性占用过多的连接,从而减少连接池耗尽的可能性。


(4)调整事务隔离级别


降低事务隔离级别可以减少每个事务持有连接的时间,从而减少连接池耗尽的可能性。但需要注意,较低的事务隔离级别可能会导致其他问题,如脏读或不可重复读。


(5)使用更高效的批量插入方法


比如MySQL的INSERT INTO ... VALUES语法。这些方法通常比使用foreach进行批量插入更高效,也更节省连接资源。


感觉每道题的答案都是一样呢?这就对喽,数据库连接池耗尽,本质问题不就是入库的速度太慢了嘛。


(6)定期检查并关闭空闲时间过长的连接,以释放连接资源。


就前面的几个问题,做一个小总结,你会发现,它们的回答大差不差。


通过现象看本质,批量插入会有什么问题?事务问题?内存不足怎么办?如何处理死锁问题?数据库连接池耗尽,如何处理?


这些问题的本质都是因为SQL执行慢,一次性SQL数据量太大,事务提交太慢导致的。


回答的核心都是:如何降低单次事务时间?



  1. 优化SQL语句

  2. 分批插入

  3. 调整事务隔离级别

  4. 使用更高效的批量插入方法


作者:哪吒编程
来源:juejin.cn/post/7359900973991362597
收起阅读 »

领导让我加水印

web
tips: 文末有完整示例代码。 领导:『小S啊,我们有个新需求🥸,需要在预览的资源上添加水印,让服务端来加水印成本太高了,在前端渲染的时候把水印加上可以吗🤨?』 小S:『加水印啊,简简单单🤏。我们项目使用的是 Vue3,使用自定义指令一下就可以加好了。领导...
继续阅读 »

tips: 文末有完整示例代码。



领导:『小S啊,我们有个新需求🥸,需要在预览的资源上添加水印,让服务端来加水印成本太高了,在前端渲染的时候把水印加上可以吗🤨?』


小S:『加水印啊,简简单单🤏。我们项目使用的是 Vue3,使用自定义指令一下就可以加好了。领导你看我操作!』


小S说着,就把生产力工具打开了。手速熟练🤠的启动了项目。


小S:『领导你看😈,我先在项目自定义指令的文件夹下新建一个自定义水印指令文件 - watermark.ts。在需要添加水印的目标 Dom 挂载时,创建一个 canvas 节点,canvas 的宽高自然要跟 Dom 的大小一样啦,层级也必须是最高的。然后我再给 canvas 里画上水印内容,最后再给 canvas 挂载到目标节点。当然啦,目标节点销毁时也要把 canvas 销毁掉。』


小S一边讲,一边就在生产力工具中敲🫳出了代码。


import type { Directive, App } from 'vue';
import { nextTick } from 'vue';

const watermarkDirective: Directive = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
// 绑定元素的父组件卸载后调用
unmounted(el) {
removeWatermark(el);
},
};

/** 创建水印 */
async function createWatermark(el, text: string) {
const canvasEl = document.createElement('canvas');
const newCanvas = !el.querySelector('canvas');

canvasEl.id = 'watermark-canvas';
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';
el.appendChild(canvasEl);
canvasEl.width = window.screen.width;
canvasEl.height = window.screen.height;
const ctx = canvasEl.getContext('2d');
ctx.rotate((-20 * Math.PI) / 180); //旋转角度
ctx.font = '24px serif';
ctx.fillStyle = 'rgba(180, 180, 180, 0.3)';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (let i = -canvasEl.width / 100; i < canvasEl.width / 100; i++) {
for (let j = -canvasEl.height / 200; j < canvasEl.height / 200; j++) {
ctx.fillText(text, i * 300, j * 300);
}
}
}

async function removeWatermark(el) {
await nextTick();
const canvasEl = el.querySelector('#watermark-canvas');
if (canvasEl) {
canvasEl.remove();
}
}

export default watermarkDirective;


小S得意😏的抖着腿,侧身向领导讲到:『这样就可以生成水印啦! 撒花🥳🥳🥳』。


领导🫲🫱:『你这样是可以实现了,但是也仅仅可以防一下小白,稍微懂点前端知识的人,都可以 F12 把控制台打开,选中水印节点,给它哐哐哐删掉。』


小S听了,一拍脑门:『是哦,我怎么没想到呢!嗯……』小S陷入了沉思,如何防止被删掉呢?小S脑子转了3圈后:『领导,我知道怎么做了!DOM3 Event 规范中有一个 MutationObserver,这个接口可以监视 DOM 进行监视,只要我的水印被删掉了,我就赶紧再生成一个水印!』


小S立刻转身,一边思索🤔着逻辑,一边在生产力工具中继续完善:


小S心里想到:『在目标节点挂载,首次添加 canvas 时,我给目标节点添加 MutationObserver 监听,并把实例化的监视器放在目标节点的自定义属性上,监听它的子节点,如果监听到子节点水印被删除,我就再新建一个水印 canvas,插入到目标节点中,对了,还要考虑到我主动删除水印的操作。水印节点也要加监视,不然手动改一下水印的CSS样式,就可以把水印给隐藏掉了。emmm……最后在 目标节点卸载时把监听移除掉。』


小S搞好了,转身给领导讲道:『领导,搞定了!使用的时候只需要引入自定义指令,在需要加水印的节点添加参数就可以啦』


<template>
<div v-watermark="watermarkOption">
<img src="xxxx">
</div>
</template>

<script setup lang="ts">
// @ts-ignore
import vWatermark from '/@/directives/watermark';

const watermarkOption = {
text: '小S水印'
}
</script>

领导看着小S加好水印,笑😼着说:『针不错,这就去给你涨工资!』


小S听了,连忙摇头🙀道:『领导,不用,不用,这都是前端切图仔的基本功!』




END




完整示例代码


import type { Directive, App } from 'vue';
import { nextTick } from 'vue';

const watermarkDirective: Directive = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
// 绑定元素的父组件卸载后调用
unmounted(el) {
removeWatermark(el);
},
};

async function createWatermark(el, text: string) {
const canvasEl = el.querySelector('canvas') || document.createElement('canvas');
const newCanvas = !el.querySelector('canvas');

if (!el.dataset.mutationObserverParent) {
const mutationObserver = new MutationObserver((records) =>
parentCheckWatermark(records, el, text),
);
mutationObserver.observe(el, {
childList: true,
});
el.dataset.mutationObserverParent = mutationObserver;
}
canvasEl.id = 'watermark-canvas';
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';
newCanvas && el.appendChild(canvasEl);
canvasEl.width = window.screen.width * 3;
canvasEl.height = window.screen.height * 3;
const ctx = canvasEl.getContext('2d');
if (!ctx) return;
ctx.rotate((-20 * Math.PI) / 180); //旋转角度
ctx.font = '24px serif';
ctx.fillStyle = 'rgba(180, 180, 180, 0.3)';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (let i = -canvasEl.width / 100; i < canvasEl.width / 100; i++) {
for (let j = -canvasEl.height / 200; j < canvasEl.height / 200; j++) {
ctx.fillText(text, i * 300, j * 300);
}
}

if (newCanvas) {
// 水印属性监听
const mutationObserver = new MutationObserver(() => canvasCheckWatermark(el, text));
mutationObserver.observe(canvasEl, {
attributes: true,
});
el.dataset.mutationObserverCanvas = mutationObserver;
}
}

/** 检查水印是否被删除 */
async function parentCheckWatermark(records, el, text) {
// 主动删除水印不处理
if (el.dataset.focusRemove) return;
const removedNodes = records[0].removedNodes;
let hasDelWatermark = false;
removedNodes.forEach((el) => {
if (el.id === 'watermark-canvas') {
hasDelWatermark = true;
}
});
// 水印被删除了
hasDelWatermark && createWatermark(el, text);
}

/** 检查水印属性是否变化了 */
async function canvasCheckWatermark(el, text) {
// 防止多次触发
if (el.dataset.canvasRending) return;
el.dataset.canvasRending = 'rending';

// 水印canvas属性变化了,重新创建
await createWatermark(el, text);
el.dataset.canvasRending = '';
}

async function removeWatermark(el) {
el.dataset.focusRemove = true;
el.dataset.mutationObserverParent?.disconnect?.();
await nextTick();
const canvasEl = el.querySelector('#watermark-canvas');
if (canvasEl) {
canvasEl.dataset.mutationObserverCanvas?.disconnect?.();
canvasEl.remove();
}
}
export default watermarkDirective;

作者:sofor
来源:juejin.cn/post/7360269869399392310
收起阅读 »

偷偷分享下我们公司的研发规范~

有程序员朋友问:啥是研发规范?还有朋友表示:鱼皮别拿咱当外人,把你们公司的研发规范发来看看?可以,必须安排!这篇文章就给大家简单分享下我们公司的研发规范,不过在开始前必须要明确 2 点:每个团队都应该根据情况定制自己的研发规范,别人的规范仅供参考,未必最适合你...
继续阅读 »

有程序员朋友问:啥是研发规范?

还有朋友表示:鱼皮别拿咱当外人,把你们公司的研发规范发来看看?

可以,必须安排!

这篇文章就给大家简单分享下我们公司的研发规范,不过在开始前必须要明确 2 点:

  1. 每个团队都应该根据情况定制自己的研发规范,别人的规范仅供参考,未必最适合你们团队。
  2. 篇幅有限,本文仅分享一些我认为很重要的规范,并且移除了我们自己的敏感信息。

一、项目整体研发流程

1)团队共同确认目标和规划

开会讨论,产出目标和规划文档

2)产品调研和需求分析

产出调研报告和需求分析文档

3)需求评审

开需求评审会,明确要做的需求和工作,评估工作量并明确工作时间节点。

4)方案设计

产出方案设计文档,比如数据库表设计、页面设计、接口设计等。

5)研发

包括各自开发、单元测试、前后端联调等

6)测试和验收

包括研发自测、产品验收、组内验收等

7)代码提交

提交可上线的代码,需要由负责人审查,通过后可合并

8)部署上线

将代码发布到服务器上,组内进行上线通知并更新上线文档,上线后需要自行验证

9)产品迭代

持续收集用户对新功能的反馈、并进行数据分析,从而验证改动效果,便于下一轮的更新迭代。

二、开发规范

开发前注意事项

1)确保自己充分理解了业务和需求,需要先进行整体的方案设计;尤其是对于重要需求和核心业务,必须先跟组内同学核对方案并通过后,才能下手开发,避免重复工作。

2)先熟悉项目再开发,建议阅读项目文档、项目代码、接口文档、前端组件文档等。

3)慎重引入新的依赖或类库、或者升级版本,重大依赖变更需要和组内其他成员确认。

4)熟悉团队已实现的功能和代码,尽量复用,避免重复开发。

5)熟悉团队内部的研发规范,并在 IDE 中进行相应的配置,比如前端配置 ESLint、Prettier 等代码规范插件。

开发中注意事项

1)开发新功能时,确保从项目仓库拉取 最新主分支 的代码。

2)每个功能都要新建自己的分支进行开发,千万不要直接修改主分支的代码!注意分支名称要使用英文、足够语义化,不要和其他人的混淆。

3)开发时,尽量复用现有的功能、模块、类、方法、对象代码。有现成的代码,就不要再重复编写。如无法复用,可以适当通过注释说明。

4)开发时,遵循团队内部的研发规范,尽量参考现有项目代码的写法,尤其是不要使用和原项目不一致的格式、命名、写法,避免特立独行。

5)开发过程中,有任何不明确的地方,不要凭空猜测,及时去联系项目的其他成员或负责人确认。

6)开发过程中,每隔一段时间(比如 1 - 3 天)可以使用 git pull 同步一下最新的主分支代码,防止合并代码冲突。

7)开发过程中,注意整体时间进度的把控,先完成再完美,有风险时及时反馈。

8)开发时,需要格外注意对异常情况的捕获和处理。

9)每个分支尽量保证纯净,尽量减少每次开发和提交时改动的代码量。建议每次开分支只改一个功能、Bug 或模块,不要把多个不相关的功能写在一起,并且非必要不修改。

10)完成部分功能开发后,一定要自测!自测时,可以 Mock 假数据。注意一定不要在线上测试、一定不要影响线上数据!

三、代码提交规范

1)只有通过测试和产品验收的代码,才能够发起合并到主分支的 PR 请求。在这之前可以提交到自己的分支。

2)发起合并到主分支的 PR 前,一定要完整阅读 3 遍自己的代码,避免不规范的写法和无意义的改动。

3)每次合并尽量只专注于一个功能或改动,避免多个功能耦合在一起合并,提高审查效率并降低改动风险。

4)每次提交时,需要在 commit 信息中提供代码改动说明,还可以通过关联需求文档、测试用例、方案文档、效果截图等方式进行补充说明。

commit 信息可参考《约定式提交》文档,但不做强制要求。

5)除非特殊情况,否则所有的代码必须经过至少一位项目负责人 Code Review 审核通过后,才能合并;并且只有合并到主分支的代码才允许发布上线。

上线规范

上线前注意事项

1)上线前,除了严格验证功能特性能否正常运行、并符合需求外,还要格外关注程序的:

  • 健壮性。比如给用户友好的错误提示、输入校验。
  • 安全性。防止越权操作、输入校验。
  • 稳定性。尽量保证调用 100% 成功,如果有几率失败,要考虑重试或容错策略。

2)除非特殊情况,只有经过产品验证的功能、通过代码审核的主分支代码才允许发布上线。

3)除非特殊情况,尽量在工作日上线(建议周二 ~ 周四),保证上线后出了问题时能够及时修复。

上线后注意事项

1)上线后,一定要再次进行完整流程的测试,尤其要重点关注权限相关的功能测试。

2)上线后,一定要在群内及时同步上线信息,周知相关的成员,如果遇到问题第一时间反馈。

3)首次上线后,需要即时配置监控告警。

4)上线验证通过、并经过内部群成员确认后,可以在外部用户群发布版本更新公告。

5)上线后,即时更新项目的更新记录文档。

6)注意,上线不是终点。上线后的一段时间(至少一周内),一定要持续观察自己负责的功能是否正常运行、持续接受用户反馈、通过数据分析来观察新功能的效果,期间有任何问题都需要即时修复处理,并且准备好下一期的改进迭代。


作者:程序员鱼皮
来源:juejin.cn/post/7360486735798927396
收起阅读 »

奇技淫巧:如何修改第三方npm包?

web
如何修改第三方npm包? 有这样一种场景:如果你在开发过程中,发现某个npm包有Bug,应该怎么办? 第一选择当然是向原作者提issue,或者Fork该仓库代码,修改以后,提交合并请求。 但这种情况有个严重的问题,就是耗时,如果遇到严格的作者,或者不活跃的作...
继续阅读 »

如何修改第三方npm包?



有这样一种场景:如果你在开发过程中,发现某个npm包有Bug,应该怎么办?


第一选择当然是向原作者提issue,或者Fork该仓库代码,修改以后,提交合并请求。


但这种情况有个严重的问题,就是耗时,如果遇到严格的作者,或者不活跃的作者,时间线可能会拉得很长。你的项目未必能等这么长时间。


还有一种可能是,你修改的代码并不具备普适性,只有你的业务场景能用到,合并被拒的概率会大大增加。


总而言之,如果能修改npm包的源包,再好不过,如果不行,则需要有个临时方案,或者替代方案。


这时,又有下面两种情况:



  1. 代码量少,可以直接修改npm包代码的,考虑补丁方案。

  2. 代码量多,或者npm包代码是压缩混淆过的,不具备修改条件。修改源码后,再修改包名,重新发布,在应用代码中更换引用。为叙文方便,我将这种方案命名为换日方案(偷天换日,李代桃僵)。


下面,详细介绍下这两种不同方案。


补丁方案


patch-package


patch-package是一个用于修复第三方依赖包的工具,使用方式非常简单。



它支持npm和yarn v1,如果是yarn v2+或者pnpm,则使用自带的patch方案(下文会介绍pnpm方案)。



安装:


$ npm i patch-package
$ yarn add patch-package postinstall-postinstall


如果只是前端使用,可以添加--dev或-D参数。如果是后端使用,为保障生产模式(会去除devDendencies依赖)也能正常使用,就不要加了。



在node_modules中找到你要修改的npm包,修改内容后,就可以运行patch-package创建patch文件了。


$ npx patch-package package-name   # 使用npm
$ yarn patch-package package-name # 使用yarn

运行后会在项目根目录下创建一个patches文件夹,并生成一个名为package-name+version.patch的文件。将该patch文件提交至版本控制中,即可在之后应用该补丁了。


以我修改的verdaccio为例,会生成一个verdaccio+4.4.0.patch的文件,内容大致如下:


diff --git a/node_modules/verdaccio/build/index.js b/node_modules/verdaccio/build/index.js
index 3a79eaa..d00974b 100644
--- a/node_modules/verdaccio/build/index.js
+++ b/node_modules/verdaccio/build/index.js
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = void 0;

+console.log('---------------')
+
var _bootstrap = require("./lib/bootstrap");

完成上述操作后,最后在package.jsonscripts中加入"postinstall": "patch-package"


"scripts": {
"postinstall": "patch-package"
}

这样当其他同事拉下代码,运行npm install或是yarn install命令时,便会自动为依赖包打上我们的补丁了。



简单来说,这个方案的原理就是记录补丁的代码与位置,利用npm的hook(postinstall会在npm install后触发),在安装完依赖以后,触发相应的脚本,将补丁覆盖到node_modules对应的包里。


当然,补丁是对应具体版本的,需要锁定版本号。这样的缺点是如果要升级的话,还得重新来一遍,不过不是有Bug或性能问题,通常不必追求新的版本。



pnpm patch


pnpm的patch自称灵感来自yarn的类似命令。由于yarn v2可能走了邪路,我们就不介绍了。


首先,执行pnpm patch @。该命令会将指定的软件包提取到一个可以随意编辑的临时目录中。


完成修改后, 运行pnpm patch-commit ( 是之前提取的临时目录,这个临时目录会长到你根本记不住,不过不用担心,命令行里会有完备的提示) 以生成一个补丁文件,并提供patchedDependencies 字段注册到你的项目中。


比如,我想修改一个is-even的包:


pnpm patch is-even  
You can now edit the following folder: /private/var/folders/sq/0jfgh1js6cs8_31df82hx3jw0000gn/T/29ba74c7c7ffd7aa157831c6436d3738

Once you're done with your changes, run "pnpm patch-commit /private/var/folders/sq/0jfgh1js6cs8_31df82hx3jw0000gn/T/29ba74c7c7ffd7aa157831c6436d3738"

按照提示,打开这个文件夹,加一行代码:
image.png


执行上面控制台的提示:


pnpm patch-commit /private/var/folders/sq/0jfgh1js6cs8_31df82hx3jw0000gn/T/e103de90617a18eee7942d1df35a2c48
Packages: -1
-
Progress: resolved 5, reused 6, downloaded 0, added 1, done

这时你会发现package.json中多了一段内容:


"pnpm": {
"patchedDependencies": {
"is-even@1.0.0": "patches/is-even@1.0.0.patch"
}
}

根目录下,也多了个文件夹patches,打开以后,你就能找到添加的代码:
image.png


打开node_modules/is-even/index.js,可以看到已经多了我们添加的代码:
image.png


删除node_modules,重新pnpm i安装依赖,仍然与现在一样,这就代表成功了。


整个流程下来,我们看得出来相比于patch-package,要稍微复杂点儿,但也是可以接受的。



注意:patches目录是一定得提交到git的。



换日方案


上面说过,如果要修改的代码较多,或者不具备修改条件,这时就需要修改源码。
到GitHub上找到npm包的源码,Fork该项目,修改代码后,再修改包名,重新发布,比如你要修改的包是lodash,可以修改为awesome-lodash,在应用代码中更换引用。


本来这个方案没什么好说的,但有一种情况,如果你修改的是个底层包,也就是说并不是你的应用代码中直接引用的,而是你引用的npm包A所依赖的,甚至可能同时被包B依赖的,这时就比较尴尬了,你不可能再去修改A和B的源码,那就太不值当了。


pnpm提供了一种别名(Aliases)的能力。


假设你发布了一个名为awesome-lodash的新包,并使用lodash作为别名来安装它:


$ pnpm add lodash@npm:awesome-lodash

不需要更改代码,所有的lodash引用都被解析到了awesome-lodash
就这么简单,上面说的问题就解决了。


再说点儿题外话,有时你会想要在项目中使用一个包的两个不同版本,很简单:


$ pnpm add lodash1@npm:lodash@1
$ pnpm add lodash2@npm:lodash@2

现在,您可以通过 require('lodash1') 引入第一个版本的 lodash 并通过 require('lodash2') 引入第二个。


与pnpm的钩子结合使用功能会更加强大,比如你想将node_modules里所有的lodash引用也替换为awesome-lodash,你可以用下面的.pnpmfile.cjs 轻松实现:


function readPackage(pkg) {
if (pkg.dependencies && pkg.dependencies.lodash) {
pkg.dependencies.lodash = 'npm:awesome-lodash@^1.0.0'
}
return pkg
}

module.exports = {
hooks: {
readPackage
}
}

pnpm功能非常强大,后面我会再详细写篇文章介绍下。


总结


在开发过程中发现npm包的Bug,首先向原作者提交issue或Fork代码修改后提交合并请求。但遇到不活跃或拒绝修改的情况,项目等待时间会很长。这时可以使用补丁方案或换日方案进行解决。


补丁方案中,如果是npm或yarn v1,可以使用patch-package工具包处理;如果是yarn v2或pnpm,可以使用各自的patch命令。


换日方案,则是修改源码,发布新的npm包后,利用pnpm的别名功能,将所有依赖原npm包的地方,全部替换为新的包。


这种场景在日常开发中还是比较常见的,这里为大家提供一种思路。当然,如果真是个Bug,别忘了提issue或PR,为开源贡献自己的一份力量,在与作者的沟通交流中,相信你也能受益匪浅。


作者:纪轻昀
来源:juejin.cn/post/7356534347509497919
收起阅读 »

【前端缓存】localStorage是同步还是异步的?为什么?

web
🧑‍💻 写在开头 点赞 + 收藏 === 学会🤣🤣🤣 首先明确一点,localStorage是同步的 🥝 一、首先为什么会有这样的问题 localStorage 是 Web Storage API 的一部分,它提供了一种存储键值对的机制。loca...
继续阅读 »

🧑‍💻 写在开头


点赞 + 收藏 === 学会🤣🤣🤣



首先明确一点,localStorage是同步的



🥝 一、首先为什么会有这样的问题


localStorage 是 Web Storage API 的一部分,它提供了一种存储键值对的机制。localStorage 的数据是持久存储在用户的硬盘上的,而不是内存。这意味着即使用户关闭浏览器或电脑,localStorage 中的数据也不会丢失,除非主动清除浏览器缓存或者使用代码删除。


当你通过 JavaScript 访问 localStorage 时,浏览器会从硬盘中读取数据或向硬盘写入数据。然而,在读写操作期间,数据可能会被暂时存放在内存中,以提高处理速度。但主要的特点是它的持久性,以及它不依赖于会话的持续性。


🍉 二、硬盘不是io设备吗?io读取不都是异步的吗?


是的,硬盘确实是一个 IO 设备,而大部分与硬盘相关的操作系统级IO操作确实是异步进行的,以避免阻塞进程。不过,在 Web 浏览器环境中,localStorage 的API是设计为同步的,即使底层的硬盘读写操作有着IO的特性。


js代码在访问 localStorage 时,浏览器提供的API接口通常会处于js执行线程上下文中直接调用。这意味着尽管硬盘是IO设备,当一个js执行流程访问 localStorage 时,它将同步地等待数据读取或写入完成,该过程中js执行线程会阻塞。


这种同步API设计意味着开发者在操作 localStorage 时不需要考虑回调函数或者Promise等异步处理模式,可以按照同步代码的方式来编写。不过,这也意味着如果涉及较多数据的读写操作时,可能对性能产生负面影响,特别是在主线程上,因为它会阻塞UI的更新和其他js的执行。


🍑 三、完整操作流程


localStorage 实现同步存储的方式就是阻塞 JavaScript 的执行,直到数据的读取或者写入操作完成。这种同步操作的实现可以简单概述如下:



  1. js线程调用: 当 JavaScript 代码执行一个 localStorage 的操作,比如 localStorage.getItem('key') 或 localStorage.setItem('key', 'value'),这个调用发生在 js 的单个线程上。

  2. 浏览器引擎处理: 浏览器的 js 引擎接收到调用请求后,会向浏览器的存储子系统发出同步IO请求。此时 js 引擎等待IO操作的完成。

  3. 文件系统的同步IO: 浏览器存储子系统对硬盘执行实际的存储或检索操作。尽管操作系统层面可能对文件访问进行缓存或优化,但从浏览器的角度看,它会进行一个同步的文件系统操作,直到这个操作返回结果。

  4. 操作完成返回: 一旦IO操作完成,数据要么被写入硬盘,要么被从硬盘读取出来,浏览器存储子系统会将结果返回给 js 引擎。

  5. JavaScript线程继续执行: js 引擎在接收到操作完成的信号后,才会继续执行下一条 js 代码。


在同步的 localStorage 操作期间,由于 js 的单线程性质,整个线程会阻塞,即不会执行其他任何js代码,也不会进行任何渲染操作,直到 localStorage 调用返回。


🍒 四、localStorage限制容量都是因为同步会阻塞的原因吗?



  1. 资源公平分享:同一用户可能会访问大量不同的网站,如果没有限制,随着时间的积累,每个网站可能会消耗大量的本地存储资源。这样会导致本地存储空间被少数几个站点占用,影响到用户访问其他网页的体验。限制大小可以确保所有网站都有公平的存储机会。

  2. 防止滥用:如果没有存储限制,网站可能会滥用 localStorage,存储大量数据在用户的设备上,这可能导致设备存储空间迅速耗尽,也可能侵犯用户的隐私。

  3. 性能限制:如之前提到的,localStorage 的操作是阻塞的。如果网站能够存储大量数据,就会加剧读写操作对页面性能的影响。

  4. 存储效率localStorage 存储的是字符串形式的数据,不是为存储大量或结构化数据设计的。当尝试存储过多数据时,效率会降低。

  5. 历史和兼容性:5MB 的限制很早就已经被大多数浏览器实现,并被作为一个非正式的标准被采纳。尽管现在有些浏览器支持更大的 localStorage,但出于跨浏览器兼容性的考虑,开发者通常会假设这个限制。

  6. 浏览器政策:浏览器厂商可能会依据自己的政策来设定限制,可能是出于提供用户更一致体验的角度,或者是出于管理用户数据的方便。


🍐 五、那indexDB会造成滥用吗?


虽然它们提供了更大的存储空间和更丰富的功能,但确实潜在地也可能被滥用。但是与相比 localStorage 增加了一些特性用来降低被滥用的风险:



  1. 异步操作IndexedDB 是一个异步API,即使它被用来处理更大量的数据,也不会像 localStorage 那样阻塞主线程,从而避免了对页面响应性的直接影响。

  2. 用户提示和权限:对于某些浏览器,当网站尝试存储大量数据时,浏览器可能会弹出提示,要求用户授权。这意味着用户有机会拒绝超出合理范围的存储请求。

  3. 存储配额和限制:尽管 IndexedDB 提供的存储容量比 localStorage 大得多,但它也不是无限的。浏览器会为 IndexedDB 设定一定的存储配额,这个配额可能基于可用磁盘空间的一个百分比或者是一个事先设定的限额。配额超出时,浏览器会拒绝更多的存储请求。

  4. 更清晰的存储管理IndexedDB 的数据库形式允许有组织的存储和更容易的数据管理。用户或开发者可以更容易地查看和清理占用的数据。

  5. 逐渐增加的存储:某些浏览器实现 IndexedDB 存储时,可能会在数据库大小增长到一定阈值时,提示用户是否允许继续存储,而不是一开始就分配一个很大的空间。




作者:ObjectX不知名程序员
来源:juejin.cn/post/7359405716090011659
收起阅读 »

为 App 增加清理缓存功能

为 App 增加清理缓存功能 不废话,直接上干货 功能预期 评估缓存情况,估算出缓存大小; 一键清除所有缓存 评估缓存大小 已知 app 的缓存目录可通过 context.getCacheDir() 获取,那么评估其内容文件的大小即可,若有其他缓存路径也可...
继续阅读 »

为 App 增加清理缓存功能


不废话,直接上干货


功能预期



  1. 评估缓存情况,估算出缓存大小;

  2. 一键清除所有缓存


评估缓存大小


已知 app 的缓存目录可通过 context.getCacheDir() 获取,那么评估其内容文件的大小即可,若有其他缓存路径也可通过此方法合并计算:


public long getFolderSize(File folder) {
   long length = 0;
   File[] files = folder.listFiles();
   
   if (files != null) {
       for (File file : files) {
           if (file.isFile()) {
               length += file.length();
          } else {
               length += getFolderSize(file);
          }
      }
  }
   return length;
}

执行方法:


// 新建异步线程防止卡顿
new Thread() {
   @Override
   public void run() {
       super.run();
long cacheSize = getFolderSize(getCacheDir());
  }
}.start();

接下来需要将缓存大小按照合理的格式显示到界面上,我按照自己的需求小于 1MB 时显示 KB 单位,小于 1KB 时显示 0 KB,使用以下方法即可完成缓存大小的文本组织:


public String formatSize(long size) {
   if (size >= 1024 * 1024) {
       return (size / (1024 * 1024)) + " MB";
  } else if (size >= 1024) {
       return (size / 1024) + " KB";
  } else {
       return "0 KB";
  }
}

清理各单位缓存


WebView 的缓存清理


对于 WebView 可以直接使用 webView.clearCache(true) 方法来进行清理,但清除缓存的界面没有 WebView 实例,因此需要现场先建立一个来执行,注意 WebView 的创建不可以在异步线程进行:


WebView webView = new WebView(me);
webView.clearCache(true);

Glide 的缓存清理


只需要注意 Glide 的缓存清理必须在异步线程执行


try {
   // Glide: You must call this method on a background thread
   Glide.get(me).clearDiskCache();
} catch (Exception e) {
   e.printStackTrace();
}

其他组件请自行按照对应技术文档进行清理


综合缓存文件清理


所有缓存文件删除即可彻底完成清理步骤


File cacheDir = context.getCacheDir();
deleteDirectory(cacheDir);

删除目录方法:


private static void deleteDirectory(File dir) {
   if (dir != null && dir.isDirectory()) {
       for (File child : dir.listFiles()) {
           // 递归删除目录中的内容
           deleteDirectory(child);
      }
  }
   if (dir != null) {
       dir.delete();
  }
}

总结


其实清理缓存是个挺没必要的工作,Glide 等组件进行缓存的主要目的也在于避免重复资源的加载加快 app 的界面呈现速度,但不可避免的可能因为用户需要或者出现缓存 bug 导致界面无法正常显示等情况需要清理 app 缓存,即便系统本身自带了缓存清理功能(应用设置- app - 存储和缓存 - 清除缓存)但毕竟有些上手门槛且各家厂商操作系统操作逻辑各异不如自己做一个清除功能在 app 内了,通过上述代码即可完成缓存大小估算和清理流程,如有其他常用组件的清理操作方法也欢迎在评论区补充。


作者:Kongzue
来源:juejin.cn/post/7304932252826288180
收起阅读 »

Android串口通信蓝牙通信中数据格式转换整理

Android 定制开发版上应用开发,免不了使用一些串口通信、蓝牙通信,考虑到每次发送的数据包需要尽可能的小,约定的协议中基本上都是一些字节流表示,因此特地将之前搜集到的一些数据格式转换的方法整理出来。在此感谢将这些code发布出来的博主们。一、Byte相关的...
继续阅读 »

Android 定制开发版上应用开发,免不了使用一些串口通信、蓝牙通信,考虑到每次发送的数据包需要尽可能的小,约定的协议中基本上都是一些字节流表示,因此特地将之前搜集到的一些数据格式转换的方法整理出来。在此感谢将这些code发布出来的博主们。

一、Byte相关的数据转换

  1. 获取Byte指定下标[0 - 7]的Bit的值,和获取Byte的所有Bit的值
/**
* 获取第i位的bit值
*/

fun Byte.getPointedBit(position: Int): Int {
return (this.toInt() shr position) and 0x1
}

/**
* 通过byte获取int类型的字节list
* IntelMode 低字节在前,如 0x55-> 0101 0101
*/

fun Byte.getBitList(intelMode: Boolean = true): List<Int> {
val list = arrayListOf<Int>()
val input = this
for (i in 0 until 8) {
val index = if (intelMode) (7 - i) else i
list.add(input.getPointedBit(index))
}
return list
}
  1. Byte转16进制字符串
/**
* 十六进制字节转字符串,不足2位的字符串则在前补0
* 其实质是Byte->Int->String
*/

fun Byte.toHexString(): String {
var hexStr = Integer.toHexString(this.toInt() and 0xFF)
if (hexStr.length == 1) {
hexStr = "0$hexStr"
}
return hexStr.uppercase(Locale.getDefault())
}
  1. Byte 中修改指定位置的Bit,这个需要绕一下,先将Byte转成一个长度为8的数组,然后修改指定下标的值,然后再将这个数组转换成一个Int,最后Int可以直接转成Byte。目前还未发现其他更好的方法,如有后续补充上。
/**
* 将byte转换成bit组成的数组
*/

fun Byte.toByteArray(): ByteArray {
val bytes = ByteArray(8)
for (i in 0 until 8) {
bytes[i] = this.getPointedBit(i).toByte()
}
return bytes
}

/**
* 一个byte所代表的int值
*/

fun ByteArray.oneByteToIntSum(): Int {
var sum = 0
for (i in this.indices) {
val tmp = this[i]
// 2 的 n 次方
sum += (tmp * 2.0.pow(i.toDouble())).toInt()
}

return sum
}

二、ByteArray相关的数据转换

  1. ByteArray转Int
/**
* 有符号,int 占 2 个字节
*/

fun ByteArray.toIntWithTwo(): Int {
return (this[0].toInt() shl 8) or (this[1].toInt() and 0xFF)
}

/**
* 无符号,int 占 2 个字节
*/

fun ByteArray.toUnSignIntWithTwo(): Int {
return (this[0].toInt() and 0xFF) shl 8 or
(this[1].toInt() and 0xFF)
}

/**
* 有符号, int 占 4 个字节
*/

fun ByteArray.toIntWithFour(): Int {
return (this[0].toInt() shl 24) or
(this[1].toInt() and 0xFF) or
(this[2].toInt() shl 8) or
(this[3].toInt() and 0xFF)
}

/**
* 无符号, int 占 4 个字节
*/

fun ByteArray.toUnSignIntWithFour(): Long {
return ((this[0].toInt() and 0xFF) shl 24 or
(this[1].toInt() and 0xFF) shl 16 or
(this[2].toInt() and 0xFF) shl 8 or
(this[3].toInt() and 0xFF)).toLong()
}

/**
* 一个Int转成2个字节的byte数组
*/

fun Int.toIntArrayFor2(): List<Int> {
val list = arrayListOf<Int>()
val lowH = (this shr 8) and 0xff
val lowL = this and 0xff
list.add(lowH)
list.add(lowL)
return list
}

/**
* 一个Int转成4个字节的byte数组
*/

fun Int.toByteArray4(): ByteArray {
val byteArray = ByteArray(4)
val highH = ((this shr 24) and 0xff).toByte()
val highL = ((this shr 16) and 0xff).toByte()
val lowH = ((this shr 8) and 0xff).toByte()
val lowL = (this and 0xff).toByte()
byteArray[0] = highH
byteArray[1] = highL
byteArray[2] = lowH
byteArray[3] = lowL
return byteArray
}
  1. ByteArray转字符串
/**
* 字节数组转字符串
*/

fun ByteArray.toSimpleString(format: Charset = Charsets.UTF_8): String {
return String(this, format)
}

/**
* 字节数组转换成16进制字符串
*/

fun ByteArray.toHexString(): String {
var result = ""
for (element in this) {
var hexStr = Integer.toHexString(element.toInt() and 0xFF)
if (hexStr.length == 1) {
hexStr = "0$hexStr"
}
result += hexStr.uppercase(Locale.getDefault())
}
return result
}
  1. ByteArray转Long
/**
* 字节数组转换为long 8个byte
*/

fun ByteArray.convertToLong(): Long {
val bais = ByteArrayInputStream(this)
val dis = DataInputStream(bais)
return dis.readLong()
}

/**
* long转换为字节数组 8个byte
*/

fun Long.convertToBytes(): ByteArray {
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.writeLong(this)
return baos.toByteArray()
}

/**
* Long 类型转成4个字节数组
* 时间只能精确到秒
*/

fun Long.convertToBytes4(): ByteArray {
var tmp = this
val bytes = ByteArray(4)
for (i in bytes.size - 1 downTo 0) {
bytes[i] = (tmp and 0xFF).toByte()
tmp = tmp shr 8
}
return bytes
}

/**
* 4个字节数组转成Long 类型
* 时间只能精确到秒
*/

fun ByteArray.convertToLong4(): Long {
var num: Long = 0
for (i in 0 until 4) {
num = num shl 8
num = num or ((this[i].toInt() and 0xFF).toLong())
}
return num
}
  1. 两个ByteArray拼接
/**
* byte数组拼接一个byte数组
*/

fun ByteArray.appendByteArray(extraBytes: ByteArray): ByteArray {
val inputSize = this.size
val extraSize = extraBytes.size
val totalSize = inputSize + extraSize
val combineBytes = ByteArray(totalSize)
System.arraycopy(this, 0, combineBytes, 0, inputSize)
System.arraycopy(extraBytes, 0, combineBytes, inputSize, extraSize)
return combineBytes
}
  1. ByteArray转Double,此种转换较为复杂,目前未找到稳定可用的代码
none
  1. ByteArray 和 BCD 格式的时间相互转换
/**
* BCD字节数组转为字符串
*/

fun ByteArray.bcdToString(): String {
val sb = StringBuilder(this.size / 2)
for (i in 0 until this.size) {
// 高四位
sb.append((this[i].toInt() and 0xF0) ushr 4)
// 低四位
sb.append(this[i].toInt() and 0x0F)
}
val retStr = sb.toString()
return if (retStr.substring(0, 1).equals("0", ignoreCase = true)) {
retStr.substring(1)
} else {
retStr
}
}

/**
* 字符串转BCD字节数组
*/

fun String.bcdToByteArray(): ByteArray {
var len = this.length
val mod = len % 2
val srcStr = if (0 != mod) {
len += 1
"0$this"
} else this
val bytes = srcStr.toByteArray()
len = if (len >= 2) len / 2 else len
val secondBytes = ByteArray(len)
var j: Int
var k: Int
for (p in 0 until srcStr.length / 2) {
val jIndex = 2 * p
j = if (bytes[jIndex].toInt().toChar() in '0'..'9') {
bytes[jIndex].toInt().toChar() - '0'
} else if (bytes[jIndex].toInt().toChar() in 'a'..'z') {
bytes[jIndex].toInt().toChar() - 'a' + 0x0a
} else {
bytes[jIndex].toInt().toChar() - 'A' + 0x0a
}
val kIndex = 2 * p + 1
k = if (bytes[kIndex].toInt().toChar() in '0'..'9') {
bytes[kIndex].toInt().toChar() - '0'
} else if (bytes[kIndex].toInt().toChar() in 'a'..'z') {
bytes[kIndex].toInt().toChar() - 'a' + 0x0a
} else {
bytes[kIndex].toInt().toChar() - 'A' + 0x0a
}
val a = (j shl 4) + k
val b = a.toByte()
secondBytes[p] = b
}
return secondBytes
}

三、String相关的类型转换,主要是方便把二进制字节流转换成易于查看的字符串

  1. 16进制字符串转ByteArray
private fun char2Byte(input: Char): Byte {
return "0123456789ABCDEF".indexOf(input).toByte()
}

/**
* 16进制字符串转字节数组,提供3种转换方式
*/

fun String.hexStringToBytes(type: Int = 0): ByteArray {
if (this.isEmpty()) {
return ByteArray(0)
}
val hexStr = this.uppercase(Locale.getDefault())
val length = hexStr.length / 2
val outBytes = ByteArray(length)
when (type) {
0 -> {
val hexCharArr = this.toCharArray()
for (i in 0 until length) {
val p = 2 * i
val p1 = char2Byte(hexCharArr[p]).toInt() shl 4
val p2 = char2Byte(hexCharArr[p + 1])
outBytes[i] = p1.toByte() or p2
}
}
1 -> {
for (i in 0 until length step 2) {
val v1 = (this[i].digitToIntOrNull(16) ?: -1) shl 4
val v2 = this[i + 1].digitToIntOrNull(16) ?: -1
outBytes[i / 2] = (v1 + v2).toByte()
}
}
else -> {
for (i in outBytes.indices) {
val subStr = this.substring(2 * i, 2 * i + 2)
outBytes[i] = subStr.toInt(16).toByte()
}
}
}
return outBytes
}
  1. 字符串Json格式转Map
/**
* json 字符串转 Map
*/

fun String.jsonStringToMap(): HashMap? {
val jsonObject: JSONObject
try {
jsonObject = JSONObject(this)
val keyIter: Iterator = jsonObject.keys()
var key: String
var value: Any
val valueMap = HashMap()
while (keyIter.hasNext()) {
key = keyIter.next()
value = jsonObject[key] as Any
valueMap[key] = value
}
return valueMap
} catch (e: JSONException) {
e.printStackTrace()
}
return null
}


作者:pursuit_hu
来源:juejin.cn/post/7226629911350542391
收起阅读 »

我早就看现在的工作流不爽了!- 前端使用 Jenkins

背景目前笔者所在的小公司的前端项目还是推送到git仓库后由另一名后端拉取代码到他电脑上再build,然后再手动同步到服务器上,比较麻烦,而且出现一个bug就要立即修复,笔者一天要说100次“哥,代码更新了,打包上传下吧,球球了”,终于我实在受不了了(上传代码的...
继续阅读 »


背景

目前笔者所在的小公司的前端项目还是推送到git仓库后由另一名后端拉取代码到他电脑上再build,然后再手动同步到服务器上,比较麻烦,而且出现一个bug就要立即修复,笔者一天要说100次“哥,代码更新了,打包上传下吧,球球了”,终于我实在受不了了(上传代码的这位哥也受不了了),于是想通过 Jenkins 实现简单的前端项目自动打包部署。

通过 docker 安装 Jenkins

通过 ssh 连接上局域网服务器 192.168.36.2,在 home 目录下新建了一个 Jenkins 文件夹,后续我们的配置文件就放在其中。

 cd
 # 将 Jenkins 相关的文件都放在这里
 mkdir jenkins
 cd jenkins
 
 # 创建 Jenkins 配置文件存放的地址,并赋予权限
 mkdir jenkins_home
 chmod -R 777 jenkins_home
 
 pwd
 # /root/jenkins

创建docker-compose.yml

 touch docker-compose.yml
 vim docker-compose.yml
 version: '3'
 services:
  jenkins:
    image: jenkins/jenkins:latest
    container_name: 'jenkins'
    restart: always
    ports:
      - "8999:8080"
    volumes:
      - /root/jenkins/jenkins_home:/var/jenkins_home

Jenkins 启动后会挂在8080端口上,本文笔者将其映射到8999端口,读者可以自行更改。

关键在于将容器中的/var/jenkins_home目录映射到宿主机的/root/jenkins/jenkins_home目录,这一步相当于将 Jenkins 的所有配置都存放在宿主机而不是容器中,这样做的好处在于,后续容器升级、删除、崩溃等情况下,不需要再重新配置 Jenkins。

使用:wq保存后可以开始构建了:

 docker compose up -d

这一步会构建容器并启动,看到如下信息就说明成功了:

 [+] Running 1/1
  Container Jenkins   Started           1.3s

查看一下容器是否在运行:

 docker ps

image-20240403133238265

这个时候通过http://192.168.36.2:8999就可以访问 Jenkins 了。

Jenkins 初次配置向导

解锁

image-20240403133538015

第一次打开会出现向导,需要填入管理员密码,获取密码有三种方式:

  1. 通过宿主机

     cat /root/jenkins/jenkins_home/secrets/initialAdminPassword
     
     # 2bf4ca040f624716befd5ea137b70560
  2. 通过 docker 进入容器

     docker exec -it jenkins /bin/bash
     
     #进入了docker
     jenkins@1c151dfc2482:/$ cat /var/jenkins_home/secrets/initialAdminPassword
     
     # 2bf4ca040f624716befd5ea137b70560

    与方法一类似,因为目录映射,这两个目录其实是同一个。

  3. 通过查看 docker log

     docker logs jenkins

    会出现一大串,最后能找到密码:

    image-20240403134001532

填入密码,点击继续。

安装插件

image-20240403134122512

选择安装推荐插件即可。

安装插件可能会非常慢,可以选择换源。

更换 Jenkins 插件源(可选)

有两种方法:

  1. 直接输入地址:

    http://192.168.36.2:8999/manage/pluginManager/advanced,在Update Site中填入清华源地址:

     https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

    点击Submit提交保存,并重启容器。

  2. 直接更改配置文件:

    宿主机中操作:

     cd /root/jenkins/jenkins_home
     vim hudson.model.UpdateCenter.xml

    替换其中的地址,然后重启容器即可。

    image-20240403135010339

创建用户

这一步建议用户名不为 admin ,不然会出现奇怪的问题,比如密码登录不上,需要用上一部的初始密码(2bf4ca040f624716befd5ea137b70560)才能登录。

我这里创建了一个 root 用户(只是名字叫 root,防止用户名太多记不住而已)。

image-20240403135909136

点击保存并完成。

实例配置按需调整即可,直接下一步,Jenkins 就准备就绪了。

image-20240403140101678

至此 Jenkins 安装就算完成了。

安装插件

笔者是一名前端,因此以前端项目为例。

前端项目的打包需要 node 环境,打包完成后通过 ssh 部署到服务器上,并且构建结果通过钉钉机器人推送到群里,因此需要三个插件。

  1. NodeJS
  2. Publish Over SSH
  3. DingTalk(可选)

在 系统管理 -> 插件管理 -> Available plugins 中搜索并安装。

image-20240403140852383

image-20240403140907525

勾选安装后重启,让插件生效。

插件配置

我们安装了三个插件,分别进行配置。

NodeJS

这个插件可以在不同的项目中使用不同的 node 环境,例如 A项目 使用 node14,B项目 使用 node20 这样。

进入 系统管理 -> 全局工具配置 -> NodeJS 安装 (在最下面)

点击新增:

image-20240403142305544

默认的这个使用的是 nodejs.org 的官方源,虽然现在 nodejs.org 的官方源国内访问也还可以,但为了保险起见,笔者还是换成阿里巴巴源。

image-20240403142424488

点击红框里的 X 删除当前安装,在点击新增安装,选择 Install from nodejs.org mirror

image-20240403142605321

镜像地址填入https://mirrors.aliyun.com/nodejs-release/,版本按需选择,笔者这里选择的是 node20-lts,并且安装了包管理工具 pnpm,如果读者的项目需要别的全局安装的包,也可以写在 Global npm packages to install ,比如 yarncnpm 之类的。

记得起一个别名:

image-20240403153355639

配置好后点击保存。

一般来说,在使用 npm 时,需要更改 npm 的源,同样在 Jenkins 中也是可以的。

安装完 NodeJS 插件后,系统设置中会多一项 Managed files

image-20240403143048480

进入后选择左侧的Add a new Config,然后选择 Npm config file,然后点击 Next

image-20240403143327739

image-20240403143449389

新增一个 NPM Registry,填入阿里巴巴镜像源:http://registry.npmmirror.com

至此 NodeJS 相关的配置就完成了。

SSH Server

打包后需要通过 SSH 部署到服务器上,因此需要先配置好 SSH 服务器。

打开 系统管理 -> 系统配置 -> Publish over SSH (在最下面):

image-20240403143805027

然后根据实际情况进行填写:

image-20240403144201158

字段解释
Name显示在 Jenkins 中的名称,可随意填写
Hostname服务器地址,ip 或 域名
UsernameSSH 登录的用户名
Remote DirectorySSH 登录后进入的目录,必须是服务器中已经存在的目录,设置好之后所有通过 SSH 上传的文件只能放在这个目录下

这里笔者使用用户名-密码的方式登录 SSH,如果要通过 SSH Key 的方式的话,需要在字段 Path to key 填入 key 文件的地址,或者直接将 key 的内容填入 Key 字段中:

image-20240403144737759

设置好可以通过Test Configuration,测试 SSH 连通性:

image-20240403144822057

出现 Success 代表 SSH 配置成功。

钉钉通知(可选)

如果不需要通过钉钉通知,可以不装 DingTalk 插件,并跳过本节内容。

钉钉部分设置

该功能需要一个钉钉群,并打开钉钉群机器人:

image-20240403145500789

点击添加机器人,选择自定义:

image-20240403145604092

这里笔者的安全设置选择了加签:

image-20240403145717147

将签名保存下来备用。

点击完成后,出现了钉钉机器人的 Webhook 地址。

image-20240403145823192

将地址保存下来备用。

至此钉钉部分的设置就结束了。

Jenkins 部分

打开 系统设置 -> 钉钉 (在最下面的未分类中):

image-20240403145150439

根据需要配置通知时机:

image-20240403145231249

然后点击机器人-新增:

image-20240403145303034

将刚刚的钉钉机器人的签名和 Webhook 地址填入对应的地方,并点击测试:

image-20240403150049799

此时钉钉机器人也在群中发了消息:

image-20240403150138516

至此钉钉机器人配置完毕。

创建任务(job)

本文中,笔者将以存储在 Git 仓库中的项目为例。

Github 项目

注意,如果想让 Github 项目全自动构建的话,需要你的 Jenkins 能被公网访问到,例如部署在云服务器上,像笔者这样部署在本地局域网中,是无法实现“提交代码 -> 自动构建 -> 自动部署”的,只能实现“提交代码 -> 手动点击开始构建 -> 自动部署”

如果在 Jenkins 新手向导里选择了 安装推荐插件,那么现在就不需要额外安装 Github 相关的插件了,否则的话需要手动安装 Github 相关的插件:

image-20240403151242880

创建项目

选择 Dashboard -> 新建任务:

image-20240403151424735

选择构建一个自由风格的软件项目,点击确定。

General

这部分可以添加钉钉机器人:

image-20240403151545166

源码管理

这里选择 Git:

输入仓库地址:https://github.com/baIder/homepage.git

image-20240403151822101

由于笔者这是一个私有仓库,因此会报错。

在下面的Credentials中,添加一个。

image-20240403151941812

image-20240403152135370

注意,这里的用户名是 Github 用户名,但是密码不是你的 Github 密码,而是你的 Github Access Token!!!

image-20240403152324115

image-20240403152429183

可以在这里创建 Token,需要勾选 admin:repo_hook 、repo 权限。

image-20240403152535951

image-20240403152729685

这里的报错是网络问题,连接 Github 懂得都懂。

image-20240403152824725

分支可以根据实际情况选择。

构建触发器

勾选GitHub hook trigger for GITScm polling,这样在 Git 仓库产生提交时,就会触发构建,属于是真正的核心。

image-20240403153134664

构建环境

勾选 Provide Node & npm bin/ folder to Path

image-20240403153444910

Build Steps

到这里,可以理解为 Jenkins 已经将仓库克隆到本地,并且已经安装好了nodenpmpnpm,接下来就是执行命令:

image-20240403153625159

我们需要执行命令:

 node -v
 pnpm -v
 
 rm -rf node_modules
 rm -rf dist
 
 pnpm install
 pnpm build

这里的pnpm build需要按情况更换为package.json中设定的命令。

image-20240403153850007

image-20240403153750787

构建后操作

经过所有的流程到这里,项目应该已经打包在dist目录下了。现在可以通过 SSH 将打包好的产物上传到服务器上了:

image-20240403154044658

image-20240403155757484

这里的 Source files 字段一定要写成dist/**/**,如果写成dist/*,则只会将第一层的文件上传。

Remove prefix 需要填写,否则会将dist这个目录也上传到服务器上。

Remote directory 是相对于配置 SSH Server 时的 Remote directory 的,本例中就是 /data/sites/homepage 。

Exec command 是文件上传后执行的命令,可以是任何命令,可以是让nginx有权限访问这些数据,重启nginx等等,根据服务器实际情况更改。

当然也可以在 Build Steps 中 build 完成后将 dist 目录打包,然后在通过 SSH 将压缩包上传到服务器,然后在 Exec command 中解压。

至此所有的配置已经完成,保存。

测试

点击左侧的 立即构建:

image-20240403154858929

image-20240403154950197

第一次构建会比较慢,因为需要下载node,安装依赖等等,可以从控制台看到,命令都如期执行了:

image-20240403155359524

构建成功,钉钉机器人也提示了(因为 Github 访问失败的原因,多试了几次):

image-20240403155855959

笔者已经配置好了nginx,因此可以直接访问网页,查看效果:

image-20240403160008179

通过 Git 提交触发构建

目前虽然构建成功了,但是需要手动点击构建,接下来实现如何将代码提交 Git 后自动触发构建。

打开仓库设置 -> Webhooks 添加一个:

image-20240403160353025

这里的 Payload URL 就是 Jenkins 地址 + /github-webhook,例如笔者的就如图所示。

但是由于笔者的 Jenkins 部署在本地局域网,因此是不行的,Github 肯定是无法访问到笔者的局域网的,有公网地址的读者可以试试,在笔者的阿里云服务器上是没有问题的。所以目前如果是 Github 项目的话,笔者需要提交代码后手动点击 立即构建:

image-20240403161026497

Gitlab 项目

实际上笔者所在公司是在局域网中部署了 Gitlab 的,因此针对 Gitlab 项目的自动化才是核心。

安装 Gitlab 插件:

image-20240403161442736

安装完毕后重启 Jenkins。

获取 Gitlab token

与 Github 的流程类似,也需要在 Gitlab 中创建一个 token:

image-20240403161807711

创建好之后保存 token 备用。

在 Jenkins 中配置 Gitlab

打开 Jenkins -> 系统管理 -> 系统配置 -> Gitlab

image-20240403162301361

这里需要新建一个Credentials,点击下方的添加:

类型选择GitLab API token,将刚刚保存的 token 填入到 API token 字段中。

image-20240403162144399

点击Test Connection

image-20240403162651637

出现Success说明配置成功。

创建项目

大多数过程与 Github 项目雷同。

General

会多出一个选项,选择刚刚添加的:

image-20240403163406501

源码管理

Git 仓库地址填 Gitlab 仓库地址,同样会报错,添加一个Credentials便可解决:

image-20240403163538105

用户名密码填登录 Gitlab 的用户名密码即可。

构建触发器

按需选择触发条件,这里笔者仅选择了提交代码:

这里红框中的 url 需要记下,后面要用。

image-20240403164252265

其他配置

与 Github 项目相同。

测试构建

点击立即构建,查看是否能构建成功:

image-20240403163945821

构建成功:

image-20240403164002293

提交代码自动构建

进入 Gitlab 仓库 -> 设置 -> 集成:

这里的 url 填入刚刚 Jenkins 构建触发器 中红框内的 url 地址。

image-20240403164203308

看情况是否开启 SSL verification。

点击 Add webhook:

image-20240403164449921

测试一下:

image-20240403164513668

可以看到 Jenkins 那边已经开始构建了:

image-20240403164551282

构建成功:

image-20240403164606737

测试 Git 提交触发构建

目前页面:

image-20240403164712339

我们将v2.0-f改成v2.0-g

image-20240403164817371

提交代码,Jenkins 开始了自动构建:

image-20240403164852625

构建成功,页面也发生了变化:

image-20240403164912343

至此,Gitlab 提交代码后自动打包并部署至服务器的流水线就完成了。

后记

本文实现了从提交代码到部署上线的自动化工作流,适合小公司的小型项目或自己的演示项目,大公司一定会有更规范更细节的流程,笔者也是从实际需求出发,希望本文能帮助到各位,由于笔者也是第一次使用 Jenkins,如有不足或错误之处,请读者批评指正。


作者:bald3r
来源:juejin.cn/post/7354406980784504870

收起阅读 »

Android 将json数据显示在RecyclerView

json数据要通过Get请求获取,这里有个重要的知识点,get请求需要拼接url的 本次拼接url的参数为phone,由于登录的时候已经填了手机号,如果这里再收集手机号就会让客户体验变差,于是我采用了SharedPreferences进行记录并调出 Share...
继续阅读 »

json数据要通过Get请求获取,这里有个重要的知识点,get请求需要拼接url的
本次拼接url的参数为phone,由于登录的时候已经填了手机号,如果这里再收集手机号就会让客户体验变差,于是我采用了SharedPreferences进行记录并调出


SharedPreferences pref=getSharedPreferences("data",MODE_PRIVATE);
String phone=pref.getString("phone","");

得到了phone之后,我采用了okhttp请求返回json,注意:进行网络请求都需要开启线程以及一些必要操作
例如


<uses-permission android:name="android.permission.INTERNET" /> 

url为你申请的网络url


 new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client=new OkHttpClient().newBuilder()
.connectTimeout(60000, TimeUnit.MILLISECONDS)
.readTimeout(60000,TimeUnit.MILLISECONDS).build();
//url/phone
Request request=new Request.Builder().url("url/phone"+phone).build();
try {
Response sponse=client.newCall(request).execute();
String string = sponse.body().string();
Log.d("list",string);
jsonJXDate(string);
}catch (IOException | JSONException e){
e.printStackTrace();
}
}
}).start();

由上可知,string即为所需的json


展示大概长这样


{
"code": 200,
"message": "成功",
"data": [
{
"id": "string",
"createTime": "2023-04-18T05:50:08.905+00:00",
"updateTime": "2023-04-18T05:50:08.905+00:00",
"isDeleted": 0,
"param": {},
"phone": "15019649343",
"commercialTenant": "string",
"payTime": "2023-04-18T05:50:08.905+00:00",
"type": "string",
"paymentType": "string",
"bills": [
{
"product": "烧烤",
"amount": "4",
"price": "60",
"subtotal": "240"
}
],
"total": "string"
},
{
"id": "643e9efb09ecf071b0fd2df0",
"createTime": "2023-04-18T13:28:35.889+00:00",
"updateTime": "2023-04-18T13:28:35.889+00:00",
"isDeleted": 0,
"param": {},
"phone": "15019649343",
"commercialTenant": "string",
"payTime": "2023-04-18T13:28:35.889+00:00",
"type": "string",
"paymentType": "string",
"bills": [
{
"product": "兰州拉面",
"amount": "5",
"price": "40",
"subtotal": "200"
}
],
"total": "string"
}
],
"ok": true
}

我所需要的是payTime,product,subtotal


有{}用JSONObject,有[]用JSONArray,一步步来靠近你的需要


JSONObject j1 = new JSONObject(data);
try {
JSONArray array = j1.getJSONArray("data");
for (int i=0;i<array.length();i++){
j1=array.getJSONObject(i);
Map<String,Object>map=new HashMap<>();
String payTime = j1.getString("payTime");
JSONObject bills = j1.getJSONArray("bills").getJSONObject(0);
String product = bills.getString("product");
String subtotal = bills.getString("subtotal");
map.put("payTime",payTime);
map.put("product",product);
map.put("subtotal",subtotal);
list.add(map);
}
Message msg=new Message();
msg.what=1;
handler.sendMessage(msg);

}catch (JSONException e){
e.printStackTrace();
}

}
public Handler handler=new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
//添加分割线
rv.addItemDecoration(new androidx.recyclerview.widget.DividerItemDecoration(
MeActivity.this, androidx.recyclerview.widget.DividerItemDecoration.VERTICAL));
MyAdapter recy = new MyAdapter(MeActivity.this, list);
//设置布局显示格式
rv.setLayoutManager(new LinearLayoutManager(MeActivity.this));
rv.setAdapter(recy);
break;
}
}
};

在adapter处通过常规layout显示后填入数据


 //定义时间展现格式
Map<String, Object> map = list.get(position);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
LocalDateTime dateTime = LocalDateTime.parse(map.get("payTime").toString(), formatter);
String strDate = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));

holder.produce.setText(map.get("product").toString());
holder.payTime.setText(strDate);
holder.price.setText(map.get("subtotal").toString());

就大功告成啦,由于后台那边还没把base64图片传上来,导致少了个图片,大致就是这样的


6a2d483f93267f3cd09f25576c1f29c.jpg


作者:m924
来源:juejin.cn/post/7224841852305588280
收起阅读 »

Android 开发中是否应该使用枚举?

前言在Android官方文档推出性能优化的时候,从一开始有这样一段说明:Enums often require more than twice as much memory as static constants. You should strictly av...
继续阅读 »

前言

Android官方文档推出性能优化的时候,从一开始有这样一段说明:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

意思是说在 Android 平台上 avoid 使用枚举,因为枚举类比一般的静态常量多占用两倍的空间。

由于枚举最终的实现原理还是类,在编译完成后,最终为每一种类型生成一个静态对象,而在内存申请方面,对象需要的内存空间远大于普通的静态常量,而且分析枚举对象的成员变量可知,每一个对象中默认都会有一个字符数组空间的申请,计算下来,枚举需要的空间远大于普通的静态变量。

如果只是使用枚举来标记类型,那使用静态常量确实更优,但是现在翻看官方文档发现,这个建议已经被删除了,这是为什么那 ? 具体看 JakeWharton 在 reddit 上的一个评论

The fact that enums are full classes often gets overlooked. They can implement interfaces. They can have methods in the enum class and/or in each constant. And in the cases where you aren't doing that, ProGuard turns them back int0 ints anyway.
The advice was wrong for application developers then. It's remains wrong now.

最重要的一句是

ProGuard turns them back int0 ints anyway.

在开启 ProGuard 优化的情况下,枚举会被转为int类型,所以内存占用问题是可以忽略的。具体可参看 ProGuard 的优化列表页面 Optimizations Page,其中就列举了 enum 被优化的项,如下所示:

class/unboxing/enum

Simplifies enum types to integer constants, whenever possible.

ProGuard官方出了一篇文章 ProGuard and R8: Comparing Optimizers(大致意思就是自己比R8强 ),既ProGuard会把枚举优化为整形.但是安卓抛弃了了ProGuard,而是使用了R8作为混淆优化工具。我们重点看下R8对枚举优化的效果如何 ?

R8对枚举优化

下面通过以下例子验证一下在真实的开发环境中R8对枚举优化的支持效果。 代码如下:

  1. 定义一个简答枚举类Language
package com.example.enum_test;

public enum Language {

English("en", "英文"), Chinese("zh", "中文");

String webName;
String zhName;

Language(String webName, String zhName) {
this.webName = webName;
this.zhName = zhName;
}
}
  1. MainActivity主要代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Language language = null;
if (Math.random() < 0.5) {
doEnumAction(Language.English);
} else {
doEnumAction(Language.Chinese);
}
// doNumberAction(CHINESE);
}


private void doEnumAction(Language language) {
switch (language) {
case English:
System.out.println("english ");
break;
case Chinese:
System.out.println("chinese");
break;
}
System.out.println(language.name());
}

3.build.gradle.kts文件内开启混淆

buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
  1. 将编译后的apk反编译结果如下(枚举类被优化):

截屏2023-11-07 20.55.14.png

以上结果可以看出,如果是一个简单的枚举类,那么枚举类将会被优化为一个整形数字。既然ProGuard/R8会把枚举优化为整形,那是不是在Android中,就可以继续无所顾忌的使用枚举了呢? 我没有找到官方对R8枚举具体的优化场景说明 ,只找了ProGuard对枚举的优化有一定的限制条件,如果枚举类存在如下的情况,将不会有优化为整形,如下所示:

  1. 枚举实现了自定义接口。并且被调用。
  2. 代码中使用了不同签名来存储枚举。
  3. 使用instanceof指令判断。
  4. 使用枚举加锁操作。
  5. 对枚举强转。
  6. 在代码中调用静态方法valueOf方法
  7. 定义可以外部访问的方法。

参考自:ProGuard 初探 · dim's blog,另外,上面的这七种情况,我并没有找到官方的说明,如果有哪位读者知道,请在评论区里留下链接,谢谢啦~

下面我们对以上的情况进行追一验证,看下这些条件是否也会对R8编译优化产生限制 , 如下 :

  1. 枚举实现了自定义接口,并且被调用。
public interface ILanguage {
int getIndex();
}


public enum Language implements ILanguage{
English("en", "英文"), Chinese("zh", "中文");

String webName;
String zhName;

Language(String webName, String zhName) {
this.webName = webName;
this.zhName = zhName;
}

@Override
public int getIndex() {
return this.ordinal();
}
}

// 调用如下
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ILanguage iLanguage = Language.Chinese;
System.out.println(iLanguage.getIndex());
}

反编译结果如下(枚举类被优化):

截屏2023-11-07 18.37.01.png

  1. 代码中使用了不同签名来存储枚举。

在对以下代码调用的时候,使用一个变量保存枚举值,由于currColor变量声明类型的不同, 导致枚举的优化结果也不同

// 枚举会被优化
Signal currColor = Signal.RED;

//发生了类型转换,变量签名不一致,枚举不会被优化
Object currColor = Signal.RED;

public void change(Signal color) {
switch (color) {
case RED:
currColor = Signal.GREEN;
break;
case GREEN:
currColor = Signal.YELLOW;
break;
case YELLOW:
currColor = Signal.RED;
break;
}
}


protected void onCreate(Bundle savedInstanceState) {

double random = Math.random();
if (random > 0.5f) {
change(GREEN);
} else if (random > 0.5 && random < 0.7) {
change(RED);
} else {
change(Signal.YELLOW);
}
// 最终也是被优化为if语句
//switch (currColor) {
// case RED:
// System.out.println("红灯");
// break;
// case GREEN:
// System.out.println("绿灯");
// break;
// case YELLOW:
// System.out.println("黄灯");
// break;
//}

if (currColor == RED) {
System.out.println("红灯");
} else if (currColor == GREEN) {
System.out.println("绿灯");
} else if (currColor == YELLOW) {
System.out.println("黄灯");
}
}

Signal currColor = Signal.RED; 时 ,枚举被优化整数

截屏2023-11-10 11.41.41.png

Object currColor = Signal.RED;时 ,枚举未被优化

截屏2023-11-10 11.34.39.png

  1. 使用instanceof指令判断。 (发生了类型转换)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean result = getObj() instanceof Language;
System.out.println(result);
}

Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}

反编译结果如下(枚举类未被优化):

截屏2023-11-10 12.13.48.png

  1. 使用枚举加锁操作。
synchronized (Language.Chinese) {
System.out.println("synchronized");
}

从反编译结果如下(枚举类未被优化):

截屏2023-11-07 18.23.22.png 可以看到在该场景下枚举类没有被优化。

  1. 不要作为一个输出或打印对象
System.out.println(RED);

从反编译结果如下(枚举类未被优化):

截屏2023-11-10 12.17.11.png

  1. 对枚举强转。 比如下代码不会出现枚举优化
  @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean result = (getObj()) != null;
System.out.println(result);
}

// 如果返回值类型和返回的枚举类型不一致时,也不会优化枚举。
@Nullable
Object getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}

反编译结果如下:(枚举类未被优化): 截屏2023-11-07 20.07.43.png

如果把返回值修改为Language则会发生优化

@Nullable
Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}

反编译结果如下:(枚举类被优化):

截屏2023-11-07 20.15.21.png

以下代码也会出现枚举被优化,把方法的返回值类型修改为 Language ,接收变量类型改为 Object

 @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Object language = getObj();
boolean result= language != null;
System.out.println(result);
}


@Nullable
Language getObj() {
return Math.random() > 0.5 ? Language.Chinese : null;
}

截屏2023-11-07 20.34.54.png

  1. 定义可以外部访问的方法。 R8对枚举的优化并不受定义外部方法的影响,如下在枚举内定义getLanguage方法后,枚举仍被优化
package com.example.enum_test;

import androidx.annotation.Nullable;

public enum Language {
English("en", "英文"), Chinese("zh", "中文");

String webName;
String zhName;

Language(String webName, String zhName) {
this.webName = webName;
this.zhName = zhName;
}

@Nullable
public Language getLanguage(String name) {
if (English.webName.equals(name)) {
return Language.English;
} else {
return null;
}
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Language language = Language.getLanguage(Math.random() > 0.5f ? "en" : "zh");
boolean result= language != null;
System.out.println(result);
}

apk反编译结果如下(枚举被优化):

截屏2023-11-07 20.45.17.png

复杂多变的枚举优化

在测试中发现一个问题 ,同样的代码,放在不同的文件内,优化效果竟然也不同。

  1. 在 MainActivity定义如下方法
public void change(Signal color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
break;
}
}

public void test() {
double random = Math.random();
if (random > 0.5f) {
change(GREEN);
} else if (random > 0.5 && random < 0.7) {
change(RED);
} else {
change(YELLOW);
}

if (currColor == RED) {
System.out.println("红灯");
} else if (currColor == GREEN) {
System.out.println("绿灯");
} else {
System.out.println("黄灯");
}
}

并在onCreate方法内执行, 从下面的反编译结果中可以看到枚举被优化了。

截屏2023-11-10 14.29.32.png

  1. 相同的代码如果定义在 TrafficLight类中, 并在MainActivityonCreate方法中运行 ,如下:
package com.example.enum_test;
import static com.example.enum_test.TrafficLight.Signal.GREEN;
import static com.example.enum_test.TrafficLight.Signal.RED;
import static com.example.enum_test.TrafficLight.Signal.YELLOW;

public class TrafficLight {

enum Signal {
GREEN, YELLOW, RED;
}

private Signal currColor = RED;

public void change(Signal color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
}

}


public void test() {
double random = Math.random();
if (random > 0.5f) {
change(GREEN);
} else if (random > 0.5 && random < 0.7) {
change(RED);
} else {
change(YELLOW);
}

if (currColor == RED) {
System.out.println("红灯");
} else if (currColor == GREEN) {
System.out.println("绿灯");
} else {
System.out.println("黄灯");
}
}
}
// onCreate 内执行
TrafficLight trafficLight = new TrafficLight();
trafficLight.test();

截屏2023-11-10 14.43.23.png

从上面的对比中发现,相同的枚举代码操作放在Activity和 放在普通类中 ,编译结果是不同的。导致这种问题的原因还是因为Activity默认是配置了防混淆的,如果对一个类成员添加了防混淆配置,编译会尽可能的对类里面相关的枚举使用优化为一个常量,但这不是一定的,枚举的优化会受到其他因素影响,例如 锁对象、类型转换等其它条件限制。而TrafficLight默认是没有被配置防混淆的,如果类内定义了枚举变量,编译器会对类进行一系列的编译优化和函数内联等处理,枚举变量被抽取到一个工厂公共类里内部,枚举变量对象被指向一个Object类型引用,编译器不会对枚举进行优化。如果对类进行防混淆配置后,该类内部枚举代码会被优化为一个整数常量,结果如下:

配置 -keep class com.example.enum_test.TrafficLight效果

截屏2023-11-10 15.16.59.png

配置#-keepclassmembernames class com.example.enum_test.TrafficLight效果

截屏2023-11-10 15.31.18.png

引用代码

截屏2023-11-10 15.17.25.png

如果对具有引用枚举类型变量的类进行了防混淆配置处理TrafficLight内的枚举引用也全部被优化为了整数类型。

如果未对TrafficLight类进行防混淆配置,这个类的相关成员可能会被抽取到一个公共类里。 currColor 就是m0f1749b属性, 该属性是一个Object类型,这也是可能是导致枚举未完全优化为整数的原因, 从 m0 的代码中可以看到编译器将多个实例的构造统一只使用了一个Object作为引用, 方法也被编译到m0类内部,可以看到m0类不是一个TrafficLight,猜测这也是编译器在对枚举进行整型优化和枚举持有类优化一种权衡和选择吧 。

截屏2023-11-10 16.05.23.png

截屏2023-11-10 16.05.54.png

枚举 、常量

从编译结果来看,枚举由于会构建多个静态对象ordinal()values()等函数和变量的存在,确实会比普通的静态对象或常量更加占用空间和内存。但是从上面的测试结果中可以看到 ,枚举在最佳情况下可以被优化为整型,达到和常量一样的效果。

截屏2023-11-10 16.19.04.png

总结

以下场景都会阻止枚举优化 :

  1. 使用instanceof指令判断。
  2. 使用枚举作为锁对象操作
  3. System.out.println(enum) 输出
  4. 枚举作为返回值返回时,返回参数的声明类型与枚举不一致,请参考 例6
  5. 混淆优化配置影响枚举优化, 如果一个类中有变量是一个枚举类型,如果该类未在proguard-rules.pro配置混淆优化处理,该类则可能会被编译器优化掉,其变量和方法会被抽取到一个公共类或者内敛到引用类里, 且枚举类不会被优化,因为枚举变量公共类被一个Object类型变量引用持有。
  6. 常规的枚举使用,R8都会对枚举进行一定程度的优化,最好的情况下会优化成一个整数常量,性能几乎不会有任何影响。

我的结论是如果我们可以通过定义普通常量的方式代替枚举,则优先通过使用定义常量解决。因为枚举本身确实会带来导致包体积和内存的增长, 而枚举被优化的环境和条件实在是过于苛刻,例如可能在输出语造成打印了一下枚举System.out.println(enum),一不小心可能就会造成举优化失败。也不是不能使用枚举,权衡易用性和性能以及使用场景,可以考虑继续使用枚举,因为枚举在有些时候确实让代码更简洁,更容易维护,牺牲点内存也无妨。况且Android官方自己也在许多地方应用了枚举,例如Lifecycle.StateLifecycle.Event等 。

小彩蛋

前几天群里在讨论 京东金融Android瘦身探索与实践 文章,内容中一点优化是关于枚举的 。

截屏2023-11-07 21.14.13.png

我感觉他们以这个例子没有很强的说服力,原因如下 :

  1. 如果对持有枚举变量的类或者变量进行混淆配置后 ,编译器会对枚举进行优化 ,TrafficLight 内枚举的引用被替换为整数,从反编译结果可以看到优化后的代码就是普通的if语句,并不会出现所谓的占用大量体积的情况。

image.png

  1. 如果枚举相关类未进行完全优化,但是例子中的change()方法并不会导致大量增加包体 ,只是增加了4行字节码指令。但是枚举的定义的确会占用一定的包体积大小,这个毋庸置疑。

使用枚举实现以及编译后字节码如下 :

public class TrafficLight {

enum Signal {
GREEN, YELLOW, RED;
}

private Signal currColor = RED;

public void change(Signal color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
}

}
}
// 22行字节码指令
.method public change(Lb1/a;)V
.registers 3
invoke-virtual {p1}, Ljava/lang/Enum;->ordinal()I
move-result p1
if-eqz p1, :cond_15
const/4 v0, 0x1
if-eq p1, v0, :cond_12
const/4 v0, 0x2
if-eq p1, v0, :cond_d
goto :goto_18
:cond_d
sget-object p1, Lb1/a;->a:Lb1/a;
:goto_f
iput-object p1, p0, Lcom/example/enum_test/TrafficLight;->currColor:Lb1/a;
goto :goto_18
:cond_12
sget-object p1, Lb1/a;->c:Lb1/a;
goto :goto_f
:cond_15
sget-object p1, Lb1/a;->b:Lb1/a;
goto :goto_f
:goto_18
return-void
.end method

使用常量实现相同功能编译后字节码如下 :

package com.example.enum_test;


public class TrafficLightConst {

public static final int GREEN = 0;
public static final int YELLOW = 1;
public static final int RED = 2;

private int currColor = RED;

public void change(int color) {
switch (color) {
case RED:
currColor = GREEN;
break;
case GREEN:
currColor = YELLOW;
break;
case YELLOW:
currColor = RED;
}

}
}
// 18行字节码指令
.method public change(I)V
.registers 4
const/4 v0, 0x1
if-eqz p1, :cond_10
const/4 v1, 0x2
if-eq p1, v0, :cond_d
if-eq p1, v1, :cond_9
goto :goto_12
:cond_9
const/4 p1, 0x0
iput p1, p0, Lcom/example/enum_test/TrafficLightConst;->currColor:I
goto :goto_12
:cond_d
iput v1, p0, Lcom/example/enum_test/TrafficLightConst;->currColor:I
goto :goto_12
:cond_10
iput v0, p0, Lcom/example/enum_test/TrafficLightConst;->currColor:I
:goto_12
return-void
.end method

参考

zhuanlan.zhihu.com/p/91459700

jakewharton.com/r8-optimiza…


作者:Lstone
来源:juejin.cn/post/7299666003364249650

收起阅读 »

android之阿拉伯语适配及注意细节

1.  AndroidManifest.xml配置文件中的 标签下,配置元素 android:supportsRtl="true"。此时当系统语言切换的时候,你的 App 也会跟着切换 UI 布局为镜像后的效果。若未增加该元素,在xml中切换语言时,...
继续阅读 »

1.  AndroidManifest.xml配置文件中的 标签下,配置元素 android:supportsRtl="true"。此时当系统语言切换的时候,你的 App 也会跟着切换 UI 布局为镜像后的效果。

若未增加该元素,在xml中切换语言时,会提示 image.png 增加后,可在xml文件中查看反转后的效果 2.  新增value-ar文件夹

image.png

image.png

image.png 把values/strings.xml文件复制到values-ar文件中,逐条翻译即可。

  1. layout中的Left/Right修改为Start/End

可使用Android Studio中自带的工具:“工具栏”-“Refactor”-“Add right-to-Left(RTL)Support” image.png

注意事项:

  • 1).此时会把所依赖gradle里的xml文件列出,记得删除,不要转换。

image.png

  • 2). 该工具只适用于项目的app模块,无法直接应用于依赖模块。如果需要在依赖模块中进行RTL转换,要逐个打开并手动进行相应的修改。
  • 3). Start属性在LTR中对应Left,End属性在LTR中对应Right,在API 17开始支持,为了兼容低版本,可以同时有Left和Start。

    即在“Add right-to-Left(RTL)Support”工具中,不勾选“Replace Left/Right Properties with Start/End Properties”

image.png

  1. 返回icon、下一个icon等,要针对阿拉伯语新建一个文件夹,放镜像后的图片,规则如下:

mipmap-xhdpi->mipmap-ldrtl-xhdpi

drawable->drawable-ldrtl

最终镜像的图片要UI同事提供,临时修改看效果可以使用镜像图片的网站:http://www.lddgo.net/image/flip

  1. TextView、EditText:利用全局样式,在style.xml中定义,在xml里使用style=”@style/xxx”即可
  • 1). TextView
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
       ...
       <item name="android:textViewStyle">@style/TextViewStyle.TextDirectionitem>
       ...
style>
<style name="TextViewStyle.TextDirection" parent="android:Widget.TextView">
        <item name="android:textDirection">localeitem>
style>
  • 2). EditText
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
       ...
       <item name="editTextStyle">@style/EditTextStyle.Alignmentitem>
       ...
style>
<style name="EditTextStyle.Alignment" parent="@android:style/Widget.EditText">
        <item name="android:textAlignment">viewStartitem>
        <item name="android:gravity">startitem>
        <item name="android:textDirection">localeitem>
style>
  1. 其他细节
  • 1).固定ltr,如阿拉伯语下的“99%”要从左到右展示,可在xml中使用
android:layoutDirection ="ltr"
  • 2).获取当前系统语言Locale.getDefault().getLanguage()

判断是否为阿拉伯语:"ar".equals(Locale.getDefault().getLanguage())

判断是否为英语:"en".equals(Locale.getDefault().getLanguage())

  • 3). drawable/xxx_selector.xml中item里有android:drawable,如勾选框。

drawable有android:autoMirrored属性,将selector的该属性设置为true,就可以让drawable在RTL布局下进行反转

image.png

  • 4).进度条的默认进度指示是从左到右,使用leftMargin;在阿拉伯语下,进度指示从右到左,使用rightMargin属性
  • 5).阿拉伯语环境下,使用SimpleDateFormat格式化时间字符串的时候,会显示为:٢٠١٥-٠٩-١٨ ٠٧:٠٣:٤٩。若要展示:2023-09-067:10:45,可以使用Locale.ENGLISH参数
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
Date now=new Date();
System.out.println(sdf .format(now));
  • 6). 加载html可用 tv.setText(Html.fromHtml(getResources().getString(R.String.xxx));
  • 7). 开机导航中设置了阿拉伯语,当前页面布局要刷新,可以重写activity的onConfigurationChanged()方法,如在该方法里重置下一步箭头、指示器样式等

image.png

  • 8).ViewPager

若是ViewPager,可使用第三方控件RtlViewPager替换: 521github.com/diego-gomez…,添加依赖,单纯替换原ViewPager即可

implementation 'com.booking:rtlviewpager:1.0.1' 

类似三方控件: 521github.com/duolingo/rt…

或者使用androidx的ViewPager2替换: developer.android.google.cn/jetpack/and…,支持RTL布局

image.png

image.png

  • 9). 固定RTL字符串的顺序

问题现象:EditText带hint,密码可见、不可见时,会调用如下方法进行设置

image.png 此时会影响hint的展示:在勾选时,hint的结束字符在右侧;不勾选时,hint的结束字符在左侧。

image.png

image.png

解决方法:此时要使用Unicode控制字符来限制整个字符串的显示方向:\u202B 和 \u202C。

image.png

有以下两种方法

a.  java代码

image.png

b.  strings.xml

image.png

最终效果:

image.png

image.png

10). Blankj的toast展示异常

android工具类Blankj的toast工具类在展示阿拉伯语时为空或者部分展示,建议使用1.30.6 及以上版本

image.png

github.com/Blankj/Andr…

11). RTL布局中出现双光标/光标截断的情形

image.png

在布局文件内加上如下两个属性即可:

android:textDirection="anyRtl"
android:textAlignment="viewStart"

若还未解决

1.可查看是否使用了android:textCursorDrawable=“@null”,若有,可尝试去掉该句。

2.在AndroidManifest.xml中查看当前App/Activity的主题,比较老的项目可能使用了android:Theme.NotitleBar/android:Theme.Light等轻量级主题,如下所示:




收起阅读 »

微信小程序dom节点最高限制16000?能忍?盘它!!

web
起因 最近参与了个微信小程序的项目,有个长列表模块 我在自测的时候(测试人员居然没发现,可能懒得加载那么多数据去验证),发现当列表加载加载500条以上的时候,会有一个 Dom limit exceeded, please check if there's an...
继续阅读 »

起因


最近参与了个微信小程序的项目,有个长列表模块 我在自测的时候(测试人员居然没发现,可能懒得加载那么多数据去验证),发现当列表加载加载500条以上的时候,会有一个 Dom limit exceeded, please check if there's any mistake you've made.(超过了Dom限制,请检查你是否犯了任何错误) 渲染报错,页面会白屏,所以猜测小程序的Dom节点应该是有最高限制的。


分析问题原因


网上查了很多资料,查到了Dom节点确实有渲染限制,有位大神还晒出了源码 如下图,限制最高16000个节点


image.png


之前只知道 包大小 限制、域名需要配置不然限制、各种授权api限制,没想到 dom节点数量还要限制 居然还有这种骚操作


图片.png


不过想想也算合理,因为小程序 小程序,小才行,哈哈,资源也不可能无限大,为的就是让你在有限的资源内完成强大的功能。


实验是检验真理的唯一标准


上文图中 代码 左上角显示版本是2019年的,那现在都2023年了,会不会有所改进呢,为了搞清楚现在到底限制多少节点,实验一波。


c5d0486839eb2ee8d6711d4985df6b4.png


136f39b2c09b1ff611f4bcb15157f65.png


测试结果如上面两张图 Dom限制数量没变还是 16000个节点,但实际可新增的 渲染节点为 15999个 ,你要是问为啥,那我告诉你,因为小程序默认根标签 page 也占用一个,一个字 牛!!


图片.png


解决方案


既然找了原因,就要想办法解决,经过一番深思熟虑(面向百度思考),总结了以下几个解决方案。


方案一 人在屋檐下不得不低头


毕竟平台是人家的,规则都是人家定的,那么就得按照规则去开发,下图是官方推荐说明(也是评分标准),单页面节点尽量不超过1000个节点,嵌套不超过30层,子节点不超过60个


image.png
动动你灵活嘴皮子,看看能不能说服 产品 说服老板,就这标准 爱做不做,爱谁谁


方案二 就是不服 就是干


长列表 数据过多、dom节点过多 确实有性能问题,但谁让需求是刚需呢,看了下网上的方案 五花八门,有利用swiper 始终只展示固定数量的 swiper-item的 根据当前的 index 显示第几页数据。还有有自己写算法动态显示的。也有很多现成的组件


核心原理 我总结了下就是,只渲染显示在屏幕的数据(为了减少白页面和滑动更丝滑,要再当前屏幕的前后再分别多渲染一屏幕),实现就是监听 scroll 事件,并且重新计算需要渲染的数据,不需要渲染的数据留一个空的 view 空白占位元素。


具体实现方案就不在这里展开了,留下几个已经实现的链接地址


可以扫码体验下腾讯官方新闻小程序的 渲染效率 感觉很哇塞




小程序长列表组件


文章1


文章2


懒人方案 官方 [Skyline 渲染引擎] list-view


注意点: 列表布局容器,仅支持作为 scroll-view 自定义模式下的直接子节点或组件直接子节点


解释下,就是要注意2点


1 scroll-view要设置自定义模式 type="custom"


2 list-view要作为 scroll-view 直接子节点(不然会失去效果)


示例代码如下


    <scroll-view type="custom">
<list-view>
...
循环列表逻辑
...
</list-view>
</scroll-view>

作者:iwhao
来源:juejin.cn/post/7245223225575374905
收起阅读 »

JavaScript变量的奥秘:从声明到使用,一文掌握!

在编程的世界里,数据是构建一切的基础。而在JavaScript中,变量就是存储数据的容器。它们就像是我们生活中的盒子,可以装下各种物品,让我们在需要的时候随时取用。今天,就让我们一起揭开变量的神秘面纱,探索它们的概念、使用规则,以及那些令人头疼的错误。一、变量...
继续阅读 »

在编程的世界里,数据是构建一切的基础。而在JavaScript中,变量就是存储数据的容器。它们就像是我们生活中的盒子,可以装下各种物品,让我们在需要的时候随时取用。

今天,就让我们一起揭开变量的神秘面纱,探索它们的概念、使用规则,以及那些令人头疼的错误。


一、变量的概念和作用

变量,顾名思义,是可以变化的量。在JavaScript中,变量是用来存储数据的,这些数据可以是数字、字符串、对象等等。想象一下,如果没有变量,我们的程序就会变得非常死板,无法灵活地处理和交换信息。

Description

注意: 变量不是数据本身,它们仅仅是一个用于存储数值的容器。可以理解为是一个个用来装东西的纸箱子。


二、变量的基本使用

1)声明变量

要想使用变量,首先需要创建变量(也称为声明变量或者定义变量),JavaScript中通常使用var关键字或者let关键字进行变量的声明操作。

语法:

var age;       //声明一个名为age的变量
let name; //声明一个名为name的变量
  • 声明变量有两部分构成:声明关键字、变量名(标识符)
  • let 即声明关键字,所谓关键字是在JavaScript中有特殊意义的词汇,比如let、var、function、if、else、switch、case、break等。

举例:

let age
  • 我们声明了一个age变量
  • age 即变量的名称,也叫标识符

2) 变量赋值

声明出来后的变量是没有值的,我们需要对声明出来的变量进行赋值操作。

变量赋值的语法为:

var age;       //声明一个名为age的变量
age = 18; //为该个age变量赋值为18

定义了一个变量后,你就能够初始化它(赋值)。在变量名之后跟上一个“=”,然后是数值。

Description

注意: 是通过变量名来获得变量里面的数据。

3)变量初始化

变量初始化就相当于声明变量和变量赋值操作的结合,声明变量并为其初始化。

变量初始化语法为:

var age = 18;   //声明变量age并赋值为18

案例如下:


<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>变量的使用</title>
</head>

<body>
<script>
// 1. 声明一个年龄变量
let age
// 2. 赋值
age = 18
console.log(age)
// 3. 声明的同时直接赋值 变量的初始化
let age2 = 18
// 小案例
let num = 20
let uname = 'pink老师'
console.log(num)
console.log(uname)
</script>
</body>

</html>

4)更新变量

变量赋值后,还可以通过简单地给它一个不同的值来更新它。

Description

注意: let 不允许多次声明一个变量。
案例如下:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>变量的使用更新</title>
</head>

<body>
<script>
// 1 声明的同时直接赋值 变量的初始化
// let age = 18
// age = 19
// // let age = 19
// console.log(age)
// 2. 声明多个变量
// let age = 18, uname = '迪丽热巴'
// console.log(age, uname)
</script>
</body>

</html>

5)声明多个变量

语法:多个变量中间用逗号隔开

let age=18,uname='pink'

**说明:**看上去代码长度更短,但并不推荐这样。为了更好的可读性,请一行只声明一个变量。

Description

输入用户名案例:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>输入用户名案例</title>
</head>

<body>
<script>
// 输出用户名案例
// 1. 用户输入
// prompt('请输入姓名')
// 2. 内部处理保存数据
let uname = prompt('请输入姓名')
// 3. 打印输出
document.write(uname)
</script>
</body>

</html>

Description


三、let 和var区别

1、var声明的特点:

  • 变量可以先使用再声明(不合理)。

  • var声明过的变量可以重复声明(不合理)。

  • 比如变量提升、全局变量、没有块级作用域等等

2、let 声明的特点:

  • let声明的变量不会被提升,即在声明之前引用let声明的变量系统会直接报错,直接阻断程序的运行。

  • let不可以在同一个作用域下重复声明同一个变量,如果用let重复声明同一个变量,那么这时候就会报错。

  • 用let声明的变量支持块级作用域,在es6提出块级作用域的概念之前,作用域只存在函数里面,或者全局。而es6提出的块级作用域则是一个大括号就是一个块级作用域,该变量只能在块级作用域里使用,否则就会报错。

注意:

var 在现代开发中一般不再使用它,只是我们可能在老版程序中看到它。

let 是为了解决 var 的一些问题而出现的,以后声明变量我们统一使用 let。
案例如下:


<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>let和var的区别</title>
</head>

<body>
<script>
// var可以重复声明,后面声明的覆盖前面的
var num1
var num1 = 10
var num1= 20
console.log(num1)

// let不能重复声明,直接编译不通过
// let num
// let num = 20
// let num = 10
// console.log(num)
</script>
</body>

</html>


四、变量命名规则与规范

规则: 必须遵守,不遵守报错 (法律层面)

  • 不能用关键字(有特殊含义的字符,JavaScript 内置的一些英语词汇,例如:let、var、if、for等)

  • 只能用下划线、字母、数字、$组成,且数字不能开头

  • 字母严格区分大小写,如 Age 和 age 是不同的变量
    **规范:**建议,不遵守不会报错,但不符合业内通识 (道德层面)

  • 起名要有意义

  • 遵守小驼峰命名法:第一个单词首字母小写,后面每个单词首字母大写。例:userName。

Description

案例如下:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>变量的命名规范</title>
</head>

<body>
<script>
// let if = 10
let num1$_ = 11
// let nav-bar = 11
// let 1num = 10
//严格区分大小写
let pink = '老师'
let Pink = '演员'
console.log(pink, Pink)
</script>
</body>

</html>


五、Strict(严格)模式

严格模式是一种限制性更强的JavaScript运行环境。在严格模式下,一些不安全或容易出错的行为会被禁止。

  • JavaScript在设计之初,并不强制要求申明变量,如果一个变量没有申明就被使用,那么该变量就自动被声明为全局变量。

  • 在同一个页面的不同的JavaScript文件中,如果都不声明,将造成变量污染。

  • ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制要求申明变量,否则报错。启用strict模式的方法是在JavaScript代码的第一行写上:

'use strict';

这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

如果浏览器不报错,说明你的浏览器太古老了,需要尽快升级。

'use strict';
// 如果浏览器支持strict模式,下面的代码将报ReferenceError错误:
abc = 'Hello, world';
console.log(abc);


六、常量const的概念和使用

有时候,我们希望某些变量的值在程序运行过程中保持不变。这时,可以使用const关键字来声明一个常量。

const是ES6引入的一个新特性,用于声明常量。常量一旦被声明并赋值后,其值就不能被改变。这为我们提供了一种保护机制,确保某些值不会被意外修改。

  • 使用场景:当某个变量永远不会改变的时候,就可以使用 const 来声明,而不是let。

  • 命名规范:和变量一致

  • 注意: 常量不允许重新赋值,声明的时候必须赋值(初始化)

案例如下:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>常量</title>
</head>

<body>
<script>
// 1.声明常量,使用常量
const PI = 3.14
console.log(PI)
//不允许更改值
//PI = 3.15
// 2. 常量声明的时候必须赋值
//const G
</script>
</body>

</html>


七、常见错误

1、常量必须要赋值

Description

2、常量被重新赋值

Description

3、变量未定义

Description

分析:

  • 提示 age变量没有定义过。

  • 很可能 age 变量没有声明和赋值。

  • 或者我们输出变量名和声明的变量不一致引起的(简单说写错变量名了)。

4、重复声明变量

Description

分析:

  • 提示 “age”已经声明。

  • 很大概率是因为重复声明了一个变量。

  • 注意let 或者const 不允许多次声明同一个变量。

变量是JavaScript编程的基础,掌握了变量的声明和使用,就能更好地理解和编写代码。希望这篇文章能帮助你更好地理解和使用变量,让你的编程之路更加顺畅。

记住,实践是最好的老师,多写代码,多尝试,你会发现,原来变量的世界,也可以如此精彩!

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

润和软件成功举办2023-2024年openEuler技术委员会会议

4月19日,由openEuler社区发起,江苏润和软件股份有限公司(以下简称“润和软件”)承办的2023-2024年openEuler技术委员会会议在南京圆满召开。润和软件董事长兼总裁周红卫代表承办方致辞,openEuler委员会执行总监、开放原子开源基金会T...
继续阅读 »

4月19日,由openEuler社区发起,江苏润和软件股份有限公司(以下简称“润和软件”)承办的2023-2024年openEuler技术委员会会议在南京圆满召开。润和软件董事长兼总裁周红卫代表承办方致辞,openEuler委员会执行总监、开放原子开源基金会TOC副主席熊伟,openEuler技术委员会主席胡欣蔚,润和软件高级副总裁钟毅,润和软件副总裁于萍及多位openEuler技术委员会委员齐聚一堂,围绕openEuler社区技术规划建设及openEuler未来发展方向开展深入研讨。

2023-2024年openEuler技术委员会会议合影

润和软件董事长兼总裁周红卫在致辞中表示,润和软件作为openEuler黄金捐赠人,坚持创新开源技术、推动开源落地、繁荣开源生态,在openEuler社区的贡献一直位于前列,并一直以openEuler的创新价值为全产业赋能。润和软件始终致力于基于openEuler的创新解决方案的产品开发与市场推广,2023年交付重大行业项目的核心业务系统过万套,实现高可靠、高性能运转。2024年,又在操作系统优势基础上提出“All in AI”战略,为客户提供智能化边端的大模型解决方案服务。未来,润和软件将一如既往地深入参与openEuler生态,提供更优的操作系统产品与服务;并与社区协同推动openEuler成为面向全球化的、最具创新力的操作系统开源社区。

润和软件董事长兼总裁周红卫发表致辞

润和软件openEuler业务研发总监张剑就“润和软件openEuler产研进展”做汇报演讲,他分享了润和软件基于openEuler打造的企业级操作系统(HopeOS)。HopeOS聚焦大数据、智能计算场景的服务器操作系统及面向边缘智能场景的嵌入式操作系统提供产品与服务,产品发面,提供包括大数据平台一体化交付、大数据平台原地迁移、基于分布式软总线的openEuler及OpenHarmony协同、基于openEuler的行业发行版定制等综合解决方案;技术服务方面,润和软件为客户提供包括操作系统定制化开发、测试、技术咨询和技术支持等全栈服务,保障关键技术可控、企业级安全可信。

润和软件openEuler业务研发总监张剑进行分享

各参会人员畅所欲言、各抒己见,针对社区重要的版本计划、关键核心特性等内容进行了深入沟通与讨论,群策群力推动openEuler技术创新发展。

2023-2024年openEuler技术委员会会议现场

润和软件凭借在操作系统、大数据、AI等领域的核心技术能力,以应急安全、电网、金融、通信、教育为核心业务场景打造包括基础架构创新方案、基于“openEuler+OpenHarmony”的云边端一体化创新方案等多场景、多维度、一体化、智能化的解决方案。

核心设备适配方面:润和软件先后与宝德、超聚变、浪潮电子等国内最具影响力的服务器生产厂商开展合作。HopeOS也已经取得了包括上述国内顶级服务器提供商在内的数十个服务器厂商的兼容认证,为openEuler在国内各核心行业的业务拓展发挥了重要作用。

核心软件适配方面:润和软件与以达梦数据库、南大通用、人大进仓、瀚高、海量为代表的主流数据库提供商,以及以东方通、保兰德为代表的主流中间件提供商展开合作,并通过产品双向适配及双向产品认证。

操作系统基础架构替换改造及技术创新优化方面:润和软件先后与各主要行业头部核心客户展开合作,在多个垂直赛道取得丰硕成果。并已完成操作系统替换部署数万套,系统运行稳定可靠。

作为openEuler社区核心共建单位和头部企业,润和软件积极参与、加速构建全球开源新生态、开创中国开源新模式。2024润和软件携手openEuler社区拓展海外业务,共同推动openEuler走向全球化。润和软件openEuler业务亮相海外,参与近日在越南河内举行的国际开源盛会FOSSASIASummit 2024,发表精彩演讲并展示创新技术成果,为来自东南亚的金融、互联网等行业用户分享openEuler关于开源、创新和协作的最新动态。

润和软件亮相FOSSASIA Summit 2024展示openEuler创新方案

未来,润和软件将持续深耕优势行业,结合自身在数据要素、人工智能等技术领域的核心优势,打造深度融合行业属性的应用级解决方案,释放科技开源所带来的澎湃创新力量,助力千行百业发展新质生产力。

收起阅读 »

如何仿一个抖音极速版领现金的进度条动画?

效果演示 不仅仅是实现效果,要封装,就封装好 看完了演示的效果,你是否在思考,代码应该怎么实现?先不着急写代码,先想想哪些地方是要可以动态配置的。首先第一个,进度条的形状是不是要可以换?然后进度条的背景色和填充的颜色,以及动画的时长是不是也要可以配置?没错,...
继续阅读 »

效果演示


20230617_064552_edit.gif


不仅仅是实现效果,要封装,就封装好


看完了演示的效果,你是否在思考,代码应该怎么实现?先不着急写代码,先想想哪些地方是要可以动态配置的。首先第一个,进度条的形状是不是要可以换?然后进度条的背景色和填充的颜色,以及动画的时长是不是也要可以配置?没错,起始位置是不是也要可以换?最好还要让速度可以一会快一会慢对吧,画笔的笔帽是不是还可以选择平的或圆的?带着这些问题,我们再开始写代码。


代码实现


我们写一个自定义View,把可以动态配置的地方想好后,就可以定义自定义属性了。


<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DoraProgressView">
<attr name="dview_progressType">
<enum name="line" value="0"/>
<enum name="semicircle" value="1"/>
<enum name="semicircleReverse" value="2"/>
<enum name="circle" value="3"/>
<enum name="circleReverse" value="4"/>
</attr>
<attr name="dview_progressOrigin">
<enum name="left" value="0"/>
<enum name="top" value="1"/>
<enum name="right" value="2"/>
<enum name="bottom" value="3"/>
</attr>
<attr format="dimension|reference" name="dview_progressWidth"/>
<attr format="color|reference" name="dview_progressBgColor"/>
<attr format="color|reference" name="dview_progressHoverColor"/>
<attr format="integer" name="dview_animationTime"/>
<attr name="dview_paintCap">
<enum name="flat" value="0"/>
<enum name="round" value="1"/>
</attr>
</declare-styleable>
</resources>

然后我们不管三七二十一,先把自定义属性解析出来。


private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
val a = context.obtainStyledAttributes(
attrs,
R.styleable.DoraProgressView,
defStyleAttr,
0
)
when (a.getInt(R.styleable.DoraProgressView_dview_progressType, PROGRESS_TYPE_LINE)) {
0 -> progressType = PROGRESS_TYPE_LINE
1 -> progressType = PROGRESS_TYPE_SEMICIRCLE
2 -> progressType = PROGRESS_TYPE_SEMICIRCLE_REVERSE
3 -> progressType = PROGRESS_TYPE_CIRCLE
4 -> progressType = PROGRESS_TYPE_CIRCLE_REVERSE
}
when (a.getInt(R.styleable.DoraProgressView_dview_progressOrigin, PROGRESS_ORIGIN_LEFT)) {
0 -> progressOrigin = PROGRESS_ORIGIN_LEFT
1 -> progressOrigin = PROGRESS_ORIGIN_TOP
2 -> progressOrigin = PROGRESS_ORIGIN_RIGHT
3 -> progressOrigin = PROGRESS_ORIGIN_BOTTOM
}
when(a.getInt(R.styleable.DoraProgressView_dview_paintCap, 0)) {
0 -> paintCap = Paint.Cap.SQUARE
1 -> paintCap = Paint.Cap.ROUND
}
progressWidth = a.getDimension(R.styleable.DoraProgressView_dview_progressWidth, 30f)
progressBgColor =
a.getColor(R.styleable.DoraProgressView_dview_progressBgColor, Color.GRAY)
progressHoverColor =
a.getColor(R.styleable.DoraProgressView_dview_progressHoverColor, Color.BLUE)
animationTime = a.getInt(R.styleable.DoraProgressView_dview_animationTime, 1000)
a.recycle()
}

解析完自定义属性,切勿忘了释放TypedArray。接下来我们考虑下一步,测量。半圆是不是不要那么大的画板对吧,我们在测量的时候就要充分考虑进去。


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
progressBgPaint.strokeWidth = progressWidth
progressHoverPaint.strokeWidth = progressWidth
if (progressType == PROGRESS_TYPE_LINE) {
// 线
var left = 0f
var top = 0f
var right = measuredWidth.toFloat()
var bottom = measuredHeight.toFloat()
val isHorizontal = when(progressOrigin) {
PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
else -> false
}
if (isHorizontal) {
top = (measuredHeight - progressWidth) / 2
bottom = (measuredHeight + progressWidth) / 2
progressBgRect[left + progressWidth / 2, top, right - progressWidth / 2] = bottom
} else {
left = (measuredWidth - progressWidth) / 2
right = (measuredWidth + progressWidth) / 2
progressBgRect[left, top + progressWidth / 2, right] = bottom - progressWidth / 2
}
} else if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
// 圆
var left = 0f
val top = 0f
var right = measuredWidth
var bottom = measuredHeight
progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
bottom - progressWidth / 2
} else {
// 半圆
val isHorizontal = when(progressOrigin) {
PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
else -> false
}
val min = measuredWidth.coerceAtMost(measuredHeight)
var left = 0f
var top = 0f
var right = 0f
var bottom = 0f
if (isHorizontal) {
if (measuredWidth >= min) {
left = ((measuredWidth - min) / 2).toFloat()
right = left + min
}
if (measuredHeight >= min) {
bottom = top + min
}
progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
bottom - progressWidth / 2
setMeasuredDimension(
MeasureSpec.makeMeasureSpec(
(right - left).toInt(),
MeasureSpec.EXACTLY
),
MeasureSpec.makeMeasureSpec(
(bottom - top + progressWidth).toInt() / 2,
MeasureSpec.EXACTLY
)
)
} else {
if (measuredWidth >= min) {
right = left + min
}
if (measuredHeight >= min) {
top = ((measuredHeight - min) / 2).toFloat()
bottom = top + min
}
progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
bottom - progressWidth / 2
setMeasuredDimension(
MeasureSpec.makeMeasureSpec(
(right - left + progressWidth).toInt() / 2,
MeasureSpec.EXACTLY
),
MeasureSpec.makeMeasureSpec(
(bottom - top).toInt(),
MeasureSpec.EXACTLY
)
)
}
}
}

View的onMeasure()方法是不是默认调用了一个


super.onMeasure(widthMeasureSpec, heightMeasureSpec)

它最终会调用setMeasuredDimension()方法来确定最终测量的结果吧。如果我们对默认的测量不满意,我们可以自己改,最后也调用setMeasuredDimension()方法把测量结果确认。半圆,如果是水平的情况下,我们的宽度就只要一半,相反如果是垂直的半圆,我们高度就只要一半。最后我们画还是照常画,只不过在最后把画到外面的部分移动到画板上显示出来。接下来就是我们最重要的绘图环节了。


override fun onDraw(canvas: Canvas) {
if (progressType == PROGRESS_TYPE_LINE) {
val isHorizontal = when(progressOrigin) {
PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
else -> false
}
if (isHorizontal) {
canvas.drawLine(
progressBgRect.left,
measuredHeight / 2f,
progressBgRect.right,
measuredHeight / 2f,
progressBgPaint)
} else {
canvas.drawLine(measuredWidth / 2f,
progressBgRect.top,
measuredWidth / 2f,
progressBgRect.bottom, progressBgPaint)
}
if (percentRate > 0) {
when (progressOrigin) {
PROGRESS_ORIGIN_LEFT -> {
canvas.drawLine(
progressBgRect.left,
measuredHeight / 2f,
(progressBgRect.right) * percentRate,
measuredHeight / 2f,
progressHoverPaint
)
}
PROGRESS_ORIGIN_TOP -> {
canvas.drawLine(measuredWidth / 2f,
progressBgRect.top,
measuredWidth / 2f,
(progressBgRect.bottom) * percentRate,
progressHoverPaint)
}
PROGRESS_ORIGIN_RIGHT -> {
canvas.drawLine(
progressWidth / 2 + (progressBgRect.right) * (1 - percentRate),
measuredHeight / 2f,
progressBgRect.right,
measuredHeight / 2f,
progressHoverPaint
)
}
PROGRESS_ORIGIN_BOTTOM -> {
canvas.drawLine(measuredWidth / 2f,
progressWidth / 2 + (progressBgRect.bottom) * (1 - percentRate),
measuredWidth / 2f,
progressBgRect.bottom,
progressHoverPaint)
}
}
}
} else if (progressType == PROGRESS_TYPE_SEMICIRCLE) {
if (progressOrigin == PROGRESS_ORIGIN_LEFT) {
// PI ~ 2PI
canvas.drawArc(progressBgRect, 180f, 180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
180f,
angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressOrigin == PROGRESS_ORIGIN_TOP) {
canvas.translate(-progressBgRect.width() / 2, 0f)
// 3/2PI ~ 2PI, 0 ~ PI/2
canvas.drawArc(progressBgRect, 270f, 180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
270f,
angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
canvas.translate(0f, -progressBgRect.height() / 2)
// 2PI ~ PI
canvas.drawArc(progressBgRect, 0f, 180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
0f,
angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
// PI/2 ~ 3/2PI
canvas.drawArc(progressBgRect, 90f, 180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
90f,
angle.toFloat(),
false,
progressHoverPaint
)
}
} else if (progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {
if (progressOrigin == PROGRESS_ORIGIN_LEFT) {
canvas.translate(0f, -progressBgRect.height() / 2)
// PI ~ 2PI
canvas.drawArc(progressBgRect, 180f, -180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
180f,
-angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressOrigin == PROGRESS_ORIGIN_TOP) {
// 3/2PI ~ PI/2
canvas.drawArc(progressBgRect, 270f, -180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
270f,
-angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
// 2PI ~ PI
canvas.drawArc(progressBgRect, 0f, -180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
0f,
-angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
canvas.translate(-progressBgRect.width() / 2, 0f)
// PI/2 ~ 2PI, 2PI ~ 3/2PI
canvas.drawArc(progressBgRect, 90f, -180f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
90f,
-angle.toFloat(),
false,
progressHoverPaint
)
}
} else if (progressType == PROGRESS_TYPE_CIRCLE) {
val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {
90f
} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
180f
} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
270f
} else {
0f
}
canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
180f + deltaAngle,
angle.toFloat(),
false,
progressHoverPaint
)
} else if (progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {
90f
} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
180f
} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
270f
} else {
0f
}
canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)
canvas.drawArc(
progressBgRect,
180f + deltaAngle,
-angle.toFloat(),
false,
progressHoverPaint
)
}
}

绘图除了需要Android的基础绘图知识外,还需要一定的数学计算的功底,比如基本的几何图形的点的计算你要清楚。怎么让绘制的角度变化起来呢?这个问题问的好。这个就牵扯出我们动画的一个关键类,TypeEvaluator,这个接口可以让我们只需要指定边界值,就可以根据动画执行的时长,来动态计算出当前的渐变值。


private inner class AnimationEvaluator : TypeEvaluator<Float> {
override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
return if (endValue > startValue) {
startValue + fraction * (endValue - startValue)
} else {
startValue - fraction * (startValue - endValue)
}
}
}

百分比渐变的固定写法,是不是应该记个笔记,方便以后CP?那么现在我们条件都成熟了,只需要将初始角度的百分比改变一下,我们写一个改变角度百分比的方法。


fun setPercentRate(rate: Float) {
if (animator == null) {
animator = ValueAnimator.ofObject(
AnimationEvaluator(),
percentRate,
rate
)
}
animator?.addUpdateListener { animation: ValueAnimator ->
val value = animation.animatedValue as Float
angle =
if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
(value * 360).toInt()
} else if (progressType == PROGRESS_TYPE_SEMICIRCLE || progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {
(value * 180).toInt()
} else {
0 // 线不需要求角度
}
percentRate = value
invalidate()
}
animator?.interpolator = LinearInterpolator()
animator?.setDuration(animationTime.toLong())?.start()
animator?.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
percentRate = rate
listener?.onComplete()
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
}

这里牵扯到了Animator。有start就一定不要忘了异常中断的情况,我们可以写一个reset的方法来中断动画执行,恢复到初始状态。


fun reset() {
percentRate = 0f
animator?.cancel()
}

如果你不reset,想连续执行动画,则两次调用的时间间隔一定要大于动画时长,否则就应该先取消动画。


涉及到的Android绘图知识点


我们归纳一下完成这个自定义View需要具备的知识点。



  1. 基本图形的绘制,这里主要是扇形

  2. 测量和画板的平移变换

  3. 自定义属性的定义和解析

  4. Animator和动画估值器TypeEvaluator的使用


思路和灵感来自于系统化的基础知识


这个控件其实并不难,主要就是动态配置一些参数,然后在计算上稍微复杂一些,需要一些数学的功底。那么你为什么没有思路呢?你没有思路最可能的原因主要有以下几个可能。



  1. 自定义View的基础绘图API不熟悉

  2. 动画估值器使用不熟悉

  3. 对自定义View的基本流程不熟悉

  4. 看的自定义View的源码不够多

  5. 自定义View基础知识没有系统学习,导致是一些零零碎碎的知识片段

  6. 数学功底不扎实


我觉得往往不是你不会,这些基础知识点你可能都看到过很多次,但是一到自己写就没有思路了。思路和灵感来自于大量源码的阅读和大量的实践。大前提就是你得先把自定义View的这些知识点系统学习一下,先保证都见过,然后才是将它们融会贯通,用的时候信手拈来。


作者:dora
来源:juejin.cn/post/7245223225575882809
收起阅读 »

按Home键时SingleInstance Activity销毁了???

前段时间,突然有朋友询问,自己写的SingleInstance Activity在按home键的时候被销毁了,刚听到这个问题的时候,我直觉怀疑是Activity在onPause或者onStop中发生了Crash导致闪退了,但是安装apk查看现象,没有发现异常日...
继续阅读 »

前段时间,突然有朋友询问,自己写的SingleInstance Activity在按home键的时候被销毁了,刚听到这个问题的时候,我直觉怀疑是Activity在onPause或者onStop中发生了Crash导致闪退了,但是安装apk查看现象,没有发现异常日志,这究竟是怎么回事呢?编写测试Demo来详细探索下


Demo代码说明


Demo日志很简单,包含MainActivity和SingleInstanceActivity两个页面,在MainActivity中的TextView点击事件中启动SingleInstanceActivity,在SingleInstanceActivity中的TextView点击事件中调用moveTaskToBack(true)切回后台,随后在MainActivity界面按Home键返回桌面,就可以看到SingleInstanceActivity被销毁了,示例代码如下所示:


 // MainActivity.kt
 class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContent {
             MyApplicationTheme {
                 // A surface container using the 'background' color from the theme
                 Surface(
                     modifier = Modifier
                        .fillMaxSize()
                        .clickable { onBtnClick() },
                     color = MaterialTheme.colorScheme.background
                ) {
                     Greeting("Android")
                }
            }
        }
    }
     fun onBtnClick() {
          startActivity(Intent(this,                                  SingleInstanceActivity::class.java))
    }
 }
 ​
 @Composable
 fun Greeting(name: String, modifier: Modifier = Modifier) {
     Text(
         text = "Hello $name!",
         modifier = modifier
    )
 }
 ​
 @Preview(showBackground = true)
 @Composable
 fun GreetingPreview() {
     MyApplicationTheme {
         Greeting("Android")
    }
 }

 // SingleInstanceActivity.java
 public class SingleInstanceActivity extends ComponentActivity {
     private static final String TAG = "SingleInstanceActivity";
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     Log.d(TAG,"SingleInstanceActivity onCreate method called",new Exception());
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_single_instance);
     findViewById(R.id.move_back).setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             moveTaskToBack(true);
        }
    });
  }
 ​
   @Override
   protected void onDestroy() {
     Log.d(TAG,"SingleInstanceActivity onDestroy method called",new Exception());
     super.onDestroy();
  }
 }

 <!-- AndroidManifest.xml文件中application节点的内容-->
 <application
     android:allowBackup="true"
     android:dataExtractionRules="@xml/data_extraction_rules"
     android:fullBackupContent="@xml/backup_rules"
     android:icon="@mipmap/ic_launcher"
     android:label="@string/app_name"
     android:roundIcon="@mipmap/ic_launcher_round"
     android:supportsRtl="true"
     android:theme="@style/Theme.MyApplication"
     tools:targetApi="31">
     <activity
         android:name=".SingleInstanceActivity"
         android:launchMode="singleInstance"
         android:exported="true" />
     <activity
         android:name=".MainActivity"
         android:exported="true"
         android:label="@string/app_name"
         android:theme="@style/Theme.MyApplication">
         <intent-filter>
             <action android:name="android.intent.action.MAIN" />
 ​
             <category android:name="android.intent.category.LAUNCHER" />
         </intent-filter>
     </activity>
 </application>

调用栈回溯


即然SingleInstanceActivity被销毁了,那么我们只需要在Activity生命周期中添加日志,来看下onDestroyed函数是怎么驱动调用的即可,从Activity生命周期可知,在Framework中框架通过ClientLifecycleManager类来管理Activity的生命周期变化,在该类的scheduleTransaction函数中,Activity的每一种生命周期类型均被包装成一个ClientTransaction来处理,在该函数中添加日志,打印调用栈,即可确定是那个地方销毁了SingleInstanceActivity,添加日志的代码如下:


24-2-3


随后编译framework.jar并push到设备上,查看日志,可以看到SingleInstanceActivity是在Task类的removeActivities方法中被销毁的,日志如下:


24-2-4


按照如上的思路,逐步类推,添加日志,查看调用栈,我们最终追溯到ActivityThread的handleResumeActivity,在该函数的最后,添加的IdlerHandler里面会执行RecentTasks的onActivityIdle方法,在该函数的调用流程里,会判断当前resume的Activity是不是桌面,是的话在HiddenTask不为空的情况下,就会执行removeUnreachableHiddenTasks的逻辑,销毁SingleInstanceActivity(这里的代码分支为android-13.0.0_r31)。


完整的正向调用流程如下图所示:


SingleInstance Task release process 1


remove-hidden-task机制


前文中我们已经跟踪到Activity销毁的调用流程,那么为什么要销毁SingleInstanceActivity呢?我们继续看前文中的日志,可以看出Activity销毁的原因是:remove-hidden-task。


24-2-6


那么这个remove-hidden-task到底是用来干嘛的呢?我们来看下代码提交信息:


24-2-1


从代码提交说明不难看出,这里的意思是:当我们向最近任务列表中添加一个任务时,会移除已不可达/未激活的Task,这里我们的SingleInstanceActivity所在的Task被判定为不可达/未激活状态,所以被这套机制移除了。


不可达/未激活的Task


那么为什么SingleInstanceActivity被认为是不可达的呢?我们进一步追踪代码,可以看到RencentTasks.removeUnreachableHiddenTasks移除的是mHiddenTasks中的任务,代码如下:


24-2-7


这样我们就只需要搞清楚什么样的Task会被加入mHiddenTasks中即可,mHiddenTasks.add的调用代码如下所示:


24-2-8


24-2-9


从上述代码可知,在removeForAddTask中通过findRemoveIndexForAddTask来查找当给定Task添加到最近任务列表时,需要被移除的Task,在findRemoveIndexForAddTask中最典型的一种场景就是当两个Task的TaskAffinity相同时,当后来的Task被添加到最近任务列表时,前一个Task会被销毁,这也就意味着在SingleInstanceActivity按Home键,MainActivity也会被销毁,经过实践,确实是这样。


解决方案


前文中已探讨了remove-hidden-task的运行机制,那么解决方案也就很简单了,给SingleInstanceActivity添加独立的TaskAffinity即可(注意:此时SingleInstanceActivity会显示在最近任务中,如果不想显示,请指定android:excludeFromRecents="true")。


影响范围


经排查,Google Pixel记性从Android 12开始支持该特性,针对国内定制厂商而言,大多数应该是在Android 13跟进的,大家可以测试看看。


作者:小海编码日记
来源:juejin.cn/post/7259311837463724069
收起阅读 »

Redis stream 用做消息队列完美吗 ?

Redis Stream 是 Redis 5.0 版本中引入的一种新的数据结构,它用于实现简单但功能强大的消息传递模式。 这篇文章,我们聊聊 Redis Stream 基本用法 ,以及如何在 SpringBoot 项目中应用 Redis Stream 。 1...
继续阅读 »

Redis Stream 是 Redis 5.0 版本中引入的一种新的数据结构,它用于实现简单但功能强大的消息传递模式。


这篇文章,我们聊聊 Redis Stream 基本用法 ,以及如何在 SpringBoot 项目中应用 Redis Stream 。



1 基础知识


Redis Stream 的结构如下图所示,它是一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。



每个 Redis Stream 都有唯一的名称 ,对应唯一的 Redis Key 。


同一个 Stream 可以挂载多个消费组 ConsumerGr0up , 消费组不能自动创建,需要使用 XGR0UP CREATE 命令创建


每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动 ,标识当前消费组消费到哪条消息了。


消费组 ConsumerGr0up 同样可以挂载多个消费者 Consumer , 每个 Consumer 并行的读取消息,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。


消费者内部有一个属性 pending_ids , 记录了当前消费者读取但没有回复 ACK 的消息 ID 列表 。


2 核心命令


01 XADD 向 Stream 末尾添加消息


使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列。基础语法格式:


XADD key ID field value [field value ...]


  • key :队列名称,如果不存在就创建

  • ID :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。

  • field value : 记录。


127.0.0.1:6379> XADD mystream * name1 value1 name2 value2
"1712473185388-0"
127.0.0.1:6379> XLEN mystream
(integer) 1
127.0.0.1:6379> XADD mystream * name2 value2 name3 value3
"1712473231761-0"

消息 ID 使用 * 表示由 redis 生成,同时也可以自定义,但是自定义时要保证递增性。



消息 ID 的格式: 毫秒级时间戳 + 序号 , 例如:1712473185388-5 , 它表示当前消息在毫秒时间戳 1712473185388 产生 ,并且该毫秒内产生到了第5条消息。



在添加队列消息时,也可以指定队列的长度


127.0.0.1:6379> XADD mystream MAXLEN 100 * name value1 age 30
"1713082205042-0"

使用 XADD 命令向 mystream 的 stream 中添加了一条消息,并且指定了最大长度为 100。消息的 ID 由 Redis 自动生成,消息包含两个字段 nameage,分别对应的值是 value130


02 XRANGE 获取消息列表


使用 XRANGE 获取消息列表,会自动过滤已经删除的消息。语法格式:


XRANGE key start end [COUNT count]


  • key :队列名

  • start :开始值, - 表示最小值

  • end :结束值, + 表示最大值

  • count :数量


127.0.0.1:6379> XRANGE mystream - + COUNT 2
1) 1) "1712473185388-0"
  2) 1) "name1"
     2) "value1"
     3) "name2"
     4) "value2"
2) 1) "1712473231761-0"
  2) 1) "name2"
     2) "value2"
     3) "name3"
     4) "value3"

我们得到两条消息,第一层是消息 ID ,第二层是消息内容 ,消息内容是 Hash 数据结构 。


03 XREAD 以阻塞/非阻塞方式获取消息列表


使用 XREAD 以阻塞或非阻塞方式获取消息列表 ,语法格式:


XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]


  • count :数量

  • milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式

  • key :队列名

  • id :消息 ID


127.0.0.1:6379> XREAD streams mystream 0-0
1) 1) "mystream"
  2) 1) 1) "1712473185388-0"
        2) 1) "name1"
           2) "value1"
           3) "name2"
           4) "value2"
     2) 1) "1712473231761-0"
        2) 1) "name2"
           2) "value2"
           3) "name3"
           4) "value3"

XRED 读消息时分为阻塞非阻塞模式,使用 BLOCK 选项可以表示阻塞模式,需要设置阻塞时长。非阻塞模式下,读取完毕(即使没有任何消息)立即返回,而在阻塞模式下,若读取不到内容,则阻塞等待。


127.0.0.1:6379> XREAD block 1000 streams mystream $
(nil)
(1.07s)

使用 Block 模式,配合 $ 作为 ID ,表示读取最新的消息,若没有消息,命令阻塞!等待过程中,其他客户端向队列追加消息,则会立即读取到。


因此,典型的队列就是 XADD 配合 XREAD Block 完成。XADD 负责生成消息,XREAD 负责消费消息。


04 XGR0UP CREATE 创建消费者组


使用 XGR0UP CREATE 创建消费者组,分两种情况:



  • 从头开始消费:


XGR0UP CREATE mystream consumer-group-name 0-0  


  • 从尾部开始消费:


XGR0UP CREATE mystream consumer-group-name $

执行效果如下:


127.0.0.1:6379> XGR0UP CREATE mystream mygroup 0-0
OK

05 XREADGR0UP GR0UP 读取消费组中的消息


使用 XREADGR0UP GR0UP 读取消费组中的消息,语法格式:


XREADGR0UP GR0UP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]


  • group :消费组名

  • consumer :消费者名。

  • count : 读取数量。

  • milliseconds : 阻塞毫秒数。

  • key : 队列名。

  • ID : 消息 ID。


示例:


127.0.0.1:6379>  XREADGR0UP group mygroup consumerA count 1 streams mystream >
1) 1) "mystream"
  2) 1) 1) "1712473185388-0"
        2) 1) "name1"
           2) "value1"
           3) "name2"
           4) "value2"

消费者组 mygroup 中的消费者 consumerA ,从 名为 mystream 的 Stream 中读取消息。



  • COUNT 1 表示一次最多读取一条消息

  • > 表示消息的起始位置是当前可用消息的 ID,即从当前未读取的最早消息开始读取。


06 XACK 消息消费确认


接收到消息之后,我们要手动确认一下(ack),语法格式:


xack key group-key ID [ID ...]

示例:


127.0.0.1:6379> XACK mystream mygroup 1713089061658-0
(integer) 1

消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 ack 确认消息已经被消费完成,整个流程的执行如下图所示:



我们可以使用 xpending 命令查看消费者未确认的消息ID


127.0.0.1:6379> xpending mystream mygroup
1) (integer) 1
2) "1713091227595-0"
3) "1713091227595-0"
4) 1) 1) "consumerA"
     2) "1"

07 XTRIM 限制 Stream 长度


我们使用 XTRIM 对流进行修剪,限制长度, 语法格式:


127.0.0.1:6379> XADD mystream * field1 A field2 B field3 C field4 D
"1712535017402-0"
127.0.0.1:6379> XTRIM mystream MAXLEN 2
(integer) 4
127.0.0.1:6379> XRANGE mystream - +
1) 1) "1712498239430-0"
  2) 1) "name"
     2) "zhangyogn"
2) 1) "1712535017402-0"
  2) 1) "field1"
     2) "A"
     3) "field2"
     4) "B"
     5) "field3"
     6) "C"
     7) "field4"
     8) "D"

3 SpringBoot Redis Stream 实战


1、添加 SpringBoot Redis 依赖


<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2、yaml 文件配置



3、RedisTemplate 配置



4、定义stream监听器



5、定义streamcontainer 并启动



6、发送消息



执行完成之后,消费者就可以打印如下日志:



演示代码地址:



github.com/makemyownli…



4 Redis stream 用做消息队列完美吗


笔者认为 Redis stream 用于消息队列最大的进步在于:实现了发布订阅模型


发布订阅模型具有如下特点:



  • 消费独立


    相比队列模型的匿名消费方式,发布订阅模型中消费方都会具备的身份,一般叫做订阅组(订阅关系),不同订阅组之间相互独立不会相互影响。


  • 一对多通信


    基于独立身份的设计,同一个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现一对多通信。



细品 Redis stream 的设计,我们发现它和 Kafka 非常相似,比如说消费者组,消费进度偏移量等。


我们曾经诟病 Redis List 数据结构用做队列时,因为消费时没有 Ack 机制,应用异常挂掉导致消息偶发丢失的情况,Redis Stream 已经完美的解决了。


因为消费者内部有一个属性 pending_ids , 记录了当前消费者读取但没有回复 ACK 的消息 ID 列表 。当消费者重新上线,这些消息可以重新被消费。


但 Redis stream 用做消息队列完美吗 ?


这个真没有!


1、Redis 本身定位是内存数据库,它的设计之初都是为缓存准备的,并不具备消息堆积的能力。而专业消息队列一个非常重要的功能是数据中转枢纽,Redis 的定位很难满足,所以使用起来要非常小心。


2、Redis 的高可用方案可能丢失消息(AOF 持久化 和 主从复制都是异步 ),而专业消息队列可以针对不同的场景选择不同的高可用策略。


所以,笔者认为 Redis 非常适合轻量级消息队列解决方案,轻量级意味着:数据量可控 + 业务模型简单 。




参考文章:



redis.io/docs/data-t…


http://www.runoob.com/redis/redis…


pdai.tech/md/db/nosql…





作者:勇哥Java实战
来源:juejin.cn/post/7357301805569687563
收起阅读 »

争论不休的一个话题:金额到底是用Long还是BigDecimal?

在网上一直流传着一个争论不休的话题:金额到底是用Long还是用BigDecimal?这个话题一出在哪都会引起异常无比激烈的讨论。。。。 比如说这个观点:算钱用BigDecimal是常识 有支持用Long的,将金额的单位设计为分,然后乘以100,使用Long...
继续阅读 »

在网上一直流传着一个争论不休的话题:金额到底是用Long还是用BigDecimal?这个话题一出在哪都会引起异常无比激烈的讨论。。。。 比如说这个观点:算钱用BigDecimal是常识


image.png


image.png


有支持用Long的,将金额的单位设计为分,然后乘以100,使用Long进行存储以及计算,这样不用担心小数点问题。


image.png


并且一些银行系统就会选择用Long


image.png


还有,最最最牛逼的万能大法:用String


image.png


成年人不做选择题,Long跟BigDecimal都用。。。


image.png


image.png


还有一种就是封装一个金额的基类,对金额进行统一处理。


image.png


排除float和double


当然,对于金额,首先我们要排除的就是float和double。它们不适合用于精确的金融计算,因为floatdouble是基于IEEE 754标准的浮点数表示,它们无法精确地表示所有的十进制小数。这会导致在进行财务计算时出现舍入误差,这些误差可能会累积并导致不可预测的结果。



关于带精度的计算,我们不推荐使用float以及double,推荐使用BigDecimal,具体原因请参考:聊一聊BigDecimal使用时的陷阱



选择Long


Long类型在Java中用于存储64位整数。它的主要优点是速度快,因为整数运算在CPU层面是非常高效的。另外,Long类型也占用较少的内存,并且整数类型(BIGINT)在数据库中占用较少的存储空间。


但是Long类型在处理金额时有几个明显的缺点:



  1. 1. 精度问题Long只能存储整数,无法直接表示小数。使用Long来表示以分为单位的金额(例如,100表示1元),此时就会失去小数的精度。即使使用某种方式来表示小数(例如,乘以100或10000),也会遇到舍入误差的问题。并且这种计算方式也会增加计算的复杂度。

  2. 2. 浮点数问题:虽然这不是直接使用Long的问题,但如果你尝试将Long与浮点数(如doublefloat)进行转换以进行计算(比如汇率计算等),还是会遇到浮点数精度问题,这可能导致在财务计算中出现不可接受的误差。


在阿里巴巴的开发手册中建议使用Long。


image.png


但是在一些金融系统当中,对小数位要求比较高的,比如精确到小数点后6位,那么我们使用Long进行存储,每次在计算时都要除以或者乘以1000000,那么计算的开销就很大了。


并且,如果在需求确认时,我们无法知道金额要求的小数位,那我们使用Long也是不行的,我们并不知道需要乘以或者除以多少个0。


选择BigDecimal


BigDecimal是Java提供的一个类,用于任意精度的算术运算。它的主要优点是提供了高精度的计算,这对于金融和货币计算来说是非常重要的。BigDecimal可以表示任意大小的正数、负数或零,并可以精确控制舍入行为。并且在数据库中存储时也有对应的类型进行匹配,比如MySQL的DECIMAL类型提供了精确的数值存储,可以匹配BigDecimal的精度。


但是BigDecimal也有一些缺点:



  1. 1. 性能:与Long相比,BigDecimal的性能较差。因为它的运算需要更多的内存和CPU时间。

  2. 2. 复杂性:使用BigDecimal进行运算比使用Long或基本数据类型更复杂。你需要考虑舍入模式、精度等因素。

  3. 3. 在数据库中需要更多的存储空间来存储小数部分。


而在Mysql的开发手册中,建议金额需要进行小数位计算时,存储要使用Decimal,否则我们要将金额乘以对应小数位的倍数变成BIGINT进行存储。


image.png


总结


基于上述对LongBigDecimal的优缺点分析,我们可以得出以下结论:


在金额计算层面,即代码实现中,推荐使用BigDecimal进行所有与金额相关的计算。BigDecimal提供了高精度的数值运算,能够确保金额计算的精确性,避免了因浮点数精度问题导致的财务误差。使用BigDecimal可以简化代码逻辑,减少因处理精度问题而引入的复杂性。


而在数据库存储方面,我们需要根据具体需求进行权衡。如果业务需求已经明确金额只需精确到分(如某些国家/地区的货币最小单位为分),并且我们确信不会涉及到需要更高精度的小数计算,那么可以使用Long类型进行存储,将金额转换为最小货币单位(如分)进行存储。这样可以节省存储空间并提高查询性能。


但是如果业务需求中金额的小数位数不确定,或者可能涉及多位小数的计算(如国际货币交易等),那么最好使用DECIMALNUMERIC类型进行存储。这些类型提供了精确的数值存储,可以确保数据库中的数据与应用程序中的BigDecimal对象保持一致,避免数据转换过程中可能引入的精度损失。




作者:码农Academy
来源:juejin.cn/post/7358670107902984229
收起阅读 »

一行代码引发的离奇问题,众多大佬纷纷参与点评,最终Typescript之父出手解决

web
故事的起因是这样的, 一个前端开发人员(也算是挺有名的,ariakit.org的作者, wrodPress的前维护者)在社交媒体上发了这么一条帖子。 短短几天就有了51.8万次的view。 简单的文案:又是使用 TypeScript 的一天.。表达了对Typ...
继续阅读 »

故事的起因是这样的, 一个前端开发人员(也算是挺有名的,ariakit.org的作者, wrodPress的前维护者)在社交媒体上发了这么一条帖子。


image-20240317131231595.png


短短几天就有了51.8万次的view。 简单的文案:又是使用 TypeScript 的一天.。表达了对Typescript的又爱又恨😂。在目前的前端市场上,Typescript已经成为标配,ts强大的类型检查机制给我们带来了非常多的好处(代码质量,强大的可维护性,代码即注释),但是其槽点也很多, 很多奇奇怪怪的问题(相信不仅是我一个人这么觉得),繁多的配置项组合,稍不注意就会引起页面爆红,代码量增多和代码组织也会引起一定的负担。但在这些并不能撼动Typescript 在目前前端社区中的地位,在开发项目中一般还是会选择typescript。


反应


话说回到这个帖子上,这个帖子发出来之后迅速引起发酵,被很多大佬转发和引用,下面的评论很多都是wait, what, why happen?类似的语气😂,有很多给出建议,比如换种写法, 重启下Typescript server试试,也有很多开发爱好者希望作者能提供一个例子来复现,他们也想看看是什么问题,看能不能尝试解决这个有趣的例子。


(ps: 在ts中有很多奇怪的东西,特别是在和编辑器配合的时候,有些时候不能判断出来是否是个bug?还是我们代码写的有问题?还是设计如此?还是编辑器的问题?还是版本兼容问题?仅代表个人看法)


复现例子


后来有大佬根据作者提供的信息复现出来了样板例子


declare function hasOwnPropertyextends AnyObject>(
object: T,
prop: keyof any,
): prop is keyof T
;

type EffectCallback = () => void;

declare const useSafeLayoutEffect: (effect: EffectCallback) => void;

type AnyObject = Record<string, any>;
export type State = AnyObject;

type AnyFunction = (...args: any) => any;
type BivariantCallbackextends AnyFunction> = {
bivarianceHack(...args: Parameters): ReturnType;
}["bivarianceHack"];
type SetStateAction = T | BivariantCallback<(prevState: T) => T>;

interface StoreState> {
getState(): S;
setStateextends keyof S>(key: K, value: SetStateAction): void;
}

export function useStoreProps<
S
extends State,
P
extends Partial,
K
extends keyof S,
>(
store: Store, props: P, key: K) {
const value = hasOwnProperty(props, key) ? props[key] : undefined;

useSafeLayoutEffect(() => {
if (value === undefined) return;
value;
// ^?
if (value === undefined) return; // toggle this to see the magic
value;
// ^?
store.setState(key, value);
});
}

将鼠标放到倒数第八行上显示value的类型:


const value: P[K] & ({} | null)

但是将鼠标放到倒数第五行时显示的value类型:


const value: P[K] & {}

真是见了鬼了。同样的操作复制了一遍,显示的类型却不一样?是的,这很Typescript😏。


提出issue


issue的地址在这


image-20240317141601821.png


这个提出issue的哥们就是复现样板例子的人,看的出来他应该是个狂热的技术爱好者,执行力也很强,从问作者要出现这种情况的代码仓库可以是否可以公开 ==> 复现样板例子 ==> 给Typescript提出issue(还尝试了自己能不能解决),执行力power👍。


Typescript之父出手解决


在提出issue之后立即就被官方定位是一个bug, 而且Typescript之父还给出了一个简化版可复现的例子:


function f1extends Record<string, any>, K extends keyof T>(x: T[K] | undefined) {
if (x === undefined) return;
x; // T[K] & ({} | null)
if (x === undefined) return;
x; // T[K] & {}
}

通过上面的例子发现null被意外的消除了。


ahejlsberg(ts之父) 写了一个规范化nullundefined在类型系统中的表现的函数解决了这个问题。


image-20240317152740323.png


至此issue被关闭。


我们打开palyground的nightly版本,可以发现这个问题被解决, 错误不在显示了。


总结


这是无意间从网上看到,然后从问题追溯到问题被一步步的解决。从帖子中可以看出来现在大部分用Typescript写项目的人又爱又恨的普遍状态。不管你是多菜的菜鸟也能感受到ts给日益庞大的前端项目带来的好处,不管你是多厉害的大牛也是会遇到一些奇怪的错误。随着Typescript的普及,社区中有很多不同的声音,有热爱者,有反对者,也有随波逐流者,但这也代表Typescript在社区中展现的旺盛生命力。质疑也好,热爱也罢,我觉得ts会越来越好。


作者:xinling_any
来源:juejin.cn/post/7347210988260147210
收起阅读 »

一行代码搞定禁用web开发者工具

web
在如今的互联网时代,网页源码的保护显得尤为重要,特别是前端代码,几乎就是明文展示,很容易造成源码泄露,黑客和恶意用户往往会利用浏览器的开发者工具来窃取网站的敏感信息。为了有效防止用户打开浏览器的Web开发者工具面板,今天推荐一个不错的npm库,可以帮助开发者更...
继续阅读 »

在如今的互联网时代,网页源码的保护显得尤为重要,特别是前端代码,几乎就是明文展示,很容易造成源码泄露,黑客和恶意用户往往会利用浏览器的开发者工具来窃取网站的敏感信息。为了有效防止用户打开浏览器的Web开发者工具面板,今天推荐一个不错的npm库,可以帮助开发者更好地保护自己的网站源码,本文将介绍该库的功能和使用方法。


功能介绍


npm库名称:disable-devtool,github地址:github.com/theajack/disable-devtool。从f12按钮,右键单击和浏览器菜单都可以禁用Web开发工具。



🚀 一行代码搞定禁用web开发者工具



该库有以下特性:



  • 支持可配置是否禁用右键菜单

  • 禁用 f12 和 ctrl+shift+i 等快捷键

  • 支持识别从浏览器菜单栏打开开发者工具并关闭当前页面

  • 开发者可以绕过禁用 (url参数使用tk配合md5加密)

  • 多种监测模式,支持几乎所有浏览器(IE,360,qq浏览器,FireFox,Chrome,Edge...)

  • 高度可配置、使用极简、体积小巧

  • 支持npm引用和script标签引用(属性配置)

  • 识别真移动端与浏览器开发者工具设置插件伪造的移动端,为移动端节省性能

  • 支持识别开发者工具关闭事件

  • 支持可配置是否禁用选择、复制、剪切、粘贴功能

  • 支持识别 eruda 和 vconsole 调试工具

  • 支持挂起和恢复探测器工作

  • 支持配置ignore属性,用以自定义控制是否启用探测器

  • 支持配置iframe中所有父页面的开发者工具禁用


使用方法


使用该库非常简单,只需按照以下步骤进行操作:


1.1 npm 引用


推荐使用这种方式安装使用,使用script脚本可以被代理单独拦截掉从而无法执行。


npm i disable-devtool

import DisableDevtool from 'disable-devtool';

DisableDevtool(options);

1.2 script方式使用


<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool'>script>

或者通过版本引用:



<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool@x.x.x'>script>

<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool@latest'>script>

1.3 npm 方式 options参数说明


options中的参数与说明如下,各方面的配置相当完善。


interface IConfig {
md5?: string; // 绕过禁用的md5值,默认不启用绕过禁用
url?: string; // 关闭页面失败时的跳转页面,默认值为localhost
tkName?: string; // 绕过禁用时的url参数名称,默认为 ddtk
ondevtoolopen?(type: DetectorType, next: Function): void; // 开发者面板打开的回调,启用时url参数无效,type 为监测模式, next函数是关闭当前窗口
ondevtoolclose?(): void; // 开发者面板关闭的回调
interval?: number; // 定时器的时间间隔 默认200ms
disableMenu?: boolean; // 是否禁用右键菜单 默认为true
stopIntervalTime?: number; // 在移动端时取消监视的等待时长
clearIntervalWhenDevOpenTrigger?: boolean; // 是否在触发之后停止监控 默认为false, 在使用ondevtoolclose时该参数无效
detectors?: Array<DetectorType>; // 启用的检测器 检测器详情
clearLog?: boolean; // 是否每次都清除log
disableSelect?: boolean; // 是否禁用选择文本 默认为false
disableCopy?: boolean; // 是否禁用复制 默认为false
disableCut?: boolean; // 是否禁用剪切 默认为false
disablePaste: boolean; // 是否禁用粘贴 默认为false
ignore?: (string|RegExp)[] | null | (()=>boolean); // 某些情况忽略禁用
disableIframeParents?:
boolean; // iframe中是否禁用所有父窗口
timeOutUrl?:
// 关闭页面超时跳转的url;
}

enum DetectorType {
Unknown = -1,
RegToString = 0, // 根据正则检测
DefineId, // 根据dom id检测
Size, // 根据窗口尺寸检测
DateToString, // 根据Date.toString 检测
FuncToString, // 根据Function.toString 检测
Debugger, // 根据断点检测,仅在ios chrome 真机情况下有效
Performance, // 根据log大数据性能检测
DebugLib, // 检测第三方调试工具 erudavconsole
};

1.4 script 方式使用属性配置


<script 
disable-devtool-auto
src='https://cdn.jsdelivr.net/npm/disable-devtool'
md5='xxx'
url='xxx'
tk-name='xxx'
interval='xxx'
disable-menu='xxx'
detectors='xxx'
clear-log='true'
disable-select='true'
disable-copy='true'
disable-cut='true'
disable-paste='true'
>
script>

1.5 事件监听


ondevtoolopen 事件的回调参数就是被触发的监测模式。可以在 ondevtoolopen 里执行业务逻辑,比如做数据上报、用户行为分析等。


DisableDevtool({
ondevtoolopen(type, next){
alert('Devtool opened with type:' + type);
next();
}
});

1.6 md5 与 tk 绕过禁用


该库中使用 key 与 md5 配合的方式使得开发者可以在线上绕过禁用。


流程如下:


先指定一个 key a(该值不要记录在代码中),使用 md5 加密得到一个值 b,将b作为 md5 参数传入,开发者在访问 url 的时候只需要带上url参数 ddtk=a,便可以绕过禁用。


disableDevtool对象暴露了 md5 方法,可供开发者加密时使用:


DisableDevtool.md5('xxx');

更多细节可查阅官方文档,中文文档地址:https://github.com/theajack/disable-devtool/blob/master/README.cn.md


最后


尽管该库可以有效地禁用浏览器的开发者工具面板,但仍然需要注意以下几点:



  • 该库只能禁用开发者工具的面板,无法阻止用户通过其他途径访问网页源码。因此,建议结合其他安全措施来保护网站。

  • 禁用开发者工具可能会对网站的调试和维护造成一定的困扰。需要调试线上代码的时候可以使用上述1.6绕过禁用进行调试。

  • 该库仅适用于现代浏览器,对于一些较旧的浏览器可能存在兼容性问题。在使用前请确保测试过兼容性。


为了进一步加强网页源码的安全性,我们可以采取以下额外措施:



  • 加密敏感代码,使用加密算法对关键代码进行加密,以防止非授权访问和修改。

  • 使用服务器端渲染,将网页的渲染过程放在服务器端,只返回最终渲染结果给客户端,隐藏源代码和逻辑。

  • 定期更新代码,定期更新代码库以充分利用新的安全特性和修复已知漏洞。


保护网页源码的安全性对于Web开发至关重要。通过使用npm库disable-devtool,并结合其他安全措施,我们可以有效地降低用户访问和修改源代码的风险。但是绝对的安全是不存在的,因此定期更新和加强安全性措施也是必要的。




作者:南城FE
来源:juejin.cn/post/7296089060833148943
收起阅读 »

一款好用到爆的可视化拖拽库

web
嗨,大家好,我是徐小夕,之前一直在研究可视化零代码相关的技术实践,也做了很多可视化搭建的产品,比如: H5-Dooring(页面可视化搭建平台) V6.Dooring(数据大屏可视化平台) formManager(表单搭建引擎) Next-Admin(基于n...
继续阅读 »

嗨,大家好,我是徐小夕,之前一直在研究可视化零代码相关的技术实践,也做了很多可视化搭建的产品,比如:



  • H5-Dooring(页面可视化搭建平台)

  • V6.Dooring(数据大屏可视化平台)

  • formManager(表单搭建引擎)

  • Next-Admin(基于nextjs和antd5.0的中后台管理系统)


最近在研发智能搭建系统(WEP)的时候发现一款非常好用的可视化拖拽插件——draggable。它在 github 上有17.4k star,提供了很多非常精美的拖拽案例, 我们使用它可以轻松实现可视化拖拽,组件排序,网格拖拽等效果,而且浏览器兼容性也非常不错,原生 javascript 开发, 可以轻松集成到 reactvue 等主流框架中。


接下来我就和大家一起介绍一下这款开源插件。



安装与使用


我们可以使用如下方式安装:


# yarn add shopify/draggable
pnpm add shopify/draggable

在项目里使用:


import {
Draggable,
Sortable,
Droppable,
Swappable,
} from 'shopify/draggable'

github地址: https://github.com/Shopify/draggable


接下来我就来和大家分享几个非常有价值的使用案例。


1. 3D效果拖拽



代码实现:


// eslint-disable-next-line import/no-unresolved
import {Draggable} from '@shopify/draggable';

// eslint-disable-next-line shopify/strict-component-boundaries
import Plate from '../../components/Plate';

export default function Home() {
const containerSelector = '#Home .PlateWrapper';
const container = document.querySelector(containerSelector);

if (!container) {
return false;
}

const draggable = new Draggable(container, {
draggable: '.Plate',
});
const plates = new Plate(container);

// --- Draggable events --- //
draggable.on('drag:start', (evt) => {
plates.setThreshold();
plates.setInitialMousePosition(evt.sensorEvent);
});

draggable.on('drag:move', (evt) => {
// rAF seems to cause the animation to get stuck?
// requestAnimationFrame(() => {});
plates.dragWarp(evt.source, evt.sensorEvent);
});

draggable.on('drag:stop', () => {
plates.resetWarp();
});

return draggable;
}

2. 可拖拽的开关效果


2.gif


代码如下:


// eslint-disable-next-line import/no-unresolved
import {Draggable} from '@shopify/draggable';

function translateMirror(mirror, mirrorCoords, containerRect) {
if (mirrorCoords.top < containerRect.top || mirrorCoords.left < containerRect.left) {
return;
}

requestAnimationFrame(() => {
mirror.style.transform = `translate3d(${mirrorCoords.left}px, ${mirrorCoords.top}px, 0)`;
});
}

function calcOffset(offset) {
return offset * 2 * 0.5;
}

export default function DragEvents() {
const toggleClass = 'PillSwitch--isOn';
const containers = document.querySelectorAll('#DragEvents .PillSwitch');

if (containers.length === 0) {
return false;
}

const draggable = new Draggable(containers, {
draggable: '.PillSwitchControl',
delay: 0,
});

let isToggled = false;
let initialMousePosition;
let containerRect;
let dragRect;
let dragThreshold;
let headings;
let headingText;

// --- Draggable events --- //
draggable.on('drag:start', (evt) => {
initialMousePosition = {
x: evt.sensorEvent.clientX,
y: evt.sensorEvent.clientY,
};
});

draggable.on('mirror:created', (evt) => {
containerRect = evt.sourceContainer.getBoundingClientRect();
dragRect = evt.source.getBoundingClientRect();

const containerRectQuarter = containerRect.width / 4;
dragThreshold = isToggled ? containerRectQuarter * -1 : containerRectQuarter;
headings = {
source: evt.originalSource.querySelector('[data-switch-on]'),
mirror: evt.mirror.querySelector('[data-switch-on]'),
};
headingText = {
on: headings.source.dataset.switchOn,
off: headings.source.dataset.switchOff,
};
});

draggable.on('mirror:move', (evt) => {
evt.cancel();
const offsetX = calcOffset(evt.sensorEvent.clientX - initialMousePosition.x);
const offsetY = calcOffset(initialMousePosition.y - evt.sensorEvent.clientY);
const offsetValue = offsetX > offsetY ? offsetX : offsetY;
const mirrorCoords = {
top: dragRect.top - offsetValue,
left: dragRect.left + offsetValue,
};

translateMirror(evt.mirror, mirrorCoords, containerRect);

if (isToggled && offsetValue < dragThreshold) {
evt.sourceContainer.classList.remove(toggleClass);
headings.source.textContent = headingText.off;
headings.mirror.textContent = headingText.off;
isToggled = false;
} else if (!isToggled && offsetValue > dragThreshold) {
evt.sourceContainer.classList.add(toggleClass);
headings.source.textContent = headingText.on;
headings.mirror.textContent = headingText.on;
isToggled = true;
}
});

const triggerMouseUpOnESC = (evt) => {
if (evt.key === 'Escape') {
draggable.cancel();
}
};

draggable.on('drag:start', () => {
document.addEventListener('keyup', triggerMouseUpOnESC);
});

return draggable;
}

3.可拖拽的网格元素


3.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Droppable/UniqueDropzone


4. 可拖拽的列表


4.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Sortable/SimpleList


5. 卡牌拖拽效果


5.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Sortable/Transformed


6. 多容器拖拽效果


6.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Sortable/MultipleContainers


7. 不规则网格拖拽


7.gif


源码地址:https://github.com/Shopify/draggable/tree/master/examples/src/content/Swappable/Floated


8. 拖拽排序动画


8.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Plugins/SortAnimation


当然还有很多有意思的拖拽案例, 大家也可以去体验一下。


今天就分享到这啦,祝大家节日快乐, 博学!


如果有收获,记得点赞 + 再看哦, 欢迎在评论区评论, 分享你的收藏干货~




作者:徐小夕
来源:juejin.cn/post/7353877562303021093
收起阅读 »

这两年,我把28年以来欠的亏都吃完了...

前言 很长一段时间没有总结一下过去几个月的状态了,今天思绪万千,脑海中浮现了很多经历和生活片段,我把它记录下来。顺便今天聊一聊认知突破,分享我在买房这段时间吃过的亏,也希望作为你的前车之鉴。 买房 21年底的时候,那时刚好毕业三年,也正是互联网公司996最流行...
继续阅读 »

前言


很长一段时间没有总结一下过去几个月的状态了,今天思绪万千,脑海中浮现了很多经历和生活片段,我把它记录下来。顺便今天聊一聊认知突破,分享我在买房这段时间吃过的亏,也希望作为你的前车之鉴。


买房


21年底的时候,那时刚好毕业三年,也正是互联网公司996最流行的阶段,由于平时我不怎么花钱,也很少买衣服,上网买东西是个矛盾体,需要花很多时间对比,经常看了一件东西很久,最后又不买。加上比较高强度的工作状态,两点一线,可以说是没时间花钱,再加上自己把钱都拿去理财了,也赚了几万块,最后一共攒了几十万下来。我从小就立志要走出农村,而且认为以后有女朋友结婚也要房子,加上当时花比较多时间在理财上面,那时候其实行情已经不好了,工作上没什么突破,比较迷茫,于是想着干脆就把钱花出去了,自己也就有动力去搞各种路子尝试赚钱。在没有经过任何对比之后就在佛山买了一套房子,房价正是高峰的时候,于是我成功站岗!因为这个契机,躲过了持续了2年多的低迷股市,却没躲过低迷的房地产。


while(true) { 坑++ }


我买的是期房,当时不知道期房会有这么多坑,比如期间不确定开发商会不会破产,我这个开发商(龙光)就差点破产了,房产证无着落,相当于花了200w买了一个无证的房子,这辈子就算是搭进去了。


对于整个购房过程也是很懵逼,对流程完全不熟悉,当时去翻了政府规划文件,看那个地段后续有没有涨价空间,然后跟着亲戚介绍的销售转圈圈,当时说给我免3年物业费,合计也有几万块。在签合同之前销售都有说可以给到,但由于第一次没有录音,导致在签合同的时候销售反口,不承认,我们也没有证据,最后吃了哑巴亏。


开始的时候谈好了一个价格167w,然后销售私下打电话给我洗脑说我给点辛苦费1.5w,他可以向领导申请多几万块优惠。我知道这是他们的销售套路,但是架不住给我优惠5w啊,中间反复拉扯最后说给他8k,采用线下现金交易的方式。这一次我有录音了,因为私底下交易没有任何痕迹,也不合法,所以留了一手,也成为我后面维权时争取话语权的基础。


中介佣金是很乐观的,当时由于我亲戚推荐我去,销售承诺税前有4w,当时看中这个返佣也促使我火急火燎的交了定金。现在3年过去了,这个佣金依旧没有到账,我一度怀疑是中介搞ABC套路把我这个钱💰吃了,其他邻居的推荐佣金都到了账,加上现在地产商没钱了,同时跟那个亲戚有些过节,这个返佣更是遥遥无期。最后通过上面的录音获得了一丝话语权,知道了这个钱还在开发商手上,一直没有拨款下来到中介公司。下面是部分聊天记录:


image.png


不接受微信语音沟通,文字可以留给自己思考的时间,同时也更好收集证据。


image.png


然后去找相关人员把信息拉出来给我看,显示开发商未付款状态,这个状态维持2年了,目前看来只能再等下去。


image.png


签合同的时候,有个律师所说是协助我们签合同、备案、办房产证等各种边缘工作,糊里糊涂交了700元律师费,不交不行,甚至律师所连发票都没有给,而我都没有意识到这个最基本的法律法规问题。现在交房了可以办理房产证了,拿证下来也就80块登记费,居然收我700,其他业主有些是600多,400多,顿时觉得智商受到了侮辱,看了网上铁头各种打假的视频,我觉得自己也应该勇敢发声。现在也在收集商家各种违规证据,提交给相关部门解决。


image.png


image.png


image.png


后面市场监督管理局收到投诉,应该是有协商,意识到没有给我们发票,过来几天之后才把发票补过来,开票日期不是付款时候的2022年,而是2024年,明显属于偷税了。目前跟他要发票的应该只有我,估算2300多户业主都没有开发票的。


当时我首付需要50w,自己手上不够,我爸干建筑一辈子,辛苦供我们两个孩子上了大学,山上建了两层楼,手里没钱。我妈是一辈子没打过工,消极派,说出来没几句好话,家里不和睦的始作俑者,更不可能有钱支持。所以我还有20w是首付贷,也就是跟开发商借的,利率10%,这个利息很高了。销售当时说可以优惠到5%,但是优惠金额是补贴到总房价里面去,其实这也是他们的一种销售套路,这亏我也吃了,2年之后我连本带息还24w。当时认为自己应该一年左右能还完,但是实际远远高估自己的能力,买完房子接着我爸又生病在医院待了几个月,前后花了十几万,人生一下子跌入了谷底。


从头再来


后面2023一年,我出去创业,模式很新,很多人不赞同,期间遇到了不少小人诋毁我们两夫妻,当时我老婆还在怀孕,但我们最后都熬过来了,还生了一个儿子,6斤多。期间一年赚了十几万,但是开支也大,加上父母要养,我爸还要吃药,房子要供,最后还是选择了先稳定下来,我重新回到了职场,空窗一年后在这个环境下拿了一个还不错的offer,同时也想自己沉淀一下。


自从有了宝宝之后,生活似乎都往好的方面发展,出版社找我出书,为了契合自己的职业发展,我选择了写书《NestJS全栈开发秘籍》,从2023年11月份开始,迄今快半年了,在收尾阶段,希望尽快与各位读者们见面。同时,等了3年的房子也收房了,由于是高层,质量相对其他邻居好,没有出现成片天花掉下来或者漏水的情况。我们经常都说他是天使宝宝,是来报恩的。


由于我们公司技术部门是属于后勤支持性质的,技术变化不大,Vue2+微前端和React管理系统那一套,没有太多的新技术扩展,意味着不确定也大。业务发展不好考虑的是减少这些部门的开支,所以不出意外最近也迎来了降薪。这不是最可怕的,对于我们技术人来讲,最可怕的是我认为在业务中成长停滞了,或者没有业务来锻炼技术,所以在业余时间也选择了参与一些开源项目,如hello-alog开源算法书的代码贡献,并且这也是选择写书的原因。很简单地说,当下一个面试官问到我的时候,我不可能什么都讲不出来,最经典的问题就是:在这个公司期间你做过最有成就感的事情是什么?现在,我有了答案!


哲学


我的人生哲学是不断改变,拥抱不确定性!这么看来,我的确在这些年上了不少当,吃了不少亏,把自己搞的很累,甚至连累到家里人。但,用我老婆经常说的一句话:人生这么长,总是要经历点什么,再说现在也没有很差。的确,不断将自己处于变化之中,当不确定性降临到普罗大众时,我们唯一的优势,就是更加从容


总结


人们还在行走,我们的故事还在继续~


WechatIMG154.jpg


作者:寻找奶酪的mouse
来源:juejin.cn/post/7349136892333981711
收起阅读 »

普通Android应用的系统签名

一、前言 对于常见的Android开发来说,普通级别的app已经满足不了需求。对系统的要求能力越来越定制化,所以针对系统权限的需求也迫在眉睫。 那怎么通过aosp的系统签名,将普通app升级为系统权限的app,使app能访问系统资源的权限呢? 二、流程 1. ...
继续阅读 »

一、前言


对于常见的Android开发来说,普通级别的app已经满足不了需求。对系统的要求能力越来越定制化,所以针对系统权限的需求也迫在眉睫。
那怎么通过aosp的系统签名,将普通app升级为系统权限的app,使app能访问系统资源的权限呢?


二、流程


1. 手动签名apk文件


a. app设置系统权限


在app项目的AndroidManifest文件的节点新增


<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:sharedUserId="android.uid.system">


b. 编译打包apk


生成对应apk


c. 准备签名文件及工具


需要准备:



  • java环境命令

  • 系统签名文件:platform.pk8、platform.x509.pem




  • signapk.jar:



    • 进入/build/tools/signapk/文件夹

    • 执行命令: mm

    • 在out/host/linux-x86/framework/目录找到signapk.jar




d. 签名打包好的apk



tips: 最好将工具等文件复制到统一文件中,比较好操作,中途会遇到各式各样的问题,操作篇尾



java -jar signapk.jar platform.x509.pem platform.pk8 app-debug.apk new.apk 

e. 安装新包测试


卸载旧包,安装新包,即可完成系统权限


2. 自动签名apk文件


每次开发时,总是要手动签名新打出的安装包,很不方便,直接在打包时完成系统签名更高效


a. pk8 私钥解密pem格式


此时会生成platform.priv.pem文件



  • [platform.priv.pem]为生成文件名称


openssl pkcs8 -in platform.pk8 -inform DER --outform PEM -out platform.priv.pem -nocrypt

b. 私钥通过公钥pem加密pk12


此时会生成platform.pk12文件



  • [platform.priv.pem]为上一步生成的文件

  • [zxxkey]为AliasName


openssl pkcs12 -export -in platform.x509.pem -inkey platform.priv.pem -out platform.pk12 -name zxxkey

需要输入两次密码:(实测store和key密码需要一致)


c. 通过java的keytool 工具生成 keystore



  • [12345678]为store密码

  • [zxxkey]为上一步设置的别名,需要与上面保持一致


jks:


keytool -importkeystore -destkeystore platform.jks -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass 12345678 -alias zxxkey

keystore:


keytool -importkeystore -destkeystore platform.keystore -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass 12345678 -alias zxxkey

d. 项目中使用签名


1)引入签名文件:


将keystore或者jks文件引入项目


2)创建keystore.properties:


keyAlias=zxxkey
keyPassword=12345678
storeFile=../key/platform.jks
storePassword=12345678

3)在app/build.gradle.kts引入signConfig:


import java.io.FileInputStream
import java.util.Properties

...

val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
keystoreProperties.load(FileInputStream(keystorePropertiesFile))

...

android {

...

signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}

buildTypes {
debug {
signingConfig = signingConfigs.getByName("release")
}
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

...
}


三、问题


1. java版本问题


Q:版本异常?


Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/android/signapk/SignApk has been compiled by a more recent version of the Java Runtime (class file version 53.0), this version of the Java Runtime only recognizes class file versions up to 52.0

A:解决方案:


升级jdk版本,52.0版本为java8,选用更高版本即可。


/home/zengxiangxi/Developer/JDK/jdk-9.0.4/bin/java


/home/zengxiangxi/Developer/JDK/jdk-9.0.4/bin/java -jar signapk.jar platform.x509.pem platform.pk8 app-debug.apk new.apk 

2. 签名问题报错


Q:找不到依赖库?


Exception in thread "main" java.lang.UnsatisfiedLinkError: no conscrypt_openjdk_jni-linux-x86_64 in java.library.path
at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2541)
at java.base/java.lang.Runtime.loadLibrary0(Runtime.java:873)
at java.base/java.lang.System.loadLibrary(System.java:1857)
at org.conscrypt.NativeLibraryUtil.loadLibrary(NativeLibraryUtil.java:54)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)


A:附带以来库路径。


-Djava.library.path="/home/zengxiangxi/Project/aosp/out/soong/host/linux-x86/lib64"


stackoverflow.com/questions/4…


~/Developer/JDK/jdk-9.0.4/bin/java -Djava.library.path="/home/zengxiangxi/Project/aosp/out/soong/host/linux-x86/lib64" -jar signapk.jar platform.x509.pem platform.pk8 app-debug.apk new.apk

四、文档链接



作者:会飞de小牛人
来源:juejin.cn/post/7299991094627500072
收起阅读 »

突破自定义View性能瓶颈

在Android应用程序中,自定义View是一个非常常见的需求。自定义View可以帮助您创建独特的UI元素,以满足您的应用程序的特定需求。然而,自定义View也可能会导致性能问题,特别是在您的应用程序需要处理大量自定义View的情况下。 在本篇文章中,我们将探...
继续阅读 »

在Android应用程序中,自定义View是一个非常常见的需求。自定义View可以帮助您创建独特的UI元素,以满足您的应用程序的特定需求。然而,自定义View也可能会导致性能问题,特别是在您的应用程序需要处理大量自定义View的情况下。


在本篇文章中,我们将探讨一些Android自定义View性能优化的技巧,以确保您的应用程序在处理自定义View时保持高效和稳定。我们将从以下几个方面进行讨论:


1. 使用正确的布局


在创建自定义View时,正确的布局是至关重要的。使用正确的布局可以帮助您最大限度地减少布局层次结构,从而提高您的应用程序的性能。


例如,如果您需要创建一个具有多个子视图的自定义View,使用ConstraintLayout代替RelativeLayout和LinearLayout可以简化布局并减少嵌套。


下面是一个示例代码:


<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">




androidx.constraintlayout.widget.ConstraintLayout>

另一个重要的布局技巧是使用ViewStub。ViewStub是一个轻量级的视图,它可以用作占位符,直到需要真正的视图时才充气。这可以大大减少布局层次结构并提高性能。


2. 缓存视图


缓存视图是另一个重要的性能优化技巧。当您使用自定义View时,通常需要创建多个实例。如果您没有正确地缓存这些实例,那么您的应用程序可能会变得非常慢。


为了缓存视图,您可以使用Android的ViewHolder模式或使用自定义缓存对象。ViewHolder模式是Android开发者广泛使用的一种技术,可以在列表或网格视图中提高性能。使用自定义缓存对象可以更好地控制视图的生命周期,并减少视图的创建和销毁。


以下是ViewHolder模式的示例代码:


class CustomView(context: Context) : View(context) {
private class ViewHolder {
// 缓存视图
var textView: TextView? = null
var imageView: ImageView? = null
// 添加其他视图组件
}

private var viewHolder: ViewHolder? = null

init {
// 初始化ViewHolder
viewHolder = ViewHolder()
// 查找视图并关联到ViewHolder
viewHolder?.textView = findViewById(R.id.text_view)
viewHolder?.imageView = findViewById(R.id.image_view)
// 添加其他视图组件的查找和关联
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制视图
}
}

3. 避免过多的绘制操作


绘制操作是自定义View中最重要的性能问题之一。如果您的自定义View需要大量的绘制操作,那么您的应用程序可能会变得非常慢。


为了避免过多的绘制操作,您可以使用View的setWillNotDraw方法来禁用不必要的绘制。您还可以使用Canvas的clipRect方法来限制绘制操作的区域。此外,您还可以使用硬件加速来加速绘制操作。


以下是一个示例代码:


class CustomView(context: Context) : View(context) {
init {
setWillNotDraw(true) // 禁用绘制
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制操作
canvas.clipRect(0, 0, width, height) // 限制绘制区域
// 添加其他绘制操作
}
}

4. 使用异步任务


如果您的自定义View需要执行耗时的操作,例如从网络加载图像或处理大量数据,那么您应该使用异步任务来执行这些操作。这可以确保您的应用程序在执行这些操作时保持响应,并且不会阻塞用户界面。


以下是一个使用异步任务加载图像的示例代码:


class CustomView(context: Context) : View(context) {
private var image: Bitmap? = null

fun loadImageAsync(imageUrl: String) {
val asyncTask = object : AsyncTask<Void, Void, Bitmap>() {
override fun doInBackground(vararg params: Void?): Bitmap {
// 执行耗时操作,如从网络加载图像
return loadImageFromUrl(imageUrl)
}

override fun onPostExecute(result: Bitmap) {
super.onPostExecute(result)
// 在主线程更新UI
image = result
invalidate() // 刷新视图
}
}

asyncTask.execute()
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制图像
image?.let {
canvas.drawBitmap(it, 0f, 0f, null)
}
// 添加其他绘制操作
}
}

5. 使用适当的数据结构


在自定义View中,使用适当的数据结构可以大大提高性能。例如,如果您需要绘制大量的点或线,那么使用FloatBuffer或ByteBuffer可以提高性能。如果您需要处理大量的图像数据,那么使用BitmapFactory.Options可以减少内存使用量。


以下是一个使用FloatBuffer绘制点的示例代码:


class CustomView(context: Context) : View(context) {
private var pointBuffer: FloatBuffer? = null

init {
// 初始化点的数据
val points = floatArrayOf(0f, 0f, 100f, 100f, 200f, 200f)
pointBuffer = ByteBuffer.allocateDirect(points.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
pointBuffer?.put(points)
pointBuffer?.position(0)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制点
pointBuffer?.let {
canvas.drawPoints(it, paint)
}
// 添加其他绘制操作
}
}

结论


在本篇文章中,我们探讨了一些Android自定义View性能优化的技巧。通过使用正确的布局,缓存视图,避免过多的绘制操作,使用异步任务和适当的数据结构,您可以确保您的应用程序在处理自定义View时保持高效和稳定。


请记住,优化自定义View的性能是一个持续的过程。您应该经常检查您的应用程序,并使用最新的技术和最佳实践来提高性能。




作者:午后一小憩
来源:juejin.cn/post/7238491755448369211
收起阅读 »

关于页面适配的一些方案

web
早期的页面使用了左右布局。左侧宽度固定,右侧宽度自适应。未使用vm、em、百分比等进行屏幕适配。所有的尺寸(宽度、高度、边框宽度、字体大小等)全部使用的px进行开发。导致只有常用的显示屏尺寸显示较为正常,但是小屏幕显示不正常。 媒体查询屏幕适配 正常显示屏的...
继续阅读 »

早期的页面使用了左右布局。左侧宽度固定,右侧宽度自适应。未使用vm、em、百分比等进行屏幕适配。所有的尺寸(宽度、高度、边框宽度、字体大小等)全部使用的px进行开发。导致只有常用的显示屏尺寸显示较为正常,但是小屏幕显示不正常。



媒体查询屏幕适配


正常显示屏的分辨率是1920 * 1080【假如缩放比例为100%】。在此尺寸下显示正常的布局和展示,如果修改分辨率为1360 * 768。则正常显示的字体等有一种放大的效果。


image.png
如果想要同1920的显示屏同样的显示效果,则需要在index.html中设置:



@media(max-width: 1440px) {
html {
zoom: 90%;
}
}

image.png


但是有一个弊端,字体会变模糊。


根据dpr适配


很多小屏幕推荐的缩放比例是150%。


此时根据dpr进行适配


    @media (-webkit-min-device-pixel-ratio: 1.5) {
html {
zoom: 0.67
}
}

注意,在此设置下,如果系统中有根据pageX, pageY进行定位时,需要额外处理。


        if (window.devicePixelRatio == 1.5) {
x = x/0.67;
y = y/0.67;
}

作者:一涯
来源:juejin.cn/post/7306749023473451045
收起阅读 »