注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

vue3项目打包时We're sorry but XXX doesn't work properly without JavaScript

vue
题引: 这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。 正文: 当界面做完之后且打包完成,就打开了 dist 文件夹里的 inde...
继续阅读 »

题引:


这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。


正文:


当界面做完之后且打包完成,就打开了 dist 文件夹里的 index.html 。突然发现页面是空白的,打开调试器之后突然发现了一个报错:

<strong>We’re sorry but XXX doesn’t work properly without JavaScript enabled</strong>



看了一下vue-router、pinia没有什么问题,调用顺序也没错。于是就往打包的文件夹查看,才发现了引用的路径是以 / 绝对路径开头的,以至于资源无法加载而导致页面空白。


发现了这个问题,直接定位到 vue.config.js 文件,如果是vite创建的话应该是 vite.config.js

//vue.config.js
export default = {
publicPath: './', //打包文件的路径
... // 其他配置
}

//vite.config.js
import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
publicPath: './', //打包文件的路径
...
})

当然,上网查了一下前端的路由也会导致这个问题的出现。可以把mode值从 history 改成 hash

import {createRouter,createWebHashHistory} from 'vue-router';

const routes = [];
const router = createRouter({
router,
history:createWebHashHistory()
})

结尾:


以上就是处理打包上线时遇到 项目在没有启用JavaScript的情况下无法正常工作 的情况。


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

安卓与串口通信-数据分包的处理

前言 本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。 其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使...
继续阅读 »

前言


本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。


其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使用时一直没有遇到过,或者准确的说,虽然遇到过,但是并没有特意的去处理:


分包?不就是传过来的数据不完整嘛,那我把这个数据丢了,等一个完整的数据不就得了。


亦或者,之前使用的都是极少量的数据,一次读取的数据只有 1 byte ,所以很少出现数据包不完整的情况。


何为分包?


严格意义上来说,其实并不存在分包的概念。


因为由于串口通信的特性,它并不知道不知道也无法知道所谓的 “包” 是什么,它只知道你给了数据给它,他就尽可能的把数据发出去。


因为串口通信时使用的是流式传输,也就是说,所有数据都是以流的形式进行发送、读取,也不存在所谓的“包”的概念。


所谓的“包”只是我们在应用层人为的规定了多少长度的数据或者满足什么样格式的数据为一个“包”。


而为了最大程度的减少通信时的请求次数,在处理数据流时,通常会尽可能多的读取数据,然后缓存起来(即所谓的缓冲数据),直至达到设置的某个大小或超过某个时间没有读取到新的数据。


例如,我们人为的规定了一个数据包为 10 字节,PLC 或 其他串口设备发送时将这 10 个字节的数据连续的发送出来。但是安卓设备或其他主机在接收时,由于上面所说的原因,可能会先读到 4 字节的数据,再读到 6 字节的数据。也就是说,我们需要的完整数据不会在一次读取中读到,而是被拆分成了不同的“数据包”,此即所谓的 “分包”:


1.gif


怎么处理分包?


其实谜底就在谜面上,通过上面对分包出现的原因进行简单的解释之后,相信大伙对于怎么解决分包问题已经有了自己的答案。


解决分包的核心原理说起来非常简单,无非就是把我们需要的完整的数据包从多次读取到的数据中取出来,再拼成我们需要的完整数据包即可。


问题在于,我们应该怎么才能知晓读取到数据属于哪个数据包呢?我们又该怎么知道数据包是否已经完整了呢?


这就取决于我们在使用串口通信时定义的协议了。


一般来说,为了解决分包问题,我们常用的定义协议的方法有以下几种:



  1. 规定所有数据为固定长度。

  2. 为一个完整的数据规定一个终止字符,读到这个字符表示本次数据包已完整。

  3. 在每个数据包之前增加一个字符,用于表示后续发送的数据包长度。


固定数据包长度


固定数据长度指我们规定每次通信时发送的数据包长度都是固定的长度,如果实际长度不足规定的长度则使用某些特殊字符如 \0 填充剩余的长度。


对于这种情况,非常好处理,只要我们每次读取数据时都判断读取到的数据长度,如果数据长度没有达到符合的固定长度,则认为读取数据不完整,就接着读取,直至数据长度符合:

val resultByte = mutableListOf<Byte>()
private fun getFullData(count: Int = 0, dataSize: Int = 20): ByteArray {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}

// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
// 超时
return ByteArray(0)
}
}
}

但是这种方式也有一个明显的缺点,那就是使用场景局限性特别强,只适合于主机发送请求,从机器回应的这种场景,因为如果是在从机不停的发送数据,而主机可能在某个时间段读取,也可能一直轮询读取的情况下,光靠数据长度判断是不可靠的,因为我们无法确保我们读到的指定长度的数据一定就是同一个完整数据,有可能参杂了上一次的数据或者下一次的数据,而一旦读取错一次,就意味着以后每次读取的数据都是错的。


增加结束符


为了解决上述方式导致的局限性,我们可以给每一帧数据增加一个结束符号,通常来说我们会规定 \r\n 即 CRLF (0x0D 0x0A)为结束符号。


所以,我们在读取数据时会循环读取,直至读取到结束符号,则我们认为本次读取结束,已经获得了一个完整的数据包:

val resultByte = mutableListOf<Byte>()
private fun getFullData(): ByteArray {
var isFindEnd = false

while (!isFindEnd) {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
if (readLen != 0) {
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) {
isFindEnd = true
}
}
}

return resultByte.toByteArray()
}

但是这个方法显然也有一个缺陷,那就是如果是单次间隔读取或者轮询时第一次读取数据有可能也是不完整的数据。


因为我们虽然读取到了结束符号,但是并不意味着这次读取的就是完整的数据,或许前面还有数据我们并没有读到。


不过这种方式可以确保轮询时只有第一次读取数据有可能不完整,但是后续的数据都是完整的。


只是单次间隔读取的话就无法保证读取到的是完整数据了。


在开头增加数据包长度


和增加结束符类似,我们也可以在数据包开头增加一个特殊字符,然后在后面紧跟着一个指定长度(1byte)字符指定接下来的数据包长度有多长。


这样,我们就可以在解析时首先查找这个开始符号,查找到之后则认为一个新的数据包开始了,然后读取之后 1byte 的字符,获取到这个数据包的长度,接下里按照这个这个指定长度,循环读取直到长度符合即可。


具体读取方式其实就是上面两种方式的结合,所以这里我就不贴代码了。


最好的情况


最方便的解决数据分包的方法当然是在数据中既包括固定数据头、固定数据尾、甚至连数据长度都是固定的。


例如某款温度传感器,发送的是数据格包为固定 10 位长度,且有结束符 CRLF,并且数据包开头有且只有 -+ (0x2B 0x2D 0x20)三种情况,那么我们在接收数据时就可以这么写:

val resultByte = mutableListOf<Byte>()
val READ_WAIT_MILLIS = 2000
private fun getFullData(count: Int = 0, dataSize: Int = 14): ByteArray {
var isFindStar = false
var isFindEnd = false
while (!isFindStar) { // 查找帧头
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
if (buffer.first() == 0x2B.toByte() || buffer.first() == 0x2D.toByte() || buffer.first() == 0x20.toByte()) {
isFindStar = true
for (i in 0 until readLen) { // 有帧头,把这次结果存入
resultByte.add(buffer[i])
}
}
}
}

while (!isFindEnd) { // 查找帧尾
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
for (i in 0 until readLen) { // 先把结果存入
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) { // 是帧尾, 结束查找
isFindEnd = true
}
}
}


// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
return ByteArray(0)
}
}

粘包呢?


上面我们只说了分包情况,但是在实际使用过程中,还有可能会出现粘包的现象。


粘包,顾名思义就是不同的数据包在一次读取中混合到了一块。


如果想要解决粘包的问题也很简单,类似于解决分包,也是需要我们在定义协议时给出能够区分不同数据包的方式,这样我们按照协议解析即可。


总结


其实串口通信中的分包或者粘包解决起来并不难,问题主要在于串口通信一般都是每个硬件设备厂商或者传感器厂商自己定义一套通信协议,而有的厂商定义的协议比较“不考虑”实际,没有给出任何能够区分不同数据包的标志,这就会导致我们在接入这些设备时无法正常的解析出数据包。


但是也并不是说就没有办法去解析,而是需要我们具体情况具体分析,比如温度传感器,虽然通信协议中没有给出数据头、数据尾、数据长度等信息,但是其实它返回的数据格式几乎都是固定的,我们只要按照这个固定格式去解析即可。


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

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码

fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}
//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。

checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


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

揭开Android视图绘制的神秘面纱

揭开Android视图绘制的神秘面纱 在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android...
继续阅读 »

揭开Android视图绘制的神秘面纱


在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android的UI开发。


什么是View?


View是Android系统中的一个基本组件,它是用户界面上的一个矩形区域,可以用来展示文本、图片、按钮等等。View可以响应用户的交互事件,比如点击、滑动等等。在Android中,所有的UI组件都是继承自View类。


View的绘制过程


View的绘制过程可以分为三个阶段:测量、布局和绘制。下面我们将逐一介绍这三个阶段。


测量阶段(Measure)


测量阶段是View绘制过程的第一个重要阶段。在测量阶段,系统会调用View的onMeasure方法,测量View的宽度和高度。在这个过程中,系统会根据View的LayoutParams和父容器的大小来计算出View的大小。


例:下面代码是一个自定义View的onMeasure方法例程。在测量过程中,我们设定了View的大小。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取宽度的Size和Mode
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 如果Mode是精确的,直接返回
if (widthMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightMeasureSpec);
return;
}

// 计算View的宽度
int desiredWidth = getPaddingLeft() + getPaddingRight() + defaultWidth;
int measuredWidth;
if (desiredWidth < widthSize) {
measuredWidth = desiredWidth;
} else {
measuredWidth = widthSize;
}

// 设置宽度和高度的Size和Mode
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeight = defaultHeight;
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(measuredWidth, measuredHeight);
}

在测量阶段结束后,系统会将计算好的宽度和高度传递给布局阶段。


布局阶段(Layout)


布局阶段是View绘制过程的第二个重要阶段。在布局阶段,系统会调用View的onLayout方法,将View放置在父容器中的正确位置。在这个过程中,系统会根据View的LayoutParams和父容器的位置来确定View的位置。


例:下面代码是一个自定义ViewGroup的onLayout方法例程。在布局过程中,我们遍历子View,并根据LayoutParams确定子View的位置和大小。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getMeasuredWidth() - getPaddingRight();
int bottom = getMeasuredHeight() - getPaddingBottom();

for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = left + lp.leftMargin;
int childTop = top + lp.topMargin;
int childRight = right - lp.rightMargin;
int childBottom = bottom - lp.bottomMargin;
child.layout(childLeft, childTop, childRight, childBottom);
}
}

绘制阶段(Draw)


绘制阶段是View绘制过程的最后一个重要阶段。在绘制阶段,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


例:下面代码是一个自定义View的onDraw方法例程。在绘制过程中,我们使用Paint对象绘制了一段文本。

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//绘制文本
String text = "Hello World";
Paint paint = new Paint();
paint.setTextSize(50);
paint.setColor(Color.RED);
paint.setAntiAlias(true);
canvas.drawText(text, 0, getHeight() / 2, paint);
}

除了绘制内容,我们还可以在绘制阶段绘制View的背景和前景。系统会调用drawBackgrounddrawForeground方法来绘制背景和前景。值得注意的是,View的绘制顺序是:先绘制背景,再绘制内容,最后绘制前景。


View的绘制流程


View的绘制流程可以看作是一个递归调用的过程,下面我们将具体介绍这个过程。


Step 1:创建View


在View绘制过程的开始阶段,我们需要创建一个View对象,并将它添加到父容器中。在这个过程中,系统会调用View的构造函数,并将View的LayoutParams传递给它。


Step 2:测量View


接下来,系统会调用View的measure方法,测量View的宽度和高度。在这个过程中,View会根据自身的LayoutParams和父容器的大小来计算出自己的宽度和高度。


Step 3:布局View


在测量完成后,系统会调用View的layout方法,将View放置在父容器中的正确位置。在这个过程中,View会根据自身的LayoutParams和父容器的位置来确定自己的位置。


Step 4:绘制背景


在布局完成后,系统会调用View的drawBackground方法,绘制View的背景。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 5:绘制内容


接下来,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 6:绘制前景


在绘制内容完成后,系统会调用View的drawForeground方法,绘制View的前景。在这个过程中,我们同样可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 7:绘制子View


接着,系统会递归调用ViewGroup的dispatchDraw方法,绘制所有子View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 8:完成绘制


最后,所有的View绘制完成,整个View树也就绘制完成。


例:下面代码是一个自定义ViewGroup的绘制流程例程。在绘制过程中,我们先画背景,再绘制每个子View的内容。

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {
super(context);
}

public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量子View的宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);

// 获取ViewGroup的宽高大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

// 设置ViewGroup的宽高
setMeasuredDimension(widthSize, heightSize);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 遍历所有子View,设置它们的位置和大小
int childCount = getChildCount();
int left, top, right, bottom;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
left = childView.getLeft();
top = childView.getTop();
right = childView.getRight();
bottom = childView.getBottom();
childView.layout(left, top, right, bottom);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画背景
canvas.drawColor(Color.WHITE);
}

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// 绘制每个子View的内容
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.draw(canvas);
}
}
}

在ViewGroup的绘制流程中,系统会先调用ViewGroup的draw方法,然后依次调用dispatchDraw方法和绘制每个子View的draw方法。ViewGroup的绘制顺序是先绘制自己的背景,再绘制每个子View的内容和背景,最后绘制自己的前景。


总结


本文详细介绍了Android View的绘制过程,包括测量阶段、布局阶段和绘制阶段。同时,我们还在代码实现的角度,详细说明了Android ViewGroup的绘制流程,帮助你更好地理解和掌握Android的UI开发。


推荐


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

那些拿20k💰的大佬在职场都是怎么沟通的❓

☀️ 前言 大家好我是小卢,职场沟通是每个职场人必备的技能,但是如何提高职场沟通能力却是需要不断学习和实践。 下面就给大家带来四点方法能很大程度提升你在职场的沟通能力。 ⌨️ 了解每个人的沟通方式 每个人的个性、经验和教育背景都不同,这些因素都会影响到个...
继续阅读 »

☀️ 前言



  • 大家好我是小卢,职场沟通是每个职场人必备的技能,但是如何提高职场沟通能力却是需要不断学习和实践。

  • 下面就给大家带来四点方法能很大程度提升你在职场的沟通能力。


⌨️ 了解每个人的沟通方式



  • 每个人的个性、经验和教育背景都不同,这些因素都会影响到个人的沟通方式。有些人可能会喜欢直接表达自己的想法和意见,而有些人可能更倾向于暗示询问

  • 在实际职场生活中,我们不仅需要了解自己的沟通方式,还需要了解对方的沟通方式。简单举个例子吧:

  • 假设你是一个刚入公司不久的产品经理,你每周或者每月都需要给上司做一个工作汇报。

    • 你知道你的上司更喜欢使用图表和数据进行沟通,你可以准备好相关的数据和图表,并在开会过程中使用它们。这可以帮助你的上司更好地理解你的意思,并更快地进入到讨论的核心问题。

    • 你知道你的上司比较喜欢使用直接表达的方式来沟通,在这种情况下,直接表达你的想法和意见可能更为有效。你可以以明确的方式表达你对项目的看法,并解释你的看法背后的原因。



  • 在与他人交流时,我们需要时刻注意自己的语言、态度和非语言信号,并根据对方的反应进行调整。这需要一定的敏感度和经验,但是通过不断地练习和反思,我们可以逐渐提高自己的职场沟通能力,并取得更好的效果。


🤗 注意语速和语调



  • 职场沟通中,语速和语调是非常重要的因素,它们往往可以决定对方对你的印象和理解,如果你的语速太快或者语调不合适,很容易让对方感到困惑或者不舒服。

  • 除了注意自己的语速和语调,我们还需要注意对方的语速和语调。如果在某次交谈中你发现对方特别激动,说话特别快导致你不能全部理解,你可以说:“你说的内容非常重要,我来总结一下刚刚你分享的信息,看看是否符合预期,以便我能更好地理解你的意思?”

  • 这样的话语不仅能够有效地表达自己的需求,也能够尊重对方的沟通方式,让双方都能够更好地理解彼此。


👂 学会倾听



  • 职场沟通不仅仅是说话,更重要的是倾听。倾听意味着不仅是听别人说话,还包括尊重对方的意见和观点,关注对方的情感和态度,以及在适当的时候提出问题和反馈,以达到更好的沟通效果。

  • 要成为一个好的倾听者,我们需要全神贯注地聆听对方说话。这意味着不要分心,不要中途打断对方,而是要给对方充足的时间和空间来表达自己的想法和意见。如果你有不同的看法或者意见,可以先把它们记在脑海里,等对方表达完后再进行回应。

  • 在倾听的过程中,我们还需要注意对方的情绪和表情。通过观察对方的肢体语言和面部表情,我们可以更好地了解对方的真实意图和情感状态,从而更好地回应和理解对方的想法和需求,建立更好的信任和合作关系。

  • 举个例子吧:假设你是一个团队的领导,正在讨论下一步的项目计划。你发现其中一个成员很少发表意见,似乎对讨论不太感兴趣。你可以采取主动倾听的方式,问他对当前的计划有何看法,或者给他更具体的问题,以激发他的参与度。这样可以让他感受到自己的意见被认真听取,也有助于整个团队更好地理解和解决问题


👺 简明扼要



  • 简明扼要是职场中非常重要的一个点。当你需要向同事或客户提出需求时,最好提前思考好问题的前提条件、现状和问题的分支情况,一次性把问题说明白,尽量减少来回问答的次数,这样可以更有效地利用大家的时间和精力。

  • 为了让自己的观点更清晰地传达给别人,你可以先说出结论和重点,然后再说明为什么这么认为,并提供相关的事实依据。在接受问题或错误的指责时,也应该直接说明问题并找到解决办法,而不是遮掩或解释,以保证工作的顺利完成。

  • 我有一个同事在公司寻求大佬帮助的时候把前置说了很久,导致一直进入不到重点,别人根本不知道你想表达什么,这不仅浪费了别人的时间,还会让人对你产生厌恶。

  • 你可以简单干脆一点:这个问题导致了 xxx,影响了 xxx 的用户,他的原因是 xxx,我的想法是 xxx ,所以想问一下有没有更好的方案?


wallhaven-8o6rmo.jpeg


👋 写在最后



  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

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

怎么算是在工作中负责?

作为打工人,受人之禄,忠人之事。但就像呼兰说的,躺有躺的价格,卷有卷的价格。身为程序员,我们在平时工作中要做到怎样才能算是“负责”了呢? 我们可以把工作边界和范围分为三部分: 个人基本能力 工作内容 工作时间 对自己的基本能力负责 基本能力包括两部分:1)...
继续阅读 »

作为打工人,受人之禄,忠人之事。但就像呼兰说的,躺有躺的价格,卷有卷的价格。身为程序员,我们在平时工作中要做到怎样才能算是“负责”了呢?


我们可以把工作边界和范围分为三部分:



  • 个人基本能力

  • 工作内容

  • 工作时间


对自己的基本能力负责


基本能力包括两部分:1)技术能力,2)熟悉公司系统的能力。


程序员是一个非常需要持续学习的职业,我们在实际工作中,遇到自己不会的问题在所难免,这时可以向别人请教,但是千万不要觉得请教完就没事儿了,我们需要思考复盘自己为什么不会,要想办法补齐自己的知识和技能短板。


我们学的东西一定要在实际工作中使用,这样才能够激发学习的积极性,同时验证自己的学习成果。当公司准备技术升级或者技术转型时,这也是我们为自己的技能升级的好机会。


很多公司都会有自己的内部系统,熟练掌握和使用这些系统,也是我们需要做到的,它的价值在于,内部系统一般都是和公司的整个监控系统集成好的,例如公司内部的SOA框架或者微服务框架,都是和公司内部的监控系统有集成的,即使这个框架再“不好”,公司内部的项目还是会使用,我们需要让自己站得高一些,去看待内部系统在整个公司级别的作用和地位,这样才能更好地发挥自己的技术能力。


对安排的工作负责


程序员职业的特殊性在于,工作本身的具体内容和难度,会随着被安排的工作内容的改变而改变。从对工作负责的角度来说,我们大部分人会付出比当初预想的更多的时间,才能让自己按时完成工作。


如果一件事情的复杂度远远超过之前的预估,在规定的时间内,自己确实无法完成,这时正确的态度不是硬着头皮上,而是将情况理清楚,早点找经理或者负责人,让他们知道事情的进度和之前没有预想到的难度,把事情重新安排一下。


从管理者的角度来看,一件事情安排的不合理,就应该早发现,早计划,重新安排资源。


对工作时间负责


对工作时间负责,是说最好在“实际上班”时间之前到,避免有人找你却找不到的情况。


这不只是为了保证工作时间,而是想强调程序员的工作不止是写代码,还有很多沟通交流的事情,要保证基本的工作时间,才能更有效的和团队交流,确保我们的工作的价值。


对于项目和团队安排的各种会议,要准时参加,如果不能参加,需要提前告知经理或者会议组织者,避免浪费大家的事情。


总之,我们工作中的责任是一点点增加的,负责任的态度和习惯,也是从平时工作中一件件事情中养成的。形成这样的习惯,成为一个受人信任的人,是我们在职场中要培养的重要品质。



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

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


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

聊聊我在阿里第一年375&晋升的心得

前言 思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。 绩效 首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。 很多同学都知道阿里的绩效是361制度,也就是30%...
继续阅读 »

前言


思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。


绩效


首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。


很多同学都知道阿里的绩效是361制度,也就是30%的人拿A,60%的人拿B,10%的人拿C,不过在阿里,我们一般不用ABC来表示,除去价值观考核,我们常用的表达是3.75、3.5、3.25,初略的对应关系如下:

361通用阿里
30%A3.75
60%B3.5 ±
10%C3.25

那么,了解了阿里的绩效制度,再来看看绩效里面的门道。


首先,讲一个职场潜规则,「团队的新人是用来背绩效的」,也就是会把差绩效(325)指标分配给新人,因为如果把325给老人,容易产生团队不稳定的因素,而且不得不承认的是,少一个老人比少一个新人的成本更大,另一方面,你作为新人,业务不熟,根基不稳,也不见得能产出多大的价值,所以对于多数新人来说,也只能接受。据我所知,只有极少的公司里面会有「绩效保护」这种政策,而且一般还是针对的应届生,社招来说,还是要看能力的。


其次,基于潜规则,大部分新人都在为保住3.5做努力,只有少数优秀的人可以拿到更好的绩效。而新人拿375在阿里是很少的存在,即使是老人,拿375都是非常不容易的,何况是一个新人。


最后,就是晋升,晋升的前提条件就是满一年且是好绩效,加上现在降本增效的大环境,有的要求连续两年375才行,甚至都不一定有名额,当然,晋升也和团队绩效和级别有关系,但总之,男上加男,凤毛麟角。


个人背景


我是21年8月份入职阿里的,2022年是在阿里的第一个整财年。


之前一直是在小厂摸爬滚打,没有大厂经历,这对于我来说,是劣势,写代码不注重安全性防护,没有code review,没有ab test,没有自动化测试,没有文档沉淀,没有技术分享,纯纯小作坊。更重要的是,小厂和大厂的做事风格流程什么的,真的是千差万别,所以当时的landing对我来说,还是很难受的。但是,我有没有自带的优势呢,也有,写作能力,但是光有写作能力还是不够的,你没东西可写也不行啊。


其实试用期结束之后就差不多进入新的财年了,对于刚刚进入状态的我,也迎来了更大的挑战。过去的一整年有较多的精力都投入在研发效能和安全生产方面,这对于以前纯业务开发的我来说,完全是一个新的领域,好在不负重托,也略有成果。


其实回想过去一年的经历来看,今天的成绩是多维度的结合,比如硬实力和软实力、个人和团队、内部和外部等多个维度,下面将分别介绍一些我个人的心得经验,仅供参考。


沟通能力


这也用说?不就是说话吗,谁不会说?


我看过很多的简历,如果有「自我评价」,几乎都会提到「具备良好的沟通能力」。
可是沟通能力看起来真的有那么简单吗?我认为不是的,实际上我甚至认为它有点难。


在职场经常会有这些问题:



  1. 这个点我没太理解,你能在解释一下吗?

  2. 为什么要这么做?为什么不这么做?

  3. 现在有什么问题?

  4. 你的诉求是什么?

  5. 讲话的时候经常被打断等等...


这些问题你是不是被问到过,或者这么问别人呢。


而这些问题的背后,则反映了沟通的不完整和不够清晰。面对他人的挑战,或向跨部门的同学讲述目标价值时,会沟通的同学会显的游刃有余,而不会沟通的同学则显得捉襟见肘。


沟通方面,其实也包含很多场景。


首先是逻辑要清晰。


面对用户的一线同事,比如销售和客服,他们都是有话术的,话术就是沟通的技巧。


为什么脸皮薄/不懂拒绝的人容易被销售忽悠?


因为销售在跟客户沟通的时候,就是有一套完整的话术,他先讲一,看你反应再决定讲二还是三;当你拒绝了A方案,他还有B方案C方案。一套完整的话术逻辑把你都囊括在里面,最后只能跟着他的思维走,这就是话术的意义。


同样的,在职场,你跟人沟通的时候,不能直说怎么做,解决方案是什么,而背景和问题同样重要,同时讲述问题的时候要尽可能的拆解清楚,避免遗漏,这样不只是让别人更理解你的意思,其实有时候换个视角,解决方案可能有更优的。


逻辑清晰,方案完善,对方就会处于一个比较舒服的状态,有时候能起到事半功倍的效果。你可能会觉得有些麻烦,但如果因为没有表达清楚而导致最后的结果不符合预期,孰轻孰重,应该拎得清的吧?


其次是要分人。


我在之前的面经中提到,自我介绍要分人,技术面试官和HR的关注点一定是不一样的,面对这种不同的出发点,你讲的东西肯定不能是同一套,要有侧重点,你跟HR讲你用了什么牛逼的技术、原理是什么,那不是瞎扯嘛。


这个逻辑在职场也是一样的,你和同事讨论技术、向领导汇报、回答领导的领导问题、跟产品、运营、跨部门沟通,甚至出现故障的时候给客满提供的话术,面对不同的角色、不同的场合,表达出来的意思一定是要经过「翻译」的,多换位思考。


即要把自己的意思传达到,也要让对方get到,这才是有效沟通。


所谓沟通能力,不只是有表达,还要有倾听。


倾听方面,比如当别人给你讲一个事情的时候,你能不能快速理解,能不能get到对方的点,这也很重要。
快速且高效,这是一个双向的过程。这里面会涉及到理解能力,而能理解的基础是基于现有的认知,也就是过往的工作经验、项目经历和知识面,这是一个积累的过程。


当然,也有可能是对方表达的不够清楚,这时候就要不耻下问,把事情搞清楚,搞不清楚就会有不确定性,就是有风险,如果最终的结果不符合预期,那么复盘的时候,委屈你一下,不过分吧😅。


最后是沟通媒介。


我们工作沟通的场景一般都是基于钉钉、微信、飞书之类的即时通讯办公平台,文字表达的好处是,它可以留档记录,方便后期翻阅查看,但是也一定程度在沟通表达的传递上,存在不高效的情况。


那这时候,语音、电话就上场了,如果你发现文字沟通比较费劲的时候,那一定不如直接讲来的更高效。


但是语音、电话就一定能聊的清楚吗,也不见得。


“聊不清楚就当面聊”,为什么当面聊就能比语音、电话聊的清楚?因为当面聊,不只是可以听到语气看到表情肢体动作,更重要的是当面聊的时候,双方一定是专注在这个事情上的,不像语音的时候还可以干其他的事,文字甚至会长时间已读不回,所以讲不清楚的时候,当面聊的效果是最好的。为了弥补留档的缺陷,完事再来个文字版的纪要同步出来,就ok了。


其他。


上面提到逻辑要清晰,要分人,还有倾听能力,和善用沟通媒介。


其实沟通里还包括反应能力,当你被突然问到一个问题的时候,能不能快速流畅的表达清楚,这个也很关键,如果你支支吾吾,反反复复的都没有说清楚,设想一下,其他人会怎么看你,“这人是不是不行啊?”,长此以往,这个信任度就会降低,而一旦打上标签,你就需要很多努力才能证明回来。


还包括争辩能力,比如在故障复盘的时候,能不能有效脱身不被拉下水,能不能大事化小小事化了,也都是沟通的技巧,限于篇幅,不展开了。


学会复盘


复盘是什么?


复盘是棋类术语,指对局完毕后,复演该盘棋的记录,以检查对局中招法的优劣与得失关键。在工作中,复盘是通过外在的表象、客观的结果找出本质,形成成功经验或失败教训,并应用到其他类似事件中,提升面向未来的能力。


所以,复盘不是流水账的记录经过和简单总结,也不是为了表彰罗列成绩,更不是为了甩锅而相互扯皮。找出本质的同时一定要有思考,这个思考可以体现在后续的一些执行事项和未来规划上,总之,就是要让「复盘」变的有意义。


什么是有意义的复盘?


就是你通过这次复盘,能知道哪些错误是不能再犯的,哪些正确的事是可以坚持去做的。会有一种「再来一次结果肯定不一样」的想法,通过有意义的复盘让「不完美」趋向「完美」。


我个人复盘的三个阶段:



  • 回顾:回顾目标、经过、结果的整个流程;

  • 分析:通过主观、客观的视角分析,失败的原因是什么,成功的原因是什么;

  • 转化:把成功经验和失败教训形成方法论,应用到类似事件中;


Owner意识


什么是owner意识?


简单讲就是主人翁精神。认真负责是工作的底线,积极主动是「owner意识」更高一级的要求。


如果你就是怀着只干活的心态,或者躺平的心态,换我是领导,也不认为你能把活做好。因为这种心态就是「做了就行,能用就行」,但有owner意识不一样,这种人做事的时候就会多思考一点、多做一点,这里面最主要的就是主动性,举个栗子,好比写代码,当你想让程序运行的更好的时候,你就会多关注代码编写的鲁棒性,而不是随便写写完成功能就行。


但人性自私,责任感也不是人人都有,更别提主动性了,所以这两种心态的人其实差别很大,有owner意识的人除了本职工作能做好之外,在涉及到跨团队的情况,也能主动打破边界去push,有责任心有担当,往往能受到团队内外的一致好评。


在其位谋其职,我其实并没有特意去固化自己的owner意识,就是怀着一个要把事情做好的心态,跟我个人性格也有关系,现在回想起来,不过是水到渠成而已。



卷不一定就有owner意识,不卷也不代表没有。



向上管理


这个其实我一开始做的并不好,甚至可以说是很差,小公司出身哪需要什么向上管理,活干好就行。


但是现在不一样了,刚入职比较大的一个感受就是,我老板(领导)其实并不太过问我的工作内容,只是偶尔问一下进度。


然而这个「问」,其实也能反应出一些过往不太在意的问题:



  1. 没有及时汇报,等到老板来问的时候其实处于一个被动的局面了,虽然也不会有什么太大的影响,可能多数人也都是这样,但是这不正说明我不够出众吗?

  2. 不确定性,什么进度?有没有遇到问题?这些都是不确定性,老板不喜欢“惊喜”,有困难要说,有风险要提前暴,该有结果的时候没有,老板也很被动,你会留下一个什么印象?


当然,向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道。不要只提问题找老板要解决方案,我会简述问题,评估影响面,还会给出解决方案的Plan A和Plan B,这样老板就是做选择题了,即使方案不够完美,老板指点一下不正是学习的好机会吗。


学会写TODO


为什么写todo?


写todo的习惯其实是在上家公司养成的,因为要写周报,如果不记录每天做了什么,写周报的时候就会时不时的出现遗漏的情况。除了记录当天所做的事情之外,我还列了第二天要做的事情。虽然一直有给自己定规划的习惯,但是颗粒度都没有这么细过。彼时的todo习惯,不仅知道当天做了什么,还规划了第二天做什么,时刻有目标,也不觉得迷茫。


进入阿里之后,虽然没有周报月报这种东西,但是这个习惯也一直保持着,在此之上,我还做了一些改良。



  1. 优先级,公司体量一旦大起来之后,业务线就很多,随之而来的事情就很多,我个人对处理工作是非常反感多线程并发的,特别是需要思考的工作,虽然能并行完成,但完成度不如专注在一件事情上的好,但是有些事情又确实得优先处理,所以就只能根据事情的紧急程度排一下优先级,基本很少有一件事需要从早到晚都投入在里面的,所以抽出身来就可以处理下一件事,所以也不会出现耽误其他事情的情况,当然线上故障除外。

  2. 消失的时间,因为真的是事情又多又杂,时常在下班的时候会有一种瞎忙的感觉,就是「忙了一天,好像什么都没干」,但又确实很忙很累,仿佛陷入一个怪圈。所以后来我就把颗粒度拆的更细,精确到小时,也不是几点钟该做什么,就是把做的事情记录下来,并备注一下投入的时间,比如上午排障答疑投入了两小时,这样到中午吃饭的时候也不至于上午就这样虚度了。让“消失的时间”有迹可循,治愈精神内耗。

  3. 归纳总结,我现在是在语雀以月度为单位记录每天的todo,这样就可以进行月度的总结和复盘,在半年度的OKR总结的时候,还有了归纳总结的来源,有经过、有结果、还有时间线,一目了然,再也不用为写总结发愁了。


总之,写todo的好处除了记录做了什么、要做什么,它还能辅助你把工作安排的更好。


有规划有目标,也不会陷入一个迷茫虚度的状态,属于一个成本非常低而收益却很高的好习惯,不止工作,学习也是如此,生活亦然。


其他方面


适应能力


于我个人来说,工作强度比以前要大很多,慢慢习惯了就行,在大厂里面阿里还不是最卷的,但钱也不是最多的;工作流程方面只是有些不清楚而已,并没有什么门槛,熟悉了就行;还有阿里味儿,确实要学很多新词儿、缩写、甚至潜台词,这没啥说的,还没见到有能独善其身的😅。


适应能力也不是随便说说,有太多的人入职新公司干的怀疑人生、浑身难受而跑路的,抛开公司的问题不说,难道就没有自己的问题吗?🐶


我把适应分为两个阶段,第一个阶段就是适应工作环境,熟悉公司、同事、产品、项目;第二个阶段就是适应之后,要想想如何迎接没有新手光环的挑战,如何去展示自己、提升自己等。


技术能力


夯实自己的技术能力尤为重要,毕竟是吃饭的家伙,是做事拿结果的重要工具和手段。


在大家技术能力的基本面都不会太差的情况下,如何在技术上建立团队影响力,就是需要思考的问题。


要找准自己在团队中的定位,并在这一领域深耕,做到一提这个技术就能想到你的效果,建立技术壁垒。


其实也不只是技术,要学会举一反三,找到自己在团队的差异性,虽然不可替代性在公司离了谁都可以转的说法下有些牵强,但是可以提高自己在团队的重要性和信任度。


信息渠道


要学会拓宽自己的信息渠道,有句话叫,掌握的信息越多,决策就越正确



  • 多看,看别人在做什么,输出什么,规划什么;

  • 多聊,找合作方,相同目标的同事,甚至其他公司的朋友,互通有无;


看完聊完要把对自己有用的信息提炼出来哦。


影响力


内部的话,主要是建立同事间的信任,技术的占比相对要多一些;


外部的话,主要是在合作方那里建立一个靠谱的口碑,如果配合超预期那就更好了,我就是被几个大佬抬了一手,虽然不起决定性作用,但是也很重要。


摆脱固化


跳脱出程序员的固化思维


程序的世界非0即1,程序员的思维都是非常严谨的,这个严谨有时候可能会显得有些“死板”,在商业化的公司里面,很多事情不是能不能的问题,而是要不要的问题。而在这里面,技术大多数都不是第一要素,出发点得是业务视角、用户视角,很多技术难点、卡点,有时候甚至不用技术也能解决。


小结



  • 沟通能力:逻辑要清晰,对象要分人,还有倾听能力,和善用沟通媒介;

  • owner意识:认真负责是工作的底线,积极主动是「owner意识」更高一级的要求;

  • 向上管理:向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道;

  • 写TODO:辅助工作,治愈内耗,一个成本低而收益高的好习惯;

  • 其他方面:拓宽信息渠道,找到技术方向,简历内部外部的影响力等;



实际上不止这些,今天就到这吧。



最后


当下的市场环境无论是求职还是晋升,都挺难的,都在降本增效,寒气逼人,我能拿到晋升的门票,诚然是实力的体现,但也有运气的成分。没晋升也不一定是你的问题,放平心态,当下保住工作才是最重要的。


哔哔了这么多,可能很多同学道理也都懂,估计就难在知行合一吧...


最后送给大家一句罗翔老师的经典名言:



人生最大的痛苦,就是你知道什么是对的,但却永远做出错误的选择,知道和做到这个巨大的鸿沟,你永远无法跨越。


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

协程和协程作用域

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。 1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。 2、父协程通过调用协程构建器(如 launch、async)来启动子...
继续阅读 »

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。


1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。




2、父协程通过调用协程构建器(如 launchasync)来启动子协程。在启动子协程时,子协程会继承父协程的上下文(包括调度器、异常处理器等)。这意味着子协程会以与父协程相同的上下文执行。父协程可以通过 join() 方法等待子协程执行完成,以确保子协程的结果可用。


父协程可以通过取消操作来取消子协程。当父协程被取消时,它会递归地取消所有的子协程。子协程会接收到取消事件,并根据取消策略来决定如何处理取消。


父协程和子协程之间的关系可以帮助管理协程的层次结构和生命周期。通过父协程启动和取消子协程,可以有效地组织和控制协程的执行流程,实现更灵活和可靠的协程编程。

fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

parentJob.join()
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#3 childJob start
main @coroutine#3 childJob complete
main @coroutine#2 parentJob complete
main @coroutine#1 parentJob cancel
Process finished with exit code 0
fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

//parentJob.join() <----------变化在这里
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#1 parentJob cancel
Process finished with exit code 0



3、协程作用域(CoroutineScope)是用于协程的上下文环境,它提供了协程的启动和取消操作的上下文。协程作用域定义了协程的生命周期,并决定了协程在何时启动、在何时取消。


协程作用域是一个接口,定义了两个主要方法:




  • launch:用于启动一个新的协程。launch 方法会创建一个新的协程,并将其添加到当前协程作用域中。启动的协程将继承父协程的上下文,并在协程作用域内执行。




  • cancel:用于取消协程作用域中的所有协程。cancel 方法会发送一个取消事件到协程作用域中的所有协程,使它们退出执行。




协程作用域与协程之间的关系是协程在协程作用域内执行的。协程作用域为协程提供了上下文环境,使得协程可以访问到必要的上下文信息,例如调度器(Dispatcher)和异常处理器(ExceptionHandler)。通过在协程作用域中启动协程,可以确保协程的生命周期受到协程作用域的管理,并且在协程作用域取消时,所有协程都会被取消。

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch(Job()) { <---------协程2不使用协程作用域的上下文,会脱离协程作用域的控制
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed 1685615335423
main @coroutine#1 Coroutine scope completed 1685615335424 <-------协程1执行完,协程作用域就执行完
Process finished with exit code 0 <---------程序退出



4、如果使用 GlobalScope.launch 创建协程,则协程会成为全局协程,它的生命周期独立于程序的其他部分。当协程的代码执行完毕后,全局协程不会自动退出,除非应用程序本身退出。因此,全局协程可以在整个应用程序的生命周期内持续执行,直到应用程序终止。


如果使用协程作用域(例如 runBlockingcoroutineScope 等)创建协程,则协程的生命周期受协程作用域的限制。当协程的代码执行完毕后,它会返回到作用域的父协程或顶级协程中,而不会自动退出。




5、当协程作用域内的所有协程执行完成后,协程作用域仍然存在,但其中的协程会被标记为完成状态。这意味着协程作用域仍然可以用于启动新的协程,但之前的协程不会再执行。


协程作用域的生命周期不仅仅依赖于其中的协程执行状态,还取决于其父协程或顶级协程的生命周期。如果协程作用域的父协程或顶级协程被取消或完成,那么协程作用域也将被取消

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch {
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed
main @coroutine#3 Coroutine 2 completed
main @coroutine#1 Coroutine scope completed
Process finished with exit code 0 <---------程序退出

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

一个96年前端的2022年中总结 (落户,看房,还贷,被裁)

人到中年中年总是觉得很累,每天忙不完的事,操不完的心,曾今那些无忧无虑的日子似乎消失的无影无踪, 工作三年一点存款没有 落户 关于落户这个事, 就是一个很突然的想法,很突然, 得知天津有个"海河英才计划",只要是本科生, 就比较容易落户,所以就想着试一试, ...
继续阅读 »


人到中年中年总是觉得很累,每天忙不完的事,操不完的心,曾今那些无忧无虑的日子似乎消失的无影无踪, 工作三年一点存款没有



落户


关于落户这个事, 就是一个很突然的想法,很突然, 得知天津有个"海河英才计划",只要是本科生, 就比较容易落户,所以就想着试一试, 心想起码这个是一个阶级的跨越(农村-->城市), 然后就在网上各种搜索,问一些了解的朋友,看看怎么操作, 最后发现好多人说有北京社保,在天津落户会被查出来, 然后就被拉入黑名单了. 最后为了稳妥还是找了一个中介帮忙操作了一下,花了1w, 等我办完,不久就有我的朋友自己办的三无人员落户, 一分钱没花. 唉😌!!!!!


看房


一开始在安居客上看, 因为穷, 基本也没啥可选择性, 一筛选也就那么几个, 然后就联系了一个销售去看房, 不得不说这个销售真的可以, 不知道他们能赚多少钱哈, 但是服务是真的不错, 一到了高铁站他们就去接你,还请你吃饭, 带着你看,看完之后还把你送到高铁站. 想着一开始就看西青和北辰的,但是西青的都好贵, 北辰好像会好点, 然后就去北辰看, 销售说他觉得武清也不错, 就带着去武清也看了, 看完之后, 心里比较了一下, 感觉被武清的哪个样板间和户型深深吸引了,并且价格也比较合适些, 然后我回到北京一周左右吧, 就跟那个销售说,打算买武清哪个, 然后然后我父母给我拿了20w, 剩下的就是我自己的,还借了朋友一些, 首付了40多, 然后就打印征信, 一顿签字就买了. 感觉很随意, 感觉买房就跟买菜似的, 不过还是有一点区别的, 买菜之后不会让我身无分文, 买房会😭. 现在就是每个月5000多贷款"真爽".


image.png


学习



  1. 缺失了刚毕业那会的激情,刚毕业那会,每天下班还会去学习, 刷视频, 看文章, 现在下班回家已经不想再打开电脑了. 刷视频(此视频非彼视频)

  2. 今年也学习了一些新的东西摸鱼之间,刷了一些课程,<破解JavaScript高级玩法,成为精通JS的原生专家> <Vue3全家桶>

  3. React技术栈是我工作一直使用的,也会持续性的学习一些,每天刷刷Medium和掘金

  4. 深度学习Nginx,进行了一半了,以前对nginx只停留在使用的层面

  5. 上半年在公司分享了一下架手架的原理以及实践

  6. 英语的学习说实话真的有点三天打鱼两天晒网了, 好在现在有一半了


img_1.png


工作


21年年底, 老东家北京这边合作的项目,终止了,然后面临了裁员, 不过当初也确实有了想跳槽的想法了,本来想,等到年终奖发了,就提离职. 没想到提前到来了, 给了正常的赔偿(n+1) 拿了三个月的赔偿, 正好月底, 算上本月的薪资,还有一些调休啥的 加起来一共是4个月, 感觉还挺爽的, 因为大概还有个20天左右吧, 就快过年了, 然后在回家和找工作抉择了一下, 决定先找找工作,然后就开始学习在掘金上查看面经, 感觉都是各种源码, 给我搞的有点懵, 毕竟缺钱嘛. 先后面试了一些公司: 金山, 58, 携程, 欢聚, 等等; 说实话,这段时间招人的还挺多的. 所以我很快就入职了, hr问我年前能入职吗? 其实那段时间疫情严重了, 老家那边也不让北京的回去, 所以在过年的前一周我入职了, (没钱的人不配拥有假期😭) . 唉, 第一次自己在外地过年.


关于兼职:



  • 今年和朋友一块干了一个公司的官网,本来也没打算要钱的,最后老板一人给了一张京东e卡

  • 还干了一个审核ppt的工作,一个ppt给150,不用改, 就说哪里写的不合适, 不过这个活有点恶心, 每个人理解不一样, 每次我这边审核后, 拿去交付,还是很多问题


生活



  1. 上半年感觉一直有疫情断断续续,大家都比较封闭,也没出去玩过, 偶尔和朋友去爬个山. 5, 6月还居家办公了好久, 记不太清了,应该得有一个月

  2. 从去年十月一到现在一直没有回过家, 有些想回去看看, 但是最近每个周末都有事😞

  3. 和女朋友去看过两次脱口秀, 感觉现场的感觉还是很棒的, 比电视看好太多

  4. 因为对象住他们单位的宿舍, 所以我自己平常下班也懒得收拾屋子, 只有礼拜天,才会大扫除一下, 或者对象来的时候😁

  5. 前段时间迷上了王者,以前从来不怎么玩游戏的, 熬夜打游戏, 导致生活节奏有点乱, 每天的精神状态也不如以前,正在积极调整. 但是吧,我告诉你们我的云中君玩的贼6的 不服来战哦

  6. 养了一只鹦鹉, 刚来还不是很好看, 现在尾巴长长的了


image.png


下半年flag



  • 完成Nginx的深度学习

  • 希望能出去转一圈, 看看外面的世界

  • 继续背单词学习

  • 看看车车, 目前感觉Crv和宋大妈还不错

  • 能再进行一次有价值的分享


作者:nanfeiyan
来源:juejin.cn/post/7124511406099005471
收起阅读 »

箭头函数函数是否有原型

web
问题:箭头函数是否有原型 今天在博客重构之余,看到某个前端群有群友这样问: 面试被问到了一个题,箭头函数是否有原型 大家觉得有吗? 首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。 箭头函数是什么 箭头函数表达式的语法比函数表达式更简...
继续阅读 »

问题:箭头函数是否有原型


今天在博客重构之余,看到某个前端群有群友这样问:



面试被问到了一个题,箭头函数是否有原型
大家觉得有吗?



首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。


箭头函数是什么



箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。



developer.mozilla.org/zh-CN/docs/…



引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。



一般来说如果问题是函数是否有原型时,那么可以好不犹豫的回答说是,但因为箭头函数的特殊性导致了答案的不确定性。


原型是什么



当谈到继承时,JavaScript 只有一种结构:对象。
每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。
原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null。
根据定义,null 没有原型,并作为这个原型链(prototype chain)中的最后一个环节。



developer.mozilla.org/zh-CN/docs/…



一个 Function 对象在使用 new 运算符来作为构造函数时,会用到它的 prototype 属性。它将成为新对象的原型。



developer.mozilla.org/zh-CN/docs/…


什么是原型在 mdn 中已经讲得很清楚了,也就是对象的prototype


试验


所以按理来说对象都有原型,那么试试就知道了。


首先看看箭头函数的原型:


const a = () => {};

console.log(a.prototype); // undefined

在浏览器控制台可以把上述代码执行一遍,可以发现结果是 undefined。


那么是否就说明箭头函数没有原型了呢?别急继续往下看。


const a = () => {};

console.log(a.prototype); // undefined

console.log(a.__proto__); // ƒ () { [native code] }

我们可以看到 a.__proto__ 是一个 native function


那么 __proto__ 又是什么呢?


__proto__是什么



Object.prototype (en-US) 的 __proto__ 属性是一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。




__proto__ 的读取器 (getter) 暴露了一个对象的内部 [[Prototype]] 。对于使用对象字面量创建的对象,这个值是 Object.prototype (en-US)。对于使用数组字面量创建的对象,这个值是 Array.prototype。对于 functions,这个值是 Function.prototype。对于使用 new fun 创建的对象,其中 fun 是由 js 提供的内建构造器函数之一 (Array, Boolean, Date, Number, Object, String 等等),这个值总是 fun.prototype。对于用 JS 定义的其他 JS 构造器函数创建的对象,这个值就是该构造器函数的 prototype 属性。




__proto__ 属性是 Object.prototype (en-US) 一个简单的访问器属性,其中包含了 get(获取)和 set(设置)的方法,任何一个 __proto__ 的存取属性都继承于 Object.prototype (en-US),但一个访问属性如果不是来源于 Object.prototype (en-US) 就不拥有 __proto__ 属性,譬如一个元素设置了其他的 __proto__ 属性在 Object.prototype (en-US) 之前,将会覆盖原有的 Object.prototype (en-US)。



developer.mozilla.org/zh-CN/docs/…


看解释可能有人不大理解,举个例子 🌰:


function F() {}
const f = new F();

console.log(f.prototype); // undefined
console.log(f.__proto__ === F.prototype); // true
console.log(f.__proto__.constructor); // F(){}

new F 也即是 F 的实例 ff__proto__ 属性指向 Fprototype


由此可以得出实例与原型的关系:new function得到实例,实例的 __proto__ 又指向function的原型,原型的构造器指向原函数。


结论


好了,理解了什么是 __proto__ 后,我们回到原来的问题上:箭头函数函数是否有原型?


通过上述的代码我们可以知道箭头函数就是Function的实例,如果你觉得不是,那么请看下面的例子:


const a = () => {};

console.log(a instanceof Function); // true
console.log(a.__proto__ === Function.prototype); // true
console.log(a.prototype); // undefined

所以最终得出两种结论:如果按中文语意那么箭头函数是Function的实例,而依据实例与原型的关系,它是有原型的;如果原型仅仅只说的是prototype,那么结论就是没有



注:以上代码结果都是在chrome113下运行得出



如有错误,欢迎指正~


参考


箭头函数
developer.mozilla.org/zh-CN/docs/…


构造器
developer.mozilla.org/zh-CN/docs/…


原型链
developer.mozilla.org/zh-CN/docs/…


Function.prototype
developer.mozilla.org/zh-CN/docs/…


__proto__
developer.mozilla.org/zh-CN/

docs/…

收起阅读 »

关于晋升的一点思考

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

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

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


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

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


准备工作


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


明确了以下几点:



  • 晋升是在做什么

  • 评委在看什么

  • 候选人要准备什么

  • 评判的标准是什么

  • 常见的坑有哪些


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



写PPT


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


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

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


迭代,迭代,还是迭代


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


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


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


相关阅读 what is OODA loop

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

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


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


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


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


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


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


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


错。


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


How?


公式:喜欢 = 熟悉 + 意外


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


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


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


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


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


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


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


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


Punchline


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


练习演讲


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


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


Talk is cheap. Show me the code.


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



  • 形象(肢体语言、着装等)

  • 声音(语速、语调、音量等)

  • 文字(逻辑、关键点等)



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


牢记表达的初衷


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


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


讲内容太多


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


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


过度粉饰或浮夸


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


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


注意衔接


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


热情


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


第一印象


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

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

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


准备问题


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


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


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



  • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等

  • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等

  • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等

  • 团队:项目管理经验,团队管理经验

  • 个人:个人特色,个人规划,自己的反思等等


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


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


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


听清楚再回答


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


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


问题背后的问题


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


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


逻辑自洽、简洁明了


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


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


其他


心态调整


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



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


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


关于评委


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


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


一些教训




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




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




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




附件


前期调研



参考文章


知乎


作者:酒红
来源:juejin.cn/post/7240805459288162360
收起阅读 »

程序员创业:从技术到商业的转变

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
继续阅读 »

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


为什么程序员要创业?


创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


创业需要具备的技能


作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


如何创业?


从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:




  1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。




  2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。




  3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。




  4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。




  5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。




结语


创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现

作者:郝学胜
来源:juejin.cn/post/7240465997002047547
自己心中的创业梦想。

收起阅读 »

App高级感营造之 高斯模糊

效果 类似毛玻璃,或者马赛克的效果。我们可以用它来提升app背景的整体质感,或者给关键信息打码。 源代码 import 'dart:ui'; import 'package:flutter/material.dart'; void main() { ...
继续阅读 »

效果


类似毛玻璃,或者马赛克的效果。我们可以用它来提升app背景的整体质感,或者给关键信息打码。


高斯模糊1.gif


高斯模糊2.gif


源代码


import 'dart:ui';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
// 高斯模糊的第一种写法 ImageFiltered 包裹要模糊的组件

/// 将子组件进行高斯模糊
/// [child] 要模糊的子组件
Widget _imageFilteredWidget1({required Widget child, double sigmaValue = 1}) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigmaValue, sigmaY: sigmaValue),
child: child,
);
}

/// 使用第一种模糊方式的案例
Widget _demo1() {
return Container(
padding: const EdgeInsets.all(50),
color: Colors.blue.shade100,
width: double.infinity,
child: Column(
children: [
_imageFilteredWidget1(
child: SizedBox(
width: 150,
child: Image.asset(
"assets/images/bz1.jpg",
fit: BoxFit.fitHeight,
),
),
),
const SizedBox(height: 100),
_imageFilteredWidget1(
child: const Text(
"测试高斯模糊",
style: TextStyle(fontSize: 30, color: Colors.blueAccent),
),
sigmaValue: 2)
],
),
);
}

/// 利用 BackdropFilter 做高斯模糊
_backdropFilterWidget2({
required Widget child,
double sigmaValueX = 1,
double sigmaValueY = 1,
}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigmaValueX, sigmaY: sigmaValueY),
child: child,
),
);
}

///
Widget _demo2() {
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
"assets/images/bz1.jpg",
fit: BoxFit.fill,
),
),
Positioned(
child: _backdropFilterWidget2(
sigmaValueX: _sigmaValueX,
sigmaValueY: _sigmaValueY,
child: Container(
width: MediaQuery.of(context).size.width - 100,
height: MediaQuery.of(context).size.height / 2,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x90ffffff),
),
child: const Text(
"高斯模糊",
style: TextStyle(fontSize: 30, color: Colors.white),
),
)),
top: 20,
),
_slider(
bottomMargin: 200,
themeColors: Colors.yellow,
title: '横向模糊度',
valueAttr: _sigmaValueX,
onChange: (double value) {
setState(() {
_sigmaValueX = value;
});
},
),
_slider(
bottomMargin: 160,
themeColors: Colors.blue,
title: '纵向模糊度',
valueAttr: _sigmaValueY,
onChange: (double value) {
setState(() {
_sigmaValueY = value;
});
},
),
_slider(
bottomMargin: 120,
themeColors: Colors.green,
title: '同时调整:',
valueAttr: _sigmaValue,
onChange: (double value) {
setState(() {
_sigmaValue = value;
_sigmaValueX = value;
_sigmaValueY = value;
});
},
),
],
),
);
}

Widget _slider({
required String title,
required double bottomMargin,
required Color themeColors,
required double valueAttr,
required ValueChanged<double>? onChange,
}) {
return Positioned(
bottom: bottomMargin,
child: Row(
children: [
Text(title, style: TextStyle(color: themeColors, fontSize: 18)),
SliderTheme(
data: SliderThemeData(
trackHeight: 20,
activeTrackColor: themeColors.withOpacity(.7),
thumbColor: themeColors,
inactiveTrackColor: themeColors.withOpacity(.4)
),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: Slider(
value: valueAttr,
min: 0,
max: 10,
onChanged: onChange,
),
),
),
SizedBox(
width: 50,
child: Text('${valueAttr.round()}',
style: TextStyle(color: themeColors, fontSize: 18)),
),
],
),
);
}

double _sigmaValueX = 10;
double _sigmaValueY = 10;

double _sigmaValue = 10;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _demo2(),
);
}
}

实现原理


实现高斯模糊,在flutter中有两种方式:


ImageFiltered


它可以对其包裹的子组件施加高斯模糊,需要传入 ImageFilter 控制模糊程度,分为X Y两个方向的模糊,实际上就是对图片进行拉伸,数字越大,模糊效果越大。


/// 将子组件进行高斯模糊
/// [child] 要模糊的子组件
Widget _imageFilteredWidget1({required Widget child, double sigmaValue = 1}) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigmaValue, sigmaY: sigmaValue),
child: child,
);
}

BackDropFilter


同样需要一个 ImageFilter参数控制模糊度,与 ImageFilter的区别是,它会对它覆盖的组件整体模糊。
所以如果我们需要对指定的子组件进行模糊的话,需要再包裹一个ClipRect裁切。


/// 利用  BackdropFilter 做高斯模糊
_backdropFilterWidget2({required Widget child, double sigmaValue = 1}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaY: sigmaValue, sigmaX: sigmaValue),
child: child,
),
);
}

由于 BackdropFilter 会对其子组件进行图形处理,所以其子组件可能会变得更加消耗性能。因此,需要谨慎使用 BackdropFilter 组件。


作者:拳布离手
来源:juejin.cn/post/7239631010429108280
收起阅读 »

不用递归也能实现深拷贝

web
前言 在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。 深...
继续阅读 »

前言


在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。


深拷贝的实现方式


我们先来看看常用的深拷贝的实现方式


JSON.parse(JSON.stringify())


利用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 把字符串解析成新的对象实现深拷贝。


这种方式代码简单,常用于深拷贝简单类型的对象。


在复杂类型的对象上会有问题:



  1. undefined、function、symbol 会被忽略或者转为 null(数组中)

  2. 时间对象变成了字符串

  3. RegExp、Error 对象序列化的结果将只得到空对象

  4. NaN、Infinity 和-Infinity,则序列化的结果会变成 null

  5. 对象中存在循环引用的情况也无法正确实现深拷贝


函数库 lodash 的 cloneDeep 方法


这种方式使用简单,而且 cloneDeep 内部是使用递归方式实现深拷贝,因此不会有 JSON 转换方式的问题;但是需要引入函数库 js,为了一个函数而引入一个库总感觉不划算。


递归方法


声明一个函数,函数中变量对象或数组,值为基本数据类型赋值到新对象中,值为对象或数组就调用自身函数。


// 手写深拷贝
function deepCopy(data) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp",
};
var copyData;
var type = map[Object.prototype.toString.call(data)];
if (type === "array") {
copyData = [];
data.forEach((item) => copyData.push(deepCopy(item)));
} else if (type === "object") {
copyData = {};
for (var key in data) {
copyData[key] = deepCopy(data[key]);
}
} else {
copyData = data;
}
return copyData;
}

递归方式结构清晰将任务拆分成多个简单的小任务执行,可读性强,但是效率低,调用栈可能会溢出,函数每次调用都会在内存栈中分配空间,而每个进程的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致溢出。


深拷贝其实是对树的遍历过程


嵌套对象很像下面图中的树。


Untitled.png


递归的思路是遍历 1 对象的属性判断是否是对象,发现属性 2 是一个对象在调用函数本身来遍历 2 对象的属性是否是对象如此反复知道变量 9 对象。


Untitled 1.png


9 对象的属性中没有对象然后返回 5 对象去遍历其他属性是否是对象,没有再返回 2 对象,最后返回到 1 对象发现其 3 属性是一个对象。


Untitled 2.png


Untitled 3.png


最后在找 4 对象。


Untitled 4.png


可以看到递归其实是对树的深度优先遍历。


那么不用递归可以实现树的深度优先遍历么?


答案是肯定的。


不用递归实现深度优先遍历深拷贝


观察递归算法可以发现实现深度优先遍历主要是两个点



  1. 利用栈来实现深度优先遍历的节点顺序

  2. 记录哪些节点已经走过了


第一点可以用数组来实现栈


const stack = [source]
while (stack.length) {
const data = stack.pop()
for (let key in data) {
if (typeof source[key] === "object") {
stack.push(data[key])
}
}
}

这样就能把所有的嵌套对象都放入栈中,就可以遍历所有的嵌套子对象。


第二点因为发现对象属性值是对象时会中断当前对象的属性遍历改去遍历子对象,因此要记录对象的遍历的状态。由于 for in 的遍历是无序的即使用一个变量存 key 也没办法知道哪些 key 已经遍历过了,需要一个数组记录所有遍历过的属性。


这里还有另一种简单的方法就是用 Object.keys 来获取对象的 key 数组放到 stack 栈中。


const stack = [...Object.keys(source).map(key => ({ key, source: source }))]
while (stack.length) {
const { key, data } = stack.pop()
if (typeof data[key] === "object") {
stack.push(...Object.keys(data[key]).map(k => ({ key: k, data: data[key] })))
}
}

这样 stack 中深度优先遍历的遍历的对象顺序也记录其中。


这里将代码优化下, 把 Object.keys 换成 Object.entries 更为精简


const stack = [...Object.entries(source)]
while (stack.length) {
const [ key, value ] = stack.pop()
if (typeof value === "object") {
stack.push(...Object.entries(value))
}
}

遍历完成下一步就是创建一个新的对象进行赋值。


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = {}
let id = 0
let cache
while (stack.length) {
const [key, value, id] = stack.pop()
if (id != undefined && cacheMap[id]) {
cache = cacheMap[id]
} else {
cache = result
}
if (typeof value === "object") {
cacheMap[id] = cache[key] = {}
stack.push(...Object.entries(value).map(item => [...item, id++]))
} else {
cache[key] = value
}
}
return result

因为对象时引用类型,因此可以通过 cacheMap[id] 来快速访问 result 的嵌套对象。


代码还可以优化:


cacheMap 可以用 WeakMap 来声明减少 id 的声明:


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = new WeakMap()
let cache
while (stack.length) {
const [key, value, parent] = stack.pop()
if (cacheMap.has(parent)) {
cache = cacheMap.get(parent)
} else {
cache = result
}
if (typeof value === "object") {
cache[key] = {}
cacheMap.set(value, cache[key])
stack.push(...Object.entries(value).map(item => [...item, value]))
} else {
cache[key] = value
}
}
return result

stack 中的数组项中的 parent 可以换成目标对象:


const result = {}
const stack = [...Object.entries(source).map(item => [...item, result])]
while (stack.length) {
const [key, value, target] = stack.pop()
if (typeof value === "object") {
target[key] = {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result

加上数组的判断最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = Array.isArray(source) ? [] : {}
const stack = [...Object.entries(source).map(item => [...item, result])]
const toString = Object.prototype.toString
while (stack.length) {
const [key, value, target] = stack.pop()
if (map[toString.call(value)] === 'object' || map[toString.call(value)] === 'array') {
target[key] = Array.isArray(value) ? [] : {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result
}

console.log(cloneDeep({ a: 1, b: '12' }))
//{ a: 1, b: '12' }
console.log(cloneDeep([{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]))
//[{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]

广度优先遍历实现深拷贝


同样的思路,实现深拷贝的最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = {}
const stack = [{ data: source, target: result }]
const toString = Object.prototype.toString
while (stack.length) {
let { target, data } = stack.unshift()
for (let key in data) {
if (map[toString.call(data[key])] === 'object' || map[toString.call(data[key])] === 'array') {
target[key] = Array.isArray(data[key]) ? [] : {}
stack.push({ data: data[key], target: target[key] })
} else {
target[key] = data[key]
}
}
}
return result
}

作者:千空
来源:juejin.cn/post/7238978371689136185
收起阅读 »

慢慢的喜欢上泛型 之前确实冷落了

前言 下图 CSDN 水印 为自身博客 什么泛型 通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可...
继续阅读 »

前言


下图 CSDN 水印 为自身博客


什么泛型



通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,同时,还可以提高代码的可读性和安全性。



泛型带来的好处



在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的



public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }

public static void main(String[] args) {
// do nothing
}

/**
* 不指定类型
*/

public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}

/**
* 指定类型
*/

public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}



上面这段代码中的 specifyType 方法中 省去了强制转换,可以在编译时候检查类型安全,可以用在类,方法,接口上。



泛型中通配符



我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?



常用的 T,E,K,V,?



本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:




  • ?表示不确定的 java 类型

  • T (type) 表示具体的一个java类型

  • K V (key value) 分别代表java键值中的Key Value

  • E (element) 代表Element

  • < T > 等同于 < T extends Object>

  • < ? > 等同于 < ? extends Object>


?无界通配符



先从一个小例子看起:



// 范围较广
static int countLegs (List<? extends Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}
// 范围定死
static int countLegs1 (List< Animal > animals ){
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}

public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不会报错
countLegs( dogs );
// 报错
countLegs1(dogs);
}



对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。



上界通配符 < ? extends E>



上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:



1.如果传入的类型不是 E 或者 E 的子类,编译不成功
2. 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用


List<? extends Number> eList = null;
eList = new ArrayList<Integer>();
//语句1取出Number(或者Number子类)对象直接赋值给Number类型的变量是符合java规范的。
Number numObject = eList.get(0); //语句1,正确

//语句2取出Number(或者Number子类)对象直接赋值给Integer类型(Number子类)的变量是不符合java规范的。
Integer intObject = eList.get(0); //语句2,错误

//List<? extends Number>eList不能够确定实例化对象的具体类型,因此无法add具体对象至列表中,可能的实例化对象如下。
eList.add(new Integer(1)); //语句3,错误

下界通配符 < ? super E>



下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object



在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。


List<? super Integer> sList = null;
sList = new ArrayList<Number>();

//List<? super Integer> 无法确定sList中存放的对象的具体类型,因此sList.get获取的值存在不确定性
//,子类对象的引用无法赋值给兄弟类的引用,父类对象的引用无法赋值给子类的引用,因此语句错误
Number numObj = sList.get(0); //语句1,错误

//Type mismatch: cannot convert from capture#6-of ? super Integer to Integer
Integer intObj = sList.get(0); //语句2,错误
//子类对象的引用可以赋值给父类对象的引用,因此语句正确。
sList.add(new Integer(1)); //语句3,正确

1. 限定通配符总是包括自己
2. 上界类型通配符:add方法受限
3. 下界类型通配符:get方法受限
4. 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
5. 如果你想把对象写入一个数据结构里,
6. 使用 ? super 通配符 如果你既想存,又想取,那就别用通配符
7. 不能同时声明泛型通配符上界和下界


?和 T 的区别


// 指定集合元素只能是T类型
List<T> list = new ArrayList<T>();
// 集合元素可以是任意类型的,这种是 没有意义的 一般是方法中只是为了说明用法
List<?> list = new Arraylist<?>();


?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :



// 可以
T t = operate();

// 不可以
?car = operate();


T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。



区别1 通过 T 来 确保 泛型参数的一致性


   public <T extends Number> void test1(List<T> dest, List<T> src) {
System.out.println();
}

public static void main(String[] args) {
test test = new test();
// integer 是number 的子类 所以是正确的
List<Integer> list = new ArrayList<Integer>();
List<Integer> list1 = new ArrayList<Integer>();
test.test1(list,list1);
}

在这里插入图片描述



通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型



public void
test(List<? extends Number> dest, List<? extends Number> src)

GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
List<String> dest = new ArrayList<>();
List<Number> src = new ArrayList<>();
glmapperGeneric.testNon(dest,src);
//上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),对于 dest 和 src 而言,就还是需要进行类型转换

区别2:类型参数可以多重限定而通配符不行



使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定



区别3:通配符可以使用超类限定而类型参数不行



类型参数 T 只具有 一种 类型限定方式



T extends A



但是通配符 ? 可以进行 两种限定



? extends A
? super A

Class和 Class<?>区别


Class<"T"> (默认没有双引号 系统会自动把T给我换成特殊字符才加的引号) 在实例化的时候,T 要替换成具体类。Class<?>它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况


作者:进阶的派大星
来源:juejin.cn/post/7140472064577634341
收起阅读 »

Vue 为什么要禁用 undefined?

web
Halo Word!大家好,我是大家的林语冰(挨踢版)~ 今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined? 敏感话题 我们会讨论几个敏感话题,包括但不限于—— 测不准的 undefined 如何引发复合 BUG? 薛定谔的...
继续阅读 »

Halo Word!大家好,我是大家的林语冰(挨踢版)~


今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined




敏感话题


我们会讨论几个敏感话题,包括但不限于——



  1. 测不准的 undefined 如何引发复合 BUG?

  2. 薛定谔的 undefined 如何造成二义性?

  3. 未定义的 undefined 为何语义不明?


懂得都懂,不懂关注,日后再说~




1. 测不准的 undefined 如何引发复合 BUG?


一般而言,开源项目对 undefined 的使用有两种保守方案:



  • 禁欲系——能且仅能节制地使用 undefined

  • 绝育系——禁用 undefined


举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0 重构 undefined


vue-void.png


事实上,直接使用 undefined 也问题不大,毕竟 undefined 表面上还是比较有安全感的。


readonly-desc.gif


猫眼可见,undefined 是一个鲁棒只读的属性,表面上相当靠谱。


虽然 undefined 自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined 到底可能整出什么幺蛾子呢?


你知道的,不同于 null 字面量,undefined 并不恒等于 undefined 原始值,比如说祂可以被“作用域链截胡”。


举个粒子,当 undefined 变身成为 bilibili,同事的内心是崩溃的。


bilibili.png


猫眼可见,写做 undefined 变量,读做 'bilbili' 字符串,这样的代码十分反人类。


这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined 是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!


这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined


换而言之,局部的同名变量 undefined 屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined


关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。


globalThis.undefined 确实是只读属性。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?


周树人.gif


我们总以为 undefined 短小精悍,但其实 globalThis.undefined 才能扬长避短。


当我们重新定义了 undefinedundefined 就名不副实——名为 undefined,值为任意值。这可能会在团队协作中引发复合 BUG。


所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。


举个粒子,常见的复合 BUG 包括但不限于:



  • 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API

  • 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践

  • 团队协作,Git 等代码版本管理工具的开发冲突


举个粒子,undefined 也可能造成类似的问题。


complex-bug.png


猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。


这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。


Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。


jenga.gif


积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。


换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。


所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。


祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。


举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——



  • 有人鞠躬尽瘁粮食安全

  • 有人精神饥荒疯狂倒奶


这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。


“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。


综上所述,要警惕测不准的 undefined 在团队开发中造成复合 BUG。




2. 薛定谔的 undefined 如何造成二义性?


除了复合 BUG,undefined 还可能让代码产生二义性。


代码二义性指的是,同一行代码,可能有不同的语义。


举个粒子,JS 的一些代码解读就可能有歧义。


mistake.png


undefined 也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。


举个粒子,代码中存在两个一龙一猪的 undefined


default.png


猫眼可见,undefined 的值并不相同,我只觉得祂们双标。


undefined 变量之所以是 'bilibili' 字符串,是因为作用域链就近屏蔽,cat 变量之所以是 undefined 原始值,是因为已声明未赋值的变量默认使用 undefined 原始值作为缺省值,所以没有使用局部的 undefined 变量。


倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。


destruct.png


猫眼可见,undefined 有没有精神分裂我不知道,但我快精神分裂了。


代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。


strict-mode.png


猫眼可见,我写你猜,谁都不爱。


大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var


问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。


语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。


事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。


请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。


语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:



  • 骨折之后骨头更加坚硬了

  • 健身也是肌肉轻度撕裂后增生

  • 记忆也是不断复习巩固


语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。


综上所述,undefined 的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。




3. 未定义的 undefined 为何语义不明?


除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined 自身的语义也很难把握。


举个粒子,因为太麻烦就全写 undefined 了。


init.png


猫眼可见,原则上允许我们可以无脑地使用 undefined 初始化任何变量,万物皆可 undefined


虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined 的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined 的悖论。


代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined 既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。


虽然但是,准确而无用的观念,终究还是无用的。undefined 的正确打开方式就是无为,使用 undefined 的最佳方式是不使用祂。




免责声明



本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。



今天的《ES6 混合理论》就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。


吾乃前端的虔信徒,传播 BUG 的福音。


我是大家的林语冰,我们一期一会,不散不见,掰掰~


作者:大家的林语冰
来源:juejin.cn/post/7240483867123220540
收起阅读 »

由阿里裁员引发的一些思考

前言 从阿里淘系离开差不多2年多了,最近阿里又来到风口浪尖上,也是打出一套眼花缭乱的组合拳, 先是马老板回国; 3.28日 阿里开启成立24年来最大组织架构变革,逍遥子张勇,宣布启动“1+6+N”组织变革,各个大业务线实行自负盈亏,有独立融资和上市的可能性;...
继续阅读 »


前言


从阿里淘系离开差不多2年多了,最近阿里又来到风口浪尖上,也是打出一套眼花缭乱的组合拳



  1. 先是马老板回国;

  2. 3.28日 阿里开启成立24年来最大组织架构变革,逍遥子张勇,宣布启动“1+6+N”组织变革,各个大业务线实行自负盈亏,有独立融资和上市的可能性;

  3. 最近又开始爆出阿里大裁员,各种小道消息什么 阿里云7%,天猫淘宝25%,然后阿里开始辟谣,且不论真假,一时间,给整个互联网圈传递一股寒气,今天就在茶话会上聊聊这个事情。


理性的看待阿里裁员


阿里裁员其实是有个心理预期的,个人觉得主要有以下原因吧



  1. 阿里 361制度末位淘汰10%,连续2年3.25就会被淘汰,每年本身就有一批人要淘汰

  2. 组织架构拆解需要自负盈亏,一些子业务之前可以吃大锅饭,现在分田到户了,就需要人员进行优化提高组织效率,达到降本增效

  3. 核心业务一直被蚕食,人才盘点降本增效,淘汰贵的产出一般的 换一拨 校招生既能补充新鲜血液又可以降低成本


目前在互联网下行这个大环境下,加之之前一直宣传的35岁危机更加放大了裁员带来的恐慌。互联网正在慢慢回归理性这是个不争的事实,甚至连老美的硅谷互联网大厂都裁了一波,看看马斯克接手推特后"大杀四方"的狠劲。


互联网的退潮期


大潮正在退去


行业也是要顺势而为,风口来了猪都上天,不过目前国内互联网已经过了之前的高速增长期,监管也在收紧,大家都在拼存量市场,都卷到到菜市场了(各种买菜 多多买菜、橙心优选等等)从蓝海杀到了红海,增长上不去了,要么开源,要么节流。开源的话寻找新赛道何其难,元宇宙的尸体还热乎着,前几年大家都选择出海,但是除了字节还算可以(也难),小米在印度被阿三罚了好几十亿,整体看来开源难度过大。 image.png 大厂纷纷启用了节流大招,字节去年就在喊去肥增瘦各个项目开始review roi要求打正,肯定打不正的有的就地正法了,教育部门、游戏部门都是重灾区; 腾讯去年也是整合内部资源,PCG(破产G)去年基本上干掉一半,其实阿里这一波跟去年腾讯一样半斤八两。 而且这几年互联网大厂之间好像有点默契,年终之后都在裁员,降低员工流动性,不仅对业务稳定带来好处也能减少薪资开支。不过跳槽涨薪确实香,之前基本2年一跳,早些时候行情好能double,最不济也有40%的涨幅,不过现在在字节已经2年多了,确实没有任何跳槽打算了


非理性招聘慢慢在回归理性


大厂员工的招聘本身就是非理性的,还记得前年微信出了一个爆炸的表情,当时看到一篇文章作者自嘲自己是清华毕业在微信研究"炸屎"表情。。。非理性主要有以下几个方面造成的:



  1. 赛马机制导致团队重复


早期在业务遇到增长瓶颈和重大的课题时候,往往采用加人的方法,《人月神话》早就证伪了技术在这方面的不可靠,可能反而会让协作效率降低。这也间接导致了大厂的山头主义,由于领导需要使用团队规模来确立地位,毕竟更多的HC,就意味着揽到更多的事情,获得更高的地位。这在早期也是被更高层的领导所默许的,腾讯大名鼎鼎的赛马机制,就是使用多支团队来做同样的事情,微信当年就是这么诞生的,观察大部门的大厂对团队的分工有时候是可以模糊的,而且资源都还不错,如果一个团队不行,就让另一个上,这也不可避免的引起了内耗,这种机制效率上有有提升,但是代价是巨大资金开销,内部组织臃肿。



  1. 人才储备过盛


前些年,大厂业务增长太强劲了,各个赛道都要投入人力,人员分工更加细化,大厂的app可能一个按钮就是一个业务线,这也经常自嘲为拧螺丝的,因此招聘规模也是空前的,先招进来再内部淘汰,挑选了最优秀的,而且也是变相的打击竞争对手,不由得想起来了华为,华为之前财大气粗,连续狂招几年,直接把中兴干的人才断档。 在当前这个战略收缩的过冬阶段,这样的裁员可能还是结构性的、长期的,只要业务不行公司可能就及时止损了,带来的就是裁员,得有个清醒的认识。



  1. 大厂员工真的不便宜


互联网作为行业天花板,经历了资本的无序扩张,资金充备,花的都是投资人的钱,互相竞争着加价招人,大厂校招生的白菜价也是其他行业所无法企及的,几年下来每年的普调、跳槽的几轮加价,大家都来到了一个薪资高位。这些都是建立在你做的业务能给公司转来更多的钱,当增长停止时候,你还能给公司赚这么多吗?从经济学的供需关系来看很简单:公司年薪100w招你来,你真的能持续给公司多赚150w吗?业务增长时候,公司开掉你,人力成本节省了100w,公司的业务会降低100w吗?如果没有那裁掉的你是个理性的选择,因为你实在是太贵了。这些年大厂不光是干掉大头兵,甚至连一些高P开始受到波及了,因为他们更贵。所以薪资来到高位的找个好业务能苟着尽量苟着,风口没了,猪是飞不上天的。加上最近几年大学生找工作难,裁员换血一波,对公司也是极好的。 再看看老美那边,大厂基本也都是正式员工(贵)+外包(便宜),国内感觉也会慢慢往这个方向发展,看看国内华为这几年,外包招的飞起,之前在阿里 QA、前端、UI都是正式工+外包搭配。(除了移动端+服务端好像没见过)


大趋势下的互联网人该如何做


黄金时代一起不复返了,看清行业的大趋势,做好心理预期:



  1. 降低自己的经济杠杆,适当降低消费欲望,毕竟手里有粮心不慌。我就把房贷提前还了大头,留点尾巴抵抵税

  2. 尽量找个稳一点的业务线,延长职业生涯,认真做事,苟住就好,边缘业务可能隔三差五就要一波拥抱变化,我是活水到了字节一个比较赚钱的业务

  3. 有余力的可以探索一些副业,可以是老本行,或者家里人脉广的也可以涉足其他行业,比如水果店啥的,看自己人脉关系了,试试看副业能不能养起来。可以是一种商业模式,我看就有很多网红收割校招生搞星球,搞培训啥的忙的不亦乐乎;也可以是发现一个痛点,上次就听说 一个还没交付的楼盘一个业主在业主群搞了个公众号,直播楼盘进度,小的私域流量也是能赚点小钱的。培养一些产品思维,注意观察生活吧,不过副业确实比较难要有耐心,最近我就在googleplay上架一个游戏app,不过自然流量太低了,国内的话现在对个人开发者太不又好了,不仅应用商店很多都需要企业资质,穿山甲、优量汇这些广告平台也是需要企业资质,基本把个人开发者路封死了,注册个企业比较麻烦成本也高,这块有兴趣后面可以展开说说,还有灰黑产的话还是要慎重,来钱快可能进去也快

  4. 要是还想在业内混的话,专业能力还是不能丢,提高个人竞争力,保持技术关注度,加强 技术的深度以及广度,做一个T型人才,尽量成为一个全栈吧。其实现在服务端go的gin框架,java的spring学起来也很快,前端搞个小程序基本上就齐活了,不仅是个人竞争力,也是副业的基础;工作中也要注意提高自己的软实力,比如 稳定的情绪、有效的沟通、适当的向上管理,做一个大家都认可的靠谱的合作伙伴


最后尽人事 听天命吧,心态还是要稳住,积极乐观的工作和生活。好了,茶也喝完了,本期的茶话会就到这里吧,祝大家工作顺利,欢迎留言讨论


作者:Android茶话会
来源:juejin.cn/post/7237489935901032504
收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身

作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
体,活下来才有输出。

收起阅读 »

python计算质数的几种方法

因为要学着写渗透工具,这几天都在上python编程基础课,听得我打瞌睡,毕竟以前学过嘛。 最后sherry老师留了作业,其中一道题是这样的: 题目:编写python程序找出10-30之间的质数。 太简单了,我直接给出答案: Prime = [11, 13, 1...
继续阅读 »

因为要学着写渗透工具,这几天都在上python编程基础课,听得我打瞌睡,毕竟以前学过嘛。
最后sherry老师留了作业,其中一道题是这样的:


题目:编写python程序找出10-30之间的质数。


太简单了,我直接给出答案:


Prime = [11, 13, 17, 19, 23, 29]
print(Prime)

输出结果:


[11, 13, 17, 19, 23, 29]

当然,这样做肯定会在下节课被sherry老师公开处刑的,所以说还是要根据上课时学的知识写个算法。


1.穷举法


回想一下上课时学了变量、列表、循环语句之类的东西,sherry老师还亲自演示了多重死循环是怎么搞出来的(老师是手滑了还是业务不熟练啊),所以我们还是要仔细思考一下不要重蹈覆辙。


思路:首先要构造一个循环,遍历所有符合条件的自然数,然后一个一个验证是否为质数,最后把符合条件的质数列出来。


# 最开始编的穷举法,简单粗暴,就是性能拉跨。
# P=因数,N=自然数
import time

t0 = time.time() # 开始时间
Min = 10 # 范围最小值
Max = 30 # 范围最大值
Result = [] # 结果

for N in range(Min, Max): # 给自然数来个遍历
for P in range(2, N):
if (N % P == 0): # 判断是否有因数
break # 有因数那就不是质数,跳出循环
else:
Result.append(N)

print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', Result)
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('计算耗时:', time.time() - t0, '秒')

执行结果(计算耗时是最后加上去的):


2023-05-28-22-35-46.png


到这里作业就搞定了。然后把其他几道题也做完了,发现很无聊,就又切回来想搞点事。这么点计算量,0秒真的有点少,不如趁这个机会烤一烤笔记本的性能,所以直接在Min和Max的值后面加几个0。试试100000-200000。


2023-05-28-23-02-03.png


很尴尬,直接卡住了,这代码有点拉跨啊,完全不符合我的风格。
倒了杯咖啡,终于跑完了。


2023-05-28-23-01-07.png


这个也太夸张,一定是哪里出了问题,很久以前用C写的代码我记得也没那么慢啊。反正周末挺闲的,不如仔细研究一下。


2.函数(CV)大法


为了拓宽一下思路,我决定借鉴一下大佬的代码。听说函数是个好东西,所以就CV了两个函数。


一个函数判断质数,另一个求范围内的所有质数,把它们拼一起,是这个样子:


# 网上学来的,自定义两个函数,但是数值稍微大点就卡死了。
import time

t0 = time.time()
Min = 100000 # 范围最小值
Max = 200000 # 范围最大值


def is_prime(n): return 0 not in [n % i for i in range(2, n//2+1)] # 判断是否为质数


def gen_prime(a, b): return [n for n in range(
a, b+1) if 0 not in [n % i for i in range(2, n//2+1)]] # 输出范围内的质数


print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', gen_prime(Min, Max))
print('计算耗时:', time.time() - t0, '秒')

稍微改动了一下,还是100000-200000,我们试试看。


2023-05-28-23-08-35.png


嗯,一运行风扇就开始啸叫,CPU都快烤炸了。看来CV大法也不行啊。
经过漫长的烤机,这次结果比上次还惨,300多秒,这两个函数本质上还是穷举法,看来这条路也走不通。


3.穷举法改


我们可以分析一下穷举法的代码,看看有没有什么改进的方法。
首先,通过九年义务教育掌握的数学知识,我们知道,质数中只有2是偶数,所以计算中可以把偶数忽略掉,只计算奇数,工作量立马减半!
其次,在用因数P判断N是否为质数时,如果P足够大的话,比如说PxP>=N的时候,那么后面的循环其实是重复无意义的。因为假设PxQ>=N,那么P和Q必然有一个小于sqrt(N),只需要计算P<=sqrt(N)的情况就行了。


因为2作为唯一一个偶数,夹在循环里面处理起来很麻烦,所以放在开头处理掉。最终的代码如下:


# 优化后的代码,减少了一些无意义的循环,比以前快多了。
import time

t0 = time.time()
Min = 100000 # 范围最小值
Max = 200000 # 范围最大值
Prime = [2, 3] # 质数列表
Result = [] # 结果
Loop = 0 # 计算循环次数

if Min <= 2:
Result.append(2)
if Min <= 3:
Result.append(3) # 先把2这个麻烦的偶数处理掉
for N in range(5, Max, 2):
for P in range(3, int(N**0.5)+2, 2): # 只计算到根号N
Loop += 1
if (N % P == 0):
break
else:
Prime.append(N)
if N > Min:
Result.append(N)

print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', Result)
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

2023-05-28-23-09-54.png


代码量虽然多了,但是效果还是很明显,100000-200000才0.4秒,快了不知道多少,看来我们的思路是对的。
我决定再加到1000000-5000000,看看能不能撑住。因为输出太多了控制台会卡死,所以改一下,只输出最后一个质数。


2023-05-28-23-19-12.png


总共花了64秒,看来还是有点费劲。


4.穷举法魔改


我们再来分析一下,如果我们用于判断的因数,不是用奇数列表,而是用生成的Prime列表里面的质数,因为质数的个数远远少于奇数,所以第二个循环会少一些工作量呢?可以试试看。但是因为这个改动,需要加一些判断语句进去,所以节省的时间比较有限。


# 别看这个代码比较长,但是跑到1000万也不会卡死,而且还很快。
import time

t0 = time.time()
Min = 1000000 # 范围最小值
Max = 5000000 # 范围最大值
Prime = [2, 3] # 质数列表
Result = [] # 结果
Loop = 0 # 计算循环次数

if Min <= 2:
Result.append(2)
if Min <= 3:
Result.append(3)
for N in range(5, Max, 2):
M = int(N**0.5) # 上限为根号N
for P in range(len(Prime)): # 在质数列表Prime中遍历
Loop += 1
L = Prime[P+1]
if (N % L == 0):
break
elif L >= M: # 上限大于根号N,判断为质数并跳出循环
Prime.append(N)
if N > Min:
Result.append(N)
break

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

还是1000000-5000000再试试看


2023-05-28-23-25-29.png


这次耗时22秒,时间又缩短了一大半,但是好像已经没多少改进的空间了,感觉穷举法已经到头了,需要新的思路。


5.埃氏筛法


其实初中数学我们就学过埃氏筛法:
如果P是质数,那么大于P的N的倍数一定不是质数。把所有的合数排除掉,那么剩下的就都是质数了。
我们可以生成一个列表用来储存数字是否是质数,初始阶段都是质数,每次得出一个质数就将它的倍数全部标记为合数。


# 速度已经起飞了。
import time

t0 = time.time()
Min = 1000000 # 范围最小值
Max = 2000000 # 范围最大值
Loop = 0 # 计算循环次数
Result = [] # 结果

Natural = [True for P in range(Max)] # 自然数列表标记为True
for P in range(2, Max):
if Natural[P]: # 标记如果为True,就是质数
if P >= Min:
Result.append(P) # 添加范围之内的质数
for N in range(P*2, Max, P): # 将质数的倍数的标记改为False
Loop += 1
Natural[N] = False

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

2023-05-29-00-11-23.png


1.6秒,比最高级的穷举法还要快上10多倍,这是数学的胜利。
再试试1-50000000。


1.png


很不错,只需要20秒。因为筛法的特性,忽略内存的影响,数值越大,后面的速度反而越快了。


6.欧拉筛法


我们可以仔细分析一下,上面的埃氏筛法在最后标记的时候,还是多算了一些东西,N会重复标记False,比如77,既是7的倍数又是11的倍数,这样会被标记两次,后面的大合数会重复标记多次,浪费了算力,所以标记的时候要排除合数。另外就是P*N大于Max时,后面的计算已经无意义了,也要跳出来。把这些重复的动作排除掉,就是欧拉筛法,也叫线性筛。


# 最终版,优化了很多细节。
import time

t0 = time.time()
Min = 1 # 范围最小值
Max = 50000000 # 范围最大值
Loop = 0 # 计算循环次数
Prime = [2]
Result = [] # 结果

if Min <= 2:
Result.append(2)
Limit = int(Max/3)+1
Natural = [True for P in range(Max+1)] # 自然数列表标记为True
for P in range(3, Max+1, 2):
if Natural[P]: # 标记如果为True,就是质数
Prime.append(P)
if P >= Min:
Result.append(P)
if P > Limit: # 超过Limit不需要再筛了,直接continue
continue
for N in Prime: # 将质数的倍数的标记改为False
Loop += 1
if P*N > Max: # 超过Max就无意义了,直接break
break
Natural[P * N] = False
if P % N == 0: # 判断是否为合数
break

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

(因为之前的版本缩进错了,所以更新了这段代码)


2.png


同样的条件下耗时11.46秒。这是因为多了一个列表和几行判断语句,加上python的解释型特性,所以实际上并不会快好几倍,但是总体效率还是有50%左右的提升。


好了,这次把老师课堂上讲的变量、列表、循环语句什么的都用上了,算是现买现卖、活学活用吧。我觉得这次的作业怎么说也能拿满分吧,sherry老师记得下次上课夸夸我。


作者:ReisenSS
来源:juejin.cn/post/7238199999732695097
收起阅读 »

Vue3 除了keep-alive,还有哪些页面缓存的实现方案

web
引言 有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。 对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页...
继续阅读 »

引言


有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。


对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页数据并重新获取新数据来达到列表页重新加载的效果。


但是,这个方案有个很不好的地方就是:如果列表页足够复杂,有下拉刷新、下拉加载、有弹窗、有轮播等,在清除缓存时,就需要重置很多数据和状态,而且还可能要手动去销毁和重新加载某些组件,这样做既增加了复杂度,也容易出bug。


接下来说说我的想到的新实现方案(代码基于Vue3)。


省流


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaocheng55…


keep-alive 缓存和清除



keep-alive 缓存原理:进入页面时,页面组件渲染完成,keep-alive 会缓存页面组件的实例;离开页面后,组件实例由于已经缓存就不会进行销毁;当再次进入页面时,就会将缓存的组件实例拿出来渲染,因为组件实例保存着原来页面的数据和Dom的状态,那么直接渲染组件实例就能得到原来的页面。



keep-alive 最大的难题就是缓存的清理,如果能有简单的缓存清理方法,那么keep-alive 组件用起来就很爽。


但是,keep-alive 组件没有提供清除缓存的API,那有没有其他清除缓存的办法呢?答案是有的。我们先看看 keep-alive 组件的props:


include - string | RegExp | Array。只有名称匹配的组件会被缓存。
exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
max - number | string。最多可以缓存多少组件实例。

从include描述来看,我发现include是可以用来清除缓存,做法是:将组件名称添加到include里,组件会被缓存;移除组件名称,组件缓存会被清除。根据这个原理,用hook简单封装一下代码:


import { ref, nextTick } from 'vue'

const caches = ref<string[]>([])

export default function useRouteCache () {
// 添加缓存的路由组件
function addCache (componentName: string | string []) {
if (Array.isArray(componentName)) {
componentName.forEach(addCache)
return
}

if (!componentName || caches.value.includes(componentName)) return

caches.value.push(componentName)
}

// 移除缓存的路由组件
function removeCache (componentName: string) {
const index = caches.value.indexOf(componentName)
if (index > -1) {
return caches.value.splice(index, 1)
}
}

// 移除缓存的路由组件的实例
async function removeCacheEntry (componentName: string) {
if (removeCache(componentName)) {
await nextTick()
addCache(componentName)
}
}

return {
caches,
addCache,
removeCache,
removeCacheEntry
}
}

hook的用法如下:


<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>

<script setup lang="ts">
import useRouteCache from './hooks/useRouteCache'
const { caches, addCache } = useRouteCache()

<!-- 将列表页组件名称添加到需要缓存名单中 -->
addCache(['List'])
</script>

清除列表页缓存如下:


import useRouteCache from '@/hooks/useRouteCache'

const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List')


此处removeCacheEntry方法清除的是列表组件的实例,'List' 值仍然在 组件的include里,下次重新进入列表页会重新加载列表组件,并且之后会继续列表组件进行缓存。



列表页清除缓存的时机


进入列表页后清除缓存


在列表页路由组件的beforeRouteEnter勾子中判断是否是从其他页面(Home)进入的,是则清除缓存,不是则使用缓存。


defineOptions({
name: 'List1',
beforeRouteEnter (to: RouteRecordNormalized, from: RouteRecordNormalized) {
if (from.name === 'Home') {
const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List1')
}
}
})

这种缓存方式有个不太友好的地方:当从首页进入列表页,列表页和详情页来回切换,列表页是缓存的;但是在首页和列表页间用浏览器的前进后退来切换时,我们更多的是希望列表页能保留缓存,就像在多页面中浏览器前进后退会缓存原页面一样的效果。但实际上,列表页重新刷新了,这就需要使用另一种解决办法,点击链接时清除缓存清除缓存


点击链接跳转前清除缓存


在首页点击跳转列表页前,在点击事件的时候去清除列表页缓存,这样的话在首页和列表页用浏览器的前进后退来回切换,列表页都是缓存状态,只要当重新点击跳转链接的时候,才重新加载列表页,满足预期。


// 首页 Home.vue

<li>
<router-link to="/list" @click="removeCacheBeforeEnter">列表页</router-link>
</li>


<script setup lang="ts">
import useRouteCache from '@/hooks/useRouteCache'

defineOptions({
name: 'Home'
})

const { removeCacheEntry } = useRouteCache()

// 进入页面前,先清除缓存实例
function removeCacheBeforeEnter () {
removeCacheEntry('List')
}
</script>

状态管理实现缓存


通过状态管理库存储页面的状态和数据也能实现页面缓存。此处状态管理使用的是pinia。


首先使用pinia创建列表页store:


import { defineStore } from 'pinia'

interface Item {
id?: number,
content?: string
}

const useListStore = defineStore('list', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
isRefresh: true,
pageSize: 30,
currentPage: 1,
list: [] as Item[],
curRow: null as Item | null
}
},
actions: {
setList (data: Item []) {
this.list = data
},
setCurRow (data: Item) {
this.curRow = data
},
setIsRefresh (data: boolean) {
this.isRefresh = data
}
}
})

export default useListStore

然后在列表页中使用store:


<div>
<el-page-header @back="goBack">
<template #content>状态管理实现列表页缓存</template>
</el-page-header>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === listStore.curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="listStore.currentPage"
:page-size="listStore.pageSize"
layout="total, prev, pager, next"
:total="listStore.list.length"
/>
</div>

<script setup lang="ts">
import useListStore from '@/store/listStore'
const listStore = useListStore()

...
</script>

通过beforeRouteEnter钩子判断是否从首页进来,是则通过 listStore.$reset() 来重置数据,否则使用缓存的数据状态;之后根据 listStore.isRefresh 标示判断是否重新获取列表数据。


defineOptions({
beforeRouteEnter (to: RouteLocationNormalized, from: RouteLocationNormalized) {
if (from.name === 'Home') {
const listStore = useListStore()
listStore.$reset()
}
}
})

onBeforeMount(() => {
if (!listStore.useCache) {
loading.value = true
setTimeout(() => {
listStore.setList(getData())
loading.value = false
}, 1000)
listStore.useCache = true
}
})

缺点


通过状态管理去做缓存的话,需要将状态数据都存在stroe里,状态多起来的话,会有点繁琐,而且状态写在store里肯定没有写在列表组件里来的直观;状态管理由于只做列表页数据的缓存,对于一些非受控组件来说,组件内部状态改变是缓存不了的,这就导致页面渲染后跟原来有差别,需要额外代码操作。


页面弹窗实现缓存


将详情页做成全屏弹窗,那么从列表页进入详情页,就只是简单地打开详情页弹窗,将列表页覆盖,从而达到列表页 “缓存”的效果,而非真正的缓存。


这里还有一个问题,打开详情页之后,如果点后退,会返回到首页,实际上我们希望是返回列表页,这就需要给详情弹窗加个历史记录,如列表页地址为 '/list',打开详情页变为 '/list?id=1'。


弹窗组件实现:


// PopupPage.vue

<template>
<div class="popup-page" :class="[!dialogVisible && 'hidden']">
<slot v-if="dialogVisible"></slot>
</div>
</template>

<script setup lang="ts">
import { useLockscreen } from 'element-plus'
import { computed, defineProps, defineEmits } from 'vue'
import useHistoryPopup from './useHistoryPopup'

const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 路由记录
history: {
type: Object
},
// 配置了history后,初次渲染时,如果有url上有history参数,则自动打开弹窗
auto: {
type: Boolean,
default: true
},
size: {
type: String,
default: '50%'
},
full: {
type: Boolean,
default: false
}
})
const emit = defineEmits(
['update:modelValue', 'autoOpen', 'autoClose']
)

const dialogVisible = computed<boolean>({ // 控制弹窗显示
get () {
return props.modelValue
},
set (val) {
emit('update:modelValue', val)
}
})

useLockscreen(dialogVisible)

useHistoryPopup({
history: computed(() => props.history),
auto: props.auto,
dialogVisible: dialogVisible,
onAutoOpen: () => emit('autoOpen'),
onAutoClose: () => emit('autoClose')
})
</script>

<style lang='less'>
.popup-page {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 100;
overflow: auto;
padding: 10px;
background: #fff;

&.hidden {
display: none;
}
}
</style>

弹窗组件调用:


<popup-page 
v-model="visible"
full
:history="{ id: id }">
<Detail></Detail>
</popup-page>


hook:useHistoryPopup 参考文章:juejin.cn/post/713994…



缺点


弹窗实现页面缓存,局限比较大,只能在列表页和详情页中才有效,离开列表页之后,缓存就会失效,比较合适一些简单缓存的场景。


父子路由实现缓存


该方案原理其实就是页面弹窗,列表页为父路由,详情页为子路由,从列表页跳转到详情页时,显示详情页字路由,且详情页全屏显示,覆盖住列表页。


声明父子路由:


{
path: '/list',
name: 'list',
component: () => import('./views/List.vue'),
children: [
{
path: '/detail',
name: 'detail',
component: () => import('./views/Detail.vue'),
}
]
}

列表页代码:


// 列表页
<template>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="list.length"
/>


<!-- 详情页 -->
<router-view class="popyp-page"></router-view>
</template>

<style lang='less' scoped>
.popyp-page {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: #fff;
overflow: auto;
}
</style>

结尾


地址:


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaoch

eng55…

收起阅读 »

用proxy改造你的console

web
前言 在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。 let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';...
继续阅读 »

前言


在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。


let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';
console.log(name1)
console.log(name2)
console.log(name3)
console.log(name4)

打印如下
image.png


那个变量 对应那个 就比较难分辨。我又不想在写代码来分辨(懒😀),那个打印对应的变量是多少。


解决方案


方案一,通过ast解析console 将变量名放在console后面,奈何esbuild不支持ast操作(不是我不会 哈哈哈哈), 故放弃。


方案二,既然vue能代理对象,那么console是不是也能被代理。


实践


第一步代理console,将原始的console,用全局变量originConsole保存起来,以便后续使用
withLogging 函数拦截console.log 重写log参数


const originConsole = window.console; 
var console = new Proxy(window.console, {
get(target, property) {
if(property === 'log') {
return withLogging(target[property])
}
return target[property] },
})

遇到问题,js中 无法获取获取变量的名称的字符串。就是说无法打印变量名。


解决方案,通过vite中的钩子函数transform,将console.log(name.x) 转化成 console.log(name.x, ['isPlugin', 'name.x'])


      transform(src, id) {
if(id.includes('src')) { // 只解析src 下的console
const matchs = src.matchAll(/console.log\((.*)\);?/g);
[...matchs].forEach((item) => {
const [matchStr, args] = item;
let replaceMatch = ''
const haveSemicolon = matchStr.endsWith(";");
const sliceIndex = haveSemicolon ? -2 : -1;
const temp = matchStr.slice(0,sliceIndex);
const tempArgs = args.split(",").map(item => {
if(item.endsWith('"')) {
return item
}
return `"${item}"`
}).join(",")
replaceMatch = `${temp},['isPlugin',${tempArgs}]);`
src = src.replace(matchStr, replaceMatch)
});
}
return {
code: src,
id,
}
},

这样最终就实现了类型于这样的输出代码


  originConsole.log('name.x=', name.x)

这样也就最终实现了通过变量输出变量名跟变量值的一一对应


最后


我将其写成了一个vite插件,vite-plugin-consoles 感兴趣的可以试试,有bug记得跟我说(●'◡'●)


源码地址:
github.com/ALiangTech/…


作者:平平无奇的阿良
来源:juejin.cn/post/7238508573667344441
收起阅读 »

前端小白的几个坏习惯

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

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


文件名命名


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



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


原因有如下几点:



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

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

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

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

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


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


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


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


使用英语,越早越好。


文件类型命名


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



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


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


原因有如下几点:



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


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


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

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


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

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


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



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

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

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


代码格式化


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



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


代码格式化有以下优点:



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

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

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

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

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


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


补充说明


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


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


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


我说两点。


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


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


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


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


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


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


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


言尽于此。


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


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


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

都JDK17了,你还在用JDK8

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

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


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


0x01 纵观发展




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




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




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




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




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




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




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




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


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

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


0x02 详细解读


JDK9 新特性


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



  1. 模块化系统(Jigsaw):


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



  1. JShell:


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



  1. 接口的私有方法:


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



  1. 改进的 try-with-resources:


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



  1. 集合工厂方法:


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



  1. 改进的 Stream API:


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


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


JDK10 新特性


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




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




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




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




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




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




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


JDK11 新特性


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




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




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




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




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




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




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


JDK12 新特性


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




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




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




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




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




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




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


JDK13 新特性


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




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




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




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




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




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




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


JDK14 新特性


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




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




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




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




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




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




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


JDK15 新特性


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




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




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




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




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




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




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


JDK16 新特性


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




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




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




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




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




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




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


JDK17 新特性


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




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




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




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




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




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




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


总结




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




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




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




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




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




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




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




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




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




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


小记


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




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




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




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




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




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




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


更多内容:


image.png


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

卸下if-else 侠的皮衣!

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

🤭当我是if-else侠的时候


😶怕出错


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


😑难调试


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


🤨交接容易挨打


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


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



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



开放封闭原则(ASD)



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



里氏替换原则(LSP)



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



依赖倒置原则(DIP)



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



迪米特原则(LOD)



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



接口隔离原则(ISP)



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



在学习下设计模式


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

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

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

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


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


策略模式


举个例子



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

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

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

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

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


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

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



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

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

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

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



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



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

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

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


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

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


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

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


结尾


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

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

收起阅读 »

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

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

我是一名网瘾少年...

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

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

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

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

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

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

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

收起阅读 »

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

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

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


leakcanary


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


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


答案是使用 activity-alias


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


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

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

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


activity-alias


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


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


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


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

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

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


activity-alias


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


三、activity-alias 还能做什么?


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

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

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


open with


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

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

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

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

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


My Browser


四、总结


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


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

五年后端外包仔的回顾

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

前言


背景:普通二本


专业:软件工程


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


实习


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


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


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


跳槽


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


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

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


精神内耗


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

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

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


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


写在最后


定几个小目标吧:



  1. 每周花时间看看书

  2. 搭建一个自己的项目

  3. 抽空刷leetcode

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


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


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

小米:阳了,被裁了

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

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


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


裁员


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


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



小米为什么要裁员


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



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

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

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

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


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


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


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


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





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




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


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


文章的最后


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


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



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

入坑两个月自研创业公司

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

一、拿offer


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


二、入职工作


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


三、人言可畏


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


四、为什么离开


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


五、收获


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


六、未来规划


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


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


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


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

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

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

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


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


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


Developer Relation/Advocate


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


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


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


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


Technical Recruiter


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


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


Technical Writer


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


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


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


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


Technical Support


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


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


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


结语


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


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


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

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

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

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


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


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


1. 枚举类


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


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


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

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

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

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

2. 枚举属性


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


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

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

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

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

3. 枚举函数


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


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

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

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

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

4. 枚举构造函数


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


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

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

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

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

5. 枚举扩展函数


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


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

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

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

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

结语


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


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


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

【Android】Kotlin 中特别的关键字

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

前言


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


1. data


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

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

2. companion object


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

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

val result = Utils.add(1, 2)

3. lateinit


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

class Person {
lateinit var name: String

fun initName() {
name = "John"
}

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

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

4. by


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

interface Car {
fun drive()
}

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

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

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

5. when


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

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

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

6. object


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

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

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

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

7. reified


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

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

val typeName = getType<Int>()

总结


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


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

2023移动端技术探索

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

1. 行业背景


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


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


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


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


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


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


2.1 21年出调研结果


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


阿里移动端技术全景图


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


阿里移动端发展趋势


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


美团移动端技术全景图


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


京东移动端技术全景图


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


京东移动端未来远景图


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


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


2.2 22年产出


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


2.2.1 阿里


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


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


2.2.2 美团


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


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


2.2.3 百度


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


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


2.2.4 分析


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



  1. 跨端/低代码

  2. 性能优化

  3. 自动化测试

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

  5. AI


3. 移动端主要方向分析


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


3.1 业务开发


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


3.2 跨端/低代码


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


3.3 性能优化


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


3.4 架构方向


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


3.5 开发平台建设


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


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


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


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


3.7 XR


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


3.8 音视频


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



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

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

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

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

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

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


3.9 AI


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




  1. 语音方向



    1. 前端信号处理

    2. 唤醒

    3. 语音识别

    4. 声纹

    5. TTS

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

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




  2. 图像方向



    1. 检测

    2. 识别

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




  3. 自然语言处理



    1. 智能问答

    2. 意图识别

    3. 文档纠错




  4. 风控




  5. 推荐




  6. 用户画像




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




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



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

  2. 训练场


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


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


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



  1. 唤醒

  2. 图像检测

  3. 语音编解码


可以放在端上应用的:



  1. ASR

  2. TTS

  3. 图像标签


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


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


4. 总结


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


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

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

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

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

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

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

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

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

本文参与环信十周年活动

收起阅读 »

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

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


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

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

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

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

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

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

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

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

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

收起阅读 »

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

自己总结了一句话。

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

                                                              ---- 致自己

收起阅读 »

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

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

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


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


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


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


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

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


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

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

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

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

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

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

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

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

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

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


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

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

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


本文参与环信十周年活动

收起阅读 »

Android深思如何防止快速点击

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

前言


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


1. AOP


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


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

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


2. kotlin


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


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


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


3. 流


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


4. 通过拦截


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

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

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


(1)拦截事件


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


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

public class FastClickHelper {

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

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

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

}

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

FastClickHelper.setOnClickListener(view, this);

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

public class FastClickHelper {

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

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

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

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

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

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

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

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

}

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


在外部直接调用

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

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


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


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


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


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


(2)拦截方法


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


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


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

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

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

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

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


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


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


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

Android-Deeplink跳转失败问题修复

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

Android Deeplink实现


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

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


检查客户端deeplink配置


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

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

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

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

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

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

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


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


分析浏览器对deeplink处理


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

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

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

Android Intents with Chrome

里面提到



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




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



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


018ff33c02e047c59196534a3a28ef8b_e15f0d6e314113b2260119e917400191.jpg


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


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


测试


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


UC浏览器


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


QQ浏览器


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


360浏览器


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


猎豹浏览器


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


一加系统默认浏览器


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


搜狗浏览器


没有提醒,直接跳转到app


chrome


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


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


结语


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



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

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

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

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

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


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



  • 最短路径处理原则


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



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



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

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



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



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


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

一点点编译优化

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

正文


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


FileWalker 剪枝 + 多线程优化


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


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

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


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































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

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


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


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

        Executors.newCachedThreadPool().invokeAll(callableList)

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


修改前:


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


修改后:


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


skip 一些非必须task


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


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


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


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

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

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


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



worker_api



二进制产物发布


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


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

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


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

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

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


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


测试结果大概如下:























目录耗时情况
global80min
common50min
app10min

结尾


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


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


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

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

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

ZYGOTE


前言


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



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

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

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


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


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


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


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


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




1.C++还是Java


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


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


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


2.Native层


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


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


Zygote的新手村:app_main.cpp

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

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

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


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


奇迹的诞生地:AndroidRuntime:


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



  1. startVM -- 启动Java虚拟机

  2. startReg -- 注册JNI

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

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

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


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




3.Java层


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


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


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


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

class ZygoteInit{

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

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

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

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

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

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



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

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

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

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




4. 戛然而止


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



  1. 创建虚拟机

  2. 注册JNI

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

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

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

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

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




5. 多说一句


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

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


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


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




6. Zygote功能图


zygote.png


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

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

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

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


自我叙述


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


入行回顾



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



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




武术梦




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


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


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




歌唱梦




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


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


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


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


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


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


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


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


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


但是我很天真


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


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


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


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


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




玩电脑




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



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


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




学it




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


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


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


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


直到现在我还在it行业里


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


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


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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


后记


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


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


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


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


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


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


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


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


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


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

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

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

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


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


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


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


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


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


一、“996”带来了什么?



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


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


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


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


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


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


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


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



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


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


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


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


三、液态职场是什么?



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


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


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


2)四天半工作制


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


3)“上4休3”的工作制


微软日本公司,也早在2019年8月曾宣布,公司开始试运行每周“上4休3”的工作制度,即每周五、六、日休息3天,周五所有办公室全部关闭。


不管是8小时工作制还是上4休3”,其实本质上都一样:都是为了迎合当下的现状,打破固有传统的工作模式,寻找更加多元化的新型职场状态,让员工能够充分休息,提升效率和创造力,也能节省企业开支,最终双方获益。


这世界变化太快了,上一秒还在“996”中疯狂内卷,下一秒就已经有先行者去探索更适合的工作节奏。液态职场时代已经到来,你准备好了吗?


四、提高工作效率,大胆对996说不!


作为打工人,不管是996还是8小时工作制,虽然都不是我们能决定的,但我们可以用法律来维护自己的权利,学会说“不”。利用好这8小时,发挥出自己的价值,提高自身的创新能力和效率,是为了更有底气的说“不”!这样才能保证企业与员工之间形成一个正向循环。如何利用好8小时?给大家分享几个提高工作效率的小技巧:




  1. 保持桌面整洁,减少其他事物对工作专注度的干扰;




  2. 巧用看板,可视化工作任务,便于进行任务管理;




  3. 排列优先级,按照任务的重要紧急程度,尽量避免并行多个任务;




  4. 随时记录工作中的创意和灵感




  5. 将重复、机械的工作自动化,解放双手;




  6. 定期复盘:不断改进与优化;




  7. 培养闭环思维:凡事有交代,件件有着落,事事有回音。




工作本应是我们热爱的样子。当我们还沉浸在无休止的工作与忙碌中,被疲惫、彷徨等负面情绪包围,开始精神内耗时,是时候明确拒绝996了!


作者:禅道程序猿
链接:https://juejin.cn/post/7217616698798096444
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

副业奇谈

楔子 在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷...
继续阅读 »

楔子


在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷偷溜去学校附近大厦最高层天台,再爬上去水塔仰望星空,俯视我们的高中,看着每个教室里面一个个奋发图强的同学,我丝毫没有半点做错事的羞愧,眼里只有天上的繁星,地下的灯光,还有旁边的那个他。


小聚


“小饿,我们95后的都已经老了,现在社会都是年轻人00后的天下,比学历能力,985、211一抓一大把,比耐力,我们身体大不如前,精力强壮的年轻人比比皆是...”


“难道你不行了?”


“你别打岔,你这一行不是也有一个35岁的梗吗,你这个前端开发岗位我了解过,是需要不断学习的,技术迭代如此之快,你跟的上吗?”


我默默的摇了摇头,诚然,我是跟不上的,vue2我都没学完vue3就已经出来了,不过我相信我还是极少数。因为我安于现状太久了,拿着不上不下的薪资,没有房贷车贷育儿的压力,不像以前住在城中村每天晚上睡觉听着管道排泄物的声音,没有压力,就没有动力,我就是这么一个充满惰性的人。


小润跟我是高中同学,那时我们的关系不错,但是毕业后各自去往自己的大学,有自己的生活,便没怎么联系了,这次出来也是近三年第一次小聚。他在一个比较老牌的做文具,做设备的大厂工作,主要内容是去一些大型物业竞标,为了竞争得到那个“标”,付出的也不少,陪酒送礼一样不落,但就算得到那个“标”,公司的绩效奖励分配制度却让小润很不满,所以他不禁感慨,“我们每个月累死累活得到的薪资,除去日常花销,本来就已经所剩不多,而且社会上还存在一种叫通货膨胀的东西,想想我们年龄越来越大,面临的职场危机,手上的筹码到底能不能支撑我们维持当前消费水平,过上自己想要的生活,这是一个比较大的问题。”我听得津津有味,虽然心里认为他的说法有点过度焦虑,但有这么一个意识,总是好的,小润看到我向他投向肯定的目光,便继续说道,“这几年我都在看书,其中看到一个企业家有一句创业名言————空手套白狼”。


空手套白狼


小润看着我一脸的疑惑,嘴角微微一笑,一脸正经的告诉我,“空手套白狼虽然百度翻译是个贬义词,但是在创业翻译过来就是用最低的成本,创造最大的价值。我想要做一些0成本,价值高,只需要付出时间的生意”。


“那么请问哪里有那么大的馅饼?”据我所知,现在谈起普通人做副业,想要0成本,要不就是什么做信息差买卖,或者视频搬运,网上一搜一大把,现在根本不是能真正获利的渠道了。当然,也可能有很多人的确做着0成本生意,闷声发大财


微信图片_20230307134118.jpg


小润从煲里夹了一块榴莲肉,放入嘴中品尝了几番后吞入腹中,真诚的向我道来,“之前你有跟我聊过你做的副业,上面的功能我看了,感觉你比较厉害,对小程序开发这一块也是比较熟悉。你有没有看过小区的停车场,白天的时候很多车位都是空闲的,极大部分都是车主开车上班,那么车子不就空闲起来了?我们可以做一个平台,让车主在平台上面登记,只要车位空闲,可以告诉平台某一个时间段空闲,让平台的其他需要在附近停车的用户看到,用户微信支付停留相对应的时间,这样不仅解决了车位紧张的问题,车位车主也能利用闲置的车位赚到一笔钱,平台也能进行抽成。”


我一听,陷入了沉思,感觉好像很有道理的样子,但又觉得哪里不对,“这种做法当然是不可能的,物业停车场大都是一个车牌对应一个停车位,不可能给别人钻这种空子。”


“那你说个der啊”


微信图片_20230307134254.jpg


“刚刚只是我在生活中发现的一些奇思妙想,就是利用闲置这个属性,接下来才是我要说的重点。你平时看街边上停着的电车多吗?”我点了点头,电车在广州这所大城市,那肯定是多的。突然,小润用筷子翻了翻鸡煲中的食物,一脸愤然的对着我说“我擦,那些肥牛都被你吃完了?”我又用筷子探寻了一下,的确,肥牛还真被我吃完了,软嫩的肥牛搭配着由榴莲和鸡煲化学反应产生的汤底,让我感觉到味蕾在跳动,入口即化,难以言喻,自然而然就多吃了几片,我尴尬又不失礼貌的问他,“要不多点一份?”


他笑了笑,摆了摆手,继续说道,“我的想法是将空闲的电车利用起来,做一个平台,平台的载体是小程序,像膜拜小程序一样,用户能找到附近的单车,而我们则是电车,但是我们不需要成本,因为在平台中,电车的信息是由车主自己主动上传上来的,所以就有两个群体,一个是车主,一个是需要用电车的用户。车主能在电车空闲的时间将电车上传到我们的平台,通过出租自己的电车进行赚钱,当出租的次数多了,不仅能回本,到时候或许还能赚点小钱。而普通用户想用电车的时候,根据小程序提供的定位,找到离他最近的那台电车,进行微信支付就能骑走,按照骑行时间进行收费,收费标准由电车车主自己提供。而我们平台的收入,则是对每笔订单进行抽成”。


我一听,又陷入了沉思,又感觉好像很有道理的样子,但又觉得哪里不对,咦,我为什么要说又?


QA



用户场景有哪些,用户需求多吗?



多,平时使用电车都是上班族居多,那上班族使用完电车后电车就闲置了,可以进行出租赚点奶茶钱,何乐而不为?况且平时下班我想去别的地方玩一下,也可以租一台电车去逛一逛,就再也不需要每个人都要买一台电车了。确实,之前去湛江游玩,也有电车提供出租,骑着电车到处逛逛吃吃,真的十分快乐,不过电车是由公司统一提供。



普通用户怎么开启这些电车呢,电车五花八门,难道要让车主统一购买我们提供的电锁进行控制?



目标电车当前只试行小牛和九号电车,用户需要开启电车的时候,在小程序可以找到电车车主联系方式,通过电话联系让他用电车钥匙开启电车,同时在小程序按下开启按钮告诉平台和用户已经开启,开始计费。用户骑行完电车后,用户致电车主进行结算并关闭电车。



客户借车后,将车的某些零件换改,偷窃,损坏,如何处理?例如将电瓶车电池换成低端电池,也能用,,但车主不知道?



这的确是个问题,我也在思考是否有必要弄押金,但是电车的押金弄小了没啥用,弄大了也不合适,没人想进行支付,所以如何平衡这个问题,是我们这个项目后续所要思考的。



用户把电车开到离起始点十万八千里,这样车主怎么找回自己的电车?



好问题,我也有想过,车主在上传电车到平台的时候,可以设置自己的使用类型,可以规定使用用户骑行归还到原位置,也可以不规定,全由车主自由设定



听起来好像真的可以落地,但是用户附近可用的电车如果多起来,在地图上展示密密麻麻,这个需要点技术,我得研究研究



我们初期可能不需要那么复杂,只需要展示一个列表,可以让用户进行筛选,用户能看到每台电车的外观,点击电车详情,就能知道用户与电车的相对位置,不需要在同一个页面展示那么多的标记(如此甚好)

// 小程序在地图上显示用户与标记方法

// js
const markers = [
{
id: 1,
// 标记的大小
width: '40px',
height: '40px',
// 标记的经纬度
longitude,
latitude,
// 标记的icon图标
iconPath
}
]
this.setData({ markers })

// wxml
// center.longitude center.latitude 为中心经纬度
<map class='map' id='map' longitude='{{center.longitude}}' latitude='{{center.latitude}}' markers="{{markers}}" scale='16'></map>


政治问题...



******<-内容加密了


我们聊了很多细节,包括首页如何设计,一键控制电车上线下线,越聊越兴奋,感觉真的可以落地,说到尽情之处,还说日后被大厂收购,实现财富自由指日可待,因为我们都知道,一个产品成熟了,稍微露出苗头,就会被人借鉴。当天晚上我回到家,就把整个大纲梳理了出来,并发给小润看。


dianche.png


但同时我们也发现问题,如果用户在骑行的途中,被车主通过车钥匙远程停车会发生什么事情,之前我们一致认为电车平台会有相对应的API提供,不仅可以获取电车信息(车辆电池,型号,外观元素等),也能有启动车辆和关停车辆的接口,但浏览了两个电车平台的官网,发现平台并没有这种东西,我们的思路一下子遇到卡壳,而且押金问题也是一个重点,热情一下子就冷却了下来,这场看似热血沸腾的副业计划就此搁置了下来。


对于做副业,我个人是非常感兴趣的,低成本的副业能赚钱是根本条件,更主要能拓展人的视野,之前我第一个副业,进行的比较顺利,但前提是市场已经有先驱,可以有模板进行复刻,而这一次纯属天马行空,没有前车之鉴,需要考虑到很多细节,如果有一些致命因素导致项目行不通,那可能这个项目就真的凉了。其实也很合理,世界上人才千千万,一个脑暴出来能赚钱的项目,为什么市场没有落地,或许不是因为没有人能想出来,更大因素是有人想出来了,但是此路不通。


省流


不亏,那顿鸡煲很香,而且是小润掏的钱


作者:很饿的男朋友
链接:https://juejin.cn/post/7207634883988635705
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信图片防撤回

了解需求 实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。 当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。 解决方案 思路 由于...
继续阅读 »

了解需求


实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。


当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。


解决方案


思路


由于当前微信不支持微信网页版登陆,因此使用itchat的方法不再适用。


后来了解到电脑端微信图片会先存储在本地,撤回后图片再从本地删除,因此只要在撤回前将微信本地图片转移到新文件夹即可。


在此使用Python的watchdog包来监视文件系统事件,例如文件被创建、修改、删除、移动,我们只需监听创建文件事件即可。


安装watchdog包:    pip install watchdog
我的python环境为python3.9版本

实现


1.首先进行文件创建事件监听,在监听事件发生后的事件处理对象为复制微信图片到新文件夹。具体代码如下。


需要注意的是微信在2022.05前,图片存储在images目录下;在2022.05后,图片存储在MsgAttach目录下,并按微信对象分别进行存储。


# 第一步:加载路径,并实时读取JPG信息
import os
import shutil
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

def mycopyfile(srcfile,dst_dir):
if not os.path.isfile(srcfile):
print ("%s not exist!"%(srcfile))
else:
fpath,fname=os.path.split(srcfile) # 分离文件名和路径
if fname.endswith('.jpg') or fname.endswith('.png') or fname.endswith('.dat'):
dst_path = os.path.join(dst_dir, fname)
shutil.copy(srcfile, dst_path) # 复制文件

class MyEventHandler(FileSystemEventHandler):
# 文件移动
# def on_moved(self, event):
# print("文件移动触发")
# print(event)


def on_created(self, event):
# print("文件创建触发")
print(event)
mycopyfile(event.src_path, dst_dir)


# def on_deleted(self, event):
# print("文件删除触发")
# print(event)
#
# def on_modified(self, event):
# print("文件编辑触发")
# print(event)

if __name__ == '__main__':

dst_dir = r"E:\03微信防撤回\weixin" #TODO:修改为自己的保存文件目录
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)

observer = Observer() # 创建观察者对象
file_handler = MyEventHandler() # 创建事件处理对象
listen_dir = r"C:\Users\hc\Documents\WeChat" #TODO:修改为自己的监听目录
observer.schedule(file_handler, listen_dir, True) # 向观察者对象绑定事件和目录
observer.start() # 启动
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()

2.由于微信保存文件以.dat格式保存,因此需要对微信文件格式进行解码,具体解码代码如下。


# weixin_Image.dat 破解
# JPG 16进制 FF D8 FF
# PNG 16进制 89 50 4e 47
# GIF 16进制 47 49 46 38
# 微信.bat 16进制 a1 86----->jpg ab 8c----jpg dd 04 --->png
# 自动计算异或 值
import os

into_path = r'E:\03微信防撤回\weixin' # 微信image文件路径
out_path = r'E:\03微信防撤回\image'

def main(into_path, out_path):

dat_list = Dat_files(into_path) # 把路径文件夹下的dat文件以列表呈现
lens = len(dat_list)
if lens == 0:
print('没有dat文件')
exit()

num = 0
for dat_file in dat_list: # 逐步读取文件
num += 1
temp_path = into_path + '/' + dat_file # 拼接路径:微信图片路径+图片名
dat_file_name = dat_file[:-4] # 截取字符串 去掉.dat
imageDecode(temp_path, dat_file_name, out_path) # 转码函数
value = int((num / lens) * 100) # 显示进度
print('正在处理--->{}%'.format(value))


def Dat_files(file_dir):
"""
:param file_dir: 寻找文件夹下的dat文件
:return: 返回文件夹下dat文件的列表
"""

dat = []
for files in os.listdir(file_dir):
if os.path.splitext(files)[1] == '.dat':
dat.append(files)
return dat

def imageDecode(temp_path, dat_file_name, out_path):
dat_read = open(temp_path, "rb") # 读取.bat 文件
xo, j = Format(temp_path) # 判断图片格式 并计算返回异或值 函数

if j == 1:
mat = '.png'
elif j == 2:
mat = '.gif'
else:
mat = '.jpg'

out = out_path + '/' + dat_file_name + mat # 图片输出路径
png_write = open(out, "wb") # 图片写入
dat_read.seek(0) # 重置文件指针位置

for now in dat_read: # 循环字节
for nowByte in now:
newByte = nowByte ^ xo # 转码计算
png_write.write(bytes([newByte])) # 转码后重新写入


def Format(f):
"""
计算异或值
各图片头部信息
png:89 50 4e 47
gif: 47 49 46 38
jpeg:ff d8 ff
"""

dat_r = open(f, "rb")

try:
a = [(0x89, 0x50, 0x4e), (0x47, 0x49, 0x46), (0xff, 0xd8, 0xff)]
for now in dat_r:
j = 0
for xor in a:
j = j + 1 # 记录是第几个格式 1:png 2:gif 3:jpeg
i = 0
res = []
now2 = now[:3] # 取前三组判断
for nowByte in now2:
res.append(nowByte ^ xor[i])
i += 1
if res[0] == res[1] == res[2]:
return res[0], j
except:
pass
finally:
dat_r.close()


# 运行
if __name__ == '__main__':
main(into_path, out_path)
复制代码
作者:空气猫
来源:juejin.cn/post/7221376169370583101
>
收起阅读 »

前端路由访问权限控制方案

web
本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考! 项目背景及路由初步设计 目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,...
继续阅读 »

本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考!



项目背景及路由初步设计


目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,相信后续还会有更多的第三方企业对接,以及更多的业务场景。


这就需要提前对系统的菜单权限进行规划,由于后期开发的不确定性以及人力资源有限,路由权限控制没有采用跟后端强耦合的方式实现,由前端自行配置并处理。


一开始由于意向企业业务上的强相关性,并没有规划太多的模块(主业务全都写在了src/views/main目录下),也没有对用户行为进行规划分类,路由控制方面也是根据平台标识手动配置的路由表


const xxx = [
'/main/nopage',
'/main/checkFace',
// ...
]

缺点


最近又做了一个B端的项目,发现上面的实现方法并不是很好,原因有以下几点:




  1. 对接平台多的话,就会出现一堆路由配置,不优雅、不美观




  2. 业务上的不一致性带来扩展的不灵活




  3. 暂时没有想起来 : )




经过一番考量,我决定这样做(其实也是常见的方法)


改进方案


首先业务实现上需要我新建一个文件目录(src/views/xxx,不能再往main目录下放了),在里面写路由组件。


期间我想过以对接的平台标识创建路由组件目录,进行业务上的隔离,没有做出实际尝试就被我舍弃了,原因是:以平台标识作为业务的根目录,跟原先的做法本质上是一致的,只是改进,相当于是补丁,而我要做的是寻找另一种解决办法。


根据Linux系统一切皆文件的思想,类似的,我还是采用了老套的办法,给每一个路由菜单赋予一个访问权限的配置。


这样做,后面维护起来也简单(有了平台标识和用户行为的划分)


{
path: "/test",
name: "Test",
meta: {
belong: ["xxx", "xxx"] // 所属平台信息,操作行为信息...
},
component: () => import("@/views/test")
},

后端同事配合规划用户平台和行为,在用户访问的时候,后端返回用户信息的同时,返回平台标识和行为标识。


同样的,在全局路由钩子里验证访问权限。



router.beforeEach((to, from, next) => {
try {
const { belong = [] } = to.meta
const authInfo = ["platform", "action"]
if (accessTokenStr) {
// 已登录, 做点什么
// belong <--> authInfo
// arrayInclude(belong, authInfo)
} else {
next()
}
} catch (err) {
console.log(err);
}
})

/**
* 判断数组元素的包含关系,不要求顺序一致
* 数组中不存在重复元素
* 用于验证路由权限
* arrA包含arrB,返回true, 否则返回false
*/

export const arrayInclude = (arrA, arrB) => {
let flag = true
for (let i = 0; i < arrB.length; i++) {
if (!arrA.includes(arrB[i])) {
flag = false
break
}
}
return flag
}


👉👉以上方案写于2021-11-06





维护总结


2023-04-03


近期业务扩展,发现上面的菜单权限控制有点不合理


这种配置不直观,有点混乱!!!


还是采用json的方式分配路由, 比如:


const menus = {
[platformId]: [
"/a"
"/b"
],
[platformId]: [
"/a"
"/b"
],
}

这样可以更加直观的显示出来某个业务包含哪些菜单,而不是像之前那样把菜单的权限配置在路由上!


总结: 路由设计要中电考虑可读性、易维护性




我是 甜点cc,个人网站(国外站点): blog.i-xiao.space/


公众号

作者:甜点cc
来源:juejin.cn/post/7239173692228255802
:【看见另一种可能】

收起阅读 »

一文搞清楚Node.js的本质

web
学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。 Node.js 是什么? Node.js 是一个基于 V8 引擎 的 JS 运行时,它由 Ryan Dahl 在 ...
继续阅读 »

学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。


Node.js 是什么?


Node.js 是一个基于 V8 引擎JS 运行时,它由 Ryan Dahl 在 2009 年创建。


这里有两个关键词,一是 JS 引擎,二是 JS 的运行时


那什么叫 JS 引擎呢?


image.png


JS 引擎就是把一些 JS 的代码进行解析执行,最后得到一个结果。


比如,上图的左边是用 JS 的语法定义一个变量 a 等于 1,b 等于 1,然后把 a 加 b 的值赋值给新的变量 c,接着把这个字符串传入 JS 引擎里,JS 引擎就会进行解析执行,执行完之后就可以得到对应的结果。


那么 JS 运行时又是什么呢?它和 JS 本身有什么区别 ?


要搞清楚上面的问题,可以看下面这张图:


image.png


从下往上看:




  1. 最下面一层是脚本语言规范ECMAScript,也就是常说的ES5、ES6语法。




  2. 往上一层就是对于该规范的实现了,如JavaScriptJScript等都属于对 ECMAScript语言规范的实现。




  3. 再往上一层就是执行引擎JavaScript 常见的引擎有 V8QuickJS等,用来解释执行代码。




  4. 最上面就是运行时环境了,比如基于 V8 封装的运行时环境有 ChromiumNode.jsDeno 等等。




可以看到,JavaScript 在第二层,Node.js 则在第四层,两个根本不是一个东西。


所以,Node.js 并不是语言,而是一个 JavaScript 运行时环境,它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。


JS 作为一门语言,有独立的语法规范,提供一些内置对象和 API(如数组、对象、函数等)。但和其他语言(C、C++等)不一样的是,JS 不提供网络、文件、进程等功能,这些额外的功能是由运行时环境实现和提供的,比如浏览器或 Node.js


所以,JS 运行时可以理解为 JS 本身 + 一些拓展的能力所组成的一个运行环境,如下面这张图:


image.png


可以看到,这些运行时都不同程度地拓展了 JS 本身的功能。JS 运行时封装底层复杂的逻辑,对上层暴露 JS API,开发者只需要了解 JS 的语法,就可以使用这些 JS 运行时做很多 JS 本身无法做到的事情。


Node.js 的组成


搞清楚了什么是Node.js后,再来看看 Node.js 的组成。


Node.js 主要是由 V8Libuv 和一些第三方库组成的。


V8引擎


V8 是一个 JS 引擎,它不仅实现了 JS 解析和执行,还支持自定义拓展。


这有什么用处呢?


比如说,在下面这张图中我们直接使用了 A 函数,但 JS 本身并没有提供 A 这个函数。这个时候,我们给 V8 引擎提供的 API 里注入一个全局变量 A ,就可以直接在 JS 中使用这个 A 函数了。正是因为 V8 支持这个自定义的拓展,才有了 Node.js 等 JS 运行时


image.png


Libuv


Libuv 是一个跨平台的异步 IO 库,它主要是封装各个操作系统的一些 API,提供网络还有文件进程这些功能


我们知道在 JS 里面是没有网络文件这些功能的,前端是由浏览器提供,而 Node.js 里则是由 Libuv 提供


image.png




  1. 左侧部分是 JS 本身的功能,也就是 V8 实现的功能。




  2. 中间部分是一些C++ 胶水代码。




  3. 右侧部分是 Libuv 的代码。




V8Libuv 通过第二部分的胶水代码粘合在一起,最后就形成了整一个 Node.js


因此,在 Node.js 里面不仅可以使用 JS 本身给我们提供的一些变量,如数组、函数,还能使用 JS 本身没有提供的 TCP、文件操作和定时器功能


这些扩展出来的能力都是扩展到V8上,然后提供给开发者使用,不过,Node.js 并不是通过全局变量的方式实现扩展的,它是通过模块加载来实现的。


第三方库工具库


有了 V8 引擎和拓展 JS 能力的 Libuv,理论上就可以写一个 JS 运行时了,但是随着 JS 运行时功能的不断增加,Libuv 已经不能满足需求,比如实现加密解密、压缩解压缩。


这时候就需要使用一些经过业界验证的第三方库,比如异步 DNS 解析 c-ares 库、HTTP 解析器 llhttp、HTTP2 解析器 nghttp2、解压压缩库 zlib、加密解密库 openssl 等等。


Node.js 代码组成


了解了 Node.js 的核心组成后,再来简单看一下 Node.js 代码的组成。


image.png


Node.js 代码主要是分为三个部分,分别是 CC++JavaScript


JS


JS 代码就是我们平时使用的那些 JS 模块,像 http 和 fs 这些模块


Node.js 之所以流行,有很大一部分原因在于选择了 JS 语言。


Node.js 内核通过 CC++ 实现了核心的功能,然后通过 JS API 暴露给用户使用,这样用户只需要了解 JS 语法就可以进行开发。相比其他的语言,这个门槛降低了很多。


C++


C++代码主要分为三个部分:




  1. 第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如 netfs 这些模块都会对应一个 C++ 模块,它主要是对底层 Libuv 的一些封装。




  2. 第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现,主要依赖于 V8




  3. 第三部分 C++ 代码是 V8 本身的代码。




C++ 代码中最重要的是了解如何通过 V8 API 把 C、C++ 的能力暴露给 JS 层使用,正如前面讲到的通过拓展一个全局变量 A,然后就可以在 JS层使用 A。


C 语言层


C 语言代码主要是包括 Libuv 和第三方库的代码,它们大多数是纯 C 语言实现的代码。


Libuv 等库提供了一系列 C API,然后在 Node.jsC++ 层对其进行封装使用,最终暴露 JS APIJS 层使用。


总结


文章第一部分介绍了Node.js的本质,它实际上是一个JS运行时,提供了网络、文件、进程等功能,类似于浏览器,提供了JS的运行环境。


第二部分介绍了Node.js的组成,它由V8引擎、Libuv及第三方库构成,Node.js核心功能大都是Libuv提供的,它封装底层各个操作系统的一些 API,因此,Node.js是跨平台的。


第三部分从代码的角度描述了Node.js的组成,包括JavaScriptC++C三部分,中间的C++部分通过对C部分的封装提供给JavaScript部分使用。


作者:小p
来源:juejin.cn/post/7238814783598297144
收起阅读 »