注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端小白的几个坏习惯

web
最近在教授前端小白学员编写一些简单的网页。在这个过程中发现了小白们比较容易遇到的一些问题或者坏习惯,在这里对它们进行一一解释。 文件名命名 有些学员的文件命名是这样的: 除了网页的内容外,所有的东西都应该用英文,而不是拼音。 原因有如下几点: 编程不是一个...
继续阅读 »

最近在教授前端小白学员编写一些简单的网页。在这个过程中发现了小白们比较容易遇到的一些问题或者坏习惯,在这里对它们进行一一解释。


文件名命名


有些学员的文件命名是这样的:



除了网页的内容外,所有的东西都应该用英文,而不是拼音。


原因有如下几点:



  1. 编程不是一个人的活动,是群体活动。我们使用的编程语言、框架和库,几乎全部都是英文。使用中文,你的协作者会难以理解你的代码。而且中英混搭会让代码阅读困难。

  2. 使用拼音和使用汉字基本上没有什么区别,甚至还不如汉字直观。

  3. 拼音很难加音标,而且即使能加音标,也很难表达真正的意思,因为同音词太多,它存在多义性,比如 heshui,你不知道它到底是在表达喝水还是河水。

  4. 使用拼音会让你显得非常不专业。

  5. 坚持使用英文编程,有利于提高英语水平。


如果英语不好,刚开始可能会难以忍受,但是一旦熬过去开始这段时间,坚持下来,将会是长期的回报。


如果你英语实在是非常差劲,可以借助一些翻译软件。比如世界上最好的翻译网站:translate.google.com/,虽然是 Google 的域名,但是大陆并没有墙。


不止是文件名,变量、函数等事物都应该使用英文命名。


使用英语,越早越好。


文件类型命名


有些同学的文件命名是这样的:



文件命名的问题上面已经解释了,这里主要来看文件后缀名的问题。


应该使用小写 .htm/.html 结尾。


原因有如下几点:



  1. 不同的操作系统处理大小写是不一样的。Windows/Mac 系统大小写不敏感,Linux 系统大小写敏感。统一命名方式会具有更好的移植性。


比如我们有如下目录结构:


├── cat.html
├── dog.html

下面的代码在 Mac/Windows 系统上正常。


<a href="./Dog.html">跳转到狗的页面</a>

但是在 Linux 系统上会出现 404。


我们开发时通常是在 Mac/Windows 系统,这时问题很难暴露,但是部署时通常是在 Linux 系统,就容易导致开发时正常,部署时异常的不一致性。



  1. 易读性,事实证明小写的单词更易于阅读。

  2. 便捷性,文件名和后缀名都保持小写,不需要额外按下 Shift 键了。

  3. htm 和 html 的区别是,在老的 DOS 系统上,文件后缀名最多只支持 3 位。所以很多语言都会把文件后缀名限制成 3 位以内。现在的操作系统已经没有这个问题了,所以 htm 和 html 的作用是完全一致的。如果你追求简洁一点,那么使用 htm 时完全没问题的。


代码格式化


有些同学的代码是这样的:



VSCode 提供了 prettier 插件,我们可以使用它对代码格式化。


代码格式化有以下优点:



  1. 代码格式化后更易于阅读和修改。比如它会自动帮你添加空格、对齐、换行等。

  2. 不需要去刻意学习代码样式了,代码格式化工具会帮你做好,并且在这个过程中你会潜移默化的学会怎么样调整代码样式。

  3. 使用统一的代码格式化,可以帮助大家在协作时保持一致,不会有比必要的争议。

  4. 新人加入项目时也可以更容易地融入到项目,不会看到风格迥异的代码。

  5. 在代码合并的时候也可以减少冲突的发生。


建议一定要开启代码格式化。


补充说明


这部分和文章内容无关,是针对评论区进行补充。


掘金没有评论置顶功能,我没办法逐一回复评论区。所以只能在文末进行统一解释。


很多人在评论区说本文水,或者在拿拼音的事情抬杠。本来我不想解释,但是负面评论的人确实不少。


我说两点。


第一,文章标题开头四字明确表明目标群体是前端小白,小白是什么概念能明白吗?一定是「xxx源码解读」才是干货硬货?


第二,关于中文好还是英文好,我不想继续争论。我从业多年,看过无数项目源码,从后端 Java JDBC、Spring、JVM、Go 到前端 React、Redux、Webpack、Babel,无一例外全是英文。或者你随便找个初具规模的互联网中大厂或者外企的程序员,看看他们公司是不是有不让用拼音和汉字的规范。


程序员群体普遍的毛病就是固执己见。永远只是站在自己的视角去观察世界,看到的永远都是自己想看到的。然后去贸然指责,这样真的会显得自己很没有修养,而且很无知。


哪怕做不到感同身受,也应该给予应有的尊重,哪怕难以理解,也不要随意贬低。这是做人的基本修养。


掘金是技术分享平台,不是贴吧/知乎。我写文章的本心只是分享内容,没有收各位一分钱。


文章内容对你有价值的话,非常感谢你的点赞支持。


文章内容对你无用的话,退出去就好了。


言尽于此。


能看懂就看,再看不懂就直接屏蔽我吧,谢谢配合。


最后希望掘金能推出文章评论置顶功能,或者文章禁止评论功能。这对创作者来说绝对是刚需。


作者:代码与野兽
来源:juejin.cn/post/7142368724144619556
收起阅读 »

都JDK17了,你还在用JDK8

Spring Boot 3.1.0-M1 已经发布一段时间了,不知道各位小伙伴是否关注了。随着Spring 6.0以及SpringBoot 3.0的发布,整个开发界也逐步进入到jdk17的时代。大有当年从jdk6 到jdk8升级过程,痛苦并快乐着。 为了不被时...
继续阅读 »

Spring Boot 3.1.0-M1 已经发布一段时间了,不知道各位小伙伴是否关注了。随着Spring 6.0以及SpringBoot 3.0的发布,整个开发界也逐步进入到jdk17的时代。大有当年从jdk6 到jdk8升级过程,痛苦并快乐着。


为了不被时代抛弃,开发者应追逐新的技术发展,拥抱变化,不要固步自封。


0x01 纵观发展




  • Pre-alpha(Dev)指在软件项目进行正式测试之前执行的所有活动




  • LTS(Long-Term Support)版本指的是长期支持版本




  • Alpha 软件发布生命周期的alpha阶段是软件测试的第一阶段




  • Beta阶段是紧随alpha阶段之后的软件开发阶段,以希腊字母第二个字母命名




  • Release candidate 发行候选版(RC),也被称为“银色版本”,是具备成为稳定产品的潜力的 beta 版本,除非出现重大错误,否则准备好发布




  • Stable release 稳定版又称为生产版本,是通过所有验证和测试阶段的最后一个发行候选版(RC)




  • Release 一旦发布,软件通常被称为“稳定版”




下面我们来看下JDK9~JDK17的发展:


版本发布时间版本类型支持时间新特性
JDK 92017年9月长期支持版(LTS)5年- 模块化系统(Jigsaw)
- JShell
- 接口的私有方法
- 改进的 try-with-resources
- 集合工厂方法
- 改进的 Stream API
JDK 102018年3月短期支持版(non-LTS)6个月- 局部变量类型推断
- G1 垃圾回收器并行全阶段
- 应用级别的 Java 类数据共享
JDK 112018年9月长期支持版(LTS)8年- HTTP 客户端 API
- ZGC 垃圾回收器
- 移除 Java EE 和 CORBA 模块
JDK 122019年3月短期支持版(non-LTS)6个月- switch 表达式
- JVM 原生 HTTP 客户端
- 微基准测试套件
JDK 132019年9月短期支持版(non-LTS)6个月- switch 表达式增强
- 文本块
- ZGC 垃圾回收器增强
JDK 142020年3月短期支持版(non-LTS)6个月- switch 表达式增强
- 记录类型
- Pattern Matching for instanceof
JDK 152020年9月短期支持版(non-LTS)6个月- 移除 Nashorn JavaScript 引擎
- ZGC 垃圾回收器增强
- 隐藏类和动态类文件
JDK 162021年3月短期支持版(non-LTS)6个月- 位操作符增强
- Records 类型的完整性
- Vector API
JDK 172021年9月长期支持版(LTS)8年- 垃圾回收器改进
- Sealed 类和接口
- kafka客户端更新
- 全新的安全存储机制

需要注意的是,LTS 版本的支持时间可能会受到 Oracle 官方政策变化的影响,因此表格中的支持时间仅供参考。


0x02 详细解读


JDK9 新特性


JDK 9 是 Java 平台的一个重大版本,于2017年9月发布。它引入了多项新特性,其中最重要的是模块化系统。以下是 JDK 9 新增内容的详细解释:



  1. 模块化系统(Jigsaw):


Jigsaw 是 JDK 9 引入的一个模块化系统,它将 JDK 拆分为约 90 个模块。这些模块相互独立,可以更好地管理依赖关系和可见性,从而提高了代码的可维护性和可重用性。模块化系统还提供了一些新的工具和命令,如 jmod 命令和 jlink 命令,用于构建和组装模块化应用程序。



  1. JShell:


JShell 是一个交互式的 Java 命令行工具,可以在命令行中执行 Java 代码片段。它可以非常方便地进行代码测试和调试,并且可以快速地查看和修改代码。JShell 还提供了一些有用的功能,如自动补全、实时反馈和历史记录等。



  1. 接口的私有方法:


JDK 9 允许在接口中定义 private 和 private static 方法。这些方法可以被接口中的其他方法调用,但不能被实现该接口的类使用。这样可以避免在接口中重复编写相同的代码,提高了代码的重用性和可读性。



  1. 改进的 try-with-resources:


在 JDK 9 中,可以在 try-with-resources 语句块中使用 final 或 effectively final 的变量。这样可以避免在 finally 语句块中手动关闭资源,提高了代码的可读性和可维护性。



  1. 集合工厂方法:


JDK 9 提供了一系列工厂方法,用于创建 List、Set 和 Map 等集合对象。这些方法可以使代码更加简洁和易读,而且还可以为集合对象指定初始容量和类型参数。



  1. 改进的 Stream API:


JDK 9 引入了一些新的 Stream API 方法,如 takeWhile、dropWhile 和 ofNullable 等。takeWhile 和 dropWhile 方法可以根据指定的条件从流中选择元素,而 ofNullable 方法可以创建一个包含一个非空元素或空元素的 Stream 对象。


除了以上几个新特性,JDK 9 还引入了一些其他的改进和优化,如改进的 Stack-Walking API、改进的 CompletableFuture API、Java 应用程序启动时优化(Application Class-Data Sharing)等等。这些新特性和改进都为 Java 应用程序的开发和运行提供了更好的支持。


JDK10 新特性


JDK10是JDK的一个短期支持版本,于2018年3月发布。它的主要特性如下:




  1. 局部变量类型推断:Java 10中引入了一种新的语法——var关键字,可以用于推断局部变量的类型,使代码更加简洁。例如,可以这样定义一个字符串类型的局部变量:var str = "hello"




  2. G1 垃圾回收器并行全阶段:JDK10中对G1垃圾回收器进行了改进,使其可以在并行全阶段进行垃圾回收,从而提高了GC效率。




  3. 应用级别的 Java 类数据共享:Java 10中引入了一项新的特性,即应用级别的 Java 类数据共享(AppCDS),可以在多个JVM进程之间共享Java类元数据,从而加速JVM的启动时间。




  4. 线程局部握手协议:Java 10中引入了线程局部握手协议(Thread-Local Handshakes),可以在不影响整个JVM性能的情况下,暂停所有线程执行特定的操作。




  5. 其他改进:Java 10还包含一些其他的改进,例如对Unicode 10.0的支持,对时间API的改进,以及对容器API的改进等等。




总的来说,JDK10主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。


JDK11 新特性


JDK11是JDK的一个长期支持版本,于2018年9月发布。它的主要特性如下:




  1. HTTP 客户端 API:Java 11中引入了一个全新的HTTP客户端API,可以用于发送HTTP请求和接收HTTP响应,而不需要依赖第三方库。




  2. ZGC 垃圾回收器:Java 11中引入了ZGC垃圾回收器(Z Garbage Collector),它是一种可伸缩且低延迟的垃圾回收器,可以在数百GB的堆上运行,且最大停顿时间不超过10ms。




  3. 移除Java EE和CORBA模块:Java 11中移除了Java EE和CORBA模块,这些模块在Java 9中已被标记为“过时”,并在Java 11中被完全移除。




  4. Epsilon垃圾回收器:Java 11中引入了一种新的垃圾回收器,称为Epsilon垃圾回收器,它是一种无操作的垃圾回收器,可以在不进行垃圾回收的情况下运行应用程序,适用于测试和基准测试等场景。




  5. 其他改进:Java 11还包含一些其他的改进,例如对Lambda参数的本地变量类型推断,对字符串API的改进,以及对嵌套的访问控制的改进等等。




总的来说,JDK11主要关注于提高Java应用程序的性能和安全性,通过引入一些新的特性和改进对JDK进行优化。其中,HTTP客户端API和ZGC垃圾回收器是最值得关注的特性之一。


JDK12 新特性


JDK12是JDK的一个短期支持版本,于2019年3月发布。它的主要特性如下:




  1. Switch 表达式:Java 12中引入了一种新的Switch表达式,可以使用Lambda表达式风格来简化代码。此外,Switch表达式也支持返回值,从而可以更方便地进行流程控制。




  2. Microbenchmark Suite:Java 12中引入了一个Microbenchmark Suite,可以用于进行微基准测试,从而更好地评估Java程序的性能。




  3. JVM Constants API:Java 12中引入了JVM Constants API,可以用于在运行时获取常量池中的常量,从而更好地支持动态语言和元编程。




  4. Shenandoah 垃圾回收器:Java 12中引入了Shenandoah垃圾回收器,它是一种低暂停时间的垃圾回收器,可以在非常大的堆上运行,且最大暂停时间不超过几毫秒。




  5. 其他改进:Java 12还包含一些其他的改进,例如对Unicode 11.0的支持,对预览功能的改进,以及对集合API的改进等等。




总的来说,JDK12主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Switch表达式和Shenandoah垃圾回收器是最值得关注的特性之一。


JDK13 新特性


JDK13是JDK的一个短期支持版本,于2019年9月发布。它的主要特性如下:




  1. Text Blocks:Java 13中引入了一种新的语法,称为Text Blocks,可以用于在代码中编写多行字符串,从而简化代码编写的复杂度。




  2. Switch 表达式增强:Java 13中对Switch表达式进行了增强,支持多个表达式和Lambda表达式。




  3. ZGC 并行处理引用操作:Java 13中对ZGC垃圾回收器进行了改进,支持并行处理引用操作,从而提高了GC效率。




  4. Reimplement the Legacy Socket API:Java 13中重新实现了Legacy Socket API,从而提高了网络编程的性能和可维护性。




  5. 其他改进:Java 13还包含一些其他的改进,例如对预览功能的改进,对嵌套访问控制的改进,以及对集合API的改进等等。




总的来说,JDK13主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Text Blocks和Switch表达式增强是最值得关注的特性之一。


JDK14 新特性


JDK14是JDK的一个短期支持版本,于2020年3月发布。它的主要特性如下:




  1. Records:Java 14中引入了一种新的语法,称为Records,可以用于定义不可变的数据类,从而简化代码编写的复杂度。




  2. Switch 表达式增强:Java 14中对Switch表达式进行了增强,支持使用关键字 yield 返回值,从而可以更方便地进行流程控制。




  3. Text Blocks增强:Java 14中对Text Blocks进行了增强,支持在Text Blocks中嵌入表达式,从而可以更方便地生成动态字符串。




  4. Pattern Matching for instanceof:Java 14中引入了一种新的语法,称为Pattern Matching for instanceof,可以用于在判断对象类型时,同时对对象进行转换和赋值。




  5. 其他改进:Java 14还包含一些其他的改进,例如对垃圾回收器和JVM的改进,对预览功能的改进,以及对JFR的改进等等。




总的来说,JDK14主要关注于提高Java应用程序的可维护性和易用性,通过引入一些新的特性和改进对JDK进行优化。其中,Records和Pattern Matching for instanceof是最值得关注的特性之一。


JDK15 新特性


JDK15是JDK的一个短期支持版本,于2020年9月发布。它的主要特性如下:




  1. Sealed Classes:Java 15中引入了一种新的语法,称为Sealed Classes,可以用于限制某个类的子类的数量,从而提高代码的可维护性。




  2. Text Blocks增强:Java 15中对Text Blocks进行了增强,支持在Text Blocks中使用反斜杠和$符号来表示特殊字符,从而可以更方便地生成动态字符串。




  3. Hidden Classes:Java 15中引入了一种新的类,称为Hidden Classes,可以在运行时动态创建和卸载类,从而提高代码的灵活性和安全性。




  4. ZGC并发垃圾回收器增强:Java 15中对ZGC垃圾回收器进行了增强,支持在启动时指定内存大小,从而提高了GC效率。




  5. 其他改进:Java 15还包含一些其他的改进,例如对预览功能的改进,对JVM和垃圾回收器的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK15主要关注于提高Java应用程序的可维护性和性能,通过引入一些新的特性和改进对JDK进行优化。其中,Sealed Classes和Hidden Classes是最值得关注的特性之一。


JDK16 新特性


JDK16是JDK的一个短期支持版本,于2021年3月发布。它的主要特性如下:




  1. Records增强:Java 16中对Records进行了增强,支持在Records中定义静态方法和构造方法,从而可以更方便地进行对象的创建和操作。




  2. Pattern Matching for instanceof增强:Java 16中对Pattern Matching for instanceof进行了增强,支持在判断对象类型时,同时对对象进行转换和赋值,并支持对switch语句进行模式匹配。




  3. Vector API:Java 16中引入了一种新的API,称为Vector API,可以用于进行SIMD(Single Instruction Multiple Data)向量计算,从而提高计算效率。




  4. JEP 388:Java 16中引入了一个新的JEP(JDK Enhancement Proposal),称为JEP 388,可以用于提高Java应用程序的性能和可维护性。




  5. 其他改进:Java 16还包含一些其他的改进,例如对垃圾回收器、JVM和JFR的改进,对预览功能的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK16主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Records增强和Pattern Matching for instanceof增强是最值得关注的特性之一。


JDK17 新特性


JDK17是JDK的一个长期支持版本,于2021年9月发布。它的主要特性如下:




  1. Sealed Classes增强:Java 17中对Sealed Classes进行了增强,支持在Sealed Classes中定义接口和枚举类型,从而提高代码的灵活性。




  2. Pattern Matching for switch增强:Java 17中对Pattern Matching for switch进行了增强,支持在switch语句中使用嵌套模式和or运算符,从而提高代码的可读性和灵活性。




  3. Foreign Function and Memory API:Java 17中引入了一种新的API,称为Foreign Function and Memory API,可以用于在Java中调用C和C++的函数和库,从而提高代码的可扩展性和互操作性。




  4. JEP 391:Java 17中引入了一个新的JEP(JDK Enhancement Proposal),称为JEP 391,可以用于提高Java应用程序的性能和可维护性。




  5. 其他改进:Java 17还包含一些其他的改进,例如对垃圾回收器、JVM和JFR的改进,对预览功能的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK17主要关注于提高Java应用程序的灵活性、可扩展性和性能,通过引入一些新的特性和改进对JDK进行优化。其中,Sealed Classes增强和Foreign Function and Memory API是最值得关注的特性之一。


总结




  • JDK9:引入了模块化系统、JShell、私有接口方法、多版本兼容性等新特性




  • JDK10:引入了局部变量类型推断、垃圾回收器接口、并行全垃圾回收器等新特性




  • JDK11:引入了ZGC垃圾回收器、HTTP客户端API、VarHandles API等新特性




  • JDK12:引入了Switch表达式、新的字符串方法、HTTP/2客户端API等新特性




  • JDK13:引入了Text Blocks、Switch表达式增强、改进的ZGC性能等新特性




  • JDK14:引入了Records、Switch表达式增强、Pattern Matching for instanceof等新特性




  • JDK15:引入了Sealed Classes、Text Blocks增强、Hidden Classes等新特性




  • JDK16:引入了Records增强、Pattern Matching for instanceof增强、Vector API等新特性




  • JDK17:引入了Sealed Classes增强、Pattern Matching for switch增强、Foreign Function and Memory API等新特性




总的来说,JDK9到JDK17的更新涵盖了Java应用程序开发的各个方面,包括模块化、垃圾回收、性能优化、API增强等等,为Java开发者提供了更多的选择和工具,以提高代码的质量和效率


小记


Java作为一门长盛不衰的编程语言,未来的发展仍然有许多潜力和机会。




  • 云计算和大数据:随着云计算和大数据的发展,Java在这些领域的应用也越来越广泛。Java已经成为了许多云计算平台和大数据处理框架的首选语言之一。




  • 移动端和IoT:Java也逐渐开始在移动端和物联网领域崭露头角。Java的跨平台特性和安全性,使得它成为了许多移动应用和物联网设备的首选开发语言。




  • 前沿技术的应用:Java社区一直在积极探索和应用前沿技术,例如人工智能、机器学习、区块链等。Java在这些领域的应用和发展也将会是未来的趋势。




  • 开源社区的发展:Java开源社区的发展也将会对Java的未来产生重要影响。Java社区的开源项目和社区贡献者数量不断增加,将会为Java的发展提供更多的动力和资源。




  • 新的Java版本:Oracle已经宣布将在未来两年内发布两个新的Java版本,其中一个是短期支持版本,另一个是长期支持版本。这将会为Java开发者提供更多的新特性和改进,以满足不断变化的需求。




总的来说,Java作为一门优秀的编程语言,具有广泛的应用和发展前景。随着技术的不断创新和社区的不断发展,Java的未来将会更加光明。


更多内容:


image.png


作者:QIANGLU
来源:juejin.cn/post/7238795712569802809
收起阅读 »

卸下if-else 侠的皮衣!

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


为了尽快脱掉这个if-else的皮衣,我们就先学习一种比较容易接受的设计模型:策略模式


策略模式


举个例子



  • 当购物类型为“苹果”时,满 100 - 20,不满 100 打 9 折

  • 当购物类型为“香蕉”时,满 100 - 30,不满 100 打 8 折

  • 当购物类型为“葡萄”时,满 200 - 50,不叠加

  • 当购物类型为“梨”时,直接打 5 折

    然后你根据传入的类型和金额,写一个通用逻辑出来,
    当我是if-else侠的时候,我估计会这样写:


funcion getPrice(type,money)
//处理苹果
if(type == 'apple'){
if(money >= 100){
return money - 20
}
return money * 0.9
}
//处理香蕉
if(type == 'banana'){
if(money >= 100){
return money - 30
}
return money * 0.8
}
//处理葡萄
if(type == 'grape'){
if(money >= 200){
return money - 50
}
return money
}
//处理梨
if(type == 'pear'){
return money * 0.5
}
}

然后我们开始来分析问题:\



  1. 违反了单一职责原则(SRP)

    一个方法里面处理了四个逻辑,要是里面的哪个代码块出事了,调试起来也麻烦

  2. 违反了开放封闭原则(ASD)

    假如我们要增加多一种水果的逻辑,就又要在这个方法中修改,然后你修改完这个方法,就跟测试说,我在这个方法增加了多一个种水果,可能要重新回归这个方法,那你看测试增加了多少工作量



改造考虑:消灭if-else, 支持扩展但是不影响原本功能!



const fruitsPrice = {
apple(money){
if(money >= 100){
return money - 20
}
return money * 0.9
},
banana(money){
if(money >= 100){
return money - 30
}
return money * 0.8
},
grape(money){
if(money >= 200){
return money - 50
}
return money
},
pear(money){
return money * 0.5
}
}

首先定义一个fruitPrice对象,里面都是各种水果价格的映射关系

然后我们调用的时候可以这样


function getPrice(type,money){
return fruitsPrice[type](money)
}

当我们要扩展新水果的时候


fruitsPrice.orange = function(money){
return money*0.4
}

综上所述:
用策略模式重构这个原本的逻辑,方便扩展,调试,清晰简明,当然这只是一个模式重构的情况,可能还有更优的情况,靠大家摸索


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!

作者:向乾看
来源:juejin.cn/post/7239267216805871671
给我点点赞,关注下!

收起阅读 »

环信十周年趴——我的程序人生

我是一名网瘾少年...记得上小学那会,每天中午我都会去学校的机房打传奇,那时候跟着老师一起弄了一个私服,感觉很牛,可能正是因为这个,才开始我的计算机编码启蒙。然而,也仅仅是启蒙了,初中和高中沉迷在了游戏中,不可自拔;也正因为如此,考了一个普通的大学,还得服从调...
继续阅读 »

我是一名网瘾少年...

记得上小学那会,每天中午我都会去学校的机房打传奇,那时候跟着老师一起弄了一个私服,感觉很牛,可能正是因为这个,才开始我的计算机编码启蒙。

然而,也仅仅是启蒙了,初中和高中沉迷在了游戏中,不可自拔;也正因为如此,考了一个普通的大学,还得服从调剂,但万幸的是专业是计算机科学与技术,我可以早一年在寝室配电脑。

整个大学依然沉迷游戏,颓废度过,到了大四,由于别的同学已经有找到实习工作的了,我才开始出现焦虑;为了未来,我踏上了去北京的火车,去学习iOS开发,当时是2015年,已经过了最火爆的时候,学成之后,我在北京四处碰壁,有些是因为我没有工作经验,有些则是因为我是培训出身,苦熬半个月,马上过年了,没办法只能打道回府。

回到老家本想着过完年再战北京,但阴差阳错,我在老家找了一份iOS开发工作,工作稳定,挣得钱够花,也就渐渐放弃了北京梦。

如今,我已在iOS开发这个领域做了6年多,在不断学习中,有很多收获,同时也用业余的时间学习python和MySQL,安卓也有涉猎,并且微信小程序可以接私活,挣外快;我坚信,继续坚持自我的修行之路,不断的提高自己的技能,一定能成为更加优秀的程序员。

生活虽然平淡如水,但总能在不经意间有一些小收获,我想,这也算一种幸福的生活。

最后,环信真的是一款优秀的产品,文档通俗易懂,接口功能丰富,在这个环信十周年之际,我祝愿环信越办越好,发展壮大,奋勇向前。

收起阅读 »

如何让安卓应用有两个入口

在使用鼎鼎大名的 leakcanary 检测内存泄漏时,我们发现,添加了 leakcanary 依赖后,再次运行 app 时,桌面上会多一个应用图标。 打开这个 Leaks 应用就能看到自己的 app 中存在的内存泄漏。 让桌面上多一个应用图标,这是怎么做到...
继续阅读 »

在使用鼎鼎大名的 leakcanary 检测内存泄漏时,我们发现,添加了 leakcanary 依赖后,再次运行 app 时,桌面上会多一个应用图标。


leakcanary


打开这个 Leaks 应用就能看到自己的 app 中存在的内存泄漏。


让桌面上多一个应用图标,这是怎么做到的呢?


答案是使用 activity-alias


一、为应用设置两个入口,分别启动两个 Activity


举个例子,通过 activity-alias 为应用程序指定另一个入口:

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:exported="false"
android:taskAffinity="second.affinity"/>
<activity-alias
android:name="SecondActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="SecondActivity"
android:targetActivity=".SecondActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

可以看到,应用入口是 MainActivity,但我们通过 activity-alias 给 SecondActivity 也设置了应用入口的 intent-filter,安装后,桌面就会有两个入口:


activity-alias


点击两个图标就会启动两个不同的 Activity。这里还给 SecondActivity 设置了 taskAffinity,目的是让 SecondActivity 启动时,被放在一个新的栈中。


二、为应用设置两个入口,启动同一个 Activity


activity-alias 添加入口时,是不是一定要启动不同的 Activity 呢?


答案是不一定。activity-alias 也允许我们为同一个 Activity 定义多个别名,从而实现一个应用程序拥有多个图标或多个启动入口的效果。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".Alias"
android:icon="@mipmap/ic_chrome"
android:label="Fake Chrome"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

可以看到,应用入口是 MainActivity,但我们通过 activity-alias 给 MainActivity 又设置了一个别名,安装后,桌面就会有两个入口:


activity-alias


点击两个图标都会启动同一个 Activity。


三、activity-alias 还能做什么?


如果我们需要设置一个 Activity 支持打开网页,通常会采用这样的做法:

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity>

这里给 MainActivity 添加了支持打开网页的 intent-filter。运行后,当遇到打开链接的请求时,就会弹出这样的对话框:


open with


除了这种方式,activity-alias 也可以实现同样的功能。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".browser"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>

另外,activity-alias 还可以给我们的应用再加一个 label 说明。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".browser"
android:label="My Browser"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>

此时再打开链接,就会在 My Application 底部展示我们新增的 label: My Browser:


My Browser


四、总结


activity-alias 为应用程序提供了更多的灵活性和可定制性。使用activity-alias,我们可以为一个Activity定义多个入口,从而增强应用程序的用户体验。


作者:wkxjc
链接:https://juejin.cn/post/7226911455878479931
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

五年后端外包仔的回顾

前言 背景:普通二本 专业:软件工程 第一次写文章,之前都是看别人写文章,觉得很厉害,自己也想尝试写写,以一个普通人的角度分享一下自己的经历,抒发一下emo情绪,顺便做个总结。 转眼间已经工作了快五年,从一个一窍不通的小白变成略知一二的老白,看着隔壁面试一个实...
继续阅读 »

前言


背景:普通二本


专业:软件工程


第一次写文章,之前都是看别人写文章,觉得很厉害,自己也想尝试写写,以一个普通人的角度分享一下自己的经历,抒发一下emo情绪,顺便做个总结。
转眼间已经工作了快五年,从一个一窍不通的小白变成略知一二的老白,看着隔壁面试一个实习生,又想起了以前。


实习


一开始我也没想到自己会进这个行业,本来是想选择数学专业,因为我对数学比较感兴趣,但是我妈觉得数学专业不太好找工作,而且那时候IT还是算比较火的行业,也感谢我妈当年选择了一条还可以的赛道= =


大四找实习的时候,遭到了面试官的毒打,我才发现自己这四年来只顾着打游戏了,问啥啥不会,才真正接触八股文。当年的八股文比现在还算简单不少,都是基础题。回想起来之前是真的有点离谱,学了ssm,背了背八股文就冲了,面试官还说我是不是经常打游戏...后面每天都投一堆简历,参加面试,最后入职了一家外包公司,开启了外包仔生活。


第一家公司算是帮助我入了这个行业吧。公司是上市公司,收了很多实习生,当时面试也没问什么,可能是把我们当做一张白纸。实习培训了公司的框架,后面转正了就开始投入项目。项目中用的是公司的框架,后端ssh,前端angularjs,做的是内部系统。化身CRUD工程师天天加班赶需求,做了两年多感觉没啥提升,除了业务基本没有成长,而且还要驻点出差。那时候驻点佛山,我坐顺风车去佛山的时候,司机问怎么从广州去到佛山工作,是不是工资很高?我:一言难尽...


跳槽


因为薪资和职业发展原因选择裸辞了,本来前公司的经理想着外包我回去一个项目工作,价格也谈好了,后面又没下文了。在这段空档期放纵了一会儿,去了旅游,吃了大餐,逛了公园。


鸣沙山.jpg
摩打.jpg
五羊.jpg

快乐了一段时间后,就跳槽到这家公司呆到了现在。这家公司其实还挺好的,走路上班而且不怎么加班,唯一的缺点就是薪资方面,总结就是钱少事少离家近。公司业务是做互联网电视的,在这里我也接触到很多以前公司没有的场景,比如高并发和大数据量的处理方案,感觉自己的CRUD能力变得成熟一点了。


精神内耗


在这个内卷、焦虑的时代中,有的人开开心心摸鱼躺平,有的人努力考公上岸,有的人努力学习保持进步,而我就是现实躺平,又想着自己要加油的状态。很喜欢黄执中的一句话

半吊子得不到幸福,你在此岸望彼岸,你两头不到岸

因为疫情原因去年年终奖没了,又想跳槽了,看了很多文章更焦虑了。老生常谈的35岁失业、大厂裁员、寒冬将至......躺平真的好快乐,打打游戏,摸摸鱼,刷刷抖音一天又过去了,大哥几年前送我的算法就只看了目录,感觉工作后静下心来看书挺难的。但是这样子温水煮青蛙感觉35岁就要去当保安了,所以还是加入内卷队伍吧。


我是一个极其懒惰的人,可能想着一出是一出,今天想看书,明天想写博客,但是只是停留在脑子里,没有付诸于行动。其实还是要动起手来,就好像我写这篇文章一样,写得不好也没啥关系,也是一种进步。所以说想到什么就做吧!


写在最后


定几个小目标吧:



  1. 每周花时间看看书

  2. 搭建一个自己的项目

  3. 抽空刷leetcode

  4. 找到一个更好的公司平台


摸鱼躺平也好,拼搏奋斗也罢,还是相信当下的决定是自己认为最好的选择,最后祝大家活成理想的样子。


作者:玛奇玛丶
链接:https://juejin.cn/post/7205467823563931685
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

小米:阳了,被裁了

随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上都处于一边发烧,一边坚持工作的状态,症状严重的小伙伴忍着疼痛还要处理公司的任务,把自己奉献给公司,然后收到了却是公司无情的裁员的消息。 年末将至,知乎和小米也登上了热...
继续阅读 »

随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上都处于一边发烧,一边坚持工作的状态,症状严重的小伙伴忍着疼痛还要处理公司的任务,把自己奉献给公司,然后收到了却是公司无情的裁员的消息。


年末将至,知乎和小米也登上了热搜。


裁员


我之前在小米的同事陆陆续续收到了通知,阳着还在工作,然后收到了裁员的消息。国内公司裁员的吃相都不怎么好看,基本上在发年终奖前会进行大比例的裁员。


2021 年的时候小米有 32000 名员工,据传 2022 年底小米要裁 6000 名员工,裁员幅度接近 20%,无论消息是否真实,但是这次裁员规模影响范围应该不小。



小米为什么要裁员


小米连续 3 个季度开始下滑,前 3 个季度,每个季度利润 20 亿,相比于去年同期的 50 亿下跌了很多,那为什么利润下跌这么多呢,主要有以下原因:



  1. 公司不赚钱,意味着主营业务开始萎缩,小米的主营业务,手机前 3 个季度卖了 4020 万部,销售额大概 425 亿,平均每部手机 1000 元,原本指望华为被制裁之后,小米能拿下这部分用户,但是最后也放弃了,这部分用户基本上都归苹果了

  2. 据调查中国的手机市场已经处于饱和状态,每年换手机的发烧友越来越少了

  3. 小米赌上全部身价大踏步地进入汽车领域,汽车是个周期长、投资大的业务,没有上百个亿,基本上不可能会有结果的

  4. 小米的股价也跌了很多,投资人很失望,我也买了很多小米的股票,基本上都是血亏


所以不得不开始降本增效,在老板的眼里,业务上升期的时候,开始疯狂砸钱招人,到达了瓶颈,业务不再增长的时候,老板就会冷静下来盘算,到底需不需要这么多人,然后开始降本增效,而裁员就是最有效的控制成本的手段。


曾经有小伙伴问过,小米的年终奖能拿多少


我在这里也只是顺口一说,大家当做饭后余兴看看就好了,小米的年终奖是 2 个月,而个人绩效是跟部门和所在事业部挂钩的,如果部门的绩效好的话,大部分人都能拿满,但是如果部门绩效不好的话,只有少数人能拿满,平均下来一个部门能拿满 2 个月的人数非常少,如果你非常的优秀,拿 3~4 个月也是有的,但是这个比例极其少,如果你和领导关系好的话,那么就另当别论了。


小米这次裁员赔偿虽然给了 N+2,但是这次裁员的吃相也比较难看,引来了小米员工的吐槽。以下图片来自网络。





而每次裁员,应届生都是最惨的,在大裁员的环境下,能不能找到工作是最大的问题,应届生和有工作经验的社招生是不一样的,无论是赔偿还是找工作的机会,相比于应届生更愿意招社招生,当然特别优秀的除外。




我之前很多在小米的同事,赔偿都给了 N + 2,但是年底被裁员时间点非常的不好,短时间内,想找到工作是非常困难的,但是先不要着急,如果你的身体还没恢复,建议先等身体恢复,在恢复期间,整理一下你的工作项目,网上搜索一下面试题,整理和回顾这些面试题,记住一定要多花时间刷算法题。


等到年后找工作会容易些,面试的成功的率也会很高,你的溢价空间也会很大,在选择公司的时候,这个阶段还是以稳为主,避开那些风险高的公司和部门。


文章的最后


遍地小阳人的冬天比以往更冷,在公司非常艰难,业务不再增长的时候,都会断臂求生,我们都要去面对被裁的风险。


站在打工者的角度,当一个人在某个环境待久了,会被表象的舒适所蒙蔽,时间久了会变得很迷茫,所以我们要想办法跳出舒适圈,保持学习的热情。



作者:程序员DHL
链接:https://juejin.cn/post/7184234182778814522
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

入坑两个月自研创业公司

一、拿offer 其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果...
继续阅读 »

一、拿offer


其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果一直拖到7月。


二、入职工作


刚入职工作时,一是有些抗拒,二呢是有些欣喜。抗拒是因为长时间呆家的惯性,以及人的惰性,我这只是呆家五个月,那些呆家一年两年的,再进入社会,真的很难,首先心理上他们就要克服自己的惰性和惯性,平时生活习惯也要发生改变


三、人言可畏


刚入职工作时,有工作几个月的老员工和我说,前公司的种种恶心人的操作,后面呢我也确实见识到了:无故扣绩效,让员工重新签署劳动协议,但是,也有很多不符实的,比如公司在搞幺蛾子的时候,居然传出来我被劝退了……


四、为什么离开


最主要的原因肯定还是因为发不出工资,打工是为了赚钱,你想白嫖我?现在公司规模也不算小了,想要缓过来,很难。即便缓过来,以后就不会出现这样的状况了?公司之前也出现过类似的状况,挺过来的老员工们我也没看到有什么优待,所以这家公司不值得我去熬。技术方面我也基本掌握了微信和支付宝小程序开发,后面不过是需求迭代。个人成长方面,虽然我现在是前端部门经理,但前端组跑的最快,可以预料后面我将面临无人可用的局面,我离职的第二天,又一名前端离职了,约等于光杆司令,没意义。


五、收获


1.不要脱产,不要脱产
2.使用uniapp进行微信和支付宝小程序开发
3.工作离家近真的很爽
4.作为技术人员,只要你的上司技术还行,你的工期他是能正常估算,有什么难点说出来,只要不是借口,他也能理解,同时,是借口他也能一下识别出来,比如,一个前端和我说:“后端需求不停调整,所以没做好。”问他具体哪些调整要两个星期?他又说不出来。这个借口就不要用了,但是我也要走了,我也没必要去得罪他。
5.进公司前,搞清楚公司目前是盈利还是靠融资活,靠融资活的创业公司有风险…


六、未来规划


关于下一份工作:
南京真是外包之城,找了两周只有外包能满足我目前18k的薪资,还有一家还降价了500…
目前offer有
vivo外包,20k
美的外包,17.5k
自研中小企业,18.5k


虽然美的外包薪资最低,但我可能还是偏向于美的外包。原因有以下几点:
1.全球手机出货量下降,南京的华为外包被裁了不少,很难说以后vivo会不会也裁。
2.美的目前是中国家电行业的龙头老大,遥遥领先第二名,目前在大力发展b2c业务,我进去做的也是和商场相关。
3.美的的办公地点离我家更近些
4.自研中小企业有上网限制,有过类似经验的开发人,懂得都懂,很难受。


关于考公:
每年10月到12月准备下,能进就进,不能再在考公上花费太多时间了。


作者:哇哦谢谢你
链接:https://juejin.cn/post/7160138475688165389
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不想写代码的程序员可以尝试的几种职位

标题不够严谨,应该是不想写业务代码的程序员可以做什么。 这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。 这里也主要分享 IT 行业内的岗位,...
继续阅读 »

标题不够严谨,应该是不想写业务代码的程序员可以做什么。


这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。


这里也主要分享 IT 行业内的岗位,要是除开行业限制,范围就太大了。


Developer Relation/Advocate


国外有很多面向开发者的技术创新公司,比如 Vercel ,PlanetScale ,Prisma ,Algolia 等。


这类公司的用户就是开发者,所以他们的市场活动也都是围绕着开发者;他们需要让更多的开发者可以更容易地把他们的技术用到他们的技术栈里,所以就有了这种岗位。用中文来表达,可能有点类似是布道师的意思?


国内更多是将技术应用起来,而不是创造一些新的技术,所以这种岗位在国内就非常少见了。当然近几年也还是有一些技术驱动型公司的,像 PingCAP ,Agora 等。


希望国内有更多像这样的公司出来。


Technical Recruiter


这个工作从 title 上就大概知道是做什么的了。


这个岗位有深有浅,深的可能是比较完整的招聘职能,浅的可能就是 HR 部门里面试和筛选技术候选人的。


Technical Writer


这个听着像是产品经理的工作,确实会和产品的职责有小部分重叠。


这是个面向内部的岗位,不喜欢对外对用户 /客户的朋友会非常喜欢。通常是一些比较大型的企业要做软件迁移或者什么系统、流程升级之类的时候,因为会牵扯到非常多的 moving parts ,所以通常都需要一个独立岗位来负责 documentation 的工作。


工作内容包括采访以及记录各部门的现有流程和业务需求,然后是新流程 /系统 /软件的手册、图表等等。


这里的“technical”不是我们研发中的技术,更多是“业务”层面的意思。同样这个岗位对技术要求不高,但是有研发背景是非常加分的。


Technical Support


通常这个岗位归属客服部门,高于普通 customer service rep 。普通的 customer support 是客户遇到问题时的第一层支持 - 基本会讲话、了解产品就能干的工作;如果第一层解决不了客户的问题,就会升级到后面 technical support 了。


这个岗位范围会更广一点,几乎任何 IT 公司都会有这种支持岗;对技术要求根据不同公司而不同,比如 Vercel 对这个岗位的技术要求肯定比 HelpScout (一个客服软件)要高。


但整体来说都不如研发要求高,但对应的薪酬待遇也没有研发那么好。


结语


其实说了这么多总结下来就是国外技术生态、开源氛围好很多,并且对技术足够的重视,促使很多技术公司的出现,然后催生了这些工作。


如果觉得本帖有启发,欢迎留言支持鼓励后续的创作。


作者:强生
链接:https://juejin.cn/post/7229223235680895031
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

深入学习 Kotlin 枚举的进阶用法:简洁又高效~

Kotlin 作为现代的、强大的编程语言,可以给开发者提供诸多特性和工具,得以帮助我们编写更加高效、更具可读性的代码。 其中一个重要的特性便是 Enum 枚举,其本质上是一种数据类型:允许你定义一组用名称区分的常量。 本篇文章将通过代码案例带你探索 Kotli...
继续阅读 »

Kotlin 作为现代的、强大的编程语言,可以给开发者提供诸多特性和工具,得以帮助我们编写更加高效、更具可读性的代码。


其中一个重要的特性便是 Enum 枚举,其本质上是一种数据类型:允许你定义一组用名称区分的常量


本篇文章将通过代码案例带你探索 Kotlin 枚举的进阶用法,进而帮助大家理解如何将 Enum 更好地应用到项目当中。


1. 枚举类


可以说 Enum Classes 是 Kotlin 中展示一组常量的绝佳方式。


具体来说,它允许你定义一组有限数量的成员来限定数据类型,并且你可以在代码的各处便捷使用这些枚举类型。


如下,我们用 enum 关键字定义一周内各天的枚举类型。

 enum class DayOfWeek {
     MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
 }

然后在代码中自由使用该枚举,比如:

 fun getWeekendDays(): List<DayOfWeek> {
     return listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
 }

2. 枚举属性


除了展示类型,Kotlin Enum 还可以拥有属性 property,这意味着开发者可以给枚举成员添加额外的信息。


比如下面,我们给 DayOfWeek 枚举增加各天在周内的序号属性。

 enum class DayOfWeek(val number: Int) {
     MONDAY(1),
     TUESDAY(2),
     WEDNESDAY(3),
     THURSDAY(4),
     FRIDAY(5),
     SATURDAY(6),
     SUNDAY(7)
 }

然后便可以获得该天的序号信息。

 fun getDayNumber(day: DayOfWeek): Int {
     return day.number
 }

3. 枚举函数


Kotlin Enum 也支持定义函数,所以可以在枚举内部定义功能性方法、供外部使用。


如下在 DayOfWeek 枚举里增加一个用来判断该天是否属于周末的 isWeekend() 函数。

 enum class DayOfWeek(val number: Int) {
     MONDAY(1),
     TUESDAY(2),
     WEDNESDAY(3),
     THURSDAY(4),
     FRIDAY(5),
     SATURDAY(6),
     SUNDAY(7);
 
     fun isWeekend(): Boolean {
         return this == SATURDAY || this == SUNDAY
    }
 }

在使用该枚举的地方,便可以直接使用该函数进行判断。

 fun printDayType(day: DayOfWeek) {
     if (day.isWeekend()) {
         println("$day is a weekend day.")
    } else {
         println("$day is a weekday.")
    }
 }

4. 枚举构造函数


既然 Enum 可以拥有属性,那么自然支持构造函数,所以开发者可以在实例构造的时候,增加充分多的信息。


比如,我们在 DayOfWeek 枚举的构造函数里,在序号以外增加该天的名称信息。

 enum class DayOfWeek(val number: Int, val displayName: String) {
     MONDAY(1, "Monday"),
     TUESDAY(2, "Tuesday"),
     WEDNESDAY(3, "Wednesday"),
     THURSDAY(4, "Thursday"),
     FRIDAY(5, "Friday"),
     SATURDAY(6, "Saturday"),
     SUNDAY(7, "Sunday");
 
     override fun toString(): String {
         return displayName
    }
 }

这样便可以获得该枚举携带的名称数据。

 fun printDayName(day: DayOfWeek) { 
     println("The day of the week is ${day.displayName}")
 }

5. 枚举扩展函数


和普通类一样,也可以针对 Enum Class 添加扩展函数。我们可以在枚举类外部,按需添加额外的功能函数。


比如这里给 DayOfWeek 枚举扩展一个获取下一天的函数。

 fun DayOfWeek.nextDay(): DayOfWeek {
     return when (this) {
         MONDAY -> TUESDAY
         TUESDAY -> WEDNESDAY
         WEDNESDAY -> THURSDAY
         THURSDAY -> FRIDAY
         FRIDAY -> SATURDAY
         SATURDAY -> SUNDAY
         SUNDAY -> MONDAY
    }
 }

像调用枚举本身定义的函数一样,自由使用该扩展函数。

 fun printNextDay(day: DayOfWeek) {
     println("The next day is ${day.nextDay()}")
 }

结语


可以看到 Kotlin Enum 可以帮助开发者定义好一组类型的常量:大大简化代码、具备更好的可读性以及提供额外的功能函数。


通过上述的进阶用法,相信大家可以使用 Enum 创造出更加健壮和高效的代码,同时也更容易理解和维护。


作者:TechMerger
链接:https://juejin.cn/post/7230775751125205029
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Android】Kotlin 中特别的关键字

前言 Kotlin 是一种现代化的静态类型编程语言,被广泛应用于 Android 开发、Web 开发等领域。与其他编程语言相比,Kotlin 中具有一些独特的关键字,本文将介绍这些关键字及其使用。 1. data data 是 Kotlin 中的一个关键字,用...
继续阅读 »

前言


Kotlin 是一种现代化的静态类型编程语言,被广泛应用于 Android 开发、Web 开发等领域。与其他编程语言相比,Kotlin 中具有一些独特的关键字,本文将介绍这些关键字及其使用。


1. data


data 是 Kotlin 中的一个关键字,用于定义数据类。数据类是一种特殊的类,用于封装数据,通常不包含任何业务逻辑。定义数据类时,只需要列出需要存储的属性即可,Kotlin 会自动生成一些常用的方法,如 toString()equals()hashCode() 等。

kotlinCopy code
data class User(val id: Int, val name: String, val age: Int)

2. companion object


companion object 是 Kotlin 中的一个关键字,用于定义伴生对象。伴生对象是一个类内部的单例对象,可以访问该类的私有成员。与 Java 中的静态方法类似,Kotlin 中的伴生对象可以定义静态方法和静态属性。

class Utils {
companion object {
fun add(a: Int, b: Int): Int {
return a + b
}
}
}

val result = Utils.add(1, 2)

3. lateinit


lateinit 是 Kotlin 中的一个关键字,用于定义延迟初始化的变量。延迟初始化的变量必须是非空类型的,并且不能使用 val 关键字定义。在变量被访问之前,它必须被初始化,否则会抛出 UninitializedPropertyAccessException 异常。

class Person {
lateinit var name: String

fun initName() {
name = "John"
}

fun printName() {
if (::name.isInitialized) {
println(name)
} else {
println("Name has not been initialized yet")
}
}
}

val person = Person()
person.initName()
person.printName()

4. by


by 是 Kotlin 中的一个关键字,用于实现委托。委托是一种将对象的某些职责交给另一个对象处理的机制,可以简化代码的编写。Kotlin 中的委托分为接口委托、类委托和属性委托三种,通过 by 关键字实现。

interface Car {
fun drive()
}

class RealCar : Car {
override fun drive() {
println("Real car is driving")
}
}

class FakeCar(private val realCar: RealCar) : Car by realCar {
override fun drive() {
println("Fake car is driving")
}
}

val realCar = RealCar()
val fakeCar = FakeCar(realCar)
fakeCar.drive()

5. when


when 是 Kotlin 中的一个关键字,用于实现类似于 switch-case 的语句。与 switch-case 不同的是,when 可以匹配更加复杂的条件表达式,并且支持任意类型的值作为条件。另外,when 的分支可以是表达式,可以方便地处理多种情况。

fun getScore(level: String): Int = when (level) {
"A" -> 90
"B" -> 80
"C" -> 70
else -> 0
}

val scoreA = getScore("A")
val scoreB = getScore("B")

6. object


object 是 Kotlin 中的一个关键字,用于定义匿名对象或单例对象。匿名对象是一种不需要定义类名的对象,可以在需要的时候创建并使用。单例对象是一种只有一个实例的对象,可以在整个应用程序中共享使用。

fun main() {
val person = object {
val name = "John"
val age = 20
}
println("${person.name} is ${person.age} years old")
}

object Config {
val host = "localhost"
val port = 8080
}

val host = Config.host
val port = Config.port

7. reified


reified 是 Kotlin 中的一个关键字,用于实现泛型类型的具体化。在 Java 中,泛型类型在运行时会被擦除,导致无法获取泛型类型的具体信息。而在 Kotlin 中,通过 reified 关键字可以将泛型类型具体化,可以在运行时获取泛型类型的信息。

inline fun <reified T> getType(): String = T::class.java.simpleName

val typeName = getType<Int>()

总结


Kotlin 中的关键字具有一些独特的特性,如数据类、伴生对象、延迟初始化、委托、when、对象表达式和具体化的泛型类型等。这些特性可以让开发者更加方便地编写代码,提高代码的可读性和可维护性。


作者:Quincy_Ye
链接:https://juejin.cn/post/7229347310449459255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

2023移动端技术探索

1. 行业背景 过去的2022年对大家来说都是困难的一年,难在疫情影响,难在宏观环境的增长放缓。没有增长带来的就是痛苦的体验,对于互联网行业,21年的主题是“反996”,到了22年风向就变成了“降本增效”、“业务搜索”以及“裁员”。再细化动移动端,经过十年的发...
继续阅读 »

1. 行业背景


过去的2022年对大家来说都是困难的一年,难在疫情影响,难在宏观环境的增长放缓。没有增长带来的就是痛苦的体验,对于互联网行业,21年的主题是“反996”,到了22年风向就变成了“降本增效”、“业务搜索”以及“裁员”。再细化动移动端,经过十年的发展,它已经步入“成熟期”,各行各业都被改造差不多了,技术上该有的轮子都有了,基础的服务也搭建差不多了,似乎真正到达瓶颈了,存量时代的小修小补对人力的需求已经是对半砍了。脉脉《抢滩数字时代·人才迁徙报告2023》报告显示:2022年企业招聘总职位数量同比减少21.67%,纯互联网职位量同比减少50.4%。


2023-01-30-23-04-03-image.png
又到了制定新一年OKR的时候了,大家都在发愁技术项目的规划,不知道在技术上去做哪些探索和突破。InfoQ发布的《中国软件技术发展洞察和趋势预测研究报告2023》第三条核心结论显示:2022年技术服务理念转变,从技术先进到业务赋能,IT部门公司定位逐渐由成本部门转向业务赋能部门,技术也更被边缘化了,个人职业发展屏障出现,这个时候我们不禁对前途迷茫甚至产生质疑。再细化到移动端,《中国软件技术发展洞察和趋势预测研究报告2023》展示的“中国技术成熟度评估曲线”中前沿和早期推广项目貌似都与移动端没有太大关系。


2023-01-31-15-59-41-image.png


本文尝试从各个方面探索移动端可以发展的方向,最大程度的“压榨”可能的技术方向(有些只是抛出问题,而不是最终答案)。


2. 近两年大厂探索方向与成果


在挖掘之前先看看大厂(可能是某个领域有所建树)这些年在做什么,看看有没有直接可以抄的作业。


2.1 21年出调研结果


21年初写OKR时对几个大厂做了调研,下面分别看看阿里、美团、京东做了什么,准备做什么:


阿里移动端技术全景图


2023-01-30-15-31-22-image.png


阿里移动端发展趋势


2023-01-30-15-31-56-image.png


美团移动端技术全景图


2023-01-30-15-45-52-image.png


京东移动端技术全景图


2023-01-30-15-32-53-image.png


京东移动端未来远景图


2023-01-30-15-43-34-image.png


看起来都大同小异,可能各个规模的公司都在建设或者建设完成。


2.2 22年产出


在看看22年大厂的输出,这里主要来自于企业技术公众号输出内容。


2.2.1 阿里


阿里推出的《2022技术人的百宝黑皮书》总结了2022年阿里年度精选终端技术栈的内容:


2023-01-31-17-21-38-image.png


2.2.2 美团


下面内容摘自美团技术发布的《2022年美团技术年货-合辑》:


2023-01-31-17-35-46-image.png


2.2.3 百度


百度App技术公众号发布2022精选文章:


2023-01-31-17-39-09-image.png


2.2.4 分析


从上面三家企业对外输出的文章看,在移动端的动作不外乎几个方向:



  1. 跨端/低代码

  2. 性能优化

  3. 自动化测试

  4. 开发平台/平台化能力

  5. AI


3. 移动端主要方向分析


结合上面整理出来的,我们看看移动端“可以”有哪些方向。


3.1 业务开发


业务开发还是主流的市场需求,这块会占大部分的比例,IT部门从成本部门转为赋能部门后,主要的工作量就是支持业务。


3.2 跨端/低代码


在降本增效的背景下,跨端还会持续搞,但是也不是新东西了。H5、React、Flutter、小程序,这些都各有利弊,不同场景用不同技术,像小程序这种更适合平台化的超级APP,规模不够大的话,性价比不高。


3.3 性能优化


同样的,性能优化也是需要持续做的事情,但是也不是新东西了,一些技术手段都比较成熟了,没有太多可挖掘的空间了。


3.4 架构方向


架构管理方向随着规模的收缩,很难出现机会了。


3.5 开发平台建设


在公司内部,类似于蚂蚁的mPaas开发平台在业务快速成长期对提效会有很大的帮助,这个时候随着业务的裂变,推出各种APP,开发平台可以避免很多重复的工作,助力应用快速上线和运营,但是在收缩期再去建设就有点不划算了。


单点的平台能力,比如监控、埋点之类的或者用第三方或者也自建完成了,对缺失的个别能力,可以根据业务需求点滴建设。


3.6 系统应用/Framework/驱动开发


随着AI、Iot、新能源的发展与兴起,释放出一些系统开发的诉求,相对于之前,嵌入式驱动开发的薪资也有所增长,也算是一个方向,但是也要记住,比起手机,电视、汽车毕竟是少数,如果纯转嵌入式的话可能沾Iot的光规模更大些,不过比起芯片,这也是比较成熟的技术,可挖掘方向不大,只是多了个写业务的战场。


3.7 XR


目前比较成熟的是VR,但是VR在端上展示主要基于H5,采集会有单独硬件,有些也支持了手机采集,但是还是那句话,市场需求不大。至于AR、元宇宙更多的是AI的综合应用了,我们也不讨论了。


3.8 音视频


音视频一直是移动端比较大和前沿的一块方向,但是现在也已趋于成熟,下面看看主要的几个方向:



  • 点播:播放器的事情,主要涉及多解码期、预加载秒开,剩下的交给系统播放器都可以完成的很好了;

  • 录制:系统录制工具,或者基于系统采集、编码、封装封装一套;

  • 视频编辑和特效处理:编辑主要是解复用--->解码--->逐帧处理--->编码--->复用的过程,逐帧处理用到视频上主要设计合成、滤镜等;音频主要是变声、声音融合,都是通用的技术,稍微体现差异的就是特效处理中与AI的结合,比如美颜、带眼镜等会用到图像检测,但是都不是门槛,也谈不上前沿探讨;

  • 直播:直播也同样有成熟的结局方案:采集推流,开源服务端以及成熟CDN,播放ijk,秒开之类的都是参数优化了;

  • 实时音视频:实时音视频开发成本比较高,主要的挑战是弱网对抗,3A处理等,由于不是通用协议,没有CDN,自己搭建机房成本高,而且不见得效果比第三方好,所以也是一件性价比比较低的事情。

  • 编解码:目前主流的还是H264,VP8,H264甚至都没有推开,限制编解码算法的主要是推广和兼容性,所以编解码器都是一些组织去搞,一个公司贸然去开发,风险很大。


3.9 AI


人类一直在追求更智能的机器,AI是未来,所以即使现在不够好,并且没有找到太多的落地场景,但是很多公司还在搞,尤其是ChatGPT的能力让大家惊讶,但是它仍然不是真正的“像人类一样”的智能。目前通用的AI主要有一下几个方向:




  1. 语音方向



    1. 前端信号处理

    2. 唤醒

    3. 语音识别

    4. 声纹

    5. TTS

    6. 作曲:抖音之前分享有过这方面实践和应用

    7. 基于特征的语音编码:比如谷歌推出的的lyra和SoundStream,Lyra的设计工作速率为3kbps,听力测试表明,在该比特率下,Lyra的性能优于任何其他编解码器,并优于Opus的8kbps,因此实现了60%以上的带宽削减。但是正如上面说的,编解码器的瓶颈主要还是在于标准的推广。




  2. 图像方向



    1. 检测

    2. 识别

    3. 图像比较(应用于UI自动化测试)




  3. 自然语言处理



    1. 智能问答

    2. 意图识别

    3. 文档纠错




  4. 风控




  5. 推荐




  6. 用户画像




  7. 元宇宙/数字人:数字人更像是一个AI的综合应用。




还有些特殊的特殊业务场景的特殊用户,比如房产领域:



  1. 户型解读(基于图像的特殊特征)

  2. 训练场


对于AI,移动端可以做哪些探索?回答这个问题先要搞明白哪些场景适配放在端上来做。Android官方给了一个决策的标准:


2023-01-31-23-57-36-image.png


上面提到的特别需要在端上应用的主要有:



  1. 唤醒

  2. 图像检测

  3. 语音编解码


可以放在端上应用的:



  1. ASR

  2. TTS

  3. 图像标签


基于这些场景端上的主要工作量是什么呢?模型训练大部分还是放在云端,端上就是加载模型,输入数据,展示输出结果,还有可能就是对引擎,框架做些优化:


2023-01-30-15-44-47-image.png


4. 总结


整体来看,整个移动端技术的发展可以说到了“山穷水尽”的地步,可挖掘的创新型内容不是很多了,大部分都是在现有体系维护和迭代。整体来看业务支撑还是主要的需求来源,车机、Iot也释放出一些机会,跨端、开发平台、性能优化、VR已趋于成熟,端智能落地的还是语音、图像这些通用的方向,深度结合业务的还有待挖掘。目前的AI解决的还是“决策”问题,从现在生成式到未来创造式通用的、”人类水平“的“智能”还有很长的路要走,谁也不能打包票说雷·库兹韦尔提出的奇点理论的“奇点”能不能到来,什么时候到来,智能的进化不止是算法层面的,还会收到算力的影响,像《流浪地球》系列中的MOSS机器人是因为量子计算机算力快的加成。


作者:轻口味
链接:https://juejin.cn/post/7239267216805429303
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

环信十周年趴——我的程序人生之RTC

        作为一名程序员,我的编程之路始于13年,当时我对计算机和编程非常感兴趣。我开始学习Java,这是我接触到的第一种编程语言。逐渐地,我发现编程可以让我创造出自己的东西...
继续阅读 »

        作为一名程序员,我的编程之路始于13年,当时我对计算机和编程非常感兴趣。我开始学习Java,这是我接触到的第一种编程语言。逐渐地,我发现编程可以让我创造出自己的东西并解决实际问题,这让我感到非常有成就感。

        随着时间的推移,我开始学习其他编程语言,包括Python和Go语言。这些语言各有优劣,但都有助于我更好地理解编程思想和实践。

        我的职业生涯中,我曾经涉足央企、CDN、RTC等多个产品线。在每个产品线中,我都学习到了新的知识和技能。我注意到,不同的产品线需要不同的技术和能力,这让我不断地学习和成长。

        我的程序人生充满了挑战和机遇。在这个过程中,我遇到了许多有趣的问题和难题,并通过不断的学习和实践找到了解决方法。我也遇到了一些非常有才华和热情的同事,他们对我的职业生涯产生了深远的影响。

        我相信,我的程序人生还有很长的路要走。未来,我将继续学习新的技术和探索新的领域,以便更好地服务于社会和行业。我坚信,编程是一项有趣而有价值的事业,它可以让我们创造出更好的世界。

本文参与环信十周年活动

收起阅读 »

《环信十周年趴——我的程序人生之iOS- Flutter》

    我是一名iOS开发工程师,已经在这个行业里摸爬滚打了14年。在这14年中,我经历了从Objective-C到Swift再到Flutter的演进,也经历了从少年时期的满怀抱负到中年危机的现实冲击。现在,我想分享一下...
继续阅读 »


    我是一名iOS开发工程师,已经在这个行业里摸爬滚打了14年。在这14年中,我经历了从Objective-C到Swift再到Flutter的演进,也经历了从少年时期的满怀抱负到中年危机的现实冲击。现在,我想分享一下我的程序人生,希望能给那些像我曾经一样,心中有梦的年轻人一些启示。

    在我刚开始从事iOS开发的时候, Objective-C还是主流语言。那时候,我用C语言的基本语法,学习Objective-C的面向对象编程,一步步攻克了这个语言的第一道难关。但是,随着时间的推移,Objective-C的局限性越来越明显。它语法笨重、效率不高,越来越难以满足我们开发的需求。于是,Swift语言应运而生。
Swift语言的出现,让我看到了iOS开发的新希望。它快速、安全、简洁,有着现代编程语言的特性,如可选类型、函数式编程等。在Swift语言的基础上,我能够编写出更加高效、更加优美的代码,也能够更加深入地理解iOS框架的本质。同时,Swift语言也为我后续学习其他语言提供了很大的帮助。

    随着移动开发的不断发展,跨平台开发成为了一个越来越重要的话题。Flutter逐渐成为了时下移动开发领域的一个热门话题。我开始探索这个新的开发框架,并迅速被它的快速高效、易于学习所吸引。Flutter允许开发者使用Dart语言编写应用程序,并能够在多个平台上运行,包括iOS和Android。这对于需要跨平台开发的人来说,是一个巨大的优势。通过学习Flutter,我不仅拓宽了自己的技能树,还为未来的发展奠定了更加坚实的基础。

    在多年的开发工作中,我不仅积累了丰富的开发经验,还面临了各种挑战。在这个过程中,我从一个满怀梦想和追求的少年成长为一个中年大叔,在职业道路上走过了漫长的历程。我曾在北京这样的城市追逐梦想,也曾为了安逸的生活而选择了哈尔滨这样的城市。我的职业生涯也因此经历了起起伏伏,有过了许多成就和贡献,也面临过中年危机和职业瓶颈。

然而,尽管我的职业生涯充满了挑战和不确定性,我始终坚信技术发展带来的机遇和变革。我相信,只要我们保持学习、适应和发展的态度,不断面对挑战并克服困难,我们终将在程序人生的道路上找到自己的位置,实现自己的价值。

    未来,我希望继续探索新的技术和框架,不断拓宽自己的技能树和学习领域。我相信,只有不断尝试新事物、不断挑战自己,才能在程序人生的道路上走得更远、更高。同时,我也希望把自己的经验和所学所悟传递给更多有志于从事程序员这个行业的年轻人,帮助他们少走弯路,实现自己的梦想。

    场面话说完了,说一下这几年要感谢的人吧。 QQ好友洪江Objective-C学习道路上的伙伴,白嫖了他一套iOS开发的课程,经常一起半夜互相改代码的兄弟,QQ好友阿林同学97年的开发者,白嫖一套游戏源码。最最需要感谢是我死皮赖脸认的师傅,凌晨三点起来给我改代码被媳妇大骂的声音至今还在我的脑海中。还有就是自己了。没想到上课坐不住板凳的自己,遇到代码后我可以变得这样的安静。

    我的程序人生,充满了挑战和机遇,也充满了不确定性和可能性。我期待着在这个道路上继续前行,不断探索、学习和成长,迎接未来的挑战和机遇。

本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026”

收起阅读 »

《环信十周年趴——程序之路也有得失,不必介怀》

我的程序生涯可谓是充满了曲折和成长的旅程。从一开始的业余爱好,到如今的职业发展,我经历了许多挑战和机遇,也积累了不少宝贵的经验。回顾起来,我最初接触编程是在大学期间。当时,我被计算机的神秘和无限可能性所吸引,开始学习编写简单的代码。起初,我对编程还不太熟悉,但...
继续阅读 »


我的程序生涯可谓是充满了曲折和成长的旅程。从一开始的业余爱好,到如今的职业发展,我经历了许多挑战和机遇,也积累了不少宝贵的经验。

回顾起来,我最初接触编程是在大学期间。当时,我被计算机的神秘和无限可能性所吸引,开始学习编写简单的代码。起初,我对编程还不太熟悉,但是通过不断的学习和实践,我渐渐掌握了一些基本的编程语言和技巧。

毕业后,我决定将编程作为我的职业。我投身于软件开发行业,从一名初级程序员开始。在职场中,我面对了各种项目和团队合作的挑战。通过不断学习和与同事的交流,我的编程能力得到了提升,我也逐渐熟悉了软件开发的流程和方法。

随着时间的推移,我逐渐担任更高级的职位,并开始负责一些重要项目的开发。同时,我也积极追求自我提升,不断学习新的编程技术和工具。我学习了机器学习和数据科学的知识,掌握了一些流行的开发框架和库。这些新技能不仅提升了我的职业竞争力,还让我能够解决更加复杂的问题。

在职场上,我也遇到了一些奇葩和不愉快的经历。有一次,我加入了一家初创公司,他们开发了一款虚拟现实游戏。我被聘为首席程序员,负责游戏的核心功能开发。开始时,我对这个机会充满了期待,希望能够在这个新兴行业有所突破。

然而,不久之后,我发现这家公司的管理层存在着一些奇葩的决策和不合理的要求。他们对于开发进度的期望过高,要求我们在短时间内完成大量的工作。同时,他们也没有给予足够的资源和支持,导致我们在技术上遇到了很多困难。

更糟糕的是,公司的管理层对于员工的待遇也非常吝啬。工资低于行业平均水平,福利待遇简直可以说是微乎其微。而且,他们还经常加班,但却不提供加班补偿。这让我感到非常不满和失望。

在与同事的交流中,我发现大家都对公司的管理方式感到不满。许多人都在考虑离职,寻找更好的机会。尽管我对这个项目充满了热情,但最终我还是做出了离职的决定。

离开这家公司后,我感到一种解脱和自由。我决定重新评估自己的职业规划,并寻找更好的工作环境。我参加了一些技术研讨会和行业活动,扩展了人脉和知识面。

通过努力和坚持,我最终找到了一家知名游戏开发公司的工作机会。这家公司有着良好的声誉和优秀的团队氛围。在这里,我得到了更好的薪资待遇和职业发展机会。与同事们的合作也非常愉快,他们互相支持和激励,共同追求技术的进步和项目的成功。

在新的公司,我不仅继续提升自己的技术能力,还积极参与项目的管理和领导工作。我逐渐晋升为高级程序员,并负责指导和培养新人。我享受着这种成长和发展的过程,同时也在职业道路上收获了不断增长的薪资。

除了职业发展,我还热衷于扩展自己的知识领域。我广泛阅读与编程相关的书籍,不仅加深了对技术的理解,还开拓了思维的广度。这些书籍不仅拓宽了我的知识面,也为我在工作中遇到的问题提供了解决思路。

在编程道路上,我结识了许多优秀的同行和导师。他们在我职业发展中起到了关键的作用。他们与我分享自己的经验和知识,给予了我很多指导和支持。有时候,在解决问题的过程中,他们的帮助让我事半功倍。

总结而言,我的程序生涯经历了起伏和挑战,但也收获了许多成长和成功。通过不断学习和努力,我掌握了新的技能,取得了薪资的增长,结识了良师益友。我学会了在职场中勇敢面对困难,果断做出改变并寻找更好的机会。这些经历让我明白了职业选择的重要性,一个良好的工作环境和管理团队对于个人的成长和幸福感至关重要。

在我的职业规划中,我也意识到了不断学习和适应新技术的重要性。随着科技的迅猛发展,编程领域也在不断演进。我持续关注行业的趋势,并主动学习新的编程语言、框架和工具。这使我能够跟上潮流,提升自己的竞争力,并为公司的发展做出贡献。

此外,我也始终注重个人的硬件装备。一台高效的电脑和适合编程需求的工具是提高工作效率的关键。我不断更新我的硬件设备,并保持其良好状态,以确保在工作中能够高效地完成任务。

在这个职业生涯中,我经历了职场的起伏和挑战,但我始终坚持不懈地追求自己的梦想和目标。通过遇到的困难和不愉快的经历,我学会了坚持和勇敢面对挑战,也学会了在逆境中寻找机会和改变。

通过不断学习、拓展技能、寻找良师益友和适应职业发展的机会,我在程序生涯中取得了成长和进步。我不仅拥有了稳定的职业发展和增长的薪资,还培养了自己的领导能力和团队合作精神。

总的来说,程序生涯是一段充满挑战和机遇的旅程。通过坚持不懈的努力和持续学习,我在职业道路上取得了一定的成就。我相信,只要保持对技术的热情和对自我提升的追求,我将继续在编程的世界中不断成长和创造出更多的价值。

自己总结了一句话。

对于命运,不必抱怨什么。因为,你就是你的上帝。

                                                              ---- 致自己

收起阅读 »

环信十周年趴——《我的程序人生》

在我的程序生涯中,我深刻体会到了编程是一场修行。这场修行中有喜悦、有痛苦、有枯燥,但最终会有收获。 我曾经是一个对编程一无所知的菜鸟,每天只是机械地敲着键盘,重复着Ctrl+C和Ctrl+V的操作。但是随着时间的推移,我开始对编程产生了兴趣,并开始了自己的修...
继续阅读 »

在我的程序生涯中,我深刻体会到了编程是一场修行。这场修行中有喜悦、有痛苦、有枯燥,但最终会有收获。


我曾经是一个对编程一无所知的菜鸟,每天只是机械地敲着键盘,重复着Ctrl+C和Ctrl+V的操作。但是随着时间的推移,我开始对编程产生了兴趣,并开始了自己的修行之路。
我通过阅读大量的编程书籍、观看视频和参加线上课程来不断提高自己的技能。我还加入了一些编程社区,和其他程序员分享经验和技巧,并不断地学习和探索新的编程技术和工具。


在这个过程中,我遇到了很多挑战和困难。有时候我会遇到难以解决的问题,或者是代码出现了错误,但是我从来没有放弃过。我不断地调试、修改和重构代码,直到找到解决问题的方法。这个过程让我深刻理解到了编程的本质,即通过不断地试错和调试来完善代码。
随着我的技能不断提高,我也开始在工作中发挥更大的作用。我可以更快速地开发出高质量的代码,并能够帮助团队解决一些复杂的问题。我也开始在各种编程竞赛中获奖,这让我更加有信心和动力去追求更高的目标。


现在,我已经成为了一名经验丰富的程序员,并且深深地热爱着编程这个行业。我相信,只要不断地学习和探索,坚持自己的修行之路,就一定能够在这条路上不断前进,并取得更大的成就。


在我的程序生涯中,我也深刻地体会到了团队合作的重要性。在开发一个项目时,需要与其他成员进行紧密的合作,共同解决遇到的问题。这需要我们有良好的沟通能力和协作精神,能够理解并尊重其他人的观点和意见。正是这种团队合作的精神,让我们能够更高效地完成任务,并取得更好的成果。
总之,编程是一项充
满挑战和机遇的工作,让我们不断地成长和进步。通过不断地学习和探索,秉持着精益求精的态度,我们可以在这条修行之路上不断前进,并取得更大的成就。

在我的程序生涯中,我经历了很多挑战,但也有很多收获和成就。我深深地热爱着编程这个行业,并会继续坚持我的修行之路,不断地提高自己的技能和素质,成为一名更加优秀和出色的程序员。


最后,作为一名环信的老用户,我希望能够在未来的日子里,继续见证环信的成长和壮大。环信是一款优秀的产品,它让人们能够更加方便和快捷地进行交流和协作,这是一项非常有意义和有价值的事情。我会继续支持和使用环信,为它的发展贡献自己的力量。

总之,编程是一项非常有趣和富有挑战性的工作。在这个过程中,我们不仅能够不断地学习和进步,还能够结识到许多志同道合的朋友。如果你也对编程感兴趣,并愿意付出努力和时间,那么欢迎加入到我们的编程修行之路中来。
在环信十周年之际,我衷心地祝愿环信能够继续壮大和发展,为更多的人提供优秀的产品和服务。同时,我也希望自己能够继续在这条修行之路上前进,不断地取得更大的成就和收获。
收起阅读 »

环信十周年趴——我的程序人生

程序人生是一段充满挑战、成长与收获的旅程。它可以让你在无限创意的空间中实现自己的天马行空的想象,也可以带给你沉淀思考、坚毅追求的人生智慧。以下是我过去几年在程序开发和编程学习中所获得的心得和体悟,希望能对正在学习编程或准备进入编程领域的朋友们提供一些借鉴和启示...
继续阅读 »

程序人生是一段充满挑战、成长与收获的旅程。它可以让你在无限创意的空间中实现自己的天马行空的想象,也可以带给你沉淀思考、坚毅追求的人生智慧。以下是我过去几年在程序开发和编程学习中所获得的心得和体悟,希望能对正在学习编程或准备进入编程领域的朋友们提供一些借鉴和启示。

首先,我的编程之路始于兴趣。在大学期间,由于对计算机技术的好奇和热爱,我开始接触编程,学习了C语言和Java等编程语言。那时候,我并没有特别强烈的目标和压力,只是因为喜欢而去学习。但是很快,我就发现编程的魅力所在:在计算机的世界中,我可以实现自己的创意和想象,探索更多未知的领域,并且不断挑战自己的能力极限。

当我逐渐深入学习编程时,我开始体会到编程所带来的快感和成就感。每当我发现一个Bug,并最终成功解决时,就有一种说不出的成就感和满足感。这种满足感不仅来自于程序运行正常,也来自于我自己的成功,对自己能力的证明。这也让我更加爱上了编程,开始不断地探索更多新技术,学习更多新知识。

但是,学习编程并不一定就是一帆风顺的。在这条通往成功的路上,不可避免地会遇到各种困难和挑战。在我的编程之路中,我也遇到过很多错综复杂的问题。有时候会卡在一个很小的细节上,一遍遍地调试改错,却迟迟无法找到问题所在;有时候会觉得编程技能难以提高,感觉每个任务都前途渺茫;有时候会疲于奔命地赶着截止日期,在压力和时间的双重压力下,一点点地完善项目。但这些困难并没有阻挡我的步伐,反而让我更加坚定了自己的信念。

在这个过程中,最重要的就是要保持坚持和耐心。无论遇到怎样的困难,都要尝试解决,尽可能的学习和掌握更多的技能和知识。有时候,我会在论坛或社区中与其他程序员交流,分享自己的问题和经验。还有时候,我会利用一些资源,如各种教程、网站和视频等,来不断提升自己的编程技能。与此同时,我还不断地进行探索和实践,开发个人项目和实验,以此来不断深入学习。这种“付出比收获多”的过程有时候会让人感到压抑,但有时间积累,功夫不负有心人,最终一定会有收获。

随着自己的不断努力和学习,我逐渐摸索出了自己所喜欢编程的领域和方向。我喜欢从事物的本质和内在逻辑入手,研究算法和数据结构的奥秘、探索计算机科学的理论界限。同时,我也很热衷于与其他领域的知识结合,如人工智能、机器学习、大数据分析等,以此来开发和创新更加复杂的系统和应用。这种对于理论和实践的结合,也是我平衡好奇心和实用性的秘诀。通过不断挑战自我而达到不断进步,从而实现个人成长和价值的提升。


在实践中,我深刻理解编程旨在解决问题的本质,更加关注代码的质量和可维护性,以此来提高项目的效率和可靠性。我认识到了一个好的程序员并不仅仅是一个语法熟练、技能娴熟的写手,他还要拥有扎实的编程基础、算法功底、代码组织和风格良好的习惯等。同时,编程对于沟通和协作能力的要求也在不断加强。在编程开发中,只有细致发掘需求,灵活妥善的应对变化,与他人协作合作,才能提供出在用户满意的产品和服务。通过以上不同方面的提升和努力,我的程序人生也不断拓展、深入,从基础语言开始,逐渐实现提炼程序本质、解决实际问题的能力。

此外,编程与人生的有趣之处还在于,无处不在并随时随地可见。尽管不是所有人都需要成为程序员,但是对于了解和掌握最基本编程思维和方法,对于从事现代社会的工作、学习和生活都是有益的。无论是自动驾驶、智能家居、科技创新,还是文化艺术、社交娱乐等等,都需要程序员来提供必要的软硬件支撑。随着新技术的崛起和应用领域的不断扩大,未来的程序人生也将更加丰富,更加精彩。在这样一个变化万千的时代,学习编程不仅仅是为了获得一份职业,它也是一种与时代同步的生活方式,一种新世界的探索和发现,更是充实自己生活的一种方式。

总之,我的程序人生虽然经历了困难和挑战,但只要坚信不懈,不断学习、改进,就能够获得切实的进步和收获。编程不仅仅是一种技术,也是一种态度,一种不断探索和改进的生活方式。在此,我希望有更多的人能够加入到编程的行列中,体验到程序人生带来的乐趣和挑战,用技术的力量来改变未来。


本文参与环信十周年活动

收起阅读 »

Android深思如何防止快速点击

前言 其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。 1. AOP 可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟...
继续阅读 »

前言


其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。


1. AOP


可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟框架。


AOP来解决这类问题其实是近些年一个比较好的思路,包括比如像数据打点,通过AOP去处理,也能得到一个比较优雅的效果。牛逼的人甚至可以不用别人写的框架,自己去封装就行,我因为对这个技术栈不熟,这里就不献丑了。

总之,如果你想快速又简单的处理这种问题,AOP是一个很好的方案


2. kotlin


使用kotlin的朋友有福了,kotlin中有个概念是扩展函数,使用扩展函数去封装放快速点击的操作逻辑,也能很快的实现这个效果。它的好处就是突出两个字“方便”


那是不是我用java,不用kotlin就实现不了kotlin这个扩展函数的效果?当然不是了。这让我想到一件事,我也有去看这类问题的文章,看看有没有哪个大神有比较好的思路,然后我注意到有人就说用扩展函数就行,不用这么麻烦。


OK,那扩展函数是什么?它的原理是什么?不就是静态类去套一层吗?那用java当然能实现,为什么别人用java去封装这套逻辑就是麻烦呢?代码不都是一样,只不过kotlin帮你做了而已。所以我觉得kotlin的扩展函数效果是方便,但从整体的解决思路上看,缺少点优雅。


3. 流


简单来说也有很多人用了Rxjava或者kotlin的flow去实现,像这种实现也就是能方便而已,在底层上并没有什么实质性的突破,所以就不多说了,说白了就是和上面一样。


4. 通过拦截


因为上面已经说了kt的情况,所以接下来的相关代码都会用java来实现。

通过拦截来达到防止快速点击的效果,而拦截我想到有2种方式,第一种是拦截事件,就是基于事件分发机制去实现,第二种是拦截方法。

相对而言,其实我觉得拦截方法会更加安全,举个场景,假如你有个页面,然后页面正在到计算,到计算完之后会显示一个按钮,点击后弹出一个对话框。然后过了许久,改需求了,改成到计算完之后自动弹出对话框。但是你之前的点击按钮弹出对话框的操作还需要保留。那就会有可能因为某些操作导致到计算完的一瞬间先显示按钮,这时你以迅雷不及掩耳的速度点它,那就弹出两次对话框。


(1)拦截事件


其实就是给事件加个判断,判断两次点击的时间如果在某个范围就不触发,这可能是大部分人会用的方式。


正常情况下我们是无法去入侵事件分发机制的,只能使用它提供的方法去操作,比如我们没办法在外部影响dispatchTouchEvent这些方法。当然不正常的情况下也许可以,你可以尝试往hook的方向去思考能不能实现,我这边就不思考这种情况了。

public class FastClickHelper {

private static long beforeTime = 0;
private static Map<View, View.OnClickListener> map = new HashMap<>();

public static void setOnClickListener(View view, View.OnClickListener onClickListener) {
map.put(view, onClickListener);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long clickTime = SystemClock.elapsedRealtime();
if (beforeTime != 0 && clickTime - beforeTime < 1000) {
return;
}
beforeTime = clickTime;

View.OnClickListener relListener = map.get(v);
if (relListener != null) {
relListener.onClick(v);
}
}
});
}

}

简单来写就是这样,其实这个就和上面说的kt的扩展函数差不多。调用的时候就

FastClickHelper.setOnClickListener(view, this);

但是能看出这个只是针对单个view去配置,如果我们想其实页面所有view都要放快速点击,只不过某个view需要快速点击,比如抢东西类型的,那肯定不能防。所以给每个view单独去配置就很麻烦,没关系,我们可以优化一下

public class FastClickHelper {

private Map<View, Integer> map;
private HandlerThread mThread;

public void init(ViewGroup viewGroup) {
map = new ConcurrentHashMap<>();
initThread();
loopAddView(viewGroup);

for (View v : map.keySet()) {
v.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int state = map.get(v);
if (state == 1) {
return true;
} else {
map.put(v, 1);
block(v);
}
}
return false;
}
});
}
}

private void initThread() {
mThread = new HandlerThread("LAZY_CLOCK");
mThread.start();
}

private void block(View v) {
// 切条线程处理
Handler handler = new Handler(mThread.getLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (map != null) {
map.put(v, 0);
}
}
}, 1000);
}

private void exclude(View... views) {
for (View view : views) {
map.remove(view);
}
}

private void loopAddView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) viewGroup.getChildAt(i);
map.put(vg, 0);
loopAddView(vg);
} else {
map.put(viewGroup.getChildAt(i), 0);
}
}
}

public void onDestroy() {
try {
map.clear();
map = null;
mThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}

}

我把viewgroup当成入参,然后给它的所有子view都设置,因为onclicklistener比较常用,所以改成了设置setOnTouchListener,当然外部如果给view设置了setOnTouchListener去覆盖我这的set,那就只能自己做特殊处理了。


在外部直接调用

FastClickHelper fastClickHelper = new FastClickHelper();
fastClickHelper.init((ViewGroup) getWindow().getDecorView());

如果要想让某个view不要限制快速点击的话,就调用exclude方法。这里要注意使用完之后释放资源,要调用onDestroy方法释放资源。


关于这个部分的思考,其实上面的大家都会,也基本是这样去限制,但是就是即便我用第二种代码,也要每个页面都调用一次,而且看起来,多少差点优雅。


首先我想的办法是在事件分发下发的过程去做处理,就是在viewgroup的dispatchTouchEvent或者onInterceptTouchEvent这类方法里面,但是我简单看了源码是没有提供方法出来的,也没有比较好去hook的地方,所以只能暂时放弃思考在这个下发流程去做手脚。


补充一下,如果你是自定义view,那肯定不会烦恼这个问题,但是你总不能所有的view都做成自定义的吧。


其次我想怎么能通过不写逻辑代码能实现这个效果,但总觉得这个方向不就是AOP吗,或者不是通过开发层面,在开发结束后想办法去注入字节码等操作,我觉得要往这个方向思考的话,最终的实现肯定不是代码层面去实现的。


(2)拦截方法


上面也说了,相对于拦截事件,假设如果都能实现的情况下,我更倾向于去拦截方法。


因为从这层面上来说,如果实现拦截方法,或者说能实现中断方法,那就不只是能做到防快速点击,而是能给方法去定制相对应的规则,比如某个方法在1秒的间隔内只能调用一次,这个就是防快速点击的效果嘛,比如某个方法我限制只能调一次,如果能实现,我就不用再额外写判断这个方法调用一次过后我设置一个布尔类型,然后下次调用再判断这个布尔类型来决定是否调用,


那现在是没办法实现拦截方法吗?当然有办法,只不过会十分的不优雅,比如一个方法是这样的。

public void fun(){
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}

那我可以封装一个类,里面去封装一些策略,然后根据策略再去决定方法要不要执行这些步骤,那可能就会写成

public void fun(){
new FunctionStrategy(FunctionStrategy.ONLY_ONE, new CallBack{
@Override
public void onAction() {
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}
})
}

这样就实现了,比如只调用一次,具体的只调用一次的逻辑就写在FunctionStrategy里面,然后第2次,第n次就不会回调。当然我这是随便乱下来表达这个思路,现实肯定不能这样写。首先这样写就很不优雅,其次也会存在很多问题,扩展性也很差。


那在代码层面还有其它办法拦截或者中断方法吗,在代码层还真有办法中断方法,没错,那就是抛异常,但是话说回来,你也不可能在每个地方都try-catch吧,不切实际。


目前对拦截方法或者中断方法,我是没想到什么好的思路了,但是我觉得如果能实现,对防止快速点击来说,肯定会是一个很好的方案。


作者:流浪汉kylin
链接:https://juejin.cn/post/7197337416096055351
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-Deeplink跳转失败问题修复

Android Deeplink实现 在Android中,Deeplnk通过声明Activity的intent-filter来实现对自定义url访问事件的捕捉。在有道背单词的项目中,我们需要通过前端分享词单的方式,将词单分享给别人,并通过点击前端页面收藏按钮,...
继续阅读 »

Android Deeplink实现


在Android中,Deeplnk通过声明Activity的intent-filter来实现对自定义url访问事件的捕捉。在有道背单词的项目中,我们需要通过前端分享词单的方式,将词单分享给别人,并通过点击前端页面收藏按钮,实现调起客户端收藏词单的功能。

从前端通过自定义url的方式调起客户端这个功能原来一直都没有什么问题,直到最近有部分用户反馈在某些浏览器下无法调起。下面我们来看一下分析查找问题的方法以及如何解决。
转载请注明来源「Bug总柴」


检查客户端deeplink配置


在AndroidManifest.xml文件中,对路由Activity配置如下:

<activity
android:name=".deeplink.RouterActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:theme="@style/Theme.Translucent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="youdao.com"
android:scheme="recite"
android:pathPattern=".*"/>
</intent-filter>

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".home.ui.MainActivity" />
</activity>

里面比较重要的部分是intent-filter中的data配置,检查后发现配置正常,可以正常拦截到 recite://youdao.com/.*的所有请求。

转到RouterActivity通过断点调试,发现并没有到达。从而可以确认是浏览器调起的时候发生了异常。


tips: adb 命令同样可以启动deeplink进行测试
adb_test.png


分析浏览器对deeplink处理


通过用户反馈,主要集中是在UC和华为自带的浏览器点击前端页面的【收藏词单】无法调起有道背单词

同时我们在chrome上面发现通过deeplink只有第一次会跳转到应用,往后几次都是没有任何相应,确实有点百思不得其解。

经过查找资料,发现了chrome的一个对Android Intent处理的介绍

Android Intents with Chrome

里面提到



One scenario is launching an app when the user lands on a page, which you can achieve by embedding an iframe in the page with a custom URI-scheme set as the src, as follows: . This works in the Chrome for Android browser, version 18 and earlier. It also works in the Android browser, of course.




The functionality has changed slightly in Chrome for Android, versions 25 and later. It is no longer possible to launch an Android app by setting an iframe's src attribute. For example, navigating an iframe to a URI with a custom scheme such as paulsawesomeapp:// will not work even if the user has the appropriate app installed. Instead, you should implement a user gesture to launch the app via a custom scheme, or use the “intent:” syntax described in this article.



翻译一下,大概的意思就是之前通过没有用户主动操作就打开app的行为在chrome25版本及之后会被禁止。开发者必须通过用户操作来触发跳转应用的行为。目前chrome的版本都已经68了,证明这个规则已经由来已久。抱着试试看的姿态,开始查找是否是前端的代码有问题。
通过chrome inspect,捕捉到前端代码果然有一处疑似iframe的使用
ebc8daf14130474bbd69103bf4e6ff5d_ac466235c97c6e9fb45c8820addbce1d.jpg


018ff33c02e047c59196534a3a28ef8b_e15f0d6e314113b2260119e917400191.jpg


随后经过对前端代码debug,果然有走了这段逻辑
ca286076d8594b55b7db5847e6d031b6_c0169d9d3acbb13b8835c5a0a8aa028f.jpg


证据确凿,可以找前端大神反馈了。经过了解,确实是之前有改动过这部分的代码,使用了iframe来处理deeplink的打开。处理的办法也相对简单,将iframe换成href来做跳转处理就可以了。


测试


最后我们对国内的浏览器试了一下deeplink是否生效


UC浏览器


会弹出一个应用打开提醒,如果用户本次没有【允许】操作,则浏览器下次会拦截打开应用行为,没有任何提醒,不知道这是一个bug还是故意为之。点击【允许】后可以跳转应用
Screenshot_20180818-112921.jpg


QQ浏览器


同样会弹出应用打开题型,如果用户本次没有【打开】,下次用户操作还是会继续提醒。点击【打开】后可以跳转应用
Screenshot_20180818-113231.jpg


360浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113459.jpg


猎豹浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113718.jpg


一加系统默认浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113921.jpg


搜狗浏览器


没有提醒,直接跳转到app


chrome


行为与搜狗浏览器类似,没有提醒,直接跳转app


测试结果除了UC浏览器第一次不点击跳转之后会跳转不了之外,
其他浏览器跳转app问题得到解决。


结语


通过这次查deeplink跳转的问题,收获了两点知识。



  • 一个是前端使用iframe来处理deeplink跳转会有问题

  • 二个是除了采用
"scheme://host/path"

这种deeplink方式之外,还可以采用

"intent://about/#Intent;action=[string];scheme=[string];package=[string];S.browser_fallback_url=[encoded_full_url];end"

的方式来触发应用intent的请求访问。


同时,在处理deeplink的规则里面,体会到了一条原则:



  • 最短路径处理原则


意思就是刚开始的时候,deeplink处理的逻辑要从根目录开始进行。比如有一个收藏词单的需求,没有使用最短路径原则可能会设计成这样



recite://youdao.com/bookId?&action=collect



对应的处理是如果action为collect就收藏词单。这个时候需求如果改成默认进来不需要收藏就非常尴尬了。因为对于旧版本而已,只认有action=collect才会处理,那就意味这如果想对默认的recite://youdao.com/bookId只是查看不收藏的需求,对于旧版本就没办法实现,会出现兼容性问题。

而最短路径处理原则,意思就是在开始的时候,尽量对最短的路径行为进行处理,具体到上面的例子,对于收藏某个词单的需求,我们可以设计deeplink为



recite://youdao.com/bookId?&action=collect



然后我们对 recite://youdao.com/bookId以及recite://youdao.com/bookId?&action=collect 都处理成收藏词单。上线之后,如果想修改默认参数行为,就可以直接改对 recite://youdao.com/bookId 的处理,这样对于旧版本仍然是可以执行的收藏行为,对于新版本就可以对应新的逻辑


作者:申国骏
链接:https://juejin.cn/post/7236924717310771255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一点点编译优化

正文 经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。 FileWalker 剪枝 + 多线程优化 我们工程内会在编译和同步阶段首先获取到整个工程的模...
继续阅读 »

正文


经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。


FileWalker 剪枝 + 多线程优化


我们工程内会在编译和同步阶段首先获取到整个工程的模型,之后计算出每个模块的version版本。之前我们通过java.nio.file.FileVisitor来进行工程文件遍历的操作。同样文件展开的api也是可以进行剪枝的,但是由于是用groovy写的,我还是不太喜欢。


本次优化我们采用了kotlin的file相关的walkTopDown(可以快速的从上到下的遍历一个文件树)语法糖。然后通过其中的onEnterdsl进行剪枝,然后我们可以通过filter进行第二波过滤,筛选出我们实际要访问的目录。最后再进行一次foreach。

fun File.walkFileTree(action: (File) -> Unit) {
walkTopDown().onEnter {
# 对于不需要的目录进行剪枝
val value = if (!(it.isDirectory && (it.name == "build" || it.name == "src"))) {
!it.isHidden
} else {
false
}
value
}.filter {
val value = if (this == it) {
false
} else {
if (it.isDirectory) {
val build = File(it, "build.gradle")
build.exists()
} else {
false
}
}
value
}.forEachIndexed { _, file ->
action.invoke(file)
}
}


优化效果如下,原本没有剪枝的版本,我们本机进行一次FileWalker需要1分钟左右的时间。使用剪枝的版本之后,我们可以把时间优化到2s左右的时间。我们跳过了些什么?































格式是否跳过
隐藏文件夹
build
src
文件
其他文件夹

通过上述的剪枝,我们可以减少非常非常多的文件遍历操作,在完成同样的能力的情况下,可以大大的加快文件的遍历操作。


另外,我们会获取对应文件下的git commit sha值,然后作为该模块的version版本,而这个操作也是有几百毫秒的耗时,而我们工程大概有800+这样的模块,所以如果按照同步的方式去执行,就会变得耗时了。


而优化方案就比较简单了,我们通过线程池提供的invokeAll方法,并发执行完之后再继续向下执行就可以完成该优化了。

        Executors.newCachedThreadPool().invokeAll(callableList)

整体优化下来,原来在CI上一次FileWalker需要1mins,因为CI的机器足够牛逼,所以优化完仅仅只需要3.5s就可以完成整个工程的遍历以及获取对应git commit sha值的操作。


修改前:


企业微信截图_9431b7d7-1599-4f3f-a916-276cde61f71d.png


修改后:


企业微信截图_9127d691-a792-41e2-bc38-62f08697ea64.png


skip 一些非必须task


在AGP的打包流程中,会插入很多预检查的任务,比如类似kotlin版本检查, compileSdk版本检查等等任务。而这些任务即时不执行,也并不会影响本次打包任务,还是可以打出对应的apk产物的。当然前提是编译没有啥问题的情况下。


我们仔细观察了下apk打包下的所有的task,并观察了下task任务耗时情况,找到了几个看起来并没有什么实际用处的任务。


企业微信截图_912eebd8-2104-4a10-bb5a-6d60637e922f.png


接下来就是如何去关闭这个任务了,其实方式还是比较简单的。我们主要用字符串的形式去获取到这个Task,然后把enable设置成false就可以了。但是前提是这个Task的输出并不会影响到后续的Task就行了。另外这几个应该是高apg新增的task,7.x才出现的低版本的是没有的。

afterEvaluate {
def metaDebugTask = tasks.findByName("checkApinkDebugAarMetadata")
def debugCheck = System.getenv().containsKey("ENABLE_APINK_DEBUG_CHECK")
if( metaDebugTask != null && !debugCheck) {
metaDebugTask.setEnabled(false)
}
def preCheck = tasks.findByName("checkApinkDebugDuplicateClasses")
if( preCheck != null && !debugCheck) {
preCheck.setEnabled(false)
}
}

企业微信截图_99e7c774-4e22-40be-bbac-73080faa80c0.png


这样我们就可以在一个构建中优化掉大概1min30s的时间了。这些手段相对来说都比较简单,另外我们还可以考虑把一些不互相依赖的任务从线性执行变成并行执行。可以参考gradle的Worker相关api。



worker_api



二进制产物发布


原始的baseversion是基于文件内容的md5 生成的一个全局统一版本号,然后再结合仓库内的gitsha生成二进制缓存。但是由于大仓内的代码量越来越大,所以一旦变更baseversion,需要消耗大概80min左右的时间重新生成所有的二进制缓存。


虽然但是,其实并没有这个必要全部模块都进行一次发布。方法签名出现问题,我们只需要让对应的模块重编即可。所以这种全局性的baseversion 就需要进行迭代了。需要让baseversion被拆解成多个,然后进行混合生成一个新值,底层改动可以影响到上层,而最上层模块也可以具备独立更新的能力。

# 后续该文件改动不会导致整个baseVersion变更
# 基于文件路径匹配的规则,可以给每个文件路径设置一个version,但是由于工程之间存在依赖,所以可以合并多个baseVersion到一起
# 测试结果如下,framework变更80min comm 变更 50min app上变更10min
# 后续会配合a8检查,直接通知各位那些模块的方法签名检查不通过,之后直接修改局部version版本就好了


# 全局默认混入所有 如果非必要情况下 别改这个版本号 !!!!!!!
- name: global
path:
version: 1
# app目录缓存

- name: app
path: app
version: 1
mixin: [ framework,common ]
# framework基础,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: framework
path: framework
version: 2
mixin:
# comm 模块,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: common
path: common
version: 2
mixin: [ framework ]

通过定义出一个新的yaml文件,我们可以定义出namepath代表相对路径,version表示版本号,mixin代表混合入别的name,而相对底层改动情况下会影响到上层模块重编。


这样,我们就可以在app下进行独立的版本号增加,让app目录下的模块进行一次重编,从而解决一部分方法签名问题导致的上层库缓存刷新。


测试结果大概如下:























目录耗时情况
global80min
common50min
app10min

结尾


以下仅代表个人看法哦,我觉得编译优化还是要从本工程实际情况出发,你的工具箱内的工具要足够多,要先知道哪些东西是慢的你才会有思考的去进行一些优化,而不是很盲目的进行尝试。


另外优化不应该破坏整个工程的情况,我们不应该魔改整个编译流程,最好就是通过最小的手段去进行一些微量的优化,小步慢跑的进行一些对应的优化。


作者:究极逮虾户
链接:https://juejin.cn/post/7217750296172429371
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我就问Zygote进程,你到底都干了啥

ZYGOTE 前言 OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote。 提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了...
继续阅读 »

ZYGOTE


前言


OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote
提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了什么呢。
还是老规矩,让我们抱着几个问题来看文章。最后在结尾,再对问题进行思考回复。



  1. 你能大概描述一遍Zygote的启动流程吗

  2. 我们为什么可以执行java代码,和zygote有什么关系

  3. Zygote到底都做了哪些事情


另外,我最近也看了一些写底层的文章。要么就是言简意赅到只知道Zygote的作用,但是完全不知道如何实现的,就像是背作文一样。要么是全篇都是代码,又臭又长,让人完全没有看下去的动力。


不过作为一个读者,太长的我可能完全不想看,太短的又真的完全就是被课文一点都不明白原理。


因此,本篇文章,仍旧会引入一些代码,方便大家通过代码方便记忆。但是又会将代码进行精简,以免给大家造成过度疲劳。我们的目的都是希望用最少的时间,能掌握更多的知识。


读源码时:不要过于纠结细节,不要过于纠结细节,不要过于纠结细节!重要的事情说三遍!!!


OK,让我们进入正题瞅瞅Zygote到底是个什么东西。




1.C++还是Java


选这个当标题当然是有原因的。我们知道的是Android系统启动的时候,运行的是Linux内核,执行的是C++代码。这是一个很有趣的事情。


因为我们在写App的时候,AndroidStudio默认给我们创建的都是Activity.java,而选择的语言,要么是Java要么就是Kotlin


启动时候运行的是C++代码,应用层却可以使用Java代码,这到底是为什么呢?其实这就是Zygote的功劳之一


2.Native层


Init进程创建Zygote时,会调用app_main.cpp的main() 方法。此时它依旧运行的是C++代码。我们通过下面的代码,来看下它到底做了什么。


这里它做的最关键的一件事就是启动AndroidRuntime(Android运行时)。另外这里需要注意的是 start方法 中的 "com.android.internal.os.ZygoteInit" 这个类似于全类名。他到底是做什么的?别着急,大概2分钟后,你可能就会得到答案。


Zygote的新手村:app_main.cpp

int main(int argc, char* const argv[])
{
// 创建Android运行时对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 代码省略...

// 调用AppRuntime.start方法,
// 而AppRuntime是AndroidRuntime的子类,并且没有重写start方法
// 因此调用的是AndroidRuntime的start方法
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}

精简后的代码告诉我们,这里一共就做了两件事,第一件创建AppRuntime,第二件调用start方法


不过,AppRuntimeAndroidRuntime的子类,他没有重写start方法,因此这里调用的是 AndroidRuntime的start() 方法。


奇迹的诞生地:AndroidRuntime:


这里我依旧只保留关键代码,源码关键注释也进行了保留。由下方代码看出这里做了三件改变命运的事情。



  1. startVM -- 启动Java虚拟机

  2. startReg -- 注册JNI

  3. 通过JNI调用Java方法,执行com.android.internal.os.ZygoteInit 的 main 方法
/*
* Start the Android runtime.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
}

/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}

有了JVM,注册了JNI,我们就可以执行Java代码了。


这里我个人建议不要过于纠结细节,比如JVM是如何创建的,JNI是如何注册的。如果感兴趣的童鞋,可以下载源码,到app_main.cpp里进行查看。这里就不进行赘述了。否则代码量太大,适得其反。




3.Java层


命运的十字路口:ZygoteInit.java:


之所以说是命运的十字路口,因为Zygote会在这里创建SystemServer,但是二者却走向了截然不同的道路。


从这里就开始执行Java代码了,当然这些Java代码是运行在JVM中的。


让我们通过代码来看一下,到底都做了些什么。

class ZygoteInit{

/**
* This is the entry point for a Zygote process. It creates the Zygote server, loads resources,
* and handles other tasks related to preparing the process for forking into applications.
* This process is started with a nice value of -20 (highest priority).
*/
// 上面是源码中的注释,小伙伴们可以自行翻译一下。
// 创建了ZygoteServer,加载资源,并且介绍了这个进程的优先级是最高的-20.
public static void main(String argv[]) {
ZygoteServer zygoteServer = null;

// 1. 预加载资源,常用的:resource,class,library等在此处进行加载
preload(bootTimingsTraceLog);

// 2. 创建ZygoteServer,实际是一个Socket用来进行跨进程间通信用的。
zygoteServer = new ZygoteServer(isPrimaryZygote);

// 3. fork出SystemServer进程,这个进程会创建AMS,ATMS,WMS,电池服务等一切只有你想不到没有它做不到的服务
forkSystemServer(abiList, zygoteSocketName, zygoteServer);

// 4.里面是一个while(true)循环,等待接收AMS创建进程的消息,类似于handler中的Looper.loop()
zygoteServer.runSelectLoop(abiList);
}
}

上面代码里的注释基本说明了ZygoteInit都干了啥。下面会再稍微总结下:



  1. 创建了ZygoteServer:这是一个Socket相关的服务,目的是进行跨进程通信。

  2. 预加载preload:预加载相关的资源。

  3. 创建SystemServer进程:通过forkSystemServer分裂出了两个进程,一个Zygote进程,一个SystemServer进程。而且由于是分裂的,所以新分裂出来的进程也拥有虚拟机,也能调用JNI,也拥有预加载的资源,也会执行后续的代码。

  4. 执行runSelectLoop():内部是一个while(true)循环,等待AMS创建新的进程的消息。(想想Looper.loop())




4. 戛然而止


没错就是这么突然,Zygote的故事到这就结束了。至于它是如何创建SystemServer,如何去创建App那就是后面的故事了。所以你还记得我的问题嘛?我替你总结一下Zygote到底做了什么:



  1. 创建虚拟机

  2. 注册JNI

  3. 回调Java方法ZygoteInit.java 的main方法,从这儿开始运行Java代码

  4. 创建ZygoteServer,内部包含Socket用于跨进程通信

  5. 预加载class,resource,library等相关资源

  6. fork 出了 SystemServer进程。他俩除了返回的pid不同,剩下一模一样。通过返回值不同来决定剩下的代码如何运行。这个留待后续进行讲解。

  7. 进入while(true)循环等待,等待AMS创建进程的消息(类似于Looper.loop()




5. 多说一句


这个系列才刚刚进行到第二章,我尽量让文章不是特别长的情况下,既有代码整理思路,又有总结方便记忆。代码是源码中摘抄的省略了很大一部分,只能保证执行顺序。有兴趣的小伙伴可以下载源码进行查看。

其实最近我看了很多文章和视频,最后却选择写文章来对知识进行总结。归根结底就是因为一句话,好记性不如烂笔头。要是真的想快速记住,真的需要亲自查看一下源码。翻源码的同时,也是在不知不觉中锻炼你阅读源码的能力。 万一以后让你学习一个新的库,你也知道大概该怎么看。


都已经到这儿了,就稍微厚个脸皮,希望各位看官,能够点个赞加个关注。毕竟一篇文章可能就要耗费几个小时的时间,上一篇文章就几个赞,感谢这几个小伙伴,让我有继续写下去的动力。 真心感谢你们。


另外,文章中有哪里出错,请及时指出。毕竟各位才是真正的大佬。希望各位Androider,可以一起取暖,度过这个互联网寒冬!加油各位!!!




6. Zygote功能图


zygote.png


作者:OpenGL
链接:https://juejin.cn/post/7172878330323173384
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一个28岁程序员入行自述和感受

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。 自我叙述 我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,...
继续阅读 »

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。


自我叙述


我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,我不可能如此普通,自己的一生不应该泯然众生,平凡平庸的度过。尤其是干我们it这一行业的,都有一个自己的程序员梦,梦想着,真的能够用 “代码改变世界”


入行回顾



你们还记得自己是什么时候,入行it行业的吗



我今年已经28岁了,想起来入行,还挺久远的,应该是2016入行的,我也算是半路出家的,中间有过武术梦 歌唱梦 但是电脑什么上学那会就喜欢玩,当然是指游戏,




武术梦




来讲讲我得第一个·梦,武术梦,可能是从小受到武打演员动作电视剧的影响,尤其那个时候,成龙大哥的电影,一直再放,我觉得学武术是很酷的一件事情,尤其那会上小学,还是初中我的体育还是非常好的,


然后我们家那个时候电视还是黑白的,电视机。哈哈哈😀电视台就那么几个,放来放去,有一个台一直重复放成龙电影,还有广告, 都是 学武术就到 xxxx学校, 我被洗脑了吧


于是真的让我爸,打电话质询了一下,可是好像他们这种武术学校都是托管式的,封闭式学习,听说很苦,,,,当然这不是重点,重点每年学费非常的贵,en~,于是乎我的这个梦想终止了,。。




歌唱梦




为啥会有唱歌想法,你猜对了,是被那个时候的好声音给影响了,那个时候好声音是真的很火,看的时候我一度以为我也可以上好声音,去当歌手然后出道,当明星,什么的。


不过不经历打击,怎么会知道自己的下线在哪里呢


我小学换了两到三个学校,到初中,再到高中,你们还记得自己读高中那会吗,高中是有专业选择的,入学军训完以后。


我们代班主任,和我们说有三个专业方向可以选择,艺术类,分美术,和唱歌,然后是文化类,然后艺术类就业考大学分数会低很多,然后一系列原因,哈哈哈,我就选择了歌唱班。


我最好伙伴他选择了,美术类就是素描。这里我挺后悔没有选择 美术类。


到了歌唱班,第一课就是到专业课有钢琴的教室,老是要测试每个同学的,音色和音高,音域
然后各自上台表演自己的拿手的一首歌,。我当时测试时候就是跟着老师的弹的钢琴键瞎唱,


表演的歌曲是张雨生《大海》 也就唱了高潮那么几句。。 😀现在想起来还很羞耻,那是我第一次在那么多人面前唱歌,


后面开始上课老师说我当时分班时候音色什么还不错,但学到后面,我是音准不太行,我发现。再加上我自己的从小感觉好像有点自卑敏感人格,到现在把,我唱歌,就越来越差,


当然我们也有乐理。和钢琴课,我就想主助攻乐理和钢琴,


但是我很天真


乐理很难学习,都是文科知识需要背诵,但是他也要有视唱,也就是唱谱子,duo,re,mi,fa,suo,la,xi,duo。。等,我发现我也学不进去


后面我又开始去学钢琴,但是钢琴好像需要一定童子功,不然可能很难学出来,于是我每天早上6点钟起来,晚上吃完饭就去钢琴教师抢占位置, 还得把门堵着怕人笑话,打扰我,


结果你们也猜到了,音乐方面天赋很重要,然后就是性格上面表演上面,要放得开,可是我第一年勉强撑过去了,后面第二年,专业课越来越多了,我感觉我越来越自卑~,然后成绩就越来越差,老师也就没太重视,嗯~好不容撑到了第二年下半年,放暑假,


但是老师布置任务暑假要自己去外面练钢琴,来了之后要考试,我还花钱去外面上了声乐课钢琴课,哎,我感觉就是浪费钱,,,,,因为没什么效果,性格缺陷加上天赋不行,基本没效果,那段时间我也很痛苦的,因为越来越感觉根本容入不进去班级体,尤其是后面高二,了专业课很多大部分是前面老师带着发生开嗓,后面自由练习,我也不好意思,不想练习,所以
到后面,高二下学习我就转学了,,,,


当然我们班转学的,不止我一个,还有一个转学的 和我一个寝室的,他是因为音高上不去,转到了文科班, 还有一个是挺有天赋,我挺羡慕的,但是人家挺喜欢学习,不喜欢唱歌什么,就申请转到了,文科班。 不过她转到文科班,没多久也不太好,后面好像退学了,,我一直想打听他的消息,都在也没打听到了




玩电脑




我对电脑的组装非常感兴趣,喜欢研究电脑系统怎么装,笔记本拆装,台式机拆装,我会拿我自己的的笔记本来做实验,自己给自己配台式机,自己给自己笔记本增加配置,哈哈哈哈。对这些都爱不释手。



这还是我很早时候,自己一点一点比价,然后去那种太平洋电脑城,电脑一条街,那种地去找人配置的。想想那时候配置这个电脑还挺激动,这是人生的第一台自己全部从零开始组装配的电脑,


本来打算,后面去电脑城上班,开一个笔记本维修,电脑装配的门面的,(因为自己研究了很多笔记本系统,电脑组装),可是好像听电脑城的人说,电脑组装什么的已经不赚钱了,没什么价格利润,都是透明的而且更新迭代非常的快,电脑城这种店铺也越来越少了,都不干了,没有新人再去干这个了,于是乎我的第一份工作失业 半道崩殂了,哈哈哈哈还没有开始就结束了。




学it




后面我又报名自学了,it编程,《xxx鸟》 但是学it我学起来,好像挺快的,挺有感觉的,入学前一个星期,要等班人数到齐才能开班,我们先来的就自己学习打字了,我每天都和寝室人,一起去打字,我感觉那段时间我过得挺开心和充实的,


后面我们觉得自带寝室不好,环境差,于是就几个人一起,搬出去住了,一起学习时候有一个年级26了,我和他关系还蛮好的,不过现在也没什么联系了,,,


学习时候,每次做项目时候我都是组长,那个时候原来是有成就感的,嗯,学习it好像改变了,我学唱歌那个时候,一些自卑性格,可能是遇到了一个好的老师吧


当然后面就顺利毕业,然后找到了工作了,,,


直到现在我还在it行业里


嗯~还想往下面写一点什么,,,下一篇分享一下我入门感受和经历吧


关注公众号,程序员三时 希望给你带来一点启发和帮助


作者:程序员三时
链接:https://juejin.cn/post/7230351646798643255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我的30岁,难且正确的事情是什么?

3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始...
继续阅读 »

3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始就一直留有1年以上的备用金,所以暂时也没太大经济压力,不至于因为囊中羞涩着急找一份谋生的工作。


刚离开公司的前两周,先花了1000多找了两个职业咨询师,了解目前的招聘环境,招聘平台,招聘数据,以及针对性的帮助我修改简历。都准备好以后,开始选公司试投简历,认真看完大部分JD后大概清楚自己的能力所匹配的公司,薪资范围。机会确实不多,移动端管理岗位,架构岗位就更少,尤其是像我这样工作不到10年,架构跟管理经验都还未满5年的人,选择更是寥寥无几。


先后参加了两个2面试,一个是小团队的移动 TL,在了解后双边意向都不大。另一个是 Android 架构方向。虽然拿了offer,薪资包平移,但我最终没去。一是发生了一点小误会,发offer前电话没告诉我职级,我以为架构岗过了其实没有,差一点点到P7。回看面试记录,提升并不困难,有能力冲一冲的,这一次并不会影响我的信心。


另一个则是我真的冷静下来了,也就有了这篇文章。


在这两周里,陆续写了一些文章,做了一些开源项目。完全是出于助人为乐回馈社区,没想到也因此结识了几个做阅读业务的同学,纷纷向我抛来橄榄枝。其中包含一个已经在行业内做到Top3的产品。这让我有些受宠若惊,毕竟我觉得我给的技术方案并非有很大门槛,只是运气好站在巨人的肩膀上想到了不同的方案而已。


正是这些非常正面的反馈,帮助我消化了很大一部分所谓的焦虑(难说我本身一点不受环境影响)。在Zhuang的帮助下,我大概做了两次自我梳理,最后在前几天我从地铁回家大概3km的步行中想明白了很多事情。


每次出去旅游时,比如我躺在草原上,看着日落,说实话我欣赏不了10分钟。因为我的思绪停不下来,我就会去想一些产品或者是管理方面的问题。我很爱工作,或者说喜欢工作时可以反复获取创造性的快乐,比如做出一个新的技术方案或者优化工作流程解决一个团队问题,都让人很兴奋。但现在,我想强迫自己来思考一些更长期的事情。


我的30岁,难而且正确的事情是什么?


是找一份工作吗?这显然不难,作为技术人,找一份薪资合理的工作不难,这恰恰是最容易的


是找一份自己喜欢的工作吗?这有一点难,更多的是需要运气。职业生涯就十几年,有几次选择的机会呢?这更多的是在合理化自己对稳定性,舒适性的追求,掩盖自己对风险的逃避。


是找一个自己喜欢的事情,并以此谋生吗?这很难,比如先找到自己长期喜欢长期坚持投入的事情就很难,再以此谋生就需要更多的运气与常年积累去等待这个运气出现,比如一些up主。这可以是顺其自然的理想,但不适合作为目标。


上面似乎都是一个个的问题,或者说看到这些问题的时候我就看到了自己的天花板了。因为我可以预见我在这些方向上的学习能力,积累速度,成长空间,资源储备。


这半年涌出了太多的新事物,像极了02年前后的互联网,14前后的移动互联网。我从去年12月5日开始使用GPT,帮助我提高工作,学习效率,帮助我做UI设计,帮助我改善代码,甚至帮助我学习开网店时做选品,做策略,可以说他已经完全融入我的工作学习了。


开发自己的GPT应用要仔细阅读OPEN AI 的API,我再次因为英语的理解速度过慢严重影响学习效率,即使是有众多实时翻译软件帮助下丝毫不会有所改善。


翻译必然会对原文做二次加工,翻译的质量也许很高,甚至超过原文,但这样意味着阅读者离原文越远。


比如我在Tandem上教老外“冰雪聪明”这个词的意思,我很难解释给她,更多的是告诉她这个词在什么场景用比较恰当,比“聪明”更高级。但是如果用翻译软件,这个词会变着花样被翻译成“很聪明”,美感全无。


在Tandem跟人瞎聊时以涉及复杂事件就词穷,直到认识了一个 西班牙的 PHD 与另一个 印尼的大学生,她们帮我找到了关键点,基础语法知识不扎实,英语的思维不足。有些时候他们会说我表达的很棒,口语也行,有些时候他们会说我瞎搞。其实很好理解,就像他们学中文一样,入门也不难,难的是随意调动有限的词汇自由组织句子进行表达,而不是脑子里先想一个母语再试着翻译成外语,难的是在陌生场景下做出正确的表达,能用已经学的知识学习新知识,也就是进入用英语学习英语的阶段。


另外一个例子就是做日常技术学习的时候,尤其是阅读源码的时候,往往是不翻译看懂一部分注释,翻译后看懂一部分,两个一结合就半懂不懂,基于这个半懂不懂的理解写大量测试去验证自己的理解,反推注释是否理解正确,这个过程非常慢,效率极低。


这就是为什么很多东西需要依赖大佬写个介绍文档,或是翻译+延伸解释之后才能高效率学习,为什么自己找不到深入学习的路径,总是觉得前方有些混沌。


记得在刚入行的前几年写过一篇学习笔记,把自定义view 在view层测量相关的代码中的注释,变量名称整个都翻译了,备注2进制标记位变化结果,再去理解逻辑就非常简单了。跟读小说没啥区别(读Java代码就像读小说,我一直这么觉得),很快就理解了。但这个过程要花太多时间了,多到比别人慢10倍不止。


所以这第一个难而正确的事情是学习英语


达到能顺畅阅读技术资料与代码的地步,才能提高我在学习效率上的天花板。


第二个是有关生活的事情,增加不同的收入手段,主业以外至少赚到1块钱


裁员给我最大的感触就是,我很脆弱,我的职业生涯很脆弱,我的生存能力很脆弱,不具备一点反脆弱性。如果没有工作我就没有任何收入,只要稍微发生一点意外,就会面临巨大的经济压力,对于我和家庭都会陷入严重的经济困难中。


市场寒冬与我有关但却不受我影响,我无法改变。同时平庸的职业经历在行业内的影响微乎其微,大佬们是不管寒风往哪吹的,他们只管找自己想做的方向,或者别人找到他们。


我就认识这样的大佬,去年让我去新公司负责组新团队,连续一两周持续对我进行电话轰炸,因为当时正负责的团队处于关键期,我有很深的“良知”情节,我婉拒了,这是优点也是缺点。


而我只有不断提高自己的能力,让人觉得有价值才能持续在这个行业跟关系网里谋生。


但是我知道,大风之下我依然是树叶,我不是树枝,成为树枝需要天时地利人和。就像在公司背小锅的永远都是一线,因为如果是管理层背锅那公司就出了决策性的大问题了,对公司而言已然就是灾难。


这几周陆续跟很多人聊了各种事情,了解他们在做什么。有双双辞职1年多就在家做私活忙得不亦乐乎,有开网店有做跨境电商的,也了解了很多用Chat GPT,Midjourney 等AI工具做实物产品在网上卖的。包括去年了解的生财有术知识星球等等,真的花心思去了解,打开知识茧房确实了解到非常多不同的方向,有一种刘姥姥进大观园的感觉。


自己做了一些实际尝试,跑了下基本流程,确实有一些门槛但各不相同。同时在这个过程中,又因为英语阅读效率低而受阻,文档我也是硬看,不懂的词翻译一下,理解不透再整句翻译,再倒回来看原文。


比如网上midjourney的教程一大把,其实大多数都不如看midjourney官方文档来的快,我从看到用到商品上架,不过几个小时,这中间还包括开通支付跟调整模型。


至于赚到1块钱,有多难呢,当我试了我才有所体会。


种一棵树最好的时间是在10年前,其次是现在。


继续保持在社区的输出,保持技术学习。休假我都不会完全休息,Gap 中当然也不会。


后记


去年公司陆续开始裁撤业务线,有的部门直接清零,公司规模从几千人下降到千人以内不过是几个月的事情,有被裁的,也有为了降低自身风险而主动走裁员名单,这也是双赢的解决方案,公司能精简人员个人可以拿到赔偿。管理层的主要工作是尽力留下核心成员,温和的送走要离开的成员,最大程度降低团队的负面情绪,做人才盘点,申请HC做些人力补充,减少团队震动保障项目支撑。没错,一边裁员一边还会招人。


彼时我个人也才刚刚在管理岗上站稳脚跟不久,团队里没有人申请主动离职算是对我挺大一个宽慰。有的团队人员流失率接近70%,相比之下我压力小得多,但我依然要交出一个名字给到部门负责人。我当然很不舍同时也为他们担忧,过去一年多大家一起相互成长,很多人也才刚刚步入新职级。


我努力寻找第三选择,功夫不负有心人,之前做过的一个项目独立出去了,成立了独立的子公司运营,新团还没搭建完。当时跟那个项目团队的产品,后端负责人配合得相当不错,我便以个人的背书将一个曾重点负责过这个项目的成员推荐过去,加上直属上级的帮助,最终在所有HC都要HRD审批的环境下平滑的将裁员变成了团队调配。现在即使我离开了母公司,他们小团队依然还不错,没有受到后续裁员影响。这位小伙伴人特别实在,他是我见过执行里最强的人,他值得这样的好运气。


作为管理者,我有些单纯的善意,不满足于工作层面的帮助。因为我觉得个人能量实在是太小了,而未来无人知晓。


作为核心部门虽然裁员的影响波及较为滞后,但明显的感觉还是研发压力骤减,加上公司为了早点达到账面盈亏平衡,对部分薪资采取缓发,在这样的背景下整个部门的氛围变了,需求评审得过且过,项目质量得过且过,此类情况比比皆是,工作的宽容度也一再升高。


作为个人来讲这真是躺平型工作,工作任务骤减但薪资还照样发,绩效照发,每天到公司跟去上学一样。我心里就出现了一个声音「你喜欢在这里继续混吗?这里如此安逸」。


今年3月意料之中的新一轮裁员到来,我几乎没有犹豫就答复了部门负责人。团里谁想留下谁不想留我很清楚,过去我们一直也保持比较健康的氛围,始终鼓励能力强的人出去看看,也明确告知留下来与出去将面临的不同风险。大家都有心理准备,但大家都没有懈怠自己的学习,技术目标按部就班,丝毫没有陷入负面漩涡,偶尔还会因为讨论技术问题忘记下班时间。


这一次,我把自己放在了名单上,当然这并不突然。我与部门负责人一直保持着较高的工作沟通频率,就向上管理这一点上,我自认做得非常不错。


离职后大家都积极找工作,我对他们非常有信心,抛开头部大厂,中厂依然是他们的主阵地,他们在各自专精的领域里技术都很扎实,尤其是去年大家一起补足了深层次的网络层知识。不出意料部分人都很快拿了offer,有的更是觉得不想面试了匆匆就入职了,这我就不行使自己好为人师的毛病了。


作者:橘子没了
链接:https://juejin.cn/post/7224068169341763643
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🔥🔥🔥996已明确违法,从此拒绝精神内耗!

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。” 对内卷严重...
继续阅读 »

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。”


对内卷严重的公司来说:一天干8小时怎么够?全天all in的状态才是我想要的。于是996疯狂盛行。


冷知识:“996”已严重违反法律规定。


早在2021年8月,最高法、人社部就曾联合发布超时加班典型案例,明确“工作时间为早9时至晚9时,每周工作6天”的内容,严重违反法律关于延长工作时间上限的规定,应认定为无效。


最近两会期间,全国政协委员蒋胜男也在提案中表示,应加强劳动法对劳动者的休息权保护。


由此,新的一波讨论已然来袭。


一、“996”带来了什么?



产品没有核心价值,缺乏核心竞争力,害怕落后于竞争激烈的市场……越来越多的管理者选择用加班、拉长工作时间来弥补技术创新的匮乏。


这种高强度的996工作制,侵占了我们的“充电”时间,甚至让我们丧失对新事物的接收力和思考能力;高强度的工作压力+长期的加班、熬夜、不规律饮食,给身体带来了沉重的负担;在忙碌了一周之后,感受到的是前所未有的迷茫与疲倦,精神内耗愈发严重


而对于企业来说,当员工沦为“执行工具”,原本的创新型发展却变成闭门造车,所以只能不停地加班、拉长工作时间,以产出更多的成果。长此以往,就形成了一种恶性循环。


在普遍“苦996久矣”的环境下,“8小时工作制”的推崇便显得尤为可贵。


二、“8小时工作制”从何而来?


8小时工作制,不应成为一个冷知识。《中华人民共和国劳动法》第三十六条规定:国家实行劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时的工时制度


8小时工作制的提出,要感谢来自英国的Robert Owen。1817年,他提出了“8小时工作制”,也就是将一天分成3等分,8小时工作、8小时娱乐、8小时休息。在当时一周普遍工作时间超过80个小时的情况下,这种要求简直是天方夜谭。


而8小时工作制得到推行,应归功于福特汽车品牌的创始人亨利·福特。1914年1月,福特公司宣布将员工的最低薪资从每天的2.34美元涨到5美元,工作时间减少至每天8小时。这项计划将会使福特公司多支付1000万美元。



在增加了员工薪资后,最直观的是员工流动率的下降。员工的稳定以及对操作的愈发熟练,增加了生产效率,从而降低成本、提高产量。最后,福特公司只用了两年时间,就将利润增加了一倍。


1926年,福特公司又宣布将员工的工作时间改为每周5天、每天8小时。亨利·福特用实际行动证明了增加工作收入、减少工作时间,对公司来说是可以实现正向创收的。


随后,8小时工作制才开始逐渐普及。随着Z时代的到来,更多新型职场状态也已经诞生。


液态职场早已到来,你准备好了吗?


三、液态职场是什么?



1)“3+2”混合办公模式


早在2022年,全国人大代表黄细花提交了建议,呼吁可推广“3+2”混合办公模式,允许员工每周可选择1-2天在家远程办公。黄细花还表示,推广“3+2”混合办公制,提高员工工作效率的同时,减轻年轻群体的生活压力,减少城市通勤压力。对女性员工而言,弹性的办公时间能让她们更好地平衡工作和生活。混合办公制对企业、员工和社会都将产生深远影响。


于是,不少企业开始了行动。携程推出了“3+2”混合办公模式的新政策:从 2022年3月起,允许员工每周三、周五在家远程办公。


2)四天半工作制


乐视也紧随其后,推出“四天半工作制”,每周三弹性工作半天。


3)“上4休3”的工作制


微软日本公司,也早在2019年8月曾宣布,公司开始试运行每周“上4休3”的工作制度,即每周五、六、日休息3天,周五所有办公室全部关闭。


不管是8小时工作制还是上4休3”,其实本质上都一样:都是为了迎合当下的现状,打破固有传统的工作模式,寻找更加多元化的新型职场状态,让员工能够充分休息,提升效率和创造力,也能节省企业开支,最终双方获益。


这世界变化太快了,上一秒还在“996”中疯狂内卷,下一秒就已经有先行者去探索更适合的工作节奏。液态职场时代已经到来,你准备好了吗?


四、提高工作效率,大胆对996说不!


作为打工人,不管是996还是8小时工作制,虽然都不是我们能决定的,但我们可以用法律来维护自己的权利,学会说“不”。利用好这8小时,发挥出自己的价值,提高自身的创新能力和效率,是为了更有底气的说“不”!这样才能保证企业与员工之间形成一个正向循环。如何利用好8小时?给大家分享几个提高工作效率的小技巧:




  1. 保持桌面整洁,减少其他事物对工作专注度的干扰;




  2. 巧用看板,可视化工作任务,便于进行任务管理;




  3. 排列优先级,按照任务的重要紧急程度,尽量避免并行多个任务;




  4. 随时记录工作中的创意和灵感




  5. 将重复、机械的工作自动化,解放双手;




  6. 定期复盘:不断改进与优化;




  7. 培养闭环思维:凡事有交代,件件有着落,事事有回音。




工作本应是我们热爱的样子。当我们还沉浸在无休止的工作与忙碌中,被疲惫、彷徨等负面情绪包围,开始精神内耗时,是时候明确拒绝996了!


作者:禅道程序猿
链接:https://juejin.cn/post/7217616698798096444
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

副业奇谈

楔子 在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷...
继续阅读 »

楔子


在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷偷溜去学校附近大厦最高层天台,再爬上去水塔仰望星空,俯视我们的高中,看着每个教室里面一个个奋发图强的同学,我丝毫没有半点做错事的羞愧,眼里只有天上的繁星,地下的灯光,还有旁边的那个他。


小聚


“小饿,我们95后的都已经老了,现在社会都是年轻人00后的天下,比学历能力,985、211一抓一大把,比耐力,我们身体大不如前,精力强壮的年轻人比比皆是...”


“难道你不行了?”


“你别打岔,你这一行不是也有一个35岁的梗吗,你这个前端开发岗位我了解过,是需要不断学习的,技术迭代如此之快,你跟的上吗?”


我默默的摇了摇头,诚然,我是跟不上的,vue2我都没学完vue3就已经出来了,不过我相信我还是极少数。因为我安于现状太久了,拿着不上不下的薪资,没有房贷车贷育儿的压力,不像以前住在城中村每天晚上睡觉听着管道排泄物的声音,没有压力,就没有动力,我就是这么一个充满惰性的人。


小润跟我是高中同学,那时我们的关系不错,但是毕业后各自去往自己的大学,有自己的生活,便没怎么联系了,这次出来也是近三年第一次小聚。他在一个比较老牌的做文具,做设备的大厂工作,主要内容是去一些大型物业竞标,为了竞争得到那个“标”,付出的也不少,陪酒送礼一样不落,但就算得到那个“标”,公司的绩效奖励分配制度却让小润很不满,所以他不禁感慨,“我们每个月累死累活得到的薪资,除去日常花销,本来就已经所剩不多,而且社会上还存在一种叫通货膨胀的东西,想想我们年龄越来越大,面临的职场危机,手上的筹码到底能不能支撑我们维持当前消费水平,过上自己想要的生活,这是一个比较大的问题。”我听得津津有味,虽然心里认为他的说法有点过度焦虑,但有这么一个意识,总是好的,小润看到我向他投向肯定的目光,便继续说道,“这几年我都在看书,其中看到一个企业家有一句创业名言————空手套白狼”。


空手套白狼


小润看着我一脸的疑惑,嘴角微微一笑,一脸正经的告诉我,“空手套白狼虽然百度翻译是个贬义词,但是在创业翻译过来就是用最低的成本,创造最大的价值。我想要做一些0成本,价值高,只需要付出时间的生意”。


“那么请问哪里有那么大的馅饼?”据我所知,现在谈起普通人做副业,想要0成本,要不就是什么做信息差买卖,或者视频搬运,网上一搜一大把,现在根本不是能真正获利的渠道了。当然,也可能有很多人的确做着0成本生意,闷声发大财


微信图片_20230307134118.jpg


小润从煲里夹了一块榴莲肉,放入嘴中品尝了几番后吞入腹中,真诚的向我道来,“之前你有跟我聊过你做的副业,上面的功能我看了,感觉你比较厉害,对小程序开发这一块也是比较熟悉。你有没有看过小区的停车场,白天的时候很多车位都是空闲的,极大部分都是车主开车上班,那么车子不就空闲起来了?我们可以做一个平台,让车主在平台上面登记,只要车位空闲,可以告诉平台某一个时间段空闲,让平台的其他需要在附近停车的用户看到,用户微信支付停留相对应的时间,这样不仅解决了车位紧张的问题,车位车主也能利用闲置的车位赚到一笔钱,平台也能进行抽成。”


我一听,陷入了沉思,感觉好像很有道理的样子,但又觉得哪里不对,“这种做法当然是不可能的,物业停车场大都是一个车牌对应一个停车位,不可能给别人钻这种空子。”


“那你说个der啊”


微信图片_20230307134254.jpg


“刚刚只是我在生活中发现的一些奇思妙想,就是利用闲置这个属性,接下来才是我要说的重点。你平时看街边上停着的电车多吗?”我点了点头,电车在广州这所大城市,那肯定是多的。突然,小润用筷子翻了翻鸡煲中的食物,一脸愤然的对着我说“我擦,那些肥牛都被你吃完了?”我又用筷子探寻了一下,的确,肥牛还真被我吃完了,软嫩的肥牛搭配着由榴莲和鸡煲化学反应产生的汤底,让我感觉到味蕾在跳动,入口即化,难以言喻,自然而然就多吃了几片,我尴尬又不失礼貌的问他,“要不多点一份?”


他笑了笑,摆了摆手,继续说道,“我的想法是将空闲的电车利用起来,做一个平台,平台的载体是小程序,像膜拜小程序一样,用户能找到附近的单车,而我们则是电车,但是我们不需要成本,因为在平台中,电车的信息是由车主自己主动上传上来的,所以就有两个群体,一个是车主,一个是需要用电车的用户。车主能在电车空闲的时间将电车上传到我们的平台,通过出租自己的电车进行赚钱,当出租的次数多了,不仅能回本,到时候或许还能赚点小钱。而普通用户想用电车的时候,根据小程序提供的定位,找到离他最近的那台电车,进行微信支付就能骑走,按照骑行时间进行收费,收费标准由电车车主自己提供。而我们平台的收入,则是对每笔订单进行抽成”。


我一听,又陷入了沉思,又感觉好像很有道理的样子,但又觉得哪里不对,咦,我为什么要说又?


QA



用户场景有哪些,用户需求多吗?



多,平时使用电车都是上班族居多,那上班族使用完电车后电车就闲置了,可以进行出租赚点奶茶钱,何乐而不为?况且平时下班我想去别的地方玩一下,也可以租一台电车去逛一逛,就再也不需要每个人都要买一台电车了。确实,之前去湛江游玩,也有电车提供出租,骑着电车到处逛逛吃吃,真的十分快乐,不过电车是由公司统一提供。



普通用户怎么开启这些电车呢,电车五花八门,难道要让车主统一购买我们提供的电锁进行控制?



目标电车当前只试行小牛和九号电车,用户需要开启电车的时候,在小程序可以找到电车车主联系方式,通过电话联系让他用电车钥匙开启电车,同时在小程序按下开启按钮告诉平台和用户已经开启,开始计费。用户骑行完电车后,用户致电车主进行结算并关闭电车。



客户借车后,将车的某些零件换改,偷窃,损坏,如何处理?例如将电瓶车电池换成低端电池,也能用,,但车主不知道?



这的确是个问题,我也在思考是否有必要弄押金,但是电车的押金弄小了没啥用,弄大了也不合适,没人想进行支付,所以如何平衡这个问题,是我们这个项目后续所要思考的。



用户把电车开到离起始点十万八千里,这样车主怎么找回自己的电车?



好问题,我也有想过,车主在上传电车到平台的时候,可以设置自己的使用类型,可以规定使用用户骑行归还到原位置,也可以不规定,全由车主自由设定



听起来好像真的可以落地,但是用户附近可用的电车如果多起来,在地图上展示密密麻麻,这个需要点技术,我得研究研究



我们初期可能不需要那么复杂,只需要展示一个列表,可以让用户进行筛选,用户能看到每台电车的外观,点击电车详情,就能知道用户与电车的相对位置,不需要在同一个页面展示那么多的标记(如此甚好)

// 小程序在地图上显示用户与标记方法

// js
const markers = [
{
id: 1,
// 标记的大小
width: '40px',
height: '40px',
// 标记的经纬度
longitude,
latitude,
// 标记的icon图标
iconPath
}
]
this.setData({ markers })

// wxml
// center.longitude center.latitude 为中心经纬度
<map class='map' id='map' longitude='{{center.longitude}}' latitude='{{center.latitude}}' markers="{{markers}}" scale='16'></map>


政治问题...



******<-内容加密了


我们聊了很多细节,包括首页如何设计,一键控制电车上线下线,越聊越兴奋,感觉真的可以落地,说到尽情之处,还说日后被大厂收购,实现财富自由指日可待,因为我们都知道,一个产品成熟了,稍微露出苗头,就会被人借鉴。当天晚上我回到家,就把整个大纲梳理了出来,并发给小润看。


dianche.png


但同时我们也发现问题,如果用户在骑行的途中,被车主通过车钥匙远程停车会发生什么事情,之前我们一致认为电车平台会有相对应的API提供,不仅可以获取电车信息(车辆电池,型号,外观元素等),也能有启动车辆和关停车辆的接口,但浏览了两个电车平台的官网,发现平台并没有这种东西,我们的思路一下子遇到卡壳,而且押金问题也是一个重点,热情一下子就冷却了下来,这场看似热血沸腾的副业计划就此搁置了下来。


对于做副业,我个人是非常感兴趣的,低成本的副业能赚钱是根本条件,更主要能拓展人的视野,之前我第一个副业,进行的比较顺利,但前提是市场已经有先驱,可以有模板进行复刻,而这一次纯属天马行空,没有前车之鉴,需要考虑到很多细节,如果有一些致命因素导致项目行不通,那可能这个项目就真的凉了。其实也很合理,世界上人才千千万,一个脑暴出来能赚钱的项目,为什么市场没有落地,或许不是因为没有人能想出来,更大因素是有人想出来了,但是此路不通。


省流


不亏,那顿鸡煲很香,而且是小润掏的钱


作者:很饿的男朋友
链接:https://juejin.cn/post/7207634883988635705
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信图片防撤回

了解需求 实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。 当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。 解决方案 思路 由于...
继续阅读 »

了解需求


实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。


当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。


解决方案


思路


由于当前微信不支持微信网页版登陆,因此使用itchat的方法不再适用。


后来了解到电脑端微信图片会先存储在本地,撤回后图片再从本地删除,因此只要在撤回前将微信本地图片转移到新文件夹即可。


在此使用Python的watchdog包来监视文件系统事件,例如文件被创建、修改、删除、移动,我们只需监听创建文件事件即可。


安装watchdog包:    pip install watchdog
我的python环境为python3.9版本

实现


1.首先进行文件创建事件监听,在监听事件发生后的事件处理对象为复制微信图片到新文件夹。具体代码如下。


需要注意的是微信在2022.05前,图片存储在images目录下;在2022.05后,图片存储在MsgAttach目录下,并按微信对象分别进行存储。


# 第一步:加载路径,并实时读取JPG信息
import os
import shutil
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

def mycopyfile(srcfile,dst_dir):
if not os.path.isfile(srcfile):
print ("%s not exist!"%(srcfile))
else:
fpath,fname=os.path.split(srcfile) # 分离文件名和路径
if fname.endswith('.jpg') or fname.endswith('.png') or fname.endswith('.dat'):
dst_path = os.path.join(dst_dir, fname)
shutil.copy(srcfile, dst_path) # 复制文件

class MyEventHandler(FileSystemEventHandler):
# 文件移动
# def on_moved(self, event):
# print("文件移动触发")
# print(event)


def on_created(self, event):
# print("文件创建触发")
print(event)
mycopyfile(event.src_path, dst_dir)


# def on_deleted(self, event):
# print("文件删除触发")
# print(event)
#
# def on_modified(self, event):
# print("文件编辑触发")
# print(event)

if __name__ == '__main__':

dst_dir = r"E:\03微信防撤回\weixin" #TODO:修改为自己的保存文件目录
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)

observer = Observer() # 创建观察者对象
file_handler = MyEventHandler() # 创建事件处理对象
listen_dir = r"C:\Users\hc\Documents\WeChat" #TODO:修改为自己的监听目录
observer.schedule(file_handler, listen_dir, True) # 向观察者对象绑定事件和目录
observer.start() # 启动
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()

2.由于微信保存文件以.dat格式保存,因此需要对微信文件格式进行解码,具体解码代码如下。


# weixin_Image.dat 破解
# JPG 16进制 FF D8 FF
# PNG 16进制 89 50 4e 47
# GIF 16进制 47 49 46 38
# 微信.bat 16进制 a1 86----->jpg ab 8c----jpg dd 04 --->png
# 自动计算异或 值
import os

into_path = r'E:\03微信防撤回\weixin' # 微信image文件路径
out_path = r'E:\03微信防撤回\image'

def main(into_path, out_path):

dat_list = Dat_files(into_path) # 把路径文件夹下的dat文件以列表呈现
lens = len(dat_list)
if lens == 0:
print('没有dat文件')
exit()

num = 0
for dat_file in dat_list: # 逐步读取文件
num += 1
temp_path = into_path + '/' + dat_file # 拼接路径:微信图片路径+图片名
dat_file_name = dat_file[:-4] # 截取字符串 去掉.dat
imageDecode(temp_path, dat_file_name, out_path) # 转码函数
value = int((num / lens) * 100) # 显示进度
print('正在处理--->{}%'.format(value))


def Dat_files(file_dir):
"""
:param file_dir: 寻找文件夹下的dat文件
:return: 返回文件夹下dat文件的列表
"""

dat = []
for files in os.listdir(file_dir):
if os.path.splitext(files)[1] == '.dat':
dat.append(files)
return dat

def imageDecode(temp_path, dat_file_name, out_path):
dat_read = open(temp_path, "rb") # 读取.bat 文件
xo, j = Format(temp_path) # 判断图片格式 并计算返回异或值 函数

if j == 1:
mat = '.png'
elif j == 2:
mat = '.gif'
else:
mat = '.jpg'

out = out_path + '/' + dat_file_name + mat # 图片输出路径
png_write = open(out, "wb") # 图片写入
dat_read.seek(0) # 重置文件指针位置

for now in dat_read: # 循环字节
for nowByte in now:
newByte = nowByte ^ xo # 转码计算
png_write.write(bytes([newByte])) # 转码后重新写入


def Format(f):
"""
计算异或值
各图片头部信息
png:89 50 4e 47
gif: 47 49 46 38
jpeg:ff d8 ff
"""

dat_r = open(f, "rb")

try:
a = [(0x89, 0x50, 0x4e), (0x47, 0x49, 0x46), (0xff, 0xd8, 0xff)]
for now in dat_r:
j = 0
for xor in a:
j = j + 1 # 记录是第几个格式 1:png 2:gif 3:jpeg
i = 0
res = []
now2 = now[:3] # 取前三组判断
for nowByte in now2:
res.append(nowByte ^ xor[i])
i += 1
if res[0] == res[1] == res[2]:
return res[0], j
except:
pass
finally:
dat_r.close()


# 运行
if __name__ == '__main__':
main(into_path, out_path)
复制代码
作者:空气猫
来源:juejin.cn/post/7221376169370583101
>
收起阅读 »

前端路由访问权限控制方案

web
本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考! 项目背景及路由初步设计 目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,...
继续阅读 »

本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考!



项目背景及路由初步设计


目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,相信后续还会有更多的第三方企业对接,以及更多的业务场景。


这就需要提前对系统的菜单权限进行规划,由于后期开发的不确定性以及人力资源有限,路由权限控制没有采用跟后端强耦合的方式实现,由前端自行配置并处理。


一开始由于意向企业业务上的强相关性,并没有规划太多的模块(主业务全都写在了src/views/main目录下),也没有对用户行为进行规划分类,路由控制方面也是根据平台标识手动配置的路由表


const xxx = [
'/main/nopage',
'/main/checkFace',
// ...
]

缺点


最近又做了一个B端的项目,发现上面的实现方法并不是很好,原因有以下几点:




  1. 对接平台多的话,就会出现一堆路由配置,不优雅、不美观




  2. 业务上的不一致性带来扩展的不灵活




  3. 暂时没有想起来 : )




经过一番考量,我决定这样做(其实也是常见的方法)


改进方案


首先业务实现上需要我新建一个文件目录(src/views/xxx,不能再往main目录下放了),在里面写路由组件。


期间我想过以对接的平台标识创建路由组件目录,进行业务上的隔离,没有做出实际尝试就被我舍弃了,原因是:以平台标识作为业务的根目录,跟原先的做法本质上是一致的,只是改进,相当于是补丁,而我要做的是寻找另一种解决办法。


根据Linux系统一切皆文件的思想,类似的,我还是采用了老套的办法,给每一个路由菜单赋予一个访问权限的配置。


这样做,后面维护起来也简单(有了平台标识和用户行为的划分)


{
path: "/test",
name: "Test",
meta: {
belong: ["xxx", "xxx"] // 所属平台信息,操作行为信息...
},
component: () => import("@/views/test")
},

后端同事配合规划用户平台和行为,在用户访问的时候,后端返回用户信息的同时,返回平台标识和行为标识。


同样的,在全局路由钩子里验证访问权限。



router.beforeEach((to, from, next) => {
try {
const { belong = [] } = to.meta
const authInfo = ["platform", "action"]
if (accessTokenStr) {
// 已登录, 做点什么
// belong <--> authInfo
// arrayInclude(belong, authInfo)
} else {
next()
}
} catch (err) {
console.log(err);
}
})

/**
* 判断数组元素的包含关系,不要求顺序一致
* 数组中不存在重复元素
* 用于验证路由权限
* arrA包含arrB,返回true, 否则返回false
*/

export const arrayInclude = (arrA, arrB) => {
let flag = true
for (let i = 0; i < arrB.length; i++) {
if (!arrA.includes(arrB[i])) {
flag = false
break
}
}
return flag
}


👉👉以上方案写于2021-11-06





维护总结


2023-04-03


近期业务扩展,发现上面的菜单权限控制有点不合理


这种配置不直观,有点混乱!!!


还是采用json的方式分配路由, 比如:


const menus = {
[platformId]: [
"/a"
"/b"
],
[platformId]: [
"/a"
"/b"
],
}

这样可以更加直观的显示出来某个业务包含哪些菜单,而不是像之前那样把菜单的权限配置在路由上!


总结: 路由设计要中电考虑可读性、易维护性




我是 甜点cc,个人网站(国外站点): blog.i-xiao.space/


公众号

作者:甜点cc
来源:juejin.cn/post/7239173692228255802
:【看见另一种可能】

收起阅读 »

一文搞清楚Node.js的本质

web
学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。 Node.js 是什么? Node.js 是一个基于 V8 引擎 的 JS 运行时,它由 Ryan Dahl 在 ...
继续阅读 »

学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。


Node.js 是什么?


Node.js 是一个基于 V8 引擎JS 运行时,它由 Ryan Dahl 在 2009 年创建。


这里有两个关键词,一是 JS 引擎,二是 JS 的运行时


那什么叫 JS 引擎呢?


image.png


JS 引擎就是把一些 JS 的代码进行解析执行,最后得到一个结果。


比如,上图的左边是用 JS 的语法定义一个变量 a 等于 1,b 等于 1,然后把 a 加 b 的值赋值给新的变量 c,接着把这个字符串传入 JS 引擎里,JS 引擎就会进行解析执行,执行完之后就可以得到对应的结果。


那么 JS 运行时又是什么呢?它和 JS 本身有什么区别 ?


要搞清楚上面的问题,可以看下面这张图:


image.png


从下往上看:




  1. 最下面一层是脚本语言规范ECMAScript,也就是常说的ES5、ES6语法。




  2. 往上一层就是对于该规范的实现了,如JavaScriptJScript等都属于对 ECMAScript语言规范的实现。




  3. 再往上一层就是执行引擎JavaScript 常见的引擎有 V8QuickJS等,用来解释执行代码。




  4. 最上面就是运行时环境了,比如基于 V8 封装的运行时环境有 ChromiumNode.jsDeno 等等。




可以看到,JavaScript 在第二层,Node.js 则在第四层,两个根本不是一个东西。


所以,Node.js 并不是语言,而是一个 JavaScript 运行时环境,它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。


JS 作为一门语言,有独立的语法规范,提供一些内置对象和 API(如数组、对象、函数等)。但和其他语言(C、C++等)不一样的是,JS 不提供网络、文件、进程等功能,这些额外的功能是由运行时环境实现和提供的,比如浏览器或 Node.js


所以,JS 运行时可以理解为 JS 本身 + 一些拓展的能力所组成的一个运行环境,如下面这张图:


image.png


可以看到,这些运行时都不同程度地拓展了 JS 本身的功能。JS 运行时封装底层复杂的逻辑,对上层暴露 JS API,开发者只需要了解 JS 的语法,就可以使用这些 JS 运行时做很多 JS 本身无法做到的事情。


Node.js 的组成


搞清楚了什么是Node.js后,再来看看 Node.js 的组成。


Node.js 主要是由 V8Libuv 和一些第三方库组成的。


V8引擎


V8 是一个 JS 引擎,它不仅实现了 JS 解析和执行,还支持自定义拓展。


这有什么用处呢?


比如说,在下面这张图中我们直接使用了 A 函数,但 JS 本身并没有提供 A 这个函数。这个时候,我们给 V8 引擎提供的 API 里注入一个全局变量 A ,就可以直接在 JS 中使用这个 A 函数了。正是因为 V8 支持这个自定义的拓展,才有了 Node.js 等 JS 运行时


image.png


Libuv


Libuv 是一个跨平台的异步 IO 库,它主要是封装各个操作系统的一些 API,提供网络还有文件进程这些功能


我们知道在 JS 里面是没有网络文件这些功能的,前端是由浏览器提供,而 Node.js 里则是由 Libuv 提供


image.png




  1. 左侧部分是 JS 本身的功能,也就是 V8 实现的功能。




  2. 中间部分是一些C++ 胶水代码。




  3. 右侧部分是 Libuv 的代码。




V8Libuv 通过第二部分的胶水代码粘合在一起,最后就形成了整一个 Node.js


因此,在 Node.js 里面不仅可以使用 JS 本身给我们提供的一些变量,如数组、函数,还能使用 JS 本身没有提供的 TCP、文件操作和定时器功能


这些扩展出来的能力都是扩展到V8上,然后提供给开发者使用,不过,Node.js 并不是通过全局变量的方式实现扩展的,它是通过模块加载来实现的。


第三方库工具库


有了 V8 引擎和拓展 JS 能力的 Libuv,理论上就可以写一个 JS 运行时了,但是随着 JS 运行时功能的不断增加,Libuv 已经不能满足需求,比如实现加密解密、压缩解压缩。


这时候就需要使用一些经过业界验证的第三方库,比如异步 DNS 解析 c-ares 库、HTTP 解析器 llhttp、HTTP2 解析器 nghttp2、解压压缩库 zlib、加密解密库 openssl 等等。


Node.js 代码组成


了解了 Node.js 的核心组成后,再来简单看一下 Node.js 代码的组成。


image.png


Node.js 代码主要是分为三个部分,分别是 CC++JavaScript


JS


JS 代码就是我们平时使用的那些 JS 模块,像 http 和 fs 这些模块


Node.js 之所以流行,有很大一部分原因在于选择了 JS 语言。


Node.js 内核通过 CC++ 实现了核心的功能,然后通过 JS API 暴露给用户使用,这样用户只需要了解 JS 语法就可以进行开发。相比其他的语言,这个门槛降低了很多。


C++


C++代码主要分为三个部分:




  1. 第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如 netfs 这些模块都会对应一个 C++ 模块,它主要是对底层 Libuv 的一些封装。




  2. 第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现,主要依赖于 V8




  3. 第三部分 C++ 代码是 V8 本身的代码。




C++ 代码中最重要的是了解如何通过 V8 API 把 C、C++ 的能力暴露给 JS 层使用,正如前面讲到的通过拓展一个全局变量 A,然后就可以在 JS层使用 A。


C 语言层


C 语言代码主要是包括 Libuv 和第三方库的代码,它们大多数是纯 C 语言实现的代码。


Libuv 等库提供了一系列 C API,然后在 Node.jsC++ 层对其进行封装使用,最终暴露 JS APIJS 层使用。


总结


文章第一部分介绍了Node.js的本质,它实际上是一个JS运行时,提供了网络、文件、进程等功能,类似于浏览器,提供了JS的运行环境。


第二部分介绍了Node.js的组成,它由V8引擎、Libuv及第三方库构成,Node.js核心功能大都是Libuv提供的,它封装底层各个操作系统的一些 API,因此,Node.js是跨平台的。


第三部分从代码的角度描述了Node.js的组成,包括JavaScriptC++C三部分,中间的C++部分通过对C部分的封装提供给JavaScript部分使用。


作者:小p
来源:juejin.cn/post/7238814783598297144
收起阅读 »

Android 14 新增权限

原文: medium.com/proandroidd… 译者:程序员 DHL 本文已收录于仓库 Technical-Article-Translation 这篇文章,主要分享在 Android 14 以上新增的权限 READ_MEDIA_VISUAL_US...
继续阅读 »



这篇文章,主要分享在 Android 14 以上新增的权限 READ_MEDIA_VISUAL_USER_SELECTED,该权限允许用户仅授予对选定媒体的访问权限(Photos / Videos)),而不是访问整个媒体库。


新的权限弹窗


当你的 App 运行在 Andrid 14 以上的设备时,如果请求访问照片,会出现以下对话框,你将看到新的选项。



受影响的行为


当我们在项目中声明新的权限 READ_MEDIA_VISUAL_USER_SELECTED ,并且用户选择 Select photos and videos(Select photos or Select videos)




  • READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限都会被拒绝




  • READ_MEDIA_VISUAL_USER_SELECTED 权限被授予时,将会被允许临时访问用户的照片和视频




  • 如果我们需要访问其他照片和视频,我们需要同时申请 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限




如何在项目中使用新的权限



  • AndroidManifest.xml 文件中添加下面的权限


<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

// new permisison
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />


  • 使用 ActivityResultContract 请求新的权限


val permissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { mapResults ->
mapResults.forEach {
Log.d(TAG, "Permission: ${it.key} Status: ${it.value}")
}
// check if any of the requested permissions is granted or not
if (mapResults.values.any { it }) {
// query the content resolver
queryContentResolver(context) { listOfImages ->
imageDataModelList = listOfImages
}
}
}

为什么要使用 RequestMultiplePermissions,因为我们需要同时请求 READ_MEDIA_IMAGES , READ_MEDIA_VIDEO 权限



  • 启动权限申请流程


OutlinedButton(onClick = {
permissionLauncher.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED))
}) {
Text("Allow to read all or select images")
}

关于 Android 12 、 Android 13 、Android 14 功能和权限的变更,点击下方链接前往查看:



最后我们看一下运行效果





全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。





Hi 大家好,我是 DHL,就职于美团、快手、小米。公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经,真诚推荐你关注我。





最新文章



开源新项目




  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读




作者:程序员DHL
来源:juejin.cn/post/7238762963908689957
收起阅读 »

如何使用localStorage判断设置值是否过期

web
简介:本文介绍了使用 localStorage 判断设置值是否过期的方法。通过设置过期时间,我们可以使用 setItemWithExpiration 函数将数据存储到 localStorage 中,并使用 getItemWithExpiration 函数获取数...
继续阅读 »

简介:本文介绍了使用 localStorage 判断设置值是否过期的方法。通过设置过期时间,我们可以使用 setItemWithExpiration 函数将数据存储到 localStorage 中,并使用 getItemWithExpiration 函数获取数据并检查是否过期。localStorage 提供简单的 API 方法来存储、检索和删除数据,并具有持久性和域隔离的特点。通过本文的方法,你可以方便地管理数据,并灵活设置过期时间,实现更多的数据存储和管理功能。



目标:在网站中实现定期弹窗功能,以提示用户。


选择实现方式:为了实现持久化存储和检测功能,我们选择使用localStorage作为首选方案。


解决方案:



  1. 存储设置值:使用localStorage将设置值存储在浏览器中,以便在用户访问网站时保持数据的持久性。

  2. 设置过期时间:根据需求,为设置值设定一个过期时间,表示多长时间后需要再次弹窗提示用户。

  3. 检测过期状态:每次用户访问网站时,检测存储的设置值是否已过期。若过期,则触发弹窗功能,提醒用户。

  4. 更新设置值:在弹窗提示后,可以根据用户的操作进行相应的更新操作,例如延长过期时间或更新设置内容。


优势:



  1. 持久化存储:使用localStorage可以将设置值保存在浏览器中,即使用户关闭了浏览器或重新启动设备,设置值仍然有效。

  2. 简单易用:localStorage提供了简单的API,方便存储和读取数据,实现起来相对简单。

  3. 跨浏览器支持:localStorage是HTML5的标准特性,几乎所有现代浏览器都支持它,因此具有良好的跨浏览器兼容性。


注意事项:



  1. 需要根据业务需求合理设置过期时间,避免频繁弹窗对用户体验造成困扰。

  2. 在使用localStorage时,要考虑浏览器的隐私设置,以确保能够正常存储和读取数据。


说一下locaStorage


localStorage 是 Web 浏览器提供的一种存储数据的机制,它允许在浏览器中存储和检索键值对数据。


以下是关于 localStorage 的一些重要特点和使用方法:




  1. 持久性:与会话存储(session storage)相比,localStorage 是一种持久性的存储机制。存储在 localStorage 中的数据在用户关闭浏览器后仍然保留,下次打开网页时仍然可用。




  2. 容量限制:每个域名下的 localStorage 存储空间通常为 5MB。这个限制是针对整个域名的,不是针对单个页面或单个 localStorage 对象的。




  3. 键值对数据存储:localStorage 使用键值对的方式存储数据。每个键和对应的值都是字符串类型。如果要存储其他数据类型(如对象或数组),需要进行序列化和反序列化操作。




  4. API 方法:


    属性方法
    localStorage.setItem(key, value)将键值对存储到 localStorage 中。如果键已存在,则更新对应的值。
    localStorage.getItem(key)根据键获取存储在 localStorage 中的值
    localStorage.removeItem(key)根据键从 localStorage 中删除对应的键值对
    localStorage.clear()清除所有存储在 localStorage 中的键值对。
    localStorage.key(index)根据索引获取对应位置的键名。



  5. 域限制:localStorage 存储的数据与特定的域名相关。不同的域名之间的 localStorage 是相互隔离的,一个网站无法访问另一个网站的 localStorage 数据。




  6. 安全性:localStorage 中的数据存储在用户的本地浏览器中,因此可以被用户修改或删除。敏感数据应该避免存储在 localStorage 中,并使用其他更安全的机制来处理。




以下是一个示例,展示如何使用 localStorage 存储和检索数据:


// 存储数据到 localStorage
localStorage.setItem('name', 'localStorage');
localStorage.setItem('size', '5mb');

// 从 localStorage 中获取数据
const name = localStorage.getItem('name');
const age = localStorage.getItem('size');

console.log(name); // 输出: localStorage
console.log(size); // 输出: 5mb

// 从 localStorage 中删除数据
localStorage.removeItem('size');

// 清除所有的 localStorage 数据
localStorage.clear();

总结来说,localStorage 是一种用于在 Web 浏览器中持久存储键值对数据的机制。它提供简单的 API 方法来存储、检索和删除数据,并具有一定的容量限制和域隔离。


判断本地存储时间的实现


存储时间与值


使用以下代码将值与过期时间存储到localStorage中:


const expiration = 24 * 60 * 60 * 1000 * 7; // 设置过期时间为七天
// const expiration = 2 * 60 * 1000; // 设置过期时间为2分钟
setItemWithExpiration("read_rule", true, expiration);

下面是相应的函数实现:


// 存储数据到LocalStorage,并设置过期时间(单位:毫秒)
function setItemWithExpiration(key, value, expiration) {
   const item = {
       value: value,
       expiration: Date.now() + expiration
  };
   localStorage.setItem(key, JSON.stringify(item));
}

获取值并判断是否过期


使用以下代码从localStorage中获取值,并判断其是否过期:


const retrievedData = getItemWithExpiration("read_rule");

下面是相应的函数实现:


// 从LocalStorage中获取数据,并检查是否过期
function getItemWithExpiration(key) {
   const item = JSON.parse(localStorage.getItem(key));
   if (item && Date.now() < item.expiration) {
       return item.value;
  }
   // 如果数据已过期或不存在,则返回 null 或其他默认值
   return null;
}

通过以上实现,可以方便地存储带有过期时间的值,并根据需要获取和判断其有效性。如果存储的数据已过期或不存在,函数getItemWithExpiration将返回null

作者:猫头_
来源:juejin.cn/post/7238794430966677564
其他您设定的默认值。

收起阅读 »

程序员的坏习惯

前言 每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。 不遵循项目规范 每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、...
继续阅读 »

前言


每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。


图片.png


不遵循项目规范


每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、魔鬼数字、提交代码覆盖他人代码等问题经常发生,如果大家能够遵循相关规范,这些问题都可以避免。


用复杂SQL语句来解决问题


程序员在开发功能时,总想着是否能用一条SQL语句来完成这个功能,于是实现的SQL语句写的非常复杂,包含各种子查询嵌套,函数转换等。这样的SQL语句一旦出现了性能问题,很难进行相关优化。


缺少全局把控思维,只关注某一块业务


新增新功能只关注某一小块业务,不考虑系统整体的扩展性,其他模块已经有相关的实现了,却又重复实现,导致重复代码严重。修改功能不考虑对其他模块的影响。


函数复杂冗长,逻辑混乱


一个函数几百行,复杂函数不做拆分,导致代码变得越来月臃肿,最后谁也不敢动。函数还是要遵循设计模式的单一职责,一个函数只做一件事情。如果函数逻辑确实复杂,需要进行拆分,保证逻辑清晰。


缺乏主动思考,拿来主义


实现相关功能,先网上百度一下,拷贝相关的代码,能够运行成功认为万事大吉。到了生产却出现了各种各样的问题,因为网上的demo程序和实际项目的在场景使用上有区别,尤其是相关的参数配置,一定要弄清楚具体的含义,不同场景下,设置参数的值不同。


核心业务逻辑,缺少相关日志和注释


很多核心的业务逻辑实现,整个方法几乎没看到相关注释和日志打印,除了自己能看懂代码逻辑,其他人根本看不懂。一旦生产出了问题,找不到有效的日志输出,问题根本无法定位。


修改代码,缺少必要测试


很多人都会存在侥幸心里,认为只是改了一个变量或者只修改一行代码,不用自测了应该没有问题,殊不知就是因为改一行代码导致了严重的bug。所以修改代码一定要进行自测。


需求没理清,直接写代码


很多程序员在接到需求后,不怎么思考就开始写代码,写着写着发现自己的理解与实际的需求有偏差,造成无意义返工。所以需要多花些时间梳理需求,整理相关思路,能规避很多不合理的问题。


讨论问题,表达没有逻辑、没有重点


讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明。需要学会沟通和表达,才能进行有效的沟通和合作。


不能从错误中吸取教训


作为一位开发人员,你会犯很多错误,这不可避免也没什么大不了的。但如果你总是犯同样的错误,不能从中吸取教训,那态度就出现问题了。


总结


关于这些坏习惯,你是否中招了,大家应该尽早规避这些坏习惯,成为一名优秀的程序员。


作者:剑圣无痕
来源:juejin.cn/post/7136455796979662862
收起阅读 »

CSS样式穿透?你准备好了吗!

web
你是否遇到过这样的情况:想要修改子元素的样式却发现使用父元素选择器无法生效。这时,你就需要了解一下CSS样式穿透的概念。 简单介绍 一般来说,我们可以通过父级选择器来选中它下面的子元素。例如: .parent .child { color: red; } ...
继续阅读 »

cover.png


你是否遇到过这样的情况:想要修改子元素的样式却发现使用父元素选择器无法生效。这时,你就需要了解一下CSS样式穿透的概念。


简单介绍


一般来说,我们可以通过父级选择器来选中它下面的子元素。例如:


.parent .child {
color: red;
}

但是,有些时候我们需要给子元素中特定的元素修改样式,而不是所有的子元素都修改。这时,我们就需要了解CSS样式穿透这个概念。


CSS样式穿透


在CSS中,我们可以使用“/deep/”、“::v-deep”、“::shadow”等方式实现CSS样式的穿透。


使用/deep/


通过使用/deep/关键字,可以达到子组件穿透自身样式的目的。例如:


.parent /deep/ .child {
color: red;
}

这种方式相比于上述普通方法,能够选中更深层次的子元素(即使用多个空格连接的子元素)。但是,由于浏览器对“/deep/”选择器支持并不友好,因此尽量避免使用。


使用::v-deep


在Vue框架中,如果需要穿透组件样式,可以使用::v-deep或者>>>选择器。例如:


.parent ::v-deep .child {
color: red;
}

这种方式只对Vue组件可用,且与/deep/的作用类似。


使用::shadow


在Web Components规范中,定义了Shadow DOM的概念,它能够使得元素的样式隔离开来,不会影响到其它元素。如果我们需要在Shadow DOM中修改样式,可以使用::shadow伪类。


parent::shadow .child {
color: red;
}

这种方式相比较于上述两种方法,更加安全和规范,但需要先了解Shadow DOM的概念。


补充说明


尽管CSS样式穿透能够方便地修改子元素样式,但是在实际开发中还是应当尽可能地避免使用它们。


CSS一直致力于封装样式,降低代码耦合度,而使用CSS样式穿透会将样式的层级深度加深,增加样式的维护成本。


此外,在跨浏览器、跨框架的情况下,CSS样式穿透的表现都不尽相同,因此建议在项目中谨慎使用。


结语


CSS样式穿透虽然能够带来方便,却也需要谨慎使用,遵循代码封装的原则,保持样式的简洁、规范和易维护。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7238999952553771066
收起阅读 »

为什么面试官这么爱问性能优化?

web
笔者是一个六年前端,没有大厂经历,也没有什么出彩的项目,所以今年以来,前端现在这种行情下并没有收到多少面试,但是为数不多的面试中,百分之九十都问到了性能优化的问题,而且问题都出奇的一致: 平时的工作中你有做过什么性能优化? 对于这个问题其实我的内心os是(...
继续阅读 »

笔者是一个六年前端,没有大厂经历,也没有什么出彩的项目,所以今年以来,前端现在这种行情下并没有收到多少面试,但是为数不多的面试中,百分之九十都问到了性能优化的问题,而且问题都出奇的一致:



平时的工作中你有做过什么性能优化?



对于这个问题其实我的内心os是(各位轻喷~):



你们怎么都这么爱问性能优化的问题?我的简历中也没有写到这个啊。


你们的业务都这么复杂吗?怎么动不动就要性能优化?


你们的代码写的这么拉吗?不优化都不能使用吗?


性能优化是一个高级前端的必要技能吗?



首先客观现实是笔者平时工作中的业务并不复杂,需要性能优化的地方确实不多,一些存在性能瓶颈的大多是使用了其他团队开发的东西,比如播放直播视频的SDK、3D地图引擎等,也找过他们进行优化,但是没用,他们也优化不动。


所以每次被问到这个问题我就很尴尬,说工作中没有遇到过性能问题,估计面试官也不信,直接说没有做过性能优化,那又显得我这个六年经验的前端太水了,连这个都不做,所以每次我只能硬说。


没吃过猪肉,还没见过猪跑吗?其实性能优化的文章我也看过很多,各种名词我还是知道一点的,比如:



  • 性能问题排查:



1.数据埋点上报


2.使用控制台的NetWork、Performance等工具


3.webpack-bundle-analyzer插件分析打包产物




  • http相关:



1.gzip压缩


2.强缓存、协商缓存




  • 图片相关:



1.图片压缩


2.图片懒加载


3.雪碧图、使用字体图标、svg




  • webpack相关:



1.优化文件搜索


2.多进程打包


3.分包


4.代码压缩


5.使用CDN




  • 框架相关:



1.vue性能优化、react性能优化


2.异步组件


3.tree shaking


4.服务端渲染




  • 代码实现



1.按需加载,逻辑后移,优先保证首屏内容渲染


2.复杂计算使用web worker


3.接口缓存、计算结果缓存


4.预加载


5.骨架屏


6.虚拟滚动



等等。


但这些绝大部分我并没有实践过,所以我都说不出口,说我没有机会实践也行,说我没有好奇心不好学不爱思考不主动发现问题也行,总之结果就是没有经验。


所以通常我硬着头皮只能说出以下这些:


1.开发前会花点时间梳理业务,全局视角过一遍交互和视觉,思考组件划分,找出项目中相似的部分,提取为公共组件和通用逻辑。


2.代码开发中尽量保证写出的代码清晰、可维护,比如:清晰的目录和文件结构、添加必要的注释、提取公共函数公共组件、组件单向数据流、组件功能尽量单一等。


3.时刻关注可能会存在性能问题的部分,比如:



路由组件异步加载


动态加载一些初始不需要用到的资源


频繁切换的组件使用KeepAlive进行缓存


缓存复杂或常用的计算结果


对实时性不高的接口进行缓存


同一个接口多次请求时取消上一次没有完成的请求


页面中存在很多接口时进行优先级排序,优先请求页面重要信息的接口,并关注同一时刻请求的接口数量,如果过多进行分批请求


对于一些确实比较慢的接口使用loading或骨架屏


懒加载列表,懒加载图片,对移出可视区的图片和dom进行销毁


关注页面中使用到的图片大小,推动后端进行图片压缩


地图撒点时使用聚合减少地图引擎渲染压力


对于一些频繁的操作使用防抖或节流


使用三方库或组件库尽量采用按需加载,减少打包体积


组件卸载时取消事件的监听、取消组件中的定时器、销毁一些三方库的实例



我工作中的实践也就以上这些,其实就是写代码的基本要求,另外我觉得如果业务复杂,以上这些也并不能阻止性能问题的出现,更多的还是当出现了问题,去思考如何解决。


比如我开源的一个思维导图项目mind-map,当节点数量多了会非常卡,调试分析思考后发现原因是做法不合理,每次画布上有操作后都是清空画布上的所有元素,然后重新创建所有元素,数据驱动视图,原理非常简单,但是因为是通过svg实现,所以就是DOM节点,这玩意我们都知道,当节点数量非常多以后,删除节点和创建节点都是非常耗时的,所以数据驱动视图的框架,比如Vue会通过虚拟DOM的diff算法对比来找出最小的更新部分,但是我没有做。。。所以。。。那么我就自然的做了一些优化,比如:



思维导图场景,大部分情况下操作的其实就是其中一个或部分节点,所以不需要重新删除创建所有元素,那么就可以通过节点复用的方式来优化,将真实节点缓存起来,渲染时通过数据唯一的id来检查是否存在可复用节点,如果没有,那么代表是新增节点,那么创建新节点即可;如果有,那么就判断节点数据是否发生改变,没有改变直接复用,如果发生了改变那么判断是否可以进行更新,如果更新成本高那么直接重新创建;另外也需要和上一次的缓存进行对比,找出本次渲染不需要的节点进行删除;当然,为了避免缓存节点数量无限膨胀,也通过LRU缓存算法来管理


对于不影响其他节点的操作只更新被操作的节点


通过setTimeout异步渲染节点,留一些中间时间来响应页面其他操作


将触发渲染的任务放到队列中,在下一帧进行处理,合并掉一些中间状态


对于鼠标移动和滚动的场景,通过节流来优化


进行一些取舍,早期节点激活时可以修改节点的所有样式,导致激活操作需要重新计算节点大小,更新节点样式,在多选和全选操作下非常耗时,所以后期改为只允许修改不改变节点大小的样式属性


其他一些细节优化:对于数据没有改变的操作不触发赋值或函数调用,一些不起眼的操作可能也是需要耗费时间的;改变了不涉及节点大小的属性不触发节点大小重新计算等



经过以上这些修改后,性能确实有了很大程度的提升,不过有些项目可以通过不断的优化来提升性能,但是有些可能就是设计缺陷,比如我开源的另一个白板项目,更好的方式其实是重做它。


写到这里其实并没有解决本文标题提出的问题:



为什么面试官这么爱问性能优化?



因为我没有怎么做过面试官,甚至面试经验其实都不太多,写这篇文章目的主要有两个:


1.想听听有面试官经验的各位的想法或建议


2.想看看和我有类似情况的面试者面对这个问题,或者说类似的问题是如何回答的


最后再牢骚几句:



有时会感慨时间过的真快,一转眼,作为一个前端已经工作了六年,即将三十而立却立不起来,这么多年的工作,更多的只是收获了六年的经历,但是并没有六年的能力,回过头看,当初的有些选择确实是错误的,也许这就是人生把。


作为一个普通的前端,在如今的行情下面试确实很艰难,尤其是我这种不擅长面试的人,不过话说回来,改变哪有不痛苦的,除了面对也没有其他办法。



作者:街角小林
来源:juejin.cn/post/7239267216805838903
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

移植五周年

这几天在医院对身体各方面进行了一次比较全面的检查,结果比较令人满意。一转眼,接受肾移植已经 5 周年了,写一篇博文,对这些年的身体以及治疗情况进行了一个汇总。 发病 在 30 岁左右进行体检时,已经发现肾功能指标不太理想,因此进行了有针对性的调整。但随着时间的...
继续阅读 »

这几天在医院对身体各方面进行了一次比较全面的检查,结果比较令人满意。一转眼,接受肾移植已经 5 周年了,写一篇博文,对这些年的身体以及治疗情况进行了一个汇总。


发病


在 30 岁左右进行体检时,已经发现肾功能指标不太理想,因此进行了有针对性的调整。但随着时间的推移和工作的繁忙,逐渐也放松了对健康的重视。尽管发病前几个月进行体检时,各方面的指标也还说得过去。但病来如山倒,对于肾脏疾病来说,后期恶化的速度是十分迅猛的。


当时手里有几个项目正在进行,资金的压力也比较大。尽管身体已经发出了明显的信号,但总想着再坚持一下,即使家人一再催促,我也始终没有进行必要的检查。直到几乎完全无法进食(吃任何东西都马上会吐)才不得已去了医院,确诊十分迅速,因为已经没有了讨论的必要。此时,肌酐在 2100+,血红蛋白 46,按照医生的话来说,能活着走到医院已经属于大惊喜了。


由于贫血严重,输了几次血,但血红蛋白并没有得到太大的改善。为了尽快进行较彻底的透析,在与医生商量后,尽管血红蛋白不足 60,还是强行进行了植入透析管的手术,开启了我的透析生涯。


透析


透析有两种形式:一种是血液透析(大多数人知道的透析方式),另一种是腹膜透析。血液透析通常在指定的血液透析中心(或医院)进行,每周三次,每次四个小时。腹膜透析则可以在家中进行,每天透析次数根据患者的身体情况而有所变化,通常为 2 到 5 次,每次透析液置换时间约为 20 分钟。


我选择了腹膜透析。相较于血液透析,腹膜透析的场地和时间更自由。另外,腹膜透析通常对残肾功能保护比较好,有利于日后的移植。腹膜透析也有两种方式,一种是手动,就是每天手工进行几次透析液的置换操作。另一种则是自动透析(APD),通常是每天临睡前将腹部的透析管连接到设备上。在患者睡眠的过程中,设备会进行多次的液体交换。早上醒来的时候便将腹透管与设备断开,白天与正常人一样可以自由行动。有这么多的优势,我自然会选择自动透析的方式。


理想很丰满,现实很残酷。透析后发现我的腹膜滲透能力不是太好。仅依赖夜间的透析机进行透析完全无法满足排出毒素和水分的需要。因此,在此基础上逐渐增加了白天的手动透析操作。到移植前,我白天需要进行 5 次手工操作,夜间进行 12 个小时的机器自动透析。已然创下了我所在地区的透析记录。


经过几个月的透析治疗,随着身体状况的逐步改善,家人便联系了移植医院,催促我进行移植手术。但是,处于某种考虑,当时我并没有太基于进行移植。在外人看来,透析是一种无趣、繁琐、束缚人的治疗方式,但对我来说,它是一种身心调理。在这几年的透析治疗中,我的心态发生了巨大的转变,变得更加平和从容。此外,透析还让我的生活变得十分有规律,为我日后的健康生活打下了基础。


移植


最终,在进行了四年的透析后,我选择进行肾移植。为了能够更好地应对这次手术,我在准备移植前已经进行了一年有针对性的锻炼。再加上之前有意无意地“错过”了几次移植的机会,在接到医院的电话时,我十分平静。内心很笃定手术会成功。


不过事情似乎没有想的那么顺利。手术当天还是出现了不理想的状况,本来只需要 4 到 6 个小时的手术进行了接近 10 个小时。而且在手术后的第五天,从很多指标上看,手术似乎出现了明显失败的迹象。最重要的是,医生怀疑移植肾出现了破碎的可能,于是又进行了一次手术。在第二次手术前,尽管从各方面的指标上来看问题不小,但我个人感觉异常良好,因此我是抱着十分轻松的心态接受第二次手术。事后从妻子的口中得知,当时医生已经让家人做最坏打算了。第二次手术进行的时间不比第一次少,而且同第一次一样,出现了十分严重的术后反应。经过 ICU 的洗礼,人没有太大的事情。好消息是,第二次手术确认了第一次手术没有太大的问题,移植肾也无大碍。


在突破了该医院移植的最长住院记录后,经过 35 天,我终于回到了家,进入了术后康复阶段。


康复


回到家后马上遇到了几个十分棘手的问题。


第一个是体能太差。这是因为在进行第二次手术后,创口部分又出现了一次比较严重的出血情况。为了加快速度,两个医生在加护病房中对创口进行了紧急处置。经过这次治疗,我被要求只能用一个姿势躺着。经过了 20 多天的卧床,虽然伤口问题不大了,但下肺萎缩严重。刚回到家一个月里,即使在静息的状态下,心率也在 100 以上。为了改善体能,我从手术后 2 个月便开始恢复体能锻炼,并将这个习惯一直保持至今,现在每天也会进行一到两个小时的健身。


第二个症状是神经震颤,这是药物他克莫司所具备的副作用,强度因人而异,而我显然是反应比较大的那个。当时手已经很难拿住筷子了,情况与帕金森症很类似。为了改善这种情况,我尝试每天写毛笔字,不是为了练字,主要是提高自己的控制力。随着药量的减少、身体对药物的逐步适应,在手术一年后,这个症状得到了明显的改善。目前除了做一些很精细的操作外,基本上看不出有什么异常。


第三点是脑子出现了问题。大量的麻醉以及短期内高剂量的激素治疗让我的思维出现了明显的窒塞。在手术后的三到四个月中,我几乎无法通过短信发出一段没有错误的文字,基本上是脑子知道想要什么,但表述总是有问题。好在自己对这种情况有清晰的认识,努力尝试通过阅读、思考、交流、学习来改善这种状况。学习 Swift、SwiftUI、Core Data 也是在这个背景下自己主动采用的一种治疗措施,想了解这段时间的情况,可以阅读 老人新兵 —— 一款 iOS APP 的开发手记一文。


应该说,几年的透析生活给了我相当大的帮助。在移植手术后的这几年,我保持了相当健康的生活方式和乐观的心态。经过数年的调养,身体的指标也越来越好。


这是我每个月经常性检查的一些指标,通过这些指标可以看出,我的肾功指标在移植后逐年好转直至正常。大多数移植患者的指标在移植后很短的时间(几天到几周)都会恢复正常,然后随着时间的推移逐渐再出现问题。而我至少到目前来看,指标还处于上升通道中。希望能够继续保持下去。


image-20230530142243400



只有经过长时间的积累,才能看出数据的价值。使用“健康笔记”App,我不仅对自己的身体指标有了清晰的认识,而且这些数据给我和医生提供了重要的参考指标,让我能有针对性地调整身体。如果你或你的家人、朋友需要长期跟踪健康数据,可以尝试使用该应用程序。请注意,该应用程序最初是为我自己编写的,对新手不够友好,但功能非常实用。



未来


尽管这些年,身体出现了或多或少的问题,不过我还是非常幸运的。遇到了善良的器官捐赠者,有最爱我的家人,总能转危为安的运气、一直碰到不错的医护,以及获得了很多朋友的支持。


明年我就到了知天命的年龄,希望能以平和的心态继续积极、健康地生活下去,做一些自己想做并且对社会有意义的事情。


祝大家身体健康。


作者:东坡肘子
来源:juejin.cn/post/7238999195825881144
收起阅读 »

你还在凭感觉来优化性能?

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 众所周知,感觉是最不靠谱的东西,这完全取决于主观意识。我们做性能优化一定要取决于一些指标,而Performance API向我们提供了访问和测量浏览器性能相关信息的方式。通...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


众所周知,感觉是最不靠谱的东西,这完全取决于主观意识。我们做性能优化一定要取决于一些指标,而Performance API向我们提供了访问和测量浏览器性能相关信息的方式。通过它,我们可以获取有关页面加载时间、资源加载性能、用户交互延迟等详细信息,用于性能分析和优化,而不是完全靠自己的感官意识,感觉更快了或者更慢了。


指标收集


1. Navigation Timing API - 页面加载时间


// 获取页面加载时间相关的性能指标
const navigationTiming = performance.timing;
console.log('页面开始加载时间: ', navigationTiming.navigationStart);
console.log('DOMContentLoaded 事件发生时间: ', navigationTiming.domContentLoadedEventEnd);
console.log('页面加载完成时间: ', navigationTiming.loadEventEnd);
console.log('页面从加载到结束所需时间',navigationTiming.loadEventEnd - navigationTiming.navigationStart)

2. Resource Timing API - 资源加载性能


// 获取资源加载性能数据
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log('资源 URL: ', resource.name);
console.log('资源开始加载时间: ', resource.startTime);
console.log('资源加载结束时间: ', resource.responseEnd);
console.log('资源加载持续时间: ', resource.duration);
});

3. User Timing API - 自定义时间点


// 标记自定义时间点
performance.mark('startOperation');
// 执行需要测量的操作

for(let i = 0;i < 10000;i++) {}

performance.mark('endOperation');
// 测量时间差
performance.measure('operationDuration', 'startOperation', 'endOperation');
const measurement = performance.getEntriesByName('operationDuration')[0];
console.log('操作执行时间: ', measurement.duration);
// 和console.time,console.timeEnd比较相似

4. Long Tasks API - 长任务性能


// 获取长任务性能数据
const longTasks = performance.getEntriesByType('longtask');
longTasks.forEach(task => {
console.log('任务开始时间: ', task.startTime);
console.log('任务持续时间: ', task.duration);
});

5. Navigation Observer API - 导航事件监测


// 创建 PerformanceObserver 对象并监听导航事件
const observer = new PerformanceObserver(list => {
const entries = list.getEntries();
entries.forEach(entry => {
console.log('导航类型: ', entry.type);
// navigate 表示页面的初始导航,即浏览器打开新的网页或重新加载当前网页。
// reload 表示页面的重新加载,即浏览器刷新当前网页。
// back_forward 表示通过浏览器的前进或后退按钮导航到页面。
console.log('导航开始时间: ', entry.startTime);
console.log('导航持续时间: ', entry.duration);
});
});
// 监听 navigation 类型的事件
observer.observe({ type: 'navigation', buffered: true });

6. LCP的采集


LCP(Largest Contentful Paint)表示最大内容绘制,指的是页面上最大的可见内容元素(例如图片、视频等)绘制完成的时间点。LCP反映了用户感知到的页面加载速度,因为它代表了用户最关注的内容何时变得可见。LCP 应在页面首次开始加载后的2.5 秒内发生。


new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Largest Contentful Paint:', entry.startTime);
  }
}).observe({type'largest-contentful-paint'bufferedtrue});


浏览器会多次报告 LCP ,而真正的 LCP 是用户交互前最近一次报告的 LCP。



7. FID的收集


FID(First Input Delay)表示首次输入延迟,衡量了用户首次与页面进行交互(例如点击按钮、链接等)时,响应所需的时间。较低的FID值表示页面对用户输入更敏感,用户可以更快地与页面进行交互,页面的 FID 应为100 毫秒或更短。


new PerformanceObserver(function(list, obs) {  
  const firstInput = list.getEntries()[0];
  const firstInputDelay = firstInput.processingStart - firstInput.startTime;
  const firstInputDuration = firstInput.duration;
  console.log('First Input Delay', firstInputDuration);
  obs.disconnect();
}).observe({type'first-input'bufferedtrue});

8. CLS的收集


CLS(Cumulative Layout Shift)表示累积布局偏移,衡量了页面在加载过程中发生的意外布局变化程度。当页面上的元素在加载过程中发生位置偏移,导致用户正在交互时意外点击链接或按钮,就会产生布局偏移。页面的 CLS 应保持在 0.1  或更少,这里的0.1表示10%。请注意,CLS 的计算可能涉及复杂的算法和权重计算,下列代码示例仅演示了基本的计算过程。


const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
let clsScore = 0;
entries.forEach(entry => {
// 计算每个布局变化的分数
clsScore += entry.value;
});
console.log('CLS 值: ', clsScore);
});

// 监听 Layout Shift 类型的条目
observer.observe({ type: 'layout-shift', buffered: true });

小结



  • 测量页面加载时间:性能 API 允许我们测量和分析网页加载所需的时间。通过使用性能计时指标,如 navigationStart、domContentLoadedEventEnd 和 loadEventEnd,我们可以准确测量页面加载过程中各个阶段的持续时间。

  • 分析资源加载性能:利用性能 API,我们可以检查网页上正在加载的各个资源(如图像、脚本、样式表)的性能。这包括跟踪资源加载时间、大小和状态码,有助于识别影响整体性能的瓶颈或问题。

  • 监测用户交互延迟:性能 API 使我们能够测量用户交互和浏览器响应之间的延迟。通过跟踪类似于 firstInputDelay(FID)和 firstInputTime 的指标,我们可以评估网页对用户操作(如点击或触摸)的响应速度,并确定改进的方向。

  • 基准测试和比较分析:性能 API 允许我们对网页的不同版本或不同网页的性能进行基准测试和比较分析。通过收集性能数据和指标,我们可以评估代码更改、优化或第三方资源对页面性能的影响,并做出明智的决策。

  • 性能优化和报告:利用性能 API 获得的洞察力,我们可以确定性能瓶颈和改进的方向。然后,可以使用这些信息来实施优化,例如减小文件大小、降低服务器响应时间、优化缓存策略和提高渲染
    作者:simple_lau
    来源:juejin.cn/post/7238779568478552122
    效率。

收起阅读 »

能把队友气死的8种屎山代码(React版)

web
前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气: 我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿: 于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。 以下是正文。...
继续阅读 »

前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气:


图片


图片


我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿:


图片


于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。


以下是正文。


(文字大部分是Henry所写,沐洒进行了一些精简和调整)




1. 直接操作DOM


const a = document.querySelector('.a');

const scrollListener = throttle(() => {
  const currentY = window.scrollY;

  if (currentY > 100) {
    a.classList.add('show');
  } else {
    a.classList.remove('show');
  }
}, 300);

window.addEventListener('scroll', scrollListener);
return () => {
  window.removeEventListener('scroll', scrollListener);
};

上面的代码在监听scroll方法的回调函数中,直接上手修改DOM的类名。众所周知,React属于响应式编程,大部份情况都不需要直接操作DOM,具体原因参考官方文档(react.dev/learn/manip…


优化方法也很简单,充分发挥响应式编程的优点,用变量代替即可:


const [refreshStatus, setRefreshStatus] = useState('');

const scrollListener = throttle(() => {
  if (tab.getBoundingClientRect().top < topH) {
    setRefreshStatus('show');
  } else {
    setRefreshStatus('');
  }
}, 300);

return <div className={['page_refresh', refreshStatus].join(' ')}/>;

2. useEffect不指定依赖


依赖参数缺失。


useEffect(() => {
    console.log('no deps=====')
    // code...
});

这样的话,每次页面有重渲染,该useEffect都会执行,带来严重的性能问题。例如我们项目中,这个useEffect内部执行的是第一点中的内容,即每次都会绑定一个scroll事件的回调,而且页面中有实时轮询接口每隔5s刷新一次列表,用户在该页面稍加停留,就会有卡顿问题出现。解决方案很简单,根据useEffect的回调函数内容可知,如果需要在第一次渲染之后挂载一个scroll的回调函数,那么就给useEffect第二个参数传入空数组即可,参考官方文档(react.dev/reference/r…


useEffect(() => {
    // code...
}, []);

3. 硬编码


硬编码,即一些数据信息或配置信息直接写死在逻辑代码中,例如


图片


这两行代码本意是从url上拿到指定的参数的值,如果没有,会用一个固定的配置做兜底。


乍一看代码逻辑很清晰,但再想深一层,兜底值具体的含义是什么?为什么要用这两个值来兜底?写这行代码的同学可能很快可以解答,但是一段时间之后,写代码的人和提需求的人都找不到了呢?


这个示例代码还比较简单,拿对应的值去后台可以找到对应的含义,如果是写死的是枚举值,而且还没有类型定义,那代码就很难维护了。


图片


解决此类问题,要么将这些内容配置化,即写到一个config文件中,使用清晰的语义化命名变量;要么,至少在硬编码的地方写上注释,交代清楚这里需要硬编码的前因后果。


4. 放任文件长度,只着眼于当下的需求


很多同学做需求、写代码都比较少从全局考虑,只关注到当前需求如何完成。从“战术”上来说没有问题,快速完成产品的需求、快速迭代产品也是大家希望看到的。


可一旦只关注“战术实现”而忽略“战略设计”,除非做的产品是月抛型的,否则一定会遇到旧逻辑难以修改的情况。


如果再加上一个文件被多达10余人修改过的情况,那么每改一行代码都会是一场灾难,例如最近接手的一个页面:


图片


单文件高达1600多行!哪怕去除300多行的注释,和300多行的模板,剩下的逻辑代码也有1000行左右,这种代码可读性就极其糟糕,必须进行拆分。


而很常见的是,由于每一任经手人都疏于考虑全局,导致大量代码毫无模块化可言,甚至出现多个useEffect的依赖是完全相同的:


图片


这里明显还有另一个问题:滥用hooks。


从行号可以看出来确实是相同的依赖写了多个useEffect,很明显是多个同学各写各的的需求引入的这些hooks。

这代码跑肯定是能跑的,但是很可能会出现多个hooks中修改同一个变量,导致其他地方在使用的时候需要搞一些很tricky的操作来修Bug。


5.变量无初始值


在typescript的加持下,对变量的类型定义可以说是日益严格了。可是在一些变量的类型定义比较复杂的情况下,可能一个变量的字段很多、层级很复杂,此时有些同学就可能想偷个懒了,例如:


const [variable, setVariable] = useState();

// some code...
const queryData = function({
    // some logic
    setVariable({ showtrue });
};

useEffect(() => {
    queryData();
}, []);

return variable.show ?  : null;

这里的问题很明显,如果queryData耗时比较长,在第一次渲染的时候,最后一行的variable.show就会报错了,因为variable的初始值是undefined。所以声明变量时,一定要根据变量的类型设置好有效默认值。


6. 三元选择符嵌套使用


网上很多人会推荐说用三元选择符代替简单的if-else,但几乎没有见过有人提到嵌套使用三元选择符的事情,如果看到如下代码,不知道各位读者会作何感想?


{condition1 === 1
    ? "数据加载中"
    : condition2
    ? "没有更多了"
    : condition3
    ? "当前没有可用房间"
    : "数据加载中"}

真的很难理解,明明只是一个简单的提示语句的判断,却需要拿出分析性能的精力去理解,多少有点得不偿失了。


这还只是一种比较简单的三元选择符的嵌套,因为当各个条件分支都为true时,就直接返回了,没有做更多的判断,如果再多做一层,都会直接把人的cpu的干爆炸了。 


替代方案: 



  1. 直接用if-else,可读性更高,以后如果要加逻辑也很方便。

  2. Early Return,也叫卫语句,这种写法能有效简化逻辑,增加可读性。


if (condition1 === 1return "数据加载中";
if (condition2) return "没有更多了";
if (condition3) return "当前没有可用房间";
return "数据加载中";

虽然不嵌套的三元选择符很简单,但是在例如jsx的模版中,仍然不建议大量使用三元选择符,因为可能会出现如下代码:


return (
    condition1 ? (
        <div className={condition2 ? cls1 : cls2}>
            {condition3 ? "111" : "222"}
            {condition4 ? (
                a : b} />
            ) : null
        

    ) : (
        
            {condition6 ? children1 : children2}
        

    )
)

类似的代码在我们的项目中频繁出现,模版中大量的三元选择符导致文件内容拉得很长,很容易看着看着就不记得自己在哪个逻辑分支上了。


像这种简单的三元选择符,做成一个简单的memo变量,哪怕是在组件内直接写变量定义(例如:const clsName = condition2 ? cls1 : cls2),最终到模板的可读性也会比上述代码高。


7. 逻辑不拆分


React hooks可以很方便地帮助开发者聚合逻辑抽离成自定义hooks,千万不要把一个页面所有的useState、useEffect等全都放在一个文件中:


图片


其实从功能上可以对页面进行拆分,拆分之后这些变量的定义也就可以拆出去了。其中有一个很简单的原则就是,如果一个逻辑同时涉及到了useState和useEffect,那么就可以一并抽离出去成为一个自定义hooks。例如接口请求大家一般都是直接在业务逻辑中做:


const Comp = () => {
    const [data, setData] = useState({});
    const [loading, setLoading] = useState(false);
    
    useEffect(() => {
        setLoading(true);
        queryData()
            .then((response) => {
                setData(response);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                setLoading(false);
            });
    });
    
    if (loading) return "loading...";
    
    return <div>{data.text}div>;
}

根据上面的原则,和数据拉取相关的内容涉及到了useState和useEffect,这整块逻辑就可以拆出去,那么最终就只剩下:


const Comp = () => {
    const { data, loading } = useQueryData();
    
    if (loading) return "loading...";
    
    return 
{data.text}
;
};

这样下来,Comp组件就变得身份清爽了。大家可以参考阿里的ahooks库,里面收集了很多前端常用的hooks,可以极大提升开发效率和减少重复代码。


8. 随意读取window对象的值


作为大型项目,很容易需要依赖别的模板挂载到window对象的内容,读取的时候需要考虑到是否有可能拿不到window对象上的内容,从而导致js报错?例如:


window.tmeXXX.a.func();

如果这个tmeXXX所在的js加载失败了,或者是某个版本中没有a这个属性或者func这个函数,那么页面就会白屏。


好啦,最近CR常出现的8种屎山代码都讲完了,你写过哪几种?你们团队的代码中又有哪些让你一口老血喷出来的不良代码呢?欢迎评论区告诉我。


作者:沐洒
来源:juejin.cn/post/7235663093748138021
收起阅读 »

关于“凌晨服务器告警!我被动把性能优化了2000%”这件事~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 大早上被叫醒! 前几天周末,大早上的时候,太阳才刚出来,我突然被老大电话叫醒了,并通知我速速进入飞书会议,说是服务器发生了警报,出现了严重事故。 进到会议才...
继续阅读 »

前言


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



大早上被叫醒!


前几天周末,大早上的时候,太阳才刚出来,我突然被老大电话叫醒了,并通知我速速进入飞书会议,说是服务器发生了警报,出现了严重事故。



进到会议才发现是我们的后端有一个接口,让监控直接红色了。由于这一块代码我比较熟,所以老大让我紧急定位并处理一下这个严重的问题~


定位问题


所以本全干工程师就开始了后端接口的问题定位~



初步定位


首先说说这个接口的背景,这个接口是提供给用户用作导入用的,用户在做导入的时候,有可能导入的数据超级大,所以会不会是因为导入的数据量太大了,所以导致此接口直接崩掉呢?


但是老大查了日志后跟我说,这个用户在以前也导入过这么大的数据量,但是那时候都没啥问题的啊~


所以我觉得这应该不是用户行为造成的,而是什么新功能造成的这个BUG~于是我看了涉及到此接口的最近的提交,发现确实是有一个新功能在最近上线了,是涉及到 json-schema 的


简单认识 json-schema


什么是 json-schema 呢?给大家举个小例子,让大家简单认识一下吧,比如下方的三个属性



  • name

  • age

  • cars


他们都有对应的数据类型,他们需要通过 json-schema 来寻找到自己对应的类型


// jsonschema
const schema = {
name: {
$ref: 1
},
age: {
$ref: 2
},
cars: {
$ref: 3
}
}

// jsonschema合集
const schemaDefination = {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
$ref: 4
},
4: {
type: 'array'
}
}

// 得出结果
const result = build(schema, schemaDefination)
console.log(result)
// {
// name: 'string',
// age: 'number',
// cars: 'array'
// }

继续定位问题


回到这个 BUG 上,我继续定位。其实刚刚上面的例子是很简单的例子,但是其实在这个功能里,数据量和负责度远远没这么简单,我们刚刚看到的 schemaDefination 其实就是所有 jsonschema 的引用的结合


// 实际上可能有几百个
const schema1 = {
$ref: 1
}
const schema2 = {
$ref: 2
}
const schema3 = {
$ref: 3
}

const schemaDefination = gen(schema1, schema2, schema3)
console.log(schemaDefination)
// 实际上可能有几百个
// {
// 1: {
// type: 'string'
// },
// 2: {
// type: 'number'
// },
// 3: {
// type: 'array'
// }
// }

也就是一开始会先根据所有 schema 去生成一个引用的集合 schemaDefination,而这个集合可能有几百个,数据量挺大


最终定位


然后到最终的时候 schema 结合 schemaDefination 去生成结果,我感觉就是在这一步导致了 BUG


// 得出结果
// 可能要 build 几百次
const result1 = build(schema1, schemaDefination)
const result2 = build(schema2, schemaDefination)
const result3 = build(schema3, schemaDefination)

为什么我觉得是这一步出问题呢?我们刚刚说了 schemaDefination 是所有 schema 的引用集合,数据量很大,你每次 build 的时候 schema 传的是一个 schema,但是你 schemaDefination 传的是集合!!!


正常来说应该是传 schema 时只需要传对应的 schemaDefination 即可,比如


// 合理的
const result1 = build({
$ref: 1
}, {
1: {
type: 'string'
}
})

// 不合理的
const result1 = build({
$ref: 1
}, {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
type: 'array'
}
})

而我们现在就是处于不合理的情况,于是我特地看了 build 这个函数的内部实现,发现有 对象序列化处理 的代码,想一下下面的模拟代码


const obj = { 几百个数据 }
while(i < 300) {
JSON.stringfy(obj)
i++
}

这样的代码会给服务器造成非常大的压力,甚至把接口给搞崩!!!


解决问题,性能提升几百倍!


上面其实我已经分析出问题所在了:传 schema 的时候不要传整个 Defination集合!,所以我们只需要传入所需的 defination, 那么性能是不是可以优化几百倍!!!


解决手段


所以我们只需要写一个函数,过滤出所需要的 defination 即可,例如


// 找出所有被 ref 的数据模型
const filter = (
schema,
schemaDefinitions,
) => {
// 进行过滤操作
}

// jsonschema
const schema = {
name: {
$ref: 1
}
}

// jsonschema合集
const schemaDefination = {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
$ref: 4
},
4: {
type: 'array'
}
}

// 过滤
const defination = filter(schema, schemaDefination)
console.log(defination)
//{
// 1: {
// type: 'string'
// },
//}

所以只需要在 build 的时候传入过滤后的 defination 即可!


const result1 = build(schema1, filter(schema1, schemaDefination))
const result2 = build(schema2, filter(schema2, schemaDefination))
const result3 = build(schema3, filter(schema3, schemaDefination))

测试无误,继续睡觉!


然后拿到一份用户的数据,在测试环境测了一下,没有发生之前那个 BUG 了!合并代码!打包上线!继续睡觉!



结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


image.png


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

对接了个三方支付,给俺气的呀

故事是这样的: 我们的商城需要在日本上线去赚小日子过得不错的日本人的钱,所以支付是首要的。就找了一家做日本本地支付的公司做对接,公司名字当然不能说,打我也不说。 第一天,很愉快,签了协议,给了开发文档。俺就准备开始撸代码了。 API文档 这开发文档,打开两秒钟...
继续阅读 »

故事是这样的:


我们的商城需要在日本上线去赚小日子过得不错的日本人的钱,所以支付是首要的。就找了一家做日本本地支付的公司做对接,公司名字当然不能说,打我也不说。


第一天,很愉快,签了协议,给了开发文档。俺就准备开始撸代码了。


API文档


这开发文档,打开两秒钟就自动挂掉了,我只能一次又一次的点击Reload


image.png


后来实在受不了了,我趁着那两三秒钟显示的时间,截图对照着看。结果就是所有的字段不能复制粘贴了,只能一个一个敲。


还是这个API文档,必输字段非必输字段乱的一塌糊涂,哪些是必输纯靠试,有的必输字段调试的时候有问题,对面直接甩过来一句,要么不传了吧。听得我懵懵的。


加密,验签


然后到了加密,验签。竟然能只给了node的demo,咱对接的大部分是后端程序员吧,没事,咱自己写。比较坑的是MD5加密完是大写的字母,这三方公司转小写了,也没有提示,折腾了一会,测了一会也就通了,还好还好。


场景支持


在之后就是支付的一个场景了,日本是没有微信支付宝这样的便捷支付的,要么信用卡,要么便利店支付



稍微说下便利店支付,就是说,客户下完单之后,会给到一个回执,是一串数字,我们且称之为支付码,他们便利店有一个类似于ATM机的柜员机,去这个机子上凭借这串支付码打印出来一个凭条,然后拿着这个凭条去找便利店的店员,现金支付



就是这个场景,就是这个数字,三方它就给客户显示一次,就一次,点击支付的时候显示一次。要是客户不截图,那么不好意思,您就重新下单吧。而且这个支付码我们拿不到,这个跳转了他们的页面,也不发个邮件告知。这明显没法用啊,我们的订单过期时间三天,客户这三天啥时候想去便利店支付都行,可是这只显示一次太扯了。

同样的请求再发一次显示的是支付进行中。这怎么玩,好说歹说他们排期开发了两周,把这个订单号重入解决了,就是说同一笔订单再次进入是可以看到那串支付码的。


测试环境不能测


最后,写完了,调通了,测一下支付,结果他们测试环境不支持日本的这个便利店支付测试,what? 测试环境不能测试?那我这么久在干什么,让我们上线上测,我的代码在测试环境还没搞完,让我上生产,最后上了,没测通,对方的问题,当天下午就把代码给回滚了。等着对方调试完继续测。


业务不完整


还有,不支持退款,作为一个支付公司,不支持退款,我们客户退款只能线下转账,闹呢。


以前对接三方的时候,遇到问题地想到的是我们是不是哪里做错了,再看看文档。对接这个公司,就跟他们公司的测试一样,找bug呢。


建议


这里是本文的重点,咱就是给点建议,作为一家提供服务的公司,不管是啥服务。



  1. 对外API文档应当可以正常显示,必输非必输定义正确,字段类型标注准确。

  2. 若是有验签的步骤,介绍步骤详细并配上各个语言的demo,并强调格式以及大小写。

  3. 牵扯到业务的,需要站在客户的角度思考,这个是否合情合理。

  4. 业务的完整性,有可能是尚未开发完全,但是得有备选的方案。


作者:奔跑的毛球
来源:juejin.cn/post/7127691522010529799
收起阅读 »

让整个网站界面无滚动条

web
界面无滚动条 滚动条的优化也有很多种,比如随便再网上搜索美化浏览器滚动条样式,就会出现些用css去美化滚动条的方案。 那种更好呢? 没有更好只有更合适 像默认的滚动条的话,他能让你方便摁着往下滑动(他比较宽)特别省劲,不用担心美化过后变细摁不到问题。 美化...
继续阅读 »

界面无滚动条


滚动条的优化也有很多种,比如随便再网上搜索美化浏览器滚动条样式,就会出现些用css去美化滚动条的方案。


那种更好呢?


没有更好只有更合适



  • 像默认的滚动条的话,他能让你方便摁着往下滑动(他比较宽)特别省劲,不用担心美化过后变细摁不到问题。

  • 美化后的滚动条样式啊更贴合网站主题,让用户体验更好。

  • 无滚动条(鼠标放上去后出现)这种更适合像一个页面好多个块,每个块的话还很多内容(都有滚动条)。如果像这种都默认都出现滚动条的话,也太不美观了。



那咱们就从无滚动条展开说说!!!



无滚动条设计



比如像element ui组件内像input的自定义模块数据过多的时候出现的下拉框内的滚动条,如下图:



element-ui里面它其实是有内部组件el-scrollbar在的。那么它是怎么实现无滚动条呢?


如下图咱们先把:hover勾选上让滚动条一直处于显示得状态。然后咱们再分析他的实现。


当我把样式稍微修改下,咱们再观察下图:


01.jpg


这么看是不是就很明白了 他其实用margin值把整个容器扩大了点然后溢出隐藏,其实滚动条还在就是给界面上看不到了而已。


然后它自己用dom画了个滚动条,如下图:


02.jpg



经过上面分析,咱们已经很清楚得了解到一个无滚动条是从那个方面实现得了。



  1. 使用margin值把滚动条给溢出隐藏掉。

  2. 使用div自己画了一个滚动条。方便咱们隐藏、显示、更改样式等。



无滚动条实现


那咱们再从细节上拆分下具体实现要考虑那些点:



  1. 需要计算滚动条得宽度用来margin扩大得距离(每个界面上得滚动条得宽度是不一样得)。

  2. 需要计算画的div滚动条得高度(这个内容多少会影响滚动条的高度)。

  3. 需要根据滚动条去transform: translateY(19.3916%);移动咱们自己画的div滚动条的。

  4. 需要根据摁着画的div滚动条,去实际更改需要滚动的高度。

  5. 需要点击滚动轴的柱子支持跳到目标的滚动位置;


一 计算界面原本滚动条的宽度



计算下界面上原本滚动条的宽度如下:



let scrollBarWidth;

export default function() {
if (scrollBarWidth !== undefined) return scrollBarWidth;

const outer = document.createElement('div');
outer.className = 'el-scrollbar__wrap';
outer.style.visibility = 'hidden';
outer.style.width = '100px';
outer.style.position = 'absolute';
outer.style.top = '-9999px';
document.body.appendChild(outer);

const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';

const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);

const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
scrollBarWidth = widthNoScroll - widthWithScroll;

return scrollBarWidth;
};


先创建了一个div, 设置成scroll, 然后再在里面嵌套一个没有滚动条的div设置宽度100%, 获取到两者的offsetWidth, 相减获取到scrollBarWidth赋值给scrollBarWidth 是惰性加载的优化,只需要计算一次就可以了。 具体展现如下图:



03.jpg


二 计算画的滚动条的高度height



计算下画的div滚动条的高度height。是用当前容器的内部高度 / 容器整个滚动条的高度 * 100计算出百分比;



比如:


const warp = this.$refs.wrap; // 或者使用documnet获取容器
const heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight); // height
const widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth); // width


解析: 如当前容器高30px,内容撑起来总共高100px,那么滚动条的高度就是当前容器的30%;



三 计算滚动条需要移动的值



计算画的div需要滚动条的高度moveY是, 获取当前容器滚动的scrollTop / 当前容器内部高度 * 100



算法一:



解析 使用transform: translateY(0%);是移动的是自己本身的百分比那么(容器滚动的scrollTop / 当前容器内部高度 * 100)算法如下:



const warp = this.$refs.wrap; // 或者使用documnet获取容器
this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);

算法二:



解析:使用定位top值,这个比较好理解滚动条的滚动 / 容器的滚动总高度 * 100得到百分比,如下:



const warp = this.$refs.wrap; // 或者使用documnet获取容器
this.moveY = ((wrap.scrollTop * 100) / wrap.scrollHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.scrollWidth);


把计算出来的moveYmoveX的方法 绑定给scroll 滚动事件就可以了。



四 摁着画的div滚动条 经行拖动



滚动条都是支持拖着上下左右移动的,那咱们也要支持下:




  • 获取当前滚动条的高度或者宽度可以使用getBoundingClientRect()如下图:

  • 获取拖着移动的距离 就是再鼠标摁下先计一个当前的x1、y1监听movex2、y2相减就是拖动的距离了。

  • 获取到拖动的距离后转成transform || top值。


一个简单的拖动组件如下:


<template>
<div
ref="draggableRef"
class="draggable"
:style="style"
>
<slot />
</div>
</template>

<script>
export default {
name: 'DraggableComponent',

props: {
initialValue: {
type: Object,
required: false,
default: () => ({ x: 0, y: 0 }),
},
},

data() {
return {
currentValue: { x: 0, y: 0 },
isDragging: false,
startX: 0,
startY: 0,
diffX: 0,
diffY: 0,
};
},

computed: {
style() {
return `left: ${this.currentValue.x + this.diffX}px; top: ${this.currentValue.y + this.diffY}px`;
},
},

watch: {
initialValue: {
handler(val) {
this.currentValue = val;
},
immediate: true,
},
},

mounted() {
this.$nextTick(() => {
const { draggableRef } = this.$refs;
if (draggableRef) {
draggableRef.addEventListener('mousedown', this.startDrag);
document.addEventListener('mousemove', this.moveDrag);
document.addEventListener('mouseup', this.endDrag);
}
});
},

beforeDestroy() {
const { draggableRef } = this.$refs;
draggableRef.removeEventListener('mousedown', this.startDrag);
document.removeEventListener('mousemove', this.moveDrag);
document.removeEventListener('mouseup', this.endDrag);
},

methods: {
startDrag({ clientX: x1, clientY: y1 }) {
this.isDragging = true;
document.onselectstart = () => false;
this.startX = x1;
this.startY = y1;
},

moveDrag({ clientX: x2, clientY: y2 }) {
if (this.isDragging) {
this.diffX = x2 - this.startX;
this.diffY = y2 - this.startY;
}
},

endDrag() {
this.isDragging = false;
document.onselectstart = () => true;
this.currentValue.x += this.diffX;
this.currentValue.y += this.diffY;
this.diffX = 0;
this.diffY = 0;
},
},
};
</script>

<style>
.draggable {
position: fixed;
z-index: 9;
}
</style>


咱们需要获取到画的滚动条的高度,然后根据拖动的距离算出来transform: translateY(0%);或者top值;

如上面拖动组件 拖动部分代码就不在重复了 咱们直接用diffX、diffY、lastX、lastY来用了。



  • diffX、diffY 是拖动差的值

  • lastX、lastY 是上一次也就是未拖动前的值translateY || top



算法一(transform)


const thumb = document.querySelector('el-scrollbar__thumb'); // element ui  el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};


const diffY = 10;
const lastY = '300'; // transform: translateY(300%);`
const moveY = (diffY / thumbHeight) + lastY;

算法二(top)


const thumb = document.querySelector('el-scrollbar__thumb'); // element ui  el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};


const diffY = 10;
const lastY = 30; // top: 30%`
const moveY = (diffY / wrap.scrollWidth * 100) + lastY;

五 点击滚动轴使滚动条跳转到该位置



  • getBoundingClientRect 的 top 是获取到距离浏览器顶部的距离。
    写一个点击事件如下


function clickTrackHandler(event) {
const wrap = this.$refs.wrap;
// 1. 减去clientX 正好能获取到需要滚动到的位置
const offset = Math.abs(e.target.getBoundingClientRect().top - e.clientX);

// 2. 利用offset 的到画的滚动条的实际位置 两种算法transform || top
const thumb = document.querySelector('el-scrollbar__thumb'); // element ui el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};

const translateY = offset / height * 100; // transform
const top = offset / wrap.scrollHeight * 100; // top

// 3、计算实际需要滚动的高度 使界面滚动到该位置。两种算法transform(scrollTop2) || top(scrollTop1)
const scrollTop1 = top * wrap.scrollHeight; // top
const scrollTop2 = translateY * wrap.clientHeight; // transform
}

总结



针对无滚动条如果是vue使用的话,再了解具体实现后可以直接用elementel-scrollbar组件就好,如果在其他框架中, 结合上面的逻辑也会很快封装一个组件。



作者:三原
来源:juejin.cn/post/7227033124856135738
收起阅读 »

不想写代码的程序员可以尝试的几种职位

标题不够严谨,应该是不想写业务代码的程序员可以做什么。 这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。 这里也主要分享 IT 行业内的岗位,...
继续阅读 »

标题不够严谨,应该是不想写业务代码的程序员可以做什么。


这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。


这里也主要分享 IT 行业内的岗位,要是除开行业限制,范围就太大了。


Developer Relation/Advocate


国外有很多面向开发者的技术创新公司,比如 Vercel ,PlanetScale ,Prisma ,Algolia 等。


这类公司的用户就是开发者,所以他们的市场活动也都是围绕着开发者;他们需要让更多的开发者可以更容易地把他们的技术用到他们的技术栈里,所以就有了这种岗位。用中文来表达,可能有点类似是布道师的意思?


国内更多是将技术应用起来,而不是创造一些新的技术,所以这种岗位在国内就非常少见了。当然近几年也还是有一些技术驱动型公司的,像 PingCAP ,Agora 等。


希望国内有更多像这样的公司出来。


Technical Recruiter


这个工作从 title 上就大概知道是做什么的了。


这个岗位有深有浅,深的可能是比较完整的招聘职能,浅的可能就是 HR 部门里面试和筛选技术候选人的。


Technical Writer


这个听着像是产品经理的工作,确实会和产品的职责有小部分重叠。


这是个面向内部的岗位,不喜欢对外对用户 /客户的朋友会非常喜欢。通常是一些比较大型的企业要做软件迁移或者什么系统、流程升级之类的时候,因为会牵扯到非常多的 moving parts ,所以通常都需要一个独立岗位来负责 documentation 的工作。


工作内容包括采访以及记录各部门的现有流程和业务需求,然后是新流程 /系统 /软件的手册、图表等等。


这里的“technical”不是我们研发中的技术,更多是“业务”层面的意思。同样这个岗位对技术要求不高,但是有研发背景是非常加分的。


Technical Support


通常这个岗位归属客服部门,高于普通 customer service rep 。普通的 customer support 是客户遇到问题时的第一层支持 - 基本会讲话、了解产品就能干的工作;如果第一层解决不了客户的问题,就会升级到后面 technical support 了。


这个岗位范围会更广一点,几乎任何 IT 公司都会有这种支持岗;对技术要求根据不同公司而不同,比如 Vercel 对这个岗位的技术要求肯定比 HelpScout (一个客服软件)要高。


但整体来说都不如研发要求高,但对应的薪酬待遇也没有研发那么好。


结语


其实说了这么多总结下来就是国外技术生态、开源氛围好很多,并且对技术足够的重视,促使很多技术公司的出现,然后催生了这些工作。


如果觉得本帖有启发,欢迎留言支持鼓励后续的创作。


其他相关文章


《找海外工作时可以做的一

些提前准备》

收起阅读 »

代码优雅之道——如何干掉过多的if else

1、前言 注意标题是过多的,所以三四个就没必要干掉了。实际开发中我们经常遇到判断条件很多的情况,比如下图有20多种情况,不用想肯定是要优化代码的,需要思考的是如何去优化? 网上很多说用switch case啊,首先不比较if else与switch case...
继续阅读 »

1、前言


注意标题是过多的,所以三四个就没必要干掉了。实际开发中我们经常遇到判断条件很多的情况,比如下图有20多种情况,不用想肯定是要优化代码的,需要思考的是如何去优化?



网上很多说用switch case啊,首先不比较if else与switch case效率问题的,只从代码整洁度来看二者没啥区别啊!我们这里更重要的是代码整洁度问题,为什么呢?来看下文的比较。


2、If else与switch case效率真的差距很大么?


网上有两种见解:


第一种是说switch…case会生成一个跳转表来指示实际的case分支的地址,而这个跳转表的索引号与switch变量的值是相等的。从而,switch…case不用像if…else那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而到达定位分支的目的。简单来说就是以空间换时间


第二种是说二者效率上差距并不大



于是我们自己去体验一下,不存在复杂业务逻辑,仅仅比较两种方式的效率:

    @Test
void contextLoads() {
testIf(100000);
System.gc();
testSwitch(100000);
}

private void testIf(Integer param) {
long start = System.currentTimeMillis();
for (int i = 0; i < param; i++) {
if (i == param-1){
System.out.println("if判断100000次");
}
}
long end = System.currentTimeMillis();
long total = end - start;
System.out.println("Test消耗时间:" + total);
}

private void testSwitch(Integer param){
long start = System.currentTimeMillis();
for (int i = 0; i < param; i++) {
switch (i){
case 99999:
System.out.println("switch判断100000次");
break;
}
}
long end = System.currentTimeMillis();
long total = end - start;
System.out.println("Test消耗时间:" + total);
}


可见差距并不大。而情况太多的时候谁还会去用if else和switch case呢?下面还是对两种方式的使用场景做简单的分析:


if else能够把复杂的逻辑关系表达得清晰、易懂,包容了程序执行的各种情况。


switch不适合业务系统的实际复杂需求,业务不断的变更迭代,一更改需求,条件的复杂度高了,switch无力处理。switch经常忘记写break,估计很多人一不小心就忘记写了。switch…case只能处理case为常量的情况。当情况不大于5种并且单一变量的值(如枚举),此时我们就可以使用switch,它的可读性比if条件更清晰。


除了上述说到枚举的这种场景,建议使用switch,其他个人愚见:只要情况不大于5种就直接使用if else


3、策略+工厂模式


上述说到情况较少时并且业务逻辑不复杂的使用if else可以让代码清晰明了。当每种情况对应的业务逻辑复杂时,建议使用策略+工厂模式。这里我们举个栗子:厂家每个季度要举行不同的活动,我们使用策略工厂模式来实现


策略接口

public interface Strategy {

/**
* 处理各种活动
* @return
*/
String dealActivity();
}

然后春夏秋冬四季活动类实现该接口


@Service
public class SpringActivity implements Strategy{
@Override
public String dealActivity() {
return "春季活动逻辑";
}
}

策略类工厂

public class StrategyFactory {
public static Strategy execute(Integer levelCode){
Strategy strategy = null;
switch (levelCode){
case 1:
strategy = new SpringActivity();
break;
case 2:
strategy = new SummerActivity();
break;
case 3:
strategy = new AutumnActivity();
break;
case 4:
strategy = new WinterActivity();
break;
default:
throw new IllegalArgumentException("活动编号错误");
}
return strategy;
}
}

然后在service层中传入对应的编码即可 ,我这里省略了service

@RestController
public class TestController {

@PostMapping("/dealActivity")
public String dealActivity(Integer code){
Strategy strategy = StrategyFactory.execute(1);
return strategy.dealActivity();
}
}


上述已经干掉了if else ,后续季度活动调整去修改对应活动策略类中逻辑即可。缺点:如果情况比这多,那么策略类会越来越多,也就是所谓的策略类膨胀,并且没有****没有一个地方可以俯视整个业务逻辑。


4、Map+函数式接口


将上述策略类全部作为方法

@Service
public class ActivityStrategyService {

public String dealSpringActivity(){
return "春季活动逻辑";
}

public String dealSummerActivity() {
return "夏季活动逻辑";
}

public String dealAutumnActivity() {
return "秋季活动逻辑";
}

public String dealWinterActivity() {
return "冬季活动逻辑";
}
}

再写个活动Service

@Service
public class ActivityService {

@Autowired
private ActivityStrategyService activityStrategyService;

@FunctionalInterface
interface ActivityFunction<A>{
//这里可以传参啊,我这里举例用不上参数
//String dealActivity(A a);
String dealActivity();
}

private final Map<Integer, ActivityFunction> strategyMap = new HashMap<>();

/**
* 初始化策略
*/
@PostConstruct
public void initDispatcher(){
strategyMap.put(1,()->activityStrategyService.dealSpringActivity());
strategyMap.put(2, ()-> activityStrategyService.dealSummerActivity());
strategyMap.put(3, ()-> activityStrategyService.dealAutumnActivity());
strategyMap.put(4, ()-> activityStrategyService.dealWinterActivity());
}

public String dealActivity(Integer code){
ActivityFunction<Integer> function = strategyMap.get(code);
//这里防止活动编号没匹配上,可以使用断言来判断从而抛出统一异常
return function.dealActivity();
}

}

改变Controller

@RestController
public class TestController {

@Autowired
private ActivityService activityService;

@PostMapping("/dealActivity")
public String dealActivity(Integer code){
// Strategy strategy = StrategyFactory.execute(1);
// return strategy.dealActivity();
return activityService.dealActivity(code);
}
}

作者:LoneWalker
链接:https://juejin.cn/post/7225056158981488699
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android渠道包自动更新

一、背景 转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题: (1)Android渠道包提交应用市场审核,工作重复&人工成本高   (2)公司目前存在多个APP、需...
继续阅读 »

一、背景


转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题:


(1)Android渠道包提交应用市场审核,工作重复&人工成本高  


(2)公司目前存在多个APP、需更多人支持,有培训成本


(3)每次发版需要人工通知项目成员渠道包审核进度 


  针对以上问题,我们设计开发了渠道包自动更新后台,用来解决渠道更新的效率问题。


二、方案调研


1、基于业务现状,做了技术调研和逻辑抽象


  不同APP支持的渠道不同,不同渠道更包api不同,如下图:


图片


针对以上调研结果,我们将通用的逻辑统一封装开发,将差异点进行配置,做到灵活配置可扩展。


2、整体的实现方案演变


初期方案,每个应用市场单独提审(需要先选择物料,选好物料后上传包文件,文件上传成功后再点击提交审核),多个应用市场需要重复该操作。


图片


上线运行了一段时间后,发现存在一些问题:单个市场提交步骤繁琐、多个应用市场需要分开多次提交。这些步骤是重复且可简化的,因此我们又对提审的过程做了封装,提供批量上传的入口,简化交互过程,做到一键提审。以下是当前运行的第二版方案:


图片


第二版方案上线后,提审同学只需要在入口处选择要更新的应用市场,然后一键上传全部物料,再点击提审按钮即可提审成功。代码内部会处理具体的逻辑,比如:根据配置规则将物料匹配到对应市场、自动匹配包文件进行提审。


三、方案设计


自动上传包含以下核心模块:



  • APP管理:支持配置多个APP信息,包括转转、找靓机、采货侠等

  • 包管理:支持下载不同渠道,不同版本的包

  • 物料管理:包括历史物料的选择,和新增物料的存储(icon、市场截图)

  • 提交审核:包括包下载、物料下载,支持按照APP配置账号密码提交审核

  • 消息提醒:对提交的结果和审核的结果进行消息通知


图片


实现效果:


提审前信息确认,选择APP,可选择单个或者多个渠道,系统自动选择包地址,用户选择物料后可一键提审多应用市场。操作简单便捷,使用成本低


图片


提审后发送消息通知,便于各方了解渠道的审核结果,对审核异常信息进行及时干预。同时自动存储不同版本的审核记录,方便后续分析。


图片


四、总结


渠道包自动更新功能,节省了大量的提交审核人力成本,打通了Android整体的持续交付过程,降低了人工学习成本。之后我们也会针对各种体验问题进行不断的改进和更新~


作者:转转技术团队
链接:https://juejin.cn/post/7238917620850147383
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

理解Kotlin中的reified关键字

标题:理解Kotlin中的reified关键字 摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。 正文: 什么是reified关键字? 在Kotli...
继续阅读 »

标题:理解Kotlin中的reified关键字


摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。


正文:


什么是reified关键字?


在Kotlin中,reified是一个特殊的关键字,用于修饰内联函数中的类型参数。这使得在函数内部可以访问类型参数的具体类型。通常情况下,由于类型擦除(type erasure),在运行时是无法直接获取泛型类型参数的具体类型的。reified关键字解决了这个问题。


使用reified关键字的条件


要使用reified关键字,需要遵循以下几点:



  1. 函数必须是内联的(使用inline关键字修饰)。

  2. 类型参数前需要加上reified关键字。


示例:reified关键字的用法


下面是一个使用reified关键字的简单示例:

inline fun <reified T> checkType(value: Any) {
if (value is T) {
println("Value is of type T.")
} else {
println("Value is NOT of type T.")
}
}

fun main() {
val stringValue = "Hello, Kotlin!"
val intValue = 42

checkType<String>(stringValue) // 输出 "Value is of type T."
checkType<String>(intValue) // 输出 "Value is NOT of type T."
}

在这个示例中,我们定义了一个内联函数checkType,它接受一个reified类型参数T。然后,我们使用is关键字检查传入的value变量是否为类型T。在main函数中,我们用不同的类型参数调用checkType函数来验证它的功能。


获取类型参数的Java类


当你使用reified关键字修饰一个内联函数的类型参数时,你可以通过T::class.java获取类型参数对应的Java类。这在需要访问泛型类型参数的具体类型时非常有用,比如在反射操作中。


下面是一个简单的例子:

import kotlin.reflect.KClass

inline fun <reified T : Any> getClass(): KClass<T> {
return T::class
}

inline fun <reified T : Any> getJavaClass(): Class<T> {
return T::class.java
}

fun main() {
val stringKClass = getClass<String>()
println("KClass for String: $stringKClass") // 输出 "KClass for String: class kotlin.String"

val stringJavaClass = getJavaClass<String>()
println("Java class for String: $stringJavaClass") // 输出 "Java class for String: class java.lang.String"
}

在这个示例中,我们定义了两个内联函数,getClassgetJavaClass,它们都接受一个reified类型参数TgetClass函数返回类型参数对应的KClass对象,而getJavaClass函数返回类型参数对应的Java类。在main函数中,我们用String类型参数调用这两个函数,并输出结果。


注意事项


需要注意的是,reified关键字不能用于非内联函数,因为它们的类型参数在运行时会被擦除。此外,reified类型参数不能用于普通类和接口,只能用于内联函数。


总结


Kotlin中的reified关键字允许我们在内联函数中访问类型参数的具体类型。它在需要访问泛型类型参数的场景中非常有用,例如在反射操作中。本文通过实例介绍了如何使用reified关键字,并讨论了相关注意事项。希望这些示例能够帮助您更好地理解和应用reified关键字。


作者:就不呵呵呵
链接:https://juejin.cn/post/7225457156816355365
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

既然有Map了,为什么还要有Redis?

一、同样是缓存,用map不行吗? Redis可以存储几十个G的数据,Map行吗? Redis的缓存可以进行本地持久化,Map行吗? Redis可以作为分布式缓存,Map只能在同一个JVM中进行缓存; Redis支持每秒百万级的并发,Map行吗? Redis有...
继续阅读 »

一、同样是缓存,用map不行吗?



  1. Redis可以存储几十个G的数据,Map行吗?

  2. Redis的缓存可以进行本地持久化,Map行吗?

  3. Redis可以作为分布式缓存,Map只能在同一个JVM中进行缓存;

  4. Redis支持每秒百万级的并发,Map行吗?

  5. Redis有过期机制,Map有吗?

  6. Redis有丰富的API,支持非常多的应用场景,Map行吗?



二、Redis为什么是单线程的?



  1. 代码更清晰,处理逻辑更简单;

  2. 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;

  3. 不存在多线程切换而消耗CPU;

  4. 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善;


三、Redis真的是单线程的吗?



  1. Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;

  2. Redis内部使用了基于epoll的多路服用,也可以多部署几个Redis服务器解决单线程的问题;

  3. Redis主要的性能瓶颈是内存和网络;

  4. 内存好说,加内存条就行了,而网络才是大麻烦,所以Redis6内存好说,加内存条就行了;

  5. 而网络才是大麻烦,所以Redis6.0引入了多线程的概念,

  6. Redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。


四、Redis优缺点


1、优点



  1. Redis是KV数据库,MySQL是关系型数据库,Redis速度更快;

  2. Redis数据操作主要在内存中,MySQL主要将数据存储在硬盘,Redis速度更快;

  3. Redis同样支持持久化(RDB+AOF),Redis支持将数据异步将内存的数据持久化到硬盘上,避免Redis宕机出现数据丢失的问题;

  4. Redis性能极高,读的速度是110000次/秒,写的速度是81000次/秒;

  5. Redis数据类型丰富,不仅支持KV键值对,还支持list、set、zset、hash等数据结构的存储;

  6. Redis支持数据的备份,即master-slave模式的数据备份;

  7. Redis支持简单的事务,操作满足原子性;

  8. Redis支持读写分离,分担读的压力;

  9. Redis支持哨兵模式,实现故障的自动转移;

  10. 单线程操作,避免了频繁的上下文切换;

  11. 采用了非阻塞I/O多路复用机制,性能卓越;


2、缺点



  1. 数据存储在内存,容易造成数据丢失;

  2. 存储容量受内存的限制,只能存储少量的常用数据;

  3. 缓存和数据库双写一致性问题;

  4. 用于缓存时,容易出现内存穿透、缓存击穿、缓存雪崩的问题;

  5. 修改配置文件后,需要进行重启,将硬盘中的数据同步到内存中,消耗的时间较长,而且数据同步的时间里Redis不能提供服务;


五、Redis常见业务场景



  1. Redis是基于内存的nosql数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作

  2. 通过list取最新的N条数据

  3. 模拟类似于token这种需要设置过期时间的场景

  4. 发布订阅消息系统

  5. 定时器、计数器

  6. 缓存加速、分布式会话、排行榜、分布式计数器、分布式锁;

  7. Redis支持事务、持久化、LUA脚本、发布/订阅、缓存淘汰、流技术等特性;


六、Redis常见数据类型



1、String


(1)String简介


String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。


(2)应用场景


① 作为缓存数据库


在Java管理系统体系中,大多数都是用MySQL存储数据,redis作为缓存,因为Redis具有支撑高并发的特性,通常能起到加速读写和降低数据库服务器压力的作用,大多数请求都会先请求Redis,如果Redis中没有数据,再请求MySQL数据库,然后再缓存到Redis中,以备下次使用。



② 计数器


Redis字符串中有一个命令INCR key,incr命令会对值进行自增操作,比如CSDN的文章阅读,视频的播放量,都可以通过Redis来计数,每阅读一次就+1,同时将这些数据异步存储到MySQL数据库中,降低MySQL服务器的写入压力。


③ 共享session


在分布式系统中,用户每次请求一般会访问不同的服务器 ,这就会导致session不同步的问题,这时,一般会使用Redis来解决这个问题,将session存入Redis,使用的时候从Redis中取出就可以了。


④ 分布式锁



  1. setnx key value,加锁

  2. del key,释放锁


(3)key操作命令



(4)set key value


SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]



  1. EX seconds,设置过期时间,单位秒

  2. PX milliseconds,设置过期时间,单位毫秒

  3. EXAT timestamp-seconds,设置过期时间,以秒为单位的UNIX时间戳

  4. PXAT timestamp-milliseconds,设置过期时间,以毫秒为单位的UNIX时间戳

  5. NX,键不存在的时候设置键值

  6. XX,键存在的时候设置键值

  7. KEEPTTL,保留设置前指定键的生存时间

  8. GET,返回指定键原本的值,若键不存在返回nil


备注:


命令不区分大小写,而key是区分大小写的。


help @类型:查看当前类型相关的操作命令。


Since the SET command options can replace SETNX, SETEX, PSETEX, GETSET, it is possible that in future versions of Redis these commands will be deprecated and finally removed。


(5)同时设置多个键值


(6)获取指定区间范围内的值


getrange、setrange。


(7)数值增减



  1. INCR key,递增数字

  2. INCRBY key increment,增加指定的数值递增

  3. DECR key,递减数值

  4. DECRBY key decrement,指定指定的数值递减


(8)获取字符串的长度,内容追加



  1. STRLEN key,获取值的长度

  2. APPEND key value,内容追加


2、List


(1)List 列表简介


List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。


列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。


主要功能有push/pop,一般用在栈、队列、消息队列等场景。



  1. left、right都可以插入添加;

  2. 如果键不存在,创建新的链表;

  3. 如果键存在,新增内容;

  4. 如果值全部移除,对应的键也会消失;


它的底层是双向链表,对两端的操作性能很高,通过索引下标操作中间的节点,性能会较差。


(2)应用场景


① 消息队列


使用 lpush + rpop或者 rpush + lpop实现消息队列,Redis还支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列。



② 作为栈使用


使用 lpush+lpop或者 rpush+rpop实现栈。



③ 文章列表


(3)常用命令



3、Hash


(1)hash简介


Hash 是一个键值对(key - value)集合,value也是一个hash,相当于 Map<String,Map<Object,Object>>


(2)常用场景


由于特殊的数据结构,hash一般作为存储bean使用,String+JSON的数据结构存储特定的应用场景。



(3)常用命令




4、Set


(1)Set类型简介


Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。


一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。


(2)应用场景


① 相同好友可见


在朋友圈场景中,对于点赞、评论的功能,通过交集实现相同还有可见的功能。


② 共同关注、共同喜好


③ 抽奖功能


(3)常用命令



5、Zset


(1)Zset 类型简介


Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。


有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。


zset k1 score1 v1 score2 v2


(2)应用场景


① 排行榜


通过score来记录点赞数,然后根据score进行排序,实现排行榜的功能。


② 延迟消息队列


订单系统,下单后需要在15分钟内进行支付操作,否则自动取消订单。


将下单后15分钟后的时间作为score,订单作为value存入Redis,消费者轮询去消费,如果消费的大于等于score,则取消该订单。


(3)Zset常用命令



6、BitMap


(1)Bitmap简介


Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。


(2)应用场景


由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。


① 签到统计


② 判断用户是否登录


③ 统计连续学习打卡的人


(3)BitMap常用命令



7、BitField


通过bitfield命令可以一次性操作多个比特位,它会执行一系列操作并返回一个响应数组,这个数组中的元素对参数列表中的相应操作的执行结果。


8、HyperLogLog


(1)HyperLogLog简介


Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。


所以,简单来说 HyperLogLog 提供不精确的去重计数。


HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。


在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。


(2)应用场景


百万级网页 UV 计数


(3)常用命令



  1. pfadd key element,添加元素

  2. pfcount key,返回指定HyperLogLog的基数的估算值;

  3. pfmerge destkey sourcekey,将多个HyperLogLog合并成一个HyperLogLog;


9、GEO


(1)GEO简介


Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。


在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。


(2)应用场景


高德地图、滴滴打车等定位软件。


(3)常用命令



10、Stream


(1)Stream简介


Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。



在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:



  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;

  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。


基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。


(2)应用场景


消息队列


(3)常用命令



七、总结


Redis是一个key-value存储系统,支持10种数据类型,总结了为何要用Redis替代map作为程序缓存、Redis为什么是单线程的、Redis的优缺点、Redis的常用场景,做了一次Redis的快速入门。


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


如果这篇文章对您有所帮助,或者有所启发的话,您的关注和点赞是我坚持写作最大的动力。


关注公众号:【哪吒编程】,在公众号中回复【掘金】,获取Java学习资料、电子书;回复【星球】加入Java学习星球,陪伴学习,共同优秀。


作者:哪吒编程
链接:https://juejin.cn/post/7207743145998794789
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

大哥,这是并发不是并行,Are You Ok?

多线程概述 基础概念 进程和线程 进程是程序运行资源分配的最小单位 进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能...
继续阅读 »

多线程概述


file


基础概念


进程和线程



进程是程序运行资源分配的最小单位



进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。


进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。



线程是CPU调度的最小单位,必须依赖于进程而存在



线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。



线程无处不在



任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。


CPU核心数和线程数的关系


多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP),CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理


多线程: Simultaneous Multithreading.简称SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。


核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但 Intel引入超线程技术后,使核心数与线程数形成1:2的关系


file


CPU时间片轮转机制


file


为什么感受不到CPU线程数的限制


我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。


时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。


什么是CPU轮转机制


百度百科对CPU时间片轮转机制原理解释如下:


如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结来,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾


时间片长度


时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。CPU时间的20%被浪费在了管理开销上了。


为了提高CPU效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s才获得运行机会。多数用户无法忍受一条简短命令要5才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发


结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。


在CPU死机的情况下,其实大家不难发现当运行一个程序的时候把CPU给弄到了100%再不重启电脑的情况下,其实我们还是有机会把它KILL掉的,我想也正是因为这种机制的缘故。


澄清并行和并发


我们举个例子,如果有条高速公路A上面并排有8条车道,那么最大的并行车辆就是8辆此条高速公路A同时并排行走的车辆小于等于8辆的时候,车辆就可以并行运行。CPU也是这个原理,一个CPU相当于一个高速公路A,核心数或者线程数就相当于并排可以通行的车道;而多个CPU就相当于并排有多条高速公路,而每个高速公路并排有多个车道。


当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。


俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。


综合来说:


并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.


并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行


两者区别:一个是交替执行,一个是同时执行.


file
感觉上是同时发生的,但是微观上还是有区别的,并行是同意时刻发生的,并发是同一时刻交替执行


file


高并发的意义



由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。多线程可以给程序带来如下好处。



1. 充分利用CPU的资源


从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。


就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU也是一样,也要充分利用。


2. 加快响应用户的时间


比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。


我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升1s,如果流量大的话,就能增加不少转换量。做过高性能web前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。


3. 可以使你的代码模块化,异步化,简单化


例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。


多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。


多线程程序需要注意事项


1. 线程之间的安全性


从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。


2. 线程之间的死锁


为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。


假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生


3. 线程太多了会将服务器资源耗尽形成死机当机


线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?


某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。


多线程应用开发的注意事项很多,希望大家在日后的工作中可以慢慢体会它的危险所在。


作者:博学谷_狂野架构师
链接:https://juejin.cn/post/7197622529599324215
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

金三银四好像消失了,IT行业何时复苏!

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过 我心中不惊有了很多疑惑和感叹 接着上一篇 一个28岁程序员入行自述和感受 自我10连问 我的心情 自去年下半年以来,互联网行业一片...
继续阅读 »

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了,
这是要哪样 我心中不由一万个 草泥🐴 路过



我心中不惊有了很多疑惑和感叹 接着上一篇


一个28岁程序员入行自述和感受


自我10连问


我的心情


自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。


我的工作生涯


我已经从业近十年,然而最近两年一直在小公司中工作,



我的技术和经历并不出色。随着年龄的增长,是否我的技能也在快速提高呢?我们该如何前进呢 ,转产品,产品到达极限,转管理,可是不会人情事故,



我们该如何继续前进呢?目前还没有人给出答案。


第一家公司


我记得那是很早的时候了,那个时候简历投递出去,就马上会收到很多回复,不像现在 ,
失联招聘前程堪忧boss直坑


你辛苦的写完简历,满怀期待投递了各大招聘平台,可等来的 却是已读未回,等的心也凉透了。


好怀念之前的高光时刻 神仙打架日子


前面我面试 几乎一周都安排满了,都面试不过来,我记得那会最多时候一天可以跑三家面试哈哈哈,也是很拼命的,有面试机会谁不想多试试呢


我第一家进入的是一个外包公司,叫xxx东软集团, 那个时候也不不懂,什么是外包给公司,只看工资给的所有offer中最高的,然后就去了哈哈哈哈。


入职第一天,我背着我自己的电脑满怀着激动就去了,然后被眼前一幕吸引了,办公的人真多啊,办公室都是拿透明玻璃隔开那种,人挺多,我一想公司还挺大的,
随后我就被带到也是一个玻璃格子办公室,里面就三个人,加我一个4个。


我害怕极了,这个时候一个稍微有一些秃顶的 大叔过来了 哈哈哈(内心台词,早就知道这一行干就了,会秃头这不会就是下一个我把


他把我安排在了靠近玻璃门的也就是大门位置,这是知道我准备随时跑路节奏吗。然后就去忙他自己的了。整个上午我就像是一个被遗忘在角落里的人一样。根本没人管我,就这样第一天结束了,我尴尬了做了一整天。


这工作和我想象的有点不太一样啊!


后面第三天还是如此,办公室里依旧是那么几个人,直到第四天,大叔来了,问我直到多线程吗,让我用多线程写一个抽奖的活动项目。(内心我想终于有事情干了,可是也高兴不起来谁知道怎么写)


不过好在,他也没有说什么时候交,只是说写完了给他看一下,经过我几天的,复制粘贴工程师 一顿谷歌,百度,终于是勉强写出来了。。。。。


后面,就又陆陆续续来了几个小伙伴,才开始新项目和开会,第一份工作大致就是这样开始了我的职业生涯。怎么说呢和我想象的有所不一样,但又有一些失望。


后面干了1年多,我就离职了原因是太累了没时间休息,一个项目接着一个项目的


第二家公司


在离开第一家公司时候,我休息了好长一段时间,调整了我自己的状态


也了解了什么是外包公司,什么是工作外派,也是我这一次就在投递简历,和面试时候刻意去避免进那种外包,和外派公司。


面试什么也还算顺利,不到半个月就拿到了offer。 但是工资总体来说比上一家是要少一点,但是我也接受了,是一家做本地生鲜电商公司,,本来生活科技有公司, 我觉得公司氛围,和公司都挺不错的,就入职了。


入职了我们那个项目经理还算很热情的,让同事帮我开git账号,开了邮箱,我自己拉取了公司项目,然后同事帮我运行调试环境第一天,项目什么都跑了起来,


你知道的,每次去一家新公司,开始新项目难的是项目复杂配置文件,和各种mave包依赖,接口,环境冲突,所以跑起来项目自己一个人摸索还是需要一些时间的。


在这家公司前期也还不错,公司维护自己项目,工作时间也比较自由和灵活,


大体流程是,每次有新的pm时候 产品经理就会组织各个部门开会


h5端-移动端-接口端开会讨论需求细节和实现,如果有问题头就会pass掉


然后产品经理就会把需求指派到每一个头,头把需求指派给组员,然后组员按照
redmine 上截止时间开发需求,


开发过程中自己去找对应接口负责方,其他业务负责方 去对接数据,没有问题了就可以提交给指定测试组测试了。


然后测试组头会把,测试分配给他们组员,进行测试。


有问题了就会在指派回来给对应负责各个开发同学去解决bug,直到测试完成。测试会让你提交堡垒环境 然后等待通知发布上线,


我们一般是晚上8点时候发布,发布时候,一般开发人员都要留守,直到发布上线没有问题了,才可以回家。如果弄得很晚的话,第二天可以晚点上班。


这一点是我觉得比较好的地方,工作时间弹性比较自由。


记得有一次生产事故。


我影响很深刻,东西上线了,然后产品经理说这个和他设计的预期的不符合要求,需要重新写,那一晚我们整组弄得很晚,就是为了等我弄好去吃饭。


你知道人在心急如焚情况下,是写不好代码的最后还是同事帮我一起完成了产品经理变态需求修改。。。。。。(也就在那时候我才知道产品经理和开发为什么不和了


因为五行相克


因为经常这样发版,然后一起吃饭公司报销。我们组员和领导关系还算不错氛围还挺好。在这一家公司我待了挺久的。


离职原因
后期因为说公司项目战略升级。空降了一位携程cto,还带来了他的手下人,我们组头,职权被削弱了,我不在由原来头管理了。再加上后面一些其他原因。老同事一个一个走了。


最后这个组只剩下我,和一个进来不久新同事。 不久我也离职了。


第三家公司


这次离职后,我调整休息了差不多有一年,中间离开上海去了江苏,因为家里,女朋友等各种事情。后面我才又从新去了上海,开始找工作。


找工作期间投奔的同事,合同事住一起。


这次面试我明显感觉,有一些慌张了,可能是太久没上班原因,有一些底气不足。好在也是找到了工作虽然不太理想。


这个过程太曲折了,后面公司终究没有扛过疫情,可以说在疫情边缘倒闭了,钱赔偿也没拿到,。。。这里就不赘述了。


IT行业如何破局 大家有什么想法和故事吗。可以关注 程序员三时公众号 进行技术交流讨论


嗯~还想往下面写一点什么,,,下一篇分享一下我现在工作和未来思考


作者:程序员三时
链接:https://juejin.cn/post/7231608749588693048
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

话说工作的“边界感”

一句话的合作例子 今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定...
继续阅读 »

一句话的合作例子


今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定制业务,所以要统一把所有的定制业务全部收口,但是这位定制同学的业务没有对应的技术研发同学,所以他就找到我的老板同步了这个情况。


分工协作的本质


其实问题的合作方式是比较简单的,但是当她跟我说最终客户定制界面也由我来开发的时候,比如定制的费用是多少、定制的时间要求等等,我就觉得问题有些奇怪了。因为按照常理来说,我负责的是工作台,但是由于有定制业务相关的逻辑,所以我要处理一定的业务逻辑,但是让我去承担这个定制页面的开发,我觉得是有问题的。


举一个简单的例子,假如我现在是一个博物馆,原来用户是直接可以免费没有任何阻挡地进入博物馆的,但是突然有一天市政府说所有公共设施要收费了,那么对于博物馆的工作人员来说肯定是支持的,但是突然你又告诉我,我这个博物馆还要去维护全市统一的收费系统,这个就是不合理的。哪怕他找我的主管沟通结果也是一样,因为我和我的主管是属于博物馆体系的工作人员,他也没有义务和责任去维护整个所有的公共设施的收费系统。但是作为公共设施系统的一部分,如果有统一的收费规则,那么对于博物馆来说也是要遵守的。


所以这面就引出了我对于业务边界上面的一个思考。我经常看到同学给我转发一段话,说跟你老板打沟通了业务的合作情况,你的老板觉得非常不错,于是这位同学就匆匆忙忙的找到我来开始谈业务,谈实施细节并且需要我快速落地。而实际上这种所谓的业务协同的情况大部分也只会停留在沟通的层面,在最终落地的时候,往往和业务同学的预期不相符。在业务同学眼里看来,就是你们阴奉阳违,恨不得马上就开始投诉。


这里面非常核心的一个误区就是业务同学往往没有划清业务界限和系统界限的边界。对于业务同学来说,边界可能不会那么明显,但对于一个系统开发的同学来说,业务和边界是非常明显的,因为系统是物理存在的,有着天然的“隔离”。所以对于业务同学,如果想要顺畅的推动业务,必须要事先清晰的划分参与方的角色和业务边界,并且可以进一步了解到系统边界在哪里。


这个由谁来做就涉及到了一个很大权责问题。简单来说就是我做了有什么好处,换句话来说做这件事和我的职务目标有什么关系?如果没有关系,我为什么要做?就算同一个公司,也有很多需要完成的事,比如公司保洁不到位,我作为公司的员工,是否也立即从事保洁?


如果是我的职务目标,我的责任有多少?我承担了既定的责任,那我是否能够承担起对应的权利?在我上次借用的博物馆的例子可以看到,如果我承担了全市的公共系统的收费设施的维护,那么我的权利在哪里?如果我的权利只是在博物馆这一个地方的收费上面,那么这就变成了权责不对等。


但是如果我做成了全市公共收费系统,并且能掌管全市所有公共设施的收费业务,那么对于这个收费系统的开发权则是相等的,但是对于我本身职务的权责又是不等的,因为公司请我来管理博物馆的,而非管理整个全市的收费系统。


所以在思考业务推进的时候,首先就要思考系统的边界和权责对等关系,如果这一层面没有理清楚的话,合作大概率是不能完成的。而很多的业务同学就以“我和你老板谈好的东西,为什么你不去做”这么简单的方式来拷问协同关系,我觉得是非常的幼稚的。


所以我希望其实我们在去和别人沟通业务的时候,往往要带着权责,带着边界的思考,去和对方去讨论,去协商,去沟通。简单来说,我在跟你聊之前,我要知道你的系统,你的业务边界在哪里?我跟你聊的时候,我要清晰地告诉你,这个事情做了对你有什么好处,对我有什么好处,哪部分应该你做,哪部分应该我来做。只有在这样的一种沟通方式下面才是真正合理的,真正是可以落地的沟通和协作方式。


而在这些问题没有达成一致之前,由谁来做都没有定下来的时候,应该先去往上升,在顶层设计里面去规划去重新思考如何从组织设计的方式去让业务协作自然的发生。


总结


这里再总结一下,这里是一个小的心得。这个案例也告诉我们,我们去沟通协同的时候要有边界感,包括业务的边界和系统的边界。只有把边界理顺了,合作才有可能。

作者:ali老蒋
链接:https://juejin.cn/post/7233808084085309477
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

代码中被植入了恶意删除操作,太狠了!

背景在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。对方拿到镜...
继续阅读 »

背景

在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。

事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。

对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。

排查过程

由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。

原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。

在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。

于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。

可疑代码

在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。

于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。

但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。

紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:

删除操作

原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。

T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。

找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。

又起波折

本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。

于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:

删除脚本

而在具体的脚本中,有如下执行操作:

删除核心依赖包

这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。

为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。

小结

原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。

当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有的,这点不接受反驳。


作者:程序新视界
链接:https://juejin.cn/post/7140066341469290532
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

电视剧里的代码真能运行吗?

大家好,欢迎来到 Crossin的编程教室 ! 前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道 ,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程...
继续阅读 »

大家好,欢迎来到 Crossin的编程教室 !


前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道


,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程序画一个爱心的桥段。


于是出于好奇,Crossin就去看了这一集(第5集,不用谢)。这一看不要紧,差点把刚吃的鸡腿给喷出来--槽点实在太多了!


忍不住做了个欢乐吐槽向的代码解读视频,在某平台上被顶到了20个w的浏览,也算蹭了一波人家电视剧的热度吧……


下面是图文版,给大家分析下剧中出现的“爱心”代码,并且来复刻一下最后男主完成的酷炫跳动爱心。


剧中代码赏析


1. 首先是路人同学的代码:



虽然剧中说是“C语言期中考试”,但这位同学的代码名叫 draw2.py,一个典型的 Python 文件,再结合截图中的 pen.forward、pen.setpos 等方法来看,应该是用 turtle 海龟作图库来画爱心。那效果通常是这样的:

import turtle as t
t.color('red')
t.setheading(50)
t.begin_fill()
t.circle(-100, 170)
t.circle(-300, 40)
t.right(38)
t.circle(-300, 40)
t.circle(-100, 170)
t.end_fill()
t.done()



而不是剧中那个命令行下用1组成的不规则的图形。


2. 然后是课代表向路人同学展示的优秀代码:



及所谓的效果:



这确实是C语言代码了,但文件依然是以 .py 为后缀,并且 include 前面没有加上 #,这显然是没法运行的。


里面的内容是可以画出爱心的,用是这个爱心曲线公式:



然后遍历一个15*17的方阵,计算每个坐标是在曲线内还是曲线外,在内部就输出#或*,外部就是-


用python改写一下是这样的:

for y in range(9, -6, -1):
for x in range(-8, 9):
print('*##*'[(x+10)%4] if (x*x+y*y-25)**3 < 25*x*x*y*y*y else '-', end=' ')
print()

效果:



稍微改一下输出,还能做出前面那个全是1的效果:

for y in range(9, -6, -1):
for x in range(-8, 9):
print('1' if (x*x+y*y-25)**3 < 25*x*x*y*y*y else ' ', end=' ')
print()


但跟剧中所谓的效果相去甚远。


3. 最后是主角狂拽酷炫D炸天的跳动爱心:



代码有两个片段:




但这两个片段也不C语言,而是C++,且两段并不是同一个程序,用的方法也完全不一样。


第一段代码跟前面一种思路差不多,只不过没有直接用一条曲线,而是上半部用两个圆形,下半部用两条直线,围出一个爱心。



改写成 Python 代码:

size = 10
for x in range(size):
for y in range(4*size+1):
dist1 = ((x-size)**2 + (y-size)**2) ** 0.5
dist2 = ((x-size)**2 + (y-3*size)**2) ** 0.5
if dist1 < size + 0.5 or dist2 < size + 0.5:
print('V', end=' ')
else:
print(' ', end=' ')
print()

for x in range(1, 2*size):
for y in range(x):
print(' ', end=' ')
for y in range(4*size+1-2*x):
print('V', end=' ')
print()

运行效果:



第二段代码用的是基于极坐标的爱心曲线,是遍历角度来计算点的位置。公式是:



计算出不同角度对应的点坐标,然后把它们连起来,就是一个爱心。

from math import pi, sin, cos
import matplotlib.pyplot as plt
no_pieces = 100
dt = 2*pi/no_pieces
t = 0
vx = []
vy = []
while t <= 2*pi:
vx.append(16*sin(t)**3)
vy.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt
plt.plot(vx, vy)
plt.show()

效果:



代码中循环时用到的2π是为了保证曲线长度足够绕一个圈,但其实长一点也无所谓,即使 π=100 也不影响显示效果,只是相当于同一条曲线画了很多遍。所以剧中代码里写下35位小数的π,还被女主用纸笔一字不落地抄写下来,实在是让程序员无法理解的迷惑行为。



但不管写再多位的π,上述两段代码都和最终那个跳动的效果差了五百只羊了个羊。


跳动爱心实现


作为一个总是在写一些没什么乱用的代码的编程博主,Crossin当然也不会放过这个机会,下面就来挑战一下用 Python 实现最终的那个效果。


1. 想要绘制动态的效果,必定要借助一些库的帮助,不然代码量肯定会让你感动得想哭。这里我们将使用之前 羊了个羊游戏 里用过的 pgzero 库。然后结合最后那个极坐标爱心曲线代码,先绘制出曲线上离散的点。

import pgzrun
from math import pi, sin, cos

no_p = 100
dt = 2*3/no_p
t = 0
x = []
y = []
while t <= 2*3:
x.append(16*sin(t)**3)
y.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt

def draw():
screen.clear()
for i in range(len(x)):
screen.draw.filled_rect(Rect((x[i]*10+400, -y[i]*10+300), (4, 4)), 'pink')

pgzrun.go()


2. 把点的数量增加,同时沿着原点到每个点的径向加一个随机数,并且这个随机数是按照正态分布来的(半个正态分布),大概率分布在曲线上,向曲线内部递减。这样,就得到这样一个随机分布的爱心效果。

...
no_p = 20000
...
while t <= 2*pi:
l = 10 - abs(random.gauss(10, 2) - 10)
x.append(l*16*sin(t)**3)
y.append(l*(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)))
t += dt
...


3. 下面就是让点动起来,这步是关键,也有一点点复杂。为了方便对于每个点进行控制,这里将每个点自定义成了一个Particle类的实例。


从原理上来说,就是给每个点加一个缩放系数,这个系数是根据时间变化的正弦函数,看起来就会像呼吸的节律一样。

class Particle():
def __init__(self, pos, size, f):
self.pos = pos
self.pos0 = pos
self.size = size
self.f = f

def draw(self):
screen.draw.filled_rect(Rect((10*self.f*self.pos[0] + 400, -10*self.f*self.pos[1] + 300), self.size), 'hot pink')

def update(self, t):
df = 1 + (2 - 1.5) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df

...

t = 0
def draw():
screen.clear()
for p in particles:
p.draw()

def update(dt):
global t
t += dt
for p in particles:
p.update(t)


4. 剧中爱心跳动时,靠中间的点波动的幅度更大,有一种扩张的效果。所以再根据每个点距离原点的远近,再加上一个系数,离得越近,系数越大。

class Particle():
...
def update(self, t):
df = 1 + (2 - 1.5 * self.f) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df


5. 最后再用同样的方法画一个更大一点的爱心,这个爱心不需要跳动,只要每一帧随机绘制就可以了。

def draw():
...
t = 0
while t < 2*pi:
f = random.gauss(1.1, 0.1)
x = 16*sin(t)**3
y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
size = (random.uniform(0.5,2.5), random.uniform(0.5,2.5))
screen.draw.filled_rect(Rect((10*f*x + 400, -10*f*y + 300), size), 'hot pink')
t += dt * 3


合在一起,搞定!



总结一下,就是在原本的基础爱心曲线上加上一个正态分布的随机量、一个随时间变化的正弦函数和一个跟距离成反比的系数,外面再套一层更大的随机爱心,就得到类似剧中的跳动爱心效果。


但话说回来,真有人会在考场上这么干吗?


除非真的是超级大学霸,不然就是食堂伙食太好--


吃太饱撑的……


作者:Crossin先生
链接:https://juejin.cn/post/7168388057631031332
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »