注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

15岁女生的2022年年终总结|15岁啦,我也开始接触编程

高考上岸 2022年,15岁的我经历高考的磨砺,成功上岸。然而由于前期填志愿时我的迷茫再加上高考理科生的身份,因此收到录取通知书时我感到有些许疑惑但也有几分合理--自动化专业。自动化?什么是自动化?   接触编程 此前我只知道它属于工科专业,在后来...
继续阅读 »

高考上岸


2022年,15岁的我经历高考的磨砺,成功上岸。然而由于前期填志愿时我的迷茫再加上高考理科生的身份,因此收到录取通知书时我感到有些许疑惑但也有几分合理--自动化专业。自动化?什么是自动化?


 


疑惑.jpeg


接触编程


此前我只知道它属于工科专业,在后来进入学校后才突然发现居然有这么多带有“自动化”的专业—-机械制造与自动化专业、电气信息工程及其自动化专业等。但我是自动化专业,怎么和前面不一样?哎﹖而且我也确实没有看错,我学的专业确实只有三个字--“自动化”。也让我好奇,它到底是一个什么神奇的专业,居然还能"分装组合”?但在完全不了解的情况下,上网搜索发现,自动化在百度百科上是这样介绍的:自动化是中国普通高等学佼本科专业,主要学习电子技术、计算机技术、网络技术、软件技术、控制技术等知识,是一个多学科交叉的专业。自动化研究方向涉及到计算机科学与技术、信息与通信工程、人工智能、网络空间信息安全、电子科学与技术、微电子学、机械工程以及电气工程等多个学科领域,研究内容从传统的控制理论、工业控制系统到信息物理融合系统,以及计算机视觉、人工智能,自动驾驶,数据挖掘等。看了半天更加疑惑了,乍一看有点高端。冷静总结探究一下,其实说的通俗一点就是学科性质交融,需要学的多而杂。其实不用说,就能知道我们肯定不可避免的要具备一定编写程序的能力。于是15岁的我也开始接触编程了。


 


开心.jpeg


对智能小车开发的道路


后来又因为对于智能小车比较感兴趣,参加了学校的社团,进行了每周一次的培训。


 


 


组装.png


小车2.png


但由于原先没有一点点编程基础和电气知识,再加上社团培训时长较短。每节课听下来都让我吞咽困难,没法消化。于是便尝试去寻找和观看相关视频,但经常是云里雾里,还是太抽象了,没有实物也没有最基础的知识加持。慢慢的失去了信心和耐心,将他搁置在一旁,不想再理会。就在我快完全忘记它的时候,它再次闯入我的视野......一个周六的晚上,感觉无事可做便抱着玩一玩的态度,用自己浅薄的编程技术编写了一段程序。拿出放在抽屉深处的开发板,不抱希望的烧录进开发板后。却突然惊奇的看见上面成功点燃的正在缓慢闪烁的led灯,我有几分不知所措。等反应过来,我又赶紧改变参数,惊喜的发现小灯也随之变化着。我这才激动的快要跳起来,这何止点燃的是灯呀,点燃的是我内心的热情!!!

开发板.png


平静后我美滋滋的躺在椅子上,盯住着屏幕上的程序,却总感觉它是不够简洁也不方便更改。“要是可以很方便调整就好了”我是这样想的。这让我恍然想起来前几天C语言学习的知识,我拿出书才发现和屏幕上的程序有几分相似,于是想要试着改一改,但是毕竟有区别。果不其然,编译过后出现一堆错误。但是这次我耐住了性子,尝试着上网查找,过了许久终于发现一段相似的代码,经过对比更改。马上进行编译,正常!烧录程序,正常!!更改数据,正常! !!恍然间突然感觉一身轻,一种愉悦的心情让我难以表达。台灯在这个时候突然熄灭,周围陷入了黑暗和死寂静,转目看时间才发现原来早已进入深夜。但是我一点也不困,回想起来感觉总是许久没有去过社团培训了,再翻看上次学习小车的时间,居然已经是一个月前的事情!

如今看来那虽然只是短短的十几行程序,也许学过一些相关知识的伙伴也能轻松写出,但是它本身承载的太多太多。慢慢的我去图书馆翻阅相关的书,即便有相关的很多型号各异的开发板和书籍,我实在不懂。但后来我仍在慢慢探索。我想,愿意既然选择继续那就总是不算迟的。


 


数字.png


    2022快进入尾声,兔年将至,愿一切顺遂!

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

我有个气人的同事......

前段时间看到掘金上发了好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具...
继续阅读 »

前段时间看到掘金上发了好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码

// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)

import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)









































































配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本

// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本

// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。

// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`;
// ....

其中渐变色的玩法

myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符

this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己的库极具自己的特色。


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

你可能一直在kt文件中写Java代码

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。 得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的...
继续阅读 »

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。


得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的编程习惯,书写 Kotlin 代码。


但实际上 Kotlin 与 Java 之间编码风格还是有很大的差异的,你的代码可能还是 Java 的咖啡味。


空判断


你大概早就听腻了 Kotlin 的空安全,可是你在代码里是否还在写if (xx != null) 这样满是咖啡味的代码呢?


现在把你的空判断代码都删除掉吧。使用 ?. 安全调用来操作你的对象。

// before
fun authWechat() {
if (api != null) {
if (!api.isWXAppInstalled) {
ToastUtils.showErrorToast("您还未安装微信客户端")
return
}
val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"
api.sendReq(req)
}
}

这段代码粗略看没什么问题吧,判断 IWXAPI 实例是否存在,存在的话判断是否安装了微信,未安装就 toast 提示


但是更符合 Kotlin 味道的代码应该是这样的

// after
fun authWechat() {
api?.takeIf { it.isWXAppInstalled }?.let {
it.sendReq(
SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}
)
} ?: api?.run { ToastUtils.showErrorToast("您还未安装微信客户端") }
}

使用?.安全调用配合 ?: Elvis 表达式,可以覆盖全部的空判断场景,再配合 takeIf 函数,可以让你的代码更加易读(字面意思上的)


上述代码用文字表达其实就是:


可空对象?.takeIf{是否满足条件}?.let{不为空&满足条件时执行的代码块} ?: run { 为空|不满足条件执行的代码块 }


这样是不是更加符合语义呢?


作用域


还是上面的例子,实例化一个req对象

val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"

更有 Kotlin 味道的代码应该是:

SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}

使用apply{} 函数可以帮我们轻松的初始化对象,或者配置参数,它更好的组织了代码结构,明确了这个闭包处于某个对象的作用域内,所有的操作都是针对这个对象的。


在 Kotlin 的顶层函数中,提供了数个作用域函数,包括上文中的 let 函数,他们大同小异,具体的使用其实更多看编码风格的取舍,例如在我司我们有如下约定:




  • apply{} 用于,修改、配置对象




  • with(obj){} 用于,读取对象的字段,用于赋值给其他变量


    with() 可以显式的切换作用域,我们常将它用于某个大的闭包内,实现局部的作用域切换,


    而且仅用作读时无需考虑作用域的入参命名问题 (多个嵌套的作用域函数往往会带来it的冲突)




  • let{} 用于配合?.用于非空安全调用,安全调用对象的函数




  • run{} 执行代码块、对象映射


    run 函数是有返回值的,其返回值是 block块的最后一行,所以它具备对象映射的能力,即将当前作用域映射为另外的对象




  • also{} 对象,另作他用




当出现超过两行的同一对象使用,无论是读、写,我们就应该考虑使用作用域函数,规范组织我们的代码,使之更具有可读性。


这几个函数其实作用效果可以互相转换,故而这只关乎编码风格,而无关对错之分。


?: Elvis 表达式

非空赋值



虽然说在 Kotlin 中可空对象,使用 ?. 可以轻松的安全调用,但是有的时候我们需要一个默认值,这种情况我们就需要用到 ?: Elvis 表达式。


例如:

val name: String = getName() ?: "default"

假如 getName() 返回的是一个 String? 可空对象,当他为空时,通过 ?: Elvis 表达式直接给予一个默认值。


配合 takeIf{} 实现特殊的三元表达式


总所周知,kotlin 中没有三元表达式 条件 ? 真值 : 假值,这一点其实比较遗憾,可能是因为 ? 被用作了空表达。


在kotlin 中我们如果需要一个三元表达该怎么做呢?if 条件 真值 else 假值,这样看起来也很简洁明了。


还有一种比较特殊的情况,就是我们判断逻辑,实际上是这个对象是否满足什么条件,也就是说既要空判断,又要条件判断,返回的真值呢又是对象本身。


这种情况代码可能会是这样的:

fun getUser(): User? = null
fun useUser(user: User) {}
// 从一个函数中获得了可空对象
val _userNullable = getUser()
// 判断非空+条件,返回对象或者构造不符合条件的值
val user =  if (_userNullable != null && _userNullable.user == "admin") {
   _userNullable
} else {
   User("guess")
}
//使用对象
useUser(user)

这个语句如果我们将if-else塞到 useUser() 函数中作为三元也不是不可以,但是看起来就比较乱了,而且我们也不得不使用一个临时变量_userNullable


如果我们使用 ?: Elvis 表达式 配合 takeIf{} 可以看起来更为优雅的表达

fun getUser(): User? = null
fun useUser(user: User) {}
// 使用`?:` Elvis 表达式简化的写法
useUser(getUser()?.takeIf { it.user == "admin" } ?: User("guest"))

这看起来就像是一个特殊的三元 真值.takeIf(条件) ?: 假值,在这种语义表达下,使用?: Elvis 表达式起到了简化代码,清晰语义的作用。


提前返回


当然 ?: Elvis 表达式还有很多其他用途,例如代码块的提前返回

fun View.onClickLike(user: String?, isGroup: Boolean = false) = this.setOnClickListener {
user?.takeUnless { it.isEmpty() } ?: return@setOnClickListener
StatisticsUtils.onClickLike(this.context, user, isGroup)
}

这里我们对入参进行了非空判断与字符长度判断,在?: Elvis 表达式后提前 return 避免了后续代码被执行,这很优雅也更符合语义。


这里不是说不能用 if 判断,那样虽然可以实现相同效果,但是额外增加了一层代码块嵌套,看起来不够整洁明了。


这些应用本质上都是利用了 ?: Elvis 表达式的特性,即前者为空时,执行后者。


使用函数对象


很多时候我们的函数会被复用,或者作为参数传递,例如在 Android 一个点击事件的函数可能会被多次复用:

// before
btnA.setOnClickListener { sendEndCommand() }
btnB.setOnClickListener { sendEndCommand() }
btnC.setOnClickListener { sendEndCommand() }

例如这是三个不同帧布局中的三个结束按钮,他们对于的点击事件是同一个,这样写其实也没什么问题,但是他不够 Kotlin 味,我们可以进一步改写

btnA.setOnClickListener(::sendEndCommand)
btnB.setOnClickListener(::sendEndCommand)
btnC.setOnClickListener(::sendEndCommand)

使用 :: 双冒号,将函数作为函数对象直接传递给一个接收函数参数的函数(高阶函数),这对于大量使用高阶函数的链式调用场合更加清晰明了,也更加函数式


ps:这里需要注意函数签名要对应,例如setOnClickListener 的函数签名是View->Unit,故而我们要修改函数与之一致

@JvmOverloads
fun sendEndCommand(@Suppress("UNUSED_PARAMETER") v: View? = null) {

}

使用 KDoc


你还在用 BugKotlinDocument 这样的插件帮你生成函数注释么?你的函数注释看起来是这样的么?

/**
* 获取全部题目的正确率,x:题目序号,y:正确率数值(float)
* @param format ((quesNum: Int) -> String)? 格式化X轴label文字
* @param denominator Int 计算正确率使用的分母
* @return BarData?
*/

这样的注释看起来没什么问题,也能正确的定位到代码中的参数,但实际上这是 JavaDoc ,并不是 KDoc,KDoc使用的是类似 Markdown 语法,我们可以改写成这样:

/**
* 获取全部题目的正确率的BarData,其中,x:题目序号,y:正确率数值(float)。
* [format] 默认值为null,用于格式化X轴label文字,
* [denominator] 除数,作为计算正确率使用的分母,
* 返回值是直接可以用在BarChart中的[BarData]。
*/

KDoc 非常强大,你可以使用 ``` 在注释块中写示例代码,或者JSON格式


例如:

/**
* 使用json填充视图的默认实现,必须遵循下面的数据格式
* ```json
* [
* {"index":0,"answer":["对"]},
* {"index":1,"answer":["错"]},
* {"index":2,"answer":["对"]},
* ]
* ```
* [result] 必须是一个JSONArray字符串
*/

在AS中他会被折叠成非常美观的注释块:


image.png






写在最后


文章最后我们看一段 ”Java“ 代码与 Kotlin 代码的对比吧:

// before
override fun onResponse(
   call: Call,
   response: Response
) {
   val avatarPathResult = response.body()
   if (avatarPathResult != null) {
       val status = avatarPathResult.status
       if (status == 200) {
           val data = avatarPathResult.data
           MMKVUtils.saveAvatarPath(data)
      } else {
           MMKVUtils.saveAvatarPath("")
      }
  } else {
       MMKVUtils.saveAvatarPath("")
  }
}

// after
override fun onResponse(
   call: Call,
   response: Response,
) {
   with(response.body()) {
       MMKVUtils.saveAvatarPath(this?.data?.takeIf { status == 200 } ?: "")
  }
}

鉴于有些同学对本文的观点有一些疑惑,这里我贴上 JetBrains 官方开发的 Ktor 项目中对各种语法糖使用的统计(基于 main 分支,23-6-9)


语句计数
if.*!= null331
if.*== null216
.let {}1210
?.let {}441
.apply {}469
?.apply {}11
run {}37
with\(.*\) \{219
.also{}119
?:1066
?.1239
.takeIf54
\.takeIf.*\?:13
.takeUnless2


这个项目可以说很能代表 JetBrains 官方对 Kotlin 语法的一些看法与标准了吧,前文我们也说了,如何取舍只关乎编码风格,而无关对错之分。


用 Java 风格是错的吗?那自然不是,只是显然空判断与安全调用两者相比,安全调用更符合 Kotlin 的风格。


重复的写对象名是错误的么?自然也不是,只是使用 apply 更优雅更 Kotlin。


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

收起阅读 »

编译优化跌落神坛

最近在一次技术分享中,有网友问我小公司可以考虑做哪些编译优化?我觉得这个课题也还是挺有必要展开下讲讲的。 编译优化方面其实我个人觉得并不一定是特别高大上的东西,除了一些特别深水区的地方,还是有些东西还是能从细微处进行展开的。今天我们就尝试下拉他下水。 组件化 ...
继续阅读 »

最近在一次技术分享中,有网友问我小公司可以考虑做哪些编译优化?我觉得这个课题也还是挺有必要展开下讲讲的。


编译优化方面其实我个人觉得并不一定是特别高大上的东西,除了一些特别深水区的地方,还是有些东西还是能从细微处进行展开的。今天我们就尝试下拉他下水。


组件化


组件化和编译优化有啥关系? 有些人甚至觉得可能会拖慢整个工程编译速度吧。


在我的认知中gradle是一个并行编译的模式,所以当我们的工程的taskGraph确定之后,很多没有依赖关系的模块是可以并行编译的。这样的情况下我们就可以充分的使用并行编译的能力。


而且从另外一个角度gradle build cache出发,我们可以拥有更细致的buildcache。如果当前模块没有变更,那么在增量过程中也可以变得更快。


巧用DI或者SPI


以前我其实挺反感DI(依赖注入)的,我认为会大大的增加工程的复杂度。毕竟掌握一门DI(依赖注入)还是挺麻烦的。


但是最近我突然想通了,我之前看GE(Gradle Enterprise)的时候,发现有些业务模块之间有依赖关系,导致了这些模块的编译顺序必须在相对靠后的一个状态,但是因为com.android.application会依赖所有业务模块,这个时候就会触发水桶理论,该模块的编译就是整个水桶最短的短板。也让整个工程的并行编译有一段时间不可用。


那么这种问题我们可以通过DI(依赖注入)或者SPI(服务发现)的形式进行解决。模块并不直接依赖业务的实现而是依赖于业务的一个抽象接口进行编程,这样可以优化既有工程模块间的依赖关系。


道理虽然简单,但是真的去抽象也会考验对应开发的代码能力。


关注AGP优化方案


这部分我觉得是很容易被开发遗忘的,比如我们最近在做的buildConfig,AIDL编译时默认关闭,还有去年到今年一直在进行的非传递R文件的改造。官方的Configuration Cache,还有后续的全局AGP通用属性,还有默认关闭Jetfied等等,这些跟随AGP迭代的属性。


结果上看关闭非必要模块的buildConfig AIDL可以让全量编译时间缩短大概2min,当然主要是我们模块多。而非传递R可以让我们的工程的R文件变更的增量缓存更好管理。


Kotlin技术栈更新


kt已经发布很长时间了,最近最让我期待的是kt2.0带来的K2的release版本。能大大的提升kotlin compiler编译速度。


另外还有kt之前发布的ksp,是一个非常牛逼的kapt的替代方案。而且官方也在逐步对ksp进行支持。比如room这个框架就已经适配好了ksp。我自己也写过好几个ksp插件。我个人认为还是非常酷的。


最后还有一些废弃东西的下架,比如KAE(kotlin-android-extensions)这种已经被明确说是后续不继续进行支持的框架。我们最近尝试的方案是通过Android Lint把所有声明KAE的进行报错处理。剩下的就是业务自行决定是改成findViewById还是viewBinding



KAEDetector



花点钱接个GE


如果这个老板不太差这么点钱,我真的觉得GE(Gradle Enterprise)是个非常好的选择。可以很直观的看出一些工程的编译问题,而且对接的gradle同学也都很专业。


不要轻易魔改Gradle


非必要的情况下,个人是不太建议同学们改这个的。非常容易破坏整个编译缓存系统!这里不仅仅只针对Transform,还有对任意编译产物进行修改的,比如xml,资源等等。当然如果可以的话,尽可能的使用最新AGP提供的一些新的api去进行修改吧。这个可以多参考下2BAB大神的一些文章。


另外比如很多非必要的Transform转化建议本地编译的时候就直接关闭了,虽然可能会出现一些本地行为不一致的情况,但是可以大大的优化一些编译速度。字节码不是炫技的工具,谨记!


总结


最近基本没有做一些特别适合分享的内容,文章也就没有更新了。各位大佬们体谅啊,抱拳了。


在下封于修,前来讨教。既分生死,也决高下。


image.png


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

匿名内部类为什么泄漏,Lambda为什么不泄漏

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《# 内存泄漏大集结:安卓开发者不可错过的性能优...
继续阅读 »

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《# 内存泄漏大集结:安卓开发者不可错过的性能优化技巧》 中我大概进行了总结,最近在开发时遇到了一个问题,就是LeakCannry 检测到的内存泄漏,LeakCannry检测的原理大概就是GC 可达性算法实现的,我们产品中最多的一个问题就是匿名内部类导致的。


案例不涉及持有外部类引用的状态下


匿名内部类如何导致内存泄漏


在Java体系中,内部类有多种,最常见的就是静态内部类、匿名内部类,一般情况下,都推荐使用静态内部类,那这是为什么呢,先看一个例子:

public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();
}
}


匿名内部类的泄漏原因:内部类持有外部类的引用,上述场景中,当外部类销毁时,匿名内部类Runnable 会导致内存泄漏,



验证这个结论


上述代码的class 文件通过Javap -c 查看后是这样的

Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class Test$1
7: dup
8: invokespecial #4 // Method Test$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
}

我们直接看main 方法中的指令:

0: new #2 // 创建一个新的 Thread 对象 
3: dup // 复制栈顶的对象引用
4: new #3 // 创建一个匿名内部类 Test$1 的实例
7: dup // 复制栈顶的对象引用
8: invokespecial #4 // 调用匿名内部类 Test$1 的构造方法
11: invokespecial #5 // 调用 Thread 类的构造方法,传入匿名内部类对象
14: invokevirtual #6 // 调用 Thread 类的 start 方法,启动线程
17: return // 返回

我们可以看到,在第4步中 使用new 指令创建了一个Test$1的实例,并且在第8步中,通过invokespecial 指令调用匿名内部类的构造方法,这样一来生成的内部类就会持有外部类的引用,从而外部类不能回收,将导致内存泄漏。


Lambda为什么不泄漏


刚开始,我以为Lambda只是语法糖,不会有其他的作用,然而,哈哈 大家估计已经想到了,



匿名内部类使用Lambda 时不会造成内存泄漏。



看代码:

public class Test {
public static void main(String[] args) {
new Thread(() -> {

}).start();
}
}

将上面的代码改为Lambda 格式


class 文件:

Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
}

第一眼看上去就已经知道了答案,在这份字节码中没有生成内部类,


在Lambda格式中,没有生成内部类,而是直接使用invokedynamic 指令动态调用run方法,生成一个Runnable对象。再调用调用Thread类的构造方法,将生成的Runnable对象传入。从而避免了持有外部类的引用,也就避免了内存泄漏的发生。


在开发中,了解字节码知识还是非常有必要的,在关键时刻,我们查看字节码,确实能帮助自己解答一些疑惑,下面是常见的一些字节码指令


常见的字节码指令


Java 字节码指令是一组在 Java 虚拟机中执行的操作码,用于执行特定的计算、加载、存储、控制流等操作。以下是 Java 字节码指令的一些常见指令及其功能:



  1. 加载和存储指令:



  • aload:从局部变量表中加载引用类型到操作数栈。

  • astore:将引用类型存储到局部变量表中。

  • iload:从局部变量表中加载 int 类型到操作数栈。

  • istore:将 int 类型存储到局部变量表中。

  • fload:从局部变量表中加载 float 类型到操作数栈。

  • fstore:将 float 类型存储到局部变量表中。



  1. 算术和逻辑指令:



  • iadd:将栈顶两个 int 类型数值相加。

  • isub:将栈顶两个 int 类型数值相减。

  • imul:将栈顶两个 int 类型数值相乘。

  • idiv:将栈顶两个 int 类型数值相除。

  • iand:将栈顶两个 int 类型数值进行按位与操作。

  • ior:将栈顶两个 int 类型数值进行按位或操作。



  1. 类型转换指令:



  • i2l:将 int 类型转换为 long 类型。

  • l2i:将 long 类型转换为 int 类型。

  • f2d:将 float 类型转换为 double 类型。

  • d2i:将 double 类型转换为 int 类型。



  1. 控制流指令:



  • if_icmpeq:如果两个 int 类型数值相等,则跳转到指定位置。

  • goto:无条件跳转到指定位置。

  • tableswitch:根据索引值跳转到不同位置的指令。



  1. 方法调用和返回指令:



  • invokevirtual:调用实例方法。

  • invokestatic:调用静态方法。

  • invokeinterface:调用接口方法。

  • ireturn:从方法中返回 int 类型值。

  • invokedynamic: 运行时动态解析并绑定方法调用


详细的字节码指令列表和说明可参考 Java 虚拟机规范(Java Virtual Machine Specification)


总结


为了解决问题而储备知识,是最快的学习方式。


在开发中,也不要刻意去设计invokedynamic的代码,但是Java开发的同学,Lambda是必选项哦


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

分享Android开发中常见的代码优化方案

前言 首先要做相关优化,就得先要大致清晰影响性能的相关因素,这样可以做针对性调优会比较有条理。 比较常见的性能调优因素有: 内存:Java 一般通过 JVM 对内存进行分配管理,主要是用 JVM 中堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常...
继续阅读 »

前言


首先要做相关优化,就得先要大致清晰影响性能的相关因素,这样可以做针对性调优会比较有条理。


比较常见的性能调优因素有:



  • 内存:Java 一般通过 JVM 对内存进行分配管理,主要是用 JVM 中堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。

  • 异常:抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。

  • 网络:对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。

  • CPU: 复杂的计算,会长时间,频繁地占用cpu执行资源;例如:代码递归调用,JVM频繁GC以及多线程情况下切换资源都会导致CPU资源繁忙。


对以上这些因素可以在代码中做相关优化处理。


延迟加载(懒加载)优化


了解预加载



  • ViewPager控件有预加载机制,即默认情况下当前页面左右相邻页面会被加载,以便用户滑动切换到相邻界面时,更加顺畅的显示出来

  • 通过ViewPager的setOffscreenPageLimit(int limit)可设置预加载页面数量


介绍延迟加载


等页面UI展示给用户时,再加载该页面数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载部分,甚至更多页面数据。可提高所属Activity的初始化速度,另一方面也可以为用户节省流量.而这种延迟加载方案已经被诸多APP所采用。


相关概括



  • 没有打开页面,就不预加载数据,当页面可见时,才加载所需数据。

  • 换句话说延迟加载就是可见时才去请求数据。

  • 实际应用开发中有哪些延迟加载案例:

    • ViewPager+Fragment 搭配使用延迟加载

    • H5网页使用延迟加载




ViewPager与Fragment延迟加载的场景



  • ViewPager中setOffscreenPageLimit(int limit)部分源码
//默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

//缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {
//当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}

if (limit != mOffscreenPageLimit) {
//经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit,用于
mOffscreenPageLimit = limit;
populate();
}
}


  • 思路分析:Fragment中setUserVisibleHint(),此方法会在onCreateView()之前执行,当viewPager中fragment改变可见状态时也会调用,当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法,使用getUserVisibleHint() 可返回fragment是否可见状态。在onActivityCreated()及setUserVisibleHint()方法中都调一次lazyLoad() 方法。
public abstract class BaseMVPLazyFragment<T extends IBasePresenter> extends BaseMVPFragment<T> {
/**
* Fragment的View加载完毕的标记
*/
protected boolean isViewInitiated;
/**
* Fragment对用户可见的标记
*/
protected boolean isVisibleToUser;
/**
* 是否懒加载
*/
protected boolean isDataInitiated;

...

/**
* 第一步,改变isViewInitiated标记
* 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isViewInitiated标记为true,并调用lazyLoad()方法
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInitiated = true;
//只有Fragment onCreateView好了,
//另外这里调用一次lazyLoad()
prepareFetchData();
//lazyLoad();
}

/**
* 第二步
* 此方法会在onCreateView()之前执行
* 当viewPager中fragment改变可见状态时也会调用
* 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
prepareFetchData();
}

/**
* 第四步:定义抽象方法fetchData(),具体加载数据的工作,交给子类去完成
*/
public abstract void fetchData();

/**
* 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
* 第一种方法
* 调用懒加载,getUserVisibleHint()会返回是否可见状态
* 这是fragment实现懒加载的关键,只有fragment 可见才会调用onLazyLoad() 加载数据
*/
private void lazyLoad() {
if (getUserVisibleHint() && isViewInitiated && !isDataInitiated) {
fetchData();
isDataInitiated = true;
}
}

/**
* 第二种方法
* 调用懒加载
*/
public void prepareFetchData() {
prepareFetchData(false);
}

/**
* 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
*/
public void prepareFetchData(boolean forceUpdate) {
if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) {
fetchData();
isDataInitiated = true;
}
}
}

多线程优化: 建议使用线程池


用线程池的好处


可重用线程池中的线程,避免频繁地创建和销毁线程带来的性能消耗;有效控制线程的最大并发数量,防止线程过大导致抢占资源造成阻塞;可对线程进行有效管理



  • RxJava,RxAndroid,底层对线程池的封装管理非常值得参考。

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

IT入门深似海,入门到放弃你学废了嘛

我一直觉得IT行业 程序员行业。甚至觉得程序员人群 是一个特殊存在的群体。 入门到放弃,是真的,IT门槛高嘛。 其实吧,IT编程门槛,是有的,但是对于感兴趣的,想学习IT编程同学来说,也是一件容易事情其实。 我突然想讲一下我学编程的第一课,也是最难的。。。...
继续阅读 »

我一直觉得IT行业 程序员行业。甚至觉得程序员人群 是一个特殊存在的群体。



入门到放弃,是真的,IT门槛高嘛。



其实吧,IT编程门槛,是有的,但是对于感兴趣的,想学习IT编程同学来说,也是一件容易事情其实。


我突然想讲一下我学编程的第一课,也是最难的。。。。。最近又经常遇到这种问题


当然还有很多问题和。是巨坑是真坑。我来讲讲初学者在学习编程时候遇到的


拦路虎


环境配置


入门编程的第一课,惨痛的第一课。


做编程,做开发集成开发环境,IDEA是必不可少的。 我相信开始学习编程的同学,下定决心了会为了表示仪式感,会在买一个新电脑在电脑上花时间和功夫。。


哈哈哈哈哈(作为过来人告诉你没有这个必要,一个普通配置电脑就可以了,不用纠结那些,不然电脑有了,你会转移注意力,学习过程会对你信心有所打击)


我是做JAVA的 当然我现在是全栈 哈哈很高大上的样子,全栈,我时常沉浸在别人 一声声大佬声音中无法自拔。


其实我不是什么大佬,全栈,是工作环境需要。


说到正题 环境配置


环境配置,这一步就会劝退很多人。其实我到现在还深受,各种复杂环境配置的困扰。有莫名的恐惧症。初学第一步。各种开发工具安装,各种环境变量设置, 各种卸载,安装,报错。各种无法启动。启动了崩溃问题


最重要还是要有耐心,


新项目


在开发导入他人新项目时候,尤其在人家项目配置环境和你本地环境配置不一致的时候。 这个时候,其他人项目都可以轻松运行,你死活都运行不了。启动不了项目。


这个时候你就没有办法开发,也没办法了解项目。此时你就会很崩溃或者着急(😁)


明明你和别人一样的环境,一样操作你的不行人家的可以,。这时候没人帮的了你,只能靠你自己。


还有如果是那种比较老的旧的项目。(你见过屎山一样代码吗


你就是你运气不佳,可能你好不容易 运行起来了。装了一套环境自己弄好了,


第二天你再来他就不行了,或者你切换其他项目,其他项目环境坏了


测试上线


这个也是学习编程后面,后面工作让你头疼抓马地方,


本地环境正常,测试环境不正常。测试环境好不容易解决了问题。预发布环境正常。生产环境不正常。 我现在都信佛了


有时候确实怀疑人生,莫名奇妙项目,莫名奇妙代码。莫名奇妙bug


竟然莫名奇妙自己好了。我什么也没改。


然后第一天正常,第二天莫名奇妙无法运行了。


总之一天莫名其妙度过了。。。


尤其在你作为全栈。各种环境切换时候,更加酸爽。。。。


这些问题可能是初级程序员-----高级程序员。必回经历的。


精神疗法


当然这些问题其实并不可怕,可能作为程序员,还会一直伴随着你 职业生涯。。




遇到问题不可怕 ,一定要有耐心,耐心,耐心。今天无法解决




明天换一个心情继续。总会解决的。



以致于我现在有点病态,遇到问题我不解决。我会睡不着觉。。。。。。


睡在床上我也想着,这个问题该怎么解决。脑袋也无法休息。。。


如果这些问题,你都可以,那么你非常适合IT和开发。


大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题和困惑迷茫


我的故事


可以关注 程序员三时公众号 进行技术交流群讨论


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

2023—疫情、毕业、两次离职、失恋、遇到新的自己

就业之前 应该大三、大二或者大四的你都会有这么一个焦虑:该怎么去选择,考公务员、考研、还是就业? 我也曾经是这其中的一员,从大三上就开始陷入焦虑。 首先是排除了考公务员,因为家里的姐姐就是毕业2年仍然在考公,算是陷入其中,我自己也觉得应该不会好到哪...
继续阅读 »

就业之前


应该大三、大二或者大四的你都会有这么一个焦虑:该怎么去选择,考公务员、考研、还是就业?


我也曾经是这其中的一员,从大三上就开始陷入焦虑。


首先是排除了考公务员,因为家里的姐姐就是毕业2年仍然在考公,算是陷入其中,我自己也觉得应该不会好到哪儿去。


然后是考研,这是个纠结了很久的问题,甚至在2023年过年的时候,我仍然有想去考研的想法,但是多数是受到了旁边的人干扰。(当然能考到一个好的学校还是很好的),但是我自认是学渣,所以,最最多也就一个双非二本研究生,期间还得附上3-4年的时间,可能是读书太久了,所以最后选择了直接就业。


高考完选了大数据专业,当初选择大数据以为是新兴专业,而且在贵州,快毕业了才发现,所谓的大数据只有大公司才有,小公司基本就前端+后端这样的模式,甚至都是全干工程师。我是从2022年前开始学习的前端,6月暑假找的实习。其实我很佩服自己那半年的时间,从js到vue和小程序,期间在小破站上学习的视频还是蛮多的。


第一段实习 — 贵阳



早9晚5.30,双休,2.5k



2022年后,当时觉得自己身上有用不完的干劲,觉得毕业后非大厂莫属。当时学校一门课程就是做小程序+后台+后端,我和室友们做了一个关于项目管理的项目,期间也大概学会了git、接口对接、项目配合这些东西,后面项目也获得了学院的作品展示。现在看起来做的什么玩意儿啊,哈哈哈。在2月到4月那段时间就是学习+做项目,期间的收获很大。


到了五月开始投简历,也是我最焦虑的一段时间,因为带来的反差真的很大,背了很多八股文,信心满满的却得不到一点回复。有几次线上面试也都凉了,其中有一次鹅厂的实习,被按在地上摩擦。运气比较好的是得到了一家线下的面试(我和室友都进了),后面在20多个人中也是我俩拿下了前端实习的2个名额,2.5k。然后开始了合租之旅...


很清楚的记得租房的时候,被中介差点骗了300块,但是最后遇到一个很好的房东。去的第一天,根本睡不着,那个月也是疯狂爆痘。在公司的3个月其实学到了好些东西,因为之前没机会去接触这么大的项目,对于git和项目配合的理解更深了。而且在空余时间也有机会学习新的知识(ts、react等),合租的时候我的室友做饭我就洗碗(他做的饭真的好吃,就是口味重了点)就这样到10月,迎来了第一波疫情,很清楚的记得是在中秋节之前开始的,疫情的时候,每天想的是怎么买到菜,到快结束的时候,3个菜都是发的白萝卜,太残忍了。坚持到10月底疫情结束,由于疫情和公司接的政府的外包,贵阳的财政情况(懂的都懂,拖欠工资),所以我开始投简历,准备下一家,最后去了一家重庆的音乐公司(因为当时女朋友也是在重庆工作)。


第二段 — 重庆



995,3k



11月初,说走就走,当时前一天得到offer,后两天我就去了重庆,实习3.5k。刚到公司,就2个人!!一个淘宝运营,一个财务,还有一个老板和总监出差去了。如果不是用的vue3+ts,估计我当时就会走。就这样就开始做起了(还有一个实习的后端)。好处是有一个技术顾问,给了整体的框架技术的搭建建议。最后选择用vue3+ts+quasar搭建的后台管理系统。运气不好的是,又一波疫情来了,在家里居家办公了近1个月。那是最阴暗的一个月了,每天在房间里都是一个人,没有人可以说话,心情好的时候写一写代码,不好的就打游戏。当时一个人也想了很多,也有了想去考研的想法。所以在疫情结束的时候,1月初,离职了,准备回家过年。


现在 - 贵阳



6.5k+300补贴+住宿
大小周,早8.30晚5.30



在这2023年过年2个月的时间里,自己想通了很多,其实自己没有那么特别,就接受了自己的平凡,当时抱着试试的态度也投了一些沿海的城市。最后得到了贵阳目前这家公司的面试。很搞笑的记得当时顺道去重庆,拖着行李箱去面试的。最后得到offer:6.5k+300补贴+住宿。


面试的时候挺简单的,没什么太难的点。入职后做的原生小程序开发,业务倒是挺麻烦,对于组件的封装和代码规范是我目前觉得最值得学习的,对于原生好处就是,可以了解更多底层一点的东西,不用组件。坏处就是:开发速度会有所降低,样式也可能没有组件的好看。



感受:工作氛围挺好,非外包的项目,也挺清闲,大概有1/2 +的时间没事做,挺适合养老。
还有就是:遇到不会的别一个人死磕,多问问。




坏处:自控不好的话容易摆烂,周末空闲时间比较少



关于感情


我们是从高中一直到现在,因为异地+她忘不了从前喜欢的,在入职后几天,我提的分手。


说实话挺难受的,但是也没有必要在继续了。但是在这段情感中也学到了很多,成长了很多,懂得了自爱。


分手之后,感觉回归到了自由,也舍得给自己花钱了(从前我是很拮据的那种)。


3月,买了人生中第一台相机:尼康D750,后来也用它拍了很多照片。其实也可以用手机拍,但是觉得相机的意义就是可以多出去走走,还有女生好像对这个很感兴趣(哈哈哈),学会很加分。


5月,认识新的朋友,去了大理、丽江。虽然像是去踩坑的,但是,也学到了一点人像拍照的技巧。感谢同行小姐姐宽宏大量(我拍的贼丑)还鼓励我。



顺便给你们避避坑:


1.景区租服饰拍照的:其实不怎么专业,精修的图还没她们自己批的好看。


2.旅游之前做好攻略!!!(我就是当天决定当天走的)


3.丽江的 茶马古道 x ,日照金山√,玉龙雪山需要预约


4.大理的洱海√,基本上玩的都是环洱海



附上几组图片:


DSC_3061.JPG


DSC_3106.JPG


最后


马上毕业了,很庆幸自己能找到这份工作。随着工作的清闲,感觉自己变得闲鱼了,还是得支棱起来。
希望越来越好!


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

实习半年碎碎念+已裸辞+求职中~

时隔再半年,再次翻开之前记录的,怎么说,有点傻又有点可爱。之前记录在语雀中,作为个人的日记进行记录,如今想要分享出来给某些人一点点帮助吧。那些可能正在遭遇跟我一样经历情况的人吧! 友情提示:别轻易裸辞!想好最坏的结果你是否可以接收,如接下来可能近半年都找不到工...
继续阅读 »

时隔再半年,再次翻开之前记录的,怎么说,有点傻又有点可爱。之前记录在语雀中,作为个人的日记进行记录,如今想要分享出来给某些人一点点帮助吧。那些可能正在遭遇跟我一样经历情况的人吧!


友情提示:别轻易裸辞!想好最坏的结果你是否可以接收,如接下来可能近半年都找不到工作!


但辞了也不要一直去懊悔什么,我们还年轻,可以跌倒很多次,但不要跌得没有意义!我的意思是不要把时间浪费在懊悔自己,质疑自己上,有点蠢说真的

来浩鲸:2022年7月4日 --> 2023年1月15日。  
来福州:2022年6月29日 --> 2023年1月15日。总历时:200天!

“官方文档是最好的学习资料,但最好的不一定适合自己”


我不知道刚入门的小白是否跟我一样,总感觉官方文档像老太婆裹脚——又长又臭。明明按照步骤来了却根本没有正确跳进下一步。然后放弃官方文档去找网上找相关资料,学习博客或B站视频什么的都比这个强。


但是官方文档确实是最权威最全最面向大众的学习资料,为什么看不下去呢?是因为看文字不喜欢吗?但是我们有时候更情愿去看他人总结的学习博客。所以可能不是这个原因。官方文档一般是由该技术的创造者或创造团队编写而成,在尽可能用简短的语言向读者表达出该技术的便捷性,适用性。在这简短的语言中往往会渗透着本人的设计思路思路与丰富的经验。所以我们有时候在看官方文档的时候觉得晦涩难懂很正常。首先不要觉得自己很菜,这么基础都看不懂。很正常的哇,前辈所想所写的我们一下子就get到,那就不应该叫做我们的前辈了。但这个并不是意味着我们放弃看官方文档,选择去看他人的总结,因为这样代表我们可能失去了一个与前辈思想碰撞,交谈沟通的机会了。


怎样理解跟前辈思想碰撞和交谈沟通呢?这就涉及到下一点了。


那我们该怎么做呢?


尽力去看懂官方文档,在你时间和精力都有限的情况下。如果给你学习该技术的时间不够,精力有限,又或者你就是看不懂,那就不必勉强自己,看不懂就看不懂,网上总有学习视频,知识博客可供我们学习。我们不一定要走那条最短最难走的路,换条长路平坦点也是可以到达我们想要的罗马圣地。


请记住,一定不要勉强自己去看那些晦涩难懂的文档,千万不要!!不要觉得自己多看几遍就看懂了,当你投入过长的时间成本后,你会下意识觉得自己其实看懂了,学会了。那怎么才算真正学懂了呢?“能够传道受业解惑也”即代表你是真正学懂了。所以学会及时止损,看不懂没有什么可丢脸的。官方文档是最好的学习材料,但可能不是最适合自己的学习材料!【亲身感受】


“总结分享,贡献开源。最好的学习方式——费曼学习方式”


思想碰撞 :可以理解为我们去努力去get官方文档隐藏的前辈们的idea,如果get到了,那可以运用实践工作中,加工成自己的知识体系。前辈们历经多少项目积攒到的经验,可以帮助我们少走很多弯路,实现弯道大超越!


沟通交流 :可以理解为当我们遇到不理解的,或者有更独特的见解,可以在文档下方的开源社区中积极献言,一般都会得到前辈的回复,若是见解合理被采纳,你则是对开源社区有贡献的人,这样不仅在简历上是一个很大的加分项,甚至可以免费结交到一个超级牛逼的导师啦!


可能会有人认为:大佬写的怎么可能会有bug?还能跟大佬进行沟通交流?但其实不是这样的,大佬写的确实一般没有什么大bug,但是一个轻量便捷优秀的架构,肯定是基于众多基础的已知架构,谁能保证地基不会有更新迭代,楼宇会一直保持稳如泰山呢?架构不是设计完就稳定的不用去管了,它能够长期的发展其背后肯定需要大量的人力精力去维持。但人不可能24小时在线,任何一个程序都有潜在的bug漏洞。


你可能会觉得上述的思想碰撞与沟通交流实在是太过遥远,但不妨换个思路,作为读者的同时我们不能也当个作者呢?可是我们有这实力吗?有的,比如受众群体可以面向同样是刚步入职场的小白呀。而且更为重要的是,如果你能把知识体系系统完整的叙述出来,以老师讲课的方式向你读者介绍你的理解,这何尝不是复盘总结呢?且根据费曼学习来说,这种方法是最能检验你是否掌握了这门技术,且也是吸取知识最好的办法。正如古人所说:传道授业解惑也~


“实战”是最有效的捷径。


任何理论都要赋予给实践的。更何况是工科类的程序代码呢?尽管你做到上述的真正学懂了,但如你没有去实战演练,你还是不懂它实践的应用场景,且很多时候,你看几十遍都不如你去敲一遍来得印象深刻。甚至可能有些人做不到传道受业解惑也没事呀,只要知道什么时候用这个知识点且会用就好了哇。因此对比上述两点,第三点是更为最为重要的!!!一定一定要去实战!!刚入职的时候,有个为期两个月的实习配培训,因为要重新学一个框架,中间还涉及到其他语言技术,妄想通过看文档就能学完这个框架,然后后续看不下去就无意识地去边理解边敲。


要问最后有什么收获?我打字速度变快了,之前需要偶尔看键盘打字,现在完全不需要了。而所谓的知识入脑啊,都是左脑进右脑出了。后面在进行项目实战的时候,知识点完全忘光,基本从零开始,但发现边学边实践的效果比只看文档高效多了。虽然在开始磕磕绊绊又慢又痛苦,但却是在最短的时间内完成最有效的产出。


分享一种怪异学习方法 ——“向下学习”,其指的是打破常规思路,由易至难一步一步学习,而是在具备一定的基础知识后,再选中一种适宜的难度,开始由难至易学习。


“项目是螺旋式上升的,但对接是落地式交付”


在我见证我所参与的一个项目进行第n次需求改版封版后,我才发现一个大型企业项目并不是简单完成一次需求即可,其需要进行多版本的迭代更新,从收集市场需求,确定产品需求,设计产品架构,安排任务进度,设计产品图,后端搭建数据库,还原设计稿,核对测试,修复维护……整个过程都在进行螺旋式向上迭代。其中的每个人都在负责不同的模块,而模块间的耦合需要进行数次的对接。交付的闭合性,沟通的效率性,责任的明确性这些都在要求对接必须是是落地式直接对接!如何理解落地式呢?就是属于自己的职责不推脱,不属于自己的问题不背锅,事事有响必应,件件追踪落实。


“先设计构思再敲码,数据结构很重要”


在被独立扔到一个新的项目中独立开发模块的过程中,我深刻发觉理论永远要去联系实践,面对后端传来的众多接口参数中,如何高效地从数据库中检索出所需要的数据?又如何将已获取的数据进行不同页面的逻辑展示?数据处理是我最薄弱的一项。在刚刚开始面对海量数据时,只知道针对某模块的需求进行设计,并未考虑到其他页面中还有可能存在共用该接口的数据;只知道利用薄弱的已知数据处理方法对接口参数进行获取展示,并未考虑到如此设计会不会影响到产品的运转性能。一股脑投进去开始敲,并未在最初好好思考如此设计的意义在哪?还在大学学数据结构的朋友啊,一定一定要好好去学这门科目,他比高数离散啥的都要重要啊(因为它最难啊!!!)


“拒绝敏感,提高钝感力,“做”一个乐观向上的打工人”


在刚入职之前,其实很多哥哥姐姐就跟我说:要做一个乐观开朗的人,大家都喜欢这样的人。尽管你的性格可能不是这样的,但这是生存之道。实在做不到那么乐观开朗,那就做一个自信不卑不亢的人。


但可能是因为从小到大没有被骂过被批过,再加上这份实习是第一次步入社会,没有一点工作经验,基础也不够扎实,以至于在刚步入社会的两个月,我几乎每天都处于自我怀疑,精神内耗中。那时候家人朋友一直在开解,但他们越是开解自己越是想裸辞。我也知道在2022年,一个什么经验都不会的应届生,会在互联网这个行业中多难生存下来。但那时候总觉得还年轻,什么都没有的年纪就什么都输得起,然后找个借口请假两个星期跑到学校打算准备准备面经冲冲秋招,但因为人性的懒惰,又或者冠冕堂皇的说:两个星期准备秋招无疑说梦话,早点认清现实也不错?


不管你有什么理由,但在学校的两个星期,你确实毫无作为。后面你退缩了,最终还是老老实实回去上班。


很多人问我为什么要离职?我也问过自己好多遍好多遍,我可以说出无数的理由去说服别人跟我占在同一条战线,却说不出一个理由去遮盖实则懦弱逃避的心。


“多读书多看报,文笔不能没有锋利,语言不能没有力量”


细数,我好想自从上大学到现在,都没有好好得读一次书,更多的可能是在看小说,然后还是那种无脑的小说…


2023年,我希望自己可以真正的去读会书,无关是否有实在意义,但请可以给你带来力量。


2022年有两次独自奔溃又自愈时刻,第一次是在准备面试的时候,发现自己真的好菜好垃圾,临时抱佛脚背面经真的很痛苦,那天校园广播响起五月天的倔强,携带黄昏的余晖,带来的不甘心勇气。第二次是想裸辞却没有勇气,深夜的被窝,海底时光机唱《我爱你我的生活》,还夹杂着断断续续的猫猫哭泣声。


结语


这篇好长好长的碎碎念,写了很久很久,中间有好多次想放弃,甚至写到现在,我都不知道我到底在写什么。反正应该没人看莫得事嘿嘿!但最后还是希望可以写下,希望在这半年实习中留点什么。
我希望,当我明年再去看这篇实习回望的时候,会觉得又幼稚好笑,但又那么可爱!


而且在写这篇文章的过程中,我就抱着很矛盾的心理:我在写什么?为什么会那么拖拉?不能精简一点吗?咋都在重复?真的像口水账哇?写那么多你有做到吗?会说不会做吗?道理谁不懂,但是实际马上去出发的还有谁?写这些有用吗?有这时间为什么不去背背面经去实战呢?大家想听你碎碎念吗?

但是最后想了想,还是记录下来吧。记录此时此刻的心情状态,记录这实习半年多的感想感悟。


若能得到您的共勉,真好!


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

谈谈我的Google日历开发之路!

序言 介绍下自己,36岁,我是一名14年的Java服务器开发工程师。在小时候就有一个计算机的梦和愿景(小时候来在东莞,有一天家里人带在东莞长安步步高大道看步步高灯火通明,告诉我说那里面的人上班吹空调,工作只需要敲敲键盘就好了)-现在想来那时候他娘的就有996了...
继续阅读 »

序言


介绍下自己,36岁,我是一名14年的Java服务器开发工程师。在小时候就有一个计算机的梦和愿景(小时候来在东莞,有一天家里人带在东莞长安步步高大道看步步高灯火通明,告诉我说那里面的人上班吹空调,工作只需要敲敲键盘就好了)-现在想来那时候他娘的就有996了。从那时起这颗种子就埋下了。初中的时候还去报了电脑培训班,那里学会了DOS也学会了CS和热血传奇,同样也那以后自己也非常喜欢关注电脑,去新华书店看计算机相关的书。中间就不说了,说多了都是泪。终究如愿以偿自己还是走上了码农这条不归路-少壮不努力,老大搞IT。后面自己也是疯狂的一味的追求技术的宽度,深度。不管时C,C++,汇编,Js等语言(Java吃饭的家伙更不用说),还是Windows编程底层,网络编程,Linux核心翻了个底朝天(可是终究自己还是太年轻了,学的多懂得多与有没有工作之间差了可是一个NASA的哈勃空间望远镜的距离-没有好没学历,你说啥都没用)。转眼间自己已经是一个有着10多年经验的码农了,在这十多年期间,什么技术总监,经理,架构师咱都干过!但自己也是一直热衷于技术,从未放弃过,也知道自己喜欢一件事情想要放弃太困难,如果某一天谈到放弃这个词,心情会非常的艰难,甚至可能会哭的像个刚失恋的女孩一样!也曾有过自暴自弃,大部分的时间在撸游戏,放弃自己(因为在一个学历为王的时代,技术和能力显得那么的微不足道)。但奈何有一颗对技术钟爱的心,所以鞭抽着让自己一直在码代码这条路上一直走,不曾放下,也没有勇气去放下! 其实自己想过无数次,不走技术路还有其他路可走?多少个晚上都没有找到答案。但每每这样想后越发的发现自己更是无路可走,对未来的渺茫和害怕!也许正如《肖申克的救赎》所说-“体制化”真的很可怕,曾经尝试过问身边的人,现在的工作如果不干了会做什么,很多人的回答都是"去别的地方去干“,而事实上几乎不会再去想别的行业了,有的人甚至非常年轻。每个人都是自己的上帝,如果你自己都放弃自己了,还有谁会救你?每个人都在忙,有的忙着生,有的忙着死。忙着追逐名利的你,忙着柴米油盐的你,停下来想一秒:你的大脑,是不是已经被体制化了?你的上帝在哪里?


尝试着简单


很多时候也会三五好友聚在一起,在网吧开黑,在篮球场上畅快淋漓,他们之间有高中就闯荡江湖的,更有初中后就为生活奔波的。但在他们眼里看不到一丝的愁。在网吧肆意的笑声,球场上拼搏的样子,完全看不出一丝生活的不如意。或许是因为每个男人不得让别人看到自己的无助,也许总是要把好的一面留给朋友的每一次见面。生活谁都不易,可能是我要的太多了。但拿到篮球的那一刻是真的快乐了,发自内心的快乐。


的确该这样,我们应该追求自己初衷的东西。虽说爱好既不能饱腹,也不能裹体,更不能遮风挡雨,但每次我们在面对自己喜爱的事情时总是显得会更加自信。或许我该把自己的欲望都降低一点点,也许会变得更好。这里既没有肯定答案,更没有前人的经验来指导我。时间,人物,地点的变化都会让事物的变化都会让未来变得扑朔迷离。正如《复仇者联盟》 奇异博士预测未来和灭霸交手1400万次,只赢了一次那样。或者《萨利机长》 在计算机模拟成功降落的可能性,在避开所有的人为因素,时间因素,环境因素后。依旧模拟了17次后才成功的降落。与其每天活在幻想着如何成功,不如想清楚自己真的需要什么,也不必每天幻想能够去大厂挑战自我(或许这只是我想要更多的一个理由和借口,或许是最后的养老,至少大家都这么想的)但这估计比奇异博士打败灭霸1400万次的概率还要低吧。之后就是简单的过日子,柴米油盐酱醋茶,上班和下班,还有英雄联盟,绝地求生的陪伴。没有了远途的负担,生活和工作逐渐变得愈发的简单和平凡。


从10年到16年,这些年间一直在朋友的公司像青蛙跳荷叶游戏那样蹦来蹦去,估计整个职业生涯社保记录上的公司名称都能打全一张A4纸了。虽然很多时候自己也曾想过好好稳定,也有过稳定。也非常珍惜那一次稳定的机会。但奈何终究还是熬不过现实。或许大多数人和我一样,有些时候明明已经很努力了,工作也做得已经足够好了,可终究还是会留下很多的遗憾,却又无能为力。


突然间的醒悟


2016年,还是多少年来着。Google 公布了Plus关闭的消息。记得那时Google为了对抗Facebook ,Plus应运而生,他带着使命而来,可惜好景不长!很快就Google就不得已要关闭他了。而我却对Plus的钟爱有加,非常喜欢Plus的交互和体验,以及内容的呈现方式。对于Plus的关闭自己也很遗憾和惋惜,然后自己也萌生了一些想法-既然你关闭,我就开发一个出来自己玩! 这也许多年来第一次自己拿起了放下多年的自己了,以前忙着学技术,现在却想好好忙忙该如何做点东西了。


回到主题,话说plus关闭的日子越来越近,我也越来越迫不及待的要干一个plus出来。那年应该是16年吧,说干就干。虽说自己是服务器架构师,一直在Java这个世界里摸爬滚打,但一直相信技术是学来的,自己有这个学习能力去面对这些问题。而事实也是,我总能在工作中表现得游刃有余,任何一份工作都非常的顺利(所以几乎工作中没有过996,不管是研发还是管理)。在决定干plus,也用技术基础充分的证明了自己能做到!(刚好赶上16年从游戏公司离职)相信自己能做到、而后的日子就是每天像个屁股上长了钉子一样闷在房间里面研发Plus。时常也会玩游戏。花了接近1个半月的时间(平常还要玩游戏),做了3个版本,终于干出来了-这里为什么要做3个版本,因为每个版本做完后,发现体验和Google
plus体验相差悬殊,做完后总是不满意,所以为了保持一致,然后又重新开发。直到满意为止,最后却是让自己很满意!这种带来的成就感,结果却是让自己一发不可收拾,后面接连的把photos,mail也索性干了,sticky。曾经也有一段时间放在服务器上运行。终究发现这个东西水土不服,或者说就是我的一次心血来潮吧,运行了半年之后服务器不再续费,也随之不了了之!


2016年年中,回到了工作岗位中,这一次又是朋友的公司。在这里的2年,就再也没涉及过这事,安安静静的工作着。


日历转折点


2018年底呆了2年多的公司卖转手给了另一个老板。而接手的老板在接触几个月后发现志不同道不合不相为谋。因此决然选择了离开。 刚好赶上年底,大概还有1个月左右的时间过年,想着自己也能早点回家,年后也能好好休息下。在接下来没有上班的日历里,回首了这几年开发的Plus,mail,sticky,photos产品,总觉得自己像一个没有长大的小孩,总是对新鲜事物充满着好奇,总是在一次一次的尝试尝试的路上(这里倒不如换个词玩),而每次玩都没有一个真正的结果。功能和交互上都已经很完整了,但从细节和体验他们缺失的太多,同时这些似乎和用户本身的需求离的太远了,这些都是我自己异想天开的,再加上开发的东西觉得欠缺了太多的体验,似乎他们从一开始貌似就决定了他们的结束,我只是享受了这个作的过程-这不是典型的找罪受? 这是一个产品真正的痛。当然我能找一个让自己全身而退的理由-一个人前后端,一个人测试,一个人产品,一个人还要设计,我已经做的够多的了,自己不免会苦笑一下,承认自己的失败又如何。正如《绝望主妇》所说的:失败并不意味着你浪费了时间和生命,而是表明你有理由重新开始。而后再一次决定需要开发一个大家能真正意义上使用的产品,真的,真的,真的告诫自己,要做一件有头有尾的事情了-日程管理。之所以决定做日程管理,因为工作中发现自己每天都会用记事本记录自己的日程,工作任务。然后写完一本后,就会扔掉,总是觉得可惜和有一些遗憾,本想着用国内的一些平台,但VIP让我痛心疾首。于是更加坚定了自己开发一个日程平台来!之前也一直有用Google日历和mac上的苹果日历,也用过国内一些日程管理平台,思来想去还是日历是我的菜,所以索性决定自己干个日历出来!另一个理由是放眼看去这个国内市场和行业,一个日程管理软件,都是VIP,包月,包季,包年,甚至SVIP。免费的不好用,好用的不免费。


日历的开始之路


在仿苹果还是Google之间犯了难。后面索性选择一致好评度高和难度更高的Google。在别人的眼里的看来选择玩这个可能有点上头,终究会啪啪打脸!但自己总觉得这玩意没难度,是别人思想高度不够,还是自己高估自己了。带着疑问找了一些一直坚守前端朋友,结果答案一致的标准-有难度。也许是怕伤我自尊心吧!


心中的热血沸腾告诉自己是时候开始日历开发之旅了。第一次的尝试,先是在房间内把自己关了3天,没挪动过屁股。对整个架构进行了详细的分析和验证,证明了方法的可行性后,花了一个礼拜的时间做了第一个版本出来,只是些核心日程事件处理-当然目标也只是一些核心功能,日程渲染。动手做后发现了整个结构体系存在很多的问题,支撑不了日历的复杂交互以及在复杂交互中会产生很多事件的冲突带来极大体验麻烦。兵来将挡水来土掩,体系架构上的问题难不倒一个架构师(架构思想不管服务器还是视图页面都是一样的)。在重新花2天时间对其重新整理和分析后,新的方案随之出来了、心也更加的澎湃和坚定了! 接下来用15天证明了方案是没问题的,整个日历也是如期而至,功能上能满足日历的要求,但是在交互体验,动画效果,总是缺乏丝滑的效果。不过由于要回家过年了,也顾不上这些问题了。于是匆忙的部署在服务器上回家过年了。


年后(2019年)初又回到了找工作的路上。命还是幸运的,很快找到了一家硬件公司,智能穿戴行业,负责业务服务器这块。当时好几个offer,而这家硬件公司地方很偏,环境也不高大上,打动我的地方是因为上午面了2轮,在等和老板洽谈的时候到中午饭点了。而同仁却早已准备了午餐,水果和午睡床。告诉我说老板下午才有空,让我先等下!就这样被打动了,坚定了决心(上一次13年在华为面试架构师岗位也是这待遇,可惜学历这道硬伤)。本着对新工作的热情和做出一番业绩的渴望,身心很快投入到了工作中。同时日历变成了我的辅助,日程中的使用让我感觉到和Google的体验的差距。不过没关系,我已经不在乎日历了。 我更在乎的怎么做出更好的业绩出来。一番努力后在这里把一个人3个人的团队干到了30+,做出了非常多的业绩,很有成就感,公司氛围相当好!最满意的公司!可到后面大了之后政治斗争也越发严重,3年合同到期,被卸磨杀驴了!只能说无尽的不舍和不甘,但又无可奈何。 最后总结会发现当一个团队有一些没有能力干事的人混进来后就是要把干事的弄死!这就是俗话说解决不了问题就把提出问题的干掉道理一样吧!不过我走后这帮人陆续的在三个月的时间全部被干掉了,我只想说苍天饶过谁!


回到主题,既然肆业了,又想起了日历,其实在这3年的使用中发现了问题,但其实每次也都知道如何去解决。三年间它帮助我解决了非常多的问题,时间管理,日程管理。而后自己决定要开发在体验上能够极致的日历。决定之后,自己把自己锁在出租屋内,刚好又是疫情,一个月没出门对整个日历进行了重新架构设计,体验的交互,丝滑程度与架构有关。经过一个月在体验上的打磨!让它成为了一个真正可用的日历,从功能到到交互再到体验,终究算是了却了自己的一桩心事,算是这么多年唯一一次能交代自己了。 回头总结和看看过去有过的很多想法,很多次都能鼓起勇气尝试去做了,但事实上在有没有结果之间差着一个体验的距离,有体验即代表有结果。没有体验,即纯属到此一游,纵使无数次的开始和结束都将无济于事。


与大家分享下结果


日历导航
视图导航v1.gif


日程操作


日程编辑.gif


日历操作


日历编辑v2.gif


当然还有特别多的功能,修改创建日历,日历主题,订阅日历,日程协作共享,短信桌面通知提醒等。


由于录制屏幕的限制,很多体验无法一一说明,如有感兴趣的朋友希望能够帮助到您。本人还是将一如既往的凭一己之力尽可能提供更多的功能和体验。 后面自己将计划开发windows插件,用于嵌套在桌面上,更加简单灵活体验。


总结


做一件事路途可能很遥远,路途可能会迷路,甚至迷失自己,但坚持自己的初衷,我觉得总是会能达到的!生活中可能会有很多的欺骗和谎言,但始终我们的坚信自己!自己也会未来继续为免费这条路走的更宽和更远,同样提供极致体验。时间很短,也很长,我们可以一件事都不做,也可以做很多事。人生是一连串选择,都是一些常见选择题,最老套的选择就是当一个受害者-随波逐流; 或者选择反抗,也可以选择是忠诚,不论时局好坏。


一个人一台电脑,一个人设计,一个人撸代码,一个人测试,希望一个人能一直走下去。


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

98年菜鸡的碎碎念

最近几天参加了B站某位up主的模拟面试,被打击了。有点不开心,写个文章输出一下自己的情绪。 如果总结自己学编程的过程中犯过最大的错误是什么,那么肯定是迷之自信了,总以为自己学的够多了。虽然 “学的越多,不会的越多” 这个道理我也懂,但是我的内心想法竟然是,大...
继续阅读 »

最近几天参加了B站某位up主的模拟面试,被打击了。有点不开心,写个文章输出一下自己的情绪。



如果总结自己学编程的过程中犯过最大的错误是什么,那么肯定是迷之自信了,总以为自己学的够多了。虽然 “学的越多,不会的越多” 这个道理我也懂,但是我的内心想法竟然是,大家都是这样的,我与大部分同龄人相比起来,已经算好了。但是实际上,自己啥也不是。



印象比较深的主要有两件事


第一件事,20年春天找实习的时候


我高中不努力,老是去网吧玩,和同学相处也不愉快,就没有那种讨论问题的氛围。我大学只上了三本,当时还傻傻的以为计算机不看学历,只看能力。也许当时我刚读大学的时候,找工作是不看学历的,但是今年大家也知道了,普通本科211硕,都有很大概率直接简历挂。每次听群里的同学吐槽校招太难了的时候,都在想他们有学历都这么难,干脆自己躺平算了,就算努力也进不去大厂了。


读大学的时候,同学都不咋学习,也许大部分同学毕业后都不想当程序员吧。相比来说,只要我稍微比同学多学一点,就已经能在班里排名中上了。这就导致我有点迷之自信吧,哈哈哈,以为自己学的东西足够先去小公司锻炼锻炼了,所以大三到大四上学期握在寝室里和同学打LOL,有时候真的是直接打一天,现在想想也挺神奇的,竟然都不会腻。



到了大四的寒假,在家学完了一个培训班的实战项目,就觉得自己挺行了,写好简历后就打算找实习了,但是没想到疫情来了,虽然我现在也不清楚当年因为疫情少招了多少实习生。当时找工作真的找的我emo了,首先简历就很少能过的,就算简历过了,约面试了,我大部分都答不出来,当时对mysql的了解只是会crud而已,面试官问我表锁和行锁,我完全答不出来。说出来不怕大家笑,当时被打击很多次以后,怕自己找不到工作,还不争气的哭了。当时有一家外包公司,说他们找实习生没什么要求的,但是我当时期望薪资是4k,他们只能给2k,我就拒绝了,我大学里经常说,端盘子都有3k,2k的工作我是死也不会去的。但是过了3 4天,还是没找到工作,觉得自己也找不到工作了,所以厚着脸皮去问那个外包公司还愿不愿意要我,他说可以,就这样,2020年春天,我找了份2k的工作,一个月22.5个工作日,我工资一天100都没有。



第二件事,就是文章开头提的这几天参加了B站某位up主的模拟面试,被打击了。


大学毕业后,从外包公司去了一家150人左右的公司,相比之前外包公司来说已经好很多了。这家公司一点都不卷,有好处和坏处,好处就是没什么压力,坏处也是没什么压力,没有学习的氛围。我大学学的比较少,所以在部门里我也是学习劲头比较足的,部门里也会组织技术分享,我也会积极参与的。但是可能我效率低,过了一段时间回顾之前学了什么,总感觉没学啥。不过在这家公司也两年了,懂的肯定是比之前多了。



前段时间那个up主说可以免费模拟面试的时候,我第一时间就报名了,也是想检验一下自己的学习成果吧。不过我发给面试官的时候,他说,我的简历没啥好挖掘的的亮点,就问我一些通用的问题,虽然我也知道自己的简历很普通,但是直接被当面这样说,还是有点尴尬的。面试的过程中,因为我知道面试官会录屏发到b站,就有点紧张,很多都没答好。事后复盘了一下,我在想如果是我同事问我同样的问题,让我教他,那我肯定会回答的更好一些。反正面试的时候表现很差吧。。。其实一次面试失败没啥,可能是我太想表现的好了,表现不好了,就有点不好接受。甚至觉得自己是废物。当天面试完挺emo的,还好有女朋友陪我,这里也说明自己情绪管理是真的有点差的。这也是以后自己要成长的点。



说了这么多,还是要吸取教训,以后好好成长才行。





  1. 视野要宽阔一些,多和优秀程序员聊天,交流,发掘别人的优点找到自己的缺点




  2. 面试学习法:定时参加面试,通过面试发现自己的不足




  3. 以教代学:有机会的话多分享,还可以锻炼自己的表达能力,没有分享的机会就自己录音,然后自己听自己的录音,发现问题




  4. 参加开源or难度更高的项目,小公司里可能项目都比较简单,简历可能会对项目夸大一部分,但是这部分终究是假的,面试官很可能会发现。你想的一些技术难点,面试官会觉得你是背的八股文。




  5. 打算写一个系列的文章《假如面试官让你介绍一下XXX》系列,这次面试也发现了自己的问题,常见的面试题自己没有提前准备好答案,所以面试官问的时候,自己是从0开始组织语言,如果没讲好,面试官会觉得你没有逻辑。




最后,虽然我也在吐槽juejin的很多博客质量低,但是我自己也写了这种无聊的文章,emmmm,但是又想有个输出自己情绪的地方。我立正挨打。



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

Android全局的通知的弹窗

需求分析 如何创建一个全局通知的弹窗?如下图所示。 从手机顶部划入,短暂停留后,再从顶部划出。 首先需要明确的是: 1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activ...
继续阅读 »

需求分析


如何创建一个全局通知的弹窗?如下图所示。


image.png


从手机顶部划入,短暂停留后,再从顶部划出。


首先需要明确的是:

1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activity或Service,但是Dialog的弹出是需要当前页面的上下文Context的。


2、Dialog弹窗必须支持手势,用户在Dialog上向上滑时,Dialog需要退出,点击时可能需要处理点击事件。


一、Dialog的编写

/**
* 通知的自定义Dialog
*/
class NotificationDialog(context: Context, var title: String, var content: String) :
Dialog(context, R.style.dialog_notifacation_top) {

private var mListener: OnNotificationClick? = null
private var mStartY: Float = 0F
private var mView: View? = null
private var mHeight: Int? = 0

init {
mView = LayoutInflater.from(context).inflate(R.layout.common_layout_notifacation, null)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mView!!)
window?.setGravity(Gravity.TOP)
val layoutParams = window?.attributes
layoutParams?.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams?.height = ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams?.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
window?.attributes = layoutParams
window?.setWindowAnimations(R.style.dialog_animation)
//按空白处不能取消
setCanceledOnTouchOutside(false)
//初始化界面数据
initData()
}

private fun initData() {
val tvTitle = findViewById<TextView>(R.id.tv_title)
val tvContent = findViewById<TextView>(R.id.tv_content)
if (title.isNotEmpty()) {
tvTitle.text = title
}

if (content.isNotEmpty()) {
tvContent.text = content
}
}


override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (isOutOfBounds(event)) {
mStartY = event.y
}
}

MotionEvent.ACTION_UP -> {
if (mStartY > 0 && isOutOfBounds(event)) {
val moveY = event.y
if (abs(mStartY - moveY) >= 15) { //滑动超过20认定为滑动事件
//Dialog消失
} else { //认定为点击事件
//Dialog的点击事件
mListener?.onClick()
}
dismiss()
}
}
}
return false
}

/**
* 点击是否在范围外
*/
private fun isOutOfBounds(event: MotionEvent): Boolean {
val yValue = event.y
if (yValue > 0 && yValue <= (mHeight ?: (0 + 40))) {
return true
}
return false
}


private fun setDialogSize() {
mView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
mHeight = v?.height
}
}

/**
* 显示Dialog但是不会自动退出
*/
fun showDialog() {
if (!isShowing) {
show()
setDialogSize()
}
}

/**
* 显示Dialog,3000毫秒后自动退出
*/
fun showDialogAutoDismiss() {
if (!isShowing) {
show()
setDialogSize()
//延迟3000毫秒后自动消失
Handler(Looper.getMainLooper()).postDelayed({
if (isShowing) {
dismiss()
}
}, 3000L)
}
}

//处理通知的点击事件
fun setOnNotificationClickListener(listener: OnNotificationClick) {
mListener = listener
}

interface OnNotificationClick {
fun onClick()
}
}

Dialog的主题

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">

<style name="dialog_notifacation_top">
<item name="android:windowIsTranslucent">true</item>
<!--设置背景透明-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--设置dialog浮与activity上面-->
<item name="android:windowIsFloating">true</item>
<!--去掉背景模糊效果-->
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowNoTitle">true</item>
<!--去掉边框-->
<item name="android:windowFrame">@null</item>
</style>


<style name="dialog_animation" parent="@android:style/Animation.Dialog">
<!-- 进入时的动画 -->
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<!-- 退出时的动画 -->
<item name="android:windowExitAnimation">@anim/dialog_exit</item>
</style>

</resources>

Dialog的动画

<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="600"
android:fromYDelta="-100%p"
android:toYDelta="0%p" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromYDelta="0%p"
android:toYDelta="-100%p" />
</set>

Dialog的布局,通CardView包裹一下就有立体阴影的效果

<androidx.cardview.widget.CardView
android:id="@+id/cd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:cardCornerRadius="@dimen/size_15dp"
app:cardElevation="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="@dimen/font_14sp" android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/size_15dp"
android:textColor="#333"
android:textSize="@dimen/font_12sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title" />


</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

二、获取当前显示的Activity的弱引用

/**
* 前台Activity管理类
*/
class ForegroundActivityManager {

private var currentActivityWeakRef: WeakReference<Activity>? = null
private var mIsActive:Boolean = false

companion object {
val TAG = "ForegroundActivityManager"
private val instance = ForegroundActivityManager()

@JvmStatic
fun getInstance(): ForegroundActivityManager {
return instance
}
}


fun getCurrentActivity(): Activity? {
var currentActivity: Activity? = null
if (currentActivityWeakRef != null) {
currentActivity = currentActivityWeakRef?.get()
}
return currentActivity
}


fun setCurrentActivity(activity: Activity) {
currentActivityWeakRef = WeakReference(activity)
}

fun setActive(isActive:Boolean){
mIsActive = isActive
}

fun getActive():Boolean=mIsActive


}

监听所有Activity的生命周期,并判断当前页面是否可以显示Dialog,参考LiveData的源码,判断当前是否是Active状态,如果是Activie状态则可以显示Dialog,如果非Active状态则等待下次Active时显示Dialog

class AppLifecycleCallback:Application.ActivityLifecycleCallbacks {

companion object{
val TAG = "AppLifecycleCallback"
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
}

override fun onActivityStarted(activity: Activity) {
}

override fun onActivityResumed(activity: Activity) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
//设置当前Active状态为true
ForegroundActivityManager.getInstance().setActive(true)
}

override fun onActivityPaused(activity: Activity) {
//设置当前Active状态为false
ForegroundActivityManager.getInstance().setActive(false)
}

override fun onActivityStopped(activity: Activity) {
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
}

在Application中注册

//注册Activity生命周期
registerActivityLifecycleCallbacks(AppLifecycleCallback())

三、封装和使用

/**
* 通知的管理类
* example:
* //发系统通知
* NotificationControlManager.getInstance()?.notify("文件上传完成", "文件上传完成,请点击查看详情")
* //发应用内通知
* NotificationControlManager.getInstance()?.showNotificationDialog("文件上传完成","文件上传完成,请点击查看详情",
* object : NotificationControlManager.OnNotificationCallback {
* override fun onCallback() {
* Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
* }
* })
*/

class NotificationControlManager {

private var autoIncreament = AtomicInteger(1001)
private var contentMap = mutableListOf<NotificationInfo>()
private var dialogList = mutableListOf<NotificationDialog>()

companion object {
const val channelId = "app"
const val description = "my application"

@Volatile
private var sInstance: NotificationControlManager? = null

@JvmStatic
fun getInstance(): NotificationControlManager {
if (sInstance == null) {
synchronized(NotificationControlManager::class.java) {
if (sInstance == null) {
sInstance = NotificationControlManager()
}
}
}
return sInstance!!
}
}


/**
* 是否打开通知
*/
fun isOpenNotification(): Boolean {
val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
)
return notificationManager.areNotificationsEnabled()
}


/**
* 跳转到系统设置页面去打开通知,注意在这之前应该有个Dialog提醒用户
*/
fun openNotificationInSys() {
val context = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val intent: Intent = Intent()
try {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS

//8.0及以后版本使用这两个extra. >=API 26
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)

//5.0-7.1 使用这两个extra. <= API 25, >=API 21
intent.putExtra("app_package", context.packageName)
intent.putExtra("app_uid", context.applicationInfo.uid)

context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()

//其他低版本或者异常情况,走该节点。进入APP设置界面
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.putExtra("package", context.packageName)

//val uri = Uri.fromParts("package", packageName, null)
//intent.data = uri
context.startActivity(intent)
}
}

/**
* 发通知
* @param title 标题
* @param content 内容
* @param cls 通知点击后跳转的Activity,默认为null跳转到MainActivity
*/
fun notify(title: String, content: String, cls: Class<*>) {
val context = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val notificationManager =
context.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
val builder: Notification.Builder
val intent = Intent(context, cls)
val pendingIntent: PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel =
NotificationChannel(channelId, description, NotificationManager.IMPORTANCE_HIGH)
notificationChannel.enableLights(true);
notificationChannel.lightColor = Color.RED;
notificationChannel.enableVibration(true);
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
notificationManager.createNotificationChannel(notificationChannel)
builder = Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)
} else {
builder = Notification.Builder(context)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.jpush_notification_icon
)
)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)

}
notificationManager.notify(autoIncreament.incrementAndGet(), builder.build())
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param filterActivityByClassNameList 过滤哪些类不显示Dialog
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
filterActivityByClassNameList: MutableList<String>? = null,
listener: OnNotificationCallback? = null
) {
val currentActivity = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
//判断是否需要过滤页面不显示Dialog
filterActivityByClassNameList?.forEach {
val className = currentActivity.javaClass.simpleName
if (className == it) {
return
}
}

val isActive = ForegroundActivityManager.getInstance()?.getActive() ?: false
if (isActive) { //Active状态
val dialog = NotificationDialog(currentActivity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else { //如果当前Activity非Active状态则把要显示的内容存储到集合中
//存到集合中
contentMap.add(NotificationInfo(title, content))
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialogWithNotLifecycle(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* set dialog click
*/
private fun setDialogClick(
dialog: NotificationDialog?,
listener: OnNotificationCallback?
) {
if (listener != null) {
dialog?.setOnNotificationClickListener(object :
NotificationDialog.OnNotificationClick {
override fun onClick() = listener.onCallback()
})
}
}

/**
* 显示没有显示过的Dialog
*/
fun showDialogNeverVisible() {
if (contentMap.isNotEmpty()) {
val iterator = contentMap.iterator()
while (iterator.hasNext()) {
val info = iterator.next()
val currentActivity =
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val dialog =
NotificationDialog(currentActivity, info.title, info.content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
}
}
}

/**
* dismiss Dialog
*/
fun dismissDialogWithLifecycle() {
if (dialogList.size > 0) {
val iterator = dialogList.iterator()
while (iterator.hasNext()) {
val dialog = iterator.next()
if (dialog != null && dialog.isShowing) {
dialog.dismiss()
}
iterator.remove()
}
}
}


interface OnNotificationCallback {
fun onCallback()
}

}
//根据需求封装
data class NotificationInfo(var title:String,var content:String)

需要注意的点是:

1、Activity处于转场时是不能显示Dialog的,此时会回调onPause方法,isActive处于false状态(这一点参考LiveData的源码),将需要显示的数据存储于集合中,待BaseActivity回调onResume时显示没有显示的Dialog。

override fun onResume() {
super.onResume()
NotificationControlManager.getInstance()?.showDialogNeverVisible()
}

2、因为dialog是延迟关闭的,可能用户立刻退出Activity,导致延迟时间到时dialog退出时报错,解决办法可以在BaseActivity的onPause方法中尝试关闭Dialog:

override fun onPause() {
super.onPause()
NotificationControlManager.getInstance()?.dismissDialogWithLifecycle()
}

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

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf

web
常见 Node.js 版本管理器比较:nvm、Volta 和 asdf 随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本...
继续阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf


随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本管理器:nvm、voltaasdf 来帮助你为你的开发环境选择合适的版本管理器。


nvm


nvm(Node Version Manager)是最古老和最受欢迎的 Node.js 版本管理器之一,至今仍在积极维护。nvm 允许开发人员在一台机器上安装和管理多个版本的 Node.js。它还提供了一个方便的命令行界面,用于在可用版本之间切换。


nvm 的工作原理是将 Node.js 的每个版本安装到下的独立目录中。使用 nvm use 在版本之间切换时,它会更新环境变量以指向相应的目录。所以可以并行安装多个版本的 Node.js,并且每个版本都有自己的一组全局安装的软件包 ~/.nvm/versions/node/$PATH


nvm 的一个缺点是它只支持 Node.js。如果需要管理其他编程语言或工具,则需要使用单独的版本管理器。另外,nvm 需要手动安装和配置,这对于初学者来说可能有点难受。


要安装 nvm,可以使用以下命令:


curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# or

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

这将从官方 NVM GitHub 仓库下载并运行 NVM 安装脚本,该仓库将在你的系统上安装 NVM。安装完成后,你可以通过运行以下命令来验证是否已安装 NVM:


nvm --version

1.1 如何使用特定版本的 Node.js


要将特定版本的 Node.js 与 nvm 配合使用,你需要执行以下步骤:



  1. 列出可用的 Node.js 版本:要查看可以使用 nvm 安装的所有可用 Node.js 版本的列表,请运行以下命令:


nvm ls-remote


  1. 安装所需版本:要安装特定版本的 Node.js,例如版本 18,请使用以下命令:


nvm install 18


  1. 使用已安装的版本:安装所需的 Node.js 版本后,你可以通过运行以下命令来使用它:


nvm use 16

设置默认版本:如果要使用特定版本的 Node.js 默认情况下,可以使用以下命令将其设置为默认版本:


nvm alias default 18

Volta


volta 是一个较新的 Node.js 版本管理器,旨在简化 Node.js 和其他工具的安装和管理,在 2019 年出世,仍在积极开发中。Volta 采用了与 nvm 不同的方法:它不是管理 Node.js 的多个版本,而是管理项目及其依赖项。当你创建新项目时,volta 会自动检测所需的 Node.js 版本并为你安装它。


volta 还支持其他工具,如 Yarn 和 Rust,开箱即用(不仅仅是 Node.js!对于使用多种编程语言并需要单个工具来管理它们的开发人员来说,这使其成为一个不错的选择。与 nvm 一样,volta 提供了一个命令行界面,用于在 Node.js 版本之间切换,但它通过使用拦截对 node 可执行文件的调用的全局填充程序来实现。


要安装 volta,你可以使用以下命令:


curl https://get.volta.sh | bash

此命令将下载并执行安装 volta 的脚本。


如何使用特定版本的 Node.js



  1. 安装所需版本:安装 volta 后,你可以使用它创建一个新项目并使用 volta install 命令设置所需的 Node.js 版本,以下命令创建一个新项目并将所需的 Node.js 版本设置为 16.0.0:


volta install node@16.0.0


  1. 在该项目的上下文中运行命令:此命令使用所需版本的 Node.js 在项目的上下文中运行 app.js 文件。


volta run node app.js


  1. 切换版本:你也可以使用 Volta 在不同版本的 Node.js 之间切换。例如,要切换到版本 10.0.0,可以使用以下命令:


volta pin node@10.0.0


  1. 设置默认版本:最后,以下命令将你的环境切换为 Node.js 版本 16.0.0 并设置为 Node.js 的默认版本:


nvm alias default 16.0.0

volta 的一个潜在缺点是它仍然是一个相对较新的工具,因此它可能不像 nvm 那样经过实战测试,而且它的社区支持有限,插件和集成也较少。


ASDF


ASDF 是一个版本管理器,旨在成为“通用语言版本管理器”。在 2015 年出世,支持广泛的编程语言和工具,包括 Node.js。ASDF 设计为可扩展,因此可以轻松地添加新语言和工具的支持。


asdf 支持几种流行的编程语言,包括 Node.js,Ruby,Python,Elixir,Java,Rust,PHP,Perl,Haskell,R,Lua 和 Earlang。这意味着你可以在一个地方管理不同的语言版本!如果要在不同语言的项目之间切换,使 asdf 成为一个不错的选择。


与 volta 一样,ASDF 管理项目及其依赖项,而不是同一工具的多个版本。创建新项目时,asdf 会自动检测所需的 Node.js 版本并为你安装。asdf 提供了一个命令行界面,用于在 Node.js 版本以及其他工具之间切换。


asdf 的一个潜在缺点是它的设置可能比 nvm 或 volta 复杂一些。你需要安装多个插件来添加对不同语言和工具的支持,并且可能需要修改 shell 配置以正确使用 asdf。


下面是如何使用 ASDF 安装和使用特定 Node.js 版本的示例:



  1. 安装 ASDF:可以使用以下命令安装 ASDF:


brew install asdf


  1. 将 Node.js 插件添加到 ASDF:你必须安装插件才能将 Node.js 添加到你的项目中


asdf plugin add nodejs


  1. 安装 Node.js 版本 18:使用以下命令使用特定版本的 Node.js:


asdf install nodejs 18


  1. 使用特定版本:


asdf global nodejs 18

nvm,volta 和 asdf 之间的差异



  1. 目的: NVM,Volta 和 ASDF 有不同的用途。NVM 专注于管理多个版本的 Node.js。而 Volta 将 Node.js 版本管理和包管理结合在一个工具中。ASDF 是一个版本管理器,支持多种编程语言,包括 Node.js。

  2. 安装: NVM、Volta 和 ASDF 的安装过程不同。NVM 可以使用 curl 命令安装,而 Volta 要求你手动下载并安装它。ASDF 可以使用 Homebrew 等包管理器安装,也可以直接从 GitHub 下载。

  3. 配置: NVM、Volta 和 ASDF 的配置过程是不同的。NVM 要求你手动更新 shell 配置文件。Volta 不需要任何手动配置。ASDF 要求你手动设置所需的插件。

  4. 自动版本检测: Volta 是唯一通过读取项目的 package.json 文件自动检测项目所需的 Node.js 版本的版本管理器。

  5. 包管理: Volta 是唯一将 Node.js 版本管理和包管理结合在一个工具中的版本管理器。NVM 和 ASDF 仅管理 Node.js 版本。


相似之处



  1. 多节点.js版本: NVM、Volta 和 ASDF 都允许你在同一台机器上管理多个版本的 Node.js。

  2. 全局和本地 node.js 版本: 所有三个版本管理器都允许你全局或本地安装 Node.js 版本。

  3. 简单命令: NVM、Volta 和 ASDF 有简单的命令来管理 Node.js 版本。

  4. 兼容性: 所有三个版本管理器都与 macOS,Linux 和 Windows 操作系统兼容。

  5. 开源: NVM、Volta 和 ASDF 都是开源项目,这意味着它们可以免费使用,并且可以由社区贡献。

  6. 版本锁定: 所有三个版本管理器都允许你锁定特定项目的 Node.js 版本,确保所有团队成员使用相同的版本。


小结


总之,nvmVoltaasdf 都是很棒的 Node.js 版本管理器,可以帮助你更改,管理和更新 Node.js 的多个版本,还可以与新版本保持同步,包括 LTS 版本。Nvm 是最古老和最受欢迎的版本管理器之一,Volta 有不同的方法,它不是管理多个版本的 Node.js,而是管理项目及其依赖项,最后 asdf 管理不同的语言版本,如果你在使用不同语言的项目之间切换,这使得 asdf

作者:夏安君
来源:juejin.cn/post/7247543825535270968
是一个不错的选择。

收起阅读 »

35岁愿你我皆向阳而生

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。 当我在20多岁研究生刚刚毕业的时候,恰逢互...
继续阅读 »

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。


当我在20多岁研究生刚刚毕业的时候,恰逢互联网蓬勃发展,到处都是机会,那时候年轻气盛,我充满了能量和热情。我渴望学习新的技术和承担具有挑战性的项目。我花费了无数的时间编码、测试和调试,常常为了追求事业目标而牺牲了个人生活。


慢慢的当我接近30岁的时候,我开始意识到我的优先事项正在转变。我在职业生涯中取得了很多成就,但我也想专注于我的个人生活和关系。我想旅行、和亲人朋友共度时光,并追求曾经被工作忽视的爱好。


现在,35岁的我发现自己处于一个独特的位置。我在自己的领域中获得了丰富的经验,受到同事和同行的尊重。然而,我也感到一种不安和渴望尝试新事物的愿望。这时候我很少在代码上花费时间,而是更多的时间花到了项目管理上,一切似乎很好,但是疫情这几年,行业了很大的影响,公司的运营也变得步履维艰,在安静的会常常想到未来的的规划。


一、焦虑情绪的来源


35岁程序员的焦虑情绪源于其所处的行业环境。技术不断发展,新的编程语言、框架、工具层出不穷,要跟上这些变化需要付出大量的时间和精力。此外,随着年龄的增长,身体和心理健康也会面临各种问题。这些因素加在一起,让35岁程序员感到无从下手,不知道该如何面对未来。


二、面对焦虑情绪的方法


1学习新技能


学习新技能是应对技术革新的必经之路。与其等待公司提供培训或者等待机会,35岁程序员应该主动寻找新技术,并投入时间和精力去学习。通过参加课程、阅读文献,甚至是找到一位 mentor,35岁程序员可以更快地适应新技术,保持竞争力。


2关注行业动态


35岁程序员要时刻关注技术行业的最新动态。阅读技术博客、参加社区活动,以及了解公司的发展方向和战略规划,这些都是成为行业领跑者所必须的。通过增强对行业趋势的了解,35岁程序员可以更好地做出决策,同时也可以通过分享经验获得他人的认可和支持。


3 与年轻人合作


与年轻的程序员合作可以带来许多好处。他们可能拥有更新的知识和技能,并且乐于探索新事物。35岁的程序员应该通过与年轻人合作,学习他们的思考方式和方法论。这样不仅可以加速学习新技能,还可以提高自己的领导能力。


每周我都会组织公司内部的技术交流活动,并积极号召大家发表文章,通过这些技术分享,我发现每个人擅长的东西不同,交流下来大家的收获都很大。


4重新审视个人价值观


在35岁之后,程序员可能会重新审视自己的职业生涯和个人发展方向。当面临焦虑情绪时,建议去回顾一下自己的愿景和目标。这有助于确定下一步的工作方向和计划。此外,35岁程序员也应该考虑个人的非技术技能,例如领导力、沟通能力和团队合作精神,这些技能对长期职业成功至关重要。


5 敞开心扉学会沟通


 程序员给大家的一个刻板印象就是不爱沟通,刻板木讷,大家都是干活的好手,但是一道人际关系处理上就显得有些不够灵活,保持竞争力的一个很关键的点也在于多沟通,多社交,让自己显得更有价值,有一句老话说的好:多一个朋友,多一条路。沟通需要技巧,交朋友更是,这也是我们需要学习的。


三、总结


35岁是程序员生涯中的一个重要节点,同时也是一个充满挑战和机会的时期。如何应对焦虑情绪,保持竞争力并保持个人发展的连续性,这需要程序员深入思考自己的职业规划和发展方向。


通过学习新技能、关注行业动态、与年轻人合作以及审视个人价值观,35岁程序员可以在未来的职业生涯中不断成长和发展。


归根到底,无论如何生活的好与坏都在于我们对待生活的态度,幸福是一种感受,相由心生,无论你处于何种生活状态,

作者:mikezhu
来源:juejin.cn/post/7246778558248632378
都希望大家向阳而生。

收起阅读 »

句柄是什么?一文带你了解!

今天又学习了一个装X概念——句柄,看字面意思,感觉跟某种器具有关,但实际上,这个词可不是用来打造家居用品的。 相信不少人跟我一样,第一眼看到这个词,脑海里只有一个大大的问号。不过,没关系,我们可以一起学习,毕竟,装X路上永远没有止境。 1、官方一点儿的定义 在...
继续阅读 »

今天又学习了一个装X概念——句柄,看字面意思,感觉跟某种器具有关,但实际上,这个词可不是用来打造家居用品的。


相信不少人跟我一样,第一眼看到这个词,脑海里只有一个大大的问号。不过,没关系,我们可以一起学习,毕竟,装X路上永远没有止境。


1、官方一点儿的定义


在计算机科学中,句柄(Handle)是一种引用或标识对象的方式,它可以用来访问或操作底层系统资源。


不同的操作系统可能会有不同的实现和用途,下面我将以不同的操作系统为例来解释句柄的意义。


1. Windows操作系统


在 Windows 中,句柄是一种整数值,用于标识和访问系统对象或资源,如窗口、文件、设备等。


句柄充当了对象的唯一标识符,通过句柄可以对对象进行操作和管理。


示例代码(C++):


HWND hWnd = CreateWindow(L"Button", L"Click Me", WS_VISIBLE | WS_CHILD, 10, 10, 100, 30, hWndParent, NULL, hInstance, NULL);
if (hWnd != NULL) {
// 使用句柄操作窗口对象
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
// ...
}

在上述代码中,通过 CreateWindow 函数创建一个按钮窗口,并将返回的句柄存储在 hWnd 变量中。然后,可以使用 hWnd 句柄来显示窗口、更新窗口等操作。


2. Linux操作系统


在 Linux 中,句柄通常称为文件描述符(File Descriptor),它是一个非负整数,用于标识打开的文件、设备、管道等。


Linux将所有的I/O操作都抽象为文件,并使用文件描述符来引用和操作这些文件。


示例代码(C):


int fd = open("file.txt", O_RDONLY);
if (fd != -1) {
// 使用文件描述符读取文件内容
char buffer[1024];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
// ...
close(fd);
}

上述代码中,通过 open 函数打开文件 file.txt,并将返回的文件描述符存储在 fd 变量中。然后,可以使用 fd 文件描述符来进行文件读取等操作。


3. macOS操作系统


在 macOS 中,句柄也称为文件描述符(File Descriptor),类似于 Linux 操作系统的文件描述符。它是一个整数,用于标识和访问打开的文件、设备等。


示例代码(Objective-C):


int fileDescriptor = open("/path/to/file.txt", O_RDONLY);
if (fileDescriptor != -1) {
// 使用文件描述符读取文件内容
char buffer[1024];
ssize_t bytesRead = read(fileDescriptor, buffer, sizeof(buffer));
// ...
close(fileDescriptor);
}

在上述代码中,通过 open 函数打开文件 /path/to/file.txt,并将返回的文件描述符存储在 fileDescriptor 变量中。然后,可以使用 fileDescriptor 文件描述符来进行文件读取等操作。


总结起来,句柄(Handle)是一种在操作系统中用于标识、访问和操作系统资源的方式。


不同的操作系统有不同的实现和命名,如 Windows 中的句柄、Linux 和 macOS 中的文件描述符。句柄提供了一种抽象层,使得程序可以使用标识符来引用和操作底层资源,从而实现对系统资源的管理和控制。


2、通俗易懂的理解


可以把句柄理解为一个中间媒介,通过这个中间媒介可控制、操作某样东西。


举个例子。door handle 是指门把手,通过门把手可以去控制门,但 door handle 并非 door 本身,只是一个中间媒介。


又比如 knife handle 是刀柄,通过刀柄可以使用刀。


跟 door handle 类似,我们可以用 file handle 去操作 file, 但 file handle 并非 file 本身。这个 file handle 就被翻译成文件句柄,同理还有各种资源句柄。


3、为什么要发明句柄?


句柄的引入主要是为了解决以下几个问题:




  1. 资源标识:操作系统中存在各种类型的资源,如窗口、文件、设备等。为了标识和引用这些资源,需要一种统一的方式。句柄提供了一个唯一的标识符,可以用于识别特定类型的资源。




  2. 封装和抽象:句柄将底层资源的具体实现进行了封装和抽象,提供了一种更高层次的接口供应用程序使用。这样,应用程序不需要了解资源的内部细节和底层实现,只需要通过句柄进行操作。




  3. 安全性和隔离:句柄可以充当一种权限验证的机制,通过句柄来访问资源可以进行权限检查,从而保证了资源的安全性。此外,句柄还可以实现资源的隔离,不同句柄之间的资源操作互不影响。




  4. 跨平台和兼容性:不同的操作系统和平台有各自的资源管理方式和实现,句柄提供了一种统一的方式来操作不同平台上的资源。这样,应用程序可以在不同的操作系统上运行,并使用相同的句柄接口来访问资源。




总之,句柄提供了一种统一、封装、安全和跨平台的解决方案,使得应用程序可以更方便地操作和管理底层系统资源。


好了,看完以上内容你也许会感叹:句柄?这不就是个把手吗!但是,别小瞧这个

作者:陈有余Tech
来源:juejin.cn/post/7246279539986743354
把手,它可是万能的!

收起阅读 »

路才走了一半,为何停下?(2023 年中总结)

楼主bg 211本,2023 的上半年一直在蔚来实习,在学校和公司两头跑,有些麻木,但也有些思考吧。 期间放一些在蔚来实习的图片吧~ 每次都是在考试周写这些东西(doge 这篇文章姑且算的上是我 2023 年的年中总结。 我最近看到了这样一句话:“网站的流...
继续阅读 »

楼主bg 211本,2023 的上半年一直在蔚来实习,在学校和公司两头跑,有些麻木,但也有些思考吧。


期间放一些在蔚来实习的图片吧~


每次都是在考试周写这些东西(doge



image.png


这篇文章姑且算的上是我 2023 年的年中总结。


我最近看到了这样一句话:“网站的流量是由于先前写的文章,你现在的成就是由于之前的努力或者有远见的选择。”


我现在的生活在我看来是舒适安逸的,有着不重的课业,单身,实习并拿着对学生来说不错的薪水,可预见的会到一个不错的公司,并且一直摆烂好像也有一个光明的未来,这份答卷是两年前的我,选定方向,然后持之以恒的努力所带来的。


然后呢,我现在的现状就是,得过且过,然后愉快的摆烂,长期躺尸带来的空虚感以及身体的虚弱感让我开始思考,如果我现在就丢下了笔,我半年后的答卷会在那里? 是依旧保持现状,被身后的小伙伴追上?还是在某一个瞬间醒悟,然后开始改变?


我认为我在前行路上丢掉了很多的东西,所以我想写篇文章来记录,来反思,这篇文章就是如此,所以这篇文章并不会有太多经历的回忆,当然,由于我这半年几乎一直在蔚来实习,其实生活也是相当的单调,接下来让我们进入正题吧。


image.png


Part One 人一旦停下了思考,未来的结果也就注定了。


看一下微信读书的记录吧:


一月份读了9h39min,二月份读了11小时21min,三月份只读了5h28min,四月份呢,也只有11h56min,五月份稍微好些,到了22h46min,六月份第一周,你只读了3min。


我知道你读了很多网络小说,比如《诡秘之主》这类小说,他确实有很多精妙的设计,你不需要动脑子,读起来当然舒适,我曾和一个朋友聊过纯粹的读书是什么,他说:“那本质上是一种玩物丧志。”我无意争论这些书是否会和我吃的饭一样融入血肉,只是对我个人而言,我希望他们留下些什么,每本书至少要让我学到一个道理,否则和沉迷某一件事物无法控制自己有什么区别?我不否认这很功利,但自我提升就是如此。


所以对前半年的第一个反思,就是真真正正的重启思考,为什么项目中的这个事情你没有考虑到,为什么你对边缘案例不够敏感?为什么一个月了这个事情你还没做完,尤其是当这个事情并不困难的情况下?哪些微小的习惯能使得你变得更坚韧?


我向来是一个行动者,但行动的往往太快了,太急于拿到这个结果,在一定程度上要学会思考,克服无脑的鲁莽。



要紧的是果敢的迈出第一步, 对与错先都不管,自古就没有把一切都设计好再开步的事。别想把一切都弄清楚,再去走路。鲁莽者要学会思考, 善思者要克服的是犹豫。 目的渴求完美,举步之际则无需周全。



共勉!


Part Two 像重病时一样珍重自己


在前两周我第一次阳了,高烧之下,身体极其难受,但反而想做些什么,打开莫言的《生死疲劳》,边读边想。


很奇怪,在这种难受的时刻,我把我的时间看的更加重要了,我不愿意去刷短视频,看没用的小说,我的清醒的时间并不多,在有限的时间中,我总先想把时间要用的更有价值。


有位朋友曾向我表明过他的一种人生态度:“我经常会想象,死亡过后是一种什么状态,虚无?黑暗?所以我每一天,都很感恩,我活着,所以我格外珍惜每一天,和我相处的每个人。”


当时生了病才明白,只有当我们意识到我们时间的珍贵,才会去珍惜当下的时间,以及相处的人。


那为什么不从现在开始,保持感恩,然后用好每天的时间呢?去见新的人也好,去学习也好,去锻炼也好,总之做我觉得有价值而非产生快感的事情。


image.png


Part Three 想想下一本读什么?


真正的阅读者每天给自己规定的读书时间是多少?3h?4h?8h?


是 1 min。


只有当开始不那么困难的时候,我们才会轻松的坚持下来,这个说的是开始。


而当我们开始阅读 1min,我们当然不会满足于此,我们还会继续去阅读。


读书是如此,背单词同样是如此,开启学习也是如此。


当我们读书/做事的时候,如果我们不想着下一件/下一本是什么,我们会做什么?


对于我个人来说,我会干完这个事情之后立刻觉得累了,躺在床上开始刷手机,一刷就是1h+,玩完了意识到自己的空虚,然后开始后悔,这就是我个人的惯性,或者说是习惯,怎么克服呢?


想想下一本读什么!


image.png


Part Four 几个道理,再开始吧


这里做了一个相当大的知识导图,我先放出来一部分截图吧~


总的来说就是觉得自己可以做的事情相当的多。


image.png


image.png


作者:阳树阳树
来源:juejin.cn/post/7247776651688706103
收起阅读 »

强制缓存这么暴力,为什么不使用协商缓存😡😡😡

web
前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存 和 协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。 强缓存和协商缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档...
继续阅读 »

前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。


强缓存和协商缓存


浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档,其中浏览器缓存就分为 强缓存协商缓存:



  1. 强缓存: 当浏览器在请求资源时,根据响应头中的缓存策略信息,判断是否使用本地缓存副本而无需发送请求到服务器。如果资源被标记为强缓存,浏览器会直接从本地缓存中加载资源,而不发送请求到服务器,从而提高页面加载速度并减轻服务器负载;

  2. 协商缓存: 协商缓存是一种缓存策略,用于在资源未过期但可能已经发生变化时,通过与服务器进行协商确定是否使用本地缓存。协商缓存通过在请求中发送特定的条件信息,让服务器判断资源是否有更新,并返回相应的状态码和响应头信息,以指示浏览器是否可以使用本地缓存;


所以根据以上所聊到的特点,浏览器缓存有以下几个方面的优点:



  1. 减少冗余的数据传输;

  2. 减少服务器负担;

  3. 加快客户端加载网页的速度;


20230624082050


浏览器会首先获取该资源缓存的 header 信息,然后根据 Cache-Controlexpires 来判断是否过期。


如图,在浏览器第一次发送请求后,需要再次发送请求时,它会经过以下几个步骤:




  1. 首先,浏览器发送请求到服务器,请求的资源可能是一个网页、css 文件、JavaScript 文件或者其他类型的文件;




  2. 当服务器接收到请求后,首先检查请求中的缓存策略,例如请求头中的 Cache-Controlexpires 字段;




  3. 如果资源被标记为强缓存,服务器会进行以下判断:



    • 如果缓存有效,即资源的过期时间未到达或过期时间在当前时间之后,服务器返回状态码为 200 ok,并在响应头中设置适当的缓存策略,例如设置 Cache-ControlExpires 字段,告诉浏览器可以使用本地缓存;

    • 如果缓存无效,即资源的过期时间已过或过期时间在当前时间之前,服务器返回新的资源,状态码为 200 ok,并在响应头中设置适当的缓存策略;




  4. 如果资源未被标记为强缓存或缓存验证失败,服务器进行协商缓存的判断:



    • 如果请求头中包含 If-Modified-Since 字段,表示浏览器之前缓存了该组员并记录了最后修改时间,服务器会根据资源的最后修改时间进行判断;

      • 如果资源的最后修改时间与 If-Modified-Since 字段的值相同或更早,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应头;

      • 如果资源的最后修改时间晚于 If-Modified-Since 字段的值,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 ok,并在响应头中设置新的最后修改时间;



    • 如果请求头中包含 If--Match 字段,表示浏览器之前缓存了该资源并记录资源的 ETag 值,服务器会根据资源的 ETag 进行判断:

      • 如果资源的 ETagIf--Match 字段的值相同,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应体;

      • 如果资源的 ETagIf--Match 字段的值不同,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 OK,并在响应头中设置新的 ETag;






  5. 浏览器接收到服务器的响应之后,根据状态码和响应头信息进行相应的处理:



    • 如果状态码为 200 OK,表示服务器返回了新的资源,浏览器使用新的资源并更新本地缓存;

    • 如果状态码为 304 Not Modified,表示资源未发生变化,浏览器使用本地缓存的副本;

    • 浏览器根据响应头中的缓存策略进行进一步处理:

      • 如果响应头中包含 Cache-Control 字段,浏览器根据其指令执行缓存策略。例如,如果响应头中的 Cache-Control 包含 no-cache,浏览器将不使用本地缓存,而是向服务器发送请求获得最新的资源;

      • 如果响应头中包含 Expires 字段,浏览器将与当前时间比较,判断资源的过期时间。如果过期时间已过,浏览器将不使用本地缓存,而是向服务器发送请求获取最新的资源;

      • 如果响应头中包含其他相关的缓存控制字段(如 ETag),浏览器可以根据这些字段进行更精确的缓存控制和验证;






其中,在上面的流程中,又有几个令人难懂的字段,主要有以下几个:



  1. ETag: 它是通过对比浏览器和服务器资源的特征值来决定是否要发送文件内容,如果一样就只发送 304 Not Modified;

  2. Expires: 设置过期时间,是绝对时间;

  3. Last-Modified: 以时刻作为标识,无法识别一秒内进行多次修改的情况,只要资源修改,无论内容是否发生实质性变化,都会将该资源返回客户端;

  4. If--Match: 当客户端发送 GET 请求时,如果之前已经键相用资源的请求时,并且服务器返回了 ETag,那么客户端可以将 ETag 的值添加到 If--Match 头中,当再次请求该资源时,客户端会将 If--Match 头发送给服务器,服务器收到请求之后,会检查 If--Match 投中的值是否与当前资源的 ETag 值匹配:

    • 如果匹配,则表示客户端所请求的资源没有发生变化,服务器会返回状态码 304 Not Modified,并且不返回实际的资源内容;

    • 如果 If--Match 头中的值与服务器上资源的 ETag 值不匹配,说明资源发生了变化,服务器会正常返回资源,并返回状态码 200 OK;




图解强缓存和协商缓存


在上面的内容中讲了这么多的理论, 你是否还是不太理解什么是 强缓存协商缓存 啊,那么接下来我们就用几张图片来弄清楚这两者的区别。


强缓存


强缓存就是文件直接从本地缓存中获取,不需要发送请求。


首次请求


20230624103449


当浏览器发送初次请求时,浏览器会向服务器发起请求,服务器接收到浏览器的请求后,返回资源并返回一个 Cache-Control 字段给客户端,在该字段中设置一些缓存相关的信息,例如最大过期时间。


再次请求


20230624103906


在前面的基础上,浏览器再次发送请求,浏览器一节接收到 Cache-Control 的值,那么这个时候浏览器它会首先检查它的 Cache-Control 是否过期,如果没有过期则直接从本地缓存中拉取资源,返回割到客户端,则无需再经过服务器。


缓存失效


20230624104233


强缓存有过期时间,那么就意味着总有一天缓存会失效,如果客户端的 Cache-Control 失效了,那么它就会像首次请求中一样,重新向服务器发起请求,之后服务器会再次返回资源和 Cache-Control 的值。


协商缓存


协商缓存也叫做对比缓存,服务端判断客户端的资源是否和服务端的一样,如果一样则返回 304,反之返回 200 和最新的资源。


初次请求


20230624112243


如果客户端是第一次向服务器发出请求,则服务器返回资源和对应的资源标识给浏览器,该资源标识就是对当前所返回资源的唯一标识,可以是 ETag 或者是 Last-Modified


之后如果浏览器再次发送请求是,浏览器就会带上这个资源表,此时服务端就会通过这个资源标识,可以判断出浏览器的资源跟服务器此时的资源是否一致,如果一致则返回 304 Not Modified,如果不一致,则返回 200,并返回资源以及新的资源标识。


不同刷新操作方式,对强制缓存和协商缓存的影响


不同的刷新操作方式对于强制缓存和写上缓存的影响如下:




  1. 普通刷新(F5刷新按钮):



    • 强制缓存的影响: 浏览器忽略强制缓存,直接向服务器发送请求,获取最新的资源,也就是强制缓存失效;

    • 协商缓存的影响: 浏览器放带有缓存验证的字段的请求,浏览器会根据验证结果返回新的资源或者 304 Not Modified;




  2. 强制刷新(Ctrl+F5Shift+刷新按钮):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器发送不带缓存验证字段的请求,服务器返回新的资源,不进行验证,也就是协商缓存失效;




  3. 禁用缓存刷新(DevTools 中的 Disable cacheNetwork 勾选 Disable cache):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器会发送带有缓存验证字段的请求,服务器会根据验证结果返回新的资源或 304 Not Modified;




这玩意也就图一乐,一般出现了问题我都是直接重启......


总结


总的来说,强制缓存是通过在请求中添加缓存策略,判断缓存是否有效,避免发送请求到服务器。而协商缓存是通过条件请求与服务器进行通信,验证缓存是否仍然有效,并在服务器返回适当的响应状态码和缓存策略。


强制缓存可以减少对服务器的请求,加快资源加载速度,但可能无法获取到最新的资源。协商缓存能够验证资源的有效性,并在需要时获取最新的资源,但会增加对服务器的请求。选择使用哪种缓存策略取决于具体的应用场景和资源的特性。


参考资料


你知道 304 吗?图解强缓存和协商缓存


作者:Moment
来源:juejin.cn/post/7248235392284721209
收起阅读 »

你真的会用<a>标签下载文件吗?

web
最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。 <a> 标签 download 这应该是最常见,最受广大人民群众喜闻...
继续阅读 »

最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。


<a> 标签 download


这应该是最常见,最受广大人民群众喜闻乐见的一种下载方式了,搭配上 download 属性, 就能让浏览器将链接的 URL 视为下载资源,而不是导航到该资源。


如果 download 再指定个 filename ,那么就可以在下载文件时,将其作为预填充的文件名。不过名字中的 /\ 会被转化为下划线 _,而且文件系统可能会阻止文件名中的一些字符,因此浏览器会在必要时适当调整文件名。


封装下载方法


贴份儿我常用的下载方法:


const downloadByUrl = (url: string, filename: string) => {
if (!url) throw new Error('当前没有下载链接');

const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
// 使用target="_blank"时,添加rel="noopener noreferrer" 堵住钓鱼安全漏洞 防止新页面window指向之前的页面
a.rel = "noopener noreferrer";
document.body.append(a);
a.click();

setTimeout(() => {
a.remove();
}, 1000);
};

Firefox 不能一次点击多次下载


这里有个兼容性问题:在火狐浏览器中,当一个按钮同时下载多个文件(调用多次)时,只能下载第一个文件。所以,我们可以利用 <a> 标签的 target 属性,将其设置成 _blank 让火狐在一个新标签页中继续下载。


// 检查浏览器型号和版本
const useBrowser = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};

return Sys;
};

添加一个浏览器判断:


const downloadByUrl = (url: string, filename: string) => {
// 略......

// 火狐兼容
if (useBrowser().browser === "firefox") {
a.target = "_blank";
}

document.body.append(a);
}

download 使用注意点


<a> 标签虽好,但还有一些值得注意的点:


1. 同源 URL 的限制



download 只在同源 URL 或 blob:data: 协议起作用



也就是说跨域是下载不了的......


首先,非同源 URL 会进行导航操作。其次,如果非要下载,那么正如上面的文档所说,可以先将其转换为 blob:data: 再进行下载,至于如何转换会在 Blob 章节中详细介绍。


2. 无法鉴权


使用 <a> 标签下载是带不了 Header 的,因此也不能携带登录态,所以无法进行鉴权。这里我们给出一个解决方案:



  1. 先发送请求获取 blob 文件流,这样就能在请求时进行鉴权;

  2. 鉴权通过后再执行下载操作。


这样是不是就能很好的同时解决问题1和问题2带来的两个痛点了呢😃


顺便提一下,location.hrefwindow.open 也存在同样的问题。


3. download 与 Content-Disposition 的优先级


这里需要关注一个响应标头 Content-Disposition,它会影响 <a>的 download 从而可能产生不同的下载行为,先看一个真实下载链接的 Response Headers


Snipaste_2023-06-20_18-19-21.png


如图所示,Content-Disposition 的 value 值为 attachment;filename=aaaa.bb。请记住,此时Content-Disposition 的 filename 优先级会大于 <a> download 的优先级。也就是说,当两者都指定了 filename 时,会优先使用 Content-Disposition 中的文件名。


接下来我们看看这个响应标头到底是什么。


Content-Disposition



在常规的 HTTP 应答中,Content-Disposition 响应标头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。



Content-Type 不同,后者用来指示资源的 MIME 类型,比如资源是图片(image/png)还是一段 JSON(application/json),而 Content-Disposition 则是用来指明该资源是直接展示在页面上的,还是应该当成附件下载保存到本地的。


当它作为 HTTP 消息主题的标头时,有以下三种写法:


Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

inline


默认值,即指明资源是直接展示在页面上的。
但是在同源 URL 情况下,<a> 元素的 download 属性优先级比 inline 大,浏览器优先使用 download 属性来处理下载(Firefox 早期版本除外)。


attachment


即指明资源应该被下载到本地,大多数浏览器会呈现一个 “保存为” 的对话框,如果此时有 filename,那么它将其优于 download 属性成为下载的预填充文件名。


<a>标签 VS Content-Disposition


介绍完 Content-Disposition,我们做一个横向比对的归纳一下:




  • download VS inline/attachment


    优先级:attachment > download > inline




  • download 的值 VS filename


    优先级:filename > download 的值




Blob 转换


前文介绍到,在非同源请情况下可以将资源当成二进制的 blob 先拿到手,再进行 <a> 的下载处理。接下来,我们介绍两种 blob 的操作:


方法1. 用作 URL(blob:)


URL.createObjectURL 可以给 FileBlob 生成一个URL,形式为 blob:<origin>/<uuid>,此时浏览器内部就会为每个这样的 URL 存储一个 URL → Blob 的映射。因此,此类 URL 很短,但可以访问 Blob。


那这就好办多了,写成代码就三行:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 生成访问 blob 的 URL
const url = URL.createObjectURL(blob);

// 调用刚刚封装的 a 标签下载方法
downloadByUrl(url, "表格文件.xlsx");

// 删除映射,释放内存
URL.revokeObjectURL(url);
};

不过它有个副作用。虽然这里有 Blob 的映射,但 Blob 本身只保存在内存中的。浏览器无法释放它。


在文档退出时(unload),该映射会被自动清除,因此 Blob 也相应被释放了。但是,如果应用程序寿命很长,那这个释放就不会很快发生。


因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob 了,它也会被挂在内存中。


不过,URL.revokeObjectURL 可以从内部映射中移除引用,允许 Blob 被删除并释放内存。所以,在即时下载完资源后,不要忘记立即调用 URL.revokeObjectURL。


方法2. 转换为 base64(data:)


作为 URL.createObjectURL 的一个替代方法,我们也可以将 Blob 转换为 base64-编码的字符串。这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读”。


更重要的是 —— 我们可以在 “data-url” 中使用此编码。“data-url” 的形式为 data:[<mediatype>][;base64],<data>。我们可以在任何地方使用这种 url,和使用“常规” url 一样。


FileReader 是一个对象,其唯一目的就是从 Blob 对象中读取数据,我们可以使用它的 readAsDataURL 方法将 Blob 读取为 base64。请看以下示例:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 声明一个 fileReader
const fileReader = new FileReader();

// 将 blob 读取成 base64
fileReader.readAsDataURL(blob);

// 读取成功后 下载资源
fileReader.onload = function () {
downloadByUrl(fileReader.result);
};
};

在上述例子中,我们先实例化了一个 fileReader,用它来读取 blob。


一旦读取完成,就可以从 fileReader 的 result 属性中拿到一个data: URL 格式的 Base64 字符串。


最后,我们给 fileReader 注册了一个 onload 事件,在读取操作完成后开始下载。


两种方法总结与对比


URL.createObjectURL(blob) 可以直接访问,无需“编码/解码”,但需要记得撤销(revoke);


Data URL 无需撤销(revoke)任何操作,但对大的 Blob 进行编码时,性能和内存会有损耗。


总而言之,这两种从 Blob 创建 URL 的方法都可以用。但通常 URL.createObjectURL(blob) 更简单快捷。


responseType


最后,我们回头说一下请求的注意点:如果你的项目使用的是 XHR (比如 axios)而不是 fetch, 那么请记得在请求时添加上 responseType 为 'blob'。


export const fetchFile = async (params) => {
return axios.get(api, {
params,
responseType: "blob"
});
};

responseType 不是 axios 中的属性,而是 XMLHttpRequest 中的属性,它用于指定响应中包含的数据类型,当为 "blob" 时,表明 Response 是一个包含二进制数据的 Blob 对象。


除了 blob 之外,responseType 还有 arraybufferjsontext等其他枚举字符串值。


总结


一言以蔽之,同源就直接使用 <a> download 下载,跨域就先获取 blob,用 createObjectURLreadAsDataURL 读取链接,再用 <a> download 下载。


参考资料


收起阅读 »

北京十年,来深圳了

离开北京是计划 2013年去的北京,至今整十年,来去匆匆。 几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。 给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。 我们...
继续阅读 »


离开北京是计划


2013年去的北京,至今整十年,来去匆匆。


几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。


给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。


我们希望孩子从他有正式的社交关系开始-幼儿园阶段,尽早适应一个省市的教育理念和节奏,不要等到中小学、甚至高中阶段突然的打断孩子的节奏,插班到一个陌生的班级。他同时要面临环境和学业的压力,不是每个孩子都能很快调整过来的。


我自己小学阶段换了好几次学校,成绩的波动很明显,不希望孩子再面临同样的风险。


另一方面,基于我们年龄的考虑,也要尽快离开,岁数太大了,换城市跳槽不一定能找到合适的岗位。


19年,基于对移动端市场的悲观,我开始考虑换一个技术方向。2020年公司内转岗,开始从事图形相关技术开发,计划2023年离开北京,是考虑要留给自己3年的时间从零开始积累一个领域的技术。


来深圳市是意外


这几年一直在关注其他城市的"落户政策"、"互联网市场"、"房价"、"政府公共服务"。有几个城市,按优先级:杭州、广州、武汉、深圳。这些都是容易落户的城市,我们想尽快解决户口的困扰。


看几组数据:




2023年5月份数据


可以看到,杭州的房价排在第6位,但是收入和工作机会排进前4,所以首选杭州,性价比之王。


广州的房价和工作收入都排第5,中策。


武汉的工作机会排进前10,但是房价在10名开外,而且老家在那边,占尽地利,下策。


深圳的房价高的吓人,和这个城市提供的医疗、教育太不匹配,下下策。


最后选择深圳是形势所逼,今年行情史上最差,外面的机会很少。我和老婆都有机会内部转岗到深圳,所以很快就决定了。


初识深圳


来之前做了基本的调研,深圳本科45岁以内 + 1个月社保可以落户。我公司在南山,老婆的在福田,落户只能先落到对应的区。


我提前来深圳,一个星期租好了房子,确定了幼儿园。


老婆步行15分钟到公司,孩子步行500米到幼儿园,我步行 + 地铁1小时到公司。


福田和南山的教育资源相对充足,有些中小学名校今年都招不满,租房也能上,比龙华、宝安、龙岗等区要好很多。


听朋友说,在龙华一个很差的公立小学1000个小孩报名,只有500个学位。


有不少房东愿意把学位给租户使用,办理起来也不麻烦,到社区录入租房信息即可。和北京一样,采取学区划分政策,按积分排名录取,非常好的学校也要摇号碰运气。


租房


中介小哥陪我看了三四天房子,把这一片小区都看了个遍。考虑近地铁、近幼儿园、有电梯、装修良好等因素。


我本来想砍200房租,中介说先砍400,不行再加。结果我说少400,房东直接说好。我原地愣住了,之前排练的戏份都用不上了,或许今年行情不好,租房市场也很冷淡吧。


小区后面是小山,比较安静。


小区附近-0


小区附近-1


小区附近-2


小区附近-3


外出溜达,路过一所小学


深圳的很多小区里都有泳池
小区-泳池


夜晚的深圳,高楼林立,给人一种压迫感,和天空格格不入。明亮的霓虹灯,和北京一样忙碌。


晚上8点的深圳


晚上10点的深圳


对教育的看法



幸运的人一生都被童年治愈,不幸的人一生都在治愈童年--阿德勒



身边的朋友,有不少对孩子上什么学校有点焦虑,因为教育和高考的压力,有好友极力劝阻我来深圳。我认为在能力的范围内尽力就好,坦然面对一切。


焦虑是对自己无能为力的事情心存妄念。 如果一个人能坦然面对结果,重视当下,不虚度每一分每一秒,人生就不应该有遗憾。人生是来看风景的,结局都是一把灰,躺在盒子里,所以不要太纠结一定要结果怎么样。


学校是培养能力的地方,学历决定一个人的下限,性格和价值观决定上限,你终究成要为你想成为的人,不应该在自我介绍时除了学历能拿出手,一无是处。


不少人不能接受孩子比自己差。可是并没有什么科学依据能证明下一代的基因一定优于上一代吧,或许他们只是不能接受孩子比他们差,他们没有面子,老无所依。我天资一般,我也非常能接受孩子天资平庸,这是上天的旨意。


有些父母根本没有做好家庭教育,试图通过卷学校、一次性的努力把培养的责任寄托于学校。挣钱是成就自己,陪伴是成就孩子,成功的父母居中取舍。


陪伴是最好的家庭教育,如果因为工作而疏忽了孩子,我认为这个家庭是失败的,失败的家庭教育会导致家庭后半生矛盾重重,断送了全家人的幸福。


一个人缺少父爱,就缺少勇敢和力量,缺少母爱就缺少细腻与温和,孩子的性格很容易不健全。除非他自己很有天赋,能自己走出童年的阴影。


因为他长大面对的社会关系是复杂的,他需要在性格层面能融入不同的群体。性格不健全的孩子更容易走向偏激、自私、虚伪、或者懦弱,很多心理学家都是自我治疗的过程中,成为心理学大师。


一个人的一生中,学历不好带来的困扰是非常局部的,但是性格带来的问题将困扰其一生,包括工作、交朋结友、娶妻生子,并且还会传染给下一代。


榜样是最好的教育方法,没有人会喜欢听别人讲大道理,言传不如身教。有些人自己过的很可怜,拼命去鸡娃,那不是培养孩子,那是转移压力,过度投资,有赌棍的嫌疑。你自己过的很苦逼,你如何能说服孩子人生的意义是幸福?鸡娃的尽头是下一代鸡娃。


你只有自己充满能量,积极面对人生,你的孩子才会乐观向上;你只有自己持续的阅读、成长,你的孩子才会心悦诚服的学习;你只有自己做到追求卓越,你的孩子才会把优秀当成习惯。


不要给孩子传递一种信号,人生是苦的,要示范幸福的能力,培养孩子积极地入世观。


作者:sumsmile
来源:juejin.cn/post/7248199693934985272
收起阅读 »

写给毕业季的学生们|我的五次 offer 选择经历

最近临近毕业季,群里有好多朋友在问面试和 offer 选择的问题,我分享下我过往的相关经历,希望能给各位朋友有所启发。 我是谁? 大家好,我是拭心,内蒙古人,16 年本科毕业于西安电子科技大学,先后在创业公司、字节跳动和喜马拉雅工作,目前定居在上海。 &nbs...
继续阅读 »

最近临近毕业季,群里有好多朋友在问面试和 offer 选择的问题,我分享下我过往的相关经历,希望能给各位朋友有所启发。


我是谁?


大家好,我是拭心,内蒙古人,16 年本科毕业于西安电子科技大学,先后在创业公司、字节跳动和喜马拉雅工作,目前定居在上海。


 
2014 年开始在 CSDN 上写作,到目前为止博客访问量约 390万:



shixin.blog.csdn.net/


先后在 GitChat 和极客时间出过小课:


image.png


常在社区做 Android 相关的技术分享,比如最近在 OPPO 做的技术交流:



 
以上是我的基本信息,介绍这些经历是为了让后面的分享更有说服力,接下来看看我过往的五次工作选择经历。
 


五次工作选择经历



我从 2015 年开始找实习,到现在经历过五次换工作,分别是实习、校招和三次社招。


2015 春季实习:屌丝开局



我从 2014 年暑假开始学习 Android 开发,当时成体系的课程不多,学习的材料主要是图书馆里很老的书和校内网的视频,当时贪玩游戏,大部分时间都用来打 LOL,因此编程技术比较差。


2015 春看着舍友们开始找实习,我也投出了实习简历,结果很惨淡:网易阿里面试均失败,鼓起勇气去腾讯面试酒店做了次“霸面”,结果也不了了之没有下文。


好在后面通过了西安一家公司的面试,每天给 100 元工资,当时想着也没有更好的机会,就接了这个 offer。


现在来看,选择接受这个 offer 是个正确的选择,当时我的水平很菜,继续面试可能也没有更好的机会,反而耽误了大好的时间。与其临渊羡鱼,不如先拿到自己能拿到的,同时退而结网。


而做的不好的是,对校招面试重点不了解,没有重视基础。考试前刷了很多面试题,但没想到阿里腾讯压根没怎么问 Android 上层技术,反而问了很多 Java 基础和算法。


 
实习不像自己做玩具,真正的商业项目让我对用到的技术有了更多的了解,也认识到自己需要补充哪些知识点。


更重要的是,有了这个实习经验后,我在秋招时找工作容易了很多。


 


2015 秋季校招:拿下 offer 后犯了懒



秋招的时候,因为有实习的经验,同时我针对性的进行了查漏补缺,面试情况比春季好了很多,大概面了六七家公司,拿到了华为(base 西安)和两家创业公司的 offer(A base 北京,B base 上海)。


当时校招都是在线下进行,有的公司会来我们学校,还有些公司会在市里包下一个酒店进行校招(当时正值移动互联网的辉煌时期,校招很大阵仗),进行面试需要坐公交跑好几个地方,有点累人。
 


在拿到几个 offer 后,我心疼自己不愿意再辛苦,就再也没关注其他公司的招聘,而是去成都重庆九寨沟旅游、在宿舍里打游戏了。


当时选择公司 B 主要看中两个点:1. 工资还可以(三家最高) 2. 上海定居比北京容易,空气也好


实习结束后因为和同事相处的比较愉快,也没有再面试其他公司,就这样决定了自己的正式工作。


现在来看,当时选择上海是对的,因为北京拿户口真的太难了;做的不好的是,在拿到几个 offer 后就心满意足,没有再去面其他公司。


这其实不对,应该再看看有没有更好的机会。马太效应(强者越强)同样适用于程序员,那些在一开始就在更好环境的程序员,往往成长的更快更好,因为他们每天接触的信息、处理的问题,都会更有价值。我暗自和校友对比过,当时去了大厂的校友,有好几个已经成长为部门/业务负责人,反观自己,在比较努力的情况下,才没有差的特别远。
 


很多人会花时间在学习编程 技术 上,但对「去哪里、和谁、做什么样的工作」却没有该有的重视。 对于应届生来说,第一份工作很重要,它很大程度决定了我们的起始速度,不要像我当时一样懒得去面凑合了事,请记住:强者愈强。


 


2017 第一次社招:被一份盒饭感动了



 


2017 年九月我从第一家公司离职,完成了毕业后第一次社招跳槽。
 


为什么要离职呢?主要是因为创业太难了,公司的盈利模式在反复调整后还是不及预期,在我离职前两三个月里基本没什么活干,大家都在默默等着领大礼包。
 


那段时间我面试了不少上海的互联网公司,比如流利说、银天下、饿了么、喜马拉雅、美团点评等等,基本都拿到了 offer 。


当时非常纠结的是饿了么和喜马拉雅选哪个,两个岗位都比较喜欢,面试官给的感觉也都不错。最后考虑再三选了喜马,让天平倾斜的是一份午饭。


在喜马面试的那天,从上午十点面试到了下午四点多,中午 12 点到 2 点休息。在我准备出去找点饭吃时,面试官亲切的给我拿来一份饭,这让我觉得非常温暖,一对比前一天去某公司面了几个小时连水都没得喝,差距太明显了。


现在来看,当时面试通过率比较高,主要是因为这三点:


1.简历很清晰,重点很突出:「用什么技术、做了什么、有什么收获」


2.知识体系比较齐全,吸取春季实习的教训,从计算机基础、Java 集合/并发/虚拟机到 Android SDK/三方库都进行了系统的学习,面试的问题基本都能回答上


3.有博客展示自己,可以让面试官对我有更多的了解


 


当时做的不对的是这几点:


1.入职太快,上家周五离职,下家周一入职,没有多留点思考做规划


2.薪资没怎么涨,不会拿着 offer 要价


 
同时又一次犯了秋招的错误:入职一周后腾讯发来了二面邀请,当时因为不想再折腾,拒绝了后面的面试流程。


 


2020 第二次社招:伤了很多 HR 的心


 



 


2020 年六月我从第二家公司离职,完成了毕业后第二次社招跳槽。


 


为什么要离职呢?主要是因为在两年九个月的工作里,(当时自认为)项目用到的技术点基本都学差不多了。


 


这次面试了更多的公司,拿到了美团点评、阿里、B 圈、字节等公司 offer。


 


能够拿到这么多 offer 的原因:


1.2017 年换完工作后梳理了面试经验并出了个课程,算是有些方法;


2.CSDN 博客访问量增加较多,获得了“博客专家”认证,算是有了点背书;


3.学习的知识有体系,并且都写了文章,记忆很深,很多细节都回答上来了


 
在这些 offer 里,最早拿到的是点评的 offer,薪资不高不低,业务不太核心,先当备胎;后来拿到阿里的 offer,薪资不多,又要换城市,阿里梦敌不过现实,最终拒掉了;B 圈给的挺多,但担心被抓不敢去;最后选择了字节,因为做的是我喜欢的纯基础架构。


老实讲当时很想去阿里,但这个 offer 给的工资没涨多少。虽然部门 leader 和 HR 一个劲的说过去好好干会发股票,但思考再三,我还是决定先把目前该拿的拿到。
 


现在来看,当时做的对的点:


1.拿着 offer 和想去的公司谈价,证明自己的价值


2.没有吃饼,该有的和表现好才有的是两码事


3.选了基础架构,让我的技术有了很大的提升,对后面发展更好


 


 


2022 年第三次社招:华丽转身


 



 


2022 年中我从第三家公司离职,完成了毕业后第三次社招跳槽。


 


为什么要离职呢?主要是因为当时做的是纯架构,需要找到可优化点、进行优化并且推广到业务,在字节的“追求极致”文化下,很多事已经被别人做过了,如果有新的机会,会有大量的人瞬间涌入。经常出现做了好几个版本的实验,最后发现数据不符合预期,或者符合预期但是业务拒绝接入。这种状态久了,有些觉得累。


 


这次工作经历给我的感受是:完全脱离业务的架构,适合年轻人去提升技术,但想做出大成绩很难,需要放平心态(技术非常牛逼的大佬可以忽略这句话)。


 


这次换工作,没面太多公司,主要是因为心里已经有所属,回到熟悉的环境,做了更重要的事。


 


现在来看,能够回去并且担任更重要的责任,主要是因为之前在公司时秉承了“利他”精神,和同事领导们相处的比较融洽,同时自己的能力也被认可。互联网的圈子很小,之前的同事很有可能再续前缘,勿以善小而不为。


 


总结


 


好了,这就是我从实习到现在的五次工作选择经历,谢谢你的阅读。


 


总结下来主要有这些经验:


1.校招基础要扎实;社招要有亮点、有背书


2.选择城市很重要,一开始要去一线城市


3.开头很重要,不要懒得去面凑合了事,记住强者愈强


4.拿到 offer 不急着决定,拿着 offer argue


5.年轻选纯架构,年长选业务架构(有的选的话)


6.与人为善,圈子很小**


 


如果对你有什么启发,欢迎留言点赞,你的鼓励就是我创作的最大动力!



作者:张拭心
来源:juejin.cn/post/7247897453914816573

收起阅读 »

API开放生态平台如何赋能企业数字化转型?

数据服务的共享开放作为企业内部发展的重要因素,已经被越来越多的企业所重视,但在此过程中,不少企业内部存在很多问题:一方面存在严重的数据孤岛现象,多个业务系统之间数据难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需...
继续阅读 »

数据服务的共享开放作为企业内部发展的重要因素,已经被越来越多的企业所重视,但在此过程中,不少企业内部存在很多问题:一方面存在严重的数据孤岛现象,多个业务系统之间数据难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需求明显,但是数据安全无法保障,部分企业客户倾向于线下提供数据进行共享,会导致数据无法实时更新而且无法控制数据的流向。这两大问题既无法提高企业的业务效能,也影响到数据的安全。

基于此,数聚变开放生态平台帮助企业构建服务共享体系,促进资源打通,实现数字能力开放、协同、融合、聚变,驱动行业进化。这其中主要包含以下五个核心功能:


  • 一、数据采集转发

软硬件一体化方案,实现电力企业各类数据共享打通,全方位保障数据安全。穿透电力行业网络分区限制,符合电力行业安全标准。支持百余种电力及工业通讯协议的数据采集转发,一站式交付服务为项目落地保驾护航。设备、协议统一管理,设备模型快速映射,沉淀企业数据资产。

  • 二、数据集成共享

从数据层面解决数据孤岛和数据不一致性问题。通过集成各系统的多种异构数据源来实现数据的即时聚合、分发和共享。同时,我们实现了多个业务系统之间的即时数据同步,确保业务操作的连接性并精确传输数据。

  • 三、数据开放要素流通

覆盖“采”“存”“管”“用”“服”数据链路,实现数据高效便捷流通。整合数据资源,提升数据质量,保障数据安全,衍生数智应用,释放数据价值。通过数据共享、数据开放和数据交易促进数据流通,加快数据要素资源化-资产化-资本化进程。

  • 四、企业数字化咨询

深耕新能源领域,洞察行业先机,促进数字化重塑。提供覆盖行业政策解读,企业战略研究,业务调研及数智化提升的全链路数字化转型规划方案。帮助企业快速定位数字化转型痛点,从业务、应用、数据、技术多位切入,全方面规划转型蓝图,合理化布局转型演进路线。

  • 五、API全生命周期管理

建立贯穿API创建-分类-发布-调用-下架全生命周期标准化管理体系。全方位API服务调用监控告警,保障服务链路安全合规。构建企业API资产目录,帮助企业沉淀API能力资产,提升API服务可复用性,促进API价值最大化。

收起阅读 »

北漂五年,我回家了。后悔吗?

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租...
继续阅读 »

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租屋两点一线。今年我觉得是时候该回家乡了。


1280X1280 (1).JPEG


(在北京大兴机场,天微微亮)


有些工作你一面试就知道是坑


决定回家乡后,我开始更新自己的简历。我想过肯定会被降薪,但是没想到降薪幅度会这么大,成都前端岗位大多都是1w左右,想要双休那就更少了。最开始面试的一些岗位是单休或者大小周,后面考虑了一下最后都放弃了。那时候考虑得很简单,一是我没开始认真找工作,只是海投了几个公司,二是我觉得我找工作这儿时间还比较短,暂时找不到满意的很正常。


辞职后,我的工作还没有着落,于是决定先不找了,出去玩一个月再说。工作了这么久,休息一下不为过吧,于是在短暂休息了一个月后,我又开始认真找工作。


但是,但是没想到成都的就业环境还蛮差的,找工作的第二个月还是没有合适的,当时甚至有点怀疑人生了,难道我做的这个决定是错误的?记得我面试过一家公司,那家公司应该是刚刚成立的,boss上写的员工数是15个,当时我想着,刚成立的公司嘛,工资最开始低点也行,等公司后续发展起来了,升职加薪岂不美滋滋。


面试时,我等了老板快半小时,当时我对这家公司的观感就不太好了。但想着来都来了,总不能浪费走的这一趟。结果,在面试的时候老板开始疯狂diss我的技术不行,会的技能太少,企图用这种话来让我降薪。我是怎么知道他想通过这种方式让我降薪呢,因为最后那老板说“虽然你技术不行,但是我很看好你的学习能力,给你开xxx工资你愿意来吗?”


也是因为这次面试,我在招聘软件上看到那种小公司都不轻易去面试了,简直浪费我时间。


1280X1280.JPEG


(回家路上骑自行车等红绿灯,我的地铁卡被我甩出去了,好险,但是这张地铁卡最后还是掉了,还是在我刚充值完100后,微笑)


终于,找了大概3个月,终于找到一家还算不错的公司,在一家教育行业的公司做前端。双休,工资虽然有打折,但是在我能接受的范围内。


有些人你一见面就知道是正确的


其实我打算回家乡还有一个重要原因是通过大厂相亲角网恋了一个女孩子,她和我是一个家乡的。我们刚认识的时候几乎每天都在煲电话粥,基本上就是陪伴入眠,哈哈哈哈哈。语言的时候她还会唱歌给我听,偏爱、有可能的夜晚......都好好听,声音软绵绵的。认识一个月后,我们回了一趟成都和她面基。一路上很紧张,面基的时候也很害怕自己有哪里做得不好的地方,害怕给她留下不好的印象。我们面基之后一个月左右就在一起啦。有些人真的是你一见面就知道她是正确的那个人,一见面心里有一个声音告诉你“嗯,就是她了!”。万幸,我遇到了。


58895b3fc4db3554881bdbcaa35384f.jpg


1280X1280 (2).JPEG


说一些我们在一起后的甜蜜瞬间吧


打语言电话的时候,听着对方的呼吸声入睡;


走在路上的时候,我牵她的手,她会很顺其自然地与我十指相扣;


在一起吃饭的时候,她会把自己最好吃的一半分享给我;



总结


回到正题,北漂五年。我回家了,后悔吗?不后悔。离开北京快一年了,有时候还是会想念自己还呆在北京的不足10平米的小出租屋里的生活,又恍惚“噢,我已经回四川了啊”。北漂五年,我还是很感激那段时间,让刚毕业的我迅速成长成可以在工作上独当一面的合格的程序员,让我能有拿着不菲的收入,有一定的积蓄,有底气重新选择;感谢大厂相亲角,让我遇见我的女朋友,让我不再是单身狗。


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

在国企做程序员怎么样?

有读者咨询我,在国企做开发怎么样? 当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。 下面分享一位国企程序员的经历,希望能给大家一些参考价值。...
继续阅读 »

有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


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

Android 自定义View 之 饼状进度条

前言   前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图: 正文   效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码...
继续阅读 »

前言


  前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图:


在这里插入图片描述


正文


  效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码不是很全的话,你可以找到文章最后的源码去查看,话不多说,我们开始吧。


一、XML样式


  根据上面的效果图,我们首先来确定XML中的属性样式,在attrs.xml中添加如下代码:

	<!--饼状进度条-->
<declare-styleable name="PieProgressBar">
<!--半径-->
<attr name="radius" />
<!--最大进度-->
<attr name="maxProgress" />
<!--当前进度-->
<attr name="progress" />
<!--进度条进度颜色-->
<attr name="progressbarColor" />
<!--进度条描边宽度-->
<attr name="strokeWidth"/>
<!--进度是否渐变-->
<attr name="gradient" />
<!--渐变颜色数组-->
<attr name="gradientColorArray" />
<!--自定义开始角度 0 ,90,180,270-->
<attr name="customAngle">
<enum name="right" value="0" />
<enum name="bottom" value="90" />
<enum name="left" value="180" />
<enum name="top" value="270" />
</attr>
</declare-styleable>

  这里的公共属性我就抽离了出来,因为之前写过圆环进度条,有一些属性是可以通用的,并且我在饼状进度条中增加了开始的角度,之前是默认是从0°开始,现在可以根据属性设置开始的角度,并且我增加了渐变颜色。


二、构造方法


  现在属性样式已经有了,下一步就是写自定义View的构造方法了,在com.easy.view包下新建一个PieProgressBar 类,里面的代码如下所示:

public class PieProgressBar extends View {

/**
* 半径
*/
private int mRadius;
/**
* 进度条宽度
*/
private int mStrokeWidth;
/**
* 进度条进度颜色
*/
private int mProgressColor;
/**
* 开始角度
*/
private int mStartAngle = 0;

/**
* 当前角度
*/
private float mCurrentAngle = 0;
/**
* 结束角度
*/
private int mEndAngle = 360;
/**
* 最大进度
*/
private float mMaxProgress;
/**
* 当前进度
*/
private float mCurrentProgress;
/**
* 是否渐变
*/
private boolean isGradient;
/**
* 渐变颜色数组
*/
private int[] colorArray;
/**
* 动画的执行时长
*/
private long mDuration = 1000;
/**
* 是否执行动画
*/
private boolean isAnimation = false;

public PieProgressBar(Context context) {
this(context, null);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PieProgressBar);
mRadius = array.getDimensionPixelSize(R.styleable.PieProgressBar_radius, 80);
mStrokeWidth = array.getDimensionPixelSize(R.styleable.PieProgressBar_strokeWidth, 8);
mProgressColor = array.getColor(R.styleable.PieProgressBar_progressbarColor, ContextCompat.getColor(context, R.color.tx_default_color));
mMaxProgress = array.getInt(R.styleable.PieProgressBar_maxProgress, 100);
mCurrentProgress = array.getInt(R.styleable.PieProgressBar_progress, 0);
//是否渐变
isGradient = array.getBoolean(R.styleable.PieProgressBar_gradient, false);
//渐变颜色数组
CharSequence[] textArray = array.getTextArray(R.styleable.PieProgressBar_gradientColorArray);
if (textArray != null) {
colorArray = new int[textArray.length];
for (int i = 0; i < textArray.length; i++) {
colorArray[i] = Color.parseColor((String) textArray[i]);
}
}
mStartAngle = array.getInt(R.styleable.PieProgressBar_customAngle, 0);
array.recycle();
}
}

  这里声明了一些变量,然后写了3个构造方法,在第三个构造方法中进行属性的赋值。


三、测量


  这里测量就比较简单了,和之前的圆环进度条差不多,代码如下所示:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST: //wrap_content
width = mRadius * 2;
break;
case MeasureSpec.EXACTLY: //match_parent
width = MeasureSpec.getSize(widthMeasureSpec);
break;
}
//Set the measured width and height
setMeasuredDimension(width, width);
}

  因为不需要进行子控件处理,所以我们只要一个圆和描边就行了,下面看绘制的方法。


四、绘制


  绘制这里就是绘制描边和进度,绘制的代码如下所示:

    @Override
protected void onDraw(Canvas canvas) {
int centerX = getWidth() / 2;
@SuppressLint("DrawAllocation")
RectF rectF = new RectF(0,0,centerX * 2,centerX * 2);
//绘制描边
drawStroke(canvas, centerX);
//绘制进度
drawProgress(canvas, rectF);
}

  在绘制之前首先要确定中心点,因为我们是一个圆环,实际上也是一个圆,圆的宽高一样,所以中心点的x、y轴的位置就是一样的,然后是确定一个矩形的左上和右下两个位置的坐标点,通过这两个点就能绘制一个矩形,接下来就是绘制进度条背景。


① 绘制描边

    /**
* 绘制描边
*
* @param canvas 画布
* @param centerX 中心点
*/
private void drawStroke(Canvas canvas, int centerX) {
Paint paint = new Paint();
paint.setColor(mProgressColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokeWidth);
paint.setAntiAlias(true);
canvas.drawCircle(centerX, centerX, mRadius - (mStrokeWidth / 2), paint);
}

  这里的要点就是我们需要设置画笔的类型为描边,然后设置描边宽度,这样我们就可以画一个空心圆,就成了描边,然后我们绘制进度。


① 绘制进度

    /**
* 绘制进度条背景
*/
private void drawProgress(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
//画笔的填充样式,Paint.Style.STROKE 描边
paint.setStyle(Paint.Style.FILL);
//抗锯齿
paint.setAntiAlias(true);
//画笔的颜色
paint.setColor(mProgressColor);
//是否设置渐变
if (isGradient && colorArray != null) {
paint.setShader(new RadialGradient(rectF.centerX(), rectF.centerY(), mRadius, colorArray, null, Shader.TileMode.MIRROR));
}
if (!isAnimation) {
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
}
//开始画圆弧
canvas.drawArc(rectF, mStartAngle, mCurrentAngle, true, paint);
}

  因为背景是一个圆环,所以这里的画笔设置就比较注意一些,看一下就会了,这里最重要的是drawArc,用于绘制及角度圆,像下图这样,画了4/1的进度,同时增加是否渐变的设置,这里的开始角度是动态的。


在这里插入图片描述


五、API方法


  还需要提供一些方法在代码中调用,下面是这些方法的代码:

    /**
* 设置角度
* @param angle 角度
*/
public void setCustomAngle(int angle) {
if (angle >= 0 && angle < 90) {
mStartAngle = 0;
} else if (angle >= 90 && angle < 180) {
mStartAngle = 90;
} else if (angle >= 180 && angle < 270) {
mStartAngle = 180;
} else if (angle >= 270 && angle < 360) {
mStartAngle = 270;
} else if (angle >= 360) {
mStartAngle = 0;
}
invalidate();
}

/**
* 设置是否渐变
*/
public void setGradient(boolean gradient) {
isGradient = gradient;
invalidate();
}

/**
* 设置渐变的颜色
*/
public void setColorArray(int[] colorArr) {
if (colorArr == null) return;
colorArray = colorArr;
}

/**
* 设置当前进度
*/
public void setProgress(float progress) {
if (progress < 0) {
throw new IllegalArgumentException("Progress value can not be less than 0");
}
if (progress > mMaxProgress) {
progress = mMaxProgress;
}
mCurrentProgress = progress;
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
setAnimator(mStartAngle, mCurrentAngle);
}

/**
* 设置动画
*
* @param start 开始位置
* @param target 结束位置
*/
private void setAnimator(float start, float target) {
isAnimation = true;
ValueAnimator animator = ValueAnimator.ofFloat(start, target);
animator.setDuration(mDuration);
animator.setTarget(mCurrentAngle);
//动画更新监听
animator.addUpdateListener(valueAnimator -> {
mCurrentAngle = (float) valueAnimator.getAnimatedValue();
invalidate();
});
animator.start();
}

  那么到此为止这个自定义View就完成了,下面我们可以在PieProgressBarActivity中使用了。


六、使用


   关于使用,我在写这个文章的时候这个自定义View已经加入到仓库中了,可以通过引入依赖的方式,例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now

dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.4'
}

   或者你在自己的项目中完成了刚才上述的所有步骤,那么你就不用引入依赖了,直接调用就好了,不过要注意更改对应的包名,否则会爆红的。


  先修改activity_pie_progress_bar.xml的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".used.PieProgressBarActivity">

<com.easy.view.PieProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:customAngle="right"
app:gradient="false"
app:gradientColorArray="@array/color"
app:maxProgress="100"
app:progress="5"
app:progressbarColor="@color/green"
app:radius="80dp" />

<CheckBox
android:id="@+id/cb_gradient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="是否渐变" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始角度:"
android:textColor="@color/black" />

<RadioGroup
android:id="@+id/rg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<RadioButton
android:id="@+id/rb_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="0%" />

<RadioButton
android:id="@+id/rb_90"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90%" />

<RadioButton
android:id="@+id/rb_180"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180%" />

<RadioButton
android:id="@+id/rb_270"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270%" />
</RadioGroup>
</LinearLayout>


<Button
android:id="@+id/btn_set_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="随机设置进度" />

<Button
android:id="@+id/btn_set_progress_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置0%进度" />

<Button
android:id="@+id/btn_set_progress_100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置100%进度" />
</LinearLayout>

在strings.xml中增加渐变色,代码如下:

    <string-array name="color">
<item>#00FFF7</item>
<item>#FFDD00</item>
<item>#FF0000</item>
</string-array>

首先要注意看是否能够预览,我这里是可以预览的,如下图所示:


在这里插入图片描述


PieProgressBarActivity中使用,如下所示:

public class PieProgressBarActivity extends EasyActivity<ActivityPieProgressBarBinding> {

@SuppressLint("NonConstantResourceId")
@Override
protected void onCreate() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//是否渐变
binding.cbGradient.setOnCheckedChangeListener((buttonView, isChecked) -> {
binding.cbGradient.setText(isChecked ? "渐变" : "不渐变");
binding.progress.setGradient(isChecked);
});
//开始角度
binding.rg.setOnCheckedChangeListener((group, checkedId) -> {
int angle = 0;
switch (checkedId) {
case R.id.rb_0:
angle = 0;
break;
case R.id.rb_90:
angle = 90;
break;
case R.id.rb_180:
angle = 180;
break;
case R.id.rb_270:
angle = 270;
break;
}
binding.progress.setCustomAngle(angle);
});
//设置随机进度值
binding.btnSetProgress.setOnClickListener(v -> {
int progress = Math.abs(new Random().nextInt() % 100);
Toast.makeText(this, "" + progress, Toast.LENGTH_SHORT).show();
binding.progress.setProgress(progress);
});
//设置0%进度值
binding.btnSetProgress0.setOnClickListener(v -> binding.progress.setProgress(0));
//设置100%进度值
binding.btnSetProgress100.setOnClickListener(v -> binding.progress.setProgress(100));
}
}

运行效果如下图所示:


在这里插入图片描述


七、源码


如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~


源码地址:EasyView


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

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:

{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"route2":["prefetchKey2"],
"route3":["prefetchKey3","prefetchKey4"]
},
"prefetcher":{
"prefetchKey":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.itemId",
"firstTime":"true"
},
"headers": {

},
"prefetchImgInResponse": [
{
"imgUrl":"$data.imgData.img",
"imgWidth":"$data.imgData.imgWidth",
"imgHeight":150
}
]
}
},
"prefetchKey2":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name2",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.productId",
"firstTime":"false"
},
"headers": {

}
},
"prefetchKey3":{
"prefetchType":"image",
"prefetchInfo":{
"imgUrl":"$route.imgUrl",
"imgWidth":"$route.imgWidth",
"imgHeight": 150
}
},
"prefetchKey4":{
"prefetchInfo":{}
}
}
}


规则解读




















































参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:

class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


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

Android:自定义View实现签名带笔锋效果

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)、抬起(ACTION_UP)、移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、...
继续阅读 »

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)抬起(ACTION_UP)移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、取消、清除、画笔的粗细,也就是对收集的点集合和线集合的增删操作以及画笔颜色宽的的更改。这些功能都在 实现一个自定义有限制区域的图例(角度自识别)涂鸦工具类(上) 中介绍过。


但就在前不久遇到一个需求是要求手签笔能够和咱们使用钢笔签名类似的效果,当然这个功能目前是有一些公司有成熟的SDK的,但我们的需求是要不借助SDK,自己实现笔锋效果。那么,如何使画笔带笔锋呢?废话不多说,先上效果图:


image.png


要实现笔锋效果我们需要考虑几个因素:笔速笔宽按压力度(针对手写笔)。因为在onTouchEvent回调的次数是不变的,一旦笔速变快两点之间距离就被拉长。此时的笔宽不能保持在上一笔的宽度,需要我们通过计算插入新的点,同时计算出对应点的宽度。同理当我们笔速慢的时候,需要通过计算删除信息相近的点。要想笔锋自然,当然贝塞尔曲线是必不可少的。


这里我们暂时没有将笔的按压值作为笔宽的计算,仅仅通过笔速来计算笔宽。

/**
* 计算新的宽度信息
*/
public double calcNewWidth(double curVel, double lastVel,double factor) {
double calVel = curVel * 0.6 + lastVel * (1 - 0.6);
double vfac = Math.log(factor * 2.0f) * (-calVel);
double calWidth = mBaseWidth * Math.exp(vfac);
return calWidth;
}

/**
* 获取点信息
*/
public ControllerPoint getPoint(double t) {
float x = (float) getX(t);
float y = (float) getY(t);
float w = (float) getW(t);
ControllerPoint point = new ControllerPoint();
point.set(x, y, w);
return point;
}

/**
* 三阶曲线的控制点
*/
private double getValue(double p0, double p1, double p2, double t) {
double a = p2 - 2 * p1 + p0;
double b = 2 * (p1 - p0);
double c = p0;
return a * t * t + b * t + c;
}

最后也是最关键的地方,不再使用drawLine方式画线,而是通过drawOval方式画椭圆。通过前后两点计算出椭圆的四个点,通过笔宽计算出绘制椭圆的个数并加入椭圆集。最后在onDraw方法中绘制。

/**
* 两点之间将视图收集的点转为椭圆矩阵 实现笔锋效果
*/
public static ArrayList<SvgPointBean> twoPointsTransRectF(double x0, double y0, double w0, double x1, double y1, double w1, float paintWidth, int color) {

ArrayList<SvgPointBean> list = new ArrayList<>();
//求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园
double curDis = Math.hypot(x0 - x1, y0 - y1);
int steps;
//绘制的笔的宽度是多少,绘制多少个椭圆
if (paintWidth < 6) {
steps = 1 + (int) (curDis / 2);
} else if (paintWidth > 60) {
steps = 1 + (int) (curDis / 4);
} else {
steps = 1 + (int) (curDis / 3);
}
double deltaX = (x1 - x0) / steps;
double deltaY = (y1 - y0) / steps;
double deltaW = (w1 - w0) / steps;
double x = x0;
double y = y0;
double w = w0;

for (int i = 0; i < steps; i++) {
RectF oval = new RectF();
float top = (float) (y - w / 2.0f);
float left = (float) (x - w / 4.0f);
float right = (float) (x + w / 4.0f);
float bottom = (float) (y + w / 2.0f);
oval.set(left, top, right, bottom);
//收集椭圆矩阵信息
list.add(new SvgPointBean(oval, color));
x += deltaX;
y += deltaY;
w += deltaW;
}

return list;
}

至此一个简单的带笔锋的手写签名就实现了。 最后附上参考链接Github.


我是一个喜爱Jay、Vae的安卓开发者,喜欢结交五湖四海的兄弟姐妹,欢迎大家到沸点来点歌!


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

有多少人忘记了gb2312

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。 本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果 -有点惊讶看似url...
继续阅读 »

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣



  1. 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果


image.png


image.png
-有点惊讶看似url编码实则url编码只是这,滋滋滋...
VeryCapture_20230227174156.gif


有点东西,开始抓包,断点,追踪的逆向之路
VeryCapture_20230227170913.gif
2. 发现是ajax加载(不简单呀纯纯的吊胃口)先来一波关键字索引(keyword)等一系列基操轻而易举的找到了他
VeryCapture_20230227171325.gif


从此开始走向了一条不归路,经过一上午的时间啥也没追到,午休之后继续战斗,经过了一两个半小时+三支长白山牌香烟的努力终于


VeryCapture_20230227173627.gif
VeryCapture_20230227173457.gif


VeryCapture_20230227171715.gif

cihui = '哈哈哈'
js = open("./RSAAA.js", "r", encoding="gbk", errors='ignore')
line = js.readline()
htmlstr = ''
while line:
htmlstr = htmlstr + line
line = js.readline()
ctx = execjs.compile(htmlstr)
result = ctx.call('invokeServer', cihui)
print(result)
const jsdom = require("jsdom");
const {JSDOM} = jsdom;
const dom = new JSDOM('<head>\n' +
' <base href="//search.dangdang.com/Standard/Search/Extend/hosts/">\n' +
'<link rel="dns-prefetch" href="//search.dangdang.com">\n' +
'<link rel="dns-prefetch" href="//img4.ddimg.cn">\n' +
'<title>王子-当当网</title>\n' +
'<meta http-equiv="Content-Type" content="text/html; charset=GB2312">\n' +
'<meta name="description" content="当当网在线销售王子等商品,并为您购买王子等商品提供品牌、价格、图片、评论、促销等选购信息">\n' +
'<meta name="keywords" content="王子">\n' +
'<meta name="ddclick_ab" content="ver:429">\n' +
'<meta name="ddclick_search" content="key:王子|cat:|session_id:0b69f35cb6b9ca3e7dee9e1e9855ff7d|ab_ver:G|qinfo:119800_1_60|pinfo:_1_60">\n' +
'<link rel="canonical" href="//search.dangdang.com/?key=%CD%F5%D7%D3\&amp;act=input">\n' +
' <link rel="stylesheet" type="text/css" href="css/theme_1.css">\n' +
' <!--<link rel="Stylesheet" type="text/css" href="css/model/home.css" />-->\n' +
' <link rel="stylesheet" type="text/css" href="css/model/search_pub.css?20211117"> \n' +
'<style>.shop_button {height: 0px;}.children_bg01 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 630px;\n' +
'}\n' +
'.children_bg02 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.children_bg03 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.narrow_page .children_bg01 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg02 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg03 a{\n' +
'width: 450px;\n' +
'}.price .search_e_price span {font-size: 12px;font-family: 微软雅黑;display: inline-block;background-color: #739cde;color: white;padding: 2px 3px;line-height: 12px;border-radius: 2px;margin: 0 4px 0 5px;}\n' +
'.price .search_e_price:hover {text-decoration: none;}</style> <link rel="stylesheet" href="http://product.dangdang.com/js/lib/layer/3.0.3/skin/default/layer.css?v=3.0.3.3303" id="layuicss-skinlayercss"><script id="temp_script" type="text/javascript" src="//schprompt.dangdang.com/suggest_new.php?keyword=好好&amp;pid=20230227105316030114015279129895799&amp;hw=1&amp;hwps=12&amp;catalog=&amp;guanid=&amp;0.918631418357919"></script><script id="json_script" type="text/javascript" src="//static.dangdang.com/js/header2012/categorydata_new.js?20211105"></script></head>');

window = dom.window;
document = window.document;
function invokeServer(url) {

var scriptOld = document.getElementById('temp_script');
if(scriptOld!=null && document.all)
{
scriptOld.src = url;
return script;
}
var head=document.documentElement.firstChild,script=document.createElement('script');
script.id='temp_script';
script.type = 'text/javascript';
script.src = url;
if(scriptOld!=null)
head.replaceChild(script,scriptOld);
else
head.appendChild(script);
return script
}



  1. 完事!当我以为都要结束了的时候恍惚直接看到了源码中的gb2312突然想起了之前做的一个萍乡房产网的网站有过类似经历赶快去尝试结果我**
    image.png
    image.png
    VeryCapture_20230227172815.gif




  2. 总结:提醒各位大佬在逆向之路中还是要先从基操开始,没必要一味的去搞攻克扒源码,当然还是要掌握相对全面的内容,其实除了个别大厂有些用些贵的东西据说某数5要20个W随着普遍某数不知道那些用了20w某数的大厂心里是什么感觉或许并不在乎这点零头哈哈毕竟是大厂,小网站的反扒手段并不是很难,俗话说条条大道通北京。


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

我,不是实习生

啊,一想我读书十几年,现在终将要脱离学校奔赴社会了。依照我这能力,我这性子,学术型人才是走不动了。 我知道,不继续深造,就要去工作赚钱。 可我一晃这几年,感觉啥也没学着。圈养在学校,老师只负责教授我课本知识,定向培养我成为一名合格的打工人。 经历面试才知道,市...
继续阅读 »

啊,一想我读书十几年,现在终将要脱离学校奔赴社会了。依照我这能力,我这性子,学术型人才是走不动了。


我知道,不继续深造,就要去工作赚钱。


可我一晃这几年,感觉啥也没学着。圈养在学校,老师只负责教授我课本知识,定向培养我成为一名合格的打工人。


经历面试才知道,市场的速度是光速,学校里是声速。企业技术走得这么前沿,我学的东西讲出来都有些羞涩。


技术和知识面落后这么多,要这么短的时间紧跟市场速度,且找到一份合适的工作,属实不容易。


没辙,遵循市场用人规则,我连夜整理面试知识和制作简历,并效仿当年八股文进士。


可一工作才发现,没完没了的工作,没完没了的 OKR,大家都没完没了,可是整体盈利又不怎么样。


作为一名实习生,实在是搞不通,谁顾着谁,谁又惧谁。我知道我会好好工作,内心还怀揣着抱负和理想,一心扑到工作上证明我自己。


电话里头我也是这么对父母说。


可是,几千块钱的薪资,我恍然不已,原来劳动用工成本可以这么低,我每天顾着怎么开销,怎么减少活动,怎么存钱,家里急用钱怎么办。


另一方面,我通过努力证明自己的实力,可以升职加薪,可是一冷静下来,怎么加班都能联想到旁边工作十几年经验老油条的现状。


联想周边企业刚加入的年轻生命,读了十几年书刚开始还没来得及做好准备,就结束在了加班不眠之夜里。


不禁在想,身边不乏努力的人,可越是努力劳动力越廉价,资源也就这么多。这些努力的成果到底在哪里,它会和我们的理想化文明向前推进挂钩吗,还是像小白鼠跑转轮一样。只要你在忙,在跑着就会有食物,仅此而已。


当我不断工作不断思考之后,我决定把一部分精力从工作拆分开来,用来做自己的事情并且赚钱的时候。


才发现,我已经离罗马中心十八里开外了。那些早就明白规则的人已经身价 A7+ 了。而我,还只是一个职场人,思想和个性逐渐被磨平的人。


好不容易开始有自主赚钱的觉悟,却又不知道从哪里开始。


眼见互联网的风吹草动,打算沾点风头做些衍生产品赚些小钱,可曾想相关情况一调查,各种衍生产品已经多如牛毛,自己的想法刚萌生就已经望尘莫及。


看来这个新时代,已经拼的主要不是个人努力,而是感知能力,谁的感知能力强,捕捉到风口和需求,谁就能够抢占先机,落后的残羹冷炙都吃不上。


回想起我同届的校友,几个校友的案例历历在目,他们在校的时候就喜欢自己捣鼓生意,周边能赚钱的项目都试了个遍,租摊做外卖、合租奶茶店、做中介介绍学生工、婚礼现场布置等等,这些都是他们课外喜欢动手做的事情,大二开始就已经有很强的人脉关系和团队,学长学姐老师都能够拉拢合作。


我曾好奇问过他,你为啥经常跑这跑那,经常上课缺席?


他告诉我,我不太喜欢课本上的东西,我喜欢捣鼓些小玩意,以后毕业自己能够做些小生意就够了。


当时我的角度跟他截然相反,上课认真听讲,班务事情积极做,国家和学校的奖学金我都拿了,但对于工作前景就是当一名程序员就行。


可以明说当时心态上有些看不起他们经常翘课,出去捣鼓小钱的行为。


事实上,是多么可笑的。确实是换了几家企业的程序员,每天殚精竭虑花在工作上,上面指哪我就打哪,固定薪资,每时每刻接收就业差,企业裁员情报,人人自危陷入焦虑和恐慌,学生时代的傲骨早就被企业文化磨平。


而那些从学生时代喜欢捣鼓生意,爱动手赚钱的人,就我所知道那些同届的校友。他们的店铺已经开连锁店了,做学生和年轻人的生意。有的已经赚国外的钱了。



▲图/ 学长已经开起了分店


这一类人,我身边认识和知道的没有一个过得不好的,他们善于利用信息和售卖信息。教你开店,拍视频的课程理论上都是售卖信息行为。


这一对比,仿佛他们才是懂社会规则的人,像是弯道超车般的越过了规则到了另外一层,他们的精力花在了资本运转身上,只要有人有需求就有机会。对于裁员、跳槽、就业,是打工人该担心的事情。


才明白,学校所教授的知识和培养的素质,大多都是培养我们成为一名工人具备的思维和能力,毕竟经济的推动和国家的发展,需要具备大量的工人。至于效果和进阶,那就让企业、社会来教你。


出来实习后,才知道自己有多么被动。跟不上社会的步伐,欠缺了多少实用的工具和能力。


被欺负了如何拿起法律的武器保护自己;找不到工作或失业如何自主赚钱;如何懂理财懂投资;脱离一定条件如何生存和陷入危险如何自救...


这些,长达十几年的学生时代里,没有专门的课程或者相关老师教授。


我所欠缺的这些知识,是屡次碰壁之后幡然醒悟,才有所接触这些内容,但此时已经千疮百孔,伤痕累累。与此同时,总能遇见大批初入社会的人依然走自己走过的路,叫,是叫不醒。在世界观和认知能力闭合之后,总是需要事教人的地步才能打开。因为,好言相劝已经不管用。



象牙塔里待了几年,一张白纸怎么渲染都好渲染,原本与人为善,感恩戴德,分清对与错,拥有理想与信念。


开始接轨社会之后,持着心善的态度连打数张好人牌,被坑蒙拐骗殆尽,才醒悟这些品质都是别人敛财的工具,得到教训之后还得分清谁值得给好人牌,谁永远坏人牌。


什么又是对与错,无人问津的人说什么话都只是一句话。反而屏幕出现权势,坐拥资源的人说的话有多不经推敲都觉得是对的,因为它是成功人士。


什么是理想,什么又是信念。去一趟公司吧,让你感受一番企业文化之后,再说你的理想是什么,有什么信念。


......


作为一名实习生,意味着即将进入社会,和不同人打交道了。或许你的内心秉持着工作的想法,又或许秉持着自己的热爱和目标。


工作从来也都不是一件轻松的事情,至少最近的环境里是这样。


工作或许能够加速让你融入社会,但同时加速你的痛苦,因为这是一个“丛林世界”,你没对错可言,也没有更多选择,或许看似有选择其实也只是一个看起来更大稍微舒适点的“囚牢”。


并且,工作本身难找的不是工作,而是放缓不了自己的心态,一头扎进内心向往的“高薪”,“体面”,“舒适” 出不来。人人向往这片区域,也就只有这么点区域,总有人失落且焦虑。


然而,大多不被看好的行业或被嫌弃的工作,往往能够带来与之热门职业持平的回报。网络出现的“北大毕业卖猪肉”,“高材生当保姆”的等被大众关注的案例屡见不鲜,甚至嗤之以鼻予以嘲讽。


殊不知“笑贫不笑娼”的社会环境,他们才是能屈能伸的强者。这些人和我那些校友有着同样的能力。动手能力强,生存能力强,就算被限制条件,也能够屈伸过的好。


职业并无贵贱区分,能掌握活法才是本质。


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

🐞 如何成为一名合格的“高级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 这几天疯狂在肝游戏,已经到了魔怔的地步,每天早上起床是想着我今天该怎么在地铁上杀爆,每天晚上躺下的时候想的是我的装备还能怎么配装…… 哈哈,今天我们继续分享怎么一步步做一个专业的开发者,还有工作中要注意什么事情。 如...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


这几天疯狂在肝游戏,已经到了魔怔的地步,每天早上起床是想着我今天该怎么在地铁上杀爆,每天晚上躺下的时候想的是我的装备还能怎么配装……


哈哈,今天我们继续分享怎么一步步做一个专业的开发者,还有工作中要注意什么事情。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的三篇文章,说不定能对你有帮助。




  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?

  3. 🐞 如何成为一名合格的“中级开发”



今天,我们继续聊一聊,看看到底是什么造就了“高级开发”,而我们应该怎么往这个方向冲刺呢?😎


❓ 什么是“高级开发”?


回顾一下我这个系列第一篇文章的定义,我这边罗列一下:



  1. 精通团队所使用的核心技术,对其应用得非常熟练。

  2. 能处理团队项目中的系统架构问题和设计🏢

  3. 有多年的编码经验(一定是真正在一线的真正写代码的时间,而不是通过经历硬凑的时间)

  4. 拥有构建“完整”解决方案的经验,能够考虑到项目的各个方面并提供全面的解决方案。🔍

  5. 在其他专业相关领域有一定经验,了解负载平衡、连接池等跨领域知识。🖥️

  6. 积极指导中级和初级开发工程师。👥


如果你能做到以上这些部分或者全部的内容,比如:



  • 你在公司中解决了很多一般开发解决不了的难题

  • 善于沟通,能够处理各方的关系和调解工作沟通

  • 在许多团队决策上能提供许多建设性的想法


等等……


就算短时间内不被授予领导者的角色,潜移默化地,你的同事都会帮你当成团队大佬和领导(更注重你的意见)


❓ “高级开发”比“中级开发”多了什么?


多得多的开发经历


现在国内公司普遍的一个潜规则是 5年以上开发经验是“高级开发”职务的基本条件


因此现在有很多的“中级开发”喜欢使用 编码经历(不是开发经历) 来判断自己是不是能胜任“高级开发”的职务。


例如:小张只有3年的工作经验,但是喜欢把实习1年和大学的编程作业1年这些时间加上,来给自己打上5年开发经验的标签。



可能因为内卷的原因,简历伪造基本上50%的概率都会遇到,大家都想把自己最好的一面展示出来,甚至不惜夸大一部分的事实。至少如果这份简历到了我这里看到,是一件非常危险的事情。



这样就导致了,很多时候我们真的没有招募到一个有高级开发实力的“高级开发”。


高级开发人员在构建解决方案、管理复杂性、处理令人困惑的业务需求、应用设计模式等方面积累了丰富的经验。因为他们做过很多次这些事情,一遍又一遍,他们可以“用心”解决许多常见问题。


“高级开发”应该要像一个成熟的成年人,很多方案的尝试不应该带有实验性,而是真正的“做过”


这种能力只能来自你从失败、成功、导师等中学到的真实经验,需要大量的练习,需要做很多次这些事情,以至于它们会印在你的大脑中!



这里提到的经验,不包括没有挑战性的工作,如果只是CRUD,你永远都不会成长



在这之前他们应该在开发经验上有很多很多时间的沉淀,研究过许多Demo,并为他们以后的解决方案奠定基础。


所以,“高级开发”需要的是比“中级开发”多得多得多的实战经验才能构建出一个属于自己的“解决方案”体系。


谦卑


“高级开发”因为在很多问题的已经有了解决方案。


而且他们已经把有效、有用、实用、简单这几个字贯彻到了实际的开发工作中。


因此由于他们的经验,“高级开发”虽然知道很多东西,对自己的能力很有信心,但是他不会再有“骄傲”的心理。


因为面对的事情太多,会开始知道其实自己不知道的事情太多了。


反而“高级开发”对如何让实现方案趋于完美有很高的追求。


❓ “高级开发”应该有怎么样的知识广度?


现在我们业内流行一个说法叫做 T型人才


这其实就要求“高级开发”要对许多其他专业领域要有基本的了解,而且要再本专业领域或多个本专业领域拥有深入的知识和技能。


例如:



  1. 我从来没有构建过分布式微服务系统,但是我知道这个系统能决什么问题,而且我大概了解构造他们的不同方法。

  2. 我从来没有在实际生产中应用前端监控平台,但是我知道他能解决什么样的问题。


就在几周前,我们公司进行数据治理的时候,我向我们应用服务团队推荐了一些使用“落地表”,“增加表索引”和“使用缓存”优化数据库性能的组合方案。我近两年没有再操作过数据库,甚至没建过索引,但是我知道它们组合起来能解决什么问题。


同样,这个也是在 什么是“高级开发” 中提到的一个“高级开发”的关键特征:在其他专业相关领域有一定经验🖥️


❓ “高级开发”应该有怎么样的知识深度?


同样,在 “T” 的垂直领域,“高级开发”应该在自己的专业领域有深入的研究,具备完整的知识和技能(这个是在开发领域的立足之本)。


也就是 什么是“高级开发” 提到的:精通团队所使用的核心技术,对其应用得非常熟练


无论如何,“高级开发”必须先是某个专业领域的专业人才为前提。


这些特定的领域可能是编程语言或框架:Vue、Angular、React、Three.js、Node、ava、Go等等


或者是一组特定的技术:系统架构、编程范式、专业解决方案、应用安全、网络安全等等


甚至是特定行业的针对性了解:医疗安全体系、金融安全体系等等


❓ 我是怎么成长过来的?


如果以我自己的职业生涯为案例。


我的第一份工作是在一家国内知名的PCB行业民企的IT部门,当时该企业内部的“OA”系统正在进行重构和维护,目标是想让OA系统以一个全新现代化的面貌展现给公司的全体职员。


可是这个系统很旧很烂,而且我需要不仅仅是在单个领域,而是在前端、后端、数据库等方面都要着手进行改造。😱


在这个过程中,因为老旧的OA系统的后端是使用VB语言开发的,而我实在是不愿意花大量的时间在其上学习这类老旧开发语言。


于是在任职的三年期间,我从0开始为公司搭建了一个使用Node的转发服务,并且基于这个Node服务,我构建了很多新的功能。👏


虽然这些功能看起来技术难度都不高,但是整个过程因为都经过自己的双手,确实让自己成长迅速,自己也学到了很多东西,包括很多不该学的(我甚至可以直接操作生产服务器和读取生产数据库)。


当然这些过程中也遇到了很多非常复杂的业务逻辑,这促使我寻找一些标准的代码实践。


为了解决这些问题,我花了很多时间(当然包括下班时间)学习了一些高级的编程知识,比如DDD,面向切面编程,设计模式等等。


然后我就会在工作中尝试使用这类代码实践,同事也会在这个时候问我这些东西怎么使用。


因为这样的环境,我不断地主导和帮助我的团队解决了很多代码组织和业务实践的难题。


而这些经验也让我在找下一份工作的时候更有优势。😎


📌 我想成为一名“高级开发”


看看自己是不是能做到下面这些事情:



  1. 你知道你真正想要深入的技术栈,并且真的在深入研究它们

  2. 每天都有学习的时间

  3. 不害怕承担难度高但是有价值的项目

  4. 真的奋斗在一线编程,而不是在管理岗位摸鱼

  5. 真的了解自己的“T”型技能树

  6. 如果你还不知道自己该学习什么,开始规划自己想拥有什么技能

  7. 了解你学的技能能解决什么问题,而不是因为热门才学习

  8. 了解设计模式(别以为设计模式不重要,它们是大多数领域的通用原则!)

  9. 如果你的工作在你的舒适区,建议你转向更有挑战的工作


🚩 避免成为“高级初学者”


现实中很多人可能并不拥有“高级开发”的职位,但是其他们已经拥有了高级开发的能力


不要对职位盲目崇拜,在国内许多地方,很多人都有高级开发工程师的头衔,甚至叫做“前端专家”


但是其实,他们可能:



  1. 他们已经在该岗位工作了好几年

  2. 他们面试很厉害,就是那个时候评了这个职级


这种情况在国内无处不在,当然也无法改变。我们只要意识到,他们可能并不具备高级开发的素养,不要盲目地模仿公司中地所谓的高级开发人员的代码,可能这些人在多年前就一直在这个舒适区待着,从来没有成长。


不要因为选错了导师而阻碍了自己成长。




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

【干货分享】安卓加固原理分享

App会面临的风险 我们首先了解一下为什么需要加固,尤其是安卓APP,下面是App目前会面临的各种风险: 而通过进行安卓加固,可以降低应用程序遭受各种恶意攻击的风险,保护用户数据和应用程序的安全性,增强用户对应用程序的信任度。 安卓加固的原理 安卓应用程序的...
继续阅读 »



App会面临的风险


我们首先了解一下为什么需要加固,尤其是安卓APP,下面是App目前会面临的各种风险:


image.png


而通过进行安卓加固,可以降低应用程序遭受各种恶意攻击的风险,保护用户数据和应用程序的安全性,增强用户对应用程序的信任度。


安卓加固的原理


安卓应用程序的加固涉及多个方面和技术。我列举了一些常见的安卓加固原理以及相关的示例代码:


1. 代码混淆(Code Obfuscation):


代码混淆通过对应用程序代码进行重命名、删除无用代码、添加虚假代码等操作,使代码难以阅读和理解,增加逆向工程的难度。常用的代码混淆工具包括ProGuard和DexGuard。


示例代码混淆配置(build.gradle):

android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

2. 反调试(Anti-debugging):


反调试技术可以检测应用程序是否在被调试,并采取相应的防护措施,例如中断应用程序的执行、隐藏关键信息等。


示例代码检测调试状态:

import android.os.Debug;

if (Debug.isDebuggerConnected()) {
// 应用程序正在被调试,采取相应的措施
}

3. 加密和密钥管理(Encryption and Key Management):


加密可以用于保护应用程序中的敏感数据。对于密钥管理,建议使用安全的存储方式,例如使用Android Keystore系统来保存和管理密钥。


示例代码使用AES加密算法对数据进行加密和解密:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionUtils {
private static final String AES_ALGORITHM = "AES";

public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedData);
}
}

4. 动态加载和反射(Dynamic Loading and Reflection):


通过动态加载和反射技术,可以将应用程序的核心逻辑和敏感代码进行动态加载和执行,增加逆向工程的难度。


示例代码使用反射加载类和调用方法:

try {
Class<?> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("myMethod");
method.invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}

5. 安全存储(Secure Storage):


对于敏感数据(如密码、API密钥等),建议使用安全的存储方式,例如使用Android Keystore系统或将数据加密后存储在SharedPreferences或数据库中。


示例代码使用Android Keystore存储密钥:

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.security.KeyStore;

public class KeyStoreUtils {
private static final String KEY_ALIAS = "my_key_alias";

public static void generateKey() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setRandomizedEncryptionRequired(false)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(spec);
keyGenerator.generateKey();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

以上就是简单的代码示例。


目前市场上加固的方式


目前市面上加固的方式一般是一套纵深防御体系,分别从代码安全、资源文件安全、数据安全和运行时环境安全维度提供安全保护。同时针对每个维度又进行了不同层次的划分,加固策略可依据实际场景进行定制化调配,安全和性能达到平衡。


所以一般会从下面几个方面进行加固:


image.png


而不同的公司或者APP对于加固的要求又会不一样,所以具体的使用,其实还是要看具体的场景,等之后有机会再展开详细讲一下。


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

Android 开发还有必要深耕吗?现状怎么样?未来前景将会怎样?

截止到今天,Android的生态发生了不少变化 以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入An...
继续阅读 »

截止到今天,Android的生态发生了不少变化


以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入Android开发行业。Android招聘市场的需求逐渐被填充,招聘要求逐步提高……


随着“互联网寒冬”的到来,大批互联网公司纷纷倒闭,大厂也纷纷裁员节流,人才供给大幅增加、需求大幅降低,造成当时的市场迅速达到饱和。培训出来的初级Android开发找不到工作,大厂被裁员的Android开发放不下薪资要求,这批人找不到工作,再加上当时自媒体的大肆渲染,Android开发可不就“凉了”吗?


毫不夸张的说,早期说得上四大组件稍微能做一点点,拿个15-20k是比较轻松的,要是你还有过完整开发经验,30k真是一点都不过分,而在“寒冬”之后,当招聘市场供给过剩时,面试官有了充分的选择权,你会四大组件,那我就有完整App独立开发经验,另一人或许有过十万级App开发经验,你说面试官会收下谁呢?岗位招聘要求也是在这时迅速拔高,整个Android市场逐渐趋于平稳,大家感觉Android开发来到了内卷期……


再来说现在:


Android凉了吗?


其实并不是Android凉了,而是技术不过硬的Android凉了


被高薪晃晕了头脑放不下身段的假高工凉了


现在的Android市场,Android初级工程师早就已经严重饱和了,供远大于求。这就导致了很多Android开发会面临被优化、被毕业、找不到工作这种情况,然后这部分人又出来说Android凉了,如此循环之下,以致于很多人都觉得Android凉了……


其核心原因只是Android开发市场由鼎盛的疯狂逐渐趋于平稳


这里也给出Android开发薪资/年限图给大家参考:


互联网公司Android开发薪资、年限.jpg


也不缺少学历突出的、能力突出的、努力突出的,这三类都可以拿到比图中同级别更可观的薪资


当然,我们并不能以薪资作为职级的标准,决定一个Android工程师到底是初级、中级、高级还是资深的,永远都不会是开发年限!


只有技术才能客观的作为衡量标准!


不管是几年经验,如果能力与工作年限不匹配,都会有被毕业的风险,如果掌握的技术达不到对应职级的标准,那别想了,毕业警告……


在很多人觉得Android凉了的时候,也不乏有Android开发跳槽进大厂拿高薪,不少在闷头提升技术水平,迄今为止还没有听过哪个Android开发大牛说“Android凉了”,当大家达到一定的高度之后,就会得知谁谁谁跳槽美团,几百万;某某某又跳进了阿里、腾讯……


不管在任何行业,任何岗位,初级技术人才总是供大于求;不管任何行业、岗位,技术过硬的也都是非常吃香的!


在初级市场”凉了“的同时,高级市场几乎是在抢人!


很多高薪、急招岗位挂上了招聘网站,往往一整年都面试不了几场,自打挂上来,就没动过了……


image.png
所以说,Android开发求职,质量才是关键!


再说到转行问题


我一直都比较佩服有大勇气转行的朋友,因为转行需要我们抛弃现有的知识技能,重新起航


佩服归佩服,身边不少之前是Android开发的朋友转行Java、Python,但他们对于目前市场还是过于乐观了,Python很火,它竞争不大吗?部分转行从0开始的,甚至连应届生都比不过~


不要轻易转行,如果要转一定要尽早转


转行有两种我认为是正常的,一种是行业消失了、没落了,继续留在业内无法施展才华。另一种是兴趣压根就不在本行,因此选一个自己感兴趣的。而现在大部分转行都是为了跟风,为了那看得见但摸不着的”风口“,而忽略了长期的发展潜力。


image.png


不管是学习力也好,精力也好,大部分人在35岁之前都属于加速期,加速期的一些选择,决定了35岁之后到底是上升还是衰落。


以Android开发转Python来说,一个Android高级转行Python会变为Python初级,这时从事Python的人都在加速提高,要想赶超在你之前的拥有同样学习力的人是不可能办到的,这就导致在转行前期极为被动,还要保证在35岁前成为Python专家或者Leader才有可能在35岁后不进入衰落期,当然这时你的Android基本也就荒废了,不说很难成为专家,高级也成为了一个很大的门槛。


如果你还想要在对应的技术领域走的更远,就不要轻易选择转行,如果实在想要转,那么越早越好、越快越好,你的竞争者都在加速提升技术水平,职场上,没人会停下等你的……


转行大部分都产生不了质变


我们所说的质变可以理解为在一个技术领域的大幅提升,或者是不相关领域的跨界


比如由高级开发变为专家,或者是由高级开发升到Leader,再或者跨界开始做一些技术相关的博客、培训、演讲、出书等等而被人所熟知。


凡是能帮助你在职业生涯中后期进入上升期的都可以看做是一次质变,而转行很少是质变,更多的都是倒退回到原点重新出发,形象点来说,你只是换了个不同的砖头接着搬砖而已。因此我们更应该去追求质变,而不是平行或者倒退,一次倒退或许可以承受,多次倒退就很难在职业生涯中后期再进入上升期。


其实不少转行的人都没有起到积极作用,毕竟都是从0开始,精进到专家绝不是一朝一夕可以完成的


或许到时又会有同样的问题:



前端凉了?前景怎么样?


Java凉了?前景怎么样?


大数据凉了?前景怎么样?


人工智能凉了?前景怎么样?


……



而另一类人,其实不管在哪个行业都可以混的风生水起!


如果是这种,那么想必也不需要考虑转行了。


所以根本不用想着Android凉了或是说要转行,与其焦虑不安,不如努力提升技术水平,毕竟在这时代,有硬技术的人到哪都吃香。


我们想要往高级进阶,建立属于自己的系统化知识体系才是最重要的,高工所需要掌握的技术不是通过蹭热点和玩黑科技,而是需要真正深入到核心技术的本质,知晓原理,知其然知其所以然。


可能不少人会觉得Android技术深度不深,技术栈不庞大,Android职业发展有限,这就真是个天大的误解。


先说技术上,Android的技术栈随着时间的推移变得越来越庞大,细分领域也越来越多,主要有应用开发、逆向安全、音视频、车联网、物联网、手机开发和SDK开发等等,每个细分领域都有很多技术栈组成,深度都足够精深,就拿所有细分领域通用的Android系统底层源码来说,就会叫你学起来生不如死。


还有AI、大数据、边缘计算、VR/AR,很多新的技术浪潮也都可以结合进移动开发的技术范畴……


那么现在Android怎么学?学什么?


这几年Android新技术的迭代明显加速了,有来自外部跨平台新物种的冲击,有去Java化的商业考量,也有Jetpack等官方自建平台的加速等多种原因。


作为Android开发者,我们需要密切关注的同时也不要盲目跟随,还是要认清趋势,结合项目现状学习。


Kotlin


Kotlin已经成为Android开发的官方语言,Android的新的文档和Sample代码都开始转向 Kotlin,在未来Java将加速被 Kotlin替代。


刚推出时,很多人都不愿意学习,但现在在面试中已经是经常会出现了,很多大公司也都已经拥抱新技术了。现在Kotlin是一个很明显的趋势了,不少新技术都需要结合Kotlin来使用,未来在工作中、面试中所占的比重肯定会更大。


Jetpack+Compose


Jetpack的意义在于帮我们在SDK基础上提供了一系列中间件工具,让我们可以摆脱不断造轮子抄轮子的窘境。同类的解决方案首先考虑Jetpack其次考虑第三方实现,没毛病。


Jetpack本身也会不断吸收优秀的第三方解决方案进来。所以作为开发者实时关注其最新动态就可以了。


Compose是Google I/O 2019 发布的新的声明式的UI框架。其实Google内部自2017年便开始立项,目前API已稳定,构建,预览等开发体验已经趋于完整。


而且新的设计思想绝对是趋势,已经在react和flutter等前端领域中得到验证,ios开发中同期推出的swiftUI更是证明了业界对于这种声明式UI开发趋势的共识。这必将是日后Android app极为重要的编程方式。


开源框架底层原理


现在的面试从头到尾都是比较有深度的技术问题,虽然那些问题看上去在网上都能查到相关的资料,但面试官基本都是根据你的回答持续深入,如果没有真正对技术原理和底层逻辑有一定的了解是无法通过的。


很多看似无理甚至无用的问题,比如 “Okhttp请求复用有没有了解”,其实是面试官想借此看看你对网络优化和Socket协议的理解情况和掌握程度,类似问题都是面试官想借此看看你对相关原理的理解情况和掌握程度,甚至进而引伸到你对架构,设计模式的理解。只有在熟知原理的前提下,你才能够获得面试官的青睐。


Framework


Framework作为Android的框架层,为App提供了很多API调用,但很多机制都是Framework包装好后直接给App用的,如果不懂这些机制的原理,就很难在这基础上进行优化。


像启动监控、掉帧监控、函数插桩、慢函数检测、ANR监控,都需要比较深入的了解Framework,才能知道怎么去监控、利用什么机制监控、函数插桩插到哪里、反射调用该反射哪个类哪个方法哪个属性……


性能优化


性能优化是软件工程的深水区,也是衡量一个程序员能力高低的标准


想要搞清楚性能优化,必须对各种底层原理有着深度的了解,对各种 case非常丰富的经验;很多朋友经常遇到措手不及的问题,大多是因为对出现问题的情况和处理思路模糊不清,导致此原因就是因为没有彻底搞懂底层原理。


性能优化始终穿插在 App 整个研发生命周期中,不管是从 0 到 1 的建立阶段,还是从 1 到 N 打磨阶段,都离不开性能优化。


音视频


伴随着疫情的反复以及5G的普及,本就火爆的音视频技术是越来越热,很多大小厂在这几年也都纷纷入局。但音视频学习起来门槛比较高,没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。


招聘市场上,同级别的音视频开发要比应用开发薪资高出30%以上。


车载


在智能手机行业初兴起时,包括BAT在内许多传统互联网企业都曾布局手机产业,但是随着手机市场的基本定型,造车似乎又成了各大资本下一个追逐的方向。百度、小米先后宣布造车,阿里巴巴则与上汽集团共同投资创立了,面向汽车全行业提供智能汽车操作系统和智能网联汽车整体解决方案的斑马网络,一时间造车俨然成了资本市场的下一个风口。


而作为移动端操作系统的霸主Android,也以一种新的姿态高调侵入造车领域


关于学习


在学习的过程中,可能会选择看博客自学、看官方文档、看书、看大厂大牛整理的知识点文档、看视频,但要按学习效率来排序的话:报培训班>看视频>知识点>书籍>官方文档>博客


报班,可能很多朋友对于报班这个事情比较抵触,但不可否认,报一个培训班是可以学到很多深层次的、成体系的技术,像之前读书一样,都是捣碎了喂给你,并且培训班其实对于新技术、新趋势是相当敏锐的,可以第一时间接触,也会规避开自学的烦恼。


看视频,基本也是由别人捣碎知识点教会你,但较之培训班的话,视频的知识成体系吗?有没有过时?


大厂大牛整理的知识点文档,大厂大牛技术还是比较可靠的,这类型的知识点文档初版基本是可以放心享用,但如果只是少数人甚至是一个人进行维护的话,当整个文档的知识体系越来越广时,其中的部分知识点可能已经过时但一直没有时间更新


书籍,相比前者就更甚了,一个技术出来,先研究、再整理、修正……直到最后出版被你买到,中间经过的这段时间就是你落后于其他人的地方了,但其中的知识点基本可以肯定成体系、无重大错误。学习比较底层的,不会有很大改动的知识点还是相当不错的。


官方文档,这一块也是我思考了很久才排好,官方文档往往是第一手资源,对于有能力看懂的朋友来说,可以直接上手品尝。但其实很多开发拿到官方文档还是看的一知半解,再者说,自己看可能会有遗漏,还是没有别人一点一点将重点翻开来解读更好


博客,网络上的博客水平参差不齐,通常大家擅长的也不是同一个技术领域,往往是学习一块看A的,另一块看B的,而且网上很多博客都是抄来自己记录的,很多API已经过时了,甚至不少连代码都是完全错误的,这样的学习,可想而知……


最后


一些个人见解,也参考了不少大佬的观点,希望可以给大家带来一些帮助,如果大家有什么不同看法,也欢迎在评论区一起讨论交流


Android路漫漫,共勉!


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

Binder Java 层服务注册过程分析

1. Java 层整体框架 在分析之前,我们要明白,Java 只是一层方便 Java 程序使用的接口,Binder 的核心功能实现都是通过 JNI 调用到 Native 层来实现的,这里先给出 Java 层的整体框架图: 接下来几篇文章我们逐步分析,解密整张...
继续阅读 »

1. Java 层整体框架


在分析之前,我们要明白,Java 只是一层方便 Java 程序使用的接口,Binder 的核心功能实现都是通过 JNI 调用到 Native 层来实现的,这里先给出 Java 层的整体框架图:



接下来几篇文章我们逐步分析,解密整张框架图。


2. 服务注册


Binder 程序示例之 Java 篇 中介绍的示例程序中,Server 端我们使用如下代码注册我们定义的服务:

ServiceManager.addService("hello", new HelloService());

addService 是 frameworks/base/core/java/android/os/ServiceManager.java 中定义的静态方法:

@UnsupportedAppUsage
public static void addService(String name, IBinder service) {
addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}

@UnsupportedAppUsage
public static void addService(String name, IBinder service, boolean allowIsolated) {
addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}

@UnsupportedAppUsage
public static void addService(String name, IBinder service, boolean allowIsolated,int dumpPriority) {
try {
getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
} catch (RemoteException e) {
Log.e(TAG, "error in addService", e);
}
}

通过层层调用,调用到 getIServiceManager().addService(name, service, allowIsolated, dumpPriority); :


2.1 getIServiceManager()


我们先看看 getIServiceManager,该方法是定义在 ServiceManager 类中的静态方法:

//frameworks/base/core/java/android/os/ServiceManager.java
@UnsupportedAppUsage
private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}

// 等价于 new ServiceManagerProxy(new BinderProxy(0))
// 但是实际过程有点曲折
sServiceManager = ServiceManagerNative
.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
return sServiceManager;
}

接着我们逐一分析三个方法调用:

BinderInternal.getContextObject()
Binder.allowBlocking
ServiceManagerNative.asInterface

2.1.1 BinderInternal.getContextObject

//frameworks/base/core/java/com/android/internal/os/BinderInternal.java
// 返回一个 BinderProxy 对象
@UnsupportedAppUsage
public static final native IBinder getContextObject();

getContextObject 是一个 native 方法,在之前的文章中我们提到 BinderInternal 在进程启动时注册了其 native 方法,其 native 实现在 frameworks/base/core/jni/android_util_Binder.cpp 中:

static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
//此处返回的是 new BpBinder(0)
sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
//此处返回的是 new BinderProxy()
return javaObjectForIBinder(env, b);
}

接着看 getContextObject 的实现:

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
return getStrongProxyForHandle(0);
}

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
sp<IBinder> result;

AutoMutex _l(mLock);

handle_entry* e = lookupHandleLocked(handle);

if (e != nullptr) {
IBinder* b = e->binder;
if (b == nullptr || !e->refs->attemptIncWeak(this)) {
if (handle == 0) {
Parcel data;
status_t status = IPCThreadState::self()->transact(
0, IBinder::PING_TRANSACTION, data, nullptr, 0);
if (status == DEAD_OBJECT)
return nullptr;
}

//走这里
b = BpBinder::create(handle);
e->binder = b;
if (b) e->refs = b->getWeakRefs();
result = b;
} else {
result.force_set(b);
e->refs->decWeak(this);
}
}

return result;
}

BpBinder* BpBinder::create(int32_t handle) {
int32_t trackedUid = -1;
if (sCountByUidEnabled) {
trackedUid = IPCThreadState::self()->getCallingUid();
AutoMutex _l(sTrackingLock);
uint32_t trackedValue = sTrackingMap[trackedUid];
if (CC_UNLIKELY(trackedValue & LIMIT_REACHED_MASK)) {
if (sBinderProxyThrottleCreate) {
return nullptr;
}
} else {
if ((trackedValue & COUNTING_VALUE_MASK) >= sBinderProxyCountHighWatermark) {
ALOGE("Too many binder proxy objects sent to uid %d from uid %d (%d proxies held)",
getuid(), trackedUid, trackedValue);
sTrackingMap[trackedUid] |= LIMIT_REACHED_MASK;
if (sLimitCallback) sLimitCallback(trackedUid);
if (sBinderProxyThrottleCreate) {
ALOGI("Throttling binder proxy creates from uid %d in uid %d until binder proxy"
" count drops below %d",
trackedUid, getuid(), sBinderProxyCountLowWatermark);
return nullptr;
}
}
}
sTrackingMap[trackedUid]++;
}
//走这里
return new BpBinder(handle, trackedUid);
}

代码看着很繁琐,实际流程其实很简单就是 new BpBinder(0)


接着看 javaObjectForIBinder 的实现:

//frameworks/base/core/jni/android_util_Binder.cpp

//当前情景下, val 的类型是 BpBinder
jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
if (val == NULL) return NULL;

if (val->checkSubclass(&gBinderOffsets)) {
// It's a JavaBBinder created by ibinderForJavaObject. Already has Java object.
jobject object = static_cast<JavaBBinder*>(val.get())->object();
LOGDEATH("objectForBinder %p: it's our own %p!\n", val.get(), object);
return object;
}

//构造 BinderProxyNativeData 结构体
BinderProxyNativeData* nativeData = new BinderProxyNativeData();
nativeData->mOrgue = new DeathRecipientList;
nativeData->mObject = val;

//gBinderProxyOffsets 中保存了 BinderProxy 类相关的信息
//调用 Java 层 GetInstance 方法获得一个 BinderProxy 对象
jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,
gBinderProxyOffsets.mGetInstance, (jlong) nativeData, (jlong) val.get());
if (env->ExceptionCheck()) { //异常处理
// In the exception case, getInstance still took ownership of nativeData.
return NULL;
}
BinderProxyNativeData* actualNativeData = getBPNativeData(env, object);
if (actualNativeData == nativeData) {
// Created a new Proxy
uint32_t numProxies = gNumProxies.fetch_add(1, std::memory_order_relaxed);
uint32_t numLastWarned = gProxiesWarned.load(std::memory_order_relaxed);
if (numProxies >= numLastWarned + PROXY_WARN_INTERVAL) {
// Multiple threads can get here, make sure only one of them gets to
// update the warn counter.
if (gProxiesWarned.compare_exchange_strong(numLastWarned,
numLastWarned + PROXY_WARN_INTERVAL, std::memory_order_relaxed)) {
ALOGW("Unexpectedly many live BinderProxies: %d\n", numProxies);
}
}
} else {
delete nativeData;
}

//返回 BinderProxy
return object;
}

native 代码调用了 BinderProxy 的 getInstance 方法:

// frameworks/base/core/java/android/os/BinderProxy.java
private static BinderProxy getInstance(long nativeData, long iBinder) {
BinderProxy result;
synchronized (sProxyMap) {
try {
result = sProxyMap.get(iBinder);
if (result != null) {
return result;
}
result = new BinderProxy(nativeData);
} catch (Throwable e) {
// We're throwing an exception (probably OOME); don't drop nativeData.
NativeAllocationRegistry.applyFreeFunction(NoImagePreloadHolder.sNativeFinalizer,
nativeData);
throw e;
}
NoImagePreloadHolder.sRegistry.registerNativeAllocation(result, nativeData);
// The registry now owns nativeData, even if registration threw an exception.
sProxyMap.set(iBinder, result);
}
return result;
}

代码很繁琐,但是从结果上来说还是比较简单的:



  • getContextObject 函数 new 了一个 BpBinder(c++结构体),其内部的 handle 是 0

  • javaObjectForIBinder 函数 new 了一个 BinderProxy(Java 对象),其内部成员 mNativeData 是一个 native 层指针,指向一个 BinderProxyNativeData,BinderProxyNativeData 的成员 mObject 指向上述的 BpBinder。


整体结构用一个图表示如下:



2.1.2 Binder.allowBlocking

    //这里传入的是 BinderProxy 对象
public static IBinder allowBlocking(IBinder binder) {
try {
if (binder instanceof BinderProxy) { //走这里
((BinderProxy) binder).mWarnOnBlocking = false;
} else if (binder != null && binder.getInterfaceDescriptor() != null
&& binder.queryLocalInterface(binder.getInterfaceDescriptor()) == null) {
Log.w(TAG, "Unable to allow blocking on interface " + binder);
}
} catch (RemoteException ignored) {
}
return binder;
}

这个方法比较简单,主要是设置 binder 的成员变量 mWarnOnBlocking 为 false。从名字来看,作用是允许阻塞调用。


2.1.3 ServiceManagerNative.asInterface

    //frameworks/base/core/java/android/os/ServiceManagerNative.java
//传入的参数是 BinderProxy
@UnsupportedAppUsage
static public IServiceManager asInterface(IBinder obj)
{
if (obj == null) {
return null;
}

//返回 null
IServiceManager in =
(IServiceManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}

//走这里,构建一个 ServiceManagerProxy
return new ServiceManagerProxy(obj);
}

//从名字来看,本来要做缓存的,但是没有做
// frameworks/base/core/java/android/os/BinderProxy.java
public IInterface queryLocalInterface(String descriptor) {
return null;
}


最终是构建一个 ServiceManagerProxy 结构体。其内部持有一个 BinderProxy 。


至此,getIServiceManager 的整体流程就分析完了。


2.2 addService

    // frameworks/base/core/java/android/os/ServiceManagerNative.java
public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);
data.writeStrongBinder(service);
data.writeInt(allowIsolated ? 1 : 0);
data.writeInt(dumpPriority);
mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
reply.recycle();
data.recycle();
}

构造两个 Parcel 结构,然后调用 mRemote.transact 发起远程过程调用。


mRemote 就是 new ServiceManagerProxy 时传入的 BinderProxy:

 public ServiceManagerProxy(IBinder remote) {
mRemote = remote;
}

进入 frameworks/base/core/java/android/os/BinderProxy.java 查看:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

//......

try {
//关注这里
return transactNative(code, data, reply, flags);
} finally {
//......
}
}

//native 方法
public native boolean transactNative(int code, Parcel data, Parcel reply,int flags) throws RemoteException;


transact 会调用 transactNative 发起远程调用,transactNative 是一个 native 方法,具体实现在 frameworks/base/core/jni/android_util_Binder.cpp

// obj 对应类型为 BinderProxy
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
if (dataObj == NULL) {
jniThrowNullPointerException(env, NULL);
return JNI_FALSE;
}

// Java 对象 转为 c++ 对象
Parcel* data = parcelForJavaObject(env, dataObj);
if (data == NULL) {
return JNI_FALSE;
}

// Java 对象 转为 c++ 对象
Parcel* reply = parcelForJavaObject(env, replyObj);
if (reply == NULL && replyObj != NULL) {
return JNI_FALSE;
}

//拿到 BinderProxyNativeData 成员的 mObject,实际是一个 BpBinder
IBinder* target = getBPNativeData(env, obj)->mObject.get();
if (target == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!");
return JNI_FALSE;
}

ALOGV("Java code calling transact on %p in Java object %p with code %" PRId32 "\n",
target, obj, code);


bool time_binder_calls;
int64_t start_millis;
if (kEnableBinderSample) {
// Only log the binder call duration for things on the Java-level main thread.
// But if we don't
time_binder_calls = should_time_binder_calls();

if (time_binder_calls) {
start_millis = uptimeMillis();
}
}

//BpBinder 发起远程调用
status_t err = target->transact(code, *data, reply, flags);

if (kEnableBinderSample) {
if (time_binder_calls) {
conditionally_log_binder_call(start_millis, target, code);
}
}

if (err == NO_ERROR) {
return JNI_TRUE;
} else if (err == UNKNOWN_TRANSACTION) {
return JNI_FALSE;
}

signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
return JNI_FALSE;
}


可以看出,绕了一圈还是通过 native 层的 BpBinder 发起远程调用,native 层的调用过程可以参考之前的文章Binder 服务注册过程情景分析之 C++ 篇


关于


我叫阿豪,2015 年本科毕业于国防科技大学指挥自动化专业,毕业后,从事信息化装备的研发工作。主要研究方向为 Android Framework 与 Linux Kernel,2023年春节后开始做 Android Framework 相关的技术分享。


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

那年毕业前,我花了一整个上午的时间走遍整个校园

web
又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情... 其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片。 那些照片 🔼 这张照片...
继续阅读 »

又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情...


其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片


那些照片


木头 2023-06-06 19.29.22.jpeg


🔼 这张照片左边黑白的是一天前拍的,右边是今天拍的,当时毕业典礼刚结束,我们要从体育馆走回计算机学院,人很多,大家边走边笑着,感觉就像... 对,就像是刚入学报到的那一天,那天也像这天一样热闹。今天的路上没有人,天气很好,只有静悄悄的阳光撒在地面上。


木头 2023-06-06 19.35.20.jpeg


🔼 这张照片是宿舍楼旁边的一条小路,路中间是校医院。七天前吃饭的时候路过这里,随手拍了一张照片,今天有风,落了一地小花,还挺好看。


木头 2023-06-06 19.40.15.jpeg


🔼 这是学校操场,28天前正好是在做毕业设计的阶段,在宿舍一坐就是一天,没思路的时候,我就喜欢一个人来看台上坐一会儿,戴着耳机灯歌,看下面跑步的人,等操场上的灯关了,我就回去。当时是一个傍晚,夕阳挺好看的。今天的天气很好,有很多人在跑步。


木头 2023-06-06 19.44.53.jpeg


🔼 老照片拍摄于596天前,快两年了吧... 那时候应该大三,中午刚下课,大家走在南广场上,一块去食堂,那天的云很好看。当时在拍新照片的时候,心里还是有点伤感,明天就要离校了,很多人很可能就再也见不到了...(现在来看,的确是这样)


木头 2023-06-06 19.49.36.jpeg


🔼 上学的时候,每天早晨我都起的很早,六点多就从宿舍出来,其实也不是为了学习或者什么,我就喜欢走在安静的校园里,阳光洒在草丛间,偶尔有鸟叫声,我觉得这种感觉很美好。拍左边图的时候应该是个深秋了吧,树叶落了一地,而右边又正好是一个盛夏,树木生长的正好!这张照片的对比感让我感到无比惊喜!


拍摄心得


其实当时拍的比这些成品照片要多得多,总共拍了20多个地方吧,最终只合成出了78张可以用的,废片率相当高


因为什么呢?


原因是当时的我没有一个准确的参照物,在拍新照片时我一般会历经以下步骤:


1、先拿出手机看看老照片的角度和位置


2、举起ipad,凭感觉走到自己认为准确的位置上


3、拍一张看一下效果


4、满意就再拍两张当备份,不满意就继续重复以上步骤,直到排除满意的


好像一个递归方法!用伪代码实现一下就是:


const takePhoto = () => {
// 1、拿出老照片看角度 + 位置
const { position } = showOldPhoto();

// 2、拿出ipad,走到对应的位置上
walkToPosition(position);

// 3、拍一张看看效果
const { isOK } = takeSomePhoto();

// 4、判断是否满意,满意就结束,不满意就继续递归
!isOk && takePhoto();
}

当然,我也没那么工匠精神,我可能还得再加一个结束条件


const takePhoto = () => {
// 如果拍5次还不满意,就
if(reTryTime > 5) {
return;
}

// others
}

这个过程是比较重复且枯燥的,当然可以适当优化一下比如我可以在ipad上看照片,这样就省掉手机这一步了,另外可以在拍摄时不断地切换照片和相机app,这样就可以稍微快点看到当前位置对不对了...


em... 当时的我真的希望有一个工具能来辅助我拍这些照片!


噢噢噢!


现在的我可以很开心的跟那时候的我说,有了!现在有了!


你可以去微信小程序里搜:历旧弥新


你就可以搜到一个看起来还蛮专业的一个小程序,UI做的也不错,不丑!


它好像提供了一个你非常需要的功能:和旧照片来一场对话


你可以非常轻松的用它来拍一张新旧照片合成的照片,


就像下图:


WechatIMG301.jpeg


你可以将你的旧照片半透明的状态覆盖到相机上(就像左边的图),可以缩放平移,把它放在准确的位置上之后,然后你就可以非常轻易的去拍摄相同角度的照片了!


嗯... 听到这里是不是感觉出来这是一个广告了哈哈哈,没错,那就是了!


打广告!


对,这就是我基于四年前的想法,最近花了几个周末开发的一个小程序,历旧弥新


名字取自 历久弥新 => ,代表一种新旧交替的含义


来看下小程序首页


木头 2023-06-06 21.31.46.jpeg


它一共包含四个功能:


1、与旧照片来一次对话


2、已有关联的照片拼接


3、快速找一个相同的拍照姿势


4、异地也可以来合照


我们也提供了比较好的一些用户拍摄过的照片,放在首页的下半部分:


木头 2023-06-06 21.54.15.jpeg


木头 2023-06-06 21.57.31.jpeg


你可以快速的进行 拍同款 !就可以拍摄类似的照片啦!


当然它或许也存在一些问题希望大家不要吝啬自己的建议,可以评论在下方哈!

作者:木头就是我呀
来源:juejin.cn/post/7242247549511663672

收起阅读 »

Vue KeepAlive 为什么不能缓存 iframe

web
最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下: <router-view v-slot="{ Component ...
继续阅读 »

最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:


  <router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>

看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。


我们先了解下 KeepAlive


KeepAlive (熟悉的可跳过本节)


被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现“假卸载”, 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated 和 deactivated


keepAlive 是需要渲染器支持的,在执行 mountComponent 时,如果发现是 __isKeepAlive 组件,那么会在上下文注入 move 方法。


function mountComponent(vnode, container, anchor) {
/**... */
const instance = {
/** ... */
state,
props: shallowReactive(props),
// KeepAlive 实例独有
keepAliveCtx: null
};

const isKeepAlive = vnode.__isKeepAlive;
if (isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
};
}
}

实现一个最基本的 KeepAlive,需要注意几个点



  1. KeepAlive 组件会创建一个隐藏的容器 storageContainer

  2. KeepAlive 组件的实例增加两个方法 _deActive_active

  3. KeepAlive 组件存在一个缓存的 Map,并且缓存的值是 vnode


const KeepAlive = {
// KeepAlive 特有的属性,用来标识
__isKeepAlive: true,
setup() {
/**
* 创建一个缓存对象
* key: vnode.type
* value: vnode
*/

const cache = new Map();
// 当前 keepAlive 组件的实例
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
// 创建隐藏容器
const storageContainer = createElement('div');

// 为 KeepAlive 组件的实例增加两个方法
instance._deActive = vnode => {
move(vnode, storageContainer);
};
instance._active = (vnode, container, anchor) => {
move(vnode, container, anchor);
};

return () => {
// keepAlive 的默认插槽就是要被缓存的组件
let rawVNode = slot.default();
// 不是组件类型的直接返回,因为其无法被缓存
if (typeof rawVNode !== 'object') {
return rawVNode;
}

// 挂载时,优先去获取被缓存组件的 vnode
const catchVNode = cache.get(rawVNode.type);
if (catchVNode) {
rawVNode.component = catchVNode.component;
// 避免渲染器重新挂载它
rawVNode.keptAlive = true;
} else {
// 如果没有缓存,就将其加入到缓存,一般是组件第一次挂载
cache.set(rawVNode.type, rawVNode);
}
// 避免渲染器真的把组件卸载,方便特殊处理
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
return rawVNode;
};
}
};

从上可以看到,KeepAlive 组件不会渲染额外的内容,它的 render 函数最终只返回了要被缓存的组件(我们称要被缓存的组件为“内部组件”)。KeepAlive 会对“内部组件”操作,主要是在其 vnode 上添加一些特殊标记,从而使渲染器能够据此执行特殊的逻辑。


function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (type === 'string') {
/** 执行普通的标签 patch */
} else if (type === Text) {
/** 处理文本节点 */
} else if (type === Fragment) {
/** 处理Fragment节点 */
} else if (typeof type === 'object') {
if (!n1) {
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor);
} else {
mountComponent(n2, container, anchor);
}
} else {
patchComponent(n1, n2, anchor);
}
}
}

function unmount(vnode) {
const { type } = vnode;
if (type === Fragment) {
/**... */
} else if (typeof type === 'object') {
if (vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree);
}
return;
}
}

从上面的代码我们可以看出,vue 在渲染 KeepAlive 包裹的组件时,如果有缓存过将执行 keepAliveInstance._activate,在卸载时将执行 keepAliveInstance._deActivate


原因


通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。


解决方案


思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。



  1. 在路由注册时,将 component 赋值为一个空组件


  {
path: "/chathub",
name: "chathub",
component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数
},


  1. 在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏


  <ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>


  1. 监听路由的变化,改变 iframe 的显/隐


const isChatHubPage = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const chatHubVisited = ref(false)

watch(
() => routes.path,
(value) => {
if (value === '/chathub') {
chatHubVisited.value = true
isChatHubPage.value = true
} else {
isChatHubPage.value = false
}
},
{
immediate: true
}
)
作者:莱米
来源:juejin.cn/post/7246310077233659941

收起阅读 »

Flutter 初探原生混合开发

转载请注明出处:juejin.cn/post/724677… 本文出自 容华谢后的博客 0.写在前面 现如今跨平台技术被越来越多的开发者提起和应用,从最早的Java到后来的RN、Weex,到现在的Compose、Flutter,大前端已经成为了趋势,很多公司...
继续阅读 »

转载请注明出处:juejin.cn/post/724677…


本文出自 容华谢后的博客



0.写在前面


现如今跨平台技术被越来越多的开发者提起和应用,从最早的Java到后来的RN、Weex,到现在的Compose、Flutter,大前端已经成为了趋势,很多公司为了节省成本,包括一些大厂已经在Android和iOS平台上使用了Flutter技术,效果还可以,贴近原生但是还会有一些卡顿的问题,好在Flutter目前还在不断的优化更新,希望越来越好吧。


Flutter从2017年发布到现在已经历经了6年,如果你现在创建一个Flutter项目,会发现已经支持了Android、iOS、Linux、MacOS、Web、Windows六大主流的操作系统平台,我以前经常会写一些在Windows上运行的小工具,使用java写的不仅复杂界面也不好看,用Flutter试了试,好像发现了新大陆,在PC上运行十分流畅,还直接支持在其他平台上运行,感觉十分不错,这也让我对未来Flutter的发展抱有期待。


Flutter开发有两种方式,一种是纯Flutter开发,一种是Flutter+原生的开发方式,正如上面所说的,Flutter在PC上运行十分流畅,可能是PC配置比较高的原因,但是在客户端上的运行效果却不如人意,启动有点慢,一些复杂列表有点卡,一些底层功能的API不支持,这就需要原生开发的介入,小部分原生+大部分Flutter开发可能是后续比较主流的一种开发方式。


本文主要讲的是在Android平台上,与Flutter混合开发的一些步骤,一起来看下吧。


1.准备


1.1 先贴下我用的开发环境:




  • 操作系统:Windows 10




  • IDE:Android Studio Flamingo




  • Android SDK:33




  • Gradle:8.0.2




  • JDK:17.0.7




  • Flutter:3.10.4




1.2 下载Flutter SDK


下载地址:docs.flutter.dev/get-started…


是个压缩包,解压到你存放开发环境的目录,然后在AS中打开 File->Settings->Languages&Frameworks,在里面配置一下SDK的路径就可以了。


1.3 配置环境变量


和Jdk一样,为了使用方便,还需要配置下环境变量,设置->关于->高级系统设置->环境变量,找到用户变量,在Path里面新增一个路径 flutter SDK的路径\bin,前面如果有值的话,别忘了在前面加个英文分号进行分割。


1.4 检测flutter状态


为了验证Flutter是否安装成功,打开cmd命令行,输入 flutter doctor 进行检测:


flutter doctor


如果出现上面的提示,是因为Android证书的问题,再输入 flutter doctor --android-licenses 进行修复:


不支持Jdk 1.8版本


可能会出现这样的错误,这个是因为JDK版本有点低,现在大部分还是用的1.8版本,安装配置下JDK 17就可以,再运行下flutter doctor,已经可以了:


flutter doctor通过


1.5 安装Flutter插件


在AS中打开 File->Settings->Plugins,安装下面两个插件:


插件


到这里,所有的准备工作就完成了,接下来去创建项目。


2.创建项目


首先创建一个标准的Android项目,在此基础上,打开 File->New->New Flutter Project 创建一个Flutter Module:


创建Flutter Module


注意Project location要选择你当前的工程目录,Project types选择Module,然后CREATE,看下创建好的目录结构:


目录结构


3.项目Flutter配置


打开项目根目录的settings.gradle配置文件,增加下面的配置:


// Flutter配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir,
'flutter_lib/.android/include_flutter.groovy'
))
include ':flutter_lib'


然后再修改下dependencyResolutionManagement,把FAIL_ON_PROJECT_REPOS 改成 PREFER_SETTINGS,增加flutter的maven仓库地址:


dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven {
allowInsecureProtocol = true
url "http://download.flutter.io"
}
}
}

找到flutter_lib->.android->Flutter->build.gradle,在android属性增加namespace,这个是Gradle 8.0新增的特性:


android {
namespace 'com.example.flutter_lib'
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
...
}

找到主app的build.gradle,在dependencies中引用flutter模块,注意模块名称是flutter,无论你创建的Moudle是什么名字,这里的名字都是flutter:


dependencies {
...
implementation project(':flutter')
}

4.开始使用


在清单文件中,增加下面的activity标签,注意这个Activity是SDK中自带的,不需要自己手动创建:


<application>
...

<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" />
</application>

在MainActivity写个跳转方法进行测试:


val intent = FlutterActivity
.withNewEngine()
.initialRoute("home")
.build(this)
startActivity(intent)

看下效果:


跳转效果


可以看到在点击跳转按钮后,有一个明显的停顿,这是因为初始化Flutter引擎比较慢导致的,那就提前初始化试试,在Application中初始化引擎:


class App : Application() {

override fun onCreate() {
super.onCreate()
// 创建 Flutter 引擎
val flutterEngine = FlutterEngine(this)
// 指定要跳转的flutter页面
flutterEngine.navigationChannel.setInitialRoute("main")
flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
// 这里做一个缓存,可以在适当的时候执行它,例如app里,在跳转前执行预加载
val flutterEngineCache = FlutterEngineCache.getInstance()
flutterEngineCache.put("default_engine_id", flutterEngine)
}
}

然后使用已经提前创建后的引擎再次跳转:


val intent = FlutterActivity
.withCachedEngine("default_engine_id")
.build(this)
startActivity(intent)

看下效果,已经非常丝滑了:


优化后跳转效果


5.写在最后


GitHub地址:github.com/alidili/Flu…


到这里,Flutter与原生混合开发的基本步骤就介绍完了,如有问题可以给我留言评论或者在GitHub中提交

作者:容华谢后
来源:juejin.cn/post/7246778558248058938
Issues,谢谢!

收起阅读 »

拉新、转化、留存,一个做不好,就可能会噶?

用户周期 对于我们各个平台来说(掘金也是),我们用户都会有一个生命周期:引入期--成长期--成熟期--休眠期--流失期。 而一般获客就在引入期,在这个时候我们会通过推广的手段进行拉新;升值期则发生在成长期和成熟期,在两个阶段,我们会通过各种裂变营销(比如红包、...
继续阅读 »

用户周期


对于我们各个平台来说(掘金也是),我们用户都会有一个生命周期:引入期--成长期--成熟期--休眠期--流失期。


而一般获客就在引入期,在这个时候我们会通过推广的手段进行拉新;升值期则发生在成长期和成熟期,在两个阶段,我们会通过各种裂变营销(比如红包、券、积分、满减等手段)去实现用户的转化;那到了休眠期和流失期,平台则会通过精准营销去实现用户的留存。


详细的表格可以看下面:


image.png


运营手法


讲完了用户周期,我们再来说一下运营手法(让大家做一只明白的羊)。目前比较主流的运营手法包括红包、优惠券、返现、积分、裂变营销、砍价、秒杀......


拼xx新人红包:


image.png


饿了X红包:


image.png


拼xx砍价


image.png


其他我们就不列举了,大家生活中肯定有很多体会。在不同的行业,我们被“安排”的方式是不一样的。我们拿3个行业来举例子:


image.png


潜在风险


那从企业的角度出发,在这些环节当中,有可能会出现如下的一些常见风险:客户端风险、账号安全风险、营销活动风险、交易支付风险、爬虫风险。我们一一来过一下这些风险:


1.客户端风险:


image.png


2.账户安全风险


image.png
3.营销活动风险


image.png
4.交易支付风险


image.png
5.爬虫风险


image.png


黑灰产风险


1.简单介绍


目前我们网络黑灰产的从业人数已经超过1000万(和今年毕业的大学生人数有的一比了),其产业造成的损失每年也已经超过千亿。如今数据泄露已经成为社会问题,也引起了各大企业的重视。并且有个点(可能会被喷),部分的黑灰产的安全攻防人员专业度已经超过我们很多安全技术人员了。毕竟只有千年做贼的,没有千年防贼的。


那在我们的AI技术加持之下,我们后续的黑灰产发展必将和产业链会进一步深度融合,同时目前有一个很显著的特征是:黑灰产正在尝试将自己的攻击行为隐藏在其他用户的行为之。


另外,目前受攻击比较多的行业会包括说电商、出行、政务、直播、广告、游戏、社交等,而分布的场景则是我们前面说的爬虫攻击、薅羊毛、账户风险、交易支付等等。


2.欺诈流程


我们来讲一下黑灰产一般的攻击手段,也就是欺诈的一个流程:


image.png


第一步:账号准备。这一步会包括说图里的社工、注册机这些比较常用和典型的,也有其他一些方式。


第二步:情报收集。这一步包括流程的体验以及工具的准备。


第三步:伺机而发。等待活动开始,直接上去薅羊毛


第四步:利益套现。他们会代理或者海鲜市场等等,进行套现。代理的话,游戏行业会用的更多,而海鲜市场,类似于电商的优惠券之类的,会用的比较多。


欺诈工具


目前主流的欺诈工具有如下几种:


1.模拟器:这个主要是针对弱防护的场景


2.设备牧场:现在是应该发展到了第三代全托管牧场。一般可以去做设备识别,针对环境监测和真机检测。


3.接码平台:这个大家应该很熟悉。我们公司做的是安全验证码,而来注册的一部分客户则来找的是接码平台。


4.打码平台:这个其实和接码平台是类似的,不过接码平台针对的是短信验证码,而打码平台针对的是滑动验证码、图片验证码等等。


举个例子:某宝KFC代下单服务泛滥
image.png


这块我就不介绍更多了,生怕有人学坏哈哈


防护措施


因为企业目前对这一块都比较重视,所以随之而来的安全产品目前也发展到了一定的阶段。总的来说,目前一般会采取如下的防御体系:


image.png


基本是3个平台+3个场景+2个服务


产品一般会组合使用(单个场景针对使用也是可以的),会包括:设备指纹+无感验证+端加固+安全SDK
平台:实施决策平台+智能建模平台+关联网络平台
场景:基本上针对的场景就是我前面说的那些营销场景


方案优势


那通过上面这一套方案,我们可以做到:


事前: 需要在事前事中事后多点进行布控,各环节分别进行防控。


事中: 事中的防控可以将更多的黑名单数据反馈到事前环节的判断。


事前: 事后的分析与建模可以将模型能力赋予事中的风险防控,同时也可以积累大量的黑样本供事前风险防控来使用。


image.png


基于标准数据接口进行的模块化组合设计,基于成熟的技术架构和技术优势,可定制、可扩展、可集成、跨平台,在个性化需求的处理方面,有着很好的优势。产品各个模块之间既可相互组合又可自定义配置,灵活的产品配置方式和架构设计思想,可结合不同的业务场景及系统状况进行相应的风险防控方案配置。


结语


在现在AI诈骗频发的时代,其实更受冲击的是金融银行的业务安全,因为我们账号汇款目前用的比较多的是人脸识别,那通过AI换脸,黑灰产完全可以实现相应的技术替换。虽然说这个是用户自己的信息泄露导致的安全问题,但是在问责上,肯定银行也会或多或少受到影响,所以最好是能够有一个合适的风控系统去进行相应的处理。


等端午结束之后吧,有空写一篇关于AI诈骗横行的当下,金融银行要如何应对。<

作者:昀和
来源:juejin.cn/post/7246571942329172027
/p>

以上。

收起阅读 »

10年技术进阶路,让我明白了这3件事

这篇也是我分享里为数不多 “进阶” 与 “成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。 十年...
继续阅读 »

这篇也是我分享里为数不多 “进阶”“成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。


十年,谁来成就你?


  离开校园,一晃已十年,时日深久,现在我已成为程序员老鸟了,从软件工程师到系统架构师,从被管理者到部门负责人,每一段经历的艰辛,如今回忆仍历历在目。各位同行你们可能正在经历的迷茫,焦虑与取舍,我也都曾经历过。


  今天我打算跟大家分享下我这些年的一个成长经历,以此篇文章为我十年的职业历程画上一个完满的句号。这篇文章虽说不是什么“绝世武功”秘籍,更没法在短时间内把我十年的“功力”全部分享于你。篇幅受限,今天我会结合过往种种挑重点说一说,大家看的过程中,记住抓重点、捋框架思路就行了。希望在茫茫人海之中,能够给到正在努力的你或多或少的帮助,亦或启发与思考。


试问,你的核心竞争力在哪?


  你曾经是否怕被新人卷或者代替?如果怕、担忧、焦虑,我可以很负责任地告诉你,那是因为你的核心竞争力还不够!这话并不好听,但,确是实在话。认清现状,踏实走好当下就行,谁能一开始或者没破茧成蝶时就一下子有所成就。


  实质上,可以这么说,经验才是我们职场老鸟的优势。 但是,经验并不是把同一件事用同一种方式重复做多少年,而是把咱们过往那么多年头的实践经验,还有被验证的理论,梳理成属于自己的知识体系,建立一套自己的思维模式,从而提升咱们的核心竞争力。


    核心竞争力的形成,并非一蹴而就,我们因为积累所以专业,因为专业所以自信,因为自信所以才有底气。积累、专业、自信、底气之间的关系,密不可分。


核心竞争力,祭出三板斧


  道理咱们都懂,能不能来点实在的?行!每当身边朋友或者后辈们,希望我给他们传授一些“功力”时,我都会给出这样的三个建议:



  1. 多面试,验本事。

  2. 写博客,而且要坚持写。

  3. 拥有自己的 Github 项目。 



  其中,博客内容和 Github 项目,将会成为咱们求职道路上的门面,这两者也是实实在在记录你曾经的输出,是非常有力有价值的证明。此外,面试官可以通过咱们的博客和 Github,在短时间内快速地了解你的能力水平等。或许你没有足够吸引、打动人的企业背景,也没有过硬的学历。但!必须有不逊于前两者的作品跟经历。


  再说说面试,我认为,它是我们接受市场与社会检验的一种有效方式。归根结底,咱们所付出的一切,都是为了日后在职业发展上走得越来越好。有朋友会说,面试官看这俩“门面”几率不大,没错,从我多年的求职经历来看,愿意看我作品的面试官也只占了 30%。


  但是,谁又能预判到会不会遇到个好机会呢?有准备,总比啥也没有强,千里马的亮点是留给赏识它的伯乐去发现的


PS:拥有自己 Github 项目与写博,都属于一种输出的方式,本文就以写博作为重点分享。写博与面试会在下文继续展开。


记忆与思考,经验与思维


  武器(三板斧)咱们已经有了,少了“内功心法”也不行。这里分享下我的一些观点,也便于大家后续能够更好地参与到具体的实践中。




  • 记忆——记忆如同对象一样是具有生命周期,久了不用就会被回收(忘记)。




  • 思考——做任何事情就如同咱们写代码Function一样,得有输入同时也得有输出,输入与输出之间还得有执行。






  •  




  日常工作中,就拿架构设计当例子。作为架构师是需要针对现有的问题场景提出解决方案,作为架构师的思考输入是业务场景、团队成员、技术选型等,而它的输出就是基于前面的多种输入参数从而产出的短期或长期的解决方案,而且最终会以文档形式保存下来。


  保存下来的目的,是为方便我们日后检索、回忆、复用。因此,在业余学习中同理,给与我们的输入是书籍、网络的资料或同行的传递等,而作为输出则是咱们记录下来的笔记、博客甚至是 Github 的项目 Demo。



基于上述,我们需要深刻意识到心法三要素:



  1. 带着明确的输出目的,才会真正地促进自己的思考。蜻蜓点水、泛泛而谈,是无法让自己形成对事物的独特见解和具象化输出,长期如此,并无良益。

  2. 只有尽可能通过深度思考过后的产出,才能够形成属于自己真正的经验。

  3. 知识的点与点之间建立联系,构成明晰的知识体系,经验与经验则形成了自己独有的思维模式。


多面试,验本事


  既然“武器”和“内功心法”咱们都有了,那么接下来得开始练“外功”了,而这一招叫"多面试,验本事"。


  我身边的同行与朋友,对我的面试行为感到奇怪:你每隔一段时间就去面试,有时拿到了 offer 还挺不错的,但是又没见想着跳槽,这是为何?


风平浪静,居安思危


  回应这个疑问之前,我想反问大家 4 个问题:



  1. 是否曾遇到过在一家公司呆了太久过于安逸,也阶段性想过离开,发现真要走可却没了跳槽的勇气?

  2. 再想一想,日子一久,你们是不是就不清楚行业与市场上,对人才能力的需求了?

  3. 是否有经历过公司意外裁员,你在找工作的时段里有没有强烈感受到那种焦虑、无助?

  4. 是否对来之不易的 offer,纠结不知道如何抉择,又或者,最终因为迫于各方面压力,勉为其难接受了不太中意的那个?



  刚提到的种种问题,那份焦虑、无助、纠结与妥协,我曾经在职场都经历过。我们想象一下,如果你现在随随便便出去面试五个公司能拿到三四个 offer,你还会有那失业的焦虑么?如果现在拿到的那几个 offer 正好都不喜欢,你全部放弃了,难道你会愁后续没有其他机会了么?显然不会!因为你有了更多底气和信心


  我再三思考,还是觉得有必要给大家分享一个我的真实经历。希望或多或少可以给你一点启发:


  2019 年,因为 A 公司业务原因,我离开了工作 3 年的安逸的环境,市场对人才的需求我已经是模糊的了,当我真正面临时,我焦虑、我无助。幸好曾经跟我合作过的老领导注意到了这我这些年的成长,向我施予援手。入职 B 公司后,我重新审视自己,并给与自己定了个计划——每半年选一批公司面试。


一年以后,因为 B 公司因疫情原因,我再次离职。这次,我没有了焦虑,取而代之的是自信与底气,裸辞在家开始了我的休假计划。在整个休假期,我拒绝了两个满足我的高薪 offer,期间我接了个技术顾问的兼职,剩余时间把以前囤下来的书看了个遍,并实践了平常没触碰到的技术盲区。三个月后,我带着饱满的精神面貌再次"出山",入职了现在这家公司。


  有人会问:你现在还有没有坚持自己的面试计划?毫无避讳回答:有!仍然是半年一次。


乘风破浪,未雨绸缪


  就前面这些问题、情况,这里结合我自己多年来的一些经验,也希望给到大家一点破局建议:保持一定的面试频率,就如上文提到的“三板斧”,面试是接受市场与社会检验,非常直接、快速、有效的一种好方式。 当然,我可不是怂恿你频繁跳槽,没有多少公司能够欣然接受不稳定的员工,特别是岗位越做越高时。


  看到这里,有些伙伴可能会想,我现在稳稳当当的、好端端的,干嘛要去面试,何必折腾自己。假若你在体制内,我这点建议或许参考意义不大。抛开体制内的讨论,大家认为真的有所谓的“稳定”的工作吗?


  我认为所谓的“稳定”,都是只是暂时的,甚至虚幻的,没有任何的人、资本、企业能给你实打实的承诺,唯一能让你“稳定”持续发展下去的,只有你的能力与眼界、格局等。


  疫情也有几年了,相信大家也有了更多思考,工作上,副业上等等各方面吧。人无远虑,必有近忧,未雨绸缪,实属必要!



放平心态,查缺补漏


  面试是相对“主观的”,这是因为“人性”的存在,你可能会听过让人哭笑不得的拒绝你的理由:



  • 连这么基础的知识都回答不上,还想应聘这岗位

  • 你的性格并不适合当管理,过于主动对团队不好


  咱们先抛开这观点的对与错。人无完人,每个人都有自己的优点与缺点,甚至你的优点可能是你的缺点。职场长路漫漫,要是把每一次的面试都当成人生中胜负的较量,那咱们最后可能会输的体无完肤。咱们付出任何的努力,也只是单纯提高“成功率”而已。听我一句劝,放平心态,以沟通交流为主,查漏补缺为辅


  近几年我以面架构师和负责人的岗位为主,面试官大多数喜欢问思想和方法论这类的问题,他们拥有不同的细节的侧重点,因此我们以梳理这些“公共”的点出发,事后复盘自己回答的完整性与逻辑性,对于含糊不清的及时找资料补全清晰,尝试模拟当时回答的场景。每一段面试,如此反复。


  作为技术人我建议,除了会干,还得会说,我们不仅有硬实力,还得有软技能。


PS:篇幅有限,具体面试经历就不展开了,如果大家对具体的面试经历感兴趣,有机会我给大家来一篇多年的"面经"。


持续进步


编程语言本身在不断进步,对于菜鸟开发者来说,需要较高的学习成本。但低代码平台天然就具备全栈开发能力,低代码程序员天然就是全栈程序员。


这里非常推荐大家试试JNPF快速开发平台,依托的是低代码开发技术原理,因此区别于传统开发交付周期长、二次开发难、技术门槛高的痛点,在JNPF后台提供了丰富的解决方案和功能模块,大部分的应用搭建都是通过拖拽控件实现,简单易上手,在JNPF搭建使用OA系统,工作响应速度更快。可一站式搭建生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。


开源链接:http://www.yinmaisoft.com/?from=jueji…


狠下心来,坚持到底


锲而舍之,朽木不折;锲而不舍,金石可镂——荀况


  要是把"多面试"比喻成以"攻"为主的招式,而"写博客"则是以"守"为主的绝招。


  回头看,今年,是我写博客的第八个年头了,虽说写博频率不高,但整体时间跨度还是挺大的。至今我还记得我写博客的初心,用博客记录我的学习笔记,同时抛砖引玉,跟同行来个思维上的碰撞。


  随着工作年限的增长,我写博客的内容慢慢从学习笔记变成了实战记录,也越来越倾向于输出经验总结和实践心得。实质上,都是在传达我的观点与见解。


  而这,至关重要。反过来看,后面机会来了,平台联系人也可以借此快速评估、判断这人会不会讲、能不能讲,讲得怎么样,成的话,人家也就快速联系咱了。进一步讲,每一次,于个人而言,都是好机会。



写博第一步,从记笔记开始


  我相信不少的同行曾经面临这样的境况,都有产生过写博客的念头,有些始终没有迈出第一步,有些中途停了下来,这里可能有不少的原因:要么不知道写什么、要么觉得写了也没人看、还有一种是想写但是比较懒等等。


我觉得,一切的学习,前期都是从模仿开始的 学习笔记,它就是很好的便于着手的一种最佳方式。相信大家在学生年代或多或少都写过日记,就算是以流水账的方式输出,博客也可以作为非常好的开启平台。


  由于在写博客的时候,潜意识里会认为写出来的东西会给更多人看,因此自己写的内容在不明确的地方都会去找资料再三确认,这是很有效的一种督促方法。确认的过程中,也会找到许多相关的知识点,自然而然就会进一步补充、完善、丰富我们自己原有或现在的知识体系


幸运,需要自己争取


  在写博客的这段时间里,除了梳理自己的知识体系之外,还能结交了一些拥有共同目标的同行,我想,这就是真正的志同道合吧。


  甚至在你的博客质量达到了一定程度——有深度与广度,会有一些意象不到的额外小收获。例如有一些兼职找到自己,各大社区平台会邀请自己合作,也会收到成就证明与礼物等等。



意外地成为了讲师


  到目前为止,正式作为讲师或者是技术顾问,以这样不同于往常的既有角色,我真切地经历了几次。虽次数不多,但每一次过后,即便时日深久,可现在回想起来,于我的成长而言,那都是一次又一次新的蜕变,真实而猛烈,且带给我一次次新生力量。


  话说回来,前面提到几次分享,有的伙伴可能会说了,这本来就性格好又爱分享的人,个例罢了,不一定适合大多数啊。说到这儿,我想,我有必要简短地跟你聊一下我自己。


跌跌撞撞,逆水行舟


  对于过往的自己,我的评价是从小就闷骚、内向的那种性格,只要在人多的时候发言就会慌会怂会紧张,自己越慌就越容易表达出错,如此恶性循环。随着我写博的篇幅越多,慢慢地我发现自己讲话时喜欢准备与思考,想好了再去表达,又慢慢地讲话就具有条理性与逻辑性了。


  当代著名哲学家陈嘉映先生,他曾在一本书里说过这样一句话,放到这里再合适不过了—— "成长无时无刻不是在克服某些与生俱来的感觉和欲望"


  回头看,一路走来,我从最初的摸索、探索、琢磨,到看到细微变化,到明显感知到更大层面的进步,再到后来的游刃有余,输出很有见地的思考,分享独到观点。


  我想,这背后,离不开一次次尝试,一次次给自己机会,一次次认真、负责地探索突破自己。其实,大多数人,还真是这么跌跌撞撞挺过来的。


伺机而动,用心准备


  2020 年,我第一次被某企业找到邀请我作为技术顾问是通过我的博客,这一次算是小试牛刀,主要以线上回答问题、交流为主。因为事先收集好了需要讨论的话题与问题,整个沟通持续了两个小时,最终也得到了对方老板的高度认可。


  此事过后,我重新审视了自己,虽然我口才并不突出,但是我基于过往积累的丰富经验与知识融合,并能够正确无误地传达输出给对方,我认为是合格的了。坦率来讲,从那之后我不再怀疑自己的表达能力。同时有另外一件事件更值得重视,基于让自己得到更多更广泛的一个关注,思前想后,概括来讲,我还是觉得落到这句话上更合适,就是:建立个人 IP


建立个人 IP


  那么,我希望打造个人 IP 的原因是什么呢?希望或多或少也可以给你提供一点可供借鉴、探讨的方向。


  我个人而言,侧重这样几个层面吧。



  1. 破局: 一个是我希望打破 35 岁魔咒,这本质上是想平稳快速度过职业发展瓶颈期;

  2. 觅友: 希望结识到拥有同样目标的同行,深度交流,构建技术圈人脉资源网;

  3. 动力 从中获取更多与工作不一样的成就感。有了强驱动力,也会使我在分享这条路上变得更坚定。


链接资源,提影响力


  在《人民的名义》里祁同伟说过一句话,咱们就是人情的社会。增加了人脉,就是增加自己的机会。当然前提是,咱们自己得需要有这个实力。


  建立个人 IP,最要提高知名度,而提知名度的主要方式是两种:写书、做讲师。后面我会展开讲,写书无疑是宣传自己的最好方式之一,但整个过程不容易,周期比较长。作为写书的简化版,我们写博客就是一种捷径了。


主动出击,勿失良机


  而作为讲师,线上线下各类形式参与各种社区峰会露脸,这也是一种方式。不过这种一般会设有门槛。


  这里不得不多提一句,就是建立 IP 它是一个循序渐进的过程,欲速则不达,任何时候咱们都得靠内容作品来说话, 当你输出的质量够了,自然而然社区人员、企业就会找到你,机会顺理成章来了。反过来讲,我们也得常盯着,或者说多留心关注业内各平台的内容风格,利用好业余零碎时间,好好梳理下某个感兴趣的内容平台,看看他们到底都倾向于打造什么样的东西。做到知己知彼,很重要。


  我认识的一个前辈,之前阿里的,他非常乐于在博客上分享自己的经验与见解,随着他分享的干货越多,博客影响力越大,某内容付费平台找到他合作出了个专栏,随着专栏的完结,他基于专栏内容又出了一本书,而现在的他已经离开了阿里,成为了自由职业者。


追求成就感,倒逼突破自我


  每一次写博客、做讲师,都能更大程度上填满我内心深处的空洞,或许是每一个支持我的留言与点赞,或许是每一节分享停顿间的掌声。如果我们抱着非常强的目的去做的时候,可能会事与愿违。就以我做讲师来说,因为我是一个新手,在前期资料准备所花费的精力与时间跟后续的课酬是不成正比的。


  作为动力源,当时我会把侧重点放到结交同行上,同时利用“费曼学习法”重新梳理知识,另外寻找机会突破自己的能力上限。



  大家有没有想过,讲课最终受益者的是谁?有些朋友会回答“双方”。但是我很负责任地告诉你,作者、讲师自己才是最大的知识受益者。


  如前面所讲,写博客为了更好地分享出更具价值性的内容,为保证专业性,咱们得再三确认不明确的点,而讲课基于写博客的基础上,还得以听众的角度,去思考、衡量、迭代,看看怎么让人家更好地理解、吸收、用得上这些知识点,甚至讲师得需要提前模拟、预估可能会在课后被提的问题。


这里总结一下,写博客与讲课的方式完全不同,因为博客是以图、文、表的方式展现,读者看不明白可以回头去看,但是讲课则没有回头路,是一环套一环的,所以梳理知识线的连贯性要求更强


  我个人认为,日常工作大多数是重复的、枯燥的,或者说,任何兴趣成了职业,那将不再是兴趣,或许只有在业余的时候获取那些许的成就感,才会重新燃起自己的那一份初心 ——行之于途而应于心。


源不深而望流之远,根不固而求木之长


  求木之长者,必固其根本;欲流之远者,必浚其源泉——魏徵


  有些同行或许会问:”打铁还需自身硬“这道理咱们都懂,成长进阶都离不开学习,但这要是天天写 BUG 的哪来那么多时间学?究竟学习的方向该怎么走呢?在这里分享下我的实际做法,以及一些切身的个人体会,希望可以提供一点借鉴、参考。


零碎时间,稳中求进


  6 年前,我确定往系统架构师这个目标发展的时候,每天都会做这么两件事:碎片化时间学习,及时产出笔记。



  • 上班通勤与中午休息,我会充分利用这些碎片时间(各 30 分钟)尽可能地学习与吸收知识,每天坚持一小时的积累,积少成多,两年后你会发现,效果非常可观,这就是一个量变到质变的过程


  而且有神经科学相关表明,”间歇式模块化学习的效果最佳,通勤路上就是实践这种模式的理想世界。“大家也可以多试试看。当然,一开始你学习某个领域的知识,可能效率没那么高,我建议你可以反复地把某一节掰开了揉碎了看或者听,直到看明白听懂了为止,接着得怎么做?如我前面说,咱们得要有输出!


  看过这样一段话,”写和想是不同的,书写本身就是逻辑推演和信息梳理的过程。“而且,研究表明,”人的记忆力会在 17-24 岁达到高峰,25 岁之后会下降,理解力的发展曲线会延后 5 年,也就是说在 30 岁之后也会下降。“


  你看,这个也直接或者间接告诉我们,还是趁早多做记录、多学习。文字也好,视频也罢,到底啥形式不重要,适合自己能长久坚持的就行,我相信你一定能从中受益。毕竟,这些累积的,可都是你自己实实在在的经验和思考沉淀!


  话说回来,其实做笔记能花多长时间,就算在工作时间花半小时也有良效,而这时间并不会对自己的工作进度造成多么大的影响,但!一定时日深久,受益良多。


构建知识 体系 丰富 思维 模式


  由于我们日常需要快速解决技术难题,很多时候从外界吸收到的知识点相对来说很零散,而知识体系是由点、线、面、体四个维度构造而成的


  那怎么做能够快速把知识串联起来呢?这里我举个简单的例子,方便大家理解。


  以我们系统性能调优出发,首先我们需要了解系统相关性能瓶颈的业务场景是什么?该功能是 I/O 密集型还是 CPU 密集型?如果是 I/O 密集型多数的性能瓶颈在数据库,这个时候我们就得了解数据库瓶颈的原因,究竟是数据量大还是压力大?如果是数据量大,基于现有的业务场景应该选择数据归档、临时表还是分库分表,这之间的方案优缺点有什么不同?适用场景怎么样?假如是数据压力大了,我们是否能用 Redis 做缓存抗压就行?


  再接着从 Redis 这个点继续思考,假如 Redis 内存满了会怎样?我们又了解到了 Redis 的内存淘汰策略,设置了 volatile-lru 策略,由于我们基本功扎实回忆起 LUR 算法是基于链表的数据结构,虽然链表的写的时间复杂度是 O(1),但是读是 O(n),不过我们得先读后写,所以为了高性能又选择 Hash 这种 O(1)的数据结构辅助读的处理。


  你看,我们是不是从问题出发到架构设计,再从数据库优化方案到 Redis 的使用,最后到数据结构,这一些系统的知识就串联起来了?


收起阅读 »

妹纸问我怎么下载B站视频?你等我一下

文章已同步至【个人博客】,欢迎访问【我的主页】😃 文章地址:blog.fanjunyang.zone/archives/do… 前言 今天有一个妹纸向我提出了一个问题 是时候"出手"了,本着助人为乐的精神,这个忙必须帮(没办法,我就喜欢帮助别人) 现在我们...
继续阅读 »

文章已同步至【个人博客】,欢迎访问【我的主页】😃

文章地址:blog.fanjunyang.zone/archives/do…



前言


今天有一个妹纸向我提出了一个问题


docker-alltube-1


是时候"出手"了,本着助人为乐的精神,这个忙必须帮(没办法,我就喜欢帮助别人)


现在我们在下载一些比如:Bilibili,YouTube等第三方视频的时候,还是比较困难的,需要找各种下载器和网站,而且还不一定能下载,一些免费好用的下载网站还不好找。
所以我们可以自己动手搭一个下载站点,来下载各大平台上的视频。


搭建的站点(大家轻点薅):dl.junyang.space/

站点的地址会随着时间更新,如果上面的地址不能访问的话,大家可以去我的 博客 ,我会把站点入口放在【顶部菜单栏】->【百宝箱】里面)


相关链接&环境配置


最好用国外的服务器,如果用国内的服务器,是下载不了YouTube等需要魔法网站的视频的


docker、docker-compose安装:blog.fanjunyang.zone/archives/de…
Nginx Proxy Manager安装使用:blog.fanjunyang.zone/archives/ng…

使用的GitHub的开源项目:github.com/Rudloff/all…

使用的Docker镜像:hub.docker.com/r/dnomd343/…


搭建方式


创建相关目录


mkdir -p /root/docker_data/alltube
cd /root/docker_data/alltube

创建yml文件


version: '3.3'
services:
alltube:
restart: always
container_name: alltube
environment:
# 自己网站的title
- 'TITLE=My Alltube Site'
- CONVERT=ON
- STREAM=ON
- REMUX=ON
ports:
# 左侧端口号换成你服务器上未使用的端口号
- '24488:80'
image: dnomd343/alltube

运行yml文件


进入/root/docker_data/alltube文件夹下面,运行命令:docker-compose up -d


或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/alltube/docker-compose.yml up -d


访问使用


可以直接使用【IP + PORT】的方式访问(需要放通对应端口号的防火墙或安全组)


最好配置反向代理,用域名访问,可以参考:blog.fanjunyang.zone/archives/ng…


她对我说


当我把下载链接发给她时,她说:你真是个好人,正好我让我男朋友也用一下。


我不能忍,然后我默默的把站点删除、下线,眼里留下了悔恨的泪水。


注意事项&问题



  • 目前解析不出来B站的视频封面(YouTube可以正常解析),不过不影响下载

  • 因为B站音视频是分开的,所以需要下载两次(一次视频、一次音频),然后整合一下就好了

  • 因国内版权限制的原因,部分资源无法解析是正常现象

  • 下载的时候可以选择视频格式


docker-alltube-2

收起阅读 »

在这个大环境下我是如何找工作的

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。 已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景...
继续阅读 »

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。
已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景下再加上全世界范围内的经济不景气我想每个人都能感受到寒意。


我还记得大约在 20 年的时候看到网上经常说的一句话:今年将是未来十年最好的一年。


由于当时我所在的公司业务发展还比较顺利,丝毫没有危机意识,对这种言论总是嗤之以鼻,直到去年国庆节附近。


虽然我们做的是海外业务,但是当时受到各方面的原因公司的业务也极速收缩(被收购,资本不看好),所以公司不得不进行裁员;
其实到我这里的时候前面已经大概有 2~3 波的优化,我们是最后一波,几乎等于是全军覆没,只留下少数的人维护现有系统。


这家公司也是我工作这么多年来少数能感受到人情味的公司,虽有不舍,但现实的残酷并不是由我们个人所决定的。


之后便开始漫长的找工作之旅,到现在也已经入职半年多了;最近看到身边朋友以及网上的一些信息,往往是坏消息多于好消息。


市场经历半年多的时间,裁员的公司反而增多,岗位也越来越少,所以到现在不管是在职还是离职的朋友或多或少都有所焦虑,我也觉得有必要分享一下我的经历。


我的预期目标


下面重点聊聊找工作的事情;其实刚开始得知要找工作的时候我并不是特别慌,因为当时手上有部分积蓄加上公司有 N+1 的赔偿,同时去年 10 月份的时候岗位相对于现在还是要多一些。


所以我当时的目标是花一个月的时间找一个我觉得靠谱的工作,至少能长期稳定的工作 3 年以上。


工作性质可以是纯研发或者是偏管理岗都可以,结合我个人的兴趣纯研发岗的话我希望是可以做纯技术性质的工作,相信大部分做业务研发的朋友都希望能做一些看似“高大上”的内容。
这一点我也不例外,所以中间件就和云相关的内容就是我的目标。


不过这点在重庆这个大洼地中很难找到对口工作,所以我的第二目标是技术 leader,或者说是核心主程之类的,毕竟考虑到 3 年后我也 30+ 了,如果能再积累几年的管理经验后续的路会更好走一些。


当然还有第三个选项就是远程,不过远程的岗位更少,大部分都是和 web3,区块链相关的工作;我对这块一直比较谨慎所以也没深入了解。


找工作流水账


因为我从入职这家公司到现在其实还没出来面试过,也不太知道市场行情,所以我的想法是先找几家自己不是非去不可的公司练练手。



有一个我个人的偏好忘记讲到,因为最近的一段时间写 Go 会多一些,所以我优先看的是 Go 相关的岗位。



第一家


首先第一家是一个 ToB 教育行业的公司,大概的背景是在重庆新成立的研发中心,技术栈也是 Go;


我现在还记得最后一轮我问研发负责人当初为啥选 Go,他的回答是:



Java 那种臃肿的语言我们首先就不考虑,PHP 也日落西山,未来一定会是 Go 的天下。



由于是新成立的团队,对方发现我之前有管理相关的经验,加上面试印象,所以是期望我过去能做重庆研发 Leader。


为此还特地帮我申请了薪资调整,因为我之前干过 ToB 业务,所以我大概清楚其中的流程,这种确实得领导特批,所以最后虽然没成但依然很感谢当时的 HR 帮我去沟通。


第二家


第二家主要是偏年轻人的 C 端产品,技术栈也是 Go;给我印象比较深的是,去到公司怎么按电梯都不知道🤣



他们办公室在我们这里的 CBD,我长期在政府赞助的产业园里工作确实受到了小小的震撼,办公环境比较好。



当然面试过程给我留下的印象依然非常深刻,我现在依然记得我坐下后面试官也就是 CTO 给我说的第一句话:



我看过你的简历后就决定今天咱们不聊技术话题了,直接聊聊公司层面和业务上是否感兴趣,以及解答我的疑虑,因为我已经看过你写的很多博客和 GitHub,技术能力方面比较放心。



之后就是常规流程,聊聊公司情况个人意愿等。


最后我也问了为什么选 Go,这位 CTO 给我的回答和上一家差不多😂


虽然最终也没能去成,但也非常感谢这位 CTO,他是我碰到为数不多会在面试前认真看你的简历,博客和 GitHub 都会真的点进去仔细阅读👍🏼。



其实这两家我都没怎么讲技术细节,因为确实没怎么聊这部分内容;这时就突出维护自己的技术博客和 GitHub 的优势了,技术博客我从 16 年到现在写了大约 170 篇,GitHub 上开源过一些高 star 项目,也参与过一些开源项目,这些都是没有大厂经历的背书,对招聘者来说也是节约他的时间。





当然有好处自然也有“坏处”,这个后续会讲到。


第三家


第三家是找朋友推荐的,在业界算是知名的云原生服务提供商,主要做 ToB 业务;因为主要是围绕着 k8s 社区生态做研发,所以就是纯技术的工作,面试的时候也会问一些技术细节。



我还记得有一轮 leader 面,他说你入职后工作内容和之前完全不同,甚至数据库都不需要安装了。



整体大概 5、6 轮,后面两轮都是 BOSS 面,几乎没有问技术问题,主要是聊聊我的个人项目。


我大概记得一些技术问题:



  • k8s 相关的一些组件、Operator

  • Go 相关的放射、接口、如何动态修改类实现等等。

  • Java 相关就是一些常规的,主要是一些常用特性和 Go 做比较,看看对这两门语言的理解。


其实这家公司是比较吸引我的,几乎就是围绕着开源社区做研发,工作中大部分时间也是在做开源项目,所以可以说是把我之前的业余爱好和工作结合起来了。


在贡献开源社区的同时还能收到公司的现金奖励,不可谓是双赢。


对我不太友好的是工作地在成都,入职后得成渝两地跑;而且在最终发 offer 的前两小时,公司突然停止 HC 了,这点确实没想到,所以阴差阳错的我也没有去成。


第四家


第四家也就是我现在入职的公司,当时是我在招聘网站上看到的唯一一家做中间件的岗位,抱着试一试的态度我就投了。
面试过程也比较顺利,一轮同事面,一轮 Leader 面。


技术上也没有聊太多,后来我自己猜测大概率也和我的博客和 Github 有关。




当然整个过程也有不太友好的经历,比如有一家成都的“知名”旅游公司;面试的时候那个面试官给我的感觉是压根没有看我的简历,所有的问题都是在读他的稿子,根本没有上下文联系。


还有一家更离谱,直接在招聘软件上发了一个加密相关的算法,让我解释下;因为当时我在外边逛街,所以没有注意到消息;后来加上微信后说我为什么没有回复,然后整个面试就在微信上打字进行。


其中问了一个很具体的问题,我记得好像是 MD5 的具体实现,说实话我不知道,从字里行间我感觉对方的态度并不友好,也就没有必要再聊下去;最后给我说之所以问这些,是因为看了我的博客后觉得我技术实力不错,所以对我期待较高;我只能是地铁老人看手机。


最终看来八股文确实是绕不开的,我也花了几天时间整理了 Java 和 Go 的相关资料;不过我觉得也有应对的方法。


首先得看你面试的岗位,如果是常见的业务研发,从招聘的 JD 描述其实是可以看出来的,比如有提到什么 Java 并发、锁、Spring等等,大概率是要问八股的;这个没办法,别人都在背你不背就落后一截了。


之后我建议自己平时在博客里多记录八股相关的内容,并且在简历上着重标明博客的地址,尽量让面试官先看到;这样先发制人,你想问的我已经总结好了😂。


但这个的前提是要自己长期记录,不能等到面试的时候才想起去更新,长期维护也能加深自己的印象,按照 “艾宾浩斯遗忘曲线” 进行复习。


选择



这是我当时记录的面试情况,最终根据喜好程度选择了现在这家公司。


不过也有一点我现在觉得但是考虑漏了,那就是行业前景。


现在的 C 端业务真的不好做,相对好做的是一些 B 端,回款周期长,同时不太吃现金流;这样的业务相对来说活的会久一些,我现在所在的公司就是纯做 C 端,在我看来也没有形成自己的护城河,只要有人愿意砸钱随时可以把你干下去。


加上现在的资本也不敢随意投钱,公司哪天不挣钱的话首先就是考虑缩减产研的成本,所以裁员指不定就会在哪一天到来。


现在庆幸的是入职现在这家公司也没有选错,至少短期内看来不会再裁员,同时我做的事情也是比较感兴趣的;和第三家有些许类似,只是做得是内部的基础架构,也需要经常和开源社区交流。


面对裁员能做的事情


说到裁员,这也是我第一次碰上,只能分享为数不多的经验。


避免裁员


当然第一条是尽量避免进入裁员名单,这个我最近在播客 作为曾经的老板,我们眼中的裁员和那些建议 讲到在当下的市场情况下哪些人更容易进入裁员名单:



  • 年纪大的,这类收入不低,同时收益也没年轻人高,确实更容易进入名单。

  • 未婚女性,这点确实有点政治不正确,但确实就是现在的事实,这个需要整个社会,政府来一起解决。

  • 做事本本分分,没有贡献也没出啥事故。

  • 边缘业务,也容易被优化缩减成本。


那如何避免裁员呢,当然首先尽量别和以上特征重合,一些客观情况避免不了,但我们可以在第三点上主动“卷”一下,当然这个的前提是你还想在这家公司干。


还有一个方法是提前向公司告知降薪,这点可能很多人不理解,因为我们大部分人的收入都是随着跳槽越来越高的;但这些好处是否是受到前些年互联网过于热门的影响呢?


当然个人待遇是由市场决定的,现在互联网不可否认的降温了,如果你觉得各方面呆在这家公司都比出去再找一个更好,那这也不失为一个方法;除非你有信心能找到一个更好的,那就另说了。


未来计划


我觉得只要一家公司只要有裁员的风声传出来后,即便是没被裁,你也会处于焦虑之中;要想避免这种焦虑确实也很简单,只要有稳定的被动收入那就无所谓了。


这个确实也是说起来轻松做起来难,我最近也一直在思考能不能在工作之余做一些小的 side project,这话题就大了,只是我觉得我们程序员先天就有自己做一个产品的机会和能力,与其把生杀大权给别人,不如握在自己手里。


当然这里得提醒下,在国内的企业,大部分老板都认为签了合同你的 24 小时都是他的,所以这些业务项目最好是保持低调,同时不能影响到本职工作。



欢迎关注作者公众号于我交流🤗。


作者:crossoverJie
来源:juejin.cn/post/7246570594991718455

收起阅读 »

经济持续低迷环境下,女全栈程序员决定转行了

引言 疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。 近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据...
继续阅读 »

引言


疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。


近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据要多很多,尤其是表示 “一周工作一小时以上” 也纳入了就业范围。


image.png


而从我自己的判断来说,记得我自己在去年8月份被裁之后就在xhs发布了一篇关于个人如何交社保的教程,去年年底,观看浏览量不是特别多,而在今年(从年初至今)浏览量以及收藏量蹭蹭往上涨,几乎是每天都有人浏览和收藏我的帖子,抛去网上数据到底如何,光从我自己的感受来看,今年失业人数比去年更多!


image.png


个人只是随手发了一个帖子,将自己如何交社保的步骤记录下来,就有持续的搜索流量,这绝不是一件好事!说明了哀鸿遍野。


一面广大青少年正值青春鼎盛却面临着就业危机,另一方面还要忍受各种开支的骤增,比如深圳统租房的出现,大批人发声:微棠gun出深圳!



曾经破旧拥挤的城中村,为每一位打工人开启了大城市的入口,虽然这个入口短暂,且在关上门的时候,会毫不犹豫抹去你所有的痕迹。

而今这个入口,它不会再破旧拥挤,但会吸取你身上的最后一滴血。



个人经历


1.行政岗转前端


自己曾经拿着一个一本工科学历,因为厌倦行政岗位的勾心斗角,从而挑灯夜战每天在公司加班学习前端到11点,半路出家转行做了前端程序员。


2.刚转行遇吸血领导


而刚转行,又遇到了极其吸血的创业公司(大小周、从0到1项目,双周迭代迭代加班到2点)。


当时不敢辞职,不外乎有几个原因:



  • 刚转行,自己认为技术还比较菜,不敢辞职,被裁了之后才发现外面一大片天地

  • 真的很忙,根本没有时间提升自我与准备面试。因为呆了两年,我自己上了一次救护车,后来离职之后也发现自己因此得了疲劳综合症

  • 比较会吃苦,当时看来觉得可以忍一忍


关于这家公司呢,我想说,我这领导是真的狗,领导是我大一届的学长,曾经担任了大厂某知名项目的组长,号称协同领域的专家,关于此人是我生活中见过最资本的一个人:



  • 针对刚毕业的新人,不培养下属却对下属有着超乎大厂的要求(毫不夸张,你没经历过就不要觉得我是在夸张)

  • 技术部的同事都是很年轻的,做事都兢兢业业,不甩锅,不摸鱼,很多事都是自发的去解决,关于技术水平,我很客观的评价,不菜

  • 在裁我的时候,我呆的时间是13个月,也就是差一个月满2年,但他忽悠我说法律都规定只能给我1+1,我还不满2年,当时对方忽悠毫不脸红,又本着学长+平时看起来正人君子的偏见,在当时就签署了合同,失去的1个月补偿金还好,最伤人的是利用了你的信任,杀人诛心。


3.持续学习


从吸血公司出来之后,进入了相对比较wlb的公司,也清楚认识到自己在程序员领域,女性并不吃香,因此自己也是一直在学习前端技术。



  • 比如自己也曾在掘金发布了上百的技术文章

  • 买教学课程

  • 从零学算法,刷Leetcode

  • github持续输出代码

  • 建立自己的技术博客


image.png


image.png


4.努力不代表有收获


曾经相信自己勤能补拙,后来发现,比你拙的一大批还比你工资高;

曾经熟悉React技术栈,却在失业时找前端兼职时因不会vue而被刷;

曾经将网上的八股文背了再背,面试一二面对答如流,却倒在了三面面试官深问你项目经验;
曾经以为深耕项目经验,学性能优化、前端工程化、架构,却因为面试不会吹牛且遇上近几年经济低迷环境,工资还是那样。

曾经以为,自己努力点,自己性格好点,不断提升,会迎来比较好的人生。

曾经以为,男女平等,男生不应该一人承担经济压力,所以放弃了沉迷貌美如花,选择了与男生一样扛水桶,挑重活,但事实是,那些每天开开心心负责貌美如花的女生比我这种埋头搞钱的女生要幸福很多,对于像花一样的女生,谁不怜爱宽容呢,谁会去宽容一个扎在程序员堆里放弃自己容貌的黄脸婆呢。(看到这里,也许有人觉得我是因为自己长的太丑了,所以才选择搞钱,然而客观来分析,我自己并不丑,虽然说不是校花班花级别,但也可以在普通人群里说的是中上,颜控党眼里也能过得去,不是普信)



然后事实是,有些人,不用长得漂亮,不用能力强,不用对外提供情绪价值或其他价值,他站在那里,就有好的收获,就有人包容就有人爱。



在经历过上述的心理历程之后,明白了职场规则,以及社会运作规律,在大环境下,每个人都在尽自己的努力维持着公平,这个世界,因为有些人经历坎坷,未能坚守住自己底线,从而世界才会有坏人的存在。但大部分情况是,没有绝对的坏人,比如你觉得领导对自己很吸血,但可能领导背后的压力是整个公司的生存(虽然我的领导真的就是单纯的吸血),比如你觉得有些人对自己戾气重,可能当时人家真的内心极其痛苦,而你刚好撞到了枪口上,比如有些人因为诸多原因对你坏,但可能对别人好。


So,个人而言,还是做好自己,看淡所有的行为,同时能有自己的盾和矛。


决定转行


明白自己确实不适合长久做程序员,因此跟大家一样,网上搜了很多搞副业赚钱的路子,排除了偏门以及刑法上的路子,结合我自己的情况,目前已经开始正式着手Vlog自媒体之路了。



  • 买拍摄工具

  • 打造自己的IP

  • 整理自己的衣着、居住环境

  • 学习自媒体知识、拍摄技巧


总的而言,作为一个硬件工科出身的妹子,一直觉得自己更喜欢软件,比如硬件我要调试半天的电路我才能把一个灯泡💡点亮,而计算机,我写一行代码就可以得到反馈,即使是错误的,也能快速做出调整。


但也不可否认,女生在敲代码方面确实跟男生比没有那么大的天赋,就好比玩游戏,大部分女生会玩游戏,但是如果说要打的特别好,男生还是居多。


所以自己也很佩服那些在代码这条路上走的很坚定的女程序员。一起加油吧。


最后,我给各位女程序猿一个小建议,如果没有很高的学历背景或比较好的人脉资源运气,我觉得趁早搞一个副业,但是绝对不要裸辞去搞副业。程序员这个岗位虽然目前已经卷的不行,但瘦死的骆驼比马大,比某些天坑行业还是好很多,我觉得我们还是很幸运的。


image.png

收起阅读 »

Spring Boot如何优雅实现结果统一封装和异常统一处理

1.概述 当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示 Restful风格是...
继续阅读 »

1.概述


当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示


Restful风格是什么?


RESTFUL(英文:Representational State Transfer,简称REST)可译为"表现层状态转化”,是一种网络应用程序的设计风格和开发方式,是资源定位和资源操作的一种风格。不是标准也不是协议。基于HTTP可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点。Restful风格最大的特点为:资源、统一接口、URI和无状态。


对于我们Web开发人员而言,restful风格简单来说就是使用一个url地址表示一个唯一的资源。然后把原来的请求参数加入到请求资源地址中。把原来请求的增,删,改,查操作路径标识,改为使用HTTP协议中请求方式GET、POST、PUT、DELETE表示。


传统的方式是:http://127.0.0.1:8080/shepherd/user/add 表示新增用户的接口,需要在路径上加以增删改查标识,如果我们要修改:那么路径是:http://127.0.0.1:8080/shepherd/user/update


但是我们基于restful风格就比较优雅:http://127.0.0.1:8080/shepherd/user,增删改查都可以用这个路径,使用请求方法来进行区别即可,如post代表新增,put代表修改等。



项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记



2.返回结果统一封装


定义一个统一的标准返回格式,有助于后端接口开发的规范性和通用性,同时也提高了前后端联调的效率,前端通过接收同一返回结构体进行相应映射处理,不用担心每个接口返回的格式都不一样而做一一适配了。


2.1 定义返回统一结构体

@Data
public class ResponseVO<T> implements Serializable {

   private Integer code;

   private String msg;

   private T data;

   public ResponseVO() {

  }

   public ResponseVO(Integer code, String msg) {
       this.code = code;
       this.msg = msg;
  }

   public ResponseVO(Integer code, T data) {
       this.code = code;
       this.data = data;
  }

   public ResponseVO(Integer code, String msg, T data) {
       this.code = code;
       this.msg = msg;
       this.data = data;
  }

   private ResponseVO(ResponseStatusEnum resultStatus, T data) {
       this.code = resultStatus.getCode();
       this.msg = resultStatus.getMsg();
       this.data = data;
  }

   /**
    * 业务成功返回业务代码和描述信息
    */
   public static ResponseVO<Void> success() {
       return new ResponseVO<Void>(ResponseStatusEnum.SUCCESS, null);
  }

   /**
    * 业务成功返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> success(T data) {
       return new ResponseVO<T>(ResponseStatusEnum.SUCCESS, data);
  }

   /**
    * 业务成功返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> success(ResponseStatusEnum resultStatus, T data) {
       if (resultStatus == null) {
           return success(data);
      }
       return new ResponseVO<T>(resultStatus, data);
  }

   /**
    * 业务异常返回业务代码和描述信息
    */
   public static <T> ResponseVO<T> failure() {
       return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
  }

   /**
    * 业务异常返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus) {
       return failure(resultStatus, null);
  }

   /**
    * 业务异常返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus, T data) {
       if (resultStatus == null) {
           return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
      }
       return new ResponseVO<T>(resultStatus, data);
  }

   public static <T> ResponseVO<T> failure(Integer code, String msg) {
       return new ResponseVO<T>(code, msg);
  }
}


这里包含了三个字段信息:



  1. code 状态值:由后端统一定义各种返回结果的状态码, 比如说code=200代表接口调用成功

  2. msg 描述:本次接口调用的结果描述,比如说后端抛出的业务异常信息就在这里体现

  3. data 数据:本次返回的数据,泛型类型意味着可以支持任意类型的返回数据


成功返回如下:

{
 "code": 200,
 "msg": "OK",
 "data": {
   "id": 123,
   "name": "shepherd"
}
}

业务异常返回如下:

{
 "code": 400,
 "msg": "当前用户不存在"
}

按照上面成功返回的示例我们接口定义如下:

    @GetMapping("/test/user")
   public ResponseVO<User> testUser() {
       User user = new User();
       user.setId(123l);
       user.setName("shepherd");
       return ResponseVO.success(user);
  }

可以看到接口方法返回类型为ResponseVO<User>,然后通过ResponseVO.success()对返回结果进行包装后返回给前端。这就意味着写一个接口都需要调用ResultData.success()这行代码对结果进行包装,有点重复劳动不够优雅的感觉。还有一种情况,有些项目服务前期为了赶时间开发时没有返回统一结构,等项目上线了有时间之后按照规范需要对后端接口返回结构进行统一,这时候如果复杂的系统已经有成百上千的接口了,如果一个个地像上面说的那样把接口返回类型改为ResponseVO<T>,再用ResponseVO.success()进行结果包装,工作量不小,也比较繁琐。


2.2 高级优雅实现统一结果封装


为了解决上面阐述的问题,我们借助于Spring Boot提供的ResponseBodyAdvice进行了高级实现。


ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。 我们在分享 Spring Boot如何对接口参数进行加解密就有提到过这个类进行返回结果参数的加密。


先来看下ResponseBodyAdvice的源码

public interface ResponseBodyAdvice<T> {
 /**
 * 是否支持advice功能
 * true 支持,false 不支持
 */
   boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);

  /**
 * 对返回的数据进行处理
 */
   @Nullable
   T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}

所以我们编写一个具体实现类即可:

@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
   @Resource
   private ObjectMapper objectMapper;

   private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;

   /**
    * 判断类或者方法是否使用了 @ResponseResultBody
    */
   @Override
   public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
       return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
  }

   /**
    * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
    */
   @SneakyThrows
   @Override
   public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
       //如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
       // 当body都为null时,下面的if判断条件都不满足,如果接口返回类似为String,会报错com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String
       Class<?> returnClass = returnType.getMethod().getReturnType();
       if (body instanceof String || Objects.equals(returnClass, String.class)) {
           String value = objectMapper.writeValueAsString(ResponseVO.success(body));
           return value;
      }
       // 防止重复包裹的问题出现
       if (body instanceof ResponseVO) {
           return body;
      }
       return ResponseVO.success(body);
  }

}


这里使用到一个自定义注解@ResponseResultBody:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {

}

注入bean

    @Bean
   public ResponseResultBodyAdvice responseResultBodyAdvice() {
       return new ResponseResultBodyAdvice();
  }

从上面我们自己定义实现类ResponseResultBodyAdvice#supports()可以看到,只要我们的Controller类或者方法上使用了ResponseResultBody注解,就会执行方法#beforeBodyWrite(),使用ResponseVO对结果进行包装统一返回。


实现类上使用了RestControllerAdvice注解,@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法,该注解特点如下:


1.通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。


2.注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。


3.@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。


4.@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。


5.@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。


6.@ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对


实现类ResponseResultBodyAdvice使用该注解就是满足上面的第3种情况,将拦截作用在所有注解了@RequestMapping的控制器的方法进行判断是否使用了注解@ResponseResultBody,从而对接口结构进行统一ResponseVO包装。


3.全局异常统一处理


使用统一返回结果时,还有一种情况,就是程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,不能正常走到我们return的ResponseVO对象返回,因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。


使用上面的@ControllerAdvice@ExceptionHandler进行全局异常统一处理:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

   /**
    * 全局异常处理
    * @param e
    * @return
    */
   @ResponseBody
   @ResponseStatus(HttpStatus.BAD_REQUEST)
   @ExceptionHandler(Exception.class)
   public ResponseVO exceptionHandler(Exception e){
       // 处理业务异常
       if (e instanceof BizException) {
           BizException bizException = (BizException) e;
           if (bizException.getCode() == null) {
               bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
          }
           return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
      } else if (e instanceof MethodArgumentNotValidException) {
           // 参数检验异常
           MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
           Map<String, String> map = new HashMap<>();
           BindingResult result = methodArgumentNotValidException.getBindingResult();
           result.getFieldErrors().forEach((item)->{
               String message = item.getDefaultMessage();
               String field = item.getField();
               map.put(field, message);
          });
           log.error("数据校验出现错误:", e);
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
      } else if (e instanceof HttpRequestMethodNotSupportedException) {
           log.error("请求方法错误:", e);
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
      } else if (e instanceof MissingServletRequestParameterException) {
           log.error("请求参数缺失:", e);
           MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
      } else if (e instanceof MethodArgumentTypeMismatchException) {
           log.error("请求参数类型错误:", e);
           MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
      } else if (e instanceof NoHandlerFoundException) {
           NoHandlerFoundException ex = (NoHandlerFoundException) e;
           log.error("请求地址不存在:", e);
           return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
      } else {
           //如果是系统的异常,比如空指针这些异常
           log.error("【系统异常】", e);
           return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
      }
  }

}


注入bean

    @Bean
  public GlobalExceptionHandler globalExceptionHandler() {
      return new GlobalExceptionHandler();
  }

通过以上步骤就可以对异常进行全局异常统一处理,这样做的好处不仅是可以对未知异常进行处理之后按照统一结构返回给前端,同时还能对异常处理之后进行error级别的日志输出,这样才能结合logback,log4j2等日志框架写入到日志文件中,以便后续查看异常错误日志排查追踪问题,否则异常信息不会被记录在error日志文件中。


4.总结


基于以上全部内容,我们讲述了如何优雅实现返回结果统一封装和全局异常统一处理,这样可以规范后端接口输出,同时也增强了项目服务的健壮性,可以说这两个统一处理是当下项目服务的必须要求,所以我们得了解一下哦。


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

mysql 到底是 join性能好,还是in一下更快呢

先总结: 数据量小的时候,用join更划算 数据量大的时候,join的成本更高,但相对来说join的速度会更快 数据量过大的时候,in的数据量过多,会有无法执行SQL的问题,待解决 事情是这样的,去年入职的新公司,之后在代码review的时候被提出说,不要...
继续阅读 »

先总结:



  1. 数据量小的时候,用join更划算

  2. 数据量大的时候,join的成本更高,但相对来说join的速度会更快

  3. 数据量过大的时候,in的数据量过多,会有无法执行SQL的问题,待解决


事情是这样的,去年入职的新公司,之后在代码review的时候被提出说,不要写join,join耗性能还是慢来着,当时也是真的没有多想,那就写in好了,最近发现in的数据量过大的时候会导致sql慢,甚至sql太长,直接报错了。这次来浅究一下,到底是in好还是join好,仅目前认知探寻,有不对之处欢迎指正


以下实验仅在本机电脑试验


一、表结构


1、用户表


image.png

 CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`gender` smallint DEFAULT NULL COMMENT '性别',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1005 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

2、订单表


image.png

CREATE TABLE `order` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`price` decimal(18,2) NOT NULL,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`status` smallint NOT NULL DEFAULT '0' COMMENT '订单状态',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=202 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

二、先来试少量数据的情况


用户表插一千条随机生成的数据,订单表插一百条随机数据


查下所有的订单以及订单对应的用户


下面从三个维度来看



多表连接查询成本 = 一次驱动表成本 + 从驱动表查出的记录数 * 一次被驱动表的成本



1、join



JOIN: explain format=json select order.id, price, user.name from order join user on order.user_id = user.id;


子查询: select order.id,price,user.name from order,user where user_id=user.id;



image.png


2、分开查



select id,price,user_id from order;



image.png



select name from user where id in (8, 11, 20, 32, 49, 58, 64, 67, 97, 105, 113, 118, 129, 173, 179, 181, 210, 213, 215, 216, 224, 243, 244, 251, 280, 309, 319, 321, 336, 342, 344, 349, 353, 358, 363, 367, 374, 377, 380, 417, 418, 420, 435, 447, 449, 452, 454, 459, 461, 472, 480, 487, 498, 499, 515, 525, 525, 531, 564, 566, 580, 584, 586, 592, 595, 610, 633, 635, 640, 652, 658, 668, 674, 685, 687, 701, 718, 720, 733, 739, 745, 751, 758, 770, 771, 780, 806, 834, 841, 856, 856, 857, 858, 882, 934, 942, 983, 989, 994, 995); [in的是order查出来的所有用户id]



image.png


如此看来,分开查和join查的成本并没有相差许多


3、代码层面


主要用php原生写了脚本,用ab进行10个同时的请求,看下时间,进行比较



ab -n 100 -c 10



in
 $mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

$result = $mysqli->query('select `id`,price,user_id from `order`');
$orders = $result->fetch_all(MYSQLI_ASSOC);

$userIds = implode(',', array_column($orders, 'user_id')); // 获取订单中的用户id
$result = $mysqli->query("select `id`,`name` from `user` where id in ({$userIds})");
$users = $result->fetch_all(MYSQLI_ASSOC);// 获取这些用户的姓名

// 将id做数组键
$userRes = [];
foreach ($users as $user) {
$userRes[$user['id']] = $user['name'];
}

$res = [];
// 整合数据
foreach ($orders as $order) {
$current = [];
$current['id'] = $order['id'];
$current['price'] = $order['price'];
$current['name'] = $userRes[$order['user_id']] ?: '';
$res[] = $current;
}
var_dump($res);

// 关闭mysql连接

$mysqli->close();

image.png


join
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

$result = $mysqli->query('select order.id, price, user.`name` from `order` join user on order.user_id = user.id;');
$orders = $result->fetch_all(MYSQLI_ASSOC);

var_dump($orders);
$mysqli->close();

image.png
看时间的话,明显join更快一些


三、试下多一些数据的情况


user表现在10000条数据,order表10000条试下


1、join


image.png


2、分开


order
image.png


user


image.png


3、代码层面


in


image.png


join


image.png


三、试下多一些数据的情况


随机插入后user表十万条数据,order表一百万条试下


1、join


image.png


2、分开


order


image.png


user


order查出来的结果过长了,,,


3、代码层面


in


image.png


join


image.png


四、到底怎么才能更好


注:对于本机来说100000条数据不少了,更大的数据量害怕电脑卡死


总的来说,当数据量小时,可能一页数据就够放的时候,join的成本和速度都更好。数据量大的时候确实分开查的成本更低,但是由于数据量大,造成循环的成本更多,代码执行的时间也就越长。实验过程中发现,当in的数据量过大的时候,sql过长会无法执行,可能还要拆开多条sql进行查询,这样的查询成本和时间一定也会更长,而且如果有分页的需求的话,也无法满足。。。


感觉这两个方法都不是太好,各位小伙伴,有没有更好的方法呢?


终于在尾巴上赶上了这个活动


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

同事问我为什么电脑屏幕上会有那么多球在飘

记得以前用的Windows电脑里面,有一个屏保程序就是在屏幕上出现很多飘来飘去的球,当球碰到电脑边缘的时候,会反弹到相反的方向,然后最近就琢磨着能不能使用Compose DeskTop也实现一个这样的效果,那以后我的Mac屏幕上也能出现好多小球,那简直是泰裤辣...
继续阅读 »

记得以前用的Windows电脑里面,有一个屏保程序就是在屏幕上出现很多飘来飘去的球,当球碰到电脑边缘的时候,会反弹到相反的方向,然后最近就琢磨着能不能使用Compose DeskTop也实现一个这样的效果,那以后我的Mac屏幕上也能出现好多小球,那简直是泰裤辣~


设计思路


我们把整体动效拆分一下总共有五步,每一步都不是很难




  • 第一步:使用循环动画不断改变小球位移的x坐标与y坐标,x坐标的变化范围是0到窗口宽度的最大值,y坐标的变化范围是0到窗口高度的最大值

  • 第二步:判断当x,y坐标到达自己的最大值的边界值的时候,将各自的变化范围的初始值与最终值互相对换一下,达到往相反方向移动的效果

  • 第三步:通过改变tween函数的durationMilliseasing属性,来改变小球的位移速度与位移路线

  • 第四步:将小球的动画需要的属性作为函数的入参,达到可以在上层定制小球动画的效果

  • 第五步:将窗口的宽度与高度更改成屏幕的宽高,背景色改成透明



让球动起来


首先我们来把球的样式做出来,球本身就是个圆形,我们使用Surface组件就可以完成,里面再包一个Box组件,这样做的目的是因为Surface没有办法设置渐变的背景色,我们如果想要让圆形看起来立体一些,就需要让背景色带点渐变,所以渐变的工作就交给里面的Box组件来完成


image.png

然后就可以把这个球放到我们的窗口里面去了,在这之前我们先创建三个常量,分别是窗口的宽高最大值以及小球的大小


image.png

然后把这三个常量分别设置给Window组件以及ball组件,代码与效果就如下图所示


image.png

接下去就是让这个球动起来了,我们通过改变球的位移坐标来实现球体的移动,这里给位移坐标的x,y分别设置一个无限循环动画,动画的初始值是为0,目标值为窗口的宽高,动画时间设置为5秒,然后让这个动画过程线性改变,实现过程如下所示


image.png

我们给Surface组件添加了offset操作符,让它接收mainxmainy的变化值,我们这个球就动起来了


0602aa2.gif

改变位移方向


现在已经让球动起来了,接下来就是要考虑如何让球“碰壁”以后反弹,由于我们的初始位置在窗口左上角,所以我们可以先做碰到下面以后的反弹以及碰到右边以后的反弹,也就是当x坐标到达或者接近x轴位移的最大值,或者y坐标到达或者接近y轴的位移最大值以后,我们将mainxmainy的初始值与目标值对调一下,这样就能往相反方向移动了,注意这里说的是位移最大值,不是窗口的宽高,因为球位移坐标是从球的左上角开始计算的,当碰到窗口边界的时候,其实位移距离是窗口的宽高减去小球的直径大小,所以我们再加上两个常量作为位移的最大值,方便后面计算时使用


image.png

然后如果想要在无限循环动画里面改变初始值与目标值,我们就要使用Animatable来切换,所以这里再创建四个Animatable的变量,分别代表x,y轴的初始值与目标值


image.png

创建好了以后,就直接把mainxmainy的初始值与目标值替换成了新建的四个Animatable变量,这样当我们去切换它们的值以后,mainxmainy的变化范围也发生了改变,而Animatable的切换函数snapTo是一个挂起函数,所以还需要一个协程作用域,我们这里使用rememberCoroutineScope函数来创建,那么小球碰到窗口下边与右边的反弹代码就有了


image.png

这边判断到达边界的条件不是mainx.value.value == offsetx.value的原因是因为通过打印日志发现,mainx或者mainy的变化值不会一直刚好是offsetX.value或者offsetY.value,所以只能把判断当两个值接近的时候当作小球移动到边界的条件,我们运行下看看反弹效果


0602aa3.gif

动图上看不出来,实际效果其实达到边界时候有点细微的抖动,这也跟我们刚刚那个边界值的判断条件有关,不过也不影响功能,我们按照这个方式把碰到左边与上边的代码也加上,一个完整的球体移动动画就做好了


image.png
0602aa4.gif

现在已经能够实现小球碰到窗口四周反弹的效果了,但是实现方式还是比较繁琐的,又是协程又是切换又是看边界值的,我们其实还有更简单的办法,因为不管是x轴的值还是y轴的值,它的变化范围始终在两个值之间,差别就是每次起始位置不同,那么这就是一个反复的过程,而我们这个循环动画其实就可以设置反复模式,使用repeatMode属性,值取RepeatMode.Reverse就可以了,我们试一下


image.png

我们看到现在我们把那四个Animatable都去掉了,mainxmainy的初始值与目标值又回到了固定值,区别就是增加了repeatMode,现在我们在看下实现效果咋样


0602aa5.gif

看起来好像差别不大,但其实碰到边界后的效果比之前要好多了,因为不用去关心那一点误差,而且也可以随意设置动画时间,之前为了让动画的变化值不要变化的太大,所以动画时间我是最小只能设置成5秒,现在的动效看起来就舒服多了


改变速度与路线


动画速度的话我们刚刚其实已经实现了,通过改变动画时间durationMills来实现,但是由于我们的easing设置的是LinearEasing线性变化,所以小球的位移路线永远都是沿着一根直线移动的,我们可以通过改变easing的值,来改变小球的位移路线,比如现在我将easing改成FastOutSlowInEasing


image.png

得到的效果就是这样的


0602aa6.gif

这球一下子就变得好像“有智商”了一样,感觉要“撞了”就马上减速,然后换个方向继续飘


属性作为参数,让小球可定制


想要定制小球动画的话,首先要确定好哪些属性可以拿出来定制,通过上面的开发,我们这个小球动画可以被定制的属性有以下几个



  • ballSize:小球的大小

  • ballColor:小球的颜色

  • xTime:小球位移x轴上的动画时间

  • yTime:小球位移y轴上的动画时间

  • xAnimateEasing:小球x轴上动画的变化速度

  • yAnimateEasing:小球y轴上动画的变化速度


这样子的话我们ball函数的参数列表就如下所示


image.png

然后再将代码中的对应位置用参数来代替


image.png

我们这里把计算最大位移的步骤也移到函数里面了,这样就可以根据不同的小球大小来计算各自的位移距离,我们这个ball函数到这里算是完成了,现在我们就可以想弄几个小球就弄几个小球了,比如我这边就弄了这么几个小球


image.png

下面就是一堆小球的效果


0602aa7.gif

我们再改下小球的样式,将小球弄成背景有点透明的样子,让飘动的小球看起来像是气泡一样,改完以后的小球代码如下


image.png

然后再将调用ball函数的地方,ballColor的入参也改成带点透明值


image.png
0604aa1.gif

如效果图所示,是不是有那么点意思了呢,现在我们进行最后一步。


将窗口透明,宽高增大为全屏


想要将窗口弄成透明的话,可以使用Window组件的transparentundecorated属性,代码如下


image.png

然后把screenWidthscreenHeight大小设置成全屏大小就可以了,我们使用ToolKit来获取屏幕宽高


image.png

还差一步,因为到了这里就算screenWidthscreenHeight设置成全屏宽高了,但实际上所在的窗口并没有真正的全屏,它跟屏幕左边留有一点距离,然后右边延伸至屏幕外边了,所以我们需要让整个窗口居中显示,使用WindowPosition,这个是WindowState里面的一个参数,我们在WindowPosition中设置成居中对齐就可以了


image.png

最终我们得到的效果就是这样的


0604aa2small.gif

总结


整体效果实现起来还是蛮容易的,总共代码加一块也不到一百行,感觉把Window设置成透明以后,DeskTop开发变得好玩多了,大家有兴趣的也可以尝试下,所有元素都可以按照自己喜好来定制,去设计属于自己的屏保程序


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

天气太热,希望这个小风扇能给你带来一点凉意

最近气温多变,这几天又回到了三十多度的高温天气,在这样的天气里面如果办公室里面不开个空调或者电风扇的话,那么是很难集中精神工作的,空调的话可能每个办公室都有,但风扇的话估计要自己去准备了,如果还没来得及准备的话,那么可以先考虑下在桌面上画个风扇看着它吹,毕竟古...
继续阅读 »

最近气温多变,这几天又回到了三十多度的高温天气,在这样的天气里面如果办公室里面不开个空调或者电风扇的话,那么是很难集中精神工作的,空调的话可能每个办公室都有,但风扇的话估计要自己去准备了,如果还没来得及准备的话,那么可以先考虑下在桌面上画个风扇看着它吹,毕竟古人有望梅止渴,我们今天就来画风扇降温


源码地址


github.com/coffeetang/…


准备工作


首先考虑下这个风扇的结构,我们这个风扇总共有这几个部分组成,分别是底座,立柱,扇框,扇叶,底座上有总开关,有可以调节风速强度的开关,那么整体来看是个上下结构,底座在最下面,其他的在上面,除了底座其余的都绘制在一个Canvas里面,大致结构如下


image.png

扇叶


风扇的扇叶,其实可以看成是在画布上画扇形,而我们画扇形就要用到函数drawArc,这个函数的传参列表如下所示


image.png

参数我们都很熟悉了,这里绘制扇形所需要用到的参数有



  • brush:用来设置渐变色的

  • startAngle:表示起始角度

  • sweepAngle:表示扇形角度

  • userCenter:表示扇形两端是否与圆心相连

  • topLeft:表示绘制扇形范围的左上角坐标

  • size:表示扇形的绘制范围

  • style:默认值就是Fill填充的,所以我们可以不用去挂心


根据参数,我们需要创建几个变量,首先是圆心坐标,它是取的Canvas的中心坐标,所以无论窗口变大变小,我们的圆心坐标都会在整个画布的中心位置


image.png

其次是我们的半径,半径的大小决定了整个扇形绘制的范围大小


image.png

那么我们绘制一个扇形的代码就是下面这样的


image.png
image.png

一个扇形就这样画出来了,而一个风扇总共有三个扇叶,咱要画三个扇形,而且是圆周上等分的,该怎么画呢?这里使用这个方法,首先创建一个数组,这个数组里面是每个扇形要用到的渐变颜色


image.png

然后再创建一个数组,这个数组里面是每个扇形的startAngle


image.png

那么首先我们就可以通过遍历数组的方式,把六个扇形都画出来


image.png
image.png

我们看到这个时候界面上展示的就是一个由六个扇形组成的大圆形,然后我们把colorList里面每隔一组颜色就把颜色改成透明的,那么这个圆形就看起来就像是三个被等分的出来的扇形一样了


image.png
image.png

扇框与立柱


扇叶已经完成了,接下来就是绘制扇框与立柱的工作,这两个都比较容易,立柱就是从圆心位置向下绘制出一条直线


image.png
image.png

然后我们在扇形靠外一点的位置绘制一个圆形作为扇框的边框,这里用到了drawPath函数,drawPath的第一个参数是Path,所以我们先将Path做出来


image.png

然后再调用drawPath函数,将framePath传进去


image.png
image.png

然后就是风扇前面的网罩,网罩常见的有从中心向外延伸出去的一条条直线,也有的就是一个个井字格组成起来的样子,这边按照前者做个网罩样式出来,这种样式与扇叶的思想有点接近,都是按照角度在一个圆周上等分的绘制样式,所以我们首先需要确定好这些角度,也有一个list维护起来


image.png

这里就是有45根线,每过8度画一根,而我们知道绘制线条用到的函数drawLine需要知道一个start坐标和一个end坐标,start都知道是圆形坐标,而end的xy坐标就要根据角度与半径算出来了,计算的代码如下所示


image.png

第一个参数就是网罩的半径长度,第二个参数为圆形x坐标或者y坐标,第三个参数是角度,那么我们就可以使用这两个函数,遍历lineAngleList来绘制出网罩


image.png
image.png

绘制风扇部分就完成了,下面开始开发底座上的开关


总开关与强度开关


底座上的开关分两个区域,左边是调节强度的区域,右边是总开关区域,总开关设计成一个滑块的样式,滑块默认在左边为关闭状态,点击或者拖动滑块,滑块滑动到右边,状态变成开启,滑块高亮,首先建立一个变量用来记录当前开关状态,再定义两个常量分别代表关闭与打开


image.png

默认为关闭状态,滑块的实现我们需要用到swipeable操作符,参数列表如下


image.png

其中我们需要用到的参数有



  • state:滑块的状态,需要监听滑块的状态来更新开关的状态值

  • anchors:锚点,某个位置对应滑块的一个状态值

  • thresholds:阈值,判读一个鼠标拖动事件滑动到某个位置的时候,这个位置属于哪种状态,那么当鼠标停止拖动时候,滑动可以animate到对应状态位置

  • orientation:拖动方向


根据需要的参数我们来创建对应的变量,滑块的代码如下所示


image.png
image.png

这里滑块的背景颜色会根据开关状态来变化,而开关的状态我们就通过监听swipeState来更新


image.png
0606aa1.gif

我们拖动滑块改变开关状态的功能如上图所示完成了,而点击滑块边上区域来改变开关状态就需要在滑块父布局上添加点击事件,点击一次更新滑块状态,而更新操作需要用到SwipeableState里面的animateTo函数,这个函数是个挂起函数,所以还需要给它提供一个协程作用域,恰好我们更新滑块状态是根据点击来触发的,所以这里选择使用LaunchedEffect函数


image.png
0606aa2.gif

滑块部分就做完了,然后是左边区域的调节强度功能,这个区域准备由三个色块组成,每个色块都可以点击,每点击一个色块,强度设置成对应级别,符合该级别的色块颜色高亮,否则就变暗,所以这里也需要一个表示强度的变量值


image.png

然后添加上三个色块以及每个色块的逻辑代码和点击事件


image.png
0606aa3.gif

这里每个色块高亮的条件都不一样,但是有个共同条件就是必须是开关状态开启的情况下才能高亮,如果关闭的话,所有色块都会变暗,另外fanState对应的值有1000,600,200的原因我们接下去会说,这个跟风扇的转速有关


让风扇转起来


让风扇转起来从代码的角度就是让扇形每次绘制的位置不同就可以了,而从我们绘制扇形的函数drawArc里面的参数来看,要更改扇形绘制的位置那就是改变startAngle的值,也就是它的初始值加上一个时刻改变的值,这个改变的范围就是0f~360f,那么对于这个循环变化的过程,我们肯定第一个想到的就是使用循环动画


image.png

这边我们看到了,fanState的值其实就是循环动画的时间,fanState的值越小说明转速越快,另外动画的初始值与目标值也加上了fanState的值,这样做的目的是为了当fanState变化时候,需要让Composable函数animateFloat触发重组,这样才能重新生成新的InfiniteRepeatableSpec对象,改变转速,不然的话,animateFloat的三个参数都不发生变化也就不发生重组,wheelState依然还是最初的值,wheelState的值拿到以后,就可以把它加给drawArc函数里面的startAngle


image.png

这里还加了一个判断,当开关是开启状态下,drawArcstartAngle才加上变化值wheelState,当关闭状态下,则不加这个值,也就是风扇处于静止状态,我们再来看看效果


0606aa5.gif

总结


我们这个风扇也做完了,用到的知识点都是平时Compose开发中常用,像是循环动画,Canvas绘制以及手势操作之类的,就这样简简单单在电脑屏幕上画出来了一个风扇,不知道看完这篇文章的你能否感受到一丝凉风~


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

程序媛员的博客之旅

写博客困境 自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。 无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没...
继续阅读 »

写博客困境


自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。


无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没办法继续更新下去了。所以工作了这么多年,自己都成为老学姐了,还是没有拿得出手的个人博客,实在是惭愧。


经过深刻的自我反省之后,我觉得阻碍我更新博客的原因,主要有以下几个方面:


1. 工作太忙。


大家都知道,相比其他工种,作为程序员的工作强度还是蛮大的。每天都有做不完的需求,开不完的会议。经常要到晚上快下班了才有时间写代码,于是加班就成了家常便饭。下了班回家也感觉很累,不想再打开电脑,只能刷刷手机、看看综艺,做一些不费脑子的娱乐活动。


2. 文字功底太差。


作为一名理科生,上学的时候语文成绩就很差,作文都靠模板以及背的素材拼凑起来。高中毕业之后,几乎没有完整写过什么。而写博客,需要高强度大量输出内容,还要有组织有架构,逻辑条理清晰,这个对我来说简直太难了。所以经常写两篇之后,发现自己写的东西惨不忍睹,于是就暂停了更新博客的计划。


3. 没什么内容可写。


虽然每天都在写代码,但是很多时候做的都是重复性工作,并没有太多有技术含量、技术深度的内容,可以支撑我写出高大上的博客。


我个人更新不下去博客的主要原因就是上面几点,相信有很多想要更新博客却坚持不下去的同学,也都有同样的感受。


如何突破困境


我想说一下,我为什么觉得自己这次能克服这几个问题,以及克服这几个问题的方法。如果大家也和我有类似的问题,可以往下读一读,看有没有什么可以借鉴的地方。主要还是给我自己未来的日更之旅打打鸡血。



一、工作太忙,没时间。



每个人的一天都是24小时,为什么有些人能做更多的事情,实现更高的成就呢?我觉得这和每个人的时间管理方式是息息相关的。掌握高效的时间管理策略,是每个高效能人士的必备技能。


我以前觉得是因为程序员的工作比其他行业更忙,所以没有时间。但是看周围,把博客或者副业运营很好的那群人,工作也不闲。所以说,这个理由只是一个对自己时间管理无能的借口而已。真正的强者,从来不会找没有时间的借口,而不去做一些尝试。


当下,为了能够实现工作、写作(其实是搞副业)和生活之间的平衡,我决定先从这几个角度来优化我的时间使用效率。


1. 为任务分配合理的优先级


事情是永远做不完的,如果想做的太多,那么时间永远都不够。我准备用重要紧急四象限法来管理任务。每拿到一个任务后,先决策这个任务是属于哪个象限的,然后再安排做的时间。
image.png


我们之所以感觉每天忙忙碌碌,却没有什么进步,主要是因为在“紧急-不重要”的事情上,浪费了太多的时间。仔细想想,上班时间有多少是浪费在了,对未来成长没有任何意义的所谓“紧急”的事情上了。而真正“重要”的事情,却被我们以没有时间做,而一直推迟。


前段时间看到的一句话,对我触动很大:
Today you do things others don't do.
Tomorrow you do things others can't do.


做“重要-不紧急”的事情,不会对你的人生产生立竿见影的效果。但是长期下去,效果一定是惊人的,而且能给你带去很多别人没有的机会。


“人们总是容易高估一天的影响,而低估长期的影响”。比如学英语、写作,可能努力了一个月都没有效果,很多人就开始放弃了,转而去寻找其他的方法。但有些人坚持了下来,于是这些人坚持了一年、两年甚至几年之后,最后到达了很高的高度,才发现原来每一天的坚持都没有浪费,最后都是有效果的。


2. 减少任务切换,提高做事情的效率


提高做事情的效率,最好的办法就是进入“心流”的状态。不管是写代码、写文字还是看书学习,在“心流”的状态下,效率比平时要提高好几倍。


“心流”的状态,就是一种忘我的境界,忘记了时间、忘记了周围所处的环境,甚至忘记了身体上的痛苦,专心沉浸在当下所做的事情上。我相信这种状态,大家多多少少都有体会,比如在废寝忘食打游戏的时候。这种状态下,人所爆发出来的潜能是巨大的。


要达到“心流”的状态,最简单易行的方法,就是减少任务的切换。就像CPU线程切换,需要缓存上一个任务的执行状态,加载下一个任务的运行环境,效率很低。人脑也是,在上下文切换的时候,需要耗费很多的时间和精力。


而工作中,经常会被工作软件的消息提醒所打断,很难进入”心流“状态。比如,正在尝试解决一个疑难的问题,但是突然来了一条工作上的消息,于是不得不中断当前的工作,去看这个消息。等处理完消息后,在回到工作,可能已经忘记之前做到哪里了,又需要花时间才能重新进入状态。


可以尝试”番茄钟"的方法。在每个番茄钟开始的时候,屏蔽消息,集中精神工作25分钟,然后再花5分钟处理这25分钟到达的消息。处理完后,进入下一个番茄钟。


3. 不要给自己定太高的目标


之前我写博客,总是一篇文章写很长,想要在一篇文章中讲完和标题有关的所有知识点。但是这样会让自己很累,每次写一篇文章都要花很长的时间和精力,到后面甚至排斥写文章这件事情。


所以这次,我决定不给自己设太高的目标,每篇技术文章,争取讲完一个知识点就可以,如果内容特别多,可以采用连载的形式。最后可以新建一篇索引的文章,将各个连载的文章串起来。


PS:时间管理是一个复杂的事情,我之前也看过一些相关的书籍,后续我也想通过更系统的文章分享出来。先在这里挖个坑,如果想看就先关注我吧,后续我会慢慢把坑都填上。



二、文字功底太差



另一个困扰我的因素,就是自己的文字功底太差了。几乎没怎么写过文章的我,不知道怎么表达自己。有时候心里有很多想说的话,但是一写起来就读不通,没办法完整表达自己的意思。


为了能顺利完成日更的目标,我决定尝试下面的方法。


1. 先写起来,自然而然就会有进步


第一个就是不管怎么样,不管写得有多烂,先写起来,以量变来引起质变。我现在的写作量,可能连那些大V一个月的量都不到,凭什么觉得自己的水平就能和人家一样。如果每天输出500字,一年就是18.25万字。坚持写,我相信写一年之后,水平肯定会有进步。


没有什么是刻意练习不能达成的,如果有,那肯定是练习不够多。


2. 多看多模仿


写文章也是有方法可以借鉴的。去看好的文章是什么样的,向优秀的文章和作者学习。


比如,我之前看一个技术博主,会在每篇文章的开头放一个脑图,描述整片文章的整体架构,我觉得这个方法就很好。首先自己可以根据这篇脑图往里填充资料,速度更快也更清晰,同时,读者也可以在看文章之前对文章的内容有一个整体的感知,很快就能定位到自己需要的内容上。之后我的文章也可以借鉴这个方法。



三、没什么内容可写



关于没什么内容可写,以前做业务开发的时候,确实有这个问题,但是现在做系统开发了,几乎每天都在学习新的知识,所以完全不愁没有内容可写了。


如果有同学想开始写博客,但是又觉得没有内容可写,可以从以下几个方面去尝试:


1. 提前想一些topic,主动积累


在开始写博客之前,提前收集一些topic。我现在就有一个文档,专门用来放我想写的文章topic,现在这个文档里面已经有几十个可以写的topic了。


提前脑暴一些topic,或者列一个知识图谱,到时候如果发现没什么内容可写,直接去list里面找一个topic就好了。


2. 主动去学习一些新的东西


对于一些业务开发的同学,可以在开发之余,主动push自己去学一些新的技术。比如看一些技术书籍和博客。


博客内容


之后我的博客,主要会围绕下面这些方向:


Android性能优化


作为一名Android开发,更新的内容主要还是在Android相关的技术点上。由于我近期工作的重点主要在性能优化方面,所以前期的文章主要会更新性能优化相关的文章,包括启动时间、存储空间、稳定性、ANR等优化方案,以及一些相关的技术原理。


Android面试集锦


等把Android性能优化相关的内容写完,会再写一些面试相关的内容。作为一个拿过各大互联网offer、一毕业就当上面试官的学姐,在面试方面还是有不少经验的。


算法题解


算法题可能也会写一些,写一些我觉得好的题目的题解(主要是算法题比较好水文章,实在不行了就来篇题解)


读书笔记


我平常也会读一些技术之外的书籍,会写一些读书笔记,到时候会更新一些这方面的内容。


新的技术方向


除了Android开发以外,未来想学习一些新的技术方向,到时候也会更新到这个博客里,比如Web3相关的内容。


杂七杂八的思考


一些思考想法,对当下事件的看法,对未来的思考,个人成长、时间管理、投资理财等等相关内容,都会记录在博客里。


总结


说了这么多,也不知道会不会有人看我写的文章,毕竟现在Android开发已经不流行了。而且ChatGPT兴起之后,普通的内容生产者,会受到非常大的冲击。可能以后查东西都不需要去搜博客文章了,直接问ChatGPT就好了。之后我的博客文章,说不定也会让ChatGPT帮我写一部分。


总之不管有没有人看,不管AI是否会把我的工作取代,我还是会把日更坚持下去。如果对我更新的内容感兴趣,欢迎点点关注呀~~


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

25岁学Java培训班学完后现状

前言 大家好,我是小江,好久不见。距离上一次发布文章已经过去三个月了,这段时间确实有点忙,都忘了稀土掘金这个平台了,最近想起来了,今晚来分享一下最近这段时间发生的事。先提前说一下,我已经在工作了。我将分别从培训班最后两个月,到找工作,最后入职来讲述我的经历。 ...
继续阅读 »

前言


大家好,我是小江,好久不见。距离上一次发布文章已经过去三个月了,这段时间确实有点忙,都忘了稀土掘金这个平台了,最近想起来了,今晚来分享一下最近这段时间发生的事。先提前说一下,我已经在工作了。我将分别从培训班最后两个月,到找工作,最后入职来讲述我的经历。


一、我在培训班最后两个月


还是老样子,我不会宣传是哪个培训班的。在培训班最后两个月,我学完了spring、springmvc、springboot、mybatisplus、Redis、springcloud、mq、es、springsecurity、Linux、Nginx、docker等技术,我个人springboot和Redis是重点,其他的看个人能力,没有实际项目积累光听是很难掌握的。


我自己的感觉就是确实进度很快,东西特别多,一两天学完一个组件,跟着老师听当时可以听懂,后面就忘了,只有个印象这个组件是干嘛的,我当时也很焦虑,进度这么快消化不了,而且了解到老师讲的也是简单的怎么用,往深的挖会有很多东西,我很害怕,最后一段时间又要开始背面试题了,感觉就是时间不够用。


最后两个月做了一个springboot项目和一个微服务项目,自己写的springboot项目的一个模块,功能也不难,就是练习使用框架开发,总共给了一个星期时间,我发现这次和前面项目嘎嘎闷头一直写不同了。首先是分析需求,设计数据库表,光这个我就花了两天时间把业务都梳理清楚,数据库也设计好了,才开始真正写代码,很多东西都封装好了,Java代码就没写多少,90%时间都花在写前端页面去了,前端确实不太熟,最后还是完美的写完了,看着项目启动页面展示的效果,我很高兴。给我的感觉就是代码量确实变少了,框架确实很方便。第二个微服务项目是我们看着老师写的,老师一边写一边讲,我都能听懂,也学到了很多。


二、学完找工作阶段


时间过得挺快的,学完走人那会我既兴奋,又恐惧,还有不舍。学完刚好四月底,我五一回老家了,在家写好了简历,背面试题,5.4号假期结束了,那天我上午9点准时Boss直聘上沟通,只投我意向的,在Boss直聘上沟通了五六十家,收到了三个面试邀请,我都安排在了5.6号面试。我第二天坐车离开家过去了,住的青年公寓。5.6号那天我早一个小时到了那公司,在楼下一直等到面试时间前10分钟上楼,我很紧张。


上去之后我说面试的,他们给了我一张纸先做题,四五个选择题我都会,填空题补充代码的我也会,后面就是两个写sql题,我不会了,我之前培训班项目用到的sql大都是增删改查,复杂点的就是多表联查。但这个题目是让我分组,子查询那些。我一时不会了,只知道思路,就只把思路写上去了。过了十分钟有人来了面试,问的都是比较基础的八股文,我都能答出个123来。下面是面试问的


Screenshot_2023-05-27-22-23-27-020_com.miui.notes.jpg
最后问我期望薪资,我是税后10k,他就让我回去等通知后面就没结果了。


后面面试我就不一一详解了,附点面试问的。


mmexport1685197778664.png


我就这样找了十几天,从刚开始的一天三面试觉得行情没那么烂,到后面一直没面试,海投了都已读不回,我心态也发生了变化,特别的焦虑。因为自己没也钱一直找家里要,一天没找到工作就一直消费下去。我感觉我自己八股文已经背的可以了,奈何后面面试都不问八股文了,直接怼着项目一直问场景,几下就看出我是包装的了。我后来想着要不要去一线城市去看看,觉得在这找不到了。都计划好了下个星期去上海杭州看看,结果周五有个面试,我去了也没抱太大希望,面试果然是问我项目场景,我没答好,然后问我springmvc源码等我都没答好,我想着没戏了也麻木了无所谓,最后他还是要了我。也很意外,进公司了解之后知道公司刚刚成立不久,正在招人,还要招几个。


入职后工作中的感受


我稀里糊涂的入职,第一天让我搞环境,我很多不会,同事都挺好帮我很多。后面直接给我任务让我开发了,给到任务后我感觉不会搞啊,业务太复杂了,微服务项目,模块太多了,十几个数据库,上千张表,我找字段都找好久,原本给我2天时间,我硬是搞了一个星期,不过领导蛮好的,虽然会说我几句,但都会一步步教我。


在这已经上班十几天了,感觉自己学到了很多东西,也慢慢能上手完成任务了。


结束语


好了,到这就分享完了,我也准备睡觉了。我感觉自己也蛮幸运的,也感觉这行确实要一直保持学习状态,一直努力下去。最后祝大家都能找到满意的工作


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

成功上岸字节!分享一些 Idea!

1. 前言想来今天已经入职字节 1 个多个月啦,身边也有很多迷茫的同学,经常询问我如何复习八股、准备面试。今天这篇文章主要给大家提供一个思路,提供一个我总结的面试公式,希望能提供一些新的维度供大家参考~大家好,我是ltyzzz。这可能是我第一次和大家做自我介绍...
继续阅读 »

1. 前言

想来今天已经入职字节 1 个多个月啦,身边也有很多迷茫的同学,经常询问我如何复习八股、准备面试。今天这篇文章主要给大家提供一个思路,提供一个我总结的面试公式,希望能提供一些新的维度供大家参考~

大家好,我是ltyzzz。这可能是我第一次和大家做自我介绍~之前一直在忙着玩 GPT 项目。

我背景是NUS计算机硕士,武汉理工EE本科,春招收获腾讯、字节等后端实习Offer,目前仍在字节实习,今天给大家分享一下面试准备经验(接近5000字),我认为也同样适用于大家日常的学习。现在我实习刚满一个月,之后我会陆续给大家分享实习的工作经验~

在开始分享之前,我想给大家抛出一个我认为的技术面试公式,仅供大家参考,欢迎大家一起讨论:

面试 = 40% 八股 + 30% 算法题 + 20% 项目经验 + 10% Idea

在接下来的面试准备经验分享中,我会着重介绍项目经验与Idea。

2. 八股&算法题

八股和算法题我想市面上资料已经数不胜数了,这里我简略说一下。

如果大家已经对此部分准备足够充分或者已经有着自己的方法论或学习路线,可以直接快进到 项目经验 & Idea

2.1 八股准备

对于八股准备,我主要以 Java Guide 和 小林Coding 为主,书本(如Redis设计与实现、JVM圣经、Java并发编程的艺术、高性能MySQL)为辅。如果大家时间紧张,可以不看书。此外,我是面试驱动复习,八股文复习与面试相互交叉,是一个相互促进的过程。

  • 第一轮复习我花费了大概1到2周的时间,粗糙浅显地过了一遍MySQL、Redis、JUC、JVM、操作系统、计算机网络、微服务等基础知识,大概是1~2天一个板块,能够简单的应付一下基础面试题,一轮复习完之后正好对应于字节跳动的一轮面试。
  • 第二轮复习我花费了大约20天的时间,着重深入地学习与复习之前各个板块的知识点,并搭配面经(百度或Google搜索:某某公司后端/前端面经),反复地查漏补缺,遇到陌生的题目或知识点,从书上或网上寻找答案,记录下来,便于之后复习。我大概看了不到50篇面经,梳理了接近150道不熟悉的面试题目,大家有需要的话,之后也可以在星球中分享给大家。第二轮复习与美团、字节、腾讯、阿里面试相互交叉。这段时间准备的很多八股面试题目,在面试中也有被问到。

大家可以参照我的复习路线与经验,以面试作为驱动力,高效地复习八股文。这一阶段不考验智商,只考验耐心、毅力。因为一轮复习的时候大家可能会很新鲜,接触或复习到很多有意思的知识点,感觉自己有很大的提升,这一阶段可能还比较有趣。但是在二轮复习尤其是穿插了面试之后,大家可能会遇到两个问题:一个是感觉到很慌,认为自己什么都不会,看一个面经慌一次;另一个是感觉到很枯燥但又不敢不看。不管是哪一个问题,都需要静下心来,戒骄戒躁,迅速调整心态,不要乱了阵脚。八股文这里我相信只要能花费20天~1个月的时间,每天拉满,一定能攻克。

2.2 算法题准备

对于算法题准备,没有任何捷径除非天赋加持,刷就完了。我当时候是LC刷了500多道题目,还有在其他平台也零散地做了一些题,总体刷了8个月。其实精刷200~300道题就足够用了。大家可以以Leetcode为主,着重做剑指Offer,最好做2~3遍。接下来我根据复习时间长短,提供两种策略。

  • 时间长且充裕:每天坚持刷LC每日一题,拓宽思路。抽出一定时间刷 LC HOT100 与 精选200 题单,也可以做一些知名的算法博主总结的题单(推荐宫水三叶姐的LC题单)。总之就是多刷多看,加上剑指Offer的题目,半年多时间可以刷够300多道题,足够应付一般算法题。
  • 时间短且紧:集中性地刷 剑指Offer,比如集中一周时间甚至更短。看题10分钟没思路的话就直接看题解,重复的刷题,反复不断地刻意训练,直到背过为止。若仍有余力,可以再抽空刷刷 LC HOT100 题单。即便时间如此之短,此时的刷题量也可达到100道题左右。

3. 项目经验

接下来,我将重点介绍 项目经验 与 "Idea"。

对于项目经验,我先为大家介绍项目,然后从项目准备中分享我的经验。

我准备了三个项目:智能停车场项目、仿B站项目、RPC项目。这些都是我自己日常学习的项目,不是实习项目。顺便说一下我在此之前只有一段很水的实习。

  • 智能停车场项目,简单总结就是一个增删改查项目,技术含量不是很高。前端通过小程序和后台管理系统展现。前端技术栈是Uniapp + Vue,后端技术栈是Springboot、SpringCloud、Mybatis等。只是用了一些简单的微服务技术(Feign、Gateway、Nacos),后台管理系统用了RBAC实现权限管理。但是在包装项目时,我添加了几个亮点:Redis数据缓存、分布式Session、分布式ID、分布式锁技术、与网络摄像头联调开启道闸。如果大家后续需要,我可以重构一遍项目后端,并开源出来,供大家学习(自认为小程序界面写的还算好看哈哈哈哈)。
  • 仿B站项目,这个项目含金量要高一些。它后端基本框架是我参考Ruoyi Cloud Plus实现的,脚手架自带了很多功能如数据脱敏、幂等、微服务限流、可观测监控、分布式Session单点登录、安全性措施等。光是脚手架自带的功能点就够在面试上聊很多。此外,该项目我着重于Redis相关的系统设计,如动态Feed流推拉、点赞评论相关的计数系统、数据缓存,运用了很多Redis的数据结构。此外,还设计了站内信、单聊群聊、视频弹幕等功能点。这个项目是我和朋友一起做的,还没有做完,所以暂时无法开源给大家。
  • RPC项目,这个项目是我参照掘金小册中的RPC做的。此类项目已经很多了,但是在面试过程中,还是会被经常问到这个项目是如何设计的。我一般会详细说出代理层、路由层、注册中心层、异步设计等的设计思路。有的面试官可能还会问压测相关的内容。如果说要将RPC项目写到简历中,一定要清楚核心功能的设计,并且反复地尝试自己练习表达几次。

现在,我来总结一下项目准备中需要注意的点:

  • 准备的项目一定要是自己非常熟悉的项目,起码写到简历当中的功能点能够经得起面试官的盘问。一般面试官也不会问的特别复杂,只要准备充分,都是可以回答上来的。不熟悉的功能点最好不要写,不打无准备之仗。
  • 准备的项目最好是两个以上,一个可以为Web前后端项目,另一个可以为框架开发、中间件开发。这样一方面可以体现你业务能力okay,熟悉常见的开发场景,当mentor或leader派活的时候,你知道如何下手去做,有自己的实现思路;另一方面可以体现你有一定的钻研自学能力与解决问题能力,能够啃动硬骨头。
  • 此外,我认为大家可以从日常开发中发现需求,自己设计网站从实际出发,去解决痛点,这样的话在面试中更是加分项。因为实习或工作中,就是从实际需求出发,解决一个个业务场景。面试官会更加认可你的项目。这一点也与我之后要说的 "Idea" 有关。

4. “Idea”

对于 "Idea",我认为占比是最少的甚至可能面试官压根不会问你,但是我认为它对我而言是最有用的,实际上也指导了我整个面试过程甚至是学习编程的过程。我这里的 "Idea"是指 你想要什么 & 你的一些灵光乍现的想法

  • "Idea"第一层:我认为需要清晰地认识到自己学习编程、想要进大厂是为了什么,这一点实际上直接或间接地指引着我们的日常学习或工作。我可以先和大家分享一下我在实习之前的 "Idea"(实习后我又有了新的认知与变化,这部分大家有兴趣我之后再做详细的分享)。

    • 我学习编程的目的就是觉得有意思,做网站、开发小程序、学习前后端、部署服务器等都很有挑战性,做出以后也很有成就感,于是一发不可收拾。想要进大厂一方面是因为大厂给钱确实多,另一方面还是因为想要进公司学到一些真正企业落地的技术,并能够真正做出一些产品或项目,直接点说就是想要干点真东西出来。此外我也对自己的职业生涯有着较为清晰的规划,我想的是工作中以后端为主,深耕技术,向架构师的方向迈进;日常学习中提升自己的技术广度,以兴趣为导向,涉猎各个领域,尝试各种新东西。
    • 我上述所说的 "Idea",确实对我面试过程中起到了推波助澜的作用。因为当面试官和我聊起日常学习、职业规划时,我整个人的头脑是清晰的,我可以清晰地给他讲述我的想法。这一点可以给面试官留下很好的印象,毕竟程序员面试并不仅仅是技术的考察,而是综合评估。此外,我还可以给大家举出一个最近组内的case,我一个同事面试其他后端实习生,但是当问到那位同学有没有投其他岗位时,他犹豫不定。他说自己不确定,之后会尝试算法岗。面试官就认为这位同学没有很清晰的规划,即使招进来,他可能心思也不会完全地投入到后端工作中。(可能还有其他多方面因素导致面试挂掉)
  • "Idea"第二层:是否有一些奇思妙想、是否正在尝试做自己的开源项目。这一点我在腾讯面试的过程中深有体会,我先和大家分享一下这段面试经历。

    • 三轮面试几乎没有问什么八股文,第三轮面试在拷打项目,另外两轮都是业务场景设计题以及聊日常学习、聊开源。尤其是第一轮面试给我的印象最深刻,面试官估计很多人都听说过,是一位PHP的开源大佬。面试中他一直在给我抛出与实际业务相贴近的场景设计题,开放题目,没有固定答案,我觉得是在考察我的思维广度和技术广度。这些问题我也都给出了自己的思考。此外,我们也聊到了开源项目,他向我分享了他做开源的初衷与过程。而我也一直想要去做自己的开源项目,我和他说了我的想法。这一场面试也让我学习到了很多在八股文中无法学习到的东西。第二轮面试中,面试官最后问了我最近在学习什么,我很自然地说了关于GPT的一些话题以及我想要做一个AIGC的简历项目。本来面试已经准备结束了,但是面试官可能对这个话题比较感兴趣,又和我聊了不到10分钟。
    • 从我这两场面试中,我感觉到有着自己的想法很重要,是否每天有在探索并思考一些新东西,并去实际地做出来一些有意思的项目或产品。这一点是我从学编程以来的兴趣所在,兴趣推动着我不断地去探索一些新东西,经常性地产出自己的"Idea"。尽管有一些似乎和后端关联性不大,但是它其实最终在一定程度上帮我拿到了Offer。而且"Idea"也可以促使自己即使在春招秋招结束后,还能够有自驱力并且快乐开心地去学习编程,这一点也是我认为最重要的。

总结一下"Idea"就是:清晰认知自我,不断尝试探索。

5. 后记

以上这就是我的面试经验分享啦~

这也是我第一次在自己的博客对外分享我的面试经验,希望能给大家带来帮助!

此外,如果大家感兴趣,我可以之后分享一下我近期实习工作的经验思考,我相信一定能够给大家带来新的启发~

后续我会继续分享一些学习心得、技术干货和实战项目,帮助大家在 金九银十 斩获各路大厂 Offer!


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

天黑了,开个灯继续看书

像素点这个词对于前端来讲可能与UI设计师们打交道的时候用的会比较多一些,比如两个控件之间距离多少个像素,这个文字离容器边距多少个像素,这里的像素通常是以一种度量单位而存在,那大家有没有试过将像素点作为一个个小的控件来使用呢?让每个像素点可以变大变小,颜色可以变...
继续阅读 »

像素点这个词对于前端来讲可能与UI设计师们打交道的时候用的会比较多一些,比如两个控件之间距离多少个像素,这个文字离容器边距多少个像素,这里的像素通常是以一种度量单位而存在,那大家有没有试过将像素点作为一个个小的控件来使用呢?让每个像素点可以变大变小,颜色可以变深变浅,那么今天这个文章我们继续在Compose DeskTop上来带大家看看像素点的另一种玩法。


画出像素点


首先我们在Canvas上画出所有像素点,下面是画像素点所需要的变量


image.png

  • screenW:画布宽度,在Canvas中通过Size实时更新

  • screenY:画布高度,在Canvas中通过Size实时更新

  • gridCount:宽度或者高度上需要分出的像素数量

  • xUnit:单个像素的宽度

  • yUnit:单个像素的高度

  • pRadius:需要绘制的小圆点半径

  • xList:所有绘制出来的小圆点的x坐标

  • yList:所有绘制出来的小圆点的y坐标


然后在Canvas里面遍历xListyList这两个集合,将所有小圆点都画出来,怎么画的大家都很熟悉了,使用drawCircle函数


image.png

前方密恐福利~


image.png

我们已经获得了一堆小黑点,现在我们来尝试下更改一些小黑点的透明值,比如从我点击某一个位置开始,该位置的透明值最小,然后逐个向外透明值变大,直到透明值变成1为止,代码如下


image.png

这里新增两个变量tapXtapY用来保存点击位置的x坐标与y坐标,在循环遍历的代码里面,新增了xdisydis两个变量表示透明值,并且从点击位置开始向外递增透明值逐个变大,到第十个黑点的时候透明值就变成1了,当屏幕点击以后,xdisydis同时都小于1的点在绘制的时候都设置透明值alpha,否则就不设置透明值,下面是效果图


0613aa1.gif

圆点的透明值已经从点击位置开始向外变大了,我们再用同样的逻辑,让圆点的半径逐个向外变大


image.png

这里就做了一个小改动,将半径去乘上刚才算出来的透明值,让那些变透明的圆点同时也能有一个大小上的变化,我们再看下效果图


0613aa2.gif

可以看到我们的小圆点已经呈现出向外扩散,并且在色值与大小上都有了一定的变化,但是如果说扩散的话这里还看着有点别扭,因为一般的扩散都是以一个圆形的形状扩散的,而这里是正方形,所以我们得想办法把这个正方形也弄成圆形,怎么弄呢?那就要改变一下计算透明值与半径大小的方式了,之前是按照向外扩散经过的圆点个数来逐渐改变圆点的透明值与半径大小的,关键代码是这俩句


image.png

那么这种计算方式肯定是斜着的方向扩散的距离要大一些,所以我们不能再限制个数了,而是限制一下扩散的距离,也就是将这个扩散的圆的半径得先确定好,比如变量circleSize就是这个扩散的半径


image.png

然后我们需要做的就是计算出两点之间的距离除上这个circleSize,得到的值如果小于1那么就是我们需要的透明值,大于等于1我们就正常处理,这里唯一需要琢磨的就是如何计算两点之间的距离,四个字,勾股定理


image.png

最后一步开根号kotlin里面有现成的函数sqrt,那么计算两个小圆点之间的距离以及透明值的代码如下所示


image.png

接下去只需要将画圆点的透明值设置成div以及半径去乘上div就好了


image.png
0614aa1.gif

我们看到效果图上扩散的区域已经变成了一个圆形了,到了这里我们像素点的主要玩法就讲完了,接下去就是利用上面讲到的知识点,来做一个开灯关灯的效果


关灯后的效果


关灯后一般都是漆黑一片,但隐约还能有点能见度,所以我们这里的黑也要带点透明度,然后圆点的个数也要增多,要让单个圆点变得不明显,所以gridCount首先增加到300


image.png

然后将非扩散区域的背景色调成有点透明的黑色,并且增大圆点半径值,目的是去除圆点之间的间隙,扩散的圆点的背景色也设置成带点透明,并且半径在乘上div的基础上再减小一点,目的是加强扩散区域的灯光朦胧感


image.png
0614aa3.gif

绘制电灯,确定扩散中心位置


到了这里,扩散区域的代码暂时先告一段落,我们将电灯绘制出来,后面电灯的灯泡就作为我们扩散的中心区域,绘制电灯都是些基本Canvas绘图技巧,不多做介绍,直接贴上电灯的代码


image.png

drawCircle函数用来画灯泡,灯泡的中心点就是我们扩散的中心坐标tapX,tapY,函数drawline是画的电线,函数drawArc是画的罩在灯泡外面的灯罩,另外tapX,tapY的具体值就从点击获取变成了一个固定值


image.png

整个电灯的代码就完成了,效果如下


image.png

调节电灯亮度


当我们在生活中调节灯泡亮度的时候,灯泡的亮度会越来越亮,颜色会越来越深,那么这边如果要实现这一点的话,就需要确定一个最亮值以及最暗值,然后通过函数animateColorAsState来创建个颜色过渡的动画过程


image.png

lightState是这个开关灯的状态,作为当前所在的函数的入参从上层传进来


image.png

我们在最外层Window函数里面建立个菜单项,添加两个选项开灯与关灯,用来控制lightState的值


image.png

有了切换状态的开关,就开启了颜色过渡的动画,灯泡的色值就用lightColor来取代


image.png
0614aa4.gif

调节灯光扩散区域大小


灯光亮度能够有个由弱到强的过程了,那么灯光的扩散范围也应该有所变化,而上面我们已经知道了,控制扩散区域大小的变量就是circleSize,所以我们只要通过改变circleSize就能达到改变扩散范围的目的了,这里同样也创建个circleSize的过渡动画


image.png

然后我们更改下绘制扩散区域的条件,之前是将div小于1作为绘制扩散区域条件,现在就不需要加这个限制了,因为灯光照射的范围肯定是整个窗口范围,所以最后扩散的区域一定是到窗口以外的地方,但是绘制的条件还是有的,那就是circleSize大于最小值的时候,所以最终的代码如下


image.png

至于为什么不将判断条件设置成判断开关的开启状态是因为当开关关闭的时候,窗口一下子就变黑了,这里也希望关闭时候也有一个过渡的效果,我们看下现在的效果


0614aa5.gif

开关灯的效果就做好了,现在我们可以找一张看书的图片,然后在最外层用Image组件展示出来,那么开灯看书的效果就做好了


image.png
0614aa6.gif

源码地址


总结


最近在各种琢磨怎么做点好玩的动画效果出来,感觉在Compose里面做动效比在Android View里面简单多了,比如像这篇文章里面说到的内容,是不是改一改,一个水波纹效果就出来了,再改一改,一个数据加载完以后的转场效果也出来了,大家也可以在自己项目里面动手试试看


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

RecyclerView优化实战指南

在 Android 开发中,RecyclerView 是一个非常常用的组件,用于展示大量数据。然而,如果不进行优化,RecyclerView 可能会导致 UI 卡顿、内存泄漏等问题。本文将介绍一些优化技巧,帮助你更好地使用 RecyclerView。 简介 R...
继续阅读 »

在 Android 开发中,RecyclerView 是一个非常常用的组件,用于展示大量数据。然而,如果不进行优化,RecyclerView 可能会导致 UI 卡顿、内存泄漏等问题。本文将介绍一些优化技巧,帮助你更好地使用 RecyclerView。


简介


RecyclerView 是 Android 的一个高级 UI 组件,用于展示大量数据。它可以自动回收不可见的视图,并且可以使用不同的布局管理器来实现不同的布局。RecyclerView 还提供了一些回调函数,允许你在视图复用时进行一些自定义操作。


RecyclerView 可以大大简化开发过程,但是如果不进行优化,它可能会导致一些性能问题。下面将介绍一些优化技巧,帮助你充分发挥 RecyclerView 的性能。


优化技巧


对于 RecyclerView,我们可以采用以下优化技巧:


1. 使用 DiffUtil


DiffUtil 是计算两个列表之间差异的工具类,可帮助 RecyclerView 局部刷新数据。使用 DiffUtil 可以提升性能,减少 UI 卡顿。在 Adapter 中重写 DiffUtil.Callback,创建新列表的 DiffResult 与旧列表进行比较,从而更新列表数据。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
fun updateData(newData: List<Data>) {
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = dataSet.size
override fun getNewListSize() = newData.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
dataSet[oldItemPosition].id == newData[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
dataSet[oldItemPosition] == newData[newItemPosition]
})
diffResult.dispatchUpdatesTo(this)
dataSet = newData
}
}

2. 使用 ViewHolder


ViewHolder 是一种模式,用于缓存 RecyclerView 中的视图,减少内存开销,提高性能。使用 ViewHolder,可以在 Adapter 中重写 onCreateViewHolder 方法创建 ViewHolder,并在 onBindViewHolder 方法中获取 ViewHolder 显示的 view,并更新数据。


代码演示:

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.title)
val subTitleTextView: TextView = itemView.findViewById(R.id.subtitle)
// ...
}

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.titleTextView.text = dataSet[position].title
holder.subTitleTextView.text = dataSet[position].subTitle
// ...
}
}

3. 使用异步加载


如果 RecyclerView 需要加载大量数据,可以考虑使用异步加载来避免 UI 卡顿。以下是异步加载的示例:在 onBindViewHolder 中使用线程池 executor 和 ImageLoader 下载图片,并在下载完成后将其设置到 ImageView 上。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
if (dataSet[position].imageURL != null) {
holder.imageView.setImageResource(R.drawable.placeholder)
holder.imageView.tag = dataSet[position].imageURL
executor.execute {
val bitmap = ImageLoader.fetchBitmapFromURL(dataSet[position].imageURL!!)
if (holder.imageView.tag == dataSet[position].imageURL) {
holder.imageView.post { holder.imageView.setImageBitmap(bitmap) }
}
}
} else {
holder.imageView.setImageBitmap(null)
}
// ...
}
}

object ImageLoader {
// ...
fun fetchBitmapFromURL(url: String): Bitmap? {
// ...
return bitmap
}
}

4. 合理使用布局管理器


RecyclerView 提供多种布局管理器,每种管理器都适用于不同的场景。我们应该根据具体需求选择适合的管理器。以下是布局管理器的示例:


代码演示:

val layoutManager = when (layoutType) {
LayoutType.LINEAR -> LinearLayoutManager(context)
LayoutType.GRID -> GridLayoutManager(context, spanCount)
LayoutType.STAGGERED_GRID -> StaggeredGridLayoutManager(spanCount, orientation)
}
recyclerView.layoutManager = layoutManager

5. 使用数据绑定


数据绑定是一种将数据直接绑定到视图上的技术,减少代码量,提高代码可读性。我们可以在 adapter_layout.xml 中使用 <layout> 标签,将数据绑定到视图的布局文件中,从而减少代码量。


代码演示:

<layout>
<data>
<variable name="data" type="com.example.Data" />
</data>
<LinearLayout ...>
<TextView android:text="@{data.title}" ... />
<TextView android:text="@{data.subtitle}" ... />
</LinearLayout>
</layout>

在 Adapter 中使用 DataBindingUtil.inflate 方法,将 layout 绑定到 Data 中并设置到 ViewHolder 上。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding.root)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.binding.data = dataSet[position]
// ...
}
// ...
}

6. 减少布局中嵌套层级


布局中的嵌套层级越多,性能就越低,所以需要尽可能减少嵌套层级。可以使用 ConstraintLayout 或者扁平布局来减少嵌套层级。


7. 设置 Recyclerview 的固定大小


在 Recyclerview 的布局中,设置 android:layout_heightandroid:layout_width 的值为具体数值,可以避免列表项的宽高随着内容的变化而变化,从而使布局横向和纵向的测量也相应变快。


8. 禁止自动滑动


当数据项发生变化,RecyclerView 默认会自动滚动到新位置。如果这种行为不是必需的,可以在 Adapter 中重写 onItemRangeChanged 方法,并在其中禁止滑动。


代码演示:

override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
if (itemCount == 1) {
notifyItemChanged(positionStart)
} else {
notifyDataSetChanged()
}
recyclerView.stopScroll()
}

9. 使用预加载


使用预加载技术可以使 RecyclerView 在滑动过程中提前加载更多数据,保证滑动的流畅性和用户体验。


这些技巧可以根据具体的应用情况来使用,针对不同的问题提供不同的解决方案,从而提升 RecyclerView 的性能。如果需要更高级的功能,可以考虑使用 RecyclerView 提供的其它高级接口。


结论


通过本文,我们介绍了一些优化 RecyclerView 的技巧,包括使用 DiffUtil、使用 ViewHolder、使用异步加载、合理使用布局管理器、使用数据绑定、减少布局中嵌套层级、设置 RecyclerView 的固定大小、禁止自动滑动、使用预加载等。我们可以根据实际需求选择合适的优化方案,提升 RecyclerView 的性能,使其更加流畅。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


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

安卓-入门kotlin协程

作者 大家好,我叫小琪; 本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队; 目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。 一些概念 在了解协程之前,我们先回顾一下线程、进程的概...
继续阅读 »

作者


大家好,我叫小琪;


本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队;


目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。


一些概念


在了解协程之前,我们先回顾一下线程、进程的概念


img

1.进程:拥有代码和打开的文件资源、数据资源、独立的内存空间,是资源分配的最小单位。


2.线程:从属于进程,是程序的实际执行者,一个进程至少包含一个线程,操作系统调度(CPU调度)执行的最小单位


3.协程:



  • 不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行

  • 进程、线程是操作系统维度的,协程是语言维度的。


协程特点



  • 异步代码同步化


下面通过一个例子来体验kotlin中协程的这一特点


有这样一个场景,请求一个网络接口,用于获取用户信息而后更新UI,将用户信息展示,用kotlin的协程这样写:

GlobalScope.launch(Dispatchers.Main) {   // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
tvName.text = user.name // 主线程更新 UI
}

而通过 Java 实现以上逻辑,我们通常需要这样写:

api.getUser(new Callback<User>() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvName.setText(user.name);
}
})
}

@Override
public void failure(Exception e) {
...
}
});

java中的这种异步回调打乱了正常代码顺序,虽说保证了逻辑上是顺序执行的,但使得阅读相当难受,如果并发的场景再多一些,将会出现“回调地狱”,而使用了 Kotlin 协程,多层网络请求只需要这么写:

GlobalScope.launch(Dispatchers.Main) {       // 开始协程:主线程
val token = api.getToken() // 网络请求:IO 线程
val user = api.getUser(token) // 网络请求:IO 线程
tvName.text = user.name // 更新 UI:主线程
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码


协程初体验


1.引入依赖

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'

2.第一个协程程序


布局中添加一个button,并为它设置点击事件

btn.setOnClickListener {
Log.i("TAG","1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
CoroutineScope(Dispatchers.Main).launch{
delay(1000) //延迟1000ms
Log.i("TAG","2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
}
Log.i("TAG","3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程....[当前线程为:main]
3.BtnClick.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]

通过CoroutineScope.launch方法开启了一个协程,launch后面花括号内的代码就是运行在协程内的代码。协程启动后,协程体里的任务就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来


进入到launch方法看看它里面的参数,

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
}

对这些参数的说明:



  • context:协程上下文,可以指定协程限制在一个特定的线程执行。常用的有Dispatchers.Default、Dispatchers.Main、Dispatchers.IO等。Dispatchers.Main即Android 中的主线程;Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求

  • start: 协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT指协程立即执行,另外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED

  • block:协程主体,即要在协程内部运行的代码,也就是上述例子花括号中的代码

  • 返回值Job:对当前创建的协程的引用。可以通过调用它的的join、cancel等方法来控制协程的启动和取消。


3.挂起函数


上面有提到”挂起“即suspend的概念,


回到上面的例子,有一个delay函数,进到这个函数看看它的定义:

public suspend fun delay(timeMillis: Long) {...}

发现多了个suspend关键字,也就是上文中提到的“挂起”,根据程序的输出结果看,首先输出了1,3,等待一秒后再输出了2,而且打印的线程显示的也是主线程,这说明,协程在遇到suspend关键字的时候,会被挂起,所谓的挂起,就是程序切了个线程,并且当这个挂起函数执行完毕后又会自动切回来,这个切回来的动作其实就是恢复,因此挂起、恢复也是协程的一个特点。所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的代码重新进入协程所在线程的过程。协程就是通过这个挂起恢复机制进行线程的切换。


关于suspend函数也有个规定:挂起函数必须在协程或者其他挂起函数中被调用,换句话说就是挂起函数必须直接或者间接地在协程中执行。


4.创建协程的其他方式


上面介绍了通过launch方法创建协程,当遇到 suspend 函数的时候 ,该协程会自动逃离当前所在的线程执行任务,此时原来协程所在的线程就继续干自己的事,等到协程的suspend 函数执行完成后又自动切回来原来线程继续往下走。 但如果协程所在的线程已经运行结束了,协程还没执行完成就不会继续执行了 。为了避免这样的情况就需要结合 runBlocking 来暂时阻塞当前线程,保证代码的执行顺序。


下面我们通过runBlocking 来创建协程

btn.setOnClickListener {
Log.i("TAG", "1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
runBlocking {
delay(1000) //延迟1000ms
Log.i("TAG", "2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
}
Log.i("TAG", "3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]
3.BtnClick.... [当前线程为:main]

可以看到运行结果顺序和上面的launch方式不同,这里的log先输出1、2,再输出3,程序会等待runBlocking中的代码块执行完后才会还执行后面的代码,因此launch是非阻塞的,而runBlocking是阻塞式的。


launch和runBlocking都是没有返回结果的,有时我们想知道协程的返回结果,拿到结果去做业务例如UI更新,这时withContext和async就派上用场了。


先看下withContext的使用场景:

 btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val startTime = System.currentTimeMillis()
val task1 = withContext(Dispatchers.IO) {
delay(2000)
Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
1 //返回结果赋值给task1
}

val task2 = withContext(Dispatchers.IO) {
delay(1000)
Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
2 //返回结果赋值给task2
}
Log.i(
"TAG",
"3.计算task1+task2 = ${task1+task2} , 耗时 ${System.currentTimeMillis() - startTime} ms [当前线程为:${Thread.currentThread().name}]"
)
}
}

输出结果为:

 1.执行task1.... [当前线程为:DefaultDispatcher-worker-3]
2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
3.计算 task1+task2 = 3 , 耗时 3032 ms [当前线程为:main]

从输出结果可以看出,通过withContext指定协程运行在一个io线程,延迟了两秒后返回结果1赋值给task1,之后程序向下执行,同样的,延迟了1s后返回结果2赋值给了task2,最后执行到步骤三,并且打印了耗时时间,可以看到,耗时是两个task的时间总和,也就是先执行完task1,在执行task到,说明withContext是串行执行的,这适用于在一个请求结果依赖另一个请求结果的场景。


如果同时处理多个耗时任务,且这几个任务都没有相互依赖时,可以使用 async ... await() 来处理,将上面的例子改为 async 来实现如下

btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val startTime = System.currentTimeMillis()
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
1 //返回结果赋值给task1
}

val task2 = async(Dispatchers.IO) {
delay(1000)
Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
2 //返回结果赋值给task2
}

Log.i(
"TAG",
"3.计算 task1+task2 = ${task1.await()+task2.await()} , 耗时 ${System.currentTimeMillis() - startTime} ms [当前线程为:${Thread.currentThread().name}]"
)
}
}

输出结果:

2.执行task2.... [当前线程为:DefaultDispatcher-worker-4]
1.执行task1.... [当前线程为:DefaultDispatcher-worker-5]
3.计算 task1+task2 = 3 , 耗时 2010 ms [当前线程为:main]

可以看到,输出的总耗时明显比withContext更短,且task2优先task1执行完,说明async 是并行执行的。


总结


本文首先通过对进程、线程、协程的区别认清协程的概念,接着对协程的特点也就是优势进行了介绍,最后通过几个实例介绍了协程的几种启动方式,并分析了其各自特点和使用场景,本文更多是对协程的概念和使用进行了简单的介绍,而协程的内容远不止这些。


结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


作者:37手游移动客户端团队
链接:https://juejin.cn/post/7245096955966177338
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »