注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

北漂五年,我回家了。后悔吗?

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租...
继续阅读 »

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租屋两点一线。今年我觉得是时候该回家乡了。


1280X1280 (1).JPEG


(在北京大兴机场,天微微亮)


有些工作你一面试就知道是坑


决定回家乡后,我开始更新自己的简历。我想过肯定会被降薪,但是没想到降薪幅度会这么大,成都前端岗位大多都是1w左右,想要双休那就更少了。最开始面试的一些岗位是单休或者大小周,后面考虑了一下最后都放弃了。那时候考虑得很简单,一是我没开始认真找工作,只是海投了几个公司,二是我觉得我找工作这儿时间还比较短,暂时找不到满意的很正常。


辞职后,我的工作还没有着落,于是决定先不找了,出去玩一个月再说。工作了这么久,休息一下不为过吧,于是在短暂休息了一个月后,我又开始认真找工作。


但是,但是没想到成都的就业环境还蛮差的,找工作的第二个月还是没有合适的,当时甚至有点怀疑人生了,难道我做的这个决定是错误的?记得我面试过一家公司,那家公司应该是刚刚成立的,boss上写的员工数是15个,当时我想着,刚成立的公司嘛,工资最开始低点也行,等公司后续发展起来了,升职加薪岂不美滋滋。


面试时,我等了老板快半小时,当时我对这家公司的观感就不太好了。但想着来都来了,总不能浪费走的这一趟。结果,在面试的时候老板开始疯狂diss我的技术不行,会的技能太少,企图用这种话来让我降薪。我是怎么知道他想通过这种方式让我降薪呢,因为最后那老板说“虽然你技术不行,但是我很看好你的学习能力,给你开xxx工资你愿意来吗?”


也是因为这次面试,我在招聘软件上看到那种小公司都不轻易去面试了,简直浪费我时间。


1280X1280.JPEG


(回家路上骑自行车等红绿灯,我的地铁卡被我甩出去了,好险,但是这张地铁卡最后还是掉了,还是在我刚充值完100后,微笑)


终于,找了大概3个月,终于找到一家还算不错的公司,在一家教育行业的公司做前端。双休,工资虽然有打折,但是在我能接受的范围内。


有些人你一见面就知道是正确的


其实我打算回家乡还有一个重要原因是通过大厂相亲角网恋了一个女孩子,她和我是一个家乡的。我们刚认识的时候几乎每天都在煲电话粥,基本上就是陪伴入眠,哈哈哈哈哈。语言的时候她还会唱歌给我听,偏爱、有可能的夜晚......都好好听,声音软绵绵的。认识一个月后,我们回了一趟成都和她面基。一路上很紧张,面基的时候也很害怕自己有哪里做得不好的地方,害怕给她留下不好的印象。我们面基之后一个月左右就在一起啦。有些人真的是你一见面就知道她是正确的那个人,一见面心里有一个声音告诉你“嗯,就是她了!”。万幸,我遇到了。


58895b3fc4db3554881bdbcaa35384f.jpg


1280X1280 (2).JPEG


说一些我们在一起后的甜蜜瞬间吧


打语言电话的时候,听着对方的呼吸声入睡;


走在路上的时候,我牵她的手,她会很顺其自然地与我十指相扣;


在一起吃饭的时候,她会把自己最好吃的一半分享给我;



总结


回到正题,北漂五年。我回家了,后悔吗?不后悔。离开北京快一年了,有时候还是会想念自己还呆在北京的不足10平米的小出租屋里的生活,又恍惚“噢,我已经回四川了啊”。北漂五年,我还是很感激那段时间,让刚毕业的我迅速成长成可以在工作上独当一面的合格的程序员,让我能有拿着不菲的收入,有一定的积蓄,有底气重新选择;感谢大厂相亲角,让我遇见我的女朋友,让我不再是单身狗。


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

在国企做程序员怎么样?

有读者咨询我,在国企做开发怎么样? 当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。 下面分享一位国企程序员的经历,希望能给大家一些参考价值。...
继续阅读 »

有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


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

Android 自定义View 之 饼状进度条

前言   前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图: 正文   效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码...
继续阅读 »

前言


  前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图:


在这里插入图片描述


正文


  效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码不是很全的话,你可以找到文章最后的源码去查看,话不多说,我们开始吧。


一、XML样式


  根据上面的效果图,我们首先来确定XML中的属性样式,在attrs.xml中添加如下代码:

	<!--饼状进度条-->
<declare-styleable name="PieProgressBar">
<!--半径-->
<attr name="radius" />
<!--最大进度-->
<attr name="maxProgress" />
<!--当前进度-->
<attr name="progress" />
<!--进度条进度颜色-->
<attr name="progressbarColor" />
<!--进度条描边宽度-->
<attr name="strokeWidth"/>
<!--进度是否渐变-->
<attr name="gradient" />
<!--渐变颜色数组-->
<attr name="gradientColorArray" />
<!--自定义开始角度 0 ,90,180,270-->
<attr name="customAngle">
<enum name="right" value="0" />
<enum name="bottom" value="90" />
<enum name="left" value="180" />
<enum name="top" value="270" />
</attr>
</declare-styleable>

  这里的公共属性我就抽离了出来,因为之前写过圆环进度条,有一些属性是可以通用的,并且我在饼状进度条中增加了开始的角度,之前是默认是从0°开始,现在可以根据属性设置开始的角度,并且我增加了渐变颜色。


二、构造方法


  现在属性样式已经有了,下一步就是写自定义View的构造方法了,在com.easy.view包下新建一个PieProgressBar 类,里面的代码如下所示:

public class PieProgressBar extends View {

/**
* 半径
*/
private int mRadius;
/**
* 进度条宽度
*/
private int mStrokeWidth;
/**
* 进度条进度颜色
*/
private int mProgressColor;
/**
* 开始角度
*/
private int mStartAngle = 0;

/**
* 当前角度
*/
private float mCurrentAngle = 0;
/**
* 结束角度
*/
private int mEndAngle = 360;
/**
* 最大进度
*/
private float mMaxProgress;
/**
* 当前进度
*/
private float mCurrentProgress;
/**
* 是否渐变
*/
private boolean isGradient;
/**
* 渐变颜色数组
*/
private int[] colorArray;
/**
* 动画的执行时长
*/
private long mDuration = 1000;
/**
* 是否执行动画
*/
private boolean isAnimation = false;

public PieProgressBar(Context context) {
this(context, null);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PieProgressBar);
mRadius = array.getDimensionPixelSize(R.styleable.PieProgressBar_radius, 80);
mStrokeWidth = array.getDimensionPixelSize(R.styleable.PieProgressBar_strokeWidth, 8);
mProgressColor = array.getColor(R.styleable.PieProgressBar_progressbarColor, ContextCompat.getColor(context, R.color.tx_default_color));
mMaxProgress = array.getInt(R.styleable.PieProgressBar_maxProgress, 100);
mCurrentProgress = array.getInt(R.styleable.PieProgressBar_progress, 0);
//是否渐变
isGradient = array.getBoolean(R.styleable.PieProgressBar_gradient, false);
//渐变颜色数组
CharSequence[] textArray = array.getTextArray(R.styleable.PieProgressBar_gradientColorArray);
if (textArray != null) {
colorArray = new int[textArray.length];
for (int i = 0; i < textArray.length; i++) {
colorArray[i] = Color.parseColor((String) textArray[i]);
}
}
mStartAngle = array.getInt(R.styleable.PieProgressBar_customAngle, 0);
array.recycle();
}
}

  这里声明了一些变量,然后写了3个构造方法,在第三个构造方法中进行属性的赋值。


三、测量


  这里测量就比较简单了,和之前的圆环进度条差不多,代码如下所示:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST: //wrap_content
width = mRadius * 2;
break;
case MeasureSpec.EXACTLY: //match_parent
width = MeasureSpec.getSize(widthMeasureSpec);
break;
}
//Set the measured width and height
setMeasuredDimension(width, width);
}

  因为不需要进行子控件处理,所以我们只要一个圆和描边就行了,下面看绘制的方法。


四、绘制


  绘制这里就是绘制描边和进度,绘制的代码如下所示:

    @Override
protected void onDraw(Canvas canvas) {
int centerX = getWidth() / 2;
@SuppressLint("DrawAllocation")
RectF rectF = new RectF(0,0,centerX * 2,centerX * 2);
//绘制描边
drawStroke(canvas, centerX);
//绘制进度
drawProgress(canvas, rectF);
}

  在绘制之前首先要确定中心点,因为我们是一个圆环,实际上也是一个圆,圆的宽高一样,所以中心点的x、y轴的位置就是一样的,然后是确定一个矩形的左上和右下两个位置的坐标点,通过这两个点就能绘制一个矩形,接下来就是绘制进度条背景。


① 绘制描边

    /**
* 绘制描边
*
* @param canvas 画布
* @param centerX 中心点
*/
private void drawStroke(Canvas canvas, int centerX) {
Paint paint = new Paint();
paint.setColor(mProgressColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokeWidth);
paint.setAntiAlias(true);
canvas.drawCircle(centerX, centerX, mRadius - (mStrokeWidth / 2), paint);
}

  这里的要点就是我们需要设置画笔的类型为描边,然后设置描边宽度,这样我们就可以画一个空心圆,就成了描边,然后我们绘制进度。


① 绘制进度

    /**
* 绘制进度条背景
*/
private void drawProgress(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
//画笔的填充样式,Paint.Style.STROKE 描边
paint.setStyle(Paint.Style.FILL);
//抗锯齿
paint.setAntiAlias(true);
//画笔的颜色
paint.setColor(mProgressColor);
//是否设置渐变
if (isGradient && colorArray != null) {
paint.setShader(new RadialGradient(rectF.centerX(), rectF.centerY(), mRadius, colorArray, null, Shader.TileMode.MIRROR));
}
if (!isAnimation) {
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
}
//开始画圆弧
canvas.drawArc(rectF, mStartAngle, mCurrentAngle, true, paint);
}

  因为背景是一个圆环,所以这里的画笔设置就比较注意一些,看一下就会了,这里最重要的是drawArc,用于绘制及角度圆,像下图这样,画了4/1的进度,同时增加是否渐变的设置,这里的开始角度是动态的。


在这里插入图片描述


五、API方法


  还需要提供一些方法在代码中调用,下面是这些方法的代码:

    /**
* 设置角度
* @param angle 角度
*/
public void setCustomAngle(int angle) {
if (angle >= 0 && angle < 90) {
mStartAngle = 0;
} else if (angle >= 90 && angle < 180) {
mStartAngle = 90;
} else if (angle >= 180 && angle < 270) {
mStartAngle = 180;
} else if (angle >= 270 && angle < 360) {
mStartAngle = 270;
} else if (angle >= 360) {
mStartAngle = 0;
}
invalidate();
}

/**
* 设置是否渐变
*/
public void setGradient(boolean gradient) {
isGradient = gradient;
invalidate();
}

/**
* 设置渐变的颜色
*/
public void setColorArray(int[] colorArr) {
if (colorArr == null) return;
colorArray = colorArr;
}

/**
* 设置当前进度
*/
public void setProgress(float progress) {
if (progress < 0) {
throw new IllegalArgumentException("Progress value can not be less than 0");
}
if (progress > mMaxProgress) {
progress = mMaxProgress;
}
mCurrentProgress = progress;
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
setAnimator(mStartAngle, mCurrentAngle);
}

/**
* 设置动画
*
* @param start 开始位置
* @param target 结束位置
*/
private void setAnimator(float start, float target) {
isAnimation = true;
ValueAnimator animator = ValueAnimator.ofFloat(start, target);
animator.setDuration(mDuration);
animator.setTarget(mCurrentAngle);
//动画更新监听
animator.addUpdateListener(valueAnimator -> {
mCurrentAngle = (float) valueAnimator.getAnimatedValue();
invalidate();
});
animator.start();
}

  那么到此为止这个自定义View就完成了,下面我们可以在PieProgressBarActivity中使用了。


六、使用


   关于使用,我在写这个文章的时候这个自定义View已经加入到仓库中了,可以通过引入依赖的方式,例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now

dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.4'
}

   或者你在自己的项目中完成了刚才上述的所有步骤,那么你就不用引入依赖了,直接调用就好了,不过要注意更改对应的包名,否则会爆红的。


  先修改activity_pie_progress_bar.xml的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".used.PieProgressBarActivity">

<com.easy.view.PieProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:customAngle="right"
app:gradient="false"
app:gradientColorArray="@array/color"
app:maxProgress="100"
app:progress="5"
app:progressbarColor="@color/green"
app:radius="80dp" />

<CheckBox
android:id="@+id/cb_gradient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="是否渐变" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始角度:"
android:textColor="@color/black" />

<RadioGroup
android:id="@+id/rg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<RadioButton
android:id="@+id/rb_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="0%" />

<RadioButton
android:id="@+id/rb_90"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90%" />

<RadioButton
android:id="@+id/rb_180"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180%" />

<RadioButton
android:id="@+id/rb_270"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270%" />
</RadioGroup>
</LinearLayout>


<Button
android:id="@+id/btn_set_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="随机设置进度" />

<Button
android:id="@+id/btn_set_progress_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置0%进度" />

<Button
android:id="@+id/btn_set_progress_100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置100%进度" />
</LinearLayout>

在strings.xml中增加渐变色,代码如下:

    <string-array name="color">
<item>#00FFF7</item>
<item>#FFDD00</item>
<item>#FF0000</item>
</string-array>

首先要注意看是否能够预览,我这里是可以预览的,如下图所示:


在这里插入图片描述


PieProgressBarActivity中使用,如下所示:

public class PieProgressBarActivity extends EasyActivity<ActivityPieProgressBarBinding> {

@SuppressLint("NonConstantResourceId")
@Override
protected void onCreate() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//是否渐变
binding.cbGradient.setOnCheckedChangeListener((buttonView, isChecked) -> {
binding.cbGradient.setText(isChecked ? "渐变" : "不渐变");
binding.progress.setGradient(isChecked);
});
//开始角度
binding.rg.setOnCheckedChangeListener((group, checkedId) -> {
int angle = 0;
switch (checkedId) {
case R.id.rb_0:
angle = 0;
break;
case R.id.rb_90:
angle = 90;
break;
case R.id.rb_180:
angle = 180;
break;
case R.id.rb_270:
angle = 270;
break;
}
binding.progress.setCustomAngle(angle);
});
//设置随机进度值
binding.btnSetProgress.setOnClickListener(v -> {
int progress = Math.abs(new Random().nextInt() % 100);
Toast.makeText(this, "" + progress, Toast.LENGTH_SHORT).show();
binding.progress.setProgress(progress);
});
//设置0%进度值
binding.btnSetProgress0.setOnClickListener(v -> binding.progress.setProgress(0));
//设置100%进度值
binding.btnSetProgress100.setOnClickListener(v -> binding.progress.setProgress(100));
}
}

运行效果如下图所示:


在这里插入图片描述


七、源码


如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~


源码地址:EasyView


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

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:

{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"route2":["prefetchKey2"],
"route3":["prefetchKey3","prefetchKey4"]
},
"prefetcher":{
"prefetchKey":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.itemId",
"firstTime":"true"
},
"headers": {

},
"prefetchImgInResponse": [
{
"imgUrl":"$data.imgData.img",
"imgWidth":"$data.imgData.imgWidth",
"imgHeight":150
}
]
}
},
"prefetchKey2":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name2",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.productId",
"firstTime":"false"
},
"headers": {

}
},
"prefetchKey3":{
"prefetchType":"image",
"prefetchInfo":{
"imgUrl":"$route.imgUrl",
"imgWidth":"$route.imgWidth",
"imgHeight": 150
}
},
"prefetchKey4":{
"prefetchInfo":{}
}
}
}


规则解读




















































参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:

class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


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

Android:自定义View实现签名带笔锋效果

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)、抬起(ACTION_UP)、移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、...
继续阅读 »

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)抬起(ACTION_UP)移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、取消、清除、画笔的粗细,也就是对收集的点集合和线集合的增删操作以及画笔颜色宽的的更改。这些功能都在 实现一个自定义有限制区域的图例(角度自识别)涂鸦工具类(上) 中介绍过。


但就在前不久遇到一个需求是要求手签笔能够和咱们使用钢笔签名类似的效果,当然这个功能目前是有一些公司有成熟的SDK的,但我们的需求是要不借助SDK,自己实现笔锋效果。那么,如何使画笔带笔锋呢?废话不多说,先上效果图:


image.png


要实现笔锋效果我们需要考虑几个因素:笔速笔宽按压力度(针对手写笔)。因为在onTouchEvent回调的次数是不变的,一旦笔速变快两点之间距离就被拉长。此时的笔宽不能保持在上一笔的宽度,需要我们通过计算插入新的点,同时计算出对应点的宽度。同理当我们笔速慢的时候,需要通过计算删除信息相近的点。要想笔锋自然,当然贝塞尔曲线是必不可少的。


这里我们暂时没有将笔的按压值作为笔宽的计算,仅仅通过笔速来计算笔宽。

/**
* 计算新的宽度信息
*/
public double calcNewWidth(double curVel, double lastVel,double factor) {
double calVel = curVel * 0.6 + lastVel * (1 - 0.6);
double vfac = Math.log(factor * 2.0f) * (-calVel);
double calWidth = mBaseWidth * Math.exp(vfac);
return calWidth;
}

/**
* 获取点信息
*/
public ControllerPoint getPoint(double t) {
float x = (float) getX(t);
float y = (float) getY(t);
float w = (float) getW(t);
ControllerPoint point = new ControllerPoint();
point.set(x, y, w);
return point;
}

/**
* 三阶曲线的控制点
*/
private double getValue(double p0, double p1, double p2, double t) {
double a = p2 - 2 * p1 + p0;
double b = 2 * (p1 - p0);
double c = p0;
return a * t * t + b * t + c;
}

最后也是最关键的地方,不再使用drawLine方式画线,而是通过drawOval方式画椭圆。通过前后两点计算出椭圆的四个点,通过笔宽计算出绘制椭圆的个数并加入椭圆集。最后在onDraw方法中绘制。

/**
* 两点之间将视图收集的点转为椭圆矩阵 实现笔锋效果
*/
public static ArrayList<SvgPointBean> twoPointsTransRectF(double x0, double y0, double w0, double x1, double y1, double w1, float paintWidth, int color) {

ArrayList<SvgPointBean> list = new ArrayList<>();
//求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园
double curDis = Math.hypot(x0 - x1, y0 - y1);
int steps;
//绘制的笔的宽度是多少,绘制多少个椭圆
if (paintWidth < 6) {
steps = 1 + (int) (curDis / 2);
} else if (paintWidth > 60) {
steps = 1 + (int) (curDis / 4);
} else {
steps = 1 + (int) (curDis / 3);
}
double deltaX = (x1 - x0) / steps;
double deltaY = (y1 - y0) / steps;
double deltaW = (w1 - w0) / steps;
double x = x0;
double y = y0;
double w = w0;

for (int i = 0; i < steps; i++) {
RectF oval = new RectF();
float top = (float) (y - w / 2.0f);
float left = (float) (x - w / 4.0f);
float right = (float) (x + w / 4.0f);
float bottom = (float) (y + w / 2.0f);
oval.set(left, top, right, bottom);
//收集椭圆矩阵信息
list.add(new SvgPointBean(oval, color));
x += deltaX;
y += deltaY;
w += deltaW;
}

return list;
}

至此一个简单的带笔锋的手写签名就实现了。 最后附上参考链接Github.


我是一个喜爱Jay、Vae的安卓开发者,喜欢结交五湖四海的兄弟姐妹,欢迎大家到沸点来点歌!


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

有多少人忘记了gb2312

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。 本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果 -有点惊讶看似url...
继续阅读 »

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣



  1. 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果


image.png


image.png
-有点惊讶看似url编码实则url编码只是这,滋滋滋...
VeryCapture_20230227174156.gif


有点东西,开始抓包,断点,追踪的逆向之路
VeryCapture_20230227170913.gif
2. 发现是ajax加载(不简单呀纯纯的吊胃口)先来一波关键字索引(keyword)等一系列基操轻而易举的找到了他
VeryCapture_20230227171325.gif


从此开始走向了一条不归路,经过一上午的时间啥也没追到,午休之后继续战斗,经过了一两个半小时+三支长白山牌香烟的努力终于


VeryCapture_20230227173627.gif
VeryCapture_20230227173457.gif


VeryCapture_20230227171715.gif

cihui = '哈哈哈'
js = open("./RSAAA.js", "r", encoding="gbk", errors='ignore')
line = js.readline()
htmlstr = ''
while line:
htmlstr = htmlstr + line
line = js.readline()
ctx = execjs.compile(htmlstr)
result = ctx.call('invokeServer', cihui)
print(result)
const jsdom = require("jsdom");
const {JSDOM} = jsdom;
const dom = new JSDOM('<head>\n' +
' <base href="//search.dangdang.com/Standard/Search/Extend/hosts/">\n' +
'<link rel="dns-prefetch" href="//search.dangdang.com">\n' +
'<link rel="dns-prefetch" href="//img4.ddimg.cn">\n' +
'<title>王子-当当网</title>\n' +
'<meta http-equiv="Content-Type" content="text/html; charset=GB2312">\n' +
'<meta name="description" content="当当网在线销售王子等商品,并为您购买王子等商品提供品牌、价格、图片、评论、促销等选购信息">\n' +
'<meta name="keywords" content="王子">\n' +
'<meta name="ddclick_ab" content="ver:429">\n' +
'<meta name="ddclick_search" content="key:王子|cat:|session_id:0b69f35cb6b9ca3e7dee9e1e9855ff7d|ab_ver:G|qinfo:119800_1_60|pinfo:_1_60">\n' +
'<link rel="canonical" href="//search.dangdang.com/?key=%CD%F5%D7%D3\&amp;act=input">\n' +
' <link rel="stylesheet" type="text/css" href="css/theme_1.css">\n' +
' <!--<link rel="Stylesheet" type="text/css" href="css/model/home.css" />-->\n' +
' <link rel="stylesheet" type="text/css" href="css/model/search_pub.css?20211117"> \n' +
'<style>.shop_button {height: 0px;}.children_bg01 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 630px;\n' +
'}\n' +
'.children_bg02 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.children_bg03 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.narrow_page .children_bg01 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg02 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg03 a{\n' +
'width: 450px;\n' +
'}.price .search_e_price span {font-size: 12px;font-family: 微软雅黑;display: inline-block;background-color: #739cde;color: white;padding: 2px 3px;line-height: 12px;border-radius: 2px;margin: 0 4px 0 5px;}\n' +
'.price .search_e_price:hover {text-decoration: none;}</style> <link rel="stylesheet" href="http://product.dangdang.com/js/lib/layer/3.0.3/skin/default/layer.css?v=3.0.3.3303" id="layuicss-skinlayercss"><script id="temp_script" type="text/javascript" src="//schprompt.dangdang.com/suggest_new.php?keyword=好好&amp;pid=20230227105316030114015279129895799&amp;hw=1&amp;hwps=12&amp;catalog=&amp;guanid=&amp;0.918631418357919"></script><script id="json_script" type="text/javascript" src="//static.dangdang.com/js/header2012/categorydata_new.js?20211105"></script></head>');

window = dom.window;
document = window.document;
function invokeServer(url) {

var scriptOld = document.getElementById('temp_script');
if(scriptOld!=null && document.all)
{
scriptOld.src = url;
return script;
}
var head=document.documentElement.firstChild,script=document.createElement('script');
script.id='temp_script';
script.type = 'text/javascript';
script.src = url;
if(scriptOld!=null)
head.replaceChild(script,scriptOld);
else
head.appendChild(script);
return script
}



  1. 完事!当我以为都要结束了的时候恍惚直接看到了源码中的gb2312突然想起了之前做的一个萍乡房产网的网站有过类似经历赶快去尝试结果我**
    image.png
    image.png
    VeryCapture_20230227172815.gif




  2. 总结:提醒各位大佬在逆向之路中还是要先从基操开始,没必要一味的去搞攻克扒源码,当然还是要掌握相对全面的内容,其实除了个别大厂有些用些贵的东西据说某数5要20个W随着普遍某数不知道那些用了20w某数的大厂心里是什么感觉或许并不在乎这点零头哈哈毕竟是大厂,小网站的反扒手段并不是很难,俗话说条条大道通北京。


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

我,不是实习生

啊,一想我读书十几年,现在终将要脱离学校奔赴社会了。依照我这能力,我这性子,学术型人才是走不动了。 我知道,不继续深造,就要去工作赚钱。 可我一晃这几年,感觉啥也没学着。圈养在学校,老师只负责教授我课本知识,定向培养我成为一名合格的打工人。 经历面试才知道,市...
继续阅读 »

啊,一想我读书十几年,现在终将要脱离学校奔赴社会了。依照我这能力,我这性子,学术型人才是走不动了。


我知道,不继续深造,就要去工作赚钱。


可我一晃这几年,感觉啥也没学着。圈养在学校,老师只负责教授我课本知识,定向培养我成为一名合格的打工人。


经历面试才知道,市场的速度是光速,学校里是声速。企业技术走得这么前沿,我学的东西讲出来都有些羞涩。


技术和知识面落后这么多,要这么短的时间紧跟市场速度,且找到一份合适的工作,属实不容易。


没辙,遵循市场用人规则,我连夜整理面试知识和制作简历,并效仿当年八股文进士。


可一工作才发现,没完没了的工作,没完没了的 OKR,大家都没完没了,可是整体盈利又不怎么样。


作为一名实习生,实在是搞不通,谁顾着谁,谁又惧谁。我知道我会好好工作,内心还怀揣着抱负和理想,一心扑到工作上证明我自己。


电话里头我也是这么对父母说。


可是,几千块钱的薪资,我恍然不已,原来劳动用工成本可以这么低,我每天顾着怎么开销,怎么减少活动,怎么存钱,家里急用钱怎么办。


另一方面,我通过努力证明自己的实力,可以升职加薪,可是一冷静下来,怎么加班都能联想到旁边工作十几年经验老油条的现状。


联想周边企业刚加入的年轻生命,读了十几年书刚开始还没来得及做好准备,就结束在了加班不眠之夜里。


不禁在想,身边不乏努力的人,可越是努力劳动力越廉价,资源也就这么多。这些努力的成果到底在哪里,它会和我们的理想化文明向前推进挂钩吗,还是像小白鼠跑转轮一样。只要你在忙,在跑着就会有食物,仅此而已。


当我不断工作不断思考之后,我决定把一部分精力从工作拆分开来,用来做自己的事情并且赚钱的时候。


才发现,我已经离罗马中心十八里开外了。那些早就明白规则的人已经身价 A7+ 了。而我,还只是一个职场人,思想和个性逐渐被磨平的人。


好不容易开始有自主赚钱的觉悟,却又不知道从哪里开始。


眼见互联网的风吹草动,打算沾点风头做些衍生产品赚些小钱,可曾想相关情况一调查,各种衍生产品已经多如牛毛,自己的想法刚萌生就已经望尘莫及。


看来这个新时代,已经拼的主要不是个人努力,而是感知能力,谁的感知能力强,捕捉到风口和需求,谁就能够抢占先机,落后的残羹冷炙都吃不上。


回想起我同届的校友,几个校友的案例历历在目,他们在校的时候就喜欢自己捣鼓生意,周边能赚钱的项目都试了个遍,租摊做外卖、合租奶茶店、做中介介绍学生工、婚礼现场布置等等,这些都是他们课外喜欢动手做的事情,大二开始就已经有很强的人脉关系和团队,学长学姐老师都能够拉拢合作。


我曾好奇问过他,你为啥经常跑这跑那,经常上课缺席?


他告诉我,我不太喜欢课本上的东西,我喜欢捣鼓些小玩意,以后毕业自己能够做些小生意就够了。


当时我的角度跟他截然相反,上课认真听讲,班务事情积极做,国家和学校的奖学金我都拿了,但对于工作前景就是当一名程序员就行。


可以明说当时心态上有些看不起他们经常翘课,出去捣鼓小钱的行为。


事实上,是多么可笑的。确实是换了几家企业的程序员,每天殚精竭虑花在工作上,上面指哪我就打哪,固定薪资,每时每刻接收就业差,企业裁员情报,人人自危陷入焦虑和恐慌,学生时代的傲骨早就被企业文化磨平。


而那些从学生时代喜欢捣鼓生意,爱动手赚钱的人,就我所知道那些同届的校友。他们的店铺已经开连锁店了,做学生和年轻人的生意。有的已经赚国外的钱了。



▲图/ 学长已经开起了分店


这一类人,我身边认识和知道的没有一个过得不好的,他们善于利用信息和售卖信息。教你开店,拍视频的课程理论上都是售卖信息行为。


这一对比,仿佛他们才是懂社会规则的人,像是弯道超车般的越过了规则到了另外一层,他们的精力花在了资本运转身上,只要有人有需求就有机会。对于裁员、跳槽、就业,是打工人该担心的事情。


才明白,学校所教授的知识和培养的素质,大多都是培养我们成为一名工人具备的思维和能力,毕竟经济的推动和国家的发展,需要具备大量的工人。至于效果和进阶,那就让企业、社会来教你。


出来实习后,才知道自己有多么被动。跟不上社会的步伐,欠缺了多少实用的工具和能力。


被欺负了如何拿起法律的武器保护自己;找不到工作或失业如何自主赚钱;如何懂理财懂投资;脱离一定条件如何生存和陷入危险如何自救...


这些,长达十几年的学生时代里,没有专门的课程或者相关老师教授。


我所欠缺的这些知识,是屡次碰壁之后幡然醒悟,才有所接触这些内容,但此时已经千疮百孔,伤痕累累。与此同时,总能遇见大批初入社会的人依然走自己走过的路,叫,是叫不醒。在世界观和认知能力闭合之后,总是需要事教人的地步才能打开。因为,好言相劝已经不管用。



象牙塔里待了几年,一张白纸怎么渲染都好渲染,原本与人为善,感恩戴德,分清对与错,拥有理想与信念。


开始接轨社会之后,持着心善的态度连打数张好人牌,被坑蒙拐骗殆尽,才醒悟这些品质都是别人敛财的工具,得到教训之后还得分清谁值得给好人牌,谁永远坏人牌。


什么又是对与错,无人问津的人说什么话都只是一句话。反而屏幕出现权势,坐拥资源的人说的话有多不经推敲都觉得是对的,因为它是成功人士。


什么是理想,什么又是信念。去一趟公司吧,让你感受一番企业文化之后,再说你的理想是什么,有什么信念。


......


作为一名实习生,意味着即将进入社会,和不同人打交道了。或许你的内心秉持着工作的想法,又或许秉持着自己的热爱和目标。


工作从来也都不是一件轻松的事情,至少最近的环境里是这样。


工作或许能够加速让你融入社会,但同时加速你的痛苦,因为这是一个“丛林世界”,你没对错可言,也没有更多选择,或许看似有选择其实也只是一个看起来更大稍微舒适点的“囚牢”。


并且,工作本身难找的不是工作,而是放缓不了自己的心态,一头扎进内心向往的“高薪”,“体面”,“舒适” 出不来。人人向往这片区域,也就只有这么点区域,总有人失落且焦虑。


然而,大多不被看好的行业或被嫌弃的工作,往往能够带来与之热门职业持平的回报。网络出现的“北大毕业卖猪肉”,“高材生当保姆”的等被大众关注的案例屡见不鲜,甚至嗤之以鼻予以嘲讽。


殊不知“笑贫不笑娼”的社会环境,他们才是能屈能伸的强者。这些人和我那些校友有着同样的能力。动手能力强,生存能力强,就算被限制条件,也能够屈伸过的好。


职业并无贵贱区分,能掌握活法才是本质。


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

🐞 如何成为一名合格的“高级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 这几天疯狂在肝游戏,已经到了魔怔的地步,每天早上起床是想着我今天该怎么在地铁上杀爆,每天晚上躺下的时候想的是我的装备还能怎么配装…… 哈哈,今天我们继续分享怎么一步步做一个专业的开发者,还有工作中要注意什么事情。 如...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


这几天疯狂在肝游戏,已经到了魔怔的地步,每天早上起床是想着我今天该怎么在地铁上杀爆,每天晚上躺下的时候想的是我的装备还能怎么配装……


哈哈,今天我们继续分享怎么一步步做一个专业的开发者,还有工作中要注意什么事情。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的三篇文章,说不定能对你有帮助。




  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?

  3. 🐞 如何成为一名合格的“中级开发”



今天,我们继续聊一聊,看看到底是什么造就了“高级开发”,而我们应该怎么往这个方向冲刺呢?😎


❓ 什么是“高级开发”?


回顾一下我这个系列第一篇文章的定义,我这边罗列一下:



  1. 精通团队所使用的核心技术,对其应用得非常熟练。

  2. 能处理团队项目中的系统架构问题和设计🏢

  3. 有多年的编码经验(一定是真正在一线的真正写代码的时间,而不是通过经历硬凑的时间)

  4. 拥有构建“完整”解决方案的经验,能够考虑到项目的各个方面并提供全面的解决方案。🔍

  5. 在其他专业相关领域有一定经验,了解负载平衡、连接池等跨领域知识。🖥️

  6. 积极指导中级和初级开发工程师。👥


如果你能做到以上这些部分或者全部的内容,比如:



  • 你在公司中解决了很多一般开发解决不了的难题

  • 善于沟通,能够处理各方的关系和调解工作沟通

  • 在许多团队决策上能提供许多建设性的想法


等等……


就算短时间内不被授予领导者的角色,潜移默化地,你的同事都会帮你当成团队大佬和领导(更注重你的意见)


❓ “高级开发”比“中级开发”多了什么?


多得多的开发经历


现在国内公司普遍的一个潜规则是 5年以上开发经验是“高级开发”职务的基本条件


因此现在有很多的“中级开发”喜欢使用 编码经历(不是开发经历) 来判断自己是不是能胜任“高级开发”的职务。


例如:小张只有3年的工作经验,但是喜欢把实习1年和大学的编程作业1年这些时间加上,来给自己打上5年开发经验的标签。



可能因为内卷的原因,简历伪造基本上50%的概率都会遇到,大家都想把自己最好的一面展示出来,甚至不惜夸大一部分的事实。至少如果这份简历到了我这里看到,是一件非常危险的事情。



这样就导致了,很多时候我们真的没有招募到一个有高级开发实力的“高级开发”。


高级开发人员在构建解决方案、管理复杂性、处理令人困惑的业务需求、应用设计模式等方面积累了丰富的经验。因为他们做过很多次这些事情,一遍又一遍,他们可以“用心”解决许多常见问题。


“高级开发”应该要像一个成熟的成年人,很多方案的尝试不应该带有实验性,而是真正的“做过”


这种能力只能来自你从失败、成功、导师等中学到的真实经验,需要大量的练习,需要做很多次这些事情,以至于它们会印在你的大脑中!



这里提到的经验,不包括没有挑战性的工作,如果只是CRUD,你永远都不会成长



在这之前他们应该在开发经验上有很多很多时间的沉淀,研究过许多Demo,并为他们以后的解决方案奠定基础。


所以,“高级开发”需要的是比“中级开发”多得多得多的实战经验才能构建出一个属于自己的“解决方案”体系。


谦卑


“高级开发”因为在很多问题的已经有了解决方案。


而且他们已经把有效、有用、实用、简单这几个字贯彻到了实际的开发工作中。


因此由于他们的经验,“高级开发”虽然知道很多东西,对自己的能力很有信心,但是他不会再有“骄傲”的心理。


因为面对的事情太多,会开始知道其实自己不知道的事情太多了。


反而“高级开发”对如何让实现方案趋于完美有很高的追求。


❓ “高级开发”应该有怎么样的知识广度?


现在我们业内流行一个说法叫做 T型人才


这其实就要求“高级开发”要对许多其他专业领域要有基本的了解,而且要再本专业领域或多个本专业领域拥有深入的知识和技能。


例如:



  1. 我从来没有构建过分布式微服务系统,但是我知道这个系统能决什么问题,而且我大概了解构造他们的不同方法。

  2. 我从来没有在实际生产中应用前端监控平台,但是我知道他能解决什么样的问题。


就在几周前,我们公司进行数据治理的时候,我向我们应用服务团队推荐了一些使用“落地表”,“增加表索引”和“使用缓存”优化数据库性能的组合方案。我近两年没有再操作过数据库,甚至没建过索引,但是我知道它们组合起来能解决什么问题。


同样,这个也是在 什么是“高级开发” 中提到的一个“高级开发”的关键特征:在其他专业相关领域有一定经验🖥️


❓ “高级开发”应该有怎么样的知识深度?


同样,在 “T” 的垂直领域,“高级开发”应该在自己的专业领域有深入的研究,具备完整的知识和技能(这个是在开发领域的立足之本)。


也就是 什么是“高级开发” 提到的:精通团队所使用的核心技术,对其应用得非常熟练


无论如何,“高级开发”必须先是某个专业领域的专业人才为前提。


这些特定的领域可能是编程语言或框架:Vue、Angular、React、Three.js、Node、ava、Go等等


或者是一组特定的技术:系统架构、编程范式、专业解决方案、应用安全、网络安全等等


甚至是特定行业的针对性了解:医疗安全体系、金融安全体系等等


❓ 我是怎么成长过来的?


如果以我自己的职业生涯为案例。


我的第一份工作是在一家国内知名的PCB行业民企的IT部门,当时该企业内部的“OA”系统正在进行重构和维护,目标是想让OA系统以一个全新现代化的面貌展现给公司的全体职员。


可是这个系统很旧很烂,而且我需要不仅仅是在单个领域,而是在前端、后端、数据库等方面都要着手进行改造。😱


在这个过程中,因为老旧的OA系统的后端是使用VB语言开发的,而我实在是不愿意花大量的时间在其上学习这类老旧开发语言。


于是在任职的三年期间,我从0开始为公司搭建了一个使用Node的转发服务,并且基于这个Node服务,我构建了很多新的功能。👏


虽然这些功能看起来技术难度都不高,但是整个过程因为都经过自己的双手,确实让自己成长迅速,自己也学到了很多东西,包括很多不该学的(我甚至可以直接操作生产服务器和读取生产数据库)。


当然这些过程中也遇到了很多非常复杂的业务逻辑,这促使我寻找一些标准的代码实践。


为了解决这些问题,我花了很多时间(当然包括下班时间)学习了一些高级的编程知识,比如DDD,面向切面编程,设计模式等等。


然后我就会在工作中尝试使用这类代码实践,同事也会在这个时候问我这些东西怎么使用。


因为这样的环境,我不断地主导和帮助我的团队解决了很多代码组织和业务实践的难题。


而这些经验也让我在找下一份工作的时候更有优势。😎


📌 我想成为一名“高级开发”


看看自己是不是能做到下面这些事情:



  1. 你知道你真正想要深入的技术栈,并且真的在深入研究它们

  2. 每天都有学习的时间

  3. 不害怕承担难度高但是有价值的项目

  4. 真的奋斗在一线编程,而不是在管理岗位摸鱼

  5. 真的了解自己的“T”型技能树

  6. 如果你还不知道自己该学习什么,开始规划自己想拥有什么技能

  7. 了解你学的技能能解决什么问题,而不是因为热门才学习

  8. 了解设计模式(别以为设计模式不重要,它们是大多数领域的通用原则!)

  9. 如果你的工作在你的舒适区,建议你转向更有挑战的工作


🚩 避免成为“高级初学者”


现实中很多人可能并不拥有“高级开发”的职位,但是其他们已经拥有了高级开发的能力


不要对职位盲目崇拜,在国内许多地方,很多人都有高级开发工程师的头衔,甚至叫做“前端专家”


但是其实,他们可能:



  1. 他们已经在该岗位工作了好几年

  2. 他们面试很厉害,就是那个时候评了这个职级


这种情况在国内无处不在,当然也无法改变。我们只要意识到,他们可能并不具备高级开发的素养,不要盲目地模仿公司中地所谓的高级开发人员的代码,可能这些人在多年前就一直在这个舒适区待着,从来没有成长。


不要因为选错了导师而阻碍了自己成长。




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

【干货分享】安卓加固原理分享

App会面临的风险 我们首先了解一下为什么需要加固,尤其是安卓APP,下面是App目前会面临的各种风险: 而通过进行安卓加固,可以降低应用程序遭受各种恶意攻击的风险,保护用户数据和应用程序的安全性,增强用户对应用程序的信任度。 安卓加固的原理 安卓应用程序的...
继续阅读 »



App会面临的风险


我们首先了解一下为什么需要加固,尤其是安卓APP,下面是App目前会面临的各种风险:


image.png


而通过进行安卓加固,可以降低应用程序遭受各种恶意攻击的风险,保护用户数据和应用程序的安全性,增强用户对应用程序的信任度。


安卓加固的原理


安卓应用程序的加固涉及多个方面和技术。我列举了一些常见的安卓加固原理以及相关的示例代码:


1. 代码混淆(Code Obfuscation):


代码混淆通过对应用程序代码进行重命名、删除无用代码、添加虚假代码等操作,使代码难以阅读和理解,增加逆向工程的难度。常用的代码混淆工具包括ProGuard和DexGuard。


示例代码混淆配置(build.gradle):

android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

2. 反调试(Anti-debugging):


反调试技术可以检测应用程序是否在被调试,并采取相应的防护措施,例如中断应用程序的执行、隐藏关键信息等。


示例代码检测调试状态:

import android.os.Debug;

if (Debug.isDebuggerConnected()) {
// 应用程序正在被调试,采取相应的措施
}

3. 加密和密钥管理(Encryption and Key Management):


加密可以用于保护应用程序中的敏感数据。对于密钥管理,建议使用安全的存储方式,例如使用Android Keystore系统来保存和管理密钥。


示例代码使用AES加密算法对数据进行加密和解密:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionUtils {
private static final String AES_ALGORITHM = "AES";

public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedData);
}
}

4. 动态加载和反射(Dynamic Loading and Reflection):


通过动态加载和反射技术,可以将应用程序的核心逻辑和敏感代码进行动态加载和执行,增加逆向工程的难度。


示例代码使用反射加载类和调用方法:

try {
Class<?> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("myMethod");
method.invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}

5. 安全存储(Secure Storage):


对于敏感数据(如密码、API密钥等),建议使用安全的存储方式,例如使用Android Keystore系统或将数据加密后存储在SharedPreferences或数据库中。


示例代码使用Android Keystore存储密钥:

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.security.KeyStore;

public class KeyStoreUtils {
private static final String KEY_ALIAS = "my_key_alias";

public static void generateKey() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setRandomizedEncryptionRequired(false)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(spec);
keyGenerator.generateKey();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

以上就是简单的代码示例。


目前市场上加固的方式


目前市面上加固的方式一般是一套纵深防御体系,分别从代码安全、资源文件安全、数据安全和运行时环境安全维度提供安全保护。同时针对每个维度又进行了不同层次的划分,加固策略可依据实际场景进行定制化调配,安全和性能达到平衡。


所以一般会从下面几个方面进行加固:


image.png


而不同的公司或者APP对于加固的要求又会不一样,所以具体的使用,其实还是要看具体的场景,等之后有机会再展开详细讲一下。


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

Android 开发还有必要深耕吗?现状怎么样?未来前景将会怎样?

截止到今天,Android的生态发生了不少变化 以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入An...
继续阅读 »

截止到今天,Android的生态发生了不少变化


以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入Android开发行业。Android招聘市场的需求逐渐被填充,招聘要求逐步提高……


随着“互联网寒冬”的到来,大批互联网公司纷纷倒闭,大厂也纷纷裁员节流,人才供给大幅增加、需求大幅降低,造成当时的市场迅速达到饱和。培训出来的初级Android开发找不到工作,大厂被裁员的Android开发放不下薪资要求,这批人找不到工作,再加上当时自媒体的大肆渲染,Android开发可不就“凉了”吗?


毫不夸张的说,早期说得上四大组件稍微能做一点点,拿个15-20k是比较轻松的,要是你还有过完整开发经验,30k真是一点都不过分,而在“寒冬”之后,当招聘市场供给过剩时,面试官有了充分的选择权,你会四大组件,那我就有完整App独立开发经验,另一人或许有过十万级App开发经验,你说面试官会收下谁呢?岗位招聘要求也是在这时迅速拔高,整个Android市场逐渐趋于平稳,大家感觉Android开发来到了内卷期……


再来说现在:


Android凉了吗?


其实并不是Android凉了,而是技术不过硬的Android凉了


被高薪晃晕了头脑放不下身段的假高工凉了


现在的Android市场,Android初级工程师早就已经严重饱和了,供远大于求。这就导致了很多Android开发会面临被优化、被毕业、找不到工作这种情况,然后这部分人又出来说Android凉了,如此循环之下,以致于很多人都觉得Android凉了……


其核心原因只是Android开发市场由鼎盛的疯狂逐渐趋于平稳


这里也给出Android开发薪资/年限图给大家参考:


互联网公司Android开发薪资、年限.jpg


也不缺少学历突出的、能力突出的、努力突出的,这三类都可以拿到比图中同级别更可观的薪资


当然,我们并不能以薪资作为职级的标准,决定一个Android工程师到底是初级、中级、高级还是资深的,永远都不会是开发年限!


只有技术才能客观的作为衡量标准!


不管是几年经验,如果能力与工作年限不匹配,都会有被毕业的风险,如果掌握的技术达不到对应职级的标准,那别想了,毕业警告……


在很多人觉得Android凉了的时候,也不乏有Android开发跳槽进大厂拿高薪,不少在闷头提升技术水平,迄今为止还没有听过哪个Android开发大牛说“Android凉了”,当大家达到一定的高度之后,就会得知谁谁谁跳槽美团,几百万;某某某又跳进了阿里、腾讯……


不管在任何行业,任何岗位,初级技术人才总是供大于求;不管任何行业、岗位,技术过硬的也都是非常吃香的!


在初级市场”凉了“的同时,高级市场几乎是在抢人!


很多高薪、急招岗位挂上了招聘网站,往往一整年都面试不了几场,自打挂上来,就没动过了……


image.png
所以说,Android开发求职,质量才是关键!


再说到转行问题


我一直都比较佩服有大勇气转行的朋友,因为转行需要我们抛弃现有的知识技能,重新起航


佩服归佩服,身边不少之前是Android开发的朋友转行Java、Python,但他们对于目前市场还是过于乐观了,Python很火,它竞争不大吗?部分转行从0开始的,甚至连应届生都比不过~


不要轻易转行,如果要转一定要尽早转


转行有两种我认为是正常的,一种是行业消失了、没落了,继续留在业内无法施展才华。另一种是兴趣压根就不在本行,因此选一个自己感兴趣的。而现在大部分转行都是为了跟风,为了那看得见但摸不着的”风口“,而忽略了长期的发展潜力。


image.png


不管是学习力也好,精力也好,大部分人在35岁之前都属于加速期,加速期的一些选择,决定了35岁之后到底是上升还是衰落。


以Android开发转Python来说,一个Android高级转行Python会变为Python初级,这时从事Python的人都在加速提高,要想赶超在你之前的拥有同样学习力的人是不可能办到的,这就导致在转行前期极为被动,还要保证在35岁前成为Python专家或者Leader才有可能在35岁后不进入衰落期,当然这时你的Android基本也就荒废了,不说很难成为专家,高级也成为了一个很大的门槛。


如果你还想要在对应的技术领域走的更远,就不要轻易选择转行,如果实在想要转,那么越早越好、越快越好,你的竞争者都在加速提升技术水平,职场上,没人会停下等你的……


转行大部分都产生不了质变


我们所说的质变可以理解为在一个技术领域的大幅提升,或者是不相关领域的跨界


比如由高级开发变为专家,或者是由高级开发升到Leader,再或者跨界开始做一些技术相关的博客、培训、演讲、出书等等而被人所熟知。


凡是能帮助你在职业生涯中后期进入上升期的都可以看做是一次质变,而转行很少是质变,更多的都是倒退回到原点重新出发,形象点来说,你只是换了个不同的砖头接着搬砖而已。因此我们更应该去追求质变,而不是平行或者倒退,一次倒退或许可以承受,多次倒退就很难在职业生涯中后期再进入上升期。


其实不少转行的人都没有起到积极作用,毕竟都是从0开始,精进到专家绝不是一朝一夕可以完成的


或许到时又会有同样的问题:



前端凉了?前景怎么样?


Java凉了?前景怎么样?


大数据凉了?前景怎么样?


人工智能凉了?前景怎么样?


……



而另一类人,其实不管在哪个行业都可以混的风生水起!


如果是这种,那么想必也不需要考虑转行了。


所以根本不用想着Android凉了或是说要转行,与其焦虑不安,不如努力提升技术水平,毕竟在这时代,有硬技术的人到哪都吃香。


我们想要往高级进阶,建立属于自己的系统化知识体系才是最重要的,高工所需要掌握的技术不是通过蹭热点和玩黑科技,而是需要真正深入到核心技术的本质,知晓原理,知其然知其所以然。


可能不少人会觉得Android技术深度不深,技术栈不庞大,Android职业发展有限,这就真是个天大的误解。


先说技术上,Android的技术栈随着时间的推移变得越来越庞大,细分领域也越来越多,主要有应用开发、逆向安全、音视频、车联网、物联网、手机开发和SDK开发等等,每个细分领域都有很多技术栈组成,深度都足够精深,就拿所有细分领域通用的Android系统底层源码来说,就会叫你学起来生不如死。


还有AI、大数据、边缘计算、VR/AR,很多新的技术浪潮也都可以结合进移动开发的技术范畴……


那么现在Android怎么学?学什么?


这几年Android新技术的迭代明显加速了,有来自外部跨平台新物种的冲击,有去Java化的商业考量,也有Jetpack等官方自建平台的加速等多种原因。


作为Android开发者,我们需要密切关注的同时也不要盲目跟随,还是要认清趋势,结合项目现状学习。


Kotlin


Kotlin已经成为Android开发的官方语言,Android的新的文档和Sample代码都开始转向 Kotlin,在未来Java将加速被 Kotlin替代。


刚推出时,很多人都不愿意学习,但现在在面试中已经是经常会出现了,很多大公司也都已经拥抱新技术了。现在Kotlin是一个很明显的趋势了,不少新技术都需要结合Kotlin来使用,未来在工作中、面试中所占的比重肯定会更大。


Jetpack+Compose


Jetpack的意义在于帮我们在SDK基础上提供了一系列中间件工具,让我们可以摆脱不断造轮子抄轮子的窘境。同类的解决方案首先考虑Jetpack其次考虑第三方实现,没毛病。


Jetpack本身也会不断吸收优秀的第三方解决方案进来。所以作为开发者实时关注其最新动态就可以了。


Compose是Google I/O 2019 发布的新的声明式的UI框架。其实Google内部自2017年便开始立项,目前API已稳定,构建,预览等开发体验已经趋于完整。


而且新的设计思想绝对是趋势,已经在react和flutter等前端领域中得到验证,ios开发中同期推出的swiftUI更是证明了业界对于这种声明式UI开发趋势的共识。这必将是日后Android app极为重要的编程方式。


开源框架底层原理


现在的面试从头到尾都是比较有深度的技术问题,虽然那些问题看上去在网上都能查到相关的资料,但面试官基本都是根据你的回答持续深入,如果没有真正对技术原理和底层逻辑有一定的了解是无法通过的。


很多看似无理甚至无用的问题,比如 “Okhttp请求复用有没有了解”,其实是面试官想借此看看你对网络优化和Socket协议的理解情况和掌握程度,类似问题都是面试官想借此看看你对相关原理的理解情况和掌握程度,甚至进而引伸到你对架构,设计模式的理解。只有在熟知原理的前提下,你才能够获得面试官的青睐。


Framework


Framework作为Android的框架层,为App提供了很多API调用,但很多机制都是Framework包装好后直接给App用的,如果不懂这些机制的原理,就很难在这基础上进行优化。


像启动监控、掉帧监控、函数插桩、慢函数检测、ANR监控,都需要比较深入的了解Framework,才能知道怎么去监控、利用什么机制监控、函数插桩插到哪里、反射调用该反射哪个类哪个方法哪个属性……


性能优化


性能优化是软件工程的深水区,也是衡量一个程序员能力高低的标准


想要搞清楚性能优化,必须对各种底层原理有着深度的了解,对各种 case非常丰富的经验;很多朋友经常遇到措手不及的问题,大多是因为对出现问题的情况和处理思路模糊不清,导致此原因就是因为没有彻底搞懂底层原理。


性能优化始终穿插在 App 整个研发生命周期中,不管是从 0 到 1 的建立阶段,还是从 1 到 N 打磨阶段,都离不开性能优化。


音视频


伴随着疫情的反复以及5G的普及,本就火爆的音视频技术是越来越热,很多大小厂在这几年也都纷纷入局。但音视频学习起来门槛比较高,没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。


招聘市场上,同级别的音视频开发要比应用开发薪资高出30%以上。


车载


在智能手机行业初兴起时,包括BAT在内许多传统互联网企业都曾布局手机产业,但是随着手机市场的基本定型,造车似乎又成了各大资本下一个追逐的方向。百度、小米先后宣布造车,阿里巴巴则与上汽集团共同投资创立了,面向汽车全行业提供智能汽车操作系统和智能网联汽车整体解决方案的斑马网络,一时间造车俨然成了资本市场的下一个风口。


而作为移动端操作系统的霸主Android,也以一种新的姿态高调侵入造车领域


关于学习


在学习的过程中,可能会选择看博客自学、看官方文档、看书、看大厂大牛整理的知识点文档、看视频,但要按学习效率来排序的话:报培训班>看视频>知识点>书籍>官方文档>博客


报班,可能很多朋友对于报班这个事情比较抵触,但不可否认,报一个培训班是可以学到很多深层次的、成体系的技术,像之前读书一样,都是捣碎了喂给你,并且培训班其实对于新技术、新趋势是相当敏锐的,可以第一时间接触,也会规避开自学的烦恼。


看视频,基本也是由别人捣碎知识点教会你,但较之培训班的话,视频的知识成体系吗?有没有过时?


大厂大牛整理的知识点文档,大厂大牛技术还是比较可靠的,这类型的知识点文档初版基本是可以放心享用,但如果只是少数人甚至是一个人进行维护的话,当整个文档的知识体系越来越广时,其中的部分知识点可能已经过时但一直没有时间更新


书籍,相比前者就更甚了,一个技术出来,先研究、再整理、修正……直到最后出版被你买到,中间经过的这段时间就是你落后于其他人的地方了,但其中的知识点基本可以肯定成体系、无重大错误。学习比较底层的,不会有很大改动的知识点还是相当不错的。


官方文档,这一块也是我思考了很久才排好,官方文档往往是第一手资源,对于有能力看懂的朋友来说,可以直接上手品尝。但其实很多开发拿到官方文档还是看的一知半解,再者说,自己看可能会有遗漏,还是没有别人一点一点将重点翻开来解读更好


博客,网络上的博客水平参差不齐,通常大家擅长的也不是同一个技术领域,往往是学习一块看A的,另一块看B的,而且网上很多博客都是抄来自己记录的,很多API已经过时了,甚至不少连代码都是完全错误的,这样的学习,可想而知……


最后


一些个人见解,也参考了不少大佬的观点,希望可以给大家带来一些帮助,如果大家有什么不同看法,也欢迎在评论区一起讨论交流


Android路漫漫,共勉!


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

Binder Java 层服务注册过程分析

1. Java 层整体框架 在分析之前,我们要明白,Java 只是一层方便 Java 程序使用的接口,Binder 的核心功能实现都是通过 JNI 调用到 Native 层来实现的,这里先给出 Java 层的整体框架图: 接下来几篇文章我们逐步分析,解密整张...
继续阅读 »

1. Java 层整体框架


在分析之前,我们要明白,Java 只是一层方便 Java 程序使用的接口,Binder 的核心功能实现都是通过 JNI 调用到 Native 层来实现的,这里先给出 Java 层的整体框架图:



接下来几篇文章我们逐步分析,解密整张框架图。


2. 服务注册


Binder 程序示例之 Java 篇 中介绍的示例程序中,Server 端我们使用如下代码注册我们定义的服务:

ServiceManager.addService("hello", new HelloService());

addService 是 frameworks/base/core/java/android/os/ServiceManager.java 中定义的静态方法:

@UnsupportedAppUsage
public static void addService(String name, IBinder service) {
addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}

@UnsupportedAppUsage
public static void addService(String name, IBinder service, boolean allowIsolated) {
addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}

@UnsupportedAppUsage
public static void addService(String name, IBinder service, boolean allowIsolated,int dumpPriority) {
try {
getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
} catch (RemoteException e) {
Log.e(TAG, "error in addService", e);
}
}

通过层层调用,调用到 getIServiceManager().addService(name, service, allowIsolated, dumpPriority); :


2.1 getIServiceManager()


我们先看看 getIServiceManager,该方法是定义在 ServiceManager 类中的静态方法:

//frameworks/base/core/java/android/os/ServiceManager.java
@UnsupportedAppUsage
private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}

// 等价于 new ServiceManagerProxy(new BinderProxy(0))
// 但是实际过程有点曲折
sServiceManager = ServiceManagerNative
.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
return sServiceManager;
}

接着我们逐一分析三个方法调用:

BinderInternal.getContextObject()
Binder.allowBlocking
ServiceManagerNative.asInterface

2.1.1 BinderInternal.getContextObject

//frameworks/base/core/java/com/android/internal/os/BinderInternal.java
// 返回一个 BinderProxy 对象
@UnsupportedAppUsage
public static final native IBinder getContextObject();

getContextObject 是一个 native 方法,在之前的文章中我们提到 BinderInternal 在进程启动时注册了其 native 方法,其 native 实现在 frameworks/base/core/jni/android_util_Binder.cpp 中:

static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
//此处返回的是 new BpBinder(0)
sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
//此处返回的是 new BinderProxy()
return javaObjectForIBinder(env, b);
}

接着看 getContextObject 的实现:

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
return getStrongProxyForHandle(0);
}

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
sp<IBinder> result;

AutoMutex _l(mLock);

handle_entry* e = lookupHandleLocked(handle);

if (e != nullptr) {
IBinder* b = e->binder;
if (b == nullptr || !e->refs->attemptIncWeak(this)) {
if (handle == 0) {
Parcel data;
status_t status = IPCThreadState::self()->transact(
0, IBinder::PING_TRANSACTION, data, nullptr, 0);
if (status == DEAD_OBJECT)
return nullptr;
}

//走这里
b = BpBinder::create(handle);
e->binder = b;
if (b) e->refs = b->getWeakRefs();
result = b;
} else {
result.force_set(b);
e->refs->decWeak(this);
}
}

return result;
}

BpBinder* BpBinder::create(int32_t handle) {
int32_t trackedUid = -1;
if (sCountByUidEnabled) {
trackedUid = IPCThreadState::self()->getCallingUid();
AutoMutex _l(sTrackingLock);
uint32_t trackedValue = sTrackingMap[trackedUid];
if (CC_UNLIKELY(trackedValue & LIMIT_REACHED_MASK)) {
if (sBinderProxyThrottleCreate) {
return nullptr;
}
} else {
if ((trackedValue & COUNTING_VALUE_MASK) >= sBinderProxyCountHighWatermark) {
ALOGE("Too many binder proxy objects sent to uid %d from uid %d (%d proxies held)",
getuid(), trackedUid, trackedValue);
sTrackingMap[trackedUid] |= LIMIT_REACHED_MASK;
if (sLimitCallback) sLimitCallback(trackedUid);
if (sBinderProxyThrottleCreate) {
ALOGI("Throttling binder proxy creates from uid %d in uid %d until binder proxy"
" count drops below %d",
trackedUid, getuid(), sBinderProxyCountLowWatermark);
return nullptr;
}
}
}
sTrackingMap[trackedUid]++;
}
//走这里
return new BpBinder(handle, trackedUid);
}

代码看着很繁琐,实际流程其实很简单就是 new BpBinder(0)


接着看 javaObjectForIBinder 的实现:

//frameworks/base/core/jni/android_util_Binder.cpp

//当前情景下, val 的类型是 BpBinder
jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
if (val == NULL) return NULL;

if (val->checkSubclass(&gBinderOffsets)) {
// It's a JavaBBinder created by ibinderForJavaObject. Already has Java object.
jobject object = static_cast<JavaBBinder*>(val.get())->object();
LOGDEATH("objectForBinder %p: it's our own %p!\n", val.get(), object);
return object;
}

//构造 BinderProxyNativeData 结构体
BinderProxyNativeData* nativeData = new BinderProxyNativeData();
nativeData->mOrgue = new DeathRecipientList;
nativeData->mObject = val;

//gBinderProxyOffsets 中保存了 BinderProxy 类相关的信息
//调用 Java 层 GetInstance 方法获得一个 BinderProxy 对象
jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,
gBinderProxyOffsets.mGetInstance, (jlong) nativeData, (jlong) val.get());
if (env->ExceptionCheck()) { //异常处理
// In the exception case, getInstance still took ownership of nativeData.
return NULL;
}
BinderProxyNativeData* actualNativeData = getBPNativeData(env, object);
if (actualNativeData == nativeData) {
// Created a new Proxy
uint32_t numProxies = gNumProxies.fetch_add(1, std::memory_order_relaxed);
uint32_t numLastWarned = gProxiesWarned.load(std::memory_order_relaxed);
if (numProxies >= numLastWarned + PROXY_WARN_INTERVAL) {
// Multiple threads can get here, make sure only one of them gets to
// update the warn counter.
if (gProxiesWarned.compare_exchange_strong(numLastWarned,
numLastWarned + PROXY_WARN_INTERVAL, std::memory_order_relaxed)) {
ALOGW("Unexpectedly many live BinderProxies: %d\n", numProxies);
}
}
} else {
delete nativeData;
}

//返回 BinderProxy
return object;
}

native 代码调用了 BinderProxy 的 getInstance 方法:

// frameworks/base/core/java/android/os/BinderProxy.java
private static BinderProxy getInstance(long nativeData, long iBinder) {
BinderProxy result;
synchronized (sProxyMap) {
try {
result = sProxyMap.get(iBinder);
if (result != null) {
return result;
}
result = new BinderProxy(nativeData);
} catch (Throwable e) {
// We're throwing an exception (probably OOME); don't drop nativeData.
NativeAllocationRegistry.applyFreeFunction(NoImagePreloadHolder.sNativeFinalizer,
nativeData);
throw e;
}
NoImagePreloadHolder.sRegistry.registerNativeAllocation(result, nativeData);
// The registry now owns nativeData, even if registration threw an exception.
sProxyMap.set(iBinder, result);
}
return result;
}

代码很繁琐,但是从结果上来说还是比较简单的:



  • getContextObject 函数 new 了一个 BpBinder(c++结构体),其内部的 handle 是 0

  • javaObjectForIBinder 函数 new 了一个 BinderProxy(Java 对象),其内部成员 mNativeData 是一个 native 层指针,指向一个 BinderProxyNativeData,BinderProxyNativeData 的成员 mObject 指向上述的 BpBinder。


整体结构用一个图表示如下:



2.1.2 Binder.allowBlocking

    //这里传入的是 BinderProxy 对象
public static IBinder allowBlocking(IBinder binder) {
try {
if (binder instanceof BinderProxy) { //走这里
((BinderProxy) binder).mWarnOnBlocking = false;
} else if (binder != null && binder.getInterfaceDescriptor() != null
&& binder.queryLocalInterface(binder.getInterfaceDescriptor()) == null) {
Log.w(TAG, "Unable to allow blocking on interface " + binder);
}
} catch (RemoteException ignored) {
}
return binder;
}

这个方法比较简单,主要是设置 binder 的成员变量 mWarnOnBlocking 为 false。从名字来看,作用是允许阻塞调用。


2.1.3 ServiceManagerNative.asInterface

    //frameworks/base/core/java/android/os/ServiceManagerNative.java
//传入的参数是 BinderProxy
@UnsupportedAppUsage
static public IServiceManager asInterface(IBinder obj)
{
if (obj == null) {
return null;
}

//返回 null
IServiceManager in =
(IServiceManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}

//走这里,构建一个 ServiceManagerProxy
return new ServiceManagerProxy(obj);
}

//从名字来看,本来要做缓存的,但是没有做
// frameworks/base/core/java/android/os/BinderProxy.java
public IInterface queryLocalInterface(String descriptor) {
return null;
}


最终是构建一个 ServiceManagerProxy 结构体。其内部持有一个 BinderProxy 。


至此,getIServiceManager 的整体流程就分析完了。


2.2 addService

    // frameworks/base/core/java/android/os/ServiceManagerNative.java
public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);
data.writeStrongBinder(service);
data.writeInt(allowIsolated ? 1 : 0);
data.writeInt(dumpPriority);
mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
reply.recycle();
data.recycle();
}

构造两个 Parcel 结构,然后调用 mRemote.transact 发起远程过程调用。


mRemote 就是 new ServiceManagerProxy 时传入的 BinderProxy:

 public ServiceManagerProxy(IBinder remote) {
mRemote = remote;
}

进入 frameworks/base/core/java/android/os/BinderProxy.java 查看:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

//......

try {
//关注这里
return transactNative(code, data, reply, flags);
} finally {
//......
}
}

//native 方法
public native boolean transactNative(int code, Parcel data, Parcel reply,int flags) throws RemoteException;


transact 会调用 transactNative 发起远程调用,transactNative 是一个 native 方法,具体实现在 frameworks/base/core/jni/android_util_Binder.cpp

// obj 对应类型为 BinderProxy
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
if (dataObj == NULL) {
jniThrowNullPointerException(env, NULL);
return JNI_FALSE;
}

// Java 对象 转为 c++ 对象
Parcel* data = parcelForJavaObject(env, dataObj);
if (data == NULL) {
return JNI_FALSE;
}

// Java 对象 转为 c++ 对象
Parcel* reply = parcelForJavaObject(env, replyObj);
if (reply == NULL && replyObj != NULL) {
return JNI_FALSE;
}

//拿到 BinderProxyNativeData 成员的 mObject,实际是一个 BpBinder
IBinder* target = getBPNativeData(env, obj)->mObject.get();
if (target == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!");
return JNI_FALSE;
}

ALOGV("Java code calling transact on %p in Java object %p with code %" PRId32 "\n",
target, obj, code);


bool time_binder_calls;
int64_t start_millis;
if (kEnableBinderSample) {
// Only log the binder call duration for things on the Java-level main thread.
// But if we don't
time_binder_calls = should_time_binder_calls();

if (time_binder_calls) {
start_millis = uptimeMillis();
}
}

//BpBinder 发起远程调用
status_t err = target->transact(code, *data, reply, flags);

if (kEnableBinderSample) {
if (time_binder_calls) {
conditionally_log_binder_call(start_millis, target, code);
}
}

if (err == NO_ERROR) {
return JNI_TRUE;
} else if (err == UNKNOWN_TRANSACTION) {
return JNI_FALSE;
}

signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
return JNI_FALSE;
}


可以看出,绕了一圈还是通过 native 层的 BpBinder 发起远程调用,native 层的调用过程可以参考之前的文章Binder 服务注册过程情景分析之 C++ 篇


关于


我叫阿豪,2015 年本科毕业于国防科技大学指挥自动化专业,毕业后,从事信息化装备的研发工作。主要研究方向为 Android Framework 与 Linux Kernel,2023年春节后开始做 Android Framework 相关的技术分享。


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

那年毕业前,我花了一整个上午的时间走遍整个校园

web
又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情... 其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片。 那些照片 🔼 这张照片...
继续阅读 »

又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情...


其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片


那些照片


木头 2023-06-06 19.29.22.jpeg


🔼 这张照片左边黑白的是一天前拍的,右边是今天拍的,当时毕业典礼刚结束,我们要从体育馆走回计算机学院,人很多,大家边走边笑着,感觉就像... 对,就像是刚入学报到的那一天,那天也像这天一样热闹。今天的路上没有人,天气很好,只有静悄悄的阳光撒在地面上。


木头 2023-06-06 19.35.20.jpeg


🔼 这张照片是宿舍楼旁边的一条小路,路中间是校医院。七天前吃饭的时候路过这里,随手拍了一张照片,今天有风,落了一地小花,还挺好看。


木头 2023-06-06 19.40.15.jpeg


🔼 这是学校操场,28天前正好是在做毕业设计的阶段,在宿舍一坐就是一天,没思路的时候,我就喜欢一个人来看台上坐一会儿,戴着耳机灯歌,看下面跑步的人,等操场上的灯关了,我就回去。当时是一个傍晚,夕阳挺好看的。今天的天气很好,有很多人在跑步。


木头 2023-06-06 19.44.53.jpeg


🔼 老照片拍摄于596天前,快两年了吧... 那时候应该大三,中午刚下课,大家走在南广场上,一块去食堂,那天的云很好看。当时在拍新照片的时候,心里还是有点伤感,明天就要离校了,很多人很可能就再也见不到了...(现在来看,的确是这样)


木头 2023-06-06 19.49.36.jpeg


🔼 上学的时候,每天早晨我都起的很早,六点多就从宿舍出来,其实也不是为了学习或者什么,我就喜欢走在安静的校园里,阳光洒在草丛间,偶尔有鸟叫声,我觉得这种感觉很美好。拍左边图的时候应该是个深秋了吧,树叶落了一地,而右边又正好是一个盛夏,树木生长的正好!这张照片的对比感让我感到无比惊喜!


拍摄心得


其实当时拍的比这些成品照片要多得多,总共拍了20多个地方吧,最终只合成出了78张可以用的,废片率相当高


因为什么呢?


原因是当时的我没有一个准确的参照物,在拍新照片时我一般会历经以下步骤:


1、先拿出手机看看老照片的角度和位置


2、举起ipad,凭感觉走到自己认为准确的位置上


3、拍一张看一下效果


4、满意就再拍两张当备份,不满意就继续重复以上步骤,直到排除满意的


好像一个递归方法!用伪代码实现一下就是:


const takePhoto = () => {
// 1、拿出老照片看角度 + 位置
const { position } = showOldPhoto();

// 2、拿出ipad,走到对应的位置上
walkToPosition(position);

// 3、拍一张看看效果
const { isOK } = takeSomePhoto();

// 4、判断是否满意,满意就结束,不满意就继续递归
!isOk && takePhoto();
}

当然,我也没那么工匠精神,我可能还得再加一个结束条件


const takePhoto = () => {
// 如果拍5次还不满意,就
if(reTryTime > 5) {
return;
}

// others
}

这个过程是比较重复且枯燥的,当然可以适当优化一下比如我可以在ipad上看照片,这样就省掉手机这一步了,另外可以在拍摄时不断地切换照片和相机app,这样就可以稍微快点看到当前位置对不对了...


em... 当时的我真的希望有一个工具能来辅助我拍这些照片!


噢噢噢!


现在的我可以很开心的跟那时候的我说,有了!现在有了!


你可以去微信小程序里搜:历旧弥新


你就可以搜到一个看起来还蛮专业的一个小程序,UI做的也不错,不丑!


它好像提供了一个你非常需要的功能:和旧照片来一场对话


你可以非常轻松的用它来拍一张新旧照片合成的照片,


就像下图:


WechatIMG301.jpeg


你可以将你的旧照片半透明的状态覆盖到相机上(就像左边的图),可以缩放平移,把它放在准确的位置上之后,然后你就可以非常轻易的去拍摄相同角度的照片了!


嗯... 听到这里是不是感觉出来这是一个广告了哈哈哈,没错,那就是了!


打广告!


对,这就是我基于四年前的想法,最近花了几个周末开发的一个小程序,历旧弥新


名字取自 历久弥新 => ,代表一种新旧交替的含义


来看下小程序首页


木头 2023-06-06 21.31.46.jpeg


它一共包含四个功能:


1、与旧照片来一次对话


2、已有关联的照片拼接


3、快速找一个相同的拍照姿势


4、异地也可以来合照


我们也提供了比较好的一些用户拍摄过的照片,放在首页的下半部分:


木头 2023-06-06 21.54.15.jpeg


木头 2023-06-06 21.57.31.jpeg


你可以快速的进行 拍同款 !就可以拍摄类似的照片啦!


当然它或许也存在一些问题希望大家不要吝啬自己的建议,可以评论在下方哈!

作者:木头就是我呀
来源:juejin.cn/post/7242247549511663672

收起阅读 »

Vue KeepAlive 为什么不能缓存 iframe

web
最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下: <router-view v-slot="{ Component ...
继续阅读 »

最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:


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

看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。


我们先了解下 KeepAlive


KeepAlive (熟悉的可跳过本节)


被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现“假卸载”, 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated 和 deactivated


keepAlive 是需要渲染器支持的,在执行 mountComponent 时,如果发现是 __isKeepAlive 组件,那么会在上下文注入 move 方法。


function mountComponent(vnode, container, anchor) {
/**... */
const instance = {
/** ... */
state,
props: shallowReactive(props),
// KeepAlive 实例独有
keepAliveCtx: null
};

const isKeepAlive = vnode.__isKeepAlive;
if (isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
};
}
}

实现一个最基本的 KeepAlive,需要注意几个点



  1. KeepAlive 组件会创建一个隐藏的容器 storageContainer

  2. KeepAlive 组件的实例增加两个方法 _deActive_active

  3. KeepAlive 组件存在一个缓存的 Map,并且缓存的值是 vnode


const KeepAlive = {
// KeepAlive 特有的属性,用来标识
__isKeepAlive: true,
setup() {
/**
* 创建一个缓存对象
* key: vnode.type
* value: vnode
*/

const cache = new Map();
// 当前 keepAlive 组件的实例
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
// 创建隐藏容器
const storageContainer = createElement('div');

// 为 KeepAlive 组件的实例增加两个方法
instance._deActive = vnode => {
move(vnode, storageContainer);
};
instance._active = (vnode, container, anchor) => {
move(vnode, container, anchor);
};

return () => {
// keepAlive 的默认插槽就是要被缓存的组件
let rawVNode = slot.default();
// 不是组件类型的直接返回,因为其无法被缓存
if (typeof rawVNode !== 'object') {
return rawVNode;
}

// 挂载时,优先去获取被缓存组件的 vnode
const catchVNode = cache.get(rawVNode.type);
if (catchVNode) {
rawVNode.component = catchVNode.component;
// 避免渲染器重新挂载它
rawVNode.keptAlive = true;
} else {
// 如果没有缓存,就将其加入到缓存,一般是组件第一次挂载
cache.set(rawVNode.type, rawVNode);
}
// 避免渲染器真的把组件卸载,方便特殊处理
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
return rawVNode;
};
}
};

从上可以看到,KeepAlive 组件不会渲染额外的内容,它的 render 函数最终只返回了要被缓存的组件(我们称要被缓存的组件为“内部组件”)。KeepAlive 会对“内部组件”操作,主要是在其 vnode 上添加一些特殊标记,从而使渲染器能够据此执行特殊的逻辑。


function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (type === 'string') {
/** 执行普通的标签 patch */
} else if (type === Text) {
/** 处理文本节点 */
} else if (type === Fragment) {
/** 处理Fragment节点 */
} else if (typeof type === 'object') {
if (!n1) {
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor);
} else {
mountComponent(n2, container, anchor);
}
} else {
patchComponent(n1, n2, anchor);
}
}
}

function unmount(vnode) {
const { type } = vnode;
if (type === Fragment) {
/**... */
} else if (typeof type === 'object') {
if (vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree);
}
return;
}
}

从上面的代码我们可以看出,vue 在渲染 KeepAlive 包裹的组件时,如果有缓存过将执行 keepAliveInstance._activate,在卸载时将执行 keepAliveInstance._deActivate


原因


通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。


解决方案


思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。



  1. 在路由注册时,将 component 赋值为一个空组件


  {
path: "/chathub",
name: "chathub",
component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数
},


  1. 在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏


  <ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>


  1. 监听路由的变化,改变 iframe 的显/隐


const isChatHubPage = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const chatHubVisited = ref(false)

watch(
() => routes.path,
(value) => {
if (value === '/chathub') {
chatHubVisited.value = true
isChatHubPage.value = true
} else {
isChatHubPage.value = false
}
},
{
immediate: true
}
)
作者:莱米
来源:juejin.cn/post/7246310077233659941

收起阅读 »

Flutter 初探原生混合开发

转载请注明出处:juejin.cn/post/724677… 本文出自 容华谢后的博客 0.写在前面 现如今跨平台技术被越来越多的开发者提起和应用,从最早的Java到后来的RN、Weex,到现在的Compose、Flutter,大前端已经成为了趋势,很多公司...
继续阅读 »

转载请注明出处:juejin.cn/post/724677…


本文出自 容华谢后的博客



0.写在前面


现如今跨平台技术被越来越多的开发者提起和应用,从最早的Java到后来的RN、Weex,到现在的Compose、Flutter,大前端已经成为了趋势,很多公司为了节省成本,包括一些大厂已经在Android和iOS平台上使用了Flutter技术,效果还可以,贴近原生但是还会有一些卡顿的问题,好在Flutter目前还在不断的优化更新,希望越来越好吧。


Flutter从2017年发布到现在已经历经了6年,如果你现在创建一个Flutter项目,会发现已经支持了Android、iOS、Linux、MacOS、Web、Windows六大主流的操作系统平台,我以前经常会写一些在Windows上运行的小工具,使用java写的不仅复杂界面也不好看,用Flutter试了试,好像发现了新大陆,在PC上运行十分流畅,还直接支持在其他平台上运行,感觉十分不错,这也让我对未来Flutter的发展抱有期待。


Flutter开发有两种方式,一种是纯Flutter开发,一种是Flutter+原生的开发方式,正如上面所说的,Flutter在PC上运行十分流畅,可能是PC配置比较高的原因,但是在客户端上的运行效果却不如人意,启动有点慢,一些复杂列表有点卡,一些底层功能的API不支持,这就需要原生开发的介入,小部分原生+大部分Flutter开发可能是后续比较主流的一种开发方式。


本文主要讲的是在Android平台上,与Flutter混合开发的一些步骤,一起来看下吧。


1.准备


1.1 先贴下我用的开发环境:




  • 操作系统:Windows 10




  • IDE:Android Studio Flamingo




  • Android SDK:33




  • Gradle:8.0.2




  • JDK:17.0.7




  • Flutter:3.10.4




1.2 下载Flutter SDK


下载地址:docs.flutter.dev/get-started…


是个压缩包,解压到你存放开发环境的目录,然后在AS中打开 File->Settings->Languages&Frameworks,在里面配置一下SDK的路径就可以了。


1.3 配置环境变量


和Jdk一样,为了使用方便,还需要配置下环境变量,设置->关于->高级系统设置->环境变量,找到用户变量,在Path里面新增一个路径 flutter SDK的路径\bin,前面如果有值的话,别忘了在前面加个英文分号进行分割。


1.4 检测flutter状态


为了验证Flutter是否安装成功,打开cmd命令行,输入 flutter doctor 进行检测:


flutter doctor


如果出现上面的提示,是因为Android证书的问题,再输入 flutter doctor --android-licenses 进行修复:


不支持Jdk 1.8版本


可能会出现这样的错误,这个是因为JDK版本有点低,现在大部分还是用的1.8版本,安装配置下JDK 17就可以,再运行下flutter doctor,已经可以了:


flutter doctor通过


1.5 安装Flutter插件


在AS中打开 File->Settings->Plugins,安装下面两个插件:


插件


到这里,所有的准备工作就完成了,接下来去创建项目。


2.创建项目


首先创建一个标准的Android项目,在此基础上,打开 File->New->New Flutter Project 创建一个Flutter Module:


创建Flutter Module


注意Project location要选择你当前的工程目录,Project types选择Module,然后CREATE,看下创建好的目录结构:


目录结构


3.项目Flutter配置


打开项目根目录的settings.gradle配置文件,增加下面的配置:


// Flutter配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir,
'flutter_lib/.android/include_flutter.groovy'
))
include ':flutter_lib'


然后再修改下dependencyResolutionManagement,把FAIL_ON_PROJECT_REPOS 改成 PREFER_SETTINGS,增加flutter的maven仓库地址:


dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven {
allowInsecureProtocol = true
url "http://download.flutter.io"
}
}
}

找到flutter_lib->.android->Flutter->build.gradle,在android属性增加namespace,这个是Gradle 8.0新增的特性:


android {
namespace 'com.example.flutter_lib'
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
...
}

找到主app的build.gradle,在dependencies中引用flutter模块,注意模块名称是flutter,无论你创建的Moudle是什么名字,这里的名字都是flutter:


dependencies {
...
implementation project(':flutter')
}

4.开始使用


在清单文件中,增加下面的activity标签,注意这个Activity是SDK中自带的,不需要自己手动创建:


<application>
...

<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" />
</application>

在MainActivity写个跳转方法进行测试:


val intent = FlutterActivity
.withNewEngine()
.initialRoute("home")
.build(this)
startActivity(intent)

看下效果:


跳转效果


可以看到在点击跳转按钮后,有一个明显的停顿,这是因为初始化Flutter引擎比较慢导致的,那就提前初始化试试,在Application中初始化引擎:


class App : Application() {

override fun onCreate() {
super.onCreate()
// 创建 Flutter 引擎
val flutterEngine = FlutterEngine(this)
// 指定要跳转的flutter页面
flutterEngine.navigationChannel.setInitialRoute("main")
flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
// 这里做一个缓存,可以在适当的时候执行它,例如app里,在跳转前执行预加载
val flutterEngineCache = FlutterEngineCache.getInstance()
flutterEngineCache.put("default_engine_id", flutterEngine)
}
}

然后使用已经提前创建后的引擎再次跳转:


val intent = FlutterActivity
.withCachedEngine("default_engine_id")
.build(this)
startActivity(intent)

看下效果,已经非常丝滑了:


优化后跳转效果


5.写在最后


GitHub地址:github.com/alidili/Flu…


到这里,Flutter与原生混合开发的基本步骤就介绍完了,如有问题可以给我留言评论或者在GitHub中提交

作者:容华谢后
来源:juejin.cn/post/7246778558248058938
Issues,谢谢!

收起阅读 »

拉新、转化、留存,一个做不好,就可能会噶?

用户周期 对于我们各个平台来说(掘金也是),我们用户都会有一个生命周期:引入期--成长期--成熟期--休眠期--流失期。 而一般获客就在引入期,在这个时候我们会通过推广的手段进行拉新;升值期则发生在成长期和成熟期,在两个阶段,我们会通过各种裂变营销(比如红包、...
继续阅读 »

用户周期


对于我们各个平台来说(掘金也是),我们用户都会有一个生命周期:引入期--成长期--成熟期--休眠期--流失期。


而一般获客就在引入期,在这个时候我们会通过推广的手段进行拉新;升值期则发生在成长期和成熟期,在两个阶段,我们会通过各种裂变营销(比如红包、券、积分、满减等手段)去实现用户的转化;那到了休眠期和流失期,平台则会通过精准营销去实现用户的留存。


详细的表格可以看下面:


image.png


运营手法


讲完了用户周期,我们再来说一下运营手法(让大家做一只明白的羊)。目前比较主流的运营手法包括红包、优惠券、返现、积分、裂变营销、砍价、秒杀......


拼xx新人红包:


image.png


饿了X红包:


image.png


拼xx砍价


image.png


其他我们就不列举了,大家生活中肯定有很多体会。在不同的行业,我们被“安排”的方式是不一样的。我们拿3个行业来举例子:


image.png


潜在风险


那从企业的角度出发,在这些环节当中,有可能会出现如下的一些常见风险:客户端风险、账号安全风险、营销活动风险、交易支付风险、爬虫风险。我们一一来过一下这些风险:


1.客户端风险:


image.png


2.账户安全风险


image.png
3.营销活动风险


image.png
4.交易支付风险


image.png
5.爬虫风险


image.png


黑灰产风险


1.简单介绍


目前我们网络黑灰产的从业人数已经超过1000万(和今年毕业的大学生人数有的一比了),其产业造成的损失每年也已经超过千亿。如今数据泄露已经成为社会问题,也引起了各大企业的重视。并且有个点(可能会被喷),部分的黑灰产的安全攻防人员专业度已经超过我们很多安全技术人员了。毕竟只有千年做贼的,没有千年防贼的。


那在我们的AI技术加持之下,我们后续的黑灰产发展必将和产业链会进一步深度融合,同时目前有一个很显著的特征是:黑灰产正在尝试将自己的攻击行为隐藏在其他用户的行为之。


另外,目前受攻击比较多的行业会包括说电商、出行、政务、直播、广告、游戏、社交等,而分布的场景则是我们前面说的爬虫攻击、薅羊毛、账户风险、交易支付等等。


2.欺诈流程


我们来讲一下黑灰产一般的攻击手段,也就是欺诈的一个流程:


image.png


第一步:账号准备。这一步会包括说图里的社工、注册机这些比较常用和典型的,也有其他一些方式。


第二步:情报收集。这一步包括流程的体验以及工具的准备。


第三步:伺机而发。等待活动开始,直接上去薅羊毛


第四步:利益套现。他们会代理或者海鲜市场等等,进行套现。代理的话,游戏行业会用的更多,而海鲜市场,类似于电商的优惠券之类的,会用的比较多。


欺诈工具


目前主流的欺诈工具有如下几种:


1.模拟器:这个主要是针对弱防护的场景


2.设备牧场:现在是应该发展到了第三代全托管牧场。一般可以去做设备识别,针对环境监测和真机检测。


3.接码平台:这个大家应该很熟悉。我们公司做的是安全验证码,而来注册的一部分客户则来找的是接码平台。


4.打码平台:这个其实和接码平台是类似的,不过接码平台针对的是短信验证码,而打码平台针对的是滑动验证码、图片验证码等等。


举个例子:某宝KFC代下单服务泛滥
image.png


这块我就不介绍更多了,生怕有人学坏哈哈


防护措施


因为企业目前对这一块都比较重视,所以随之而来的安全产品目前也发展到了一定的阶段。总的来说,目前一般会采取如下的防御体系:


image.png


基本是3个平台+3个场景+2个服务


产品一般会组合使用(单个场景针对使用也是可以的),会包括:设备指纹+无感验证+端加固+安全SDK
平台:实施决策平台+智能建模平台+关联网络平台
场景:基本上针对的场景就是我前面说的那些营销场景


方案优势


那通过上面这一套方案,我们可以做到:


事前: 需要在事前事中事后多点进行布控,各环节分别进行防控。


事中: 事中的防控可以将更多的黑名单数据反馈到事前环节的判断。


事前: 事后的分析与建模可以将模型能力赋予事中的风险防控,同时也可以积累大量的黑样本供事前风险防控来使用。


image.png


基于标准数据接口进行的模块化组合设计,基于成熟的技术架构和技术优势,可定制、可扩展、可集成、跨平台,在个性化需求的处理方面,有着很好的优势。产品各个模块之间既可相互组合又可自定义配置,灵活的产品配置方式和架构设计思想,可结合不同的业务场景及系统状况进行相应的风险防控方案配置。


结语


在现在AI诈骗频发的时代,其实更受冲击的是金融银行的业务安全,因为我们账号汇款目前用的比较多的是人脸识别,那通过AI换脸,黑灰产完全可以实现相应的技术替换。虽然说这个是用户自己的信息泄露导致的安全问题,但是在问责上,肯定银行也会或多或少受到影响,所以最好是能够有一个合适的风控系统去进行相应的处理。


等端午结束之后吧,有空写一篇关于AI诈骗横行的当下,金融银行要如何应对。<

作者:昀和
来源:juejin.cn/post/7246571942329172027
/p>

以上。

收起阅读 »

10年技术进阶路,让我明白了这3件事

这篇也是我分享里为数不多 “进阶” 与 “成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。 十年...
继续阅读 »

这篇也是我分享里为数不多 “进阶”“成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。


十年,谁来成就你?


  离开校园,一晃已十年,时日深久,现在我已成为程序员老鸟了,从软件工程师到系统架构师,从被管理者到部门负责人,每一段经历的艰辛,如今回忆仍历历在目。各位同行你们可能正在经历的迷茫,焦虑与取舍,我也都曾经历过。


  今天我打算跟大家分享下我这些年的一个成长经历,以此篇文章为我十年的职业历程画上一个完满的句号。这篇文章虽说不是什么“绝世武功”秘籍,更没法在短时间内把我十年的“功力”全部分享于你。篇幅受限,今天我会结合过往种种挑重点说一说,大家看的过程中,记住抓重点、捋框架思路就行了。希望在茫茫人海之中,能够给到正在努力的你或多或少的帮助,亦或启发与思考。


试问,你的核心竞争力在哪?


  你曾经是否怕被新人卷或者代替?如果怕、担忧、焦虑,我可以很负责任地告诉你,那是因为你的核心竞争力还不够!这话并不好听,但,确是实在话。认清现状,踏实走好当下就行,谁能一开始或者没破茧成蝶时就一下子有所成就。


  实质上,可以这么说,经验才是我们职场老鸟的优势。 但是,经验并不是把同一件事用同一种方式重复做多少年,而是把咱们过往那么多年头的实践经验,还有被验证的理论,梳理成属于自己的知识体系,建立一套自己的思维模式,从而提升咱们的核心竞争力。


    核心竞争力的形成,并非一蹴而就,我们因为积累所以专业,因为专业所以自信,因为自信所以才有底气。积累、专业、自信、底气之间的关系,密不可分。


核心竞争力,祭出三板斧


  道理咱们都懂,能不能来点实在的?行!每当身边朋友或者后辈们,希望我给他们传授一些“功力”时,我都会给出这样的三个建议:



  1. 多面试,验本事。

  2. 写博客,而且要坚持写。

  3. 拥有自己的 Github 项目。 



  其中,博客内容和 Github 项目,将会成为咱们求职道路上的门面,这两者也是实实在在记录你曾经的输出,是非常有力有价值的证明。此外,面试官可以通过咱们的博客和 Github,在短时间内快速地了解你的能力水平等。或许你没有足够吸引、打动人的企业背景,也没有过硬的学历。但!必须有不逊于前两者的作品跟经历。


  再说说面试,我认为,它是我们接受市场与社会检验的一种有效方式。归根结底,咱们所付出的一切,都是为了日后在职业发展上走得越来越好。有朋友会说,面试官看这俩“门面”几率不大,没错,从我多年的求职经历来看,愿意看我作品的面试官也只占了 30%。


  但是,谁又能预判到会不会遇到个好机会呢?有准备,总比啥也没有强,千里马的亮点是留给赏识它的伯乐去发现的


PS:拥有自己 Github 项目与写博,都属于一种输出的方式,本文就以写博作为重点分享。写博与面试会在下文继续展开。


记忆与思考,经验与思维


  武器(三板斧)咱们已经有了,少了“内功心法”也不行。这里分享下我的一些观点,也便于大家后续能够更好地参与到具体的实践中。




  • 记忆——记忆如同对象一样是具有生命周期,久了不用就会被回收(忘记)。




  • 思考——做任何事情就如同咱们写代码Function一样,得有输入同时也得有输出,输入与输出之间还得有执行。






  •  




  日常工作中,就拿架构设计当例子。作为架构师是需要针对现有的问题场景提出解决方案,作为架构师的思考输入是业务场景、团队成员、技术选型等,而它的输出就是基于前面的多种输入参数从而产出的短期或长期的解决方案,而且最终会以文档形式保存下来。


  保存下来的目的,是为方便我们日后检索、回忆、复用。因此,在业余学习中同理,给与我们的输入是书籍、网络的资料或同行的传递等,而作为输出则是咱们记录下来的笔记、博客甚至是 Github 的项目 Demo。



基于上述,我们需要深刻意识到心法三要素:



  1. 带着明确的输出目的,才会真正地促进自己的思考。蜻蜓点水、泛泛而谈,是无法让自己形成对事物的独特见解和具象化输出,长期如此,并无良益。

  2. 只有尽可能通过深度思考过后的产出,才能够形成属于自己真正的经验。

  3. 知识的点与点之间建立联系,构成明晰的知识体系,经验与经验则形成了自己独有的思维模式。


多面试,验本事


  既然“武器”和“内功心法”咱们都有了,那么接下来得开始练“外功”了,而这一招叫"多面试,验本事"。


  我身边的同行与朋友,对我的面试行为感到奇怪:你每隔一段时间就去面试,有时拿到了 offer 还挺不错的,但是又没见想着跳槽,这是为何?


风平浪静,居安思危


  回应这个疑问之前,我想反问大家 4 个问题:



  1. 是否曾遇到过在一家公司呆了太久过于安逸,也阶段性想过离开,发现真要走可却没了跳槽的勇气?

  2. 再想一想,日子一久,你们是不是就不清楚行业与市场上,对人才能力的需求了?

  3. 是否有经历过公司意外裁员,你在找工作的时段里有没有强烈感受到那种焦虑、无助?

  4. 是否对来之不易的 offer,纠结不知道如何抉择,又或者,最终因为迫于各方面压力,勉为其难接受了不太中意的那个?



  刚提到的种种问题,那份焦虑、无助、纠结与妥协,我曾经在职场都经历过。我们想象一下,如果你现在随随便便出去面试五个公司能拿到三四个 offer,你还会有那失业的焦虑么?如果现在拿到的那几个 offer 正好都不喜欢,你全部放弃了,难道你会愁后续没有其他机会了么?显然不会!因为你有了更多底气和信心


  我再三思考,还是觉得有必要给大家分享一个我的真实经历。希望或多或少可以给你一点启发:


  2019 年,因为 A 公司业务原因,我离开了工作 3 年的安逸的环境,市场对人才的需求我已经是模糊的了,当我真正面临时,我焦虑、我无助。幸好曾经跟我合作过的老领导注意到了这我这些年的成长,向我施予援手。入职 B 公司后,我重新审视自己,并给与自己定了个计划——每半年选一批公司面试。


一年以后,因为 B 公司因疫情原因,我再次离职。这次,我没有了焦虑,取而代之的是自信与底气,裸辞在家开始了我的休假计划。在整个休假期,我拒绝了两个满足我的高薪 offer,期间我接了个技术顾问的兼职,剩余时间把以前囤下来的书看了个遍,并实践了平常没触碰到的技术盲区。三个月后,我带着饱满的精神面貌再次"出山",入职了现在这家公司。


  有人会问:你现在还有没有坚持自己的面试计划?毫无避讳回答:有!仍然是半年一次。


乘风破浪,未雨绸缪


  就前面这些问题、情况,这里结合我自己多年来的一些经验,也希望给到大家一点破局建议:保持一定的面试频率,就如上文提到的“三板斧”,面试是接受市场与社会检验,非常直接、快速、有效的一种好方式。 当然,我可不是怂恿你频繁跳槽,没有多少公司能够欣然接受不稳定的员工,特别是岗位越做越高时。


  看到这里,有些伙伴可能会想,我现在稳稳当当的、好端端的,干嘛要去面试,何必折腾自己。假若你在体制内,我这点建议或许参考意义不大。抛开体制内的讨论,大家认为真的有所谓的“稳定”的工作吗?


  我认为所谓的“稳定”,都是只是暂时的,甚至虚幻的,没有任何的人、资本、企业能给你实打实的承诺,唯一能让你“稳定”持续发展下去的,只有你的能力与眼界、格局等。


  疫情也有几年了,相信大家也有了更多思考,工作上,副业上等等各方面吧。人无远虑,必有近忧,未雨绸缪,实属必要!



放平心态,查缺补漏


  面试是相对“主观的”,这是因为“人性”的存在,你可能会听过让人哭笑不得的拒绝你的理由:



  • 连这么基础的知识都回答不上,还想应聘这岗位

  • 你的性格并不适合当管理,过于主动对团队不好


  咱们先抛开这观点的对与错。人无完人,每个人都有自己的优点与缺点,甚至你的优点可能是你的缺点。职场长路漫漫,要是把每一次的面试都当成人生中胜负的较量,那咱们最后可能会输的体无完肤。咱们付出任何的努力,也只是单纯提高“成功率”而已。听我一句劝,放平心态,以沟通交流为主,查漏补缺为辅


  近几年我以面架构师和负责人的岗位为主,面试官大多数喜欢问思想和方法论这类的问题,他们拥有不同的细节的侧重点,因此我们以梳理这些“公共”的点出发,事后复盘自己回答的完整性与逻辑性,对于含糊不清的及时找资料补全清晰,尝试模拟当时回答的场景。每一段面试,如此反复。


  作为技术人我建议,除了会干,还得会说,我们不仅有硬实力,还得有软技能。


PS:篇幅有限,具体面试经历就不展开了,如果大家对具体的面试经历感兴趣,有机会我给大家来一篇多年的"面经"。


持续进步


编程语言本身在不断进步,对于菜鸟开发者来说,需要较高的学习成本。但低代码平台天然就具备全栈开发能力,低代码程序员天然就是全栈程序员。


这里非常推荐大家试试JNPF快速开发平台,依托的是低代码开发技术原理,因此区别于传统开发交付周期长、二次开发难、技术门槛高的痛点,在JNPF后台提供了丰富的解决方案和功能模块,大部分的应用搭建都是通过拖拽控件实现,简单易上手,在JNPF搭建使用OA系统,工作响应速度更快。可一站式搭建生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。


开源链接:http://www.yinmaisoft.com/?from=jueji…


狠下心来,坚持到底


锲而舍之,朽木不折;锲而不舍,金石可镂——荀况


  要是把"多面试"比喻成以"攻"为主的招式,而"写博客"则是以"守"为主的绝招。


  回头看,今年,是我写博客的第八个年头了,虽说写博频率不高,但整体时间跨度还是挺大的。至今我还记得我写博客的初心,用博客记录我的学习笔记,同时抛砖引玉,跟同行来个思维上的碰撞。


  随着工作年限的增长,我写博客的内容慢慢从学习笔记变成了实战记录,也越来越倾向于输出经验总结和实践心得。实质上,都是在传达我的观点与见解。


  而这,至关重要。反过来看,后面机会来了,平台联系人也可以借此快速评估、判断这人会不会讲、能不能讲,讲得怎么样,成的话,人家也就快速联系咱了。进一步讲,每一次,于个人而言,都是好机会。



写博第一步,从记笔记开始


  我相信不少的同行曾经面临这样的境况,都有产生过写博客的念头,有些始终没有迈出第一步,有些中途停了下来,这里可能有不少的原因:要么不知道写什么、要么觉得写了也没人看、还有一种是想写但是比较懒等等。


我觉得,一切的学习,前期都是从模仿开始的 学习笔记,它就是很好的便于着手的一种最佳方式。相信大家在学生年代或多或少都写过日记,就算是以流水账的方式输出,博客也可以作为非常好的开启平台。


  由于在写博客的时候,潜意识里会认为写出来的东西会给更多人看,因此自己写的内容在不明确的地方都会去找资料再三确认,这是很有效的一种督促方法。确认的过程中,也会找到许多相关的知识点,自然而然就会进一步补充、完善、丰富我们自己原有或现在的知识体系


幸运,需要自己争取


  在写博客的这段时间里,除了梳理自己的知识体系之外,还能结交了一些拥有共同目标的同行,我想,这就是真正的志同道合吧。


  甚至在你的博客质量达到了一定程度——有深度与广度,会有一些意象不到的额外小收获。例如有一些兼职找到自己,各大社区平台会邀请自己合作,也会收到成就证明与礼物等等。



意外地成为了讲师


  到目前为止,正式作为讲师或者是技术顾问,以这样不同于往常的既有角色,我真切地经历了几次。虽次数不多,但每一次过后,即便时日深久,可现在回想起来,于我的成长而言,那都是一次又一次新的蜕变,真实而猛烈,且带给我一次次新生力量。


  话说回来,前面提到几次分享,有的伙伴可能会说了,这本来就性格好又爱分享的人,个例罢了,不一定适合大多数啊。说到这儿,我想,我有必要简短地跟你聊一下我自己。


跌跌撞撞,逆水行舟


  对于过往的自己,我的评价是从小就闷骚、内向的那种性格,只要在人多的时候发言就会慌会怂会紧张,自己越慌就越容易表达出错,如此恶性循环。随着我写博的篇幅越多,慢慢地我发现自己讲话时喜欢准备与思考,想好了再去表达,又慢慢地讲话就具有条理性与逻辑性了。


  当代著名哲学家陈嘉映先生,他曾在一本书里说过这样一句话,放到这里再合适不过了—— "成长无时无刻不是在克服某些与生俱来的感觉和欲望"


  回头看,一路走来,我从最初的摸索、探索、琢磨,到看到细微变化,到明显感知到更大层面的进步,再到后来的游刃有余,输出很有见地的思考,分享独到观点。


  我想,这背后,离不开一次次尝试,一次次给自己机会,一次次认真、负责地探索突破自己。其实,大多数人,还真是这么跌跌撞撞挺过来的。


伺机而动,用心准备


  2020 年,我第一次被某企业找到邀请我作为技术顾问是通过我的博客,这一次算是小试牛刀,主要以线上回答问题、交流为主。因为事先收集好了需要讨论的话题与问题,整个沟通持续了两个小时,最终也得到了对方老板的高度认可。


  此事过后,我重新审视了自己,虽然我口才并不突出,但是我基于过往积累的丰富经验与知识融合,并能够正确无误地传达输出给对方,我认为是合格的了。坦率来讲,从那之后我不再怀疑自己的表达能力。同时有另外一件事件更值得重视,基于让自己得到更多更广泛的一个关注,思前想后,概括来讲,我还是觉得落到这句话上更合适,就是:建立个人 IP


建立个人 IP


  那么,我希望打造个人 IP 的原因是什么呢?希望或多或少也可以给你提供一点可供借鉴、探讨的方向。


  我个人而言,侧重这样几个层面吧。



  1. 破局: 一个是我希望打破 35 岁魔咒,这本质上是想平稳快速度过职业发展瓶颈期;

  2. 觅友: 希望结识到拥有同样目标的同行,深度交流,构建技术圈人脉资源网;

  3. 动力 从中获取更多与工作不一样的成就感。有了强驱动力,也会使我在分享这条路上变得更坚定。


链接资源,提影响力


  在《人民的名义》里祁同伟说过一句话,咱们就是人情的社会。增加了人脉,就是增加自己的机会。当然前提是,咱们自己得需要有这个实力。


  建立个人 IP,最要提高知名度,而提知名度的主要方式是两种:写书、做讲师。后面我会展开讲,写书无疑是宣传自己的最好方式之一,但整个过程不容易,周期比较长。作为写书的简化版,我们写博客就是一种捷径了。


主动出击,勿失良机


  而作为讲师,线上线下各类形式参与各种社区峰会露脸,这也是一种方式。不过这种一般会设有门槛。


  这里不得不多提一句,就是建立 IP 它是一个循序渐进的过程,欲速则不达,任何时候咱们都得靠内容作品来说话, 当你输出的质量够了,自然而然社区人员、企业就会找到你,机会顺理成章来了。反过来讲,我们也得常盯着,或者说多留心关注业内各平台的内容风格,利用好业余零碎时间,好好梳理下某个感兴趣的内容平台,看看他们到底都倾向于打造什么样的东西。做到知己知彼,很重要。


  我认识的一个前辈,之前阿里的,他非常乐于在博客上分享自己的经验与见解,随着他分享的干货越多,博客影响力越大,某内容付费平台找到他合作出了个专栏,随着专栏的完结,他基于专栏内容又出了一本书,而现在的他已经离开了阿里,成为了自由职业者。


追求成就感,倒逼突破自我


  每一次写博客、做讲师,都能更大程度上填满我内心深处的空洞,或许是每一个支持我的留言与点赞,或许是每一节分享停顿间的掌声。如果我们抱着非常强的目的去做的时候,可能会事与愿违。就以我做讲师来说,因为我是一个新手,在前期资料准备所花费的精力与时间跟后续的课酬是不成正比的。


  作为动力源,当时我会把侧重点放到结交同行上,同时利用“费曼学习法”重新梳理知识,另外寻找机会突破自己的能力上限。



  大家有没有想过,讲课最终受益者的是谁?有些朋友会回答“双方”。但是我很负责任地告诉你,作者、讲师自己才是最大的知识受益者。


  如前面所讲,写博客为了更好地分享出更具价值性的内容,为保证专业性,咱们得再三确认不明确的点,而讲课基于写博客的基础上,还得以听众的角度,去思考、衡量、迭代,看看怎么让人家更好地理解、吸收、用得上这些知识点,甚至讲师得需要提前模拟、预估可能会在课后被提的问题。


这里总结一下,写博客与讲课的方式完全不同,因为博客是以图、文、表的方式展现,读者看不明白可以回头去看,但是讲课则没有回头路,是一环套一环的,所以梳理知识线的连贯性要求更强


  我个人认为,日常工作大多数是重复的、枯燥的,或者说,任何兴趣成了职业,那将不再是兴趣,或许只有在业余的时候获取那些许的成就感,才会重新燃起自己的那一份初心 ——行之于途而应于心。


源不深而望流之远,根不固而求木之长


  求木之长者,必固其根本;欲流之远者,必浚其源泉——魏徵


  有些同行或许会问:”打铁还需自身硬“这道理咱们都懂,成长进阶都离不开学习,但这要是天天写 BUG 的哪来那么多时间学?究竟学习的方向该怎么走呢?在这里分享下我的实际做法,以及一些切身的个人体会,希望可以提供一点借鉴、参考。


零碎时间,稳中求进


  6 年前,我确定往系统架构师这个目标发展的时候,每天都会做这么两件事:碎片化时间学习,及时产出笔记。



  • 上班通勤与中午休息,我会充分利用这些碎片时间(各 30 分钟)尽可能地学习与吸收知识,每天坚持一小时的积累,积少成多,两年后你会发现,效果非常可观,这就是一个量变到质变的过程


  而且有神经科学相关表明,”间歇式模块化学习的效果最佳,通勤路上就是实践这种模式的理想世界。“大家也可以多试试看。当然,一开始你学习某个领域的知识,可能效率没那么高,我建议你可以反复地把某一节掰开了揉碎了看或者听,直到看明白听懂了为止,接着得怎么做?如我前面说,咱们得要有输出!


  看过这样一段话,”写和想是不同的,书写本身就是逻辑推演和信息梳理的过程。“而且,研究表明,”人的记忆力会在 17-24 岁达到高峰,25 岁之后会下降,理解力的发展曲线会延后 5 年,也就是说在 30 岁之后也会下降。“


  你看,这个也直接或者间接告诉我们,还是趁早多做记录、多学习。文字也好,视频也罢,到底啥形式不重要,适合自己能长久坚持的就行,我相信你一定能从中受益。毕竟,这些累积的,可都是你自己实实在在的经验和思考沉淀!


  话说回来,其实做笔记能花多长时间,就算在工作时间花半小时也有良效,而这时间并不会对自己的工作进度造成多么大的影响,但!一定时日深久,受益良多。


构建知识 体系 丰富 思维 模式


  由于我们日常需要快速解决技术难题,很多时候从外界吸收到的知识点相对来说很零散,而知识体系是由点、线、面、体四个维度构造而成的


  那怎么做能够快速把知识串联起来呢?这里我举个简单的例子,方便大家理解。


  以我们系统性能调优出发,首先我们需要了解系统相关性能瓶颈的业务场景是什么?该功能是 I/O 密集型还是 CPU 密集型?如果是 I/O 密集型多数的性能瓶颈在数据库,这个时候我们就得了解数据库瓶颈的原因,究竟是数据量大还是压力大?如果是数据量大,基于现有的业务场景应该选择数据归档、临时表还是分库分表,这之间的方案优缺点有什么不同?适用场景怎么样?假如是数据压力大了,我们是否能用 Redis 做缓存抗压就行?


  再接着从 Redis 这个点继续思考,假如 Redis 内存满了会怎样?我们又了解到了 Redis 的内存淘汰策略,设置了 volatile-lru 策略,由于我们基本功扎实回忆起 LUR 算法是基于链表的数据结构,虽然链表的写的时间复杂度是 O(1),但是读是 O(n),不过我们得先读后写,所以为了高性能又选择 Hash 这种 O(1)的数据结构辅助读的处理。


  你看,我们是不是从问题出发到架构设计,再从数据库优化方案到 Redis 的使用,最后到数据结构,这一些系统的知识就串联起来了?


收起阅读 »

妹纸问我怎么下载B站视频?你等我一下

文章已同步至【个人博客】,欢迎访问【我的主页】😃 文章地址:blog.fanjunyang.zone/archives/do… 前言 今天有一个妹纸向我提出了一个问题 是时候"出手"了,本着助人为乐的精神,这个忙必须帮(没办法,我就喜欢帮助别人) 现在我们...
继续阅读 »

文章已同步至【个人博客】,欢迎访问【我的主页】😃

文章地址:blog.fanjunyang.zone/archives/do…



前言


今天有一个妹纸向我提出了一个问题


docker-alltube-1


是时候"出手"了,本着助人为乐的精神,这个忙必须帮(没办法,我就喜欢帮助别人)


现在我们在下载一些比如:Bilibili,YouTube等第三方视频的时候,还是比较困难的,需要找各种下载器和网站,而且还不一定能下载,一些免费好用的下载网站还不好找。
所以我们可以自己动手搭一个下载站点,来下载各大平台上的视频。


搭建的站点(大家轻点薅):dl.junyang.space/

站点的地址会随着时间更新,如果上面的地址不能访问的话,大家可以去我的 博客 ,我会把站点入口放在【顶部菜单栏】->【百宝箱】里面)


相关链接&环境配置


最好用国外的服务器,如果用国内的服务器,是下载不了YouTube等需要魔法网站的视频的


docker、docker-compose安装:blog.fanjunyang.zone/archives/de…
Nginx Proxy Manager安装使用:blog.fanjunyang.zone/archives/ng…

使用的GitHub的开源项目:github.com/Rudloff/all…

使用的Docker镜像:hub.docker.com/r/dnomd343/…


搭建方式


创建相关目录


mkdir -p /root/docker_data/alltube
cd /root/docker_data/alltube

创建yml文件


version: '3.3'
services:
alltube:
restart: always
container_name: alltube
environment:
# 自己网站的title
- 'TITLE=My Alltube Site'
- CONVERT=ON
- STREAM=ON
- REMUX=ON
ports:
# 左侧端口号换成你服务器上未使用的端口号
- '24488:80'
image: dnomd343/alltube

运行yml文件


进入/root/docker_data/alltube文件夹下面,运行命令:docker-compose up -d


或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/alltube/docker-compose.yml up -d


访问使用


可以直接使用【IP + PORT】的方式访问(需要放通对应端口号的防火墙或安全组)


最好配置反向代理,用域名访问,可以参考:blog.fanjunyang.zone/archives/ng…


她对我说


当我把下载链接发给她时,她说:你真是个好人,正好我让我男朋友也用一下。


我不能忍,然后我默默的把站点删除、下线,眼里留下了悔恨的泪水。


注意事项&问题



  • 目前解析不出来B站的视频封面(YouTube可以正常解析),不过不影响下载

  • 因为B站音视频是分开的,所以需要下载两次(一次视频、一次音频),然后整合一下就好了

  • 因国内版权限制的原因,部分资源无法解析是正常现象

  • 下载的时候可以选择视频格式


docker-alltube-2

收起阅读 »

在这个大环境下我是如何找工作的

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。 已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景...
继续阅读 »

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。
已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景下再加上全世界范围内的经济不景气我想每个人都能感受到寒意。


我还记得大约在 20 年的时候看到网上经常说的一句话:今年将是未来十年最好的一年。


由于当时我所在的公司业务发展还比较顺利,丝毫没有危机意识,对这种言论总是嗤之以鼻,直到去年国庆节附近。


虽然我们做的是海外业务,但是当时受到各方面的原因公司的业务也极速收缩(被收购,资本不看好),所以公司不得不进行裁员;
其实到我这里的时候前面已经大概有 2~3 波的优化,我们是最后一波,几乎等于是全军覆没,只留下少数的人维护现有系统。


这家公司也是我工作这么多年来少数能感受到人情味的公司,虽有不舍,但现实的残酷并不是由我们个人所决定的。


之后便开始漫长的找工作之旅,到现在也已经入职半年多了;最近看到身边朋友以及网上的一些信息,往往是坏消息多于好消息。


市场经历半年多的时间,裁员的公司反而增多,岗位也越来越少,所以到现在不管是在职还是离职的朋友或多或少都有所焦虑,我也觉得有必要分享一下我的经历。


我的预期目标


下面重点聊聊找工作的事情;其实刚开始得知要找工作的时候我并不是特别慌,因为当时手上有部分积蓄加上公司有 N+1 的赔偿,同时去年 10 月份的时候岗位相对于现在还是要多一些。


所以我当时的目标是花一个月的时间找一个我觉得靠谱的工作,至少能长期稳定的工作 3 年以上。


工作性质可以是纯研发或者是偏管理岗都可以,结合我个人的兴趣纯研发岗的话我希望是可以做纯技术性质的工作,相信大部分做业务研发的朋友都希望能做一些看似“高大上”的内容。
这一点我也不例外,所以中间件就和云相关的内容就是我的目标。


不过这点在重庆这个大洼地中很难找到对口工作,所以我的第二目标是技术 leader,或者说是核心主程之类的,毕竟考虑到 3 年后我也 30+ 了,如果能再积累几年的管理经验后续的路会更好走一些。


当然还有第三个选项就是远程,不过远程的岗位更少,大部分都是和 web3,区块链相关的工作;我对这块一直比较谨慎所以也没深入了解。


找工作流水账


因为我从入职这家公司到现在其实还没出来面试过,也不太知道市场行情,所以我的想法是先找几家自己不是非去不可的公司练练手。



有一个我个人的偏好忘记讲到,因为最近的一段时间写 Go 会多一些,所以我优先看的是 Go 相关的岗位。



第一家


首先第一家是一个 ToB 教育行业的公司,大概的背景是在重庆新成立的研发中心,技术栈也是 Go;


我现在还记得最后一轮我问研发负责人当初为啥选 Go,他的回答是:



Java 那种臃肿的语言我们首先就不考虑,PHP 也日落西山,未来一定会是 Go 的天下。



由于是新成立的团队,对方发现我之前有管理相关的经验,加上面试印象,所以是期望我过去能做重庆研发 Leader。


为此还特地帮我申请了薪资调整,因为我之前干过 ToB 业务,所以我大概清楚其中的流程,这种确实得领导特批,所以最后虽然没成但依然很感谢当时的 HR 帮我去沟通。


第二家


第二家主要是偏年轻人的 C 端产品,技术栈也是 Go;给我印象比较深的是,去到公司怎么按电梯都不知道🤣



他们办公室在我们这里的 CBD,我长期在政府赞助的产业园里工作确实受到了小小的震撼,办公环境比较好。



当然面试过程给我留下的印象依然非常深刻,我现在依然记得我坐下后面试官也就是 CTO 给我说的第一句话:



我看过你的简历后就决定今天咱们不聊技术话题了,直接聊聊公司层面和业务上是否感兴趣,以及解答我的疑虑,因为我已经看过你写的很多博客和 GitHub,技术能力方面比较放心。



之后就是常规流程,聊聊公司情况个人意愿等。


最后我也问了为什么选 Go,这位 CTO 给我的回答和上一家差不多😂


虽然最终也没能去成,但也非常感谢这位 CTO,他是我碰到为数不多会在面试前认真看你的简历,博客和 GitHub 都会真的点进去仔细阅读👍🏼。



其实这两家我都没怎么讲技术细节,因为确实没怎么聊这部分内容;这时就突出维护自己的技术博客和 GitHub 的优势了,技术博客我从 16 年到现在写了大约 170 篇,GitHub 上开源过一些高 star 项目,也参与过一些开源项目,这些都是没有大厂经历的背书,对招聘者来说也是节约他的时间。





当然有好处自然也有“坏处”,这个后续会讲到。


第三家


第三家是找朋友推荐的,在业界算是知名的云原生服务提供商,主要做 ToB 业务;因为主要是围绕着 k8s 社区生态做研发,所以就是纯技术的工作,面试的时候也会问一些技术细节。



我还记得有一轮 leader 面,他说你入职后工作内容和之前完全不同,甚至数据库都不需要安装了。



整体大概 5、6 轮,后面两轮都是 BOSS 面,几乎没有问技术问题,主要是聊聊我的个人项目。


我大概记得一些技术问题:



  • k8s 相关的一些组件、Operator

  • Go 相关的放射、接口、如何动态修改类实现等等。

  • Java 相关就是一些常规的,主要是一些常用特性和 Go 做比较,看看对这两门语言的理解。


其实这家公司是比较吸引我的,几乎就是围绕着开源社区做研发,工作中大部分时间也是在做开源项目,所以可以说是把我之前的业余爱好和工作结合起来了。


在贡献开源社区的同时还能收到公司的现金奖励,不可谓是双赢。


对我不太友好的是工作地在成都,入职后得成渝两地跑;而且在最终发 offer 的前两小时,公司突然停止 HC 了,这点确实没想到,所以阴差阳错的我也没有去成。


第四家


第四家也就是我现在入职的公司,当时是我在招聘网站上看到的唯一一家做中间件的岗位,抱着试一试的态度我就投了。
面试过程也比较顺利,一轮同事面,一轮 Leader 面。


技术上也没有聊太多,后来我自己猜测大概率也和我的博客和 Github 有关。




当然整个过程也有不太友好的经历,比如有一家成都的“知名”旅游公司;面试的时候那个面试官给我的感觉是压根没有看我的简历,所有的问题都是在读他的稿子,根本没有上下文联系。


还有一家更离谱,直接在招聘软件上发了一个加密相关的算法,让我解释下;因为当时我在外边逛街,所以没有注意到消息;后来加上微信后说我为什么没有回复,然后整个面试就在微信上打字进行。


其中问了一个很具体的问题,我记得好像是 MD5 的具体实现,说实话我不知道,从字里行间我感觉对方的态度并不友好,也就没有必要再聊下去;最后给我说之所以问这些,是因为看了我的博客后觉得我技术实力不错,所以对我期待较高;我只能是地铁老人看手机。


最终看来八股文确实是绕不开的,我也花了几天时间整理了 Java 和 Go 的相关资料;不过我觉得也有应对的方法。


首先得看你面试的岗位,如果是常见的业务研发,从招聘的 JD 描述其实是可以看出来的,比如有提到什么 Java 并发、锁、Spring等等,大概率是要问八股的;这个没办法,别人都在背你不背就落后一截了。


之后我建议自己平时在博客里多记录八股相关的内容,并且在简历上着重标明博客的地址,尽量让面试官先看到;这样先发制人,你想问的我已经总结好了😂。


但这个的前提是要自己长期记录,不能等到面试的时候才想起去更新,长期维护也能加深自己的印象,按照 “艾宾浩斯遗忘曲线” 进行复习。


选择



这是我当时记录的面试情况,最终根据喜好程度选择了现在这家公司。


不过也有一点我现在觉得但是考虑漏了,那就是行业前景。


现在的 C 端业务真的不好做,相对好做的是一些 B 端,回款周期长,同时不太吃现金流;这样的业务相对来说活的会久一些,我现在所在的公司就是纯做 C 端,在我看来也没有形成自己的护城河,只要有人愿意砸钱随时可以把你干下去。


加上现在的资本也不敢随意投钱,公司哪天不挣钱的话首先就是考虑缩减产研的成本,所以裁员指不定就会在哪一天到来。


现在庆幸的是入职现在这家公司也没有选错,至少短期内看来不会再裁员,同时我做的事情也是比较感兴趣的;和第三家有些许类似,只是做得是内部的基础架构,也需要经常和开源社区交流。


面对裁员能做的事情


说到裁员,这也是我第一次碰上,只能分享为数不多的经验。


避免裁员


当然第一条是尽量避免进入裁员名单,这个我最近在播客 作为曾经的老板,我们眼中的裁员和那些建议 讲到在当下的市场情况下哪些人更容易进入裁员名单:



  • 年纪大的,这类收入不低,同时收益也没年轻人高,确实更容易进入名单。

  • 未婚女性,这点确实有点政治不正确,但确实就是现在的事实,这个需要整个社会,政府来一起解决。

  • 做事本本分分,没有贡献也没出啥事故。

  • 边缘业务,也容易被优化缩减成本。


那如何避免裁员呢,当然首先尽量别和以上特征重合,一些客观情况避免不了,但我们可以在第三点上主动“卷”一下,当然这个的前提是你还想在这家公司干。


还有一个方法是提前向公司告知降薪,这点可能很多人不理解,因为我们大部分人的收入都是随着跳槽越来越高的;但这些好处是否是受到前些年互联网过于热门的影响呢?


当然个人待遇是由市场决定的,现在互联网不可否认的降温了,如果你觉得各方面呆在这家公司都比出去再找一个更好,那这也不失为一个方法;除非你有信心能找到一个更好的,那就另说了。


未来计划


我觉得只要一家公司只要有裁员的风声传出来后,即便是没被裁,你也会处于焦虑之中;要想避免这种焦虑确实也很简单,只要有稳定的被动收入那就无所谓了。


这个确实也是说起来轻松做起来难,我最近也一直在思考能不能在工作之余做一些小的 side project,这话题就大了,只是我觉得我们程序员先天就有自己做一个产品的机会和能力,与其把生杀大权给别人,不如握在自己手里。


当然这里得提醒下,在国内的企业,大部分老板都认为签了合同你的 24 小时都是他的,所以这些业务项目最好是保持低调,同时不能影响到本职工作。



欢迎关注作者公众号于我交流🤗。


作者:crossoverJie
来源:juejin.cn/post/7246570594991718455

收起阅读 »

经济持续低迷环境下,女全栈程序员决定转行了

引言 疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。 近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据...
继续阅读 »

引言


疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。


近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据要多很多,尤其是表示 “一周工作一小时以上” 也纳入了就业范围。


image.png


而从我自己的判断来说,记得我自己在去年8月份被裁之后就在xhs发布了一篇关于个人如何交社保的教程,去年年底,观看浏览量不是特别多,而在今年(从年初至今)浏览量以及收藏量蹭蹭往上涨,几乎是每天都有人浏览和收藏我的帖子,抛去网上数据到底如何,光从我自己的感受来看,今年失业人数比去年更多!


image.png


个人只是随手发了一个帖子,将自己如何交社保的步骤记录下来,就有持续的搜索流量,这绝不是一件好事!说明了哀鸿遍野。


一面广大青少年正值青春鼎盛却面临着就业危机,另一方面还要忍受各种开支的骤增,比如深圳统租房的出现,大批人发声:微棠gun出深圳!



曾经破旧拥挤的城中村,为每一位打工人开启了大城市的入口,虽然这个入口短暂,且在关上门的时候,会毫不犹豫抹去你所有的痕迹。

而今这个入口,它不会再破旧拥挤,但会吸取你身上的最后一滴血。



个人经历


1.行政岗转前端


自己曾经拿着一个一本工科学历,因为厌倦行政岗位的勾心斗角,从而挑灯夜战每天在公司加班学习前端到11点,半路出家转行做了前端程序员。


2.刚转行遇吸血领导


而刚转行,又遇到了极其吸血的创业公司(大小周、从0到1项目,双周迭代迭代加班到2点)。


当时不敢辞职,不外乎有几个原因:



  • 刚转行,自己认为技术还比较菜,不敢辞职,被裁了之后才发现外面一大片天地

  • 真的很忙,根本没有时间提升自我与准备面试。因为呆了两年,我自己上了一次救护车,后来离职之后也发现自己因此得了疲劳综合症

  • 比较会吃苦,当时看来觉得可以忍一忍


关于这家公司呢,我想说,我这领导是真的狗,领导是我大一届的学长,曾经担任了大厂某知名项目的组长,号称协同领域的专家,关于此人是我生活中见过最资本的一个人:



  • 针对刚毕业的新人,不培养下属却对下属有着超乎大厂的要求(毫不夸张,你没经历过就不要觉得我是在夸张)

  • 技术部的同事都是很年轻的,做事都兢兢业业,不甩锅,不摸鱼,很多事都是自发的去解决,关于技术水平,我很客观的评价,不菜

  • 在裁我的时候,我呆的时间是13个月,也就是差一个月满2年,但他忽悠我说法律都规定只能给我1+1,我还不满2年,当时对方忽悠毫不脸红,又本着学长+平时看起来正人君子的偏见,在当时就签署了合同,失去的1个月补偿金还好,最伤人的是利用了你的信任,杀人诛心。


3.持续学习


从吸血公司出来之后,进入了相对比较wlb的公司,也清楚认识到自己在程序员领域,女性并不吃香,因此自己也是一直在学习前端技术。



  • 比如自己也曾在掘金发布了上百的技术文章

  • 买教学课程

  • 从零学算法,刷Leetcode

  • github持续输出代码

  • 建立自己的技术博客


image.png


image.png


4.努力不代表有收获


曾经相信自己勤能补拙,后来发现,比你拙的一大批还比你工资高;

曾经熟悉React技术栈,却在失业时找前端兼职时因不会vue而被刷;

曾经将网上的八股文背了再背,面试一二面对答如流,却倒在了三面面试官深问你项目经验;
曾经以为深耕项目经验,学性能优化、前端工程化、架构,却因为面试不会吹牛且遇上近几年经济低迷环境,工资还是那样。

曾经以为,自己努力点,自己性格好点,不断提升,会迎来比较好的人生。

曾经以为,男女平等,男生不应该一人承担经济压力,所以放弃了沉迷貌美如花,选择了与男生一样扛水桶,挑重活,但事实是,那些每天开开心心负责貌美如花的女生比我这种埋头搞钱的女生要幸福很多,对于像花一样的女生,谁不怜爱宽容呢,谁会去宽容一个扎在程序员堆里放弃自己容貌的黄脸婆呢。(看到这里,也许有人觉得我是因为自己长的太丑了,所以才选择搞钱,然而客观来分析,我自己并不丑,虽然说不是校花班花级别,但也可以在普通人群里说的是中上,颜控党眼里也能过得去,不是普信)



然后事实是,有些人,不用长得漂亮,不用能力强,不用对外提供情绪价值或其他价值,他站在那里,就有好的收获,就有人包容就有人爱。



在经历过上述的心理历程之后,明白了职场规则,以及社会运作规律,在大环境下,每个人都在尽自己的努力维持着公平,这个世界,因为有些人经历坎坷,未能坚守住自己底线,从而世界才会有坏人的存在。但大部分情况是,没有绝对的坏人,比如你觉得领导对自己很吸血,但可能领导背后的压力是整个公司的生存(虽然我的领导真的就是单纯的吸血),比如你觉得有些人对自己戾气重,可能当时人家真的内心极其痛苦,而你刚好撞到了枪口上,比如有些人因为诸多原因对你坏,但可能对别人好。


So,个人而言,还是做好自己,看淡所有的行为,同时能有自己的盾和矛。


决定转行


明白自己确实不适合长久做程序员,因此跟大家一样,网上搜了很多搞副业赚钱的路子,排除了偏门以及刑法上的路子,结合我自己的情况,目前已经开始正式着手Vlog自媒体之路了。



  • 买拍摄工具

  • 打造自己的IP

  • 整理自己的衣着、居住环境

  • 学习自媒体知识、拍摄技巧


总的而言,作为一个硬件工科出身的妹子,一直觉得自己更喜欢软件,比如硬件我要调试半天的电路我才能把一个灯泡💡点亮,而计算机,我写一行代码就可以得到反馈,即使是错误的,也能快速做出调整。


但也不可否认,女生在敲代码方面确实跟男生比没有那么大的天赋,就好比玩游戏,大部分女生会玩游戏,但是如果说要打的特别好,男生还是居多。


所以自己也很佩服那些在代码这条路上走的很坚定的女程序员。一起加油吧。


最后,我给各位女程序猿一个小建议,如果没有很高的学历背景或比较好的人脉资源运气,我觉得趁早搞一个副业,但是绝对不要裸辞去搞副业。程序员这个岗位虽然目前已经卷的不行,但瘦死的骆驼比马大,比某些天坑行业还是好很多,我觉得我们还是很幸运的。


image.png

收起阅读 »

Spring Boot如何优雅实现结果统一封装和异常统一处理

1.概述 当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示 Restful风格是...
继续阅读 »

1.概述


当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示


Restful风格是什么?


RESTFUL(英文:Representational State Transfer,简称REST)可译为"表现层状态转化”,是一种网络应用程序的设计风格和开发方式,是资源定位和资源操作的一种风格。不是标准也不是协议。基于HTTP可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点。Restful风格最大的特点为:资源、统一接口、URI和无状态。


对于我们Web开发人员而言,restful风格简单来说就是使用一个url地址表示一个唯一的资源。然后把原来的请求参数加入到请求资源地址中。把原来请求的增,删,改,查操作路径标识,改为使用HTTP协议中请求方式GET、POST、PUT、DELETE表示。


传统的方式是:http://127.0.0.1:8080/shepherd/user/add 表示新增用户的接口,需要在路径上加以增删改查标识,如果我们要修改:那么路径是:http://127.0.0.1:8080/shepherd/user/update


但是我们基于restful风格就比较优雅:http://127.0.0.1:8080/shepherd/user,增删改查都可以用这个路径,使用请求方法来进行区别即可,如post代表新增,put代表修改等。



项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记



2.返回结果统一封装


定义一个统一的标准返回格式,有助于后端接口开发的规范性和通用性,同时也提高了前后端联调的效率,前端通过接收同一返回结构体进行相应映射处理,不用担心每个接口返回的格式都不一样而做一一适配了。


2.1 定义返回统一结构体

@Data
public class ResponseVO<T> implements Serializable {

   private Integer code;

   private String msg;

   private T data;

   public ResponseVO() {

  }

   public ResponseVO(Integer code, String msg) {
       this.code = code;
       this.msg = msg;
  }

   public ResponseVO(Integer code, T data) {
       this.code = code;
       this.data = data;
  }

   public ResponseVO(Integer code, String msg, T data) {
       this.code = code;
       this.msg = msg;
       this.data = data;
  }

   private ResponseVO(ResponseStatusEnum resultStatus, T data) {
       this.code = resultStatus.getCode();
       this.msg = resultStatus.getMsg();
       this.data = data;
  }

   /**
    * 业务成功返回业务代码和描述信息
    */
   public static ResponseVO<Void> success() {
       return new ResponseVO<Void>(ResponseStatusEnum.SUCCESS, null);
  }

   /**
    * 业务成功返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> success(T data) {
       return new ResponseVO<T>(ResponseStatusEnum.SUCCESS, data);
  }

   /**
    * 业务成功返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> success(ResponseStatusEnum resultStatus, T data) {
       if (resultStatus == null) {
           return success(data);
      }
       return new ResponseVO<T>(resultStatus, data);
  }

   /**
    * 业务异常返回业务代码和描述信息
    */
   public static <T> ResponseVO<T> failure() {
       return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
  }

   /**
    * 业务异常返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus) {
       return failure(resultStatus, null);
  }

   /**
    * 业务异常返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus, T data) {
       if (resultStatus == null) {
           return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
      }
       return new ResponseVO<T>(resultStatus, data);
  }

   public static <T> ResponseVO<T> failure(Integer code, String msg) {
       return new ResponseVO<T>(code, msg);
  }
}


这里包含了三个字段信息:



  1. code 状态值:由后端统一定义各种返回结果的状态码, 比如说code=200代表接口调用成功

  2. msg 描述:本次接口调用的结果描述,比如说后端抛出的业务异常信息就在这里体现

  3. data 数据:本次返回的数据,泛型类型意味着可以支持任意类型的返回数据


成功返回如下:

{
 "code": 200,
 "msg": "OK",
 "data": {
   "id": 123,
   "name": "shepherd"
}
}

业务异常返回如下:

{
 "code": 400,
 "msg": "当前用户不存在"
}

按照上面成功返回的示例我们接口定义如下:

    @GetMapping("/test/user")
   public ResponseVO<User> testUser() {
       User user = new User();
       user.setId(123l);
       user.setName("shepherd");
       return ResponseVO.success(user);
  }

可以看到接口方法返回类型为ResponseVO<User>,然后通过ResponseVO.success()对返回结果进行包装后返回给前端。这就意味着写一个接口都需要调用ResultData.success()这行代码对结果进行包装,有点重复劳动不够优雅的感觉。还有一种情况,有些项目服务前期为了赶时间开发时没有返回统一结构,等项目上线了有时间之后按照规范需要对后端接口返回结构进行统一,这时候如果复杂的系统已经有成百上千的接口了,如果一个个地像上面说的那样把接口返回类型改为ResponseVO<T>,再用ResponseVO.success()进行结果包装,工作量不小,也比较繁琐。


2.2 高级优雅实现统一结果封装


为了解决上面阐述的问题,我们借助于Spring Boot提供的ResponseBodyAdvice进行了高级实现。


ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。 我们在分享 Spring Boot如何对接口参数进行加解密就有提到过这个类进行返回结果参数的加密。


先来看下ResponseBodyAdvice的源码

public interface ResponseBodyAdvice<T> {
 /**
 * 是否支持advice功能
 * true 支持,false 不支持
 */
   boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);

  /**
 * 对返回的数据进行处理
 */
   @Nullable
   T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}

所以我们编写一个具体实现类即可:

@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
   @Resource
   private ObjectMapper objectMapper;

   private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;

   /**
    * 判断类或者方法是否使用了 @ResponseResultBody
    */
   @Override
   public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
       return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
  }

   /**
    * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
    */
   @SneakyThrows
   @Override
   public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
       //如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
       // 当body都为null时,下面的if判断条件都不满足,如果接口返回类似为String,会报错com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String
       Class<?> returnClass = returnType.getMethod().getReturnType();
       if (body instanceof String || Objects.equals(returnClass, String.class)) {
           String value = objectMapper.writeValueAsString(ResponseVO.success(body));
           return value;
      }
       // 防止重复包裹的问题出现
       if (body instanceof ResponseVO) {
           return body;
      }
       return ResponseVO.success(body);
  }

}


这里使用到一个自定义注解@ResponseResultBody:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {

}

注入bean

    @Bean
   public ResponseResultBodyAdvice responseResultBodyAdvice() {
       return new ResponseResultBodyAdvice();
  }

从上面我们自己定义实现类ResponseResultBodyAdvice#supports()可以看到,只要我们的Controller类或者方法上使用了ResponseResultBody注解,就会执行方法#beforeBodyWrite(),使用ResponseVO对结果进行包装统一返回。


实现类上使用了RestControllerAdvice注解,@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法,该注解特点如下:


1.通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。


2.注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。


3.@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。


4.@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。


5.@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。


6.@ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对


实现类ResponseResultBodyAdvice使用该注解就是满足上面的第3种情况,将拦截作用在所有注解了@RequestMapping的控制器的方法进行判断是否使用了注解@ResponseResultBody,从而对接口结构进行统一ResponseVO包装。


3.全局异常统一处理


使用统一返回结果时,还有一种情况,就是程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,不能正常走到我们return的ResponseVO对象返回,因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。


使用上面的@ControllerAdvice@ExceptionHandler进行全局异常统一处理:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

   /**
    * 全局异常处理
    * @param e
    * @return
    */
   @ResponseBody
   @ResponseStatus(HttpStatus.BAD_REQUEST)
   @ExceptionHandler(Exception.class)
   public ResponseVO exceptionHandler(Exception e){
       // 处理业务异常
       if (e instanceof BizException) {
           BizException bizException = (BizException) e;
           if (bizException.getCode() == null) {
               bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
          }
           return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
      } else if (e instanceof MethodArgumentNotValidException) {
           // 参数检验异常
           MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
           Map<String, String> map = new HashMap<>();
           BindingResult result = methodArgumentNotValidException.getBindingResult();
           result.getFieldErrors().forEach((item)->{
               String message = item.getDefaultMessage();
               String field = item.getField();
               map.put(field, message);
          });
           log.error("数据校验出现错误:", e);
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
      } else if (e instanceof HttpRequestMethodNotSupportedException) {
           log.error("请求方法错误:", e);
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
      } else if (e instanceof MissingServletRequestParameterException) {
           log.error("请求参数缺失:", e);
           MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
      } else if (e instanceof MethodArgumentTypeMismatchException) {
           log.error("请求参数类型错误:", e);
           MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
      } else if (e instanceof NoHandlerFoundException) {
           NoHandlerFoundException ex = (NoHandlerFoundException) e;
           log.error("请求地址不存在:", e);
           return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
      } else {
           //如果是系统的异常,比如空指针这些异常
           log.error("【系统异常】", e);
           return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
      }
  }

}


注入bean

    @Bean
  public GlobalExceptionHandler globalExceptionHandler() {
      return new GlobalExceptionHandler();
  }

通过以上步骤就可以对异常进行全局异常统一处理,这样做的好处不仅是可以对未知异常进行处理之后按照统一结构返回给前端,同时还能对异常处理之后进行error级别的日志输出,这样才能结合logback,log4j2等日志框架写入到日志文件中,以便后续查看异常错误日志排查追踪问题,否则异常信息不会被记录在error日志文件中。


4.总结


基于以上全部内容,我们讲述了如何优雅实现返回结果统一封装和全局异常统一处理,这样可以规范后端接口输出,同时也增强了项目服务的健壮性,可以说这两个统一处理是当下项目服务的必须要求,所以我们得了解一下哦。


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

mysql 到底是 join性能好,还是in一下更快呢

先总结: 数据量小的时候,用join更划算 数据量大的时候,join的成本更高,但相对来说join的速度会更快 数据量过大的时候,in的数据量过多,会有无法执行SQL的问题,待解决 事情是这样的,去年入职的新公司,之后在代码review的时候被提出说,不要...
继续阅读 »

先总结:



  1. 数据量小的时候,用join更划算

  2. 数据量大的时候,join的成本更高,但相对来说join的速度会更快

  3. 数据量过大的时候,in的数据量过多,会有无法执行SQL的问题,待解决


事情是这样的,去年入职的新公司,之后在代码review的时候被提出说,不要写join,join耗性能还是慢来着,当时也是真的没有多想,那就写in好了,最近发现in的数据量过大的时候会导致sql慢,甚至sql太长,直接报错了。这次来浅究一下,到底是in好还是join好,仅目前认知探寻,有不对之处欢迎指正


以下实验仅在本机电脑试验


一、表结构


1、用户表


image.png

 CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`gender` smallint DEFAULT NULL COMMENT '性别',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1005 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

2、订单表


image.png

CREATE TABLE `order` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`price` decimal(18,2) NOT NULL,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`status` smallint NOT NULL DEFAULT '0' COMMENT '订单状态',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=202 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

二、先来试少量数据的情况


用户表插一千条随机生成的数据,订单表插一百条随机数据


查下所有的订单以及订单对应的用户


下面从三个维度来看



多表连接查询成本 = 一次驱动表成本 + 从驱动表查出的记录数 * 一次被驱动表的成本



1、join



JOIN: explain format=json select order.id, price, user.name from order join user on order.user_id = user.id;


子查询: select order.id,price,user.name from order,user where user_id=user.id;



image.png


2、分开查



select id,price,user_id from order;



image.png



select name from user where id in (8, 11, 20, 32, 49, 58, 64, 67, 97, 105, 113, 118, 129, 173, 179, 181, 210, 213, 215, 216, 224, 243, 244, 251, 280, 309, 319, 321, 336, 342, 344, 349, 353, 358, 363, 367, 374, 377, 380, 417, 418, 420, 435, 447, 449, 452, 454, 459, 461, 472, 480, 487, 498, 499, 515, 525, 525, 531, 564, 566, 580, 584, 586, 592, 595, 610, 633, 635, 640, 652, 658, 668, 674, 685, 687, 701, 718, 720, 733, 739, 745, 751, 758, 770, 771, 780, 806, 834, 841, 856, 856, 857, 858, 882, 934, 942, 983, 989, 994, 995); [in的是order查出来的所有用户id]



image.png


如此看来,分开查和join查的成本并没有相差许多


3、代码层面


主要用php原生写了脚本,用ab进行10个同时的请求,看下时间,进行比较



ab -n 100 -c 10



in
 $mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

$result = $mysqli->query('select `id`,price,user_id from `order`');
$orders = $result->fetch_all(MYSQLI_ASSOC);

$userIds = implode(',', array_column($orders, 'user_id')); // 获取订单中的用户id
$result = $mysqli->query("select `id`,`name` from `user` where id in ({$userIds})");
$users = $result->fetch_all(MYSQLI_ASSOC);// 获取这些用户的姓名

// 将id做数组键
$userRes = [];
foreach ($users as $user) {
$userRes[$user['id']] = $user['name'];
}

$res = [];
// 整合数据
foreach ($orders as $order) {
$current = [];
$current['id'] = $order['id'];
$current['price'] = $order['price'];
$current['name'] = $userRes[$order['user_id']] ?: '';
$res[] = $current;
}
var_dump($res);

// 关闭mysql连接

$mysqli->close();

image.png


join
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

$result = $mysqli->query('select order.id, price, user.`name` from `order` join user on order.user_id = user.id;');
$orders = $result->fetch_all(MYSQLI_ASSOC);

var_dump($orders);
$mysqli->close();

image.png
看时间的话,明显join更快一些


三、试下多一些数据的情况


user表现在10000条数据,order表10000条试下


1、join


image.png


2、分开


order
image.png


user


image.png


3、代码层面


in


image.png


join


image.png


三、试下多一些数据的情况


随机插入后user表十万条数据,order表一百万条试下


1、join


image.png


2、分开


order


image.png


user


order查出来的结果过长了,,,


3、代码层面


in


image.png


join


image.png


四、到底怎么才能更好


注:对于本机来说100000条数据不少了,更大的数据量害怕电脑卡死


总的来说,当数据量小时,可能一页数据就够放的时候,join的成本和速度都更好。数据量大的时候确实分开查的成本更低,但是由于数据量大,造成循环的成本更多,代码执行的时间也就越长。实验过程中发现,当in的数据量过大的时候,sql过长会无法执行,可能还要拆开多条sql进行查询,这样的查询成本和时间一定也会更长,而且如果有分页的需求的话,也无法满足。。。


感觉这两个方法都不是太好,各位小伙伴,有没有更好的方法呢?


终于在尾巴上赶上了这个活动


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

同事问我为什么电脑屏幕上会有那么多球在飘

记得以前用的Windows电脑里面,有一个屏保程序就是在屏幕上出现很多飘来飘去的球,当球碰到电脑边缘的时候,会反弹到相反的方向,然后最近就琢磨着能不能使用Compose DeskTop也实现一个这样的效果,那以后我的Mac屏幕上也能出现好多小球,那简直是泰裤辣...
继续阅读 »

记得以前用的Windows电脑里面,有一个屏保程序就是在屏幕上出现很多飘来飘去的球,当球碰到电脑边缘的时候,会反弹到相反的方向,然后最近就琢磨着能不能使用Compose DeskTop也实现一个这样的效果,那以后我的Mac屏幕上也能出现好多小球,那简直是泰裤辣~


设计思路


我们把整体动效拆分一下总共有五步,每一步都不是很难




  • 第一步:使用循环动画不断改变小球位移的x坐标与y坐标,x坐标的变化范围是0到窗口宽度的最大值,y坐标的变化范围是0到窗口高度的最大值

  • 第二步:判断当x,y坐标到达自己的最大值的边界值的时候,将各自的变化范围的初始值与最终值互相对换一下,达到往相反方向移动的效果

  • 第三步:通过改变tween函数的durationMilliseasing属性,来改变小球的位移速度与位移路线

  • 第四步:将小球的动画需要的属性作为函数的入参,达到可以在上层定制小球动画的效果

  • 第五步:将窗口的宽度与高度更改成屏幕的宽高,背景色改成透明



让球动起来


首先我们来把球的样式做出来,球本身就是个圆形,我们使用Surface组件就可以完成,里面再包一个Box组件,这样做的目的是因为Surface没有办法设置渐变的背景色,我们如果想要让圆形看起来立体一些,就需要让背景色带点渐变,所以渐变的工作就交给里面的Box组件来完成


image.png

然后就可以把这个球放到我们的窗口里面去了,在这之前我们先创建三个常量,分别是窗口的宽高最大值以及小球的大小


image.png

然后把这三个常量分别设置给Window组件以及ball组件,代码与效果就如下图所示


image.png

接下去就是让这个球动起来了,我们通过改变球的位移坐标来实现球体的移动,这里给位移坐标的x,y分别设置一个无限循环动画,动画的初始值是为0,目标值为窗口的宽高,动画时间设置为5秒,然后让这个动画过程线性改变,实现过程如下所示


image.png

我们给Surface组件添加了offset操作符,让它接收mainxmainy的变化值,我们这个球就动起来了


0602aa2.gif

改变位移方向


现在已经让球动起来了,接下来就是要考虑如何让球“碰壁”以后反弹,由于我们的初始位置在窗口左上角,所以我们可以先做碰到下面以后的反弹以及碰到右边以后的反弹,也就是当x坐标到达或者接近x轴位移的最大值,或者y坐标到达或者接近y轴的位移最大值以后,我们将mainxmainy的初始值与目标值对调一下,这样就能往相反方向移动了,注意这里说的是位移最大值,不是窗口的宽高,因为球位移坐标是从球的左上角开始计算的,当碰到窗口边界的时候,其实位移距离是窗口的宽高减去小球的直径大小,所以我们再加上两个常量作为位移的最大值,方便后面计算时使用


image.png

然后如果想要在无限循环动画里面改变初始值与目标值,我们就要使用Animatable来切换,所以这里再创建四个Animatable的变量,分别代表x,y轴的初始值与目标值


image.png

创建好了以后,就直接把mainxmainy的初始值与目标值替换成了新建的四个Animatable变量,这样当我们去切换它们的值以后,mainxmainy的变化范围也发生了改变,而Animatable的切换函数snapTo是一个挂起函数,所以还需要一个协程作用域,我们这里使用rememberCoroutineScope函数来创建,那么小球碰到窗口下边与右边的反弹代码就有了


image.png

这边判断到达边界的条件不是mainx.value.value == offsetx.value的原因是因为通过打印日志发现,mainx或者mainy的变化值不会一直刚好是offsetX.value或者offsetY.value,所以只能把判断当两个值接近的时候当作小球移动到边界的条件,我们运行下看看反弹效果


0602aa3.gif

动图上看不出来,实际效果其实达到边界时候有点细微的抖动,这也跟我们刚刚那个边界值的判断条件有关,不过也不影响功能,我们按照这个方式把碰到左边与上边的代码也加上,一个完整的球体移动动画就做好了


image.png
0602aa4.gif

现在已经能够实现小球碰到窗口四周反弹的效果了,但是实现方式还是比较繁琐的,又是协程又是切换又是看边界值的,我们其实还有更简单的办法,因为不管是x轴的值还是y轴的值,它的变化范围始终在两个值之间,差别就是每次起始位置不同,那么这就是一个反复的过程,而我们这个循环动画其实就可以设置反复模式,使用repeatMode属性,值取RepeatMode.Reverse就可以了,我们试一下


image.png

我们看到现在我们把那四个Animatable都去掉了,mainxmainy的初始值与目标值又回到了固定值,区别就是增加了repeatMode,现在我们在看下实现效果咋样


0602aa5.gif

看起来好像差别不大,但其实碰到边界后的效果比之前要好多了,因为不用去关心那一点误差,而且也可以随意设置动画时间,之前为了让动画的变化值不要变化的太大,所以动画时间我是最小只能设置成5秒,现在的动效看起来就舒服多了


改变速度与路线


动画速度的话我们刚刚其实已经实现了,通过改变动画时间durationMills来实现,但是由于我们的easing设置的是LinearEasing线性变化,所以小球的位移路线永远都是沿着一根直线移动的,我们可以通过改变easing的值,来改变小球的位移路线,比如现在我将easing改成FastOutSlowInEasing


image.png

得到的效果就是这样的


0602aa6.gif

这球一下子就变得好像“有智商”了一样,感觉要“撞了”就马上减速,然后换个方向继续飘


属性作为参数,让小球可定制


想要定制小球动画的话,首先要确定好哪些属性可以拿出来定制,通过上面的开发,我们这个小球动画可以被定制的属性有以下几个



  • ballSize:小球的大小

  • ballColor:小球的颜色

  • xTime:小球位移x轴上的动画时间

  • yTime:小球位移y轴上的动画时间

  • xAnimateEasing:小球x轴上动画的变化速度

  • yAnimateEasing:小球y轴上动画的变化速度


这样子的话我们ball函数的参数列表就如下所示


image.png

然后再将代码中的对应位置用参数来代替


image.png

我们这里把计算最大位移的步骤也移到函数里面了,这样就可以根据不同的小球大小来计算各自的位移距离,我们这个ball函数到这里算是完成了,现在我们就可以想弄几个小球就弄几个小球了,比如我这边就弄了这么几个小球


image.png

下面就是一堆小球的效果


0602aa7.gif

我们再改下小球的样式,将小球弄成背景有点透明的样子,让飘动的小球看起来像是气泡一样,改完以后的小球代码如下


image.png

然后再将调用ball函数的地方,ballColor的入参也改成带点透明值


image.png
0604aa1.gif

如效果图所示,是不是有那么点意思了呢,现在我们进行最后一步。


将窗口透明,宽高增大为全屏


想要将窗口弄成透明的话,可以使用Window组件的transparentundecorated属性,代码如下


image.png

然后把screenWidthscreenHeight大小设置成全屏大小就可以了,我们使用ToolKit来获取屏幕宽高


image.png

还差一步,因为到了这里就算screenWidthscreenHeight设置成全屏宽高了,但实际上所在的窗口并没有真正的全屏,它跟屏幕左边留有一点距离,然后右边延伸至屏幕外边了,所以我们需要让整个窗口居中显示,使用WindowPosition,这个是WindowState里面的一个参数,我们在WindowPosition中设置成居中对齐就可以了


image.png

最终我们得到的效果就是这样的


0604aa2small.gif

总结


整体效果实现起来还是蛮容易的,总共代码加一块也不到一百行,感觉把Window设置成透明以后,DeskTop开发变得好玩多了,大家有兴趣的也可以尝试下,所有元素都可以按照自己喜好来定制,去设计属于自己的屏保程序


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

天气太热,希望这个小风扇能给你带来一点凉意

最近气温多变,这几天又回到了三十多度的高温天气,在这样的天气里面如果办公室里面不开个空调或者电风扇的话,那么是很难集中精神工作的,空调的话可能每个办公室都有,但风扇的话估计要自己去准备了,如果还没来得及准备的话,那么可以先考虑下在桌面上画个风扇看着它吹,毕竟古...
继续阅读 »

最近气温多变,这几天又回到了三十多度的高温天气,在这样的天气里面如果办公室里面不开个空调或者电风扇的话,那么是很难集中精神工作的,空调的话可能每个办公室都有,但风扇的话估计要自己去准备了,如果还没来得及准备的话,那么可以先考虑下在桌面上画个风扇看着它吹,毕竟古人有望梅止渴,我们今天就来画风扇降温


源码地址


github.com/coffeetang/…


准备工作


首先考虑下这个风扇的结构,我们这个风扇总共有这几个部分组成,分别是底座,立柱,扇框,扇叶,底座上有总开关,有可以调节风速强度的开关,那么整体来看是个上下结构,底座在最下面,其他的在上面,除了底座其余的都绘制在一个Canvas里面,大致结构如下


image.png

扇叶


风扇的扇叶,其实可以看成是在画布上画扇形,而我们画扇形就要用到函数drawArc,这个函数的传参列表如下所示


image.png

参数我们都很熟悉了,这里绘制扇形所需要用到的参数有



  • brush:用来设置渐变色的

  • startAngle:表示起始角度

  • sweepAngle:表示扇形角度

  • userCenter:表示扇形两端是否与圆心相连

  • topLeft:表示绘制扇形范围的左上角坐标

  • size:表示扇形的绘制范围

  • style:默认值就是Fill填充的,所以我们可以不用去挂心


根据参数,我们需要创建几个变量,首先是圆心坐标,它是取的Canvas的中心坐标,所以无论窗口变大变小,我们的圆心坐标都会在整个画布的中心位置


image.png

其次是我们的半径,半径的大小决定了整个扇形绘制的范围大小


image.png

那么我们绘制一个扇形的代码就是下面这样的


image.png
image.png

一个扇形就这样画出来了,而一个风扇总共有三个扇叶,咱要画三个扇形,而且是圆周上等分的,该怎么画呢?这里使用这个方法,首先创建一个数组,这个数组里面是每个扇形要用到的渐变颜色


image.png

然后再创建一个数组,这个数组里面是每个扇形的startAngle


image.png

那么首先我们就可以通过遍历数组的方式,把六个扇形都画出来


image.png
image.png

我们看到这个时候界面上展示的就是一个由六个扇形组成的大圆形,然后我们把colorList里面每隔一组颜色就把颜色改成透明的,那么这个圆形就看起来就像是三个被等分的出来的扇形一样了


image.png
image.png

扇框与立柱


扇叶已经完成了,接下来就是绘制扇框与立柱的工作,这两个都比较容易,立柱就是从圆心位置向下绘制出一条直线


image.png
image.png

然后我们在扇形靠外一点的位置绘制一个圆形作为扇框的边框,这里用到了drawPath函数,drawPath的第一个参数是Path,所以我们先将Path做出来


image.png

然后再调用drawPath函数,将framePath传进去


image.png
image.png

然后就是风扇前面的网罩,网罩常见的有从中心向外延伸出去的一条条直线,也有的就是一个个井字格组成起来的样子,这边按照前者做个网罩样式出来,这种样式与扇叶的思想有点接近,都是按照角度在一个圆周上等分的绘制样式,所以我们首先需要确定好这些角度,也有一个list维护起来


image.png

这里就是有45根线,每过8度画一根,而我们知道绘制线条用到的函数drawLine需要知道一个start坐标和一个end坐标,start都知道是圆形坐标,而end的xy坐标就要根据角度与半径算出来了,计算的代码如下所示


image.png

第一个参数就是网罩的半径长度,第二个参数为圆形x坐标或者y坐标,第三个参数是角度,那么我们就可以使用这两个函数,遍历lineAngleList来绘制出网罩


image.png
image.png

绘制风扇部分就完成了,下面开始开发底座上的开关


总开关与强度开关


底座上的开关分两个区域,左边是调节强度的区域,右边是总开关区域,总开关设计成一个滑块的样式,滑块默认在左边为关闭状态,点击或者拖动滑块,滑块滑动到右边,状态变成开启,滑块高亮,首先建立一个变量用来记录当前开关状态,再定义两个常量分别代表关闭与打开


image.png

默认为关闭状态,滑块的实现我们需要用到swipeable操作符,参数列表如下


image.png

其中我们需要用到的参数有



  • state:滑块的状态,需要监听滑块的状态来更新开关的状态值

  • anchors:锚点,某个位置对应滑块的一个状态值

  • thresholds:阈值,判读一个鼠标拖动事件滑动到某个位置的时候,这个位置属于哪种状态,那么当鼠标停止拖动时候,滑动可以animate到对应状态位置

  • orientation:拖动方向


根据需要的参数我们来创建对应的变量,滑块的代码如下所示


image.png
image.png

这里滑块的背景颜色会根据开关状态来变化,而开关的状态我们就通过监听swipeState来更新


image.png
0606aa1.gif

我们拖动滑块改变开关状态的功能如上图所示完成了,而点击滑块边上区域来改变开关状态就需要在滑块父布局上添加点击事件,点击一次更新滑块状态,而更新操作需要用到SwipeableState里面的animateTo函数,这个函数是个挂起函数,所以还需要给它提供一个协程作用域,恰好我们更新滑块状态是根据点击来触发的,所以这里选择使用LaunchedEffect函数


image.png
0606aa2.gif

滑块部分就做完了,然后是左边区域的调节强度功能,这个区域准备由三个色块组成,每个色块都可以点击,每点击一个色块,强度设置成对应级别,符合该级别的色块颜色高亮,否则就变暗,所以这里也需要一个表示强度的变量值


image.png

然后添加上三个色块以及每个色块的逻辑代码和点击事件


image.png
0606aa3.gif

这里每个色块高亮的条件都不一样,但是有个共同条件就是必须是开关状态开启的情况下才能高亮,如果关闭的话,所有色块都会变暗,另外fanState对应的值有1000,600,200的原因我们接下去会说,这个跟风扇的转速有关


让风扇转起来


让风扇转起来从代码的角度就是让扇形每次绘制的位置不同就可以了,而从我们绘制扇形的函数drawArc里面的参数来看,要更改扇形绘制的位置那就是改变startAngle的值,也就是它的初始值加上一个时刻改变的值,这个改变的范围就是0f~360f,那么对于这个循环变化的过程,我们肯定第一个想到的就是使用循环动画


image.png

这边我们看到了,fanState的值其实就是循环动画的时间,fanState的值越小说明转速越快,另外动画的初始值与目标值也加上了fanState的值,这样做的目的是为了当fanState变化时候,需要让Composable函数animateFloat触发重组,这样才能重新生成新的InfiniteRepeatableSpec对象,改变转速,不然的话,animateFloat的三个参数都不发生变化也就不发生重组,wheelState依然还是最初的值,wheelState的值拿到以后,就可以把它加给drawArc函数里面的startAngle


image.png

这里还加了一个判断,当开关是开启状态下,drawArcstartAngle才加上变化值wheelState,当关闭状态下,则不加这个值,也就是风扇处于静止状态,我们再来看看效果


0606aa5.gif

总结


我们这个风扇也做完了,用到的知识点都是平时Compose开发中常用,像是循环动画,Canvas绘制以及手势操作之类的,就这样简简单单在电脑屏幕上画出来了一个风扇,不知道看完这篇文章的你能否感受到一丝凉风~


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

程序媛员的博客之旅

写博客困境 自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。 无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没...
继续阅读 »

写博客困境


自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。


无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没办法继续更新下去了。所以工作了这么多年,自己都成为老学姐了,还是没有拿得出手的个人博客,实在是惭愧。


经过深刻的自我反省之后,我觉得阻碍我更新博客的原因,主要有以下几个方面:


1. 工作太忙。


大家都知道,相比其他工种,作为程序员的工作强度还是蛮大的。每天都有做不完的需求,开不完的会议。经常要到晚上快下班了才有时间写代码,于是加班就成了家常便饭。下了班回家也感觉很累,不想再打开电脑,只能刷刷手机、看看综艺,做一些不费脑子的娱乐活动。


2. 文字功底太差。


作为一名理科生,上学的时候语文成绩就很差,作文都靠模板以及背的素材拼凑起来。高中毕业之后,几乎没有完整写过什么。而写博客,需要高强度大量输出内容,还要有组织有架构,逻辑条理清晰,这个对我来说简直太难了。所以经常写两篇之后,发现自己写的东西惨不忍睹,于是就暂停了更新博客的计划。


3. 没什么内容可写。


虽然每天都在写代码,但是很多时候做的都是重复性工作,并没有太多有技术含量、技术深度的内容,可以支撑我写出高大上的博客。


我个人更新不下去博客的主要原因就是上面几点,相信有很多想要更新博客却坚持不下去的同学,也都有同样的感受。


如何突破困境


我想说一下,我为什么觉得自己这次能克服这几个问题,以及克服这几个问题的方法。如果大家也和我有类似的问题,可以往下读一读,看有没有什么可以借鉴的地方。主要还是给我自己未来的日更之旅打打鸡血。



一、工作太忙,没时间。



每个人的一天都是24小时,为什么有些人能做更多的事情,实现更高的成就呢?我觉得这和每个人的时间管理方式是息息相关的。掌握高效的时间管理策略,是每个高效能人士的必备技能。


我以前觉得是因为程序员的工作比其他行业更忙,所以没有时间。但是看周围,把博客或者副业运营很好的那群人,工作也不闲。所以说,这个理由只是一个对自己时间管理无能的借口而已。真正的强者,从来不会找没有时间的借口,而不去做一些尝试。


当下,为了能够实现工作、写作(其实是搞副业)和生活之间的平衡,我决定先从这几个角度来优化我的时间使用效率。


1. 为任务分配合理的优先级


事情是永远做不完的,如果想做的太多,那么时间永远都不够。我准备用重要紧急四象限法来管理任务。每拿到一个任务后,先决策这个任务是属于哪个象限的,然后再安排做的时间。
image.png


我们之所以感觉每天忙忙碌碌,却没有什么进步,主要是因为在“紧急-不重要”的事情上,浪费了太多的时间。仔细想想,上班时间有多少是浪费在了,对未来成长没有任何意义的所谓“紧急”的事情上了。而真正“重要”的事情,却被我们以没有时间做,而一直推迟。


前段时间看到的一句话,对我触动很大:
Today you do things others don't do.
Tomorrow you do things others can't do.


做“重要-不紧急”的事情,不会对你的人生产生立竿见影的效果。但是长期下去,效果一定是惊人的,而且能给你带去很多别人没有的机会。


“人们总是容易高估一天的影响,而低估长期的影响”。比如学英语、写作,可能努力了一个月都没有效果,很多人就开始放弃了,转而去寻找其他的方法。但有些人坚持了下来,于是这些人坚持了一年、两年甚至几年之后,最后到达了很高的高度,才发现原来每一天的坚持都没有浪费,最后都是有效果的。


2. 减少任务切换,提高做事情的效率


提高做事情的效率,最好的办法就是进入“心流”的状态。不管是写代码、写文字还是看书学习,在“心流”的状态下,效率比平时要提高好几倍。


“心流”的状态,就是一种忘我的境界,忘记了时间、忘记了周围所处的环境,甚至忘记了身体上的痛苦,专心沉浸在当下所做的事情上。我相信这种状态,大家多多少少都有体会,比如在废寝忘食打游戏的时候。这种状态下,人所爆发出来的潜能是巨大的。


要达到“心流”的状态,最简单易行的方法,就是减少任务的切换。就像CPU线程切换,需要缓存上一个任务的执行状态,加载下一个任务的运行环境,效率很低。人脑也是,在上下文切换的时候,需要耗费很多的时间和精力。


而工作中,经常会被工作软件的消息提醒所打断,很难进入”心流“状态。比如,正在尝试解决一个疑难的问题,但是突然来了一条工作上的消息,于是不得不中断当前的工作,去看这个消息。等处理完消息后,在回到工作,可能已经忘记之前做到哪里了,又需要花时间才能重新进入状态。


可以尝试”番茄钟"的方法。在每个番茄钟开始的时候,屏蔽消息,集中精神工作25分钟,然后再花5分钟处理这25分钟到达的消息。处理完后,进入下一个番茄钟。


3. 不要给自己定太高的目标


之前我写博客,总是一篇文章写很长,想要在一篇文章中讲完和标题有关的所有知识点。但是这样会让自己很累,每次写一篇文章都要花很长的时间和精力,到后面甚至排斥写文章这件事情。


所以这次,我决定不给自己设太高的目标,每篇技术文章,争取讲完一个知识点就可以,如果内容特别多,可以采用连载的形式。最后可以新建一篇索引的文章,将各个连载的文章串起来。


PS:时间管理是一个复杂的事情,我之前也看过一些相关的书籍,后续我也想通过更系统的文章分享出来。先在这里挖个坑,如果想看就先关注我吧,后续我会慢慢把坑都填上。



二、文字功底太差



另一个困扰我的因素,就是自己的文字功底太差了。几乎没怎么写过文章的我,不知道怎么表达自己。有时候心里有很多想说的话,但是一写起来就读不通,没办法完整表达自己的意思。


为了能顺利完成日更的目标,我决定尝试下面的方法。


1. 先写起来,自然而然就会有进步


第一个就是不管怎么样,不管写得有多烂,先写起来,以量变来引起质变。我现在的写作量,可能连那些大V一个月的量都不到,凭什么觉得自己的水平就能和人家一样。如果每天输出500字,一年就是18.25万字。坚持写,我相信写一年之后,水平肯定会有进步。


没有什么是刻意练习不能达成的,如果有,那肯定是练习不够多。


2. 多看多模仿


写文章也是有方法可以借鉴的。去看好的文章是什么样的,向优秀的文章和作者学习。


比如,我之前看一个技术博主,会在每篇文章的开头放一个脑图,描述整片文章的整体架构,我觉得这个方法就很好。首先自己可以根据这篇脑图往里填充资料,速度更快也更清晰,同时,读者也可以在看文章之前对文章的内容有一个整体的感知,很快就能定位到自己需要的内容上。之后我的文章也可以借鉴这个方法。



三、没什么内容可写



关于没什么内容可写,以前做业务开发的时候,确实有这个问题,但是现在做系统开发了,几乎每天都在学习新的知识,所以完全不愁没有内容可写了。


如果有同学想开始写博客,但是又觉得没有内容可写,可以从以下几个方面去尝试:


1. 提前想一些topic,主动积累


在开始写博客之前,提前收集一些topic。我现在就有一个文档,专门用来放我想写的文章topic,现在这个文档里面已经有几十个可以写的topic了。


提前脑暴一些topic,或者列一个知识图谱,到时候如果发现没什么内容可写,直接去list里面找一个topic就好了。


2. 主动去学习一些新的东西


对于一些业务开发的同学,可以在开发之余,主动push自己去学一些新的技术。比如看一些技术书籍和博客。


博客内容


之后我的博客,主要会围绕下面这些方向:


Android性能优化


作为一名Android开发,更新的内容主要还是在Android相关的技术点上。由于我近期工作的重点主要在性能优化方面,所以前期的文章主要会更新性能优化相关的文章,包括启动时间、存储空间、稳定性、ANR等优化方案,以及一些相关的技术原理。


Android面试集锦


等把Android性能优化相关的内容写完,会再写一些面试相关的内容。作为一个拿过各大互联网offer、一毕业就当上面试官的学姐,在面试方面还是有不少经验的。


算法题解


算法题可能也会写一些,写一些我觉得好的题目的题解(主要是算法题比较好水文章,实在不行了就来篇题解)


读书笔记


我平常也会读一些技术之外的书籍,会写一些读书笔记,到时候会更新一些这方面的内容。


新的技术方向


除了Android开发以外,未来想学习一些新的技术方向,到时候也会更新到这个博客里,比如Web3相关的内容。


杂七杂八的思考


一些思考想法,对当下事件的看法,对未来的思考,个人成长、时间管理、投资理财等等相关内容,都会记录在博客里。


总结


说了这么多,也不知道会不会有人看我写的文章,毕竟现在Android开发已经不流行了。而且ChatGPT兴起之后,普通的内容生产者,会受到非常大的冲击。可能以后查东西都不需要去搜博客文章了,直接问ChatGPT就好了。之后我的博客文章,说不定也会让ChatGPT帮我写一部分。


总之不管有没有人看,不管AI是否会把我的工作取代,我还是会把日更坚持下去。如果对我更新的内容感兴趣,欢迎点点关注呀~~


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

25岁学Java培训班学完后现状

前言 大家好,我是小江,好久不见。距离上一次发布文章已经过去三个月了,这段时间确实有点忙,都忘了稀土掘金这个平台了,最近想起来了,今晚来分享一下最近这段时间发生的事。先提前说一下,我已经在工作了。我将分别从培训班最后两个月,到找工作,最后入职来讲述我的经历。 ...
继续阅读 »

前言


大家好,我是小江,好久不见。距离上一次发布文章已经过去三个月了,这段时间确实有点忙,都忘了稀土掘金这个平台了,最近想起来了,今晚来分享一下最近这段时间发生的事。先提前说一下,我已经在工作了。我将分别从培训班最后两个月,到找工作,最后入职来讲述我的经历。


一、我在培训班最后两个月


还是老样子,我不会宣传是哪个培训班的。在培训班最后两个月,我学完了spring、springmvc、springboot、mybatisplus、Redis、springcloud、mq、es、springsecurity、Linux、Nginx、docker等技术,我个人springboot和Redis是重点,其他的看个人能力,没有实际项目积累光听是很难掌握的。


我自己的感觉就是确实进度很快,东西特别多,一两天学完一个组件,跟着老师听当时可以听懂,后面就忘了,只有个印象这个组件是干嘛的,我当时也很焦虑,进度这么快消化不了,而且了解到老师讲的也是简单的怎么用,往深的挖会有很多东西,我很害怕,最后一段时间又要开始背面试题了,感觉就是时间不够用。


最后两个月做了一个springboot项目和一个微服务项目,自己写的springboot项目的一个模块,功能也不难,就是练习使用框架开发,总共给了一个星期时间,我发现这次和前面项目嘎嘎闷头一直写不同了。首先是分析需求,设计数据库表,光这个我就花了两天时间把业务都梳理清楚,数据库也设计好了,才开始真正写代码,很多东西都封装好了,Java代码就没写多少,90%时间都花在写前端页面去了,前端确实不太熟,最后还是完美的写完了,看着项目启动页面展示的效果,我很高兴。给我的感觉就是代码量确实变少了,框架确实很方便。第二个微服务项目是我们看着老师写的,老师一边写一边讲,我都能听懂,也学到了很多。


二、学完找工作阶段


时间过得挺快的,学完走人那会我既兴奋,又恐惧,还有不舍。学完刚好四月底,我五一回老家了,在家写好了简历,背面试题,5.4号假期结束了,那天我上午9点准时Boss直聘上沟通,只投我意向的,在Boss直聘上沟通了五六十家,收到了三个面试邀请,我都安排在了5.6号面试。我第二天坐车离开家过去了,住的青年公寓。5.6号那天我早一个小时到了那公司,在楼下一直等到面试时间前10分钟上楼,我很紧张。


上去之后我说面试的,他们给了我一张纸先做题,四五个选择题我都会,填空题补充代码的我也会,后面就是两个写sql题,我不会了,我之前培训班项目用到的sql大都是增删改查,复杂点的就是多表联查。但这个题目是让我分组,子查询那些。我一时不会了,只知道思路,就只把思路写上去了。过了十分钟有人来了面试,问的都是比较基础的八股文,我都能答出个123来。下面是面试问的


Screenshot_2023-05-27-22-23-27-020_com.miui.notes.jpg
最后问我期望薪资,我是税后10k,他就让我回去等通知后面就没结果了。


后面面试我就不一一详解了,附点面试问的。


mmexport1685197778664.png


我就这样找了十几天,从刚开始的一天三面试觉得行情没那么烂,到后面一直没面试,海投了都已读不回,我心态也发生了变化,特别的焦虑。因为自己没也钱一直找家里要,一天没找到工作就一直消费下去。我感觉我自己八股文已经背的可以了,奈何后面面试都不问八股文了,直接怼着项目一直问场景,几下就看出我是包装的了。我后来想着要不要去一线城市去看看,觉得在这找不到了。都计划好了下个星期去上海杭州看看,结果周五有个面试,我去了也没抱太大希望,面试果然是问我项目场景,我没答好,然后问我springmvc源码等我都没答好,我想着没戏了也麻木了无所谓,最后他还是要了我。也很意外,进公司了解之后知道公司刚刚成立不久,正在招人,还要招几个。


入职后工作中的感受


我稀里糊涂的入职,第一天让我搞环境,我很多不会,同事都挺好帮我很多。后面直接给我任务让我开发了,给到任务后我感觉不会搞啊,业务太复杂了,微服务项目,模块太多了,十几个数据库,上千张表,我找字段都找好久,原本给我2天时间,我硬是搞了一个星期,不过领导蛮好的,虽然会说我几句,但都会一步步教我。


在这已经上班十几天了,感觉自己学到了很多东西,也慢慢能上手完成任务了。


结束语


好了,到这就分享完了,我也准备睡觉了。我感觉自己也蛮幸运的,也感觉这行确实要一直保持学习状态,一直努力下去。最后祝大家都能找到满意的工作


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

成功上岸字节!分享一些 Idea!

1. 前言想来今天已经入职字节 1 个多个月啦,身边也有很多迷茫的同学,经常询问我如何复习八股、准备面试。今天这篇文章主要给大家提供一个思路,提供一个我总结的面试公式,希望能提供一些新的维度供大家参考~大家好,我是ltyzzz。这可能是我第一次和大家做自我介绍...
继续阅读 »

1. 前言

想来今天已经入职字节 1 个多个月啦,身边也有很多迷茫的同学,经常询问我如何复习八股、准备面试。今天这篇文章主要给大家提供一个思路,提供一个我总结的面试公式,希望能提供一些新的维度供大家参考~

大家好,我是ltyzzz。这可能是我第一次和大家做自我介绍~之前一直在忙着玩 GPT 项目。

我背景是NUS计算机硕士,武汉理工EE本科,春招收获腾讯、字节等后端实习Offer,目前仍在字节实习,今天给大家分享一下面试准备经验(接近5000字),我认为也同样适用于大家日常的学习。现在我实习刚满一个月,之后我会陆续给大家分享实习的工作经验~

在开始分享之前,我想给大家抛出一个我认为的技术面试公式,仅供大家参考,欢迎大家一起讨论:

面试 = 40% 八股 + 30% 算法题 + 20% 项目经验 + 10% Idea

在接下来的面试准备经验分享中,我会着重介绍项目经验与Idea。

2. 八股&算法题

八股和算法题我想市面上资料已经数不胜数了,这里我简略说一下。

如果大家已经对此部分准备足够充分或者已经有着自己的方法论或学习路线,可以直接快进到 项目经验 & Idea

2.1 八股准备

对于八股准备,我主要以 Java Guide 和 小林Coding 为主,书本(如Redis设计与实现、JVM圣经、Java并发编程的艺术、高性能MySQL)为辅。如果大家时间紧张,可以不看书。此外,我是面试驱动复习,八股文复习与面试相互交叉,是一个相互促进的过程。

  • 第一轮复习我花费了大概1到2周的时间,粗糙浅显地过了一遍MySQL、Redis、JUC、JVM、操作系统、计算机网络、微服务等基础知识,大概是1~2天一个板块,能够简单的应付一下基础面试题,一轮复习完之后正好对应于字节跳动的一轮面试。
  • 第二轮复习我花费了大约20天的时间,着重深入地学习与复习之前各个板块的知识点,并搭配面经(百度或Google搜索:某某公司后端/前端面经),反复地查漏补缺,遇到陌生的题目或知识点,从书上或网上寻找答案,记录下来,便于之后复习。我大概看了不到50篇面经,梳理了接近150道不熟悉的面试题目,大家有需要的话,之后也可以在星球中分享给大家。第二轮复习与美团、字节、腾讯、阿里面试相互交叉。这段时间准备的很多八股面试题目,在面试中也有被问到。

大家可以参照我的复习路线与经验,以面试作为驱动力,高效地复习八股文。这一阶段不考验智商,只考验耐心、毅力。因为一轮复习的时候大家可能会很新鲜,接触或复习到很多有意思的知识点,感觉自己有很大的提升,这一阶段可能还比较有趣。但是在二轮复习尤其是穿插了面试之后,大家可能会遇到两个问题:一个是感觉到很慌,认为自己什么都不会,看一个面经慌一次;另一个是感觉到很枯燥但又不敢不看。不管是哪一个问题,都需要静下心来,戒骄戒躁,迅速调整心态,不要乱了阵脚。八股文这里我相信只要能花费20天~1个月的时间,每天拉满,一定能攻克。

2.2 算法题准备

对于算法题准备,没有任何捷径除非天赋加持,刷就完了。我当时候是LC刷了500多道题目,还有在其他平台也零散地做了一些题,总体刷了8个月。其实精刷200~300道题就足够用了。大家可以以Leetcode为主,着重做剑指Offer,最好做2~3遍。接下来我根据复习时间长短,提供两种策略。

  • 时间长且充裕:每天坚持刷LC每日一题,拓宽思路。抽出一定时间刷 LC HOT100 与 精选200 题单,也可以做一些知名的算法博主总结的题单(推荐宫水三叶姐的LC题单)。总之就是多刷多看,加上剑指Offer的题目,半年多时间可以刷够300多道题,足够应付一般算法题。
  • 时间短且紧:集中性地刷 剑指Offer,比如集中一周时间甚至更短。看题10分钟没思路的话就直接看题解,重复的刷题,反复不断地刻意训练,直到背过为止。若仍有余力,可以再抽空刷刷 LC HOT100 题单。即便时间如此之短,此时的刷题量也可达到100道题左右。

3. 项目经验

接下来,我将重点介绍 项目经验 与 "Idea"。

对于项目经验,我先为大家介绍项目,然后从项目准备中分享我的经验。

我准备了三个项目:智能停车场项目、仿B站项目、RPC项目。这些都是我自己日常学习的项目,不是实习项目。顺便说一下我在此之前只有一段很水的实习。

  • 智能停车场项目,简单总结就是一个增删改查项目,技术含量不是很高。前端通过小程序和后台管理系统展现。前端技术栈是Uniapp + Vue,后端技术栈是Springboot、SpringCloud、Mybatis等。只是用了一些简单的微服务技术(Feign、Gateway、Nacos),后台管理系统用了RBAC实现权限管理。但是在包装项目时,我添加了几个亮点:Redis数据缓存、分布式Session、分布式ID、分布式锁技术、与网络摄像头联调开启道闸。如果大家后续需要,我可以重构一遍项目后端,并开源出来,供大家学习(自认为小程序界面写的还算好看哈哈哈哈)。
  • 仿B站项目,这个项目含金量要高一些。它后端基本框架是我参考Ruoyi Cloud Plus实现的,脚手架自带了很多功能如数据脱敏、幂等、微服务限流、可观测监控、分布式Session单点登录、安全性措施等。光是脚手架自带的功能点就够在面试上聊很多。此外,该项目我着重于Redis相关的系统设计,如动态Feed流推拉、点赞评论相关的计数系统、数据缓存,运用了很多Redis的数据结构。此外,还设计了站内信、单聊群聊、视频弹幕等功能点。这个项目是我和朋友一起做的,还没有做完,所以暂时无法开源给大家。
  • RPC项目,这个项目是我参照掘金小册中的RPC做的。此类项目已经很多了,但是在面试过程中,还是会被经常问到这个项目是如何设计的。我一般会详细说出代理层、路由层、注册中心层、异步设计等的设计思路。有的面试官可能还会问压测相关的内容。如果说要将RPC项目写到简历中,一定要清楚核心功能的设计,并且反复地尝试自己练习表达几次。

现在,我来总结一下项目准备中需要注意的点:

  • 准备的项目一定要是自己非常熟悉的项目,起码写到简历当中的功能点能够经得起面试官的盘问。一般面试官也不会问的特别复杂,只要准备充分,都是可以回答上来的。不熟悉的功能点最好不要写,不打无准备之仗。
  • 准备的项目最好是两个以上,一个可以为Web前后端项目,另一个可以为框架开发、中间件开发。这样一方面可以体现你业务能力okay,熟悉常见的开发场景,当mentor或leader派活的时候,你知道如何下手去做,有自己的实现思路;另一方面可以体现你有一定的钻研自学能力与解决问题能力,能够啃动硬骨头。
  • 此外,我认为大家可以从日常开发中发现需求,自己设计网站从实际出发,去解决痛点,这样的话在面试中更是加分项。因为实习或工作中,就是从实际需求出发,解决一个个业务场景。面试官会更加认可你的项目。这一点也与我之后要说的 "Idea" 有关。

4. “Idea”

对于 "Idea",我认为占比是最少的甚至可能面试官压根不会问你,但是我认为它对我而言是最有用的,实际上也指导了我整个面试过程甚至是学习编程的过程。我这里的 "Idea"是指 你想要什么 & 你的一些灵光乍现的想法

  • "Idea"第一层:我认为需要清晰地认识到自己学习编程、想要进大厂是为了什么,这一点实际上直接或间接地指引着我们的日常学习或工作。我可以先和大家分享一下我在实习之前的 "Idea"(实习后我又有了新的认知与变化,这部分大家有兴趣我之后再做详细的分享)。

    • 我学习编程的目的就是觉得有意思,做网站、开发小程序、学习前后端、部署服务器等都很有挑战性,做出以后也很有成就感,于是一发不可收拾。想要进大厂一方面是因为大厂给钱确实多,另一方面还是因为想要进公司学到一些真正企业落地的技术,并能够真正做出一些产品或项目,直接点说就是想要干点真东西出来。此外我也对自己的职业生涯有着较为清晰的规划,我想的是工作中以后端为主,深耕技术,向架构师的方向迈进;日常学习中提升自己的技术广度,以兴趣为导向,涉猎各个领域,尝试各种新东西。
    • 我上述所说的 "Idea",确实对我面试过程中起到了推波助澜的作用。因为当面试官和我聊起日常学习、职业规划时,我整个人的头脑是清晰的,我可以清晰地给他讲述我的想法。这一点可以给面试官留下很好的印象,毕竟程序员面试并不仅仅是技术的考察,而是综合评估。此外,我还可以给大家举出一个最近组内的case,我一个同事面试其他后端实习生,但是当问到那位同学有没有投其他岗位时,他犹豫不定。他说自己不确定,之后会尝试算法岗。面试官就认为这位同学没有很清晰的规划,即使招进来,他可能心思也不会完全地投入到后端工作中。(可能还有其他多方面因素导致面试挂掉)
  • "Idea"第二层:是否有一些奇思妙想、是否正在尝试做自己的开源项目。这一点我在腾讯面试的过程中深有体会,我先和大家分享一下这段面试经历。

    • 三轮面试几乎没有问什么八股文,第三轮面试在拷打项目,另外两轮都是业务场景设计题以及聊日常学习、聊开源。尤其是第一轮面试给我的印象最深刻,面试官估计很多人都听说过,是一位PHP的开源大佬。面试中他一直在给我抛出与实际业务相贴近的场景设计题,开放题目,没有固定答案,我觉得是在考察我的思维广度和技术广度。这些问题我也都给出了自己的思考。此外,我们也聊到了开源项目,他向我分享了他做开源的初衷与过程。而我也一直想要去做自己的开源项目,我和他说了我的想法。这一场面试也让我学习到了很多在八股文中无法学习到的东西。第二轮面试中,面试官最后问了我最近在学习什么,我很自然地说了关于GPT的一些话题以及我想要做一个AIGC的简历项目。本来面试已经准备结束了,但是面试官可能对这个话题比较感兴趣,又和我聊了不到10分钟。
    • 从我这两场面试中,我感觉到有着自己的想法很重要,是否每天有在探索并思考一些新东西,并去实际地做出来一些有意思的项目或产品。这一点是我从学编程以来的兴趣所在,兴趣推动着我不断地去探索一些新东西,经常性地产出自己的"Idea"。尽管有一些似乎和后端关联性不大,但是它其实最终在一定程度上帮我拿到了Offer。而且"Idea"也可以促使自己即使在春招秋招结束后,还能够有自驱力并且快乐开心地去学习编程,这一点也是我认为最重要的。

总结一下"Idea"就是:清晰认知自我,不断尝试探索。

5. 后记

以上这就是我的面试经验分享啦~

这也是我第一次在自己的博客对外分享我的面试经验,希望能给大家带来帮助!

此外,如果大家感兴趣,我可以之后分享一下我近期实习工作的经验思考,我相信一定能够给大家带来新的启发~

后续我会继续分享一些学习心得、技术干货和实战项目,帮助大家在 金九银十 斩获各路大厂 Offer!


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

天黑了,开个灯继续看书

像素点这个词对于前端来讲可能与UI设计师们打交道的时候用的会比较多一些,比如两个控件之间距离多少个像素,这个文字离容器边距多少个像素,这里的像素通常是以一种度量单位而存在,那大家有没有试过将像素点作为一个个小的控件来使用呢?让每个像素点可以变大变小,颜色可以变...
继续阅读 »

像素点这个词对于前端来讲可能与UI设计师们打交道的时候用的会比较多一些,比如两个控件之间距离多少个像素,这个文字离容器边距多少个像素,这里的像素通常是以一种度量单位而存在,那大家有没有试过将像素点作为一个个小的控件来使用呢?让每个像素点可以变大变小,颜色可以变深变浅,那么今天这个文章我们继续在Compose DeskTop上来带大家看看像素点的另一种玩法。


画出像素点


首先我们在Canvas上画出所有像素点,下面是画像素点所需要的变量


image.png

  • screenW:画布宽度,在Canvas中通过Size实时更新

  • screenY:画布高度,在Canvas中通过Size实时更新

  • gridCount:宽度或者高度上需要分出的像素数量

  • xUnit:单个像素的宽度

  • yUnit:单个像素的高度

  • pRadius:需要绘制的小圆点半径

  • xList:所有绘制出来的小圆点的x坐标

  • yList:所有绘制出来的小圆点的y坐标


然后在Canvas里面遍历xListyList这两个集合,将所有小圆点都画出来,怎么画的大家都很熟悉了,使用drawCircle函数


image.png

前方密恐福利~


image.png

我们已经获得了一堆小黑点,现在我们来尝试下更改一些小黑点的透明值,比如从我点击某一个位置开始,该位置的透明值最小,然后逐个向外透明值变大,直到透明值变成1为止,代码如下


image.png

这里新增两个变量tapXtapY用来保存点击位置的x坐标与y坐标,在循环遍历的代码里面,新增了xdisydis两个变量表示透明值,并且从点击位置开始向外递增透明值逐个变大,到第十个黑点的时候透明值就变成1了,当屏幕点击以后,xdisydis同时都小于1的点在绘制的时候都设置透明值alpha,否则就不设置透明值,下面是效果图


0613aa1.gif

圆点的透明值已经从点击位置开始向外变大了,我们再用同样的逻辑,让圆点的半径逐个向外变大


image.png

这里就做了一个小改动,将半径去乘上刚才算出来的透明值,让那些变透明的圆点同时也能有一个大小上的变化,我们再看下效果图


0613aa2.gif

可以看到我们的小圆点已经呈现出向外扩散,并且在色值与大小上都有了一定的变化,但是如果说扩散的话这里还看着有点别扭,因为一般的扩散都是以一个圆形的形状扩散的,而这里是正方形,所以我们得想办法把这个正方形也弄成圆形,怎么弄呢?那就要改变一下计算透明值与半径大小的方式了,之前是按照向外扩散经过的圆点个数来逐渐改变圆点的透明值与半径大小的,关键代码是这俩句


image.png

那么这种计算方式肯定是斜着的方向扩散的距离要大一些,所以我们不能再限制个数了,而是限制一下扩散的距离,也就是将这个扩散的圆的半径得先确定好,比如变量circleSize就是这个扩散的半径


image.png

然后我们需要做的就是计算出两点之间的距离除上这个circleSize,得到的值如果小于1那么就是我们需要的透明值,大于等于1我们就正常处理,这里唯一需要琢磨的就是如何计算两点之间的距离,四个字,勾股定理


image.png

最后一步开根号kotlin里面有现成的函数sqrt,那么计算两个小圆点之间的距离以及透明值的代码如下所示


image.png

接下去只需要将画圆点的透明值设置成div以及半径去乘上div就好了


image.png
0614aa1.gif

我们看到效果图上扩散的区域已经变成了一个圆形了,到了这里我们像素点的主要玩法就讲完了,接下去就是利用上面讲到的知识点,来做一个开灯关灯的效果


关灯后的效果


关灯后一般都是漆黑一片,但隐约还能有点能见度,所以我们这里的黑也要带点透明度,然后圆点的个数也要增多,要让单个圆点变得不明显,所以gridCount首先增加到300


image.png

然后将非扩散区域的背景色调成有点透明的黑色,并且增大圆点半径值,目的是去除圆点之间的间隙,扩散的圆点的背景色也设置成带点透明,并且半径在乘上div的基础上再减小一点,目的是加强扩散区域的灯光朦胧感


image.png
0614aa3.gif

绘制电灯,确定扩散中心位置


到了这里,扩散区域的代码暂时先告一段落,我们将电灯绘制出来,后面电灯的灯泡就作为我们扩散的中心区域,绘制电灯都是些基本Canvas绘图技巧,不多做介绍,直接贴上电灯的代码


image.png

drawCircle函数用来画灯泡,灯泡的中心点就是我们扩散的中心坐标tapX,tapY,函数drawline是画的电线,函数drawArc是画的罩在灯泡外面的灯罩,另外tapX,tapY的具体值就从点击获取变成了一个固定值


image.png

整个电灯的代码就完成了,效果如下


image.png

调节电灯亮度


当我们在生活中调节灯泡亮度的时候,灯泡的亮度会越来越亮,颜色会越来越深,那么这边如果要实现这一点的话,就需要确定一个最亮值以及最暗值,然后通过函数animateColorAsState来创建个颜色过渡的动画过程


image.png

lightState是这个开关灯的状态,作为当前所在的函数的入参从上层传进来


image.png

我们在最外层Window函数里面建立个菜单项,添加两个选项开灯与关灯,用来控制lightState的值


image.png

有了切换状态的开关,就开启了颜色过渡的动画,灯泡的色值就用lightColor来取代


image.png
0614aa4.gif

调节灯光扩散区域大小


灯光亮度能够有个由弱到强的过程了,那么灯光的扩散范围也应该有所变化,而上面我们已经知道了,控制扩散区域大小的变量就是circleSize,所以我们只要通过改变circleSize就能达到改变扩散范围的目的了,这里同样也创建个circleSize的过渡动画


image.png

然后我们更改下绘制扩散区域的条件,之前是将div小于1作为绘制扩散区域条件,现在就不需要加这个限制了,因为灯光照射的范围肯定是整个窗口范围,所以最后扩散的区域一定是到窗口以外的地方,但是绘制的条件还是有的,那就是circleSize大于最小值的时候,所以最终的代码如下


image.png

至于为什么不将判断条件设置成判断开关的开启状态是因为当开关关闭的时候,窗口一下子就变黑了,这里也希望关闭时候也有一个过渡的效果,我们看下现在的效果


0614aa5.gif

开关灯的效果就做好了,现在我们可以找一张看书的图片,然后在最外层用Image组件展示出来,那么开灯看书的效果就做好了


image.png
0614aa6.gif

源码地址


总结


最近在各种琢磨怎么做点好玩的动画效果出来,感觉在Compose里面做动效比在Android View里面简单多了,比如像这篇文章里面说到的内容,是不是改一改,一个水波纹效果就出来了,再改一改,一个数据加载完以后的转场效果也出来了,大家也可以在自己项目里面动手试试看


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

RecyclerView优化实战指南

在 Android 开发中,RecyclerView 是一个非常常用的组件,用于展示大量数据。然而,如果不进行优化,RecyclerView 可能会导致 UI 卡顿、内存泄漏等问题。本文将介绍一些优化技巧,帮助你更好地使用 RecyclerView。 简介 R...
继续阅读 »

在 Android 开发中,RecyclerView 是一个非常常用的组件,用于展示大量数据。然而,如果不进行优化,RecyclerView 可能会导致 UI 卡顿、内存泄漏等问题。本文将介绍一些优化技巧,帮助你更好地使用 RecyclerView。


简介


RecyclerView 是 Android 的一个高级 UI 组件,用于展示大量数据。它可以自动回收不可见的视图,并且可以使用不同的布局管理器来实现不同的布局。RecyclerView 还提供了一些回调函数,允许你在视图复用时进行一些自定义操作。


RecyclerView 可以大大简化开发过程,但是如果不进行优化,它可能会导致一些性能问题。下面将介绍一些优化技巧,帮助你充分发挥 RecyclerView 的性能。


优化技巧


对于 RecyclerView,我们可以采用以下优化技巧:


1. 使用 DiffUtil


DiffUtil 是计算两个列表之间差异的工具类,可帮助 RecyclerView 局部刷新数据。使用 DiffUtil 可以提升性能,减少 UI 卡顿。在 Adapter 中重写 DiffUtil.Callback,创建新列表的 DiffResult 与旧列表进行比较,从而更新列表数据。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
fun updateData(newData: List<Data>) {
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = dataSet.size
override fun getNewListSize() = newData.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
dataSet[oldItemPosition].id == newData[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
dataSet[oldItemPosition] == newData[newItemPosition]
})
diffResult.dispatchUpdatesTo(this)
dataSet = newData
}
}

2. 使用 ViewHolder


ViewHolder 是一种模式,用于缓存 RecyclerView 中的视图,减少内存开销,提高性能。使用 ViewHolder,可以在 Adapter 中重写 onCreateViewHolder 方法创建 ViewHolder,并在 onBindViewHolder 方法中获取 ViewHolder 显示的 view,并更新数据。


代码演示:

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.title)
val subTitleTextView: TextView = itemView.findViewById(R.id.subtitle)
// ...
}

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.titleTextView.text = dataSet[position].title
holder.subTitleTextView.text = dataSet[position].subTitle
// ...
}
}

3. 使用异步加载


如果 RecyclerView 需要加载大量数据,可以考虑使用异步加载来避免 UI 卡顿。以下是异步加载的示例:在 onBindViewHolder 中使用线程池 executor 和 ImageLoader 下载图片,并在下载完成后将其设置到 ImageView 上。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
if (dataSet[position].imageURL != null) {
holder.imageView.setImageResource(R.drawable.placeholder)
holder.imageView.tag = dataSet[position].imageURL
executor.execute {
val bitmap = ImageLoader.fetchBitmapFromURL(dataSet[position].imageURL!!)
if (holder.imageView.tag == dataSet[position].imageURL) {
holder.imageView.post { holder.imageView.setImageBitmap(bitmap) }
}
}
} else {
holder.imageView.setImageBitmap(null)
}
// ...
}
}

object ImageLoader {
// ...
fun fetchBitmapFromURL(url: String): Bitmap? {
// ...
return bitmap
}
}

4. 合理使用布局管理器


RecyclerView 提供多种布局管理器,每种管理器都适用于不同的场景。我们应该根据具体需求选择适合的管理器。以下是布局管理器的示例:


代码演示:

val layoutManager = when (layoutType) {
LayoutType.LINEAR -> LinearLayoutManager(context)
LayoutType.GRID -> GridLayoutManager(context, spanCount)
LayoutType.STAGGERED_GRID -> StaggeredGridLayoutManager(spanCount, orientation)
}
recyclerView.layoutManager = layoutManager

5. 使用数据绑定


数据绑定是一种将数据直接绑定到视图上的技术,减少代码量,提高代码可读性。我们可以在 adapter_layout.xml 中使用 <layout> 标签,将数据绑定到视图的布局文件中,从而减少代码量。


代码演示:

<layout>
<data>
<variable name="data" type="com.example.Data" />
</data>
<LinearLayout ...>
<TextView android:text="@{data.title}" ... />
<TextView android:text="@{data.subtitle}" ... />
</LinearLayout>
</layout>

在 Adapter 中使用 DataBindingUtil.inflate 方法,将 layout 绑定到 Data 中并设置到 ViewHolder 上。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding.root)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.binding.data = dataSet[position]
// ...
}
// ...
}

6. 减少布局中嵌套层级


布局中的嵌套层级越多,性能就越低,所以需要尽可能减少嵌套层级。可以使用 ConstraintLayout 或者扁平布局来减少嵌套层级。


7. 设置 Recyclerview 的固定大小


在 Recyclerview 的布局中,设置 android:layout_heightandroid:layout_width 的值为具体数值,可以避免列表项的宽高随着内容的变化而变化,从而使布局横向和纵向的测量也相应变快。


8. 禁止自动滑动


当数据项发生变化,RecyclerView 默认会自动滚动到新位置。如果这种行为不是必需的,可以在 Adapter 中重写 onItemRangeChanged 方法,并在其中禁止滑动。


代码演示:

override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
if (itemCount == 1) {
notifyItemChanged(positionStart)
} else {
notifyDataSetChanged()
}
recyclerView.stopScroll()
}

9. 使用预加载


使用预加载技术可以使 RecyclerView 在滑动过程中提前加载更多数据,保证滑动的流畅性和用户体验。


这些技巧可以根据具体的应用情况来使用,针对不同的问题提供不同的解决方案,从而提升 RecyclerView 的性能。如果需要更高级的功能,可以考虑使用 RecyclerView 提供的其它高级接口。


结论


通过本文,我们介绍了一些优化 RecyclerView 的技巧,包括使用 DiffUtil、使用 ViewHolder、使用异步加载、合理使用布局管理器、使用数据绑定、减少布局中嵌套层级、设置 RecyclerView 的固定大小、禁止自动滑动、使用预加载等。我们可以根据实际需求选择合适的优化方案,提升 RecyclerView 的性能,使其更加流畅。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


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

安卓-入门kotlin协程

作者 大家好,我叫小琪; 本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队; 目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。 一些概念 在了解协程之前,我们先回顾一下线程、进程的概...
继续阅读 »

作者


大家好,我叫小琪;


本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队;


目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。


一些概念


在了解协程之前,我们先回顾一下线程、进程的概念


img

1.进程:拥有代码和打开的文件资源、数据资源、独立的内存空间,是资源分配的最小单位。


2.线程:从属于进程,是程序的实际执行者,一个进程至少包含一个线程,操作系统调度(CPU调度)执行的最小单位


3.协程:



  • 不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行

  • 进程、线程是操作系统维度的,协程是语言维度的。


协程特点



  • 异步代码同步化


下面通过一个例子来体验kotlin中协程的这一特点


有这样一个场景,请求一个网络接口,用于获取用户信息而后更新UI,将用户信息展示,用kotlin的协程这样写:

GlobalScope.launch(Dispatchers.Main) {   // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
tvName.text = user.name // 主线程更新 UI
}

而通过 Java 实现以上逻辑,我们通常需要这样写:

api.getUser(new Callback<User>() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvName.setText(user.name);
}
})
}

@Override
public void failure(Exception e) {
...
}
});

java中的这种异步回调打乱了正常代码顺序,虽说保证了逻辑上是顺序执行的,但使得阅读相当难受,如果并发的场景再多一些,将会出现“回调地狱”,而使用了 Kotlin 协程,多层网络请求只需要这么写:

GlobalScope.launch(Dispatchers.Main) {       // 开始协程:主线程
val token = api.getToken() // 网络请求:IO 线程
val user = api.getUser(token) // 网络请求:IO 线程
tvName.text = user.name // 更新 UI:主线程
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码


协程初体验


1.引入依赖

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'

2.第一个协程程序


布局中添加一个button,并为它设置点击事件

btn.setOnClickListener {
Log.i("TAG","1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
CoroutineScope(Dispatchers.Main).launch{
delay(1000) //延迟1000ms
Log.i("TAG","2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
}
Log.i("TAG","3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程....[当前线程为:main]
3.BtnClick.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]

通过CoroutineScope.launch方法开启了一个协程,launch后面花括号内的代码就是运行在协程内的代码。协程启动后,协程体里的任务就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来


进入到launch方法看看它里面的参数,

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
}

对这些参数的说明:



  • context:协程上下文,可以指定协程限制在一个特定的线程执行。常用的有Dispatchers.Default、Dispatchers.Main、Dispatchers.IO等。Dispatchers.Main即Android 中的主线程;Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求

  • start: 协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT指协程立即执行,另外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED

  • block:协程主体,即要在协程内部运行的代码,也就是上述例子花括号中的代码

  • 返回值Job:对当前创建的协程的引用。可以通过调用它的的join、cancel等方法来控制协程的启动和取消。


3.挂起函数


上面有提到”挂起“即suspend的概念,


回到上面的例子,有一个delay函数,进到这个函数看看它的定义:

public suspend fun delay(timeMillis: Long) {...}

发现多了个suspend关键字,也就是上文中提到的“挂起”,根据程序的输出结果看,首先输出了1,3,等待一秒后再输出了2,而且打印的线程显示的也是主线程,这说明,协程在遇到suspend关键字的时候,会被挂起,所谓的挂起,就是程序切了个线程,并且当这个挂起函数执行完毕后又会自动切回来,这个切回来的动作其实就是恢复,因此挂起、恢复也是协程的一个特点。所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的代码重新进入协程所在线程的过程。协程就是通过这个挂起恢复机制进行线程的切换。


关于suspend函数也有个规定:挂起函数必须在协程或者其他挂起函数中被调用,换句话说就是挂起函数必须直接或者间接地在协程中执行。


4.创建协程的其他方式


上面介绍了通过launch方法创建协程,当遇到 suspend 函数的时候 ,该协程会自动逃离当前所在的线程执行任务,此时原来协程所在的线程就继续干自己的事,等到协程的suspend 函数执行完成后又自动切回来原来线程继续往下走。 但如果协程所在的线程已经运行结束了,协程还没执行完成就不会继续执行了 。为了避免这样的情况就需要结合 runBlocking 来暂时阻塞当前线程,保证代码的执行顺序。


下面我们通过runBlocking 来创建协程

btn.setOnClickListener {
Log.i("TAG", "1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
runBlocking {
delay(1000) //延迟1000ms
Log.i("TAG", "2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
}
Log.i("TAG", "3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]
3.BtnClick.... [当前线程为:main]

可以看到运行结果顺序和上面的launch方式不同,这里的log先输出1、2,再输出3,程序会等待runBlocking中的代码块执行完后才会还执行后面的代码,因此launch是非阻塞的,而runBlocking是阻塞式的。


launch和runBlocking都是没有返回结果的,有时我们想知道协程的返回结果,拿到结果去做业务例如UI更新,这时withContext和async就派上用场了。


先看下withContext的使用场景:

 btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val startTime = System.currentTimeMillis()
val task1 = withContext(Dispatchers.IO) {
delay(2000)
Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
1 //返回结果赋值给task1
}

val task2 = withContext(Dispatchers.IO) {
delay(1000)
Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
2 //返回结果赋值给task2
}
Log.i(
"TAG",
"3.计算task1+task2 = ${task1+task2} , 耗时 ${System.currentTimeMillis() - startTime} ms [当前线程为:${Thread.currentThread().name}]"
)
}
}

输出结果为:

 1.执行task1.... [当前线程为:DefaultDispatcher-worker-3]
2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
3.计算 task1+task2 = 3 , 耗时 3032 ms [当前线程为:main]

从输出结果可以看出,通过withContext指定协程运行在一个io线程,延迟了两秒后返回结果1赋值给task1,之后程序向下执行,同样的,延迟了1s后返回结果2赋值给了task2,最后执行到步骤三,并且打印了耗时时间,可以看到,耗时是两个task的时间总和,也就是先执行完task1,在执行task到,说明withContext是串行执行的,这适用于在一个请求结果依赖另一个请求结果的场景。


如果同时处理多个耗时任务,且这几个任务都没有相互依赖时,可以使用 async ... await() 来处理,将上面的例子改为 async 来实现如下

btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val startTime = System.currentTimeMillis()
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
1 //返回结果赋值给task1
}

val task2 = async(Dispatchers.IO) {
delay(1000)
Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
2 //返回结果赋值给task2
}

Log.i(
"TAG",
"3.计算 task1+task2 = ${task1.await()+task2.await()} , 耗时 ${System.currentTimeMillis() - startTime} ms [当前线程为:${Thread.currentThread().name}]"
)
}
}

输出结果:

2.执行task2.... [当前线程为:DefaultDispatcher-worker-4]
1.执行task1.... [当前线程为:DefaultDispatcher-worker-5]
3.计算 task1+task2 = 3 , 耗时 2010 ms [当前线程为:main]

可以看到,输出的总耗时明显比withContext更短,且task2优先task1执行完,说明async 是并行执行的。


总结


本文首先通过对进程、线程、协程的区别认清协程的概念,接着对协程的特点也就是优势进行了介绍,最后通过几个实例介绍了协程的几种启动方式,并分析了其各自特点和使用场景,本文更多是对协程的概念和使用进行了简单的介绍,而协程的内容远不止这些。


结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


作者:37手游移动客户端团队
链接:https://juejin.cn/post/7245096955966177338
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

uni-app实现微信小程序蓝牙打印

web
打印流程 小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备...
继续阅读 »

打印流程


小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备写入数据


1.初始化蓝牙模块 uni.openBluetoothAdapter


注意:其他蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。


uni.openBluetoothAdapter({
success(res) {
console.log(res)
}
})

2.开始搜索附近的蓝牙设备 uni.startBluetoothDevicesDiscovery


此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索。


uni.startBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

3.获取搜索到的蓝牙列表 uni.getBluetoothDevices


获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备(不是很准确,有时会获取不到)。


uni.getBluetoothDevices({
success(res) {
console.log(res)
}
})

4.监听寻找到新设备的事件 uni.onBluetoothDeviceFound


监听寻找到新设备的事件,跟第三步一起使用,确保能获取附近所有蓝牙设备。


uni.onBluetoothDeviceFound(function (devices) {
console.log(devices)
})

5.连接蓝牙设备 uni.createBLEConnection


若APP在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,避免用户每次都要连接才能打印,省略二三四步减少资源浪费。


uni.createBLEConnection({
deviceId:获取到蓝牙的deviceId,
success(res) {
console.log(res)
}
})

6.关闭搜索蓝牙设备事件 uni.stopBluetoothDevicesDiscovery


停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。


uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

7.获取蓝牙设备的所有服务 uni.getBLEDeviceServices


uni.getBLEDeviceServices({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success(res) {
console.log('device services:', res.services)
}
})

8.获取服务的所有特征值 uni.getBLEDeviceCharacteristics


uni.getBLEDeviceCharacteristics({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
success(res) {
console.log('device getBLEDeviceCharacteristics:', res.characteristics)
}
})
三种不同特征值的id
for (var i = 0; i < res.characteristics.length; i++) {
if (!notify) {
notify = res.characteristics[i].properties.notify;
if (notify) readId = res.characteristics[i].uuid;
}
if (!indicate) {
indicate = res.characteristics[i].properties.indicate;
if (indicate) readId = res.characteristics[i].uuid;
}
if (!write) {
write = res.characteristics[i].properties.write;
writeId = res.characteristics[i].uuid;
}
if ((notify || indicate) && write) {
/* 获取蓝牙特征值uuid */
success &&
success({
serviceId,
writeId: writeId,
readId: readId,
});
finished = true;
break;
}

9.向蓝牙设备写入数据 uni.writeBLECharacteristicValue


向低功耗蓝牙设备特征值中写入二进制数据。注意:必须设备的特征值支持 write 才可以成功调用。


并行调用多次会存在写失败的可能性。


APP不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。


若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。


安卓平台上,在调用 notifyBLECharacteristicValueChange 成功后立即调用 writeBLECharacteristicValue 接口,在部分机型上会发生 10008 系统错误


// 向蓝牙设备发送一个0x00的16进制数据
const buffer = new ArrayBuffer(1)
const dataView = new DataView(buffer)
dataView.setUint8(0, 0)
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
// 这里的value是ArrayBuffer类型
value: buffer,
success(res) {
console.log('writeBLECharacteristicValue success', res.errMsg)
}
})

写在最后


DEMO地址:gitee.com/zhou_xuhui/… (plus可能会报错,demo中注释掉就好,不影响流程)


打印机CPCL编程参考手册(CPCL 语言):http://www.docin.com/p-2160

作者:我是真的菜呀
来源:juejin.cn/post/7246264754141773885
10502…

收起阅读 »

LeCun再爆金句:ChatGPT?连条狗都比不上!语言模型喂出来的而已

【新智元导读】 LeCun昨天在一场辩论中再贬ChatGPT!形容这个AI模型的智力连狗都不如。 图灵三巨头之一的LeCun昨日又爆金句。 「论聪明程度,ChatGPT可能连条狗都不如。」 这句话来自本周四LeCun在Vivatech上和Jacques Att...
继续阅读 »
【新智元导读】 LeCun昨天在一场辩论中再贬ChatGPT!形容这个AI模型的智力连狗都不如。

图灵三巨头之一的LeCun昨日又爆金句。


「论聪明程度,ChatGPT可能连条狗都不如。」


这句话来自本周四LeCun在Vivatech上和Jacques Attalie的一场辩论,可谓精彩纷呈。


图片


CNBC甚至直接把这句话放到了标题里,而LeCun也在之后火速转推。


“ChatGPT和狗:比不了一点”


LeCun表示,当前的AI系统,哪怕是ChatGPT,根本就不具备人类的智能水平,甚至还没有狗聪明。


要知道,在AI爆炸发展的今天,无数人已经为ChatGPT的强大性能所折服。在这种情况下,LeCun的这句话可谓惊世骇俗。


图片


不过,LeCun一贯的观点都是——不必太过紧张,如今的AI智能水平远远没到我们该担忧的地步。


而其他的科技巨头则基本和LeCun持截然相反的意见。


比如同为图灵三巨头的Hinton和Bengio,以及AI届人士由Sam Altman挑头签的公开信,马斯克的危机言论等等。


在这种大环境下,LeCun一直「不忘初心」,坚定认为现在真没啥可担心的。


LeCun表示,目前的生成式AI模型都是在LLM上训练的,而这种只接受语言训练的模型聪明不到哪去。


「这些模型的性能非常有限,他们对现实世界没有任何理解。因为他们纯粹是在大量文本上训练的。」


而又因为大部分人类所拥有的知识其实和语言无关,所以这部分内容AI是捕捉不到的。


LeCun打了个比方,AI现在可以通过律师考试,因为考试内容都停留在文字上。但AI绝对没可能安装一个洗碗机,而一个10岁的小孩儿10分钟就能学会怎么装。


图片


这就是为什么LeCun强调,Meta正尝试用视频训练AI。视频可不仅仅是语言了,因此用视频来训练在实现上会更加艰巨。


LeCun又举了个例子,试图说明什么叫智能上的差别。


一个五个月大的婴儿看到一个漂浮的东西,并不会想太多。但是一个九个月大的婴儿再看到一个漂浮的物体就会感到非常惊讶。


因为在九个月大的婴儿的认知里,一个物体不该是漂浮着的。


LeCun表示,如今我们是不知道如何让AI实现这种认知能力的。在能做到这一点之前,AI根本就不可能拥有人类智能,连猫猫狗狗的都不可能。


图片


Attali:我也要签公开信


在这场讨论中,法国经济和社会理论家Jaques Attali表示,AI的好坏取决于人们如何进行利用。


然而他却对未来持悲观态度。他和那些签公开信的AI大牛一样,认为人类会在未来三四十年内面临很多危险。


他指出,气候灾难和战争是他最为关注的问题,同时担心AI机器人会「阻挠」我们。


Attali认为,需要为AI技术的发展设置边界,但由谁来设定、设定怎么样的边界仍是未知的。


这和前一阵子签的两封公开信所主张的内容相同。


图片


图片


当然,公开信LeCun也是压根没搭理,发推高调表示哥们儿没签。


图片


LeCun炮轰ChatGPT——没停过


而在此之前,LeCun针对ChatGPT不止讲过过一次类似的话。


就在今年的1月27日,Zoom的媒体和高管小型聚会上,LeCun对ChatGPT给出了一段令人惊讶的评价——


「就底层技术而言,ChatGPT并不是多么了不得的创新。虽然在公众眼中,它是革命性的,但是我们知道,它就是一个组合得很好的产品,仅此而已。」


图片


「除了谷歌和Meta之外,还有六家初创公司,基本上都拥有非常相似的技术。」


此外,他还表示,ChatGPT用的Transformer架构是谷歌提出的,而它用的自监督方式,正是他自己提倡的,那时OpenAI还没诞生呢。


当时闹得更大,Sam Altman直接在推上给LeCun取关了。


1月28日,LeCun梅开二度,继续炮轰ChatGPT。


他表示,「大型语言模型并没有物理直觉,它们是基于文本训练的。如果它们能从庞大的联想记忆中检索到类似问题的答案,他们可能会答对物理直觉问题。但它们的回答,也可能是完全错误的。」


而LeCun对LLM的看法一以贯之,从未改变。从昨天的辩论就可以看出,他觉得语言训练出来的东西毫无智能可言。


今年2月4日,LeCun直白地表示,「在通往人类级别AI的道路上,大型语言模型就是一条歪路」。


图片


「依靠自动回归和响应预测下一个单词的LLM是条歪路,因为它们既不能计划也不能推理。」


当然,LeCun是有充分的理由相信这一点的。


ChatGPT这种大语言模型是「自回归」。AI接受训练,从一个包含多达14000亿个单词的语料库中提取单词,预测给定句子序列中的最后一个单词,也就是下一个必须出现的单词。


图片


Claude Shannon在上个世纪50年代开展的相关研究就是基于这一原则。


原则没变,变得是语料库的规模,以及模型本身的计算能力。


LeCun表示,「目前,我们无法靠这类模型生成长而连贯的文本,这些系统不是可控的。比如说,我们不能直接要求ChatGPT生成一段目标人群是13岁儿童的文本。


其次,ChatGPT生成的文本作为信息来源并不是100%可靠的。GPT的功能更像是一种辅助工具。就好比现有的驾驶辅助系统一样,开着自动驾驶功能,也得把着方向盘。


而且,我们今天所熟知的自回归语言模型的寿命都非常短,五年算是一个周期,五年以后,过去的模型就没有人再会用了。


而我们的研究重点,就应该集中在找到一种是这些模型可控的办法上。换句话说,我们要研究的AI,是能根据给定目标进行推理和计划的AI,并且得能保证其安全性和可靠性的标准是一致的。这种AI能感受到情绪。」


图片


要知道,人类情绪的很大一部分和目标的实现与否有关,也就是和某种形式的预期有关。


而有了这样的可控模型,我们就能生成出长而连贯的文本。


LeCun的想法是,未来设计出能混合来自不同工具的数据的增强版模型,比如计算器或者搜索引擎。


像ChatGPT这样的模型只接受文本训练,因此ChatGPT对现实世界的认识并不完整。而想要在此基础上进一步发展,就需要学习一些和整个世界的感官知觉、世界结构有关的内容。


然而好玩儿的是,Meta自己的模型galactica.ai上线三天就被网友喷的查无此人了。


图片


原因是胡话连篇。


笑。


参考资料:http://www.cnbc.com/2023/0

作者:新智元
来源:juejin.cn/post/7246334166950150202
6/15/…

收起阅读 »

悟了两星期终于悟了,移动端适配核心思想——没讲懂揍我

web
移动端开发与pc端适配的不同 pc端布局常用方案 所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱) 先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的...
继续阅读 »

移动端开发与pc端适配的不同


pc端布局常用方案


所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱)


先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的形式。也就是说所有的内容都写在版心容器盒子里,这个容器盒子设置:margin: 0 auto; & min-width: <版心宽度> & width: <版心宽度>就可以保证:




  • 当用户的屏幕(浏览器)宽度很大,或者缩放浏览器到很小比例时,此时浏览器的宽度大于版心盒子的width,版心容器会自动生成margin-left & margin-right,总会保证版心容器处于页面的正中心。


    这里可以提一嘴pc端浏览器缩放的原理:页面所有元素的css宽高都不会改变,只是css像素的在屏幕上展示的大小缩水了,具体点来说,原本700px * 700px的盒子在浏览器上用10cm * 10cm面积(的物理像素)渲染,但现在用原本<浏览器缩放比率> * 10cm * 10cm面积(的物理像素)渲染。




  • 当用户的屏幕小于版心盒子的width,出现横向滚动条,版心盒子的左右margin为0,width内的内容可滑动滚动条完整查看。




可以参考大淘宝pc端官网就是版心布局的实践。


好了,那么问题来了,移动端为啥不能照搬pc端的这种适配方案呢?


我们有必要先梳理一下移动端对页面进行渲染展示的逻辑:


移动端页面渲染的逻辑


<meta name="viewport">的情况


在html文档里没有<meta name="viewport">标签配置的情况下(通过对比即可理解<meta>标签的意义):


plus:如下整个流程篇口语话主要是梳理核心思路,没有一字一板的细节考究



  1. 我们项目中布局写的所有dom元素的css大小都正常(完全按照css大小的预期)在一个非常大的空间进行渲染,这个空间可能不是无限大,但是为了帮助理解,因为这个空间的大小一般不影响我们项目的正常布局,所以我们可以理解为无限大,这是第一步,即项目页面就像在pc端一样完全按照css写的大小以及布局进行渲染。



  1. 因为我们的移动端设备没有电脑屏幕那么大,所以会把第一步在“很大空间”渲染的页面进行缩小,直至缩小到我们的大页面宽度正好与手机屏幕的宽度一样即可。所以第二步相当于为了让用户把页面看全,手机自动把页面缩小至屏幕内。


为了帮助大家理解,也验证我上面的说法,我写了如下的pc端的版心布局的页面:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

我把上面的页面部署到osrc(一个国内的免费部署网站,类似于vercel)上了(不可用chrome浏览器的移动端去模拟移动端访问的场景,chrome浏览器只是模拟了屏幕大小,而并没有模拟移动端环境,也就是说根本不会处理<meta>标签,所以这里我们需要部署),大家可以自行用pc端和移动端访问体验(实践一下绝对秒懂我上面的文字),为了照顾没有双端设备的读者,我截一下图(直接喂饭到胃哈哈)


pc端访问:


pc端访问版心布局.png


移动端访问:


移动端访问版心布局.jpg


清晰了吧兄弟们,我们写死的1200px宽的container盒子因为手机本身没这么大,所以缩小之后塞进了手机屏幕中,仔细看手机中的文字,已经被缩小的看不清了。


配置<meta name="viewport">的情况


暂时只给我们的index.html<meta name="viewport">添加一个content="width=device-width, initial-scale=1.0"


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

重新部署后访问查看效果,有meta的页面,部署地址<meta>标签是针对移动端的,所以pc端完全没影响,跟上面一样。现在我们访问移动端效果如下,我没有缩小图片,注意观察页面底部出现滚动条了(纵向滚动条有滚动所以文字没展示,不重要):


设置meta后移动端访问效果.jpg


解释一下content="width=device-width, initial-scale=1.0"的作用。


解读<meta> & dip & 布局视口(自认为最精华,全网少数不带偏新人的解释)


其实相当于我们在content字段中进行了两项配置,第一项是width=device-width,第一个width是指布局视口的宽度,引出概念,何为布局视口?还记得我们上面说的在没有<meta>时的那个非常大的布局空间嘛,就是它!我们让这个空间的宽度等于device-widthdevice-width就是指dip即设备独立像素,第二个概念,何为dip(device independent piexl设备独立像素)呢?听我的,完全不要被网上各种乱七八糟的解释弄迷糊了,什么dpr,什么物理像素,我只能说那些东西与我们开发者并没有直接关系,笔者读了几户所有能搜到的各种移动端入门文章,一言难尽... ,我来给出对于dip的理解,每一个型号的移动设备都具有的一个大小,这个大小是用设备独立像素dip来描述的,所以它仅仅是一个描述大小的单位,但是这个单位究竟是多大呢,换句话说dip有何特殊性呢?


在移动端不缩放的情况下,一个css像素等于一个设备独立像素dip

(chrome浏览器的移动端开发工具里显示的就是dip大小)也就是说,我们让布局视口的宽度等于设备的dip宽度,这里注意:布局视口由原来的”无限大“现在改为一个具体的数值,并不会影响页面的正常布局,页面还是会完整渲染, 只是最后不用缩小放进屏幕了,因为我们缩小的目的就是让布局视口完整的展现在屏幕中。因为屏幕不能展示完整整个页面的布局,所以底部出现滚动条。用户可以滚动访问页面全部内容。


其实这里initial-scale=1.0的作用就是让移动端浏览器不自行缩放,不然的话浏览器会把如上页面再缩小,然后放到手机屏幕里去。


关于<meta name="viewport">的最佳实践


简简单单如下:


<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

为什么说这是最佳实践,论证它其实还缺少一个关键点,也就是移动端css单位的选取,概括来说,width=device-width配置与移动端css单位的选取两者相辅相成,共同构成了“最佳实践”。


先说一下css单位选取——以vw为例:vw是相对屏幕大小的一个长度单位,1vw等于移动设备屏幕宽度的1%


如何理解“最佳实践”?


首先width=device-width保证了无论何种机型的移动设备,我们开发时写页面的布局视口始终等于屏幕宽度,但看这一点,确实没啥用。如果再来一个条件:页面中所有长度单位用vw来表达。细品!


如何细品?别忘了初心,我们的目标是在不同的移动设备上都能有统一的展示效果,开品:我们用不同dip宽度的设备打开网页,首先布局视口的大小会随着设备dip的不同而不同,也就是始终等于dip宽度:


布局视口宽度 === 设备dip宽度,

并且我们的所有元素大小单位都是vw,也就是说页面中所有元素大小都以屏幕宽度为参照物。最终的效果就是,一个dip宽度超级小的设备打开网页,与一个dip宽度非常大的设备打开网页,看到的页面内容是完全相似的,也就是每个元素在页面中所占的比例不同设备都一样(不同点就在于屏幕本身的大小不一样)!


一般<meta>标签的content中还会设置initial-scale=1.0, maximum-scale=1.0, user-scalable=no,即不让页面进行缩放,感觉这个看需求吧,不让缩小应该是必须的,因为可以想一想,用户缩小完全没有意义呐!(需要大家自己去理解,属于只可意会),至于让不让放大,应该是看情况吧,反正移动端淘宝官网是允许放大的。


移动端适配方案理解


主流的有vw方案、flexible + rem方案,总而言之,把元素的大小用rem来表示或者vw表示,本质都是以手机屏幕宽度为参考,vw比较直接,表达的意思就是1vw等于手机屏幕宽度的百分之一;rem比较间接,通过flexible.js先把得知屏幕宽度是多少px,然后设置<html>font-size,进而所有元素的rem其实还是表达占屏幕宽度的百分之多少。


当然两种方案都有一些技术细节问题需要解决,比如1px问题、安全区域问题等等。这里就不多说了。


相信能一步一步走到这里的同志,对移动端适配绝对有了一个清晰的把握。


2023.6.19,3: 59。

作者:荣达
来源:juejin.cn/post/7246001188448731196
更文不易,点个赞吧!

收起阅读 »

像支付宝那样“致敬”第三方开源代码

前言 通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源...
继续阅读 »

前言


通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源代码的任何信息。
image.png


不过,作为一个有“追求”的码农,我们还是想对开源软件致敬一下的,毕竟,没有他们我都不知道怎么写代码。然而,我们的 App 里用了那么多第三方开源插件,总不能一个个找出来一一致敬吧?怎么办?其实,Flutter 早就为我们准备好了一个组件,那就是本篇要介绍的 AboutDialog


AboutDialog 简介


AboutDialog 是一个对话框,它可以提供 App 的基本信息,如 Icon、版本、App 名称、版权信息等。
image.png


同时,AboutDialog还提供了一个查看授权信息(View Licenses)的按钮,点击就可以查看 App 里所有用到的第三方开源插件,并且会自动收集他们的 License 信息展示。所以,使用 AboutDialog 可以让我们轻松表达敬意。怎么使用呢?非常简单,我们点击一个按钮的时候,调用 showAboutDialog 就搞定了,比如下面的代码:


IconButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: '岛上码农',
applicationVersion: '1.0.0',
applicationIcon: Image.asset('images/logo.png'),
applicationLegalese: '2023 岛上码农版权所有'
);
},
icon: const Icon(
Icons.info_outline,
color: Colors.white,
),
),

参数其实一目了然,具体如下:



  • context:当前的 context

  • applicationName:应用名称;

  • applicationVersion:应用版本,如果要自动获取版本号也可以使用 package_info_plus 插件。

  • applicationIcon:应用图标,可以是任意的 Widget,通常会是一个App 图标图片。

  • applicationLegalese:其他信息,通常会放置应用的版权信息。


点击按钮,就可以看到相应的授权信息了,点击一项就可以查看具体的 License。我看了一下使用的开源插件非常多,要是自己处理还真的很麻烦。
image.png


可以说非常简单,当然,如果你直接运行还有两个小问题。


按钮本地化


AboutDialog 默认提供了两个按钮,一个是查看授权信息,一个是关闭,可是两个按钮 的标题默认是英文的(分别是VIEW LICENSES和 CLOSE)。
image.png


如果要改成本地话的,还需要做一个自定义配置。我们扒一下 AboutDialog 的源码,会发现两个按钮在DefaultMaterialLocalizations中定义,分别是viewLicensesButtonLabelcloseButtonLabel。这个时候我们自定义一个类集成DefaultMaterialLocalizations就可以了。


class MyMaterialLocalizationsDelegate
extends LocalizationsDelegate<MaterialLocalizations>
{
const MyMaterialLocalizationsDelegate();

@override
bool isSupported(Locale locale) => true;

@override
Future<MaterialLocalizations> load(Locale locale) async {
final myTranslations = MyMaterialLocalizations(); // 自定义的本地化资源类
return Future.value(myTranslations);
}

@override
bool shouldReload(
covariant LocalizationsDelegate<MaterialLocalizations> old) =>
false;
}

class MyMaterialLocalizations extends DefaultMaterialLocalizations {
@override
String get viewLicensesButtonLabel => '查看版权信息';

@override
String get closeButtonLabel => '关闭';

}

然后在 MaterialApp 里指定本地化localizationsDelegates参数使用自定义的委托类对象就能完成AboutDialog两个按钮文字的替换。


return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AboutDialogDemo(),
localizationsDelegates: const [MyMaterialLocalizationsDelegate()],
);

添加自定义的授权信息


虽然 Flutter 会自动收集第三方插件,但是如果我们自己使用了其他第三方的插件的话,比如没有在 pub.yaml 里引入,而是直接使用了源码。那么还是需要手动添加一些授权信息的,这个时候我们需要自己手动添加了。添加的方式也不麻烦,Flutter 提供了一个LicenseRegistry的工具类,可以调用其 addLicense 方法来帮我们添加授权信息。具体使用如下:


LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。\f如有问题可以加本人微信交流,微信号:island-coder。',
);
});

这个方法可以在main方法里调用。其中第一个参数是一个数组,是因为可以允许多个开源代码共用一份授权信息。同时,如果一份开源插件有多个授权信息,可以多次添加,只要名称一致,Flutter就会自动合并,并且会显示该插件的授权信息条数,点击查看时,会将多条授权信息使用分割线分开,代码如下所示:


void main() {
runApp(const MyApp());
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder。',
);
});

LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'使用时请注明来自岛上码农、。',
);
});
}

image.png


总结


本篇介绍了在 Flutter 中快速展示授权信息的方法,通过 AboutDialog 就可以轻松搞定,各位“抄代码”的码农们,赶紧用起来向大牛们致敬吧!



我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder


👍🏻:觉得有收获请点个赞鼓励一下!


🌟:收藏文章,方便回看哦!


💬:评论交流,互相进步!


作者:岛上码农
来源:juejin.cn/post/7246328828837871677

收起阅读 »

值得学习的JavaScript调试技巧

web
引言 最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各...
继续阅读 »

引言


最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各位。


一:console.dir


在打印dom节点时,普通的console.log是纯文本格式,而dir的打印是以对象的方式。因此在输出dom节点时,务必使用dir打印


<div id="main">
<div class="box1">
<p>p1</p>
</div>

</div>

let oD = document.querySelector('.box1')
console.log(oD)//普通的log输出
console.dir(oD)//dir输出方式

image.png


二:二次发起请求


在调试接口时,通常我们会刷新页面然后观察network的接口信息,如果项目加载时间过长,刷新页面查看接口的效率是十分低的。



  1. 对接口请求右键

  2. 选择Relpy xhr发送请求


image.png


三:接口请求参数修改


借助浏览器控制台可以不用修改代码就可以发送不同参数的新请求了。具体操作如下



  1. 对接口请求右键

  2. 选择copy。

  3. 再选择copy as fetch。

  4. 在console区域粘贴上面的请求信息,然后修改请求体参数。

  5. 然后切换到networkl查看最新请求的结果


效果展示


24.gif


四:css查看伪类hover,active样式


在控制台右侧选择:hov可以选择对应dom各种伪类状态下的css样式,十分的便捷


image.png


五:css样式跳转到对应文件查看


选择css样式,按住alt点击就可以跳到对应文件查看具体代码


25.gif


六:控制台输出选择的dom


首先在页面选择指定的位置dom,然后在在控制台使用$0就表示当前选中的dom了


26.gif


七:展开全部dom


有时候我们在页面查找一个dom时,它嵌套层级特别深。这巨他妈蛋疼一层层展开,这个时候我们就需要找到一键全部展开来帮助我们解决这个问题了。


27.gif


右键选择expand就可以展开选择的dom了。


八:断点调试


断点调试是本节最后一个内容了,它也是最核心的内容了,玩的6的是真的6,老大说我搞懂断点调试和对应的堆栈上下文就可以毕业了。(毕业=辞退?还是。。。)下面我列举的仅仅是入门级别的断点调试,只是说明如何上手操作,里面许多东西还望大家多多探索。


1. 打断点方式


代码中:debugger


在需要断点的地方写入debugger,此时程序运行后代码就会卡在这里,等待主人的安排


let a = 10
debugger
a++

浏览器中:



  1. 选择sources

  2. 在指定代码行左侧单击


image.png


2. 断点间调试


第一种断点调试是十分常用的方式,代码会从当前断点直接运行到下一个断点处执行,中间经过代码都默认被执行且跳过。如下图红色按钮就是断点间调试。


image.png


例子演示


28.gif


我们在上图中打了3个断点,逐个点击,首先从断点15行直接跳到断点17行,最后跳到19行。由于异步最后执行,所以最后又跳到断点15行结束。断点经过的地方鼠标移动到变量上可以查看其内部数据。


3. 逐步调试


逐步调试很明显就是字面意思,从当前断点位置开始一行一行的运行代码,稍微注意的是,遇到函数不进入函数的内部,而是直接执行完函数。


image.png


例子演示


29.gif


4. 进入与进出函数调试


逐步调试遇到函数是不进入函数内部的,因此需要借助进入和进出调试方式控制函数的访问


image.png


例子演示


30.gif


5. 逐步调试详细版


上面讲述了第一种逐步调试方式,其遇到函数是不进入函数内部的,而是直接执行函数。因此下面这种方式是逐步调试的详细版,它也是从断点位置逐步的调试运行,遇到函数也会进入函数的内部进行逐步执行。


image.png


九:React/Vue中尝试


有吊毛说react和vue咋调试?嗯,那个吊毛其实就是我,其实也很简单滴。



  1. 在需要调试的代码位置插入debugger

  2. 在浏览器控制台需要查看变量的地方插入断点

  3. 使用各种调试连招一顿操作就行。


代码例子


例如下面的例子,页面最后显示的num是多少?最后是101,不了解批量setState的开始肯定蒙,我们调试看看


import React,{useEffect, useState} from "react";
const Home = () => {
const [num,setNum] = useState(1)
useEffect(()=>{
debugger
setNum(100)
setTimeout(() => {
setNum(num+100)
}, 0);
},[])
return (
<div>num:{num}</div>
)
}
export default Home;

调试演示
根据调试发现,进入定时器的时候num还未更新,还是1。


31.gif


作者:前端兰博
来源:juejin.cn/post/7246376735838060603
收起阅读 »

前端时钟翻页效果,一看就会,一写就fei

web
最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。 一、元素拆解 从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一...
继续阅读 »

最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。


22.gif


一、元素拆解


动画拆解.png


从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一面显示旧文字的下半部分,另一面显示新文字的上半部分。翻转的动画,我们考虑采用FLIP的思想:



  1. 先实现【动画结束帧】的样式;

  2. 再从【动画开始帧】播放。


二、实现结束帧样式


准备工作:用vue脚手架创建一个模板项目,并添加一个div容器:


image.png


<!-- App.vue -->
<template>
<div id="app">
<test-comp/>
</div>
</template>


<!-- Test.vue -->
<template>
<div class="card"></div>
</template>


<style lang="less" scoped>
.card {
position: relative;
border: solid 4px black;
width: 400px;
height: 400px;
perspective: 1000px;
}
</style>


2.1 实现静止的上半面板


image.png


<template>
<div class="card">
<div class="half-card top-half"></div>
<!-- <div class="half-card bottom-half">财</div> -->
</div>

</template>

<style lang="less" scoped>
/* ... */
.half-card {
position: absolute;
width: 100%;
height: 50%;
overflow: hidden;
background-color: #2c292c;
color: white;
font-size: 320px;
}
.top-half {
line-height: 400px;
}
</style>


我们知道line-height配合font-size可以控制文字在垂直方向的位置,大多数情况下,文字顶部与容器顶部的距离公式为(line-height - font-size) / 2。


记容器高度h,文字大小f,容器只显示文字上半部分的情况下,上述距离的值为h - f / 2,即(line-height - f) / 2 = h - f / 2,所以line-height为2h(400px)。


2.2 实现静止的下半面板


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<div class="half-card bottom-half"></div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.bottom-half {
top: 50%;
line-height: 0;
}
</style>


在容器只显示文字下半部分的情况下,完整的文字顶部距离容器顶部的距离是-f / 2,那么就有(line-height - f) / 2 = - f / 2,即line-height = 0;


2.3 实现旋转面板


2.3.1 旋转面板的正面————新文字的上半部分


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<div class="half-card front-side"></div>
<!-- <div class="half-card back-side">发</div> -->
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.rotating-half {
position: absolute;
width: 100%;
height: 50%;
.half-card {
height: 100%;
}
}
.front-side {
line-height: 400px;
}
</style>


2.3.2 旋转面板的背面————旧文字的下半部分


怎么让一个div背对我们?只要让它绕着自己的腰部横线翻转180度即可(翻跟斗)。


image.png
image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<!-- <div class="half-card front-side">财</div> -->
<div class="half-card back-side"></div>
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.back-side {
line-height: 0;
transform: rotateX(180deg); // !!!!!!!!!!!
}
</style>


现在,如果把正面也加上,会发现这样一个问题:两个面的位置是重叠的,在模板中后声明的背面元素(即使它是背对着我们)会覆盖正面元素。我们想让这两个面在背对我们的状态下都不显示,这就需要到如下的css属性:backface-visibility: hidden。


此外,现在一个旋转面板中带有两个“面”,我们想要这两个面随着父元素面板的3d旋转一起旋转,也就是保持相对静止,这就需要设置旋转面板【将子元素纳入自己的3d变换空间】:transform-style: preserve-3d。


加上css后,让旋转面板简单地旋转一下,看看效果(效果图有点慢):


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-style: preserve-3d;
.half-card {
/* ... */
backface-visibility: hidden;
}
/* to delete */
transition: transform 1s;
&:hover { transform: rotateX(-180deg); }
}
/* ... */
</style>

2.gif


至此,三个面板静态效果已经完成:


image.png


三、播放动画


在第二节已经得到了动画结束时的状态。接下来需要从动画开始的状态进行播放。


3.1 设置好旋转轴


在目标动画中,旋转面板应该是绕着底边进行旋转的。把【变换原点】设置为底边的中点,这样,经过这个点的X轴就和底边所在的直线重合,绕X轴旋转就等价于绕底边旋转:


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-origin: center bottom;
}
/* ... */
</style>

3.2 找到动画开始帧,使用animate播放动画


动画开始时,旋转面板在主面板的下半区域。要从上半区域(无变换状态)到达下半区域,需要绕着底边逆时针旋转180度,因此开始帧所处于的变换状态就是rotateX(-180deg),从而得到动画的关键帧:


【transform: rotateX(-180deg)】->【transform: none】。


我们给旋转面板加上ref,然后在组件挂载完毕时播放即可:


<script>
export default{
mounted() {
this.$refs.rotate?.animate?.(
[
{ offset: 0, transform: 'rotateX(-180deg)' },
// { offset: 1, transform: 'none' },
],
{
duration: 1000,
easing: 'ease-in-out',
},
);
},
};
</script>

2.gif


四、应用


这样的UI组件可能会用于记录时间、比赛分数变化啥的,自然是不能把值写死。考虑如下的应用场景:


<!-- App.vue -->
<template>
<div id="app" class="flex-row">
<test-comp :value="scoreLGD"/>
<h1>VS</h1>
<test-comp :value="scoreLiquid"/>
</div>
</template>


<script>
import TestComp from './Test';
export default {
components: { TestComp },
data() { return {
scoreLGD : 15,
scoreLiquid: 13,
};
},
mounted() {
setInterval(() => {
this.scoreLGD = this.randomInt(99);
this.scoreLiquid = this.randomInt(99);
}, 5000);
},
/* ... */
};

在该场景下,翻页组件需要在更新时而不是挂载时执行动画(因为没有上一个值)。因此我们在组件内部维护一个记录上一个值的状态,然后把动画从挂载阶段移动到更新阶段:


<template>
<div class="card">
<!-- 旧文字上 -->
<div
v-if="staleValue !== undefined"
class="half-card top-half">

{{ staleValue }}
</div>
<!-- 新文字下 -->
<div class="half-card bottom-half">{{ value }}</div>
<!-- 旋转面板 -->
<div ref="rotate" class="rotating-half">
<!-- 新文字上 -->
<div class="half-card front-side">{{ value }}</div>
<!-- 旧文字下 -->
<div
v-if="staleValue !== undefined"
class="half-card back-side">

{{ staleValue }}
</div>
</div>
</div>

</template>

<script>
export default {
props: ['value'],
data() { return { staleValue: undefined }; },
watch: {
value(_, old) { this.staleValue = old; },
},
updated() {
this.$refs.rotate?.animate?.(
[{ offset: 0, transform: 'rotateX(-180deg)' }],
{ duration: 1000, easing: 'ease-in-out' },
);
},
};
</script>


基本完成:


22.gif


总结一下


实现翻页效果 = 实现两块静态面板 + 实现一块双面旋转面板 + 播放旋转动画。


这里用vue写了demo, react应该也差不多,将updated换成layoutEffect等等。


另外,动画也可以用类名加css实现,当元素不在视口可以不播放,一些样式可以改成props配置。总之应该有不少地方还可以迭代优化下。


参考文章如下,分析思路基本一致,代码实现上有差异:

【1】优雅的时钟翻页效果,让你的网页时钟与众不同!

【2】原生JS实现

一个翻页时钟

收起阅读 »

程序员有没有必要成为业务领域专家 ?

看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。 于是,我做了如下的回答: 非常有必要。 1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。 乔布斯,你是一个聪明又有影响力的人。但是很...
继续阅读 »


看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。


于是,我做了如下的回答:


非常有必要


1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。



乔布斯,你是一个聪明又有影响力的人。但是很遗憾也很明显,很多时候你根本不知道自己在做什么。我希望你能用清楚的语言解释一下 Java 编程语言以及其变种是如何阐述 OpenDoc(开源技文档)内置的一些想法。等你说完以后,你能不能跟我们说一说你自己过去七年都干了些什么?



面对这样犀利的提问,乔布斯平静的喝了一口水,低头沉思了几秒,开口这样回答道:



有时候你能取悦一部分的人,但是当你想要作出改变的时候,最难的是某些事情别人做的是对的。我相信 OpenDoc 肯定有一些功能,没有任何其他东西能做到。我其实也不太懂,我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面,例如让你每年一个产品能够卖百八十亿美元。


我经常发现,你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去。在座的没有人比我犯过更多这样的错误,我也搞到伤痕累累,我知道这就是原因,当我们尝试去为苹果思考战略和愿景,都是从能为用户带来什么巨大利益出发,我们可以给用户带来什么,而不是先找一群工程师,大家坐下来,看看我们有什么吊炸天的技术,然后怎么把它卖出去。



我非常认同乔布斯的话。


程序员有的时候沉迷在自己的世界里,执拗的以为“代码就是全部”


但现实并非如此,编码的目的是创造产品或者提供服务,从而在这个商业社会实现更大的价值


而程序员成长为业务领域专家,能够更加深刻的理解公司的产品或者服务,从而更有优势为公司做出贡献。当个人的贡献上升时,公司的认同和利益也会随之而来。




这个回答一天内得到不少赞同,也是我意想不到的,因为我并不觉得我回答得好,看来很多同学都认可这个观点。



熟悉我的朋友都知道 ,我对技术非常有激情,曾经也认为技术意味着一切。


只是后来,工作中遇到越来越多的挫折,很多好朋友也友善的提醒我,不要太执着于技术,我也越来越认识到自己认知的局限性


我不断的去读书、听演讲、思考,依稀之间得到一个结论:"一个 IT 公司的成功 ,技术固然是重要的一环,而公司的产品、用户人群、经营模式是另一个我很少关注且非常重要的维度"。


偶然间我看了乔布斯的一个视频,视频的两句话让我醍醐灌顶。




  • 我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面




  • 你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去




懂业务是一种认知模式,人的能力是多层次的,技术和懂业务并非互斥的关系。


亲爱的程序员朋友,技术是我们的立身之本,但是业务同样重要 , 真诚的希望你做一个既懂技术又懂业务的工程师。




如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质

作者:勇哥java实战分享
来源:juejin.cn/post/7246224746005954616
量的文章,非常感谢!

收起阅读 »

腾讯视频技术团队偷懒了?!

腾小云导读 PC Web 端、手机 H5 端、小程序端、App 安卓端、App iOS 端......在多端时代,一个应用往往需要支持多端。若每个端都独立开发一套系统来支持,将消耗巨大的人力和经费!腾讯视频团队想到一个“偷懒”的方法——能不能只开发一套基础系统...
继续阅读 »


动图封面


腾小云导读


PC Web 端、手机 H5 端、小程序端、App 安卓端、App iOS 端......在多端时代,一个应用往往需要支持多端。若每个端都独立开发一套系统来支持,将消耗巨大的人力和经费!腾讯视频团队想到一个“偷懒”的方法——能不能只开发一套基础系统,通过兼容不同平台的特性,来快速编译出不同平台的应用呢?本篇特邀腾讯视频团队为你分享快速编译出支持多端的应用、一套代码行走天下的“偷懒”历程。欢迎阅读。


目录


1 背景


2 设计思路


3 具体实现


4 总结


01、 背景


腾讯视频搜索在多个端都存在:安卓 App 端搜索、iOS App 端搜索、H5 端搜索、小程序端搜索、PC Web 端、PC 客户端搜索。每个端,除了个别模块的样式有细微差异之外,其他都一样,如下面的图片所示。



按照以前的现状,安卓 App 端搜索一套代码、iOS App 端搜索一套代码、手机 H5 端一套代码、小程序端搜索一套代码、PC 客户端一套代码、PC Web 端一套代码......每套代码都是独立开发,独立维护,成本非常高。并且,后端的搜索接口以前也是分散在多个不同的协议中,有的平台是 jce 协议的接口,有的是 PB 协议的接口,也是五花八门。


随着业务增长的需求,我们已经没有足够的时间来维护各自一套独立的系统,我们打算进行升级改革!治理的办法就是:收敛!把后端不同平台的接口都归一到同一个接口中,通过平台号来区分;前端也将不同平台的代码,收敛归一成一套代码,通过条件编译来兼容适配不同平台的差异性,不同的平台,在蓝盾流水线中配置不同的参数来上线,从而达到多合一的效果。


总体来说,我们团队就实现一个“多端合一的万能模板”的想法达成一致。并且,我们希望使用 hippy-vue 技术栈。


理由有以下:


Hippy 是公司级别的中台框架,有专门的团队在进行问题的修复和功能的迭代开发,并且广泛应用到了很多公司级的应用中,暂时不会出现“荒芜丢弃”的局面; Hippy 是为了抹平 iOS、Android 双端差异,提供接近 Web 的开发体验而生,在上层支持了 React 和 Vue 两套界面框架,前端开发人员可以通过它,将前端代码转换为终端的原生指令,进行原生终端 App 开发; Hippy 在底层进行了大量的优化,使利用 Hippy 框架开发的终端 App 应用,在启动速度,可复用列表组件、渲染效率、动画速度、网络通信等方面都提供了业内顶尖的性能表现,值得信赖; Hippy 在上层支持 Vue 技术栈,正好我们团队目前所有的前端项目也都统一为 Vue 技术栈,开发人员上手毫无违和感。

02、设计思路


系统的架构图如下所示:



通用模版为了简化开发、提高开发效率,在模版中集成了大量现有组件和工具包,具体可以分为以下三层:



  • 第三方工具层


在通用工具库中,模版包装并提供了很多常用方法,比如 cookie 的设置和 cookie 的获取方法;DOM 的操作方法;Cache 的设置,Cache 的获取,Cache 的过期时间等。在第三方接入库中,模版已经接好了 Aegis 监控,Tab 实验的实验值获取,大同上报等;在打包编译库中,模版提供了通用的 Hippy App 打包安卓脚本和 IOS 脚本、H5 的打包脚本、小程序地打包脚本、一套代码,运行不同的打包命令,执行不同的编译打包脚本,就可以生成不同平台对应的发布包。编译打包在后续还会详细讲解。



  • 数据管理层


在这层中,模版集成了跟数据处理相关的模块。


在 Store 层,由于该模块是基于 Vue2 实现的(Vue3 会在下一个版本中提供),模版已经集成好了 Vuex、State、Getters、Mutations、Actions 等,并且都有实例代码(该模版是基于 Vue2 实现的,Vue3 会在下一个版本中提供);


在 Model 层,模版提供了一套将 PB 文件转化为 TS 类文件的方法,方便快速接入后端PB协议接口请求;同时,还包装了接口通用请求方法,以及全局的统一错误处理上报方法;在数据配置中,模版提供了全局的常量配置文件,应用的版本配置文件(版本的配置对 Hippy App 的应用非常实用),以及 UI 样式的配置(正常模式样式还是暗黑模式样式,宽屏,窄屏等)。



  • UI 层


为了提高开发速度,提高开发效率,模版提供了示例页面代码。同时,根据脚手架来选择是否需要路由,来动态添加应用的路由;以及常用的基础组件库。这些组件库中的组件,是从众多 Hippy 应用中提取出来,实用又高效。


03、具体实现


本文将从 Hippy App 端实现,Hippy H5 端的实现和 Hippy 微信小程序端端实现来分别展开介绍。


下图是 Hippy App 端实现逻辑。



App 端的入口文件为 main-native.ts。在里面,声明了一个 App 实例,指定 phone 下的一些属性设置,比如状态栏、背景色等等。同时,需要用到的 native 组件,都需要在 main-native 中进行声明绑定,才可以在页面中使用。


例如:下图示例中注册声明了两个 native 组件,LottieView 和 VideoView,在页面中就可以直接使用这两个 native 组件。


Vue.registerElement('LottieView');
Vue.registerElement('VideoView', {
component: {
name: 'VideoView',
processEventData(event: any, nativeEventName: string, nativeEventParams: any) {
// To do something for the native component event
return event;
},
},});

main-native 中还有一个重要的方法:app.$start() 方法。


该方法为 Hippy 引擎初始化完成后回调到 Hippy 前端的方法;Hippy 端跟 App 方法进行通信,通过 jsbridge 来进行,模版中已经封装好了具体方法;Hippy 请求后端接口,通过 fetch 协议,也有具体的协议方法封装;Hippy 在 App 内部的跳转,是通过伪协议跳转来实现的。


Hippy App 应用的部署分为以下三种情况:



  • 本地调试


本地调试是通过 Hippy + Chrome Devtools 来完成,通过 WS 通道转发消息,具体流程如下图。




  • 部署测试环境


模版中引入了环境变量参数,同时在代码模版中做了大量环境变量的兼容逻辑,比如测试环境用测试环境的接口,正式环境用正式环境的接口;测试环境用测试环境的 CDN,静态文件上传到测试环境,测试环境部署测试环境的离线包等;测试环境的调试我们是通过离线包的方式来实现的,有专门的测试环境流水线接入使用,只需要稍微做少许调整即可,有需要的可以私聊。



  • 部署正式环境


正式环境流程会做这样几件事情:正式环境接口、正式环境的 CDN、正式环境的日志上、部署正式环境的离线包平台、图片的特殊处理。因为 App 端是采用离线包的形式,如果所有本地图片都打包到离线包中,会导致离线包包体积很大,会影响到 App 的整体体积大小和离线包的下载速度。模版中做了针对图片的特殊脚本处理:引入了图片编译大小变量:STATIC_SIZE_LIMIT。当大于该限制条件的图片都一律上传到 CDN,如果想保留的,则需要增加特殊声明:inline。


具体流程如下图所示:



Hippy H5 的实现流程如下图所示。



Hippy H5 的实现跟 App 的实现流程类似,但是差异如下:


App 的入口文件为 main-native.ts,h5 的入口文件为 main.ts 文件。H5 的入口文件中,没有关于 iphone 的设置,跟 Web 的设置一样;H5 的路由用 vue-router,页面中的路由跳转都是 H5 超链接,不是伪协议;H5 的本地调试很简单,跟 Vue Web 一样,都是在本地起 http-server 来测试;测试环境的部署和正式环境的部署都是采用的服务器来部署,不是离线包。

这里重点讨论一下大同上报的实现。大同上报在 App 端的上报参数声明跟 H5 端的上报参数声明不一致,如何统一这些差异?模版中的解决方案是:封装自定义标签 Directive。


具体实现如下:在 Directive 标签中兼容 App 和 H5 的不一致。


/**
* @example
*
* <element v-report="elementReportInfo" />
* <element v-report="{ eid, ...extra }" />
* <page v-report="{ pgid, ...extra }" />
* <page v-report.page="assertPageReport" />
*/

Vue.directive('report', {
bind(el) {
el.addEventListener('layout', throttledForceReport);
},
unbind(el) {
el.removeEventListener('layout', throttledForceReport);
},
inserted: setReport,
update: setReport,
} as DirectiveOptions);

很多人可能会问,Hippy App 跟 Hippy H5 有很多不同的地方,如果写两套代码,会不会导致代码的体积变得很大?答案是一定的,为了解决以上问题,该万能模版提供了条件编译。引入环境变量:isNative。然后,根据该条件,进行条件编译,不同的平台,生成不同平台的代码,避免了生成大量冗余代码。


Hippy 微信小程序的实现流程如下图所示:



小程序的实现是基于 Taro Vue 框架。该框架跟 Hippy Vue 框架天然兼容,但是也有一些小程序的特殊地方:


小程序的入口文件约定为 app.ts,创建 app 实例是在 app.ts 中来完成;小程序的主页面文件为 app.vue,在其中定义小程序的状态栏,标题栏,页面等;小程序的全局配置在 app.config.ts 中;小程序的构建脚本在 script 中的 index.js。小程序的代码是基于 Hippy Vue 的代码通过 Taro 自动构建转化而成,很多配置都是自动生成的,只需要在开发的时候,遵循约定的命名规范即可。

为了一套代码能够同时支持 App,H5 和微信小程序,需要遵循一些约定的规范,否则在从 Hippy Vue 转化为 Taro Vue 的时候会遇到一些问题:


文件夹命名规范:全部小写加“-”, 例如:node-redis, agent-base 等,不要用大驼峰等;文件的命名规范:跟文件夹命名规范一致,全部小写加“-”,例如:eslint-recomment.js;属性的命名:也采用小写加“-”, 例如:data-url。

04、总结


目前该模版已经在腾讯视频的搜索场景落地,并且上线应用,但是,还是有一些需要共同打磨的地方:


Vue3 的支持:目前我们是基于 Hippy Vue2 来实现的。随着 Vue3 的广泛应用,后续我们需要升级到 Vue3。


组件丰富:通用组件的种类还不是特别丰富,只是基于我们腾讯视频搜索场景进行的封装,后续可以补充更多更丰富的组件。


迭代升级:通用组件目前还是通过源代码的方式存放在代码模版中,不利于后续组件的升级迭代。计划后续会把组件给迁移到我们的应用组件库平台 Athena,该平台我们会后续发专文介绍,大家敬请期待。


以上是本次分享全部内容,如果觉得文章还不错的话欢迎分享~


-End-


原创作者|熊才刚


技术责编|陈恕胜



你有什么开发提效小技巧?欢迎在腾讯云开发者公众号评论区中分享你的经验和看法。我们将选取1则最有意义的分享,送出腾讯云开发者-文化衫1件(见下图)。6月21日中午12点开奖。



图片


图片
图片
图片


作者:腾讯云开发者
来源:juejin.cn/post/7246056370624495671
收起阅读 »

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

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

就业之前


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


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


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


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


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


第一段实习 — 贵阳



早9晚5.30,双休,2.5k



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


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


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


第二段 — 重庆



995,3k



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


现在 - 贵阳



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



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


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



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




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



关于感情


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


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


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


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


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



顺便给你们避避坑:


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


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


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


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



附上几组图片:


DSC_3061.JPG


DSC_3106.JPG


最后


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

作者:ibeen
来源:juejin.cn/post/7232175144219148349
来。
希望越来越好!

收起阅读 »

用js脚本下载某书的所有文章

web
前言 在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。 想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是...
继续阅读 »

前言


在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。


想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是bug了,试了好几次都这样,官方渠道只能放弃了。


手动一篇一篇粘贴的成本太高了,不仅有发布的文章,还有各种没有发布的笔记在里面,各种文章笔记加起来好几百篇呢,既然是工程师,就用工程师思维解决实际问题,能用脚本下载个人账号的下的所有文章吗?


思考.gif


思路梳理


由于是下载个人账号下的所有文章,包含发布的和未发布的,来看下个人账号的文章管理后台


文集模式.png


根据操作以及分析浏览器控制台 网络 请求得知,文章管理后台逻辑是这样的,默认查询所有文集(文章分类列表), 默认选中第一个文集,查询第一个文集下的所有文章,默认展示第一篇文章的内容,点击文集,获取当前文集下的所有文章,默认展示文集中的第一篇文章,点击文章获取当前文章数据,来分析一下相关的接口请求


获取所有文集


https://www.jianshu.com/author/notebooks 这个 Get 请求是获取所有 文集,用户信息是放在 cookie


分析请求模式.png


来看下返回结果


[
    {
        "id": 51802858,
        "name": "思考,工具,痛点",
        "seq": -4
    },
    {
        "id": 51783763,
        "name": "安全",
        "seq": -3
    },
    {
        "id": 51634011,
        "name": "数据结构",
        "seq": -2
    },
    ...
]

接口返回内容很简单,一个 json 数据,分别是:id、文集名称、排序字段。


获取文集中的所有文章


https://www.jianshu.com/author/notebooks/51802858/notes 这个 Get 请求是根据 文集id 获取所有文章,51802858"思考,工具,痛点" 文集的id, 返回数据如下


[
    {
        "id": 103888430, // 文章id
        "slug": "984db49de2c0",
        "shared": false,
        "notebook_id": 51802858, // 文集id
        "seq_in_nb": -4,
        "note_type": 2,
        "autosave_control": 0,
        "title": "2022-07-18", // 文章名称
        "content_updated_at": 1658111410,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    {
        "id": 98082442,
        "slug": "6595bc249952",
        "shared": false,
        "notebook_id": 51802858,
        "seq_in_nb": -3,
        "note_type": 2,
        "autosave_control": 3,
        "title": "架构图",
        "content_updated_at": 1644215292,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    ...
]

接口返回的 json 数据里包含 文章id文集名称,这是接下来需要的字段,其他字段暂时忽略。


获取文章内容


https://www.jianshu.com/author/notes/98082442/content 这个 Get 请求是根据 文章id 获取文章 Markdown 格式内容, 98082442《架构图》 文章的id, 接口返回为 Markdown 格式的字符串


{"content":"![微服务架构图 (3).jpg](https://upload-images.jianshu.io/upload_images/6264414-fa0a7893516725ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"}

现在,我们了解清楚了文集,文章,以及文档内容的获取方式,接下来开始脚本实现。


代码实现


由于我是前端攻城狮,优先考虑使用 js 来实现下载,还有一个考虑因素是,接口请求里面的用户信息是通过读取 cookie 来实现的,js 脚本在浏览器的控制台执行发起请求时,会自动读取 cookie,很方便。


如果要下载个人账号下所有文章的话,根据梳理出来的思路编写代码就行


获取所有文集id


fetch("https://www.jianshu.com/author/notebooks")
  .then((res) => res.json())
  .then((data) => {
    // 输出所有文集
    console.log(data);
  })

使用fetch函数进行请求,得到返回结果,上面的代码直接在浏览器控制台执行即可,控制台输出效果如下


输出所有文集.png


根据文集数据获取所有文章


上一步得到了所有文集,使用 forEach 循环所有文集,再根据 文集id 获取对应文集下的所有文章,依然使用 fetch 进行请求


...
let wenjiArr = [];
wenjiArr = data; // 文集json数据
let articleLength = 0;
wenjiArr.forEach((item, index) => {
  // 根据文集获取文章
  fetch(`https://www.jianshu.com/author/notebooks/${item.id}/notes`)
    .then((res2) => res2.json())
    .then((data2) => {
      console.log("输出文集下的所有文章:", data2);
    });
});

根据文章id获取文章内容,并下载 Markdown 文件


有了文章 id, 根据 id 获取内容,得到的内容是一个对象,对象中的 content 属性是文章的 Markdown 字符串,使用 Blob 对象和 a 标签,通过 click() 事件实现下载。


在这里的代码中使用 articleLength 变量记录了一下文章数量,使用循环中的文集名称和文章名称拼成 Markdown 文件名 item.name - 《item2.title》.md


...
console.log(item.name + " 文集中的文章数量: " + data2.length);
articleLength = articleLength + data2.length;
console.log("articleLength: ", articleLength);
data2.forEach(async (item2, i) => {
// 根据文章id获取Markdown内容
fetch(`https://www.jianshu.com/author/notes/${item2.id}/content`)
.then((res3) => res3.json())
.then((data3) => {
console.log(data3);
const blob = new Blob([data.content], {
type: "text/markdown",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name + " - 《" + item2.title + `》.md`;
link.click();
});
});

代码基本完成,运行


在浏览器控制台中运行写好的代码,浏览器下方的下载提示嗖嗖的显示,由于没有做任何处理,当前脚本执行过程中报错了,文章下载了几十个以后就停止了,提示 429



HTTP 请求码 429 表示客户端发送了太多的请求,服务器无法处理。这种错误通常表示服务器被攻击或过载。



文章内容太多了,意料之中的情况,需要改进代码


思路改进分析


根据问题分析,脚本里的代码是循环套循环发请求的,这部分改造一下试试效果。


把每个循环里面发送 fetch 请求的外面是加个 setTimeout, 第一个循环里面的 setTimeout 延迟参数设置为 1000 * indexindex 为当前循环的索引,第一个请求0秒后执行,后面每一次都加1秒后执行,由于文集的数量不多,大约20个,这一步这样实现是没问题的。


重点是第二个循环,根据文集获取所有文章,每个文集里多的文章超过50篇,少的可能2,3篇,这里面的 setTimeout 延迟参数这样设置 2000 * (i + index)i 为第二个循环的索引,这样保证在后面的请求中避免了某个时间段发送大量请求,导致丢包的问题。


再次执行代码,对比控制台输出的文章数量和下载目录中的文章(项目)数量,如果一致,说明文章都下载好了


390.png


下载.png


改造后的完整代码地址


github.com/gywgithub/F…


思考


整体来看,文章下载脚本的逻辑并不复杂,接口参数也简单明确,两个 forEach 循环,三个 fetch 请求,把获取到的文章内容实用 a 标签下载下来就行了。关于大量请求发送导致 429 或者请求丢失的问题,脚本中使用了一种方案,当时还想到了另外两种方案:


请求同步执行


通过同步的方式先得到所有文集下的所有文章,再根据文章列表数组同步发请求下载,请求一个个发,文章一篇篇下载


Promise.all


使用 Promise.all() 分批发送请求,避免一次请求发送太多



也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步 ^-^



作者:草帽lufei
来源:juejin.cn/post/7245184987531018300

收起阅读 »

这道面试题真的很变态吗?😱

web
最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题 题...
继续阅读 »

最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题


题目


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

这里我会要求面试者从上到下依次说出执行结果。普遍多的面试者给出的答案是:foo1、foo2、foo1、foo2。虽然在我看来这是一道简单的面试题,但是也不至于这么简单吧😱~~~


当然面试本来就是一个相互讨论的过程,那就和面试者沟通下这道题我的理解,万一我理解错了呢😂


解答


拆分函数表达式


首先我会让面试者先看前面两个函数


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

这时候大部分人基本上都可以答对了,是:foo1、foo2。再有很少数的人答不对那就只能”施主,出门右转“😌。接着根据我当时的心情可能会稍作追问(美女除外🙂):


foo()
var foo = function () {
console.log("foo1")
}

这时候又有一部分的人答不上来了。这毫无疑问是肯定会报错的啊


image.png


我们都知道用var定义的变量会变量提升,所以声明会被拿到函数或全局作用域的顶部,并且输出undefined。所以当执行foo()的时候,foo还是undefined,所以会报错。由于js从按照顺序从上往下执行,所以当执行foo = function(){}的时候,才对foo进行赋值为一个函数。我们重新看拆分之后的代码


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

foo首先会变量提升,然后进行赋值为function。所以当执行第一个foo的时候,此时foo就是我们赋值的这个函数。接着执行第二个foo的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数。所以上面的代码会打印:foo1、foo2。


这种定义函数的方式,我们称为函数表达式。函数表达式是将函数作为一个值赋给一个变量或属性


函数表达式我们拆分完了,下面就看看函数声明吧。


拆分函数声明


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

大部分人其实都卡在了这里。函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。说到这里其实大多数人就已经明白了。这里使用了函数声明定义了两个foo函数,由于函数声明提升,第二个foo会覆盖第一个foo,所以当调用第一个foo的时候,其实已经被第二个foo覆盖了,所以这两个打印的都是foo2。


当两段代码结合


当开始解析的时候,函数声明就已经提升了,第四个foo会覆盖第三个foo。然后js开始从上往下执行,第一个赋值操作之后执行foo()后,打印了”foo1“,第二个赋值之后执行foo(),打印了"foo2"。下面两个foo的执行其实是第二个赋值了的foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖foo。


总结


我们整体分析代码的执行过程



  1. 通过函数表达式定义变量foo并赋值为一个匿名函数,该函数在被调用时打印"foo1"。

  2. 接着,通过函数表达式重新定义变量foo,赋值为另一个匿名函数,该函数在被调用时打印"foo2"。

  3. 使用函数声明定义了两个名为foo的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时foo是第二个赋值的函数。

  4. 然后调用foo(),输出"foo2"。

  5. 再调用foo(),也输出"foo2"。


其实就一个点: 函数表达式相对于函数声明的一个重要区别是函数声明在代码解析阶段就会被提升(函数声明提升),而函数表达式则需要在赋值语句执行到达时才会创建函数对象


小伙伴们,以上是我的理解,欢迎在评论区留言,大家相互讨论相互学习。


之前的描述确实有点不妥,所以做了改动,望大家谅解,还

作者:翰玥
来源:juejin.cn/post/7237051958993469496
是本着相互学习的态度

收起阅读 »

别再无聊地显示隐藏了,Vue 中使用过渡动画让你的网页更有活力

web
Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。 1. 你知道什么是过渡动画吗 过渡动画是指在 DOM 元素从一个状态到...
继续阅读 »

Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。


1. 你知道什么是过渡动画吗


过渡动画是指在 DOM 元素从一个状态到另一个状态发生变化时,通过添加过渡效果使得这个变化看起来更加平滑自然的动画效果。在 Vue 中,过渡动画可以应用到以下几个场景中:



  • 显示和隐藏元素

  • 动态添加或删除元素

  • 元素位置的变化


2. Vue 过渡动画的实现方法


2.1 CSS 过渡


Vue 提供了 transition 组件来支持过渡动画。我们可以在需要应用过渡动画的元素外层包裹一个 transition 组件,并通过设置 CSS 样式或绑定动态 class 来实现过渡动画的效果。


Vue 的过渡动画通过添加 CSS 类名来实现。我们可以通过为需要过渡的元素添加 v-ifv-show 指令来控制元素的显示和隐藏,然后使用 transition 组件进行动画效果的设置。


下面我写个示例给大家参考一下,我将给按钮添加过渡动画效果:


<template>
<button @click="show=!show">Toggle</button>
<transition name="fade">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

在上面的代码思路中,我们在 transition 包裹的 div 元素上使用了 v-if 指令来控制元素的显示和隐藏。同时,我们给 transition 组件添加了一个 name 属性,并使用 CSS 样式来定义过渡动画的效果。其中,.fade-enter-active.fade-leave-active 分别表示进入和离开时的过渡动画,而 .fade-enter.fade-leave-to 则分别表示进入和离开时元素的样式。


2.2 JS 过渡


除了使用 CSS 过渡外,在 Vue 中也可以使用 JavaScript 过渡来实现动画效果。JS 过渡相比于 CSS 过渡的优势在于它可以更加灵活地控制过渡动画。


它与 CSS 过渡不同,Javascript 过渡可以更加灵活地控制过渡动画,可以实现更加丰富的效果。Vue 提供了事件钩子函数,使得我们可以自定义过渡动画的效果。


image.png


Vue 中提供了以下事件钩子函数:



  • before-enter

  • enter

  • after-enter

  • enter-cancelled

  • before-leave

  • leave

  • after-leave

  • leave-cancelled


我们可以使用 transition 组件的 mode 属性来设置过渡的模式,如果使用了 mode 属性,Vue 将会自动调用对应的钩子函数,我们可以通过这些钩子函数来自定义过渡效果。


下面是我写的一个基于 JS 过渡的演示Demo,我们将为按钮添加自定义的过渡动画:


<template>
<button @click="show=!show">Toggle</button>
<transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
},
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
enter(el, done) {
anime({
targets: el,
opacity: 1,
translateX: [20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
},
leave(el, done) {
anime({
targets: el,
opacity: 0,
translateX: [-20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
}
}
};
</script>

在上面的前端页面中,我们通过设置 transition 组件的 css 属性为 false 来禁用 CSS 过渡,然后我们使用了 before-enterenterleave 等钩子函数来自定义过渡动画。在这个示例代码中,我们使用了第三方动画库 Anime.js 来实现元素进入和离开时的动画效果,同时在 anime 动画完成后,我们还需要手动调用 done 函数来告知 Vue 过渡动画已经完成。


3. 小结一下


通过我写的这篇文章的介绍,可以让大家多了解了 Vue 过渡动画的基本概念,并且掌握了如何在 Vue 中实现过渡动画。不论是使用 CSS 过渡还是 JavaScript 过渡,都可以帮助我们为用户提供更加友好的用户体验。我希望本文对您有所帮助,如果您有任何疑问或建议,欢迎在评论区留言。


作者:Cosolar
来源:juejin.cn/post/7241874482574114875
收起阅读 »

for循环的代价

web
for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。 作用域是什么? 要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变...
继续阅读 »

for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。


作用域是什么?


要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变量类型,因为如果没有变量程序只能执行一些简单的任务。但是引入变量之后程序怎么才能准确的找到自己需要的变量。这就需要建立一套规则让程序能够准确的找到需要的变量,这样的规则被称为作用域。


块级作用域


块级作用域如同全局作用域和函数作用域一样,只不过块级作用域由花括号({})包裹的代码块创建的。在块级作用域内声明的变量只能在该作用域内访问,可以使用 let 或 const 关键字声明变量,可以在块级作用域内创建变量。
所以引擎在编译时是通过花括号({})包裹和声明关键字判断是否创建块级作用域,因此绝大多数的语句是没有作用域的,同时从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
基于这个原则,switch语句被设计为有且仅有一个作用域,无论它有多少个case语句,其实都是运行在一个块级作用域环境中的。
一些简单的、显而易见的块级作用域包括:


// 例1
try {
// 作用域1
}
catch (e) { // 表达式e位于作用域2
// 作用域2
}
finally {
// 作用域3
}

// 例2
//(注:没有使用大括号)
with (x) /* 作用域1 */; // <- 这里存在一个块级作用域

// 例3, 块语句
{
// 作用域1

除此之外,按上述理解,for语句也可以满足上述的条件。


for循环作用域


并不是所有的for循环都有自己的作用域,有且仅有


for ( <let/const> ...) ...

这个语法有自己的块级作用域。当然,这也包括相同设计的for await和for .. of/in ..。例如:


for await ( <let/const> x of ...) ...
for ( <let/const> x ... in ...)
for ( <let/const> x ... of ...) ...

已经注意到了,这里并没有按照惯例那样列出“var”关键字。简单理解就是不满足创建的条件。Js引擎在编译时,会对标识符进行登记,而为了兼容,将标识符分为了两类varNames 和 lexicalNames。以前 var 声明、函数声明将会登记在varNames,为了兼容varNames只有全局作用域和函数作用域两种,所以编译时会就近登记在全局作用域和函数作用域中且变量有“提升”效果。Es6新增的声明关键词将登记在lexicalNames,编译时会就近创建块级作用或就近登记在函数作用域中。



varNames 和 lexicalNames属性只是一个用于记录标识符的列表,是通过词法作用域分析,在当前作用域中做登记的。它们记录了当前作用域中的变量和函数的名称,以及它们的作用域信息,帮助 JavaScript 引擎在代码执行时正确地解析标识符的作用域。



关于作用域还有一点要说明,JavaScript采用词法作用域,这意味着变量的作用域在代码编写时就确定了,而不是在运行时确定。这与动态作用域不同,动态作用域是根据函数的调用栈来确定变量的作用域。
举个例子:


function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();

词法作用域下log的结果是2,动态作用域下log的是3。



词法作用域是指 JavaScript 引擎在编译时如何确定变量的作用域。在 JavaScript 中,词法作用域是指变量的作用域是由代码的位置(词法)决定的,而不是由运行时的作用域链决定的。



变量的作用域和可见性是由词法作用域和作用域链来决定的,作用域链是基于词法作用域和函数作用域来确定的,这也证明了 JavaScript 采用的是词法作用域。


for循环隐藏作用域


首先,必须要拥有至少一个块级作用域。如之前讲到的,满足引擎创建的条件。但是这一个作用域貌似无法解释下面这段代码


for(let i=0;i<10;i++){
let i=1;
console.log(i) // 1
}

这段代码时可以正常运行的,而我们知道let语句的变量不能重复声明的,所以对for循环来说一个作用域是满足了这个场景的。
但是这段代码依然可以执行,那JS引擎是如何处理的呢?
只能说明循环体又创建了一个块级作用域,事实如你所见,JS引擎确实对for循环的每个循环体都创建了一个块级作用域。
举个栗子,以下代码中使用 let 声明变量 i


for (let i = 0; i < 5; i++) {
console.log(i);
}

在编译时,JavaScript 引擎会将循环体包裹在一个块级作用域中,类似于以下代码:


{
let i;
for (i = 0; i < 5; i++) {
console.log(i);
}
}

每次循环都会创建一个新的块级作用域,因此,在循环中声明的变量 i 只能在当前块级作用域中访问,不会污染外部作用域的变量。而通过作用域链每个循环体内都可以访问外层变量i。
而我们知道从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好,所以这也算是代价之一吧。
就算如此设计还是无法解释下面这段代码。


for (let i = 0; i < 5; i++) {
setTimeout(()=>{console.log(i)})
}

如果按上述的理解,那最后log时访问的都是外层的变量i,最后的结果应该都是4,可事实却并非如此。当定时器被触发时,函数会通过它的闭包来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个for迭代都已经结束了。这种情况下,访问i,获取到的也是上层作用域中的i,此刻i的值应该是最后一次赋。
之所能按我们预想的输出1,2,3,4,那是因为JavaScript 引擎在创建循环体作用域的时候,会在该作用域中声明一个新的变量 i,并将其初始化为当前的迭代次数,这个新的变量 i 会覆盖外层的变量 i。这个过程是由 JavaScript 引擎自动完成的,我们并不需要手动

作者:chtty
来源:juejin.cn/post/7245641209913360445
创建或赋值这个变量。

收起阅读 »

前端如何破解 CRUD 的循环

web
据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中… 很多开发工作也如此单调而乏...
继续阅读 »

据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中…


很多开发工作也如此单调而乏味,比如今天要讲的中后台开发的场景。中后台业务基本上就是一些数据的增删改查、图表,技术含量不高,比较容易范式化。


前端如何破除这种 CRUD 的单调循环呢?








低代码


过去几年前端的低代码很火,这些低代码平台通常支持创建数据模型后,一键生成对应的增删改查页面:




模型驱动生成页面





💡 本文提及的低代码是狭义低代码,你可以认为就是可视化搭建平台





低代码在过去几年就是 「雷声大,雨点小」,跟现在的 AI 颇为相似。


不管是大厂还是小厂都在搞低代码,包括笔者也参与过几个低代码项目,但是小厂支撑不起来这样的资源投入,最后都胎死腹中。我相信很多读者也经历过这种情况。
大部分公司只是尾随市场营销噱头,盲目跟风,压根就没有做这种低代码平台资源准备和沉淀。


作为前端,能参与到低代码项目的开发是一件非常兴奋的事情,毕竟是少数前端能主导的项目,架构、组件设计、编辑器的实现可玩性很高,可以跟同行吹很久。


作为用户(开发者)呢?可能会排斥和质疑,不管怎么说,它并没有发挥市场所期望的价值。




最主要的原因是:它解决不了复杂的问题




低代码直观、门槛低, 前期开发确实很爽,可视化数据建模、拖拉拽生成页面、流程编排,很快就可以把一些简单的业务开发出来。


然而软件编码本身占用研发流程的比例,据 ChatGPT 估算大约只有 20% ~ 30%。而且业务持续变化,代码也需要持续迭代。试想一下如何在这些低代码平台上进行重构和检索?






总的来说,有一些缺点:




  • 复杂的业务逻辑用低代码可能会更加复杂。低代码应该是特定领域问题的简化和抽象,如果只是单纯将原有的编码工作转换为 GUI 的模式,并没有多大意义。


    例如流程编排,若要用它从零搭建一个复杂的流程,如果照搬技术语言去表达它,那有可能是个地狱:


    流程编排


    理想的流程编排的节点应该是抽象程度更高的、内聚的业务节点,来表达业务流程的流转。然而这些节点的设计和开发其实是一件非常有挑战性的事情。




  • 软件工程是持续演进的,在可维护性方面,目前市面上的低代码平台并不能提供可靠的辅助和验证。因此企业很难将核心的稳态业务交给这些平台。




  • 还有很多… 平台锁定,缺乏标准,性能问题、复用、扩展性、安全问题、黑盒,可迁移性,研发成本高,可预测性/可调试性差,高可用,版本管理,不能自动化…








当然,低代码有低代码的适用场景,比如解决特定领域问题(营销活动页面,海报,数据大屏,表单引擎、商城装修、主页),POC 验证。即一些临时的/非核心的敏态业务



💡 目前有些低代码平台也有「出码能力」,让二开有了一定的可行性。




💡 AI 增强后的低代码可能会更加强大。但笔者依旧保持观望的态度,毕竟准确地描述软件需求,本身就是就是软件研发的难题之一,不然我们也不需要 DDD中的各种方法论,开各种拉通会,或许也不需要需求分析师,产品…


非专业用户直接描述需求来产出软件,大多是不切实际的臆想









中间形态


有没有介于可视化低代码平台和专业代码之间的中间形态?既能保持像低代码平台易用性,同时维持代码的灵活性和可维护性。


我想那就是 DSL(domain-specific language) 吧? DSL 背后体现的是对特定领域问题的抽象,其形式和语法倒是次要的。



💡 DSL 的形式有很多,可以创建一门新的微语言(比如 SQL, GraphQL);可以是一个 JSON 或者 YAML 形式;也可以基于一门现有的元语言(比如 Ruby、Groovy,Rust…)来创建,这些元语言,提供的元编程能力,可以简洁优雅地表达领域问题,同时能够复用元语言 本身的语言能力和基础设施。



严格上可视化低代码平台也是一种‘可视化’ 的 DSL,笔者认为它的局限性更多还是来源‘可视化’,相对的,它优点也大多来源’可视化‘



这又牵扯到了持续了半个多世纪的: GUI vs CLI(程序化/文本化) 之争。这个在《UNIX 编程艺术》中有深入的探讨。命令行和命令语言比起可视化接口来说,更具表达力,尤其是针对复杂的任务。另外命令行接口具有高度脚本化的能力。缺点就是需要费劲地记忆,易用性差,透明度低。当问题规模变大、程序的行为日趋单一、过程化和重复时, CLI 也常能发挥作用。

如果按照友好度和问题域的复杂度/规模两个维度来划分,可以拉出以下曲线:

友好曲线


中间会出现一个交叉点,在这个交叉点之后,命令行的简要行和表达力变得要比避免记忆负担更有价值。


《反 Mac 接口》一书中也进行了总结:可视化接口在处理小数量物体简单行为的情况下,工作的很好,但是当行为或物体的数量增加是,直接操作很快就编程机械重复的苦差…



也就是说,DSL 的形式会约束 DSL 本身的表达能力。




正如前文说的,如果‘低代码’仅仅是将原本的编码工作转换为 GUI 形式,其实并没有多大意义,因为没有抽象。


反例:JSON GUI vs JSON


JSON GUI vs JSON






正例: VSCode 案例


setting in json


setting in gui


充分利用 GUI 的优势,提供更好的目录组织、文本提示、数据录入的约束和校验。






我们可能会说 GUI 形式用户体验更好,门槛低更低,不用关心底层的细节。其实并不一定是 GUI 带来的,而是抽象后的结果。GUI 只不过是一种接口形式




回到正题,为了摆脱管理后台 CRUD 的 「西西弗斯之石」: 我们可以创建一个 DSL,这个 DSL 抽象了管理端的各种场景,将繁琐的实现细节、重复的工作封装起来,暴露简洁而优雅的用户接口(User Interface)。



💡 小结。DSL 是可视化低代码与 pro code 之间的中间中间形态,权衡了易用性/灵活性和实现成本。DSL 的形式会直接影响它的表达能力,但比形式更重要的是 DSL 对特定问题域的抽象。


我们不必重新发明一门语言,而是复用元语言的能力和生态,这基本上是零成本。











抽象过程


典型的增删改查页面:


CRUD


分析过程:



  1. 后端增删改查主要由两大组件组成: 表单表格

  2. 而表单和表格又由更原子的’字段’组成。字段的类型决定了存储类型、录入方式、和展示方式

  3. 字段有两种形态:编辑态预览态。表格列、详情页通常是预览态,而表单和表格筛选则使用编辑态。




预览态和编辑态


借鉴低代码平台的组件库/节点库,我们可以将这些‘字段’ 提取出来, 作为表单和表格的‘原子’单位, 这里我们给它取个名字,就叫原件(Atomic)吧。


低代码平台


原件将取代组件库里面的表单组件,作为我们 CRUD 页面的最小组成单位。它有且只有职责:


原件



  • 数据类型和校验。原件代表的是一种数据类型,可以是基础类型,比如数字、字符串、布尔值、枚举;也可以是基础类型上加了一些约束和交互,比如邮件、手机号码、链接;甚至可能有业务属性,比如用户,商品,订单,二维码。

  • 数据的预览。

  • 数据的录入,严格约束为 value/onChange 协议。好处是方便进行状态管理,可能保证原件实现的统一性。






接着组合原件来实现表单和表格组件,满足 CRUD 场景:


CRUD


理想状态下,我们仅需声明式地指定表格的列和原件类型,其余的技术细节应该隐藏起来。表格伪代码示例:


# 创建包含 名称、创建时间、状态三列的表格,其中可以搜索名称和创建时间
Table(
columns(
column(名称,name, queryable=true)
column(创建时间, created, data-range, queryable=true)
column(状态, status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
)
)



表单伪代码示例:


# 创建包含 名称、状态、地址的表单
Form(
item(名称,name, required=true)
item(状态,status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
item(地址, address, address)
)



如上所示,本质上,开发者就应该只关注业务数据本身,而应该忽略掉前端技术实现的噪音(比如状态管理、展示风格、分页、异常处理等等)。






表格和表单为了适应不同的需求,还会衍生出不同的展现形式:


概览图


原件 + 核心的表单/表格能力 + 场景/展示形式,一套「组合拳」下来,基本就可以满足常见的后台 CRUD 需求了。








约定大于配置


前端的在研发流程中相对下游,如果上游的产品定义,UI 设计,后端协议没有保持一致性,就会苦于应付各种混乱的差异,复用性将无从谈起。


为了最小化样板代码和沟通成本,实现开箱即用的效果。我们最好拉通上下游,将相关的规范确定下来,前端开发者应该扮演好串联的角色。




这些规范包含但不限于:



  • 页面的布局

  • UI 风格

  • 提示语

  • 验证规则

  • 数据的存储格式

  • 通用的接口(比如文件上传,导入导出)



概览图


组件库可以内置这些约定,或者提供全局的配置方式。这些规范固化后,我们就享受开箱即用的快感了。








实现示例


基于上述思想,我们开发了一套组件库(基于 Vue 和 element-ui),配合一套简洁的 DSL,来快速开发 CRUD 页面。





💡 这套组件库耦合了我们自己的约定。因此可能不适用于外部通用的场景。本文的意义更多是想启发读者,去构建适合自己的一套解决方案。



列表页定义:


表格示例


import { defineFatTable } from '@wakeadmin/components'

/**
* 表格项类型
*/

export interface Item {
id: number
name: string
createDate: number
}

export const MyTable = defineFatTable<Item>(({ column }) => {
// 可以在这里放置 Vue hooks
return () => ({
async request(params) {
/* 数据获取,自动处理异常和加载状态 */
},
// 删除操作
async remove(list, ids) {
/*列删除*/
},
// 表格列
columns: [
// queryable 标记为查询字段
column({ prop: 'name', label: '名称', queryable: true }),
column({ prop: 'createDate', valueType: 'date-range', label: '创建时间', queryable: true }),
column({
type: 'actions',
label: '操作',
actions: [{ name: '编辑' }, { name: '删除', onClick: (table, row) => table.remove(row) }],
}),
],
})
})

语法类似于 Vue defineComponent,传入一个’setup’, 。这个 setup 中可以放置一些逻辑和状态或者 Vue hooks,就和 Vue defineComponent 定义一样灵活。


返回关于表格结构的”声明”。最优的情况下,开发者只需要定义表格结构和后端接口,其余的交由组件库处理。


当然复杂的定制场景也能满足,这里可以使用 JSX,监听事件,传递组件支持的任意 props 和 slots。






表单页示例:


表单示例


import { defineFatForm } from '@wakeadmin/components'
import { ElMessageBox } from 'element-plus'

export default defineFatForm<{
// 🔴 这里的泛型变量可以定义表单数据结构
name: string
nickName: string
}>(({ item, form, consumer, group }) => {
// 🔴 这里可以放置 Vue Hooks

// 返回表单定义
return () => ({
// FatForm props 定义
initialValue: {
name: 'ivan',
nickName: '狗蛋',
},

submit: async (values) => {
await ElMessageBox.confirm('确认保存')
console.log('保存成功', values)
},

// 🔴 子节点
children: [
item({ prop: 'name', label: '账号名' }),
item({
prop: 'nickName',
label: '昵称',
}),
],
})
})


💡 和 tailwind 配合食用更香。我们假设整体的页面是符合UI规范的,细微的调整使用 tw 会很方便







全局配置:


import { provideFatConfigurable } from '@wakeadmin/components'
import { Message } from 'element-ui'

export function injectFatConfigurations() {
provideFatConfigurable({
// ...
// 统一处理 images 原件上传
aImagesProps: {
action: '/upload',
},
// 统一 date-range 原件属性
aDateRangeProps: {
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'yyyy-MM-dd',
shortcuts: [
{
text: '最近一周',
onClick(picker: any) {
picker.$emit('pick', getTime(7))
},
},
{
text: '最近一个月',
onClick(picker: any) {
picker.$emit('pick', getTime(30))
},
},
{
text: '最近三个月',
onClick(picker: any) {
picker.$emit('pick', getTime(90))
},
},
],
},
})
}





更多示例和深入讲解见这里








更多实现


前端社区有很多类似的产品,比如:



  • XRender。中后台「表单/表格/图表」开箱即用解决方案

  • Antd ProComponents。ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面

  • 百度 Amis 。 用 JSON 作为 DSL,来描述界面


读者不妨多参考参考。








总结


简单来说,我们就是从提供「毛坯房」升级到了「精装房」,精装房的设计基于我们对市场需求的充分调研和预判。目的是对于 80% 的用户场景,可以实现拎包入住,当然也允许用户在约束的范围内改装。


本文主要阐述的观点:



  • 低代码平台的高效和易用大多来源于抽象,而不一定是 GUI,GUI ≠ 低代码。

  • 摆脱「西西弗斯之石」 考验的是开发者的抽象能力,识别代码中固化/重复的逻辑。将模式提取出来,同时封装掉底层的实现细节。最终的目的是让开发者将注意力关注到业务本身,而不是技术实现细节。

  • 用声明式、精简、高度抽象 DSL 描述业务 。DSL 的形式会约束他的表达能力,我们并不一定要创建一门新的语言,最简单的是复用元语言的生态和能力。

  • 约定大于配置。设计风格、交互流程、数据存储等保持一致性,才能保证抽象收益的最大化。因此规范很重要。这需要我们和设计、产品、后端深入沟通,达成一致。

  • 沉淀原件。低代码平台的效率取决于平台提供的组件能力、数量和粒度。比如前端的组件库,亦或者流程引擎的节点,都属于原件的范畴。

  • 要求不要太高,没有万精油方案,我们期望能满足 80% 常见的场景,这已经是一个很好的成绩。至于那 20% 的个性需求,还是从毛坯房搞起吧。








扩展阅读


收起阅读 »

你的代码着色好看吗?来这里看看吧!

web
如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。 那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。 你只需要使用一个叫做 highlight.js 的第三方...
继续阅读 »

如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。


那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。


你只需要使用一个叫做 highlight.js 的第三方库,就可以轻松实现代码着色的效果。



highlight.js 是一个非常强大和流行的库,它可以自动识别和着色超过 190 种编程语言。


它支持多种主题和样式,让你可以根据自己的喜好选择合适的配色方案。


在本文中,子辰将向你介绍如何使用 highlight.js 来为你的代码着色,以及它的基本原理和优势。


让我们开始吧!


如何使用 highlight.js


使用 highlight.js 的方法有两种:一种是通过 npm 下载并安装到你的项目中,另一种是通过 CDN 引入到你的网页中。


这里我们以 CDN 的方式为例,如果你想使用 npm 的方式,可以参考官方文档。


首先,我们需要在网页中引入 highlight.js 的 JS 文件和 CSS 文件。


JS 文件是核心文件,负责识别和着色代码,CSS 文件是样式文件,负责定义代码的颜色和格式。



我们可以从 CDN 中选择一个合适的 JS 文件和 CSS 文件。


highlight.js 提供了多个 CDN 服务商,你可以根据自己的需求选择一个,这里我们以 jsDelivr 为例。


JS 文件的链接如下:


<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>

CSS 文件的链接则需要根据你想要的主题来选择。


highlight.js 提供了很多主题,你可以在官网上预览每个主题的效果,并找到对应的 CSS 文件名,这里我们以 github-dark 为例。


CSS 文件的链接如下:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.min.css">

将上面两个链接分别放到网页的 head 标签中,就完成了引入 highlight.js 的步骤。


接下来,我们需要在网页中写一些代码,并用 pre 标签和 code 标签包裹起来。


pre 标签用于保留代码的格式,code 标签用于标识代码内容。例如:


<pre>
<code id="code-area">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
height: 100vh;
width: 100vw;
}
</code>
</pre>

注意,我们给 code 标签添加了一个 id 属性,方便后面通过 JS 获取它。


最后,我们需要在网页中添加一些 JS 代码,来调用 highlight.js 的方法,实现代码着色的功能。


highlight.js 提供了两个主要的方法:highlightElement 和 highlight。


这两个方法都可以实现代码着色的效果,但是适用于不同的场景。


highlightElement


highlightElement 方法适用于当你的代码是直接写在网页中的情况。


这个方法接受一个元素作为参数,并将该元素内部的文本内容进行着色处理。例如:


// 获取 code 元素
const codeEle = document.getElementById("code-area");
// 调用 highlightElement 方法,传入 code 元素
hljs.highlightElement(codeEle);

如果一切顺利,你应该能看到类似下图的效果:



代码已经被着色了,并且你可以看到代码被替换成了一个个标签,标签被加上了样式。


在最后的原理里我们在详细的说一下。


highlight


highlight 方法适用于当你的代码是通过 Ajax 请求获取到的纯文本数据的情况。


这个方法接受一个字符串作为参数,并返回一个对象,包含着色后的文本内容和代码的语言。例如:


<script>
const codeEle = document.getElementById('code-area')
// 比如说现在 code 就是 Ajax 返回的数据,lang 就是代码语言,content 就是代码内容
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
// 我们接下来可以使用 hljs.highlight,将代码内容与代码语言传入进去
const result = hljs.highlight(code.content, {
language: code.lang
})
// 它会返回一个结果,我们打印到控制台看看
console.log('result >>> ', result)
</script>


我们可以看到,打印出来的是一个对象,code 是它原始的代码,language 是它的语言,而 value 就是它着色后的代码。


那么现在要做的就是将 value 添加到 code 元素里边去。


<script>
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
const result = hljs.highlight(code.content, {
language: code.lang
})
const codeEle = document.getElementById('code-area')
codeEle.innerHTML = result.value
</script>


我们可以看到,代码确实被着色了,但是和之前的有所差别,我们看一下是什么原因。



打开控制后我们发现,用这种方式 code 元素就没有办法被自动加上类样式了,所以说我们就需要手动给 code 加上类样式才可以。


// 通过 className 为 code 手动添加类样式,并添加类的语言
codeEle.className = `hljs language-${code.lang}`

highlight.js 的语言支持


无论使用哪种方法,都需要注意指定代码所属的语言。


如果不指定语言,highlight.js 会尝试自动识别语言,并可能出现错误或不准确的结。


指定语言可以通过两种方式:



  • 在 code 标签中添加 class 属性,并设置为 language-xxx 的形式,其中 xxx 是语言名称。

  • 在调用 highlightElement 或 highlight 方法时,在第二个参数中传入一个对象,并设置 language 属性为语言名称。



上图是 highlight.js 支持的语言,可以看到有很多种,需要用其他语言的时候,language 设置成指定的语言名称就可以了。


原理


它的原理你可能已经猜到了,在 highlightElement 里我们简单说了一下,现在再看下图:



之所以可以实现着色,其实就是查找和替换的过程,将原来的纯文本替换为元素标签包裹文本,元素是可以加上样式的,而样式就是我们引入的 css 文件。


这就是它的基本原理了。


总结


其实有时候我们去设置 Mackdown 的自定义样式呢,在代码区域设置的时候也是这样设置的,当然类样式的名字呢,基本上都是标准的格式。


好了,这个库分享介绍给你了,库的原理也为你做了简单的科普,希望对你有所帮助。


如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收

作者:子辰Web草庐
来源:juejin.cn/post/7245584147456507965
藏或分享给你的朋友!

收起阅读 »

前端没了?也许是刚开始

前段时间社区上大肆讨论「前端已死」,各种唱衰前端的言论此起彼伏,真是闻者落泪,听者伤心。 最近又听说某大厂取消大前端部门,前端被拆分到各个业务组。很多前端高 P 或离职,或被裁,或转后端。 这是前端的落日? 今天就瞎聊聊,聊到哪算哪。 前端技术发展趋于稳定 过...
继续阅读 »

前段时间社区上大肆讨论「前端已死」,各种唱衰前端的言论此起彼伏,真是闻者落泪,听者伤心。


最近又听说某大厂取消大前端部门,前端被拆分到各个业务组。很多前端高 P 或离职,或被裁,或转后端。


这是前端的落日?


今天就瞎聊聊,聊到哪算哪。


前端技术发展趋于稳定


过去前端最被开发者诟病的是发展太快了,三天不学前端就跟不上了。


在我个人的前端经历中,技术栈从 Jquery 到 Angular,再到 React,前端技术快速切换,同时配套的前端研发体系也在不断推倒重建。


仅仅 React,从 V14 到 V15,再到 V16,一路快速迭代,每一个版本都有质的飞跃。尤其是 V16.8 hooks 出来之后,各种生态工具又得重建。


但最近几年,前端技术发展似乎慢了下来,React 16.8 版本还是 19 年发布的,距离现在已经四年多时间了,在这四年多时间内,React 虽然发布了 V17、V18,但并没有什么质的提升。对于我而言,只要能用 Hooks,16/17/18 哪个版本无所谓。


前端生态趋于稳定,是好事还是坏事?


我以前很羡慕 JAVA 开发同学,学习一套技术体系可以用到老。现在似乎前端也可以这样了,我认为这是一件非常好的事情,是前端成熟的标志。


只有前端技术体系不再发展,前端工程师才能 100% 精力投入在产品建设上。不用总是分精力去学习新技术,新轮子,最大程度复用过去的学习成果和基础设施。


比如现在有个需求是计算 43✖️5 的结果,我们会使用现有的方法,计算出结果,而不是要去思考有没有更好的计算方法。


image.png


本质上是我们的计算方法已经非常成熟稳定了,我们只要专注需求本身。前端什么时候稳定到这个程度,就皆大欢喜了。


公司中前端部门的发展


再聊聊一个公司中,前端团队定位的问题。



  1. 公司发展初期,产品建设高速期,前端的首要目标是建设产品,这时候前端通常在业务部门中,大家全力做产品。

  2. 随着公司发展,产品越来越多,各个业务部门的前端交流少,使用的技术五花八门,重复造轮子。这时候公司通常会搞一个大前端部门,使其研制出来一套在公司内成熟的前端解决方案,统一各种基建,提高人均效能。

  3. 当前端解决方案统一到一定程度之后,大前端部门的使命就结束了,这时候前端又会被拆分到业务部门中。


这里说说阿里大前端部门拆分,我认为原因有两个:



  1. 阿里前端基建相对已经成熟,拆分之后对前端研发效能影响不大。

  2. 公司不挣钱,不吃大锅饭了。以前是各个业务部门一起挣钱,一起养着大前端团队。现在公司不赚钱了,要改革,要求各个业务线自己挣钱自己花,所以前端开发也要回归到业务团队中。


最近并不只是「前端已死」了,我认为各个工种都在面临裁员潮,原罪是公司不赚钱了。在发展阶段啥问题都没有,一旦发展停滞,那什么都是问题,一个一个开刀。


对于个体来讲,如果想在这个公司发展,就要尽力帮助公司成长。另外就是居安思危,提升个人竞争力,只有跑的比别人快,才能在逆风中活下来,才能比别人更容易找到工作。


这里不得不吐槽下,很多人乐意躺平,你说要学习,要往前走一步,他会喷你卷,喷你带坏了风气,喷你是资本主义的走狗。大无语~


一个前端创业机会


之前在蚂蚁,前端基建非常完善,开发者真正的只用关心业务,不用去思考技术的东西。在公司内部基本上做到了只用关心 43✖️5 这个需求,不用考虑该用哪种计算方法。


从蚂蚁离开后,非常怀念蚂蚁的前端解决方案,但在社区上,并没有类似的收费或者免费方案。
据我所知,各个大公司,都会有自己的前端解决方案。但中小公司,基本上没有能力去自建一套类似解决方案。


阿里的后端解决方案,有商业化的产品「云效」。


蚂蚁的后端解决方案,有商业化的产品「SOFAStack」。


微软的后端解决方案,有商业化的产品「Azure DevOps」。


为什么后端有,前端没有呢?


我认为是后端技术栈稳定,基于这一套技术栈的生态也就稳定,发展多年下来势必会有成熟的解决方案。大公司产出商业化解决方案,给中小公司用。


前端技术栈过去发展快,生态不稳定,大公司内部的解决方案也是最近一两年才出来,所以前端目前还没有类似的解决方案。


所以我认为前端解决方案,是未来一个确定性的机会。


这个解决方案覆盖前端研发的整个生命流程,包括不限于:



  • 产品:埋点、数据分析等等

  • 开发:迭代协作、脚手架、组件库、逻辑库、联调、国际化、跨端等等

  • 上线:灰度、回滚、CDN、监控告警、性能、SEO、SSR、SSG、离线包、安全等等


希望未来前端解决方案,像乘法解决方案一样,能成为行业统一规范,让开发者真正的只用关心业务,不用关心技术。


前端的未来


说几点自己的思考:



  1. 前端技术趋于稳定,开始有商业化的前端解决方案出来。

  2. 前端不会消失,但门槛会进一步降低,低端前端饱和。

  3. 高级前端依旧紧缺,因为高级前端并不是只是看前端技能,而是综合考虑技术能力、业务能力、沟通能力、情商、职业素养、工作经验等等,这个过去现在未来都不会变。

  4. 前端工程师可能会变成用户体验工程师,后端考虑存储并发等,前端考虑用户体验,为整体用户体验负责。

  5. 前端会进一步蚕食桌面端开发、移动端开发的生存空间。

  6. AI 可能会改变前端研发模式。


总结


也许前端技术栈稳定之后,才是真正的开始。


文中所有观点未经论证,纯属 YY,欢迎理性讨论。

作者:前端技术砖家
来源:juejin.cn/post/7245874747390083109

收起阅读 »

996.ICU发起人勾结境外势力,被判颠覆国家政权罪刑拘!

996  ICU996.ICU指工作996、生病ICU,也就是工作从早上9点上班到晚上9点下班,每周工作6天,生病了就住进ICU。2019年3月27日,一个名为996ICU的项目在GitHub上传开。程序员们揭露“996ICU”互联网公司,...
继续阅读 »

996  

ICU


996.ICU指工作996、生病ICU,也就是工作从早上9点上班到晚上9点下班,每周工作6天,生病了就住进ICU。2019年3月27日,一个名为996ICU的项目在GitHub上传开。程序员们揭露“996ICU”互联网公司,抵制互联网公司的996工作制度。 [1-2]  

2019年4月8日,反996许可发布满一周,已有104个项目采用该许可。 [3] 

2019年4月11日,人民日报针对“996工作制”发表评论员文章《强制加班不应成为企业文化》 [4]  ;

马老师的"996是福报”更加让996广为社会传播和诟病

今天中国BAT这些公司能够996,我认为是我们这些人修来的福报。这个世界上,我们每一个人都希望成功,都希望美好生活,都希望被尊重,我请问大家,你不付出超越别人的努力和时间,你怎么能够实现你想要的成功?今天我们拥有这么多资源,我们带着巨大的使命,希望在未来能够让天下没有难做的生意,你不付出可以吗?不可以。 [5]  (马云评)

然后,ToB技术社区:qidao123.com臭名昭著的“996 ICU”谁曾料到这居然是有人和境外势力勾结导演传播的呢?








天网恢恢疏而不漏

始作俑者最终等待啊他的是牢狱之灾!

来源:https://mp.weixin.qq.com/s/YQLQPlyF_ljSrV0UPNgtTA

收起阅读 »

优雅的使用位运算,省老多事了!!!

web
你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势? 位运算符号的基本了解 首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换...
继续阅读 »

你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势?


位运算符号的基本了解


首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换为二进制查看,也可以通过 0b 开头来手动创建一个二进制数字:


(3).toString(2) // 11
0b00000011 // 3 前面的几位0可以省略,可以简写为 0b11

1. 与 &


按位对比两个二进制数,如果对应的位都为 1,则结果为 1,否则为 0


console.log((1 & 3) == 1) // true

对比图例如下所示:



2. 或 |


按位对比两个二进制数,如果对应的位有一个 1,则结果为 1,否则为 0


console.log((1 | 3) == 3) // true

对比图例如下所示:



3. 异或 ^


按位对比两个二进制数,如果对应的位有且只有一个 1,则结果为 1,否则为 0


console.log((1 ^ 3) == 2) // true

对比图例如下所示:



4. 非 ~


按位对操作的二进制数取反,即 1 变 0,0 变 1,任何数的非运算符计算结果都是 -(x + 1)


const a = -1 // ~a = -(-1 + 1) = 0
console.log(~a) // 0
const b = 5 // ~b = -(5 + 1) = -6
console.log(~b) // -6

一个数和它的取反数相加的结果总为 -1


5. 左移 <<


左移会将二进制值的有效位数全部左移指定位数,被移出的高位(最左边的数字)丢弃,但符号会保留,低位(最右边的数字)会自动补0


console.log(1 << 2) // 4

图例如下所示:



6. 右移 >>


和左移相反的操作,将二进制的操作数右移指定位数,高位补0,低位丢弃!


console.log(4 >> 2) // 1

参考资料均来自 MDN,除了这些常用的符号之外,文档还标注了所有的JS操作符号,感兴趣的同学可以看一看!


有什么用?


说了这么多符号,对于操作符的影响是加深了,但是有什么用呢?二进制数字难理解,位操作符也难理解,二进制和十进制的互转不写个代码都心算不了,相信各位同学肯定有如此费解,我们先来看一段 Vue 的源代码,其中定义了很多状态类的字段!


源码位置戳这里



以及 Vue 中对其的使用,源码位置戳这里



我们可以看到,Vue 定义了一系列状态列标识一个 Dom 是属于什么类型,并用 VNode 中的一个字段 shapeFlag 来完成存储和判断,对状态的存储只用到了一个字段一个数字,就可以进行多种状态的判断!


我们尝试着设计一种类似的判断结构出来如何?


我有N个权限


假设系统中的用户我们规定其有增删改查四个权限,我们可以设计一个枚举类来标识拥有的四个权限:


enum UserPerm {
CREATE = 1 << 0,
DELETE = 1 << 1,
UPDATE = 1 << 2,
SELECT = 1 << 3,
}

我们设计的时候,完全不必在意上述的二进制的十进制值是什么,只需要清楚的是,上述枚举的 1 在二进制位的哪个位置,如 1 的 二进制为 00000001,将其左移 1(1 << 1), 就变成了 00000010, 依次类推,我们用一个二进制串中的每一位来标识一个权限,这样一个字符串中只要出现对应位置的 1, 则该用户就拥有对应位置的权限,如图:



有什么好处呢?


我们知道二进制是可以转换为十进制的,这样子我们就可以用一个数字来表示多个权限,如一个用户完整的拥有四个权限,那他的二进制为 0b1111, 那么其状态为数字 15


如果一个用户只有 CREATESELECT 的权限,那么二进制表达为 0b1001,十进制数字为 9


后端数据库中,前端用户信息中,接口返回都只有一列一个字段就可以表示,那么用户信息应该是下面的形式:


const userInfo = {
name: '泰罗凹凸曼',
phone: '15888888888',
perm: 9, // 代表其只有 CREATE 和 SELECT 两种权限
}

权限的判断


如何判断这个用户是否具备某一个权限呢?那就需要请出我们的 与运算符(&),参考 Vue 的做法:


console.log(userInfo.perm & UserPerm.CREATE) // 9 & (1 << 0) = 1

console.log(userInfo.perm & UserPerm.UPDATE) // 返回 0, 0代表不通过

如果 userInfo.perm 中包含 CREATE,就会返回 CREATE 的值,否则返回 0,在JS中,任何非0的数字都可以通过 if 判断,所以我们只需要一个判断就足够了!


if (userInfo.perm & UserPerm.CREATE) {
console.log('有创建权限')
} else {
console.log('没有创建权限')
}

什么原理?我们之前给过与运算符的图例,接下来我们看一下如上两句代码的图例所示:



我们看到,上下的符号位如果对不上的话,返回的结果都是 0,这样子我们就轻松实现了权限的判断


权限的增删


那么我们如何实现对一个用户的权限更新呢,比如给上面的用户新增一个 UPDATE 权限,这个时候我们就需要 或运算符(|)


比如:


userInfo.perm | UserPerm.UPDATE // 1001 | 0100 = 1101 = 13

这样子我们就对一个用户权限进行了增加,或的规则我们上面也给过图例,这里大家可以自己尝试理解一下,无非是两个二进制数 10010100 之间的或运算,只有其中一位为 1 则为 1,这两个数字计算的结果自然是 1101


那么如何实现权限删除呢?异或运算符(^)给你答案!有且只有一个 1,返回 1,否则为 0,删除对我们刚刚添加的 UPDATE 权限的方法:


userInfo.perm ^ UserPerm.UPDATE // 1101 ^ 0100 = 1001

非常简单是吧?看到这里,相信你已经完全理解位运算符在权限系统的妙用了,如果我这个时候需要添加一个新的权限,如分享权限,那么我只有用第五位的1来表示这个权限就可以啦


enum UserPerm {
SHARE = 1 << 5
}

// 添加分享权限
userInfo.perm | UserPerm.SHARE

以前的方案


我们以前在做用户标识的时候,通常会定义一个数组来表示,然后执行数组判断来进行权限的判断


const userPerm = ['CREATE', 'UPDATE', 'DELETE', 'SELECT']

// 判断有无权限
if (userPerm.includes('CREATE')) {
// ...
}

// 增加权限
user.perm.push('UPDATE')

// 删除权限
user.perm.splice(user.perm.indexOf('UPDATE'), 1)

相信大家也可以看出来,无论是从内存占用,效率,便捷程度来说位运算符的形式都是完胜,这也是会被各大项目使用的原因之一!快去你的项目中实践吧,记得写好注释哦!


结语


今天带大家认识了位运算符在权限系统的妙用,小伙伴们还有什么使用位运算符的巧妙思路,可以在评论中给出来哦!继续加油吧,快去实践少年!


祝大家越来越牛逼!


去探索,不知道的东西还多着呢,我是泰罗凹凸曼,M78星云最爱写代码的,我们下一篇再会!


作者:泰罗凹凸曼
来源:juejin.cn/post/7244809939838844984
收起阅读 »

晋升涨薪?不,晋升要命!

最近有个朋友来问我晋升的事,其实他没什么产出,只不过因为做的那块业务还不错,赚了好几个小目标,然后顺理成章的被提名了。所以他慌的要死! 你猜他来问什么内容?他什么都想问,又什么都问不出来,因为我也没晋升过。不过这并不妨碍我写点什么,谁让我会编呢? 我要不要提...
继续阅读 »

最近有个朋友来问我晋升的事,其实他没什么产出,只不过因为做的那块业务还不错,赚了好几个小目标,然后顺理成章的被提名了。所以他慌的要死!


你猜他来问什么内容?他什么都想问,又什么都问不出来,因为我也没晋升过。不过这并不妨碍我写点什么,谁让我会编呢?



我要不要提名晋升


我要不要提名晋升?你要是能问出这问题,要不刚毕业,要不加班加迷糊了!


晋升意味着涨薪,涨薪意味着每个月能多吃几顿海底捞,多看几部电影,这世道和谁过不去都不能和钱过不去。


但晋升往往是领导说了才算,除非你是向园,还有个董事长爷爷。所以,怎么说服他就成了一道槛。


工作三要素:A-能力;B-岗位;C-环境(其他人和事),而晋升基本只和 AB 有关。 果你在自己岗位上,已经承担了下一级该承担的责任。然后能力又达到了下一级所要求的水平,再不提名晋升就没天理了


例如我是P6,但是我一直在做P7的事情,同时在抗P7的责任,并且表现不错。那么我对标P7不就是既定事实嘛,既然是事实谁又能阻止你提名?


另外还有 C,如果把晋升与环境挂钩,晋升的理由变成了诸如  “如果我在他的位置上,我能做得比他更好”、“为什么他是P9而我是P8”,以这些理由提名晋升,属实是自寻烦恼,说不定明年你就成为人才输送给社会了。


从这个角度上看,我那位朋友已经晋升失败了。不过你们也不要太关注这个,缘分这个东西不是说有就有。就像你能恰好看见我这篇帖子,然后顺手点赞、收藏、在看一样



提名之后,如何准备答辩


晋升靠的是硬实力,以及10%的运气。 你想去吹牛也不是不行,就怕到时候下不来台。仅仅是 P6 升 P7 的答辩,上面坐着的都是P9级别的大佬和砖家。


PPT 以真实、简朴为主,凡是在 PPT 上花费超过10小时的,我觉得都有耍流氓的嫌疑。这些内容应该是这段时间你所积累的工作成果。平时没事拿个小笔记记一下,关键时刻它能像宋江一样救你的命。【推荐你用语雀,真的很好用】


有了 PPT,你得去讲出来吧。讲话作为一门艺术,对于我们理工科的同学还是有一定难度的,所以我建议你有空去参加下吐槽大会。没有条件?那就创造条件,公司里找几个段子手还不是轻而易举。相比之下,产品经理的优势比我们大多了。



如何把实力讲透?这里面是有一定技巧的,3分讲结果,7分讲过程。光讲结果不讲过程,30分钟的答辩,你5分钟就完成了,还是包含自我介绍的那种。


3分成效如何讲?——把我在当前岗位上,如何把手里的工作做上了一个新台阶 这种感觉讲出来,就是,因为你的努力而带来了什么改变?


7分过程如何讲?——把事情的复杂度、岗位的挑战、面临的困难讲清楚,把你做事的匠心讲清楚,你把你的做事的方法、思路讲清楚。说白了,就是“我解了一个挺难的题,我是这样那样解的”;


关于答辩与专家评委


评委扮演的角色很简单,评审的过程,就是评委向答辩人学习的过程。每个人都存在未涉及的领域,你看 ChatGPT 用了上亿的数据训练,花了几十亿美金,现在连小学数学题都解不出来


三个评委,花45分钟与答辩人进行深度交流,如果评委们都表示没有收获,学不到东西(无论是学到知识还是方法或者心态),那么答辩人晋升不通过,也不冤枉。


这个道理够简单吧,神雕侠侣里黄老邪为什么会和杨过拜把子,一方面是杨过的性格和黄老邪很像,另一方面是因为能从杨过那学到点东西。


回到现实,我们每个人都有直接的体会。如果有个大牛(至少他在当前的工作中是专业的)跟我们交流,我们一定有收获。如果对方十分平庸(或者在工作中能力一般),我们收获就比较少。


晋升通过,意味着什么


意味着加薪,年终奖多了点


意味着岗位(B)对你的能力(A)要求更大了一些


意味着你离 3.25 更近了一些,我知道的几个同事,每次晋升之后的第一个季度或半年度,都会拿一次3.25。


晋升不通过,意味着什么


恭喜你,终于松了口气!


结尾


如果这篇文章对您有所帮助,可以关注我的公众号。



我是车辙,掘金小册《SkyWalking》作者

作者:车辙cz
来源:juejin.cn/post/7244783947820449853
,一个神奇的程序员。

收起阅读 »

js的垃圾回收机制

web
概论 对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。 有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。 我也不是科班出身...
继续阅读 »

概论


对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。


有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。


我也不是科班出身,很多东西不清不楚的。但我感觉计算机行业有个很好的地方,就是学了的知识很快就能得到验证,就能在生产上应用。这种成就感是我当年干机械的时候所无法体验到的。


前几天就有个报障说有个项目越用越卡,但是排查不出问题,我最近正好在学习垃圾回收内存泄漏,就立马能分析出来是不是内存不足产生的影响,就很开心。


本文我会采用图解的方式,尽量照着js垃圾回收的演变历史讲解。


一,什么是垃圾回收


GCGarbage Collection,也就是我们常说的垃圾回收。


我们知道,js是v8引擎编译执行的,而代码的执行就需要内存的参与,内存往往是有限的,为了更好地利用内存资源,就需要把没用的内存回收,以便重新使用。


比如V8引擎在执行代码的过程中遇到了一个函数,那么我们会创建一个函数执行上下文环境并添加到调用栈顶部,函数的作用域里面包含了函数中所有的变量信息,在执行过程中我们分配内存创建这些变量,当函数执行完毕后函数作用域会被销毁,那么这个作用域包含的变量也就失去了作用,而销毁它们回收内存的过程,就叫做垃圾回收。


如下代码:


var testObj1={
a:1
}
testObj1={
b:2
}

对应的内存情况如下:


1,垃圾的产生.drawio.png
其中堆中的{a:1}就变成了垃圾,需要被GC回收掉。


在C / C++中,需要开发者跟踪内存的使用和管理内存。而js等高级语言,代码在执行期间,是V8引擎在为我们执行垃圾回收。


那么既然已经有v8引擎自动给我们回收垃圾了,为啥我们还需要了解V8引擎的垃圾回收机制呢?这是因为依据这个机制,还有些内存无法回收,会造成内存泄漏。具体的表现就是随着项目运行时间的变成,系统越来越卡滞,需要手动刷新浏览器才能恢复。


了解V8的垃圾回收机制,才能让我们更好地书写代码,规避不必要的内存泄漏。


二,内存的生命周期


如上所说,内存应该存在这样三个生命周期:




  1. 分配所需要的内存:在js代码执行的时候,基本数据类型存储在栈空间,而引用数据类型存储在堆空间。




  2. 使用分配的空间:可能对对应的值做一些修改。




  3. 不需要时将其释放回收。


    如下代码:


    function fn(){
    //创建对象,分配空间
    var testObj={
    a:1
    }
    //修改内容
    testObj.a=2
    }
    fn()//调用栈执行完毕,垃圾回收

    对应的内存示意图:




2,内存的生命周期.drawio.png


三,垃圾回收的策略


当函数执行完毕,js引擎是通过移动ESP(ESP:记录当前执行状态的指针)来销毁函数保存在栈当中的执行上下文的,栈顶的空间会被自动回收,不需要V8引擎的垃圾回收机制出面。


然而,堆内存的大小是不固定的,那堆内存中的数据是如何回收的呢?


这就引出了垃圾回收的策略。


通常用采用的垃圾回收有两种方法:引用计数(reference counting)标记清除(mark and sweep)


3.1,引用计数(reference counting)


如上文第二节中所说,testObj对象存放在堆空间,我们想要使用的时候,都是通过指针来访问,那么是不是只要没有额外的指针指向它,就可以判定为它不再被使用呢?


基于这个想法,人们想出了引用计数的算法。


它工作原理是跟踪每个对象被引用的次数,当对象的引用次数变为 0 时,则判定该对象为无用对象, 可以被垃圾回收机制进行回收。


    function fn(){
//创建对象,分配空间
var testObj1={
a:1
}//引用数:1
var testObj2=testObj1//引用数:2
var testObj3=testObj1//引用数:3
var testObj4={
b:testObj1
}//引用数:4
testObj1=null//引用数:3
testObj2=null//引用数:2
testObj3=null//引用数:1
testObj4=null//引用数:1
}
fn()//调用栈执行完毕,垃圾回收

如上代码,引用次数变成0后,堆内存中的对应内存就会被GC。


如下图,当testObj1-4都变成null后,原来的testObj4引用数变成0,而{a:1}这时候的引用数还为1(有一个箭头指向它),而{b:1002}被回收后,它的引用数就变成0,故而最后也被垃圾回收。


3,引用计数的计数数量.drawio.png


引用计数的优点:


引用计数看起来很简单,v8引擎只需要关注计数器即可,一旦对象的引用数变成0,就立即回收。

但是很明显的,引用计数存在两个缺点:


1,每个对象都需要维护一个计数器去记录它的引用数量。
2,如果存在相互循环引用的对象,因为各自的引用数量无法变成0(除非手动改变),因而无法被垃圾回收。

对于第二点,如下代码:


function fn(){
//创建对象,分配空间
var testObj1={
a:testObj2
}
var testObj2={
b:testObj1
}
}
fn()

当fn执行完毕后的内存情况如下,因为两个对象相互引用,导致引用数到不了0,就无法被GC:


4.循环引用.drawio.png


因为引用计数的弊端,后续的浏览器开始寻找新的垃圾回收机制,从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记清除算法的改进。


3.2,标记清除(mark and sweep)


标记清除是另一种常见的垃圾回收机制。


它工作原理是找出所有活动对象并标记它们,然后清除所有未被标记的对象


其实现步骤如下:



  1. 根节点:垃圾回收机制的起点是一组称为根的对象(有很多根对象),根通常是引擎内部全局变量的引用,或者是一组预定义的变量名,例如浏览器环境中的 Window 对象和 Document 对象。

  2. 遍历标记:从根开始遍历引用的对象,将其标记为活动对象。每个活动对象的所有引用也必须被遍历并标记为活动对象。

  3. 清除:垃圾回收器会清除所有未标记的对象,并使空间可用于后续使用。


因为能从根节点开始被遍历到的(有被使用到的),就是有用的活动对象,而剩余不能被链接到的则是无用的垃圾,需要被清除。


对于前文引用计数中循环引用的例子,就因为从根对象触发,无法遍历到堆空间中的那两个循环引用的对象,就会把它判定为垃圾对象,从而回收。


如下代码:


var obj1={
a:{
b:{
c:3
}
}
}
var obj2={
d:1
}
obj2=null

如下图,从根节点无法遍历到obj2了,就会把d垃圾回收。


5,标记清除.png


按照这个思路,标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题:当新对象需要空间存储时,需要遍历空间以找到能够容纳对象大小size的区域:


6,标记清除新增对象.png


这样效率比较低,因而又有了标记整理(Mark-Compact)算法 ,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会先将活动对象向内存的一端移动,然后再回收未标记的垃圾内存:


7,标记整理算法.png


四,V8引擎的分代回收


如上文所说,在每次垃圾回收时都要检查内存中所有的对象,这样的话对于一些占用空间大、存活时间长的对象,要是和占用空间小、存活时间短的对象一起检查,那不是平白浪费很多不必要的检查资源嘛。


因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,那怎么优化呢?


类似于信誉分,信誉分高的,检查力度就应该小一些嘛。把信誉分抽象一下,其实说的就是分层级管理,于是就有了弱分代假设。


4.1,弱分代假设(The Weak Generational Hypothesis)



  1. 多数对象的生命周期短

  2. 生命周期长的对象,一般是常驻对象


V8的GC也是基于假设将对象分为两代: 新生代和老生代。


对不同的分代执行不同的算法可以更有效的执行垃圾回收。


V8 的垃圾回收策略主要基于分代式垃圾回收机制,将堆内存分为新生代和老生代两区域,采用不同的策略来管理垃圾回收。


他们的内存大小如下:


64位操作系统32位操作系统
V8内存大小1.3G(1432MB)0.7g(716MB)
新生代空间32MB16MB
老生代空间1400MB700MB

4.2,新生代的垃圾回收策略


新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收。


在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。Scavenge算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。


Scavenge 算法中将新生代内存一分为二,Semi space FromSemi space To,新生区通常只支持 1~8M 的容量。这块区域使用副垃圾回收器来回收垃圾。


工作方式也很简单:


1,等From空间满了以后,垃圾回收器就会把活跃对象打上标记。
2,把From空间已经被标记的活动对象复制到To空间。
3,将From空间的所有对象垃圾回收。
4,两个空间交换,To空间变成From空间,From变成To空间。以此往复。

而判断是否是活跃对象的方法,还是利用的上文说的从根节点遍历,满足可达性则是活跃对象。


具体流程如下图所示,假设有蓝色指针指向的是From空间,没有蓝色指针指向的是To空间:


8,新生代的垃圾回收策略.drawio.png


从上图可以明显地看到,这种方式解决了上文垃圾回收后内存碎片不连续的问题,相当于是利用空间换时间。


现在新生代空间的垃圾回收策略已经了解,那新生代空间中的对象又如何进入老生代空间呢?


4.3,新生代空间对象晋升老生代空间的条件


1,复制某个对象进入to区域时,如果发现内存占用超过to区域的25%,则将其晋升老生代空间。(因为互换空间后要留足够大的区域给新创建对象)
2,经过两次fromto互换后,还存活的对象,下次复制进to区域前,直接晋升老生代空间。

4.4,老生代空间的垃圾回收策略


老生代空间最初的回收策略很简单,这在我们上文也讲过,就是标记整理算法。


1,先根据可达性,给所有的老生代空间中的活动对象打上标记。
2,将活动对象向内存的一端移动,然后再回收未标记的垃圾内存。

这样看起来已经很完美了,但是我们知道js是个单线程的语言,就目前而言,我们的垃圾回收还是全停顿标记:js是运行在主线程上的,一旦垃圾回收生效,js脚本就会暂停执行,等到垃圾回收完成,再继续执行


这样很容易造成页面无响应的情况,尤其是在多对象、大对象、引用层级过深的情况下。


于是在这基础上,又有了增量标记的优化。


五,V8优化


5.1,增量标记


前文所说,我们给老生代空间中的所有对象打上活动对象的标记,是从一组根节点出发,根据可达性遍历而得。这就是全量地遍历,一次性完成,


但因为js是单线程,为了避免标记导致主线程卡滞。于是人们想出来和分片一样的思路:主线程每次遍历一部分,就去干其他活,然后再接着遍历。如下图:


9,增量标记.png


增量标记就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记,这样就算页面卡滞,因为时间很短,使用者也感受不到,体验就好了很多。


但是这又引发了一个新的问题:每次遍历一部分节点就停下来,下次继续怎么识别到停顿点,然后继续遍历呢?


V8又引入了三色标记法。


5.2,三色标记法


首先要明确初心:三色标记法要解决的问题是遍历节点的暂停与恢复。


使用两个标志位编码三种颜色:白色(00),灰色(10)和黑色(11)。


白色:指的是未被标记的对象,可以回收


灰色:指自身被标记,成员变量(该对象的引用对象)未被标记,即遍历到它了,但是它的下线还没遍历。不可回收


黑色:自身和成员变量都被标记了,是活动对象。不可回收


1,从已知对象开始,即roots(全局对象和激活函数), 将所有非root对象标记置为白色
2,将root对象变黑,同时将root的直接引用对象abc标记为灰色
3,将abc标记为黑色,同时将它们的直接引用对象标记为灰色
4,直到没有可标记灰色的对象时,开始回收所有白色的对象

10,三色标记法.drawio.png


如上图所示,如果第一次增量标记只标记到(2),下次开始时,只要找到灰色节点,继续遍历标记即可。


而遍历标记完成的标志就是内存中不再有灰色的。于是这时候就可以把白色的垃圾回收掉。


那这样就解决了遍历节点的暂停与恢复问题,同时支持增量标记。


(ps:其实这里我有个疑惑,暂停后重新开始的时候,不也要遍历寻找灰色节点嘛,每次恢复都要遍历找灰色节点,不是也耗时嘛?)


5.3,写屏障


按照上文对标记的描述,其实有一个前提条件:在标记期间,代码运行不会变更对象的引用情况。


比如说我采用的是增量标记,前脚刚做好的标记,后脚就被js脚本修改了引用关系,那不是会导致标记结果不可信嘛?如下图:


11,写屏障.drawio.png


就像上图一样,D已经被判定成垃圾了,但是下一个分片的js又引用了它,这时候如果删除,必然不对,所以V8 增量使用 写屏障 (Write-barrier) 机制,即一旦有黑色对象引用白色对象,该机制会强制将引用的白色对象改为灰色,从而保证下一次增量 GC 标记阶段可以正确标记,这个机制也被称作 强三色不变性


那在我们上图的例子中,将对象 B 的指向由对象 C 改为对象 D 后,白色对象 D 会被强制改为灰色。


这样一来,就不会将D判定为垃圾 ,并且图中新增的垃圾C在本轮垃圾回收中也不会回收,而是在下一轮回收了。


5.4,惰性清理


上文的增量标记和三色标记法以及写屏障只是对标记方式的优化。目的是采用分片的思想将标记的流程碎片化。


而清理阶段同样可以利用这个思想。


V8的懒性清理,也称为惰性清理(Lazy Sweeping),是一种垃圾回收机制,用于延迟清理未标记对象所占用的内存空间,以减少垃圾回收期间的停顿时间。


当增量标记结束后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,于是可以将清理的过程延迟一下,让JavaScript逻辑代码先执行;也无需一次性清理完所有非活动对象内存,垃圾回收器可以按需逐一进行清理,直到所有的页都清理完毕。


六,垃圾回收总结


6.1,初始的垃圾回收策略:从引用计数到标记清除


对于js的垃圾回收,最开始的时候,是采用引用计数的算法,但是因为引用计数存在循环引用导致垃圾无法清除,于是又引入了标记清除算法,而标记清除算法存在碎片空间问题,于是又优化成标记整理算法。


随着技术的发展,v8引擎的垃圾回收机制也在不断完善。


6.2,弱分代假设,划分新老生代空间采用不同策略


第一次完善是采用弱分代假设,为了让内存占用大、存活时间长的对象减少遍历,采用分代模型,分成了新分代和老分代空间,垃圾回收采取不同的策略。


新生代空间以空间换时间,拆分成from和to空间互换位置,解决垃圾回收后内存不连续的问题。


将满足条件的对象晋升到老生代空间。而老生代空间采用标记整理算法。


6.3,从全停顿到引入分片思想


因为js是单线程,如果垃圾回收耗时过长,就会阻塞页面响应。


为了解决标记阶段的全停顿问题,引入了增量标记算法。但是非黑即白的标记算法在下一次重新开始标记时无法找到上次的中断点,所以使用三色标记法。此外,为了避免增量标记过程中js脚本变更引用关系,v8又增加了写屏障。


同样的,为了解决清理阶段的全停顿问题,引入了惰性清理。


七,本系列其他文章


最近在整理js基础,下面是已经完成的文章:


js从编译到执行过程 - 掘金 (juejin.cn)


从异步到promise - 掘金 (juejin.cn)


从promise到await - 掘金 (juejin.cn)


浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)


作用域和作用域链 - 掘金 (juejin.cn)


原型链和原型对象 - 掘金 (juejin.cn)


this的指向原理浅谈 - 掘金 (juejin.cn)


js的函数传参之值传递 - 掘金 (juejin.cn)


js的事件循环机制 - 掘金 (juejin.cn)


从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)


八,本文参考文章:


「硬核JS」你真的了解垃圾回收机制吗 - 掘金 (juejin.cn)


一文带你快速掌握V8的垃圾回收机制 - 掘金 (juejin.cn)


[深入浅出]JavaScript GC 垃圾回收机制 - 掘金 (juej

in.cn)

收起阅读 »

23美团一面:双检锁单例会写吗?(总结所有单例模式写法)

面试经历 (后来别人跟我说这种写法nacos里也常见) 记录一次面试经历 2023.06.01 美团海笔的,本来以为笔的情况也不咋好 看到牛客网上一堆ak说没面试机会的 结果也不知怎地到了一面 (略过自我介绍和项目介绍~) 面试官:会写单例吗,写个单例看看 ...
继续阅读 »

面试经历


(后来别人跟我说这种写法nacos里也常见)


记录一次面试经历


2023.06.01


美团海笔的,本来以为笔的情况也不咋好 看到牛客网上一堆ak说没面试机会的


结果也不知怎地到了一面


image.png


(略过自我介绍和项目介绍~)


面试官:会写单例吗,写个单例看看


我:


// 饿汉式
public class SingleObject {
private static SingleObject instance = new SingleObject();

//让构造函数为 private
private SingleObject(){}

public static SingleObject getInstance(){
return instance;
}
}

面试官:嗯 你这个单例在没有引用的时候就创建了对象?优化一下


我:应该是懒汉模式!


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
// 多了一个判断是否为null的过程
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

面试官:你这个线程不安全啊 再优化一下?


我:那就加个锁吧


public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

面试官:这种写法 多线程来了的话会阻塞在一起 能否再优化?


我:。。。 不会了


面试官:回去看看双检锁单例


···


之后问了数据库事务



  1. 读未提交(read uncommitted)

  2. 读已提交(read committed)

  3. 可重复读(repeatable read)

  4. 序列化(serializable)以及默认是哪个(repeatable read) 、


数据库的范式了解吗 等等


不出意外:


image.png


还是非常可惜的 这次机会 再加油吧


单例模式整理


学习自菜鸟教程


1.饿汉式


懒加载 no
多线程安全 yes
缺点:没有实现懒加载,即还未调用就创建了对象


public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

2.懒汉式(线程不安全)


懒加载 yes
多线程安全 no


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

3.懒汉式(线程安全)


懒加载 yes
多线程安全 yes
缺点:和面试官说的那样,多线程访问会阻塞


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

4.双检锁/双重校验锁(DCL,即 double-checked locking)


public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

这段代码是一个单例模式的实现,使用了双重检查锁定的方式来保证线程安全和性能。双重检查锁定是指在加锁前后都进行了一次判空的操作,以避免不必要的加锁操作。


而为了保证双重检查锁定的正确性,需要使用volatile关键字来修饰singleton变量,以禁止指令重排序优化。(JUC的知识串起来了!)如果没有volatile关键字修饰,可能会出现一个线程A执行了new Singleton()但是还没来得及赋值给singleton,而此时另一个线程B进入了第一个if判断,判断singleton不为null,于是直接返回了一个未初始化的实例,导致程序出错。


使用volatile关键字可以确保多线程环境下的可见性和有序性,即一个线程修改了singleton变量的值,其他线程能够立即看到最新值,并且编译器不会对其进行指令重排序优化。这样就能够保证双重检查锁定的正确性。


学到了 !!!


后来别人提醒:


image.png


image.png


(牛逼。。)


5.登记式/静态内部类


public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这段代码是一个单例模式的实现,使用了静态内部类的方式来保证线程安全和性能。静态内部类是指在外部类中定义一个静态的内部类,该内部类可以访问外部类的所有静态成员和方法,但是外部类不能访问内部类的成员和方法。


在这个单例模式的实现中,SingletonHolder就是一个静态内部类,它里面定义了一个静态的、final的、类型为Singleton的变量INSTANCE。由于静态内部类只有在被调用时才会被加载,因此INSTANCE对象也只有在getInstance()方法被调用时才会被初始化,从而实现了懒加载的效果。


由于静态内部类的加载是线程安全的,因此不需要加锁就可以保证线程安全。同时,由于INSTANCE是静态final类型的,因此保证了它只会被实例化一次,并且在多线程环境下也能正确地被发布和共享。


这种方式相对于双重检查锁定来说更加简单和安全,因此在实际开发中也比较常用。


6. 枚举


public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

这段代码是使用枚举类型实现单例模式的一种方式。在Java中,枚举类型是天然的单例,因为枚举类型的每个枚举值都是唯一的,且在类加载时就已经被初始化。


在这个示例代码中,Singleton是一个枚举类型,其中只定义了一个枚举值INSTANCE。由于INSTANCE是一个枚举值,因此它在类加载时就已经被初始化,并且保证全局唯一。在使用时,可以通过Singleton.INSTANCE来获取该单例对象。


需要注意的是,虽然枚举类型天然的单例特性可以保证线程安全和反序列化安全,但是如果需要延迟初始化或者有其他特殊需求,仍然需要使用其他方式来实现单例模式。


7. 容器式单例


Java中可以使用容器来实现单例模式,比如使用Spring框架中的Bean容器。下面是一个使用Spring框架实现单例的示例代码:



  1. 定义一个单例类,比如MySingleton:


public class MySingleton {
private static MySingleton instance;
private MySingleton() {}
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
public void doSomething() {
// do something
}
}


  1. 在Spring的配置文件中定义该类的Bean:


<bean id="mySingleton" class="com.example.MySingleton" scope="singleton"/>


  1. 在Java代码中获取该Bean:


ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
MySingleton mySingleton = (MySingleton) context.getBean("mySingleton");
mySingleton.doSomething();

在上面的代码中,我们在Spring的配置文件中定义了一个名为mySingleton的Bean,它的类是com.example.MySingleton,作用域为singleton(即单例)。然后在Java代码中,我们通过ApplicationContext获取该Bean,并调用它的doSomething()方法。


使用Spring框架可以方便地管理单例对象,同时也可以很容易地实现依赖注入和控制反转等功能。


总结完成 继续加油!!!


作者:ovO
来源:juejin.cn/post/7244820297290465317
收起阅读 »