注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

实现一个简易的热🥵🥵更新

简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
继续阅读 »

简单模拟一个热更新


什么是热更新



热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



热更新的优点


实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


webpack 中的热更新



在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



原理如下:


客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


(1)找到被替换的模块并卸载它。


(2)下载新的模块代码,并对其进行注入和执行。


(3)重新渲染或更新应用程序的相关部分。


保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


代码模拟



在同一个目录下创建 server.js 和 watcher.js



server.js

const http = require("http");
const server = http.createServer((req, res) => {
res.statusCode = 200;
// 设置字符编码为 UTF-8,若有中文也不乱码
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("offer get!!!");
});

server.listen(7777, () => {
console.log("服务已启动在 7777 端口");
process.send("started");
});

// 监听来自 watcher.js 的消息
process.on("message", (message) => {
if (message === "refresh") {
// 重新加载资源或执行其他刷新操作
console.log("重新加载资源");
}
});


(1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


(2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


(3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


(4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


(5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


watcher.js

const fs = require("fs");
const { fork } = require("child_process");

let childProcess = null;

const watchFile = (filePath, callback) => {
fs.watch(filePath, (event) => {
if (event === "change") {
console.log("文件已经被修改,重新加载");

// 如果之前的子进程存在,终止该子进程
childProcess && childProcess.kill();

// 创建新的子进程
childProcess = fork(filePath);
childProcess.on("message", callback);
}
});
};

const startServer = (filePath) => {
// 创建一个子进程,启动服务器
childProcess = fork(filePath);
childProcess.on("message", () => {
console.log("服务已启动!");
// 监听文件变化
watchFile(filePath, () => {
console.log("文件已被修改");
});
});
};

// 注意文件的相对位置
startServer("./server.js");


watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


(1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


(2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


(3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


(4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


(5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


(6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


效果图


打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样




当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


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

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

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

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


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


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


1、他的问题


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


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


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


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


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


2、几个建议


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


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


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


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



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

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

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


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


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


3、写在最后


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


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


无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒

作者:程序员晚枫
来源:juejin.cn/post/7209447968218841144
:如果不是十分热爱,请务必三思~

收起阅读 »

JavaScript中return await究竟有无用武之地?

web
我先回答:有的,参考文章末尾。 有没有区别?  先上一个Demo,看看async函数中return时加和不加await有没有区别: function bar() { return Promise.resolve('this from bar().'); ...
继续阅读 »

我先回答:有的,参考文章末尾。



有没有区别?


 先上一个Demo,看看async函数中return时加和不加await有没有区别:


function bar() {
return Promise.resolve('this from bar().');
}

async function foo1() {
return await bar(); // CASE#1 with await
}

async function foo2() {
return bar(); // CASE#2 without await
}

// main
(() => {
foo1().then((res) => {
console.log('foo1:', res); // res is string: 'this from bar().'
})
foo2().then((res) => {
console.log('foo2:', res); // res is string: 'this from bar().'
})
})();

 可能在一些社区或团队的编程规范中,有明确要求:不允许使用非必要的 return await。给出的原因是这样做对于foo函数而言,会增加等待bar函数返回的Promise出结果的时间(但其实它可以不用等,因为马上就要return了嘛,这个时间应留给foo函数的调用者去等)。


 如果你觉得上面的文字不大通顺,直接看代码,问:以上例子中,foo1()函数和foo2()函数的写法对程序的执行过程有何影响?


 先说结论:async 函数中 return await promise;return promise; 从宏观结果来看是一样的,但微观上有区别。


有什么区别?


 基于上面的Demo改造一下,做个试验:


const TAG = Symbol();
const RESULT = Promise.resolve('return from bar().');
RESULT[TAG] = 'TAG#RESULT';

function bar() {
return RESULT;
}

async function foo1() {
return await bar();
}

async function foo2() {
const foo2Ret = bar();
console.log('foo2Ret(i):', foo2Ret[TAG], foo2Ret === RESULT); // 'TAG#RESULT', true (1)
return foo2Ret; // without await
}

// main
(() => {
const foo1Ret = foo1();
console.log('foo1Ret:', foo1Ret[TAG], foo1Ret === RESULT); // undefined, false (2)
console.log('--------------------------------------------');
const foo2Ret = foo2();
console.log('foo2Ret(o):', foo2Ret[TAG], foo2Ret === RESULT); // undefined, false (3)
})();

 从注释标注的执行结果可以看到:



  • (1)处没有疑问,foo2Ret 本来就是 RESULT

  • (2)处应该也没有疑问,foo1Ret 是基于 RESULT 这个Promise的结果重新包装的一个新的Promise(只是这个Promise的结果和Result是一致的);

  • (3)处应该和常识相悖,竟然和(2)不一样?是的,对于 async 函数不管return啥都会包成Promise,而且不是简单的通过 Pomise.resolve() 包装。


 那么结论就很清晰了,async 函数中 return await promise;return promise; 至少有两个区别:



  1. 对象上的区别:

    • return await promise; 会先把promise的结果解出来,再构造成新的Promise

    • return await promise; 直接在promise的基础上构造Promise,也就是套了两个Promise(两层Promise的状态和结果是一致的)



  2. 时间上的区别:假设 bar() 函数耗时 10s

    • foo1() 中的写法会导致这10s消耗在 foo1() 函数的执行上

    • foo2() 的写法则会让10s的消耗在 foo2() 函数的调用者侧,也就是注释为main的匿名立即函数




 从对象上的区别看,不论怎样async函数都会构造新的Promise对象,有无await都节约不了内存;从时间上来看,总体的等待时长理论上是一样的,怎么写对结果都没啥影响嘛。


 举个不大恰当的例子:你的上司交给你一个重要任务让你完成后发邮件给他,你分析了下后发现任务需要同事A做一部分,遂找他。同事A完成他的部分需要2天。这个时候你有两个做法选择:一、做完自己的部分后等着A出结果,有结果后再发邮件回复上司;二、将自己的部分完成后汇报给上司,并跟和上司说已经告知A:让A等完成他的部分后直接回邮件给上司。


 如果,我是说假如果哈,如果,这个重要任务本来要求必须在12h内完成,但实际耗时了两天严重超标......请问上述例子中哪种做法更容易获取N+1大礼包?


到底怎么写?


 回到代码层,通过上述分析可以知道,一个主要是耗时归属问题,一个是async函数“总是”会返回的那个Promise对象不是由Promise.resolve()简单包装的(因为Promise.resolve(promise) === promise),可以得到两个编码指南:



强调下,async函数不是通过Pomise.resolve()简单包装的,其实进一步思考下也不难理解,因为它要考虑执行有异常的场景,甚至还可能根据不同的Promise状态做一些其他的操作(比如日志输出、埋点统计?我瞎猜的)



// 避免非必要的 return await 影响模块耗时统计的准确性
async function foo() {
return bar();
}

// 除非你要处理执行过程中的异常
async function foo() {
try {
return await bar();
} catch (_) {
return null;
}
}
// 或:
async function foo() {
return bar().catch(() => null);
}

// async 函数中避免对返回值再使用多余的 Pomise 包装
async function bar() {
return 'this is from bar().'; // YES
}
async function bar() {
return Promise.resolve('this is from bar().'); // !!! NO !!!
}

回到标题:JavaScript中return await有无用武之地?


答:有的,当需要消化掉依赖 Promise

作者:Chavin
来源:juejin.cn/post/7268593569781350455
执行中的异常时。

收起阅读 »

吐槽大会,来瞧瞧资深老前端写的垃圾代码

web
阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉 忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑...
继续阅读 »

阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉



忍无可忍,不吐不快。


本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


---------------------------------------------更新------------------------------------------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


image.png


组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


image.png


条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


image.png


滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


image.png


留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


image.png


image.png


丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


image.png


一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


image.png


变态的链式取值和赋值


都懒得说了,各位观众自己看吧。


image.png


代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


image.png


这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


image.png


杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


image.png


less、scss 混用


这是最奇葩的。


image.png


特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。


image.png


写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


不过说这么多,成事在人。


不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

作者:北岛贰
来源:juejin.cn/post/7265505732158472249

收起阅读 »

实现一个简易的热🥵🥵更新

web
简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
继续阅读 »

简单模拟一个热更新


什么是热更新



热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



热更新的优点


实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


webpack 中的热更新



在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



原理如下:


客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


(1)找到被替换的模块并卸载它。


(2)下载新的模块代码,并对其进行注入和执行。


(3)重新渲染或更新应用程序的相关部分。


保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


代码模拟



在同一个目录下创建 server.js 和 watcher.js



server.js


const http = require("http");
const server = http.createServer((req, res) => {
res.statusCode = 200;
// 设置字符编码为 UTF-8,若有中文也不乱码
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("offer get!!!");
});

server.listen(7777, () => {
console.log("服务已启动在 7777 端口");
process.send("started");
});

// 监听来自 watcher.js 的消息
process.on("message", (message) => {
if (message === "refresh") {
// 重新加载资源或执行其他刷新操作
console.log("重新加载资源");
}
});


(1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


(2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


(3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


(4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


(5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


watcher.js


const fs = require("fs");
const { fork } = require("child_process");

let childProcess = null;

const watchFile = (filePath, callback) => {
fs.watch(filePath, (event) => {
if (event === "change") {
console.log("文件已经被修改,重新加载");

// 如果之前的子进程存在,终止该子进程
childProcess && childProcess.kill();

// 创建新的子进程
childProcess = fork(filePath);
childProcess.on("message", callback);
}
});
};

const startServer = (filePath) => {
// 创建一个子进程,启动服务器
childProcess = fork(filePath);
childProcess.on("message", () => {
console.log("服务已启动!");
// 监听文件变化
watchFile(filePath, () => {
console.log("文件已被修改");
});
});
};

// 注意文件的相对位置
startServer("./server.js");


watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


(1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


(2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


(3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


(4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


(5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


(6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


效果图


打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样


image.png


当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


image.png

收起阅读 »

工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
继续阅读 »

前言



哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



正文



不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




这种写法埋了一个不大不小的雷。




用一段测试代码就可以展示出来问题



1.jpg



打印结果如下:



2.jpg



很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




比如我如果换成2023-12-30又不会有问题了



3.jpg



另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



4.jpg



避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



总结




  1. 日期时间格式统一使用yyyy小写;

  2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人

作者:程序员济癫
来源:juejin.cn/post/7269013062677823528
了。


收起阅读 »

简历中不写年龄、毕业院校、预期薪资会怎样?

无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。 之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推...
继续阅读 »

无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。


之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推导,那么这些事实和观点是不足信的,需要慎重对待。


视频的内容是这样的:“不管你有多自信,简历中的个人信息都不要这样写。1、写了期望薪资,错!2、写了户籍地址,错!3、写了学历文凭,错!4、写了离职原因,错!5、写了生日年龄,错!6、写了自我评价,错!


正确写法,只需要写姓名和手机号、邮箱及求职意向即可,简历个人信息模块的作用是让HR顺利联系到你,所有任何其他内容都不要写在这里……”


针对这条视频的内容,有两个不同的表现:第一就是分享和点赞数量还可以,都破千了;第二就是评论区很多HR和求职着提出了反对意见。


第一类反对意见是:无论求职者或HR都认为这样的简历是不合格的,如果不提供这些信息,根本没有预约面试的机会,甚至国内的招聘平台的简历模板都无法通过。第二类,反对者认为,如果不写这些信息,特别是预期薪资,会导致浪费双方的时间。


针对上述质疑,作者的回复是:”看了大家的评论,我真的震惊,大家对简历的误解是如此至深……“


仔细看完视频和评论,在视频的博主和评论者之间产生了一个信息差。博主说的”个人信息“不要写,给人了极大的误导。是个人信息栏不要写,还是完全不写呢?看评论,大多数人都理解成了完全不写。博主没有说清楚是不写,还是写在别处,这肯定是作者的锅。


本人也筛选过近千份简历,下面分享一下对这则视频中提到的内容的看法:


第一,户籍、离职原因可以不写


视频中提到的第2项和第4项的确可以不写。


户籍这一项,大多数情况下是可以不写的,只用写求职城市即可,方便筛选和推送。比如,你想求职北京或上海的工作,这个是必须有的,而你的户籍一般工作没有强制要求。但也有例外,比如财务、出纳或其他特殊岗位,出于某些原因,某些公司会要求是本地的。写不写影响没那么大。


离职原因的确如他所说的,不建议写,是整个简历中都不建议写。这个问到了再说,或者填写登记表时都会提到,很重要,要心中有准备,但没必要提前体现。


第二,期望薪资最好写上


关于期望薪资这个有两种观点,有的说可以不写,有的说最好写上。其实都有道理,但就像评论中所说:如果不写,可能面试之后,薪资相差太多,导致浪费了双方的时间。


其实,如果可以,尽量将期望薪资写上,不仅节省时间,这里还稍微有一个心理锚定效应,可以把薪资写成范围,而范围的下限是你预期的理想工资。就像讨价还价时先要一个高价,在简历中进行这么一个薪资的锚定,有助于提高最终的薪资水平。


第三,学历文凭一定要写


简历中一定要写学历文凭,如果没有,基本上是会默认为没有学历文凭的,是不会拿到面试邀约的。仔细想了一下,那则视频的像传达的意思可能是不要将学历文凭写作个人信息栏,而是单独写在教育经历栏中。但视频中没有明说,会产生极大的误导。


即便是个人信息栏,如果你的学历非常漂亮,也一定要写到个人信息栏里面,最有价值,最吸引眼球的信息,一定要提前展现。而不是放在简历的最后。


第四,年龄要写


视频中提到了年龄,这个是招聘衡量面试的重要指标,能写尽量写上。筛选简历中有一项非常重要,就是年龄、工作经历和职位是否匹配。在供大于求的市场中,如果不写年龄,为了规避风险,用人方会直接放弃掉。


前两个月在面试中,也有遇到因为年龄在30+,而在简历中不写年龄的。作为面试官,感觉是非常不好的,即便不写,在面试中也需要问,最终也需要衡量年龄与能力是否匹配的问题。


很多情况下,不写年龄,要么认为简历是不合格的,拿不到面试机会,要么拿到了面试机会,但最终只是浪费了双方的时间。


第五,自我评价


这一项与文凭一样,作者可能传达的意思是不要写在个人信息栏中,但很容易让人误解为不要写。


这块真的需要看情况,如果你的自我评价非常好,那一定要提前曝光,展现。


比如我的自我评价中会写到”全网博客访问量过千万,CSDN排名前100,出版过《xxx》《xxx》书籍……“。而这些信息一定要提前让筛选简历的人感知到,而不是写在简历的最后。


当然,如果没有特别的自我评价,只是吃苦耐劳、抗压、积极自主学习等也有一定的积极作用,此时可以考虑放在简历的后面板块中,而不是放在个人信息板块中。这些主观的信息,更多是一个自我声明和积极心态的表现。


最后的小结


经过上面的分析,你会看到,并不是所有的结论都有统一的标准的。甚至这篇文章的建议也只是一种经验的总结,一个看问题的视角而已,并不能涵盖和适用所有的场景。而像原始视频中那样,没有分析,没有推导,没有数据支撑,没有对照,只有干巴巴的结论,外加的煽动情绪的配音,就更需要慎重对待了。


在写这篇文章的过程中,自己也在想一件事:任何一个结论,都需要在特定场景下才能生效,即便是牛顿的力学定律也是如此,这才是科学和理性的思维方式。如果没有特定场景,很多结

作者:程序新视界
来源:juejin.cn/post/7268593569782054967
论往往是不成立的,甚至是有害的。

收起阅读 »

良言难劝该死鬼,居然有人觉得独立开发做三件套是件好事

没想到某个大V居然写了一篇公众号文章回应独立开发者三件套。认为无脑做大路货也好! 我其实挺烦这种清新圣母派,就是无论你做什么,反正我都不拦着,我就鼓励你 follow your heart。人生没有白走的路。口头上一百个支持,谁都不得罪,等你真吃亏了这些人就不...
继续阅读 »

Pasted Graphic.png


没想到某个大V居然写了一篇公众号文章回应独立开发者三件套。认为无脑做大路货也好!


我其实挺烦这种清新圣母派,就是无论你做什么,反正我都不拦着,我就鼓励你 follow your heart。人生没有白走的路。口头上一百个支持,谁都不得罪,等你真吃亏了这些人就不知道哪去了。


我提两个点:首先这个作者就不是干独立开发的,你就听不会游泳的人跟你讲如何游泳这靠谱吗,IT王语嫣是吧。要是你真独立开发有成绩,你这么说我也觉得多少有点信用背书。现在说这话立场就跟某些博主推荐东西一样,你买我推荐,我买我不买。哪天他要是自己下场做笔记我也服。


其次他的观点就是反正做出来就是成功。你还好意思问别人怎么定义成功,你这成功定义的,既然我做什么都是成功,我为什么要做大路货,我做其他也算是成功。既然第一款做笔记失败概率大,不还是要面临到底我要做什么问题吗。既然都是做,为什么不做点有趣的东西



但是我要补充说明一个观点,独立开发起步雷区里是没有番茄钟的。番茄钟还是挺新手友好的。



这就是我讽刺做这些是独立开发死亡加速三件套的原因,因为独立开发的真正内核是独创性。独创性可以是不赚钱的,可以是小众的,可以是无用但有趣的,但是不应该是我脑子一热没想法我跟风做一个。既然要做独立开发,就不能用战术上的勤奋掩盖战略上的懒惰。最关键的产品定位你跳过不考虑就是捡芝麻丢西瓜。问题的核心不是做不做三件套,是你有没有想好产品的差异化是什么,能不能通过差异化获得产品的定价权,而不是做另外一个xx。


良言难劝该死鬼,以上的言论是我糊涂了。我觉得做笔记、做记账光明是前途的,做出来就很厉害了。世界上虽然有很多记账了,但是说不定就缺你这个记账。做你想做的,just do it!


作者:独立开花卓富贵
来源:juejin.cn/post/7268896098827403301

收起阅读 »

一个上午,我明白了,为什么总说人挣不到认知以外的钱

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中 01 接下来,以我昨天上午的一段经历,分析下为什么我挣不到认知以外的钱 在上班的路上,我在微信的订阅号推荐里面看到了这张图,当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过...
继续阅读 »

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中


01


接下来,以我昨天上午的一段经历,分析下为什么我挣不到认知以外的钱


在上班的路上,我在微信的订阅号推荐里面看到了这张图,当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。




(读者可以先停5s 思考下,假设是你看到这张图,会有什么想法)



02


当我坐上工位后,我看到我参加的社群里也有有发了上图,并附上了一段文字:


“养老金类型的公众号容易出爆文。


小白玩转职场这个号,篇篇10w+,而且这并不是一个做了很久的老号,而是今年5月才注册不久的号。 之前这个号刚做的时候是往职场方向发展,所以取名叫小白玩转职场,但是发了一阵后数据不是很好于是就换风格做了养老金的内容。


换到养老金赛道后就几乎篇篇10w+。 这些内容一般从官方网站找就好,选一些内容再加上自己想法稍微改下,或者直接通过Chatgpt辅助,写好标题就行”。


同时,文字下面有社群圈友留下评论说:“这是个好方向啊,虽然公众号文章已经同质化很严重了,但可以往视频号、带货等方向发展”。



读者可以先停5s 思考下,假设是你看到这段文字,会有什么想法。如果你不知道公众号赚钱的模式,那你大概率看不出这段话中的赚钱信息的



我想了想,对噢,确实可以挣到钱,于是将这则信息发到了程序员副业交流的微信群里。



然后,就有群友在交流:“他这是转载还是什么,不可能自己天天写吧”,“这种怎么冷启动呢,不会全靠搜索吧,“做他这种类型的公众号挺多吧,怎么做到每篇10w的”



有没有发现,这3个问题都是关注的怎么做的问题?怎么写的,怎么启动的,怎么每篇10w。


这就是我们大部分人的认知习惯,看到一个信息或别人赚钱的点子后,我们大部分人习惯去思考别人是如何做到的,是不是真的,自己可不可以做到。


可一般来说,我们当下的认知是有限的,大概率是想不出完整的答案的的,想不出来以后,然后就会觉得这个事情有点假,很难或者不适合。从而就错过这条信息了。



我当时觉得就觉得可能大部分群友会错过这则信息了,于是,在群里发了下面这段话


“分享一个点子后


首先去看下点子背后的商业变现机会,如带货/流量主/涨粉/等一系列


而后,才去考虑执行的问题,执行的话


1、首先肯定要对公众号流量主的项目流程进行熟悉


2、对标模仿,可以去把这个公众号的内容全看一看,看看别人为什么起号


3、做出差异化内容。”


发完后还有点小激动(嗯,我又秀了波自己的认知)。可到中午饭点,我发现我还是的认知太低了。


03


在中午吃饭时,我看到亦仁(生财有术的老大)发了这段内容



我被这段话震撼到了,我发现我现在的思考习惯,还是只停留在最表面的看山是山的地步。


我仅仅看到了这张图流量大,没有去思考它为什么这么大流量?



因为他通过精心制作的文章,为老年用户介绍了养老金的方方面面,所以,才会有流量


简单说,因为他满足了用户的需求



同样,我也没有思考还有没有其他产品可以满足用户的这个需求,我仅仅是停留在了视频号和公众号这两个产品砂锅。



只要满足用户需求,就不只有一个产品形态,对于养老金这个信息,我们可以做直播,做课程,做工具,做咨询,做1对1私聊的。这么看,就能有无数的可能



同时,我想到了要做差异化,但没有想到要通过关键字挖掘,去挖掘长尾词。



而亦仁,则直接就挖掘了百万关键字,并无偿分享了。



这才知道,什么叫做看山不是山了。


之前知道了要从“需求 流量 营销 变现”的角度去看待信息,也知道“需求为王”的概念。


可我看到这则信息时,还是没有考虑到需求这一层,更没有形成完整的闭环思路。


因此,以后要在看到这些信息时,去刻意练习“需求 流量 营销 变现”这个武器库,去关注他用什么产品,解决了用户什么需求,从哪里获取到的流量的,怎么做营销的,怎么做变现的。


04


于是,我就把这些思考过程也同样分享到了群里。


接着,下午我就看到有群友在自己的公众号发了篇和养老金相关的文章,虽然文章看上去很粗糙,但至少是起步了。


同时,我也建了个项目交流群,方便感兴趣的小伙伴交流进步(一群人走的更远)


不过我觉得在起步之前,也至少得花一两天时间去调研下,去评估这个需求有哪些地方自己可以切入进去,值不值得切入,能切入的话,怎么切入。


对了,可能你会关心我写了这么多,自己有没有做养老金相关的?


我暂时还没有,因为我目前关心的领域还在出海工具和个人IP上。

作者:刘卡卡
来源:juejin.cn/post/7268590610189418533

收起阅读 »

228欢乐马事件,希望大家都能平安健

iOS
我这个人体质很奇怪,总能遇到一些奇怪的事。比如六年前半夜打车回家,差点被出租车司机拉到深山老林。比如两年前去福州出差,差点永远回不了家。比如十点从实验室下班,被人半路拦住。比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为夷...
继续阅读 »

我这个人体质很奇怪,总能遇到一些奇怪的事。

  • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。
  • 比如两年前去福州出差,差点永远回不了家。
  • 比如十点从实验室下班,被人半路拦住。
  • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)

不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




事情起因是这样的:


去年朋友B突然告诉我:发现了你的闲鱼账号。


:我没有闲鱼啊?


他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


当时朋友F 说我太敏感了,他觉得只是巧合。


但我觉得不是巧合,因为LolitaAnn是我自己造的词。




把我的沸点搬到小红书


又是某一天, 朋友H告诉我:你的小红书上热门了。


:?我没有小红书啊?


然后他们给我看,有个人的小红书完全照搬我的沸点。


为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。




现在想了想,ip一样,极有可能是一个人干的。




闲鱼再次被挖出来


今年,有人在掘金群里说我卖周边。


我跑到那个群解释,说我被人冒充了。


群友很热心,都去举报那个人的昵称。昵称被举报下去了。


但是几天之后:




看到有人提醒我,它名字又改回来了。


当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




冒充我的人被揪出来了


2.28的时候, 朋友C突然给我发了一段聊天记录。


是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。




事情到今天终于知道是谁冒充我了


虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


发现是谁冒充我,我第一反应必然是喷一顿。


刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。






牵扯出一堆小号


本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


直到我看到了这两条:




因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


但是我确信他有很多小号,比如:




比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……

  • 你们有没有想过为什么fixbug不许助攻了

  • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?

  • 你们有没有想过为什么掘金活动必得奖励越来越少了?


有这种操作在,普通用户根本没办法玩吧。


所以最后,我就把这个事交给官方了。




处理结果


所幸官方很给力,都处理了,感谢各位官方大大。



本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 





我再叨叨几句

  • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。

  • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。

  • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。

  • 又不是没生活,专注点自己的东西,别老偷别人沸点。

  • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。


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

孤独的游戏少年

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



楔子


又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据颜色分为红色和绿色阵营,旗子翻盖为建筑,正面为单位,被子和枕头作为地图障碍,双手不断的移动着双方阵营的象棋,脑海中演练着星级争霸的游戏画面,将两枚不同阵营的旗子进行碰撞后,通过我对两枚旗子主观的判断,一方阵营的旗子直接被销毁,进入回收,一方的单位对建筑物全部破坏取得游戏胜利。因为我的父亲酷爱玩这款游戏,年少的我也被其玩法、画面所深深吸引,不过最主要的还是父亲获胜或者玩累后,能幸运的奖励我玩上几把。星际争霸成了我第一个启蒙游戏,那时候怎么样都获胜不了,直到发现了show me the money。因为Blizzard这个英文单词一直在游戏的启动界面一闪一闪,那时候还以为这款游戏的名字叫Blizzard,最后才发现,其实Blizzard是魔鬼的意思。


纸笔乐趣


小学一二年级的时候家里管得严,电视也不给我看,电脑直接上锁,作为家里的独生子女,没有同龄人的陪伴,闲暇时间要不就看《格林童话》、《安徒生童话》、《伊索寓言》,要不就打开这副象棋在那里自娱自乐。



起源



在某一天的音乐课上,老师喉咙不舒服,在教室播放猫和老鼠给我们观看,正当我看的津津有味,前面的同学小张突然转过头来,问我“要不要跟我玩个游戏”。小孩子一般都比较贪玩,我直接拒绝了他,“我想看猫和老鼠”。小张满脸失落转了回去,因为我是个特别怕伤害别人情绪的人,所以不忍心又用铅笔的橡皮端戳了戳他,问他“什么游戏,好玩不”。他顿时也来了精神,滔滔不绝的介绍起了这款游戏。


“游戏的规则非常的简单~~在一张纸上中间画一条分割线,双方有各自的基地,每人基地都有5点血量,双方通过猜拳,获胜的一方可以在自己阵营画一个火柴人,火柴人可以攻击对面的火柴人和基地,基地被破坏则胜利。管你听没听懂,试一把就完了!”



我呆呆的看着他,“这么无聊,赢了又怎样?”。他沉默了一会,好像我说的的确有点道理,突然想到了一个好办法,“谁获得胜利扇对方一巴掌”。我顿时来了兴趣。随着游戏逐渐深入,我们的猜拳速度和行动力也越来越快,最后发现扇巴掌还是太狠,改为扇对方的手背,这节课以双方手背通红结束了。


游戏改良


这个《火柴人对战》小游戏在班里火了一会儿,但很快就又不火了,我玩着玩着也发现没啥意思,总是觉得缺少点什么,但毕竟我也只是个没有吃APTX4869的小学生,想不出什么好点子。时间一晃,受到九年义务教育的政策,我也成功成为了一名初中生。在一节音乐课上,老师想让我们放松放松,给我们班看猫和老鼠,隔壁同桌小王撕了一张笔记本的纸,问我有没有玩过《火柴人对战》游戏,只能说,熟悉的配方,熟悉的味道。


当天晚上回到家,闲来无事,我在想这个《火柴人游戏》是不是可以更有优化,这种形式的游戏是不是可以让玩家更有乐趣。有同学可能会问,你那个年代没东西玩的吗?既然你诚心诚意发问,那我就大发慈悲的告诉你。玩的东西的确很多,但是能光明正大摆在课桌上玩的基本没有,一般有一个比较新鲜的好玩的东西,都会有一群人围了过来,这时候老师会默默站在窗户旁的阴暗角落,见一个收一个。


坐在家里的椅子上,我整理了一下思绪,突然产生了灵感,将《魔兽争霸》《游戏王》这两个游戏产生化学反应,游戏拥有着资源,单位,建筑,单位还有攻击力,生命值,效果,攻击次数。每个玩家每回合通过摇骰子的方式获得随机能源点,能源能够解锁建筑,建筑关联着高级建筑和单位,通过单位进行攻击,直至对方玩家生命值为0,那么如何在白纸上面显示呢?我想到比较好的解决方案,单位的画像虽然名字叫骷髅,但是在纸上面用代号A表示,建筑骷髅之地用代号1表示。我花了几天时间,弄了两个阵营,不死族和冰结界。立刻就拿去跟同桌试玩了一下,虽然游戏很丰富,但是有一个严重的弊端就是玩起来还挺耗费时间的,而且要人工计算单位之间的扣血量,玩家的剩余生命,在纸片上去完成这些操作,拿个橡皮擦来擦去,突然觉得有点蠢,有点尴尬,突然明白,一张白纸的承受能力是有限的。之后,我再也没有把游戏拿出来玩过,但我没有将他遗忘,而是深深埋藏在我的心里。


筑梦


直到大学期间《炉石传说》横空出世,直到《游戏王》上架网易,直到我的项目组完成1.0后迎来空窗期一个月,我再也蚌埠住了,之前一直都对微信小游戏很有兴趣,每天闲着也是闲着,所以我有了做一个微信小游戏的想法。而且,就做一款在十几年前,就已经被我设计好的游戏。


但是我不从来不是一个好学的人,领悟能力也很低,之前一直在看cocos和白鹭引擎学习文档,也很难学习下去,当然也因为工作期间没有这么多精力去学习,所以我什么框架也不会,不会框架,那就用原生。我初步的想法是,抛弃所有花里胡哨的动效,把基础的东西做出来,再作延伸。第一次做游戏,我也十分迷茫,最好的做法肯定是打飞机————研究这个微信项目如何用js原生,做出一个小游戏。



虽然微信小游戏刚出来的时候看过代码,但是也只是一扫而过,而这次带着目标进行细细品味,果然感觉不一样。微信小游戏打飞机这个项目是js原生使用纯gL的模式编写的,主要就是在canvas这个画布上面作展示和用户行为。

  // 触摸事件处理逻辑
touchEventHandler(e) {
e.preventDefault()

const x = e.touches[0].clientX
const y = e.touches[0].clientY

const area = this.gameinfo.btnArea

if (x >= area.startX
&& x <= area.endX
&& y >= area.startY
&& y <= area.endY) this.restart()
}

点击事件我的理解就是用户点击到屏幕的坐标为(x, y),如果想要一个按钮上面做处理逻辑,那么点击的范围就要落在这个按钮的范围内。当我知道如何在canvas上面做点击行为时,我感觉我已经成功了一半,接下来就是编写基础js代码。


首先这个游戏确定的元素分别为,场景,用户,单位,建筑,资源(后面改用能源替代),我先将每个元素封装好一个类,一边慢慢的回忆着之前游戏是如何设计的,一边编程,身心完全沉浸进去,已经很久很久没有试过如此专注的去编写代码。用了大概三天的时间,我把基本类该有的逻辑写完了,大概长这个样子



上面为敌方的单位区域,下方为我方的单位区域,单位用ABCDEFG表示,右侧1/1/1 则是 攻击力/生命值/攻击次数,通过点击最下方的icon弹出创建建筑,然后创建单位,每次的用户操作,都是一个点击。


一开始我设想的游戏名为想象博弈,因为每个单位每个建筑都只有名称,单位长什么样子的就需要玩家自己去脑补了,我只给你一个英文字母,你可以想象成奥特曼,也可以想象成哥斯拉,只要不是妈妈生的就行。



湿了


游戏虽然基本逻辑都写好了,放到整个微信小游戏界别人一定会认为是依托答辩,但我还是觉得这是我的掌上明珠,虽然游戏没有自己的界面,但是它有自己的玩法。好像上天也认可我的努力,但是觉得这个游戏还能更上一层楼,在某个摸鱼的moment,我打开了微信准备和各位朋友畅谈人生理想,发现有位同学发了一幅图,上面有四个格子,是赛博朋克风格的一位篮球运动员。他说这个AI软件生成的图片很逼真,只要把你想要的图片告诉给这个AI软件,就能发你一幅你所描绘的图片。我打开了图片看了看,说实话,质感相当不错,在一系列追问下,我得知这个绘图AI软件名称叫做midjourney



midjourney



我迫不及待的登录上去,询问朋友如何使用后,我用我蹩脚的英格力士迫不及待的试了试,让midjourney帮我画一个能源的icon,不试不要紧,一试便湿了,眼睛留下了感动地泪水,就像一个阴暗的房间打开了一扇窗,一束光猛地照射了进来。




对比我之前在iconfont下载的免费图标,midjourney提供这些图片简直就是我的救世主,我很快便将一开始的免费次数用完,然后氪了一个30美刀的会员,虽然有点肉痛,但是为了儿时的梦想,这点痛算什么


虽然我查找了一下攻略,别人说可以使用gpt和midjourney配合起来,我也试了一下,效果一般,可能姿势没有对,继续用我的有道翻译将重点词汇翻译后丢给midjourney。midjourney不仅可以四选一,还可以对图片不断优化,还有比例选择,各种参数,但是我也不需要用到那么多额外的功能,总之一个字,就是棒。


但当时的我突然意识到,这个AI如此厉害,那么会不会对现在行业某些打工人造成一定影响呢,结果最近已经出了篇报道,某公司因为AI绘图工具辞退了众多插画师,事实不一定真实,但是也不是空穴来风,结合众多外界名人齐心协力抵制gpt5.0的研发,在担心数据安全之余,是否也在担心着AI对人类未来生活的各种冲击。焦虑时时刻刻都有,但解决焦虑的办法,我有一个妙招,仍然还是奖励自己


门槛


当我把整个小游戏焕然一新后,便兴冲冲的跑去微信开放平台上传我的伟大的杰作。但微信突然泼了我一盆冷水,上传微信小游戏前的流程有点出乎意外,要写游戏背景、介绍角色、NPC、介绍模块等等,还要上传不同的图片。我的小游戏一共就三个界面,有六个大板块要填写,每个板块还要两张不同的图片,我当时人就麻了。我只能创建一个单位截一次图,确保每张图片不一样。做完这道工序,还要写一份自审自查报告。


就算做完了这些前戏,我感觉我的小游戏还是难登大雅之堂,突然,我又想到了这个东西其实是不是也能运行在web端呢,随后我便立刻付诸行动,创建一个带有canvas的html,之前微信小游戏是通过weapp-adapter这个文件把canvas暴露到全局,所以在web端使用canvas的时候,只需要使用document.getElementById('canvas')暴露到全局即可。然后通过http-server对应用进行启动,这个小游戏便以web端的形式运行到浏览器上了,终于也能理解之前为啥微信小游戏火起来的时候,很多企业都用h5游戏稍微改下代码进行搬运,原来两者之间是有异曲同工之妙之处的。


关于游戏





上面两张便是两个种族对应的生产链,龙族是我第一个创建的,因为我自幼对龙产生好感和兴趣,何况我是龙的传人/doge。魔法学院则是稍微致敬一下《游戏王》中黑魔导卡组吧。


其实开发难度最难的莫过于是AI,也就是人机,如何让人机在有限的资源做出合理的选择,是一大难题,也是我后续要慢慢优化的,一开始我是让人机按照创建一个建筑,然后创建一个单位这种形式去做运营展开,但后来我想到一个好的点子,我应该可以根据每个种族的特点,走一条该特点的独有运营,于是人机龙族便有了龙蛋破坏龙两种流派,强度提升了一个档次。


其实是否能上架到微信小游戏已经不重要了,重要的是这个过程带给我的乐趣,一步一步看着这个游戏被创建出来的成就感,就算这个行业受到什么冲击,我需要被迫转行,我也不曾后悔,毕竟是web前端让我跨越了十几年的时光,找到了儿时埋下的种子,浇水,给予阳光,让它在我的心中成长为一棵充实的参天大树


h5地址:hslastudio.com/game/


github地址: github.com/FEA-Dven/wa…


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

关于晋升的一点思考

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
继续阅读 »

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


准备工作


我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


明确了以下几点:

  • 晋升是在做什么
  • 评委在看什么
  • 候选人要准备什么
  • 评判的标准是什么
  • 常见的坑有哪些

首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



写PPT


然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。

do {
确认思路框架;
填充内容细节;
模拟答辩;
获取意见并判断是否还需要修改;
} while(你觉得还没定稿);

我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


迭代,迭代,还是迭代


在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


相关阅读 what is OODA loop

wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


btw,这套理论对日常工作生活中的大部分事情都适用。


一个信息论的最新研究成果


我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


错。


这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


How?


公式:喜欢 = 熟悉 + 意外


从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


这个配比是15.87% ——《科学美国人》


也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


能够否定自己,是一种能力


我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


Punchline


Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


练习演讲


经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


Talk is cheap. Show me the code.


我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:

  • 形象(肢体语言、着装等)
  • 声音(语速、语调、音量等)
  • 文字(逻辑、关键点等)


演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


牢记表达的初衷


我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


讲内容太多


述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


过度粉饰或浮夸


为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


注意衔接


作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


热情


在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


第一印象


这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


准备问题


当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


问题准备我包含了这几个模块:

  • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等
  • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等
  • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等
  • 团队:项目管理经验,团队管理经验
  • 个人:个人特色,个人规划,自己的反思等等

其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


在回答问题上,也有一些要点需要注意:


听清楚再回答


问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


问题背后的问题


评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


逻辑自洽、简洁明了


一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


其他


心态调整


我的心态经历过过山车般的起伏,可以看到



在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


关于评委


我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


一些教训

  • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。

  • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。

  • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。


附件


前期调研



参考文章


知乎


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

Widget开发流程

iOS
本文所用到的 Demo 可以在这里下载: github.com/zhenyunzhan… 一、创建Widget Extension 1、创建Widget Target 点击 Project —> 添加新的Target —> 搜索Widget Ext...
继续阅读 »

本文所用到的 Demo 可以在这里下载: github.com/zhenyunzhan…


一、创建Widget Extension


1、创建Widget Target


点击 Project —> 添加新的Target —> 搜索Widget Extension —> 点击Widget Extension —> 点击 Next




2、添加配置信息


Include Configuration Intent 是创建 intentdefinition 文件用的,可以让用户动态配置组件,可以先不勾选,后期可以手动创建




3、添加Widget


创建好之后就可以运行程序,程序运行完成之后,长按主屏幕进入编辑状态,点击主屏幕右上方添加按钮,找到程序,就可以添加Widget,简单体验下了


二、大致了解 Widget 文件


查看创建完 Widget Extension 后默认生成的 xxxx Widget.swift 文件,Xcode 为我们生成了 Widget 所需的模版代码,如下这个文件




widget的入口 @main标识的部分




view EntryView 是我们实际展示的UI




数据 Entry 是用来携带widget展示需要的数据




Provider Timeline的提供对象,包含TimelineEntry & ReloadPolicy,用来后续刷新 Widget 内容




三、开发


以Demo为例,做一个展示古诗内容的Widget,间隔一段时间后自动更新widget内容,并且可以自动选择古诗内容来跟新Widget,例子如下:


展示古诗内容 -> 长按后可编辑小组件 -> 进入选择界面 -> 选择并更新




四、静态配置 StaticConfiguration


创建完 Widget Extension Target之后,系统会给我们创建好一个Widget的开发模板


1、TimelineEntry


自己创建的模型作为参数,模型 (itemModel) 用 swift 或者 OC创建均可




2、界面样式


界面有三种尺寸的类型,每种尺寸还可以准备不同的布局,另外界面填充的数据就来源于 TimelineEntry




3、Timeline时间线


实现 TimelineProvider 协议 getTimeline 方法,主要是构建 Entry 和 reloadPolicy,用这两个参数初始化 Timeline ,之后再调用completion回调,回调会走到 @main ,去更新 Widget 内容。


demo中是每次刷新 Timeline ,创建一个 Entry, 则更新一次主屏幕的 Widget 内容, 刷新间隔为 60 分钟,注意:

  • atEnd 所有的entry执行完之后,再次调用 getTimeline 构造数据

  • after(date) 不管这次构造的entry 是否执行完,等系统时间到达date之后,就会在调用getTimeline

  • never 最后一个 entry 展示完毕之后 Widget 就会一直保持那个 entry 的显示内容




开发完成后,可以运行代码,试一下效果,此时的更新时间有点长,可以改为 5 秒后再试。


五、动态配置 IntentConfiguration


程序运行到这里,有的会想,怎么实现编辑小组件功能,动态配置 widget 的显示内容呢?




1、创建 intentdefinition 文件


command + N 组合键创建新 File —> 搜索 intent




选择xxx.intentdefinition文件 —>点击下方 + ,选择intent创建 —> 对intent命名






这个 intent 文件包含了你所有的(intents),通过这个文件编译到你的app中,系统将能够读取你的 intents ,一旦你定义了一个intent文件,Xcode也会为你生成一个intent类别


2、可以添加到 intent 中的参数类型


参数类型有多种,下方为一些示例
参数类型分别为:String、Boolean、Integer时的展示




你也可以用自己定义的类型去设置,参数也支持多个值




3、如何为小组件添加丰富的配置


a、确定配置对象


以这个demo为例,小组件只能显示一首古诗,但是app中有很多首古诗,这就可以创建多个 古诗 组件,然后通过动态配置,每个小组件想要显示不同的古诗。这样的例子还有很多,比如某个人有多张银行卡,每个组件显示不同银行卡余额




b、配置intent文件


category选项设置为View,然后勾选下图中的选项,现在我们可以只关注小组件选项,将快捷指令的勾选也取消,如下图




c、intent添加参数


使用参数列表中的 + 按钮,添加一个参数




Type类型可以选择自定义的type




参数添加完后,系统会在ClickBtnIntent类中生成相应的属性




随后ClickBtnIntent 的实例将在运行时传递到 小组件扩展中,让你的小组件知道用户配置了什么,以及要显示什么




d、代码中的更改


StaticConfiguration 切换为 IntentConfiguration,相应的provider也改为IntentTimelineProvider,provider就不上截图了,可以去demo中的ClickBtn.swift文件查看




现在运行APP,然后长按古诗小组件,选择编辑小组件,会弹出带有Btn Type的参数,点击Btn Type一栏弹出带有搜索的列表页面。 效果如下:




显示的Btn Type就是下图中框选Display Name,自己可以随便起名字,中英文均可




目前,带有搜索的列表页面是一个空白页面,如果想要使其有数据,则要都选Dynamic Options复选框,为其添加动态数据




e、如何为列表添加动态数据?


勾选了Dynamic Options复选框,系统会自动生成一个ClickBtnIntentHandling协议,可以点开ClickIntent类去查看,现在有了intent文件,有了新的可遵守协议,就需要有一个Extension去遵守协议,实现协议里边的方法,为搜索列表提供数据



  • 点击Project —> 新建target —> 搜索intent —> 选择 Intents Extentsion







  • 贴上类的方法,以及方法对应的效果图




f、注意点


实现IntentHandler时,Xcode会报找不到ClickBtnIntentHandling这个协议的错误,

  • 引入头文件 Intents
  • 需要将下图所标的地方做下修改



六、APP创建多个Widget


这个比较简单,按照demo中的例子处理一下就可以,如下图:




目前测试,最多可以同时创建五个不同的Widget


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

LangChain 是 LLM 交响乐的指挥家

本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。 深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。 LangChain 是...
继续阅读 »

本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。


深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。


LangChain 是一个框架,它一直是我作为开发者旅途中的规则改变者。 LangChain 是一个独特的工具,它利用大语言模型(LLMs)的力量为各种使用案例构建应用程序。Harrison Chase 的这个创意于 2022 年 10 月作为开源项目首次亮相。从那时起,它就成为 GitHub 宇宙中一颗闪亮的明星,拥有高达 42,000 颗星,并有超过 800 名开发者的贡献。


LangChain 就像一位大师,指挥着 OpenAI 和 HuggingFace Hub 等 LLM 模型以及 Google、Wikipedia、Notion 和 Wolfram 等外部资源的管弦乐队。它提供了一组抽象(链和代理)和工具(提示模板、内存、文档加载器、输出解析器),充当文本输入和输出之间的桥梁。这些模型和组件链接到管道中,这让开发人员能够轻而易举地快速构建健壮的应用程序原型。本质上,LangChain 是 LLM 交响乐的指挥家。


LangChain 的真正优势在于它的七个关键模块:

  1. 模型:这些是构成应用程序主干的封闭或开源 LLM
  2. 提示:这些是接受用户输入和输出解析器的模板,这些解析器格式化 LLM 模型的输出。
  3. 索引:该模块准备和构建数据,以便 LLM 模型可以有效地与它们交互。
  4. 记忆:这为链或代理提供了短期和长期记忆的能力,使它们能够记住以前与用户的交互。
  5. :这是一种在单个管道(或“链”)中组合多个组件或其他链的方法。
  6. 代理人:根据输入决定使用可用工具/数据采取的行动方案。
  7. 回调:这些是在 LLM 运行期间的特定点触发以执行的函数。

GitHub:python.langchain.com/


什么是提示模板?


在语言模型的世界中,提示是一段文本,指示模型生成特定类型的响应。顾名思义,提示模板是生成此类提示的可重复方法。它本质上是一个文本字符串,可以接收来自最终用户的一组参数并相应地生成提示。


提示模板可以包含语言模型的说明、一组用于指导模型响应的少量示例以及模型的问题。下面是一个简单的例子:

from langchain import PromptTemplate

template = """
I want you to act as a naming consultant for new companies.
What is a good name for a company that makes {product}?
"""

prompt = PromptTemplate(
input_variables=["product"],
template=template,
)

prompt.format(product="colorful socks")

在此示例中,提示模板要求语言模型为生产特定产品的公司建议名称。product 是一个变量,可以替换为任何产品名称。


创建提示模板


在 LangChain 中创建提示模板非常简单。可以使用该类创建简单的硬编码提示 PromptTemplate。这些模板可以采用任意数量的输入变量,并且可以格式化以生成提示。以下是如何创建一个没有输入变量、一个输入变量和多个输入变量的提示模板:

from langchain import PromptTemplate

# No Input Variable 无输入变量
no_input_prompt = PromptTemplate(input_variables=[], template="Tell me a joke.")
print(no_input_prompt.format())

# One Input Variable 一个输入变量
one_input_prompt = PromptTemplate(input_variables=["adjective"], template="Tell me a {adjective} joke.")
print(one_input_prompt.format(adjective="funny"))

# Multiple Input Variables 多个输入变量
multiple_input_prompt = PromptTemplate(
input_variables=["adjective", "content"],
template="Tell me a {adjective} joke about {content}."
)
print(multiple_input_prompt.format(adjective="funny", content="chickens"))

总结


总之,LangChain 中的提示模板是为语言模型生成动态提示的强大工具。它们提供了对提示的灵活性和控制,能够有效地指导模型的响应。无论是为特定任务创建语言模型还是探索语言模型的功能,提示模板都可以改变游戏规则。


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

AI孙燕姿翻唱爆火,多亏这个开源项目!广西老表带头打造,上手指南已出

明敏 发自 凹非寺 量子位 | 公众号 QbitAI AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的? 关键在于一个开源项目。 最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。 而如果在各大教程...
继续阅读 »
明敏 发自 凹非寺 量子位 | 公众号 QbitAI

AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的?


关键在于一个开源项目




最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。


而如果在各大教程中溜达一圈后就会发现,其中的关键秘诀,还是要靠一个名为so-vits-svc的开源项目。



它提供了一种音色替换的办法,项目在今年3月发布。


贡献成员应该大部分都来自国内,其中贡献量最高的还是一位玩明日方舟的广西老表。




如今,项目已经停止更新了,但是星标数量还在蹭蹭上涨,目前已经到了8.4k。


所以它到底实现了哪些技术能引爆这波趋势?


一起来看。


多亏了一个开源项目


这个项目名叫SoftVC VITS Singing Voice Conversion(歌声转换)。


它提供了一种音色转换算法,采用SoftVC内容编码器提取源音频语音特征,然后将矢量直接输入VITS,中间不转换成文本,从而保留了音高和语调。


此外,还将声码器改为NSF HiFiGAN,可以解决声音中断的问题。


具体分为以下几步:

  • 预训练模型
  • 准备数据集
  • 预处理
  • 训练
  • 推理

其中,预训练模型这步是关键之一,因为项目本身不提供任何音色的音频训练模型,所以如果你想要做一个新的AI歌手出来,需要自己训练模型。


而预训练模型的第一步,是准备干声,也就是无音乐的纯人声。


很多博主使用的工具都是UVR_v5.5.0


推特博主@歸藏介绍说,在处理前最好把声音格式转成WAV格式,因为So-VITS-SVC 4.0只认这个格式,方便后面处理。


想要效果好一些,需要处理两次背景音,每次的设置不同,能最大限度提高干声质量。


得到处理好的音频后,需要进行一些预处理操作。


比如音频太长容易爆显存,需要对音频切片,推荐5-15秒或者再长一点也OK。


然后要重新采样到44100Hz和单声道,并自动将数据集划分为训练集和验证集,生成配置文件。再生成Hubert和f0。


接下来就能开始训练和推理了。


具体的步骤可以移步GitHub项目页查看(指路文末)。


值得一提的是,这个项目在今年3月上线,目前贡献者有25位。从贡献用户的简介来看,很多应该都来自国内。


据说项目刚上线时也有不少漏洞并且需要编程,但是后面几乎每一天都有人在更新和修补,现在的使用门槛已经降低了不少。


目前项目已经停止更新了,但还是有一些开发者创建了新的分支,比如有人做出了支持实时转换的客户端。




项目贡献量最多的一位开发者是Miuzarte,从简介地址判断应该来自广西。




随着想要上手使用的人越来越多,也有不少博主推出了上手难度更低、更详细的食用指南。


歸藏推荐的方法是使用整合包来推理(使用模型)和训练,还有B站的Jack-Cui展示了Windows下的步骤指南(http://www.bilibili.com/read/cv2237…


需要注意的是,模型训练对显卡要求还是比较高的,显存小于6G容易出现各类问题。


Jack-Cui建议使用N卡,他用RTX 2060 S,训练自己的模型大概用了14个小时


训练数据也同样关键,越多高质量音频,就意味着最后效果可以越好。


还是会担心版权问题


值得一提的是,在so-vits-svc的项目主页上,着重强调了版权问题。



警告:请自行解决数据集的授权问题。因使用未经授权的数据集进行培训而产生的任何问题及其一切后果,由您自行承担责任。存储库及其维护者、svc开发团队,与生成结果无关!





这和AI画画爆火时有点相似。


因为AI生成内容的最初数据取材于人类作品,在版权方面的争论不绝于耳。


而且随着AI作品盛行,已经有版权方出手下架平台上的视频了。


据了解,一首AI合成的《Heart on My Sleeve》在油管和Tik Tok上爆火,它合成了Drake和Weekend演唱的版本。


但随后,Drake和Weekend的唱片公司环球音乐将这个视频从平台上下架了,并在声明里向潜在的仿冒者发问,“是要站在艺术家、粉丝和人类创造性表达的一边,还是站在Deepfake、欺诈和拒付艺术家赔偿的一边?”


此外,歌手Drake也在ins上对AI合成翻唱歌曲表达了不满。


而另一边,也有人选择拥抱这项技术。


加拿大歌手Grimes表示,她愿意让别人使用自己的声音合成歌曲,但是要给她一半版权费。


GitHub地址:

github.com/svc-develop…


参考链接:

[1]mp.weixin.qq.com/s/bXD1u6ysY…

[2]http://www.vulture.com/article/ai-…


—  —


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

PAG动效框架源码笔记 (四)渲染框架

iOS
前言 PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等 TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题: 绘...
继续阅读 »

前言


PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等


TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题:


绘制一个Texture纹理对象,一般需要经历哪些过程?


渲染流程


通常情况下,绘制一个Texture纹理对象到目标Layer上,可以抽象为以下几个阶段:


1. 获取上下文: 通过EGL获取Context绘制上下文,提供与渲染设备交互的能力,比如缓冲区交换、Canvas及Paint交互等


2. 定义着色器: 基于OpenGL的着色器语言(GLSL)编写着色器代码,编写自定义顶点着色器和片段着色器代码,编译、链接加载和使用它们


3. 绑定数据源: 基于渲染坐标系几何计算绑定顶点数据,加载并绑定纹理对象给GPU,设置渲染目标、混合模式等


4. 渲染执行: 提交渲染命令给渲染线程,转化为底层图形API调用、并执行实际的渲染操作




关于OpenGL完整的渲染流程,网上有比较多的资料介绍,在此不再赘述,有兴趣的同学可以参考 OpenGL ES Pipeline


框架层级


TGFX框架大致可分为三大块:


1. Drawable上下文: 基于EGL创建OpenGL上下文,提供与渲染设备交互的能力


2. Canvas接口: 定义画布Canvas及画笔Paint,对外提供渲染接口、记录渲染状态以及创建绘制任务等


3. DrawOp执行: 定义并装载着色器函数,绑定数据源,执行实际渲染操作


为了支持多平台,TGFX定义了一套完整的框架基类,实现框架与平台的物理隔离,比如矩阵对象Matrix、坐标Rect等,应用上层负责平台对象与TFGX对象的映射转化

- (void)setMatrix:(CGAffineTransform)value {
pag::Matrix matrix = {};
matrix.setAffine(value.a, value.b, value.c, value.d, value.tx, value.ty);
_pagLayer->setMatrix(matrix);
}

Drawable上下文


PAG通过抽象Drawable对象,封装了绘制所需的上下文,其主要包括以下几个对象


1. Device(设备): 作为硬件设备层,负责与渲染设备交互,比如创建维护EAGLContext等


2. Window(窗口): 拥有一个Surface,负责图形库与绘制目标的绑定,比如将的opengl的renderBuffer绑定到CAEAGLLayer上;


3. Surface(表面): 创建canvas画布提供可绘制区域,对外提供flush绘制接口;当窗口尺寸发生变化时,surface会创建新的canvas


4. Canvas(画布): 作为实际可绘制区域,提供绘制api,进行实际的绘图操作,比如绘制一个image或者shape等



详细代码如下:


1、Device创建Context
std::shared_ptr<GLDevice> GLDevice::Make(void* sharedContext) {
if (eaglShareContext != nil) {
eaglContext = [[EAGLContext alloc] initWithAPI:[eaglShareContext API]
sharegroup:[eaglShareContext sharegroup]];
} else {
// 创建Context
eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (eaglContext == nil) {
eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
}
}
auto device = EAGLDevice::Wrap(eaglContext, false);
return device;
}

std::shared_ptr<EAGLDevice> EAGLDevice::Wrap(EAGLContext* eaglContext, bool isAdopted) {
auto oldEAGLContext = [[EAGLContext currentContext] retain];
if (oldEAGLContext != eaglContext) {
auto result = [EAGLContext setCurrentContext:eaglContext];
if (!result) {
return nullptr;
}
}
auto device = std::shared_ptr<EAGLDevice>(new EAGLDevice(eaglContext),
EAGLDevice::NotifyReferenceReachedZero);
if (oldEAGLContext != eaglContext) {
[EAGLContext setCurrentContext:oldEAGLContext];
}
return device;
}

// 获取Context
bool EAGLDevice::makeCurrent(bool force) {
oldContext = [[EAGLContext currentContext] retain];
if (oldContext == _eaglContext) {
return true;
}
if (![EAGLContext setCurrentContext:_eaglContext]) {
oldContext = nil;
return false;
}
return true;
}

2、Window创建Surface,绑定RenderBuffer
std::shared_ptr<Surface> EAGLWindow::onCreateSurface(Context* context) {
auto gl = GLFunctions::Get(context);
...
gl->genFramebuffers(1, &frameBufferID);
gl->bindFramebuffer(GL_FRAMEBUFFER, frameBufferID);
gl->genRenderbuffers(1, &colorBuffer);
gl->bindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
gl->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);
auto eaglContext = static_cast<EAGLDevice*>(context->device())->eaglContext();
// 绑定到CAEAGLLayer上
[eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
...
GLFrameBufferInfo glInfo = {};
glInfo.id = frameBufferID;
glInfo.format = GL_RGBA8;
BackendRenderTarget renderTarget = {glInfo, static_cast<int>(width), static_cast<int>(height)};
// 创建Surface
return Surface::MakeFrom(context, renderTarget, ImageOrigin::BottomLeft);
}

// 通过renderTarget持有context、frameBufferID及Size
std::shared_ptr<Surface> Surface::MakeFrom(Context* context,
const BackendRenderTarget& renderTarget,
ImageOrigin origin, const SurfaceOptions* options) {
auto rt = RenderTarget::MakeFrom(context, renderTarget, origin);
return MakeFrom(std::move(rt), options);
}

3、Surface创建Canvas及flush绘制
Canvas* Surface::getCanvas() {
// 尺寸变化时会清空并重新创建canvas
if (canvas == nullptr) {
canvas = new Canvas(this);
}
return canvas;
}

bool Surface::flush(BackendSemaphore* signalSemaphore) {
auto semaphore = Semaphore::Wrap(signalSemaphore);
// drawingManager创建tasks,装载绘制pipiline
renderTarget->getContext()->drawingManager()->newTextureResolveRenderTask(this);
auto result = renderTarget->getContext()->drawingManager()->flush(semaphore.get());
return result;
}

4、渲染流程
bool PAGSurface::draw(RenderCache* cache, std::shared_ptr<Graphic> graphic,
BackendSemaphore* signalSemaphore, bool autoClear) {
// 获取context上下文
auto context = lockContext(true);
// 获取surface
auto surface = drawable->getSurface(context);
// 通过canvas画布
auto canvas = surface->getCanvas();
// 执行实际绘制
onDraw(graphic, surface, cache);
// 调用flush
surface->flush();
// glfinish
context->submit();
// 绑定GL_RENDERBUFFER
drawable->present(context);
// 释放context上下文
unlockContext();
return true;
}

Canvas接口


Canvas API主要包括画布操作及对象绘制两大类:


画布操作包括Matrix矩阵变化、Blend融合模式、画布裁切等设置,通过对canvasState画布状态的操作实现绘制上下文的切换


对象绘制包括Path、Shape、Image以及Glyph等对象的绘制,结合Paint画笔实现纹理、文本、图形、蒙版等多种形式的绘制及渲染

class Canvas {
// 画布操作
void setMatrix(const Matrix& matrix);
void setAlpha(float newAlpha);
void setBlendMode(BlendMode blendMode);

// 绘制API
void drawRect(const Rect& rect, const Paint& paint);
void drawPath(const Path& path, const Paint& paint);
void drawShape(std::shared_ptr<Shape> shape, const Paint& paint);
void drawImage(std::shared_ptr<Image> image, const Matrix& matrix, const Paint* paint = nullptr);
void drawGlyphs(const GlyphID glyphIDs[], const Point positions[], size_t glyphCount,
const Font& font, const Paint& paint);
};
// CanvasState记录当前画布的状态,包括Alph、blend模式、变化矩阵等
struct CanvasState {
float alpha = 1.0f;
BlendMode blendMode = BlendMode::SrcOver;
Matrix matrix = Matrix::I();
Path clip = {};
uint32_t clipID = kDefaultClipID;
};

// 通过save及restore实现绘制状态的切换
void Canvas::save() {
auto canvasState = std::make_shared<CanvasState>();
*canvasState = *state;
savedStateList.push_back(canvasState);
}

void Canvas::restore() {
if (savedStateList.empty()) {
return;
}
state = savedStateList.back();
savedStateList.pop_back();
}

DrawOp执行


DrawOp负责实际的绘制逻辑,比如OpenGL着色器函数的创建装配、顶点及纹理数据的创建及绑定等


TGFX抽象了FillRectOp矩形绘制Op,可以覆盖绝大多数场景的绘制需求


当然,其还支持其它类型的绘制Op,比如ClearOp清屏、TriangulatingPathOp三角图形绘制Op等

class DrawOp : public Op {
// DrawOp通过Pipiline实现多个_colors纹理对象及_masks蒙版的绘制
std::vector<std::unique_ptr<FragmentProcessor>> _colors;
std::vector<std::unique_ptr<FragmentProcessor>> _masks;
};

// 矩形实际绘制执行者
class FillRectOp : public DrawOp {
FillRectOp(std::optional<Color> color, const Rect& rect, const Matrix& viewMatrix,
const Matrix& localMatrix);
void onPrepare(Gpu* gpu) override;
void onExecute(OpsRenderPass* opsRenderPass) override;
};

总结


本文结合OpenGL讲解了TGFX渲染引擎的大概框架结构,让各位有了一个初步认知


接下来将结合image纹理绘制介绍TGFX渲染引擎详细的绘制渲染流程,欢迎大家关注点赞!


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

SwiftUI关于菜单 iOS 的长按 & macOS 右键的实现

iOS
长按 按钮或者图片出现菜单是个很平常的操作。 从app的icon 到app 内部的按钮 可以将内部的一些操作整合到这个特点内 SwiftUI 自带的菜单选择 ContextMenu 代码 iOS 效果 macOS 在mac上不是长按了,是右键的菜单操作 文案...
继续阅读 »

长按 按钮或者图片出现菜单是个很平常的操作。


从app的icon 到app 内部的按钮 可以将内部的一些操作整合到这个特点内


SwiftUI 自带的菜单选择 ContextMenu


代码




iOS 效果



macOS


在mac上不是长按了,是右键的菜单操作



文案可能要修改一下,应该叫 右键


这里有一个有趣的点,mac 版本的样式是没有图标。必须加一句

Button(action: { fileData.selectedFilesToOperate = [item] //单个  
fileWindow.isShowMoveFileView = true })
{ Label("移动", systemImage: "folder")
.labelStyle(.titleAndIcon)
}

但是现实的情况往往没有如此的简单,至少产品和老板的需求,都不是那么简单。下面几个我自己遇到的情况
可能不太全面,但是按图索骥应该可以给看遇到相似问题的人一点启发的感觉


问题1 菜单 不能太单调,分别来显示

Section {
Button1
Button2 ....
}

用section 包裹 可以让菜单有明显的分区



问题2 菜单里面放点别的


那再放开一点,,contextMenu 内部 放点别的

      contextMenu {
// picker
// list
// toggle
// image...
}



放入单选记得选什么的 Picker



放入子菜单


这里用到了 Menu 这个标签


这个表情 也是个菜单,点击就有,不用长按。


菜单里面放菜单的效果


Menu {

                            Picker(selection: $sort, label: Text("Sorting options")) {

                                Text("Size").tag(0)

                                Text("Date").tag(1)

                                Text("Location").tag(2)

                            }

                        } label: {

                            Label("Sort", systemImage: "arrow.up.arrow.down")

                        }

这个效果挺有意思,和mac 的右键的子菜单一个效果。



这个放一切UI的效果,确实比较有趣。有兴趣可以尝试放入更丰富的控件。


SwiftUI 的控件我个人感觉的套路

  1. 一切view 都是声明的方式,靠@State 或者@Publish 一些的Modify来控制控件的显示数据
  2. 因为没有了生命周期,对于onAppair 和DisAppair的控制放在了每一个控件上的@ViewBuilder上,这个可以自定义,开始的时候都用自带的 @ViewBuilder
  3. View 都是Struct,class用的不多。
  4. View 里面包View,尽量做到了控件复用。而且是挑明了就是,比如之前的Text里面label,Button里面的Label,NavigationLink里面的View(也可以一切不同类型的View)

个人感觉这些都是在表面SwiftUI 打破以前Swift UIKit或者是OC中的UIKit的思维逻辑。


既: UI廉价 刷新廉价


让程序员 特别是iOS 开发过程中,不同状态的刷新UI ,回调刷新UI的开发复杂度


总结


对于一个控件的开始编写,到不停叠加复杂的情况,还有许多场景还没遇到和想到。目前SwiftUI的源码和网上的资料,还不如OC 如此内核的解析资料丰富。但是未来的iOS开发 一定是SwiftUI的时代,特别是对于个人开发者相比OC 友好程度明显。


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

禁止别人调试自己的前端页面代码

web
🎈 为啥要禁止? 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码 🎈 无限 debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点...
继续阅读 »

🎈 为啥要禁止?



  • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据

  • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码


禁止调试


🎈 无限 debugger



  • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

  • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的

  • 基础代码如下:


/**
* 基础禁止调试代码
*/

(() => {
function ban() {
setInterval(() => {
debugger;
}, 50);
}
try {
ban();
} catch (err) { }
})();

基础禁止调试


🎈 无限 debugger 的对策



  • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大

  • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger

  • 这种方式虽然能去掉碍眼的 debugger,但是无法通过左侧的行号添加 breakpoint


取消禁止对策


🎈 禁止断点的对策



  • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpointfalse 也无用

  • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


(() => {
function ban() {
setInterval(() => { debugger; }, 50);
}
try {
ban();
} catch (err) { }
})();

禁止断点


🎈 忽略执行的代码



  • 通过添加 add script ignore list 需要忽略执行代码行或文件

  • 也可以达到禁止无限 debugger


忽略执行的代码


🎈 忽略执行代码的对策



  • 那如何针对上面操作的恶意用户呢

  • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对

  • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件

  • 当然使用的时候,为了更加的安全,最好使用加密后的脚本


// 加密前
(() => {
function ban() {
setInterval(() => {
Function('debugger')();
}, 50);
}
try {
ban();
} catch (err) { }
})();

// 加密后
eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\b"+d(a)+"\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));

解决对策


🎈 终极增强防调试代码



  • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下

  • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();

  • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容

  • 当然使用的时候,为了更加的安全,最好加密后再使用


(() => {
function block() {
if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
}
setInterval(() => {
(function () {
return false;
}
['constructor']('debugger')
['call']());
}, 50);
}
try {
block();
} catch (err) { }
})();

终极增强防调试

收起阅读 »

记录用前端代替后端生成zip的过程,速度快了 57 倍!!!

业务场景: 产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。 管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)...
继续阅读 »

业务场景:


产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。


管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)至少需要 10s。有什么方法能够优化下。



因为代码不具备可复用性,因此部分代码直接省略,思路为主


原始逻辑


  public async getZip(themeId: string, res: any) {
const theme = await this.model.findById(themeId); // 从数据库

// 这里需要借用一个服务器上的主题模板文件夹 template/,

/*
theme = {
wallpapers: [
{ url: 'https://亚马逊云.com/1.jpg', ... },
...
]
}
*/


// for 循环遍历 theme.wallpapers , 并通过 fetch 请求 url,将其写进 template/static/wallpapers 文件夹中
theme.wallpapers.map((item) => {
const response = await fetch(item.url);
const buffer = new Uint8Array(await response.arrayBuffer());
await fs.writeFile(`template/wallpapers/${fileName}`, buffer);
})

// ... 还有其他一些处理

// 将 template 压缩成 zip 文件,发送给前端
}

思考 ing ...


1 利用图片可以被浏览器缓存


当一次下载主题从请求亚马逊云的图片数据,这步没有问题。 但是当重复下载的时候,之前下载过的图片又会再次下载,操作人员每次都需要等个十几秒,这就不太友好了。这部分时间花费还是挺多的。


可以利用下浏览器能够将图片缓存到 disk cache 中的特点,将这部分的代码逻辑放到前端完成,因为还需要对压缩包中的文件做一些处理,因此需要借助下 jszip 这个库。


看下改后的代码



onDownload () {
// 请求拿到 theme 数据
const theme = api.getTheme()
const template = api.getTemplate() // Blob

const zip = new JSZip()
await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

console.time('handle images')
const wallpaperList = theme.wallpapers
for (const wallpaper of wallpaperList) {
const response = await fetch(wallpaper.url) // 请求图片数据
const buffer = new Uint8Array(await response.arrayBuffer())
const fileName = wallpaper.url.split('/').pop()
zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true }) // 写进压缩包
}
console.timeEnd('handle images') // 统计用时

// 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
...

// 导出 template.zip
zip.generateAsync({ type: 'base64' }).then(
(base64) => {
const link = document.createElement('a')
link.href = 'data:application/zip;base64,' + base64
link.download = 'template.zip'
link.target = '_blank'
link.style.display = 'none'

document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
(err) => {
console.log('打包失败', err)
}
)
}

优化完成


当第一次下载时,handle images 步骤耗时 20 - 21 s,流程和后端差不多。


当第二次下载时,handle images 步骤耗时 0.35s - 0.45 s。会直接读取 disk cache 中的图片数据,50 ms 内就完成了。


速度快了 57 倍有余!!!, 你还能想到其他优化方式吗?继续往后看 🍒


第一次请求各个图片耗时
image.png


第二次请求各个图片耗时
image.png


2 并发请求


我们都知道,浏览器会为每个域名维持 6 个 TCP 链接(再拓展还有域名分片知识),我们是否可以利用这个特点做些什么?


答案是:并发上传


通过上面的代码,可以看到,每个图片请求都是串行的,一个图片请求完了再进行下一个图片请求。我们一次请求 4 个图片,这样就更快了。


首先写一个能够管理并发任务的类


export class TaskQueue {
public queue: {
task: <T>() => Promise<T>
resolve: (value: unknown) => void
reject: (reason?: any) => void
}[]
public runningCount: number // 正在执行的任务数量
public tasksResloved?: (value: unknown) => void
public tasksRejected?: (reason?: any) => void

public constructor(public maxConcurrency: number = 4) { // 最多同时执行 4 个任务
this.queue = [] // 任务队列
this.runningCount = 0
}

// 添加任务
public addTask(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject })
})
}

// 执行
public run() {
return new Promise((resoved, rejected) => {
this.tasksResloved = resoved
this.tasksRejected = rejected
this.nextTask()
})
}

private nextTask() {
if (this.queue.length === 0 && this.runningCount === 0) {
this.tasksResloved?.('done')
return
}

// 如果任务队列中还有任务, 并且没有到最大执行任务数,就继续取出任务执行
while (this.queue.length > 0 && this.runningCount < this.maxConcurrency) {
const { task, resolve, reject } = this.queue.shift()
this.runningCount++
task()
.then((res) => {
this.runningCount--
resolve(res)
this.nextTask()
})
.catch((e) => {
this.runningCount--
reject(e)
this.nextTask()
})
}
}
}


改造代码


onDownload () {
// 请求拿到 theme 数据
const theme = api.getTheme()
const template = api.getTemplate() // Blob

const zip = new JSZip()
await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

console.time('handle images')
const wallpaperList = theme.wallpapers

// 注释之前的逻辑
// for (const wallpaper of wallpaperList) {
// const response = await fetch(wallpaper.url)
// const buffer = new Uint8Array(await response.arrayBuffer())
// const fileName = wallpaper.url.split('/').pop()
// zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
// }

const taskQueue = new TaskQueue() // 新建任务队列,默认同时执行 4 个
for (const wallpaper of wallpaperList) {
taskQueue
.addTask(() => fetch(wallpaper.url)) // 添加任务
.then(async (res) => { // 任务执行完后的回调
const buffer = new Uint8Array(await (res as Response).arrayBuffer())
const fileName = wallpaper.url.split('/').pop()
zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
})
.catch((e) => console.log('壁纸获取失败', e))
}
await taskQueue.run() // 等待所有图片都拿到
console.timeEnd('handle images') // 统计用时

// 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
...

// 导出 template.zip
zip.generateAsync({ type: 'base64' }).then(
(base64) => {
const link = document.createElement('a')
link.href = 'data:application/zip;base64,' + base64
link.download = 'template.zip'
link.target = '_blank'
link.style.display = 'none'

document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
(err) => {
console.log('打包失败', err)
}
)
}

大功告成!


当第一次下载时,handle images 步骤耗时 7 s,速度是之前的 3 倍。


当第二次下载时,handle images 步骤耗时 0.25s,速度是之前的 1.4 - 1.8


3 更多的可能


越来越感觉到计算机网络的重要性, 还有未实现的优化方式:



  1. 域名分片,更多的并发(也有劣势 ,比如 每个域都需要额外的 DNS 查找成本以及建立每个 TCP 连接的开销, TCP 慢启动带宽利用不足)

  2. 升级 HTTP2 这不是靠前端一人能够完成的


如果学到了新知识,麻烦点个
作者:marh
来源:juejin.cn/post/7267418197746270271
👍 和 ⭐

收起阅读 »

公司没钱了,工资发不出来,作为员工怎么办?

公司没钱了,工资发不出来,作为员工怎么办? 现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。 员工遇到这种情况,无非以下几种选择。 认...
继续阅读 »

公司没钱了,工资发不出来,作为员工怎么办?


现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。


员工遇到这种情况,无非以下几种选择。



  1. 认同公司的决策,愿意跟公司共同进退。

  2. 不认同公司的决策,我要离职。

  3. 不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

  4. 不认同公司的决策,我也不主动离职。准备跟公司battle,”你们这么做是不合法滴“


你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我N+1的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


如果公司后面没钱了,欠的工资还拿得到吗?


我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么

作者:石云升
来源:juejin.cn/post/7156242740034928671
股东还需要按照股份比例偿还债务。

收起阅读 »

差点让我崩溃的“全选”功能

web
今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图: 开始,我是这样写代码的: f...
继续阅读 »

今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图:


1679229898519.png
开始,我是这样写代码的:


    for (let i = 0; i < aCheckbox.length; i++) {
aCheckbox[i].checked = this.checked;
}
});

for (let i = 0; i < aCheckbox.length; i++) {
aCheckbox[i].addEventListener("click", function () {
for (let index = 0; index < aCheckbox.length; index++) {
if (aCheckbox[index].checked) {
oAllchecked.checked = aCheckbox[index].checked;
} else {
oAllchecked.checked = !aCheckbox[index].checked;
}
}
});
}

点击全选这个功能不难,主要问题出现在如何保证另外两个复选框在其中一个没有选中的情况下,全选的这个复选框没有选中。苦思良久,最后通过查找资料看到了如今的代码:


    aCheckbox[i].addEventListener("click", function () {
let flag = true;
for (let index = 0; index < aCheckbox.length; index++) {
console.log(aCheckbox[index].checked);
if (!aCheckbox[index].checked) {
flag = false;
break;
}
}
oAllchecked.checked = flag;
});
}

功能完美就解决,第一个代码问题的原因是‘aCheckbox[index].checked’这个判断不能解决两个复选框什么时候一个选中一个没选中的问题。这个问题不解决也就不能让全选复选框及时更新正确的选中状态了。


而下面这个代码通过设置一个中间值flag,及时记录每个复选框按钮的选中状态,能准确的赋值给全选功能的复

作者:一个对前端不离不弃的中年菜鸟
来源:juejin.cn/post/7212942861518864421
选框按钮。于是这个需求就解决了~

收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。 代码如下: // utils.js const XLSX = require('xlsx') // 将一个sheet转成最终...
继续阅读 »

  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。

  2. 代码如下:


// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/

export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

<el-button
@click="clickExportBtn"
>
<i class="el-icon-download"></i>下载数据
</el-button>
<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
</el-icon>
<p>loading...</p>
</div>


clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}


  1. 解决方案探究:




  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:



    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。

    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。

    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。

    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。




  • 查阅资料后找到了如下几种方案:





      1. 使用 setTimeout 使 openDownloadDialog 异步执行


      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }




      1. 对 openDownloadDialog 内部进行优化



      • 避免大循环或递归逻辑

      • 将计算工作分批进行

      • 使用 Web Worker 隔离耗时任务


        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。





            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。





            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。





            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。





            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。





            1. 代码应该是自包含的,不依赖外部变量或状态。





            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。





            1. 避免修改或依赖全局作用域,比如定义全局变量等。






        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。


          // 创建 Worker
          const worker = new Worker('downloadWorker.js');

          // 点击下载时向 Worker 发送消息
          function clickDownload() {

          showLoading();

          worker.postMessage({
          url: fileURL,
          filename: 'report.xlsx'
          });

          worker.onmessage = function(e) {
          // 收到下载完成的消息
          hideLoading();
          }

          }

          // 显示 loading
          function showLoading() {
          loading.style.display = 'block';
          }

          // 隐藏 loading
          function hideLoading() {
          loading.style.display = 'none';
          }

          // downloadWorker.js

          onmessage = function(e) {
          const url = e.data.url;
          const filename = e.data.filename;

          // 创建并点击链接触发下载
          const a = document.createElement('a');
          a.href = url;
          a.download = filename;
          a.click();

          postMessage('下载完成');
          }

          <div id="loading" style="display:none;">
          Downloading...
          </div>

          <button onclick="clickDownload()">
          Download
          </button>

          <script src="downloadWorker.js"></script>








      1. 使用 requestIdleCallback 进行调度


      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }



    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:


      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。


      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。


      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。


      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。


      所以需要权衡执行速度和避免阻塞之间的关系:




      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。




      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。




      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。


      此外,可以结合两者试试:


      clickExport() {

      this.loadingSummaryData = true;

      setTimeout(() => {

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
      });

      this.loadingSummaryData = false;

      }, 200);

      }

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。





      1. 分析线程模型,找到具体的阻塞点



      • 使用 Performance 工具分析线程

      • debugger 及 console.log 打印关键函数的执行时间

      • 检查是否有非主线程的任务阻塞了主线程





      1. 调整 vue 的批量更新策略


      new Vue({
      config: {
      // 修改批量更新的队列长度
      batchUpdateDuration: 100
      }
      })

      作者:李卫泽
      来源:juejin.cn/post/7268050036474609683
      i>


收起阅读 »

const声明的变量还能修改?原理都在这了

web
前言 const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。 本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。 耐心看完,...
继续阅读 »

前言


const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。


本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。


耐心看完,你一定有所收获。


giphy.gif


正文


const关键字用于声明一个变量,该变量的值在其生命周期中不会被重新赋值。


现象


1. 基本数据类型


对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。


const num = 42;
// num = 43; // 这会抛出错误

2. 对象


对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。


const girlfriend = {
name: "小宝贝"
};

girlfriend.name = "亲爱的"; // 这是允许的,因为你只是修改了对象的一个属性

// girlfriend = { name: "亲爱的" }; // 这会抛出错误,因为你试图改变obj的引用

假如你有个女朋友,也许并没有,但我们可以假设,她的名字或者你平时叫她的昵称是"小宝贝"。


有一天,你心血来潮,想换个方式叫她,于是叫她"亲爱的"。这完全没问题,因为你只是给她换了个昵称,她本人并没有变。


但是,如果有一天你看到另一个女生,你却说:“哎,这不是亲爱的吗?”这就出大问题了!因为你把一个完全不同的人当成了你的女朋友。


这就像你试图改变girlfriend的引用,把它指向了一个新的对象。


JavaScript不允许这样做,因为你之前已经明确地告诉它,girlfriend就是那个你叫"小宝贝"的女朋友,你不能突然把另一个人说成她。


154eb98c10eaf8356b5da0e44b9e9fe6.gif


简单来说,你可以随时给你的女朋友起个新昵称,但你不能随便把别的女生当成你的女朋友。


3. 数组


对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。


const arr = [1, 2, 3];

arr[0] = 4; // 这是允许的,因为你只是修改了数组的一个元素
arr.push(5); // 这也是允许的,因为你只是向数组添加了一个元素

// arr = [6, 7, 8]; // 这会抛出一个错误,因为你试图改变arr的引用

假设arr是你的超市购物袋,里面有三个苹果,分别标记为1、2和3。


你检查了第一个苹果,觉得它不够新鲜,所以你把它替换成了一个新的苹果,标记为4。这就像你修改数组的一个元素。这完全可以,因为你只是替换了袋子里的一个苹果。


后来,你决定再放一个苹果进去,标记为5。这也没问题,因为你只是向袋子里添加了一个苹果。


苹果再变,袋子仍然是原来的袋子。


但是,当你试图拿个新的装着6、7、8的购物袋来代替你原来的袋子时,就不对了。你不能拿了一袋子苹果,又扔在那不管,反而又去拿了一袋新的苹果。


你礼貌吗?


f2e0de05371993107839d315b5639a30.jpg


你可以随时替换袋里的苹果或者放更多的苹果进去,但你不能拿了一袋不要了又拿一袋。


原理


在JavaScript中,const并不是让变量的值变得不可变,而是让变量指向的内存地址不可变。换句话说,使用const声明的变量不能被重新赋值,但是其所指向的内存中的数据是可以被修改的。


使用const后,实际上是确保该变量的引用地址不变,而不是其内容。


结合上面两个案例,女朋友和购物袋就好比是内存地址,女朋友的外号可以改,但女朋友是换不了的,同理袋里装的东西可以换,但袋子仍然是那个袋子。


当使用const声明一个变量并赋值为一个对象或数组,这个变量实际上存储的是这个对象或数组在内存中的地址,形如0x00ABCDEF(这只是一个示例地址,实际地址会有所不同),而不是它的内容。这就是为什么我们说变量“引用”了这个对象或数组。


实际应用


这种看似矛盾的特性实际上在开发中经常用到。


例如,在开发过程中,可能希望保持一个对象的引用不变,同时允许修改对象的属性。这可以通过使用const来实现。


考虑以下示例:


假设你正在开发一个应用,该应用允许用户自定义一些配置设置。当用户首次登录时,你可能会为他们提供一组默认的配置。但随着时间的推移,用户可能会更改某些配置。


// 默认配置
const userSettings = {
theme: "light", // 主题颜色
notifications: true, // 是否开启通知
language: "en" // 默认语言
};

// 在某个时间点,用户决定更改主题颜色和语言
function updateUserSettings(newTheme, newLanguage) {
userSettings.theme = newTheme;
userSettings.language = newLanguage;
}

// 用户调用函数,将主题更改为"dark",语言更改为"zh"
updateUserSettings("dark", "zh");

console.log(userSettings); // 输出:{ theme: "dark", notifications: true, language: "zh" }

在这个例子中,我们首先定义了一个userSettings对象,它包含了用户的默认配置。尽管我们使用const来声明这个对象,但我们仍然可以随后更改其属性来反映用户的新配置。


这种模式在实际开发中很有用,因为它允许我们确保userSettings始终指向同一个对象(即我们不会意外地将其指向另一个对象),同时还能够灵活地更新该对象的内容以反映用户的选择。


为什么不用let


以上所以案例中,使用let都是可行,但它的语义和用途相对不同,主要从这几个方面进行考虑:



  1. 不变性:使用const声明的变量意味着你不打算重新为该变量赋值。这为其他开发人员提供了一个明确的信号,即该变量的引用不会改变。在上述例子中,我们不打算将userSettings重新赋值为另一个对象,我们只是修改其属性。因此,使用const可以更好地传达这一意图。

  2. 错误预防:使用const可以防止意外地重新赋值给变量。如果你试图为const变量重新赋值,JavaScript会抛出错误。这可以帮助捕获潜在的错误,特别是在大型项目或团队合作中。

  3. 代码清晰度:对于那些只读取和修改对象属性而不重新赋值的场景,使用const可以提高代码的清晰度,可以提醒看到这段代码的人:“这个变量的引用是不变的,但其内容可能会变。”


一般我们默认使用const,除非确定需要重新赋值,这时再考虑使用let。这种方法旨在鼓励不变性,并使代码更加可预测和易于维护。


避免修改


如果我们想要避免修改const声明的变量,当然也是可以的。


例如,我们可以使用浅拷贝来创建一个具有相同内容的新对象或数组,从而避免直接修改原始对象或数组。这可以通过以下方式实现:


const originalArray = [1, 2, 3];
const newArray = [...originalArray]; // 创建一个原始数组的浅拷贝
newArray.push(4); // 不会影响原始数组
console.log(originalArray); // 输出: [1, 2, 3]
console.log(newArray); // 输出: [1, 2, 3, 4]

总结


const声明的变量之所以看似可以被修改,是因为const限制的是变量指向的内存地址的改变,而不是内存中数据的改变。这种特性在实际开发中有其应用场景,允许我们保持引用不变,同时修改数据内容。


然而,如果我们确实需要避免修改数据内容,可以采取适当的措施,如浅拷贝。


9a9f1473841eca9a3e5d7e1408145a4b.gif

收起阅读 »

好烦啊,为什么点个链接还让我确认一下?

web
万丈苍穹水更深,无限乾坤尽眼中 背景 最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图: 很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或...
继续阅读 »

万丈苍穹水更深,无限乾坤尽眼中



背景


最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图:


A6C73047-4041-4584-9F97-BA04C896D73E.png


很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或CRSF攻击,所以请不要像标题一样抱怨,多点一下也花不了2S时间。


原理


那么这个是如何实现的呢,原理其实很简单。


a标签的onclick事件可以被拦截,当返回false时不会默认跳转。


那么具体如何实现呢,拿掘金来举例:


        function SetSafeA(whiteDomList: string[], safeLink = 'https://link.juejin.cn/?target=') {
          const aArr = document.getElementsByTagName('a')
          Array.from(aArr).forEach(item=>{
            item.onclick = ()  => {
              let target = item.getAttribute('href')!
              if(/^\//.test(target)) {
                // 相对本站链接
                return true
              }
             const isSafe = undefined !==  whiteDomList.find(item=>{
                 return target.indexOf(item) !== -1
              })
              if(!isSafe) {
                window.open(`${safeLink}${target}`, '_blank')
              } else {
return true
}
              return false
            }
          })
        }

可以随便找一个网页在控制台执行一下,都能跳到掘金的中转页,中转页的代码就不写了^_^


实践


刚好最近遇到一个使用场景,公司APP产品里面都有各自用户协议,其中SDK协议我们都是直接跳转链接的,结果在部分渠道如小天才,步步高等对用户信息非常敏感的平台上,要求所有的链接必须要跳转到平台默认的安全浏览器上,不能在APP内打开。那么协议有很多如何快速处理呢。由于项目用到了vue,这里就想到使用指令,通过批量添加指令来达到快速替换,比如'<a' =>'<a v-link="x"',代码如下:


Vue.directive('outlink', {
  bind: (el, binding) => {
    el.outlink = () => {
      if (GetEnv() === 'app') {
        const from = isNaN(+binding.value) ? 1 : +binding.value
        const url = el.getAttribute('href')
        if (url && url !== '' && url != 'javascript:;') {
          window.location.href = `${GetSchemeByFrom(from)}://outside_webview?url=${url}`
        }
        return false
      }
    }
    el.onclick = el.outlink
  },
  unbind: (el) => {
    el.onclick = null
    delete el.outlink
  }
})

这里我们传入了from值来区分APP平台,然后调用APP提供的相应scheme跳转到客户端的默认浏览器,如下:


DE2DFA5F-ED19-4e74-97B6-2D19246D5D84.png


结语


链接拦截可以做好事,也可以做一些hack,希望使用的人保持一颗爱好和平的心;当然遇到让你确认安全的

作者:CodePlayer
来源:juejin.cn/post/7161712791089315877
链接时,也请你保持一颗感谢的心。

收起阅读 »

兄弟,不要试图在业务代码中炫技。

你好呀,我是歪歪。 最近项目迭代非常密集,导致组里面的同事都在同一个微服务里面进行不同需求的迭代开发。 由于我们的代码提交规则规定,提交代码必须有一个 review 环节,所以有时候我会去仔细看同事提交的代码,但是有一说一,绝大部分情况下我没有仔细的去看,只是...
继续阅读 »

你好呀,我是歪歪。


最近项目迭代非常密集,导致组里面的同事都在同一个微服务里面进行不同需求的迭代开发。


由于我们的代码提交规则规定,提交代码必须有一个 review 环节,所以有时候我会去仔细看同事提交的代码,但是有一说一,绝大部分情况下我没有仔细的去看,只是草草的瞟上几眼,就点击了通过。


其实我之前也会非常仔细的去看的,但是不得不说这个 review 的过程真的会占据比较多的时间,在需求不密集的时候做起来没有问题。


但是一旦任务在手上堆起来了,就很难去仔细 review 别人的代码了,分身乏术。


去年有一段时间就是忙的飞起,多线程并发需求迭代,别人提交代码之后,我就是无脑点通过。


我并没有核对最终落地的代码和我最初的设计方案是否匹配,而且由于代码不是我开发的,我甚至没有看过,等出了问题,排查问题的时候我再去找代码,就发现根本不知道写在哪里的。


方案设计和代码落地之间的断层,这样带来的一个后果就是我后期完全失去了对服务的掌握。


每天都担心,生怕出线上问题。但是每天也不知道哪个地方会出现问题,就很恼火。


对每一次我点进 review 通过的代码负责,这是我写进年度计划的一句话。


所以,今年为了避免这个现象的再次出现,在同事对一个完整的功能点提交之后,即使再忙,我自己会花时间仔细去 review 一次对应的代码,然后拿着我看完之后记录的问题再去找对应的同事给我答疑,确保我们至少在业务逻辑的理解上是一致的。


通过这个方式,我又重新掌握了主动权。


在这个过程中还暴露出一个问题,各个开发同事的编码风格各异,经常可以闻到一些代码的“坏味道”。


比如,我见过一个新增操作,所有的逻辑都在一个 controller 里面,没有所谓的 biz 层、service 层、dao 层,一把梭直接把 mapper 注入到了 controller 里面,在一个方法里面从数据校验到数据库交互全部包圆了。


功能能用吗?


能用。


但是我们常常提到的一个词是“技术含量”。


这样代码是有“技术含量”的代码吗?


我觉得可以说是毫无技术含量了,就是偷懒了,觉得怎么方便就怎么来了。


那如果我要基于对于这一段代码继续开发新功能,我能做什么呢?


我无能为力,原来的代码实在不想去动。


我只能保证在这堆“屎山”上,我新写出来的代码是干净的、清晰的,不继续往里面扔垃圾。


我读过一本书叫做《代码整洁之道》,里面有一个规则叫做“童子军军规”。


军规中有一句话是这样的:让营地比你来时更干净。


类比到代码上其实就是一件很小的事情,比如只是改好一个变量名、拆分一个有点过长的函数、消除一点点重复代码,清理一个嵌套 if 语句...


这是让项目代码随着时间流逝而越变越好的最简单的做法,持续改进也是专业性的内在组成部分。


我觉得我对于这一点“规则”落实的还是挺好的,看到一些不是我写的,但是我觉得可以有更好的写法时,而且改动起来非常简单,不影响核心功能的时候,我会主动去改一下。


我能保证的是,这段代码在经过我之后,我没有让它更加混乱。


把一段混乱的代码,拆分的清晰起来,再后来的人愿意按照你的结构继续往下写,或者继续改进。


你说这是在写“有技术含量”的代码吗?


我觉得不是。


但是,我觉得这应该是在追求写“有技术含量”的代码之前,必须要具备的一个能力。而且是比写出“有技术含量”的代码更加重要的一个基础能力。


先不说代码优雅的事儿了,至少得让代码整体看起来不混乱。


一个人维护一个项目,想要把代码搞优雅是一件很简单的事情,但是如果是好几个人一起维护就有点不好做了。


只有大家相互磨合,最后慢慢的形成好的、较为统一风格。


所以我最近也是持续在找一些关于代码风格、代码规范、代码重构这方面的好的资料在组分享,总是能慢慢有所改变的。


比如这周,我就找到了“京东云开发者”的一篇文章:



《让代码优雅起来:记一次代码微重构实践 | 京东云技术团队》

juejin.cn/post/725714…



在这篇文章里面,作者给到了一个完整的关于代码重构的示例。


把一个功能代码,从这样冗长臃肿的代码:



最终拆分为了三个类,每个类各司其职。


这个类只是负责组装对象:



金额计算拆分到了枚举类里面去:




这才是符合面向对象编程的思想。


这部分代码具体是干啥的,以及重构前后的代码是怎么样的,如果你感兴趣可以自己打开链接看一下。


我这边主要还是赞同作者的一个观点:不要觉得重构前的代码每次修改也就肉眼可见的几个地方,没必要在这上面花费时间。


其实我觉得还是很有必要的,大家写代码的时候都想要追求技术含量,追求优雅性,这就是一个体现的地方,为什么不改呢?


但是我还得补充一句,结合个人截至目前有限的职业生涯和工作经验来说,我有一点小小的体会:



写业务代码,代码可读性的优先级甚至比代码写的优雅、写的有技术含量更高,且高的多。不要试图在业务代码中炫技。



我前面分享的“记一次代码微重构实践”文章的最后也列举了两个引用的地方,我也放在这里,共勉之。


软件工程中的“破窗效应”:



破窗效应指的是在软件开发过程中,如果存在低质量的代码或设计,如果不及时修复,就会导致其他开发人员也采用同样的低质量方案。这会逐渐升级到更严重的问题,导致软件系统变得难以维护、扩展和改进。因此,在软件开发中,及时解决问题和保持代码质量非常重要,以避免破窗效应对于整个项目造成的负面影响。



同时看看 Martin Fowler 在《重构:改善既有代码的设计》一书中对重构的部分解释:



重构的每个步骤都很简单,甚至显得有些过于简单:你只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质

作者:why技术
来源:juejin.cn/post/7268183236740317225
量。


收起阅读 »

都什么年代了,还在用传统方式写代码?

前言 还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。 本文将分享 AI 时代的编程新实践,教你...
继续阅读 »

前言


还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。


本文将分享 AI 时代的编程新实践,教你如何从一个 "Ctrl + C"、 "Ctrl + V" 工程师,变成一个 "Tab + Enter" 工程师🤣。


开发流程


软件的一般研发流程为:

  1. 需求分析
  2. 程序设计
  3. 代码编写
  4. 软件测试
  5. 部署上线

我们在这里主要关心步骤2~4,因为与 AI 结合得比较紧密。虽然需求分析也可以借助 AI,但不是本文的重点,故不做讨论。


程序设计


经过需求分析、逻辑梳理后,在编写实际代码前,需要进行程序设计。


此环节的产物是设计文档,是什么类型的设计文档不重要,重要的是伪代码的输出。


虽然《Code Complete》早就推荐过伪代码的实践,但对此人们容易有一个误区:认为写伪代码花的时间,已经够把实际代码写好了。但 AIGC 时代,此问题可以轻松破解:AI 写代码的速度肯定比人快,因此,只要能找到方法能让 AI 生成符合需求的代码,就值得花时间去研究。而伪代码,就是让 AI 快速生成符合期望的实际代码的最好方式。


为什么这么说呢?因为想要让 AIGC 符合期望,恰当的 Prompt 必不可少。但如何写好这个 Prompt,需要提供多少上下文,才能让 AI 更好地理解我们的意图,这是需要技巧、需要调试的。而经过精心设计的伪代码,本身已经提供了足够的上下文,且意图足够聚焦,减轻了对 Prompt 的要求,又提高了 AIGC 的成功率。


我们来看一下伪代码示例:

plainText = JSON.stringify(data)
digest = hash(plainText) // 防篡改
secret = Symmetric.getKey()
cipherText = encryptText(data, secret) // 防内容泄密


pair = Asymmetric.getPair()
cipherSecret = encryptKey(secret, pair.getPrivateKey()) // 防密钥泄露

return {
digest,
cipherText,
cipherSecret,
pair.getPublicKey(),
}

上述代码主要表达的内容是:

  1. 对初始数据进行加密
  2. 返回加密后的相关内容
  3. 涉及到了一系列的算法

对于伪代码的编写,有如以建议:

  1. 不要纠结类型,不要局限于某一种编程语言的语法,不用试图写能编译通过的代码——这只会限制了你的表达
  2. 命名一定要准确,符合领域术语,这一点很关键。这一点可以通过查找资料、看书、问 AI 来实现。千万不要只是生硬地汉译英,不专业的表达会妨碍 AI 的理解

代码编写


代码编写分为以下几步:

  1. 把伪代码转换成目标语言可执行的代码
  2. 根据项目情况,对生成的代码进行改造
  3. 利用 AI 编码辅助工具编写剩余的代码

生成真实代码


让 AI 生成代码的 prompt 很简单,示例如下(根据目标语言进行修改):

please transform the following pseudo code into practical java code ```
your code```please transform the following pseudo code into practical java code ```
your code```

使用 chatGPT 结果截图:



完整代码如下:

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.*;

public class EncryptionExample {

public static void main(String[] args) throws Exception {
// Prepare the license data
LicenseData licenseData = new LicenseData();
licenseData.setLicenseId("123");
licenseData.setLicenseType("Standard");

// Convert the license data to JSON string
ObjectMapper objectMapper = new ObjectMapper();
String plainText = objectMapper.writeValueAsString(licenseData);

// Hash the plain text
String digest = hash(plainText);

// Generate a symmetric key
SecretKey secretKey = generateSymmetricKey();

// Encrypt the license data with the symmetric key
byte[] cipherText = encryptText(plainText, secretKey);

// Generate an asymmetric key pair
KeyPair keyPair = generateAsymmetricKeyPair();

// Encrypt the symmetric key with the private key
byte[] cipherSecret = encryptKey(secretKey.getEncoded(), keyPair.getPrivate());

// Print the results
System.out.println("Plain Text: " + plainText);
System.out.println("Digest: " + digest);
System.out.println("Cipher Text: " + new String(cipherText));
System.out.println("Cipher Secret: " + new String(cipherSecret));
}

public static String hash(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
return bytesToHex(hash);
}

public static SecretKey generateSymmetricKey() throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
return keyGenerator.generateKey();
}

public static byte[] encryptText(String data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data.getBytes());
}

public static byte[] encryptKey(byte[] secret, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(secret);
}

public static KeyPair generateAsymmetricKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}

public static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("x", b));
}
return result.toString();
}

public static class LicenseData {
private String licenseId;
private String licenseType;

public String getLicenseId() {
return licenseId;
}

public void setLicenseId(String licenseId) {
this.licenseId = licenseId;
}

public String getLicenseType() {
return licenseType;
}

public void setLicenseType(String licenseType) {
this.licenseType = licenseType;
}
}
}

把上述代码 copy 下来,放到工程中,根据需要改造即可。


这里特别要说下,强烈推荐使用原版 AI,而不是寻找平替,同样的 prompt,下图是某一平替输出的结果:


只生成了函数声明,没有生成函数实现。二者对比,未免相形见绌。



辅助编程工具


改造的过程中,少不了 AI pair programming tools。对此,我推荐使用 Amazon 的 CodeWhisperer,原因很简单,跟 GitHub Copilot 相比,它是免费的😃。


CodeWhisperer 的安装可以看文末的安装教程,我们先来看一下它是怎么辅助我们编码的。


第一种方式是最简单的,那就是什么都不管,等待智能提示即可,就好像 IDEA 原来的提示一样,只不过更智能。


下图示例中,要把原来的中文异常提示,修改成英文,而我只输入了两个字符 IM, 就得到了智能提示,补全了完整的英文字符串!



可以注意到,上图的智能建议一共有 5 条,相应的快捷键为:

  1. 方向键 ->,查看下一条提示
  2. 方向键 <-,查看上一条提示
  3. Tab,采用该提示
  4. Esc,拒绝提示

我们再来看第二种 CodeWhisperer 的使用方式,编写注释,获得编码建议。



最后一种就是编写一个空函数,让 CodeWhisperer 根据函数名去猜测函数的实现,这种情况需要足够的上下文,才能得到令人满意的结果。


软件测试


AI 生成的内容,并不是完全可信任的,因此,单元测试的重要性变得尤为突出。


对上述代码编写测试代码后,实际上并不能一次通过,因为前面 AI 生成的代码参数有误。


此时需要一边执行单测,一边根据结果与 AI 进行交互:



经过修改,最终测试用例通过👏!



总结


本文通过案例,展示了 AI 如何结合软件研发的流程,提升我们的编程效率的。


其中,个人认为最重要的是编写伪代码与进行单元测试。有趣的是,这两样实践在 AIGC 时代之前,就已经被认为是最佳实践。这给我们启示:某些方法论、实践经得起时间的考验,技术更新迭代,它们历久弥新。


另外,AI 是否能进一步渗透我们的工作流,还有待探索。此文作引抛砖引玉之用,期待大家的后续分享。


附:CodeWhisperer 安装


下载 2023 年的 IDEA,打开 Plugins Marketplace,找到 AWS Toolkit



安装完成、重启 IDEA 后,点击左下角,按下图所示操作:




如果第一次使用,就点击 1 处进行注册,如果已经有账号了,就点击 2 处使用自己的账号登录。



注册、登录、授权成功后,出现如图所示页面,即可使用 CodeWhisperer。



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

前端重新部署如何通知用户刷新网页?

1.目标场景 有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。 2.思考解决方案 如何去解决这个问题 思考中... 如果后端可以配合我们的话我们可以使用webSoc...
继续阅读 »



1.目标场景


有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。


2.思考解决方案


如何去解决这个问题
思考中...


如果后端可以配合我们的话我们可以使用webSocket 跟后端进行实时通讯,前端部署完之后,后端给个通知,前端检测到Message进行提示,还可以在优化一下使用EvnentSource 这个跟socket很像只不过他只能后端往前端推送消息,前端无法给后端发送,我们也不需要给后端发送。


以上方案需要后端配合,奈何公司后端都在忙,需要纯前端实现。


重新进行思考...


根据和小伙伴的讨论得出了一个方案,在项目根目录给个json 文件,写入一个固定的key值然后打包的时候变一下,然后代码中轮询去判断看有没有变化,有就提示。




果然是康老师经典不知道。




但是写完之后发现太麻烦了,需要手动配置json文件,还需要打包的时候修改,有没有更简单的方案,
进行第二轮讨论。


第二轮讨论的方案是根据打完包之后生成的script src 的hash值去判断,每次打包都会生成唯一的hash值,只要轮询去判断不一样了,那一定是重新部署了.




3.代码实现

interface Options {
timer?: number
}

export class Updater {
oldScript: string[] //存储第一次值也就是script 的hash 信息
newScript: string[] //获取新的值 也就是新的script 的hash信息
dispatch: Record<string, Function[]> //小型发布订阅通知用户更新了
constructor(options: Options) {
this.oldScript = [];
this.newScript = []
this.dispatch = {}
this.init() //初始化
this.timing(options?.timer)//轮询
}


async init() {
const html: string = await this.getHtml()
this.oldScript = this.parserScript(html)
}

async getHtml() {
const html = await fetch('/').then(res => res.text());//读取index html
return html
}

parserScript(html: string) {
const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/ig) //script正则
return html.match(reg) as string[] //匹配script标签
}

//发布订阅通知
on(key: 'no-update' | 'update', fn: Function) {
(this.dispatch[key] || (this.dispatch[key] = [])).push(fn)
return this;
}

compare(oldArr: string[], newArr: string[]) {
const base = oldArr.length
const arr = Array.from(new Set(oldArr.concat(newArr)))
//如果新旧length 一样无更新
if (arr.length === base) {
this.dispatch['no-update'].forEach(fn => {
fn()
})

} else {
//否则通知更新
this.dispatch['update'].forEach(fn => {
fn()
})
}
}

timing(time = 10000) {
//轮询
setInterval(async () => {
const newHtml = await this.getHtml()
this.newScript = this.parserScript(newHtml)
this.compare(this.oldScript, this.newScript)
}, time)
}

}

代码用法

//实例化该类
const up = new Updater({
timer:2000
})
//未更新通知
up.on('no-update',()=>{
console.log('未更新')
})
//更新通知
up.on('update',()=>{
console.log('更新了')
})

4.测试


执行 npm run build 打个包


安装http-server


使用http-server 开个服务




重新打个包npm run build




这样子就可以检测出来有没有重新发布就可以通知用户更新了。


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

超出范围自动滚动、支持彩色流动特效的UILabel封装

iOS
JKRShimmeringLabel 特征 支持炫彩字支持炫彩流动字支持超出显示范围自动滚动文本支持RTL下的对称显示和滚动支持Frame布局支持Xib和StoryBoard内使用支持AutoLayout布局 使用 源码连接 和原生UILabel一样用,只需...
继续阅读 »

JKRShimmeringLabel


特征





  1. 支持炫彩字

  2. 支持炫彩流动字

  3. 支持超出显示范围自动滚动文本

  4. 支持RTL下的对称显示和滚动

  5. 支持Frame布局

  6. 支持Xib和StoryBoard内使用

  7. 支持AutoLayout布局


使用


源码连接


和原生UILabel一样用,只需要设置mask属性(一张彩色的图片遮罩)即可。


原有项目的UILabel替换


因为JKRAutoScrollLabel和JKRShimmeringLabel本身就是继承UILabel,可以直接把原有项目的UILabel类,替换成JKRAutoScrollLabel或JKRShimmeringLabel即可。


JKRAutoScrollLabel


超出范围自动滚动的Lable,需要设置attributedText,不能设置text。要同时支持流动彩字,设置mask即可。不需要彩色可以不设置mask,只有自动滚动的特性。


// Frame布局,字体支持炫彩闪动,同时超出显示范围自动滚动

NSMutableAttributedString *textForFrameAttr = [[NSMutableAttributedString alloc] initWithString:@"我是滚动测试文本Frame布局,看看我的效果" attributes:@{NSForegroundColorAttributeName: UIColorHex(FFFFFF), NSFontAttributeName: [UIFont systemFontOfSize:19 weight:UIFontWeightBold]}];

self.autoScrollLabelForFrame = [[JKRAutoScrollLabel alloc] initWithFrame:CGRectMake(isRTL ? kScreenWidth - 10 - 300 : 10, CGRectGetMaxY(title0.frame) + 10, 300, 24)];

// 滚动文本需要设置 attributedText 才能生效

self.autoScrollLabelForFrame.attributedText = textForFrameAttr;

// 设置文字颜色的mask图片遮罩,如果不需要字体炫彩,不设置即可

self.autoScrollLabelForFrame.mask = [self maskImage];

[self.view addSubview:self.autoScrollLabelForFrame];


JKRShimmeringLabel


支持流动彩字,设置mask即可,如果还需要超出范围自动滚动,需要使用JKRAutoScrollLabel。


// Frame布局,字体支持炫彩闪动

self.shimmerLabelForFrame = [[JKRShimmeringLabel alloc] initWithFrame:CGRectMake(isRTL ? kScreenWidth - 10 - 300 : 10, CGRectGetMaxY(title1.frame) + 10, 300, 24)];

self.shimmerLabelForFrame.text = @"我是彩色不滚动文本Frame布局,看看我的效果";

self.shimmerLabelForFrame.font = [UIFont systemFontOfSize:19];

// 设置文字颜色的mask图片遮罩,如果不需要字体炫彩,不设置即可

self.shimmerLabelForFrame.mask = [self maskImage];

[self.view addSubview:self.shimmerLabelForFrame];


Xib使用


控件支持xib和autolayout的场景,和UILabel一样设置约束即可,自动滚动和彩色动画,会自动支持。只需要正常配置约束,然后设置mask彩色遮罩即可。


同时,因为JKRShimmeringLabel和JKRAutoScrollLabel本身就是继承UILabel的,所以UILabel在Xib中的文本自动填充宽度、约束优先级等等特性,也都可以正常使用。


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

iOS 线程安全和锁机制

iOS
一、线程安全场景 多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。 比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。 1. 购票案例 用代码示例如下...
继续阅读 »

一、线程安全场景


多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。


比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。


1. 购票案例




用代码示例如下:

@IBAction func ticketSale() {

        tickets = 30

        let queue = DispatchQueue.global()

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

    }

    //卖票

    func sellTicket() {

        var oldTicket = tickets

        sleep(UInt32(0.2))

         oldTicket -= 1

         tickets = oldTicket

        print("还剩\(tickets)张票 ---- \(Thread.current)")

    }

同时有三条线程进行卖票,对余票进行减操作,三条线程每条卖10次,原定票数30,应该剩余票数为0,下面看下打印结果:




可以看到打印票数不为0


2. 存钱取钱案例


先用个图说明




上图可以看出,存钱和取钱之后的余额理论上应该是500,但由于存钱取钱同时访问并修改了余额,导致数据错乱,最终余额可能变成了400,下面用代码做一下验证说明:

//存钱取钱

    @IBAction func remainTest() {

        remain = 500

        let queue = DispatchQueue.global()

        queue.async {

            for _ in 0..<5 {

                self.saveMoney()

            }

        }

        queue.async {

            for _ in 0..<5 {

                self.drawMoney()

            }

        }

    }

    //存钱

    func saveMoney() {

       var oldRemain = remain

        sleep(2)

        oldRemain += 100

        remain = oldRemain

        print("存款100元,账户余额还剩\(remain)元 ----\(Thread.current)")

    }

    

    //取钱

    func drawMoney() {

        var oldRemain = remain

         sleep(2)

         oldRemain -= 50

         remain = oldRemain

        print("取款50元,账户余额还剩\(remain)元 ---- \(Thread.current)")

    }

上述代码存款5次100,取款5次50,最终的余额应该是 500 + 5 * 100 - 5 * 50 = 750




如图所示,可以看到在存款取款之间已经出现错乱了



上述两个案例之所以出现数据错乱问题,就是因为有多个线程同时操作了同一资源,导致数据不安全而出现的。



那么遇到这个问题该怎么解决呢?自然而然的,我们想到了对资源进行加锁处理,以此来保证线程安全,在同一时间,只允许一条线程访问资源。


加锁的方式大概有以下几种:

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock

1. OSSpinLock 自旋锁




OSSpinLock 是自旋锁,在系统框架 libkern/OSAtomic




如图,系统提供了以下几个API

  • 定义lock let osspinlock = OSSpinLock()
  • OSSpinLockTry

官方给定的解释如下

Locks a spinlock if it would not block
return false, if the lock was already held by another thread,
return true, if it took the lock successfully.


尝试加锁,加锁成功则继续,加锁失败则直接返回,不会阻塞线程

  • OSSpinLockLock
Although the lock operation spins, it employs various strategies to back

off if the lock is held.

加锁成功则继续,加锁失败,则会阻塞线程,处于忙等状态

  • OSSpinLockUnlock: 解锁

使用
@IBAction func ticketSale() {

        osspinlock = OSSpinLock()

        tickets = 30

        let queue = DispatchQueue.global()

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

    }

    //卖票

    func sellTicket() {

        OSSpinLockLock(&osspinlock)

        var oldTicket = tickets

        sleep(UInt32(0.2))

         oldTicket -= 1

         tickets = oldTicket

        print("还剩\(tickets)张票 ---- \(Thread.current)")

        OSSpinLockUnlock(&osspinlock)

    }



可以看到,最终的余票数量已经是正确的了,这里要注意的是osspinlock需要做成全局变量或者属性,多个线程要用这同一把锁去加锁和解锁,如果每个线程各自生成锁,则达不到要加锁的目的了


那么自旋锁是怎么样做到加锁保证线程安全的呢?
先来介绍下让线程阻塞的两种方法:

  • 忙等:也就是自旋锁的原理,它本质上就是个while循环,不停地去判断加锁条件,自旋锁没有让线程真正的阻塞,只是将线程处在while循环中,系统CPU还是会不停地分配资源来处理while循环指令。
  • 真正阻塞线程: 这是让线程休眠,类似于Runloop里的match_msg() 实现的效果,它借助系统内核指令,让线程真正停下来处于休眠状态,系统的CPU不再分配资源给线程,也不会再执行任何指令。系统内核用的是symcall指令来让线程进入休眠

它的原理就是,自旋锁在加锁失败时,让线程处于忙等状态,让线程停留在临界区之外,一旦加锁成功,就可以进入临界区对资源进行操作。




通过这个可以看到,苹果在iOS10之后就弃用了OSSpinLock,官方建议用 os_unfair_lock来代替,那么为什么要弃用呢?因为在iOS10之后线程可以设置优先级,在优先级配置下,可以产生优先级反转,使自旋锁卡住,自旋锁本身已经不再安全。


2. os_unfair_lock


os_unfair_lock 是苹果官方推荐的,自iOS10之后用来替代 OSSpinLock 的一种锁

  • os_unfair_lock_trylock: 尝试加锁,加锁成功返回true,继续执行。加锁失败,则返回false,不会阻塞线程。
  • os_unfair_lock_lock: 加锁,加锁失败,阻塞线程继续等待。加锁成功,继续执行。
  • os_unfair_lock_unlock : 解锁

使用:

//卖票

    func sellTicket() {

        os_unfair_lock_lock(&unfairlock)

        var oldTicket = tickets

        sleep(UInt32(0.2))

         oldTicket -= 1

         tickets = oldTicket

        print("还剩\(tickets)张票 ---- \(Thread.current)")

        os_unfair_lock_unlock(&unfairlock)

    }

打印结果和OSSpinLock一样,os_unfair_lock摒弃了OSSpinLock的while循环实现的忙等状态,而是采用了真正让线程休眠,从而避免了优先级反转问题。


3. pthread_mutex


pthread_mutexpthread跨平台的一种解决方案,mutex 为互斥锁,等待锁的线程会处于休眠状态。
互斥锁的初始化比较麻烦,主要为以下方式:

  1. var ticketMutexLock = pthread_mutex_t()
  2. 初始化属性:
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

3. 初始化锁:pthread_mutex_init(&ticketMutexLock, &attr)

关于互斥锁的使用,主要提供了以下方法:

  1. 尝试加锁:pthread_mutex_trylock(&ticketMutexLock)
  2. 加锁:pthread_mutex_lock(&ticketMutexLock)
  3. 解锁:pthread_mutex_unlock(&ticketMutexLock)
  4. 销毁相关资源:pthread_mutexattr_destory(&attr)pthread_mutex_destory(&ticketMutexLock)

使用方式如下:




要注意,在析构函数中要将锁进行销毁释放掉
在初始化属性中,第二个参数有以下几种方式:




PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL,代表普通的互斥锁
PTHREAD_MUTEX_ERRORCHECK 代表检查错误锁
PTHREAD_MUTEX_RECURSIVE 代表递归互斥锁


互斥锁的底层原理实现也是通过阻塞线程,等待锁的线程处于休眠状态,CPU不再给等待的线程分配资源,和上面讲到的os_unfair_lock原理类似,都是通过内核调用symcall方法来休眠线程,通过这个对比也能推测出,os_unfair_lock实际上也可以归属于互斥锁


3.1 递归互斥锁



如图所示,如果是上述场景,方法1里面嵌套方法2,正常调用时,输出应该为:




若要对上述场景保证线程安全,先用普通互斥锁添加锁试下




结果打印如下:




和预想中的不一样,如果懂得锁机制便会明白,图中所示的rsmText2中加锁失败,需要等待rsmText1中的锁释放后才可加锁,所以rsmText2方法开始等待并阻塞线程,程序无法再执行下去,那么rsmText1中锁释放的逻辑就无法执行,就这样造成了死锁,所以只能打印rsmText1中的输出内容。
解决这个问题,只需要给两个方法用两个不同的锁对象进行加锁就可以了,但是如果是针对于同一个方法递归调用,那么就无法通过不同的对象去加锁,这时候应该怎么办呢?递归互斥锁就该用上了。








如上,已经可以正常调用并加锁
那么递归锁是如何避免死锁的呢?简而言之就是允许对同一个对象进行重复加锁,重复解锁,加锁和解锁的次数相等,调用结束时所有的锁都会被解开


3.2 互斥锁条件 pthread_cond_t

互斥锁条件所用到的常见方法如下:

  1. 定义一个锁: var condMutexLock = pthread_mutex_t()
  2. 初始化锁对象:pthread_mutex_init(&condMutexLock)
  3. 定义条件对象:var condMutex = pthread_cond_t()
  4. 初始化条件对象:pthread_cond_init(&condMutex, nil)
  5. 等待条件:pthread_cond_wait(&condMutex, &condMutexLock) 等待过程中会阻塞线程,知道有激活条件的信号发出,才会继续执行
  6. 激活一个等待该条件的线程:pthread_cond_signal(&condMutex)
  7. 激活所有等待该条件的线程pthread_cond_broadcast(&condMutex)
  8. 解锁:pthread_mutex_unlock(&condMutexLock)
  9. 销毁锁对象和销毁条件对象:pthread_mutex_destroy(&condMutexLock) pthread_cond_destroy(&condMutex)

下面设计一个场景:

  • 在一个线程里对dataArr做remove操作,另一个线程里做add操作
  • dataArr为0时,不能进行删除操作
@IBAction func mutexCondTest(_ sender: Any) {

        initMutextCond()

    }

    func initMutextCond() {

        //初始化属性

        var attr = pthread_mutexattr_t()

        pthread_mutexattr_init(&attr)

        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

        //初始化锁

        pthread_mutex_init(&condMutexLock, &attr)

        //释放属性

        pthread_mutexattr_destroy(&attr)

        //初始化cond

        pthread_cond_init(&condMutex, nil)

        _testDataArr()

        

    }

    func _testDataArr() {

        let threadRemove = Thread(target: self, selector: #selector(_remove), object: nil)

        threadRemove.name = "remove 线程"

        threadRemove.start()

        

        sleep(UInt32(1))

        let threadAdd = Thread(target: self, selector: #selector(_add), object: nil)

        threadAdd.name = "add 线程"

        threadAdd.start()

        

    }

    @objc func _add() {

        //加锁

        pthread_mutex_lock(&condMutexLock)

        print("add 加锁成功---->\(Thread.current.name!)开始")

        sleep(UInt32(2))

        dataArr.append("test")

        print("add成功,发送条件信号 ------ 数组元素个数为\(dataArr.count)")

        pthread_cond_signal(&condMutex)

        //解锁

        pthread_mutex_unlock(&condMutexLock)

        print("解锁成功,\(Thread.current.name!)线程结束")

    }

    @objc func _remove() {

        //加锁

        pthread_mutex_lock(&condMutexLock)

        print("remove 加锁成功,\(Thread.current.name!)线程开启")

        if(dataArr.count == 0) {

            print("数组内没有元素,开始等待,数组元素为\(dataArr.count)")

            pthread_cond_wait(&condMutex, &condMutexLock)

            print("接收到条件更新信号,dataArr元素个数为\(dataArr.count),继续向下执行")

        }

        dataArr.removeLast()

        print("remove成功,dataArr数组元素个数为\(dataArr.count)")

        

        //解锁

        pthread_mutex_unlock(&condMutexLock)

        print("remove解锁成功,\(Thread.current.name!)线程结束")

    }

    

    deinit {

//        pthread_mutex_destroy(&ticketMutexLock)

        pthread_mutex_destroy(&condMutexLock)

        pthread_cond_destroy(&condMutex)

    }

输出结果为:




从打印结果来看,如果不满足条件时进行条件等待 pthread_cond_wati,remove 线程是解锁,此时线程是休眠状态,然后等待的add 线程进行加锁成功,处理add的逻辑。


当add 操作完毕时,通过 pthread_cond_signal发出信号,remove线程收到信号后被唤醒,然后remove线程会等待add线程解锁后,再进行加锁处理后续的逻辑.


整个过程中一共用到了三次加锁,三次解锁,这种锁可以处理线程依赖的场景.


4. NSLock, NSRecursiveLock, NSCondition


上文中提到了mutex普通互斥锁 mutex递归互斥锁mutext条件互斥锁,这几种锁都是基于C语言的API,苹果在此基础上做了面向对象的封装,分别对应如下:

  • NSLock 封装了 pthread_mutex_t的 attr类型为 PTHREAD_MUTEX_DEFAULT 或者 PTHREAD_MUTEX_NORMAL 普通锁
  • NSRecursiveLock 封装了 pthread_mutex 的 attr类型为PTHREAD_MUTEX_RECURSIVE递归锁
  • NSCondition 封装了 pthread_mutex_t 和 pthread_cond_t

底层实现和 pthread_mutex_t一样,这里只看下使用方式即可:


4.1 NSLock
//普通锁 
let lock = NSLock()
lock.lock()
lock.unlock()

4.2 NSRecursiveLock
let lock = NSRecursiveLock()
lock.lock()
lock.unlock()

4.3 NSCondition
let condition = NSCondition()
condition.lock()
condition.wait()
condition.signal()//condition.broadcast()
condition.unlock()

4.4 NSConditionLock

这个是NSCondition 的进一步封装,该锁允许我们在锁中设定条件具体条件值,有了这个功能,我们可以更加方便的多条线程的依赖关系和前后执行顺序


下面用一个场景来模拟下顺序控制的功能,有三条线程执行A,B,C三个方法,要求按A,C,B的顺序执行

@IBAction func conditionLockTest(_ sender: Any) {

       let threadA = Thread(target: self, selector: #selector(A), object: nil)

        threadA.name = "ThreadA"

        threadA.start()

       let threadB = Thread(target: self, selector: #selector(B), object: nil)

        threadB.name = "ThreadB"

        threadB.start()

       let threadC = Thread(target: self, selector: #selector(C), object: nil)

        threadC.name = "ThreadC"

        threadC.start()

    }

    @objc func A() {

        conditionLock.lock()

        print("A")

        sleep(UInt32(1))

        conditionLock.unlock(withCondition: 3)

    }

    @objc func B() {

        conditionLock.lock(whenCondition: 2)

        print("B")

        sleep(UInt32(1))

        conditionLock.unlock()

    }

    @objc func C() {

        conditionLock.lock(whenCondition: 3)

        print("C")

        conditionLock.unlock(withCondition: 2)

    }

输出结果为:

A

C

B

5. dispatch_semaphore


信号量 的初始值可以用来控制线程并发访问的最大数量,初始值为1,表示同时允许一条线程访问资源,这样可以达到线程同步的目的

  • 创建信号量:dispatch_semaphore_create(value)

  • 等待:dispatch_semaphore_wait(semaphore, 等待时间) 信号量的值 <= 0,线程就休眠等待,直到信号量 > 0,如果信号量的值 > 0,则就将信号量的值递减1,继续执行下面的程序

  • 信号量值+1: dispatch_semaphore_signal(semaphore)


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

【iOS】高效调试 iOS APP 的 UI

iOS
调试是程序是开发过程中必不可少的环节,每当我们完成一段代码或者发现一些问题都需要对程序进行调试。高效的调试能帮我们节省大量的开发时间。这篇文章我将分享一些提高调试效率的工具和它们的使用方法。 在开发iOS APP的时候我们最频繁进行调试的莫过于UI了。 一、U...
继续阅读 »

调试是程序是开发过程中必不可少的环节,每当我们完成一段代码或者发现一些问题都需要对程序进行调试。高效的调试能帮我们节省大量的开发时间。这篇文章我将分享一些提高调试效率的工具和它们的使用方法。


在开发iOS APP的时候我们最频繁进行调试的莫过于UI了。


一、UI的调试


开发中我们经常需要多次修改UI元素的样式进行微调,查看效果并确定正确的数值。

Xcode

如下图所示,Xcode 提供了完备的UI调试工具。




在左边,我们可以看到完整对视图树,中间有各个视图对3D拆分展示,右边,可以看到当前选中的视图的一些信息。


Xcode在进行UI调试的时候,会暂停APP,视图的信息也只能查看不能方便的修改。在UI调试的时候需要修改代码然后重新编译运行才能看到最终的效果。


在频繁调试UI样式的时候是很耗费时间的(如果电脑性能非常好可能会耗费的时间可能会短一些)所以这不是最佳的选择。

LookIn

在这里向大家介绍一款视图调试工具Lookin,它是由腾讯的QMUI团队开发并开源的一款免费的UI调试工具。


有了它,我们就能进行高效的UI调试。


使用方法也非常简单,具体可以查看官方的集成指导


接下来我将分几点简单的介绍一下这个工具的强大功能。

查看与修改UI

Lookin 可以查看与修改 iOS App 里的 UI 对象,类似于 Xcode 自带的 UI Inspector 工具,不需要重新编译运行。而且借助于“控制台”和“方法监听”功能,Lookin 还可以进行 UI 之外的调试。



独立运行
此外,虽然 Lookin 主体是一款 macOS 程序,它亦可嵌入你的 iOS App 而单独运行在 iPhone 或 iPad 上。



显示变量名
Lookin 会显示变量名,以及 indexPath 等各种提示。



显示手势
添加了手势的 UIView,或添加了 Target-Action 的 UIControl,左侧都会被加上一个小蓝条,点击即可查看信息或调试手势



测距
按住 Option 键,即可测量任意两个 view 之间的距离



导出文件

通过手机或电脑将当前 iOS App 的 UI 结构导出为 Lookin 文件以备之后查看,或直接转发给别人。
当测试发现BUG时可以完美对固定现场,并可以将文件发送给开发者查看当时的视图结构。


二、热重载


💉Injection III


Lookin已经帮我们解决了很多问题,但当我们修改了代码的业务逻辑,或者修改了UI的加载逻辑,或者对代码进行了比较大的改动,此时还是需要重新编译运行才能使新的代码生效。同样会耗费许多时间编译、重新运行、点击屏幕到达刚才修改的页面的时间。


这个时候就是我们的第二款高效开发的得力助手登场的时候了。


它就是 💉 Injection III,一款开源免费的热重载工具。


Injection III 是一款能在iOS开发时实现类似Web前端那样热重载的工具。他会监听代码文件的变化,当代码发生改变,他会将改变的部分自动编译成一个动态链接库,然后动态的加载到程序中,达到不重启APP直接热重载的目的。


下面我简单介绍一下如何使用它。


我们可以在 Mac App Store 上下载InjectionIII。打开后会在状态栏有一个蓝色的注射器图标,选择Open Project 打开工程所在目录开始监听我们的文件更改。




接下来在工程中进行一些配置,


Xcodebuild settingOther Linker Flags 中添加-Xlinker -interposable


AppDelegateapplicationDidFinishLaunching方法中加入如下代码:

#if DEBUG
//for iOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
//for tvOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()
//Or for macOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endif

接下来,编译运行你的APP,此时控制台会打印Injection的相关信息




同时状态栏的图标也会变为红色。此时说明Injection启动成功。


接下来你就可以修改一下代码,并保存,Injection会自动编译并自动将其注入到模拟器中运行的APP。控制台也会打印相关的信息。




同时,它会为被注入的类提供一个回调@objc func injected() ,当某个类被注入时,会调用该方法。


我们可以在这里刷新UI,就能做到实时更新UI了。


注意事项


虽然Injection很强大,但它也有很多限制:

  • 你可以修改class、enum、struct 的成员方法的实现,但如果是inline函数则不行,如果有这种情况需要重新编译运行。

  • 它只支持模拟器,不支持真机。

  • 你不能修改class、enum、struct的布局(即成员变量和方法的顺序),如果有这种情况需要重新编译运行。

  • 你不能增加或删除存储类型的属性和方法,如果有这种情况需要重新编译运行。


更多详情可以参见官方的说明:InjectionIII/README.md at main · johnno1962/InjectionIII (github.com)


虽然 Injection III 有很多限制,但它依然能为我们带来非常大的效率提升。


另一个热重载神器: krzysztofzablocki/Inject


krzysztofzablocki/Inject: Hot Reloading for Swift applications! (github.com)


它配合 Injection III 可以更方便的实现热重载和界面自动刷新,实现类似Swift UI的自动刷新效果,但是,它只支持Swift,并且通过Swift Package Manager进行安装。


三、写在最后


实用的工具很多,找到一款既强大又好用的工具,并且把它用好能够很大的提升我们开发的效率。


希望大家能喜欢我分享的这两款工具,希望它们能为大家带来效率的提升。


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

iOS UITableView 图片刷新闪烁问题记录

iOS
一. 问题背景 项目中遇到一个问题,就是当App不在首页的时候,切换到其他App比如微信,然后返回App当前页面,然后从当前页面返回首页,会在首页viewWillAppear这里去拉取是否有未完成订单的接口,刷新UITableView,这时会出现广告位闪烁问题...
继续阅读 »

一. 问题背景


项目中遇到一个问题,就是当App不在首页的时候,切换到其他App比如微信,然后返回App当前页面,然后从当前页面返回首页,会在首页viewWillAppear这里去拉取是否有未完成订单的接口,刷新UITableView,这时会出现广告位闪烁问题。




二. 问题排查


1.原因分析


这个问题经过断点调试和排除法,发现只要当App进入后台后,回来刷新首页的UITableView都有可能出现闪烁现象。


因此首先我们对图片的加载做延迟操作,并在Cell生成方法调用里面添加相关打印:






可以看到如下打印日志:




从打印日志我们可以看出来,调用reloadData方法后,原来UITableViewcell位置会调整。


但是如果我们App没有进入后台,而是直接调用UITableViewreloadData方法,并不会出现闪烁现象。


因此可以这里可以推测应该是进入后台做了什么操作导致,回到App刷新才会导致闪烁。


因为使用的是SDWebImage加载框架加载,我们合理的怀疑是加载图片的SDWebImage框架,进入后台的处理逻辑导致的,因此我们先使用imageCacheDict字典写下图片加载和缓存逻辑:




经测试,进入后台,再返回App刷新不会出现闪烁现象。


因此可以肯定UITableView调用reloadData方法闪烁原因是SDWebImage,在进入后台的时候对内存缓存做了相关操作导致。


我们都知道SDWebImage,默认是使用NSCache来做内存缓存,而NSCache在进入后台的时候,默认会清空缓存操作,导致返回App调用UITableView调用reloadData方法时候,SDWebImage需要根据图片地址重新去磁盘获取图像数据,然后解压解码渲染,因为是从缓存磁盘直接获取图像数据,没有渲染流程,因此会造成闪烁。


为了验证这个猜想,我们使用YYWebImage加载框架来做对比实验:

首先注释掉YYWebImage进入后台清空内存缓存的逻辑: 


然后进入后台,返回App调用UITableView调用reloadData刷新,发现一切正常。

原因总结:

  • 第一个原因是UITableView调用reloadData方法,由于UITableViewCell的复用,会出现Cell位置调整现象

  • 由于SDWebImage使用了NSCache做内存缓存,当App进入后台,NSCache会清空内存缓存,导致返回App后调用UITableView调用reloadData,刷新去加载图片的时候,需要从SDWebImage的磁盘中重新获取图片数据,然后重新解压解码渲染,因为从磁盘中读取速度快,两者原因导致了闪烁。


三. 解决方案


因为该现象是由如上两个原因导致,因此针对这两个原因,有如下两种解决方案:

1. 解决UITableViewCell复用问题


可以通过设置ReusableCellWithIdentifier不同,保证广告cell不进行复用。

 NSString *cellId = [NSString stringWithFormat:@"%ld-%ld-FJFAdTableViewCell", indexPath.section, indexPath.row];

2. 从后台返回后,提早进行刷新操作

当从后台返回App前台的时候或者视图添加到父视图的时候,先执行下UITableView调用reloadData方法,提前通过SDWebImage去从磁盘中加载图片。


从后台返回前台:

[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(willEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
- (void)willEnterForeground {
[self.tableView reloadData];
NSLog(@"--------------------------willEnterForeground");
}

视图添加到父视图:

- (void)willMoveToParentViewController:(UIViewController *)parent {
[self.tableView reloadData];
}

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

RunLoop:iOS开发中的神器,你真的了解它吗?

iOS
在iOS开发中,RunLoop是一个非常重要的概念,它提供了一个事件循环机制,用于处理各种事件,例如用户交互、网络请求、定时器等等。RunLoop不仅是iOS开发中的核心之一,而且在其他平台的开发中也有广泛的应用。本文将为您介绍Swift中RunLoop的基本...
继续阅读 »

iOS开发中,RunLoop是一个非常重要的概念,它提供了一个事件循环机制,用于处理各种事件,例如用户交互、网络请求、定时器等等。RunLoop不仅是iOS开发中的核心之一,而且在其他平台的开发中也有广泛的应用。本文将为您介绍SwiftRunLoop的基本概念和使用方法。


什么是RunLoop?


RunLoop是一个事件循环机制,它用于在iOS应用程序中处理各种事件。RunLoop在应用程序的主线程中运行,它负责管理该线程中的事件,并确保UI更新等重要任务能够顺利执行。RunLoop还负责处理其他线程发送的事件,例如网络请求等等。


RunLoop的基本思想是循环地处理事件。当RunLoop启动时,它会进入一个无限循环,等待事件的发生。当有事件发生时,RunLoop会调用相应的处理方法来处理该事件,并继续等待下一个事件的发生。RunLoop会一直运行,直到被手动停止或应用程序退出。


RunLoop与线程


iOS中,每个线程都有一个RunLoop,但默认情况下,RunLoop是被禁用的。要使用RunLoop,必须手动启动它,并将其添加到线程的运行循环中。


例如,要在主线程中使用RunLoop,可以使用如下代码:

RunLoop.current.run()

这将启动主线程的RunLoop,并进入一个无限循环,等待事件的发生。


RunLoop模式


RunLoop模式是RunLoop的一个重要概念,它定义了RunLoop在运行过程中需要处理的事件类型。一个RunLoop可以有多个模式,但在任何时刻只能处理一个模式。每个模式都可以包含多个输入源(input source)和定时器(timer)RunLoop会根据当前模式中的输入源和定时器来决定下一个事件的处理方式。


RunLoop提供了几个内置模式,例如:

  1. NSDefaultRunLoopMode:默认模式,处理所有UI事件、定时器和PerformSelector方法。
  2. UITrackingRunLoopMode:跟踪模式,只处理与界面跟踪相关的事件,例如UIScrollView的滚动事件。
  3. NSRunLoopCommonModes:公共模式,同时包含NSDefaultRunLoopModeUITrackingRunLoopMode。 RunLoop还允许开发者自定义模式,以满足特定需求。

定时器


iOS开发中,定时器是一种常见的事件,例如每隔一段时间刷新UI、执行后台任务等等。RunLoop提供了定时器(timer)机制,用于在指定时间间隔内执行某个操作。


例如,要在主线程中创建一个定时器并启动它,可以使用如下代码:

let timer = Timer(timeInterval: 1.0, repeats: true) { timer in // 定时器触发时执行的操作 } RunLoop.current.add(timer, forMode: .common)

这将创建一个每隔1秒钟触发一次的定时器,并在公共模式下添加到主线程的RunLoop中。


在添加定时器时,需要指定它所属的RunLoop模式。如果不指定模式,则默认为NSDefaultRunLoopMode。如果需要在多个模式下都能响应定时器事件,可以使用NSRunLoopCommonModes


输入源


输入源(input source)是一种与RunLoop一起使用的机制,用于处理异步事件,例如网络请求、文件读写等等。RunLoop在运行过程中,会检查当前模式下是否有输入源需要处理,如果有则会立即处理。


输入源可以是一个Port、Socket、CFFileDescriptor等等。要使用输入源,必须将其添加到RunLoop中,并设置回调函数来处理输入事件。


例如,要在主线程中使用输入源,可以使用如下代码:

let inputSource = InputSource()
inputSource.setEventHandler {
// 输入源触发时执行的操作
}
RunLoop.current.add(inputSource, forMode: .common)

这将创建一个输入源,并在公共模式下添加到主线程的RunLoop中。


Perform Selector


Perform Selector是一种调用方法的方式,可以在RunLoop中异步执行某个方法。在调用方法时,可以设置延迟执行时间和RunLoop模式。该方法会在指定的时间间隔内执行,直到被取消。


例如,要在主线程中使用Perform Selector,可以使用如下代码:

RunLoop.current.perform(#selector(doSomething), target: self, argument: nil, order: 0, modes: [.default])

这将在默认模式下异步执行doSomething方法。


RunLoop的常用操作


除了上述基本操作之外,RunLoop还提供了其他常用操作,例如:

  1. stop:停止RunLoop的运行。
  2. runUntilDate:运行RunLoop直到指定日期。
  3. runMode:运行RunLoop指定模式下的事件处理循环。
  4. currentMode:获取当前RunLoop的运行模式。

RunLoop与线程安全


iOS开发中,多线程是一个常见的问题。RunLoop在处理异步事件时,可能会导致线程不安全的问题。为了保证RunLoop的线程安全,可以使用以下方法:

  1. 使用RunLoopQueue,在队列中使用RunLoop来执行异步操作。
  2. 在主线程中使用RunLoop来处理异步事件,避免跨线程操作。

结论


RunLoopiOS开发中非常重要的一个概念,它提供了一种事件循环机制,用于处理各种事件。RunLoop的基本思想是循环地处理事件,当有事件发生时,RunLoop会调用相应的处理函数来处理事件。RunLoop还提供了定时器、输入源、Perform Selector等机制来处理异步事件。了解RunLoop的工作原理,可以帮助我们更好地理解iOS应用的运行机制,避免出现一些奇怪的问题。


最后,我们再来看一下RunLoop的一些注意事项:

  1. 不要在主线程中阻塞RunLoop,否则会导致UI卡顿。
  2. 避免使用RunLoopNSDefaultRunLoopMode模式,因为这个模式下会处理大量UI事件,可能会导致其他事件无法及时处理。
  3. 在使用RunLoop的过程中,需要注意线程安全问题。

RunLoop是一种事件循环机制,通过它,我们可以很方便地处理各种事件,避免出现一些奇怪的问题。在日常开发中,我们需要经常使用RunLoop,所以建议大家多多练习,掌握RunLoop的各种用法。


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

作为一个前端为什么要学习 Rust ?

这里抛出一个问题 作为一个前端为什么要去学习 Rust ? 这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言? 那么今天...
继续阅读 »

这里抛出一个问题


作为一个前端为什么要去学习 Rust ?


这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言?


那么今天我就谈谈我自己对于这个问题的看法~,主要是分为 5 点:

  • 性能
  • 跨平台特性
  • 安全性
  • 职业视野
  • 职业竞争力

性能


Rust 可以给 Node.js 提供一个性能逃生通道,当我们使用 Node.js 遇到性能瓶颈或 CPU 密集计算场景的时候,便可以使用 Rust 编写 Native Addon 解决这个问题了,Native Addon 就是一个二进制文件,也就是 xxx.node 文件,比如 swc(对应 babel)、Rspack(对应Webpack)、Rome(对应 eslint、prettier、babel、webpack 等,目标是代替我们所熟悉的所有前端工具链...),上面提到的工具链就都是使用 Rust 编写的,性能比 Node.js 对应功能的包都有了极大的提高,同时 Rust 也是支持多线程的,你编写的多线程代码在 Node.js 中一样可以跑,这就可以解决了 Node.js 不擅长 CPU 密集型的问题。在前端架构领域目前 Rust 已经差不多是标配了,阿里、字节内部的前端基建目前都开始基于 Rust 去重构了。


跨平台


可以编写高性能且支持跨平台的 WebAssembly 扩展,可以在浏览器、IOT 嵌入式设备、服务端环境等地方使用,并且也拥有很不错的性能;和上面提到的 Native Addon 不一样, Native Addon 在不同的平台上都需要单独的进行编译,不支持跨平台;但是 WebAssembly 不一样,虽然它的性能没 Native Addon 好,但是跨平台成本很低,我编写的一份代码在 Node.js 中执行没问题,在 Deno 中跑也没问题,在 Java 或者 Go 中跑也都没问题,甚至在单片机也可以运行,只要引入对应的 Wasm 运行时即可。现在 Docker 也已经有 WebAssembly 版本了;同时 Rust 也是目前编写 WebAssembly 最热门的语言,因为它没有垃圾回收,性能高,并且有一个超好用的包管理器 cargo。


安全


Rust 编译器真的是事无巨细,它保证你编写的代码不会出低级错误,比如一些类型上的错误和内存分配上的错误,基本上只要 Rust 代码能够编译通过,就可以安心上线,在服务端、操作系统等领域来说这也是个很好的特性,Linux 系统和安卓系统内核都已经开始使用 Rust ,这还信不过嘛?


视野


Rust 可以提升自己在服务端领域的视野,Rust 不同于 Node.js 这个使用动态 JS 语言的运行时,它是一门正儿八经的静态编译型编程语言,并且没有垃圾回收,可以让我们掌握和理解计算机的一些底层工作机制,比如内存是如何分配和释放的,Rust 中使用所有权、生命周期等概念来保证内存安全,这对我们对于编程的理解也可以进一步提升,很多人说学习了 Rust 之后对自己编写其它语言的代码也有了更深的理解,毕竟计算机底层的概念都是相通的,开阔自己的编程思维。


职业竞争力


这个问题简单,你比别人多一门技能,比如 WebAssembly 和 Native Addon 都可以作为 Node.js 性能优化的一种手段,面试的时候说你会使用 Rust 解决 Node.js 性能问题,这不是比别人多一些竞争力吗?面试官那肯定也会觉得你顶呱呱~ 另外虽然目前 Rust 的工作机会比较少,但是也不代表没有,阿里和字节目前都有关于前端基建的岗位,会 Rust 是加分项,另外 Rust 在 TIOBE 编程语言榜排名中已经冲进了前 20,今年 6 月份是第 20 名,7 月份是第 17 名,流行度开始慢慢上来了,我相信以后工作机会也会越来越多的。


总结


不过,总的来说,这还是得看自己个人的学习能力,学有余力的时候可以学习一下 Rust,我自己不是 Rust 吹啊,我学习 Rust 的过程中真的觉得很有趣,因为里面的很多概念在前端领域中都是接触不到的,学了之后真的像是打开了新世界的大门,包括可以去看 Deno 的源码了,可以了解到一个 Js 运行时是怎么进行工作的,这些都是与我们前端息息相关的东西,即使哪天不做前端了,可以去转服务端或嵌入式方向,起码编程语言这一关不需要费多大力气了,Rust 是目前唯一一门从计算机底层到应用层都有落地应用的语言。不多说了,学就完事了,技多不压身嘛


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

只改了五行代码接口吞吐量提升了10多倍

背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
继续阅读 »

背景


公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


然而压测一开,100 的并发,吞吐量居然只有 50 ...


image.png


而且再一查,100的并发,CPU使用率居然接近 80% ...




从上图可以看到几个重要的信息。


最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


再一看百分位,大部分的请求响应时间都在4s。无语了!!!


所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


分析过程


定位“慢”原因



这里暂时先忽略 CPU 占用率高的问题



首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



  • 锁 (同步锁、分布式锁、数据库锁)

  • 耗时操作 (链接耗时、SQL耗时)


结合这些先配置耗时埋点。



  1. 接口响应时长统计。超过500ms打印告警日志。

  2. 接口内部远程调用耗时统计。200ms打印告警日志。

  3. Redis访问耗时。超过10ms打印告警日志。

  4. SQL执行耗时。超过100ms打印告警日志。


上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


<!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
<!-- 压测时可以认为 type = 1 是写死的 -->
update table set field = field - 1 where type = 1 and filed > 1;

上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


优化后的效果:


image.png


嗯...


emm...


好! 这个优化还是很明显的,提升提升了近2倍。




此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


最大值: 已经从 5s -> 2s


百分位值: 4s -> 1s


这已经是很大的提升了。


继续定位“慢”的原因


通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


我们继续看日志,此时日志出现类似下边这种情况:


2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



  1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

  2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

  3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


按照这三个思路做了以下操作:


首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


唉,一顿操作猛如虎。


PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


定位CPU使用率高的原因


CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



  1. 有额外的线程存在。

  2. 代码有部分CPU密集操作。


然后继续一顿操作:



  1. 观察服务活跃线程数。

  2. 观察有无CPU占用率较高线程。


在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


image.png


没有很高就证明大家都很正常,只是多而已...


此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


在看的过程中发现这段日志:


"http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
java.lang.Thread.State: RUNNABLE
at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
......
......

上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


通过堆栈信息很快定位到执行位置:


<!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

而RedisMaster类


@Component
@Scope("prototype")
public class RedisMaster implements IRedisTool {
// ......
}

没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


叹气!!!


赶紧改代码,直接使用万能的 new 。


在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


long start = System.currentTimeMillis();
// ......
long end = System.currentTimeMillis();
long runTime = start - end;


或者Hutool提供的StopWatch:


这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


StopWatch watch = new StopWatch();
watch.start();
// ......
watch.stop();
System.out.println(watch.getTotalTimeMillis());

而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





最终结果:



image.png





排查涉及的命令如下:



查询服务进程CPU情况: top –Hp pid


查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


打印当前堆栈信息: jstack -l pid >> stack.log


总结


结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



  • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

  • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

  • JVM : 内存大小,分配,垃圾收集器都想换...


总归一通瞎搞,能想到的都试试。


后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




最后5行代码有哪些:



  1. new Redis实例:1

  2. 耗时统计:3

  3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


TODO


问题虽然解决了。但是原理还不清楚,需要继续深挖。



为什么createBean对性能影响这么大?



如果影响这么大,Spring为什么还要有多例?


首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


image.png


org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


image.png



System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



继续学习性能优化知识




  • 吞吐量与什么有关?


首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



  • CPU使用率的高低与哪些因素有关?


CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



  • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20
    作者:FishBones
    来源:juejin.cn/post/7185479136599769125
    左右。

收起阅读 »

前段时间面试了一些人,有这些槽点跟大家说说

大家好,我是拭心。 前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备 去年工...
继续阅读 »

大家好,我是拭心。


前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。


image.png


简历书写和自我介绍



  1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备


image.png




  1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了




  2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求




  3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色




  4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多




  5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!




  6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)




  7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点




  8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说




  9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到




  10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点




  11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略




  12. 你可以这样审视自己的简历和自我介绍:


    a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点


    b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考


    c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点




面试问题


image.png




  1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质




  2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任




  3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质




项目经历


项目经历就是我们过往做过的项目。


项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。


有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?


大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。


image.png


在项目经历上,面试者常见的问题有这些:



  1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

  2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

  3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)


出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。


在看面试者的项目经历时,面试官主要关注这三点:


1. 之前做的项目有没有难度


2. 项目经验和当前岗位需要的是否匹配


3. 经过这些项目,这个人的能力有哪些成长


因此,我们在日常工作和准备面试时,可以这样做:



  1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

  2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

  3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

  4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

  5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

  6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

  7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验


技能知识点


技能知识点就是我们掌握的编程语言、技术框架和工具。


相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。


image.png


在技能知识点方面,面试者常见的问题有这些:



  1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

  2. 技术不对口:没有岗位需要的领域技术

  3. 技术过剩:能力远远超出岗位要求


第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。


第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。


第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。


在我面试的人里,通过面试的都有这些特点:



  1. 技术扎实:不仅仅基础好,还有深度

  2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事


有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?


软素质


这里的「软素质」指面试时考察的、技术以外的点。


程序员的日常工作里,除了写代码还需要做这些事:



  1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

  2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

  3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


image.png


因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:



  1. 理解能力和沟通表达能力

  2. 业务能力

  3. 稳定性


第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。


第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。


业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。


遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。


第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。


针对以上这三点,我们可以这样做:



  1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

  2. 回答问题时有逻辑条理,可以采用类似总分总的策略

  3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

  4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标


总结


好了,这就是我前段时间面试的感悟和吐槽。


总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。

作者:张拭心
来源:juejin.cn/post/7261604248319918136

收起阅读 »

揭秘:Android屏幕中你不知道的刷新机制

前言 之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了: 16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时...
继续阅读 »

前言


之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:


16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时机调用是不同的,如果操作是在16.6ms快结束的时候去绘制的,那么岂不是就是时间少于16.6ms,也会产生丢帧的问题?再者熟悉绘制的朋友都知道请求绘制是一个Message对象,那这个Message是会放进主线程Looper的队列中吗,那怎么能保证在16.6ms之内会执行到这个Message呢?


View ## invalidate()


既然是绘制,那么就从这个方法看起吧


public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate
) {
......
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
.....
}
}

主要关注这个p,最终调用的是它的invalidateChild()方法,那么这个p到底是个啥,ViewParent是一个接口,那很明显p是一个实现类,答案是ViewRootImpl,我们知道View树的根节点是DecorView,那DecorView的Parent是不是ViewRootImpl呢


熟悉Activity启动流程的朋友都知道,Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView()最终其实是调用了 WindowManagerGlobal 的 addView() 方法,我们就从这里开始看,因为是隐藏类,所以这里借助Source Insight查看WindowManagerGlobal


public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow
) {
synchronized (mLock) {
.....
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
....
view.assignParent(this);
...
}
}
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
}
}

参数是ViewParent,所以在这里就直接将DecorView和ViewRootImpl给绑定起来了,所以也验证了上述的结论,子 View 里执行 invalidate() 之类的操作,最后都会走到 ViewRootImpl 里来


ViewRootImpl##scheduleTraversals


根据上面的链路最终是会执行到scheduleTraversals方法


void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码方法不长,首先如果mTraversalScheduled为false,进入判断,同时将此标志位置位true,第二句暂时不管,后续会讲到,主要看postCallback方法,传递进去了一个mTraversalRunnable对象,可以看到这里是一个请求绘制的Runnable对象
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

doTraversal方法里面,又将mTraversalScheduled置位了false,对应上面的scheduleTraversals方法,可以看到一个是postSyncBarrier(),而在这里又是removeSyncBarrier(),这里其实涉及到一个很有意思的东西,叫同步屏障,等会会拉出来单独讲解,然后调用了performTraversals(),这个方法应该都知道了,View 的测量、布局、绘制都是在这个方法里面发起的,代码逻辑太多了,就不贴出来了,暂时只需要知道这个方法是发起测量的开始。


这里我们暂时总结一下,当子View调用invalidate的时候,最终是调用到ViewRootImpl的performTraversals()方法的,performTraversals()方法又是在doTraversal里面调用的,doTraversal又是封装在mTraversalRunnable之中的,那么这个Runnable的执行时机又在哪呢


Choreographer##postCallback


回到上面的scheduleTraversals方法中,mTraversalRunnable是传递进了Choreographer的postCallback方法之中


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis
) {
if (DEBUG_FRAMES) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

可以看到内部好像有一个类似MessageQueue的东西,将Runnable通过delay时间给存储起来的,因为我们这里传递进来的delay是0,所以执行scheduleFrameLocked(now)方法


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
private boolean isRunningOnLooperThreadLocked() {
return Looper.myLooper() == mLooper;
}

这里有一个判断isRunningOnLooperThreadLocked,看着像是判断当前线程是否是主线程,如果是的话,调用scheduleVsyncLocked()方法,不是的话会发送一个MSG_DO_SCHEDULE_VSYNC消息,但是最终都会调用这个方法


public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}

如果mReceiverPtr不等于0的话,会去调用nativeScheduleVsync(mReceiverPtr),这是个native方法,暂不跟踪到C++里面去了,看着英文方法像是一个安排信号的意思 之前是把CallBack存储在一个Queue之中了,那么必然有执行的方法


void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG_FRAMES) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

看一下这个方法在哪里调用的,走到了doFrame方法里面


void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
try {
.....
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
.....
}

那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals() 开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements
Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
scheduleVsync();
return;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

可以看到,是在onVsync回调中,Message msg = Message.obtain(mHandler, this),传入了this,然后会执行到run方法里面,自然也就执行了doFrame方法,所以最终问题也就来了,这个onVsync()这个是什么时候回调来的,这里查询了网上的一些资料,是这么解释的



FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。



那也就是说,onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,


同步屏障


总结下上面的知识,我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。


那么这样是不是产生一个问题,因为我们知道,平常Handler发送的消息都是同步消息的,也就是Looper会从MessageQueue中不断去取Message对象,一个Message处理完了之后,再去取下一个Message,那么绘制的这个Message如何尽量保证能够在16.6ms之内执行到呢,


这里就使用到了一个同步屏障的东西,再次回到scheduleTraversals代码


void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

mHandler.getLooper().getQueue().postSyncBarrier(),这一句上面没有分析,进入到方法里


private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

可以看到,就是生成Message对象,然后进一些赋值,但是细心的朋友可能会发现,这个msg为什么没有设置target,熟悉Handler流程的朋友应该清楚,最终消息是通过msg.target发送出去的,一般是指Handler对象


那我们再次回到MessageQueue的next方法中看看


Message next() {
for (;;) {
....
synchronized (this) {
...
//对,就是这里了,target==null
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}
}
}

可以看到有一个Message.target==null的判断, do while循环遍历消息链表,跳出循环时,msg指向离表头最近的一个消息,此时返回msg对象


可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息


这样的话,能够尽快的将绘制的Message给取出来执行,嗯,这里为什么说是尽快呢,因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行

作者:花海blog
来源:juejin.cn/post/7267528065809907727

收起阅读 »

毕业两年,我的生活发生了翻天覆地的变化

去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。 我学会接受了自己的平凡 大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减...
继续阅读 »

去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。


c129b212a0f596a998f904adaf8772c.jpg
我学会接受了自己的平凡


大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减少,于是校招、找工作的时候就认清现实,好像是个offer就去。


毕业后,我从中选了最满意的一个offer,前往深圳。


在公司的一年,我浑浑噩噩,每天感觉时间像不够用似的。每天升级打怪,学到了很多,后面因为公司业务原因,跳槽了。但是很开心的是在这里认识到了很多小伙伴,大家现在也有联系,时不时出来喝个酒。也和公司技术很牛的大佬成了朋友。也学会接受了自己的平凡,原来真的有那种写代码很轻松,把写代码当游戏,把工作当乐趣的人呀,真的是降维打击我这个小菜鸡。


78840c794394308c40b286f3321073b.jpg


人生就是不断的坍塌,然后自我重建。


一年过去了,我好像没了刚出社会的冲劲,偶尔下班也会学习,但是没有像刚毕业一样有很多的学习热情,闲暇时间就会多刷技术贴,技术文章。


跳槽跑路了,结果我发现我从刀山跑到了火海。入职后我才知道我所在部门的前端再过几天就要跑路了,相当于就我一个啥也不熟悉的来接锅了。组长带人和我交接时用了两小时,然后留下一脸蒙蔽的我。总之,后面度过了艰难的两个月,好歹算是背着锅缓慢前行。后面公司又出了不少幺蛾子,挺了7个月,忍不了又跑路了。但是这几个月吃的苦也让我的工作能力上涨,技能增多,抗压能力增强。于是我发现“人生就是不断的坍塌,然后自我重建,最后堆成了现在的我”。


相亲也不是不行


话说我年纪也不大,但是不知道为什么毕业两年,时间飞逝,居然就开始有点年龄焦虑。工作后也没什么渠道去认识女孩子。办公室一屋子男生,问他们对象哪儿来的,都说自己的对象是new来的。


也不是没被家里人拉去相亲过,第一年我觉得自己还小,也考虑到在家乡相亲的岂不是要异地啊,无比抗拒。第二年我成熟了,(不是,被毒打了)发现工作后是真难找对象啊。


转折点在某个风和日丽的下午,大数据都知道我单身了。我刷脉脉看到了相亲贴,然后知道了大厂相亲角公众号这个平台,这个公众号标榜用户都是阿里、字节、百度、腾讯、华为等大厂的单身同学。因为注册需要企业认证,最开始不太信任平台,就没注册。先进了他们的单身群观望,后面群里面每天都发一些嘉宾推荐,然后想到这种有门槛的,用户都是经过审核的,岂不是更可靠。感觉确实还蛮靠谱的,于是就注册了。被拉到上墙群后发现上墙群里的群友们都好优秀,小厂的我夹缝求生。


我算是发现了,人的观念是会一直变的,想当初我怎么也想不到自己会去相亲吧。


66b61a33c9903edc332893e26b27945.jpg


总结


毕业两年,我的生活好像变了很多,又好像没啥变化,曾经我不能接受的,现在又行了。曾经觉得自己可以了,现在也认清

作者:苍苍尔
来源:juejin.cn/post/7158708534471819278
现实了。哈哈哈哈哈。

收起阅读 »

卸下if-else 侠的皮衣!- 状态模式

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

🤭当我是if-else侠的时候


😶怕出错


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


😑难调试


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


🤨交接容易挨打


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


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



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



开放封闭原则(ASD)



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



里氏替换原则(LSP)



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



依赖倒置原则(DIP)



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



迪米特原则(LOD)



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



接口隔离原则(ISP)



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



在学习下设计模式


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

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

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

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


之前文章学习了**策略模式适配器模式,有兴趣可以过去看看,下面我们来学习适配器模式**


场景:做四种咖啡的咖啡机


- 美式咖啡(american):只吐黑咖啡
- 普通拿铁(latte):黑咖啡加点奶
- 香草拿铁(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡(mocha):黑咖啡加点奶再加点巧克力


用if-else来写,如下


class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/

// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
}

// 关注咖啡机状态切换函数
changeState(state) {
// 记录当前状态
this.state = state;
if(state === 'american') {
// 这里用 console 代指咖啡制作流程的业务逻辑
console.log('我只吐黑咖啡');
} else if(state === 'latte') {
console.log(`给黑咖啡加点奶`);
} else if(state === 'vanillaLatte') {
console.log('黑咖啡加点奶再加香草糖浆');
} else if(state === 'mocha') {
console.log('黑咖啡加点奶再加点巧克力');
}
}
}


分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


class CoffeeMaker {
constructor() {
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
// 初始化牛奶的存储量
this.leftMilk = '500';
}
stateToProcessor = {
that: this,
american() {
this.that.leftMilk = this.that.leftMilk - 100
console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
console.log('吐黑咖啡');
},
latte() {
this.american()
console.log('加点奶');
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆');
},
mocha() {
this.latte();
console.log('再加巧克力');
}
}

changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState('latte');
mk.changeState('mocha');



这个状态模式实际上跟策略模式很像,但是状态模式会关注里面的状态变化,就像上诉代码能检测咖啡牛奶量,去除了if-else,能很好的扩展维护



结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!给我点点赞,关注下!

作者:向乾看
来源:juejin.cn/post/7267207014382829579

收起阅读 »

老板搞这些事情降低我写码生产力,我把他开除了

web
Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Ki...
继续阅读 »

Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Kill Developer Productivity,不敢私藏干货,赶紧来分享给大家,希望能共同避免



简介


几周前,我突然发现:在工作 4 小时内,我的工作时间和有效的编码时间差了整整 2 小时。为了重回正轨,我决定主动减少阻碍,来缩小这个差距,争取能写更多代码,把无关的事情抛开。这个时间差越大,我的效率就越低。


和其他行业的人相比,程序员在这方面遇到的困境更甚。这些障碍往往会导致码农信心下降、写代码和优化的时间变少,职业倦怠率更高。影响创造力和热情。


根据本周 CodeTime 的全球平均编码时间,约 45%的总编码时间都是消极编码。时间和资金都在被浪费。


低效的开发流程是罪魁祸首。


1. 会议


会议


低效的会议是导致开发人员生产力下降的最不必要的因素之一。编程需要心流。平均而言,进入心流状态大约需要 30 分钟。但是由于乱七八糟会议,专注力就被打断了,码农必须重复这个过程。


有时 10 分钟的会议硬拖到一个小时,这也很浪费时间。减少了用于实际编程和解决问题的时间。有些会议还需要码农无用的出席。如果这次会议和码农的专业知识无关,根本没必要让他们参会。


2. 技术债(Fix it Later)


技术债


技术债,简单来说就是“以后再修”的心态。先采用捷径实现,妄图后面有空再修改成更优的方式。


最开始,先让功能可用就行,而优化留到以后。短期来看这可能行得通,因为它可以加快项目进度,你可能在 deadline 前完成。但是反复这么做就会留下大量待完成的工作。会使维护、扩展和优化软件变得更加困难。


技术债会以多种方式阻碍码农的生产力。列举一些:




  • Code Review 的瓶颈:当技术债增加时,会增加 Code Review 所花费的时间。




  • 更多 Bug:由于关注点都在速度而不是优化上,会导致引入隐藏的 Bug。




  • 代码质量降低:只为了让它可以跑,会导致糟糕的代码质量、随意的 Code Review,甚至没有代码注释,随意乱写复杂的代码等。




上述所有点都需要额外的时间来处理。因此,这会拖长项目的时间线。


3. Code Review


Code Review


Code Review 需要时间,如果 Review 时间过长,会延迟新代码的集成并放缓整个开发过程。有时候码农提出 PR 但 Code Reviewer 没有时间进行审查。会码农处理下一任务的时间。在进行下一个任务的同时,再回头 Code Review 时会有上下文切换。会影响码农的专注力和生产力。


对于 Code Review,码农可能不得不参加多个会议,减少了码农的生产力。代码反馈往往不明确或过于复杂,需要进一步讨论来加深理解,解决问题通常需要更长时间。Code Review 对一个组织来说必不可少且至关重要,但是需要注意方式和效率。


4. 微观管理 (Micromanagement)(缺乏自治)


微观管理


微观管理是一种管理方式,主管密切观察和管理下属的工作。在码农的语境下,当管理者想要控制码农的所有编码细节时就发生了。这可能导致码农对他们的代码、流程、决策和创造力的降低。


举例来说:




  • 缺乏动力:微观管理可能表明组织对码农能力的信任不足。这样,码农很容易感到失去动力。




  • 缺乏创造力:开发软件是一项需要专注以探索创造性解决方案的创作性任务。但是微观管理会导致码农对代码的控制较少,阻碍码农的创造力。




  • 决策缓慢:码农必须就简单的决定向管理层寻求确认,在这个过程中大量时间被浪费。




在所有这些情况下,码农的生产力都会下降。


5. 职业倦怠


职业倦怠


职业倦怠是码农面临的主要问题之一。面对复杂具有挑战性的项目和紧迫的 deadline,以及不断提高代码质量的压力都可能导致职业倦怠。这最终会导致码农的生产力下降,会显著减弱码农的注意力和写代码的能力。


这也会导致码农的创造力和解决问题的能力下降。这最终会导致开发周期变慢。


6. 垃圾文档


垃圾文档


文档对码农至关重要,因为它传达有关代码、项目和流程的关键信息。垃圾文档可能会导致开发周期被延迟,因为码农需要花更多时间试图理解代码库、项目和流程。这会导致码农生产力降低。


在码农入职期间,提供垃圾文档会导致码农在设置项目、管理环境、理解代码等任务上花费更多时间。在缺乏清晰文档的情况下,维护和修改现有代码变得困难。由于担心破坏功能,码农可能会犹豫重构或进行更改。因此,码农的生产力将浪费在非核心任务上。


7. 痴心妄想的 Deadline


痴心妄想的Deadline


Deadline 是使码农发疯的原因之一。你必须在较短的时间窗口内完成大量工作时,你会很容易感到沮丧。这可能导致职业倦怠、代码质量差、疏忽 Code Review 等。这将导致技术债的积累。因此,码农的生产力会下降。


Deadline 对计划开发周期是必要的,但是通过设置不切实际的 Deadline 来向码农施加压力,会让他们承受压力。在压力下,整体生产力和代码质量都会下降。


总结


上文提到的会议、技术债积累、拖沓的 Code Review、微观管理、导致职业倦怠的压力、垃圾代码文档以及为项目设置不切实际的 Deadline 等因素会阻碍码农的生产力。我试图阐明软件开发人员在追求效率和创造性解决方案的过程中面临的挑战。


其中的重点是,这些挑战是可以通过在码农的健康和高生产力之间建立平衡来克服的。你可以使用一些码农工具来帮助管理你的生产力、深度专注和工作效率。


下面是一些可以帮助提高生产力的工具:




  • FocusGuard:这是一个 Chrome 扩展,可以通过屏蔽网站帮助你保持专注。




  • Code Time:这是一个 VSCode 扩展,用于跟踪你的编码时间和活动编码时间。




  • JavaScript Booster:这个 VSCode 扩展可以为代码重构提供建议。你也给其他编程语言找找这种扩展。




  • Hatica:虽然上述工具局限于一个任务:专注于编码,但 Hatica 可以处理更多工作。它通过改进工作流程、识别瓶颈和跟踪进度来帮助工程团队提高码农生产力。通过这种方式,它可以给码农节约大量的时间。在这里了解更多关于这个工程管理平台的信息。





作者:ssh_晨曦时梦见兮

来源:juejin.cn/post/7267578376050114614


收起阅读 »

拿到优秀员工了

大家好呀,我是楼仔。 上周拿了个优秀员工,又收获了一枚证书,刚好抽屉里也塞了几个,发现装不下了,准备打包一并带回家。 收拾的时候,旁边的同事看到了,“楼哥,你咋这么多证书,是准备集齐后,召唤神龙么?” 尼玛,他这句话,差点让我笑出声。 后来发现,来小米已经 ...
继续阅读 »

大家好呀,我是楼仔。


上周拿了个优秀员工,又收获了一枚证书,刚好抽屉里也塞了几个,发现装不下了,准备打包一并带回家。


收拾的时候,旁边的同事看到了,“楼哥,你咋这么多证书,是准备集齐后,召唤神龙么?”


尼玛,他这句话,差点让我笑出声。



后来发现,来小米已经 4 年了,刚好一年一个,中间也换了几个部门,但是发现每个部门都能做得还不错,这个其实挺难的,下面就给大家简单分享一下个人经验。


可以再好一点


我们有个小群,除了我是菜鸡以外,其余基本都是大佬,有一次有个大佬在群里问 “大家觉得,在公司工作,什么是你觉得最重要的呢?”


有同学回答是技术,也有回答情绪管理,但是有一位同学的回答,让我印象深刻,就三个字“超预期”。


是的,很多同学都能把事情做到 70 分,甚至 80 分,但是如何才能做到让同事、老板对你印象深刻呢,答案就是超预期。


比如你是一个核心研发,写的代码质量高,甚至可以提前完成,完全不用老板操心,那么就能到 80 分,但是如果你还能带领大家一起把事情做好,慢慢成为项目的领头羊,那么这就是“超预期”。


当然,我们不可能所有事情都做到超预期,甚至我们也不知道有些事情是否能超预期,但是只要我们对自己高要求,反复琢磨,如何才能做得更好一点点,首先让自己满意,然后重要事情能超预期,就非常棒了。


打破边界,持续学习


偶尔让别人觉得你做得好,会比较容易,如果一直让别人觉得你做得好,就很难了。


我举个例子,比如今年你是核心研发,但是别人发现,你居然能把项目管理也做得很好,别人对你投来赞许的目光。


但是如果到了明年,你还是能把项目管理做得很好,可能也还好,但是如果过了 2-3 年呢,别人感觉你项目管理还不错,但是还会投来赞许的目光么?


这个就叫边际递减效应,就好比吃苹果,吃第一个苹果,感觉很好吃,但是当你一次性吃到第 6 个苹果时,你可能就觉得没那么好吃的。


所以工作也一样,你需要突破自己的边界,还是拿项目管理举例,比如我今年把项目管理做好,但是明年,我沉淀了一些项目管理的方法论,然后对部门进行培训,并指导他人如何进行项目管理,是不是又更进一步了?


那如何才能突破自己的边界呢?答案就是持续学习,不断突破自己。


当然,这个学习,肯定不是漫无目的地学习,你需要知道自己的短板,以及你岗位上限需要具备的技能,试探自己的边界,然后有针对地学习。


年龄焦虑


其实做计算机这一行,无论你是研发,还是产品、测试,很多同学都有年龄焦虑,这个很正常。


当你在公司,如果一直原地踏步,哪怕你现在表现得很好,但是当你年纪更大时,你具备的技能和你的年龄不匹配,当公司进行人员优化时,你可能就很危险了。


比如小王在公司里面,每天老老实实敲代码,兢兢业业,平时连假都不会请,这工作劲,是不是连他自己都感动?


当小王到了 40 岁,如果还是只会写代码,哪怕他代码水平一流,每天还是兢兢业业,公司如果要进行人员优化,你觉得他安全么?


所以当你的能力,能匹配上你的年龄,我想就不会那么焦虑了。


可能有同学会和我杠,那我到 40 多岁,然后被裁了呢?如果你到 40 多岁,都还没完成自己的财富积累,还没有提前准备好自己的退路,那真不能怪别人了。


........................................................


二哥上周六来武汉了,我和小灰给二哥接风,带着二哥的老婆和孩子,溜达了一整天。


小朋友没看过企鹅,就去了海洋世界,二哥不喜欢露脸,那就狗头伺候(二哥看到会不会打我),年龄虽然大了,但是精气神还是要有的。


大家猜猜,最右边的小伙伴是谁呢?


作者:楼仔
来源:juejin.cn/post/7266265543412793398

收起阅读 »

24岁 我裸辞啦😀

21年-22年间快速发展 23年-至今停滞不前 1.引言👺     交了辞呈估计有半个月了,自从我把钉钉状态改成last date 08-24之后清静了许多,现在坐在工位上突然回想起刚入职那会,罢了罢了,索性记录一下2年开发的心路历程,以纪念我咋从腼腆小白到相...
继续阅读 »

21年-22年间快速发展

23年-至今停滞不前


1.引言👺


    交了辞呈估计有半个月了,自从我把钉钉状态改成last date 08-24之后清静了许多,现在坐在工位上突然回想起刚入职那会,罢了罢了,索性记录一下2年开发的心路历程,以纪念我咋从腼腆小白相由薪生 怨气深重的打工🐶


image.png


2.工作背景💻︎


    事情还得从今年年初说起,那时候刚接手负责省厅的项目的研发,以前都是做自研或者市级的项目,作为一个小菜鸡内心还是有些(很大的)雀跃。参与需求调研=>了解用户以前的核心业务=>技术选型分析=>项目从零搭建到一期部署上线,都给我带来了很大的成就感。

    成就感的buff加持坚持不了多久,日渐疲惫,😇加上一直带我的领导跳槽之后暴露的问题越来越多由于需求改动很大且频繁,加上非gis专业,边自学边开发,从三月份开始了每日加班生活。

     简单来说就是:



  • 一个星期重做1-2版设计(没办法预测,省厅主任一句话不喜欢就得全部推翻)

  • 由于需要用到数广提供的平台部署生产环境(更新一次最少一个小时),实际开发时间为1-1.5天(包含推送、部署上线、测试)

  • 一周演示2次,通常是:演示(一天)=>修改/开发(1-2天)=>演示(1天)=>反复修改(1-2天)=>演示

  • 原型跟UI跟开发几乎是同步修改的

  • 开发替实施背锅


01b6054e850b3282dc1d12e3fea39f1.jpg


衍生出一系列问题可想而知,实施人员与产品没办法把握甲方要的效果,加上能拒绝不合理需求的领导走了,实施人员没有开发经验,实施运维的活推给我们开发做,我与她沟通也较为困难(当前业务如果这样做的话一定会被要求重做的、时间赶不及等等),我能做的只有反馈2个人给我这边一起开发,捋清楚需求、接口再分模块派任务等等。

    本来是我一个人骂骂咧咧😶‍,后来参与进来的全部人都在骂骂咧咧。。。

    第一次跟同事(实施项目经理)吵架:(因为她是女孩子,另一个经理让我们多理解包容一下。。。后面我实在忍不住了。。。🧐)


63641c20d9110e8b78474d6520a35c4.jpg


周五园区发通知说周六停电一天,我本以为可以好好休息,周末刷刷剧之类的,因为周四刚演示(前几天都加班到很晚)真的很累,下午的时候实施人员开完会说出新需求,周一前上线,领导就一句话让我们几个今晚扛电脑去另一个园区把项目赶出来(我们加班是没有加班费的,只有周末加班能调休,调休假期很难批)


昨天加班到一两点的同事们受不了了。。。开始抗议🤯


661f841e113699854488b618d8e8614.jpg

然后公司大领导出面开导我们了。。。


image.png

省流版:



  • 大家都在加班 研发突然不想加班 有点不负责任

  • 无加班费,无餐补无打车费,但是加班超过晚上八点半 早上可以晚半个钟打卡

  • 不涨工资,收益好的话(我们公司规模蛮大的,1000人+,绩效都是按项目统一定好的)绩效应该能多个几十块


3.摆脱内耗👋


看着领导发的话,有点内疚,我知道我能力不足,很多时候没能统筹好,理解业务起来也很慢,效率不高,还拉着同组同事一起加班,我下班回到家躺在床上望着天花板发呆,房间到是很安静,脑子很吵


是不是我不够认真 想的不够周全


是不是我太矫情了 加点班就不舒服


是不是我太敏感了 被人说两句就不得劲


想了很多下次我要安排好时间,动态设计参数等等等


后来躲在被窝偷偷哭了很久 给我妈打电话 我妈说:人家多少工资 你多少工资啊一起担责


嗯???啊???对啊 。。。我突然脑子短路。。。害得我哭饿了吃了一份烤鸡 两个冰淇淋 一份红豆糖水才缓过来,啊。。。就这样结束了我的内耗


image.png


想通了,然后我就在公司里开启了“一分钱一分货”的上班原则


PS:我是来赚钱的,两年没涨薪了,我的能力值得最少工资有百分之20%的涨幅,加班有加班费也是合理的(这句话听起来怪怪的),加上gis行业就业面窄,好领导还走了,加上还要出差,那也是时候换个更好的平台了


总之很心累


至于我。。。那晚没有在群里说话,而是第二天直接交了辞呈,表示周六要去复诊,以后也不加班了

然后周一被各个领导约去谈谈心,以前部门的同事也问我是不是受啥委屈了


和领导谈的时候,他们都只是表示会争取涨薪跟绩效。我没有聊太多,提出需要涨基本工资而不是绩效后没有明确的答复,还是决定裸辞。
613827a4b1e752a43404146dd4c914c.jpg


4.初入职场回忆


     21年入职这里,对于刚毕业的想蹲在广州的小菜鸡来说还是一份很体面的工作,面试的时候有些题没答上来,面试官(也是一直带我的前辈/领导)鼓励我按照自己的思路解答(我觉得当时的回答不沾边),感觉这个领导温柔又靠谱就选择了这里。


     该说不说,新哥就像老父亲一样💙


30755cdfe988e8ec4a4506175fdc7e8.jpg


团队里的各位靓仔美女前辈一直以来也对我很包容


3bb53172ffb7eecca3022c9c8b7b3a9.png
2a19a340e763803ba1ca303d084ca37.png
76ddce2a0bfba9a92356979338cd35c.png


那时候我的猪猪好像跟我一样 也在闪闪发光


4a0b8489318c5021c6f7ef127d305a0.jpg


055b0561e9ccbb74b9130d7df66360c.jpg


5.入行契机


    大学念的软件工程专业,虽然读书的时候java学不会C#看不懂但还是莫名觉得会留在互联网行业,
写写代码或者做剪辑。

    由于我的人设是个社恐话痨,虽是个吹水佬但不想做售前或营销之类(每天需要接触很多客户的)岗位,加上做前端有种“QQ秀”的吸引力,OK~决定往前端发展。

    其实真正开始学习前端是从毕业前的六个月(培训班太贵了,就在B站自学,主要还是我这个人比较抠),一直到现在仍然觉得女生做技术也很酷,环境也相对包容,与同事间的病情交流也十分和睦,头发还十分茂盛😎。


687b65bbb8e136be27fc8cd82044220.jpg


6.最后(关于未来)


目前即将成为无业游民,有合适的岗位积极争取,也做好了几个月待业的准备,嘎嘎刷题不焦虑。




  • 先出去旅游散散心,或者逛逛广州的博物馆 尝尝美食,吃好喝好睡好 给自己充充电




  • 做更详细的职业规划:



    • 继续做前端

    • 转岗产品 or 数据分析岗

    • 前端=>逐渐转产品 / 管理岗




  • 坐标广州,或许各位大佬可以给我一点建议(听劝!!🤟)或者内推~感激不尽😁




  • 最后的最后,躺平计划尚未成功,还需继

    作者:慌张的葡萄
    来源:juejin.cn/post/7267496335845163068
    续努力~



收起阅读 »

大佬都在用的 IDE 主题,21k Star!

大家好,我是 Zhero! 作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采...
继续阅读 »

大家好,我是 Zhero!


作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采用深色调和高对比度的设计风格,成为黑暗系编程主题的杰出代表。Dracula 主题的界面清晰简洁,代码高亮显示明确鲜明,使得代码结构更加清晰易读。使用 Dracula 主题不仅能减少眼睛的疲劳,还能让我们更专注于代码的编写和理解。如果你正在寻找一个优秀的代码编辑器主题,不妨给 Dracula 一试,相信它会给你带来全新的编程体验。


来源



Dracula 主题源自于一种热门的色彩风格,也被称为“Dracula”。它最初由 Zeno Rocha 在 TextMate 编辑器上设计和实现。随着其受欢迎程度的不断增加,Dracula Color Scheme 成为一个跨平台的开源项目,并得到了许多编辑器和 IDE 的支持。


JetBrains 公司注意到了 Dracula 这种深色调和高对比度的设计,并将其引入了他们的 IDE 产品线。现在,IntelliJ IDEA、PyCharm、WebStorm 等 JetBrains 的 IDE 都提供了官方支持的 Dracula 主题插件。这款黑暗炫彩的主题受到了广大程序员的喜爱,成为了他们工作中常用的选择之一。无论是在日常编码还是阅读代码时,Dracula 主题都能为程序员带来舒适的使用体验。


设计风格



Dracula 主题的设计具有以下魅力:

  • 深邃紫罗兰色基调: Dracula 的主题采用深邃的紫罗兰色作为基调,给人一种神秘而吸引人的感觉。
    1. 高对比度的前景和背景: Dracula 主题使用高对比度的前景和背景色,使得代码内容的层次分明,易于阅读和理解。
    2. 强调重要内容的语法高亮: Dracula 主题使用明亮的绿色进行语法高亮,能够清晰地强调代码中的重要部分,帮助程序员更好地理解代码逻辑。
    3. FLAT 扁平化设计风格: Dracula 主题采用简洁大方的 FLAT 扁平化设计风格,界面整洁清晰,让代码更加突出。
    4. 黑客文化与美学融合: Dracula 主题融合了黑客文化中的深色基调和对于对比度和视觉冲击的美学追求。它既展现了黑客式的科技感,又兼具艺术家般的美学气质。

    通过这些设计特征,Dracula 主题确保了代码的可读性,提供了令人愉悦的编程体验,并为开发者们带来了独特的视觉享受。


    优点


    Dracula 主题在技术上具有以下优势:

  • 中性深色背景的精心调配: Dracula 主题采用中性深色背景,软化了强光对眼睛的刺激,减轻了长时间工作导致的眼睛疲劳问题。
    • 明暗分明的前景和背景: Dracula 主题使用明暗分明的前景和背景色,使得代码的视觉层次感强,识别度高,提高了代码的可读性和理解效率。
    • 温暖色菜单栏和标识色边框: Dracula 主题在菜单栏和标识色边框上采用温暖色,增加了页面元素的识别度,帮助用户更好地找到所需功能。
    • 强调重要内容的明亮色彩: Dracula 主题使用明亮的色彩来突出重要的内容,提高了可关注点的辨识度,使开发者能够更快速地定位和理解关键代码部分。
    • 条件颜色支持: Dracula 主题提供了适应不同环境光照条件的条件颜色支持,确保在不同的工作环境中都能有良好的显示效果。

    Dracula 主题带来的用户体验提升包括:

  • 减轻眼睛疲劳问题: 通过精心调配的色彩和对比度,Dracula 主题可以减轻长时间工作导致的眼睛疲劳问题。
    • 提高代码可读性和理解效率: 明暗分明的视觉层次感和明亮色彩的使用使得代码更易于阅读和理解,提高了开发者的工作效率。
    • 丰富的语法色彩增强趣味性: Dracula 主题提供丰富的语法色彩,使得编程过程更具趣味性和乐趣,激发开发者的工作热情。
    • 酷炫的外观满足个性化追求: Dracula 主题具有独特的外观设计,满足技术宅对个性化的追求,让开发环境更具魅力和个性。
    • 对色弱用户友好: Dracula 主题经过精心设计,在保证美观的同时也考虑到了色弱用户的需求,不会造成视觉障碍。

    正因为这些优势,Dracula 主题备受码农的青睐。它极大地提升了 JetBrains IDE 的美观性和可用性,无论是初学者还是老手,都能够享受到 Dracula 主题带来的舒适的用户体验。


    支持产品


    到目前为止,Dracula 主题已经广泛支持341+款应用程序,涵盖了各个平台和工具。除了 JetBrains IDE,Dracula 还适用于许多其他知名的应用程序,其中包括但不限于以下几个:

    • Notepad++: Dracula 主题为 Notepad++ 提供了独特的外观,使得文本编辑器更加美观和舒适。
    • iTerm: Dracula 主题为 iTerm 终端应用程序带来了独特的配色方案,提升了终端界面的可视性和使用体验。
    • Visual Studio Code: Dracula 主题为 Visual Studio Code 提供了一套酷炫且易于辨识的代码颜色方案,让开发者能够更好地编写和调试代码。
    • Vim: Dracula 主题为 Vim 编辑器提供了一种简洁而又优雅的配色方案,使得代码在终端中的显示更加清晰明了。
    • Terminal.app: Dracula 主题为 macOS 上的 Terminal.app 终端应用程序提供了一种时尚和易于识别的配色方案,提升了终端的可用性和美观性。
    • Zsh: Dracula 主题兼容 Zsh 终端的配色方案,使得命令行界面更加美观和个性化。

    这些应用程序只是 Dracula 主题所支持的众多应用程序中的一部分,它们的加入使得 Dracula 主题在各个平台和工具上都能够提供一致的视觉体验,满足开发者对于美观和可用性的追求。



    查看更多支持产品:



    draculatheme.com



    使用


    下面我用 IDEA 实例给大家展示一下如何使用吧!



    1. 前往插件市场,搜索Dracula,点击安装




    1. 前往 Preferences > Appearance & Behavior > Appearance,从下拉菜单中选择Dracula



    1. 前往 Preferences > Editor > Color Scheme,从下拉菜单中选择Dracula



    通过上述步骤,您可以启用Dracula主题啦!


    总结


    Dracula 主题为 JetBrains IDE 带来了卓越的高对比度黑暗风格,本文我为大家介绍一下它的优点。如果你还没有尝试过这款插件,快去试试吧!


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

    🔥搞钱必看🔥 盘点那些靠谱的程序员副业,狠狠提升财富值💯

    这是一个职业生涯三叶草模型,它分为兴趣、价值、能力三个维度,完美的主职业最好同时满足这三项。但事情往往未必那么如意,如果主职业没能同时满足,那么剩下的部分,完全可以用副业填充。 或者,通俗点说,做副业的第一目标一般是赚钱,想大幅度增加“价值”尤其是物质价值这个...
    继续阅读 »


    这是一个职业生涯三叶草模型,它分为兴趣、价值、能力三个维度,完美的主职业最好同时满足这三项。但事情往往未必那么如意,如果主职业没能同时满足,那么剩下的部分,完全可以用副业填充。


    或者,通俗点说,做副业的第一目标一般是赚钱,想大幅度增加“价值”尤其是物质价值这个维度,那就让我们进入正题,看看程序员可以做的副业都有哪些。


    内容变现


    特点:高投入、门槛中等、长期主义


    公众号


    现在短视频更为吸引流量的时代,做公众号可能已经不是最优解,但做公众号的思路可以推广到一切内容副业上。认真写的内容也终将被看到,现在获取的粉丝反而更为核心、粘性更高。

    公众号的赚钱途径主要有三个:流量主、接受打赏、接广告。

    到达一定流量后,官方会邀请开通“流量主”功能,每篇推文末尾会有广告卡片,被点击即可获得收益。也就是说,开通该功能后,不需要做额外动作,发送推文就有收入。



    第二种是接受打赏,是很直观的读者对作者的物质激励。不过引导比较少的情况下,很少有读者自行去做打赏这个动作的,这部分收入就当做运营公众号的额外小激励吧。



    公众号积累粉丝,长期下来阅读量和粉丝比较好会有广告商找来做广告,俗称“恰饭”,这种收入相比前面两者来说就是“大单子”了,一单一般来说根据公众号质量和流量,会上百、上千、甚至上万、几十万不等。那这种收入就比较可观了,而且有广告主找到,也往往意味着,这个公众号质量还不错。


    为什么说公众号的思路可以推广到一切内容副业呢?因为内容可复用,就可以搭建自己的技术博客网站、分发到其他技术社区,而获利思路也是一致的,无非是流量和知名度的获益。具体的平台运营方式可能不会完全相同,还需要自己提前学习,不断在使用中进行探索。


    知识专栏


    如果你的写作水平还可以,也有可以成体系、系统化的知识输出,那可以尝试整理一个系列课程,以付费知识专栏的形式推出。



    极客时间、掘金小册、慕课网……非常多技术社区都推出了付费专栏,除了内容创作者自发上传的零散内容,一般也有这种成体系的、可以系统性掌握一部分技术的课程,大多数价格不高,一般在百元以内,正因为如此付费门槛也很低,用户乐意为此支付。这种付费专栏如果是线上分成形式,相当于是一个一劳永逸持续入账的项目。


    视频博主


    同样地,如果内容足够好的话,也可以考虑做视频博主,这分两个方向,个人博主和视频教程。不过相比于图文内容的专栏,视频的门槛会更高,对表达能力、授课能力、互动能力都有要求,还可能需要学习相应的录制、剪辑、运营知识。


    做个人视频博主,有平台本身给到内容创作者的流量收益,也有接广告恰饭的收益,一个细水长流,一个来得快但未必持久。长期下来还是需要持续更新、长久维护,小流量不断的同时,打广告有规律。出视频教程,则是与对应平台签约,看自己的版权要求、价格要求、付费机制,自行选择即可。


    技术变现


    特点:技术门槛低、项目导向、投入一般


    接外包


    说起程序员赚外快、副业来,一般最容易想到的就是接私活接外包,比如有很多外包服务平台如猪八戒、一品威客、解放号、码客帮、码市、程序员客栈等等,不过看起来价格并不很美好,大家可以自行筛选。比较靠谱的是熟人介绍,这样需求确认、尾款交付时都会比单纯陌生人更顺利。所以重要的还是积累技术、积累资源、积累人脉,逐渐打造自己的渠道和核心竞争力。


    自主开发项目


    如果技术水平不错,有想法有新意,但不想写文章、不愿录视频,依然只想跟代码打交道,那就可以自主开发项目来变现。根据自己的技术栈还有创意想法,可以是不同的产品,如桌面应用程序、手机APP、小程序、各种插件等。当然,产品要做到通过付费来盈利,门槛确实比较高了,而且除了技术,对运营能力也有一定的要求。做得好的完全可以做独立开发者来养活自己。


    比如这样在禅道插件市场上传插件的用户,已经获得了很高的持续性收益。


    其他方向


    特点:门槛高低不一、收入不一、形式灵活


     除了前面的内容导向和技术导向,其实还有很多跟技术、跟主业完全没有任何关系的方向。比如培训师,往往是按天收费,费用很高,不过当然也需要极高的知识壁垒;比如经常调侃的跑滴滴、送外卖,也能见证一些人生百态;比如有的人做手绘涂鸦、游戏陪玩这种兴趣驱动的副业,满足兴趣爱好的同时还能获得收入;比如很多脑力劳动者跑去做的轻体力活,如便利店收银、酒店前台、快餐店兼职,在机械操作的体力活中解放大脑。


    总之一句话:副业形式千千万,还是得靠实力干。短期的在线刷单、杀猪盘投资等等都被翻来覆去地反诈骗讲烂了,但任何宣称短期的、高回报率,都可能是带着引号的“副业”,需要擦亮眼睛加以鉴别。



    大多数副业还是有门槛的,需要投入精力和时间,找好方向,坚持长期主义,去做吧!


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

    为什么开发者应该多关注海外市场

    在上一篇《单枪匹马程序员,月营收从 0 到 1 万刀,近 90% 的毛利》里有掘友评论,这样的产品只有国外有,国内缺乏生态。 这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。 早在 2000 年(点 com 泡沫...
    继续阅读 »

    在上一篇《单枪匹马程序员,月营收从 0 到 1 万刀,近 90% 的毛利》里有掘友评论,这样的产品只有国外有,国内缺乏生态。


    这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。




    早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie 是 independent 的意思,意在独立(解脱)于各种束缚,比如:朝九晚五的工作时间、固定的办公室、领导、或者是投资人。


    而国内在最近几年也涌现了一拨独立开发者,多数以工程师为主,当然做的产品也是面向国内的市场。有做地不错的,像 Baye 的熊猫吃短信、vulgur 的极简时钟、Kenshin 的简阅等;但综合我这两年来对海外一些独立产品的研究,海外市场或许是更好的选择。


    当然凡是都有个前提,就是你没有一个豪华的创始团队或者是顶级投资人的背书,就是个人或者两三人的小团队。这个条件我觉得可以覆盖 90% 的中国的开发者;对于另外 10% 的拥有资源或者金主爸爸靠山的个人或者团队,不仅“可以”还“应该”去磕中国市场。但这不是今天要讨论的主题。


    在 BAT TMD 等巨头和背靠资源的精英创业者们的夹缝里,我觉得只有做面向海外市场的小产品是更有胜率一点;做国内市场面临的四个问题:


    第一、不存在足够的空间给个人/小团队做独立产品存活。


    Slack 大家应该都知道,在美国已经上市了,市值 200 亿刀。Slack 一直是被 qiang 的,但是为什么国内没有出现 Slack 这样的产品作为一个信息中心来连接各个办公工具?


    其实有,还不少,但都没活太久。一部分原因是腾讯阿里都非常重视这个“商业流量入口”,不想有可能被对方占有了。另外是国内互联网生态,从 BAT TMD 巨头到小软件公司,都太封闭;不仅不开放,还相互制约,都想把自己流量的守住,所以就同时出现了三个 Slack:

    • 微信出个企业微信(还封杀了 wetools)
    • 阿里出个钉钉
    • 字节出个飞书

    在这种巨头虎视眈眈且相互对抗的格局里,作为三缺(缺钱、缺资源、缺核心门槛)的个人或者团队是无法存活的。或许在 2010 年至 2016 年间还有草根产品团队依靠“热钱”注入有爆发的可能性,时至今日,特别是这个蜜汁 2020 的局势,是不太可能的了。


    即使,你找到了一个空白的利基市场(niche),你接下来面对三个问题:需求验证(试错)、推广、和商业化。


    第二点、需求验证或者叫“试错”成本高。


    由于国情不同,咱们需要经过一些不可避免的审核流程来保证互联网的干净。这个没话说,在哪做事就守哪的规矩。但这“需求验证”的门槛可就提高了不少。


    比如要做个网站吧,备案最快也两周。做游戏?有没有版号?做 app ?有没有软著?小程序(从用户端来讲)是个不错的创新,但是你最烦看到的是不是“审核不通过,类目不符合”?稍微做点有用户互动的功能都需要公司主体。公司注册、银行开户、做帐、以及各种实名制等;这些虽然都不是不可达到的门槛,但是去完成这些要耗费大量的精力,对于本身就单打独斗的开发者来说 - 太累了。


    再看看海外,简直不要太爽。做 app 还是需要经过苹果和谷歌的审核,但几乎不会对程序本身以外的东西设置门槛。网站注册个域名,30 秒改个 DNS 指到你的 IP,Netlify 或 Vercel 代码一推,就自动构建、部署、上线了。哪怕你不会写代码或者会写代码但是想先验证一下需求,看看潜在用户的响应如何,国外有不少非常值得一样的 no code 或 low code 平台。这个以后可以单独写一篇。


    OK,国内你也通过重重难关项目终于上线了,你面临剩下的两个问题:推广和商业化。


    第三点、推广渠道少 && 门槛高。


    海外市场的推广渠道更多元,比如 ProductHunt, IndieHackers, BetaList 等。这些平台不仅国内没有,我想表达的更重要一点是,这些平台用户都比较真诚和热心会实在地给你提建议,给你写反馈。国内也有几个论坛/平台,但是用户氛围和友好度就和上述几个没法比了。


    在付费推广方面,国内门槛(资质、资金)都挺高,感觉只有大品牌才能投得起广告,这对于缺资金的团队来讲,又是闭门羹。而国外,facebook 和 google 拿个 50、100 刀去做个推广计划都没问题。


    可以以非常低的成本来获取种子用户或者验证需求。


    行吧,推广也做得不错,得到了批种子用户并且涨势还不错;就最后一步了,商业化。


    第四点、商业化选择少。


    说商业化选择少可能说过了。国内是由于巨头间的竞争太激烈,出现各种补贴手段,导致互联网用户习惯于免费的互联网产品,甚至觉得应该倒贴给他来使用;伸手党、白 piao 党挺多;付费以及版权意识都还有改善空间。


    想想你都给哪些浏览器插件付费过?“插件还需要付钱?!”


    而海外用户的付费意愿足够强烈,在以后的案例分享中就能体会到。一个小小的浏览器插件,做得精美,触碰到了用户的购买欲望,解决了他一个痛点,他就愿意购买。


    下一篇就分享国外浏览器插件的产品案例。


    顺便再说一个「免费 vs 付费」的问题,这个不针对哪个国家,全世界都一样。免费(不愿意付费)的用户是最难伺候的,因为他们不 value 你的产品,觉得免费的就是创造者轻易做出来的、廉价的。如果依着这部分不愿意付费的客户来做需求,产品只会越做越难盈利。




    掘友们,下一篇文章见了


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

    iOS时钟翻转动画

    iOS
    最近项目有个时间倒计时的功能,网上也有很多优秀的第三方。这个功能应用还是比较广泛的,就稍微研究了一下。有好几种方法实现,笔者选取较简单一种。 原理其实很简单,就是一个的绕X轴的翻转动画,只是在翻转过程中针对特殊情况做 特殊处理就行,下面也会讲到。 效果图 思...
    继续阅读 »

    最近项目有个时间倒计时的功能,网上也有很多优秀的第三方。这个功能应用还是比较广泛的,就稍微研究了一下。有好几种方法实现,笔者选取较简单一种。


    原理其实很简单,就是一个的绕X轴的翻转动画,只是在翻转过程中针对特殊情况做 特殊处理就行,下面也会讲到。


    效果图




    思路


    以一次完整动画为例,分步骤解析:


    第一步:


    新建3个UILable,分别是正在显示(currentLabel)、下一个显示(nextLabel)、做动画的(animationLabel)。


    第二步:


    首先在每次动画前给nextLabel设置默认的X轴起始角度翻转,这样处理是为了能够只显示上半部分,下半部分被隐藏(zPosition不改动的情况下),如下图,红色的是nextLabel,绿色的是currentLabel,灰色的是animationLabel




    代码:

    // 设置默认的X轴起始角度翻转,为了能够只显示上半部分,下半部分被隐藏(zPosition不改动的情况下)
    func setupStartRotate() -> CATransform3D {
    var transform = CATransform3DIdentity
    transform.m34 = CGFLOAT_MIN
    transform = CATransform3DRotate(transform, .pi*kStartRotate, -1, 0, 0)
    return transform
    }

    第三步:


    使用CADisplayLink做动画,笔者这里设置固定的刷新帧率为60(因为存在不同的刷新帧率设备),且动画执行时间0.5s,即每次刷新帧率时动画执行了2/60进度。


    接下来使用CATransform3DRotateanimationLabel沿着X轴进行翻转动画,这时候我们会发现动画的进度超过一半时,会存在如下问题:




    上图这个是倒计时 2 变为 1 的过程,且动画进度超过一半时的显示画面。我们换个角度看看:




    可知在当前情况下,灰色的标签显示的是 2 的上部分的背面,但是应该显示的是 1 的下部分,这显示是有问题的。这么说有点拗口,简单来说就是一个物体在3D空间中沿X轴翻转大于90度时,我们看到的实际是物体的上下和前后均颠倒的二维平面,所以才会出现如此的不和谐。


    所以解决这个问题,使动画更和谐流畅,我们需要物体翻转的动画在临界点翻转到90度时,即与屏幕垂直的时候,为了正确显示,即需要将动画的animationLabel同时沿着Y和Z轴翻转,并切换文字,将2切换成1。即:

    if animateProgress >= 0.5 {
    t = CATransform3DRotate(t, .pi, 0, 0, 1);
    t = CATransform3DRotate(t, .pi, 0, 1, 0);
    animationLabel.text = nextLabel.text
    }else{
    animationLabel.text = currentLabel.text
    }

    此时的过程就是 2 在翻转超过90时,将之沿着Y和Z轴翻转,并切换为1,看到的就是动图显示的过程了。


    到这里一个完整的翻转动画就结束了,后面使用CADisplayLink定时重复上述动画就可以了。


    后续也使用这个动画写了一个时间显示的和倒计时的demo,具体的代码在下面的链接,感兴趣的可以查阅指导下。


    RCFoldAnimation


    若存在什么不对的地方,欢迎指正!


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

    从 Mac 远程登录到 iPhone

    iOS
    简介 平时在使用 Mac 的过程中,经常会使用终端输入命令来执行一些操作。在越狱开发的过程中,同样需要在 iOS 系统上输入一些命令来执行一些任务。那么如何才能在 iOS 系统上输入命令呢,在 iOS 上安装一个终端命令行工具,然后在 iPhone 那小小的屏...
    继续阅读 »

    简介


    平时在使用 Mac 的过程中,经常会使用终端输入命令来执行一些操作。在越狱开发的过程中,同样需要在 iOS 系统上输入一些命令来执行一些任务。那么如何才能在 iOS 系统上输入命令呢,在 iOS 上安装一个终端命令行工具,然后在 iPhone 那小小的屏幕上用触摸屏输入命令吗?虽然说理论上和实际上都是可行的,但是通过手指触摸屏幕来输入命令的方式效率比较低,也不是很方便。这里还是推荐在 Mac 上远程登录到 iOS 系统,这样就可以使用 Mac 的键盘输入命令到 iOS 上去执行,更加方便,快捷。


    SSL、openSSL、SSH、openSSH


    SSL(Secure Sockets Layer)是一种用于在计算机网络上进行安全通信的协议。SSL 最初由 Netscape 开发,后来发展为 TLS(Transport Layer Security)。SSL/TLS 用于在客户端和服务器之间建立安全的加密连接,以保护敏感数据的传输,例如在网页浏览器和服务器之间的数据传输。


    OpenSSL 是一个强大的、商业级的、功能齐全的开源工具包,它提供了一组库和命令行工具,用于处理 SSL/TLS 协议和加密算法,是 SSL 协议的一款开源实现工具。OpenSSL 可以用于创建和管理数字证书、实现安全传输和通信,以及进行加密和解密等操作。它不仅支持 SSL/TLS 协议,还支持多种加密算法和密码学功能。


    SSH(Secure Shell)是一种用于安全远程登录和数据传输的网络协议。它为计算机之间的通信提供了加密和身份验证,以确保通信的机密性和完整性。SSH 使用公钥密码体制进行身份验证,并使用加密算法来保护数据的传输。


    OpenSSH 是一个开源的 SSH 实现,它提供了 SSH 客户端和服务器的功能,用于安全远程登录、命令执行和文件传输。它包括客户端 ssh 和服务器 sshd、文件传输实用程序 scp 和 sftp 以及密钥生成工具 (ssh-keygen)、运行时密钥存储 (ssh-agent) 和许多支持程序。它是 Linux 和其他类 Unix 系统中最常见的 SSH 实现,也支持其他操作系统。


    SSL 最早出现于 1994 年,用于 Web 浏览器和服务器之间的安全通信。OpenSSL 和 SSH 都起源于 1995 年,OpenSSL 是一个加密工具包,而 SSH 是用于安全远程登录和数据传输的协议。OpenSSH 是 SSH 协议的开源实现,起源于 1999 年,为 SSH 提供了广泛使用的实现。


    OpenSSH 通常依赖于 OpenSSL。OpenSSH 使用 OpenSSL 库来实现加密和安全功能,包括加密通信、密钥生成、数字证书处理等。OpenSSL 提供了各种加密算法和密码学功能,使 OpenSSH 能够建立安全的 SSH 连接,并保护通信数据的机密性和完整性。在大多数情况下,安装 OpenSSH 时,系统会自动安装或链接到已经安装的 OpenSSL 库。这样,OpenSSH 就能够使用 OpenSSL 的功能来实现加密和安全性,而不必重新实现这些复杂的加密算法和协议。


    因此,可以说 OpenSSH 依赖于 OpenSSL,OpenSSL 提供了 OpenSSH 所需的加密和安全功能,使得 OpenSSH 成为一种安全、可靠的远程登录和数据传输工具。这些安全协议和工具对于保护通信和数据安全至关重要。


    实践


    对以上名词概念有了基本的了解之后,我们可以进行实践操作。如果感觉还是迷迷糊糊也不要紧,实践起来就会感觉简单多了。主要是对 OpenSSH 这个开源库提供的常用命令的使用。Mac 系统自带了这个工具所以不需要进行配置,而 iOS 系统上默认是没有安装这个工具的,包括越狱之后的 iOS 也没有,所以需要先下载安装这个工具。


    安装过程很简单,如下图所示,在 Cydia 上搜索 OpenSSH 下载并按照提示进行安装就好了。



    安装好之后,就可以在 Mac 上远程登录到越狱 iOS 了。iOS 系统默认提供了两个用户,一个是 root 用户,是 iOS 中最高权限的用户,我们在逆向开发过程中基本都是使用这个用户。还有一个是 mobile 用户,是普通权限用户,iOS 平时进行 APP 安装,卸载基本都是使用这个用户,但是我们在逆向开发中很少或者基本不会使用到这个用户,这里有个了解就够了。


    Cydia 首页有 OpenSSH 访问教程,这个文档详细的记载了如何从 Mac 远程登录到 iOS 设备上,并且也提供了修改默认密码的方法。建议英文不错的同学直接阅读这篇文档,不想看的就看我后面的介绍也可以。文档位置如下图所示



    通过默认账号密码登录到 iPhone


    ssh 提供了两种登录到服务器的方式,第一种是使用账号和密码。第二种是免密码登录。下面先介绍第一种

    1. 越狱 iPhone 在 Cydia 上安装 OpenSSH
    2. 确认 iPhone 和 Mac 电脑在同一个局域网下,在 Mac 打开终端,输入以下命令
      ssh root@iPhone的IP地址
      第一次连接会出现 Are you sure you want to continue connecting (yes/no/[fingerprint])? 提示,输入 yes 确认进行连接
    3. 输入默认的初始密码 alpine ,这里终端为了安全并不会显示密码的明文
    4. 之后就会看到终端切换到了 iPhone:~ root# 用户,代表成功登录到远程 iPhone 手机的 root 用户上了。这个时候,你在 Mac 终端输入的指令都会被发送到 iPhone 上,如下图 

       如果你觉得还不过瘾,可以输入 reboot 命令,体会一下远程操纵手机的快乐(重启之后,你可能需要重新越狱一下 iPhone 了😶)
    5. 输入 exit 退出登录

    刚刚我们登录的是 root 用户。在 iOS 中,除了 root 用户,还有一个 mobile 用户。其中 root 用户是 iOS 中最高权限的用户。mobile 是普通权限用户,其实平时越狱调试过程中,很少会使用这个 mobile 用户,这里只是介绍一下。


    能够成功登录 iPhone 之后,建议修改一下用户的默认密码,既然做逆向开发了,当然对安全也要注意一点。在登录 root 用户之后,输入:passwd 可以修改 root 用户的密码,输入 passwd mobile 可以修改 mobile 用户的密码。


    通过免密码方式登录到 iPhone


    OpenSSH 除了默认的账号密码登录的方式,还提供了免密码登录的方式。需要进一步完成一些配置才可以实现。服务器(在当前情况下,iPhone是服务器,Mac是客户端)的 ~/.ssh 目录下需要添加一个 authorized_keys 文件,里面记录可以免密登录的设备的公钥信息。当有客户端(Mac)登录的时候,服务器会查看 ~/.ssh/authorized_keys 文件中是否记录了当前登录的客户端的公钥信息,如果有就直接登录成功,没有就要求输入密码。所以我们要做的就是将 Mac 设备的公钥信息追加到 iPhone 的 authorized_keys 文件内容的最后面。追加是为了不影响其他的设备。完成这个操作需要先确保我们的 Mac 设备上已经有 ssh 生成的公钥文件。


    打开 Mac 终端,输入 ls ~/.ssh 查看是否已经存在 id_rsa.pub 公钥文件,.pub就是公钥文件的后缀




    如果没有看到公钥文件,需要使用 ssh-keygen 命令生成该文件。按回车键接受默认选项,或者根据需要输入新的文件名和密码。这将生成一个公钥文件(id_rsa.pub)和一个私钥文件(id_rsa)。


    使用 SSH 复制公钥到远程服务器。使用以下命令将本地计算机(Mac)上的公钥复制到远程服务器(iPhone)。请将user替换为您的远程服务器用户名,以及remote_server替换为服务器的域名或IP地址。

    ssh-copy-id user@remote_server



    在远程服务器(iPhone)上设置正确的权限。确保远程服务器上的~/.ssh文件夹权限设置为 700,并将~/.ssh/authorized_keys文件的权限设置为 600。这样可以确保SSH可以正确识别公钥并允许免密码登录。如下图所示:




    .ssh 文件夹前面的 drwx------ 是 Linux 和类 Unix 系统中表示文件或目录权限的一种格式。在这个格式中,每一组由10个字符组成,代表文件或目录的不同权限。让我们逐个解释这些字符的含义:




    所以,drwx------ 表示这是一个目录,并且具有以下权限:

    • 文件所有者具有读、写和执行权限。
    • 文件所有者所在组没有任何权限。
    • 其他用户没有任何权限。

    后面 9 个字符分为三组,每组从左至右如果有对应的权限就是421相加起来就是 7 后面都是0。所以 .ssh 文件夹的权限是正确的值 700,如果不是 700 的使用 chmod 700 .ssh 进行提权。authorized_keys 文件的权限是 rw 就是 420 相加起来就是 6 。后面都是 0,所以 authorized_keys 的权限也是正确的值 600。同样如果不是 600,使用 chmod 600 authorized_keys 命令修改权限。


    配置完成后,您现在可以使用 SSH 免密码登录到远程服务器(iPhone)。在 Mac 上,使用以下命令连接到远程服务器:

    ssh root@10.10.20.155

    这将直接连接到远程服务器,而无需输入密码。




    通过 USB 有线的方式登录到 iPhone


    配置为免密码登录之后,还可以进一步使用 USB 有线连接的方式登录到手机。如果你经常使用 WiFi 这种方式远程登录调试就会发现偶尔会碰到指令输入,响应卡顿,反应慢的情况,这样的体验显然让人感到不爽。所以,在大部分情况下,更推荐使用 USB 有线连接登录到 iPhone 上,这样使用的过程中,就像在本地输入命令操作一样流畅。


    iproxy 是一个用于端口转发的命令行工具。它通常用于在 iOS 设备和计算机之间建立端口映射,从而将 iOS 设备上运行的服务暴露到计算机上。这对于开发者来说非常有用,因为可以通过本地计算机访问 iOS 设备上运行的服务,而无需将服务部署到公共网络上。


    iproxyusbmuxd 的一部分,后者是一个用于连接和管理 iOS 设备的 USB 通信的守护进程。usbmuxd 允许通过 USB 连接与 iOS 设备进行通信,并且iproxy 则负责在本地计算机和iOS设备之间建立端口转发。


    通常,您可以在命令行中使用 iproxy 命令来建立端口转发,例如:

    iproxy local_port device_port

    其中,local_port 是本地计算机上的端口号,device_port 是 iOS 设备上的端口号。执行此命令后,iOS 设备上的服务将通过 device_port 映射到本地计算机上的 local_port


    请注意,使用 iproxy 需要先安装 libusbmuxd 包。在 macOS 上,您可以使用 Homebrew 来安装 libusbmuxd

    brew install libusbmuxd

    安装好之后,就可以使用 iproxy 命令了,使用 iproxy 将本机 10010 端口和 USB 设备的 22 端口进行映射的命令如下:

    iproxy 10010 22



    这里本机的端口 10010 可以设置为你想要的其他端口,但是不能是系统保留的端口(系统保留的端口有哪些,可以看百度的介绍)。端口转发设置完成之后,这个终端就不要关闭,也不要管它了,新建另一个端口进行 ssh 登录。此时,需要给 ssh 加上指定端口参数,命令如下:

    ssh -p 10010 root@localhost

    同样第一次使用这种方式建立连接会给出提示,输入 yes 确认




    之后,在 iPhone 设备上输入命令调试时,再也不会遇到卡顿,慢,延迟的现象啦。玩得开心~


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

    我只是用了个“笨”方法,一个月后不再惧怕英文文档

    在日常工作中,尤其是程序员时时刻刻都会与英文打交道,虽然我们尽可能的在互联网和中文书籍中寻找我们需要的信息,但是,有时候总是不尽人意。对待翻译过来的文档或者书本可能有些定义依然无法明确理解,回到它原有的场景中才能明白究竟是什么意思?阅读英文文档应该是我们的基本...
    继续阅读 »

    在日常工作中,尤其是程序员时时刻刻都会与英文打交道,虽然我们尽可能的在互联网和中文书籍中寻找我们需要的信息,但是,有时候总是不尽人意。对待翻译过来的文档或者书本可能有些定义依然无法明确理解,回到它原有的场景中才能明白究竟是什么意思?阅读英文文档应该是我们的基本技能。


    本文笔者将会从以下几个方面来分享一个月的时间,笨方法跨越英文障碍的落地方法:



    • 遇到的 Bug 百度没有结果怎么办

    • 中文翻译文献模棱两可怎么办

    • 寻找外文文献的发现过程是什么样的

    • 如何解决英文文档中的复杂句子

    • 如何结合文档学习语法

    • 结合实例带你操作“笨”方法找到适合自己的路


    在本文内,笔者想在这儿分享的一点工作体会。在标题中重点引号标注的一个字——“笨”。既然是“笨”方法,就说明我们都很容易理解,很容易上手,很容易操作,很容易见效。


    之所以没有说,两天速成,七天见效,因为这些似乎是赤脚医生行走江湖的招数。工作或者学习,还是务实一点比较能走长远。


    那么笔者的方法究竟“笨”在哪儿呢?展示几张图就很快明白了。



    1. 如果在出差的路上,或者咖啡馆,或者自习室,可以拿出英文资料,或者是需要查看的技术文档,或者需要学习的产品手册。将不能够准确理解的段落、句子、单词摘抄到随身携带的抄写本上,这样便于针对细节深入处理。


    9b292760-6c15-11ea-a6ec-45ee4806df26.jpeg


    (图片来自手机拍摄,不太美观)


    上图是笔者曾经在了解 JWT 的涉及到的实现机制的过程中,对于 Auth0 提供的 JWT 指南的阅读过程中留下的笔记。


    当再次看到笔记的时候对 JWT-handbook 第 16 页的部分内容就能够了然于心。


    那么我们在来看它对应的原始的手册中的内容如下:


    b0126510-6c15-11ea-848f-1ff50c7c2559.png


    (图片来自:jwt-handbook)


    通过这种方式,相信一定程度上就能感受到对于英文手册,技术指南的文档也能够看上一看,并获取到一些有意义的信息。



    1. 接着上面,如下的内容是笔者对于原文流程图下方的注解的学习过程。


    c00f90a0-6c15-11ea-8b2e-a93d3912f08f.jpeg


    通过剖析一张图和图对应的注解,就能够发现能够很轻松的干掉一篇 122 页技术手册中的其中一页,前前后后可能也不到几分钟的时间,如果基础比较好的情况下,也许几秒钟就解决了无法在各式各样的解读后的博客中看到的原始概念和流程。进而对关键的技术点有更加清晰的认识。



    1. 如果在家的情况下,有时候会觉得使用电脑打字的速度要比用手抄本写笔记的速度要快,你就可以像下图这样,在电脑主屏幕上用文本编辑器来记下笔记,用扩展显示器显示 pdf,这样不用切来切去,影响阅读速度。


    d5c4d0e0-6c15-11ea-98a4-2b16cffe68b2.jpeg



    1. 要说上面是笔者使用的“笨”方法, 那下面就可以说明,它“笨”的程度。笔者大概记下了这么多页的笔记,看完一遍 JWT handbookThe OAuth 2.0 Authorization Framework。这个过程大概花了一个月的空闲时间。所以如果你的英语基础更好的话,那就更不用担心了,连一个月的时间都用不了。


    1ff4d200-6c16-11ea-937f-590540467001.jpeg


    曾经多次看到全英文的技术指南,都只能看几句话就放弃,在阅读两篇英文技术文档之后,对于其他的文档来说,都会尝试拿来读一读,而且随着阅读量的增加速度和理解能力都会逐渐增强。


    上面分享完了笔者曾经的经历,便能直观的认识到,对于技术人员或者产品经理,应对工作中的英语非要先背诵大量的词汇,并不像想象的那样:买两本厚厚的语法书,啃上半年一年才能够掌握它。


    知识点来源于生活和工作,同样它又应用于生活和工作。就像我们的母语汉语一样,从咿呀学语到长篇大论,也并不是长篇大论之前一直在背诵汉字,学习句法。这需要一个过程,但是在这个过程中,随着知识的积累,在积累的过程中一定不耽误成长和认识新的事物。


    所以,我们不必恐惧我们遇到的其他语种的文献,它像我们学习汉语一样,只是有一个过程。相对于曾经考试过程中遇到的令人头疼的“完形填空”,“阅读理解”这些,兴许比我们看到的技术文档要更难,因为技术文档对于技术人来说,它所涉及的内容与我们工作的范畴是高度结合的。


    如果要学习 Spring Security,那么我们自然不会去看关于发表的“新冠肺炎”的最新文献。随着技术词汇的总结,我们便能够对于相关的技术文档更容易理解,更快速的阅读掌握。因为它首先在行业上就存在一定的专一性,这样就为我们学习圈定了范围。


    接下来,从以下几个方面来分享。


    遇到的 Bug 百度没有结果怎么办


    我们在工作中如果遇到的问题,经常会去百度一些问题,尤其是具体的 IDE 中的一些异常。例如,在 Eclipse 中有如下的错误提示:


    MongoDB shell version v3.4.5 connecting to: mongodb://127.0.0.1:27017 2020-0

    如果第一次遇见这个问题,无从下手的时候,可能首先会去百度,查找结果,如下:


    5dd80560-6c16-11ea-86a9-9df84f90745d.png


    如果得到的答案,没有我想要的,或者是解决不了我的问题的情况下怎么办呢?


    Google


    74a76830-6c16-11ea-a6ec-45ee4806df26.png


    不同的搜索引擎,由于它的搜索机制不一样,自然呈现给我们的检索结果就不一样,所以当无路可走的时候,常常会使用这几种方法综合起来,来搜寻符合需要的方案。


    必应


    有时候,我们无法访问谷歌的情况下,可以尝试使用必应的国际版。
    8581d460-6c16-11ea-a8b7-1d883493075c.png
    (图片来自:cn.bing.com


    输入我们之前的问题,得到如下结果:


    985662e0-6c16-11ea-8b2e-a93d3912f08f.png


    上面几种检索结果的区别对于我们来说有时候未必不是一件好事,因为它能够展现出更多的推荐方案供我们选择。


    Stack Overflow


    仔细的从谷歌和必应国际版的检索结果中会发现它得到的结果都有 Stack Overflow 提供的方案。那么 Stack Overflow 是何方神圣呢?可以把它看成是中国的 CSDN、博客园这一类的技术问答网站。


    如果无法登陆 Google,而且不习惯于 biying 的搜索引擎的情况下,那么可以直接去 Stack Overflow 查找或者询问你自己的问题。


    a8684ef0-6c16-11ea-98a4-2b16cffe68b2.png


    (图片来自:stackoverflow.com/


    MSDN


    如果是微软技术栈的工程师或者是产品经理,可以考虑使用 MSDN。


    bb1928d0-6c16-11ea-a3c8-9bd79d9bd149.png


    (图片来自:docs.microsoft.com


    cd755300-6c16-11ea-bd54-bd0372f44fbe.png


    (图片来自:docs.microsoft.com


    在使用 MSDN 的时候有一个优点,就是它支持中文,也支持英文。这样可能能够为我们阅读英文文档提供一个参照。如果英文的版本的确无法看下去的情况下,便可以参考中文的说明。


    其他


    当然不乏一些其他优秀的网站、论坛,比如日本的 Fresheye、俄罗斯的 Yandex、韩国的 NAVER,如下图:


    dd0bcd80-6c16-11ea-a3c8-9bd79d9bd149.png


    (图片来自:search.fresheye.com


    edcb37a0-6c16-11ea-98a4-2b16cffe68b2.png
    (图片来自:yandex.com


    fe7a9c30-6c16-11ea-a3c8-9bd79d9bd149.png


    (图片来自:search.naver.com/


    从这也能看出来,尽管我们是学习技术的,或者是产品部门的一个产品专员,但我们一样能够通过我们的努力打开世界上每一扇我们感兴趣的大门。


    接下来,说一说文章起始位置的第二个问题。


    中文翻译文献模棱两可怎么办


    对于这一点,我们引用 DAN OLSEN 的一本书的封面的内容来说明。如图:


    0f2322f0-6c17-11ea-86a9-9df84f90745d.png


    (图片来自:THE LEAN PRODUCT PLAYBOOK


    封面上能够得到的信息是:



    DAN OLSEN


    THE LEAN PRODUCT PLAYBOOK


    HOW TO INNOVATE WITH MINIMUM VIABLE PRODUCTS AND RAPID CUSTOMER FEEDBACK


    WILEY



    其中 DAN OLSEN 是作者、WILEY 是指这本书是由 WILEY 出版的。从领英上的介绍来看,这本书是 DAN OLSEN 是他很得意的一本畅销书。


    25d69950-6c17-11ea-96fb-af457604317b.png


    (图片来自:http://www.linkedin.com


    这也是笔者接触到的第一本专业的关于产品的书。通过它来印证我们遇到模棱两可的解释的时候,该如何处理。



    THE LEAN PRODUCT PLAYBOOK


    HOW TO INNOVATE WITH MINIMUM VIABLE PRODUCTS AND RAPID CUSTOMER FEEDBACK


    WILEY



    假设你不知道它的意思的情况下,分别在 Google 翻译和百度翻译上翻译的结果如下:


    3833fc00-6c17-11ea-98a4-2b16cffe68b2.png


    (图片来自:translate.google.cn


    465a6bc0-6c17-11ea-a8b7-1d883493075c.png


    (图片来自:fanyi.baidu.com


    通过对比就能够知道它的含义就是精益产品手册,对于 PLAYBOOK 的翻译结果稍有不同,如果深究的情况下, 我们可以去看一下 PLAYBOOK 的英文解释。


    5818a840-6c17-11ea-86a9-9df84f90745d.png


    (图片来自:dictionary.cambridge.org


    68a9af60-6c17-11ea-9561-3fe89f4fa56e.png


    (图片来自:http://www.merriam-webster.com


    774d1750-6c17-11ea-a3c8-9bd79d9bd149.png


    (图片来自:http://www.oxfordlearnersdictionaries.com


    相信对于你的感到模糊的词汇或者句子,在谷歌、百度、韦氏、剑桥、牛津五家线上词典的围攻下将你的疑问降低到零。


    这就是笔者在日常工作和学习中遇到的模棱两可的问题的解决方案。


    当时引起笔者注意的是如下这一段话:(引自:THE LEAN PRODUCT PLAYBOOK



    The Lean Product Process will guide you through the critical thinking steps required to achieve product-market fit. In the next chapter, I begin describing the details of the process, but before I do, I want to share an important hight-level concept: seperating problem space from solution space. I have been discussing this concept in my talks for years and am glad to see those terms used more frequently these days.


    Any product that you actually build exists in solution space,as do any product designs that you create-such as mockups,wireframes,or prototypes. Solution space includes any product or representation of a product that is used by or intended for use by a customer It is the opposite of a blank slate.When you build a product,you have chosen a specific implementation.Whether you've done so explicitly or not, you've determined how the product looks,what it does,and how it works.


    In contrast,there is no product or design that exists in problem space. Instead, problem space is where all the customer needs that you'd like your product to deliver live. You shouldn't interpret the word "needs" too narrowly: Whether it's a customer pain point, a desire, a job to be done, or a user story, it lives in problem space.



    如果我是一名产品经理或者产品专员,我想在这里我便应该仔细的去推敲 DAN OLSEN 说的这一句话中包含了些什么信息?


    什么是 problem space,什么是 solution space,这两个概念是它的范畴中包含了哪些元素,它们之间有什么关系,它们又是如何促进产品创新的。可以带着这些问题去别的章节中去寻找答案。


    另外,这里想要提醒的一点是,在国内我们是可以使用谷歌翻译的,它对应的域名是 translate.google.cn,而不是 translate.google.com,笔者曾经由于一直输入 .com 所以导致困惑了好多天。


    寻找外文文献的发现过程是什么样的


    白岩松老师在一个《对白》中曾经说过一句这样的话,笔者很赞同。“人找书是很难的,但是书找书是容易的,越读书,越知道该读什么书”。


    在工作过程中,我们如果需要找到一个问题的解决方案,可能需要在搜索过程中不断调整检索词,希望能够通过检索词来搜索出自己需要的有价值的内容。




    1. 在工作中,常常为了对项目结构有清晰的认知,需要我们能够画出“软件架构图” ,如果你搜索,“软件架构图”,检索后可以看到几乎第一屏展现出来的有一多半的都是实现软件架构的工具的广告。这往往是一件令人很头疼的事情。




    2. 调整检索词,改为“software architecture”,在百度中的结果会得到一些相对有用的信息。




    8d276bc0-6c17-11ea-9561-3fe89f4fa56e.png


    这时候标注出来的 PDF、读书、图文的字样便出现了。




    1. 再次细化检索词,使用“Software Architecture Patterns”。




    2. 通过查看能够进一步得到某一本书的信息:




    9c5c8c60-6c17-11ea-a8b7-1d883493075c.png


    (图片来自:http://www.oreilly.com/library


    这种情况下,就能够对这本书中的部分内容,查看是否是存在自己想要调研的相关内容,而后便能进一步去学习。



    1. 如果你需要查看这本书,继续在搜索引擎中搜索,比如“software architecture patterns pdf”,能够看到很多好心人分享出来的书,这种方法往往都比较有效,只要耐心的搜索检索词。


    aaf5e230-6c17-11ea-86a9-9df84f90745d.png



    1. 找到后打开对应的文件,浏览目录,这时候看到了它对架构师经常要打交道的”层图“的介绍,这就为我们平时的工作中的画图能够提供参考和建议。


    bde328d0-6c17-11ea-937f-590540467001.png


    (图片来自:Software Architecture Patterns


    这一部分,基本就介绍了,在我们对于英文文献的寻找过程是怎样的,输入中文检索词,如果中文检索词无法满足要求情况下,可以考虑将中文检索词转换为英文检索词,用转换的英文检索词,一步步优化,结合不同搜索引擎的检索结果去找到符合自己目标的内容。


    另外上面的例子中提到的 Software Architecture Patterns 篇幅比较小,也比较适合查看。同时架构图在软件设计过程中又比较重要,我们可以尝试通过这种方式的查找和学习、阅读,逐渐地对于架构图不会敬而远之,不会感觉遥不可及。


    有些情况下在某些书中,会对于一些概念在脚注或者引用文献都会进行说明,这样便可以在阅读过程中对于相关的概念,去其他的书中寻找,这就形成了一个书找书的过程,它是实现起来还是比较便捷有价值的。但是在阅读过程中想要提醒的一点是,我们不要过度发散,导致忘记了最初需要解决的问题,导致花费大量的时间,需要解决的问题没有解决。


    如何解决英文文档中的复杂句子


    在阅读过程中,我们可能会发现长句理解起来相对麻烦一点,这里我们拿上文中提到的 Software Architecture Patterns 的一个段落来解剖:



    Each layer in the architecture forms an abstraction around the work that needs to be done to satisfy a particular business request. For example, the presentation layer doesn’t need to know or worry about how to get customer data; it only needs to display that information on a screen in particular format. Similarly, the business layer doesn’t need to be concerned about how to format customer data for display on a screen or even where the customer data is coming from; it only needs to get the data from the persistence layer, perform business logic against the data (e.g., calculate values or aggregate data), and pass that information up to the presentation layer.



    文章无非就是通过这种类似的长短句形成的段落堆砌出来的,所以解决了词,解决了句子,自然没什么可怕的。看到这种段落的时候,可以试试去拆分:


    d0655190-6c17-11ea-8f43-edb1924172bb.png


    将一个段落拆分,按照标点符号拆分成四个部分。再对每一个部分进行分解,再把其中一句单独分离出来。


    比如第一句:


    de4346a0-6c17-11ea-a8b7-1d883493075c.png


    只要能够找到句子中的主语、谓语、宾语,基本上就能确定大概的意思。这类比于汉语中实际上,也比较容易理解,“谁”“干了”“什么”。其余的内容只是对这几个元素的修饰和补充说明。


    架构中的每一层,形成一个围绕工作的抽象,什么工作?需要被实现用来满足业务需求的工作。


    那么上一句就能够基本理解它的意思,架构中的每一层实际上是对于满足特定业务需要的抽象,再简化就是架构图中的层,是对于特定业务的抽象。


    回到原文中结合上下文,便能够对第一句进一步理解。


    而这时候,如果你不加分析,直接去 Google 翻译或者百度翻译上看它的翻译结果,可能会让你更加迷糊。


    ec0faf80-6c17-11ea-937f-590540467001.png


    f457bde0-6c17-11ea-a3c8-9bd79d9bd149.png


    从这个结果来看,我们在阅读过程中,所有的工具只能是参考,需要我们结合自己实际情况,不断补充,不断调查,不断积累和完善。


    如何结合文档学习语法


    从上文中的复杂句子的分解过程中,实际上就能看到语法的影子,如果只是在文档的学习中遇到的问题解决的情况下,如果无法正确理解的时候,从词性、时态、句型、从句等方面去针对性的查找。这儿就不再深入扩展了。


    可以查找关于高中、托福、雅思一类的专业英语语法书籍,比如在世界上颇有影响力的 Practical English Usage,它的中译本是《牛津英语用法指南》。


    笔者从书中摘抄一段内容,兴许这一小段就会纠正原本的错误认识:



    动词形式(”时态“)和时间


    动词形式和时间没有直接关系。例如,像 went 这样的动词过去式不但可以表示过去的事情(如 We went to Morocco last January 我们去年 1 月份去了摩洛哥),而且可以表示发生在现在或将来的不真实的或不确定的事情(如 It would be better if we went home how 我们现在回家更好)。动词现在式可以表示将来(如 I'm seeing Peter tomorrow 我明天要见彼得)。另外,进行式和完成式不单单表示时间有关的概念,还可表示动作的持续、完成或目前的重要性。


    (摘自:《牛津英语用法指南》)



    这就熟悉又陌生的语法,可能从高中毕业后再也没有系统的学习过过于英语语法的内容,但是没关系,只要用到就有机会学习。


    结合实例带你操作“笨”方法找到适合自己的路


    前面的内容,笔者分享了关于在工作中遇到的问题,查找、分解的方法。总的来说大概几个方面,包括了:



    • 搜索

    • 翻译

    • 找文献

    • 分解复杂句子

    • 查找语法知识


    希望这些能够对你的工作中遇到的头疼的英语问题有所帮助。


    下面推荐了相关的文档,你可以使用它,也可以按照前面介绍的方法结合自己遇到的实际问题去寻找对应的文献。


    研发的同学可以尝试去读一下,Windows 上 ZIP 版本 Redis 的三个文档:


    0407dae0-6c18-11ea-8b2e-a93d3912f08f.png


    (图片来自:github.com/



    • Redis on Windows Release Notes

    • Redis on Windows

    • Windows Service Documentation


    产品的同学,笔者目前只发现了上文提到的 DAN OLSEN 的 THE LEAN PRODUCT PLAYBOOK,有好资源的同学可以分享,共同学习。


    另外,闲暇时有兴趣的同学可以去浏览、了解日常的消息(国内可访问)



    13ff6120-6c18-11ea-86a9-9df84f90745d.png


    (图片来自:apnews.com/



    242a5370-6c18-11ea-937f-590540467001.png


    (图片来自: http://www.afp.com



    3db41b50-6c18-11ea-9561-3fe89f4fa56e.png


    (图片来自:globalnews.ca/


    这次的分享到这儿基本结束了,希望你可以用更快的,更简单的方法,去克服工作中遇到的问题。欢迎共同学习,携手共进。


    作者:问问计算机
    来源:juejin.cn/post/7149197829477662728
    收起阅读 »

    怎么去选择一个公司?

    一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。 那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。 企业文化和价值观 行业势头 工资待遇 公司规模 人才水平 企业文化和价值观 无...
    继续阅读 »

    一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。


    那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。



    • 企业文化和价值观

    • 行业势头

    • 工资待遇

    • 公司规模

    • 人才水平


    企业文化和价值观


    无法适应企业文化和价值观的员工,注定会被边缘化,获取不到资源,直到被淘汰。而适应企业文化和价值观的员工,在公司做事情则更能够得心应手。


    如何选择适合自己的企业文化和价值观


    如果你打算在一个公司长期发展,可以试着找找里面的熟人,聊聊公司内部的做事风格,比如晋升、奖金、淘汰、组内合作、跨部门合作以及如何处理各种意外情况等,这样就能实际感受到企业的文化和价值观了,然后再根据自己的标准,判断是否适合自己。


    行业势头


    行业一般会有风口期、黄金发展期和下降期三个阶段。



    • 处于下降趋势的行业要慎重考虑。

    • 处于风口期的行业发展趋势还不是很明显,如果你之前从事的行业和新的风口相关,那么不妨试试;如果你对这些风口背后的行业不是很熟悉,那不妨等风口的势头明朗了,再做打算。

    • 处于黄金发展期的行业发展已经稳定,有成熟的盈利模式,在这样的行业中积累经验,会在行业的发展上升期变得越来越值钱。如果你对这些行业感兴趣,不妨考虑相关的公司。


    工资待遇


    工资待遇不仅仅包括固定工资,还有一次性收入、奖金、股票以及各种福利等。


    很多新入职的员工会有一些的奖金,例如签字费、安家费等,这些是一次性的,有时还会附加”规定时间内不能离职”等约束条件。这部分钱的性价比比较低,但一般金额还不错。


    奖金主要看公司,操作空间很大,它和公司的经营状况关联紧密,谈Offer时约定的数额到后面不一定能够兑现,尤其是这两年整个互联网行业都不景气,很多公司的奖金都“打骨折”甚至直接取消了。


    其他福利一般包括商业医疗保险、年假、体检、补贴等,它和公司所在行业有关联,具有公司特色。


    股票也是待遇中很重要的一部分,很多公司在签Offer时会约定一定数量的股票,但是会分四年左右结清,这需要考虑你能坚持四年吗?四年之后没有股票要怎么办?


    公司规模


    如果待遇和岗位差不多,建议优先选择头部大公司,这样你可以学到更多的经验,接触更有挑战的业务场景,让自己成长的更快。


    如果你看好一个行业,那么需要努力进入这个行业的头部公司。


    人才水平


    一个公司的人才水平,决定了公司对人才的态度和公司内部合作与管理的风格。


    举个例子,如果一个公司里程序员的水平都很一般,那么这个公司就更倾向于不相信员工的技术能力,并制定非常细致和严格的管理规范和流程,避免员工犯错。如果你的水平高,就会被各种管理规范和流程束缚住。同时,如果你发现与你合作的人的水平都很“感人”,你也需要调整自己的风格,让自己的工作成果能够适应公司普遍的水平。




    此文章为极客时间3月份Day26学习笔记,内容来自《职场生存

    手册》课程。

    收起阅读 »

    iOS - 人脸识别

    iOS
    前言 最近公司提出了一个有趣的新需求,需要开发一个功能来自动识别用户前置摄像头中的人脸,并且能够对其进行截图。 话不多说,直接开整...技术点:AVCaptureSession:访问和控制设备的摄像头,并捕获实时的视频流。Vision:提供了强大的人脸识别和分...
    继续阅读 »

    前言


    最近公司提出了一个有趣的新需求,需要开发一个功能来自动识别用户前置摄像头中的人脸,并且能够对其进行截图。


    话不多说,直接开整...

    • 技术点:
    • AVCaptureSession:访问和控制设备的摄像头,并捕获实时的视频流。
    • Vision:提供了强大的人脸识别和分析功能,能够快速准确地检测和识别人脸。

    效果




    开始


    首先,工程中引入两个框架

    import Vision
    import AVFoundation

    接下来,我们需要确保应用程序具有访问设备摄像头的权限。先判断是否拥有权限,如果没有权限我们通知用户去获取

    let videoStatus = AVCaptureDevice.authorizationStatus(for: .video)
    switch videoStatus {
    case .authorized, .notDetermined:
    print("有权限、开始我们的业务")
    case .denied, .restricted:
    print("没有权限、提醒用户去开启权限")
    default:
    break
    }

    然后,我们需要对摄像头进行配置,包括确认前后置摄像头、处理视频分辨率、设置视频稳定模式、输出图像方向以及设置视频数据输出


    配置


    确认前后置摄像头:


    使用AVCaptureDevice类可以获取设备上的所有摄像头,并判断它们是前置摄像头还是后置摄像头

    // 获取所有视频设备
    let videoDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .unspecified).devices

    // 筛选前置摄像头和后置摄像头
    var frontCamera: AVCaptureDevice?
    var backCamera: AVCaptureDevice?

    for device in videoDevices {
    if device.position == .front {
    frontCamera = device
    } else if device.position == .back {
    backCamera = device
    }
    }

    // 根据需要选择前置或后置摄像头
    let cameraDevice = frontCamera ?? backCamera

    处理视频分辨率:


    可以通过设置AVCaptureSession的sessionPreset属性来选择适合的视频分辨率。常见的分辨率选项包括.high、.medium、.low等。

    let captureSession = AVCaptureSession()
    captureSession.sessionPreset = .high


    输出图像方向:


    可以通过设置AVCaptureVideoOrientation来指定输出图像的方向。通常,我们需要根据设备方向和界面方向进行调整。

    if let videoConnection = videoOutput.connection(with: .video) {
    if videoConnection.isVideoOrientationSupported {
    let currentDeviceOrientation = UIDevice.current.orientation
    var videoOrientation: AVCaptureVideoOrientation

    switch currentDeviceOrientation {
    case .portrait:
    videoOrientation = .portrait
    case .landscapeRight:
    videoOrientation = .landscapeLeft
    case .landscapeLeft:
    videoOrientation = .landscapeRight
    case .portraitUpsideDown:
    videoOrientation = .portraitUpsideDown
    default:
    videoOrientation = .portrait
    }

    videoConnection.videoOrientation = videoOrientation
    }
    }

    视频数据输出:


    可以使用AVCaptureVideoDataOutput来获取摄像头捕捉到的实时视频数据。首先,创建一个AVCaptureVideoDataOutput对象,并将其添加到AVCaptureSession中。然后,设置代理对象来接收视频数据回调。

    let videoOutput = AVCaptureVideoDataOutput()
    captureSession.addOutput(videoOutput)

    let videoOutputQueue = DispatchQueue(label: "VideoOutputQueue")
    videoOutput.setSampleBufferDelegate(self, queue: videoOutputQueue)

    视频处理、人脸验证


    接下来,我们将对视频进行处理,包括人脸验证和圈出人脸区域。我们将在AVCaptureVideoDataOutputSampleBufferDelegate 的代理方法中来实现这些功能

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {

    guard let bufferRef = CMSampleBufferGetImageBuffer(sampleBuffer) else {
    return
    }

    let detectFaceRequest = VNDetectFaceRectanglesRequest()
    let detectFaceRequestHandler = VNImageRequestHandler(cvPixelBuffer: bufferRef, options: [:])

    do {
    try detectFaceRequestHandler.perform([detectFaceRequest])
    guard let results = detectFaceRequest.results else {
    return
    }

    DispatchQueue.main.async { [weak self] in
    guard let self = self else {
    return
    }

    // 移除先前的人脸矩形
    for layer in self.layers {
    layer.removeFromSuperlayer()
    }
    self.layers.removeAll()

    for observation in results {
    let oldRect = observation.boundingBox
    let w = oldRect.size.width * self.view.frame.size.width
    let h = oldRect.size.height * self.view.frame.size.height
    let x = oldRect.origin.x * self.view.bounds.size.width
    let y = self.view.frame.size.height - (oldRect.origin.y * self.view.frame.size.height) - h

    // 添加矩形图层
    let layer = CALayer()
    layer.borderWidth = 2
    layer.cornerRadius = 3
    layer.borderColor = UIColor.orange.cgColor
    layer.frame = CGRect(x: x, y: y, width: w, height: h)

    self.layers.append(layer)
    }

    // 将矩形图层添加到视图的图层上
    for layer in self.layers {
    self.view.layer.addSublayer(layer)
    }
    }
    } catch {
    print("错误: \(error)")
    }
    }

    结尾


    识别单个人脸的时候没有太大问题,但是多个人脸位置不是很准确,有知道原因的小伙伴告知一下


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

    高斯模糊

    前言 通常,图像处理软件会提供"模糊"(blur)滤镜,使图片产生模糊的效果。 “模糊”的算法不只一种,高斯模糊只是其中一种,甚至它只是其中效率很差的一种。 在Android中使用高斯模糊,需要使用到 JNI 技术,Android Studio开发之 JNI...
    继续阅读 »

    前言


    通常,图像处理软件会提供"模糊"(blur)滤镜,使图片产生模糊的效果。



    “模糊”的算法不只一种,高斯模糊只是其中一种,甚至它只是其中效率很差的一种。


    在Android中使用高斯模糊,需要使用到 JNI 技术,Android Studio开发之 JNI 篇已具体讨论JNI的用法等。本文主要讲述高斯模糊原理及编码等。


    高斯模糊原理


    所谓"模糊",可以理解成每一个像素都取周边像素的平均值。



    如图所示,2是中间点,周围点都是1。中间点取周围点平均值,就会变成1。在数值上,这是一种"平滑化"。在图形上,就相当于产生"模糊"效果,"中间点"失去细节。


    显然,计算平均值时,取值范围越大,"模糊效果"越强烈。


    如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。


    高斯模糊根据正态分布,决定周围点的权重值。



    正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。


    计算平均值的时候,我们只需要将"中心点"作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。


    正态分布的一维公式为:



    由于每次计算都是以中间点为原点,所以u为标准差,即为0。所以公式进一步进化为:



    由于图像是二维的,需要根据二维正态分布函数来计算权重值,它的公式以及曲线如下:



    不过为了代码效率问题,不会采用二维正态分布的计算方式,而是分别对 X 轴和 Y 轴进行两次高斯模糊,也能达到效果(即通过一维正态分布计算权重)。


    高斯模糊代码


    先分别计算正态分布各参数,sigma与高斯模糊半径有关系,2.57既是1除以根号2 PI得来。

    float sigma = 1.0 * radius / 2.57;
    float deno = 1.0 / sigma * sqrt(2.0 * PI);
    float nume = -1.0 / (2.0 * sigma * sigma);

    因为对于每一个像素点来说,周围点在正态分布中所占的权重值都是一样的,所以正态分布计算一次即可。

    float *gaussMatrix = (float *) malloc(sizeof(float) * (radius + radius + 1));
    float gaussSum = 0.0;
    for (int i = 0, x = -radius; x <= radius; ++x, ++i) {
    float g = deno * exp(1.0 * nume * x * x);
    gaussMatrix[i] = g;
    gaussSum += g;
    }

    因为是以中间点自身为原点,所以 x 的取值范围是从 -radius 到 radius,计算结果存储的数组中。请注意周围点权重值与数组的对应关系,x 等于 -radius 时,而 i 等于0,后文会用到。


    由于并没有计算所有的周围点,所以权重总合必然不为1,所以需要归一化,设法使权重值为一。

    int len = radius + radius + 1;
    for (int i = 0; i < len; ++i) {
    gaussMatrix[i] /= gaussSum;
    }

    先进行 x 轴的模糊。

      for (int y = 0; y < h; ++y) {
    //取一行像素数据,注意像素总数组的访问方式是 x + y * w
    memcpy(rowData, pix + y * w, sizeof(int) * w);
    for (int x = 0; x < w; ++x) {
    float r = 0, g = 0, b = 0;
    gaussSum = 0;
    //以当前坐标点 x、y 为中心,查看前后一个模糊半径的周围点,根据正态分布
    //重新计算像素点的颜色值
    for (int i = -radius; i <= radius; ++i) {
    // k 表示周围点的真实坐标
    int k = x + i;
    // 边界上的像素点,它的周围点只有正常的一半,所以要保证 k 的取值范围
    if (k >= 0 && k <= w) {
    // 取到周围点的像素,并根据 argb 的排列方式,计算 r、g、b分量
    int color = rowData[k];
    int cr = (color & 0x00ff0000) >> 16;
    int cg = (color & 0x0000ff00) >> 8;
    int cb = (color & 0x000000ff);
    //真实点坐标为 k,与它对应的权重数组下标是 i + radius
    //前文中计算正态分布权重时已经说明相关的对应关系。
    //根据正态分布的权重关系,计算中心点的 r g b各分量
    int index = i + radius;
    r += cr * gaussMatrix[index];
    g += cg * gaussMatrix[index];
    b += cb * gaussMatrix[index];
    gaussSum += gaussMatrix[index];
    }
    }
    //因为边界点的存在,gaussSum值不一定为1,所以需要除以gaussSum,归一化。
    int cr = (int) (r / gaussSum);
    int cg = (int) (g / gaussSum);
    int cb = (int) (b / gaussSum);
    //根据权重值与各周围点像素相乘之和,得到新的中间点像素。
    pix[y * w + x] = cr << 16 | cg << 8 | cb | 0xff000000;
    }
    }

    y轴的模糊原理和x轴基本一样,这里就不再重复说明了。


    JNI图片接口


    JNI中处理图片,需要引用 bitmap.h,头文件中主要定义三个方法。

      int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
    AndroidBitmapInfo* info);
    int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
    int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);

    AndroidBitmap_getInfo:获取图片信息,比如宽、高、图片格式等
    AndroidBitmap_lockPixels:顾名思义,锁定像素
    AndroidBitmap_unlockPixels:解锁。


    AndroidBitmap_lockPixels 和 AndroidBitmap_unlockPixels 成对调用,在两个方法之间可对图片像素进行相应处理,解锁像素以后,对图片的调整效果可以立即看到,并不需要再重新生成图片了。


    ps:有时并不知道 JNI 有哪些接口可以调用,最好的方式就是看源码,有哪些接口,一目了然。


    其它模糊方法


    除了高斯模糊之外,还有其它模糊方法,比如说 fastblur,不过这个算法还没看明白,此处不再详述,具体代码本人的github上都有,欢迎访问。


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