注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

开发的功能不都是经过上线测试,为什么上线后还会那么多 Bug ?

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。 大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅? 本篇只是毫无意义的「故事」,内容纯属「虚构」,如有...
继续阅读 »

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。


大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅?




本篇只是毫无意义的「故事」,内容纯属「虚构」,如有雷同,自己「反思」。



对于这个话题,我想用一个「虚构」的故事来介绍下,这个故事里我有一个「朋友」,他在某电商项目组,当时恰逢经历了公司内部的「双十一需求立项」:


立项时


老板:“这次的需求很简单,主要就是参考去年 TB 的预热活动来走,核心就是提高客单量和活跃,具体细节你们和产品沟通就行”


产品:“TB 去年的活动预热大家应该都了解吧,我们这次主要就是复刻一个类似的活动,时间一个月,具体有***,总的来说,双十一活动一定要准时上线,这次运营那边投入很多经费,需求方面我会把控好,核心围绕老板的思路,细节大家可以看文档,基本就是这些内容,一个月应该够的,大家没问题吧?”


开发:“没问题,保证完成任务”


3 天后:


老板:“我刚看了 JD 好像推出了一个不错的小游戏,我觉得这个可以导入到这边活动里,这样可以提高用户的活跃,用户活跃了,消费自然就起来了”


开会


产品:“鉴于老板的意见,我觉得 JD 的这个游戏活动效果可以提升我们的复购,所以我计划在原本需求基础上增加一个支线活动。


产品:“大家不要紧张,支线会和当前主线同步开发,支线活动比较灵活,不对账号级别做限制,另外「设计」那边看下入口放哪里比较合适。”


开发:“上线时间还是没变吗?”


产品:“双十一日期会变吗?老板说了大家抓紧点,功能不多,时间还是够的”


10 天后:


老板:“我刚和x总沟通了下,他觉得我们的活动少了互动,不利于留存,你看下怎么处理”


开会


产品:“经过这几天和老板的探讨,我们发现这次活动少了一些互动性,必须增加一些交互游戏,这样才能提升日活和用户体验。


产品:“这样吧,这部分功能也是比较迫切,我知道任务比较重,但是为了这次能取到较好成果,大家加把劲,接下来周末幸苦下大家,先不休假,后面调休补回来,具体需求大家可以看文档,有一些调整,不多,时间还是够的”


开发:“。。。。”


14 天后:


老板:“我看大家工作的热情还是可以的,刚运营提了可以增加一些视频支持,我觉得这是一个很好的意见”


开会


产品:“目前看起来我们的开发进度还比较乐观,运营的同学说了他们录制了一些活动视频,我看了还不错,就在主会场增加一些视频播放的功能吧,细节你们直接和设计讨论下”


产品:“这个应该不难吧,把分享和下载加上,视频播放这么基础的东西,应该不耽误事,我看网上开源服务就挺多的。”


开发:“。。。。”


20 天后:


老板:“我刚去开发那里看了下效果,感觉分会场的效果挺好的,做支线太可惜了,给他加回主流程里”


开会


产品:“老板那边觉得分会场的效果更好,让我们把分会场的效果做到主会场里的核心交互里,分会场部分的逻辑就不要了,入口也取消。


产品:“大家不要激动,都是现成的东西,改一改很快的,不过项目进度目前看来确实起有点拉垮,接下来大家晚上多幸苦点,我们晚上11点后再下班,我申请给大家报销打车的费用”


开发:“。。。。”


23 天后:


老板:“整体效果我觉得可以,就是好像有一些糙,你们再过一过,另外大家开个会统一下目标,看看能不能有新的补充”


产品:“可以的,过去我们也一直在开会对齐,基本每两天都会对齐一次”


开会


产品:“我和设计对了下,发现有一些细节可以优化,比如一些模块的颜色可以细调一下,还有这些按键的动画效果也加上,我知道工期紧张,所以我从「隔壁」项目组借了几个开发资源,未来一周他们可以帮助大家缓解下压力”


开发:“。。。。”


28 天后:


开会


产品:“好了,项目可以提测了,相信之前测试也陆续介入跟进了需求,应该问题不大,目前看起来「燃尽图」还可以,测试完了尽快上线,老板那边也在催了”


测试:“不行啊,今天走了用例,一堆问题,而且提测版本怎么和用例还有需求文档不一致,这个提测的版本开发是不是自己没做过测试啊,一大堆问题,根本测不下去,这样我们很难做”


产品:“这个我来沟通,开发接下来这几天大家就不要回家了,马上活动上线,一起攻克难关”


产品:“我也理解大家很努力了,但是这次提测的质量确实不行,你们还是要自己多把把关,不能什么问题都等到 QA 阶段才暴露,这样不利于项目进度,需求一直都很明确,大家把 Bug 尽可能修一修,有什么问题我们及时沟通,尽快解决,敏捷开发”


开发:“。。。。”


上线前一天晚上 10 点:


开会


测试:“不行,还有 20 几个问题没有确认,这种情况我不能签字,上线了有问题谁负责。”


产品:“一些问题其实并不影响正常使用,目前主流程应该没问题,让开发把 P0 的两个问题修了之后先上线,剩下的在运营过程中逐步更新就好了,有问题让运营先收集和安抚”


开发:“上线了脏数据不好弄,会有一些账号同步的问题,而且用户等级可能还有坑”


产品:“没什么不好弄的,到时候有问题的用户让运营做个标志,接下来小步快跑修复就好了,时间不等人,明天就是上线时间, 活动上不去对老板和运营都没办法交代”


项目上线:


老板:“运营和我反馈了好多问题,你们版本上线是怎么测试的,要反思一下xxxx”


开会


产品:“我说过用户要集齐碎片和好友砍价之后才能给优惠券,等级不够的不让他们砍价成功,为什么只完成砍价的新人拿到大额优惠券?”


产品:“什么?因为账号数据绑定有 Bug ,不同渠道用户合并账号就可以满足?为什么会有这个 Bug ,测试那边没覆盖到吗?”


测试:“我不知道啊,需求文档没有说还能账号合并,而且这个功能之前没说过要限制用户等级吧?”


产品:“我出的需求肯定有,文档里肯定有,另外开发你既然知道问题,为什么不提前沟通,现在用户都消费了,这个事故你和测试 55 责,后面复盘的时候要避免这样的问题再次发生”


开发:“。。。。。”



最后


所以大家觉得是谁应该背锅?是开发的能力不够,还是测试用例的覆盖缺失?说起来,其实在此之前,我在掘金也遇到了一个 “Bug” ,比如:



文章 Markdown 里图片链接的 content-type 字段如果不符合 image/*** 规格,那么发布出来的时候链接就不会被掘金转码,所以不会有图片水印,同时直接指向了我自己的图床地址。



那么你觉得这个是 Bug 吗?明明是用户不遵循规范。但是这样不就留下漏洞了吗?



如果在文章审核通过之后,我修改图床上的图片内容,这样不就可以绕过审核在掘金展示一些「违规」内容了吗?



所以有时候一些功能的初衷是好的,但是引发的问题却又很隐蔽,那么这能怪「测试用例覆盖不到位吗」?


那么,你觉得「经过测试,为什么上线后还会那么多 Bug 」更多可能是谁的问题?


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

28岁小公司程序员,无车无房不敢结婚,要不要转行?

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
继续阅读 »

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


**最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


1、他的问题


小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


2、几个建议


平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


但是这次听到小青的问题,说实话,我也不知道该说什么。


在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


如果想继续留在帝都,我能想到的有以下几个办法:



  • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

  • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

  • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


受限于自己的经验,我只能给出以上几个建议了。


大家还有什么更有效的建议,欢迎在评论区交流~


3、写在最后


说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒:如果不是十分热爱,请务必三思~


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

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


这些经验对正在阅读文章的你有用吗,欢迎一起交流,让我们一起交流您遇到的问题。 


这篇文章是去年写的,今天增加了点内容,掘金上同步更新了一下,希望可以被更多的人看到。


如果这篇文章说道你心里了,可以点赞、分享、评论、收藏、转发哦。


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

Android如何设计一个全局可调用的ViewModel对象?

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象? 一、思路 viewModel对象是存储在ViewModelStore中的,那么如果我们创建...
继续阅读 »

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象?


一、思路


viewModel对象是存储在ViewModelStore中的,那么如果我们创建一个全局使用的ViewModelStore并且在获取viewModel对象的时候从它里面获取就可以了。


viewModel是通过ViewModelProviderget方法获取的,一般是ViewModelProvider(owner: ViewModelStoreOwner, factory: Factory).get(ViewModel::class.java)


如何将ViewModelProviderViewModelStore关联起来? 纽带就是ViewModelStoreOwner, ViewModelStoreOwner是一个接口,需要实现getViewModelStore()方法,而该方法返回的就是ViewModelStore:

public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore(); //返回一个ViewModelStore
}

让某个类实现这个接口,重写方法返回我们定义的ViewModelStore就可以了。


至于上面ViewModelProvider构造方法的第二个参数Factory是什么呢?


源码中提供了二种Factory,一种是NewInstanceFactory,一种是AndroidViewModelFactory,它们的主要区别是:




  • NewInstanceFactory创建ViewModel时,会为每个Activity或Fragment创建一个新的ViewModel实例,这会导致ViewModel无法在应用程序的不同部分共享数据。(ComponentActivity源码getDefaultViewModelProviderFactory方法)




  • AndroidViewModelFactory可以访问应用程序的全局状态,并且ViewModel实例可以在整个应用程序中是共享的。




根据我们的需求,需要用的是AndroidViewModelFactory。


二、具体实现


1、方式一:可以全局添加和获取任意ViewModel


定义Application,Ktx.kt文件

import android.app.Application

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

定义全局可用的ViewModelOwner实现类

object ApplicationScopeViewModelProvider : ViewModelStoreOwner {

private val eventViewModelStore: ViewModelStore = ViewModelStore()

override fun getViewModelStore(): ViewModelStore {
return eventViewModelStore
}

private val mApplicationProvider: ViewModelProvider by lazy {
ViewModelProvider(
ApplicationScopeViewModelProvider,
ViewModelProvider.AndroidViewModelFactory.getInstance(appContext)
)
}

fun <T : ViewModel> getApplicationScopeViewModel(modelClass: Class<T>): T {
return mApplicationProvider.get(modelClass)
}
}

定义一个ViewModel通过StateFlow定义发送和订阅事件的方法

class EventViewModel : ViewModel() {

private val mutableStateFlow = MutableStateFlow(0)

fun postEvent(state: Int) {
mutableStateFlow.value = state
}

fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
val eventScope = scope ?: viewModelScope
eventScope.launch {
mutableStateFlow.collect {
method.invoke(it)
}
}
}
}

定义一个调用的类



object FlowEvent {

//发送事件
fun postEvent(state: Int) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.postEvent(state)
}

//订阅事件
fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.observeEvent(scope, method)
}
}

测试代码如下:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

FlowEvent.observeEvent {
printMsg("MainActivity observeEvent before :$it")
}
//修改值
FlowEvent.postEvent(1)


FlowEvent.observeEvent {
printMsg("MainActivity observeEvent after :$it")
}

}

}

//日志
内容:MainActivity observeEvent before :0 线程:main @coroutine#1
内容:MainActivity observeEvent before :1 线程:main @coroutine#1
内容:MainActivity observeEvent after :1 线程:main @coroutine#2

2、方式二:更方便在Activity和Fragment中调用


定义Application,让BaseApplication实现ViewModelStoreOwner

//BaseApplication实现ViewModelStoreOwner接口
class BaseApplication : Application(), ViewModelStoreOwner {

private lateinit var mAppViewModelStore: ViewModelStore
private var mFactory: ViewModelProvider.Factory? = null

override fun onCreate() {
super.onCreate()
//设置全局的上下文
setApplicationContext(this)
//创建ViewModelStore
mAppViewModelStore = ViewModelStore()

}

override fun getViewModelStore(): ViewModelStore = mAppViewModelStore

/**
* 获取一个全局的ViewModel
*/
fun getAppViewModelProvider(): ViewModelProvider {
return ViewModelProvider(this, this.getAppFactory())
}

private fun getAppFactory(): ViewModelProvider.Factory {
if (mFactory == null) {
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
}
return mFactory as ViewModelProvider.Factory
}
}

Ktx.kt文件也有变化,如下

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

//定义扩展方法
inline fun <reified VM : ViewModel> Fragment.getAppViewModel(): VM {
(this.requireActivity().application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

//定义扩展方法
inline fun <reified VM : ViewModel> AppCompatActivity.getAppViewModel(): VM {
(this.application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

BaseActivityBaseFragment中调用上述扩展方法

abstract class BaseActivity: AppCompatActivity() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}
abstract class BaseFragment: Fragment() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}

测试代码

class MainActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

eventViewModel.observeEvent {
printMsg("MainActivity observeEvent :$it")
}

findViewById<AppCompatButton>(R.id.bt).setOnClickListener {
//点击按钮修改值
eventViewModel.postEvent(1)
//跳转到其他Activity
Intent(this, TwoActivity::class.java).also { startActivity(it) }
}
}

}
class TwoActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_two)

eventViewModel.observeEvent {
printMsg("TwoActivity observeEvent :$it")
}
}
}

日志

内容:MainActivity observeEvent :0 线程:main @coroutine#1
内容:MainActivity observeEvent :1 线程:main @coroutine#1
内容:TwoActivity observeEvent :1 线程:main @coroutine#2

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

2023年的现代安卓开发

免责声明这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择...
继续阅读 »


免责声明

这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.

我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择.

什么是Android?

Android是一个基于Linux内核的开源操作系统, 由谷歌开发.它被广泛用于各种设备, 包括智能手机, 平板电脑, 电视和智能手表.

目前, 安卓是世界上移动设备使用最多的操作系统;根据statcounter的报告, 以过去12个月为样本, 安卓的市场份额为71.96%.

接下来, 我将提到一个工具, 库, 架构, 指南和其他实用工具的清单, 我认为这些工具对在Android上构建现代应用程序非常重要.

Kotlin ❤️

0_piQN_I004o_ugTCN.webp

Kotlin是由JetBrains开发的一种编程语言.由谷歌推荐, 谷歌在2017年5月正式宣布了它(见这里的出版物).它是一种现代编程语言, 具有与Java的兼容性, 可以在JVM上运行, 这使得它在Android应用开发中的采用速度非常快.

无论你是否是安卓新手, 你都应该考虑将Kotlin作为你的首选, 不要逆水行舟🏊🏻 😎, 谷歌在2019年谷歌I/O大会上宣布了这一做法.使用Kotlin, 你将能够使用现代语言的所有功能, 包括Coroutines的强大实力和使用为Android生态系统开发的现代库.

官方kotlin文档在这里

Jetpack Compose 😍

0_kG-9BQIyUm8MblpZ.webp

Jetpack Compose是Android推荐的用于构建本地UI的现代工具包.它简化并加速了Android上的UI开发.

Jetpack Compose是Android Jetpack库的一部分, 使用Kotlin编程语言来轻松创建本地用户界面.同时, 它还与其他Android Jetpack库(如LiveData和ViewModel)集成, 使其更容易建立反应性和可维护的Android应用程序.

Jetpack Compose的一些主要特点包括:

  1. 声明式UI.
  2. 可定制的小工具.
  3. 易于与现有代码集成.
  4. 实时预览.
  5. 改进性能.

资源:

Jetpack Compose文档

Android Jetpack

0_3LHozcwxQYiKVhPG.webp

Jetpack是一套库, 帮助开发人员遵循最佳实践, 减少模板代码, 并编写在不同的Android版本和设备上一致运行的代码, 以便开发人员可以专注于他们关心的代码.

它的一些最常用的工具是:

Material Design

1_D3MK4AocfnktSnVGe4rg0g.webp

Material Design是一个由指导方针, 组件和工具组成的适应性系统, 支持用户界面设计的最佳实践.在开源代码的支持下, Material Design简化了设计师和开发人员之间的合作, 并帮助团队快速建立漂亮的产品.

Material Design网站

Material Design得到了来自谷歌的设计师和开发人员的支持, 它将使我们有一个指南来为我们的Android, Flutter和Web的UI/UX工作.

目前, Material Design的最后一个版本是3, 你可以看到更多这里.

Clean Architecture

0_KgFh38gn_lDEuoB9.webp

Clean Architecture的概念是由Robert C. Martin提出的.它的基础是通过将软件划分为不同的层来分离责任.

特点:

  1. 独立于框架.
  2. 可测试.
  3. 独立于用户界面.
  4. 独立于数据库.
  5. 独立于任何外部代理.

依赖性规则

博文Clean Architecture对依赖性规则做了很好的描述.

使得这个架构发挥作用的首要规则是依赖性规则.这个规则说, 源代码的依赖关系只能指向内部.内圈的任何东西都不能知道外圈的任何东西.特别是, 外圈中声明的东西的名字不能被内圈中的代码所提及.这包括, 函数, 类, 变量或任何其他命名的软件实体.

博文Clean Architecture

安卓系统中的Clean Architecture

  • 表示层: Activities, Fragments, ViewModels, 其他视图组件.
  • 领域层: 用例, 实体, 仓库, 其他的域组件.
  • 数据层: 存储库的实现, 映射器, DTO等.

Presentation层的架构模式

架构模式是一种更高层次的策略, 旨在帮助设计一个软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案.架构模式类似于设计模式, 但它们的规模更大, 解决的是更多的全局性问题, 如系统的整体结构, 组件之间的关系以及数据的管理方式.

在Presentation层中, 我们有一些架构模式, 其中我想强调以下几点:

  • MVVM
  • MVI

我不想逐一解释, 因为在互联网上你可以找到太多的相关信息.

此外, 你还可以看看应用架构指南

0_QJ56TjhdXPcQweAk.webp

依赖注入

依赖注入是一种软件设计模式, 它允许客户端从外部来源获得其依赖, 而不是自己创建.它是一种在对象和其依赖关系之间实现反转控制(IoC)的技术.

模块化

模块化是一种软件设计技术, 它允许你将一个应用程序划分为独立的模块, 每个模块都有自己的功能和责任.

0_NNUw83lZ228t5yLD.webp

模块化的好处

可重复使用: 通过拥有独立的模块, 它们可以在应用程序的不同部分甚至在其他应用程序中重复使用.

严格的可见性控制: 模块使你能够轻松地控制你向你的代码库的其他部分暴露的内容.

可定制的交付Google Play的特性交付使用应用程序捆绑的高级功能, 允许你有条件地或按需交付你的应用程序的某些功能.

可扩展性: 通过独立的模块, 功能可以被添加或删除而不影响应用程序的其他部分.

易于维护: 通过将应用程序分为独立的模块, 每个模块都有自己的功能和责任, 更容易理解和维护代码.

易于测试: 通过拥有独立的模块, 它们可以被隔离测试, 这使得检测和修复错误变得容易.

架构的改进: 模块化有助于改善应用程序的架构, 使代码有更好的组织和结构.

改进协作: 通过独立的模块, 开发人员可以同时工作在应用程序的不同部分, 不受干扰.

构建时间: 一些Gradle功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化来提高构建性能.

更多内容请见官方文档.

网络

序列化

在本节中, 我想提及我认为的两个重要工具: MoshiRetrofit一起广泛使用, 以及Kotlin Serialization, 这是Jetbrain的Kotlin团队的赌注.

MoshiKotlin Serialization是Kotlin和Java的两个序列化/反序列化库, 允许你将对象转换成JSON或其他序列化格式, 反之亦然.两者都提供了一个用户友好的界面, 为在移动和桌面应用程序中使用而优化.Moshi主要专注于JSON序列化, 而Kotlin Serialization则支持各种序列化格式, 包括JSON.

图像加载

要从互联网上加载图片, 有几个第三方库可以帮助你处理这个过程.图片加载库为你做了很多繁重的工作;它们既能处理缓存(这样你就不会多次下载图片), 也能处理网络逻辑以下载图片并在屏幕上显示.

Reactivity / Thread Management反应性/线程管理

1_jm3wnFbTBvURFtLlcQAYRg.webp

当我们谈论反应式编程和异步进程时, 我们的第一选择是Kotlin Coroutines;由于suspend函数Flow, 我们可以满足所有这些需求.然而, 我认为在这一节中值得强调的是RxJava的重要性, 即使在Android应用程序的开发中.对于我们这些已经在Android上工作了几年的人来说, 我们知道RxJava是一个非常强大的工具, 它有非常多的功能来处理数据流.今天我仍然认为RxJava是一个值得考虑的替代方案.

本地存储

在构建移动应用程序时, 很重要的一点是要有在本地持久化数据的能力, 比如一些会话数据或缓存数据等等.根据你的应用程序的需要, 选择合适的存储方式是很重要的.我们可以存储非结构化的数据, 如键值或结构化的数据, 如数据库.请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.

1_rILOhf6I_dtR-ircBkKvtQ.webp

建议:

测试

R8优化

R8是默认的编译器, 它将你项目的Java字节码转换为在Android平台上运行的DEX格式.它是一个帮助我们混淆和减少应用程序代码的工具, 通过缩短类和其属性的名称, 消除项目内未使用的代码和资源.想了解更多, 请查看Android文档中关于缩减, 混淆和优化你的应用程序.

1_KzoahZDnZ25lv5ydi39JSw.webp

  • 代码缩减
  • 资源缩减
  • 混淆
  • 优化

Play特性交付

Google Play的应用服务模式, 称为动态交付, 使用Android App Bundles为每个用户的设备配置生成和提供优化的APK, 因此用户只下载运行你的应用所需的代码和资源.

Android文档

0_FitxQQeB7XC7MVUq.webp

自适应布局

0_MHJwbEuvl8cXDjeq.webp

随着具有不同外形尺寸的移动设备使用的增长, 我们需要有一些工具, 使我们的Android应用程序能够适应不同类型的屏幕.这就是为什么Android为我们提供了Window Size类, 简单地说, 它是三个大的屏幕格式组, 为我们开发设计标记了关键点.这样我们就避免了考虑许多屏幕设计的复杂性, 将我们的可能性减少到三组, 即: CompatMedium 和 Expanded..

Windows Size类

1_5Tm17OKlC5n0oy6L641A5g.webp

1_Qv1nt0JJzQPzFfr2G78ulg.webp

支持不同的屏幕尺寸

我们拥有的另一个重要资源是经典布局, 这是预定义的屏幕设计, 可以用于我们的安卓应用中的大多数场景, 还向我们展示了如何将其适应大屏幕的指南.

1_XASUz4kVTK4I0dH8F5slYQ.gif

其他相关资源

Form-Factor培训

Google I/O 2022上的Form Factors

性能

0_QcvMmljmmcvCuqfN.webp

当我们为Android开发应用程序时, 我们必须确保用户体验更好, 不仅是在应用程序的开始, 而且在整个执行过程中.出于这个原因, 重要的是要有一些工具, 使我们能够对可能影响应用程序性能的情况进行预防性分析和持续监测, 因此, 这里有一个工具清单, 可以帮助你达到这个目的:

应用内更新

当你的用户在他们的设备上保持你的应用程序的更新时, 他们可以尝试新的功能, 以及从性能改进和错误修复中获益.虽然有些用户在他们的设备连接到无计量的连接时启用后台更新, 但其他用户可能需要被提醒安装更新.应用内更新是Google Play核心库的一项功能, 提示活跃用户更新你的应用.

应用内更新功能在运行Android 5.0(API级别21)或更高的设备上得到支持.此外, 应用内更新仅支持Android移动设备, Android平板电脑和Chrome OS设备.

0_m8wEQzEW1M1fwwKC.webp

应用内评论

Google Play应用内评论API让你可以提示用户提交Play Store的评分和评论, 而不需要离开你的应用或游戏, 这很方便.

一般来说, 应用内评论流程可以在你的应用的整个用户旅程中的任何时候被触发.在流程中, 用户可以使用1至5星系统对你的应用程序进行评分, 并添加一个可选的评论.一旦提交, 评论将被发送到Play Store并最终显示出来.

为了保护用户隐私和避免API被滥用, 您的应用程序应遵循关于何时请求应用内评论评论提示的设计的严格准则.

应用内评论文档

0_--T1rkTL7DEGJT9B.webp

辅助功能

0_fO3BnqLh8b-H_zLo.webp

辅助功能是软件设计和建造的一个重要特征, 除了改善他们的用户体验外, 还为有可访问性需求的人提供了使用应用程序的能力.这个概念旨在改善的一些残疾是:有视力问题的人, 色盲, 听力问题, 灵巧问题和认知障碍等等.

考虑的因素:

  • 增加文本的可见性(颜色对比, 可调整文本).
  • 使用大型, 简单的控件
  • 描述每个用户界面元素

查看辅助功能--Android文档

安全性

0_Fk42FqLrujNE0O1Z.png

安全性是我们在开发保护设备的完整性, 数据的安全性和用户的信任的应用程序时必须考虑的一个方面, 甚至是最重要的方面, 这就是为什么我在下面列出了一系列的提示, 将帮助你实现这一目的.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />
  • 不要将应用程序配置所需的密钥, 令牌或敏感数据直接放在项目库内的文件或类中.使用local.properties代替.

版本目录

Gradle提供了一种集中管理项目依赖关系的标准方式, 称为版本目录;它在7.0版本中试验性地引入, 并在7.4版本中正式发布.

优点是:

  • 对于每个目录, Gradle都会生成类型安全的访问器, 这样你就可以在IDE中用自动完成的方式轻松添加依赖关系.
  • 每个目录对一个构建的所有项目都是可见的.它是一个集中的地方, 可以声明一个依赖的版本, 并确保对该版本的改变适用于每个子项目.
  • 目录可以声明依赖包, 这是通常一起使用的"依赖包组".
  • 目录可以将依赖的组和名称与它的实际版本分开, 并使用版本参考来代替, 这样就可以在多个依赖之间共享一个版本声明.

更多请查看

Logger

Logger是一种软件工具, 用于登记有关程序执行的信息;重要事件, 错误调试信息和其他可能对诊断问题或了解程序如何工作有用的信息.记录器可以被配置为将信息写入不同的位置, 如日志文件, 控制台, 数据库, 或通过将信息发送到日志服务器.

Linter

0_T3lk9cUYryUAo6G1.webp

Linter是一种编程工具, 用于分析程序源代码, 以发现代码中的潜在问题或漏洞.这些问题可能是语法问题, 不恰当的代码风格, 缺乏文档, 安全问题等等, 它们会对代码的质量和可维护性产生影响.


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

深入理解Android-Runtime

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介...
继续阅读 »
image.png

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介绍应用Profile-Guided Optimization(PGO)技术对应用启动速度进行优化的可行性。转载请注明来源「申国骏」

App运行时演进

JVM

Android原生代码使用Java或者Kotlin编写,这些代码会通过javac或者kotlinc编译成.class文件,在Android之前,这些.class文件会被输入到JVM中执行。JVM可以简单分为三个子系统,分别是Class Loader、Runtime Data Area以及Execution Engine。其中Class Loader主要负责加载类、校验字节码、符号引用链接及对静态变量和静态方法分配内存并初始化。Runtime Data负责存储数据,分为方法区、堆区、栈区、程序计数器以及本地方法栈。Execution Engine负责二进制代码的执行以及垃圾回收。

image.png

Execution Engine中,会采用Interpreter或者JIT执行。其中Interpreter表示在运行的过程中对二进制代码进行解释,每次执行相同的二进制代码都进行解释比较浪费资源,因此对于热区的二进制代码会进行JIT即时编译,对二进制代码编译成机器码,这样相同的二进制代码执行时,就不用再次进行解释。

image.png

DVM(Android 2.1/2.2)

JVM是stack-based的运行环境,在移动设备中对性能和存储空间要求较高,因此Android使用了register-based的Dalvik VM。从JVM转换到DVM我们需要将.class文件转换为.dex文件,从.class转换到.dex的过程需要经过 desugar -> proguard -> dex compiler三个过程,这三个过程后来逐步变成 proguard -> D8(Desugar) 直到演变到今天只需要一步R8(D8(Desugar))。

image.png

我们主要关注Android中Runtime Engine与JVM的区别。在Android早期的版本里面,只存在Interpreter解释器,到了Android2.2版本将JIT引入,这个版本Dalvik与JVM的Runtime Engine区别不大。

image.png

ART-AOT(Android 4.4/5.0)

为了加快应用的启动速度和体验,到了Android4.4,Google提供了一个新的运行时环境ART(Android Runtime),到了Android5.0,ART替换Dalvik成为唯一的运行时环境。

image.png

ART运行时环境中,采用了AOT(Ahead-of-time)编译方式,即在应用安装的时候就将.dex提前编译成机器码,经过AOT编译之后.dex文件会生成.oat文件。这样在应用启动执行的时候,因为不需要进行解释编译,大大加快了启动速度。

image.png

然而AOT带来了以下两个问题:

  1. 应用安装时间大幅增加,由于在安装的过程中同时需要编译成机器码,应用安装时间会比较长,特别在系统升级的时候,需要对所有应用进行重新编译,出现了经典的升级等待噩梦。

image.png

  1. 应用占用过多的存储空间,由于所有应用都被编译成.oat机器码,应用所占的存储空间大大增加,使得本来并不充裕的存储空间变得雪上加霜。

进一步思考对应用全量进行编译可能是没有必要的,因为用户可能只会用到一个应用的部分常用功能,并且全量编译之后更大的机器码加载会占用IO资源。

ART-PGO(Android 7.0)

从Android7.0开始,Google重新引入了JIT的编译方式,不再对应用进行全量编译,结合AOT、JIT、Interpreter三者的优势提出了PGO(Profile-guided optimization)的编译方式。

在应用执行的过程中,先使用Interpreter直接解释,当某些二进制代码被调用次数较多时,会生成一个Profile文件记录这些方法存储起来,当二进制代码被频繁调用时,则直接进行JIT即时编译并缓存起来。

当应用处于空闲(屏幕关闭且充电)的状态时,编译守护进程会根据Profile文件进行AOT编译。

当应用重新打开时,进行过JIT和AOT编译的代码可以直接执行。

这样就可以在应用安装速度以及应用打开速度之间取得平衡。

image.png

image.png

JIT 工作流程:

image.png

ART-Cloud Profile(Android 9.0)

不过这里还是有一个问题,就是当用户第一次安装应用的时候并没有进行任何的AOT优化,通常会经过用户多次的使用才能使得启动速度得到优化。

image.png

考虑到一个应用通常会有一些用户经常使用执行的代码(例如启动部分以及用户常用功能)并且大多数时候会有先行版本用于收集Profile数据,因此Google考虑将用户生成的Profile文件上传到Google Play中,并在应用安装时同时带上这个Profile文件,在安装的过程中,会根据这个Profile对应用进行部分的AOT编译。这样当用户安装完第一次打开的时候,就能达到较快的启动速度。

image.png

image.png

Profile in cloude 需要系统应用市场支持,在国内市场使用Google Play的占比非常低,因此cloud profile的优化在国内几乎是没有作用的,不过Profile的机制提供了一个可以做启动优化的思路。早在2019年,支付宝就在秒开技术的回应的里面提到过profile-based compile的技术,参考:如何看待今日头条自媒体发布谣言称「支付宝几乎秒开是因为采用华为方舟编译器」?,这也是我们一直研究Profile技术的原因。困扰着我们的一直有两个问题,第一个问题是如何生成Profile文件,第二个问题是怎么使用生成的Profile文件。对于第一个问题的解决相对还是有思路的,因为app运行就会生成profile文件,因此我们手动运行几次app就能在文件系统中收集到这个文件,不过如何以一种较为自动化的手段收集仍然是个问题。第二个问题我们知道Profile文件最终生成的位置,因此我们可以把生成的文件放到相应的系统目录,不过大多数手机和应用都没有权限直接放置这个文件。因此Profile优化技术一直都没有落地,直到Baseline Proflie让我们看到了希望。

Baseline Profile

Baseline Profile是一套生成和使用Profile文件的工具,在2022年一月份开始进入视野,随后在Google I/O 2022随着Jetpack新变化得到广泛关注。其背景是Google Map加快了发版速度,Cloud Profle还没完全收集好就上新版,导致Cloud Proflie失效。还有一个背景是Jetpack Compose 不是系统代码,因此没有完全编译成机器码,而且Jetpack Compose库比较大,因此在Profile生成之前使用了Jetpack Compose的应用启动会产生性能问题。最后Google为了解决这些问题,创造了收集Profile的BaselineProfileRule Macrobenchmark以及使用Profile的ProfileInstaller。

使用Baseline Profile的机制可以在Android7及以上的手机上得到应用的启动加速,因为从上述知道Android7就已经开始有PGO(Profile-guided optimization)的编译方式。生成的Profile文件会打包到apk里面,并且会结合Google Play的Cloud Profile来引导AOT编译。虽然在国内基本上用不了Cloud Profile,不过Baseline Profile是可以独立于Google Play单独使用的。

image.png

在使用了Baseline Proflie之后,有道词典的启动速度从线上统计上看,冷启动时间有15%的提升。

这篇文章主要介绍了Android Runtime的演进以及对于应用启动的影响,下一篇文章我会详细介绍关于Profile&dex文件优化、Baseline Profile工具库原理,以及在实际操作上如何使用的问题,敬请大家期待一下!

收起阅读 »

什么情况下Activity会被杀掉呢?

首先一个报错来作为开篇:Caused by androidx.fragment.app.Fragment$InstantiationException Unable to instantiate fragment xxx: could not find Fra...
继续阅读 »

首先一个报错来作为开篇:

Caused by androidx.fragment.app.Fragment$InstantiationException
Unable to instantiate fragment xxx: could not find Fragment constructor

这个报错原因就是Fragment如果重载了有参的构造方法,没有实现默认无参构造方法。Activity被回收又回来尝试重新恢复Fragment的时候报错的。


那如何模拟Activity被回收呢?

可能有人知道,一个方便快捷的方法就是:打开 开发者选项 - 不保留活动,这样每次Activity回到后台都会被回收,也就可以很方便的测试这种case。


但抛开这种方式我怎么来复现这种情况呢?

这里我提出一种方式:我是不是可以打开我的App,按Home回到后台,然后疯狂的打开手机里其他的大型应用或者游戏这类的能占用大量手机内存的App,等手机内存占用大的时候是不是可以复现这种情况呢?


结论是不可以,不要混淆两个概念,系统内存不足App内存不足,两者能引起的后果也是不同的



  • 系统内存不足 -> 杀掉应用进程

  • App内存不足 -> 杀掉后台Activity


首先明确一点,Android框架对进程创建与管理进行了封装,对于APP开发者只需知道Android四大组件的使用。当Activity, Service, ContentProvider, BroadcastReceiver任一组件启动时,当其所承载的进程存在则直接使用,不存在则由框架代码自动调用startProcessLocked创建进程。所以说对APP来说进程几乎是透明的,但了解进程对于深刻理解Android系统是至关关键的。


1. 系统内存不够 -> 杀掉应用进程


1.1. LKM简介

Android底层还是基于Linux,在Linux中低内存是会有oom killer去杀掉一些进程去释放内存,而Android中的lowmemorykiller就是在此基础上做了一些调整来的。因为手机上的内存毕竟比较有限,而Android中APP在不使用之后并不是马上被杀掉,虽然上层ActivityManagerService中也有很多关于进程的调度以及杀进程的手段,但是毕竟还需要考虑手机剩余内存的实际情况,lowmemorykiller的作用就是当内存比较紧张的时候去及时杀掉一些ActivityManagerService还没来得及杀掉但是对用户来说不那么重要的进程,回收一些内存,保证手机的正常运行。


lowmemkiller中会涉及到几个重要的概念:

/sys/module/lowmemorykiller/parameters/minfree:里面是以”,”分割的一组数,每个数字代表一个内存级别

/sys/module/lowmemorykiller/parameters/adj: 对应上面的一组数,每个数组代表一个进程优先级级别


比如:

/sys/module/lowmemorykiller/parameters/minfree:18432, 23040, 27648, 32256, 55296, 80640

/sys/module/lowmemorykiller/parameters/adj: 0, 100, 200, 300, 900, 906


代表的意思是两组数一一对应:



  • 当手机内存低于80640时,就去杀掉优先级906以及以上级别的进程

  • 当内存低于55296时,就去杀掉优先级900以及以上的进程


可能每个手机的配置是不一样的,可以查看一下手头的手机,需要root。


1.2. 如何查看ADJ

如何查看进程的ADJ呢?比如我们想看QQ的adj

-> adb shell ps | grep "qq" 
UID PID PPID C STIME TTY TIME CMD
u0_a140 9456 959 2 10:03:07 ? 00:00:22 com.tencent.mobileqq
u0_a140 9987 959 1 10:03:13 ? 00:00:07 com.tencent.mobileqq:mini3
u0_a140 16347 959 0 01:32:48 ? 00:01:12 com.tencent.mobileqq:MSF
u0_a140 21475 959 0 19:47:33 ? 00:01:25 com.tencent.mobileqq:qzone

# 看到QQ的PID为 9456,这个时候打开QQ,让QQ来到前台
-> adb shell cat /proc/9456/oom_score_adj
0

# 随便打开一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
700

# 再随便打开另外一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
900

我们可以看到adj是在根据用户的行为不断变化的,前台的时候是0,到后台是700,回到后台后再打开其他App后是900

常见ADJ级别如下:























































































ADJ级别取值含义
NATIVE_ADJ-1000native进程
SYSTEM_ADJ-900仅指system_server进程
PERSISTENT_PROC_ADJ-800系统persistent进程
PERSISTENT_SERVICE_ADJ-700关联着系统或persistent进程
FOREGROUND_APP_ADJ0前台进程
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_APP_ADJ200可感知进程,比如后台音乐播放
BACKUP_APP_ADJ300备份进程
HEAVY_WEIGHT_APP_ADJ400重量级进程
SERVICE_ADJ500服务进程
HOME_APP_ADJ600Home进程
PREVIOUS_APP_ADJ700上一个进程
SERVICE_B_ADJ800B List中的Service
CACHED_APP_MIN_ADJ900不可见进程的adj最小值
CACHED_APP_MAX_ADJ906不可见进程的adj最大值

So,当系统内存不足的时候会kill掉整个进程,皮之不存毛将焉附,Activity也就不在了,当然也不是开头说的那个case。


2. App内存不足 -> 杀掉后台Activity


上面分析了是直接kill掉进程的情况,一旦出现进程被kill掉,说明内存情况已经到了万劫不复的情况了,抛开内存泄漏的情况下,framework也需要一些策略来避免无内存可用的情况。下面我们来找一找fw里面回收Activity的逻辑(代码Base Android-30)。



Android Studio查看源码无法查看com.android.internal包名下的代码,双击Shift,勾选右上角Include non-prject Items.



入口定位到ActivityThreadattach方法,ActivityThread是App的入口程序,main方法中创建并调用atttach

// ActivityThread.java
private void attach(boolean system, long startSeq) {
...
// Watch for getting close to heap limit.
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
// mSomeActivitiesChanged在生命周期变化的时候会修改为true
if (!mSomeActivitiesChanged) {
return;
}
Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
});
...
}

这里关注BinderInternal.addGcWatcher, 下面有几个点需要理清:



  1. addGcWatcher是干嘛的,这个Runnable什么时候会被执行。

  2. 这里的maxMemory() / totalMemory() / freeMemory()都怎么理解,值有什么意义

  3. releaseSomeActivities()做了什么事情,回收Activity的逻辑是什么。


还有一个小的点是这里还用了mSomeActivitiesChanged这个标记位来标记让检测工作不会过于频繁的执行,检测到需要releaseSomeActivities后会有一个mSomeActivitiesChanged = false;赋值。而所有的mSomeActivitiesChanged = true操作都在handleStartActivity/handleResumeActivity...等等这些操作Activity声明周期的地方。控制了只有Activity声明周期变化了之后才会继续去检测是否需要回收。


2.1. GcWatcher

BinderInternal.addGcWatcher是个静态方法,相关代码如下:

public class BinderInternal {
private static final String TAG = "BinderInternal";
static WeakReference<GcWatcher> sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
static Runnable[] sTmpWatchers = new Runnable[1];

static final class GcWatcher {
@Override
protected void finalize() throws Throwable {
handleGc();
sLastGcTime = SystemClock.uptimeMillis();
synchronized (sGcWatchers) {
sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
}
for (int i=0; i<sTmpWatchers.length; i++) {
if (sTmpWatchers[i] != null) {
sTmpWatchers[i].run();
}
}
sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}

public static void addGcWatcher(Runnable watcher) {
synchronized (sGcWatchers) {
sGcWatchers.add(watcher);
}
}
...
}

两个重要的角色:sGcWatcherssGcWatcher



  • sGcWatchers保存了调用BinderInternal.addGcWatcher后需要执行的Runnable(也就是检测是否需要kill Activity的Runnable)。

  • sGcWatcher是个装了new GcWatcher()的弱引用。


弱引用的规则是如果一个对象只有一个弱引用来引用它,那GC的时候就会回收这个对象。那很明显new出来的这个GcWatcher()只会有sGcWatcher这一个弱引用来引用它,所以每次GC都会回收这个GcWatcher对象,而回收的时候会调用这个对象的finalize()方法,finalize()方法中会将之前注册的Runnable来执行掉。
注意哈,这里并没有移除sGcWatcher中的Runnable,也就是一开始通过addGcWatcher(Runnable watcher)进来的runnable一直都在,不管执行多少次run的都是它。


为什么整个系统中addGcWatcher只有一个调用的地方,但是sGcWatchers确实一个List呢?我在自己写了这么一段代码并且想着怎么能反射搞到系统当前的BinderInternal一探究竟的时候明白了一点点,我觉着他们就是怕有人主动调用了addGcWatcher给弄了好多个GcWatcher导致系统的失效了才搞了个List吧。。


2.2. App可用的内存

上面的Runnable是如何检测当前的系统内存不足的呢?通过以下的代码

        Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) { ... }

看变量名字就知道,在使用的内存到达总内存的3/4的时候去做一些事情,这几个方法的注释如下:

    /**
* Returns the amount of free memory in the Java Virtual Machine.
* Calling the gc method may result in increasing the value returned by freeMemory.
* @return an approximation to the total amount of memory currently available for future allocated objects, measured in bytes.
*/
public native long freeMemory();

/**
* Returns the total amount of memory in the Java virtual machine.
* The value returned by this method may vary over time, depending on the host environment.
* @return the total amount of memory currently available for current and future objects, measured in bytes.
*/
public native long totalMemory();

/**
* Returns the maximum amount of memory that the Java virtual machine will attempt to use.
* If there is no inherent limit then the value java.lang.Long#MAX_VALUE will be returned.
* @return the maximum amount of memory that the virtual machine will attempt to use, measured in bytes
*/
public native long maxMemory();

首先确认每个App到底有多少内存可以用,这些Runtime的值都是谁来控制的呢?


可以使用adb shell getprop | grep "dalvik.vm.heap"命令来查看手机给每个虚拟机进程所分配的堆配置信息:

yocn@yocn ~ % adb shell getprop | grep "dalvik.vm.heap"
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [512k]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [8m]
[dalvik.vm.heaptargetutilization]: [0.75]

这些值分别是什么意思呢?



  • [dalvik.vm.heapgrowthlimit]和[dalvik.vm.heapsize]都是当前应用进程可分配内存的最大限制,一般heapgrowthlimit < heapsize,如果在Manifest中的application标签中声明android:largeHeap=“true”,APP直到heapsize才OOM,否则达到heapgrowthlimit就OOM

  • [dalvik.vm.heapstartsize] Java堆的起始大小,指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小,后面再根据需要逐渐向系统申请更多的物理内存,直到达到MAX

  • [dalvik.vm.heapminfree] 堆最小空闲值,GC后

  • [dalvik.vm.heapmaxfree] 堆最大空闲值

  • [dalvik.vm.heaptargetutilization] 堆目标利用率


比较难理解的就是heapminfree、heapmaxfree和heaptargetutilization了,按照上面的方法来说:
在满足 heapminfree < freeMemory() < heapmaxfree的情况下使得(totalMemory() - freeMemory()) / totalMemory()接近heaptargetutilization


所以一开始的代码就是当前使用的内存到达分配的内存的3/4的时候会调用releaseSomeActivities去kill掉某些Activity.


2.3. releaseSomeActivities

releaseSomeActivities在API 29前后差别很大,我们来分别看一下。


2.3.1. 基于API 28的版本的releaseSomeActivities实现如下:
// step①:ActivityManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized(this) {
final long origId = Binder.clearCallingIdentity();
try {
ProcessRecord app = getRecordForAppLocked(appInt);
mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// step②:ActivityStackSupervisor.java
void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
TaskRecord firstTask = null;
ArraySet<TaskRecord> tasks = null;
for (int i = 0; i < app.activities.size(); i++) {
ActivityRecord r = app.activities.get(i);
// 如果当前有正在销毁状态的Activity,Do Nothing
if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
return;
}
// 只有Activity在可以销毁状态的时候才继续往下走
if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
|| r.state == PAUSED || r.state == STOPPING) {
continue;
}
if (r.task != null) {
if (firstTask == null) {
firstTask = r.task;
} else if (firstTask != r.task) {
// 2.1 只有存在两个以上的Task的时候才会到这里
if (tasks == null) {
tasks = new ArraySet<>();
tasks.add(firstTask);
}
tasks.add(r.task);
}
}
}
// 2.2 只有存在两个以上的Task的时候才不为空
if (tasks == null) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
return;
}
// If we have activities in multiple tasks that are in a position to be destroyed,
// let's iterate through the tasks and release the oldest one.
// 2.3 遍历找到ActivityStack释放最旧的那个
final int numDisplays = mActivityDisplays.size();
for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
// Step through all stacks starting from behind, to hit the oldest things first.
// 从后面开始遍历,从最旧的开始匹配
for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
final ActivityStack stack = stacks.get(stackNdx);
// Try to release activities in this stack; if we manage to, we are done.
// 尝试在这个stack里面销毁这些Activities,如果成功就返回。
if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
return;
}
}
}
}

上面代码都加了注释,我们来理一理重点需要关注的点。整个流程可以观察tasks的走向



  • 2.1 & 2.2: 第一次循环会给firstTask赋值,当firstTask != r.task的时候才会给tasks赋值,后续会继续对tasks操作。所以单栈的应用不会回收,如果tasks为null,就直接return了,什么都不做

  • 2.3: 这一大段的双重for循环其实都没有第一步遍历出来的tasks参与,真正释放Activity的操作在ActivityStack中,所以尝试找到这些tasks对应的ActivityStack,让ActivityStack去销毁tasks,直到成功销毁。


继续查看releaseSomeActivitiesLocked:

// step③ ActivityStack.java
final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks, String reason) {
// Iterate over tasks starting at the back (oldest) first.
int maxTasks = tasks.size() / 4;
if (maxTasks < 1) {
maxTasks = 1;
}
// 3.1 maxTasks至少为1,至少清理一个
int numReleased = 0;
for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
final TaskRecord task = mTaskHistory.get(taskNdx);
if (!tasks.contains(task)) {
continue;
}
int curNum = 0;
final ArrayList<ActivityRecord> activities = task.mActivities;
for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
final ActivityRecord activity = activities.get(actNdx);
if (activity.app == app && activity.isDestroyable()) {
destroyActivityLocked(activity, true, reason);
if (activities.get(actNdx) != activity) {
// Was removed from list, back up so we don't miss the next one.
// 3.2 destroyActivityLocked后续会调用TaskRecord.removeActivity(),所以这里需要将index--
actNdx--;
}
curNum++;
}
}
if (curNum > 0) {
numReleased += curNum;
// 移除一个,继续循环需要判断 maxTasks > 0
maxTasks--;
if (mTaskHistory.get(taskNdx) != task) {
// The entire task got removed, back up so we don't miss the next one.
// 3.3 如果整个task都被移除了,这里同样需要将获取Task的index--。移除操作在上面3.1的destroyActivityLocked,移除Activity过程中,如果task为空了,会将task移除
taskNdx--;
}
}
}
return numReleased;
}



  • 3.1: ActivityStack利用maxTasks 保证,最多清理tasks.size() / 4,最少清理1个TaskRecord,同时,至少要保证保留一个前台可见TaskRecord,比如如果有两个TaskRecord,则清理先前的一个,保留前台显示的这个,如果三个,则还要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有则只清理一个,保留两个,如果没有,则继续清理次老的,保留一个前台展示的,如果有四个,类似,如果有5个,则至少两个清理。一般APP中,很少有超过两个TaskRecord的。




  • 3.2: 这里清理的逻辑很清楚,for循环,如果定位到了期望的activity就清理掉,但这里这个actNdx--是为什么呢?注释说activity从list中移除了,为了能继续往下走,需要index--,但在这个方法中并没有将activity从lsit中移除的操作,那肯定是在destroyActivityLocked方法中。继续追进去可以一直追到TaskRecord.java#removeActivity(),从当前的TaskRecord的mActivities中移除了,所以需要index--。




  • 3.3: 我们弄懂了上面的actNdx--之后也就知道这里为什么要index--了,在ActivityStack.java#removeActivityFromHistoryLocked()中有

	if (lastActivity) {
removeTask(task, reason, REMOVE_TASK_MODE_DESTROYING);
}

如果task中没有activity了,需要将这个task移除掉。


以上就是基于API 28的releaseSomeActivities分析。


2.3.2. 基于29+的版本的releaseSomeActivities实现如下:
// ActivityTaskManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized (mGlobalLock) {
final long origId = Binder.clearCallingIdentity();
try {
final WindowProcessController app = getProcessController(appInt);
app.releaseSomeActivities("low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// WindowProcessController.java
void releaseSomeActivities(String reason) {
// Examine all activities currently running in the process. Candidate activities that can be destroyed.
// 检查进程里所有的activity,看哪些可以被关掉
ArrayList<ActivityRecord> candidates = null;
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Trying to release some activities in " + this);
for (int i = 0; i < mActivities.size(); i++) {
final ActivityRecord r = mActivities.get(i);
// First, if we find an activity that is in the process of being destroyed,
// then we just aren't going to do anything for now; we want things to settle
// down before we try to prune more activities.
// 首先,如果我们发现一个activity正在执行关闭中,在关掉这个activity之前什么都不做
if (r.finishing || r.isState(DESTROYING, DESTROYED)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Abort release; already destroying: " + r);
return;
}
// Don't consider any activities that are currently not in a state where they can be destroyed.
// 如果当前activity不在可关闭的state的时候,不做处理
if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
|| r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
continue;
}

if (r.getParent() != null) {
if (candidates == null) {
candidates = new ArrayList<>();
}
candidates.add(r);
}
}

if (candidates != null) {
// Sort based on z-order in hierarchy.
candidates.sort(WindowContainer::compareTo);
// Release some older activities
int maxRelease = Math.max(candidates.size(), 1);
do {
final ActivityRecord r = candidates.remove(0);
r.destroyImmediately(true /*removeFromApp*/, reason);
--maxRelease;
} while (maxRelease > 0);
}
}

新版本的releaseSomeActivities放到了ActivityTaskManagerService.java这个类中,这个类是API 29新添加的,承载部分AMS的工作。
相比API 28基于Task栈的回收Activity策略,新版本策略简单清晰, 也激进了很多。


遍历所有Activity,刨掉那些不在可销毁状态的Activity,按照Activity堆叠的顺序,也就是Z轴的顺序,从老到新销毁activity。


有兴趣的读者可以自行编写测试代码,分别在API 28和API 28+的手机上测试看一下回收策略是否跟上面分析的一致。

也可以参考我写的TestKillActivity,单栈和多栈的情况下在高于API 28和低于API 28的手机上的表现。


总结:



  1. 系统内存不足时LMK会根据内存配置项来kill掉进程释放内存

  2. kill时会按照进程的ADJ规则来kill

  3. App内存不足时由GcWatcher来决定回收Activity的时机

  4. 可以使用getprop命令来查看当前手机的JVM内存分配和OOM配置

  5. releaseSomeActivities在API 28和API 28+的差别很大,低版本会根据Task数量来决定清理哪个task的。高版本简单粗暴,遍历activity,按照z order排序,优先release掉更老的activity。

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

从前端变化看终身就业

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各...
继续阅读 »

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各种担心、顾虑;当然还有工作,一份安稳的、相对不错的收入是一家人生活的保障,也让希望增加了不少,今天跟大家一起聊聊工作吧!



终身就业还是终身职业



作为社会人、搬砖人,已经错过了最好的机会;也许是从来就不曾有过机会,已经失去了终生职业的可能(公务员、事业单位....);在国内很少有人能在一家企业供职到领社保,听说国外(比如德国、日本)有,我想说的是: 而今我们(在能力不是超强、又没有Backgroud的情况下)应该努力追求终生就业; 



企业自然想要你终生职业,但实际呢? 不管在任何一家单位,企业总是希望员工极度忠诚,可以胜任公司各种任务,把公司当自己家,有奉献精神;错吗? ---当然是对的; 如果我是老板我也会这么想;


很不幸,现实和理想总是惊为天人,面对糟糕的经济形式,活下去成为企业第一要务,而精简人员总是成为企业断臂求生的一种惯用方式,现实就是残酷如此;于个人而言,在不乐观的环境中能有一份相对稳定的工作,有个可观的收入就显得极为重要,这就需要追求终生就业的能力(不论什么行业);


互联网的发展与前端



说说自己这10多年的心路历程,可能是反面教材,如果能为你带来一些参考或借鉴或一些帮助我是很高兴的;



进入编程世界,与PHP的初恋

进入编程世界,源于羡慕!(2010年)看到同学用HTML写了一个表单,当时觉得觉得很高级,很厉害; 当时他学的是PHP(号称最好的语言),所以也就不自然的被影响、认可了PHP;


转行

从高中到大学,想从事的一直是健身相关的行业和工作,但是真正接触了发现似乎跟自己设想的那么好;当然这不是真正导致转行的原因;真正原因是朋友找我(大学专业:软件工程,但是从来没有学过)做一个网站,我却屁都不会,跟亲戚、朋友说自己还是软件相关专业毕业的;所以,为了装X,也为了对得起软件工程专业那个本本(成人自考),放弃了自己研究多年的健身,毅然报了培训班学起了PHP; 诸位有没有跟我一样的呢?


痛苦的学习

大学几年浑浑噩噩的过了,去报PHP培训时候,老师说:“学过C很容易学的,PHP先从HTML开始,很容易上手的”;交钱的时候自以为学过(上课虽然在睡觉)也或多或少听了一些,没吃过猪肉还没见过猪跑啊(实际上啥都不会),应该问题不大;想起同学说(10年刚毕业)在深圳(拿6-8K),就开始幻想上了;


最难的Table + CSS布局

学习PHP的路上,最让我难堪的竟然是HTML和CSS;保守起见,老师选择了(相比DIV + CSS)更为简单的Table(用Dreamweaver拖拽) + CSS;然而,半个月过去了,竟然连写个百度首页都写不出来;呜呼哀哉,布局难难于上青天!恰逢十一国庆节,老师留的任务就是写个百度首页,如果连百度首页都写不出来,那说明不适合走这一行;结果呢? 一周过后还是写不出来,唉,每天上课时候会想到一万个放弃,回家后每一分钟都会想到N多个放弃;后来想着,钱也交了,就多坚持一下吧,就稀里糊涂的把课程学完了(HTML CSS Javascript PHP)


找到适合自己的方向

课程结束的时候,老师给个建议: PHP感觉有难度,就好好把div + css + jQuery学好,做前端、做前端、做前端!然而,入门的我还是选择了做PHP,一年多的时间学会了从切图、写页面、写PHP、写SQL语句、搭建服务器,天呐,完全飘了(实际上还是个小白),直到偶然机会(2013年)做了前端,突然找到了码页面的灵感,这种所见即所得的搬砖工作很有感觉,哈哈哈;


其实,这里想说的是:1是坚持;2是老师的层次比我高很多,他在很早就给我指明了道路,而执着于自己的愚见(当然也不全是错,也有收获),最后还是走上了老师指导的方向!


诸位,如果你们有个好的老师、高人指导,那是极为幸福的事情,一定要珍惜!


PS: (2011年)《编写高质量代码--web前端修炼之道》这本书对我前端方面的能力提升帮助非常大; 同时也感谢作者: 阿当,在我成长道路上的一些指导和帮助;


PS: 现在互联网平台很发达,在学习视频课程、阅读技术类书籍、技术资料的时候,建议可以尝试联系一下作者(或译者);很多技术大牛还是很乐意给一些建议和指导的<致敬>;


学会听取建议、做出自己的判断

3年后,厚着脸皮请教老师接下来该学点啥能让薪资再增长一些,对未来有帮助; 老师给了一个方向: “Web GIS”,这一次照做了,掌握了一些Gis相关的基础,了解了Arcgis for javascript的常用方法等,结合近期的招聘,我觉得这算是很好的扩展了自己的选择;


PS: 建议菜鸟多向行业内的大牛请教,向身边段位高的朋友、同事多请教;


拥抱变化



互联网变化之快,技术更新之快,已经让很多人发出"学不动"的呼喊,但是我想说的是,只要你还想吃这碗饭,学不动还是要学;



yu6.png


学习&&提升

记得入行时前端面试:

- 会不会处理IE6、IE8兼容,有没有写过hack

- DIV + CSS 怎么实现div的垂直居中和水平居中,有几种实现方式

- 块级标签和行内标签的区别

- jQuery的prop方法和attr方法的区别

- Ajax有没有用过

- 会不会PS切图?gif和png的区别

- 什么是闭包?举个栗子


再后来学习了: 


- Bootstrap (不用了)

- AngularJs \ BackboneJS (不用了)

- requireJs \ seajs (不用了)

- grunt \ gulp \ bower(不用了)

- 响应式布局 (几乎不用了)


现在用的Vue \ React 也写了有好多年了,我想很快也会被新的所替代吧;

18年花了接近一万大洋购买了珠峰架构的课程,系统的学习了几个月,算是第二次技术比较大的提升吧,当然收入也相应的提高了一些;


PS: 想分享的是,很多技能可能生命周期很短,但是,身处当下我们还是要去积极学习,哪怕后来不用了,但是里面的一些思想会给我们未来某个时候带来很多帮助(懂得Bootstrap的设计思想就容易理解less\sass的使用,看到ElementUI、AntD等就一看就懂);


PS: 决定工作岗位、薪资的技术只是一部分,切勿过于迷恋于某个技术,跟随时代、拥抱变化,市场才是决定二者的最重要的因素!


运动&&养生

说点轻松又严肃的,各位看官,身体才是革命的本钱! 10年的老菜鸟目前除了颈椎不舒服(怪手机不怪写代码)外,其他的还好,论加班还能跟年轻人一战,哈哈哈! 这当然得益于过去多年的习惯:


- 经常跑步、爬山、健身;

- 很少胡吃海喝,水果吃的多,烧烤啤酒几乎不碰;

- 每天吃饭不吃饱,原则上是不饿就行;


PS: 建议大家适当的增加运动; 如果歇了很久,要启动你的小马达,要慢慢来,勿操之过急; 最重要的是坚持;


"舍"&&"得"


舍得之间品味人生,舍得之间自有方寸;然而,舍 && 得又何其的难;



- 菜鸟期间的我是舍不得花钱买课程学习的,心疼钱啊; 后来受朋友影响开始花钱去买课程,花钱找老师学习(有的技能人家凭什么告诉你呢?),发现自己的进步突然就快了很多、收获也很大(为什么工作后就不舍得花钱学习了?);


- 知识就是金钱,如今我们知道听歌、追剧都要买VIP,为什么找工作的时候不知道购买VIP呢(我好多朋友、同事上BOSS刷招聘说每天都是那几个,殊不知买了VIP后消息就多了很多,你都没购买服务,招聘APP凭什么给你最新的资讯呢?)


- 工作、学习之余一定要花点时间去陪陪家人、运动、多走一走(哪怕是带小孩玩、哪怕去公园晒晒太阳、去商场逛逛看看美女),工作、技术很重要,人生的全部还有很多;工作是个弹力球,掉下去还有机会弹起来,而身体、家庭是玻璃球,要是碎了那就。。。


踏平坎坷成大道,路就在脚下

- 说了那么多,此刻会想什么呢? 代码要一行一行的写,日子还得一天一天的过,我曾因为负债累累(每个月却只发一次工资)而着急,然急又能如何,倒不如平静以对,正如《论语》中有云: "吾尝终日不食 终夜不寝,以思,无益,不如学也"!


- 环境不友好,是不是就没有机会了? ----当然有机会,当然有路可走! 

- 路在哪? ---- 路在脚下


前端的路该怎么走


各位,我们看到招聘APP上前端岗的需求量比往年同期少很多,这个是事实;与此同时企业还是有各种各样的需求的; 2023年了,还是以过去的思维去看(劳资会Vue 、 React),无异于缘木求鱼,那一定会让你感动悲观;何不换个思路、换个角度呢?



- 大前端方向还有很大空间: Vue\React + Flutter(或类似) + 小程序,正所谓:“山重水复疑无路 柳暗花明又一村”


- 前端 + GIS(或3D),观察BOSS上关于Webgis的招聘就知道了,如果能先于大多数人掌握了GIS、3D方面的知识,那选择是不是广阔了很多,正所谓: "有心栽花花不开 无心插柳柳成荫",何必要拘泥于某一种形态呢


- 前端架构师也是一些技术深度追求者的方向


(个人在二线城市,结合自己的经历和对Boss上岗位、薪资变化的观察,提出的拙见,欢迎批评、指导)


结语


- 强哥说了:“风浪越大鱼越贵”,挑战与机遇共存,我们应当在大变化的浪潮中调整自己的帆,拥抱惊涛骇浪和变化,磨砺出终身就业的能力!



  • 不要给自己贴标签(强哥:“我就是个卖鱼的”),现在的处境不代表未来没有机会、希望(到强盛集团);


- 编码之路上是: 路漫漫其修远兮 吾将上下而求索


- 人生道路上需要另一种气度《定风波·莫听穿林打叶声》---苏轼 : "莫听穿林打叶声 何妨吟啸且徐行; 竹杖芒鞋轻胜马,谁怕? 一蓑烟雨任平生; 料峭春风吹酒醒,微冷,山头斜照却相迎; 回首向来萧瑟处,归去, 也无风雨也无晴" 。


作者:风雪中的兔子
来源:juejin.cn/post/7220800667589197885
收起阅读 »

跟我一起探索 HTTP-HTTP缓存

web
概览 HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。 可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。 此外,当响应可复用时,源服务器不需要处理...
继续阅读 »

概览


HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。


可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。


此外,当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。


缓存的正确操作对系统的稳定运行至关重要。


不同种类的缓存


HTTP Caching 标准中,有两种不同类型的缓存:私有缓存共享缓存


私有缓存


私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。


另一方面,如果个性化内容存储在私有缓存以外的缓存中,那么其他用户可能能够检索到这些内容——这可能会导致无意的信息泄露。


如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令。


Cache-Control: private

个性化内容通常由 cookie 控制,但 cookie 的存在并不能表明它是私有的,因此单独的 cookie 不会使响应成为私有的。


请注意,如果响应具有 Authorization 标头,则不能将其存储在私有缓存(或共享缓存,除非 Cache-Control 指定的是 public)中。


共享缓存


共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存托管缓存


代理缓存


除了访问控制的功能外,一些代理还实现了缓存以减少网络流量。这通常不由服务开发人员管理,因此必须由恰当的 HTTP 标头等控制。然而,在过去,过时的代理缓存实现——例如没有正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。


Kitchen-sink 标头如下所示,用于尝试解决不理解当前 HTTP 缓存规范指令(如 no-store)的“旧且未更新的代理缓存”的实现。


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

然而,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能传输响应而不能充当缓存。因此,在这种情况下,无需担心甚至无法看到响应的过时代理缓存的实现。


另一方面,如果 TLS 桥接代理通过在 PC 上安装来自组织管理的 CA 证书,以中间人方式解密所有通信,并执行访问控制等,则可以查看响应的内容并将其缓存。但是,由于证书透明度(certificate transparency)在最近几年变得很普遍,并且一些浏览器只允许使用证书签署时间戳(signed certificate timestamp)颁发的证书,因此这种方法需要应用于企业策略。在这样的受控环境中,无需担心代理缓存“已过时且未更新”。


托管缓存


托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。示例包括反向代理、CDN 和 service worker 与缓存 API 的组合。


托管缓存的特性因部署的产品而异。在大多数情况下,你可以通过 Cache-Control 标头和你自己的配置文件或仪表板来控制缓存的行为。


例如,HTTP 缓存规范本质上没有定义显式删除缓存的方法——但是使用托管缓存,可以通过仪表板操作、API 调用、重新启动等实时删除已经存储的响应。这允许更主动的缓存策略。


也可以忽略标准 HTTP 缓存规范协议以支持显式操作。例如,可以指定以下内容以选择退出私有缓存或代理缓存,同时使用你自己的策略仅在托管缓存中进行缓存。


Cache-Control: no-store

例如,Varnish Cache 使用 VCL(Varnish Configuration Language,一种 DSL逻辑来处理缓存存储,而 service worker 结合缓存 API 允许你在 JavaScript 中创建该逻辑。


这意味着如果托管缓存故意忽略 no-store 指令,则无需将其视为“不符合”标准。你应该做的是,避免使用 kitchen-sink 标头,但请仔细阅读你正在使用的任何托管缓存机制的文档,并确保你选择的方式可以正确的控制缓存。


请注意,某些 CDN 提供自己的标头,这些标头仅对该 CDN 有效(例如,Surrogate-Control)。目前,正在努力定义一个 CDN-Cache-Control 标头来标准化这些标头。


缓存的类型


启发式缓存


HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存


例如,采取以下响应。此回复最后一次更新是在 1 年前。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

<!doctype html>


试探性地知道,整整一年没有更新的内容在那之后的一段时间内不会更新。因此,客户端存储此响应(尽管缺少 max-age)并重用它一段时间。复用多长时间取决于实现,但规范建议存储后大约 10%(在本例中为 0.1 年)的时间。


启发式缓存是在 Cache-Control 被广泛采用之前出现的一种解决方法,基本上所有响应都应明确指定 Cache-Control 标头。


基于 age 的缓存策略


存储的 HTTP 响应有两种状态:freshstalefresh 状态通常表示响应仍然有效,可以重复使用,而 stale 状态表示缓存的响应已经过期。


确定响应何时是 fresh 的和何时是 stale 的标准是 age。在 HTTP 中,age 是自响应生成以来经过的时间。这类似于其他缓存机制中的 TTL


以下面的示例响应为例(604800 秒是一周):


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>


存储示例响应的缓存会计算响应生成后经过的时间,并将结果用作响应的 age


对于该示例的响应,max-age 的含义如下:



  • 如果响应的 age 小于一周,则响应为 fresh

  • 如果响应的 age 超过一周,则响应为 stale


只要存储的响应保持新鲜(fresh),它将用于兑现客户端请求。


当响应存储在共享缓存中时,有必要通知客户端响应的 age。继续看示例,如果共享缓存将响应存储了一天,则共享缓存将向后续客户端请求发送以下响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>


收到该响应的客户端会发现它在剩余的 518400 秒内是新鲜(fresh)的,这是响应的 max-ageAge 之间的差异。


Expires 或 max-age


在 HTTP/1.0 中,新鲜度过去由 Expires 标头指定。


Expires 标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。


Expires: Tue, 28 Feb 2022 22:22:22 GMT

但是时间格式难以解析,也发现了很多实现的错误,有可能通过故意偏移系统时钟来诱发问题;因此,在 HTTP/1.1 中,Cache-Control 采用了 max-age——用于指定经过的时间。


如果 ExpiresCache-Control: max-age 都可用,则将 max-age 定义为首选。因此,由于 HTTP/1.1 已被广泛使用,无需特地提供 Expires


Vary 响应


区分响应的方式本质上是基于它们的 URL:


使用 url 作为键


但是响应的内容并不总是相同的,即使它们具有相同的 URL。特别是在执行内容协商时,来自服务器的响应可能取决于 AcceptAccept-LanguageAccept-Encoding 请求标头的值。


例如,对于带有 Accept-Language: en 标头并已缓存的英语内容,不希望再对具有 Accept-Language: ja 请求标头的请求重用该缓存响应。在这种情况下,你可以通过在 Vary 标头的值中添加“Accept-Language”,根据语言单独缓存响应。


Vary: Accept-Language

这会导致缓存基于响应 URLAccept-Language请求标头的组合进行键控——而不是仅仅基于响应 URL。


使用 url 和语言作为键


此外,如果你基于用户代理提供内容优化(例如,响应式设计),你可能会想在 Vary 标头的值中包含“User-Agent”。但是,User-Agent 请求标头通常具有非常多的变体,这大大降低了缓存被重用的机会。因此,如果可能,请考虑一种基于特征检测而不是基于 User-Agent 请求标头来改变行为的方法。


对于使用 cookie 来防止其他人重复使用缓存的个性化内容的应用程序,你应该指定 Cache-Control: private 而不是为 Vary 指定 cookie。


验证响应


过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证


验证是通过使用包含 If-Modified-SinceIf--Match 请求标头的条件请求完成的。


If-Modified-Since


以下响应在 22:22:22 生成,max-age 为 1 小时,因此你知道它在 23:22:22 之前是新鲜的。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>


到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified


由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。


HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

收到该响应后,客户端将存储的陈旧响应恢复为新鲜的,并可以在剩余的 1 小时内重复使用它。


服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。


为了解决这些问题,ETag 响应标头被标准化作为替代方案。


ETag/If--Match


ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。


举个例子,如果 ETag 标头使用了 hash 值,index.html 资源的 hash 值是 deadbeef,响应如下:


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600

<!doctype html>


如果该响应是陈旧的,则客户端获取缓存响应的 ETag 响应标头的值,并将其放入 If--Match 请求标头中,以询问服务器资源是否已被修改:


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If--Match: "deadbeef"

如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If--Match 值相同,则服务器将返回 304 Not Modified


但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。



备注: 在评估如何使用 ETagLast-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETagLast-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified 是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETagLast-Modified



强制重新验证


如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。


通过在响应中添加 Cache-Control: no-cache 以及 Last-ModifiedETag——如下所示——如果请求的资源已更新,客户端将收到 200 OK 响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified 响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>


max-age=0must-revalidate 的组合与 no-cache 具有相同的含义。


Cache-Control: max-age=0, must-revalidate

max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。


然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache 这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。


但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0must-revalidate 组合——你应该只使用 no-cache


不使用缓存


no-cache 指令不会阻止响应的存储,而是阻止在没有重新验证的情况下重用响应。


如果你不希望将响应存储在任何缓存中,请使用 no-store


Cache-Control: no-store

但是,一般来说,实践中“不缓存”的原因满足以下情况:



  • 出于隐私原因,不希望特定客户以外的任何人存储响应。

  • 希望始终提供最新信息。

  • 不知道在过时的实现中会发生什么。


在这种情况下,no-store 并不总是最合适的指令。


以下部分更详细地介绍了这些情况。


不与其他用户共享


如果具有个性化内容的响应意外地对缓存的其他用户可见,那将是有问题的。


在这种情况下,使用 private 指令将导致个性化响应仅与特定客户端一起存储,而不会泄露给缓存的任何其他用户。


Cache-Control: private

在这种情况下,即使设置了 no-store,也必须设置 private


每次都提供最新的内容


no-store 指令阻止存储响应,但不会删除相同 URL 的任何已存储响应。


换句话说,如果已经为特定 URL 存储了旧响应,则返回 no-store 不会阻止旧响应被重用。


但是,no-cache 指令将强制客户端在重用任何存储的响应之前发送验证请求。


Cache-Control: no-cache

如果服务端不支持条件请求,你可以强制客户端每次都访问服务端,总是得到最新的 200 OK 响应。


兼容过时的实现


作为忽略 no-store 的过时实现的解决方法,你可能会看到使用了诸如以下内容的 kitchen-sink 标头:


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

推荐使用 no-cache 作为处理这种过时的实现的替代方案,如果从一开始就设置 no-cache 就没问题,因为服务器总是会收到请求。


如果你关心的是共享缓存,你可以通过添加 private 来防止意外缓存:


Cache-Control: no-cache, private

no-store 丢失了什么


你可能认为添加 no-store 是选择退出缓存的正确方法。


但是,不建议随意授予 no-store,因为你失去了 HTTP 和浏览器所拥有的许多优势,包括浏览器的后退/前进缓存。


因此,要获得 Web 平台的全部功能集的优势,最好将 no-cacheprivate 结合使用。


重新加载和强制重新加载


可以对请求和响应执行验证。


重新加载强制重新加载操作是从浏览器端执行验证的常见示例。


重新加载


为了从页面错误中恢复或更新到最新版本的资源,浏览器为用户提供了重新加载功能。


在浏览器重新加载期间发送的 HTTP 请求的简化视图如下所示:


GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If--Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT

请求中的 max-age=0 指令指定“重用 age 为 0 或更少的响应”——因此,中间存储的响应不会被重用。


请求通过 If--MatchIf-Modified-Since 进行验证。


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 no-cache 的情况下,在 JavaScript 中调用 fetch() 来重现(注意 reload 不是这种情况下的正确模式):


// 注意:“reload”不是正常重新加载的正确模式;“no-cache”才是
fetch("/", { cache: "no-cache" });

强制重新加载


出于向后兼容的原因,浏览器在重新加载期间使用 max-age=0——因为在 HTTP/1.1 之前的许多过时的实现中不理解 no-cache。但是在这个用例中,no-cache 已被支持,并且强制重新加载是绕过缓存响应的另一种方法。


浏览器强制重新加载期间的 HTTP 请求如下所示:


GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache

由于这不是带有 no-cache 的条件请求,因此你可以确定你会从源服务器获得 200 OK


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 reload 的情况下,在 JavaScript 中调用 fetch() 来重现(注意它不是 force-reload):


// 注意:“reload”——而不是“no-cache”——是“强制重新加载”的正确模式
fetch("/", { cache: "reload" });

避免重新验证


永远不会改变的内容应该被赋予一个较长的 max-age,方法是使用缓存破坏——也就是说,在请求 URL 中包含版本号、哈希值等。


但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。


为了防止这种情况,immutable 指令可用于明确指示不需要重新验证,因为内容永远不会改变。


Cache-Control: max-age=31536000, immutable

这可以防止在重新加载期间进行不必要的重新验证。


删除存储的响应


基本上没有办法删除用很长的 max-age 存储的响应。


想象一下,来自 https://example.com/ 的以下响应已被存储。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000

<!doctype html>


一旦响应在服务器上过期,你可能希望覆盖该响应,但是一旦存储响应,服务器就无法执行任何操作——因为由于缓存,不再有请求到达服务器。


规范中提到的方法之一是使用不安全的方法(例如 POST)发送对同一 URL 的请求,但对于许多客户端而言,通常很难故意这样做。


还有一个 Clear-Site-Data: cache 标头和值的规范,但并非所有浏览器都支持它——即使使用它,它也只会影响浏览器缓存,而不会影响中间缓存。


因此,除非用户手动执行重新加载、强制重新加载或清除历史操作,否则应该假设任何存储的响应都将保留其 max-age 期间。


缓存减少了对服务器的访问,这意味着服务器失去了对该 URL 的控制。如果服务器不想失去对 URL 的控制——例如,在资源被频繁更新的情况下——你应该添加 no-cache,以便服务器始终接收请求并发送预期的响应。


请求折叠


共享缓存主要位于源服务器之前,旨在减少到源服务器的流量。


因此,如果多个相同的请求同时到达共享缓存,中间缓存将代表自己将单个请求转发到源,然后源可以将结果重用于所有客户端。这称为请求折叠


当请求同时到达时会发生请求折叠,因此即使响应中给出了 max-age=0no-cache,它也会被重用。


如果响应是针对特定用户个性化的,并且你不希望它在折叠中共享,则应添加 private 指令:


请求折叠


常见的缓存模式


Cache-Control 规范中有很多指令,可能很难全部理解。但是大多数网站都可以通过几种模式的组合来覆盖。


本节介绍设计缓存的常见模式。


默认设置


如上所述,缓存的默认行为(即对于没有 Cache-Control 的响应)不是简单的“不缓存”,而是根据所谓的“启发式缓存”进行隐式缓存。


为了避免这种启发式缓存,最好显式地为所有响应提供一个默认的 Cache-Control 标头。


为确保默认情况下始终传输最新版本的资源,通常的做法是让默认的 Cache-Control 值包含 no-cache


Cache-Control: no-cache

另外,如果服务实现了 cookie 或其他登录方式,并且内容是为每个用户个性化的,那么也必须提供 private,以防止与其他用户共享:


Cache-Control: no-cache, private

缓存破坏


最适合缓存的资源是静态不可变文件,其内容永远不会改变。而对于会变化的资源,通常的最佳实践是每次内容变化时都改变 URL,这样 URL 单元可以被缓存更长的时间。


例如,考虑以下 HTML:


<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>

在现代 Web 开发中,JavaScript 和 CSS 资源会随着开发的进展而频繁更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,则显示将中断。


所以上面的 HTML 用 max-age 缓存 bundle.jsbuild.css 变得很困难。


因此,你可以使用包含基于版本号或哈希值的更改部分的 URL 来提供 JavaScript 和 CSS。一些方法如下所示。


# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js

# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

由于缓存根据它们的 URL 来区分资源,因此如果在更新资源时 URL 发生变化,缓存将不会再次被重用。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

通过这种设计,JavaScript 和 CSS 资源都可以被缓存很长时间。那么 max-age 应该设置多长时间呢?QPACK 规范提供了该问题的答案。


QPACK 是一种用于压缩 HTTP 标头字段的标准,其中定义了常用字段值表。


一些常用的缓存头值如下所示。


36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000

如果你选择其中一个编号选项,则可以在通过 HTTP3 传输时将值压缩为 1 个字节。


数字“37”、“38”和“41”分别代表一周、一个月和一年。


因为缓存会在保存新条目时删除旧条目,所以一周后存储的响应仍然存在的可能性并不高——即使 max-age 设置为 1 周。因此,在实践中,你选择哪一种并没有太大的区别。


请注意,数字“41”具有最长的 max-age(1 年),但具有 public


public 值具有使响应可存储的效果,即使存在 Authorization 标头。



备注: 只有在设置了 Authorization 标头时需要存储响应时才应使用 public 指令。否则不需要,因为只要给出了 max-age,响应就会存储在共享缓存中。



因此,如果响应是使用基本身份验证进行个性化的,public 的存在可能会导致问题。如果你对此感到担忧,你可以选择第二长的值 38(1 个月)。


# response for bundle.v123.js

# If you never personalize responses via Authorization
Cache-Control: public, max-age=31536000

# If you can't be certain
Cache-Control: max-age=2592000

验证响应


不要忘记设置 Last-ModifiedETag 标头,以便在重新加载时不必重新传输资源。对于预构建的静态文件生成这些标头很容易。


这里的 ETag 值可能是文件的哈希值。


# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

此外,可以添加 immutable 以防止重新加载时验证。


组合结果如下所示。


# bundle.v123.js
200 OK HTTP/1.1
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

缓存破坏是一种通过在内容更改时更改 URL 来使响应在很长一段时间内可缓存的技术。该技术可以应用于所有子资源,例如图像。


备注: 在评估 immutable 和 QPACK 的使用时:如果你担心 immutable 会更改 QPACK 提供的预定义值,请考虑在这种情况下,immutable 部分可以通过将 Cache-Control 值分成两行来单独编码——尽管这取决于特定 QPACK 实现使用的编码算法。


Cache-Control: public, max-age=31536000
Cache-Control: immutable

主要资源


与子资源不同,主资源不能使用缓存破坏,因为它们的 URL 不能像子资源 URL 一样被修饰。


如果存储以下 HTML 本身,即使在服务器端更新内容,也无法显示最新版本。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

对于这种情况,no-cache 将是合适的——而不是 no-store——因为我们不想存储 HTML,而只是希望它始终是最新的。


此外,添加 Last-ModifiedETag 将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE

该设置适用于非个性化 HTML,但对于使用 cookie 进行个性化的响应(例如,在登录后),不要忘记同时指定 private


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly

favicon.icomanifest.json.well-known 和无法使用缓存破坏更改 URL 的 API 端点也是如此。


大多数 Web 内容都可以通过上述两种模式的组合来覆盖。


有关托管缓存的更多信息


使用前面章节描述的方法,子资源可以通过缓存破坏来缓存很长时间,但主资源(通常是 HTML 文档)不能。


缓存主要资源很困难,因为仅使用 HTTP 缓存规范中的标准指令,在服务器上更新内容时无法主动删除缓存内容。


但是,可以通过部署托管缓存(例如 CDN 或 service worker)来实现。


例如,允许通过 API 或仪表板操作清除缓存的 CDN 将通过存储主要资源并仅在服务器上发生更新时显式清除相关缓存来实现更积极的缓存策略。


如果 service worker 可以在服务器上发生更新时删除缓存 API 中的内容,它也可以这样做。


作者:demo007x
来源:juejin.cn/post/7237022394790281271
收起阅读 »

时光里能否开出花

时光里能否开出花 昨天偶然翻出了一个老电影《何时是读书天》,有人会觉得压抑而难以理解,有人会关注里面的狗血情节。每个人的故事虽然都不同,但是归宿终究还是回到一条道上--如何让自己幸福。 似乎情感文章不应该发到技术圈,作为一名技术人,每天最多的事大概就是用充沛的...
继续阅读 »

时光里能否开出花


昨天偶然翻出了一个老电影《何时是读书天》,有人会觉得压抑而难以理解,有人会关注里面的狗血情节。每个人的故事虽然都不同,但是归宿终究还是回到一条道上--如何让自己幸福。


似乎情感文章不应该发到技术圈,作为一名技术人,每天最多的事大概就是用充沛的精力设计项目,规划项目,反思项目。但自然和世界不是人造事物,作为一个人,在生活和人生上始终要屈从于人类最原始的感情。


回顾


从毕业到现在,感觉就是一眨眼的事,但好像经历了很多。一开始每天想的是coding,怎么处理扩展性最好,一般在工作时间奴鲁推进,最喜欢下班在路上在思想中徘徊,这个节点和那个环节的对接。这样过了一段时间(说不好,感觉我的感受时间被冻结了),仿佛触碰到了瓶颈,感觉成了业务和上游数据生产者的工具,对于被拆解后的一个个任务也感觉到无趣。


还记得我在上学时,那时候会遇到各样的人,很容易遇到志同道合的人。和同学探讨小说《时间移民》,他抛出问题‘如果你不断穿越,到未来500年,一万年,这时候人类实现了意识上传,可以完成任何幻想,你会怎么办。’只是幻想在思想命题‘上帝能否造出一块它举不起的石头’这样的寻找假设漏洞的简单辩论的我,只能空洞地制造一些空泛的回答。那时候我还不明白‘人生的定义’,‘幸福的定义’,好像历史,自然,那些在长河中熠熠生辉的事物,在我的脑中只是一个个概念的字符。


那时候只是简单翻看《乔布斯传》,就以为伟大的产品,创意这些就是一个个文字意思,现在想来,真是无知的幸福。这些堵上多少人的创意才可能推进历史,流传成故事,里面浓缩了怎样的心血、放弃、执着。埋头在选择题,问答题的我,思考意识不到,这样的故事就是你以后的主线,每一次抉择、痛苦和兴奋,甚至连变成故事藏于大山的资格都没有。


当我带着只是‘识别纸质文字大脑’的我,进入工作,人生变成了一副绘声绘色,崇山峻岭的彩色图,不断打破自信而又建立信心,越发感受到我应该认识到‘人生应该是个什么样’。


触碰电影的感受


这部电影故事线就不赘述了,主暗线都很明显。虽然电影名以读书为主旨,但电影里读书的情节只在几个片段,没有主人公一脸淡然地拿着书在晨光或者夕阳下沉思,结合上下文,能看到主角在一个孤独且消沉的环境中抑制着情感捧着书,真如‘一个瘦弱的身子里藏着一个深邃的灵魂’。电影里,送牛奶,收营员这些普通的元素,现在看来有些狗血的故事线,我仍然忍不住和主角共情。不断有人问主角‘你孤独吗’,质疑和打击一直追随主角,甚至结尾你以为获得了幸福,又一瞬间落入谷底。面对‘未来怎么过’的疑问,主人公还是说着‘继续读书’。我压抑着心中的冲动,仿佛感受不同时光相同归途的念头。


我觉得最温馨的就是主人公握着书入睡那一段,此刻‘我‘很平凡(只是一个收银员和一个兼职送牛奶),又不平凡(面对满屋的书,谁能否认我的灵魂?)


我认为最精彩的就是结尾幸福的坠落,有人觉得艺术应该是给以人希望满足,但圆满结局的艺术映照在现实总是个例。尽全力抓住也不到才是常态,怎么走下去?这就是真实的艺术给我这类历经了社会法则洗礼的人的共鸣。


前后又看了两遍,仍然感觉意犹未尽,沉溺在这个我赋予自己感受的梦呓中,始终无法回转。


结语


克制,传统,礼节,在这个文化内敛的国度上,组成了我‘做一个学生’的前半段旅程,接着坐在这个不断重组的人生列车上,经历起伏旋转,在书中我迸发了情绪。


面对纷至沓来地时代变幻,无论是框架迭代还是AI变革,努力是普通人的必需品。但另一方面,心中压抑着的情感,可能需要感受另一段情感发泄。


愿你我都能像电影里的主人公,坚韧的人生里开出一朵花。


小记



看起来普通的文字,可能很多人就像看一个故事。只有讲述者才晓得说出来有多么不易。因为信息的快速传播,人们失去了对文字的敬畏,以为那不过是一个个文字,正是这样才组成了鲜活的人生。想起欧阳修每读朋友的信,总要焚香净手。是时代塑造了人,还是人推进了时代?




在感觉无光的那一段旅程,我想拾起信心,但又重新被自己打破。我碰见了《被讨厌的勇气》,真的是欲罢不能。它并不能一瞬间改变人,在面对列车跌宕,仍然会俯下身去,但我认为我心中仍然如花。推荐时刻处于自我怀疑的人读一读。不能说技术人的圈子很小,但是一般的技术人执拗于自己的逻辑,期盼着向外界伸出触角,又因为担心被拒绝,最终被局限在自我的心灵空间里。建议这类处境的人可以通过这本书了解一下阿德勒心理学,我们不是负担着‘原罪’的人。


作者:用户6970670035699
来源:juejin.cn/post/7238443713873559607

收起阅读 »

Vue+Element-UI 中 el-table 动态合并单元格 :span-method 方法

web
合并单元格 记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊 el-table官方提供了合并单元格的方法与返回格式 如下: 根据叙述有了如下思路: 因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求...
继续阅读 »
合并单元格


记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊



el-table官方提供了合并单元格的方法与返回格式 如下:

在这里插入图片描述

根据叙述有了如下思路:

因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求所以我们通过遍历table的数据比较前后两个元素是否相等, 来构造一个spanArr用来存放rowspan, 最后通过rowspan的值来判断colspan的值😊.


案例如下, 这是我需要处理的一个表格:

需要根据数据动态的合并

在这里插入图片描述

对应的配置数组为

在这里插入图片描述


处理数据


因为获取的数据的非统一性, 我们首先要将数据根据我们想要合并的字段进行排序分组, 这里我实现了一个简单的方法来处理数据:


// data 为 表格数据 , params 为需要合并的字段
groupBy (data, params) {
const groups = {};
data.forEach(v => {
// 获取data中的传入的params属性对应的属性值
const group = JSON.stringify(v[params]);
// 把group作为groups的key,初始化value,循环时找到相同的v[params]时不变
groups[group] = groups[group] || [];
// 将对应找到的值作为value放入数组中
groups[group].push(v);
})
// 返回处理好的二维数组
return Object.values(groups);
},

此时打印一下我们的数据console.log(this.groupBy(this.tableListData.items, 'FirstIndex'))

在这里插入图片描述

如图, 我们已经将数据分好组并合并在一个数组中啦, FirstIndex相同的在一个数组


构造控制合并的数组spanArr


这里实现了一个方法, 用来构造一个spanArr数组赋予rowspan,即控制行合并



  • 接收重构数组 let arr = []

  • 设置索引 let pos = 0

  • 控制合并的数组 this.spanArr = []


先将groupby()处理好的数据再次用arr进行处理:连接所有数组成员为一个新数组

this.groupBy(this.tableListData.items, 'FirstIndex').map(v => (arr = arr.concat(v)))


现在处理好了数据,需要赋予原数据了:this.tableListData.items = arr


但是因为我是写在getSpanArr(data, params)方法中的,已经通过形参data将 this.tableListData.items传入了这里,如果想方便封装调用的话,不用每次使用都需要再次写入 this.tableListData.items = arr

于是想到一个办法,js数组的shift()和push()是直接修改数组所占内存的方法。

所以有:


arr.map(res => {
// 每次遍历都删除data && this.tableListData.items的第一个元素
data.shift()
// 每次遍历都将arr数组元素对应push进 data && this.tableListData.items
data.push(res)
})

还需要定义一个redata存放arr要合并字段的value

const redata = arr.map(v => v[params])


reduce处理spanArr数组 ⭐⭐


使用reduce方法比较redata前后两个元素是否相等,相等的话spanArr中对应索引的元素的值+1,并且在其后增加一个0占位(防止合并过后表格数据错位),否则的话增加一个1占位,并记录当前索引,往复循环,构造一个给 rowspan 取值判断合并的数组:


  const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
// old 上一个元素 cur 当前元素 i 索引
if (i === 0) {
// 第一次判断先增加一个 1 占位 ,索引为0
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})

看一下现在的数据spanArr, 这里传的参数为SecondIndex, 即表格的第二列

在这里插入图片描述

数组中大于0的数字就是我们数据中要合并的这组数据的数量, 同时也是这组数据需要合并的列数,而0就是代表这列不合并, 依次遍历,实现合并所选字段这一列的最终目的 如图理解:

在这里插入图片描述


返回最终结果


最后一步啦😊根据官方给的方法把我们处理好的spanArr传给rowspan即可


spanMethod({ row, column, rowIndex, columnIndex }) {
// 第一列
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}

效果如图!

在这里插入图片描述


完整代码


就很nice, !!最后把完整代码贴上:


// ......
mounted() {
this.getSpanArr(this.tableListData.items, 'FirstIndex');
},
methods: {
groupBy (data, params) {
const groups = {}
data.forEach(v => {
const group = JSON.stringify(v[params])
groups[group] = groups[group] || []
groups[group].push(v)
})
return Object.values(groups)
},
getSpanArr (data, params) {
let arr = []
let pos = 0
this.spanArr = []
this.groupBy(data, params).map(v => (arr = arr.concat(v)))
arr.map(res => {
data.shift()
data.push(res)
})
const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
if (i === 0) {
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})
},
spanMethod({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}
}

完美! 撒花!!!🎉🎉🎉


作者:小星星__
来源:juejin.cn/post/7238478149049483301
收起阅读 »

改写el-table表格排序, 支持多列排序远程排序!!!

web
改写el-table的默认排序 提示:在el-table封装的表格基础上改写排序方法 前言 我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下: 在列中设置sortable属性即可实现以该列为基...
继续阅读 »

改写el-table的默认排序


提示:在el-table封装的表格基础上改写排序方法




前言


我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下:



在列中设置sortable属性即可实现以该列为基准的排序,接受一个Boolean,默认为false。





一、el-table支持调接口排序吗?


el-table默认的排序支持从接口获取排序的数据



sortable: 对应列是否可以排序,如果设置为 custom,则代表用户希望远程排序,需要监听 Table 的 sort-change 事件



二、el-table支持多列排序吗?


默认的排序很简单, 加一个参数就可以了, 而且会自动根据数据进行排序, 但是我们会发现, 默认的排序只支持一列进行排序, 当我们排过一列之后在点击另一列的排序图标, 之前的排序就会消失😨.


三、如何实现多列远程排序?



  1. 自己写一个组件插入到表头的位置实现排序

  2. 根据el-table已有的属性以及抛出的方法实现多列排序


如果手动封装一个组件肯定能实现, 但是比较麻烦, 所以就研究了el-table相关了一些属性和方法, 思路如下:



header-cell-class-name: 表头单元格的 className 的回调方法,也可以使用字符串为所有表头单元格设置一个固定的className



在点击表头的时候排序的列以及是升降序保存到一个数组对象ordersList里, 然后通过header-cell-class-name属性设置选中的样式.


四、核心代码


	data: {
return {
ordersList: [],
}
}
// 点击表头
handleHeaderCLick(column){
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)

},
handleOrderChange (orderColumn, orderState) {
let result = this.ordersList.find(e => e.orderColumn === orderColumn)
if (result) {
result.orderState = orderState
} else {
this.ordersList.push({
orderColumn: orderColumn,
orderState: orderState,
})
}
// 调接口查询,在传参的时候把ordersList进行处理成后端想要的格式(这里是把数据抛出, 外部调用组件的地方处理)
this.sendInfo(this.ordersList, 'sort-change')
},
// 上面缺点是只能通过点击表头切换排序状态,点击小三角排序不会触发,处理sort-change事件和点击表头一样
sortChange({column}) {
// 有些列不需要排序,提前返回
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)
},
// 设置列的排序为我们自定义的排序
handleHeaderClass({ column }) {
column.order = column.multiOrder
}

这样外部拿到的就是一个所有排序的数组, 包括prop以及当前列的排序规则(ascending/descending/null), 将其处理成正确的入参格式即可.




在这里插入图片描述

在这里插入图片描述


如此, 就实现了多列远程排序, 欢迎大家一起讨论学习😊~


作者:小星星__
来源:juejin.cn/post/7238479015723089980
收起阅读 »

如何应对核心员工提离职?

最近一年互联网行情不好,很多大厂都在裁员,但裁员并不是不要人做事了。原来你这个岗位10个人做,企业有钱赚养得起,现在企业不怎么赚钱了,只能养4个人了。那么会有六个被裁掉。这时候对企业价值最大的4个人会被留下。也许因为人更少了,反而工资还会有所提升。 越是大公司...
继续阅读 »

最近一年互联网行情不好,很多大厂都在裁员,但裁员并不是不要人做事了。原来你这个岗位10个人做,企业有钱赚养得起,现在企业不怎么赚钱了,只能养4个人了。那么会有六个被裁掉。这时候对企业价值最大的4个人会被留下。也许因为人更少了,反而工资还会有所提升。


越是大公司,人员越冗余。开掉一批人对项目进度影响其实不大。但如果掌握核心技术的员工离职,可能项目真的就黄了。


我朋友老张最近就跟我抱怨他公司技术能力最强的哥们要离职。根据他的描述,这离职的哥们属于1个打10个那种。公司里有些问题只有他能解决,公司一直想要培养个接班人,但大多数都只学到了点皮毛。现在就问我该怎么办?所以,今天就和大家聊聊这个话题。


企业最怕的就是最优秀的那批员工离职。而且这部分人只要提了离职基本上就很难挽回了。


为什么会离职?


为什么环境这么差,还有人会主动离职?因为环境再差,总有一些企业还在招人,越是对能力要求高的岗位,越难招。所以,那些真正优秀的人才是不用担心工作问题的。


马云曾说员工提离职,就两个原因,钱给少了,或者心受委屈了。其实还有一类,是工作不能给自己带来成长了。 很多人对工作追求的是成长,是获得尊重、获得一些更高级的意义。你想要挽回对方,首先得弄清楚对方离职的原因。不过这种时候,大概率已经找我下家了。


PS:绝不建议大家裸辞,除非你是准备离职后休息两月。但就算要休息两月,也记得找人把社保交了,别断社保哈。


能不能留下来?


不管对方是否找好下家,作为公司管理者还是要去做努力争取对方留下,万一对方还没有跟下家确定好,只是有意向呢?所以在对方提出离职后,不要去做正式离职沟通。先找理由拒绝,然后约个时间私下里做一次沟通。可以找个地方,边吃饭边聊天。


在这个私下沟通的场景下要表示不希望对方离开,要是遇到了什么难处可以如实说。如果是薪资这块问题,差别太大你可能拿不定主意。但如果是因为什么工作太忙,家里事情很多这类问题。完全可以拍板让对方调整工作时间。


这里我讲一个案例,以前有个朋友跟我说,公司太卷了,最近感觉身体不行了。所以准备离职换个轻松点的工作环境?我说:“啥叫轻松点的工作环境”


他说:"每天能正常下班,不用经常加班熬夜。这样我就能有更多时间睡觉,还能抽出一部分时间出来健身啥的。"


我问:“那为什么不在现在公司里就调整下工作时间呢?”


他说:"公司这么忙,我要这么做,老板估计也会开了我的"。


我反问说:“你都要离职了,还怕他辞退啊”


就这样过了一年,对方也没离职,工作也没耽误。工作时间越长并不表示工作效率越高。我真的建议很忙的人能抽出一部分时间来冥想,每天10多分钟就行,让自己脑袋空一下。你会获得很多不一样的收获。


需要我做什么?


如果对方已经下定决定要走了,那么还可以问对方,现在自己能够做点什么。如果对方希望早点走完流程,那就帮忙让流程走快点。当然,流程走快了,后面接手人肯定会有问题还会请教你,这点可以直接说。


如果对方对未来也有迷茫,有犹豫。那么作为管理者,你肯定也有着丰富的见识,在自己能力范围内的话,帮助对方去分析利弊,提供建议参考。


员工离职,特别是核心员工离职,管理者可能会有点生气。毕竟会影响到自己的项目。但把格局放大,未来就没有再合作的机会?现在很少一个人会在公司呆一辈子。人来人走是平常。现在离开,未必不会再回来。虽然现在留不住人,但我可以留心。你以真诚待人,别人也会真诚待你。


我记得在《联盟》这本书里说过,很多大公司都有前员工联盟,公司里有专门人进行管理。好处很多。


首先前员工可以为企业带来声誉和良好的社会效应;


其次前员工可以给企业引进人才;


再次前员工还能给企业带来更多新的行业信息。


甚至公司一些新的产品都可以给到前员工试用,你找其他人还需要培训,前员工就不需要。


对于我们自己来说,我们是一起战斗过的战友。不管企业有没有正式组织,都应该常联系。


有什么办法能避免核心员工提离职?


1. 上工治未病,最好的方法就是不给对方提离职的机会。


离职过的人都知道,从想离职到提出离职,中间是有很长的时间的。而且在这个过程中,总会露出一些异常的行为。比如,开始抱怨公司的某件事情;在一些以前经常发表建议的场景下,变得不爱沟通,该怎样就怎样吧;工作没精神,不再主动推进某些工作等等。反正总会有点异常。作为管理者,如果你不能提前发生这些异常,那是失职了。你可以说自己很忙,但再怎么忙,都要抽出时间来关注这些核心员工。不仅仅是工作状态,还有家庭状态。你要是真关心员工,什么问题都好解决。


2. 把核心员工跟项目收益做强绑定,增加离职成本。


管理者不仅要会画饼,还要会分饼。如何分饼决定了饼的大小。既然都说对方是核心员工了,那么就应该让对方享受到同样的待遇。公司现在没这么多钱没关系,拿出部分期权、股权总可以吧。做成了,大家一起赚钱,失败了,是我们没做好,咱也认。


3. 核心员工要离职创业?行。我投资


核心员工如果愿意舍弃这么好的收益去创业,那么作为公司为什么就不能参与进去呢?既然挡不住,那我就不挡了。我大大方方的把投资方案公布出来。你想离职创业?可以,有好项目,我们公司愿意做你的天使投资人。如果项目真的好,那么公司赚了。如果项目不好,帮对方分析弊端,也许对方就

作者:石云升
来源:juejin.cn/post/7141021800624095246
不离职了。都是好事。

收起阅读 »

项目开发过程中,成员提离职,怎么办?

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。 项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。 ...
继续阅读 »

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。


项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。


通常情况下,一个员工向上级提出离职,那意味着他已经下决心走了,你留得住人,留不住心。而且这段时间,最好别派太多活,他只想早点交接完早点离开。


我们试着从环境、问题本身、问题主体三个方面来思考解决方案。


  • 环境从问题发生的环境看,如果我们有一个好的氛围,好的企业文化。员工会不会突然突出离职?或者哪怕提出离职,会不会给我们更多一点时间,在离职期间仍然把事情做好?如果答案是肯定的,那么管理者可以尝试从问题发生的上游解决问题。
  • 提前安排更多的资源来做项目,预防资源不足的情况发生。比如整体预留了20%的开发时间做缓冲,或者整体安排的工作量比规划的多20%。

问题本身


从问题本身思考,员工离职导致的问题是资源不够用。

  • 新增资源,能不能快速找到替代离职员工的人?或者我们能不能使用外包方式完成需求?跟团队商量增加一些工作时间或提高工作效率?
  • 减少需求,少做一些不是很重要的需求,把离职员工的需求分给其他人。


这2个解决方案其实都有一个前提,那就是离职人员的代码是遵循编码规范的,这样接手的人才看得懂。否则,需要增加的资源会比原来规划的多很多。这种问题不能靠员工自觉,而应该要有一套制度来规范编码。


问题的主体


我们不一定能解决问题,但可以解决让问题发生的人。这样问题就不存在了。比如,既然问题出现在张三面前,那就想办法搞定张三,让他愿意按计划把项目完成。如果公司里没人能搞定这个事,这里还有另一个思路,就是想想谁能解决这个问题,找那个能解决问题的人。


从环境、问题本身、问题的主体三个维度来分析,我们得到了好几个解决方案。我们接着分析哪种方案更靠谱。


解决方案分析


方案一,从环境角度分析,让问题不发生。这种成本是最小的。但如果问题已经发生,那这个方案就没用了。


方案二,在项目规划的时候,提前安排更多资源。这招好是好,但前提是你公司有那么多资源。大部分公司都是资源不足。


方案三,新增资源,这个招人不会那么快,就算招进来了,一时半会还发挥不出多大的价值。请外包的话,其实跟招人一样,一时半会还发挥不出多大的价值,成本还更高,也不适合。至于跟团队成员商量提高工作效率或者大家加个班赶上进度,这也是一个解决方案。不过前提是团队还有精力承担这些工作。


方案四,减少需求。这个成本最小,对大部分公司其实也适用。关键是需求管理要做好,对需求的优先级有共识。


方案五,解决让问题发生的人。这个如果不是有大的积怨,也是一个比较好的方案。对整个项目来说,成本也不会很大,项目时间和质量都有保证。


项目管理里有一个生命周期概念,越是在早期发生问题,成本越小。越到后期成本越大。所以,如果让我选,我会选择方案一。但如果已经发生,那只能在四和五里选一个。


实战经验


离职是一场危机管理


让问题不发生,那么解决之道就是不让员工离职。尤其是不让核心骨干员工提离职。离职就是一场危机管理。


这里的本质的是人才是资产,我们在市场上看到很多案例,很多企业的倒闭并不是因为经营问题,而是管理层的大批量流失,资本市场也不看好管理层流失的企业。了解这点,你就能理解为什么人才是资产了。所以对企业来说,核心员工离职不亚于一场危机。


下面分享一个危机管理矩阵,这样有助于我们对危机进行分类。


横轴是一件事情发生之后,危害性有多大,我们分为大、中、小。纵轴就是这件事发生的概率,也可以分为大、中、小。然后就形成了九种不同的类型。



我自己的理解是,有精力的话,上图红色区域是需要重点关注的。如果精力有限,就关注最右边那三种离职后,危害性特别大的员工(不管概率发生的大小)。要知道给企业造成大影响的往往是那些发生概率小的,因为概率大的,你肯定有预防动作,而那些你认为不会离职的员工,突然一天找到你提离职,你连什么准备都没,这种伤害是最大的。


理论上所有岗位都应该准备好”接班人“计划,但实际上很多公司没办法做到。在一些小公司是一个萝卜一个坑,这个岗位人员离职,还得现招。这不合理,但这就是现状。


公司如何管理危机?


好,回到公司身上,公司如何管理危机?


第一,稳住关键性员工,让员工利益和公司利益进行深入绑定。


那些创造利润最大的前10~20%的员工,就应该获得50%甚至更高的收益。当然除了金钱上的激励外,还要有精神上的激励,给他目标,让他有成就感等等。


第二,有意识地培养关键岗位的接班人或者助理。


比如通过激励鼓励他们带新人、轮岗等等


第三,人员的危机管理是动态变化的,要时不时地明确团队各成员的位置。


比如大公司每年都会做人才盘点。


第四,当危机真的出现后,要有应对方案。


也就是把危机控制在可承受的范围内。比如,项目管理中的planB方案,真遇到资源不够,时间不够的情况下,我们能不能放弃一些不重要的需求?亦或者能不能先用相对简单但可用的方案?


离职管理的核心是:降低离职发生的概率和降低离职造成危害的大小。


离职沟通


如果事情已经发生了,管理者应该先通过离职沟通,释放自己的善意。我会按照如下情况跟离职员工沟通


第一,先做离职沟通,了解对方为什么离职?还有没有留下来的可能,作为管理者有什么能帮他做的?


第二,确定走的话,确认下对方期望的离职时间,然后根据公司情况,协商一个双方都能接受的离职时间点。不要因为没有交接人,就不给明确时间。


第三,征求对方意见,是否需要公布离职。然后一起商量这段时间的工作安排。比如,你会坦诚告知会减少工作量,但哪些工作是需要他继续支持的。希望他能一如既往地高效完成工作。


第四,如果还没有交接人到岗,最好在一周内安排人员到岗,可以考虑内部换岗,内招、猎聘等手段尽快让人员到岗。


第五,如果已经到离职时间,但还没有交接人,作为公司管理者,你就是最好的交接人。在正式交接工作之前,要理清楚需要哪些相关的资料,做好文档分类。如果实在对离职员工的工作不了解,可以让离职人员写一封日常工作的总结。


如果做完这些,离职员工还是消极怠工。作为管理者能做得就比较有限,可以尝试以下几个方法


1、再进行一次沟通。表明现在公司的情况,希望他给予支持。


2、看看自己能给予对方哪些帮助,先把这些落实好。比如写推荐信。另外有些公司入职的时候会做背景调查,这也是你能够帮助到他的。


3、如果你有权利,可以跟离职员工商量是否可以以兼职的方式来完成后续工作。这种方式对大家都好,他可以早点离职,你也不用担心因为时间仓促招错人。


如果做完以上这些还不行,那么就考虑减少一些需求,用更简单的方案先用着,后期做迭代。至于说让团队加班加点赶进度,这个要根据项目实际情况来定。


总结:今天给大家分享了一个简单分析问题的方法。然后重点聊了一下项目成员突然要离职,项目负责人有哪些应对方案。如果你看完有收获,欢迎留言讨论。


作者:石云升
来源:juejin.cn/post/7147319129542770702
收起阅读 »

我裸辞了

前言 时间过得真快,不知不觉已经23岁了。今年是我工作的第4年,入职现在的公司也2年了。完成了2年前定下的目标,成功弥补了自己项目上的短板以及技术深度的不足。 经过一番深思熟虑后,我决定裸辞,向下一个目标出发。 为什么离职 有位企业家曾说过,员工离职就两个原因...
继续阅读 »

前言


时间过得真快,不知不觉已经23岁了。今年是我工作的第4年,入职现在的公司也2年了。完成了2年前定下的目标,成功弥补了自己项目上的短板以及技术深度的不足。


经过一番深思熟虑后,我决定裸辞,向下一个目标出发。


为什么离职


有位企业家曾说过,员工离职就两个原因:



  • 工资没给够

  • 心受委屈了


其实,去年7月份的时候,我就萌生离职想法了,那个时候公司做了制度改革,将入职时谈的薪资进行了拆分,拆了20%出来做绩效。



  • 升职加薪按照每月的绩效考核,取出平均分,超过85分才有机会

  • 请假回来后,需要通过加班把你请假所耗费的工时补回来,否则扣绩效分

  • 上班忘记打卡直接扣全勤奖300块(全勤奖是算在入职时所谈的薪资里)

  • 下班忘记关显示器扣绩效分

  • 工位上吃东西扣绩效分


绩效制度推出后,一改再改,条件越来越苛刻。


想拿到B绩效你必须卷起来,超时超额完成任务(加班加点完成手头工作),请假(事假、病假)回来后,需要自己通过加班来把工时补回来。



制度刚推出来的时候,我就打开了boss直聘开始看机会,刷了2周,联系了很多家。最终就1家收了我简历,最后约了面试,其他的要么是送达,要么是已读未回。



到了面试当天,是前端主管面的我,问的更多的是项目相关的问题,问了一些js相关的问题以及基础的数据结构和算法,我都回答的不错。前前后后聊了1个多小时,面完后hr就过来跟我聊了下,他问了我三个问题:



  • 刚才的面试感觉怎么样?

  • 你觉得我们的产品怎么样?

  • 你还有什么想了解的吗?


这些问题回答后,他就说:那行,今天的面试就到这,后面合适的话微信通知你进行复试。他把我送出公司后,正好看到了那个面我的前端主管在等电梯,我跟他打了招呼,在电梯上简单聊了下,他问我现在还在不在职、现在的公司在哪里、住在哪里。电梯下到1楼后,他去买咖啡了,我跟他道了别。


回去后,过了两天也没有复试的消息,我就主动发微信问了hr,他给我的结果是:能力尚未达到高级🤡



还是怂了


那场面试得到结果后,我就在自己的群里跟群友聊了下这件事,他们说,这或许只是一个委婉拒绝你的理由,市场上人太多了,可能有人要价比你低。很多群友都说很难,互联网寒冬,他们也在boss直聘上联系了很多,也都是已读未回和送达,面试机会寥寥无几。


看到这么多群友说难,我心里也打起了退堂鼓,要不就再等等吧,我本本分分做事,做好自己的工作,只要能拿到自己正常的薪资就行,先不跳了,等市场好些了再跳吧。



冰冻三尺,非一日之寒


时间来到今年3月份,领导给我发放当月考核表的时候,本来能拿到B绩效(组里来了新人,是我在带,给我加了绩效分),但是,他又从其他地方扣掉了这些分。


这波操作触及到我的底线了,不能再忍让下去了。我还是走吧,想到公司制度规定了每年4月份会有一次薪资涨幅,如果我现在走的话,有点不划算,那就等5月15日拿到涨幅后的薪资再提离职吧。临走前把薪资base提升一点也挺好的。



很多群友也在劝我三思,今年的市场行情比去年还差,还是建议骑驴找马,找到了再辞职。




不过,我觉得行情差就差吧,我的条件本来就很差了(对我过往不了解的读者可以阅读我的另一篇文章:一枚19岁程序员的自学之路),再差又能差到哪里去呢,我始终坚信自己的努力总有一天会得到回报的,天无绝人之路。



提出离职


时间来到2023年5月15号,薪资没有得到增长。我的期望没有如期而至,不过无所谓了,裸辞吧。



跟领导在大会议室聊了5分钟左右吧,开场白说完后,他说:我猜到了,其实去年制度改革后,我就知道公司留不住你了。说说理由吧。


我:主要有两点吧,制度和薪资,绩效薪资这个制度本身没有问题,但是我觉得它应该是在我入职时所谈的薪资之外。


领导:行,了解了,你更希望薪资是固定的对吧。那你期望薪资能涨到多少?


我:说了我的期望


领导:我们公司的制度你也知道,薪资的涨幅很小的。那你打算什么时候走?


我:按照制度来吧,一个月后走。


领导:行,好聚好散嘛。


再然后就是说了一些我手头上还有哪些工作,虽然提了离职,但是工作上可不能懈怠摆烂之类的话,讲完后,就结束了这场谈话。


寻求内推


感谢各位读者阅读本文,如果你们公司有前端开发岗位在招聘的话,可以内推我下我🤗。


我的联系方式:



写在最后


至此,文章就分享完毕了。


我是神奇的程序员,一位前端开发工程师。


如果你对我感兴趣,请移步我的个人网站,进一步了解。



  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊

  • 本文首发于神奇的程序员公众号,未经许可禁止转载💌


作者:神奇的程序员
来源:juejin.cn/post/7233407035772616763
收起阅读 »

快速使用MQTT Flutter版 SDK实现消息收发

1. 前提条件1.1 部署Flutter开发环境配置好Flutter开发环境。1.2 导入项目依赖在yaml文件里配置mqtt_client: ^9.8.1 在iOS开发中需要增加一下代码到位于ios/Runner/Info.plist中的Info.plist...
继续阅读 »

1. 前提条件

1.1 部署Flutter开发环境

配置好Flutter开发环境。

1.2 导入项目依赖

在yaml文件里配置

mqtt_client: ^9.8.1

在iOS开发中需要增加一下代码到位于ios/Runner/Info.plist中的Info.plist*文件中:


<key>NSLocalNetworkUsageDescription</key>
<string>Looking for local tcp Bonjour service</string>
<key>NSBonjourServices</key>
<array>
<string>mqtt.tcp</string>
</array>

Android 

Android AndroidManifest.xml 增加如下代码


<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

2. 实现流程

2.1 获取初始化信息

登录console后台
1.点击菜单栏【MQTT】→【服务概览】→【服务配置】,获取「连接地址」、「连接端口」、「AppID」以「及REST API地址」等信息。
注:clientID由两部分组成,组织形式为“deviceID@AppID”,deviceID由用户自定义,AppID见【服务配置】。
示例:正确的clientID格式为:“device001@aitbj0”;
2.点击菜单栏【应用概览】→【应用详情】→【开发者ID】,获取「Client ID」与「ClientSecret」信息。
3.初始化代码


static const String restapi= "https://api.cn1.mqtt.chat/app/$appID/"; //环信MQTT REST API地址 通过console后台[MQTT]->[服务概览]->[服务配置]下[REST API地址]获取

static const String endpoint= "**"; //环信MQTT服务器地址 通过console后台[MQTT]->[服务概览]->[服务配置]下[连接地址]获取
static const int port = **; // 协议服务端口 通过console后台[MQTT]->[服务概览]->[服务配置]下[连接端口]获取
static const String appID= "**"; // appID 通过console后台[MQTT]->[服务概览]->[服务配置]下[AppID]获取
static late String deviceId ;// 自定义deviceID
static late String clientID ;// deviceId + '@' + appID
static const String appClientId= "**"; //开发者ID 通过console后台[应用概览]->[应用详情]->[开发者ID]下[ Client ID]获取
static const String appClientSecret= "**"; // 开发者密钥 通过console后台[应用概览]->[应用详情]->[开发者ID]下[ ClientSecret]获取

static void init() async{
deviceId = "deviceId";
clientID = "$deviceId@$appID";
}

 

2.2 获取token

  • 首先获取App Token
Dio dio = Dio();
Response<Map<String,dynamic>> data = await dio.post("${restapi}openapi/rm/app/token",data: {"appClientId": appClientId, "appClientSecret": appClientSecret});
var token = (data.data!["body"] as Map<String, dynamic> )["access_token"];
  • 然后根据App Token获取User Token,User Token作为连接服务的密码
Response<Map<String,dynamic>> data2 = await dio.post("${restapi}openapi/rm/user/token",data: {"username": "username", "cid": clientID},options: Options(headers:  <String, dynamic>{r'Authorization': token}));
var mqtttoken = (data2.data!["body"] as Map<String, dynamic> )["access_token"];

2.3 连接服务器

创建MqttAndroidClient对象,并配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

var client = MqttServerClient.withPort(endpoint, clientID, port);
/// 是否打印mqtt日志信息
client.logging(on: true);
/// 设置协议版本,默认是3.1,根据服务器需要的版本来设置
/// _client.setProtocolV31();
client.setProtocolV311();
/// 保持连接ping-pong周期。默认不设置时关闭。
client.keepAlivePeriod = 60;
/// 设置自动重连
client.doAutoReconnect();
/// 设置超时时间,单位:毫秒
client.connectTimeoutPeriod = 60000;
/// 连接成功回调
client.onConnected = _onConnected;
/// 连接断开回调
client.onDisconnected = _onDisconnected;
/// 取消订阅回调
client.onUnsubscribed = _onUnsubscribed;
/// 订阅成功回调
client.onSubscribed = _onSubscribed;
/// 订阅失败回调
client.onSubscribeFail = _onSubscribeFail;
/// ping pong响应回调
client.pongCallback = _pong;
client.connect(username,mqtt_token);



static void _onConnected() {
LogManager.log.d("连接成功....");
_initTopic();
}

static void _onDisconnected() {
LogManager.log.d("连接断开");
}

static void _onUnsubscribed(String? topic) {
LogManager.log.d("取消订阅 $topic");
}

static void _onSubscribed(String topic) {
LogManager.log.d("订阅 $topic 成功");
}

static void _onSubscribeFail(String topic) {
LogManager.log.e("订阅主题: $topic 失败");
}

static void _pong() {
LogManager.log.d("Ping的响应");
}


2.4 订阅(subscribe)

2.4.1 订阅主题
当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。在连接成功后调用

client.subscribe(topic, MqttQos.atLeastOnce);

2.4.2 取消订阅

_client?.unsubscribe(topic)

2.5 收发消息

2.5.1 发送消息
配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

var builder = MqttClientPayloadBuilder();
builder.addUTF8String("This is a message");
client.publishMessage("topic", MqttQos.atLeastOnce, builder.payload!);

2.5.2 接收消息
配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

_client?.updates?.listen((event) {
var recvMessage = event[0].payload as MqttPublishMessage;

LogManager.log.d("原始数据-----:${recvMessage.payload.message}");
/// 转换成字符串
LogManager.log.d(
"接收到了主题${event[0].topic}的消息: ${const Utf8Decoder().convert(recvMessage.payload.message)}");
});
收起阅读 »

咱不吃亏,也不能过度自卫

这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。 我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。 小刘一听,感觉自己有被指控的风险。 他立刻严厉起来:“每天都来公司,不一定就算全勤。没打...
继续阅读 »

这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。


我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。


小刘一听,感觉自己有被指控的风险。


他立刻严厉起来:“每天都来公司,不一定就算全勤。没打卡我是不统计的”。


最后小刘一查,发现是自己统计错了。


小刘反而更加强势了:“这种事情,你应该早点跟我反馈,而且多催着我确认。你自己的事情都不上心,扣个钱啥的只能自己兜着”


这就是明显的不愿意吃亏,即使自己错了,也不愿意让自己置于弱势。


你的反应,决定别人怎么对你。这种连言语的亏都不吃的人,并不会让别人敬畏,反而会让人厌恶,进而影响沟通


我还有一个同事老王。他是一个职场老人,性格嘻嘻哈哈,业务能力也很强。


以前同事小赵和老王合作的时候,小赵宁愿经两层人传话给老王,也不愿意和他直接沟通。


我当时感觉小赵不善于沟通。


后来,当我和老王合作的时候,才体会到小赵的痛苦。


因为,老王是一个什么亏都不吃的人,谁来找他理论,他就怼谁。


你告诉他有疏漏,他会极力掩盖问题,并且怒怼你愚昧无知。


就算你告诉他,说他家着火了。他首先说没有。你一指那不是烧着的吗?他回复,你懂个屁,你知道我几套房吗?我说的是我另一个家没着火。


有不少人,从不吃亏,无论什么情况,都不会让自己处于弱势。


这类人喜欢大呼小叫,你不小心踩他脚了,他会大喊:践踏我的尊严,和你拼了!


心理学讲,愤怒源于恐惧,因为他想逃避当前不利的局面


人总会遇到各种不公的待遇,或误会,或委屈。


遇到争议时,最好需要确认一下,排除自己的问题。


如果自己没错,那么比较好的做法就是:“我认为你说得不合理,首先……其次……最后……”。


不盲目服软,也不得理不饶人,全程平心静气,有理有据。这种人绝对人格魅力爆棚,让人敬佩。


最后,有时候过度强硬也是一种策略,可以很好地过滤和震慑一些不重要的事物。


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

flutter有哪些架构的框架?该怎么选择

flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架: BLoC (Business Logic Co...
继续阅读 »

flutter有哪些架构的框架?


Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:



  1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。
    Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。

  2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。

  3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。
    以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


除了上面提到的框架之外,还有以下几个Flutter架构框架:



  1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。

  2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。

  3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。

  4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。


总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


Flutter BLoC


Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


如何选择使用


选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


GetX和BLoC的优缺点


GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


GetX


优点:


简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


缺点:


社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


BLoC


优点:


灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


缺点:


学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

一位程序员,做了一个浏览器插件,赚了 4 万美元

序 今天分享的是一个能为开发者带来 4 万刀收入的浏览器插件 ReaderMode;这又是一篇旧文, 4 万刀已经是前年(2021 年)的事了。 这个插件花了 8 个月时间累计营收 1 万刀,但仅仅一年多后,累计收入就翻了 4 倍。听起来好像不太真实,但看完你...
继续阅读 »


今天分享的是一个能为开发者带来 4 万刀收入的浏览器插件 ReaderMode;这又是一篇旧文, 4 万刀已经是前年(2021 年)的事了。


这个插件花了 8 个月时间累计营收 1 万刀,但仅仅一年多后,累计收入就翻了 4 倍。听起来好像不太真实,但看完你可能会对浏览器插件市场有不一样的看法。


想法 → 第一版上线


最早,一位马来西亚的程序员 Ryzal 在筹划着做个小产品来赚点生活费;基于他日常的观察认为浏览器插件是一个可以比较低成本的快速验证想法的方式,所以就决定在插件领域下工夫;在看完一圈插件,然后结合自己平时浏览网页时对各种广告以及乱七八糟排版的不爽,于是就想一个可以让用户无干扰、沉浸式阅读文章的插件应该会很受欢迎。


本来就是想着可以快速验证想法,他马上就开始了;只花了一个周末,就出了 MVP 版 - 最小可行产品。


把 MVP 发给了几个朋友和群组里,打算着是收集一些用户的吐槽和建议。结果竟然有几个人直接付费购买了 pro 版。



浏览器插件也有人愿意付费?!



国内可能比较少见,但在国外需要付费使用的插件还是挺常见的。很大程度也是海外用户付费意愿比较强的缘故,更多的内容在上一篇《为什么开发者应该多关注海外市场》有展开详谈。


商业模式


ReaderMode 采用的商业模式是 freemium,大白话说就是基础功能免费使用,高级功能付费使用。Freemium 是国外 to C 的产品比较主流的商业模式了,比如 Notion, Slack, Dropbox 等。


这种模式的好处是,用户没有心理门槛,有点好奇心的小伙伴都可能会试试(反正不花钱)。这样基础用户量就有了,但凡你的产品是有亮点的(即使是免费的功能),就会有一批忠实用户;这些忠实的用户会帮你转发,推荐给更多的用户,这就是口碑传播 word of mouth (WoM)。WoM 传播未必是最快的,但是一定是转化效率最高的传播之一。


用户基数大了,自然就会“漏斗”出付费用户了。


可以说 freemium 模式下的免费用户是营销推广的利器;其实从财务角度来看,你可以把免费版的投入(时间、精力、钱等)看作是市场推广费用。


从这个角度来思考免费档的功能以及其设计能创造出更高的 ROI。


Soft launch


回到时间线上,Ryzal 根据 beta 用户的反馈做了一些调整,花了一个月时间才把产品打磨到可以对外发布的程度。


在正式发布之前,他还做了一次叫 soft launch,就是在自己个人的渠道小范围地做一次宣发。这也是国外独立开发者比较常用的发布策略。其目的有多层:



  • 二次验证需求

  • 获得更多的关注者

  • 压力测试

  • “大家来找 bug“

  • 为正式发布暖场


这是 Ryzal 当时 soft launch 发的推;留意他还提供了个 8 折优惠码吸引早期试用用户。


image.png


这次 soft launch 算不错,给 ReaderMode 带了 100 多位用户。顺势他在这条推下面做了个小调研:


image.png


很明显这是为了在 ProductHunt 上面做正式发布做准备了;前面提到的为正式发布暖场的意图也在这里显现出来了。


为什么在 PH 的正式发布那么重要?还需要暖场?


ProductHunt launch


首先,PH 是什么?



PH 是一个新产品发现平台。提交新产品的方式可以是创造者自己提交,或者是用户自己发现了好用好玩的产品提交到平台。



由于平台每天都会收到的数十条新品提交,为了鼓励优质的产品,他有一个排名机制;这个排名的依据是当天新产品的 upvote(可以简单理解为点赞)数量。


PH 的流量非常大,如果能在当天的发布排名中占到前 5,带来的曝光是非常可观的;更不用说如果进入前 3,还会有专门的徽章。


image.png


之前分享的独立设计师独立开发者案例,都是在 PH 获取早期用户。对于初创产品,把发布节奏和细节把控好,产品冷启动的问题可能一下子就解决了,随之还带来一大拨免费 PR 流量。


其实,不仅是独立产品、创业公司,就连大公司也会把新产品提交到平台,可见 PH 在行业内的影响力之大。


从 0 到 2000 美元


Ryzal 把发布的内容都准备好后,在 PH 上正式发布并顺利地拿下来了当天的最高赞。


image.png


ReaderMode 的 PH 链接


更加幸运的是,不仅 PH 的创始人 Ryan(推特大 V)为其推广,连 LifeHacker 这种大媒体都留意到了这个产品并对其作出了报导。


短短发布后的 24 小时内,这个产品就收了 2000 美元;而这离写下第一行代码应该只有不到 2 个月的时间。


再次证明了,产品力本身就是最好的营销技巧。


增长到 40,000 美元


整体下来从发布到 5000 用户,两周时间,没花一分钱。有了一个成功的 PH 发布,带来近 10 家媒体(可能实际更多)的曝光,这个产品已经有不少 credibility 了;3 个月后,1 万用户达成。


image.png


截止至 2021 年 5 月左右的收入,总计 4 万多刀,分两部分组成。



  1. 一部分产品的直接收入:


image.png



  1. 另一部分是通过一些 deal sites(比如 AppSumo)的收入:


image.png


接下来就躺赚了?


不存在的,这是许多开发者对做独立产品的一个误解。


事实上,Ryzal 在 PH 发布后的第二天就开始了新一轮的迭代。今天,ReaderMode 已不再是一个简单的插件了,而是一个



All-in-one reading, bookmarking, highlighting and research app.



所以,开发者们码起来、迭代起来!


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

当我再次用Kotlin完成五年前已经通过Kotlin完成的项目后

关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合. 如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从...
继续阅读 »

关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合.


如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从0到1, F点的位置连成线, 就是ABC这三个点的贝塞尔曲线.


Bezier


两次完成的感受


虽然时隔五年, 但是对这个项目的印象还是比较深刻的(毕竟当时找啥资料都不好找).


当时的项目还用的是Kotlin Synthetic来进行数据绑定(虽然现在已经被弃用了), 对于当时还一直用findViewById和@BindView的我来说, 这是对我最大的惊喜. 是的, 当时用Kotlin最大惊喜就是这个. 其它的感觉就是这个"语法糖"看起来还挺好用的. 而现在, 我可以通过Compose来完成页面的布局. 最直观的结果是代码量的减少, 初版功能代码(带xml)大概有800行, 而这次完成整个功能大概只需要450行.


在使用过程中对"Compose is function"理念的理解更深了一步, 数据就是数据. 将数据作为一个参数放到Compose这个function中, 在数据变化的时候重新调用function, 达到更新UI的效果. 显而易见的事情是我们不需要的额外的持有UI的对象了, 我们不必考虑UI中某个元素和另一个元素直接的关联, 不必考虑某个元素响应什么样的操作. 我们只需要考虑某个Compose(function) 在什么样的情况下(入参)需要表现成什么样子.


比如Change Point按钮点下时, 会更改mInChange的内容, 从而影响许多其它元素的效果, 如果通过View来实现, 我需要监听Change Point的点击事件, 然后依次修改影响到的元素(这个过程中需要持有大量其它View的对象). 不过当使用Compose后, 虽然我们仍要监听Change Point的点击事件, 但是对对应Change Point的监听动作来说, 它只需要修改mInChange的内容就行了, 修改这个值会发生什么变化它不需要处理也不要知道. 真正需要变化的Compose来处理就可以了(可以理解为参数变化了, 重新调用了这个function)


特性的部分使用的并不多, 比较项目还是比较小, 很多特性并没有体现出来.


最令我感到开心的是, 再一次完成同样的功能所花费的时间仅仅只有半天多, 而5年前完成类似的功能大概用了一个多星期的时间. 也不知道我和Kotlin这5年来哪一方变化的更大😆.


贝塞尔曲线工具


先来看一下具有的功能, 主要的功能就是绘制贝塞尔曲线(可绘制任意阶数), 显示计算过程(辅助线的绘制), 关键点的调整, 以及新增的绘制进度手动调整. 为了更本质的显示绘制的结果, 此次并没有对最终结果点进行显示优化, 所以在短时间变化位置大的情况下, 可能出现不连续的现象.


3_point_bezier


more_point_bezier


bizier_change


bezier_progress


代码的比较


既然是同样的功能, 不同的代码, 即使是由不同时期所完成的, 将其相互比较一下还是有一定意义的. 当然比较的内容都尽量提供相同实现的部分.


屏幕触摸事件监测层


主要在于对屏幕的触碰事件的监测


初版代码:

override fun onTouchEvent(event: MotionEvent): Boolean {


touchX = event.x
touchY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
toFindChageCounts = true
findPointChangeIndex = -1
//增加点前点击的点到屏幕中
if (controlIndex < maxPoint || isMore == true) {
addPoints(BezierCurveView.Point(touchX, touchY))
}
invalidate()
}
MotionEvent.ACTION_MOVE ->{
checkLevel++
//判断当前是否需要检测更换点坐标
if (inChangePoint){
//判断当前是否长按 用于开始查找附件的点
if (touchX == lastPoint.x && touchY == lastPoint.y){
changePoint = true
lastPoint.x = -1F
lastPoint.y = -1F
}else{
lastPoint.x = touchX
lastPoint.y = touchY
}
//开始查找附近的点
if (changePoint){
if (toFindChageCounts){
findPointChangeIndex = findNearlyPoint(touchX , touchY)
}
}

//判断是否存在附近的点
if (findPointChangeIndex == -1){
if (checkLevel > 1){
changePoint = false
}

}else{
//更新附近的点的坐标 并重新绘制页面内容
points[findPointChangeIndex].x = touchX
points[findPointChangeIndex].y = touchY
toFindChageCounts = false
invalidate()
}
}

}
MotionEvent.ACTION_UP ->{
checkLevel = -1
changePoint = false
toFindChageCounts = false
}

}
return true
}

二次代码:

 Canvas(
...
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
model.pointDragStart(it)
},
onDragEnd = {
model.pointDragEnd()
}
) { _, dragAmount ->
model.pointDragProgress(dragAmount)
}
}
.pointerInput(Unit) {
detectTapGestures {
model.addPoint(it.x, it.y)
}
}
)
...

/**
* change point position start, check if have point in range
*/
fun pointDragStart(position: Offset) {
if (!mInChange.value) {
return
}
if (mBezierPoints.isEmpty()) {
return
}
mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}
}

/**
* change point position end
*/
fun pointDragEnd() {
bezierPoint = null
}

/**
* change point position progress
*/
fun pointDragProgress(drag: Offset) {
if (!mInChange.value || bezierPoint == null) {
return
} else {
bezierPoint!!.x.value += drag.x
bezierPoint!!.y.value += drag.y
calculate()
}
}

可以看到由于Compose提供了Tap和Drag的详细事件, 从而导致新的代码少许多的标记位变量.


而我之前一度认为是语法糖的特性来给我带来了不小的惊喜.


譬如这里查找点击位置最近的有效的点的方法,


初版代码:

//判断当前触碰的点附近是否有绘制过的点
private fun findNearlyPoint(touchX: Float, touchY: Float): Int {
Log.d("bsr" , "touchX: ${touchX} , touchY: ${touchY}")
var index = -1
var tempLength = 100000F
for (i in 0..points.size - 1){
val lengthX = Math.abs(touchX - points[i].x)
val lengthY = Math.abs(touchY - points[i].y)
val length = Math.sqrt((lengthX * lengthX + lengthY * lengthY).toDouble()).toFloat()
if (length < tempLength){
tempLength = length

if (tempLength < minLength){
toFindChageCounts = false
index = i
}
}
}

return index
}


而二次代码:

        mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}

和Java的Stream类似, 链式结构看起来更加的易于理解.


贝塞尔曲线绘制层


主要的贝塞尔曲线是通过递归实现的


初版代码:

//通过递归方法绘制贝塞尔曲线
private fun drawBezier(canvas: Canvas, per: Float, points: MutableList<Point>) {

val inBase: Boolean

//判断当前层级是否需要绘制线段
if (level == 0 || drawControl){
inBase = true
}else{
inBase = false
}


//根据当前层级和是否为无限制模式选择线段及文字的颜色
if (isMore){
linePaint.color = 0x3F000000
textPaint.color = 0x3F000000
}else {
linePaint.color = colorSequence[level].toInt()
textPaint.color = colorSequence[level].toInt()
}

//移动到开始的位置
path.moveTo(points[0].x , points[0].y)

//如果当前只有一个点
//根据贝塞尔曲线定义可以得知此点在贝塞尔曲线上
//将此点添加到贝塞尔曲线点集中(页面重新绘制后之前绘制的数据会丢失 需要重新回去前段的曲线路径)
//将当前点绘制到页面中
if (points.size == 1){
bezierPoints.add(Point(points[0].x , points[0].y))
drawBezierPoint(bezierPoints , canvas)
val paint = Paint()
paint.strokeWidth = 10F
paint.style = Paint.Style.FILL
canvas.drawPoint(points[0].x , points[0].y , paint)
return
}


val nextPoints: MutableList<Point> = ArrayList()

//更新路径信息
//计算下一级控制点的坐标
for (index in 1..points.size - 1){
path.lineTo(points[index].x , points[index].y)

val nextPointX = points[index - 1].x -(points[index - 1].x - points[index].x) * per
val nextPointY = points[index - 1].y -(points[index - 1].y - points[index].y) * per

nextPoints.add(Point(nextPointX , nextPointY))
}

//绘制控制点的文本信息
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
if (isMore && level != 0){
canvas.drawText("0:0", points[0].x, points[0].y, textPaint)
}else {
canvas.drawText("${charSequence[level]}0", points[0].x, points[0].y, textPaint)
}
for (index in 1..points.size - 1){
if (isMore && level != 0){
canvas.drawText( "${index}:${index}" ,points[index].x , points[index].y , textPaint)
}else {
canvas.drawText( "${charSequence[level]}${index}" ,points[index].x , points[index].y , textPaint)
}
}
}
}

//绘制当前层级
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
canvas.drawPath(path, linePaint)
}
}
path.reset()

//更新层级信息
level++

//绘制下一层
drawBezier(canvas, per, nextPoints)

}



二次代码:

{
lateinit var preBezierPoint: BezierPoint
val paint = Paint()
paint.textSize = mTextSize.toPx()

for (pointList in model.mBezierDrawPoints) {
if (pointList == model.mBezierDrawPoints.first() ||
(model.mInAuxiliary.value && !model.mInChange.value)
) {
for (point in pointList) {
if (point != pointList.first()) {
drawLine(
color = Color(point.color),
start = Offset(point.x.value, point.y.value),
end = Offset(preBezierPoint.x.value, preBezierPoint.y.value),
strokeWidth = mLineWidth.value
)
}
preBezierPoint = point

drawCircle(
color = Color(point.color),
radius = mPointRadius.value,
center = Offset(point.x.value, point.y.value)
)
paint.color = Color(point.color).toArgb()
drawIntoCanvas {
it.nativeCanvas.drawText(
point.name,
point.x.value - mPointRadius.value,
point.y.value - mPointRadius.value * 1.5f,
paint
)
}
}
}
}

...
}


/**
* calculate Bezier line points
*/
private fun calculateBezierPoint(deep: Int, parentList: List<BezierPoint>) {
if (parentList.size > 1) {
val childList = mutableListOf<BezierPoint>()
for (i in 0 until parentList.size - 1) {
val point1 = parentList[i]
val point2 = parentList[i + 1]
val x = point1.x.value + (point2.x.value - point1.x.value) * mProgress.value
val y = point1.y.value + (point2.y.value - point1.y.value) * mProgress.value
if (parentList.size == 2) {
mBezierLinePoints[mProgress.value] = Pair(x, y)
return
} else {
val point = BezierPoint(
mutableStateOf(x),
mutableStateOf(y),
deep + 1,
"${mCharSequence.getOrElse(deep + 1){"Z"}}$i",
mColorSequence.getOrElse(deep + 1) { 0xff000000 }
)
childList.add(point)
}
}
mBezierDrawPoints.add(childList)
calculateBezierPoint(deep + 1, childList)
} else {
return
}
}

初版开发的时候受个人能力限制, 递归方法中既包含了绘制的功能也包含了计算下一层的功能. 而二次编码的时候受Compose的设计影响, 尝试将所有的点状态变为Canvas的入参信息. 代码的编写过程就变得更加的流程.


当然, 现在的我和五年前的我, 开发的能力一定是不一样的. 即便如此, 随着Kotlin的不断发展, 即使是同样用Kotlin完成的项目, 随着新的概念的提出, 更多更适合新的开发技术的出现, 我们仍然从Kotlin和Compose收获更多.


我和Kotlin的小故事


初次认识Kotlin是在2017的5月, 当时Kotlin还不是Google所推荐的Android开发语言. 对我来说, Kotlin更多的是个新的技术, 在实际的工作中也无法进行使用.


即使如此, 我也尝试开始用Kotlin去完成更多的内容, 所幸如此, 不然这篇文章就无法完成了, 我也错过了一个更深层次了解Kotlin的机会.


但是即便2018年Google将Kotlin作为Android的推荐语言, 但Kotlin在当时仍不是一个主流的选择. 对我来说以下的一些问题导致了我在当时对Kotlin的使用性质不高. 一是新语言, 社区构建不完善, 有许多的内容需要大家填充, 带来就是在实际的使用情况中会遇到各种的问题, 这些问题在网站中没有找到可行的解决方案. 二是可以和Java十分便捷互相使用的特性, 这个特性是把双刃剑,
虽然可以让我更加无负担的使用Kotlin(不行再用Java写呗.). 但也使得我认为Kotlin是个Java++或者Java--. 三是无特殊性, Kotlin并没有带来什么新的内容, Kotlin能完成的事情Java都能做完成, (空值和data class之类的在我看来更多的是一个语法糖.) 那么我为什么要用一种新的不熟悉的技术来完成我都需求?


所幸的是, 还是有更多的人在不断的推进和建设Kotlin. 也吸引了越来越多的人加入. 近年来越来越多的项目中都开始有着Kotlin的踪迹, 我将Kotlin添加到现有的项目中也变得越来越能被大家所接受. 也期待可以帮助到更多的人.


相关代码地址:


初次代码


二次代码


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

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

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

我正在参加「掘金·启航计划」

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

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

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

今天的内容大致如下:

1、分析UI,如何布局

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

3、部分源码剖析

4、开源地址及实用总结

一、分析UI,如何布局

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

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

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

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

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

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

设置属性

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

定义方法

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

三、关键源码剖析

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

定义身份简称数组

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

遍历省份简称

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

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

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

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

addRectView(textView, layout, 0.1f)
}

追加最后一个View

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

  /**
* AUTHOR:AbnerMing
* INTRODUCE:追加最后一个View
*/
private fun addEndView(layout: LinearLayout?) {
val endViewLayout = LinearLayout(context)
endViewLayout.gravity = Gravity.RIGHT
//删除按钮
val endView = RelativeLayout(context)
//添加删除按钮
val deleteImage = ImageView(context)
deleteImage.setImageResource(R.drawable.view_ic_key_delete)
endView.addView(deleteImage)

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

endView.layoutParams = params

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


}

四、开源地址及使用总结

开源地址:github.com/AbnerMing88…

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

Maven具体调用

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

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

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

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

代码使用

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

总结

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


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

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可

@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样

fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:

fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑

val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。

@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。

@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:

single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


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

Android 在一个APP里打开另一个APP

前言 不知道你们有没有注意过,每次打开一些软件的时候都会有广告引导页,有时候手滑点到了,会有进入手机上的另一个APP,这有没有引起你的注意呢? 运行效果图 # 正文 为了测试这个功能,首先要创建两个项目,然后运行起来都安装在你的手机上,这里为了方便了解,取名...
继续阅读 »

前言


不知道你们有没有注意过,每次打开一些软件的时候都会有广告引导页,有时候手滑点到了,会有进入手机上的另一个APP,这有没有引起你的注意呢?


运行效果图


在这里插入图片描述


# 正文
为了测试这个功能,首先要创建两个项目,然后运行起来都安装在你的手机上,这里为了方便了解,取名就是应用A和应用B,流程就是A应用里面打开B应用。

首先当然是创建项目了


DemoA


在这里插入图片描述


DemoB


在这里插入图片描述


创建好之后,别的先不管,都在手机上安装一下再说


在这里插入图片描述


① 打开另一个APP


接下来在DemoA的MainActivity里面写一个按钮,用于点击之后打开DemoB应用

	<Button
android:id="@+id/btn_open_b"
android:text="打开DemoB"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

在这里插入图片描述


也在DemoB的布局文件改一下显示内容

<TextView
android:textSize="18sp"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DemoB" />

运行一下


在这里插入图片描述


这样就打开了。那假如我要传递数据到DemoB呢?


② 数据传递


传数据其实就跟平时单个APP内部不同页面传数据类似,也是用Intent


在这里插入图片描述


然后在另一个APP里面接收并显示出来。现在先修改一下DemoB的布局,增加一个TextView用来显示接收的内容。

<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textColor="#000"
android:textSize="16sp" />

DemoB的MainActivity里


在这里插入图片描述


一旦两个应用程序里面改动了代码你就要在手机上运行一下,否则你改动的代码就不会生效


然后运行一下:


在这里插入图片描述


传值的问题就解决了。


③ 打开指定页面


通过包名跳转APP是进入默认的启动页面,你可以打开你的AndroidManifest.xml文件查看


在这里插入图片描述


那个Activity下面有这个默认启动就是那个

            <intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

至于要打开指定的页面有两个方法


1.通过包名、类名


首先在DemoB的下面再创建一个TestActivity,简单加一个TextView


在这里插入图片描述


在这里插入图片描述


因为是要DemoB的TestActivity页面,所以这个activity在AndroidManifest.xml中需要配置


android:exported 属性,布尔类型,是否支持其他应用访问目标 Activity,默认值为 true;android:exported="true"


否则你跳转会报错的,现在运行DemoB,使改动的代码生效
然后修改DemoA里面MainActivity的代码


在这里插入图片描述


运行效果


在这里插入图片描述


这样就可以了。


2.通过Action


修改DemoB的AndroidManifest.xml


在这里插入图片描述


然后运行在手机上,再修改DemoA的MainActivity


在这里插入图片描述


运行效果


在这里插入图片描述


其实还有一种方式是通过URL打开另一个APP,但是我不推荐这样做,为什么?没有原因...


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

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


作者:爱海贼的无处不在
来源:juejin.cn/post/7232625387296096312
收起阅读 »

30 岁了!通过 AI 问答完成了这篇思考文章

大家好,我是 shixin。 岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。 说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危...
继续阅读 »

大家好,我是 shixin。


岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。


说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危机、生活压力。


无论怎样我终究还是和三十岁相遇了,既然逃不掉,那今天就和它聊一聊。


三十岁意味着什么


我拿着这个问题问了 ChatGPT,它根据我的上下文给的回答如下:




可以看到,它给的回答还是蛮好的,基本上道出了现在困扰我的一些点。


三十岁,工作和生活对我的要求更高了。


工作方面,现在需要考虑的比以前更多了一些。除了个人贡献还需要做团队贡献,为自己的小组和整个团队带来更大价值,把自己知道的技术和经验传播给更多小伙伴。


家庭方面,快到要小孩的时候了。理论上三十岁已经年纪不小应该响应国家号召,但无奈生娃养娃的成本太大,还得多奋斗几年才有底气。今年准备先把婚礼办了(疫情影响婚礼日期改了好几次,上帝保佑这次顺利),过两年再考虑要孩子吧。


至于工作生活的平衡,老实讲目前还没有足够的资本,还得在工作上投入大部分时间。如何解决这种情况呢?是个值得思考的问题。


三十岁前我的人生


三十岁前,我的人生里有很多意想不到


十岁的我,没有想到未来我会去包头,更没有想到会在高中遇到现在的老婆。那时的我在呼和浩特,有四五个很要好的朋友,搬家的时候心里有一万个不舍。


十五岁的我,没有想到我会去西安读书,学的是计算机。那时的我还在想方设法溜到网吧通宵打游戏。


二十岁的我,没有想到我会从事安卓开发,也没有想到会去上海工作。那时的我在盲目瞎学,手机上写 OJ,看小甲鱼和黑马程序员,图书馆借了几本很老的 MFC 和 HTML CSS 书,跟着例子敲出来一个 H5 打飞机游戏。


二十五岁的我,没有想到我会在上海定居。那时我想的是干几年去西安定居,在那里离老家近一点,买房压力也小一点。后来机缘巧合,在买房时和几个前辈朋友聊了聊,听了他们的劝导,改成在上海定居。




ChatGPT 的这段回答让我泪目。有时候打的字越多,越渴望得到认可的回复,这种感觉,它给到了。



三十岁的我,虽然没有 100% 达到五年前预想的目标,但好在完成了一些当时觉得很难的事,比如买房、写书、直播分享,这些事是我成长的见证,也让我沉淀下一些经验和教训。


希望自己可以继续保持的


我希望自己继续保持的第一个点:在损失可以接受的情况下,多尝试多探索。


之前打德扑的时候,我属于比较激进和浪的那种,这种性格的缺点是会浪费很多筹码,但优点是过程很有趣,也常常会博到一些额外的收益。


生活里也是类似,在大学做小生意的时候,我愿意多跑几家店看看有没有价格更合适的货,也愿意多推开一扇门去问一下是否有需求,虽然收到不少白眼、也没赚大钱,但这段经历让我意识到:反正被拒绝也没什么损失,多试一次就多一个机会。


第二个需要继续保持的点:多种善因。


过往人生的关键节点,让我深刻的感受到:当下的果,往往来自过去不经意间种下的因。


就拿今年的几件事来说:



  1. 二月有机会在社区里做分享,缘自去年国庆主动报名 GDE 项目,虽然没通过筛选,但好在建立了联系,有这种机会时人家才会想到我



  1. 上周组里做的 ReactNative 技术培训,缘自字节时做的 Diggo 项目,在其中提升了前端开发技术,以至于后面做 RN 很顺畅,从而走在团队前头


今年很多事都是之前种下的善因结出的果实,除了满足,还需要多想想:



  1. 怎样为以后种下更多善因



  1. 现在要做的事,从长期来看,重复多次后的收益是怎样的



第三个需要继续保持的点:每日、每周、每年必做计划。


每日预则立,不立则废。我是一个善忘的人,如果哪天没有定好计划,基本上就稀里糊涂的过去了。首次发现这个问题,是我写2016 年度总结的时候,回顾发现好多细节都不记得了,有的月份里可能只记得一两件事,剩下的日子都进了黑洞无影无踪。


从那以后我就经常做记录、做计划,既然内存不够用,那就用磁盘缓存。做好每天的计划后,即使被突发事情分了心,我也可以及时调整状态回归高优。在日积月累下,才渐渐地完成了一件件看似很难的事,比如一篇有价值的文章、一个高质量的开源库(github.com/shixinzhang…)。



希望自己可以避免的


除了需要继续保持的,我也有很多后悔的事,比如做错事、说错话、浪费时间。


总结原因后,大概有这几点需要避免:



  1. 避免思想上的懒惰,少说这样的话:没办法、算了、就这样吧;多说:我试试、或许这样做就可以



  1. 避免和他人比较,比别人优秀或者差都不重要,重要的是有没有持续前进



  1. 避免没有进展的时候硬逼自己,多思考方向、方法是不是有问题



  1. 避免花钱的时候只看价钱,不对比购买后的体验和长期区别



  1. 避免做计划的时候过于悲观,目标定高点才可能做的更好



  1. 避免追求完美而不愿意开始,做完比做好优先级更高



  1. 避免在累的时候不休息,贪图享乐而继续浑浑噩噩




  1. 避免骄傲自满、自我膨胀,骄傲一来羞耻就来了




大胆想象一下,三十五岁的我


借用亚马逊的逆向工作法,先想象一下我 35 岁的情况:



  1. 第一种可能:独立开发了某个产品,为细分领域的人提供了独特的价值,从而获得不错的收益,业务比较忙的时候雇佣了几个助手



  1. 第二种可能:继续打工,但因为技术较好、沟通表达能力不错、有商业思维,担任某个业务的技术负责人



  1. 第三种可能:因为工作经验和年纪薪资不匹配被裁,投简历基本没有回复,最后忍痛降薪 50% 接了个 offer


要达到第一种情况,需要具备技术广度,可以独立完成产品的需求调研、设计、全栈开发和运营,更重要的是,尽早捕捉到信息,挖掘出其中的信息不平衡点或者需求点。这种情况对人的要求更高、风险也更高。


要达到第二种情况,需要付出的努力比上面略微少一点,需要具备一定的技术深度和广度、提升对公司业务和行业趋势的了解,主导完成一些有价值的事,同时在公司内部有一定的影响力。这种情况比第一种更稳一点。


要避免第三种情况,需要经常了解市场相关岗位的要求,不断提升自己的技术和业务价值以匹配要求,最好有代表性的作品和影响力。


总结


这篇文章是我三十岁当天开始动笔写的,因为种种原因拖到今天才完成,实在不应该(捂脸哭。


总是听人讲“三十而立”,为了看自己到底立没立,我看了好些名人的视频,想从中寻找答案。



到现在我悟了,所谓的“立“就是建立、确定、稳固。人活着最重要的就是吃饱和开心,三十岁,能够有一技之长和自我融洽的三观,就算是立住了吧!



作者:张拭心
来源:juejin.cn/post/7210386831451357221
收起阅读 »

Go 开发短网址服务笔记

这篇文章是基于课程 Go 开发短地址服务 做的笔记 项目地址:github.com/astak16/sho… 错误处理 在处理业务逻辑时,如果出错误了,需要统一处理错误响应的格式,这样可以方便前端处理错误信息 所以需要定义一个 Error 接口,它包含了 er...
继续阅读 »

这篇文章是基于课程 Go 开发短地址服务 做的笔记


项目地址:github.com/astak16/sho…


错误处理


在处理业务逻辑时,如果出错误了,需要统一处理错误响应的格式,这样可以方便前端处理错误信息


所以需要定义一个 Error 接口,它包含了 error 接口,以及一个 Status() 方法,用来返回错误的状态码


type Error interface {
error
Status() int
}

这个接口用来判断错误类型,在 go 中可以通过 e.(type) 判断错误的类型


func respondWithError(w http.RespondWrite, err error) {
switch e.(type) {
case Error:
respondWithJSON(w, e.Status(), e.Error())
default:
respondWithJSON(w, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}

go 中 实现 Error 接口,只需要实现 Error()Status() 方法即可


func () Error() string {
return ""
}
func () Status() int {
return 0
}

这样定义的方法,只能返回固定的文本和状态码,如果想要返回动态内容,可以定义一个结构体


然后 ErrorStatus 方法接受 StatusError 类型


这样只要满足 StatusError 类型的结构体,就可以返回动态内容


所以上面的代码可以修改为:


type StatusError struct {
Code int
Err error
}
func (se StatusError) Error() string {
return se.Err.Error()
}
func (se StatusError) Status() int {
return se.Code
}

middlerware


RecoverHandler


中间件 RecoverHandler 作用是通过 defer 来捕获 panic,然后返回 500 状态码


func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Println("Recover from panic %+v", r)
http.Error(w, http.StatusText(500), 500)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

LoggingHandler


LoggingHandler 作用是记录请求耗时


func (m Middleware) LoggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
end := time.Now()
log.Printf("[%s] %q %v", r.Method, r.URL.Path, end.Sub(start))
}
return http.HandlerFunc(fn)
}

中间件使用


alicego 中的一个中间件库,可以通过 alice.New() 来添加中间件,具体使用如下:


m := alice.New(middleware.LoggingHandler, middleware.RecoverHandler)
mux.Router.HandleFunc("/api/v1/user", m.ThenFunc(controller)).Methods("POST")

生成短链接


redis 连接


func NewRedisCli(addr string, passwd string, db int) *RedisCli {
c := redis.NewClient(&redis.Options{
Addr: addr,
Password: passwd,
DB: db,
})

if _, err := c.Ping().Result(); err != nil {
panic(err)
}
return &RedisCli{Cli: c}
}

生成唯一 ID


redis 可以基于一个键名生成一个唯一的自增 ID,这个键名可以是任意的,这个方法是 Incr


代码如下:


err = r.Cli.Incr(URLIDKEY).Err()
if err != nil {
return "", err
}

id, err := r.Cli.Get(URLIDKEY).Int64()
if err != nil {
return "", err
}

fmt.Println(id) // 每次调用都会自增

存储和解析短链接


一个 ID 对应一个 url,也就是说当外面传入 id 时需要返回对应的 url


func Shorten() {
err := r.Cli.Set(fmt.Sprintf(ShortlinkKey, eid), url, time.Minute*time.Duration(exp)).Err()
if err != nil {
return "", err
}
}
func UnShorten() {
url, err := r.Cli.Get(fmt.Sprintf(ShortlinkKey, eid)).Result()
}

redis 注意事项


redis 返回的 error 有两种情况:



  1. redis.Nil 表示没有找到对应的值

  2. 其他错误,表示 redis 服务出错了


所以在使用 redis 时,需要判断返回的错误类型


if err == redis.Nil {
// 没有找到对应的值
} else if err != nil {
// redis 服务出错了
} else {
// 正确响应
}

测试


在测试用例中,如何发起一个请求,然后获取响应的数据呢?



  1. 构造请求


var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`)
req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")


  1. 捕获 http 响应


rw := httptest.NewRecorder()


  1. 模拟请求被处理


app.Router.ServeHTTP(rw, req)


  1. 解析响应


if rw.Code != http.ok {
t.Fatalf("Excepted status created, got %d", rw.Code)
}

resp := struct {
Shortlink string `json:"shortlink"`
}{}
if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil {
t.Fatalf("should decode the response", err)
}

最终完整代码:


var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`)
req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")

rw := httptest.NewRecorder()
app.Router.ServeHTTP(rw, req)

if rw.Code != http.ok {
t.Fatalf("Excepted status created, got %d", rw.Code)
}
resp := struct {
Shortlink string `json:"shortlink"`
}{}

if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil {
t.Fatalf("should decode the response")
}

代码


log.SetFlags(log.LstdFlags | log.Lshortfile)


作用是设置日志输出的标志


它们都是标志常量,用竖线 | 连接,这是位操作符,将他们合并为一个整数值,作为 log.SetFlags() 的参数



  • log.LstdFlags 是标准时间格式:2022-01-23 01:23:23

  • log.Lshortfile 是文件名和行号:main.go:23


当我们使用 log.Println 输出日志时,会自动带上时间、文件名、行号信息


recover 函数使用


recover 函数类似于其他语言的 try...catch,用来捕获 panic,做一些处理


使用方法:


func MyFunc() {
defer func() {
if r := recover(); r != nil {
// 处理 panic 情况
}
}
}

需要注意的是:



  1. recover 函数只能在 defer 中使用,如果在 defer 之外使用,会直接返回 nil

  2. recover 函数只有在 panic 之后调用才会生效,如果在 panic 之前调用,也会直接返回 nil

  3. recover 函数只能捕获当前 goroutinepanic,不能捕获其他 goroutinepanic


next.ServerHttp(w, r)


next.ServeHTTP(w, r),用于将 http 请求传递给下一个 handler


HandleFunc 和 Handle 区别


HandleFunc 接受一个普通类型的函数:


func myHandle(w http.ResponseWriter, r *http.Request) {}
http.HandleFunc("xxxx", myHandle)

Handle 接收一个实现 Handler 接口的函数:


func myHandler(w http.ResponseWriter, r *http.Request) {}
http.Handle("xxxx", http.HandlerFunc(myHandler))

他们的区别是:使用 Handle 需要自己进行包装,使用 HandleFunc 不需要


defer res.Body.Close()


为什么没有 res.Header.Close() 方法?


因为 header 不是资源,而 body 是资源,在 go 中,一般操作资源后,要及时关闭资源,所以 gobody 提供了 Close() 方法


res.Bodyio.ReadCloser 类型的接口,表示可以读取响应数据并关闭响应体的对象


w.Write()


代码在执行了 w.Writer(res) 后,还会继续往下执行,除非有显示的 returepanic 终止函数执行


func controller(w http.ResponseWriter, r *http.Request) {
if res, err := xxx; err != nil {
respondWithJSON(w, http.StatusOK, err)
}
// 这里如果有代码,会继续执行
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
res, _ json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(res)
}

需要注意的是,尽管执行了 w.Writer() 后,还会继续往下执行,但不会再对响应进行修改或写入任何内容了,因为 w.Write() 已经将响应写入到 http.ResponseWriter 中了


获取请求参数


路由 /api/info?shortlink=2


a.Router.Handle("/api/info", m.ThenFunc(a.getShortlinkInfo)).Methods("GET")

func getShortlinkInfo(w http.ResponseWriter, r *http.Request) {
vals := r.URL.Query()
s := vals.Get("shortlink")

fmt.Println(s) // 2
}

路由 /2


a.Router.Handle("/{shortlink:[a-zA-Z0-9]{1,11}}", m.ThenFunc(a.redirect)).Methods("GET")

func redirect(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shortlink := vars["shortlink"]

fmt.Println(shortlink) // 2
}

获取请求体


json.NewDecoder(r.Body) 作用是将 http 请求的 body 内容解析为 json 格式


r.body 是一个 io.Reader 类型,它代表请求的原始数据


如果关联成功可以用 Decode() 方法来解析 json 数据


type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

func controller(w http.ResponseWriter, r *http.Request){
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
fmt.Println(err)
}
fmt.Println(user)
}

new


用于创建一个新的零值对象,并返回该对象的指针


它接受一个类型作为参数,并返回一个指向该类型的指针


适用于任何可分配的类型,如基本类型、结构体、数组、切片、映射和接口等


// 创建一个新的 int 类型的零值对象,并返回指向它的指针
ptr := new(int) // 0

需要注意的是:new 只分配了内存,并初始化为零值,并不会对对象进行任何进一步的初始化。如果需要对对象进行自定义的初始化操作,可以使用结构体字面量或构造函数等方式


往期文章



  1. Go 项目ORM、测试、api文档搭建

  2. Go 实现统一加载资源的入口<
    作者:uccs
    来源:juejin.cn/post/7237702874880393274
    /a>

收起阅读 »

2023了,该用一下pnpm了

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为"最先进的包管理工具"。


npm,yarn,pnpm的安装区别


首先我创建了三个文件夹分别是npm,yarn和pnpm用于比较三者之间的区别。首先初始化项目,然后安装了express来观察三个文件夹的区别。


npm和yarn的node_modules都是点开之后一眼看不到尽头。


image.png image.png


pnpm的node_modules略有区别。


image.png


npm/yarn 包结构分析


出现这种情况,是因为yarn和npm安装依赖包,会在node_modules下都平铺出来。现在安装了express,但express中也会有很多不同的依赖。在这些依赖里面有可能又引用了新的依赖,导致node_modules一点开就一望无际了。


初心是好的,因为平铺就可以复用很多依赖。如果说我package Apackage B都用了lodash@3.0.0这个包,那么使用平铺,我只需要下载一次lodash即可,节约了装包时间和存储空间。


但是真实开发情况往往是现在下载了五个依赖,其中A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0。咋整?


npmyarn目前给出的方案是将其中一个版本,(假设是)lodash@3.0.0的版本放在根目录的node_modules下面,而将需要lodash@4.0.0版本安装到C,D,E的node_modules下。如下所示,A, B可以直接使用lodash@3.0.0,而4,5,6想要使用就只能独自安装lodash@4.0.0


├─── lodash@3.0.0
├─── package-A
├─── package-B
├─── package-C
│ └── lodash@4.0.0
├─── package-D
│ └── lodash@4.0.0
├─── package-E
│ └── lodash@4.0.0

pnpm包结构分析


按照上文的例子,如果pnpm也安装五个包,A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0


├─ .pnpm
│ └── lodash@3.0.0
│ └── lodash@4.0.0
│ └── package-A@1.0.0
│ └── package-B@1.0.0
│ └── package-C@1.0.0
│ └── package-D@1.0.0
│ └── package-E@1.0.0
├──── package-A 符号链接
├──── package-B 符号链接
├──── package-C 符号链接
├──── package-D 符号链接
├──── package-E 符号链接

pnpm文件夹的node_modules下除了.pnpm文件夹外,就剩下一个package A,B,C,D,E,并且这五个包都是符号链接,它们真正的地址都是在.pnpm下。


也就是说,pnpm通过.pnpm/<name>@<version>/node_modules/<name>找到不同的包,这不仅解决了包重复下载的问题,还顺手解决了幽灵依赖的问题。



幽灵依赖:即开发者并未在package.json中下载相关包,但是在开发过程中却可以直接引用的问题。就是因为npm将依赖直接在node_modules下直接展开,导致开发者可以直接引用。问题就是当开发者升级一些包的时候,那些幽灵依赖可能并不存在于新的版本中,导致项目崩溃。



.pnpm store


pnpm牛皮的地方不只是借用了符号链接解决了包引用的问题,更是借助了硬链接解决了整个直接所有的项目依赖都给整合了,一个包全局只保存一份,并且是通过链接,速度比复制还要快的多。


借一张pnpm官网的图。


image.png


从图可以看出,.pnpm store就是依赖的实际存储位置,Mac/linux在{home dir}>/.pnpm-store/v3,windows在当前盘/.pnpm-store/v3。这样就会有个好处,你在多个项目使用的是同一个依赖时,一个包全局只保存一份,这也太省空间了吧。(只要你下载过一次,如果你没有清理.pnpm store,第二次就算你不联网照样能帮你install。)


pnpm store清理


但是,随着使用时间越长,pnpm store也会越来越大。并且随着项目版本的迭代,可能很多包都不再需要了pnpm store依旧会保留着它。此时我们需要定时清理一下。


未引用的包是系统上的任何项目中都未使用的包。 在大多数安装操作之后,包有可能会变为未引用状态,例如当依赖项变得多余时。


最好的做法是 pnpm store prune 来清理存储,但不要太频繁。 有时,未引用的包会再次被需要。 这可能在切换分支和安装旧的依赖项时发生,在这种情况下,pnpm 需要重新下载所有删除的包,会暂时减慢安装过程。


请注意,当 存储服务器正在运行时,这个命令是禁止使用的。


pnpm store prune

作者:simple_lau
来源:juejin.cn/post/7237856777588670521
收起阅读 »

原型模式与享元模式

原型模式与享元模式 原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢? 其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循...
继续阅读 »

原型模式与享元模式


原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢?


其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循环体中赋值一个对象,此时我们就可以采用原型模式来优化对象的创建过程;而在有些场景下,我们则可以避免重复创建多个实例,在内存中共享对象就好了。


今天我们就来看看这两种模式的适用场景,看看如何使用它们来提升系统性能。


原型模式


原型模式是通过给出一个原型对象来指明所创建的对象的类型,然后使用自身实现的克隆接口来复制这个原型对象,该模式就是用这种方式来创建出更多同类型的对象。


使用这种方式创建新的对象的话,就无需再通过new实例化来创建对象了。这是因为Object类的clone方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对new实例化来说,更佳。


实现原型模式


我们现在通过一个简单的例子来实现一个原型模式:


   //实现Cloneable 接口的原型抽象类Prototype
class Prototype implements Cloneable {
//重写clone方法
public Prototype clone(){
Prototype prototype = null;
try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
//实现原型类
class ConcretePrototype extends Prototype{
public void show(){
System.out.println("原型模式实现类");
}
}

public class Client {
public static void main(String[] args){
ConcretePrototype cp = new ConcretePrototype();
for(int i=0; i< 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
clonecp.show();
}
}
}


要实现一个原型类,需要具备三个条件:



  • 实现Cloneable接口:Cloneable接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用clone方法。在JVM中,只有实现了Cloneable接口的类才可以被拷贝,否则会抛出CloneNotSupportedException异常。

  • 重写Object类中的clone方法:在Java中,所有类的父类都是Object类,而Object类中有一个clone方法,作用是返回对象的一个拷贝。

  • 在重写的clone方法中调用super.clone():默认情况下,类不具备复制对象的能力,需要调用super.clone()来实现。


从上面我们可以看出,原型模式的主要特征就是使用clone方法复制一个对象。通常,有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程,然而这种复制只是对象引用的复制,也就是a和b对象指向了同一个内存地址,如果b修改了,a的值也就跟着被修改了。


我们可以通过一个简单的例子来看看普通的对象复制问题:


class Student {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student();
stu1.setName("test1");

Student stu2 = stu1;
stu2.setName("test2");

System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}


如果是复制对象,此时打印的日志应该为:


学生1:test1
学生2:test2


然而,实际上是:


学生1:test2
学生2:test2


通过clone方法复制的对象才是真正的对象复制,clone方法赋值的对象完全是一个独立的对象。刚刚讲过了,Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。


//学生类实现Cloneable接口
class Student implements Cloneable{
private String name; //姓名

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}
//重写clone方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student(); //创建学生1
stu1.setName("test1");

Student stu2 = stu1.clone(); //通过克隆创建学生2
stu2.setName("test2");

System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}


运行结果:


学生1:test1
学生2:test2


深拷贝和浅拷贝


在调用super.clone()方法之后,首先会检查当前对象所属的类是否支持clone,也就是看该类是否实现了Cloneable接口。


如果支持,则创建当前对象所属类的一个新对象,并对该对象进行初始化,使得新对象的成员变量的值与当前对象的成员变量的值一模一样,但对于其它对象的引用以及List等类型的成员属性,则只能复制这些对象的引用了。所以简单调用super.clone()这种克隆对象方式,就是一种浅拷贝。


所以,当我们在使用clone()方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。


//定义学生类
class Student implements Cloneable{
private String name; //学生姓名
private Teacher teacher; //定义老师类

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Teacher getTeacher() {
return teacher;
}

public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
//重写克隆方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}

//定义老师类
class Teacher implements Cloneable{
private String name; //老师姓名

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}

//重写克隆方法,对老师类进行克隆
public Teacher clone() {
Teacher teacher= null;
try {
teacher= (Teacher) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}
public class Test {

public static void main(String args[]) {
Teacher teacher = new Teacher (); //定义老师1
teacher.setName("刘老师");
Student stu1 = new Student(); //定义学生1
stu1.setName("test1");
stu1.setTeacher(teacher);

Student stu2 = stu1.clone(); //定义学生2
stu2.setName("test2");
stu2.getTeacher().setName("王老师");//修改老师
System.out.println("学生" + stu1.getName + "的老师是:" + stu1.getTeacher().getName);
System.out.println("学生" + stu1.getName + "的老师是:" + stu2.getTeacher().getName);
}
}


运行结果:


学生test1的老师是:王老师
学生test2的老师是:王老师


观察以上运行结果,我们可以发现:在我们给学生2修改老师的时候,学生1的老师也跟着被修改了。这就是浅拷贝带来的问题。


我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象,代码如下:


   public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
Teacher teacher = this.teacher.clone();//克隆teacher对象
student.setTeacher(teacher);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}


适用场景


前面我详述了原型模式的实现原理,那到底什么时候我们要用它呢?


在一些重复创建对象的场景下,我们就可以使用原型模式来提高对象的创建性能。例如,我在开头提到的,循环体内创建对象时,我们就可以考虑用clone的方式来实现。


例如:


for(int i=0; i<list.size(); i++){
Student stu = new Student();
...
}


我们可以优化为:


Student stu = new Student();
for(int i=0; i<list.size(); i++){
Student stu1 = (Student)stu.clone();
...
}


除此之外,原型模式在开源框架中的应用也非常广泛。例如Spring中,@Service默认都是单例的。用了私有全局变量,若不想影响下次注入或每次上下文获取bean,就需要用到原型模式,我们可以通过以下注解来实现,@Scope(“prototype”)。


享元模式


享元模式是运用共享技术有效地最大限度地复用细粒度对象的一种模式。该模式中,以对象的信息状态划分,可以分为内部数据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运行而改变;外部数据则是在不同运行时被标记了不同的值。


享元模式一般可以分为三个角色,分别为 Flyweight(抽象享元类)、ConcreteFlyweight(具体享元类)和 FlyweightFactory(享元工厂类)。抽象享元类通常是一个接口或抽象类,向外界提供享元对象的内部数据或外部数据;具体享元类是指具体实现内部数据共享的类;享元工厂类则是主要用于创建和管理享元对象的工厂类。


实现享元模式


我们还是通过一个简单的例子来实现一个享元模式:


//抽象享元类
interface Flyweight {
//对外状态对象
void operation(String name);
//对内对象
String getType();
}


//具体享元类
class ConcreteFlyweight implements Flyweight {
private String type;

public ConcreteFlyweight(String type) {
this.type = type;
}

@Override
public void operation(String name) {
System.out.printf("[类型(内在状态)] - [%s] - [名字(外在状态)] - [%s]\n", type, name);
}

@Override
public String getType() {
return type;
}
}


//享元工厂类
class FlyweightFactory {
private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();//享元池,用来存储享元对象

public static Flyweight getFlyweight(String type) {
if (FLYWEIGHT_MAP.containsKey(type)) {//如果在享元池中存在对象,则直接获取
return FLYWEIGHT_MAP.get(type);
} else {//在响应池不存在,则新创建对象,并放入到享元池
ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
FLYWEIGHT_MAP.put(type, flyweight);
return flyweight;
}
}
}


public class Client {

public static void main(String[] args) {
Flyweight fw0 = FlyweightFactory.getFlyweight("a");
Flyweight fw1 = FlyweightFactory.getFlyweight("b");
Flyweight fw2 = FlyweightFactory.getFlyweight("a");
Flyweight fw3 = FlyweightFactory.getFlyweight("b");
fw1.operation("abc");
System.out.printf("[结果(对象对比)] - [%s]\n", fw0 == fw2);
System.out.printf("[结果(内在状态)] - [%s]\n", fw1.getType());
}
}


输出结果:


[类型(内在状态)] - [b] - [名字(外在状态)] - [abc]
[结果(对象对比)] - [true]
[结果(内在状态)] - [b]


观察以上代码运行结果,我们可以发现:如果对象已经存在于享元池中,则不会再创建该对象了,而是共用享元池中内部数据一致的对象。这样就减少了对象的创建,同时也节省了同样内部数据的对象所占用的内存空间。


适用场景


享元模式在实际开发中的应用也非常广泛。例如Java的String字符串,在一些字符串常量中,会共享常量池中字符串对象,从而减少重复创建相同值对象,占用内存空间。代码如下:


 String s1 = "hello";
String s2 = "hello";
System.out.println(s1==s2);//true


还有,在日常开发中的应用。例如,池化技术中的线程池就是享元模式的一种实现;将商品存储在应用服务的缓存中,那么每当用户获取商品信息时,则不需要每次都从redis缓存或者数据库中获取商品信息,并在内存中重复创建商品信息了。


总结


原型模式和享元模式,在开源框架,和实际开发中,应用都十分广泛。


在不得已需要重复创建大量同一对象时,我们可以使用原型模式,通过clone方法复制对象,这种方式比用new和序列化创建对象的效率要高;在创建对象时,如果我们可以共用对象的内部数据,那么通过享元模式共享相同的内部数据的对象,就可以减少对象的创建,实现系统调优。


本文由mdnic

e多平台发布

收起阅读 »