注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

3个bug导致Kafka消息丢失,我人麻了

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug 发送MQ消息居然加了超时熔断 在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QP...
继续阅读 »

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug


发送MQ消息居然加了超时熔断


在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QPS超过20且失败率大于5%时触发熔断。这意味着当QPS=20时,只要有一条消息发送超时,整个系统就会熔断,无法继续发送MQ消息。
hystrix.command.default.circuitBreaker.errorThresholdPercentage=5


HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),
@HystrixProperty(name = "execution.timeout.enabled", value = "true")})
public void doSendMessage(Message message){
// 发送消息
}

之前系统一直运行正常,直到最近系统请求量上升才触发了这个bug。现在已经找不到是谁配置了这个过于激进的熔断策略了。真的非常气人!


一般情况下,发送MQ消息不会失败。但是在服务刚启动且未预热时,可能会有少量请求超过100毫秒,被Hystrix判断为失败。而恰好当时QPS超过了20,导致触发了熔断。


为什么发送MQ消息还需要加入熔断机制呢? 我很不理解啊


MQ(消息队列)本身就是用来削峰填谷的,可以支持非常高的并发量。无论是低峰期还是高峰期,只要给MQ发送端添加熔断机制都会导致数据严重不一致!我真的不太明白,为什么要在发送MQ消息时加入熔断机制。


另外,为什么要设定这么激进的熔断策略呢?仅有5%的失败率就导致服务100%不可用,这是哪个天才的逻辑呢?至少在失败率超过30%且QPS超过200的情况下,才需要考虑使用熔断机制吧。在QPS为20的情况下,即使100%的请求都失败了,也不会拖垮应用服务,更何况只是区区5%的失败率呢。


这是典型的为了熔断而熔断!把熔断变成政治正确的事情。不加熔断反而变成异类,会被人瞧不起!


吞掉了异常


虽然添加熔断策略,会导致发送MQ失败抛出熔断异常,但是上层代码考虑了消息发送失败的情况。流程中包含分布式重试方案,但是排查问题时我才发现,重试策略居然没有生效!这是什么原因?


在一番排查后我发现,发送MQ的代码 吞掉了异常信息,没有向上抛出!


去掉无用的业务逻辑后,我把代码粘贴到下面。


try{
doSendMessage(msg);
}catch(Exception e){
log.error("发送MQ异常:{}", msg, e);
//发送失败MQ消息到公司故障群!
}

消息发送异常后,仅仅在系统打印了ERROR日志,并将失败消息发送到了公司的IM群里。然而,这样的处理方式根本无法让上层方法意识到消息发送失败的情况,更别提察觉到由于熔断而导致的发送失败了。在熔断场景下,消息根本没有被发送给MQ,而是直接失败。因此,可以确定消息一定丢失了。


面试时我们经常会被问到”如何保证消息不丢“。大家能够滔滔不绝地说出七八个策略来确保消息的可靠性。然而当写起代码时,为什么会犯下如此低级的错误呢?


仅仅打印ERROR日志就能解决问题吗?将故障消息上报到公司的群里就有人关注吗?考虑到公司每天各种群里都会涌现成千上万条消息,谁能保证一定有人会关注到!国庆节放假八天,会有人关注公司故障群的消息吗?


很多人在处理异常时习惯性的吞掉异常,害怕把异常抛给上游处理。系统应该处理Rpc调用失败、MQ发送失败的场景,不应该吞掉异常,而是应该重试!一般流程都会有整体的分布式重试机制,出问题不怕、出异常也不怕,只要把问题抛出,由上游发起重试即可。


悄咪咪的把异常吞掉,不是处理问题的办法!


于是我只能从日志中心捞日志,然后把消息手动发送到MQ中。我真的想问,这代码是人写的吗?


服务关闭期间,生产者先于消费者关闭,导致消息发送失败


出问题的系统流程是 先消费TopicA ,然后发送消息到Topic B。但是服务实例关闭期间,发送TopicB消息时,报错 producer has closed。为什么消费者还未关闭,生产者先关闭呢?


这个问题属于服务优雅发布范畴,一般情况下都应该首先关闭消费者,切断系统流量入口,然后再关闭生产者实例。


经过排查,发现问题的原因是生产者实例注册了shutdown hook钩子程序。也就是说,只要进程收到Kill信息,生产者就会启动关闭流程。这解释了为什么会出现这个问题。


针对这个问题,我修改了策略,删除了生产者注册shutdown hook钩子的逻辑。确保消费者先关闭!生产者后关闭。


总结


如果有人问我:消息发送失败的可能原因,我是肯定想不到会有这三个原因的。也是涨见识了。


很多人滔滔不绝的谈着 消息不丢不重,背后写的代码却让人不忍直视!


作者:他是程序员
来源:juejin.cn/post/7288228582692929547
收起阅读 »

Linux当遇到kill -9杀不掉的进程怎么办?

web
前言 在Linux中,我们经常使用kill或者kill -9来杀死特定的进程,但是有些时候,这些方法可能无法终止某些进程。本文将详细解释为什么会出现这种情况,以及如何处理这种问题。 无法被杀死的进程: 首先,我们来理解一下为什么有些进程无法被杀死。通常,这是因...
继续阅读 »

前言


在Linux中,我们经常使用kill或者kill -9来杀死特定的进程,但是有些时候,这些方法可能无法终止某些进程。本文将详细解释为什么会出现这种情况,以及如何处理这种问题。


无法被杀死的进程:


首先,我们来理解一下为什么有些进程无法被杀死。通常,这是因为这些进程处于以下两种状态之一:


僵尸进程(Zombie Process):


当一个进程已经完成了它的运行,但是其父进程还没有读取到它的结束状态,那么这个进程就会成为僵尸进程。僵尸进程实际上已经结束了,所以你无法使用kill命令来杀掉它。



内核态进程:


如果一个进程正在执行某些内核级别的操作(即进程处在内核态),那么这个进程可能无法接收到kill命令发送的信号。


查找和处理僵尸进程:


如果你怀疑有僵尸进程存在,你可以使用以下命令来查找所有的僵尸进程:


ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'

这个命令实际上是由两个命令通过管道(|)连接起来的。管道在Linux中的作用是将前一个命令的输出作为后一个命令的输入。命令的两部分是 ps -A -ostat,ppid,pid,cmd 和 grep -e '^[Zz]'。



  • ps -A -ostat,ppid,pid,cmd:这是ps命令,用来显示系统中的进程信息。

    • -A:这个选项告诉ps命令显示系统中的所有进程。

    • -o:这个选项允许你定义你想查看的输出格式。在这里,你定义的输出格式是stat,ppid,pid,cmd。这会让ps命令输出每个进程的状态(stat)、父进程ID(ppid)、进程ID(pid)以及进程运行的命令(cmd)。



  • grep -e '^[Zz]':这是grep命令,用来在输入中查找匹配特定模式的文本行。

    • -e:这个选项告诉grep命令接下来的参数是一个正则表达式。

    • '^[Zz]':这是你要查找的正则表达式。^符号表示行的开始,[Zz]表示匹配字符“Z”或者“z”。因此,这个正则表达式会匹配所有以“Z”或者“z”开头的行。在ps命令的输出中,状态为“Z”或者“z”的进程是僵尸进程。




因为僵尸进程已经结束了,所以你无法直接杀掉它。但是,你可以试图杀掉这些僵尸进程的父进程。杀掉父进程之后,僵尸进程就会被init进程(进程ID为1)接管,然后被清理掉。


你可以使用以下命令来杀掉父进程:


kill -HUP [父进程的PID]

请注意,在杀掉父进程之前,你需要确定这样做不会影响到系统的其他部分。另外,这个方法并不保证能够清理掉所有的僵尸进程。


查找和处理内核态进程:


如果一个进程处在内核态,那么这个进程可能无法接收到kill命令发送的信号。在这种情况下,你需要首先找到这个进程的父进程,然后试图杀掉父进程。你可以使用以下命令来查找进程的父进程:


cat /proc/[PID]/status | grep PPid

这个命令会输出进程的父进程的ID,由两个独立的命令组成,通过管道(|)连接起来。我会分别解释这两个命令,然后再解释整个命令:



  • cat /proc/[PID]/status :

    • 这是一个cat命令,用于显示文件的内容。在这个命令中,它用于显示一个特殊的文件/proc/[PID]/status。

    • /proc是一个特殊的目录,它是Linux内核和用户空间进行交互的一种方式。在/proc目录中,每个正在运行的进程都有一个与其PID对应的子目录。每个子目录中都包含了关于这个进程的各种信息。

    • /proc/[PID]/status文件包含了关于指定PID的进程的各种状态信息,包括进程状态、内存使用情况、父进程ID等等;



  • grep PPid :

  • 这是一个grep命令,用于在输入中查找匹配特定模式的文本行。在这个命令中,它用于查找包含PPid的行。在/proc/[PID]/status文件中,PPid一行包含了这个进程的父进程的PID;
    然后,你可以使用以下命令来杀掉父进程:


kill -9 [父进程的PID]

同样,你需要在杀掉父进程之前确定这样做不会影响到系统的其他部分。另外,这个方法并不保证能够杀掉所有的内核态进程。


结论:


在Linux系统中,处理无法被杀死的进程可以是一项挑战,尤其是当你无法确定进程状态或者无法影响父进程的时候。以上的方法并不保证能够解决所有问题。如果你尝试了所有的方法,但问题仍然存在,或者你不确定如何进行,那么你可能需要联系系统管理员,或者寻求专业的技术支持。


总的来说,处理无法被杀死的进程需要对Linux的进程管理有深入的理解,以及足够的耐心和谨慎。希望这篇文章能够帮助你更好地理解这个问题,以及如何解决这个问题。


作者:泽南Zn
来源:juejin.cn/post/7288116632785420303
收起阅读 »

h5调用手机摄像头踩坑

web
1. 背景 一般业务也很少接触摄像头,有也是现成的工具库扫个二维码。难得用一次,记录下踩坑。 2.调用摄像头的方法 2.1. input <!-- 调用相机 --> <input type="file" accept="image/*" ca...
继续阅读 »

1. 背景


一般业务也很少接触摄像头,有也是现成的工具库扫个二维码。难得用一次,记录下踩坑。


2.调用摄像头的方法


2.1. input


<!-- 调用相机 -->
<input type="file" accept="image/*" capture="camera">
<!-- 调用摄像机 -->
<input type="file" accept="video/*" capture="camcorder">
<!-- 调用录音机 -->
<input type="file" accept="audio/*" capture="microphone">

这个就不用多说了,缺点就是没办法自定义界面,它是调用的系统原生相机界面。


2.2. mediaDevices


由于我需要自定义界面,就像下面这样:
image.png


所以我选择了这个方案,这个api使用起来其实很简单:


<!-- 创建一个video标签用来播放摄像头的视屏流 -->
<video id="video" autoplay="autoplay" muted width="200px" height="200px"></video>
<button onclick="getMedia()">开启摄像头</button>

async getMedia() {
// 获取设备媒体的设置,通常就video和audio
const constraints = {
// video配置,具体配置可以看看mdn
video: {
height: 200,
wdith: 200,
},
// 关闭音频
audio: false
};
this.video = document.getElementById("video");
// 使用getUserMedia获取媒体流
// 媒体流赋值给srcObject
this.video.srcObject = await window.navigator.mediaDevices.getUserMedia(constraints);
// 直接播放就行了
this.video.play();
}

image.png
可以看到这个效果。


这个api的配置可以参考MDN


// 截图拍照
takePhoto() {
const video = document.getElementById("video");
// 借助canvas绘制视频的一帧
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext('2d');
ctx.drawImage(this.video, 0, 0, 300, 300);
},
// 停止
stopMedia() {
// 获取媒体流
const stream = this.video.srcObject;
const tracks = stream.getTracks();
// 停止所有轨道
tracks.forEach(function (track) {
track.stop();
})
this.video.srcObject = null;
}

3.坑


如果你复制我的代码,在localhost上肯定能运行,但是你想在手机上试试的时候就会发现很多问题。


3.1. 需要https


由于浏览器的安全设置,除了localhosthttps连接,你都没办法获取到navigator.mediaDevices,打印出来是undefined。如果要在手机上测试,你要么用内网穿透代理一个https,要么部署在https域名的服务器上测试。


3.2. 设置前后摄像头


默认是使用user设备,也就是前摄像头,想要使用后摄像头也是有配置的,


async getMedia() {
// ...
let constraints = {
video: {
height: 200,
wdith: 200,
// environment设备就是后置
facingMode: { exact: "environment" },
},
audio: false
};
// ...
}

3.3. 设置显示区域大小


我的需求是铺满整个设备,所以我想当然的直接把video样式宽高设置成容器大小:


#video {
width: 100%;
height: 100%;
}

async getMedia() {
// ....
// 将宽高设置成容器大小
const pageSize = document.querySelector('.page').getBoundingClientRect()
let constraints = {
video: {
height: pageSize.height,
width: pageSize.width,
facingMode: { exact: "environment" },
},
audio: false
};
//....
}

image.png
发现这个视频横着而且没有铺满屏幕。


通过输出video的信息可以看到,设备返回的视频流宽高是反的:


image.png


所以配置换一下就行了:


    let constraints = {  
video: {
height: pageSize.width,
width: pageSize.height,
},
};

作者:头上有煎饺
来源:juejin.cn/post/7287965561035210771
收起阅读 »

叫声【全栈工程师】,你敢应吗?

上面是我打开百度百科,写着对全栈工程师的解释:是指掌握多种技能,可以胜任前端和后端,能用多种技能独立完成产品的人。 对于这个答案我是保持观望的态度。如果说能同时开发前端和后端,还能独立完成产品,它就是全栈工程师的话,那计算机专业的大学生做完毕业设计之后就都是全...
继续阅读 »

图片


上面是我打开百度百科,写着对全栈工程师的解释:是指掌握多种技能,可以胜任前端和后端,能用多种技能独立完成产品的人。


对于这个答案我是保持观望的态度。如果说能同时开发前端和后端,还能独立完成产品,它就是全栈工程师的话,那计算机专业的大学生做完毕业设计之后就都是全栈了。


对于百科的这个定义,我感觉确实有点宽泛了,于是我就重新编辑了这个百度百科:


图片


小伙伴们可能不知道百度百科是可以随意编辑的,我整整花了一分钟的时间,精心编辑了一个百度百科的概念:全栈工程师是指在web项目开发中,独立掌握web前端、安卓开发、ios开发、后端技术(Java,PHP,Node,关系型数据库,分布式开发等技术)的综合性、高素质软件工程师。 目前为止这个词条的审批还没有通过。


再回到全栈工程师这个称呼上,我第一次听到这个词是在2015年,那时候前后端分离的开发模式刚刚开始被普及。因为2015年之前的web开发项目,前端几乎都是用模板套jQuery来做的。像ember、backbone、angularjs这些框架,小公司几乎用不起来。


但是在2015年这个节点,web项目井喷式地增长,像react这样新兴的轻量级框架,开始走进了中小公司。前后端分离的开发模式也越来越多的被大家使用起来了,在这个技术背景之下,全栈工程师这个词被提的就越来越多,而被称为全栈工程师的人主要分成以下三类。


第一类:


在公司的技术部门独挡一面,被同事称为问题终结者或者是bug收割机,这样的全栈工程师其实也是所有对技术追求的程序员而奋斗的目标,我确实很佩服这样的全栈。


第二类:


主要分布在中小公司,名头是【全栈工程师】,其实是【全干工程师】。小公司为了节约人力成本,前后端就找一个人干,甚至可能一个公司就一个程序员把所有的活都干了。


图片


各种的压榨劳动力,然后还给了一个好的名头(全栈工程师),没事再画画饼,说公司上市之后,你就是技术总监,然后享受各种股份,期权,你就财富自由了。现实情况就是你累倒了,老板财富自由了。


但是在2015年前后那个时候大家还是很吃这一套的,所以很多人愿意天天加班,最后大多数人也是什么也没得到。现在大家看招聘网站上,小公司招聘的全栈工程师基本都是这个套路:


图片


就是说想用更少的钱去招人干更多的活,这工作基本上干起来就是一地鸡毛。


第三类:


相比上面的【全干工程师】,这一类才坑,培训机构。借着【全栈工程师】的这个名号忽悠大学生。比如一个机构,它以前是教java的,后面就加了点前端的课程,又或者以前是教前端的,然后加了点Node课程,就说我们是全栈工程师培训机构。


许多学生们纷纷交钱报班,等毕业了才知道自己学的这个【全栈工程师】只能去小作坊企业,996公司,或者是去一些非软件,非互联网干一些辅助工作。


总结:


真正正规的软件公司或者互联网公司都是专人专岗的,就算它招了全栈工程师,也是高新的技术专家,怎么可能招一个培训班刚毕业,包装2年工作经验的职场新人呢。所以大部分情况下,我确实不太喜欢全栈工程师这个岗位(称呼),因为这个词总是能和【忽悠大学生】,【压榨劳动力】这些联系到一起。


作者:程序员Winn
来源:juejin.cn/post/7287975566349828155
收起阅读 »

天天摸鱼度日,34岁的我被裁了。。。

程序猿的35岁危机 程序猿的35岁危机已经不是个新鲜话题了,不管行内还是行外多少都听过了解过。行外的一听:咦~😒,程序猿狗都不干!、行内的一听笑一笑也就过去了,大部分都是当成个笑点,并未真正思考过(当然也可能是逃避)。 程序员毕竟是有点脑力活的职业,到了3...
继续阅读 »

程序猿的35岁危机



程序猿的35岁危机已经不是个新鲜话题了,不管行内还是行外多少都听过了解过。行外的一听:咦~😒,程序猿狗都不干!、行内的一听笑一笑也就过去了,大部分都是当成个笑点,并未真正思考过(当然也可能是逃避)。


程序员毕竟是有点脑力活的职业,到了35岁基本上就无法在一线开发上熬了,不像拧螺丝,我感觉我60岁拧的应该也不慢哈哈哈。所以如果能预测到35岁升不到管理岗或者未转行,那就要开始思考如果被裁之后能干什么了。



  • 卖煎饼(有贷在身的就别想了,卖不完根本卖不完

  • 短视频等副业(有点搞头,趁早

  • 独立AI应用工程师(有精力才行)

  • ......


我预想这个问题的时候,发现我竟然什么都不会,舒适圈待久了,也没有动力去学新的东西,我意识到这样下去肯定不行,得找点后路。



屌丝现状



我:从小到大都是那种处在中间层的人物,没有做过领头羊,也没有拖过别人的腿。成绩属于中层;身高处于中层(180🤪);家庭属于中层。当然肉夹馍我只吃中间那层。


正是因为都是夹心饼干,让我一直处于舒适圈,既拿不出寒门学子的冲劲,家底也不支持我躺的安静,导致我高不成低不就的状况,没错,就是这款冰红茶,屌丝款极具性价比。


image.png


虽然有点技术追求,也看过写过不少源码,算法题也做了不少,但是就是缺少一鞭子让我跳出那个圈子,刚校招时,信誓旦旦的觉得自己技术牛逼,肯定能进大厂,但是现实总会给你迎头一棒,于是就进了一家中厂,刚开始的时候内心还是满船清梦,心态良好,路漫漫其修远兮,吾将上下而求索。


到了后面,才发现满腔热血早已被各种业务琐事冷却,许多精力都用来应对客户以及各种内部无厘头,加上加班,回到茅屋根本无力再精进功力,慢慢地,曾经想在大厂大显身手的念头也羞于提起,毕业两年未到就想着养老躺平,各种摸鱼技术倒是进步了不少,哈哈哈,这让我想起老父亲常常训我的话:


你学习要有打游戏这份劲,早就上清华啦🤡你学习要有打游戏这份劲,早就上清华啦🤡


回想起来,已经许久未跟家里通过一通电话了,筹划许久带父母国庆去旅行也没有实现,只寄了几个老母亲下不了口的双流老妈麻辣兔头,估计也是得等我回去消灭了。


image.png


我呢,属于是人小穷志不穷,志虽然不穷,但是也不多,有点杞人忧天,喜欢想很远很远的可能存在的隐患,差不多是时态里面的一般过去现在将来时(最近在从头梳理英语哈哈)。想的东西特别多,行动的屈指可数。



  • 35岁危机之前一直在想

  • 健身

  • 博客写作

  • 骑行,吉他 还算过得去

  • ......


以俺目前的情况来看,如果不努力一把进大厂搞点青春血汗钱,大概率是浑浑噩噩,凄凄惨惨戚戚的在中小厂熬到35岁,然后一大把年纪,卷不过小鲜肉,熬不过鸡汤,然后下岗。当然这对于有点咸鱼觉悟的我来说是万万不可能让它发生的,摸鱼我也要可持续性摸鱼!


咱就是说现今这种社会夹心饼的情况,不知道有多少Javaer跟我一样,每天上班💼就是打几行代码,摸摸鱼,像你们一样看看掘金。每天就在掘金找找别人生活失意的文章来抚慰内心的荒芜不安,看到别人过的不咋样,自己也就烂的心安理得了;看到别人凭自己的努力进大厂进外企又心痒痒,打开leetcode,肯触c肯触v,搞定,今天又骗过了自己。


我相信应该有不少人就像我这样,别否认,说的就是你


image.png


内心虽有鸿鹄之志,怎耐无破而后立之决心\color{green}{内心虽有鸿鹄之志,怎耐无破而后立之决心
}


image.png


都怪自己没有穷的叮当响,激发不出意志潜力哈哈哈哈


话说回来,虽然知道这样不好,自欺欺人,结果不会帮着你一起行骗,但是就是少了份冲劲,没有一个小镇做题家该有的决心觉悟,内心又渴望自己能够提升进步,却一而再的给自己留退路找借口。



痛定思痛



为了自己的前途着想(持续摸鱼😀),彻夜冥想,找了三条可以应对危机又不那么累的路。


退路一:考公


考公现在很卷,2022年212.3万人通过了资格审查,实际录用人数只有3.12万,报录比高达68:1,虽然但是,只要我们心态放好,别去选那些几千个人招一个的,趁早考,多考几次,希望还是很大的,最重要的是坚持以及学习方法,再不行还可以考事业编。


退路二:外企


程序员去外企主要是英语能力要跟的上,虽然也可能有危机,但是概率小很多,况且英语能力摆在那,就算去干其他的也不至于入不敷出(关键在于把英语学好,过程重要‼️,最终能不能去外企放倒是其次)


退路三:放弃自我,精神小伙


其实为什么现在很多网红都是学历比较低,因为学历低的人接受教育比较少,没那么多包袱,许多想做的事情不会有心理负担,而高学历的人,很多都不愿意抛头露面,认为那是丢脸的事情。另外高学历的人道德感比较强,不愿意做一些违心的话题,特别是不愿意炒作。


加上如今畸形的社会审美,低俗的短视频反而更能获得注目,优秀的产出只有少数人驻足。


如果我们能放好心态,告诉自己赚钱不寒碜,跟网红们卷起来,高等的教育能让我们经受住网络看客的审察,这也许是优势🖖



总结



像我这样处在中间高不成低不就的程序员,心态放好就是一种幸福,每天打打代码偶尔旅旅游,陪陪父母,不会大富大贵也不会食不果腹,摆烂会不安,努力又泡汤,我想这应该就是属于我的道。


最后:如果你已经为自己选好了退路,就全力以赴,不要每天想起来就学一点,造成退路已经妥了的假象。我希望你可以两手抓,而不是说你在备考公务员,你觉得你有退路了,目前的工作就不重视,态度散漫,到时还没开始考试你就失业了。




这是我在掘金的第一篇文章,主要是记录下自己的所思所想,当然如果这篇文章能引起读者们的一些思考,那也是掘掘子啊👍🏽,哈哈哈哈。




但使龙城飞将在,不教胡马度阴山


作者:lvresse
来源:juejin.cn/post/7287788617916448802
收起阅读 »

开源框架 NanUI 作者转行卖钢材,项目暂停开发

NanUI 作者在国庆节发布了停更公告,称该项目将暂停开发,原因是去年被裁员失业后,他已转行销售钢材,现在很难腾出时间来开发和维护 NanUI 项目。他说道:为了生存,本人只能花费更多的时间和精力去谈单,去销售,去收款,因此已经很难再腾出时间来开发和维护 Na...
继续阅读 »

NanUI 作者在国庆节发布了停更公告,称该项目将暂停开发,原因是去年被裁员失业后,他已转行销售钢材,现在很难腾出时间来开发和维护 NanUI 项目。

他说道:

为了生存,本人只能花费更多的时间和精力去谈单,去销售,去收款,因此已经很难再腾出时间来开发和维护 NanUI 项目,对此我深感无奈,也希望后面生活和工作稳定后能腾出时间来继续维护 NanUI。

NanUI 作者表示,他所在公司因疫情于去年(2022 年)初彻底宣布裁减所有开发岗位,因此他也只能顺应大流在 36 岁这个尴尬的年纪失业。


via https://github.com/XuanchenLin/NanUI/discussions/367

NanUI 界面组件是一个开放源代码的 .NET/.NET Core 窗体应用程序(WinForms)界面框架。它适用于希望使用 HTML5/CSS3 等前端技术来构建 Windows 窗体应用程序用户界面的 .NET 开发人员。

NanUI 基于谷歌可嵌入的浏览器框架 Chromium Embedded Framework (CEF),因此用户可以使用各种前端技术 HTML5/CSS3/JavaScript 和流行前端框架 React/Vue/Angular/Blazor 设计和开发 .NET 桌面应用程序的用户界面。

同时,NanUI 独创的 JavaScript Bridge 可以方便地实现浏览器端与 .NET 之间的通信和数据交换。

使用 NanUI 界面框架将为传统的 WinForm 应用程序的用户界面设计和开发工作带来无限种可能!

作者:oschina
来源:www.oschina.net/news/261033

收起阅读 »

东野圭吾:我的人生就像在白夜里走路。

东野圭吾在他的小说《白夜行》中写道:“我的人生就像在白夜里走路。” 这个比喻意味着主人公的生活充满了复杂、模糊和充满挑战的情境。 无尽的白昼与黑夜的迷雾 在北极圈的白夜中,太阳在天空中持续存在,使白昼看似无尽。然而,这并不意味着光明总是清晰可见。黑夜的迷雾可以...
继续阅读 »

塞尔维亚,麦田里小麦的特写.jpg


东野圭吾在他的小说《白夜行》中写道:“我的人生就像在白夜里走路。” 这个比喻意味着主人公的生活充满了复杂、模糊和充满挑战的情境。


无尽的白昼与黑夜的迷雾


在北极圈的白夜中,太阳在天空中持续存在,使白昼看似无尽。然而,这并不意味着光明总是清晰可见。黑夜的迷雾可以让人看不清事物的真相,正如生活中的挑战和不确定性常常让我们感到困惑。


想象一个人在职场中努力工作,但却不确定自己是否能获得升职的机会。尽管工作充满了希望和努力,但不确定性就像白夜中的迷雾一样,感到困惑和不安。


不断前行的决心


白夜行走的比喻也表达了主人公面对人生困难时的坚韧和决心。尽管光明可能模糊不清,但主人公依然坚持前行,不放弃。


与小说中的主人公一样,许多人在面临困境时也表现出坚定的决心。一个创业者可能会面对种种困难,但他仍然坚持前进,努力实现自己的梦想,这就像白夜行走一样,充满了挑战但仍然坚定不移。


坚韧、勇气和希望


东野圭吾的隐喻提醒我们,生活中的挑战和困难虽然充满不确定性,但我们可以通过坚韧、勇气和希望来克服它们,就像在白夜中行走一样,坚持不懈,永不放弃。


无法逃避的内心挣扎


生活中的白夜可以理解为内心的无法抽离的挣扎与矛盾。人们往往在自己的内心与欲望之间产生冲突,这是不可避免的。这一内心挣扎如同白夜里无法躲避的光线,照亮了我们内心的深处。一个人可能在事业与家庭之间感到分裂,不知道如何平衡,这种内心挣扎让他感到仿佛在白夜中走路,无法找到前进的方向。


生活中的无尽选择


白夜中走路也可以看作是生活中的无尽选择。在现代社会,人们面临着诸多选择,这些选择需要我们思考、决策和承担责任。每一个选择都可能影响我们的人生道路,就像白夜中的每一步都可能改变我们的方向。人们需要在无数个可能性中寻找自己的道路,这种选择的过程就像在白夜中摸索前进。


不确定性与未知的未来


白夜行走也表现了生活的不确定性和未知性。无论我们做多少计划,未来仍然充满了变数和未知的因素。就像在白夜中,我们无法预测下一步会有什么,生活中的未来同样充满了谜团。这种不确定性让人们感到焦虑和无助,需要在不确定的环境中前行。


生活中的很多成功故事都展示了人们在困境中坚持不懈,战胜了巨大的挑战。这些人的故事启示我们,尽管人生可能充满了白夜的迷雾,但通过坚强的意志和积极的态度,我们可以找到前行的道路,并最终达到成功的彼岸。


作者:晒晒心里话
来源:juejin.cn/post/7286127842421047332
收起阅读 »

如何在10分钟内让Android应用大小减少 60%?

一个APP的包之所以大,主要包括一下文件 代码 lib so本地库 资源文件(图片,音频,字体等) 瘦身就主要瘦这些。 一、打包的時候刪除不用的代码 buildTypes {        debug {            ...        ...
继续阅读 »

一个APP的包之所以大,主要包括一下文件



  • 代码

  • lib

  • so本地库

  • 资源文件(图片,音频,字体等)


瘦身就主要瘦这些。


一、打包的時候刪除不用的代码


buildTypes {
       debug {
           ...
           shrinkResources true // 是否去除无效的资源文件(如果你的Debug也需要瘦身)
      }
       release {
           ...
           shrinkResources true // 是否去除无效的资源文件
      }
  }

二、减少不必要的打包


defaultConfig {
   ...
   //打包的语言类型(语种的翻译)
   resConfigs "en", "de", "fr", "it"
   //打包的文件夹
   resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}

或者


android {
 ...
 splits {
   density {
     enable true
     exclude "ldpi", "tvdpi", "xxxhdpi"
     compatibleScreens 'small', 'normal', 'large', 'xlarge'

     //reset()
     //include 'x86', 'armeabi-v7a', 'mips'
     //universalApk true
  }
}

三、lib


尽量不用太复杂的lib,轻量级lib是首选。如果你的应用没用到兼容库,可以考虑去掉support包。


四、资源文件


我们可以通过Lint工具找到没有使用的资源(在Android Studio的“Analyze”菜单中选择“Inspect Code…”)


五、把现有图片转换为webP


我们可以通过 智图 或者isparta将其它格式的图片转换成webP格式,isparta可实现批量转换。


五、图片相关



  • 在Android 5.0及以上的版本可以通过tintcolor实现只提供一张按钮的图片,在程序中实现按钮反选效果,前提是图片的内容一样,只是正反选按钮的颜色不一样。


Drawable.setColorFilter( 0xffff0000, Mode.MULTIPLY )


  • 在Android 5.0及以上的版本,可以使用VectorDrawable和SVG图片来替换原有图片


六、混淆


1 构建多个版本



  • 在gradle中的buildTypes中增加不同的构建类型,使用applicationSuffixversionNameSuffix可以生成多个版本在同一设备上运行

  • 创建src/[buildType]/res/设置不同的ic_launcher以区别不同版本


2 混淆参数


{ 
   debug { minifyEnabled false }
   release {
     signingConfig signingConfigs.release
     minifyEnabled true
     proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
}

minifyEnabled true



  • 是否要启用通过 ProGuard 实现的代码压缩(true启用)

  • 请注意,代码压缩会拖慢构建速度,因此您应该尽可能避免在调试构建中使用。 :Android Studio 会在使用Instant Run时停用 ProGuard。


proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'



  • getDefaultProguardFile(‘proguard-android.txt')方法可从 Android SDKtools/proguard/文件夹获取默认 ProGuard 设置。

  • 提示: 要想做进一步的代码压缩,可尝试使用位于同一位置的proguard-android-optimize.txt文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

  • proguard-rules.pro文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle文件旁)。

  • 要添加更多各构建变体专用的 ProGuard 规则,请在相应的productFlavor代码块中再添加一个proguardFiles属性。例如,以下 Gradle 文件会向flavor2产品风味添加flavor2-rules.pro。现在flavor2使用所有三个 ProGuard 规则,因为还应用了来自release代码块的规则。

  • 每次构建时 ProGuard 都会输出下列文件 dump.txt 说明 APK 中所有类文件的内部结构。mapping.txt:提供原始与混淆过的类、方法和字段名称之间的转换。seeds.txt:列出未进行混淆的类和成员。usage.txt:列出从 APK 移除的代码。这些文件保存在/build/outputs/mapping/release/

  • 要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行-keep代码。例如: -keeppublicclassMyClass

  • 您还可以向您想保留的代码添加[@Keep] (developer.android.com/reference/a…)注解。在类上添加@Keep可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。

  • 在使用-keep选项时,有许多事项需要考虑;如需了解有关自定义配置文件的详细信息,请阅读ProGuard 手册问题排查一章概述了您可能会在混淆代码时遇到的其他常见问题。

  • 请注意,您每次使用 ProGuard 创建发布构建时都会覆盖mapping.txt文件,因此您每次发布新版本时都必须小心地保存一个副本。通过为每个发布构建保留一个mapping.txt文件副本,您就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试。

  • 在每次添加库的时候,需要及时进行make a release build

  • DexGuard时Proguard同一个团队开发的软件, 优化代码,分离dex文件从而解决65k方法限制的文件


关于proguard-android.txt文件:


-dontusemixedcaseclassnames: 表示混淆时不使用大小写混淆类名。 -dontskipnonpubliclibraryclasses:不跳过library中的非public方法。 -verbose: 打印混淆的详细信息。 -dontoptimize: 不进行优化,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。 -dontpreverify: 不进行预校验。 -keepattributes Annotation :对注解参数进行保留。 -keep public class com.google.vending.licensing.ILicensingService -keep public class com.android.vending.licensing.ILicensingService: 表示不混淆上述声明的两个类。


proguard中一共有三组六个keep关键字的含义


keep  保留类和类中的成员,防止它们被混淆或移除。
keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers  只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames  只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers  保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames  保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。

keepclasseswithmember和keep关键字的区别: 如果这个类没有native的方法,那么这个类会被混淆


-keepclasseswithmember class * {
   native <methods>;
}

不管这个类有没有native的方法,那么这个类不会被混淆


-keep class * {
   native <methods>;
}



另外、 你可以使用 APK Analyser 分解你的 APK


Android Studio 提供了一个有用的工具:APK Analyser。APK Analyser 将会拆解你的应用并让你知道 .apk 文件中的那个部分占据了大量空间。让我们看一下 Anti-Theft 在没有经过优化之前的截图。


img


从 Apk Analyser 的输出来看,应用的原大小是 3.1MB。经过 Play 商店的压缩,大致是 2.5MB。


从截图中可以看出主要有 3 个文件夹占据了应用的大多数空间。



classes.dex —— 这是 dex 文件,包含了所有会运行在你的 DVM 或 ART 里的字节码文件。 res —— 这个文件夹包含了所有在 res 文件夹下的文件。大部分情况下它包含所有图片,图标和源文件,菜单文件和布局。



img



resources.arsc —— 这个文件包含了所有 value 资源。这个文件包含了你 value 目录下的所有数据。包括 strings、dimensions、styles、intergers、ids 等等。



img


你有两个默认的混淆文件。


proguard-android-optimize.txt proguard-android.txt 就像文件名写的那样,“proguard-android-optimize.txt”是更积极的混淆选项。我们将这个作为默认的混淆配置。你可以在 /app 目录下的 proguard-rules.pro 里添加自定义的混淆配置。


 release {
//Enable the proguard
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), "proguard-rules.pro"

//Other parameters
debuggable false
jniDebuggable false
renderscriptDebuggable false
signingConfig playStoreConfig //Add your own signing config
pseudoLocalesEnabled false
zipAlignEnabled true
}

通过设置 minifyEnabled 为 true,混淆将会移除所有未使用的方法、指令以减小 classes.dex 文件。


这是启用了 minify 之后的 APK。


七、AndroidStudio使用lint清除无用的资源文件 在使用AndroidStudio进行App开发的时候,我们经常会在项目中引用多种资源文件,包括图片,布局文件,常量引用定义。随着项目版本开发的迭代,每一期的资源会有变动必定会留下一些无用的资源这个时候我们手动去一个一个寻找效率就会很低下。这个时候我们就要学会AndroidStudio使用lint清除无用的资源文件。



  • 打开AndroidStudio在项目中,点击最上方的菜单栏Analyze -> Run Inspection by Name 如下图:


img



  • 点击 Run Inspection by Name会弹出一个对话框。在对话框里面输入unused resource 如下图:


img



  • 然后点击下拉列表中的unused resource。 之后会弹出一个对话框如下图


img


结尾


好啦,如此文章到这里就结束了,希望这篇文章能够帮到正在看的你们,能够解决Android小伙伴们应用内存问题~


更多Android进阶指南 可以详细Vx关注公众号:Android老皮 解锁               《Android十大板块文档》


1.Android车载应用开发系统学习指南(附项目实战)


2.Android Framework学习指南,助力成为系统级开发高手


3.2023最新Android中高级面试题汇总+解析,告别零offer


4.企业级Android音视频开发学习路线+项目实战(附源码)


5.Android Jetpack从入门到精通,构建高质量UI界面


6.Flutter技术解析与实战,跨平台首要之选


7.Kotlin从入门到实战,全方面提升架构基础


8.高级Android插件化与组件化(含实战教程和源码)


9.Android 性能优化实战+360°全方面性能调优


10.Android零基础入门到精通,高手进阶之路


敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔


作者:花海blog
来源:juejin.cn/post/7287473826060763197
收起阅读 »

实现转盘抽奖功能

web
1、实现转盘数据动态配置(可通过接口获取) 2、背景色通过分隔配置 3、转动速度慢慢减速,最后停留在每一项的中间,下一次开始从本次开始 4、当动画停止后在对应事件中自定义生成中奖提示。 5、本次中奖概率随机生成,也可自定义配置 实现代码 html <te...
继续阅读 »

1、实现转盘数据动态配置(可通过接口获取)


2、背景色通过分隔配置


3、转动速度慢慢减速,最后停留在每一项的中间,下一次开始从本次开始


4、当动画停止后在对应事件中自定义生成中奖提示。


5、本次中奖概率随机生成,也可自定义配置


实现代码


html


<template>
<div class="graph-page">
<div class="plate-wrapper" :style="`${bgColor};`">
<div class="item-plate" :style="plateCss(index)" v-for="(item, index) in plateList" :key="index" >
<img :src="item.pic" alt="">
<p>{{item.name}}</p>
</div>
</div>
<div @click="handleClick" class="btn"></div>
</div>
</template>


css


<style lang="less" scoped>
.graph-page {
width: 540px;
height: 540px;
margin: 100px auto;
position: relative;
}
.plate-wrapper {
width: 100%;
height: 100%;
border-radius: 50%;
border: 10px solid #98d3fc;
overflow: hidden;
}
.item-plate {
position: absolute;
left: 0;
right: 0;
top: -10px;
margin: auto;
}
.item-plate img {
width: 30%;
height: 20%;
margin: 40px auto 10px;
display: block;
}
.item-plate p {
color: #fff;
font-size: 12px;
text-align: center;
line-height: 20px;
}
.btn {
width: 160px;
height: 160px;
background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/btn_lottery.png') no-repeat center / 100% 100%;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
cursor: pointer;
}
.btn::before {
content: "";
width: 41px;
height: 39px;
background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/icon_point.png') no-repeat center / 100% 100%;
position: absolute;
left: 0;
right: 0;
top: -33px;
margin: auto;
}
</style>


js


其中背景色采用间隔配置,扇形背景采用锥形渐变函数conic-gradient可实现。


每个转项的宽度和高度可参照以下图片,所有奖品的div都定位在圆心以上,根据圆心转动,所以旋转点为底部中心,即:transform-origin: 50% 100%;


可采用监听transitionend事件判断动画是否结束,可自定义中奖提示。


lADPJwKt5iekh_DNA1bNBJI_1170_854.jpg_720x720g.jpg


<script>
export default {
data() {
return {
plateList: [],
isRunning: false, //判断是否正在转动
rotateAngle: 0, //转盘每项角度
baseRunAngle: 360 * 5, //总共转动角度,至少5圈
totalRunAngle: 0, //要旋转的总角度
activeIndex: 0, //中奖index
wrapDom: null //转盘dom
}
},
computed: {
bgColor(){ //转盘的每项背景
let len = this.plateList.length
let color = ['#5352b3', '#363589']
let colorVal = ''
this.plateList && this.plateList.forEach((item, index)=>{
colorVal += `${color[index % 2]} ${(360/len)*index}deg ${(360/len)*(index+1)}deg,`
})
return `background: conic-gradient(${colorVal.slice(0, -1)})`
},
plateCss(){ //转盘的每项样式
if(this.plateList && this.plateList.length){
return (i) => {
return `
width: ${Math.floor(2 * 270 * Math.sin(this.rotateAngle / 2 * Math.PI / 180))}px;
height: 270px;
transform: rotate(${this.rotateAngle * i + this.rotateAngle / 2}deg);
transform-origin: 50% 100%;
`

}
}
return ()=>{''}
},
},
created(){
this.plateList= [
{ name: '手机', pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e' },
{ name: '手表', pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '苹果', pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG' },
{ name: '棒棒糖', pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG' },
{ name: '娃娃', pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG' },
{ name: '木马', pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '德芙', pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '玫瑰', pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG' }
]
this.rotateAngle = 360 / this.plateList.length
this.totalRunAngle = this.baseRunAngle + 360 - this.activeIndex * this.rotateAngle - this.rotateAngle / 2
},
mounted(){
this.$nextTick(()=>{
this.wrapDom = document.getElementsByClassName('plate-wrapper')[0]
})
},
beforeDestroy(){
this.wrapDom.removeEventListener('transitionend', this.stopRun)
},
methods:{
handleClick(){
if(this.isRunning) return
this.isRunning = true
const ind = Math.floor(Math.random() * this.plateList.length)//通过随机数返回奖品编号
this.activeIndex = ind
this.startRun()
},
startRun(){
// 设置动画
this.wrapDom.setAttribute('style', `
${this.bgColor};
transform: rotate(${this.totalRunAngle}deg);
transition: all 4s ease;
`
)
this.wrapDom.addEventListener('transitionend', this.stopRun) // 监听transition动画停止事件
},
stopRun(){
this.isRunning = false
this.wrapDom.setAttribute('style', `
${this.bgColor};
transform: rotate(${this.totalRunAngle - this.baseRunAngle}deg);
`
)
}
}
}
</script>

参考来源:juejin.cn/post/718031…


作者:李某某的学习生活
来源:juejin.cn/post/7287125076369801279
收起阅读 »

听说你会架构设计?来,弄一个公交&地铁乘车系统

1. 引言 1.1 上班通勤的日常 “叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。 突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。 这个时候,通勤的老难题又摆...
继续阅读 »

1. 引言


1.1 上班通勤的日常


“叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。



突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。


这个时候,通勤的老难题又摆在了你面前:要不要吃完这口面包、刷牙和洗脸,还是先冲出门赶车?


好不容易做出了一个艰难的决定——放下面包,快步冲出门。你拿出手机,点开了熟悉的地铁乘车 App 或公交地铁乘车码小程序。


然后,一张二维码在屏幕上亮了起来,这可是你每天通勤的“敲门砖”。





你快步走到地铁站,将手机二维码扫描在闸机上,"嗖"的一声,闸机打开,你轻松通过,不再需要排队买票,不再被早高峰的拥挤闹心。


你走进地铁车厢,挤到了一个角落,拿出手机,开始计划一天的工作。


1.2 公交&地铁乘车系统


正如上文所说,人们只需要一台手机,一个二维码就可以完成上班通勤的所有事项。


那这个便捷的公交或地铁乘车系统是如何设计的呢?它背后的技术和架构是怎样支撑着你我每天的通勤生活呢?


今天让我们一起揭开这个现代都市打工人通勤小能手的面纱,深入探讨乘车系统的设计与实现


在这个文章中,小❤将带你走进乘车系统的世界,一探究竟,看看它是如何在短短几年内从科幻电影中走出来,成为我们日常生活不可或缺的一部分。


2. 需求设计


2.1 功能需求





  • 用户注册和登录: 用户可以通过手机应用或小程序注册账号,并使用账号登录系统。




  • 路线查询: 用户可以查询地铁的线路和站点信息,包括发车时间、车票价格等。




  • 获取乘车二维码: 系统根据用户的信息生成乘车二维码。




  • 获取地铁实时位置: 用户可以查询地铁的实时位置,并查看地铁离当前站台还有多久到达。




  • 乘车扫描和自动支付: 用户在入站和出站时通过扫描二维码来完成乘车,系统根据乘车里程自动计算费用并进行支付。




  • 交易记录查询: 用户可以查询自己的交易历史记录,包括乘车时间、金额、线路等信息。




2.2 乘车系统的非功能需求


乘车系统的用户量非常大,据《中国主要城市通勤检测报告-2023》数据显示,一线城市每天乘公交&地铁上班的的人数普遍超过千万,平均通勤时间在 45-60 分钟,并集中在早高峰和晚高峰时段。


所以,设计一个热点数据分布非均匀、人群分布非均匀的乘车系统时,需要考虑如下几点:




  • 用户分布不均匀,一线城市的乘车系统用户,超出普通城市几个数量级。




  • 时间分布不均匀,乘车系统的设计初衷是方便上下班通勤,所以早晚高峰的用户数会高出其它时间段几个数量级。




  • 高并发: 考虑到公交车/地铁系统可能同时有大量的用户在高峰时段使用,系统需要具备高并发处理能力。




  • 高性能: 为了提供快速的查询和支付服务,系统需要具备高性能,响应时间应尽可能短。




  • 可扩展性: 随着用户数量的增加,系统应该容易扩展,以满足未来的需求。




  • 可用性: 系统需要保证24/7的可用性,随时提供服务。




  • 安全和隐私保护: 系统需要确保用户数据的安全和隐私,包括支付信息和个人信息的保护。




3. 概要设计


3.1 核心组件





  • 前端应用: 开发手机 App 和小程序,提供用户注册、登录、查询等功能。




  • 后端服务: 设计后端服务,包括用户管理、路线查询、二维码管理、订单处理、支付系统等。




  • 数据库: 使用关系型数据库 MySQL 集群存储用户信息、路线信息、交易记录等数据。




  • 推送系统: 将乘车后的支付结果,通过在线和离线两种方式推送给用户手机上。




  • 负载均衡和消息队列: 考虑使用负载均衡和消息队列技术来提高系统性能。




3.2 乘车流程


1)用户手机与后台系统的交互


交互时序图如下:



1. 用户注册和登录: 用户首先需要在手机应用上注册并登录系统,提供个人信息,包括用户名、手机号码、支付方式等。


2. 查询乘车信息: 用户可以使用手机应用查询公交车/地铁的路线和票价信息,用户可以根据自己的出行需求选择合适的线路。


3. 生成乘车二维码: 用户登录后,系统会生成一个用于乘车的二维码,这个二维码可以在用户手机上随时查看。这个二维码是城市公交系统的通用乘车二维码,同时该码关联到用户的账户和付款方式,用户可以随时使用它乘坐任何一辆公交车或地铁。


2)用户手机与公交车的交互


交互 UML 状态图如下:





  1. 用户进站扫码: 当用户进入地铁站时,他们将手机上的乘车码扫描在进站设备上。这个设备将扫描到的乘车码发送给后台系统。




  2. 进站数据处理: 后台系统接收到进站信息后,会验证乘车码的有效性,检查用户是否有进站记录,并记录下进站的时间和地点。




  3. 用户出站扫码: 用户在乘车结束后,将手机上的乘车码扫描在出站设备上。




  4. 出站数据处理: 后台系统接收到出站信息后,会验证乘车码的有效性,检查用户是否有对应的进站记录,并记录下出站的时间和地点。




3)后台系统的处理




  1. 乘车费用计算: 基于用户的进站和出站地点以及乘车规则,后台系统计算乘车费用。这个费用可以根据不同的城市和运营商有所不同。




  2. 费用记录和扣款: 系统记录下乘车费用,并从用户的付款方式(例如,支付宝或微信钱包)中扣除费用。




  3. 乘车记录存储: 所有的乘车记录,包括进站、出站、费用等信息,被存储在乘车记录表中,以便用户查看和服务提供商进行结算。




  4. 通知用户: 如果有需要,系统可以向用户发送通知,告知他们的乘车费用已被扣除。




  5. 数据库交互: 在整个过程中,系统需要与数据库交互来存储和检索用户信息、乘车记录、费用信息等数据。




3. 详细设计


3.1 数据库设计



  • 用户信息表(User) ,包括用户ID、手机号、密码、支付方式、创建时间等。

  • 二维码表 (QRCode) ,包括二维码ID、用户ID、城市ID、生成时间、有效期及二维码数据等。

  • 车辆&地铁车次表 (Vehicle) ,包括车辆ID、车牌或地铁列车号、车型(公交、地铁)、扫描设备序列号等。

  • 乘车记录表 (TripRecord) ,包括记录ID、用户ID、车辆ID、上下车时间、起止站点等。

  • 支付记录表 (PaymentRecord) ,包括支付ID、乘车记录ID、交易时间、交易金额、支付方式、支付状态等。


以上是一些在公交车&地铁乘车系统中需要设计的数据库表及其字段的基本信息,后续可根据具体需求和系统规模,还可以进一步优化表结构和字段设计,以满足性能和扩展性要求。


详细设计除了要设计出表结构以外,我们还针对两个核心问题进行讨论:



  • 最短路线查询




  • 乘车二维码管理




3.2 最短路线查询


根据交通部门给的公交&地铁路线,我们可以绘制如下站点图:



假设图中的站点有 A-F,涉及到的交通工具有地铁 1 号线和 2 路公交,用户的起点和终点分别为 A、F 点。我们可以使用 Dijkstra 算法来求两点之间的最短路径,具体步骤为:


步骤已遍历集合未遍历集合
1选入A,此时最短路径 A->A = 0,再以 A 为中间点,开始寻找下一个邻近节点{B、C、D、E、F},其中与 A 相邻的节点有 B 和 C,AB=6,AC=3。接下来,选取较短的路径节点 C 开始遍历
2选取C,A->C=3,此时已遍历集合为{A、C},以 A 和 C 为中间点,开始寻找下一个邻近节点{B、D、E、F},其中与 A、C 相邻的节点有 B 和 D,AB=6,ACD=3+4=7。接下来,选取较短的路径节点 B 开始遍历
3选取B,A->B=6,此时已遍历集合为{A、C、B},A 相邻的节点已经遍历结束,开始寻找和 B、C 相近的节点{D、E、F},其中与 B、C 相邻的节点有 D,节点 D 在之前已经有了一个距离记录(7),现在新的可选路径是 ABD=6+5=11。显然第一个路径更短,于是将 D 的最近距离 7 加入到集合中
4选取D,A->D=7,此时已遍历集合为{A、C、B、D},寻找 D 相邻的节点{E、F},其中 DE=2,DF=3,选取最近路径的节点 E 加入集合
5选取 E,A->E=7+2=9,此时已遍历集合为{A、C、B、D、E},继续寻找 D 和 E 相近的节点{F},其中 DF=3,DEF=2+5=7,于是F的最近距离为7+3=10.
6选取F,A->F=10,此时遍历集合为{A、C、B、D、E、F}所有节点已遍历结束,从 A 点出发,它们的最近距离分别为{A=0,C=3,B=6,D=7,E=9,F=10}

在用户查询路线之前,交通部门会把公交 & 地铁的站点经纬度信息输入到路线管理系统,并根据二维的空间经纬度编码存储对应的站点信息。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成 4 个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识用户或站点的位置信息。再通过 Redis 的 GeoHash 算法,来获取用户出发点附近的所有站点信息。


GeoHash 算法的原理是将一个位置的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有站点


一旦获得了起始地点的经纬度,系统就可以根据附近的站点信息,调用路线管理系统来查找最佳的公交或地铁路线。


一旦用户选择了一条路线,导航引擎启动并提供实时导航指引。导航引擎可能会使用地图数据和 GPS 定位来指导用户前往起止站点。


3.3 乘车二维码管理


乘车码是通过 QR 码(Quick Response Code)技术生成的,它比传统的 Bar Code 条形码能存更多的信息,也能表示更多的数据类型,如图所示:



二维码的生成非常简单,拿 Go 语言来举例,只需引入一个三方库:


import "github.com/skip2/go-qrcode"

func main() {
    qr,err:=qrcode.New("https://mp.weixin.qq.com",qrcode.Medium)
if err != nil {
    log.Fatal(err)
else {
    qr.BackgroundColor = color.RGBA{50,205,50,255//定义背景色
    qr.ForegroundColor = color.White //定义前景色
    qr.WriteFile(256,"./wechatgzh_qrcode.png"//转成图片保存
    }
}

以下是该功能用户和系统之间的交互、二维码信息存储、以及高并发请求处理的详细说明:



  1. 用户与系统交互: 用户首先在手机 App 上登录,系统会验证用户的身份和付款方式。一旦验证成功,系统根据用户的身份信息和付款方式,动态生成一个 QR 码,这个 QR 码包含了用户的标识信息和相关的乘车参数。

  2. 二维码信息存储: 生成的二维码信息需要在后台进行存储和关联。通常,这些信息会存储在一个专门的数据库表中,该表包含以下字段:



    • 二维码ID:主键ID,唯一标识一个二维码。

    • 用户ID:与乘车码关联的用户唯一标识。

    • 二维码数据:QR码的内容,包括用户信息和乘车参数。

    • 生成时间:二维码生成的时间戳,用于后续的验证和管理。

    • 有效期限:二维码的有效期,通常会设置一个时间限制,以保证安全性。



  3. 高并发请求处理: 在高并发情况下,大量的用户会同时生成和扫描二维码,因此需要一些策略来处理这些请求:



    • 负载均衡: 后台系统可以采用负载均衡技术,将请求分散到多个服务器上,以分担服务器的负载。

    • 缓存优化: 二维码的生成是相对耗时的操作,可以采用 Redis 来缓存已生成的二维码,避免重复生成。

    • 限制频率: 为了防止滥用,可以限制每个用户生成二维码的频率,例如,每分钟只允许生成 5  次,这可以通过限流的方式来实现。




总之,通过 QR 码技术生成乘车码,后台系统需要具备高并发处理的能力,包括负载均衡、缓存和频率限制等策略,以确保用户能够快速获得有效的乘车二维码。


同时,二维码信息需要被安全地存储和管理,比如:加密存储以保护用户的隐私和付款信息。



不清楚如何限流的,可以看我之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 乘车系统的发展


4.1 其它设计


除此之外,公交车或地铁的定位和到站时间计算可能还涉及定位设备、GPS 系统、NoSQL 数据库、用户 TCP 连接管理系统等核心组件,并通过实时数据采集、位置处理、到站时间计算和信息推送等流程来为用户提供准确的乘车信息。


同时,自动支付也是为了方便用户的重要功能,可以通过与第三方支付平台的集成来实现。


4.2 未来发展


公交车/地铁乘车系统的未来发展可以包括以下方向:



  • 智能化乘车: 引入智能设备,如人脸自动识别乘客、人脸扣款等。

  • 大数据分析: 利用大数据技术分析乘车数据,提供更好的服务。


在设计和发展过程中,也要不断考虑用户体验、性能和安全,确保系统能够满足不断增长的需求。


由于篇幅有限,文章就到此结束了。


希望读者们能对公交&地铁乘车系统的设计有更深入的了解,并和小❤一起期待未来更多的交通创新解决方案叭~


作者:xin猿意码
来源:juejin.cn/post/7287495466514055202
收起阅读 »

少一点功利主义,多一点傻逼似的坚持

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己 坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的...
继续阅读 »

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己


坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的酝酿,它会迸发处惊人的力量!


不过有一关是很难过的,这一关基本上可以刷掉百分之九十五的人,那就是否有长期主义,是否能够忍受“没有回报”,因为人的本性就是贪婪,而我们从小受到的教育就是“付出就有收获”,所以我们在做每一件事的时候,心里第一反应是我做这件事能给我带来多少收获。


比如读一本书,其实很多时候我们都是带有目的性的,比如觉得事业不顺,人生失意,或者想赚快钱,那么这时候就会去快速翻阅一些诸如《快速致富》的书籍,然后加满鸡血后,第二天依旧是十二点起,起来又卷入精神内耗中,反反复复,最终宝贵是时光!


又比如你看到别人赚到了钱,于是眼睛一红,就问他怎么赚的,别人稍微指点后,你就暗下决心要搞钱,前几天到几个月期间赚了几块钱,你就失落了,你在想,这条路子行不通,于是就放弃了,又去折腾其它的了。


上述的例子是百分之九十的人的真实写照,那么我觉得可以总结为两点:


1.只要没有得到应有的回报,就觉得是损失


2.极强的功利主义


首先对于这一点,我觉得是我们最容易犯的错,比如当一个人说你去坚持做这件事情,一个月会有一千的附加收入,你去做了,而实际上只拿到了50元的收入,这时候你就会极度的不平衡,感到愤怒,你会觉得花了这么多时间才得到50元,老子不干了,实际上你在这个过程中学到的东西远比1000块多,不过你不会觉得,这时候你宁愿去刷短视频,追剧,你也不会去做这件事了。


所以当你心中满是“付出多少就应该得到多少回报”的时候,你不可能做好事,也不会得到更好的回报,因为你心中总是在想“会不会0回报”,“这玩意究竟靠谱不靠谱”,克服这种心态是一件十分难的事情!


第二点,我觉得我们应该少一点功利主义,多一点傻逼似的坚持,这说得有点理想主义了,人本质就是贪婪的,如果赚不到钱,我就不做,对我没好处,我也不会做,我有写文章的习惯其实从大学就开始了,以前没发公众号,之前朋友经常说我,你写的有什么卵用?能赚钱吗?有人看吗?


一开始我还会在乎,在问自己,你干嘛写这些,因为写个人的感悟和生活这种文章确实会有一定的心里压力,朋友说:”你自己都是这个鸟样,有什么资格去给别人说教“,不过随着时间的推移,我不再去在乎这些了。


就单拿写文章这件事来说,虽然没赚到钱,不过在这个过程中,我逐渐不再浮躁,能静下心来写,也结实了朋友,这是一种对自己的总结,对技术的总结,也是一种锻炼,虽然现在文笔依然很差,不过我依然会像一个傻逼一样去坚持。


时间是最奇妙的东西,你的一些坚持一定会在相应的时间点迸发处惊人的力量!


回头想一下,你没写文章,没看书,没学习,没出去看世界,而是拿着个手机躺在床上刷短视频,像个清朝抽鸦片的人一样,那么你又收获了多少呢?


作者:刘牌
来源:juejin.cn/post/7278245506719825955
收起阅读 »

转全栈之路,会比想象中的艰难

背景 我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。 在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了...
继续阅读 »

背景


我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。


在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了一家公司,也很想记录一下到底有什么不同。


大前端部门业务部门
组织人数近30人,纯前端方向近40人,分为不同方向,前端背景1人
工作模式由于同事都在天南海北,需要通过视频会议进行沟通纯下线沟通,所有同事都base深圳
沟通效率较低,每次沟通都需要调试设备,共享屏幕等,并且见不到面很多信息会失真高,直接面谈,肢体语言这些信息不会丢失
工作节奏有排期压力,有承诺客户交付时间。如果排期不合理会很疲惫。没有排期压力,前端工作量相比之前轻松
设计资源有专门的UED团队出图,前端不需要思考如何进行交互,这部分工作由设计师承担无设计资源,交互的好坏完全取决于研发的审美水平与自我要求
前端技术建设每个季度会有横向建设,有组件库共建等机会,前端技术相对先进部门内部无前端建设,依赖公司基建与之前经验
同事组成深圳base全员年轻化,校招生为主,因为年龄相同且技术方向相同,天然就有很多话题资深员工多,校招生占比很低,且划分不同方向,一般自己方向的人自己内部沟通较多
和+1的关系base不同,沟通频率很低。因为主要是做业务方的需求,沟通内容主要在支持工作的进展上。base相同,沟通频率比以前高5-10倍,除同步开发进展,还会针对产品迭代方向,用户体验等问题进行沟通
技术成长受限于部门性质以及绩效评价体系,员工需要在前端技术领域保持专业且高效,但工作一定年限后有挑战性的业务需求不足,容易遇到职业发展瓶颈。因为前端人数多,所以存在横向建设的空间,可以共建组件库等基建,非常自然的会接触这些需求。角色划分不明确,前后端可以相互支援彼此,大家摘掉前后端的标签,回归通用开发的角色。技术成长依赖自驱力与公司技术水平。研发人少,没有内部的横向建设机会。

纠结


为什么要转全栈?究竟有什么收益?我会在心里时不时问自己这个问题。任何一门技能,从入门到精通,都需要很多时间的学习与实践,在初期都会经历一段相当痛苦的时光。除了学习不轻松,能否创造出更大的价值也是一个问号。


但这次转全栈,有天时地利人和的作用,总结下来就是:



  1. Leader支持:和Leader沟通过,Leader觉得在我们团队多做多学对个人,对团队都有益处,欢迎我大胆尝试

  2. 后端同学支持:我们团队的细分项目多,后端工作饱和,可以分一个相对独立的活给我

  3. 全栈化背景:原先的大前端部门已经有部分前端转为全栈开发职能,部门层面鼓励全栈化发展

  4. 需求清晰:有些开发之所以忙碌是因为开会和对齐耗时太多。但是我目前拿到的prd都非常清晰,拿到就能直接开发,对齐扯皮的时间几乎不计,我只需要完成开发工作即可。这节约了我大量时间成本。想到之前经常是一天开个1-2小时会,搞得很疲惫。

  5. 工作熟练:从实习开始算起,我已经有2年多的开发经验,可以在预期时间内完成需求开发和bugfix,因此安全的预留时间精力转全栈。


其实不仅仅是我,和很多做前端的同事、朋友也都聊过,其实内心各有各的纠结。基本上大家的内心想法就是想在有限的条件下学习后端,并在有限的条件下承担一部分后端开发。


想学后端的原因:



  1. 纯属好奇心,想研究一下后端技术栈

  2. 前端作为最终的执行角色,话语权低

  3. 业务参与度低,可以半游离业务的存在,较边缘化。未来如果希望成长为管理,难以做业务管理,只能做技术管理,想象空间天花板就是成为管理一批前端的技术管理。

  4. 工作遇到天花板,想多了解一下其他的内容


想在有限条件下学习后端的原因:



  1. 工作比较忙碌,没那么多时间学习

  2. 学习一门技能要算ROI,学太多了如果既不能升职也不能加薪就没有意义

  3. 不确定市场对于全栈人才的反应,不想all in


想承担一部分后端开发的原因:



  1. 学习任何一门技能只有理论没有实践遗忘速度最快,马上就会回归到学习之前

  2. 掌握后端技能但没有企业级实战经验,说服力很弱


不想学习后端的原因:



  1. 国内市场上的全栈岗位数量稀少,如果后端岗位有10个,前端岗位有3个,那么可能就只有1个全栈岗位

  2. 普通前后端开发薪酬基本上没有区别,未来谁更好晋升在当前的经济背景也难说

  3. 大概率前端依然是自己的职业发展主线,学多一门技能可能会分摊本可以提升前端能力的时间精力

  4. 做舒适圈里面的事情很舒服,谁知道多做会不会有好处


我就是在这种纠结中一路学过来,从8月开始,痛苦且挣扎,不过到目前为止还可以接受。学到现在甚至已经有点麻木。但我也确实不知道继续在前端领域还能专精什么技能,现有的业务没有那么大的挑战性让我快速成长,所以想跳脱出来看看更大的世界。


学习路线


曲线学习


如果说做前端开发我是如鱼得水,那做后端开发就是经常呛到水。


记得我刚开始做前端实习的时候,真心感到前端知识好像黑洞,永远也学不完。由此非常佩服之前的同事,怎么把这些代码写出来的,有些代码后面一看写的还不错,甚至可能会感觉脊背发凉,是自己太弱还是自己太强?


在实习的时候,我的学习曲线可以说是一个向外扩散的圆。比如我第一次接触webpack的时候,根本不了解这是什么工具,之前一直在用jQuery写项目,所有的js都是明文写好,然后通过script引入到html中。所以一开始我会去查这个webpack到底是什么内容,但脑海中对他的印象还是非常模糊。接着我又因为webpack了解到了babel,css-loader这些概念,又去学习。又发现这需要利用到node,又去学习了《深入浅出node.js》。再后来又了解到了sourcemap等概念。直到正式加入字节半年后,我自己配了一次webpack,并且阅读了他的源码。进行了debug,进行了一次webpack插件开发的分享,才有信心说自己是真的弄明白了。不过这个弄明白,也仅限于排查bug,配项目,进行plugin和loader的开发,如果遇到更难的领域,那又将解锁一块黑洞。


怎么学


学习后端,要学的内容也一点都不少,作为新人会遇到非常多的问题。



  1. 怎么学 - 是死皮赖脸的逮住后端同学使劲问,还是多自己研究研究?遇到所有同事都不会的问题怎么处理?

  2. 学到什么程度 - 究竟要学到怎样的程度才能进入项目开发,而不犯下一些非常愚蠢的问题呢?

  3. 学习顺序 - 最简单的办法就是去看项目,看到不懂的分析一下这是什么模块的,看看要不要系统性的去了解。


我比较喜欢一开始就系统性的学,学完后再查缺补漏,再开启第二轮学习。


比如Go,官网就有很详细的文档,但未必适合新人去学。我跟着官网学了一阵子之后跑b站找视频学习了。然后又Google了一些资料,大致讲了一下反射、切片的原理,以及一些错误用法。学习Go大概用了2-3周。刚学完直接去看项目还是会觉得非常不适应,需要不断的让自己去阅读项目代码,找到Go的那种感觉。


然后需要学习很多公司内部的基建



  • 微服务架构 - 公司内部所有服务都是微服务架构,需要了解服务发现、服务治理、观测、鉴权这些基本概念以及大致的原理。为了在本地开发环境使用微服务,还需要在本地安装doas,用来获取psm的token。

  • RDS - 公司内的项目分为了各种环境,非常复杂。可以自己先创建一个MySQL服务自测,看看公司的云平台提供了哪些能力。

  • Redis - 大致了解即可,简单用不难

  • RPC - 微服务通过RPC传递,RPC协议通过IDL来定义接口传输格式,像字节会在api管理平台做封装。你定义好的IDL可以直接生成一个gopkg上传到内部镜像,然后其他用户直接go get这个库就能调用你的服务。但如果你是node服务,就可以在本地通过字节云基建的工具库自动生成代码。

  • Gorm - 所有的MySQL最终如果通过go程序调用,都需要经过gorm的封装,来避免一些安全问题。同时也可以规避一些低级错误。还需要了解gen怎么使用,将MySQL库的定义自动生成为orm代码。


还要好好学习一下MySQL的用法,这边花了一周看完了《MySQL必知必会》,然后去leetcode刷题。国庆节刷了大概80道MySQL的题目,很爽。从简单的查询,到连接、子查询、分组、聚合,再到比较复杂的窗口函数、CTE全刷了个遍,刷上瘾了。


接着就可以去看项目代码了,这一部分还是蛮折腾的,对于新人来说。本身阅读别人的代码,对于很多开发者来说就是一件痛苦的事情,何况是去阅读自己不熟悉的语言的别人的代码。


我最近接手的一个半废弃项目,就很离谱。开发者有的已经离职了,提交记录是三四年前的。PRD也找不全,到今天权限还没拿齐,明天再找人问问。这边可能是真的上下文就是要丢失的,没法找了。只能自己创建一个新的文档,把相关重点补充一下。


明天找一下这个项目的用户,演示一下怎么使用,然后根据对用法的理解进行开发……


收获


新鲜感


一直写前端真的有点腻,虽然现在技术还在迭代,但也万变不离其宗。而且真的是有点过分内卷了,像一个打包工具从webpack -> esbuild -> vite -> turbopack -> rspack。不可否认的是这些开发者的努力,为前端生态的繁荣做出了贡献。但对于很多业务来说,其实并没有太大的性能问题,对于这部分项目来说升级的收益很小。比如云服务的控制台,基本都是微前端架构,每个前端项目都非常小,就算用webpack热更新也不会慢。而且webpack使用下来是最稳定的,我现在的项目用的是vite,会存在样式引入顺序的问题,导致开发环境和生产环境的页面区别。


后端技术栈不管好还是不好,反正对我来说是很新鲜的。虽然我之前Python、Go也都用过,也用Python写出了完整的项目,但论企业级开发这算第一次~各方面都更正规


Go写起来很舒服,虽然写同样的需求代码量比TypeScript多一堆……习惯之后还是可以感受到Go的简单与安心。Go打包就没那么多事,你本地怎么跑服务器那边就怎么跑,不像前端可能碰到一堆兼容性问题。


真的有学到


我前几个月买了掘金大佬神说要有光的小课《Nest 通关秘籍》,据我了解我的几个同事也买了。不过我没坚持下来,因为工作上实在是没有使用到Nest的机会。我无法接受学了两三个月却无法在工作里做出产出的感觉。


但这一次学了可以立马去用,可以在工作中得到检验,可以接受用户的检验。我就会得到价值感与成就感。


而且字节的Go基建在我认知里很牛叉,一家以Go为主的大厂,养得起很多做基建的人。比如张金柱Gorm的作者,竟然就在字节,我前几天知道感觉牛人竟然……


Go的学习资料也非常多,还有很多实战的,真的像突然打开了新世界的大门~


与业务更近,以及更平和的心态


如果我没有学后端,会在“前端已死”的氛围里胡思乱想,忽略了前端的业务价值,前端依旧是很重要的岗位。让后端来写前端不是不行,但只有分工才能达到最高的效率。对于一个正常的业务团队来说,也完全没必要让后端去硬写前端,好几个后端配一个前端,也不是什么事。


就我目前的工作经验来看,后端可以和业务的使用者更近的对接。我们这里的后端开发会和非常多用户对接需求,了解他们的真实使用场景,思考他们背后的需求,可能还能弥补一下产品思考上的不周。和用户对齐数据传递、转换、存储、查询、以及需要不需要定时任务等等,这些后端会去负责。


而前端负责最终的交互,基本可以不用碰到使用者,基本上只需要根据后端给的接口文档,调用接口把内容渲染在表格上即可。碰到用户提反馈一般在于,加载慢(往往是数据请求很慢,但是用户会觉得是前端的问题)、交互不满意(交互美不美真的是一个很难量化的问题,按理说这属于UI的绩效)、数据请求失败(前后端接口对齐虽然体验越来越好,但是开发阶段经常改动还是免不了,最后导致前后端没有同步)。


之前开周会的时候,我基本上说不上什么话。一个是刚转岗,确实不熟。另一个是前端半游离于业务的状态,单纯的把接口内容渲染出来也很难有什么思考,导致开会比较尴尬。基本是后端在谈解决了什么oncall,解决了什么技术问题,有什么业务建设的思考等等。


这次看了别人代码之后非常期盼未来能独立owner一个方向,享受闭环一个小功能的乐趣。


职业安全感


我学的这项技能能够立马投入到工作中进行自我检验,因此我相信自己学的是“有效技能”。我理解的无效技能指学了用不上,然后忘了,花了很多时间精力最后不升职不加薪。之前看李运华大佬的网课《大厂晋升指南》里面有提到,有人花了半年把编译原理这个看似非常重要的计算机基础课学的很扎实,但因为业务不需要,不产生业务价值,也不可能获得提拔的机会。


其实内部全栈化我的理解,还有一个原因,那就是灵活调度。现在这个背景下,老板更希望用有限的人力去做更多事情。有些业务前端过剩了,但是缺后端,这个时候如果直接去招后端,一方面增加成本,再就是没有解决剩的前端,反之也是。在盘点hc的时候就容易出现调整。


多学一些有效技能,提高解决问题的深度、广度,让自己更值钱。我想不管是什么职能,最终都要回归到为业务服务的目标上。


End


写到这里,我依旧在转全栈的路上,只是想给自己一个阶段性的答案。


脱离舒适圈,进入拉伸区,需要付出,需要勇气,也需要把握机遇。给自己多一种可能,去做,去挑战自己不会的。我相信他山之石可以攻玉,越往深处走,就越能触类旁通。


作者:程序员Alvin
来源:juejin.cn/post/7287426666417700919
收起阅读 »

聊聊2023年怎么入局小游戏赛道?

web
一、微信小游戏赛道发展史 第一阶段:轻度试水期,2017~2019年 微信小游戏于2017年底上线,初期以轻度休闲为主,例如棋牌、合成消除以及益智相关游戏类型。一是开发门槛不高,产品可以快速上线; 二是大部分厂商并无计划投入过多资金,仅试水。在变现方式上,极大...
继续阅读 »

一、微信小游戏赛道发展史


第一阶段:轻度试水期,2017~2019年


微信小游戏于2017年底上线,初期以轻度休闲为主,例如棋牌、合成消除以及益智相关游戏类型。一是开发门槛不高,产品可以快速上线;


二是大部分厂商并无计划投入过多资金,仅试水。在变现方式上,极大部分以IAA为主。


第二阶段:官方孵化期,2019~2021年


2019年官方推出“游戏优选计划",为符合标准的产品提供全生命周期服务,包括前期产品的立项和调优,以及后期的增长、变现等。


20050414514227.jpg


出现了一批《三国全明星》、《房东模拟器》、《乌冬的旅店》等这样的精品游戏。


第三阶段:快速爆发期,2022年至今


在官方鼓励精品化下,手游大厂开始进入,产品逐渐开始偏向中重度化。三国、仙侠、神话、西游以及传奇等传统中重度游戏占比逐渐加大。


全流量拓展投放,库存近百亿。腾讯全域流量、字节系、快手、百度、B站等基本全部渠道均可进行买量,真正进入前所未有的爆发期!


WechatIMG3428.jpg


二、该赛道持续高增长的原因


1、小游戏的链路相比于APP更加顺畅,无需下载,点击即玩。游戏买量中用户损失最大的部分就是“点击-下载-激活"。而在小游戏的链路中,用户可一键拉起微信直达小游戏登录页面,无需等待,导流效率极高。

2、微信生态提供的统一的实名认证底层能力。

3、小游戏链路可以绕开IOS的IDFA获取率不足的问题,实现IOS平台的高效精准归因。

4、各大游戏开发引擎特别是unity对小游戏平台的优化和适配能力提升。

5、顺畅的微信支付链路。

6、高效开放的社交裂变自然流量来源和社群运营能力。


三、小游戏和app游戏的买量核心差异


1、买量技术框架


小游戏在多数广告平台的技术链路都是从H5改进而来的。APP付费游戏在安卓常见的SDK数据上报在小游戏链路因为无法进行分包而彻底被API上报所取代。


API上报不同于SDK上报,有着成熟且空间巨大的广告主自主策略优化玩法。


2、买量目标


小游戏买首次付费、每次付费的比例要高于买 ROI。这一点和APP游戏也有明显不同,小游戏品类分散,人群宽泛且行业刚起步缺乏历史数据,对广告系统来说ROI买量的达成难度要高,效果相对不稳定。


3、素材打法


APP游戏大盘消耗以重度为主,素材中洗重度用户的元素较多;小游戏则是轻中度玩法为主,素材多面向泛人群,更注重对真实核心玩法的表现。


四、广告买量为什么在小游戏赛道中很重要


1、买量红利巨大,再好的产品都要靠买量宣发


微信小游戏链路在转化率上有着明显的优势。这会让小游戏产品在相同的前端出价上,要比同类型的APP产品买量竞争力更强。


而小游戏的研发成本并不算高,一旦跑出一款好产品,跟进者众多。在产品同质化比较严重时,快速买量拿量能力就决定了产品和业务的规模,除非大家有信心做出一款不怕被抄的爆品中的精品。


2、技术能力及格不难,做好很难


小游戏的买量技术相关的问题,如果只想将就用,可能一两个研发简单做一个月就能做到跑通。


但是如果想把买量技术能力做完善,这里依然有很大的门槛,而且会成为拉开规模差距的核心能力之一。这里我们给出几个细节,篇幅原因不具体展开。


归因方式


不同于APP生态已经比较成熟统一的设备ID和IP+ UA模糊匹配,小游戏链路因为微信生态、平台能力和开发成本不同,在不同平台存在多种归因方式,主要有平台自采集,启动参数,监测匹配等。


有效触点


因为小游戏不用去应用商店或者落地页下载,因而看广告但是不直接点击,而是选择去微信自己搜索启动的流量占比要高一些。为了适应这一情况,有些媒体平台会选择在小游戏链路将之前 APP的默认有效触点前置到播放或者视频浏览上。这里会让监测归因方式需要处理的数据提升两个数量级,对归因触点识别的难度也会加大。


效果衡量


因为支付通道的原因,腾讯系的平台和小游戏后台都只能收集安卓的付费数据,不能收集ios的数据,导致IAP类型的产品追踪完整ROI需要自建中台或者采买三方,打通归因和付费埋点数据。


数据口径


因为数据 采集来源不同,时间统计口径不同,小游戏链路下数据分析对运营和投放人员有着较高的要求,需要科学成熟的数据分析工具作为辅助。


3、渠道分散且需要掌控节奏


因为小游戏更为顺畅的用户链路,导致其转化率要比APP链路更高。因此小游戏在一些腰部平台甚至尾部平台都能有很好的跑量能力。APP游戏很多规模不大的产品可能只需要在巨量、腾讯和快手进行买量,现在小游戏完全可以尝试在百度信息流、B站、微博甚至是知乎等平台进行买量。


除了大家熟知的流量平台以外,长尾流量渠道往往是很多小游戏能闷声发财的致胜法宝。比如:陌陌、番茄、书旗等具有大量用户流量的非主流流量平台,一方面这些流量渠道取决于发行商的商务能力,另一方面也需要具备相应的技术能力。以业内新晋的小游戏发行技术 FinClip 来说,以嵌入SDK的方式,就可以让任何APP流量平台具备运行微信小游戏的能力。这意味着,小游戏在平台无需做任何跳转,用户转化链路降到最短。当然,腰尾部流量平台对小游戏在落地页资产、微信数据授权、链路技术支持等方面都还不是完全成熟,还属于比较小众的渠道方式。


小游戏发行领域,达人营销和自然裂变也是重要的渠道手段。通过合适的技术手段,达人营销和裂变也可以做到精准的效果追踪和按效果付费。


五、怎么入局小游戏赛道?


小游戏=变现方式游戏品质玩法受众裂变运营买量能力


以IAP或者混合变现的形式入局成功率会更大一些。


游戏品质主要和研发成本正相关:



  • 50万成本以下的小游戏往往因为玩法过于休闲,长线留存天花板低,美术品质不够,同质化竞争过于严重等原因导致很难获得预期的规模。

  • 200万成本以上的游戏又会因为试错成本太高,研发周期过长,不够紧跟市场热点等原因不被看好。

  • 因此,一般推荐50万到200万的成本,通过自有产研团队从APP转型,或者与稳定合作CP定制的方式获取第一款试水的产品。


具体的玩法和受众:



  • 一些在APP赛道被验证的轻中度的合成、抽卡和挂机类玩法都是在小游戏领域被广泛看好证的。

  • 在APP受限于受众规模小和付费渗透率低的小众玩法,如女性向Q萌养成,解密等玩法都有着亮眼的表现。

  • 整个小游戏的生态从开发者侧也更偏向于中长尾,多种垂直品类共存发展的趋势。


小游戏有着顺畅的玩家加群和裂变分享路径:



  • 持续运营私域群流量可以显著拉升核心用户的留存活跃,配合节日礼包等活动也可以提升付费率。

  • 小游戏无需应用商店下载,也不会有H5官网下载被微信拦截的情况,配合一些魔性和话题性的分享引导,很容易在已有一定用户规模的前提下实现比APP更快的自然增长,让用户规模更上一层。


作者:Finbird
来源:juejin.cn/post/7287494827701682176
收起阅读 »

也谈一下 30+ 程序员的出路

前言 前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。 从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了 大量的人面临这个问题:大龄程序员就业竞争力差,未...
继续阅读 »

前言


前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。


3.png


从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了


大量的人面临这个问题:大龄程序员就业竞争力差,未来该如何安身立命?


先说我个人的看法:



  • 除非你有其他更好的资源,否则没有更好的出路

  • 认真搞技术,保持技术能力,你大概率不会失业(至少外包还在招人,外包也不少挣...)


考公之我见


如果真的上岸了,极大概率不会失业,这是最大的优势。


有优势肯定也有劣势,要考虑全面。凡事都符合能量守恒定律。


你得到什么,你就得付出什么。或者你爸爸、爷爷提前付出为你过了,或者你儿子、孙子到最后为你买单。


任何一个企业、单位,无论什么形式,无论效率高低,总是需要人干活的,甚至有很多脏活累活。


你有依靠当然好。但你如果孤零零的进去,这些活你猜会是谁干?


什么,努力就一定能有收获?—— 对,肯定有收货。但收件人不一定是谁。(也符合能量守恒定律)


转岗,转什么?


去干产品经理,那不跟程序员一样吗?只是不写代码了而已。文档,不一定就比代码好写。


努力晋升转管理岗,那得看公司有没有坑。当下环境中,公司业务不增长的话,也不可能多出管理岗位。


其他没啥可转的岗位了,总不能转岗做 HR 吧~ 木讷的程序员也干不了 HR 。


副业,红利期早已过去


做自媒体,做讲师,红利期早就过去了。我去年开始在某音上做小视频,到现在也就积累不到 2000 粉丝,播放量非常少。


接外包,这得看你本事了。这不单单是一个技术活,你这是一个人干了一个公司所有角色的活:推广、需求、解决方案、开发、测试、部署、维护、升级…


不过,虽然现在副业情况不好,但我还是建议大家,在业余时候多输出技术内容(博客、视频、开源等),看能否积累一些流量和粉丝。以后说不定副业情况会好起来,到时候你临时抱佛脚可来不及。


回归二线城市


相比于一线城市的互联网公司,二线城市对于年龄的容忍度更高一些。我认识很多 35-40 岁的人,在二线城市做开发工作也非常稳定。


在二线城市最好能找一个传统行业的软件公司,如做医疗,财务,税务,制造业等软件产品的。这种软件的特点是,不要求有多么高精尖的技术,也不要求什么大数据、极致性能,它对业务流程和功能的依赖更多一些。你只要能尽快把业务功能熟悉起来(挺多专业知识,不是那么容易的),你在公司就基本稳定了,不用去卷技术。


二线城市是非常适合安家定居的。房价便宜,生活节奏慢 —— 当然,工资也会相对低一些。


另外,回归二线城市也不是说走就走的,你得提前准备、规划,把路铺好。


总结


当前互联网、软件行业,已经没有了前些年的增量,但依然有大量的存量,依然需要大量技术人员去维护当前的系统和功能。


所以别总想着去转行(除非有其他好的资源),其他行业也不会留着好位子等着你。有那个精力多给自己充充电,有竞争力是不会失业的。只要互联网和软件行业还存在,就一直需要前端工作。


作者:前端双越老师
来源:juejin.cn/post/7287020579831267362
收起阅读 »

iOS面试题目——hook block(2)

// 题目:实现下面的函数,将 block 的实现修改成打印所有入参,并调用原始实现//// 例如:// void(^block)(int a, NSString *b) = ^(int a, NSString *b){// NSLog(@"blo...
继续阅读 »
// 题目:实现下面的函数,将 block 的实现修改成打印所有入参,并调用原始实现
//
// 例如:
// void(^block)(int a, NSString *b) = ^(int a, NSString *b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,@"aaa");
// 这里输出 "123,aaa" 和 "block invoke"

分析:这个题目其实和题目一的本质是一样的,都是替换block的实现(即Hook Block),不过,相比较于题目一,这个题的侧重点在于:1、打印所有入参;2、调用原实现。针对这两个问题,我们逐一解析。

1、打印所有入参

对于已知参数个数和参数类型的block,要实现这个,其实并不难,只需要我们再声明替换函数的时候和block的参数对齐即可:

//这里要注意的是,第一个参数必须声明为block本身。
//针对 void(^block)(int a, NSString *b) ,我们可以将函数声明为如下形式:
void replace_bloke2(id block, int a, NSString *b);
2、调用原实现
上一个题目中,我们仅仅是将invoke的值替换了,也就是说我们舍弃了invoke原本的函数指针地址,即原本的实现;如果我们全局变量,将其先存储,再进行替换,然后在replace_bloke2函数中调用,是否就达到了目的呢?
//声明一个函数指针,用来存储invoke的值
void(*origin_blockInvoke2)(id block,int a,NSString *b);

void replace_bloke2(id block, int a, NSString *b) {
NSLog(@"%d,%@",a,b);
origin_blockInvoke2(block,a,b);
}

void HookBlockToPrintArguments(id block){

// 解析 block 为 struct Block_layout 结构体
struct Block_layout *layout = (__bridge struct Block_layout *)block;
// 修改内存属性
vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
vm_size_t vmsize = 0;
mach_port_t object = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
if (ret != KERN_SUCCESS) {
NSLog(@"获取失败");
return;
}
vm_prot_t protection = info.protection;
// 判断内存是否可写
if ((protection&VM_PROT_WRITE) == 0) {
// 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
return;
}
}
// 保存原来的invoke
origin_blockInvoke2 = (void *)layout->invoke;
layout->invoke = (uintptr_t)replace_bloke2;
}



收起阅读 »

Xcode 15下,包含个推的项目运行时崩溃的处理办法

升级到Xcode15后,部分包含个推的项目在iOS17以下的系统版本运行时,会出现崩溃,由于崩溃在个推Framework内部,无法定位到具体代码,经过和个推官方沟通,确认问题是项目支持的最低版本问题。需要将项目的最低版本修改为iOS12.0或更高具体修改位置:...
继续阅读 »

升级到Xcode15后,部分包含个推的项目在iOS17以下的系统版本运行时,会出现崩溃,由于崩溃在个推Framework内部,无法定位到具体代码,经过和个推官方沟通,确认问题是项目支持的最低版本问题。

需要将项目的最低版本修改为iOS12.0或更高

具体修改位置:Target-General-Minimum Deployments-iOS 12.0



问题来源

收起阅读 »

聊聊陈旧的插件化

不长不短的职业生涯里,有一段搞插件化的经历,当时所在的团队也是行业里比较知名的最早搞插件化的团队之一。虽然理论上是使用方,但因为业务的需要,要把大插件拆成更小颗粒度的小插件,所以会比较深度的做源码级别的定制修改。 1 什么是插件化 插件化要解决的问题总的来说有...
继续阅读 »

不长不短的职业生涯里,有一段搞插件化的经历,当时所在的团队也是行业里比较知名的最早搞插件化的团队之一。虽然理论上是使用方,但因为业务的需要,要把大插件拆成更小颗粒度的小插件,所以会比较深度的做源码级别的定制修改。


1 什么是插件化


插件化要解决的问题总的来说有三个方面



  • 动态性:也就是更新功能无需依赖发版,动态下发应用的新功能。

  • 包体积:一个巨型的APP功能模块很多,包体积自然小不了。插件化可以把不同的功能模块制作成单独的插件,按需下载应用,有效控制包体积。同时,对于一些“边缘功能”,对于每个用户个体来说可能,使用不到,插件化按需下载的优势也就体现出来了。

  • 热修复: 对于线上的bug,利用插件化技术的动态性,可以在不发版的情况下实现热修。


说了这么多,简单的讲,插件化就是不依赖于发版使APP具备动态更新的能力。业界也管这个叫【免安装】。


2 怎么实现插件化


Android要实现插件化就是要解决三方面的问题。



  • 代码动态化。

  • 组件插件化。

  • 资源的插件化


2.1 代码(类)的动态化


正常情况下,程序员写代码 -> 打包APK -> 发版 -> 用户安装、使用。


现在要解决这样一个问题,不重新安装app的情况下,如何让程序员编写的代码在已经被用户安装了的APP上跑起来。


Java语言的特性天然具备动态性。ClassLoader可以动态加载类文件(.class ,.dex,.jar)。Android插件化的基石之一在于此。


编写代码然后打包成dex或者apk,然后App获取到对应的类文件,利用classLoader动态加载类,创建对象,利用反射就可以调用类/对象的方法,获取类/对象的属性。


让代码能够动态的下发动态的执行。


当然这只是一个最基本的原理,里面还有涉及到很多的细节,比如



  • 不同插件是相同classloader加载还是不同classloader加载。

  • 宿主APP与插件APP是否是使用同一ClassLoader。

  • 如果涉及到不同ClassLoader,加载的类如何进行通信。


对于这些问题的解决,不同的插件化框架也有不同的方案,各有利弊,如果大家感兴趣,后续会单独开篇详细的聊一聊。


2.2 组件插件化


上一节,说到我们利用classloader的动态加载机制配合反射,可以让代码动态化起来。有一个很重要的问题,Android系统中Activity、Service等组件是系统组件。他的特点是系统调用系统管理的。比如Activity著名的那些回调函数,都是System_Server进程那挂了号,对于系统进程来讲是有感知的。另外一方面我们每创建一个Activity组件都要在Manifest.xm里注册上,这个动作的意义就是让系统知道我们的应用里有哪些组件。相应的AMS都会对注册进行校验。


如果我们动态的下发一个Activity类,是不能像正常的类一样运行起来。如何实现组件的插件化?


简单的说,就是占坑+转掉.


既然不能动态的在Manifest.xml清单文件里动态的注册,但是可以在Manifest里预埋几个等用的时候拿出来用,解决注册问题。


既然生命周期函数都是系统调用的,不能我们触发,我们可以实现转调。简单的说启动一个插件Activty的时候,其实先启动占坑的Activity -> 加载创建插件Activity(当作一个普通的类对象) -> 占坑的Activity转调插件Activity。


关于组件的插件化大概思想如此,具体实现上也不同框架也会有不同的方案,hook的点也不一样。Replugin hook了ClassLoader,使得在加载占坑activity的时候替换为了加载插件的Activity。VirtualApk hook 了Instrumentation来模拟系统启动Activity等。


当然真正实现起来还是有一些问题需要解决,比如多进程的实现、不同启动模式的实现等。


2.3 资源的插件化


正常开发我们使用诸如 R.xx.x的方式索引资源,但是如果我们在一个插件的Activity中如果不做处理,直接使用该方式去是索引不到资源的。因为此时是在宿主的Resource中查找插件的资源。


插件Apk中的图片,layout等资源也是需要进行插件化处理,使得能够正确的访问到插件中的资源。资源的插件化核心是对插件APK中的Resource对象实例化,这样通过Resource对像代码中可能访问到插件的资源。


实现 的方式主要有两种,




  • 一种是把插件中的资源合并到宿主中,这样使用宿主的Resource对象既能访问到插件的资源也能访问到宿主的资源。这种方式也会带来一个比较头疼的问题,资源冲突问题,通常的方案是id固定,这里就不做展开。




  • 另外一种方案为插件创建单独的Resource对象。




packageArchiveInfo.applicationInfo.publicSourceDir = archiveFilePath    
packageArchiveInfo.applicationInfo.sourceDir = archiveFilePath
val resource = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo)

3 其他


经过以上,可以实现一个插件化最核心的东西,除此之外,还需要做



  • 插件的安装,插件apk的解压,释放。

  • 插件的注册,使得宿主和其他插件能够发现目标插件并与之通信。

  • 试想这样一种场景,宿主中已经依赖了某个library(A),我们插件中也依赖A。作为插件中A的这个依赖是不是就是重复的,如何解决这一个问题。

  • 编译器插件的生成。


4 结


从比较宏观的视角聊了下,插件化解决的问题,以及实现一个插件化大概的主体思路,是很粗颗粒度的描述。每一部分单独拆出来去分析研究会有很多东西挖掘出来。也在文中埋了一些坑,今后视具体情况再做分享。


thx 😊


作者:Drummor
来源:juejin.cn/post/7283087306604314636
收起阅读 »

喝了100杯酱香拿铁,我顿悟了锁的精髓

大家好,我是哪吒。 上一篇提到了锁粒度的问题,使用“越细粒度的锁越好”,真的是这样吗?会不会产生一些其它问题? 先说结论,可能会产生死锁问题。 下面还是以购买酱香拿铁为例: 1、定义咖啡实体类Coffee @Data public class Coffee ...
继续阅读 »

大家好,我是哪吒。


上一篇提到了锁粒度的问题,使用“越细粒度的锁越好”,真的是这样吗?会不会产生一些其它问题?


先说结论,可能会产生死锁问题。


下面还是以购买酱香拿铁为例:



1、定义咖啡实体类Coffee


@Data
public class Coffee {
// 酱香拿铁
private String name;

// 库存
public Integer inventory;

public ReentrantLock lock = new ReentrantLock();
}

2、初始化数据


private static List<Coffee> coffeeList = generateCoffee();

public static List<Coffee> generateCoffee(){
List<Coffee> coffeeList = new ArrayList<>();
coffeeList.add(new Coffee("酱香拿铁1", 100));
coffeeList.add(new Coffee("酱香拿铁2", 100));
coffeeList.add(new Coffee("酱香拿铁3", 100));
coffeeList.add(new Coffee("酱香拿铁4", 100));
coffeeList.add(new Coffee("酱香拿铁5", 100));
return coffeeList;
}

3、随机获取n杯咖啡


// 随机获取n杯咖啡
private static List<Coffee> getCoffees(int n) {
if(n >= coffeeList.size()){
return coffeeList;
}

List<Coffee> randomList = Stream.iterate(RandomUtils.nextInt(n), i -> RandomUtils.nextInt(coffeeList.size()))
.distinct()// 去重
.map(coffeeList::get)// 跟据上面取得的下标获取咖啡
.limit(n)// 截取前面 需要随机获取的咖啡
.collect(Collectors.toList());
return randomList;
}

4、购买咖啡


private static boolean buyCoffees(List<Coffee> coffees) {
//存放所有获得的锁
List<ReentrantLock> locks = new ArrayList<>();
for (Coffee coffee : coffees) {
try {
// 获得锁3秒超时
if (coffee.lock.tryLock(3, TimeUnit.SECONDS)) {
// 拿到锁之后,扣减咖啡库存
locks.add(coffee.lock);
coffeeList = coffeeList.stream().map(x -> {
// 购买了哪个,就减哪个
if (coffee.getName().equals(x.getName())) {
x.inventory--;
}
return x;
}).collect(Collectors.toList());
} else {
locks.forEach(ReentrantLock::unlock);
return false;
}
} catch (InterruptedException e) {
}
}
locks.forEach(ReentrantLock::unlock);
return true;
}

3、通过parallel并行流,购买100次酱香拿铁,一次买2杯,统计成功次数


public static void main(String[] args){
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 通过parallel并行流,购买100次酱香拿铁,一次买2杯,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Coffee> getCoffees = getCoffees(2);
//Collections.sort(getCoffees, Comparator.comparing(Coffee::getName));
return buyCoffees(getCoffees);
})
.filter(result -> result)
.count();

stopWatch.stop();
System.out.println("成功次数:"+success);
System.out.println("方法耗时:"+stopWatch.getTotalTimeSeconds()+"秒");
for (Coffee coffee : coffeeList) {
System.out.println(coffee.getName()+"-剩余:"+coffee.getInventory()+"杯");
}
}


耗时有点久啊,20多秒。


数据对不对?



  • 酱香拿铁1卖了53杯;

  • 酱香拿铁2卖了57杯;

  • 酱香拿铁3卖了20杯;

  • 酱香拿铁4卖了22杯;

  • 酱香拿铁5卖了19杯;

  • 一共卖了171杯。


数量也对不上,应该卖掉200杯才对,哪里出问题了?


4、使用visualvm测一下:


果不其然,出问题了,产生了死锁。


线程 m 在等待的一个锁被线程 n 持有,线程 n 在等待的另一把锁被线程 m 持有。



  1. 比如美杜莎买了酱香拿铁1和酱香拿铁2,小医仙买了酱香拿铁2和酱香拿铁1;

  2. 美杜莎先获得了酱香拿铁1的锁,小医仙获得了酱香拿铁2的锁;

  3. 然后美杜莎和小医仙接下来要分别获取 酱香拿铁2 和 酱香拿铁1 的锁;

  4. 这个时候锁已经被对方获取了,只能相互等待一直到 3 秒超时。



5、如何解决呢?


让大家都先拿一样的酱香拿铁不就好了。让所有线程都先获取酱香拿铁1的锁,然后再获取酱香拿铁2的锁,这样就不会出问题了。


也就是在随机获取n杯咖啡后,对其进行排序即可。


// 通过parallel并行流,购买100次酱香拿铁,一次买2杯,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Coffee> getCoffees = getCoffees(2);
// 根据咖啡名称进行排序
Collections.sort(getCoffees, Comparator.comparing(Coffee::getName));
return buyCoffees(getCoffees);
})
.filter(result -> result)
.count();

6、再测试一下



  • 成功次数100;

  • 咖啡卖掉了200杯,数量也对得上。

  • 代码执行速度也得到了质的飞跃,因为不用没有循环等待锁的时间了。



看来真的不是越细粒度的锁越好,真的会产生死锁问题。通过对酱香拿铁进行排序,解决了死锁问题,避免循环等待,效率也得到了提升。


作者:哪吒编程
来源:juejin.cn/post/7287429638020005944
收起阅读 »

Kotlin中 四个提升逼格的关键字你都会了吗?

开篇看结论 let let扩展函数的实际上是一个作用域函数,当你需要去定义一个变量在一个特定的作用域范围内,let函数的是一个不错的选择;let函数另一个作用就是可以避免写一些判断null的操作。 let函数的一般结构 object.let{ it.to...
继续阅读 »

开篇看结论


img


let


let扩展函数的实际上是一个作用域函数,当你需要去定义一个变量在一个特定的作用域范围内,let函数的是一个不错的选择;let函数另一个作用就是可以避免写一些判断null的操作。



  • let函数的一般结构


object.let{
it.todo()//在函数体内使用it替代object对象去访问其公有的属性和方法
...
}

//另一种用途 判断object为null的操作
object?.let{//表示object不为null的条件下,才会去执行let函数体
it.todo()
}


  • let函数的kotlin和Java转化


//kotlin

fun main(args: Array<String>) {
val result = "testLet".let {
println(it.length)
1000
}
println(result)
}

//java

public final class LetFunctionKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var2 = "testLet";
int var4 = var2.length();
System.out.println(var4);
int result = 1000;
System.out.println(result);
}
}


  • let函数使用前后的对比


mVideoPlayer?.setVideoView(activity.course_video_view)
mVideoPlayer?.setControllerView(activity.course_video_controller_view)
mVideoPlayer?.setCurtainView(activity.course_video_curtain_view)
------------------------------------------------------------------------------------------------------------------------------
mVideoPlayer?.let {
it.setVideoView(activity.course_video_view)
it.setControllerView(activity.course_video_controller_view)
it.setCurtainView(activity.course_video_curtain_view)
}


  • let函数适用的场景


场景一: 最常用的场景就是使用let函数处理需要针对一个可null的对象统一做判空处理。 场景二: 然后就是需要去明确一个变量所处特定的作用域范围内可以使用


with



  • with函数使用的一般结构


with(object){
//todo
}


  • with函数的kotlin和Java转化


//kotlin
fun main(args: Array<String>) {
val user = User("Kotlin", 1, "1111111")

val result = with(user) {
println("my name is $name, I am $age years old, my phone number is $phoneNum")
1000
}
println("result: $result")
}
------------------------------------------------------------------------------------------------------------------------------
//java
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
User user = new User("Kotlin", 1, "1111111");
String var4 = "my name is " + user.getName() + ", I am " + user.getAge() + " years old, my phone number is " + user.getPhoneNum();
System.out.println(var4);
int result = 1000;
String var3 = "result: " + result;
System.out.println(var3);
}


  • with函数使用前后的对比


override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
with(item){
holder.tvNewsTitle.text = StringUtils.trimToEmpty(titleEn)
holder.tvNewsSummary.text = StringUtils.trimToEmpty(summary)
holder.tvExtraInf.text = "难度:$gradeInfo | 单词数:$length | 读后感: $numReviews"
}
}
------------------------------------------------------------------------------------------------------------------------------
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
ArticleSnippet item = getItem(position);
if (item == null) {
return;
}
holder.tvNewsTitle.setText(StringUtils.trimToEmpty(item.titleEn));
holder.tvNewsSummary.setText(StringUtils.trimToEmpty(item.summary));
String gradeInfo = "难度:" + item.gradeInfo;
String wordCount = "单词数:" + item.length;
String reviewNum = "读后感:" + item.numReviews;
String extraInfo = gradeInfo + " | " + wordCount + " | " + reviewNum;
holder.tvExtraInfo.setText(extraInfo);
}


  • with函数的适用的场景 适用于调用同一个类的多个方法时,可以省去类名重复,直接调用类的方法即可,经常用于Android中RecyclerView中onBinderViewHolder中,数据model的属性映射到UI上


run



  • run函数使用的一般结构


object.run{
//todo
}


  • run函数的kotlin和Java转化


//java
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
User user = new User("Kotlin", 1, "1111111");
String var5 = "my name is " + user.getName() + ", I am " + user.getAge() + " years old, my phone number is " + user.getPhoneNum();
System.out.println(var5);
int result = 1000;
String var3 = "result: " + result;
System.out.println(var3);
}
------------------------------------------------------------------------------------------------------------------------------
//kotlin
fun main(args: Array<String>) {
val user = User("Kotlin", 1, "1111111")

val result = user.run {
println("my name is $name, I am $age years old, my phone number is $phoneNum")
1000
}
println("result: $result")
}


  • run函数使用前后对比


override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
with(item){
holder.tvNewsTitle.text = StringUtils.trimToEmpty(titleEn)
holder.tvNewsSummary.text = StringUtils.trimToEmpty(summary)
holder.tvExtraInf = "难度:$gradeInfo | 单词数:$length | 读后感: $numReviews"
...
}
}
// 使用后
override fun onBindViewHolder(holder: ViewHolder, position: Int){
getItem(position)?.run{
holder.tvNewsTitle.text = StringUtils.trimToEmpty(titleEn)
holder.tvNewsSummary.text = StringUtils.trimToEmpty(summary)
holder.tvExtraInf = "难度:$gradeInfo | 单词数:$length | 读后感: $numReviews"
...
}
}


  • run函数使用场景


适用于let,with函数任何场景。因为run函数是let,with两个函数结合体,准确来说它弥补了let函数在函数体内必须使用it参数替代对象,在run函数中可以像with函数一样可以省略,直接访问实例的公有属性和方法,另一方面它弥补了with函数传入对象判空问题,在run函数中可以像let函数一样做判空处理


apply



  • apply函数使用的一般结构


object.apply{
//todo
}


  • apply函数的kotlin和Java转化


//java
public final class ApplyFunctionKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
User user = new User("Kotlin", 1, "1111111");
String var5 = "my name is " + user.getName() + ", I am " + user.getAge() + " years old, my phone number is " + user.getPhoneNum();
System.out.println(var5);
String var3 = "result: " + user;
System.out.println(var3);
}
}

//kotlin
fun main(args: Array<String>) {
val user = User("Kotlin", 1, "1111111")
val result = user.apply {
println("my name is $name, I am $age years old, my phone number is $phoneNum")
1000
}
println("result: $result")
}


  • apply函数使用前后的对比


//使用前
mSheetDialogView = View.inflate(activity, R.layout.biz_exam_plan_layout_sheet_inner, null)
mSheetDialogView.course_comment_tv_label.paint.isFakeBoldText = true
mSheetDialogView.course_comment_tv_score.paint.isFakeBoldText = true
mSheetDialogView.course_comment_tv_cancel.paint.isFakeBoldText = true
mSheetDialogView.course_comment_tv_confirm.paint.isFakeBoldText = true
mSheetDialogView.course_comment_seek_bar.max = 10
mSheetDialogView.course_comment_seek_bar.progress = 0
//使用后
mSheetDialogView = View.inflate(activity, R.layout.biz_exam_plan_layout_sheet_inner, null).apply{
course_comment_tv_label.paint.isFakeBoldText = true
course_comment_tv_score.paint.isFakeBoldText = true
course_comment_tv_cancel.paint.isFakeBoldText = true
course_comment_tv_confirm.paint.isFakeBoldText = true
course_comment_seek_bar.max = 10
course_comment_seek_bar.progress = 0

}
//多级判空
if (mSectionMetaData == null || mSectionMetaData.questionnaire == null || mSectionMetaData.section == null) {
return;
}
if (mSectionMetaData.questionnaire.userProject != null) {
renderAnalysis();
return;
}
if (mSectionMetaData.section != null && !mSectionMetaData.section.sectionArticles.isEmpty()) {
fetchQuestionData();
return;
}

mSectionMetaData?.apply{
//mSectionMetaData不为空的时候操作mSectionMetaData
}?.questionnaire?.apply{
//questionnaire不为空的时候操作questionnaire
}?.section?.apply{
//section不为空的时候操作section
}?.sectionArticle?.apply{
//sectionArticle不为空的时候操作sectionArticle
}

also



  • also函数使用的一般结构


object.also{
//todo
}

复制



  • also函数编译后的class文件


//java
public final class AlsoFunctionKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var2 = "testLet";
int var4 = var2.length();
System.out.println(var4);
System.out.println(var2);
}
}
//kotlin
fun main(args: Array<String>) {
val result = "testLet".also {
println(it.length)
1000
}
println(result)
}


  • also函数的适用场景


适用于let函数的任何场景,also函数和let很像,只是唯一的不同点就是let函数最后的返回值是最后一行的返回值而also函数的返回值是返回当前的这个对象。一般可用于多个扩展函数链式调用


最后


如果你看到了这里,觉得文章写得不错就给个赞呗?


更多Android进阶指南 可以详细Vx关注公众号:Android老皮 解锁            《Android十大板块文档》


1.Android车载应用开发系统学习指南(附项目实战)


2.Android Framework学习指南,助力成为系统级开发高手


3.2023最新Android中高级面试题汇总+解析,告别零offer


4.企业级Android音视频开发学习路线+项目实战(附源码)


5.Android Jetpack从入门到精通,构建高质量UI界面


6.Flutter技术解析与实战,跨平台首要之选


7.Kotlin从入门到实战,全方面提升架构基础


8.高级Android插件化与组件化(含实战教程和源码)


9.Android 性能优化实战+360°全方面性能调优


10.Android零基础入门到精通,高手进阶之路


敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔


作者:花海blog
来源:juejin.cn/post/7282752001900494882
收起阅读 »

命运坎坷,奶奶逝去了

你虽远去,但我仍会带着你的行囊和灵魂继续前行。 入院始末 前年年末,那时候我家还在搞自建房,究其建房的原因,一方面是家里的老房子真的不能住了,一方面是我奶奶觉得没有面子,她经常说:“谁家都修了,只有我们家还是那座破烂的小平房,要不是你爸不争气我们家也不至于...
继续阅读 »

你虽远去,但我仍会带着你的行囊和灵魂继续前行。



入院始末


image.png
前年年末,那时候我家还在搞自建房,究其建房的原因,一方面是家里的老房子真的不能住了,一方面是我奶奶觉得没有面子,她经常说:“谁家都修了,只有我们家还是那座破烂的小平房,要不是你爸不争气我们家也不至于这样,你看看你几个叔叔都比我家好,曾经的时候我们家算是村里好的了,可现在却成了这样”,那时我也劝导她:“平安健康就好了,那个房子能住就行,不要在意那么多”,也许当时也是自己不想待在这个地方所以心里是不想建房的,后面在我奶奶一再啰嗦下,我爸妈终于决定建房在老家了。这也是她日思夜想的心愿,也算是我爸妈尚未完成的责任。


但是建房开始就不是很顺利,在前年过年前的时候我爸把我妈给他个几千块钱输个精光,因此我奶奶和他大吵了一架,从此时他俩就矛盾不断,我爸也是那种脾气急躁、嘴巴啰嗦的人,所以二人只要待在一块就会
吵闹,我爸经常对我奶奶饭菜指指点点,我奶奶总是说:“辛辛苦苦做的菜,有得吃就不错”。所以每次吃饭都能看见他们挂着个脸,我奶奶也经常给我打电话诉苦,说我爸我爸这么样,我爸也给我打电话说:“我奶奶对他太刻薄”,我也只好两头讨好,甚至严重的时候,只要我爸回家晚一点,我奶奶就会怀疑他去赌钱,他们俩就像不融的水火,也是时代的代沟。在建房期间,基本都是我爸在做主,但什么事情我奶奶总是喜欢
指指点点,我爸当然不会听他的,很多大事情上都是我爸一个人做主,也许我爸眼里她只是个老人而已。
虽然我爸有点好赌,但好在聪明伶俐,整个房子从下地基到后面的装修都有条有序的完成了,从此时开始
我奶奶的态度稍微好转了一些,在后续二三月里我奶奶搬进了新房子,我爸则继续出门打工,我奶奶总说我一个在家自由自在比他在时好在多了。那时总能看见她的笑容也许是在别的亲戚面前有了面子,别人在她面前夸房子弄得好的时候,她心里肯定乐开了花,毕竟那是她想了二十多年的事情。


今年年初,我因为一些原因辞职了当时准备前往上海找工作,但阴差阳错我被疫情困在了老家,当时因为我女朋友还有我奶奶亲人都太远的原因,我就选择在老家省会工作,那段时间算是我陪在她身边的最后一段时光。虽然奶奶年纪大了,但是做饭依旧是她的拿手好戏,我也很愧疚那段时间没做过几顿饭给她吃,也愧疚都忙着参加面试和陪女朋友了,也没有给她多少关心。有时候我甚至吃完晚饭就上楼和女朋友温电话煲去了,这也是我一直不能释怀的原因之一。


在贵阳工作几个月以后,贵州遭受疫情的影响,各个城市都被封了一段时间,我也从那时候开始没有再见到过她健康的样子,在疫情结束以后,我总想着下周去见他,可总是因为各种事情耽搁了,我总想着,还有时间还有时间,不怕这周不行就下周吧,但是哪有那么多个下周啊,直到一个加班的夜晚,我们正在开着需求评审会,商讨着接下来如何加班搞定这个项目,我小妹就给我打来了电话,“哥,奶奶突然晕倒了”,情急之下我让我大妹打车回了家,并找了车把我奶奶送到县医院,也许这也就是她噩梦的开始吧,我妹将她送到医院之后挂了一个急诊的号,医院开了心电图,胸部ct的单子,我当时也很奇怪,因为那几天我奶奶一直喊她胃疼,为什么不是腹部平扫,那时候我让我妹反复给医生强调我奶奶几天没吃东西,可是终究还是呕不过那个医生,他们在做各种检查之时,我正在做核酸,以便明早赶高铁回去,我很自责,没有劝得动早点她去医院,这也那个年代人的通病,生病总一直拖,一直要到严重才想着去医院。那天我也在焦急等待着结果,直到ct结果出来,那时我反复和我妹沟通确定病情,但是她也没有怎么说明白,彻夜未眠,直到核酸结果出来赶了最早一班的高铁。


受尽折磨


怀着愧疚和悲痛的心情我到了她跟前,看了一眼就不忍心再看下去了。迈开沉重的步伐找到负责的大夫,那个医生高高瘦瘦的看起来30岁左右的样子,他转身问我:“你就是xxx的家属是吧,她的情况是比较严重的不排除这几天情况恶化的情况”,我问道:“大夫是什么情况导致的”,他答道:“初步判断为胰腺炎也不排除是胸膜炎,需要住几天院观察一下。”,我也信以为真回头就去照顾我奶奶去了,那段时间,每天挂水到晚上三四点钟,基本没有进食,在这期间最让我心疼的事情就是她不想麻烦我们上厕所,每次都是快憋不住了才叫我和我妹扶她去厕所,并且经常说不要在这里守住了快去睡觉了、快去吃饭吧、叫个人来换哈你,虽然病重,但所幸的是她还能勉强走几步但是由于几天没进食精神已经不这么好了,但是思维都还是清晰了,好几次她都在给我说“小志,我们出院算了,在这边背都睡疼了”,我只能告诉她:“在挂两天水就可以出院了”,然后转头我又开始难受,不知道最后的结果会怎么样。直到第三天那个医生给他做了个腹部ct并且叫来他们主任,那个主任满脸愁容,并把我叫到一边,告诉我“你奶奶这个是胃穿孔引起的那个腹腔里面已经有一个很大的囊肿了”。


我紧张问道:“严重吗?胃穿孔好像不严重啊!为什么现在才查到?为什么之前就不给拍腹部ct”,那个主任也细心给我解释很多问题,就是做手术后大概率会好,医院每天都在大量做这个手术,在一番气愤的沟通之后,我急忙沟通我爸以及将我几个姑姑的机票也都定下来了,隔天我爸就到了,果然是我考虑欠缺了,他很大概率考虑的手术的风险,并考虑并发症等等并第一时间给我奶奶的后家打了电话,我爸说是避免他们有什么想法,后来再三确定之下,我们决定给她动手术,并告知我奶奶,但她怎么也没想到这只是痛苦的开始,那天我爸是早上到的,我们下午一点左右把我奶奶送进的手术室,其实当时我是往好的方向想的,因为我隔壁床很大的伤口都已经好得差不多了,三个小时过去了,虽然嘴上那么说但心里肯定多多少少都有一点担心的,毕竟手术过程毕竟会出现各种风险,当五点过的时候,她被推出手术室,那种印象也许我到死都不会忘记,浑身颤抖双目无神嘴里一直喊着:“冷!冷!牙齿一直在打颤,双手止不住的去动肚子上的伤口,我和我爸一人拉一只手不让她碰伤口”。


image.png
他这一生除了生孩子以外就没有经历过这么大的痛苦了吧,我看到那瞬间眼泪都已经湿润了眼眶,从二楼到九楼的距离没想到那么远她一路呻吟,直到从手术室送到病床上,那时候我奶奶还有120斤左右几个人才勉强把她搬到病床上,打开被子那瞬间我都呆了,没想到她身上那么多管子:胃管、腹腔管、左右侧胃管子、尿管、氧气管,这得多难受啊。但是不管是谁都没法替她分担任何痛苦,每个人的身体都是独立的,任何病痛来临时也只能接着。那几夜基本都是我和我几个亲人轮换着睡觉,夜夜高烧,输液经常到半夜,我们降温换药水,在一旁细心的照料她。终于体内的炎症终于快好了,从不发烧的时候开始,我们已经感到宽心,我也回去上班了。虽然还是经常一吐一整天,但是只要慢慢吃得了饭病情就会好转。


苍天无情


当我奶奶准备出院时,疫情放开了,病毒如大火般瞬间将你围住让你避无可避,当然我奶奶也不能幸免,,但是我奶奶发烧二天之后就痊愈,因为病情只剩保养在医院已无意义,做完一系列检查,他们就将我奶奶接回了家。原本以为我奶奶会慢慢好转,在接下来的一周里面,还是只能进食一点点,并且吃啥吐啥,每当吐起来扯到伤口,她总是抱怨说:“死了算了,着不住了",这让我们又心疼又无奈,我爸告诉我说:“吃了二十多年的药,那个医生说她的胃大部分都已经坏了,如果能好久慢慢好了如果不能就那人财两空”。再接我奶出院后的一周,我几个姑姑决然的离开的奶奶回到他们远嫁的地方,借口是各种各样的。我爸后来劝导说:“孝顺各凭良心,我们在她人生最后对得起自己,对得起她就行,至于你姑姑他们有自己的家庭不必强求”。当然我心里还是怨恨他们的。在之后的两天我奶奶喊的胸疼,我一下心就凉了,当时我就让我爸把我奶奶送到我们镇的医院,一拍ct肺部感染,还是大意了我奶奶再次住进医院,并且因为疫情的冲击县医院的病床都已被占用,只剩我们镇的医院能勉强用了下。我奶奶当时住进医院的情绪是非常差的,因为她知道她的女儿们抛下了她。


在心理和病痛的折磨下,她的情况越来越差,而我请了长假并带上我女朋友准备看她,我妈说她听到这个消息,那天早上吃了二大碗粥,我心里一下就酸了,泪水忍不住流下来,直到我再次看到她,整个人都瘦了一圈,在医院一直挂蛋白葡萄糖维持营养。在年前几天我们决定接她回家过年,再接她回家之后,我家各路亲戚都来看了她,也许他们看来我奶奶已经不长久了。写到这我已经写不下去了,接下来的种种我都让我悲痛不已。


关于她的回忆


WX20230201-142343@2x.png
在悲痛之中,我一次又一次回想起那些日子。我奶奶是50年代出生的人,也是经受苦难最多的一代,在我印象中她小的时候好像很少吃饱饭,经常都是吃米糠什么的,偶尔过年才能吃一顿好的。我奶奶是一个非常有主见并且胆子大的人,在我很小的时候就敢带着我走南闯北,虽说她只上过一年级但是记忆超群只要去过一遍的地方都能记得住,江苏、浙江、广州、贵阳、四川都留下过她的足迹,在我五岁那年我和她一起去江苏南通,到一处火车站的地方,她把单独丢在一个座位旁边并放了一包瓜子,后来被人当着我的面偷走了,因为这件事情一直嘲笑我好多年。


我小的时候,只有别家有的本地美食,我们家从来不缺,也许那时都是吃的回忆吧。还记得那时我们每逢赶集你都会买一堆好吃的回来,每次赶集的傍晚,我和大妹都会蹲在门口等你回来;那时她在一旁做米酒而我熟睡;那时她在炉火在熬油渣而我被刚出锅的油渣烫伤在一旁哭泣;那时她在秋腊肉我在一旁偷吃、那时她总是笑我包不好汤圆,然而我直到现在也包不好汤圆,我还是喜欢偷吃腊肉,我依旧会在米酒桌上睡着可是曾经的你已经不在了。但是欢乐之余,还是少不了你的鞭策,每当很晚回家你总是竹条早早就准备好了。记得有一次我在路上游玩被大奶哄骗去吃酒席,再吃完酒席之后都已经很晚,再快到家的山坡上我大奶早就溜之大吉,而我看到我爷爷提着的葵花杆我早就双腿发软,我当场挨了几棍,之后我让我去找我外面找我的奶奶,当我来到我某家亲戚家里后又被竹条一顿恶打。现在想想还是很好笑的,但是你老二早已不在人间。


长大以后,每次回家都给你诉说各种糟心或者开心的事情,你也不厌其烦听完,就像小时候我生病在家
要写作文,你和我有说有笑,给我出的一些主意。虽然你已经逝去,但你永远活在我的心里。前二年你自己
挑了下葬的地方,那时我总以为生死对你来说还是很久远的,但是你永远不知道明天和意外哪个先来。我们如你遗愿把你葬在那里。


image.png


最后的最后


image.png
还记得最后那天,我烧完洋芋,在你床前吃时,你表现很有兴趣,你告诉我:“你们在吃啥,给我吃点嘛”,我知道那时你已经有点意识模糊了,我把它放到你鼻前说到:“给你吃点好,这个软的”,我用勺子挖了一些放到她干涸的嘴皮前,她轻轻一抿,我问道好吃吗?她笑着说:“好吃,要是有点辣椒面就好了”,当时听完我就忍不住哭了,她都多久没笑了,也许这个笑容是对她或说对我最大的宽慰。在晚一些时候,我守在她身边忍不住的流泪我一回想到以前总有各种借口不回家就感到愧疚,一想到她奄奄一息,一想到她的女儿们离他远去,我就忍不住的抽泣睡着好像听到了什么,用一只手缓缓伸出手指,指在她的脸上,告诉我们不必太悲伤。看完之后我的情绪彻底瓦解了,在大厅难过很久很久,直到第二天中午她还是没有撑到看到她那些狠心的女儿们。


她本身也是一个倔强不服输的人,病情初期她总是说:“不要担心,我熬过这个冬天,我就慢慢好了”,但随着病情的严重,她在和亲戚的对话中说道:“我可能要瘫”,我甚至不敢想象那种病痛与精神的双重折磨下她内心的恐惧,知道一段时间后,她说:“你们该准备后事可以准备了”,这时我不知道她是否已坦然的面对死亡,我直到现在都是愧疚的,我们应该在努力一点想办法给她找到大医院的床位,但我又怕她再次经受折磨。记得除夕那天,我带着我妹和我妈们问她还有没有什么对我们说的,只见她微微偏过头说了一句:“也没有什么说的”,那时我感觉心里空落落的,我想在我心里只有她才配得上慈母这个称号。在临终那个晚上她没能将我认错,我就知道我奶奶将离我远去,这两次经历让我此后的夜晚常常泪流满面。


我一直处于迷迷糊糊的状态,直到那些处理丧事的先生,开始敲锣打鼓,让我觉得那锣声震耳欲聋,仿佛在告诉我:“你奶奶再也回不来了,每当事情不可挽回,你才感叹时间匆匆。曾经的我以为都已是遗憾。”,我从来都是不信鬼神之说的,只是出于对生命的敬畏,我选择诚恳的参加各种仪式,以完成她生命这最后的仪式感。


现在想起那时她离开的场景,不痛苦也不开心,仿佛像是日出日落那样的平常,我也感受不到她的离去,
我感觉她在我心里还活着,也许下一秒在某个地方就能看到她。我不明白她对生命的理解是什么,我只知道
她这大半辈子都在为这个家奔波,她总是在负重前行;她总是在担心你吃不饿睡不好;她总是闲不住总是在忙碌。


每次回家的路都能回想到这些,也想起她送我离开家乡,这时她就在心里璀璨的活着。


你虽远去,但我仍会带着你的行囊和灵魂继续前行。


image.png


每日一题


出于尊重,就略过吧。


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

提升接口性能的39个方法,两万字总结,太全了!

为了更好评估后端接口性能,我们需要对不同行为的耗时进行比较。从上图可以看出,一个CPU周期少于1纳秒,而一次从北京到上海的跨地域访问可能需要约30毫秒。怎么计算跨地域耗时呢? 我们已知光在真空中传播,折射率为 1,其光速约为 c=30 万公里/秒,当光在其他...
继续阅读 »

image.png


为了更好评估后端接口性能,我们需要对不同行为的耗时进行比较。从上图可以看出,一个CPU周期少于1纳秒,而一次从北京到上海的跨地域访问可能需要约30毫秒。怎么计算跨地域耗时呢?



我们已知光在真空中传播,折射率为 1,其光速约为 c=30 万公里/秒,当光在其他介质里来面传播,其介质折射自率为 n,光在其中的速度就降为 v=c/n,光纤的材料是二氧化硅,其折射率 n 为 1.44 左右,计算延迟的时候,可以近似认为 1.5,我们通过计算可以得出光纤中的光传输速度近似为 v=c/1.5= 20 万公里/秒。




以北京和深圳为例,直线距离 1920 公里,接近 2000 公里,传输介质如果使用光纤光缆,那么延迟时间 t=L/v = 0.2 万公里/20 万公里/秒=10ms ,也就是说从北京到深圳拉一根 2000 公里的光缆,单纯的距离延迟就要 10ms ,实际上是没有这么长的光缆的,中间是需要通过基站来进行中继,并且当光功率损耗到一定值以后,需要通过转换器加强功率以后继续传输,这个中转也是要消耗时间的。另外数据包在网络中长距离传输的时候是会经过多次的封包和拆包,这个也会消耗时间。




综合考虑各种情况以后,以北京到深圳为例,总的公网延迟大约在 40ms 左右,北京到上海的公网延迟大约在 30ms,如果数据出国的话,延迟会更大,比如中国到美国,延迟一般在 150ms ~ 200ms 左右,因为要经过太平洋的海底光缆过去的。



如果让你进行后端接口的优化,你是首选优化代码行数?还是首选避免跨地域访问呢?


在评估接口性能时,我们需要首先找出最耗时的部分,并优化它,这样优化效果才会立竿见影。上图提供了一个很好的参考。


需要注意的是,上图中没有显示机房内网络的耗时。一次机房内网络的延迟(Ping)通常在1毫秒以内,相比跨地域网络延迟要少很多。


对于机房内的访问,Redis缓存的访问耗时通常在1-5毫秒之间,而数据库的主键索引访问耗时在5-15毫秒之间。当然,这两者最大的区别不仅仅在于耗时,而更重要的是它们在承受高并发访问方面的能力。Redis单机可以承受10万并发(往往瓶颈在网络带宽和CPU),而MySQL要考虑主从读写分离和分库分表,才能稳定支持5千并发以上的访问。


1. 优化前端接口


1.1 核心数据和非核心数据拆分为多个接口


我曾经对用户(会员)主页接口进行了优化,该接口返回的数据非常庞大。由于各个模块的数据都在同一个接口中,只要其中一部分数据的查询耗时较长,整体性能就会下降,导致接口的失败率增加,前端无法展示核心数据。这主要是因为核心数据和非核心数据没有进行隔离,耗时数据和非耗时数据没有分开。


对于庞大的接口,我们需要先梳理每个模块中数据的获取逻辑和性能情况,明确前端必须展示和重点关注的核心数据,并确保这些数据能够快速、稳定地响应给前端。而非核心的数据和性能较差的数据则可以拆分到另外的接口中,即使这些接口的失败率较高,对用户影响也不大。


这种优化方式除了能保证快速返回核心数据,也能提高稳定性。如果非核心数据故障,可以单独降级,不会影响核心数据展示,大大提高了稳定性。


1.2 前端并行调用多个接口


后端提供给前端的接口应保证能够独立调用,避免出现需要先调用A接口再调用B接口的情况。如果接口设计不合理,前端需要的总耗时将是A接口耗时与B接口耗时之和。相反,如果接口能够独立调用,总耗时将取决于A接口和B接口中耗时较长的那个。显然,后者的性能更优。


在A接口与B接口都依赖相同的公共数据的情况下,会导致重复查询。为了优化总耗时,重复查询是无法避免的,因此应着重优化公共数据的性能。


在代码设计层面,应封装每个模块的取值逻辑,避免A接口与B接口出现重复代码或拷贝代码的情况。


1.3 使用MD5加密,防篡改数据,减少重复校验


在提单接口中,需要校验用户对应商品的可见性、是否符合优惠活动规则以及是否可用对应的优惠券等内容。由于用户可能篡改报文来伪造提单请求,后端必须进行校验。然而,由于提单链路本身耗时较长,多次校验以上数据将大大增加接口的耗时。那么,是否可以不进行以上内容的校验呢?


是可以的。在用户提单页面,商品数据、优惠活动数据以及优惠券等数据都是预览接口校验过的。后端可以生成一个预览Token,并将预览结果存在缓存中,前端在提单接口中指定预览Token。后端将校验提单数据和预览数据是否一致,如果不一致,则说明用户伪造了请求。


为了避免预览数据占用过多的缓存空间,可以设置一个过期时间,例如预览数据在15分钟内不进行下单操作,则会自动失效。另外,还可以对关键数据进行MD5加密处理,加密后的数据只有64位,数据量大大减少。后端在提单接口中对关键数据进行MD5加密,并与缓存中的MD5值进行比对,如果不一致,则说明用户伪造了提单数据。


更详细请参考# 如何防止提单数据被篡改?


1.4 同步写接口改为异步写接口


在写接口耗时较高的情况下,可以采取将接口拆分为两步来优化性能。首先,第一步是接收请求并创建一个异步任务,然后将任务交给后端进行处理。第二步是前端轮训异步任务的执行结果,以获取最终结果。


通过将同步接口异步化,可以避免后端线程资源被长时间占用,并且可以避免浏览器和服务器的socket连接被长时间占用,从而提高系统的并发能力和稳定性。


此外,还可以在前端接口设置更长的轮训时间,以有效提高接口的成功率,降低同步接口超时失败的概率,提升系统的性能和用户体验。


1.5 页面静态化


在电商领域,商品详情页和活动详情页通常会有非常高的流量,特别是在秒杀场景或大促场景下,流量会更高。同时,商品详情页通常包含大量的信息,例如商品介绍、商品参数等,导致每次访问商品详情都需要访问后端接口,给后端接口带来很大的压力。


为了解决这个问题,可以考虑将商品详情页中不会变动的部分(如商品介绍、头图、商品参数等)静态化到html文件中,前端浏览器直接访问这些静态文件,而无需访问后端接口。这样做可以极大地减轻商品详情接口的查询压力。


然而,对于未上架的商品详情页、后台管理等页面,仍然需要查询商品详情接口来获取最新的信息。


页面静态化需要先使用模版工具例如Thymeleaf等,将商品详情数据渲染到Html文件,然后使用运维工具(rsync)将html文件同步到各个nginx机器。前端就可以访问对应的商品详情页。


当商品上下架状态变化时,将对应Html文件重新覆盖或置为失效。


1.6 不变资源访问CDN



CDN(内容分发网络)是一种分布式网络架构,它将网站的静态内容缓存在全球各地的服务器上,使用户能够从最近的服务器获取所需内容,从而加速用户访问。这样,用户不需要从原始服务器请求内容,可以减少因网络延迟导致的等待时间,提高用户的访问速度和体验。



通过注入静态Html文件到CDN,可以避免每次用户的请求都访问原始服务器。相反,这些文件会被缓存在CDN的服务器上,因此用户可以直接从离他们最近的服务器获取内容。这种方式可以大大减少因网络延迟导致的潜在用户流失,因为用户能够更快地获取所需的信息。


此外,CDN的使用还可以提高系统在高并发场景下的稳定性。在高并发情况下,原始服务器可能无法承受大量的请求流量,并可能导致系统崩溃或响应变慢。但是,通过将静态Html文件注入到CDN,让CDN来处理部分请求,分担了原始服务器的负载,从而提高了整个系统的稳定性。


通过将商品详情、活动详情等静态Html文件注入到CDN,可以加速用户访问速度,减少用户因网络延迟而流失的可能性,并提高系统在高并发场景下的稳定性。


2. 调用链路优化


调用链路优化重点减少RPC的调用、减少跨地域调用。


2.1 减少跨地域调用


刚才我提到了北京到上海的跨地域调用需要耗费大约30毫秒的时间,这个耗时是相当高的,所以我们应该特别关注调用链路上是否存在跨地域调用的情况。这些跨地域调用包括Rpc调用、Http调用、数据库调用、缓存调用以及MQ调用等等。在整理调用链路的时候,我们还应该标注出跨地域调用的次数,例如跨地域调用数据库可能会出现多次,在链路上我们需要明确标记。我们可以考虑通过降低调用次数来提高性能,因此在设计优化方案时,我们应该特别关注如何减少跨地域调用的次数。


举个例子,在某种情况下,假设上游服务在上海,而我们的服务在北京和上海都有部署,但是数据库和缓存的主节点都在北京,这时候就无法避免跨地域调用。那么我们该如何进行优化呢?考虑到我们的服务会更频繁地访问数据库和缓存,如果让我们上海节点的服务去访问北京的数据库和缓存,那么跨地域调用的次数就会非常多。因此,我们应该让上游服务去访问我们在北京的节点,这样只会有1次跨地域调用,而我们的服务在访问数据库和缓存时就无需进行跨地域调用。


2.2 单元化架构:不同的用户路由到不同的集群单元


如果主数据库位于北京,那么南方的用户每次写请求就只能通过跨地域访问来完成吗?实际上并非如此。数据库的主库不仅可以存在于一个地域,而是可以在多个地域上部署主数据库。将每个用户归属于最近的地域,该用户的请求都会被路由到所在地域的数据库。这样的部署不仅提升了系统性能,还提高了系统的容灾等级,即使单个机房发生故障也不会影响全网的用户。


这个思想类似于CDN(内容分发网络),它能够将用户请求路由到最近的节点。事实上,由于用户的存储数据已经在该地域的数据库中,用户的请求极少需要切换到其他地域。


为了实现这一点,我们需要一个用户路由服务来提供用户所在地域的查询,并且能够提供高并发的访问。


除了数据库之外,其他的存储中间件(如MQ、Redis等)以及Rpc框架都需要具备单元化架构能力。


当我们无法避免跨地域调用时,我们可以选择整体上跨地域调用次数最少的方案来进行优化。


2.3 微服务拆分过细会导致Rpc调用较多


微服务拆分过细会导致更多的RPC调用,一次简单的请求可能就涉及四五个服务,当访问量非常高时,多出来的三五次Rpc调用会导致接口耗时增加很多。


每个服务都需要处理网络IO,序列化反序列化,服务的GC 也会导致耗时增加,这样算下来一个大服务的性能往往优于5个微服务。


当然服务过于臃肿会降低开发维护效率,也不利于技术升级。微服务过多也有问题,例如增加整体链路耗时、基础架构升级工作量变大、单个需求代码变更的服务更多等弊端。需要你权衡开发效率、线上性能、领域划分等多方面因素。


总之应该极力避免微服务过多的情况。


怎么评估微服务过多呢?我的个人经验是:团队内平均一个人两个服务以上,就是微服务过多了。例如三个人的团队6个服务,5个人的团队10个服务。


2.4 去掉中间商,减少Rpc调用


当整个系统的调用链路中涉及到过多的Rpc调用时,可以通过去除中间服务的方式减少Rpc调用。例如从A服务到E服务的调用链路包含了4次Rpc调用(A->B->C->D->E),而我们可以评估中间的B、C、D三个服务的功能是否冗余,是否只是作为转发服务而没有太多的业务逻辑,如果是的话,我们可以考虑让A服务直接调用E服务,从而避免中间的Rpc调用,减少系统的负担。


总的来说,无论是调用链路过长或是微服务过多,都可能导致过多的Rpc请求,因此可以尝试去除中间的服务来优化系统性能。


2.5 提供Client工具方法处理,而非Rpc调用


如果中间服务有业务逻辑,不能直接移除,可以考虑使用基于Java Client工具方法的服务提供方式,而非Rpc方式。


举例来说,如果存在一个调用链路为A->B->C,其中B服务有自己的业务逻辑。此时B服务可以考虑提供一个Java Client jar包给A服务使用。B服务所依赖的数据可以由A服务提供,这样就减少1次 A 服务到B 服务的Rpc调用。


这样做有一个好处,当A、B都共同依赖相同的数据,A服务查询一遍就可以提供给自己和B服务Client使用。如果基于Rpc方式,A、B都需要查询一遍。微服务过多也不好啊!


通过改变服务提供方式,尽量减少Rpc调用次数和开销,从而优化整个系统的性能。


例如社交关注关系服务。在这个服务中,需要查询用户之间的关注关系。为了提高服务性能,关注服务内部使用缓存来存储关注关系。为了降低高并发场景下的调用延迟和机器负载,关注服务提供了一个Java Client Jar查询关注关系,放弃了上游调用rpc接口的方式。这样做的好处是可以减少一次Rpc调用,避免了下游服务因GC 停顿而导致的耗时。


2.6 单条调用改为批量调用


无论是查询还是写入,都可以使用批量调用来代替单条调用。比如,在查询用户订单的详情时,应该批量查询多个订单,而不是通过循环逐个查询订单详情。批量调用虽然会比单条调用稍微耗时多一些,但是循环调用的耗时却是单条调用的N倍,所以批量查询耗时要低很多。


在接口设计和代码流程中,我们应该尽量避免使用for循环进行单条查询或单条写入操作。正如此文所提到的,批量插入数据库的性能可能是单条插入的3-5倍。# 10亿数据如何插入Mysql,10连问,你想到了几个?


2.7 并行调用


在调用多个接口时,可以选择串行调用或并行调用的两种方式。串行调用是指依次调用每个接口,一个接口完成后才能调用下一个接口,而并行调用是指同时调用多个接口。可以看出并行调用的耗时更低,因为串行调用的耗时是多个接口耗时的总和,而并行调用的耗时是耗时最高的接口耗时。


为了灵活实现多个接口的调用顺序和依赖关系,可以使用Java中的CompletableFuture类。CompletableFuture可以将多个接口的调用任务编排成一个有序的执行流程,可以实现最大程度的并发查询或并发修改。


例如,可以并行调用两个接口,然后等待两个接口全部成功后,再对查询结果进行汇总处理。这样可以提高查询或修改的效率。


CompletableFuture<Void> first = CompletableFuture.runAsync(()->{  
            System.out.println("do something first");
Thread.sleep(200);
        });
        CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
            System.out.println("do something second");
Thread.sleep(300);
        });
        CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(first, second).whenComplete((m,k)->{
            System.out.println("all finish do something");
        });

allOfFuture.get();//汇总处理结果

CompletaleFuture 还支持自定义线程池,支持同步调用、异步调用,支持anyOf任一成功则返回等多种编排策略。由于不是本文重点,不再一一说明


2.8 提前过滤,减少无效调用


在某些活动匹配的业务场景里,相当多的请求实际上是不满足条件的,如果能尽早的过滤掉这些请求,就能避免很多无效查询。例如用户匹配某个活动时,会有非常多的过滤条件,如果该活动的特点是仅少量用户可参加,那么可首先使用人群先过滤掉大部分不符合条件的用户。


2.9 拆分接口


前面提到如果Http接口功能过于庞大,核心数据和非核心数据杂糅在一起,耗时高和耗时低的数据耦合在一起。为了优化请求的耗时,可以通过拆分接口,将核心数据和非核心数据分别处理,从而提高接口的性能。


而在Rpc接口方面,也可以使用类似的思路进行优化。当上游需要调用多个Rpc接口时,可以并行地调用这些接口。优先返回核心数据,如果处理非核心数据或者耗时高的数据超时,则直接降级,只返回核心数据。这种方式可以提高接口的响应速度和效率,减少不必要的等待时间。


3. 选择合适的存储系统


无论是查询接口还是写入接口都需要访问数据源,访问存储系统。读高写低,读低写高,读写双高等不同场景需要选择不同的存储系统。


3.1 MySQL 换 Redis


当系统查询压力增加时,可以把MySQL数据异构到Redis缓存中。


3.1.1 选择合适的缓存结构


Redis包含了一些常见的数据结构,包括字符串(String)、列表(List)、有序集合(SortSet)、哈希(Hash)和基数估计(HyperLogLog)、GEOHash等。


在不同的应用场景下,我们可以根据需求选择合适的数据结构来存储数据。举例来说,如果我们需要存储用户的关注列表,可以选择使用哈希结构(Hash)。对于需要对商品或文章的浏览量进行去重的情况,可以考虑使用基数估计结构(HyperLogLog)。而对于用户的浏览记录,可以选择列表(List)等结构来存储。如果想实现附近的人功能,可以使用Redis GEOHash结构。


Redis提供了丰富的API来操作这些数据结构,我们可以根据实际需要选择适合的数据结构和相关API来简化代码实现,提高开发效率。


关于缓存结构选择可以参考这篇文章。# 10W+TPS高并发场景【我的浏览记录】系统设计


3.1.2 选择合适的缓存策略


缓存策略指的是何时更新缓存和何时将缓存标记为过期或清理缓存。主要有两种策略。


策略1:是当数据更新时,更新缓存,并且在缓存Miss(即缓存中没有所需数据)时,从数据源加载数据到缓存中。


策略2:是将缓存设置为常驻缓存,即缓存永远不过期。当数据更新时,会即时更新缓存中的数据。这种策略通常会占用大量内存空间,因此一般只适用于数据量较小的情况下使用。另外,定时任务会定期将数据库中的数据更新到缓存中,以兜底缓存数据的一致性。


总的来说,选择何种缓存策略取决于具体的应用需求和数据规模。如果数据量较大,一般会选择策略1;而如果数据量较小且要求缓存数据的实时性,可以考虑策略2。


关于缓存使用,可以参考我的踩坑记录:#点击这里了解 第一次使用缓存翻车了


3.2 Redis 换 本地缓存


Redis相比传统数据库更快且具有更强的抗并发能力。然而,与本地缓存相比,Redis缓存仍然较慢。前面提到的Redis访问速度大约在3-5毫秒之间,而使用本地缓存几乎可以忽略不计。


如果频繁访问Redis获取大量数据,将会导致大量的序列化和反序列化操作,这会显著增加young gc频率,也会增加CPU负载。


本地缓存的性能更强,当使用Redis仍然存在性能瓶颈时,可以考虑使用本地缓存。可以设置多级缓存机制,首先访问本地缓存,如果本地缓存中没有数据,则访问Redis分布式缓存,如果仍然不存在,则访问数据库。通过使用多级缓存策略来实现更高效的性能。


本地缓存可以使用Guava Cahce 。参考本地缓存框架Guava Cache


也可以使用性能更强的Caffeine。点击这里了解


Redis由于单线程架构,在热点缓存应对上稍显不足。使用本地缓存可以极大的解决缓存热点问题。例如以下代码创建了Caffeine缓存,最大长度1W,写入后30分钟过期,同时指定自动回源取值策略。


public LoadingCache<String, User> createUserCache() {
return Caffeine.newBuilder()
.initialCapacity(1000)
.maximumSize(10000L)
.expireAfterWrite(30L, TimeUnit.MINUTES)
//.concurrencyLevel(8)
.recordStats()
.build(key -> userDao.getUser(key));
}

3.3 Redis 换 Memcached


当存在热点key和大key时,Redis集群的负载会变得不均衡,从而降低整个集群的性能。这是因为Redis是单线程执行的系统,当处理热点key和大key时,会对整个集群的性能产生影响。


相比之下,Memcached缓存是多线程执行的,它可以更好地处理热点key和大key的问题,因此可以更好地应对上述性能问题。如果遇到这些问题,可以考虑使用Memcached进行替代。


另外,还可以通过使用本地缓存并结合Redis来处理热点key和热点大key的情况。这样可以减轻Redis集群的负担,并提升系统的性能。


3.4 MySQL 换 ElasticSearch


在后台管理页面中,通常需要对列表页进行多条件检索。MySQL 无法满足多条件检索的需求,原因有两点。第一点是,拼接条件检索的查询SQL非常复杂且需要进行定制化,难以进行维护和管理。第二点是,条件检索的查询场景非常灵活,很难设计合适的索引来提高查询性能,并且难以保证查询能够命中索引。


相比之下,ElasticSearch是一种天然适合于条件检索场景的解决方案。无论数据量的大小,对于列表页查询和检索等场景,推荐首选ElasticSearch。


可以将多个表的数据异构到ElasticSearch中建立宽表,并在数据更新时同步更新索引。在进行检索时,可以直接从ElasticSearch中获取数据,无需再查询数据库,提高了检索性能。


3.5 MySQL 换 HBase


MySQL并不适合大数据量存储,若不对数据进行归档,数据库会一直膨胀,从而降低查询和写入的性能。针对大数据量的读写需求,可以考虑以下方法来存储订单数据。


首先,将最近1年的订单数据存储在MySQL数据库中。这样可以保证较高的数据库查询性能,因为MySQL对于相对较小的数据集来说是非常高效的。


其次,将1年以上的历史订单数据进行归档,并将这些数据异构(转储)到HBase中。HBase是一种分布式的NoSQL数据库,可以存储海量数据,并提供快速的读取能力。


在订单查询接口上,可以区分近期数据和历史数据,使得上游系统能够根据自身的需求调用适当的订单接口来查询订单详情。


在将历史订单数据存储到HBase时,可以设置合理的RowKey。RowKey是HBase中数据的唯一标识,在查询过程中可以通过RowKey来快速找到目标数据。通过合理地设置RowKey,可以进一步提高HBase的查询性能。


通过将订单数据分别存储在MySQL和HBase中,并根据需求进行区分查询,可以满足大数据量场景的读写需求。MySQL用于存储近期数据,以保证查询性能;而HBase用于存储归档的历史数据,并通过合理设置的RowKey来提高查询性能。


4.代码层次优化


4.1 同步转异步


将写请求从同步转为异步可以显著提升接口的性能。


以发送短信接口为例,该接口需要调用运营商接口并在公网上进行调用,因此耗时较高。如果业务方选择完全同步发送短信,就需要处理失败、超时、重试等与稳定性有关的问题,且耗时也会非常高。因此,我们需要采用同步加异步的处理方式。


公司的短信平台应该采用Rpc接口发送短信。在收到请求后,首先进行校验,包括校验业务方短信模板的合法性以及短信参数是否合法。待校验完成后,我们可以将短信发送任务存入数据库,并通过消息队列进行异步处理。而对业务方提供的Rpc接口的语义也发生了变化:我们成功接收了发送短信的请求,稍后将以异步的方式进行发送。至于发送短信失败、重试、超时等与稳定性和可靠性有关的问题,将由短信平台保证。而业务方只需确保成功调用短信平台的Rpc接口即可


4.2 减少日志打印


在高并发的查询场景下,打印日志可能导致接口性能下降的问题。我曾经不认为这会是一个问题,直到我的同事犯了这个错误。有同事在排查问题时顺手打印了日志并且带上线。第二天高峰期,发现接口的 tp99 耗时大幅增加,同时 CPU 负载和垃圾回收频率也明显增加,磁盘负载也增加很多。日志删除后,系统回归正常。


特别是在日志中包含了大数组或大对象时,更要谨慎,避免打印这些日志。


4.3 使用白名单打印日志


不打日志,无法有效排查问题。怎么办呢?


为了有效地排查问题,建议引入白名单机制。具体做法是,在打印日志之前,先判断用户是否在白名单中,如果不在,则不打印日志;如果在,则打印日志。通过将公司内的产品、开发和测试人员等相关同事加入到白名单中,有利于及时发现线上问题。当用户提出投诉时,也可以将相关用户添加到白名单,并要求他们重新操作以复现问题。


这种方法既满足了问题排查的需求,又避免了给线上环境增加压力。(在测试环境中,可以完全开放日志打印功能)


4.4 避免一次性查询过多数据


在进行查询操作时,应尽量将单次调用改为批量查询或分页查询。不论是批量查询还是分页查询,都应注意避免一次性查询过多数据,比如每次加载10000条记录。因为过大的网络报文会降低查询性能,并且Java虚拟机(JVM)倾向于在老年代申请大对象。当访问量过高时,频繁申请大对象会增加Full GC(垃圾回收)的频率,从而降低服务的性能。


建议最好支持动态配置批量查询的数量。当接口的性能较差时,可以通过动态配置批量查询的数量来优化接口的性能,根据实际情况灵活地调整每次查询的数量。


4.5 避免深度分页


深度分页指的是对一个大数据集进行分页查询时,每次只查询一页的数据,但是要获取到指定页数的数据,就需要依次查询前面的页数,这样查询的范围就会越来越大,导致查询效率变低。


在进行深度分页时,MySQL和ElasticSearch会先加载大量的数据,然后根据分页要求返回少量的数据。这种处理方式导致深度分页的效率非常低,同时也给MySQL和ElasticSearch带来较高的内存压力和CPU负载。因此,我们应该尽可能地避免使用深度分页的方式。


为了避免深度分页,可以采用每次查询时指定最小id或最大id的方法。具体来说,当进行分页查询时,可以记录上一次查询结果中的最小id或最大id(根据排序方式来决定)。在进行下一次查询时,指定查询结果中的最小id或最大id作为起始条件,从而缩短查询范围。这样每次只获取前N条数据,可以提高查询效率。


关于分页可以参考 我的文章# 四选一,如何选择适合你的分页方案?


4.6 只访问需要用到的数据


为了查询数据库和下游接口所需的字段,我们可以采取一些方法。例如,商品数据的字段非常多,如果每次调用都返回全部字段,将导致数据量过大。因此,上游可以指定使用的字段,从而有效降低接口的数据量,提升接口的性能。


这种方式不仅可以减少网络IO的耗时,而且还可以减少Rpc序列化和反序列化的耗时,因为接口的数据量较少。


对于访问量极大的接口来说,处理这些多余的字段将会增加CPU的负载,并增加Young GC的次数。因此不要把所有的字段都返回给上游!应该按需定制。


4.7 预热低流量接口


对于访问量较低的接口来说,通常首次接口的响应时间较长。原因是JVM需要加载类、Spring Aop首次动态代理,以及新建连接等。这使得首次接口请求时间明显比后续请求耗时长。


然而在流量较低的接口中,这种影响会更大。用户可能尝试多次请求,但依然经常出现超时,严重影响了用户体验。每次服务发布完成后,接口超时失败率都会大量上升!


那么如何解决接口预热的问题呢?可以考虑在服务启动时,自行调用一次接口。如果是写接口,还可以尝试更新特定的一条数据。


另外,可以在服务启动时手动加载对应的类,以减少首次调用的耗时。不同的接口预热方式有所不同,建议使用阿里开源的诊断工具arthas,通过监控首次请求时方法调用堆栈的耗时来进行接口的预热。


arthas使用文档 arthas.aliyun.com/doc/trace.h…


使用arthas trace命令可以查看 某个方法执行的耗时情况。
trace com.xxxx.ClassA function1
image.png


5. 数据库优化


5.1 读写分离


增加MySQL数据库的从节点来实现负载均衡,减轻主节点的查询压力,让主节点专注于处理写请求,保证读写操作的高性能。


除此之外,当需要跨地域进行数据库的查询时,由于较高网络延迟等问题,接口性能可能变得很差。在数据实时性不太敏感的情况下,可以通过在多个地域增加从节点来提高这些地域的接口性能。举个例子,如果数据库主节点在北京,可以在广州、上海等地区设置从节点,在数据实时性要求较低的查询场景,可有效提高南方地区的接口性能。


5.2 索引优化


5.2.1查询更新务必命中索引


查询和更新SQL必须命中索引。查询SQL如果没命中索引,在访问量较大时,会出现大量慢查询,严重时会导致整个MySQL集群雪崩,影响到其他表、其他数据库。所以一定要严格审查SQL是否命中索引。可以使用explain命令查看索引使用情况。


在SQL更新场景,MySQL会在索引上加锁,如果没有命中索引会对全表加锁,全表的更新操作都会被阻塞住。所以更新SQL更要确保命中索引。


因此,为了避免这种情况的发生,需要严格审查SQL是否命中索引。可以使用"explain"命令来查看SQL的执行计划,从而判断是否有使用索引。这样可以及早发现潜在的问题,并及时采取措施进行优化和调整。


除此之外,最好索引字段能够完全覆盖查询需要的字段。MySQL索引分主键索引和普通索引。普通索引命中后,往往需要再查询主键索引获取记录的全部字段。如果索引字段完全包含查询的字段,即索引覆盖查询,就无需再回查主键索引,可以有效提高查询性能。


更详细请参考本篇文章 # 深入理解mysql 索引特性


5.2.2 常见索引失效的场景



  1. 查询表达式索引项上有函数.例如date(created_at) = 'XXXX'等.字符处理等。mysql将无法使用相应索引

  2. 一次查询(简单查询,子查询不算)只能使用一个索引

  3. != 不等于无法使用索引

  4. 未遵循最左前缀匹配导致索引失效

  5. 类型转换导致索引失效,例如字符串类型指定为数字类型等。

  6. like模糊匹配以通配符开头导致索引失效

  7. 索引字段使用is not null导致失效

  8. 查询条件存在 OR,且无法命中索引。


5.2.3 提高索引利用率


当索引数量过多时,索引的数据量就会增加,这可能导致数据库无法将所有的索引数据加载到内存中,从而使得查询索引时需要从磁盘读取数据,进而大大降低索引查询的性能。举例来说,我们组有张表700万条数据,共4个索引,索引数据量就达到2.8GB。在一个数据库中通常有多张表,在进行分库分表时,可能会存在100张表。100张表就会产生280GB的索引数据,这么庞大的数据量无法全部放入内存,查询索引时会大大降低缓存命中率,进而降低查询和写入操作的性能。简而言之,避免创建过多的索引。


可以选择最通用的查询字段作为联合索引最左前缀,让索引覆盖更多的查询场景。


5.3 事务和锁优化


为了提高接口并发量,需要避免大事务。当需要更新多条数据时,避免一次性更新过多的数据。因为update,delete语句会对索引加锁,如果更新的记录数过多,会锁住太多的数据,由于执行时间较长,会严重限制数据库的并发量。


间隙锁是MySQL在执行更新时为了保证数据一致性而添加的锁定机制。虽然更新的记录数量很少,但MySQL可能会锁定比更新数量更大的范围。因此,需要注意查询语句中的where条件是否包含了较大的范围,这样可能会锁定不应该被锁定的记录。


如果有批量更新的情况,需要降低批量更新的数量,缩小更新的范围。


其次在事务内可能有多条SQL,例如扣减库存和新增库存扣减流水有两条SQL。因为两个SQl在同一个事务内,所以可以保证原子性。但是需要考虑两个SQL谁先执行,谁后执行?


建议先扣库存,再增加流水。


扣减库存的更新操作耗时较长且使用了行锁,而新增流水的速度较快但是串行执行,如果先新增流水再扣减库存,会导致流水表被锁定的时间更长,限制了流水表的插入速度,同时会阻塞其他扣减库存的事务。相反,如果先扣减库存再新增流水,流水表被锁定的时间较短,有利于提高库存扣减的并发度。


5.4 分库分表,降低单表规模


MySQL单库单表的性能瓶颈很容易达到。当数据量增加到一定程度时,查询和写入操作可能会变得缓慢。这是因为MySQL的B+树索引结构在单表行数超过2000万时会达到4层,同时索引的数据规模也会变得非常庞大。如果无法将所有索引数据都放入内存缓存中,那么查询索引时就需要进行磁盘查询。这会导致查询性能下降。参考# 10亿数据如何插入Mysql,10连问,你想到了几个?


为了克服这个问题,系统设计在最初阶段就应该预测数据量,并设置适合的分库分表策略。通过将数据分散存储在多个库和表中,可以有效提高数据库的读写性能。此外,分库分表也可以突破单表的容量限制。


分库分表工具推荐使用 Sharding-JDBC


5.5 冗余数据,提高查询性能


使用分库分表后,索引的使用受到限制。例如,在关注服务中,需要满足两个查询需求:1. 查询用户的关注列表;2. 查询用户的粉丝列表。关注关系表包含两个字段,即关注者的fromUserId和被关注者的toUserId。


对于查询1,我们可以指定fromUserId = A,即可查询用户A的关注列表。


对于查询2,我们可以指定toUserId = B,即可查询用户B的粉丝列表。


在单库单表的情况下,我们可以设计fromUserId和toUserId这两个字段作为索引。然而,当进行分库分表后,我们面临选择哪个字段作为分表键的困扰。无论我们选择使用fromUserId还是toUserId作为分表键,都会导致另一个查询场景变得难以实现。


解决这个问题的思路是:存储结构不仅要方便写入,还要方便查询。既然查询不方便,我们可以冗余一份数据,以便于查询。我们可以设计两张表,即关注列表表(Follows)和粉丝列表表(Fans)。其中,Follows表使用fromUserId作为分表键,用于查询用户的关注列表;Fans表使用toUserId作为分表键,用于查询用户的粉丝列表。


通过冗余更多的数据,我们可以提高查询性能,这是常见的优化方案。除了引入新的表外,还可以在表中冗余其他表的字段,以减少关联查询的次数。


关注关系设计 请参考 #解密亿级流量【社交关注关系】系统设计


5.6 归档历史数据,降低单表规模


MySQL并不适合存储大数据量,如果不对数据进行归档,数据库会持续膨胀,从而降低查询和写入的性能。为了满足大数据量的读写需求,需要定期对数据库进行归档。


在进行数据库设计时,需要事先考虑到对数据归档的需求,为了提高归档效率,可以使用ctime(创建时间)进行归档,例如归档一年前的数据。


可以通过以下SQL语句不断执行来归档过期数据:


delete from order where ctime < ${minCtime} order by ctime limit 100;


需要注意的是,执行delete操作时,ctime字段应该有索引,否则将会锁住整个表


另外,在将数据库数据归档之前,如果有必要,一定要将数据同步到Hive中,这样以后如果需要进行统计查询,可以使用Hive中的数据。如果归档的数据还需要在线查询,可以将过期数据同步到HBase中,这样数据库可以提供近期数据的查询,而HBase可以提供历史数据的查询。可参考上述MySQL转HBase的内容。


5.7 使用更强的物理机 CPU/内存/SSD硬盘


MySQL的性能取决于内存大小、CPU核数和SSD硬盘读写性能。为了适配更强的宿主机,可以进行以下MySQL优化配置


innodb_buffer_pool_size


缓冲池是数据和索引缓存的地方。默认大小为128M。这个值越大越好决于CPU的架构,这能保证你在大多数的读取操作时使用的是内存而不是硬盘。典型的值是5-6GB(8GB内存),20-25GB(32GB内存),100-120GB(128GB内存)。


max_connections


数据库最大连接数。可以适当调大数据库链接


innodb_flush_log_at_trx_commit


控制MySQL刷新数据到磁盘的策略。



  1. 默认=1,即每次事务提交都会刷新数据到磁盘,安全性最高不会丢失数据。

  2. 当配置为0、2 会每隔1s刷新数据到磁盘, 在系统宕机、mysql crash时可能丢失1s的数据。


innodb_thread_concurrency


innodb_thread_concurrency默认是0,则表示没有并发线程数限制,所有请求都会直接请求线程执行。



当并发用户线程数量小于64,建议设置innodb_thread_concurrency=0;
在大多数情况下,最佳的值是小于并接近虚拟CPU的个数;



innodb_read_io_threads


设置InnoDB存储引擎的读取线程数。默认值是4,表示使用4个线程来读取数据。可以根据服务器的CPU核心数来调整这个值。例如调整到16甚至32。


innodb_io_capacity


innodb_io_capacity InnoDB可用的总I/O容量。该参数应该设置为系统每秒可以执行的I/O操作数。该值取决于系统配置。当设置innodb_io_capacity时,主线程会根据设置的值来估算后台任务可用的I/O带宽


innodb_io_capacity_max: 如果刷新操作过于落后,InnoDB可以超过innodb_io_capacity的限制进行刷新,但是不能超过本参数的值


默认情况下,MySQL 分别配置了200 和2000的默认值。
image.png


当磁盘为SSD时,可以考虑设置innodb_io_capacity= 2000,innodb_io_capacity_max=4000


6. 压缩数据


6.1 压缩数据库和缓存数据


压缩文本数据可以有效地减少该数据所需的存储空间,从而提高数据库和缓存的空间利用率。然而,压缩和解压缩的过程会增加CPU的负载,因此需要仔细考虑是否有必要进行数据压缩。此外,还需要评估压缩后数据的效果,即压缩对数据的影响如何。


例如下面这一段文字我们使用GZIP 进行压缩



假设上游服务在上海,而我们的服务在北京和上海都有部署,但是数据库和缓存的主节点都在北京,这时候就无法避免跨地域调用。那么我们该如何进行优化呢?考虑到我们的服务会更频繁地访问数据库和缓存,如果让我们上海节点的服务去访问北京的数据库和缓存,那么跨地域调用的次数就会非常多。因此,我们应该让上游服务去访问我们在北京的节点,这样只会有1次跨地域调用,而我们的服务在访问数据库和缓存时就无需进行跨地域调用。



该段文字使用UTF-8编码,共570位byte。使用GZIP 压缩后,变为328位Byte。压缩效果还是很明显的。


压缩代码如下


//压缩
public static byte[] compress(String str, String encoding) {
if (str == null || str.length() == 0) {
return null;
}
byte[] values = null;
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(out);
gzip.write(str.getBytes(encoding));
gzip.close();
values = out.toByteArray();
out.close();
} catch (IOException e) {
log.error("gzip compress error.", e);
throw new RuntimeException("压缩失败", e);
}
return values;
}
// 解压缩
public static String uncompressToString(byte[] bytes, String encoding) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
String value = out.toString(encoding);
out.close();
return value;
} catch (IOException e) {
log.error("gzip uncompress to string error.", e);
throw new RuntimeException("解压缩失败", e);
}
}

值得一提的是使用GZIP压缩算法的cpu负载和耗时都是比较高的。使用压缩非但不能起到降低接口耗时的效果,可能导致接口耗时增加,要谨慎使用。除此之外,还有其他压缩算法在压缩时间和压缩率上有所权衡。可以选择适合的自己的压缩算法。


image.png


7. 系统优化


7.1 优化GC


无论是Young GC还是Full GC,在进行垃圾回收时都会暂停所有的业务线程。因此,需要关注垃圾回收的频率,以确保对业务的影响尽可能小。


插播提问:为什么young gc也需要stop the world ? 阿里面试官问我的,把我问懵逼了。


一般情况下,通过调整堆大小和新生代大小可以解决大部分垃圾回收问题。其中,新生代是用于存放新创建的对象的区域。对于Young GC的频率增加的情况,一般是系统的请求量大量增长导致。但如果young gc增长非常多,就需要考虑是否需要增加新生代的大小。


因为如果新生代过小,很容易被打满。这导致本可以被Young GC掉的对象被晋升(Promotion)到老年代,过早地进入老年代。这样一来,不仅Young GC频繁触发,Full GC也会频繁触发。


gc场景非常多,建议参考美团的技术文章详细概括了9种CMS GC问题。# Java中9种常见的CMS GC问题分析与解决


7.2 提升服务器硬件


如果cpu负载较高 可以考虑提高每个实例cpu数量,提高实例个数。同时关注网络IO负载,如果机器流量较大,网卡带宽可能成为瓶颈。


高峰期和低峰期如果机器负载相差较大,可以考虑设置弹性伸缩策略,高峰期之前自动扩容,低峰期自动缩容,最大程度提高资源利用率。


8. 交互优化


8.1 调整交互顺序


我曾经负责过B端商品数据创建,当时产品提到创建完虚拟商品后要立即跳转到商品列表页。当时我们使用ElasticSearch 实现后台管理页面的商品查询,但是ElasticSearch 在新增记录时,默认是每 1 秒钟构建1次索引,所以如果创建完商品立即跳转到商品列表页是无法查到刚创建的商品的。于是和产品沟通商品创建完成跳转到商品详情页是否可以,沟通后产品也认可这个交互。


于是我无需调整ElasticSearch 构建索引的时机。(后来了解到 ElasticSearch 提供了API。新增记录后,可立即构建索引,就不存在1秒的延迟了。但是这样操作索引文件会非常多,影响索引查询性能,不过后台管理对性能要求不高,也能接收。)


通过和产品沟通交互和业务逻辑,有时候能解决很棘手的技术问题。有困难,不要闷头自己扛哦~


8.2 限制用户行为


在社交类产品中用户关注功能。如果不限制用户可以关注的人数,可能会出现恶意用户大量关注其他用户的情况,导致系统设计变得复杂。


为了判断用户A是否关注用户B,可以查看A的关注列表中是否包含B,而不是检查B的粉丝列表中是否包含A。这是因为粉丝列表的数量可能非常庞大,可能达到上千万。而正常用户的关注列表通常不会很多,一般只有几百到几千人。


为了提高关注关系的查询性能,可将关注列表数据导入到Redis Hash结构中。系统通过限制用户的最大关注上限,避免出现Redis大key的情况,也避免大key过期时的性能问题,保证集群的整体性能的稳定。避免恶意用户攻击系统。


可以看这篇文章 详细了解关注系统设计。# 解密亿级流量【社交关注关系】系统设计


作者:他是程序员
来源:juejin.cn/post/7287420810318299190
收起阅读 »

用代码预测未来买房后的生活

web
背景 最近家里突然计划买房,打破了我攒钱到财务自由的规划。所以开始重新计算自己背了房贷之后的生活到底如何。 一开始通过笔记软件来进行未来收入支出推算。后来发现太过麻烦,任何一项收入支出的改动,都会影响到后续结余累计值的计算。 所以干脆发挥传统艺能,写网页! 逻...
继续阅读 »

背景


最近家里突然计划买房,打破了我攒钱到财务自由的规划。所以开始重新计算自己背了房贷之后的生活到底如何。


一开始通过笔记软件来进行未来收入支出推算。后来发现太过麻烦,任何一项收入支出的改动,都会影响到后续结余累计值的计算。


所以干脆发挥传统艺能,写网页!


逻辑



  • 假设当前年收入稳定不变,在 50 岁之后收入降低。

  • 通过 上一年结余 + 收入-房贷-生活支出-特殊情况支出 的公式得到累加计算每年的结余资金。

  • 通过修改特使事件来模拟一些如装修、买车的需求。

  • 最后预测下 30 年后的生活结余,从而可知未来的生活质量。


实现


首先,创建一个 HTML 文件 feature.html,然后咔咔一顿写。


<!DOCTYPE html>
<html lang="zh-CN" dir="ltr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="https://cn.vuejs.org/logo.svg" />
<title>生涯模拟</title>
<meta name="description" content="人生经费模拟器" />

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<style>
body {
margin: 0;
padding: 0;
}

.content {
background: #181818;
height: 100vh;
}

.time-line {
height: 100%;
overflow: auto;
}

.time-line-item {
position: relative;
padding: 10px 40px;
}

.flex-wrap {
display: flex;
flex-direction: row;
align-items: center;
}

.tli-year {
line-height: 24px;
font-size: 18px;
font-weight: bold;
color: #e5eaf3;
}

.tli-amount {
font-size: 14px;
color: #a3a6ad;
margin: 0 20px;
}

.tli-description {
margin-top: 6px;
line-height: 18px;
font-size: 12px;
color: #8d9095;
}

.tli-description-event {
color: #f56c6c;
}
</style>
</head>
<body>
<div id="app">
<div class="content">
<div class="time-line">
<div v-for="item in data" :key="item.year" class="time-line-item">
<div class="flex-wrap">
<span class="tli-year">{{ item.year }}年</span>
<span class="tli-amount">¥{{ item.ammount / 10000 }} 万</span>
</div>
<div
v-for="desc in item.descriptions"
class="tli-description flex-wrap"
:class="desc.normal ? '' : 'tli-description-event'">

<span style="margin-right: 20px">{{ desc.name }}</span>
<span v-show="desc.ammount">{{ desc.ammount }}</span>
</div>
</div>
</div>
</div>
</div>

<script>
const { createApp, ref, onMounted } = Vue;

const config = {
price: 6000000, // 房价
startAmount: 1850000, // 启动资金
income: 26000 * 12, // 年收入
loan: 15700 * 12, // 年贷款
live: 7000 * 12, // 年支出
startYear: 2023, // 开始还贷年份
// 生活事件
events: [
{ year: 2024, ammount: 0, name: "大女儿一年级" },
{ year: 2026, ammount: 0, name: "小女儿一年级" },
{ year: 2028, ammount: 0, name: "老爸退休" },

{ year: 2027, ammount: -300000, name: "装修" },
{ year: 2031, ammount: -300000, name: "买车" },
{ year: [2028, 2036], ammount: 7500 * 12, name: "老房子房租" },
{ year: 2036, ammount: 3500000, name: "老房子卖出" },
],
};

createApp({
setup() {
const data = ref([]);

onMounted(() => {
genData();
});

function genData() {
const arr = [];
const startYear = config.startYear;
const endYear = startYear + 30;

for (let year = startYear; year < endYear; year++) {
if (year === startYear) {
arr.push({
year,
ammount: config.startAmount - config.price * 0.3,
descriptions: [
{
name:
"开始买房,房价" +
config.price / 10000 +
"万,首付" +
(config.price * 0.3) / 10000 +
"万",
ammount: 0,
},
],
});
} else {
const latestAmount = arr[arr.length - 1].ammount;

const filterDescs = config.events.filter((item) => {
if (Array.isArray(item.year)) {
return item.year[0] <= year && item.year[1] >= year;
}
return item.year === year;
});

let descAmount = 0;
if (filterDescs.length > 0) {
descAmount = filterDescs
.map((item) => item.ammount)
.reduce((acc, val) => acc + val);
}

const income = config.income;

arr.push({
year,
ammount:
latestAmount +
income -
config.loan -
config.live +
descAmount,
descriptions: [
{
name: "月收入",
ammount: income / 12,
normal: true,
},
{
name: "月贷款",
ammount: -config.loan / 12,
normal: true,
},
{
name: "月支出",
ammount: -config.live / 12,
normal: true,
},
{
name: "月结余",
ammount: (income - config.loan - config.live) / 12,
normal: true,
},
...filterDescs,
],
});
}
}

data.value = arr;
}

return {
data,
};
},
}).mount("#app");
</script>
</body>
</html>


PS: 之所以用 vue 呢是因为写起来顺手且方便(小工具而已,方便就行。不必手撕原生 JS DOM)。


效果


通过修改 config 中的参数来定义生活中收支的大致走向。外加一些标注和意外情况的支出。得到了下面这个图。


image.png


结论



  • 倘若过上房贷生活,那么家里基本一直徘徊在没钱的边缘,需要不停歇的工作,不敢离职。压力真的很大。30 年后除了房子其实没剩下多少积蓄了。

  • 修改配置,将房贷去掉,提高生活支出,那么 30 年后大概能存下 500w 的收入。


以上没有算通货膨胀和工资的上涨,这个谁也说不准。只是粗浅的计算。


所以,感觉上买房真的是透支了未来长期生活质量和资金换来的。也不知道买房的决定最终会如何。


作者:VioletJack
来源:juejin.cn/post/7287144390601244672
收起阅读 »

211 毕业就入职 30 人的小公司是什么体验

为什么“选择”了 30 人的小公司? 作为一个 211 毕业的学生,进入 30 人的小公司不管是 8 年前还是现在,应该都是比较稀少的,但是当面的我阴差阳错进了这样一个小公司。 为什么我选择进入这样一个 30 人的小公司呢?主要原因是因为没得选。 当时我在大学...
继续阅读 »

为什么“选择”了 30 人的小公司?


作为一个 211 毕业的学生,进入 30 人的小公司不管是 8 年前还是现在,应该都是比较稀少的,但是当面的我阴差阳错进了这样一个小公司。


为什么我选择进入这样一个 30 人的小公司呢?主要原因是因为没得选。


当时我在大学读的商科,跟计算机有关的课程只学了计算机基础、数据库基础和 C 语言编程基础,而且那个时候觉得这几门课程都是编外课程,没有好好学,C 语言课程期末考试还是老师放水以 60 分擦边通过。


社会消息闭塞,大学都要毕业了,也不知道社会上有哪些岗位,同寝室的同学也在打游戏中度过。


之后被一个考验小组拉进去考验,他们都准备的金融学专硕,我家穷,就准备考经济学硕士,结果没考上(现在还是比较庆幸没考上的,否则现在不知道干啥去了,个人性格也不适合证券之类的工作)。


没考上,毕业之后也不知道干啥,就来北京又考了一年,又没考上。之后进了一个小的 Java 培训机构培训,从此入行!


毕竟没什么基础,结课之后面试了几家,因为生活难以为继了,选择第一个给 offer 的 30 人小公司。


现在工作 8 年了,也经历了从 30 人的小公司、 2000 人+的传统上市企业、互联网大小厂,有兴趣可以看之前的文章:。


与大公司相比,小公司有哪些不好的地方


首先,工作环境一般都是一栋楼里面的一个小办公室,甚至有的直接在居民楼里办公,办公环境没有大公司好;


其次,薪资福利待遇相比大公司更低,而且社保等基础福利打折扣,很多小公司缴纳社保和公积金都是按照当地最低标准缴纳,相对大部分大公司会少很多钱;


再次,管理混乱,不管是老板还是管理者,都没有受过相应的教育或者训练,比较随心所欲,很多决策都是老板的一言堂,很难总结出来统一的成功经验。


小公司有哪些优点


首先,小公司对能力的培养更加全面,你可能需要同时干产产品经理、开发、测试、运维等多个角色的活,更能理解整个软件的生命周期,如果你要换岗位,如果你有在小公司的工作经历,可能会更加容易。


其次,小公司更加自由,做一个项目,它不会限制你使用的技术,只要你能实现需求,不会管你用的什么技术、什么技术方案,你可以更加容易的实现你的技术想法,验证你的想法。


再次,小公司可能更好交朋友,因为小公司人少,更多的是刚毕业的学生,更容易真心相待,我现在从进入社会之后交的朋友,有好几个都是第一家小公司的时候交的。


最后,培养更加全面,公司有一个同事,因为各方面比较优秀,在甲方爸爸的心中认可度比较高,自己成立了一个小公司,还是接原来甲方的需求,成功的从小员工变身为老板,后来还扩招了好几个员工,妥妥的打败大厂一般总监。


收获


感谢这家公司,给了我这样一个,没有技术背景、没有实习经历、技术也不够强的毕业生一个入行的机会。


在这家公司,我收获了 IT 圈的第一波朋友,也收获了工程化的思想,积攒了各类技术的经验,为我之后的工作提供了丰厚的积累。


而且,在这里,我积累了大量的技术经验和经历,也为跳槽到大公司提供了跳板。


最后,欢迎大家分享自己入职小公司的经历,让更多人了解小公司,给自己的职业选择多一个方向!


作者:六七十三
来源:juejin.cn/post/7287053284787683363
收起阅读 »

如果失业了,我们还能干啥?

这个事其实一直存在脑子的。为啥呢?因为我们听到太多了,太多了,35岁是个坎。事实上,找工作也是如此,很多行业都是有年龄限制的。找不到自己原来的行业的工作了。那就只有转行了。   对于我们这种菜鸟级别人,现实是残酷的。转行又谈何容易呀?但是真的到那一天,地步了,...
继续阅读 »

这个事其实一直存在脑子的。为啥呢?因为我们听到太多了,太多了,35岁是个坎。事实上,找工作也是如此,很多行业都是有年龄限制的。找不到自己原来的行业的工作了。那就只有转行了。


  对于我们这种菜鸟级别人,现实是残酷的。转行又谈何容易呀?但是真的到那一天,地步了,也不得不转。这不仅仅是我一个的想法,同事也是,群里的网友也是。于是乎,我们失业了,我们能干啥?经常被讨论起来。


   我也经常观察和想一些可行的。太远太陌生的咱也想不到。我想到的是开滴滴,顺风车,送外卖,送快递,干工地,开一个小餐馆,干保安,干搬运,干家政服务,干修理,洗空调。最后就是回老家养牛养鸡养鸭养猪之类。


  我先说几个我亲眼看到的,我觉得是非常可行的。


    之前公司有一个小小的箱子需要扔掉,然后叫了物业过来。大概是50x50x80这么大小。你们可知道这么一点东西,扔掉要多少钱么?100块。听到简直不敢相信。还有换灯泡,物业过来帮忙换多少钱一个?50元。就那么一两分钟的事。如果你不愿意,那只有自己换了。所以公司一个都没有叫物业做。扔箱子交给收废品的,换灯炮就我们男同事换。


     到了现在的公司,于是又遇到相同的事,这次换一个灯泡,你们听了都会惊讶的。400多一个。真的贵得离谱。只是咱没有工具,还有公司不允许,不然我就能干好。


    空调原来是要洗的,不过之前是不知道怎么洗,现在看了他们洗一次,知道非常简单。洗一台大概30分钟。收费是50到70元一台。真的很容易!


所以我把这些看到的分享到一个群里。说以后咱干这个!


这些天我还拍到一些服务图片。


一 收费服务


image.png


二 拆卸代扔服务


image.png


他们收费都比较贵,那咱比他们便宜三分之一?是不是可以把业务接过来?


   这些肯定比干工地轻松一些,而且赚得不比工地少。只要把服务干好了,回头客,口碑好了,不愁没有活。也不会存在所谓失业了。


三 其他大佬建议


image.png


image.png


  有时候自己焦虑,是因为害怕,习惯了熟悉路径。不愿意改变罢了。其实都是未必要的。


  正所谓车到山前必有路,船到桥头自然直,一切顺其自然!只要自己不懒,不要所谓的面子,一生还是可以顺顺当当的。


  当然,如果有厉害的高人指引,带路,贵人相助,那肯定可以过得更好。那是另当别论了。


  这些就是我当前想到的,了解到的。


作者:大巨头
来源:juejin.cn/post/7286762580877901865
收起阅读 »

iOS面试题目——hook block(1)

// 1、实现下面的函数,将 block 的实现修改为 NSLog(@"Hello world"); //也就是说,在调用完这个函数后调用用block()时,并不调用原始实现,而是打 "Hello world" void HookBlockToPrintHe...
继续阅读 »
// 1、实现下面的函数,将 block 的实现修改为 NSLog(@"Hello world");
//也就是说,在调用完这个函数后调用用block()时,并不调用原始实现,而是打 "Hello world"

void HookBlockToPrintHelloWorld(id block){

}


分析:题目的意思很明白,就是要实现一个函数,将作为参数传入的block的实现修改为一句log。有研究过block的结构的都知道,block实际上就是一个接口体。而它的实现部分,是作为函数指针被保存在结构体的`invoke`中(即第16个字节处):

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    uintptr_t invoke; // 此处保存的是实现代码的起始地址
    struct Block_descriptor_1 *descriptor;
    // imported variables
};


所以这题的解法也很明朗:就是将block结构体中的invoke函数指针地址替换为我们的函数:

// 定义一个函数
void block1_replace(void){
NSLog(@"Hello world");
};
void HookBlockToPrintHelloWorld(id block){
    // 解析 block 为 struct Block_layout 结构体
struct Block_layout *layout = (__bridge struct Block_layout *)block;

    //此处不能直接修改,因为该处地址属性为不可写状态,强行替换,会导致程序崩溃
    //layout->invoke = (uintptr_t)block1_replace;

    //修改内存属性
    vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
    vm_size_t vmsize = 0;
    mach_port_t object = 0;
    vm_region_basic_info_data_64_t info;
    mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
    kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
    if (ret != KERN_SUCCESS) {
        NSLog(@"获取失败");
        return;
    }
    vm_prot_t protection = info.protection;
    // 判断内存是否可写
    if ((protection&VM_PROT_WRITE) == 0) {
        // 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
        if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
            return;
}
}
layout->invoke = (uintptr_t)block1_replace;
}


收起阅读 »

做个清醒的程序员之成为少数派

阅读时长约10分钟,统计2604个字。 这是一篇插队的文章。 本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。 周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,...
继续阅读 »

阅读时长约10分钟,统计2604个字。


这是一篇插队的文章。


本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。


周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,最近更新比较频繁,加了很多人。这位老兄一上来先是肯定了我的文章,随后指出了文中的错误。坦率地讲,自从复活博客之后,这还是第一位指出我错误的朋友,一下子我就来了兴趣。


在本系列文集的《序》中,我原文是这样写的:



我一直奉行一句话:“有道无术,尚可求也;有术无道,则止于术”。这句话出自老子的《道德经》,而且很好理解。



他指出《道德经》里其实没有这句话。但是呢,本着对读者负责的态度,我在写文章的时候确实去查了一下。程序员这个职业大家都懂,比较较真,至少我是这样的。于是我就找到了一些依据,来证明我说的是对的。但很快便发现事实其实不是这样,这位老兄所言非虚,我引的这句话确实并不出自《道德经》。所以,我要在这里向所有读过我上篇文章的朋友道个歉。澄清一下:“有道无术,尚可求也;有术无道,则止于术”,尽管这句话有几分道理,但真的不是《道德经》原文。


好了,故事就到这里结束了。说到这,大家应该也能理解我为什么要把这篇文章拿来插队。一方面趁热打铁,有错误及时声明,另一方面这个故事对我有新的启发。


这位老兄,名为张鸿羽。稍加细聊后,我得知鸿羽兄是有背过原文的,而我没有。我只是看到大部分都这样说,便信以为真,然后也跟着这样说。显然,我成为了大多数人中的一份子。而鸿羽兄是少数派中的一份子。有时候,真理真的掌握在少数人手中。


回想过去几年的工作历程,特别是刚开始工作的那几年,我做的很多工作都是“探索型”的。所谓“探索型”,就是对新技术,或者说是公司的研发部门未曾使用过的技术进行尝试摸索。当然,尝试新技术之前,要能发现新技术。而一项新技术的诞生,总会伴随着官方的宣传,以及一些支持它、拥护它的人高声叫好。但只有真正尝试过,特别是用新技术来实现较为复杂系统的时候,才会知道这项新技术到底优势在哪,劣势又在哪。


诚然,如果让我来总结我尝试新技术、新框架的经验,我会说:大部分新技术或是框架确实弥补了已有框架的不足,但其弥补的程度往往并不是质变的,只是小步优化。甚至有些新兴技术在弥补的同时,还引入了其它的问题。这对于使用它的开发者来说,的确是个坏消息。


但话说回来,没尝试用过,又怎能知道答案呢?技术的发展本就是这样一步一个坎,有时候走一步还退两步的呀。


这或许就是我等软件开发者的宿命,对于现存的技术框架,总是有这样或那样的不满意,觉得用着不顺手。期盼着某一天,某个技术大佬,或者团体,发明了一种新的框架,甚至是新的编程语言。或是直接起义,自己创造一款新的技术框架,能真正地解决那些令我们不满的问题,让软件开发编程成为真正的享受。


但现实是,很多新的技术框架的诞生,都伴随着类似的口号。也总会有勇敢的开发者尝鲜,也总会经历被坑,然后不断填坑的过程。而这些敢于尝鲜的开发者,就是那些最终会成为“少数派”的人。他们知道在各种美好的宣传背后,隐藏着多深的坑。对于这些坑,又该用什么方法去填。


“少数派”或许才是那些头脑最清醒的那一小撮人群。


但是,成为“少数派”不仅意味着失败的尝试,还有大多数人的不理解。甚至更严重一些,就是诋毁,百口莫辩。这需要一颗强大的内心,和与时间做朋友的勇气以及态度。


不过,我为什么鼓励程序员要做“少数派”,而不是成为“大多数”呢?还有另外一个原因,那就是由行业特征决定的。我相信程序员大多都活跃在互联网行业,这个行业是赢家通吃的指数型结构。有点类似财富分配,大部分的财富掌握在少数人的手里。而且无论如何数学建模,或是提高那些穷人的初始资金,最终推演的结局依然如此。


如今,在中国,乃至全世界,所谓“互联网大厂”无非就是那几家,而剩下的呢?数字上远远超过我们熟知的那些大厂,但拥有的财富值却位于指数图表中的长尾之中。这就是指数型的行业的特征,也是程序员这个群体的特征。


如果大家有查相关的数据,可以发现优秀程序员的工作效率往往是普通程序员的好几倍,尽管薪水上的差距不是这样。而大多数都是普通程序员,优秀程序员只属于“少数派”。优秀程序员,拿到需求,会做足够的分析,到了动手的时候,则像个流水线的工人;普通程序员,拿到需求就想赶快动手,面临的有可能是回炉重造。优秀程序员,会充分考虑到使用场景,采用防御式编程来规避可能带来的缺陷;普通程序员,想的只是实现需求,把程序健壮性扔给测试人员。优秀程序员,会考虑代码的可读性,为代码添加合适的注释、每个方法或函数的功能单一、清晰;普通程序员,急于求成,不注重代码规范,导致日后维护困难……


但是,追求效率和追求质量,大多数公司都会选择前者。但做多和做好,结果往往相差甚远。


大部分人倾向于做多、扩张、追求规模化。但殊不知做大的后果往往是成本的上升,利润却不一定变高。但做好却不一样,它追求的是平衡收支,而不是盲目追求利润。更好的做法其实是在做好之前,不要做大。要相信好产品,自然会带来口碑。过分追求大规模,反倒会使高利润远去。而把事情做好的心态,看似发展得慢,实则是条捷径。


回顾我创作的历程,之前的我总想着多写,多写就是扩张,意味着规模。但这种心态往往做不出好书,因为这是效率当先,质量次之的做法。但我身边也有的人,创作很用心,不着急让书早日面试,很认真地创作,比我的速度慢一些。这便是把事情做好的心态。你猜结果如何?人家一年十几万的稿酬,我却只有可怜的几万块。


所以,上面那套理论并不是我胡乱写的,或是从哪本书里看到,就抄过来的。而是真的付出了血和泪,总结出的道理。在此,我劝你做个“清醒”的人。追求效率没错,一旦做得过火,则会适得其反。


另一方面,如果只想成为大多数,可不可以呢?当然也可以,只不过互联网行业或许不再适合。那些符合正态分布的行业才是想成为大多数的那类人的理想去处。


比如,餐饮行业。现在,大家可以想一想,有没有那家餐馆,或是哪个餐饮品牌,能做到赢家通吃?似乎没有,如果也去查这方面的数据,就会发现餐饮行业其实并不是指数分布,而是呈正态分布的。只要能做到普通中位数的水平,就OK了。


真正的高手一般都是“少数派”。他们不仅能力拔群,思考问题时的方法、对世界的认知和一般人都有区别。若要成为软件开发工程师中的“高手”,必须成为“少数派”,成为战场上的传说。


作者:萧文翰
来源:juejin.cn/post/7214855127625302053
收起阅读 »

被裁员半年了,谈谈感想

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。 进入公司后经历了几次组...
继续阅读 »

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。

进入公司后经历了几次组织架构调整,也不断变化着业务形态,但本着拥抱变化的心态,想着会越来越好,又想着自己技术在同事间也不会排到后面,所以对于这个突发状况毫无准备。


心路历程


首月


刚刚经历裁员,下个月会有工资、奖金和赔偿金入账,赔偿金不扣税,同时对于市场环境没有了解,比较乐观。首月的想法就是写简历,并开始投递,先投不想去的公司找面试经验;找学习资料、刷题;期望薪资是不需要涨薪,大概平薪就行。

首月面了三家公司,发现了自己的诸多漏洞,项目比较垂类,讲解过程混乱;基础知识复习不足,很多新出来的延展概念了解不够。


第二个月


上个月期盼的奖金到账了,有些庆幸,又有些失落。庆幸的是收到一笔不菲的补偿金,失落的是下月开始就没有收入了。

发现面试机会变少了,整月才面了三四家,这个月发现的问题,更多的是从架构角度来的,诸如幂等、一致性hash等场景,个人了解的相对简单了。


第三个月


广深的工作机会实在是少,开始同时投递其他城市的岗位试水。月初一家公司现场面了4轮都很顺利,第二天最后一轮CTO面,被嘲讽之前业务简单,比较受打击。月底面其他城市的岗位,一面过后第二天晚上10点又被拉上线做一面的补充面。

开始焦虑了,一想到还没找到工作,补偿金估计一两个月也会花完,可能要动用积蓄了,心跳就加速,越想越加速。努力想让自己变得不去想,只去想没有掌握的知识点,算是熬过了这个月。


第四个月


这个月,感觉蛮顺利,月初面一家大厂,技术面、主管面、HR面、提交资料都很顺利,感觉稳了,每天都看看公众号的面试状态,希望能快点沟通offer;月中也走完了一家中厂的4轮面试流程;月底又走完了另一家新能源车企的面试流程。

整个月过完,自己感觉飘了,感觉同时手握3个offer机会,晚几天随便一家给offer call就去了。个人心态一下子就变了,月内简历几乎没怎么投了,看知识点好像也没那么认真了。


第五、第六个月


好吧,上个月的3个机会,全都没有等来,继续面试。心态有点躺平,焦虑感少了,颓废感来了,BOSS直聘岗位几乎能投的都投过了,没有面试的日子,会过得略显浑浑噩噩,不知道要做什么。
陆续来了几个offer,也终于决定下来了,降薪差不多40%,但好在稳定性应该有保障。


心态的转变




  • 从渴望周末,到期盼工作日


    工作时渴望周末的休息 ,没找到工作时,每一个周末的到来,都意味着本周没有结果,而过完周末,意味着过完了1/4月。感觉日子过得好快,以前按天过,现在按周过,半年时间感觉也只是弹指一挥间。

    每一个周一的到来,意味着拥抱新的机会。每周的面试频率比较高时,会感到更充实;面试频率低下来时,焦虑感会时不时的涌上心头,具体表现是狂刷招聘软件,尝试多投递几个职位。




  • 肯定 -> 否定 -> 肯定


    找工作初期,信心满满。定制计划,每天刷多少题,每天看什么知识点,应该按照什么节奏投递简历,自己全都规划好了

    中期,备受打击,总有答不上来的问题,有些之前看过的知识点,临场也会突然忘记,感觉太糟糕了。

    后期,受的打击多了,自己不会的越来越少,信心又回来了




可能能解决你的问题


要不要和家里人说


自己这半年下来,没有和家里人说,每周还是固定时间给家里打电话,为了模拟之前在路边遛弯打电话,每次电话都会坐在阳台。

个人情况是家在北方,本人在南方,和爸妈说了只能徒增他们的焦虑,所以我先瞒着了。


被裁员,是你的问题吗?


在找工作的初期,总会这样问自己,是不是自己选错了行业,是不是自己不该跳槽,会陷入一种自责的懊恼情绪。请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术。


用什么招聘软件


我用了BOSS直聘和猎聘两个,建议准备好了的话,可以再多搞几个平台海投。另外需要注意几点:



  1. 招聘者很久没上线,对应岗位应该是不招的

  2. 猎聘官方会不定期打电话推荐岗位,个人感觉像是完成打电话KPI,打完电话或加完微信后就没有后续跟进消息了

  3. 你看岗位信息,招聘者能看到你的查看记录,如果对某个岗位感兴趣,怕忘记JD要求,可以截图保存,避免暴露特别感兴趣的想法被压价


在哪复习


除非你已经有在家里持续专注学习的习惯,否则不管你有没有自己的书房,建议还是去找一个自习室图书馆,在安静的氛围中,你会更加高效、更加专注。

如果只能在家里复习,那么远离你的手机,把手机放到其他房间,并确保有电话你能听到,玩手机会耗费你的专注力和执行力。

(你在深圳的话,可以试试 南山书房 ,在公众号可以预约免费自习室,一次两小时)


如何度过很丧的阶段


多多少少都会有非常沮丧的阶段,可能是心仪的offer最终没有拿到手,可能是某些知识点掌握不牢的自我批判。

沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,社会在一直进步,你也不能落下。


一些建议


1. 项目经历


讲清楚几点:




  • 项目背景


    让人明白项目解决了什么问题,大概是怎么流转的,如果做的比较垂类,还需要用通俗易懂的话表达项目中的各个模块。




  • 你在其中参与的角色


    除了开发之外,是否还承担了运维、项目管理等职责,分别做了什么




  • 取得的成果


    你的高光时刻,比如解决了线上内存泄漏问题、消息堆积问题、提升了多少QPS等,通常这些亮点会被拿出来单独问,所以成果相关的延展问题也需要提前想好




还比较重要的是,通过项目介绍,引导面试官的问题走向,面试只通过几十分钟的时间来对你做出评价,其实不够客观,你需要做的是在这几十分钟的时间内尽可能的放大你的优势



除此之外,还需要做项目的延展思考



比如我自己,刚工作时做客户端开发,负责客户端埋点模块的重构,面试时被问到,“如果让你设计一个埋点服务端系统,你会考虑哪些方面”? 对于这类问题,个人感觉需要在场景设计类题目下功夫,需要了解诸如秒杀抢购等场景的架构实现方案,以及方案解决的痛点问题,这类问题往往需要先提炼痛点问题,再针对痛点问题做优化。


2. 知识点建议


推荐两个知识点网站,基本能涵盖80%的面试知识点,通读后基本能实现知识点体系化

常用八股 -- JavaGuide

操作系统、网络、MYSQL、Redis -- 小林coding


知识成体系,做思维导图进行知识记忆

那么多知识点,你是不可能全都记全的,部分知识点即使滚瓜烂熟了,半个月后基本也就忘光了。让自己的知识点成框架、成体系,比如Redis的哨兵模式是怎么做的,就需要了解到因为要确保更高的可用性,引入了主备模式,而主备模式不能自动进行故障切换,所以引入了哨兵模式做故障切换。

不要主观认为某个知识点不会被问到

不要跳过任何一个知识点,不能一味的把认为不重要的知识点往后放,因为放着放着可能就不会去看了。建议对于此类知识点,先做一个略读,做到心中大概有数,细节不必了解很清楚,之后有空再对细节查漏补缺。

之前看到SPI章节,本能认为不太重要,于是直接略过,面试中果然被问到(打破双亲委派模型的方式之一),回过头再去看,感觉其实不难,别畏惧任何一个知识点。

理论结合实践

不能只背理论,需要结合实践,能实践的实践,不能实践的最好也看看别人的实现过程。

比如线程顺序打印,看知识点你能知道可以使用join、wait/notify、condition、单线程池等方式完成,但如果面试突然让你写,对于api不熟可能还是写不出。

又比如一些大型系统的搭建,假如是K8S,你自己的机器或者云服务器没有足够的资源支撑一整套系统的搭建,那么建议找一篇别人操作的博客细品。

不要强关联知识点

被面试官问到一些具体问题,不要强行回答知识点,可能考察的是一个线上维护经验,此时答知识点可能给面试官带来一个理论帝,实操经验弱的感觉。

举两个例子,被问过线上环境出问题了,第一时间要如何处理?,本能的想到去看告警、基于链路排查工具排查是哪个环节出了问题,但实际面试官想得到的答案是版本回滚,第一时间排查出问题前做了什么更新动作,并做相应动作的回滚;又被问过你调用第三方服务失败了,如何本地排查问题?,面试官想考察的是telnet命令,因为之前出现过网络环境切换使用不同hosts配置,自己回答的是查看DNS等问题,这个问题问的并不漂亮,但是也反映出强关联知识点的问题。

建立自己的博客,并长期更新

养成写博客的习惯,记录自己日常遇到的问题,日常的感受,对于某些知识点的深度解析等。面试的几十分钟,你的耐心,你解决问题的能力,没办法完全展示,这时候甩出一个持续更新的博客,一定是很好的加分项。同时当你回顾时,也是你留下的积累和痕迹。



半年很长,但放在一生来看却又很短

不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你早日上岸



作者:雪梨酒色托帕石
来源:juejin.cn/post/7274229908314308666
收起阅读 »

compose 实现时间轴效果

新项目完全用了compose来实现,这两天有个时间轴的需求,搜索了一下完全由compose实现的几个,效果都不算特别好,而且都是用canvas画的,这样的话和原来的view没什么区别,不能发挥compose可定制组合的长处,所以自己实现了一个。由于我自己平时基...
继续阅读 »

新项目完全用了compose来实现,这两天有个时间轴的需求,搜索了一下完全由compose实现的几个,效果都不算特别好,而且都是用canvas画的,这样的话和原来的view没什么区别,不能发挥compose可定制组合的长处,所以自己实现了一个。由于我自己平时基本不写文章,并且内容也是偏向compose新手的,所以可能写的比较啰嗦,大佬们想看的可以直接跳到第三部分。欢迎指导!



在开始之前,先介绍一下这次实现的重点:Layout


Layout用于实现自定义的布局,可用于测量和定位其布局子项。我们可以用这个实现之前自定义view的效果,不过这里画的不是点线之类的东西,而是composable,并且只用计算放的位置就好,基于此我们可以实现有多个插槽的布局。


先来看一下UI效果是什么样的
体检报告详情.png


一、分解UI


通过观察UI,我们可以将每个item分解为以下四个元素:圆点、线、时间、内容。一个合格的组件,要允许使用者随意定义各个元素位置的实现,比如圆点可能变成方的,或者换成图片,线也可能是条实线,并且颜色是渐变的。所以这里这几个元素准确的来说,应该是四个插槽,这几个插槽提供了默认的样式是长这样。


圆点槽和时间槽是垂直居中对齐的,圆点槽和线槽是水平居中对齐的,内容槽和时间槽是左对齐,在圆点槽和时间槽中间有一定间距,我们管他叫内容距左间距。


每个item的最大宽度是圆点槽的宽+内容距左间距+内容的宽。每个item的最大高度是圆点或者时间槽的最大高度+内容的高度,不直接用时间槽的高度是因为圆点槽如果放个图片的话,可能高度比时间槽的高度要高。


由于这个线应该是连接两个圆点槽的,所以它的最大高度和最小高度其实都是一个,取决于两个圆点之间的距离,正好是一个item的高度。


在多个item时,第一个元素的线从点开始往下,而最后一个则没有线(说高度为0也行)


二、实现每个插槽的默认UI



  • 圆点


这个很简单,任意一个空的组件设置下修饰符就可以了。


Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape) // 变圆
.background(MaterialTheme.colorScheme.primary)
)


  • 线


实线很好实现,也通过background就可以


// 实线单色
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(MaterialTheme.colorScheme.primary)
)

// 渐变也简单
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(
Brush.linearGradient(
listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primaryContainer
)
)
)
)

虚线稍微麻烦一点,Brush中没有直接实现虚线的方法,所以我用drawBehind来实现了。drawBehind这里的作用和Canvas()是一样的,你可以直接用canvas来实现,重点就是里面的pathEffect。


Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.drawBehind {
drawLine(
color = Color.LightGray,
strokeWidth = size.width,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = size.height),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(8.dp.toPx(), 4.dp.toPx())
)
)
}
)


  • 时间


简单一个Text就可以。


Text("2023928日")


  • 内容


根据具体的内容来实现。


三、通过自定义的Layout将小UI组装起来


现在我们根据第一步的思路,来定义一个组件。


@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit, // 圆点槽
line: @Composable () -> Unit, // 线槽
time: @Composable () -> Unit,// 时间槽
content: @Composable () -> Unit, // 内容槽
contentStartOffset: Dp = 8.dp // 内容距左间距
)

然后我们将第二步中的插槽的默认UI放上去。主要是圆点槽和线槽。


@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit = {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
)
},
line: @Composable () -> Unit = {
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.drawBehind {
drawLine(
color = Color.LightGray,
strokeWidth = size.width,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = size.height),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(8.dp.toPx(), 4.dp.toPx())
)
)
}
)
},
time: @Composable () -> Unit,
content: @Composable () -> Unit,
contentStartOffset: Dp = 8.dp
)

定义好以后就可以开始做实现了,上面已经说过,我们是通过自定义Layout来实现的,那么先看一下Layout的构成。


@UiComposable
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit, // 可组合子项。
modifier: Modifier = Modifier, // 布局的修饰符
measurePolicy: MeasurePolicy //布局的测量和定位的策略
)

这其中的content,就是指我们这四个槽的内容。


Layout(
modifier = modifier,
content = {
dot()
// 通过ProvideTextStyle给时间槽提供了一个默认字体颜色。
ProvideTextStyle(value = LocalTextStyle.current.copy(color = Color(0xff999999))) {
time()
}
content()
line()
},
measurePolicy = ...

我们可以看到在content中,我们将四个槽的内容全放进去了,那他们的位置和大小是怎么决定的呢,就是在measurePolicy中定义的。
MeasurePolicy类要求我们必须实现measure方法。


fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
)
: MeasureResult

measurables列表中的每个Measurable都对应于布局的一个布局子级,就是我们刚才在content中传入的内容,将按先后顺序存入这个列表。可以使用Measurable.measure方法来测量子级的大小。该方法需要子级自己所需要的约束Constraints(就是这个子级的最小最大尺寸);不同的子级可以用不同的约束来测量,而不是统一用给出的这个constraints参数。测量子级会返回一个Placeable,它的属性有该子级经过对应约束测量后的大小(一旦经过测量,这个子级的大小就确定了,不能再次测量)。最后在MeasureResult中,设置每个子级的位置就可以。


现在我们的代码变成了这样:


@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit = {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
)
},
line: @Composable () -> Unit = {
Box(modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.drawBehind {
drawLine(
color = Color.LightGray,//Color(0xffeeeeee)
strokeWidth = size.width,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = size.height),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(8.dp.toPx(), 4.dp.toPx())
)
)
}
)
},
time: @Composable () -> Unit,
content: @Composable () -> Unit,
contentStartOffset: Dp = 8.dp,
position: TimelinePosition = TimelinePosition.Center
) {
Layout(
modifier = modifier,
content = {
dot()
ProvideTextStyle(value = LocalTextStyle.current.copy(color = Color(0xff999999))) {
time()
}
content()
line()
},
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
TODO: 具体的四个子级测量大小的位置设置。
}
}
)
}

现在我们来做具体的实现。
我们先来测量一下这里的圆点槽的大小。
val dot = measurables[0].measure(constraints)
因为我们在content中第一个传入的就是dot(),所以这里measurables[0]就是圆点槽组件,这样就得到了其对应的Placeable。
我们先放置下这个圆点槽显示下看看效果。


override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
)
: MeasureResult {
val dot = measurables[0].measure(constraints)

return layout(constraints.maxWidth, constraints.maxHeight) {
dot.place(0, 0, 1f)
}
}

理论上我们应该看到一个大小8dp,主题色的圆点在左上角。大家可以跑一下看看是不是符合预期。


要指出的是,这个方法给出的constraints并不是合适dot的约束,其最小宽度将可能远远大于dot的宽,这将导致测量后dot的宽远超设定的8dp。所以这里我们需要使用dot正确的约束, 而这个圆点槽理论上是不限制大小的,所以其最小宽度应该设置为0。我们依次将圆,时间,和内容的大小也测量出来。


val constraintsFix = constraints.copy(minWidth = 0)
val dot = measurables[0].measure(constraintsFix)
val time = measurables[1].measure(constraintsFix)
val content = measurables[2].measure(constraintsFix)

之所以不一并把线槽的大小也测量了,是因为我们在第一步中说的,线槽的高度,实际上是由圆点或者时间槽的最大高度+内容的高度来决定的。


val topHeight = max(time.height, dot.height) // 取圆点槽和时间槽中最大槽位的高度。
val lineHigh = topHeight + content.height // 整个组件的高度
val line = measurables[3].measure(
constraints.copy(
minWidth = 0,
minHeight = lineHeight,
maxHeight = lineHeight
)
)

至此我们已经将四个槽位的大小全部确定了下来。接下来就该指定每个槽位的位置,在第一步我们已经分析过每个槽位应该所在的位置。


val height = topHeight + content.height // 整个组件的高度
// 时间或内容的最大宽度 + 内容距左间距 + 圆点宽度 = 整个组件的宽度
val width =
max(content.width, time.width) + contentStartOffset.roundToPx() + dot.width

return layout(width, height) { // 设置layout占据的大小
val dotY = (topHeight - dot.height) / 2 // 计算圆点槽y轴位置
dot.place(0, dotY, 1f) // 放圆点槽
val timeY = (topHeight - time.height) / 2 // 计算时间槽y轴位置
time.place(dot.width + contentStartOffset.roundToPx(), timeY) // 放时间槽
content.place(dot.width + contentStartOffset.roundToPx(), topHeight) // 放内容槽,x和时间槽一样,形成左对齐效果。
line.place(
dot.width / 2, // x在圆中间
dotY + dot.height // y从圆的最下面开始
)
}

至此我们就有了一个时间轴节点组件,马上在LazyColumn或者Column中试试效果吧!


四、完善效果


如果你刚才测试了效果,你会发现,在列表中最后一个节点,也有虚线,并且长度超出了列表,而最后一个节点,不应该显示虚线才对。所以我们要来完善一下效果。


@Composable
fun TimelineItem(
modifier: Modifier = Modifier,
dot: @Composable () -> Unit = ...,
line: @Composable () -> Unit = ...,
time: @Composable () -> Unit,
content: @Composable () -> Unit,
contentStartOffset: Dp = 8.dp,
isEnd: Boolean = false, // 添加是否为最后一个节点的参数
)
...
//在最后根据是否是最后一个节点来设置是否放置线槽内容。
if (!isEnd){
line.place(
dot.width / 2,
dotY + dot.height
)
}

而在调用时,只要简单的根据是否位于列表最后就可以了,调用示例:


LazyColumn(
Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
itemsIndexed(list.itemSnapshotList) { index, item ->
item?.let {
TimelineItem(
modifier = Modifier.fillMaxWidth(),
time = {
Text(text = it.time)
},
content = {
Column {
// 最好在Column最上面和最下面也添加个spacer来间隔开
}
},
isEnd = index == list.itemCount - 1
)
}
}
}

最后


至此本文就结束啦,由于内容比较简单,且所以的代码均有表现,为了不占篇幅,就不再粘贴完整代码内容了。如果本文有错误之处或者可以改进的地方,请大家一定回复指正;如果文章的内容也对你有帮忙,也请回复鼓励我,谢谢大家!


作者:拎壶冲
来源:juejin.cn/post/7283719464906244151
收起阅读 »

为什么需要弱引用 wp?

Android 中的智能指针是通过引用计数的方式方式来实现内存自动回收的。在大多数情况下我们使用强指针 sp 就好了,那么弱指针 wp 的存在意义有是什么呢? 从使用的角度来说,wp 扮演的是一个指针缓存的角色,想用时候可以用,但不想因此阻止资源被释放。其实,...
继续阅读 »

Android 中的智能指针是通过引用计数的方式方式来实现内存自动回收的。在大多数情况下我们使用强指针 sp 就好了,那么弱指针 wp 的存在意义有是什么呢?


从使用的角度来说,wp 扮演的是一个指针缓存的角色,想用时候可以用,但不想因此阻止资源被释放。其实,简单的裸指针也能很好地完成指针缓存的功能,其功能性并不是 wp 存在的必要条件。


wp 存在的核心原因是:解决循环引用导致的死锁问题


1. 循环引用导致的死锁问题


接下来,我们就通过一个简单的示例程序来演示循环引用导致的死锁问题


首先有两个类,其内部都有一个智能指针指向对方,形成循环引用:


Class A : public RefBase
{
public:
A()
{

}

virtual ~A()
{

}

void setB(sp& b)
{
mB = b;
}

private:
sp mB;
}

Class B : public RefBase
{
public:
B()
{

}

virtual ~B()
{

}

void setA(sp& a)
{
mA = a;
}

private:
sp
mA;
}

整体结构如下图所示:



接下来看 main 函数:


int main(int argc, char** argv)
{
//初始化两个指针
A *a = new A();
B *b = new B();

// 触发构造函数调用 spA 内部强弱计数值 (1,1)
sp
spA = a;
// 触发构造函数调用 spB 内部强弱计数值 (1,1)
sp spB = b;

//setB 内部有赋值操作 mB = b,触发等于操作符函数重载
//spB 内部强弱计数值 (2,2)
spA->setB(spB);

//setA 内部有赋值操作 mA = a,触发等于操作符函数重载
//spA 内部强弱计数值 (2,2)
spB->setA(spA);

return 0;
// spA 析构 内部强弱计数值 (1,1),内存无法回收
// spB 析构 内部强弱计数值 (1,1),内存无法回收
}

//等于操作符函数重载
template<typename T>
sp& sp::operator =(const sp& other) {
// Force m_ptr to be read twice, to heuristically check for data races.
T* oldPtr(*const_castvolatile*>(&m_ptr));
T* otherPtr(other.m_ptr);
// 强弱引用计数分别加 1
if (otherPtr) otherPtr->incStrong(this);
if (oldPtr) oldPtr->decStrong(this);
if (oldPtr != *const_castvolatile*>(&m_ptr)) sp_report_race();
m_ptr = otherPtr;
return *this;
}

从这个示例可以看出,在循环引用的情况下,指针指针在作用域结束后,强弱引用计数值无法变回 (0,0),内存无法回收,导致内存泄漏;


2. 解决方案


只需要把其中一个智能指针改为弱引用即可解决上面的问题:


Class A : public RefBase
{
public:
A()
{

}

virtual ~A()
{

}

void setB(sp& b)
{
mB = b;
}

private:
sp mB;
}

Class B : public RefBase
{
public:
B()
{

}

virtual ~B()
{

}

//函数参数也要变一下
void setA(sp
& a)
{
//触发另外的等于操作符函数重载
mA = a;
}

private:
//这里改成 wp 弱引用
wp
mA;
}

主函数稍作修改:


int main(int argc, char** argv)
{
//初始化两个指针
A *a = new A();
B *b = new B();

// 触发构造函数调用 spA 内部强弱计数值 (1,1)
sp
spA = a;
// 触发构造函数调用 spB 内部强弱计数值 (1,1)
sp spB = b;

//setB 内部有赋值操作 mB = b,触发等于操作符函数重载
//spB 内部强弱计数值 (2,2)
spA->setB(spB);

//setA 内部有赋值操作 mA = a,触发等于操作符函数重载
//spA 内部强弱计数值 (1,2)
spB->setA(spA);

return 0;
// spB 析构 内部强弱计数值 (1,1),内存无法回收
// spA 析构 内部强弱计数值 (0,1),强引用为 0 ,回收 sp
spA 内部的目标对象 A,
// 随着 A 的析构, A 的成员变量 mB 也开始析构, 目标对象 B 强弱引用计数减 1,内部强弱计数值变为 (0,0),回收目标对象 B 以及内部管理对象,B 对象的内存回收工作完成,接着触发 B 对象的成员 mA 的析构函数
// mA 执行析构函数,弱引用计数减 1,内部强弱计数值变为 (0,0),回收 A 对象内部对应的管理对象,A 对象的内存回收工作完成
}

//等于操作符函数重载
template<typename T>
wp& wp::operator = (const sp& other)
{
weakref_type* newRefs =
other != nullptr ? other->createWeak(this) : nullptr; //增加弱引用计数
T* otherPtr(other.m_ptr);
if (m_ptr) m_refs->decWeak(this);
m_ptr = otherPtr;
m_refs = newRefs;
return *this;
}

当程序的一个引用修改为 wp 时,main 函数结束时:



这样就解决了上一节中提出的内存泄漏问题!


3. 总结



  • wp 的基本作用:wp 扮演了指针缓存的角色,想用时候可以用,但不想因此阻止资源被释放

  • wp 存在的根本原因:解决循环引用导致的死锁问题

作者:阿豪讲Framework
来源:juejin.cn/post/7283376651906646035

收起阅读 »

一篇文章让你的网站拥有CDN级的访问速度,告别龟速个人服务器~

web
通常来说,前端加快页面加载的手段无非是缩小文件、减少请求等几种常见的方式,但如果说页面加载慢的本质原因是因为没有CDN服务和服务器带宽限制这些非前端代码因素,那么前端代码再怎么优化,加载速度还是会差强人意。 最常见的就是我们在各大云平台白嫖的新人专享的服务器或...
继续阅读 »

通常来说,前端加快页面加载的手段无非是缩小文件、减少请求等几种常见的方式,但如果说页面加载慢的本质原因是因为没有CDN服务和服务器带宽限制这些非前端代码因素,那么前端代码再怎么优化,加载速度还是会差强人意。


最常见的就是我们在各大云平台白嫖的新人专享的服务器或者是那种配置很低的服务器,虽说能用,但是用个IP访问网站就算了,关键是还是很慢,一个1M的JS文件都能加载几秒钟。


关于彻底解决这个问题,我有一个一劳永逸的办法……


首先我们要明确的是,访问速度慢是因为服务器带宽限制以及没有CDN的支持,带宽限制就是从服务器获取资源的最大速度,CDN就是内容分发网络,简单理解就是你在世界上任意位置访问某个CDN资源,通过CDN服务就可以从离你最近的一台CDN服务器上获取资源,简单粗暴地优化远距离访问导致的物理延迟的问题。


CDN前后对比


首先我们来看一个小网站直接部署在一个某云平台最基础的服务器上访问的速度:


image.png
可以看到的是加载速度惨不忍睹,这还只是一个页面的网站,如果再大一点加上没有浏览器缓存的第一次访问,网站的响应速度应该随随便便破10秒。


接着我们再看看经过CDN加速的网站访问速度:


image.png


可以看到的是速度有了极大的提升,而且我们访问的资源除了index.html,也就是上图中的第一行请求是直接访问我们自己的服务器获取的,其他都是走的CDN服务。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="http://static.admin.rainbowinpaper.cn/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>纸上的彩虹-管理端</title>
<script type="module" crossorigin src="http://static.admin.rainbowinpaper.cn/assets/index.f1217c6c.js"></script>
<link rel="stylesheet" href="http://static.admin.rainbowinpaper.cn/assets/index.a5fafcaf.css">
</head>
<body>
<div id="main-app"></div>
</body>
</html>

首先我们访问的地址是:admin.rainbowinpaper.cn,而网站中所有资源的加载地址是:static.admin.rainbowinpaper.cn,所以后者就是一个映射到CDN服务的地址。


准备域名


在我们准备把自己的项目接入CDN之前我们首先要注册一个域名并且备案好,关于域名如何注册备案的问题,我这里不过多赘述,你可以去的买服务器的云平台搜索域名注册,随便买个几块钱一年的便宜域名,然后按照平台提示的备案流程完成后续操作,我这里从准备好域名说起。


有了域名后我们就可以先把自己用IP访问服务器改用域名访问,操作方法也很简单,就是在你所购买的平台的域名管理里面加一行解析:


image.png


如图所示,类型为A,将域名指向ipv4地址,注意打开解析域名必须要备案,不然会被屏蔽访问


现在试试直接用域名能不能访问到你的网站。


准备CDN


网上提供CDN服务的平台有很多,我这里以七牛云作为CDN服务平台,毕竟免费的CDN服务真的很香。


首先我们去七牛云注册一个账号,然后新建一个存储空间:


image.png
然后绑定自定义域名:


image.png


这里我们可以随便写一个二级域名,比如我们的域名是rainbowinpaper.cn,那我们的加速域名就可以填写img.rainbowinpaper.cn


其他的保持默认,我们直接创建,当我们在七牛云新建域名的时候需要验证你对当前域名的所有权,所以需要按照七牛云的提示去管理你域名的平台加一条解析记录,这一条仅作为验证所有权,无实际作用,大致如下:


image.png


当七牛云验证成功后,你需要再加一条域名的解析记录,就是解析你刚才在七牛云填写的加速域名:


image.png


注意值那一行,是七牛云提供的CNAME。关于如何配置,七牛云也有帮助文档可以查看,都很简单。


当我们配置好了再回七牛云域名管理就能看到如下的状态:


image.png


现在我们可以去刚刚创建的空间里面上传一张图片,查看详情里面的链接是否能访问,如果访问到我们刚才上传的图片,就说明成功了。


image.png


到此为止我们的准备工作都完成了,准备上代码!


自动化上传打包文件


前面我提到了,访问网站除了index.html是从服务器获取的其他文件都是从CDN服务器上获取的,其原理就是修改了项目打包时的base值(图中所示的是vite项目的配置,其他打包工具请自行兼容),让所有引入的静态文件指向CDN的加速域名,而不是从源服务器去获取。


image.png


到这里指向变了,但是我们不可能每次更新项目都要手动上传打包文件到七牛云里面,所以我们需要写一个脚本自动将打包文件上传到七牛云。话不多说直接上代码:


/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const qiniu = require('qiniu');
const chalk = require('chalk');

const { ak, sk, bucket } = {
ak: '你的ak',
sk: '你的sk',
bucket: '你刚才创建的存储空间名',
};

const mac = new qiniu.auth.digest.Mac(ak, sk);

const config = new qiniu.conf.Config();
// 你创建空间时选择的存储区域
config.zone = qiniu.zone.Zone_z2;
config.useCdnDomain = true;

const bucketManager = new qiniu.rs.BucketManager(mac, config);

/**
* 上传文件方法
* @param key 文件名
* @param file 文件路径
* @returns {Promise<unknown>}
*/

const doUpload = (key, file) => {
console.log(chalk.blue(`正在上传:${file}`));
const options = {
scope: `${bucket}:${key}`,
};
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
return new Promise((resolve, reject) => {
formUploader.putFile(uploadToken, key, file, putExtra, (err, body, info) => {
if (err) {
reject(err);
}
if (info.statusCode === 200) {
resolve(body);
} else {
reject(body);
}
});
});
};

const getBucketFileList = (callback, marker, list = []) => {
!marker && console.log(chalk.blue('正在获取空间文件列表'));
const options = {
limit: 100,
};
if (marker) {
options.marker = marker;
}
bucketManager.listPrefix(bucket, options, (err, respBody, respInfo) => {
if (err) {
console.log(chalk.red(`获取空间文件列表出错 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err)}`));
throw err;
}
if (respInfo.statusCode === 200) {
// 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候,
// 指定options里面的marker为这个值
const nextMarker = respBody.marker;
const { items } = respBody;
const newList = [...list, ...items];
if (!nextMarker) {
console.log(chalk.green(`获取空间文件列表成功 ✓`));
console.log(chalk.blue(`需要清理${newList.length}个文件`));
callback(newList);
} else {
getBucketFileList(callback, nextMarker, newList);
}
} else {
console.log(chalk.yellow(`获取空间文件列表异常 状态码${respInfo.statusCode}`));
console.log(chalk.yellow(`异常信息:${JSON.stringify(respBody)}`));
}
});
};

const clearBucketFile = () =>
new Promise((resolve, reject) => {
getBucketFileList(items => {
if (!items.length) {
resolve();
return;
}
console.log(chalk.blue('正在清理空间文件'));
const deleteOperations = [];
// 每个operations的数量不可以超过1000个,如果总数量超过1000,需要分批发送
items.forEach(item => {
deleteOperations.push(qiniu.rs.deleteOp(bucket, item.key));
});
bucketManager.batch(deleteOperations, (err, respBody, respInfo) => {
if (err) {
console.log(chalk.red(`清理空间文件列表出错 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err)}`));
reject();
} else if (respInfo.statusCode >= 200 && respInfo.statusCode <= 299) {
console.log(chalk.green(`清理空间文件成功 ✓`));
resolve();
} else {
console.log(chalk.yellow(`获取空间文件列表异常 状态码${respInfo.deleteusCode}`));
console.log(chalk.yellow(`异常信息:${JSON.stringify(respBody)}`));
reject();
}
});
});
});

const publicPath = path.join(__dirname, '../../dist');

const uploadAll = async (dir, prefix) => {
if (!prefix){
console.log(chalk.blue('执行清理空间文件'));
await clearBucketFile();
console.log(chalk.blue('正在读取打包文件'));
}
const files = fs.readdirSync(dir);
if (!prefix){
console.log(chalk.green('读取成功 ✓'));
console.log(chalk.blue('准备上传文件'));
}
files.forEach(file => {
const filePath = path.join(dir, file);
const key = prefix ? `${prefix}/${file}` : file;
if (fs.lstatSync(filePath).isDirectory()) {
uploadAll(filePath, key);
} else {
doUpload(key, filePath)
.then(() => {
console.log(chalk.green(`文件${filePath}上传成功 ✓`));
})
.catch(err => {
console.log(chalk.red(`文件${filePath}上传失败 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err)}`));
console.log(chalk.blue(`再次尝试上传文件${filePath}`));
doUpload(file, filePath)
.then(() => {
console.log(chalk.green(`文件${filePath}上传成功 ✓`));
})
.catch(err2 => {
console.log(chalk.red(`文件${filePath}再次上传失败 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err2)}`));
throw new Error(`文件${filePath}上传失败,本次自动化构建将被强制终止`);
});
});
}
});
};

uploadAll(publicPath).finally(() => {
console.log(chalk.green(`上传操作执行完毕 ✓`));
console.log(chalk.blue(`请等待确认所有文件上传成功`));
});


代码逻辑就是获取存储空间所有文件后删除,然后获取本地打包文件后上传,这样存储空间的文件不会一直堆积,所以这个存储空间只能存放项目的静态文件。


其中需要注意的是,需要在七牛云的秘钥管理中生成一对密钥写入代码中。


package.json中写入上传指令:


image.png


运行指令,打印日志如下:


image.png


这时候再到七牛云的空间看下看见文件是否已经存在,这时候再访问下网站,如果能正确加载网站,说明就大功告成了。


说在后面


我之前在做自动化部署的时候发现自己的网站总是访问的很慢,但又是因为不想花更多的钱买更好的服务器,所以就被迫去研究到底哪些方法可以立竿见影的让网站加快访问速度,于是就有了本文。


总而言之,实践是检验真理的唯一标准,网上关于加快网页加载的文章一大堆,不是说它们没用,只是我们都是在前人的经验上去直接照搬的,这样就缺少了自己实践成功的那种成就感,关于这些技术点的由来可能还是一知半解,所以看过别人的文章,不如自己亲自实验一番。


最后,如有问题欢迎评论区讨论。


作者:纸上的彩虹
来源:juejin.cn/post/7283682738498273317
收起阅读 »

ThreadLocal使用不规范,上线两行泪

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜 ThreadLocal是Java中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点...
继续阅读 »

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜





ThreadLocalJava中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点,对于此网上已经有很多经典文章来进行分析,但今天我们主要分析笔者在项目中遇到的一个错误使用ThreadLocal的示例,并针对错误原因进行深入剖析,理论结合实践让你更加透彻的理解ThreadLocal的使用。


前言


Java中的ThreadLocal是一种用于在多线程环境中存储线程局部变量的机制,它为每个线程都提供了独立的变量副本,从而避免了线程之间的竞争条件。事实上,ThreadLocal的工作原理是在每个线程中创建一个独立的变量副本,并且每个线程只能访问自己的副本。


进一步,ThreaLocal可以在当前线程中独立的保存信息,这样就方便同一个线程的其他方法获取到该信息。 因此,ThreaLocal的一个最广泛的使用场景就是将信息保存,从而方便后续方法直接从线程中获取。


使用ThreadLocal出现的问题


明白了ThreaLocal的应应用场景后,我们来看一段如下代码:



控制层



@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

@Autowire
private UserService userService;

@GetMapping("get-userdata-byId")
public CommonResult<Object> getUserData(Integer uid) {

return userService.getUserInfoById(uid);

}


服务层



@Service
public class UserService {

ThreadLocal<UserInfo> locals = new ThreadLocal<>();

public CommonResult<UserInfo> getUserInfoById ( String uid) {
UserInfo info = locals.get();

if (info == null) {
// 调用uid查询用户
UserInfo userInfo = UserMapper.queryUserInfoById(uid);
locals.set(userInfo);
}
// ....省略后续会利用UserInfo完成某些操作

return CommonResult.success(info);
}
}

(注:此处为了方便复现项目代码进行了简化,重点在于理解ThreaLocal的使用)


先来简单介绍一下业务逻辑,前台通过url访问/user/get-userdata-byId后,后端会根据传入的uid信息查询用户信息,以避免进而根据用户信息执行相应的处理逻辑。进一步,在服务层中会缓存当前id对应的用户信息,避免频繁的查询数据库。


直观来看,上述代码似乎没问题。但最近用户反馈会出现这样一个问题,就是用户A登录系统后,查询到的可能是用户B的信息,这个问题就很诡异。遇到问题不要慌,不妨来看看笔者是如何进行思考,来定位,解决问题的。


首先,用户A登录系统后,前端访问/user/get-userdata-byId时携带的uid信息肯定是用户Auid信息;进一步,传到控制层getUserData处的uid信息肯定是用户Auid。所以,发生问题一定发生在UserService中的getUserInfoById方法。


进一步,由于用户传入的uid信息没有问题,那么传入getUserInfoById方法也肯定没有问题,所以问题发生地一定在getUserInfoById中获取用户信息的位置。所以不难得出这样的猜测,即问题大概率在 UserInfo info = locals.get()这行代码。


为了加深理解,我们再来回顾一下问题。"即用户A登录,最终却查询到用户B相关的信息"。 其实,这个问题本质其实在于数据不一致。众所周知,造成数据不一致的原因有很多,但归根到底其实无非就是:“存在多线程访问的资源信息,进一步,多线程的存在导致数据状态的改变原因不唯一”


Spring中的Bean都是单例的,也就是说Bean中成员信息是共享的。换句话说, 如果Bean中会操纵类的成员变量,那么每次服务请求时,都会对该变量状态进行改变,也就会导致该变量成员那状态不断发生改变。


具体到上述例子,UserService中的被方法操纵的成员是什么?当然是locals这个成员变量啦! 至此,问题其实已经被我们定位到了,导致问题发生的原因在于locals变量。


说到此,你可能你会疑惑ThreadLocal不是可以保证线程安全吗?怎么使用了线程安全的工具包还会导致线程安全问题?


问题复现


况且你说是ThreadLocal出问题那就是ThreadLocal出问题吗?你有证据吗?所以,接下来我们将通过几行简单的代码,复现这个问题。



@RestController
@RequestMapping("/th")
public class UserController {

ThreadLocal<Integer> uids = new ThreadLocal<>();

@GetMapping("/u")
public CommonResult getUserInfo(Integer uid) {
Integer firstId = uids.get();
String firstMsg = Thread.currentThread().getName() + " id is " + firstId;
if (firstId == null) {
uids.set(uid);
}

Integer secondId = uids.get();
String secondMsg = Thread.currentThread().getName() + " id is " + secondId;

List<String> msgs = Arrays.asList(firstMsg,secondMsg);
return CommonResult.success(msgs);


}
}


  1. 第一次访问:uid=1


image.png



  1. 第二次访问:uid=2
    image.png


可以看到,对于第二次uid=2的访问,这次就出现了 Bug,显然第二次获取到了用户1的信息。其实,从这里就可以看出,我们最开始的猜测没有任何问题。


拆解问题发生原因


既然知道了发生问题的原因在于ThreadLocal的使用,那究竟是什么导致了这个问题呢?事实上,我们在使用ThreadLocal时主要就是使用了其的get/set方法,这就是我们分析的切入口。先来看下ThreadLocalset方法。


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

可以看到,ThreadLocalset方法逻辑大致如下:



  1. 首先,通过Thread.currentThread获取到当前的线程

  2. 然后,获取到线程当中的属性ThreadLocalMap。接着,对ThreadLocalMap进行判断,如果不为空,就直接更新要保存的变量值;否则,创建一个threadLocalMap,并且完成赋值。


进一步,下图展示了Thrad,ThreadLocal,ThredLocalMap三者间的关系。


image.png


回到我们例子,那导致出现访问错乱的原因是什么呢?其实很简单,原因就是 Tomcat 内部会维护一个线程池,从而使得线程被重用。从图中可以看到两次请求的线程都是同一个线程: http-nio-8080-exec-1,所以导致数据访问出现错乱。


image.png


那有什么解决办法吗?其实很简单,每次使用完记得执行remove方法即可。因为如果不调用remove方法,当面临线程池或其他线程重用机制可能会导致不同任务之间共享ThreadLocal数据,这可能导致意外的数据污染或不一致性。就如我们的例子那样。


总结


至此,我们以一个实际生产中遇到的一个问题为例由浅入深的分析了ThreadLocal使用不规范所带来的线程不安全问题。可以看到排查问题时,我们用到的不仅仅只有ThreadLocal的知识,更有多线程相关的知识。


可能平时我们也会抱怨学了很多线程知识,但工作中却很少使用。因为日常代码中基本写不到多线程相关的功能。但事实却是,很多时候只是我们没有意识到多线程的使用。例如,在Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境,否则接口也不可能支持这么高的并发,并不能单纯认为没有显式开启多线程就不会有线程安全问题。此外,虽然jdk提供很多线程安全的工具类,但其也有特定的使用规范,如果不遵循规范依旧会导致线程安全问题, 并不是使用了线程安全的工具类就一定不会出问题!


最后,再多提一嘴,学了的知识一定要用起来,可能你为了应付面试也曾看过ThreadLocal相关的面经,也知道使用ThreadLocal要执行remove,否则可能会导致内存泄露但编程的很多东西,确实需要自己实际操作,否则知识并不会凭空进入你的脑海。


选择了程序员这条路,注定只能不断的学习,大家一起共勉啦!另外,祝大家双节快乐!


作者:毅航
来源:juejin.cn/post/7283692887573184547
收起阅读 »

大专还有机会进大厂吗??

好多同学问我,月哥,大专还有机会进大厂吗?? 我虽然是做培训的,,,但是每当遇到这样的问题,,我总是不知道该如何的回答,,,我很想安慰你,但是说出去的话,只能打击你!!我不是神呢!以往有大专进大厂的案例,现在基本为0了。虽然我很想挣你的钱,但是我知道,我不可...
继续阅读 »

好多同学问我,月哥,大专还有机会进大厂吗??



  • 我虽然是做培训的,,,但是每当遇到这样的问题,,我总是不知道该如何的回答,,,我很想安慰你,但是说出去的话,只能打击你!!我不是神呢!以往有大专进大厂的案例,现在基本为0了。虽然我很想挣你的钱,但是我知道,我不可能给你画饼的,因为这个饼硌🦷

  • 如果你以前没有进过大厂,没有一线大厂的履历,以后应该也不会了,至少今年这个行情下,985进大厂的都不多,你99.9999%进不去了,留下0.0001%给你幻想下。


他们都说不卡学历



  • 我们不卡学历的,你只要足够优秀,,,然后简历给我看下,,,哦,不好意思,你不符合我们的招聘要求,,,问其原因,,拜拜了您!

  • 然后找内部的leader问,今年人太多了,985,211的都一堆简历,,,学历低的基本不看的,浪费时间,,,,,就尼玛现实,,,,!

  • 就算你很牛,但是,我就不给你面试机会,你怎么地。

  • 内推有用吗??基本没用,除非你简历本身就有类似上千star的开源项目的核心作者这种经验,不然很难让他们相信,你很牛批,,,而且面试的难度也是巨大的,,,如果面试官不是你二大爷的话!手动滑稽一波。


我考在职的计算机研究生,,,,



  • 你要说他毫无用处吧,也不合适,那么含金量基本为0.000001,,,我就不展开说了,懂的都懂!


所以,,,,铁子们



  • 疯狂刷题,进不了大厂,但是涨薪才是王道,进不进大厂不是那么重要,,,,围城而已,只是一份工作而已,虽然钱多些,但是这个钱不是很好拿!


想去大厂的路



  • 写好的文章,打造社区影响力

  • 参与开源,有好的开源项目

  • 学好算法和英语,远程国外


以上都是长线作战的事,很多人都坚持不下来的。但是坚持下来就会有很大的提升,光说不练假把式,想要得到,就得先做到,参考月哥的经历,得非常非常的长线的努力才行!


志不达,为人生常态




  • 我很想告诉你努力有用,但是,确实用处不大,因为你做不到,很多同学都是口嗨,连安静的看书一个小时都做不到,何谈进阶!




  • 调整好自己的心态,,,莫焦虑!得刷题!不然你连工作都不好找呢!




  • 努力提升自己,不必要一定去大厂,很多小厂也不错,涨薪水才是王道!



作者:前端要努力
来源:juejin.cn/post/7277912168493465640
收起阅读 »

放弃熬夜,做清晨的霸主🔥

☀️ 前言 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)。 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。 在尝试早起将近一个月的时间后,我...
继续阅读 »

☀️ 前言



  • 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)

  • 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。

  • 在尝试早起将近一个月的时间后,我发现,我的效率确实是有了质的提升,接下来我会根据HeyMatt老师提到的方法和我一些实践来进行说明,感兴趣的小伙伴欢迎收藏慢慢看。


🕐 极致利用晚上时间的错觉



  • 会不会有很多小伙伴会有这种情况,每天辛勤劳作后,到了11点半大脑就会提示你:累了一天了,要不要放松一下呢?视频里说到,这种大脑暗示的放松大体分为三种:

    • 开始刷视频,打个游戏,借助浅层的刺激感来放松

    • 点个宵夜,搞个小烧烤吃吃,借助食物换取特定心境

    • 想一些过往能够牵动情绪的往事,沉浸在起伏连绵的情绪中



  • 绝了,以上三种我都尝试过,全中,但是作为程序员我还会有其他的几种:

    • 学习知识📖

    • 优化代码💼

    • 加快需求进度,赶需求🏃



  • 我经常会有这种想法,如果晚上11点半到1点半我可以把这些事情做完或者做多一点,那么我的时间就会被延长🕐。

  • 错❌,看了这个视频后我真的悟了,我花掉了N个晚上的两个小时,但是换不回来人生相应的发展,甚至很多质量很差的决策、代码都是在这个时间段产出的。

  • 可能你确实在这晚上获得了很多愉悦感,但是这个愉悦感是没有办法持续的第二天又赖床又想逃避,你会去想我白白浪费了晚上两个小时刷剧,过了一个晚上这个愉悦感在你早上醒来的时候会忽然转化为你的焦虑感

  • 确实是这样的,特别是在周末熬夜的时候,你会潜意识的特别晚睡,第二天让睡眠拉满,直接到中午才起床,但其实这样不是浪费了更多的时间吗?


🤔 三个风险



  • HeyMatt老师提到在熬夜的这些时间,面临了至少三个风险。


时间的消耗不可控



  • 就拿我来举例,我前段时间老是想着公司需求怎么做,需求的方案是不是不完整,是不是有可以优化的点,要修复的Bug怎么定位,怎么解决。

  • 我不自觉的就会想,噢我晚上把它给搞定,那么第二天就可以放下心去陪家人出去走走。

  • 可是事实呢?运气好一点或许可以在2个小时解决1点准时睡觉,但是运气不好时,时间会损耗越来越多,2个半小时,3个小时,4个小时,随着时间的消逝,问题没有解决就会越发焦虑,不禁查看时间已经凌晨3-4点了。

  • 就更不用说以前大学的时候玩游戏,想着赢一局就睡觉,结果一晚上都没赢过...😓


精神方面的损耗



  • 当我们消耗了晚上睡眠时间来工作、来学习、来游戏,那么代价就是你第二天会翻倍的疲惫。

  • 你会不自觉的想要睡久一点,因为这样才能弥补你精神的损耗,久而久之你就会养成晚睡晚起的习惯,试问一下自己有多久没有在周末看过清晨的阳光了?

  • 再说回我,当我前一个晚上没有解决问题带着焦虑躺在床上时,我脑子会不自觉全是需求、Bug,这真的不夸张,我真的睡着了都会梦到我在敲代码。这其实就是一种极度焦虑而缺乏休息的大脑能干出来的事情。

  • 我第二天闹钟响了想起我还有事情没做完,就会强迫自己起床,让自己跟**“想休息的大脑”**打架,久而久之这危害可想而知。


健康维度的损耗



  • 随着熬夜次数的增多,年龄的增长,很多可见或不可见的身体预警就会越来越多,具体有什么危害,去问AI吧,它是懂熬夜的。



🔥 做清晨的霸主



  • 那么怎么解决这些问题呢,其实很简单,把晚上11.30后熬夜的时间同等转化到早上即可,比如11.30-1.30,那么就转化到6.30-8.30,这时候就会有同学问了:哎呀小卢,你说的这么简单,就是起不来呀!!

  • 别急,我们都是程序员,最喜欢讲原理了,HeyMatt老师也将原理告诉了我们。


赖床原理



  • 其实我们赖床起不来的很大一部分原因是自己想太多了。

  • 闹钟一响,你会情不自禁去思考,“我真的要现在起床吗?” “我真的需要这一份需要早起的工作吗?” “我起床之后我需要干什么?” “这么起来会不会很累,要不还是再睡一会,反正今天不用上班?”

  • 这时候咱们大脑就处于一种**“睡眠”“清醒”**的重叠状态,就跟叠buffer一样,大脑没有明确的收到指令是要起床还是继续睡。

  • 当我们想得越多,意识就变得越模糊,但是大脑不愿意去思考,大脑无法清晰地识别并执行指令,导致我们又重新躺下了。


练就早起



  • 在一次采访中,美国作家 Jocko Willink 老师提出了一种早起方法::闹钟一响,你的大脑什么都不要想,也不需要去想,更不用去思考,让大脑一片空白,你只需执行动作即可。

  • 而这个动作其实特别简单,就是坐起来--->站起来--->去洗漱,什么都不用想,只用去做就好。

  • 抱着试一试的心态,我尝试了一下这种方法,并在第二天调整了闹钟到 6:30。第二天闹钟一响,直接走进卫生间刷个牙洗个脸,瞬间清醒了,而且我深刻的感觉到我的专注力精神力有着极大的提升,大脑天然的认为现在是正常起床,你是需要去工作和学习👍。

  • 绝了,这个方法真的很牛*,这种方法非常有效,让我觉得起床变得更容易了,推荐大家都去试试,你会回来点赞的。


克服痛苦



  • 是的没错,上面这种办法是会给人带来痛苦的,在起床的那一瞬间你会感觉仿佛整个房间的温度都骤降了下来,然后,你使劲从被窝里钻出来,脚底下着地的瞬间,你感到冰凉刺骨,就像是被一桶冰水泼醒一样。你感到全身的毛孔都瞬间闭合,肌肉僵硬,瑟瑟发抖,好像一股冰冷的气流刺痛着你的皮肤。

  • 但是这种痛苦是锐减的,在三分钟之后你的痛苦指数会从100%锐减到2%

  • 带着这种征服痛苦的快感,会更容易进入清晨的这两小时的写作和工作中。


✌️ 我得到了什么



  • 那么早起后,我收获了什么呢❓❓


更高效的工作时间



  • 早起可以让我在开始工作前有更多的时间来做自己想做的事情,比如锻炼、读书、学习新技能或者提升自己的专业知识等,这些事情可以提高我的效率专注力,让我在工作时间更加高效。

  • 早起可以让我更容易集中精力,因为此时还没有太多事情干扰我的注意力。这意味着我可以更快地完成任务,更少地分心更少地出错


更清晰的思维



  • 早上大脑比较清醒,思维更加清晰,这有助于我更好地思考解决问题,我不用担心我在早上写的需求方案是否模糊,也能更好的做一些决策

  • 此外,早起还可以让我避免上班前匆忙赶路的情况,减少心理上的紧张压力


更多可支配的时间



  • 早起了意味着早上两个最清醒的时间随便我来支配,我可以用半小时运动,再用10分钟喝个咖啡,然后可以做我喜欢做的事情。

  • 可以用来写代码,可以用来写文章,也可以用来运营个人账号

  • 可以让我有更多的时间规划安排工作,制定更好的工作计划时间管理策略,从而提高工作效率减少压力


更好的身体健康



  • 空腹运动对我来说是必须要坚持的一件事情,早起可以让我有更多的时间来锻炼身体,这对程序员来说非常重要,因为长时间的坐着工作容易导致身体不健康

  • 用来爬楼,用来跑步,用来健身环等等等等,随便我支配,根本不用担心下班完了后缺乏运动量。


👋 写在最后



  • 我相信,我坚持了一年后,我绝对可以成为清晨的霸主,你当然也可以。

  • 而且通过早起不思考这个方法,很多在生活有关于拖延的问题都可以用同样的方式解决,学会克服拖延直接去做,在之后就会庆幸自己做出了正确的决定

  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

作者:快跑啊小卢_
来源:juejin.cn/post/7210762743310417977
收起阅读 »

IT外传:会议室里的技术副主管

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑,单名一个“常”字,是一名资深程序员。最近,他新入职了一家IT公司,岗位是后端开发。 入职后,他的领导周主管,给他安排了一项任务:对一类表单图片的特定区域进行处理。 这个表单图片,是...
继续阅读 »

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



老郑,单名一个“常”字,是一名资深程序员。最近,他新入职了一家IT公司,岗位是后端开发。


入职后,他的领导周主管,给他安排了一项任务:对一类表单图片的特定区域进行处理。


pic.png


这个表单图片,是由前端动态生成的,主要做信息收集用。现在要求前端生成时,附带一份内容项与坐标区域的配套信息。比如图片的1/4到1/2的区域范围是教育经历模块,1/2到2/3的区间是工作经历部分。而老郑要做的,就是将这些区域裁剪下来。


代码很简单,用opencv就行。从原图中裁出一个区域,python就一句话crop_img=img[y1:y2, x1:x2]


pic2.png


为了验证用百分比标注二次还原的效果,老郑还专门用js写了一个制作表单的前端页面。他在生成的同时,也记录一份同名标注的json文件。一试,效果很好。


老郑等着项目启动,因为他要对接生成表单的项目组。这天他正在工作,产品经理叫他赶紧到大会议室来一趟,请求支援。


老郑不明白什么事情,就去了。


会议室很大很气派,里面已经聚集了十多个人。大家鸦雀无声,气氛凌冽,似乎会议被中止了。这里面,老郑只认识一个人,就是产品经理董非凡。这个方案就是董非凡和老郑一起讨论出来的。现在董非凡对他们组里的技术进行宣讲时,出现了问题。


“老郑,你给大家说说你的想法!”


很明显,董非凡已经给老郑留出了C位。老郑拉开豪华转椅坐下,说道:“就是咱们前端在生成的时候,将几个关键点的坐标记录一下……”


巨大的方形会议桌的对角线,斜躺着一个黑衣人。黑衣人说:“你说的这个,实现不了!”


老郑瞬间一怔:“实现……不了,为什么实现不了?”


董非凡拉扯了老郑一下:“郑工,你把你实现的给大家看看!”


老郑用浏览器访问他写的表格制作页面,按下F12,调到Console选项,操作了一番,控制台输出一串串坐标信息。


老郑说:“这可以的呀!我不是前端,就会几句js,但是我觉得能实现!”


黑衣人说:“你能实现,并不代表我们能实现。我们和你用的不是一种实现方式!”


老郑被这种傲慢刺激到了,他回怼说:“那你们就换我这种方式”。


老郑感觉自己是新人,而且不清楚黑衣人是谁,压了压情绪。他咧着嘴笑了笑,说:“哎呀,我不干涉你们用哪种方式,我又不懂,只要能给我提供数据就行呗!”


黑衣人问对面的一个小伙子:“咱们能实现吗?”


小伙子点了点头说可以的,他说可以通过计算div的偏移量来获取指定区域的坐标。


黑衣人打断了小伙子,让他不要说了。


黑衣人对老郑说:“做是可以做,但是我需要抛一个风险,这种方式可能会出错!”


“出错?”老郑又是一怔,“为什么会出错?!”


黑衣人说:“这个世上没有绝对不出错的事情。”


老郑压了压情绪,和颜悦色地说:“哎呀,这个你也出个错,我这也出个错,一串起来,我们这个活可没法干喽”


黑衣人解释道:“任何事情都没法保证全对,你不这样觉得吗?你能保证你的代码100%没问题?”


董非凡插话说:“那个……出错没关系,我们可以调嘛!我们保证在理想条件下能走通,然后到实际场景中,我们再去做容错嘛!后面还有对接,自测,测试。”


会议结束了。


老郑问董非凡,会上那个黑衣人是谁啊?


董非凡说,他是负责生成模板业务的技术副主管。


“副主管?那正主管呢?”


“正主管就是做你对面那个!他是做Java的,他管后端。前端的讨论,他不参与”


后来,需求有所细化。不但要裁切大区域模块,而且还要把里面更细致的信息也裁出来,就比如教育经历中的学校名称区域。


pic3.png


需求是这个需求。但是,谁来推动呢?周主管跟老郑说,你去组织一个会议,跟兄弟部门说说需求,然后要个工期。


老郑问:“有必要开会吗?我去前端小伙子的工位旁跟他说一下”


周主管说:“得开会。拉上我,叫上对方的前端小伙子,以及小伙子的主管,还有产品经理。我们要在会上正式提出需求,然后讨论技术可行性,确定什么时间能提供给我们。会后写一个会议纪要,通知相关领导。”


老郑立马约了一个15分钟的会,他觉得是随手返回数据的事情,说完的功夫就做完了。约会议,只不过是把问工期搬到会上有一个仪式感。


周主管感觉15分钟时间太短了,要约长一点,至少30分钟。老郑协调了半天,这几个人的会议日程都有冲突。周主管表示,大家忙的话,会议可以延期。上午没空就下午开,下午没空就明天开。实在不行,可以加班开这个会。


老郑觉得还是算了,赶紧开吧。于是就约了下午的会议。结果开会时,就老郑和小伙子去了,其他人都被叫去开各种临时会了。


老郑和小伙子面对面。老郑说,这个会是领导要求开的。


“我发你的需求看过了吧?我们也在线沟通过细节,应该没啥问题”


小伙子说没问题。


这个会议好像1分钟就结束了。


老郑想,还有没到会的领导,要不要等等他们?否则,我们这一结束,他们再过来,会指责这会议没开。


老张和小伙子先是聊了聊技术,后又聊了聊技术。


大约二十分钟后,差不多了。老张想问问小伙子,多返回那一个位置,大概需要做多久。


此时,上次那个黑衣人,也就是主管前端的副主管,急匆匆地过来了。


“哎呀,幸好赶上了。那个需求看了吗?”黑衣人问小伙子。


小伙子还没来得及搭茬,副主管说:“我看过了,哎呦,我反正是没想到实现思路”


小伙子不说话了。


老郑不愿意和他多聊,老郑说:“需求我俩讨论清楚了,现在需要定一下开发周期。这可不是我要啊,是我领导要,最后还得形成会议纪要。不知道是现在能给呀,还是得回去研究研究……”


小伙子仍然不说话。副主管说:“这个时间啊,还真不好说。咱们都是干技术的,我不说你也懂。这种研究性的工作,没有试过谁知道呢?顺利的话,可能五分钟就出来了。当然,也可能一周才能给你。从我的角度来看,现在仍然没有思路,不知道该怎么去实现。但是,我们保证,努努力,不管克服多大的困难,最后肯定是要搞出来的。这样吧,给你一个最大时间,一周内做完。不是说从今天起一周之后,也可能这周三、周四就做完了,提前做完了就当是给你一个惊喜”。


老郑说了句好的,就结束了会议。他回去写下会议纪要:第一,双方已明确对接需求;第二,一周内完成交付。然后他就开始写代码了。项目没有负责人,这意味着谁都能管,同时谁也没法管。就算他明知道半天能干完,又能怎样呢?和对方领导去讨价还价?说我不行,你行你上啊!这除了树敌,没有任何好处。这可能就是环境、氛围,或者称之为“文化”。


此时,老郑的内心波澜不惊。唯一让他思绪泛起一点波纹的,是他从黑衣副主管身上,看到了以前的自己。


大约7年前,老郑还是一家上市公司的中层干部、小股东。公司为了加强技术体系的横向建设,从所有业务线中,每个工种抽离出一个人,这些人合伙组成了一个叫技术研究院的组织。老郑当时被选中,负责整个公司有关移动端(Android、iOS)的技术攻关、工期评估、框架管理。


起初还好。移动端的开发者多是老郑面试并招进来的,而且很多业务也是老郑的一手项目。但是到后来,随着人员流动,加上老郑开始脱离了具体业务,将更多精力投入到了写文档和申报材料当中。他再也不知道每个业务的具体功能,如何实现。渐渐地,他提出的一些思路,大家不再支持,他说话也没有人听从。


有一次,老郑发现会议室在开会。他从缝里看到了一个事业部的产品、技术在讨论问题。老郑推门进去。他依然清楚地记得那个iOS兄弟姓宋,产品经理姓李。iOS兄弟的实现思路和报工期的方式,明显违背了老郑定的策略。老郑当场发飙了。老郑说,你们今天提需求明天就上线,这样制定计划是有风险的。我是由公司任命的研究院副院长,也是股东,我要对公司负责。吧啦吧啦他说了一通。


小宋和小李并没有理会老郑,反而是事业部的罗总闻讯过来,连忙给老郑道歉。后来,事业部越来越独立。老郑也慢慢没有了存在的意义。临走前,老郑专门找到产品经理小李,跟他道了歉。老郑说他们事业部的开发效率越来越高了,紧跟市场的脚步,蒸蒸日上,我以前的想法是错的。其实,到现在为止,老郑也没有搞清楚,到底是自己玩死了自己,还是公司玩死了自己。而小李也只是客套几句就去忙了。


而今的黑衣副主管,多少也有点这个意思。他们并不关注事情本身(没有精力),只关注通用的流程。不管是1分钟的活,还是一个月的任务(很难分辨),都要开满各项会议(总是没错),要显得很艰难,要留出足够多的抵抗风险的时间。


“咚咚~”小伙发来了一条消息。他说,我先给你个测试版的对接着。过两天我再从群里发布个正式版。


老郑望向窗外,笑了笑。


作者:TF男孩
来源:juejin.cn/post/7283375143769096253
收起阅读 »

劝你放弃纸上谈兵

引言 纸上谈兵,汉语成语,拼音是zhǐ shàng tán bīng,意思是指在纸面上谈论打仗。比喻空谈理论,不能解决实际问题。也比喻空谈不能成为现实。 WEB 开发发展到现在,各种优秀的框架以及丰富的网络资源,让 WEB 开发入门门槛降到了很低很低,但是...
继续阅读 »

引言



纸上谈兵,汉语成语,拼音是zhǐ shàng tán bīng,意思是指在纸面上谈论打仗。比喻空谈理论,不能解决实际问题。也比喻空谈不能成为现实。



WEB 开发发展到现在,各种优秀的框架以及丰富的网络资源,让 WEB 开发入门门槛降到了很低很低,但是并不是证明 WEB 开发没有门槛了,也不能证明 WEB 开发就没有难度了


最近在学校做项目,依稀能听到这种事情:老师给了一个XXX题目,甲同学看了看题目,思考了一下,觉得这个项目挺简单的,甚至有可能看不上这个简单的项目……说实话,以前我也是抱着这种心态,觉得老师给的那些项目又简单又 low,做出来也是浪费时间,没有什么太多意义


不过,后来在我做了一个“秒杀商城”的项目以后,我就开始认真对待每一个看似简单的项目了。为什么呢?


秒杀商城


讲实话,这个项目就是纯拿来练手,为了往简历上写项目经历的项目。但是我也是在这个项目里面遇到了很多很多的问题......


期初想到这个项目的时候,我就在想,网上一大堆电商项目,也有很多高并发的商城秒杀项目供我参考,那我做这个还不是简简单单么


然后,在我真正开始做这个项目的时候,我遇到了很多很多的没有预料的甚至见都没见过的问题,我想,假如我是面试官,我想要判断一个人是否真的做过这个项目,下面这些问题就够了


在使用消息队列的时候,前端如何判断消息是否正确消费


第一次接触消息队列的时候,因为消息队列没有返回值让我挺难受的。但是在做高并发,尤其是秒杀项目的时候,消息队列仍然是首选


那么如何解决前端判断消息是否消费呢(比如订单是否已经支付完成)?


其实这个问题的答案很简单,简单到我在第一次听到这个答案的时候都有点不敢相信:轮询(即设置一个周期定时器,一直调用接口进行查询)。当然,也可以用其他的方式,这种方式只是最简单最好理解的一种


前端JavaScript的Number型与Java的long型最大长度问题


遇到这个问题的时候是因为项目里面生成全局唯一 id 使用到了雪花算法,雪花算法生成的就是一个64位的唯一 id,正好就是 Java 的 long 型最大位数。也正是因为这个算法用到了64位数字,所以就会遇到与 JavaScript 的 Number 类型数字的最大长度问题了


我们先来输出一下 Java 和 JavaScript 的最大值


public class Main{
public static void main(String[] args) {
System.out.println(Long.MAX_VALUE);
}
}
// 9223372036854775807
// 0x7fffffffffffffffL

console.log(Number.MAX_VALUE)
// 1.7976931348623157e+308

通过上面两段代码可以最直观的看出来一个问题,Java 的 long 型最大值与 JavaScript 的 Number 最大值不一样


然后我们再看一下下面这个代码:


var x = 9223372036854775807;
console.log(x)
// 9223372036854776000

这里只是把 Java 的 long 型最大值在 JavaScript 中输出出来,结果是最后三位数字变为0,倒数第四位数字进行四舍五入了。那么在开发的时候肯定就会遇到问题


这个问题解决办法其实也很好理解,做过大数相加的那种算法题的应该都知道,那就是用字符串表示数字。这个问题的解决办法就是后端传这种 long 型数据的时候使用字符串就好了


feign远程调用问题


这个问题只会让习惯不好的同学遇到(比如我),因为我在传统的 spring boot 开发的时候,一般不会在RequestMapping接口处写@RequestParam注解,然后在我第一次使用 feign 的时候,我仍旧在服务生产者处不写@RequestParam注解,结果导致 feign 远程调用失败


当然,这个问题的解决过程也是很顺畅,因为网上很多人(估计大部分都是新手)都遇到了这个坑


小结


实际上,上面的这些问题都不是特别难的问题,都是我们平时开发遇到的一些遇到了随便查一下就知道的问题。但是这些问题足以证明自己是否真真正正的做过这些项目


回到正题,如果仍旧是纸上谈兵,不去切实的自己动手尝试一遍,那么这个项目又怎么愿意写在简历上呢?我们常说见微知著,其实我认为往往就是这些地方就可以见微知著


作者:大爆米花
来源:juejin.cn/post/7283438991473426467
收起阅读 »

我的发!地表最强扫一扫

web
在很久很久以前,我亲爱的同事们在对接二维码扫描业务的时候,都是使用的微信官方自带的扫一扫,比如这样 wx.scanQRCode({ needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果, scanType: ["qrC...
继续阅读 »

在很久很久以前,我亲爱的同事们在对接二维码扫描业务的时候,都是使用的微信官方自带的扫一扫,比如这样


wx.scanQRCode({
needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType: ["qrCode","barCode"], // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
}
});

所以我扫码就一定得依赖微信,在普通的浏览器中打开就GG,并且还要绑定公众号,烦的一批。


然后我就在想,扫码不就是靠摄像头捕捉图像进行解码出内容嘛,那肯定会有原生的解决方案。


Google Google Google Google ......


果然是有的,Web API中也提供了一个实验性的功能,Barcode Detection API


image.png


它提供了一个detect方法,可以接收图片元素、图片二进制数据或者是ImageData,最终返回一个包含码信息的Promise对象。


但是呢,这个功能的浏览器兼容性比较差,看了caniuse,心凉了一半。


image.png


但我相信大神们肯定有自己的解决方案,继续Google呗。


Google Google Google Google ......


还真有这么一个库,html5-qrcode,它在zxing-js的基础之上,又增加了对多种码制的解码支持,站在巨人的肩膀上又跟高了一层。


html5-qrcode支持的码有:


CodeExample
QR Codeimage.png
AZTECimage.png
CODE_39image.png
CODE_93image.png
CODE_128image.png
ITFimage.png
EAN_13image.png
EAN_8image.png
PDF_417image.png
UPC_Aimage.png
UPC_Eimage.png
DATA_MATRIXimage.png
MAXICODE*
RSS_14*
RSS_EXPANDED*image.png

我个人觉得非常够用了,平时用的最多的还是二维码、条形码,其他的码也都少见。


关键是人家还支持了各种浏览器,可以说已经是很良心了(什么UC浏览器的,其实我都瞧不上,不支持就不支持,无所吊谓)


image.png


来看看官方提供的demo效果


chrome-capture-2023-8-27.gif


好好好,很棒。但是他们没有提供框架支持,那么我又可以站在巨人的肩膀上的巨人的肩膀上造轮子了。


先来看看我自己封装的React组件


demo.gif


使用方法也简单


function App() {
const scanCodeRef = useRef();
const [scanResult, setScanResult] = useState('');

function startScan() {
scanCodeRef.current?.initScan();
}

return (
<div>
<button onClick={startScan}>扫一扫</button>
<p>扫描结果: {scanResult}</p>
<ScanQrCodeH5
ref={scanCodeRef}
scanTips="请一定要对准二维码哦~"
onScanSuccess={(text) =>
{
setScanResult(text);
}}
// onScanError={(err) => {
// console.log(err);
// }}
/>
</div>

);
}

三二一,上链接,rc-qrcode-scan


这次的版本没有加入从相册选择图片进行解码,下个版本将会加入,希望能帮到掘友们。


2023-09-28更新,掘友们我把从相册选择加进去了。


作者:AliPaPa
来源:juejin.cn/post/7283080455852359734
收起阅读 »

我转产品了-前端转产品是一种什么样的体验

程序之路 入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、 webapp 。然后是 node/express/koa ,开始涉及全栈。 代码...
继续阅读 »

程序之路


入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、
webapp 。然后是 node/express/koa ,开始涉及全栈。
代码管理工具也从 svn 到 git ,然后制定提交规范,分支管理规范,结合 gitflow/githook 以及各种 lint 保证团队开发风格及可维护
性。
产品发布的方式从 ftp 上传,到 npm/nodejs/shell 脚本,然后再到 jenkins/docker/git 多分支多环境部署。


从第 3 年之后就感觉技术没什么提升了 ,后面都是在各个小作坊担任前端组长角色(其实感觉就是救火队长),哪里项目急去哪里,哪里有难题去哪里。实际比 UI、比测试、比实习产品的地位还低,基本没有话语权。



为什么转产品


严格来说,并不是专门的喜欢产品这个职位,而是希望了解产品经理所做的事。因为在软件开发的工作里,工作的内容和返工程序大大取决与产品对用户需求的理解能力,业务熟悉能力。而作为前端,经常只集中精力在处理页面还原、交互实现、数据对接、浏览器兼容等工作上面。对整个系统的业务逻辑是比较片面的。


如果对用户需求和产品业务有所了解,那可能在开发之前就能发现需求上的不必要性,发现设计上的错误,而减少程序开发的返工率。


总的说来,是期望:



  • 拒绝无效编程

  • 深入理解业务

  • 培养跨部门沟通能力

  • 培养产品设计能力


是否适合转产品


根据上面所说的几点理解,我自身而言并不拒绝,这是在心理方面。


在能力方面,我认为我是可以去学习和培养得到这份能力的。因为自己做的一个程序库 demo,得到了第一份前端工作。前端工作 2 年后,老板尝试让我做产品,并在过程中得到老板的一些建议。1、做产品就不要去考虑程序实现;2、如果自己是对的,就要去坚持,争执得面红耳赤也没有关系。对于这两点建议现在我是如何理解的,后面我会讲。


在习惯方面,我经常会吐槽 xx 产品应如何实现,经常觉得 xx 产品很难用,也经常自己开发 xx 小工具。当然这里我想说:人人都是产品经理,我是认可这句话的。因为产品的受众就是大众,而大众的感受就是产品。至于我自己的 xx 小工具,当然也会被吐槽,不过我觉得这并不影响“喜欢做产品”这个习惯,而做出好产品,是在做产品的过程中去获得的能力。


如何得到这份工作


严格意义上的这份工作,大家都知道一般而言薪资是比开发要低一些的。我说下我能给到的:



  • 接受作为入门岗产品的薪资,不考虑自己的开发经验的工资

  • 能陪开发一起加班,一起赶项目

  • 能在与客户的需求讨论阶段,通过自己的开发经验给出符合客户所需和较低开发成本的解决方案

  • 能处理好产品核心的工作,例如需求文档、原型设计等(仅限于我当前对产品职务的了解)

  • 必要时可为前端团队提供技术方案


真正意义的产品岗的入门工作


我入职这家公司,公司管理层有征求我的意见。问公司现在缺产品,把我拉来填这个位置,问我的想法之类。我接受之后,在这个公司的职位就正式为产品了。


前期的工作,是与另一个兼职的产品去客户现场去了解需求,我做会议纪要,每场会我都在。


领导的意思是,因为兼职的那个产品可能会照顾不到。所以期望我今后能全权接受他手上的项目和往后的产品项目。


另一个公司的项目是两个团队开发的,公司一个团队,公司外部一个团队。这个项目有二期,计划我来接手二期。因为一期临近上线,把我接去做测试,说是我也刚好可以熟悉一下这个项目。虽然在之前我的理解中,产品就是产品,测试就是测试,心里多少有一点抵触。但想到确实在测试过程中多少可以增加对系统的了解,也坦然接受了。


然后在临近上线时,客户认为当前的产品流程不符合需要。需要修改流程,还要增加一个额外的流程。本来项目时间有所有延后,又加上客户添加需求,所以双方决定延后半月上线,但要添加新的流程以及再加一个二期功能。


这个二期功能中有一个拓客功能就是我将来要设计的模块。现在相当于我要提前介入。


不过好在这个系统的客户都还比较好相处,在客户现场做测试、改 BUG、讨论需求的这几天里,经常各种好吃好喝的东西都拿过来。饭点也问大家的口味情况,不重样的给大家点餐。系统有不少的问题,客户也没发脾气(这个我至今没理解)。


一个拓客模块原型


在我的构思中,是打算把整个拓客功能高度抽象化,尽量减少与原系统的耦合度。希望将来其他系统能便于复用这个模块,因为拓客功能是面向 C 端程序常见需求,并且流程也容易标准化。


所以构思了很多东西。


当与客户讲了这些东西之后,客户表示很多东西都有考虑到位。当然也有客户的自己侧重点的东西和必要上的东西的考量,这些东西在前期可能作为产品是比较难感知到的。



与客户讨论需求的部分心得


心得来源于分歧。


虽然这次需求沟通总的来说达到了自己的预取。但这边负责人后面批评了我,所为什么我要给客户讲这么多东西?为什么要答应他们?我们做不完!


我说我没有答应他们什么,我只是尽可能的去了解客户想做的,和让客户知道我想做的。后面我有意识到,由于这个模块是在这半月之类要临时加上去的,负责人害怕客户会认为我给他讲的那些功能就是这半月之类要上的功能。


所以,在这种情况下,在与客户表达功能的同时,要避免客户对功能产生错误的预期。


所以我后面单独找客户聊了,由于时间紧迫,之前给他讲的那些功能并不能完全实现。然后给他展示我这边能给到的一个满足他拓客条件的简化版本。客户表示理解,欣然接受,这个简化版本也与团队进行了同步,没什么问题。


另外,对于一个功能的实现,有很多做法和分支。我们不用一开始做得很细,当与客户沟通,得到客户想做的方向之后(当然客户想做的方向不一定正确,而如何能提前知道客户的方向不正确,这可能是更上一层的能力,比客户更了解客户所面临的问题)。


一个需求文档


拓客所处的项目第一期进行了近一年左右,神奇的是居然没有还没有需求文档。现在项目要上线了,负责人要去找客户结账了才想到要这文档。然后这文档让我来写,对于半路介入这个项目并且刚试岗这个职位的我来说简直头皮发麻。因为据我了解需求文档这东西巨细无遗,需要深入到系统的每个流程和细节。


谁让我现在是这个角色,我不入地狱谁入地狱?随后我反手就找公司把公司的需求文档模板发我一下。模板发了,但我一眼看过去,只知道需要填些什么内容,像是一个骷髅,却想不有内容的样子应会是怎样的,不知道一个有血肉甚至是有灵魂的样子是怎样的。


然后又让公司把以前的其他项目发我一份。然后公司随手发我一份,我打开一看,好家伙,161 页,部分内容如下:



以我之前的了解,需求文档这东西主要是用于验收的(实际开发中需求文档根本来不及跟上需求的变化)。而验收时为了表达工作量,需求文档通常都是内容越多越好。


所以这真也是个体力活。


为了让需求文本能与现有的实现相符合,我打开了现在的系统,现在的系统有些流程还跑不通,然后又根据我的之前的测试结果和现有原型的理解,进行梳理,先把页面和功能拉出来,大概如下:


# 后台管理系统
- 登录
- 用户名
- 密码
- 验证码
- 记住密码
- 系统管理
- 区域架构
- 展开和折叠
- 上级区域
- 名称
- 排序
- 状态是启用还是停用
- 区域层级
- 搜索 -- 名称、层级、状态
......
......
......

# 小程序
- 推广中心
- 统计面板
- 奖励总金额 -- 考虑隐私问题暂不展示应邀人员的细目
- 注册人数 -- 考虑隐私问题暂不展示应邀人员的细目
- 去提现 -- 跳转到体现页面
- 去提现
- 展示总的可提现金额
- 输入想提现的金额发起提现申请
- 展示提现申请记录

- 登录
- 有手机号时授权登录
- 无需要号时通过验证码登录,并进行实名认证
- 设置安全密码
......
......
......


然后根据页面和功能点去展开描述。具了解,需求文档需要包含以下内容:


- 产品概述
- 功能概述
- 用户需求
- 功能分析
- 非功能性需求
- 界面设计
- 数据需求
- 约束和假设

而在功能需求中,有几点是常见的:


- 功能概述
- 功能分析
- 界面设计
- 数据需求

看起来就是功能概括是怎样的?功能具体是怎样的?界面怎样的?数据库设计是怎样的?


很明显,数据库设计这个我暂时细致不了,而且我看现有的需求文档中也不是每个功能都把数据库设计放上去的。总之我认为,能基本把功能描述清楚,看起来够分量就行啦。


那么基于上面我列出的功能结构,例如:


- 登录
- 用户名
- 密码
- 验证码
- 记住密码

是很容易能推导出来:


- 功能概述
- 功能分析
- 界面设计

这东西的:


### 功能概述
本功能旨在提供用户登录系统的功能,包括输入用户名、密码和验证码,并提供记住密码的选项。

### 功能分析
用户登录功能主要涉及以下几个要素:

1. 用户名:用户需要输入其注册时使用的用户名。
2. 密码:用户需要输入与用户名对应的密码。密码应该以安全的方式进行存储和传输,例如使用哈希算法进行加密。
3. 验证码:为了增加登录的安全性,可以添加验证码功能,要求用户输入验证码。验证码通常是由字母和数字组成的随机字符串,用于验证用户的真实性。
4. 记住密码:提供一个选项,让用户选择是否记住密码。如果用户选择记住密码,下次登录时系统会自动填充用户名和密码。

### 界面设计
用户登录界面应包含以下元素:

- 用户名输入框:用于输入用户名。
- 密码输入框:用于输入密码。密码应以隐藏或替代字符的形式显示。
- 验证码输入框:用于输入显示的验证码。
- 验证码图片:用于显示验证码的图像,以便用户看到并输入。
- 记住密码复选框:用于让用户选择是否记住密码。
- 登录按钮:用户点击此按钮以提交登录表单并尝试登录系统。


然后我就以这种方式完成了 98 页的需求文档,这样应该能先交差一版了。


image.png


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7283766477802864675
收起阅读 »

降低代码可读性的 12 个技巧

工作六七年以来,接手过无数个烂摊子,屎山雕花、开关编程已经成为常态。 下面细数一下 降低代码可读性,增加维护难度的 13 个编码“技巧”。 假设一个叫”二狗“ 的程序员,喜欢做以下事情。 1. 二狗积极拆分微服务,一个表对应一个微服务 二狗十分认可微服务的设计...
继续阅读 »

工作六七年以来,接手过无数个烂摊子,屎山雕花、开关编程已经成为常态。 下面细数一下 降低代码可读性,增加维护难度的 13 个编码“技巧”。


假设一个叫”二狗“ 的程序员,喜欢做以下事情。


1. 二狗积极拆分微服务,一个表对应一个微服务


二狗十分认可微服务的设计思想。认为微服务可以独立开发和发布,每次改动不会影响其他系统。大大提高了开发人员的效率和线上稳定性。还可以在新服务里使用新的技术,例如JDK 21


于是狗哥把微服务的思想发挥到极致,每一张表都是一个服务。系统的应用架构图十分壮观。狗哥自豪的跟新同学讲解自己设计的系统。新同学看着十几个服务陷入了思考,不停地问着每个服务的作用,干了什么。狗哥很满足。


新同学第一次开发需求,表现很差。虽然他要改10个服务,但是每个服务只改动了一点点。并且由于服务之间都是Rpc调用,需要定义大量的接口,他需要发布好多的 jar,定义版本号,解决测试环境版本冲突,测试和上线阶段可把他忙坏了。


光是梳理上线顺序,新同学就请教了狗哥 三次。 最后还是狗哥帮他上线了3 个服务,新同学才赶在 凌晨 3 点前把所有的服务发完。看着新同学买了奶茶的份上,狗哥这次才没有和领导吐槽,“这个同学不行啊,上个线都这么费劲”


微服务过多,也困扰着狗哥。虽然线上流量不高,但是由于 “微服务太多,系统架构复杂",接口性能不行。


于是狗哥开始进行重构,他重新加了一个开关,新逻辑可以减少Rpc,调用提高性能。狗哥在代码中加了注释 "新逻辑"。


狗哥把代码上线了,但是在线上环境不敢放开,只在测试环境打开了开关。


2. 二狗积极重构代码,但是线上不放量


狗哥喜欢对代码进行重构,狗哥和领导吹牛,说“ 重构后的代码性能更强,更稳定”。 狗哥还添加了注释 ”这是新逻辑“。


但是狗哥在线上比较谨慎,并没有进行放量。只是在测试环境,放开了全量。


新接手的同学不知道线上还没放量,看到“这是新逻辑” ,他就在狗哥的“新逻辑”上改代码。测试环境验证一切正常,到了线上阶段却怎么也跑不通。


此时新同学才发现 ”新逻辑“ 的开关没有打开,你猜,他敢打开这个开关吗? 于是他只能删代码,在旧逻辑上重新开发。 等到改完代码,再上线时,已经天亮了。


由于这次上线问题,大家一起熬夜加班,需求上线被推迟。新同学被产品和测试一顿骑脸输出。新同学委屈的想要离职。


3. 二狗喜欢挑战自我,方法长度一定要超过1000行


二狗写代码天马行空。二狗认为提炼新方法会打断自己的编码思路,代码越长,逻辑越连贯,可读性越高。二狗还认为 优秀的程序员写的方法都是 非常长的。这能体现个人的能力。


二狗不光自己写超长的方法,在改别人的代码时,也从不提炼新的方法。二狗总是在原来的方法中添加更长的一段代码。


新同学接手代码时速度很慢,即使加班到凌晨,也不理解狗哥代码设计的艺术。狗哥还向领导抱怨,”你最近招的人不行啊,一个小需求开发这么久,上线还出了bug。“


4. 二狗喜欢挑战自我,一个方法 if/try/else 要嵌套10层以上


二狗写代码十分认真,想到哪里就写哪里。 if/else/try catch 层层嵌套。 狗哥的思路很快,并且思考全面,
嵌套十几层的代码一点bug都没有,测试同学都夸赞狗哥 ”代码质量真高啊“,一个bug都没有。


新同学接手新代码时,看到嵌套十几层的代码,大脑瞬间就要爆炸。想要骂人,但是看到代码作者是狗哥……


无奈之下,自己实在看不懂这段代码,于是点了一杯奶茶,走到了狗哥工位旁,”狗哥,多喝点水,给你点了一杯奶茶。…………这段代码能给我讲讲吗?“


狗哥过几天和领导闲聊天,“新来的同学人不错,还给我点奶茶喝”


5. 二狗认为变量命名是艺术,要随机命名,不要和业务逻辑有关系


二狗觉得写代码是艺术,就好像画画一样。”你见过几个人能看懂 梵高的画?” 狗哥曾经和旁边人吹牛。


二狗写代码思路十分奇特,有时候来不及想变量如何命名,有时候是懒得想变量命名。狗哥经常随便就命名了,例如 str1,str2,list1,list2等等。不得不说,狗哥的思维还是敏捷的,这么多变量命名都能记住,还不出bug。


但是狗哥记性不大行,过一两个月就不太记得这些变量的意义了。


6. 二狗积极写注释,但是写了错误的注释


一个成熟稳重的程序员改别人代码时会十分慎重,如果有代码注释,他们一定会十分认真阅读并尝试理解它。


二狗喜欢把注释引入错误的方向,例如 “是” 改成 “不是”,“更好”改成”更差“,把两处不相干的注释交换一下位置 等。


新接手的同学点了一杯奶茶,虚心求助二狗,“狗哥,你写的这段注释有什么深意啊,我看了三天,也不理解啊”。


到时候狗哥就可以给新同学一边装B,一边讲代码了。当然还要看心情,要是不口渴,可以讲讲。


7. 二狗改代码很认真,但是注释从来不改


二狗改代码真的非常认真,但是他不喜欢改注释。最终代码大改特改,注释纹丝不动。最终代码和注释不相干,部分正确,部分错误。


新接手的同学研究了两天也没搞明白。于是求助了狗哥


到时候狗哥就可以大展神威了 。”那段注释是错的,你别管,就当没有!“


狗哥顺便还说了一句,”优秀的代码不需要写注释,也不知道是哪个XX 写的注释“,成功收割新同学的"钦佩"之情。


8. 二狗喜欢复制代码


狗哥写代码十分着急,根本来不及重构。他总是想到一段代码,就复制过来。神奇的是,狗哥经常这么写,但是也没出什么问题。


但新同学就惨了,在改完狗哥的代码后,总被测试同学背地里吐槽,“一点小需求咋这么多bug,跟狗哥比差远了”。原来新同学改了一处,忘了改另外几处,代码被复制了好多遍,他实在无法全面梳理。


于是每次代码写完,新同学都要不停的研究代码,总是害怕自己少改了哪些地方,下班时间越来越晚。并且新同学也不敢把雷同的代码重构到一起。(“你们猜猜他为什么不敢?)


慢慢的,组里的人都被迫向狗哥学习,狗哥成功输出了自己的编码习惯。


9. 二狗积极写技术方案,但是最终代码实现不按照技术方案来


二狗非常喜欢写技术方案,大部分时间都花在技术方案上,总是把技术方案打磨的 滑不留手。 但是在写代码时,狗哥总觉得按照方案设计写代码,时间上根本来不及啊,还是简单来吧,凑活实现吧。


例如狗哥曾经设计了一套复杂的Redis秒杀库存系统,但是实现时选择了最Low的 数据库同步扣减方案。


狗哥写的流程图和实际代码也没什么关系。 但是流程图旁边加满了注释和说明,让人觉得 ”这个技术方案很权威“。


新同学熟悉项目时,从公司文档中搜到了很多技术方案,本以为可以很快熟悉系统,但是发现技术方案和代码不太一样。越看越迷惑。


于是点了奶茶再次走向了狗哥,狗哥告诉他,“那个技术方案太复杂,排期紧张,开发来不及。你就当没那个技术方案。”


10. 二狗十分自信,从不打日志。


二狗对自己的代码十分自信,认为不会出现任何问题,所以他从来不打日志。每次开发代码时,狗哥的思维天马行空,但是从来不想加个日志会有助于排查问题。


直到有一天,线上真的出问题了,除了异常堆栈,找不到其他有效的日志。大家面面相觑,不知道怎么办。狗哥挺身而出,重新加了日志,上线。 故障持续了不知道有多久……,看着狗哥忙碌,领导不停地询问还需要多久才能上线。


复盘会上,有人对狗哥不写日志的行为进行批判,狗哥却在 狡辩 “加了日志,就能避免这次故障吗? 出问题还不是因为你们系统出了bug,跟我不打日志有啥关系。” 双方陷入了无限的扯皮之中……


11. 二狗积极学习,引用一个高大上的框架 解决一个小问题


二狗非常喜欢学习,学习了很多高大上的框架。最近二狗学习了规则引擎,觉得这是个好东西,恰好最近在进行重构。于是二狗把 drools、avatior、SPEL等规则引擎、表达式求值 等框架引入系统。只是为了解决策略模式的问题。即何种条件下使用哪种策略。 狗哥在系统架构图里,着重讲了规则引擎部分,十分自豪。


新同学熟悉系统后,光是规则引擎部分就看了足足一周。但是还是不知道怎么修改代码。于是向狗哥请教。狗哥告诉他说," 你在这个地方 加一行代码 rule.type == 12 ,走这个 CommonStrategy 策略类就可以了。“


新同学恍然大悟,原来这就是规则引擎啊。但是为什么不用策略模式呢?好像策略模式不费事啊! 狗哥技术就是强啊,杀鸡用核弹。


12. 二狗积极造轮子,能造轮子的程序员才是牛掰的程序员


二狗非常喜欢造轮子,他对开源软件的大神们心向往之,觉得自己应该向他们学习。狗哥认为 造轮子才能更快地成长。


于是在狗哥的积极学习下,组里的 分布式锁 没有使用 redission,而是自己用setnx搞的。虽然后面出了问题,但是狗哥的技术得到了锻炼。# 不用Redssion硬造轮子,结果翻车了…


总结


降低代码可读性的方式方法 包括但不限于以上12种;


像二狗这样的程序员包括但不限于二狗。


大家不要向二狗学习,因为他是真的。


作者:他是程序员
来源:juejin.cn/post/7286155742850449471
收起阅读 »

突如其来的秋季反思

反思来的很突然,人随运走,兴由事发。 一切很突然,一切又有迹可循。 五月份时,Boss让我停掉一切研发事项,开始统筹变更管理;九月初,我从研发转为项目管理; 通俗来说,某些原因导致的医疗器械中的DMR变化,这些变化及其追溯即所谓变更管理 巨变之下,回顾了近两年...
继续阅读 »

反思来的很突然,人随运走,兴由事发。


一切很突然,一切又有迹可循。


五月份时,Boss让我停掉一切研发事项,开始统筹变更管理;九月初,我从研发转为项目管理;


通俗来说,某些原因导致的医疗器械中的DMR变化,这些变化及其追溯即所谓变更管理


巨变之下,回顾了近两年的历程,所思所想,记于下文。


养性与养气



20年冬季,身体不适,去看中医。诊断脉弦数,热邪亢盛,肝风内动之象。开了些药,听了一堆医嘱。


在狂奔的途中撞上了墙,一个踉跄,转身后,竟看到了歇斯底里的自己。



那时,我突然意识到,我在工作中走了歪路,并且已产生了很多不好的影响。用时下的词描述为 "极度精神内耗"



理想的书籍是智慧的钥匙 -- 托尔斯泰



与其这样内耗,不如先把个人技术提升的事情先放一放,将业余时间用来读一读书。于是买了一本想读很久的书《管子》。


我仍然记得,小学的苏教版课本上,有一句:仓廪实则知礼节,衣食足则知荣辱 语出 《管子-牧民》,老师给我们讲了管仲帮助齐桓公称霸的历史,并诵读了部分章句,并告诫我们以后有机会一定要读一读这部鸿篇巨著。


买这本书的理由很片面:找一本感兴趣又难读的书来磨性子。先秦文章,词句远比唐宋时期晦涩。理念振聋发聩,章句浩然磅礴,对我而言是不二之选。



读这本书的过程中,我开始思考公司的管理,团队的做事方式。并且真正理解一个道理:“不要陷在自己的世界中钻牛角尖,要去和高人探讨,如果不能和真人讨论,就去读高人的书籍”。



从这时起,开始了养性、养气。


作者按:方法上不必强求一致,如果读者诸君能够旁证自身,发现也应该做出自我调整,养性、养气,那么本章节就真正触达有缘人了



中庸中提到: 天命之谓性,率性之谓道,修道之谓教。喜怒哀乐之未发谓之中,发而皆中节谓之和。中也者,天下之大本也;和也者,天下之达道也。致中和,天地位焉,万物育焉。



通俗地讲,养性就是控制情绪,适度地释放,有节制,达到很平和的状态。而养气是养浩然气,知善恶辩是非明黑白,不可一味和稀泥。



居天下之广居,立天下之正位,行天下之大道;得志与民由之,不得志独行其道;富贵不能淫,贫贱不能移,威武不能屈,此之谓大丈夫。 -- 孟子 滕文公



价值证明的陷阱


再后来,我入职了新公司。此时我一直在思考一个问题:



如果在工作中花了很多心血和精力,但如何体现出价值 -- 价值证明问题



可能在大部分公司,都有这样的不利因素:需要打工人自己举证自己的价值


一旦陷入到这样的怪圈中,永远是吃亏的。



你如何证明自己本职工作做得很出色?


你如何证明你做了本质工作之外的内容,并对公司产生了价值?


你如何证明……



上面的BOSS无非是想用这种方式逼底层人内卷罢了,只要你去想了,你就输了。



公司的核心是商业化,不要奢求他人能管理好贪欲



目标契合与捆绑


而解法也不难,让上级无法否认你的价值即可!如果你所在的公司,上级很轻易就可以否定别人的价值,那么就可以考虑换工作了。


可以将目标分成两部分:



  • 一部分是明面上的,紧扣上级的考核点,对齐公司的核心价值,如果公司的核心价值很低,那么也可以考虑换工作了。

  • 另一部分是私下里的,用于个人成长。时下难以在一家公司干到退休,人总要成长。这部分目标是朝下一个职位的模板对齐的

    • 能融合进当前工作的,就将其打造为超预期

    • 不能融合的,就需要付出个人时间了




以这种方式切入,上级难以否定你所创造的价值(否则是自我否定),公司也难以否定全员价值。与此同时,自己也可以借机成长(达成自我目标)。不可否认,这一方式可以避免自己浪费精力,好钢永远用在刀刃上。


日常需要留意:



  • 商业画布,但一般难以接触,甚至没有明确

  • 业务布局、产品规划,用于分清主次

  • 市场分析、一般也难以接触,留心Boss们的分享

  • 各种大会,先听出基调和逻辑,用正说反说折中说去拆解话术,还原真实想法


结语


近半年,也常和朋友聊中年危机之类的话题,时常感慨万千,虽然还有几年才到年纪,但总要先做好准备。


这次的思考比较随性,并未仔细提炼主题并围绕行文,个人观点大体如下:



  • 大部分公司管理者认为程序员是"生产工具",并且利用各种方式让人成为高产的工具

  • 我们需要认识到这一点,并打破这一点。关键在于形成自我核心价值观、逻辑体系自洽。就可以免疫PUA等手段,并且不露于形色

  • 读书、读好书是一种有效方式

  • 规避自我证明价值这类陷阱

  • 用"目标契合与捆绑" 这一方式,在工作中不浪费精力


作者:leobertlan
来源:juejin.cn/post/7285373518837383223
收起阅读 »

Web 版 PS 用了哪些前端技术?

web
经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + Emscripten、Web Components + Lit、Service Workers + Workbox 和新的 Web API 的支...
继续阅读 »

经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + Emscripten、Web Components + Lit、Service Workers + Workbox 和新的 Web API 的支持,终于在近期推出了 Web 版 Photoshop(photoshop.adobe.com),这在实现高度复杂和图形密集型软件在浏览器中运行方面具有重大意义!


图片


本文就来看看 Photoshop 所使用的 Web 能力、进行的性能优化以及未来可能的发展方向。


愿景:在浏览器中使用 Photoshop


Adobe 的愿景就是将 Photoshop 带到浏览器中,让更多的用户能够方便地使用它进行图像编辑和平面设计。过去几十年,Photoshop一直是图像编辑和平面设计的黄金标准,但它只能在桌面上运行。现在,通过将它移植到浏览器中,就打开一个全新的世界。


Web 版 Photoshop 承诺了无处不在、无摩擦的访问体验。用户只需打开浏览器,就能即时开始使用 Photoshop 进行编辑和协作,而不需要安装任何软件。而且,由于Web是一个跨平台的运行环境,它可以屏蔽底层操作系统的差异,使Photoshop 能够在不同的平台上与用户进行互动。


图片


另外,通过链接的功能,共享工作流变得更加方便。Photoshop文档可以通过URL直接访问。这样,创作者可以轻松地将链接发送给协作者,实现更加便捷的合作。


但是,实现这个愿景面临着重大的技术挑战,要求重新思考像Photoshop这样强度大的应用如何在Web上运行。


使用新的 Web 能力


最近几年出现了一些新的 Web 平台能力,可以通过标准化和实现最终使类似于Photoshop这样的应用成为可能。Adobe工程师们创新地利用了几个关键的下一代API。


使用 OPFS 实现高性能本地文件访问


Photoshop 操作涉及读写可能非常大的PSD文件。这要求有效访问本地文件系统,新的Origin Private File System API (OPFS) 提供了一个快速、特定于源的虚拟文件系统。



Origin Private File System (OPFS) 是一个提供了快速、安全的本地文件系统访问能力的 Web API。它允许Web应用以原生的方式读取和写入本地文件,而无需将文件直接暴露给Web环境。OPFS通过在浏览器中运行一个本地代理和使用特定的文件系统路径来实现文件的安全访问。



 
const opfsRoot = await navigator.storage.getDirectory();

使用 OPFS 可以快速创建、读取、写入和删除文件。例如:


 
// 创建文件
const file = await opfsRoot.getFileHandle('image.psd', { create: true });

// 获取读写句柄
const handle = await file.createSyncAccessHandle();

// 写入内容

handle.write(buffer);

// 读取内容
handle.read(buffer);

// 删除文件
await file.remove();

为了实现绝对快的同步操作,可以利用Web Workers获取 FileSystemSyncAccessHandle


这个本地高性能文件系统在浏览器中实现Photoshop所需的高要求文件工作流程非常关键。它能够提供快速而可靠的文件读写能力,使得Photoshop能够更高效地处理大型文件。这种优化的文件系统为用户带来更流畅的图像编辑和处理体验。


释放WebAssembly的强大潜力


WebAssembly是重新在JavaScript中实现Photoshop计算密集型图形处理的关键因素之一。为了将现有的 C/C++ 代码库移植到 JavaScript 中,Adobe使用了Emscripten编译器生成WebAssembly模块代码。


在此过程中,WebAssembly具备了几个至关重要的能力:



  • SIMD:使用SIMD向量指令可以加速像素操作和滤波。

  • 异常处理:Photoshop的代码库中广泛使用C++异常。

  • 流式实例化:由于Photoshop的WASM模块大小超过80MB,因此需要进行流式编译。

  • 调试:Chrome浏览器在DevTools中提供的WebAssembly调试支持是非常有用的

  • 线程:Photoshop使用工作线程进行并行执行任务,例如处理图像块:


 
// 线程函数
void* tileProcessor(void* data) {
// 处理图像块数据
return NULL;
}

// 启动工作线程
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, tileProcessor, NULL);
pthread_create(&thread2, NULL, tileProcessor, NULL);

// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

利用 P3 广色域


P3色域比sRGB色域更广阔,能够显示更多的颜色范围。然而长时间以来,在 Web 上sRGB一直是唯一的色域标准,其他更宽广的色域如P3并没有被广泛采用。


图片


Photoshop利用新的color()函数和Canvas API来充分发挥P3色域的鲜艳度,从而实现更准确的颜色呈现。通过使用这些功能,Photoshop能够更好地展示P3色域所包含的更丰富、更生动的颜色。


 
color: color(display-p3 1 0.5 0)

Web Components 提供UI的灵活性


Photoshop是 Adobe Creative Cloud 生态系统中的一部分。通过使用基于 Lit[1] 构建的标准化 Web Components 策略,可以实现应用之间 UI 的一致性。



Lit 是一个构建快速、轻量级 Web Components 库。它的核心是一个消除样板代码的组件基础类,它提供了响应式状态、作用域样式和声明性模板系统,这些系统都非常小、快速且具有表现力。



图片


Photoshop 的 UI 元素来自于Adobe 的 Web Components 库:Spectrum[2],该库实现了Adobe的设计系统。


Spectrum Web Components 具有以下特点:



  • 默认支持无障碍访问:开发时考虑到现有和新兴浏览器规范,以支持辅助技术。

  • 轻量级:使用 Lit Element 实现,开销最小。

  • 基于标准:基于 Web Components 标准,如自定义元素和 Shadow DOM 构建。

  • 框架无关:由于浏览器级别的支持,可以与任何框架一起使用。


此外,整个 Photoshop 应用都是使用基于 Lit 的 Web Components 构建的。Lit的模板和虚拟DOM差异化使得UI更新效率高。当需要时,Web Components 的封装性也使得轻松地集成其他团队的 React 代码成为可能。


总体而言,Web Components 的浏览器原生自定义元素结合Lit的性能,为Adobe构建复杂的 Photoshop UI 提供了所需的灵活性,同时保持了高效性。


优化 Photoshop 在浏览器中的性能


尽管新的 Web Components 提供了基础,但像Photoshop这样的密集型桌面应用仍然需要进行广泛的跟踪和性能优化工作,以提供一流的在线体验。


图片


使用 Service Workers 缓存资源和代码


Service Workers 可以让 Web 应用在用户首次访问后将其代码和资源等缓存到本地,以便在后续加载时可以更快地呈现。尽管 Photoshop 目前还不支持完全离线使用,但它已经利用了 Service Workers 来缓存其 WebAssembly 模块、脚本和其他资源,以提高加载速度。


图片


Chrome DevTools Application 面板 > Cache storage 展示了 Photoshop 预缓存的不同类型资源,包括在Web上进行代码拆分后本地缓存的许多JavaScript代码块。这些被本地缓存的JavaScript代码块使得后续的加载非常快速。这种缓存机制对于加载性能有着巨大的影响。在第一次访问之后,后续的加载通常非常快速。


Adobe 使用了 Workbox[3] 库,以更轻松地将 Service Worker 缓存集成到构建过程中。


当资源从Service Worker缓存中返回时,V8引擎使用一些优化策略:



  • 安装期间缓存的资源会被立即进行编译,并立即进行代码缓存,以实现一致且快速的性能表现。

  • 通过Cache API 进行缓存的资源,在第二次加载时会经过优化的缓存处理,比普通缓存更快速。

  • V8能够根据资源的缓存重要性进行更积极的编译优化。


这些优化措施使得 Photoshop 庞大的缓存 WebAssembly 模块能够获得更高的性能。


图片


流式编译和缓存大型WebAssembly模块


Photoshop的代码库需要多个大型的WebAssembly模块,其中一些大小超过80MB。V8和Chrome中的流式编译支持高效处理这些庞大的模块。


此外,当第一次从 Service Worker 请求 WebAssembly 模块时,V8会生成并存储一个优化版本以供缓存使用,这对于 Photoshop 庞大的代码尺寸至关重要。


并行图形操作的多线程支持


在 Photoshop 中,许多核心图像处理操作(如像素变换)可以通过在多个线程上进行并行执行来大幅提速。WebAssembly 的线程支持能够利用多核设备进行计算密集型图形任务。


这使得 Photoshop 可以将性能关键的图像处理函数移植到 WebAssembly,并使用与桌面端相同的多线程方法来实现并行处理。


通过 WebAssembly 调试优化


对于开发过程中的诊断和解决性能瓶颈来说,WebAssembly 调试支持非常重要。Chrome DevTools 具备分析 WASM 代码、设置断点和检查变量等一系列功能,这使得WASM的调试与JavaScript有着相同的可调试性。


图片


将设备端机器学习与 TensorFlow.js 集成


Photoshop 最近的 Web 版本包括了使用 TensorFlow.js[4] 提供 AI 功能的能力。在设备上运行模型而不是在云端运行,可以提高隐私、延迟和成本效益。



TensorFlow.js 是一款面向JavaScript开发者的开源机器学习库,能够在浏览器客户端中运行。它是 Web 机器学习方案中最成熟的选项,支持全面的 WebGL 和 WebAssembly 后端算子,并且未来还将可选用WebGPU后端以实现更快的性能,以适应新的Web标准。



“选择主题”功能利用机器学习技术,在图像中自动提取主要前景对象,大大加快了复杂选区的速度。


下面是一幅日落的插图,想将它改成夜晚的场景。使用了"选择主题"和 AI prompt 来尝试选择最感兴趣的区域以进行更新。


图片


Photoshop 能够根据 AI prompt 生成一幅更新后的插图:


图片


根据 AI prompt,Photoshop 生成了一幅基于此的更新插图:


图片


该模型已从 TensorFlow 转换为 TensorFlow.js 以启用本地执行:


 
// 加载选择主题模型
const model = wait tf.loadGraphModel('select_subject.json');

// 对图像张量运行推理
const {mask, background} = model.execute(imgTensor);

// 从掩码中细化选择

Adobe 和 Google 合作通过为 Emscripten 开发代理 API 来解决 Photoshop 的 WebAssembly 代码和 TensorFlow.js 之间的同步问题。这使的框架之间可以无缝集成。



由于Google团队通过其各种支持的后端(WebGL,WASM,Web GPU)改进了 TensorFlow.js 的硬件执行性能,这使模型的性能提高了30%到200%,在浏览器中能够实现接近实时的性能。



关键模型针对性能关键的操作进行了优化,例如Conv2D。Photoshop 可以根据性能需求选择在设备上还是在云端运行模型。


Photoshop 未来在 Web 上的发展


Photoshop 在 Web 上的普遍应用是一个巨大的里程碑,但这只是可能性的冰山一角。


随着浏览器厂商不断发展和完善标准和性能,Photoshop 将继续在 Web 上扩展,通过渐进增强来上线更多功能。而且,Photoshop 只是一个开始。Adobe计划在网络上积极构建其整个 Creative Cloud 套件,在浏览器中解锁更多复杂的设计应用。


Adobe 与浏览器工程师的合作将持续推动 Web 平台的进步,通过提升标准和改进性能,开发出更具雄心的应用。前方等待着我们的,是充满无限可能性的未来!



Photoshop 网页版目前可以在以下桌面版浏览器上使用:



  • Chrome 102+

  • Edge 102+

  • Firefox 111+



作者:QdFe
来源:juejin.cn/post/7285942684174778431
收起阅读 »

经济持续低迷环境下,女全栈程序员决定转行了

引言 疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。 近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据...
继续阅读 »

引言


疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。


近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据要多很多,尤其是表示 “一周工作一小时以上” 也纳入了就业范围。


image.png


而从我自己的判断来说,记得我自己在去年8月份被裁之后就在xhs发布了一篇关于个人如何交社保的教程,去年年底,观看浏览量不是特别多,而在今年(从年初至今)浏览量以及收藏量蹭蹭往上涨,几乎是每天都有人浏览和收藏我的帖子,抛去网上数据到底如何,光从我自己的感受来看,今年失业人数比去年更多!


image.png


个人只是随手发了一个帖子,将自己如何交社保的步骤记录下来,就有持续的搜索流量,这绝不是一件好事!说明了哀鸿遍野。


一面广大青少年正值青春鼎盛却面临着就业危机,另一方面还要忍受各种开支的骤增,比如深圳统租房的出现,大批人发声:微棠gun出深圳!



曾经破旧拥挤的城中村,为每一位打工人开启了大城市的入口,虽然这个入口短暂,且在关上门的时候,会毫不犹豫抹去你所有的痕迹。

而今这个入口,它不会再破旧拥挤,但会吸取你身上的最后一滴血。



个人经历


1.行政岗转前端


自己曾经拿着一个一本工科学历,因为厌倦行政岗位的勾心斗角,从而挑灯夜战每天在公司加班学习前端到11点,半路出家转行做了前端程序员。


2.刚转行遇吸血领导


而刚转行,又遇到了极其吸血的创业公司(大小周、从0到1项目,双周迭代迭代加班到2点)。


当时不敢辞职,不外乎有几个原因:



  • 刚转行,自己认为技术还比较菜,不敢辞职,被裁了之后才发现外面一大片天地

  • 真的很忙,根本没有时间提升自我与准备面试。因为呆了两年,我自己上了一次救护车,后来离职之后也发现自己因此得了疲劳综合症

  • 比较会吃苦,当时看来觉得可以忍一忍


关于这家公司呢,我想说,我这领导是真的狗,领导是我大一届的学长,曾经担任了大厂某知名项目的组长,号称协同领域的专家,关于此人是我生活中见过最资本的一个人:



  • 针对刚毕业的新人,不培养下属却对下属有着超乎大厂的要求(毫不夸张,你没经历过就不要觉得我是在夸张)

  • 技术部的同事都是很年轻的,做事都兢兢业业,不甩锅,不摸鱼,很多事都是自发的去解决,关于技术水平,我很客观的评价,不菜

  • 在裁我的时候,我呆的时间是13个月,也就是差一个月满2年,但他忽悠我说法律都规定只能给我1+1,我还不满2年,当时对方忽悠毫不脸红,又本着学长+平时看起来正人君子的偏见,在当时就签署了合同,失去的1个月补偿金还好,最伤人的是利用了你的信任,杀人诛心。


3.持续学习


从吸血公司出来之后,进入了相对比较wlb的公司,也清楚认识到自己在程序员领域,女性并不吃香,因此自己也是一直在学习前端技术。



  • 比如自己也曾在掘金发布了上百的技术文章

  • 买教学课程

  • 从零学算法,刷Leetcode

  • github持续输出代码

  • 建立自己的技术博客


image.png


image.png


4.努力不代表有收获


曾经相信自己勤能补拙,后来发现,比你拙的一大批还比你工资高;

曾经熟悉React技术栈,却在失业时找前端兼职时因不会vue而被刷;

曾经将网上的八股文背了再背,面试一二面对答如流,却倒在了三面面试官深问你项目经验;
曾经以为深耕项目经验,学性能优化、前端工程化、架构,却因为面试不会吹牛且遇上近几年经济低迷环境,工资还是那样。

曾经以为,自己努力点,自己性格好点,不断提升,会迎来比较好的人生。

曾经以为,男女平等,男生不应该一人承担经济压力,所以放弃了沉迷貌美如花,选择了与男生一样扛水桶,挑重活,但事实是,那些每天开开心心负责貌美如花的女生比我这种埋头搞钱的女生要幸福很多,对于像花一样的女生,谁不怜爱宽容呢,谁会去宽容一个扎在程序员堆里放弃自己容貌的黄脸婆呢。(看到这里,也许有人觉得我是因为自己长的太丑了,所以才选择搞钱,然而客观来分析,我自己并不丑,虽然说不是校花班花级别,但也可以在普通人群里说的是中上,颜控党眼里也能过得去,不是普信)



然后事实是,有些人,不用长得漂亮,不用能力强,不用对外提供情绪价值或其他价值,他站在那里,就有好的收获,就有人包容就有人爱。



在经历过上述的心理历程之后,明白了职场规则,以及社会运作规律,在大环境下,每个人都在尽自己的努力维持着公平,这个世界,因为有些人经历坎坷,未能坚守住自己底线,从而世界才会有坏人的存在。但大部分情况是,没有绝对的坏人,比如你觉得领导对自己很吸血,但可能领导背后的压力是整个公司的生存(虽然我的领导真的就是单纯的吸血),比如你觉得有些人对自己戾气重,可能当时人家真的内心极其痛苦,而你刚好撞到了枪口上,比如有些人因为诸多原因对你坏,但可能对别人好。


So,个人而言,还是做好自己,看淡所有的行为,同时能有自己的盾和矛。


决定转行


明白自己确实不适合长久做程序员,因此跟大家一样,网上搜了很多搞副业赚钱的路子,排除了偏门以及刑法上的路子,结合我自己的情况,目前已经开始正式着手Vlog自媒体之路了。



  • 买拍摄工具

  • 打造自己的IP

  • 整理自己的衣着、居住环境

  • 学习自媒体知识、拍摄技巧


总的而言,作为一个硬件工科出身的妹子,一直觉得自己更喜欢软件,比如硬件我要调试半天的电路我才能把一个灯泡💡点亮,而计算机,我写一行代码就可以得到反馈,即使是错误的,也能快速做出调整。


但也不可否认,女生在敲代码方面确实跟男生比没有那么大的天赋,就好比玩游戏,大部分女生会玩游戏,但是如果说要打的特别好,男生还是居多。


所以自己也很佩服那些在代码这条路上走的很坚定的女程序员。一起加油吧。


最后,我给各位女程序猿一个小建议,如果没有很高的学历背景或比较好的人脉资源运气,我觉得趁早搞一个副业,但是绝对不要裸辞去搞副业。程序员这个岗位虽然目前已经卷的不行,但瘦死的骆驼比马大,比某些天坑行业还是好很多,我觉得我们还是很幸运的。


掘金还没有评论置顶功能,就只能编辑在文章尾部了,更新:


这个文章呢,其实是在我自己很痛苦情况下写的,头痛+抑郁+想自杀,敲不进代码但不得不上班写的,另外,文章也只是阐述了我职场的不顺,还有其他很多方面都很痛苦,真实情况文章阐述不到十分之一,所以我真的劝各位键盘侠,别站在道德制高点来欺负一个跟你无冤无仇的人了,如今社会压力那么大,本身就导致抑郁症自杀的人那么多,戾气重可以理解,但别伤害别人,你只看到别人的一部分,不要成为压死骆驼的最后一颗稻草。真的,做个人吧。


另外,文章因用词不当引起的一些男女对立问题(这些不属于键盘侠,内容比较客观属于良好讨论,我也虚心接受),就当我是在放屁,我在评论区也虚心接受了这点,之后关于此类问题我就不回复了。


希望大家将焦点放在程序员职业发展方向上,一起谈谈中年之后的就业方向、副业等。


至于那些一上来就说玩流量、故意挑起战争、花瓶、网红的键盘侠们,在我没骂你之前就gun,真的,我脾气很爆...


作者:傲娇的萌
来源:juejin.cn/post/7246304095375097915
收起阅读 »

千里之行,始于发心

“千里之行,始于足下”,这两句话出自《道德经·第 64 章》,每个人小时候都会被问及:长大了想做什么?想成为什么样的人?我记得喜之郎之前有一则广告:长大后我要当太空人,爷爷奶奶可高兴了... 每个孩子都梦想着自己长大能够成为警察、科学家、作家、医生.........
继续阅读 »

“千里之行,始于足下”,这两句话出自《道德经·第 64 章》,每个人小时候都会被问及:长大了想做什么?想成为什么样的人?我记得喜之郎之前有一则广告:长大后我要当太空人,爷爷奶奶可高兴了...


每个孩子都梦想着自己长大能够成为警察、科学家、作家、医生......然而,当我们长大后,又有多少人能够实现自己的愿望呢?老子在道德经中点明了踏上成功之路的方法:千里之行始于足下。再类比到学习上来,难道不是这样吗?有了学习的目标还要有行动,立即开始就是迈向成功的第一步,也就是说要 “始于足下”。


老子说的“千里之行始于足下”,的确没错,但是我认为真正的千里之行,应该始于发心,只有我们拥有做好这件事的心,即便千里之行遇到各种困难,最终我们会坚持下去,直到成功的那一刻。


在开始分享学习方法之前,我们先思考一个问题:什么是学习?大家可以在脑海中过一遍,从上学到现在工作,我们基本都在不停地学习新的知识,看起来学习不就是一种行为嘛,那到底是怎样的一种行为?


我先给学习下个定义,它分为三个过程,第一个过程是理解,第二个过程叫记忆,第三个过程叫应用。一个事情你理解了,并且过了一段时间之后你记住了,再过一段时间后能熟练应用了,这才是一个完整的学习过程。不管少了哪个环节,学习都不能持久,最后的结果就是没学习到,这就是对学习的定义。


之前也看到不少小伙伴们在群里问 :编程怎么学啊?学完后又忘了!买了直播课听老师讲的时候都能听懂,自己写就不会了!我怎么这么笨啊!是不是脑子有什么问题啊等等。其实最主要的原因就是你以为自己学明白了,理解了,但其实你并没有真正的理解,你所谓的学习只停留在学习过程中第一个环节,只有当你理解了,记住了,并且能熟练应用了,这才能称你学会了。接下来给大家分享一个我用了两年的学习方法,个人觉得挺不错的,这也是世界公认的一个高效学习方法。


费曼学习法


这个方法我还是从我老师那里听来的,费曼学习法是由理查德·费曼提出来的,1965 年的时候也获得诺贝尔物理学奖,爱因斯坦曾说过:“If you can't explain it simply, you don't understand it well enough.” 意思就是说如果不能向他人简单解释一件事,就还没有真正弄懂它,如果你想弄清楚某个知识点,那就把它解释清楚!实际上,把自己正在学的知识教给他人,也正是费曼学习法的核心理念!费曼学习法是一种以教代学的学习方式。


假如我们通过直播课或者技术书学习某个前端技术,当学完某个知识点后,我们的大脑有可能会给自己一个错觉,就是自认为我学会了,学懂了,因为你还处在学习的一个过程理解阶段,真正的学习是分为三个阶段的理解、记忆、应用。检测自己是否真正学会了的方式就是利用费曼学习法,当你在给对方阐述的过程中,如果你对其中的知识点有不理解的地方,你会产生断层,会讲不下去,会给自己讲蒙了,当你发现讲不下去的时候,就是你对知识点理解不透彻的时候,然后回去接着学,学完之后再重新整理一遍继续给别人讲,直到可以重头到尾能给被人讲的明明白白了,甚至讲到被人听明白的时候,就证明你对这个知识点学习没有问题了,这就起到了查漏补缺的作用。为了让别人听得懂,首先自己得懂。在分享前,你会在大脑重新过一遍知识点,这就加深了对知识的理解,分享时,我们需要充分调动和提取大脑中的知识,这能够加强对知识的记忆和理解。


别着急记笔记


请大家先闭眼,在脑海中回想一个情景:语文老师正在讲课,讲台下的学生们握着笔杆子不停地写来写去,生怕错过一个知识点,仿佛只有全都记下来心里才会踏实,内心会有一丝欣慰......


接下来分享的学习方法也就一句话:第一次学习的时候不要记笔记,第一次学的时候,认真听,认真看,能记住多少算多少,就这么简单。


这是为什么呢?首先第一遍记的东西可能不是重点,会记很多笔记,第二个,我们需要了解一下我们的大脑,其实我们的大脑是非常喜欢省事的,当我们把知识点记到笔记本上,此时我们的大脑会记住,这些知识都记在本子上,就不会帮我们记在大脑里,我们如何将一个东西记得深刻,只有一个办法,就是让你的大脑不安全,当大脑没有安全感的时候,让你的内心无处安放的时候,反复记两遍准能记住,所以不要事事都给自己充分的安全感,尤其是记忆这个东西,当你记在本子上,笔记特别全,等你回顾的时候,什么也想不起来,而你记得最多就是这些知识点我记在本子上了,那么这些东西什么时候才能成为自己的呢?记都记不住,等到应用的时候怎么可能灵活呢!说这些不是说不让大家记笔记,只是第一遍学习不要记笔记,一些重点知识,容易忘的东西还是需要记下来的,以便之后的复习浪费时间。


我们应该放好自己的心态,也要认清一个事实:不管学任何东西都不要指望一遍就能学会学通,至少要抱着学两遍的心态去学习,所以第一遍的时候不要记笔记,能理解多少算多少,记不住没关系,等第二遍学的时候,看自己能想起哪些知识点,什么是清晰的?什么是模糊的?什么是根本想不起来的?然后把清晰的东西验证一遍,模糊的东西再学一遍,记不住的东西标记一下,这时就需要记笔记了,对于那些模糊记不住的重点记下来,这样的话,学习就能把握一个很好的节奏,知识重点拿下了,记不住的也拿下了,那以后基本上就是忘记这些东西了,以后忘了再看看就行了,这就要比你第一次记笔记牢靠得多,深刻得多,第一次记笔记觉得哪哪都是重点,内心也不放心,恨不得全都记下来。


我们需要经常这么锻炼,你越让自己内心不安全,战战兢兢,大脑越让你记忆深刻,不断这么训练自己,你的记忆力以后会非常强大。


保持独立思考


什么才是独立思考?比如说:当我们面对同样的信息时,有些人就能产出独到的见解,令人印象深刻。在做项目开发时,有的人就能给出新颖可行的方案,我们通常将这些人归结为懂得如何思考的人。


有这样一句话:“当我们一旦融入某个群体中,那么你就会传染上他们习惯以及思维方式,做出一些荒谬绝伦却毫不自知的事情”。


我们每天接收的信息量非常多,但大多数人在看到这些信息之前就已经停止了思考,无论是看新闻,还是刷抖音短视频,获得的知识都只是碎片化信息,但是很多人都把这些碎片化信息当成了知识的全部,缺乏思考的能力,某个博主或者专家说什么就是什么。在思考面前,我们停下了前进的脚步。孰不知,我们接收到的信息都是经过加工的,甚至我们看到那些到处炫耀生活的短视频,都是其他人经过包装后,潜移默化间灌输给我们的。当我们逐渐依赖其他人给出的“答案”时,我们的独立思考能力,就会在这种思维影响下一点一点地消失了。如果我们任其发展不反抗,无异于是对自我的扼杀。一个真正有思想的人,一定是懂得如何独立思考且拥有独立人格的人,他们看待事物不会透过有色眼镜歪曲揣测。而我们应该抱着敞开心扉,拥抱多元化的态度,以一种客观的方式分析遇到的每一件事,在反复的锤炼中,也许我们会发现自己已经不知不觉拥有了独立思考的能力。


坚持不懈


坚持这件事,归根结底就是意志力的较量,谁能挺到最后,谁就是胜利者。而常常放弃,或许不是因为我们无法坚持,而是给自己的退路太多。我觉得这对于 21 世纪的年轻人来说这是世界性的一个难题,我们大多很难去坚持一件事情,但是反观父母年代的人,他们貌似跟我们这个年代的人不太一样,他们坚持一件事情好像比我们容易点。这其实是有一个深刻的原因的,说简单点就是时代变化太快了,在父母那个年代生活条件有限,做什么都不方便,过节走个亲戚,一走就是几十里路,买个东西要走很远才能到镇上,收割庄稼基本都是人工,在父母那个年代里,生活节奏很慢,看起来干什么都像似在浪费时间,但是恰恰他们很勤奋,正是因为他们的生活节奏慢,所以他们的忍耐能力增加了,所以他们成长的快,他们可以接受一切比较慢的事情,需要坚持的事情,在父母那个年代,能上学,能读书,他们会觉得特别好,他们会珍惜这样的机会。


再看看我们现在的生活,一切都变得非常快,做什么都很方便,出门有滴滴打车,饿了有美团送上门,网速提升到 5G 了,但是像学习、减肥还像父母年代那样原始,依然需要我们付出时间和精力,它并没有随着时代的发展而被赋能,也没有随着时代的进步而被简洁化。但是请看看现在的我们,现在的我们变得浮躁了,没有办法接受一切慢的事物,在这种快节奏的时代下,那些需要花时间才能厚积薄发的事情我们如何坚持下去?那这是不是我们这个时代面临最严峻的问题?


世界上一切修行的方法都可以用金刚经里的一句话来总结,叫作善护念。就是说你想做任何事情,想在任何方面取得成就,无论是事业,还是爱情,都可以用善护念来直达最后的高度。什么是善护念呢?用一句话来说就是保护好我们的初心,一个人是否能够坚持下去,取决于自己的发心,他是真正热爱并且发自内心真正想做好这件事情,他坚持的动力都是从内心中生出来的,而不是说外在强加在身上的,更不是说我们喊着去坚持,但内心不想做的事情。


华严经里释迦摩尼也说过一句话:“众生皆具如来智慧德相”。每个人心中都有一座佛,每个人都想成功,那为什么就不能坚持做好一件事情?只因妄想执著,不能证得,各种各样的诱惑蚕食着本心,以至于走着走着却忘了自己的初心。六祖慧能受五祖弘忍大师点拨的时候恍然大悟,说了五句感慨了一下:“何期自性,本自清净;何期自性,本不生灭;何期自性,本自具足;何期自性,本无动摇;何期自性,能生万法”。其中第三句“何期自性,本自具足”跟王阳明的“圣人之道,悟性自足”其实是一样的意思,虽然说一个修佛,一个修儒家,但他们最终修炼到正果的时候,真正取得成果的时候,得出的结论几乎是一样的。只要能守护好自己的初心,坚持是件很简单的事情,最终的成功自然水到渠成。


最后的话


有一首诗叫做《纽约比加州时间早三小时》,它结尾处写道:


其实每个人在自己的时区有自己的步程。不用嫉妒或嘲笑他们。


他们都在自己的时区里,你也是!


生命就是等待正确的行动时机。


所以,放轻松。


你没有落后。


你没有领先。


在命运为你安排的属于自己的时区里,一切都准时。


我很喜欢这首诗,它时刻提醒着我,每个人都有自己的时区,不必着急,未来之路,愿与君共勉!🤝


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

《最近解决的一个bug与最近蹦出的一些想法》

0、一句话概括bug的原因 项目更换了邮箱服务器,原服务器支持的账号格式在新服务器上不被支持;即发送给新服务器的账号错误。 1、最近解决的一个bug (1)bug: java程序通知阿里云邮箱服务器发送邮件失败。 异常报错信息:AuthenticationFa...
继续阅读 »

0、一句话概括bug的原因


项目更换了邮箱服务器,原服务器支持的账号格式在新服务器上不被支持;即发送给新服务器的账号错误。


1、最近解决的一个bug


(1)bug:


java程序通知阿里云邮箱服务器发送邮件失败。

异常报错信息:AuthenticationFailedException: 526 Authentication failure[0]。


(2)背景:


弃用原邮箱服务器、更换为阿里云邮箱服务器后,所有版本的项目向服务器发出的邮件请求均无响应。


(3)排错:


AuthenticationFailedException,翻译过来就是认证不通过异常;认证不通过的原因一般是:服务器错误、用户名错误、用户名密码不匹配。

阿里云官方排错参考连接:阿里邮箱如何通过SMTP程序发信

使用参数在Foxmail中配置,可成功进行SMTP发信;这一步,确定了服务器无错、用户名无错、用户名与密码匹配。

那么,哪里出了问题?

翻阅官网原文: 



经排查,SMTP服务器配置、端口没有错误;那么问题就藏在代码逻辑和参数中。

当时对代码逻辑和参数并未产生质疑:代码延用的是之前对接服务器的部分;需要变动的参数都存在了数据库,并且这些参数在Foxmail上已被验证通过。把问题甩给阿里云人工,工程师查看操作日志后确定服务器接收的账号密码出错。基于出错点,重新复盘:服务器没问题,数据库的帐号密码没问题,那就是java程序处理后并向服务器发送的账号密码出了问题!

程序拿到了正确的帐号密码,却向服务器发送了错误的。在可能出错的代码块内排查:从src文件夹代码到hutool工具类库源码一路debug,发现阿里云邮箱服务器识别不了邮件账号;同样的代码逻辑,发送给原服务器的有效账号是“tairui”,而阿里云服务器需要的是“tairui@aliyun.com”。

最终重新拼接邮件账号字符串,问题解决。


2、最近蹦出的一些想法


(1)软件工程师,是一个什么样的职业?


软件工程师,听上去就是一群建库删库、增删改查数据、开发软件的哥们。

程序员可以像创造了一个又一个世界的操盘手。这个世界的规则都由他说了算:每个对象都是这个社会中的个体,每名个体通过传递消息建立他们的父子、兄弟、恋爱关系;每名个体的本质在于其所处的社会关系,整个社会的本质又是个体间关系的总和。

程序员也仅仅是社会分工的一个角色。他是一名与一个挖水沟的工人并没有太大区别的工人,同样从事着机械性的造轮子工作,同样为社会分工的目的而劳动。


(2)如何从事这样的职业?


跳过基础入门、背八股、刷面经的步骤,假设X已经顺利入职并从事着软件开发的工作,问初入职场的X如何在这个岗位上发热?

得意识到学习能力才是终身竞争力。剔除天赋、运气的因素,剩下的能让X在职场里披荆斩棘的可控因素中,主要因素就是学习能力。

得想明白程序员需要学习的到底是什么。语言是一个工具,框架更是;框架每年都在变,语言的核心思想却贯穿始终。X至少得吃透一门编程语言的教材,形成一个系统的编程思维,以便将来使用其他语言工具时能够一通百通。


(3)不断解决bug的感觉,就像精神鸦片,给平平无奇的工作添加了欢乐。


在毕业后工作满一年的时间跨度里,常常因为解决了一个问题而兴奋,不断地收获工作中的小确幸。

希望每一名劳动者能够在岗位上找到兴趣点,这就像是:在一个六年级毕业的暑假,午后阳光炙热,你怀揣着印着周杰伦半身像的雪碧,一路小跑到大伯家,按下乳白色主机和大屁股显示屏的开关键,伴随着XP系统的开机声急促地呼吸,在IE浏览器上输入www.4399.com;此刻,渴求的眼神、激动的指关节和涌上脸颊的绯红,让你忘记阳光的毒辣、酸胀的肌肉和在气管上切割的空气。

.

.

.

工作满一周年记

20230610 19:10


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

在 SwiftUI 中创建一个环形 Slider

iOS
前言 Slider 控件是一种允许用户从一系列值中选择一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指选择器。有时将这种类型的选择器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如何在 SwiftUI 中定义一个环形的 Slider...
继续阅读 »


前言


Slider 控件是一种允许用户从一系列值中选择一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指选择器。有时将这种类型的选择器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如何在 SwiftUI 中定义一个环形的 Slider。


初始化环形轮廓


ZStack中的三个圆环开始。一个灰色的圆环代表滑块的路径轮廓,一个淡红色的圆弧代表沿着圆环的进度,一个圆圈代表当前光标或拇指的位置。将滑块的范围设置为0.0到1.0,并硬编码一个直径和一个的当前位置进度 - 0.33。

struct CircularSliderView1: View {
let progress = 0.33
let ringDiameter = 300.0

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

var body: some View {
VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
}
.frame(width: ringDiameter, height: ringDiameter)

Spacer()
}
.padding(80)
}
}



将进度值和拇指位置绑定


将进度变量更改为状态变量并添加默认 Slider。这个 Slider 用于修改进度值,并在圆形滑块上实现足够的代码以使拇指和进度弧响应。当前值显示在环形 Slider 的中心。

struct CircularSliderView2: View {
@State var progress = 0.33
let ringDiameter = 300.0

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: 78, weight: .bold, design:.rounded))
}
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.shadow(radius: 3)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
}
.frame(width: ringDiameter, height: ringDiameter)


VStack {
Text("Progress: \(progress, specifier: "%.1f")")
Slider(value: $progress,
in: 0...1,
minimumValueLabel: Text("0.0"),
maximumValueLabel: Text("1.0")
) {}
}
.padding(.vertical, 40)

Spacer()
}
.padding(.vertical, 40)
.padding()
}
}
}


添加触摸手势


DragGesture 被添加到滑块圆圈,并且使用临时文本视图显示拖动手势的当前位置。可以看到 x 和 y 坐标围绕包含环形 Slider 的位置中心的变化情况。

struct CircularSliderView3: View {
@State var progress = 0.33
let ringDiameter = 300.0

@State var loc = CGPoint(x: 0, y: 0)

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

private func changeAngle(location: CGPoint) {
loc = location
}

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: 78, weight: .bold, design:.rounded))
}
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.blue)
.shadow(radius: 3)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged() { value in
changeAngle(location: value.location)
}
)
}
.frame(width: ringDiameter, height: ringDiameter)

Spacer().frame(height:50)

Text("Location = (\(loc.x, specifier: "%.1f"), \(loc.y, specifier: "%.1f"))")

Spacer()
}
.padding(.vertical, 40)
.padding()
}
}
}


为不同的坐标值设置滑块位置


圆形滑块上有两个表示进度的值,用于显示进度弧度的progress值和用于显示滑块光标的rotationAngle。应该只有一个属性来保存滑块进度。视图被提取到一个单独的结构中,该结构具有圆形滑块上进度的一个绑定值。


滑块的range的可选参数也是可用的。这需要对进度进行一些调整,以计算已设置的角度以及拇指在圆形滑块上位置的旋转角度。另外调用onAppear根据View出现前的进度值计算旋转角度。

struct CircularSliderView: View {
@Binding var progress: Double

@State private var rotationAngle = Angle(degrees: 0)
private var minValue = 0.0
private var maxValue = 1.0

init(value progress: Binding<Double>, in bounds: ClosedRange<Int> = 0...1) {
self._progress = progress

self.minValue = Double(bounds.first ?? 0)
self.maxValue = Double(bounds.last ?? 1)
self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}

private var progressFraction: Double {
return ((progress - minValue) / (maxValue - minValue))
}

private func changeAngle(location: CGPoint) {
// 为位置创建一个向量(在 iOS 上反转 y 坐标系统)
let vector = CGVector(dx: location.x, dy: -location.y)

// 计算向量的角度
let angleRadians = atan2(vector.dx, vector.dy)

// 将角度转换为 0 到 360 的范围(而不是负角度)
let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians

// 根据角度更新滑块进度值
progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue)) + minValue
rotationAngle = Angle(radians: positiveAngle)
}

var body: some View {
GeometryReader { gr in
let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9
let sliderWidth = radius * 0.1

VStack(spacing:0) {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9),
style: StrokeStyle(lineWidth: sliderWidth))
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: radius * 0.7, weight: .bold, design:.rounded))
}
// 取消注释以显示刻度线
//Circle()
// .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.6),
// style: StrokeStyle(lineWidth: sliderWidth * 0.75,
// dash: [2, (2 * .pi * radius)/24 - 2]))
// .rotationEffect(Angle(degrees: -90))
Circle()
.trim(from: 0, to: progressFraction)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: sliderWidth, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.shadow(radius: (sliderWidth * 0.3))
.frame(width: sliderWidth, height: sliderWidth)
.offset(y: -radius)
.rotationEffect(rotationAngle)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged() { value in
changeAngle(location: value.location)
}
)
}
.frame(width: radius * 2.0, height: radius * 2.0, alignment: .center)
.padding(radius * 0.1)
}

.onAppear {
self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}
}
}
}


CircularSliderView 的三种不同视图被添加到View中以测试和演示 Circular Slider 视图的不同功能。

struct CircularSliderView5: View {
@State var progress1 = 0.75
@State var progress2 = 37.5
@State var progress3 = 7.5

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.06, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
CircularSliderView(value: $progress1)
.frame(width:250, height: 250)

HStack {
CircularSliderView(value: $progress2, in: 1...10)

CircularSliderView(value: $progress3, in: 0...100)
}

Spacer()
}
.padding()
}
}
}


总结


本文展示了如何定义响应拖动手势的圆环滑块控件。可以设置滑块视图的大小,并且滑块按预期工作。可以向控件添加更多参数以设置颜色或圆环内显示的值的格式。


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

DNS

DNS DNS:Domain Name System 域名系统,应用层协议,是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,基于C/S架构,服务器端:53/udp, 53/tcp实际上,每一台 DNS 服务器都...
继续阅读 »

DNS


DNS:Domain Name System 域名系统,应用层协议,是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,基于C/S架构,服务器端:53/udp, 53/tcp实际上,每一台 DNS 服务器都只负责管理一个有限范围(一个或几个域)内的主机域 名和 IP 地址的对应关系,这些特定的 DNS 域或 IP 地址段称为 zone(区域)。根据地址解 析的方向不同,DNS 区域相应地分为正向区域(包含域名到 IP 地址的解析记录)和反向区 域(包含 IP 地址到域名的解析记录)

根域: 全球根服务器节点只有13个,10个在美国,1个荷兰,1个瑞典,1个日本


  • 一级域名:Top Level Domain: tld
  • 三类:组织域、国家域(.cn, .ca, .hk, .tw)、反向域
  • com, edu, mil, gov, net, org, int,arpa
  • 二级域名:magedu.com
  • 三级域名:study.magedu.com
  • 最多可达到127级域名

ICANN(The Internet Corporation for Assigned Names and Numbers)互联网名称与数字地址分配机构,负责在全球范围内对互联网通用顶级域名(gTLD)以及国家和地区顶级域名(ccTLD)系统的管理、以及根服务器系统的管理


DNS服务器类型


  • 缓存域名服务器:只提供域名解析结果的缓存功能,目的在于提高查询速度和效率, 但是没有自己控制的区域地址数据。构建缓存域名服务器时,必须设置根域或指定其他 DNS 服务器作为解析来源。
  • 主域名服务器:管理和维护所负责解析的域内解析库的服务器
  • 从域名服务器 从主服务器或从服务器"复制"(区域传输)解析库副本

序列号:解析库版本号,主服务器解析库变化时,其序列递增

刷新时间间隔:从服务器从主服务器请求同步解析的时间间隔

重试时间间隔:从服务器请求同步失败时,再次尝试时间间隔

过期时长:从服务器联系不到主服务器时,多久后停止服务

通知机制:主服务器解析库发生变化时,会主动通知从服务器


DNS查询类型及原理


查询方式

  • 递归查询:一般客户机和本地DNS服务器之间属于递归查询,即当客户机向DNS服务器发出请求后,若DNS服务器本身不能解析,则会向另外的DNS服务器发出查询请求,得到最终的肯定或否定的结果后转交给客户机。此查询的源和目标保持不变,为了查询结果只需要发起一次查询。(不需要自己动手)

  • 迭代查询:一般情况下(有例外)本地的DNS服务器向其它DNS服务器的查询属于迭代查询,如:若对方不能返回权威的结果,则它会向下一个DNS服务器(参考前一个DNS服务器返回的结果)再次发起进行查询,直到返回查询的结果为止。此查询的源不变,但查询的目标不断变化,为查询结果一般需要发起多次查询。(需要自己动手)


查询原理过程


正向解析查询过程:

1 先查本机的缓存记录

2 查询hosts文件

3 查询dns域名服务器,交给dns域名服务器处理 以上过程称为递归查询:我要一个答案你直接会给我结果

4 这个dns服务器可能是本地域名服务器,也有个缓存,如果有直接返回结果,如果没有则进行下一步

5 求助根域服务器,根域服务器返回可能会知道结果的一级域服务器,让他去找一级域服务器

6 求助一级域服务器,一级域服务器返回可能会知道结果的二级域服务器让他去找二级域服务器

7 求助二级域服务器,二级域服务器查询发现是我的主机,把查询到的ip地址返回给本地域名服务器

8 本地域名服务器将结果记录到缓存,然后把域名和ip的对应关系返回给客户端


DNS的分布式互联网解析库 



正向解析


各种资源记录


区域解析库:由众多资源记录RR(Resource Record)组成

记录类型:A, AAAA, PTR, SOA, NS, CNAME, MX


  • SOA:Start Of Authority,起始授权记录;一个区域解析库有且仅能有一个SOA记录,必须位于解析库的第一条记录SOA,是起始授权机构记录,说明了在众多 NS 记录里哪一台才是主要的服务器。在任何DNS记录文件中,都是以SOA ( Startof Authority )记录开始。SOA资源记录表明此DNS名称服务器是该DNS域中数据信息的最佳来源。
  • A(internet Address):作用,域名解析成IP地址
  • AAAA(FQDN): --> IPV6
  • PTR(PoinTeR):反向解析,ip地址解析成域名
  • NS(Name Server):,专用于标明当前区域的DNS服务器,服务器类型为域名服务器
  • CNAME : Canonical Name,别名记录
  • MX(Mail eXchanger)邮件交换器
  • TXT:对域名进行标识和说明的一种方式,一般做验证记录时会使用此项,如:SPF(反垃圾邮件)记录,https验证等

安装配置实操


下载安装bind文件,关闭防火墙进行后续操作

cd到etc下的文件夹查询对应软件,编辑named.conf文件




wq保存退出,接下来修改named.rfc1912.zones文件




wq保存退出后cd到var/named的文件下,复制local的文件作为自定义网站的文件模板进行编辑



wq保存退出之后检查有效性,首先修改网卡DNS为当前主机地址然后重启



 接下来开启程序systemctl start named




反向解析


和正向相似,在named.rfc1912文件下添加一段命令之后重新创建一个zones文件,将类型A换成PTR




主从复制


首先需要两台服务器,以我自己的192.168.222.100和192.168.222.200为例

进入etc下的named.conf文件修改两个any




修改etc下的named.rfc1912文件


 

 复制一份named.localhost作为模板进行修改
 

对网卡配置进行修改,然后重启网卡和启动named程序 


 接下来对第二台从服务器进行修改
同上,对named.conf文件进行修改两个any



 接下来修改从服务器的rfc文件




此时自动在slave文件夹下生成主服务器的文件



 修改主服务器的配置,双方重启后从也会变更









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

在前端领域摸爬滚打7年,我终于掌握了这些沉淀技巧

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。 所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序...
继续阅读 »

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。


所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序员的你有所帮助。


我的经历可能会为新手提供一些有用的建议和思路。


01 萌芽之初,点燃编程学习的梦想


对于一些90后的朋友来说,网游填满了他们的高中时期,甚至是初中。


他们经常因为不走寻常路去打游戏,在回来时被门卫大爷逮个正着。尽管我没有沉迷于游戏,但我仍然被游戏所吸引。


在游戏中,我一直认为只有玩家和 NPC 的存在,但是,玩得越多,你会发现还有一些不寻常的角色,那就是“工作室”。部分“工作室”利用一些技术手段批量、自动地在游戏中完成任务以赚取游戏产出。


虽然这种行为不可取,但是他们使用的技术确实让我感兴趣。


这时候,代码的种子已经悄悄埋藏在我的内心深处,等待发芽。


高中毕业后,卸下学业负担,我开始利用暑期学习了一些脚本精灵、Tc 简单编程和易语言编程,这也是我第一次接触编程基础语法,如条件判断、循环、遍历和条件选择,再加上社区提供的一些识图插件,我就像一个蹩脚的裁缝,东拼西凑,左缝右补,费劲巴拉缝制成一件衣服,却不合身。


虽然实现了自动登录游戏的功能,但很不幸运的是,这样的小功能也还是过不去游戏的自检程序,万物皆有裨益,万事皆可为师,正是这一次编程体验促使了我后来的专业选择。


02 踏上编程学习之路,从安卓到前端,每一步都算数


英语是我成长路上的一块绊脚石,在选择专业时,我想躲开英语,于是选择了同为计算机系下的软件外包服务专业,结果发现,只要是技术,英语的要求都是一样的。


当然,我选择这个专业还有另外一个动机 -- 它开设了Android课程。毕竟,那时我刚拿到一款安卓手机,能在手机上开发自己的App是何等酷炫的体验啊!


那时,有一本厚重的《疯狂 Android 讲义》成了我的启蒙之书,我翻过无数遍,上课、参加编程比赛、实习工作、这本书我一直在用,为我第一份工作立下了汗马功劳。


临近毕业,是先就业还是先培训,许多软件相关专业的毕业生都面临着这样的选择。


所以,你要想明白,你到底需要的是什么?


我选择参加培训是出于两个原因:第一是为了将平时自学的知识整合起来,第二是希望能够认识更多的小伙伴,以便进行技术交流。编程最忌讳的就是闭门造车,不进行沟通交流。


然而,选择参加培训并不是每个人的选择。


如果你有能力自己阅读技术书籍,并且知道如何获取最新的技术信息,那么参加培训完全没有必要。


只有当你需要别人的指点和帮助来梳理技能,或者需要更好的机会来进行技术交流时,参加培训才是一个好的选择。


但是,如果你仅仅因为听说培训完就能很赚钱而选择花钱加入,那么你就要好好思考一下了,周围打水漂的人确实不在少数。


培训结束后,2015 年 12 月 7 号,我入职了第一家公司,担任 Android 开发工程师。


人生有时候做一个决策,一个行动,当时只道是寻常,当它的价值在未来某一刻兑现时,你会感谢当时努力的自己。


如果没有大学时翻过无数遍的《疯狂 Android 讲义》,我不可能找到这份工作。


03 学前端到底在学什么


工作后,我第一次真正进入团队开发模式(我是不会告诉你我当初使用百度云盘定时同步代码的,炸过一次硬盘),由于业务需要一定的前端支持(合同模板),所以在一次小组会议上,组长建议我们要着手学习前端技术(Angular1.x)。


到了17年左右,公司的业务开始由原 Pad 端转移到手机端。我和其他几个新入职的小伙伴经过一上午的 Vuejs2.x 培训后,就开始上手开发了。


也是在这次前端项目开发中,我第一次接触到了闭包导致循环失灵的问题,第一次把一个页面写到 3 千多行(烂,不懂拆分)。


由于这次前端项目开发的经验不足,导致迭代两年后,项目能编译出 200MB 的内容。我只能通过各种查找和大量的 webpack 参数调试,将产物压缩回了20MB 左右。对于我来说,这也是一次很大的成长。


我非常推荐各位小伙伴在工作中多承担,因为开发经验绝非是你熟背八股题得到的,开发经验只能是来自大量的项目实战。


多做练习,多遇困难,多做总结,得到的才是自己的。开发经验决定了你的下一个项目能否走得更顺利。


选择成为前端程序员是一件比较苦的事情,因为这个领域的技术更新非常频繁,如果你不持续学习,那么你就会落后,这也是“前端很累”的一个根本原因。


实际上,现在还有一些人对前端存在偏见,因为他们认为不就一个 JavaScript,能有多难?


但是事实上,很多前端构建技术的底层实现并不是用 JavaScript 语言编写的,而是基于了其它编程语言如 Golang(代:ESBuild)和Rust(代表:SWC)“包装”起来的,利用这些语言的特点来弥补 JavaScript 的不足。


前端学习的基础是 JavaScript,但不仅仅是 JavaScript,如果你认为学习 JavaScript 就是学习前端,那么你可能会走进死胡同。


04 正确的学习编程方式一定是这样的


在学校里,老师一定告诉过你两个正确的学习方式,其中一个是要做笔记,另一个是要能够向同学清晰地讲解。


繁多的技术是不可能靠记忆实现的,因此做笔记和写博客是记录学习过程和分享学习成果的捷径。


现在,我也发现很多在校的同学积极在各大技术社区分享自己的学习经验,这也印证了这条成长途径的正确,同时也激励我们这些已经做了多年程序员的伙伴要更加努力。


不论你是学习新的编程语言还是新的框架,都需要为其配置对应环境,但有很多框架的环境配置其实对于第一次接触的小伙伴来说并不友好,就比如我最初在从Android转前端的时候就因为安装NodeJsNpm这些东西而烦恼,因为当时莫名其妙就提示你Python2的模块找不到了,要不就是安装依赖超时了,在环境搭建问题上花费太长时间真的不划算。


为了避免环境搭建影响学习进度,我们可以使用一些在线的 IDE 环境,例如 CodePen、CodeSandBox、Stackblitz、JSRun 等。


但是,它们在依赖安装、操作习惯和响应速度上仍然有一些上手难度。


我最近一段时间一直在使用 1024Code  社区提供的在线 IDE,它提供了很多热门语言和框架的代码空间模板,免配置环境,即开即用随时学习新技术。


它支持多人开发和在线分享,无论是和朋友一起开发项目还是找大佬请教问题,都非常轻松。


05 学习编程,高效沉淀需要技巧


我发现之前写博客时做的案例很难沉淀下来。往往只是写完一遍,很少再打开运行。


但是在 1024Code 中,可以以卡片的形式记录每一个案例,也可以将一系列案例放到一个集合中归类。


此外,1024Code 还支持在个人主页中渲染 Markdown,为小伙伴打造炫酷的个人主页提供了便利。


最令人赞叹的是,1024Code 紧跟最近比较火的 ChatGPT,将其接入到了 IDE 中,让你在编码的同时可以更快速地查找解决方案。下面我给大家简单地展示一下:


在社区主页中,案例以卡片的形式展示。你可以点击你感兴趣的案例,一键运行。边浏览源码,边跟着作者提供的 README 进行学习。


如果你想在此基础上练习或二次开发,还可以 fork 一份到自己的工作空间。如果你发现作者的代码有不合理的地方,还可以在评论区大胆地给他留言,大家可以共同成长。



1024Code 提供了众多空间模板,涵盖了多种编程语言和框架,例如针对数据统计和 AI 模型训练的 Python,以及让许多程序员感到头疼的 C++。


此外,它还支持其它主流的热门编程语言和框架。



Markdown 是编程小伙伴们最常用的笔记格式之一,因此无需专门学习其语法。只需要多看几遍,就可以自然而然地掌握。


此外,你还可以参考社区中其他小伙伴的主页,来打造自己独特的个人主页。



接下来,我要展示一段时间以来我制作的合集。


最初,这个合集是为了帮助那些不熟悉滴滴 LF 框架如何使用 Vue3+TS 编写的小伙伴们而制作的。


我还将合集地址提交到了 LF 仓库,希望能够帮助那些正在转向 Vue3+TS 的小伙伴们。



最重磅的就是 ChatGPT 了。


在使用 1024Code 的 IDE 进行开发过程中,如果遇到问题,你可以快速打开 ChatGPT 来协助你查找答案,而不需要离开当前页面。


ChatGPT 支持上下文连续问答模式,虽然它不能解决你所有的问题,甚至会给出错误的答案,但对于一些常规类编程问题或正在做毕业设计的小伙伴们,它还是能够显著提升效率的。



总结


最后,我再为你做一些总结、建议和对未来的期待:

  • 我建议你要有很强的动力来学习编程,因为坚持并不是易事;

  • 我建议你坚守自己慎重选择的专业,因为不忘初心方得始终;

  • 我建议你在面对技术培训时要清醒认知,因为明确目标的选择才适合自己;

  • 我建议你在工作中抓住一切学习的机会,因为努力的人很多,只有不断学习才能跟上技术的发展;

  • 我建议你在编程学习时要善用工具、做好笔记、写博客,不断沉淀自己的知识和经验;


最后的最后,愿我们所有付出都将是沉淀,所有美好终会如期而至。


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

再聊聊秋招焦虑

我想,关于秋招,大家是真的焦虑的。 作为一个刚起了两个月的公众号,平时写写技术相关的内容,阅读量少则几十,多则也不超过500。结果一聊到秋招这个话题,阅读量直接干到27000去了,让我硬生生地过了一把大V的瘾。(手动狗头) 现代版的“国家不幸诗家幸,赋到沧桑...
继续阅读 »

我想,关于秋招,大家是真的焦虑的。


作为一个刚起了两个月的公众号,平时写写技术相关的内容,阅读量少则几十,多则也不超过500。结果一聊到秋招这个话题,阅读量直接干到27000去了,让我硬生生地过了一把大V的瘾。(手动狗头)



现代版的“国家不幸诗家幸,赋到沧桑句便工”有没有?


所以,我决定再聊十块钱的“秋招焦虑”这个话题,以一个过来人的身份,开导开导同学们。如果同学们真的干了这碗鸡汤后,觉得暖心暖胃,精神上舒服一些,那也不枉我敲这么多字了。


首先说下,产生焦虑的原因是什么?


参照了知乎上的众多答案,我觉得最精辟的一个答案是:担心某件不好的事情,在未来的某个时间点将会发生,但主观上又拒绝接纳。


而往往焦虑的人的认知特点是:


  • 高估不好的事情的发生概率;
  • 高估不好的事情所带来的后果;
  • 低估自己应对挫折的能力;

我们把这种认知特点带入到秋招中,同学们的想法大概是这样:


卧槽,今年校招这么卷,完全就是一片哀鸿遍野的景象,那我肯定找不到工作了。一旦形成毕业即失业的情况,那我前面二十多年爬冰卧雪、寒窗苦读就完全看不到价值了。试问读书的意义是什么呢?完了,生不逢时啊,我这辈子基本上也就交待了。


而在这种焦虑的情绪下,最常见的应激反应就是回避行为。即:有条件的打算润到国外,没条件的誓死考公。


下面,开始灌鸡汤,各位同学听好。


超过80%的人,你不必焦虑


其实各行各业都一样,你只要能超过行业内80%的人,基本上就没什么好担心的,降低一些预期,找到一份差不多的工作肯定没问题。


逻辑是这样的,如果一个行业的top 20%都凉凉了,那证明这个行业不仅仅是不景气,而是被这个时代所淘汰了。如果真的到了这个地步,那最焦虑的肯定不是作为个体的你了。


按照top 20%这个人物画像进行圈选的话,很多专业能力不错,有实习经历的211 985同学,其实是都在其内的。


你如果还在焦虑的话,那就是世上本无事,庸人自扰之了。



当然,再说一句,如果你不在这20%的池子里面,那你还轮不到焦虑,你最先做好的的是四个字:反求诸己。


反求诸己,很多事情上都是这么一个道理,就是苦练基本功。


正常周期变化,你无须焦虑


一般情况下,经济和行业周期大概在8到10年左右。任何人漫长的职业生涯中,都会经历几个起起伏伏的过程,有时候早经历一次,未必是坏事。


郭德纲的原话是这样说的:


吃亏要趁早,一帆风顺不是什么好事。从小大伙娇生惯养,没有人跟他说过什么话,65岁走在街上谁瞪他眼就会猝死。从出生开始一天打八个嘴巴,这样的到25岁就是铁罗汉、活金刚一样什么都不在乎,吃亏要趁早。


其实话糙理不糙,当你经历了萧条的经济周期,再遇到经济转好,行业内一片欣欣向荣的时候,你会怀着更加感恩的心态,拥有更成熟的理性思考,再也不会把风口机遇和平台资源当做个人能力。


说得粗鄙一点儿就是,变得更加有逼数儿了,不好高骛远,不过度消费,这样反而让你以后的路走得更平稳一些。



不就是一次秋招吗,你焦虑个毛


有的同学性格过于敏感脆弱,总是会把一件不好的事情的后果,弄得跟世界末日一样。


其推理过程大概这样:


少了一枚铁钉,掉了一只马掌。掉了一只马掌,失去一匹战马。失去一匹战马,失去一场战役。败了一场战役,毁了一个王朝。



等于“千里之堤,毁于蚁穴”的道理,让他给用到这里了。


其实真的不用这么想,就算秋招不太理想,还有春招,春招就算也不理想,直接走社招投简历不就完了。退一万步说,大不了转行,只要你不懒不傻不笨,照样能在别的行业混得风生水起。


真的真的没必要,认为秋招GG了,整个人生都黯淡了。人这一辈子很长,有很多翻盘的机会,也有很多盛极而衰的事例,没必要计较一时之成败。


像男人一样,扛过焦虑期


永远不要低估人类应对挫折和低谷的能力。


想想我们的父母辈,正好赶上了国企的下岗潮,捧了半辈子的铁饭碗丢了。就像刘欢的那首《重头再来》所唱到的那样,”辛辛苦苦已度过半生,今夜重又走进风雨“。


但是,基本上没看到下岗的哪家哪户断粮饿死,最不济的也是出去打工或者做点儿小生意,还有的因此而混出一些名堂出来呢。


就像曾国藩的那句名言所说的一样:生平长进,全在受挫受辱之时,需咬牙立志,受不得穷,立不得品,受不得屈,必做不得事。


我们再来看看褚时健老爷子,以前是云南红塔集团的董事长,后来由于贪污受贿被判处无期徒刑,和妻子双双入狱,女儿自杀,他的低谷堪称绝望之谷。


2002年,74岁出狱二次创业,开始做褚橙。到了2014年,”褚橙“销售额达到了一亿多元,纯利润7000多万。因此,人们又称它为”励志橙“。



难怪见过大世面的王石都这样感慨道:


橙子挂果要6年,他那时已经75岁高龄了。试想一下,一个75岁的老人,戴一个大墨镜,穿着颇圆领衫,兴致勃勃地跟我谈论橙子挂果是什么场景。我当时就想,如果我遇到他那样的挫折,到了他那个年纪,我会想什么?我知道,我一定不会像他那样勇敢。


所以,像个男人一样,顶住压力,行动起来吧。扛过这段焦虑期吧,阴霾只是暂时的,未来终将是美好的。


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

Xcode 升级到14.3以后 调试与打包遇到的坑

iOS
前言 是苹果逼的,通知说2023年4月25日之后,所有的App都要在iOS16的SDK上打包。不然也不会有那么多事情(呜呜呜🥹)。 1.Xcode 14.3版本运行项目报错 问题如下:ld: file not found: /Applications/Xcod...
继续阅读 »

前言


是苹果逼的,通知说2023年4月25日之后,所有的App都要在iOS16的SDK上打包。不然也不会有那么多事情(呜呜呜🥹)。


1.Xcode 14.3版本运行项目报错


问题如下:

ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
clang: error: linker command failed with exit code 1 (use -v to see invocation)

报错信息看,都是在链接库的时候因为找不到静态库(libarclite_iphonesimulator.a/libarclite_iphoneos.a)而报错。利用访达的前往文件夹功能快速来到报错信息中的目录,发现连 arc目录都不存在,更不用说静态库文件。


开发人员解释说,因为系统已经内置有 ARC相关的库,所以没必要再额外链接,至少Xcode 14支持的最低部署目标iOS 11及以上版本的系统肯定是没问题的。如果应用部署目标不低于iOS 11还出现问题,那么应该是第三方库的部署目标有问题。


所以解决方案也很清晰了,将所有依赖库和应用最低部署版本都限制在iOS11以上即可。


解决方案:

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end

2. 升级Xcode14以后项目报错 Stored properties cannot be marked potentially unavailable with '@available'


这是依赖库报错,把其中一个库升级到了最新的版本,不报错了。但是还有一个库没办法升级,因为我们的项目是Flutter项目,不知道是哪个三方库的依赖库,百度了好久没找到办法,最后还是强大的Google到方法:


在iOS目录下:

执行pod install
然后再执行pod update

最终可以了


3. Xcode升级到14.3 archieve打包失败

mkdir -p /Users/hsf/Library/Developer/Xcode/DerivedData/Ehospital-crirdmppgluxkodauexhkenjuxet/Build/Intermediates.noindex/ArchiveIntermediates/Ehospital/BuildProductsPath/Release-iphoneos/复旦云病理.app/Frameworks
Symlinked...
rsync --delete -av --filter P .*.?????? --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/AliyunOSSiOS.framework" "/Users/hsf/Library/Developer/Xcode/DerivedData/Ehospital-crirdmppgluxkodauexhkenjuxet/Build/Intermediates.noindex/ArchiveIntermediates/Ehospital/InstallationBuildProductsLocation/Applications/复旦云病理.app/Frameworks"
building file list ... rsync: link_stat "/Users/hsf/Desktop/medical/app/iOS/Ehospital/../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/AliyunOSSiOS.framework" failed: No such file or directory (2)
done

sent 29 bytes received 20 bytes 98.00 bytes/sec
total size is 0 speedup is 0.00
rsync error: some files could not be transferred (code 23) at /AppleInternal/Library/BuildRoots/97f6331a-ba75-11ed-a4bc-863efbbaf80d/Library/Caches/com.apple.xbs/Sources/rsync/rsync/main.c(996) [sender=2.6.9]
Command PhaseScriptExecution failed with a nonzero exit code

找到...-frameworks.sh 文件,替换

source="$(readlink "${source}")"

source="$(readlink -f "${source}")"

4. The version of CocoaPods used to generate the lockfile (1.3.1) is higher than the version of the current executable (1.1.0.beta.1). Incompatibility issues may arise.


这个比较简单,更新cocoapods就行。

sudo gem install cocoapods

5. Warning: CocoaPods minimum required version 1.6.0 or greater not installed…

sudo gem install cocoapods

6. Cocoapods 更新卡死在1.5.3,但控制台一直提示说有新版本


主要就是ruby的问题了。别问我怎么知道的,花了一天的时间。

ruby -v 查看版本

若比较低,现在一般都3.x了,所以要升级


用以下命令就可以升级了,可能需要科学上网。

brew update
brew install ruby

升级完成以后,ruby -v后其实还是原来的版本👌,这是因为环境变量没有配置。因此,还有一个步骤就是配置环境变量。

vi ~/.zshrc 

拷贝 export PATH="/usr/local/opt/ruby/bin:$PATH" 放进去


英文输入法下 按下esc键 输入 :wq


最后再执行

source ~/.bash_profile

然后更新gem

gem update #更新所有包
gem update --system #更新RubyGems软件

最后再更新pod

sudo gem install cocoapods

注意现在可能会提示说更新到了1.12.1了,但实际上还是1.5.3,所以还要执行另外一个命令。

sudo gem install -n /usr/local/bin cocoapods

这个就可以有效升级了。


7. gem常用命令

gem -v #gem版本
gem update #更新所有包
gem update --system #更新RubyGems软件
gem install rake #安装rake,从本地或远程服务器
gem install rake --remote #安装rake,从远程服务器
gem install watir -v(或者--version) 1.6.2#指定安装版本的
gem uninstall rake #卸载rake包
gem list d #列出本地以d打头的包
gem query -n ''[0-9]'' --local #查找本地含有数字的包
gem search log --both #从本地和远程服务器上查找含有log字符串的包
gem search log --remoter #只从远程服务器上查找含有log字符串的包
gem search -r log #只从远程服务器上查找含有log字符串的包
gem help #提醒式的帮助
gem help install #列出install命令 帮助
gem help examples #列出gem命令使用一些例子
gem build rake.gemspec #把rake.gemspec编译成rake.gem
gem check -v pkg/rake-0.4.0.gem #检测rake是否有效
gem cleanup #清除所有包旧版本,保留最新版本
gem contents rake #显示rake包中所包含的文件
gem dependency rails -v 0.10.1 #列出与rails相互依赖的包
gem environment #查看gem的环境

结语


有些坑现在只是知道这样做就行,还不知道为什么。后面再补补吧。


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

iOS-解决定位权限卡顿问题

iOS
一、简介 在iOS系统中,定位权限获取是一个涉及进程间同步通信的方法,如果频繁访问可能会导致卡顿或者卡死。在一些打车或者地图类的APP中,定位权限的卡顿报错可能是大头,亟需解决! 下面是系统类提供的访问定位权限的方法:// CLLocationManager是...
继续阅读 »

一、简介


在iOS系统中,定位权限获取是一个涉及进程间同步通信的方法,如果频繁访问可能会导致卡顿或者卡死。在一些打车或者地图类的APP中,定位权限的卡顿报错可能是大头,亟需解决!
下面是系统类提供的访问定位权限的方法:

// CLLocationManager是系统的定位服务管理类
open class CLLocationManager : NSObject {
// 1.下面方法是访问系统设置中定位是否打开
@available(iOS 4.0, *)
open class func locationServicesEnabled() -> Bool

// 2.1 iOS 14.0之后,访问定位的授权状态
@available(iOS 14.0, *)
open var authorizationStatus: CLAuthorizationStatus { get }

// 2.2 iOS 14.0之后,访问定位的授权状态
@available(iOS, introduced: 4.2, deprecated: 14.0)
open class func authorizationStatus() -> CLAuthorizationStatus
}

二、从卡顿堆栈例子中分析问题


为了解决这个卡顿,首先要分析卡顿报错堆栈。接下来举一个定位权限频繁获取导致的卡顿的堆栈:

0 libsystem_kernel.dylib _mach_msg2_trap + 8
1 libsystem_kernel.dylib _mach_msg2_internal + 80
2 libsystem_kernel.dylib _mach_msg_overwrite + 388
3 libsystem_kernel.dylib _mach_msg + 24
4 libdispatch.dylib __dispatch_mach_send_and_wait_for_reply + 540
5 libdispatch.dylib _dispatch_mach_send_with_result_and_wait_for_reply + 60
6 libxpc.dylib _xpc_connection_send_message_with_reply_sync + 240
7 Foundation ___NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__ + 16
8 Foundation -[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:] + 2236
9 Foundation -[NSXPCConnection _sendSelector:withProxy:arg1:arg2:arg3:] + 136
10 Foundation __NSXPCDistantObjectSimpleMessageSend3 + 76
11 CoreLocation _CLCopyTechnologiesInUse + 30852
12 CoreLocation _CLCopyTechnologiesInUse + 25724
13 CoreLocation _CLClientStopVehicleHeadingUpdates + 104440
14 MyAPPName +[ZLLocationRecorder locationAuthorised] + 40
15 ... // 以下略
  • 首先从第14行找到是ZLLocationRecorder类的locationAuthorised方法调用后,执行到了系统库函数,最终导致了卡死、卡顿。

  • 对堆栈中第0-13行中的方法做一番了解,初步发现xpc_connection_send_message_with_reply_sync函数涉及进程间同步通信,可能会阻塞当前线程点击查看官方方法说明



该函数说明:Sends a message over the connection and blocks the caller until it receives a reply.

  • 接下来添加符号断点xpc_connection_send_message_with_reply_sync, 注意如果是系统库中的带下划线的函数,我们添加符号断点的时候一般需要少一个下划线_. 执行后,从Xcode的方法调用栈视图中查看,可以发现ZLLocationRecorder类的locationAuthorised方法内部中调用CLLocationManager类的locationServicesEnabledauthorizationStatus方法都会来到这个符号断点.所以确定了是这两个方法导致的卡顿。(调试时并未发现卡顿,只是线上用户的使用环境更加复杂,卡顿时间长一点就被监控到了,我们目前卡顿监控是3秒,卡死监控是10s+)。

  • 然后通过CLLocationManager类的authorizationStatus方法说明,发现也是说在权限发生改变后,系统会保证调用代理方法locationManagerDidChangeAuthorization(_:),所以就产生了我们的解决方案,最终上线后也是直接解决了这个卡顿,并且APP启动耗时监控数据也因此变好了一些。


三、具体的解决方案



 注意点:设置代理必须在有runloop的线程,如果业务量不多的话,就在主线程设置就可以。


四、Demo类,可以直接用

import CoreLocation

public class XLLocationAuthMonitor: NSObject, CLLocationManagerDelegate {
// 单例类
@objc public static let shared = XLLocationAuthMonitor()

/// 定位服务是否可用, 这里设置成变量避免过于频繁调用系统方法时产生卡顿,系统方法涉及进程间通信
@objc public private(set) var serviceEnabled: Bool {
set {
threadSafe { _serviceEnabled = newValue }
}

get {
threadSafe { _serviceEnabled ?? CLLocationManager.locationServicesEnabled() }
}
}

/// 定位服务授权状态
@objc public private(set) var authStatus: CLAuthorizationStatus {
set {
threadSafe { _authStatus = newValue }
}

get {
threadSafe {
if let auth = _authStatus {
return auth
}
if #available(iOS 14.0, *) {
return locationManager.authorizationStatus
} else {
return CLLocationManager.authorizationStatus()
}
}
}
}

/// 计算属性,这里返回当前定位是否可用
@objc public var isLocationEnable: Bool {
guard serviceEnabled else {
return false
}

switch authStatus {
case .authorizedAlways, .authorizedWhenInUse:
return true
case .denied, .notDetermined, .restricted:
return false
default: return false
}
}

// MARK: - 内部使用的私有属性
private lazy var locationManager: CLLocationManager = CLLocationManager()
private let _lock = NSLock()
private var _serviceEnabled: Bool?
private var _authStatus: CLAuthorizationStatus?

private override init() {
super.init()
// 如果是主线程则直接设置,不是则在mainQueue中设置
DispatchQueue.main.safeAsync {
self.locationManager.delegate = self
}
}

private func threadSafe<T>(task: () -> T) -> T {
_lock.lock()
defer { _lock.unlock() }
return task()
}

// MARK: - CLLocationManagerDelegate
/// iOS 14以上调用
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if #available(iOS 14.0, *) {
authStatus = locationManager.authorizationStatus
serviceEnabled = CLLocationManager.locationServicesEnabled()
}
}

/// iOS 14以下调用
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
authStatus = status
serviceEnabled = CLLocationManager.locationServicesEnabled()
}
}

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

小米:阳了,被裁了

hi 大家好,我是 DHL。公众号:ByteCode ,专注有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、大厂面经。 随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上...
继续阅读 »

hi 大家好,我是 DHL。公众号:ByteCode ,专注有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、大厂面经。



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


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


裁员


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


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



小米为什么要裁员


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


  1. 公司不赚钱,意味着主营业务开始萎缩,小米的主营业务,手机前 3 个季度卖了 4020 万部,销售额大概 425 亿,平均每部手机 1000 元,原本指望华为被制裁之后,小米能拿下这部分用户,但是最后也放弃了,这部分用户基本上都归苹果了
  2. 据调查中国的手机市场已经处于饱和状态,每年换手机的发烧友越来越少了
  3. 小米赌上全部身价大踏步地进入汽车领域,汽车是个周期长、投资大的业务,没有上百个亿,基本上不可能会有结果的
  4. 小米的股价也跌了很多,投资人很失望,我也买了很多小米的股票,基本上都是血亏

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


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


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


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




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



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


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


文章的最后


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


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

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