注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

谈谈外网刷屏的量子纠缠效果

web
大家好,我卡颂。 最近被一段酷炫的量子纠缠效果刷屏了: 原作者是@_nonfigurativ_,一位艺术家、程序员。 今天简单讲讲他的核心原理。 基础概念 首先我们需要知道两个概念: 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点 窗口坐标系,页面窗口...
继续阅读 »

大家好,我卡颂。


最近被一段酷炫的量子纠缠效果刷屏了:


acda85f4-d21d-407e-b433-b88a4a65468b.gif


原作者是@_nonfigurativ_,一位艺术家、程序员。



今天简单讲讲他的核心原理。


基础概念


首先我们需要知道两个概念:




  • 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点




  • 窗口坐标系,页面窗口左上角就是窗口坐标系的圆点





如果只用一台电脑,不外接屏幕的话,我们会有:




  • 一个屏幕坐标系




  • 打开几个页面,每个页面有各自的窗口坐标系




如果外接了屏幕(或外接pad),那么就存在多个屏幕坐标系,这种情况的计算需要用到管理屏幕设备的API —— window.getScreenDetails,在本文的讨论中不涉及这种情况。


当我们打开一个新页面窗口,窗口的左上角就是窗口坐标系的圆点,如果要在页面正中间画个圆,那圆心的窗口坐标系坐标应该是(window.innerWidth / 2, window.innerHeight / 2)



对于一个打开的窗口:




  • 他的左上角相对于屏幕顶部的距离为window.screenTop




  • 他的左上角相对于屏幕左边的距离为window.screenLeft





所以,我们可以轻松得出圆的圆心在屏幕坐标系中的坐标:



位置检测


在效果中,当打开两个页面,他们能感知到对方的位置并作出反应,这是如何实现的呢?



当前,我们已经知道圆心在屏幕坐标系中的坐标。如果打开多个页面,就会获得多个圆心的屏幕坐标系坐标


现在需要做的,就是让这些页面互相知道对方的坐标,这样就能向对应的方向做出连接的特效。


同源网站跨页面通信的方式有很多,比如:




  • Window.postMessage




  • LocalStorageSessionStorage




  • SharedWorker




  • BroadcastChannel




甚至Cookie也能用于跨页面通信(可以在同源的所有页面之间共享)。


在这里作者使用的是LocalStorage



只需要为每个页面生成一个唯一ID


const pageId = Math.random().toString(36).substring(2); // 生成一个随机的页面ID

每当将圆心最新坐标存储进LocalStorage时:


localStorage.setItem(
pageId,
JSON.stringify({
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
})
);

在另一个页面通过监听storage事件就能获取对方圆心的屏幕坐标系坐标


window.addEventListener("storage", (event) => {
if (event.key !== pageId) {
// 来自另一个页面
const { x, y } = JSON.parse(event.newValue);
// ...
}
});

再将对方圆心的屏幕坐标系坐标转换为自身的窗口坐标系坐标,并在该坐标绘制一个圆,就能达到类似窗口叠加后,下面窗口的画面出现在上面窗口内的效果。


通俗的讲,所有页面都会绘制其他页面的圆,只是有些圆在页面窗口外,看不见罢了。



考虑到页面性能,检测圆心的屏幕坐标系坐标渲染圆相关操作可以放到requestAnimationFrame回调中执行。


后记


上述只是该效果的核心原理。要完全复刻效果,还得考虑:




  • 渲染大量粒子(我们示例中用代替),且多窗口通信时的性能问题




  • 窗口移动时的阻尼效果




  • 当前的实现是在同一个屏幕坐标系中,如果要跨屏幕实现,需要使用window.getScreenDetails




不得不感叹跨界(作者是艺术家 + 程序员)迸发的想象力真的不一般。



作者:魔术师卡颂
来源:juejin.cn/post/7304531203771301923
收起阅读 »

[自定义View]一个简单的渐变色ProgressBar

web
Android原生ProgressBar 原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。 style: style效果@android:style/Widget.ProgressBar.Horizontal水平进...
继续阅读 »

Android原生ProgressBar



原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。



style:


style效果
@android:style/Widget.ProgressBar.Horizontal水平进度条
@android:style/Widget.ProgressBar.Small小型圆形进度条
@android:style/Widget.ProgressBar.Large大型圆形进度条
@android:style/Widget.ProgressBar.Inverse反色进度条
@android:style/Widget.ProgressBar.Small.Inverse反色小型圆形进度条
@android:style/Widget.ProgressBar.Large.Inverse反色大型圆形进度条
@android:style/Widget.Material**MD风格

原生的特点就是单调,实现基本的功能,使用简单样式不复杂;要满足我们期望的效果就只能自定义View了。


自定义ProgressBar



自定义View的实现方式有很多种,继承已有的View,如ImageView,ProgressBar等等;也可以直接继承自View,在onDraw中绘制需要的效果。
要实现的效果是一个横向圆角矩形进度条,内容为渐变色。
所以在设计时要考虑到可以定义的属性:渐变色、进度等。



<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="progress">
<attr name="progress" format="float" />
<attr name="startColor" format="color" />
<attr name="endColor" format="color" />
</declare-styleable>
</resources>

View实现



这里直接继承子View,读取属性,在onDraw中绘制进度条。实现思路是通过定义Path来绘制裁切范围,确定绘制内容;再实现线性渐变LinearGradient来填充进度条。然后监听手势动作onTouchEvent,动态绘制长度。


同时开放公共方法,可以动态设置进度颜色,监听进度回调,根据需求实现即可。



package com.cs.app.view

/**
*
*/

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.cs.app.R

class CustomProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val progressPaint = Paint()
private val backgroundPaint = Paint()
private var progress = 50f
private var startColor = Color.parseColor("#4C87B7")
private var endColor = Color.parseColor("#A3D5FE")
private var x = 0f
private var progressCallback: ProgressChange? = null

init {
// 初始化进度条画笔
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.FILL

// 初始化背景画笔
backgroundPaint.isAntiAlias = true
backgroundPaint.style = Paint.Style.FILL
backgroundPaint.color = Color.GRAY

if (attrs != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.progress)
startColor = typedArray.getColor(R.styleable.progress_startColor, startColor)
endColor = typedArray.getColor(R.styleable.progress_endColor, endColor)
progress = typedArray.getFloat(R.styleable.progress_progress, progress)
typedArray.recycle()
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val width = width.toFloat()
val height = height.toFloat()

//绘制Path,限定Canvas边框
val path = Path()
path.addRoundRect(0f, 0f, width, height, height / 2, height / 2, Path.Direction.CW)
canvas.clipPath(path)

//绘制进度条
val progressRect = RectF(0f, 0f, width * progress / 100f, height)
val colors = intArrayOf(startColor, endColor)
val shader = LinearGradient(0f, 0f, width * progress / 100f, height, colors, null, Shader.TileMode.CLAMP)
progressPaint.shader = shader
canvas.drawRect(progressRect, progressPaint)
}

override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
x = event.rawX

//实现点击调整进度
progress = (event.rawX - left) / width * 100
progressCallback?.onProgressChange(progress)
invalidate()
}

android.view.MotionEvent.ACTION_MOVE -> {
//实现滑动调整进度
progress = (event.rawX - left) / width * 100
progress = if (progress < 0) 0f else if (progress > 100) 100f else progress
progressCallback?.onProgressChange(progress)
invalidate()
}

else -> {}
}
return true
}

fun setProgress(progress: Float) {
this.progress = progress
invalidate()
}

fun setOnProgressChangeListener(callback: ProgressChange) {
progressCallback = callback
}

interface ProgressChange {
fun onProgressChange(progress: Float)
}
}

示例


class CustomViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_view)
val progressTv: TextView = findViewById(R.id.progress_textview)
val view: CustomProgressView = findViewById(R.id.progress)
view.setProgress(50f)

view.setOnProgressChangeListener(object : CustomProgressView.ProgressChange {
override fun onProgressChange(progress: Float) {
progressTv.text = "${progress.toInt()}%"
}
})
}
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#0E1D3C"
android:layout_height="match_parent">


<com.cs.app.view.CustomProgressView
android:id="@+id/progress"
android:layout_width="200dp"
android:layout_height="45dp"
app:endColor="#A3D5FE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.274"
app:progress="60"
app:startColor="#4C87B7" />


<TextView
android:id="@+id/progress_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:textColor="#ffffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果如下:


HnVideoEditor_2023_11_23_144034156.gif
作者:LANFLADIMIR
来源:juejin.cn/post/7304531564342837287
收起阅读 »

2023小红书Android面试之旅

一面 自我介绍 看你写了很多文章,拿你理解最深刻的一篇出来讲一讲 讲了Binder相关内容 Binder大概分了几层 哪些方法调用会涉及到Binder通信 大概讲一下startActivity的流程,包括与AMS的交互 全页面停留时长埋...
继续阅读 »

一面




  • 自我介绍




  • 看你写了很多文章,拿你理解最深刻的一篇出来讲一讲


    讲了Binder相关内容




  • Binder大概分了几层




  • 哪些方法调用会涉及到Binder通信




  • 大概讲一下startActivity的流程,包括与AMS的交互




  • 全页面停留时长埋点是怎么做的


    我在项目中做过的内容,主要功能是计算用户在每个Activity的停留时长,并且支持多进程。这里的多进程支持主要是通过以ContentProvider作为中介,然后通过ContentResolver.call方法去调用它的各种方法以实现跨进程




  • 动态权限申请是什么


    详见 Android动态权限申请从未如此简单 这篇文章




  • 你做的性能监测工具,FPS是怎么采集的




  • 性能监测工具用在了什么场景




  • 有没有通过这个性能监测工具去做一些优化




  • 图片库,例如Glide,一般对Bitmap有哪些优化点




  • 过期的Bitmap可以复用吗




  • 有没有基于ASM插桩做过一些插件




  • 讲了一下当时做过的一个个人项目 FastInflate


    这个项目没能达到最终的目标,但通过做这个项目学习了很多新知识,比如APT代码生成、阅读了LayoutInflater源码、AppCompatDelegateImpl实现的LayoutInflater.Factory2会极大的拖慢布局创建的速度等




  • 怎么优化布局创建速度


    提示了预加载,但我当时脑抽在纠结xml的缓存,没想到可以提前把视图先创建好




  • 说一下你觉得你最擅长或者了解最透的点


    我回答的自定义View




  • 解决过View的滑动冲突吗




  • 讲解了一个之前写过的开源控件 SwipeLoadingLayout




  • 一般遇到困难的解决方案是什么




  • 算法题:反转链表




  • 反问阶段




    • 咱们组主要负责哪些内容




    • 主要使用Java还是Kotlin


      Kotlin




    • 小红书的面试一般是怎么个流程?多少轮?


      一般三轮技术面,一轮HR面




    • 面试完一般多久会给到结果


      比较快,一两天的样子






二面




  • 自我介绍




  • 为什么这个时间节点想要出来换工作呢




  • 在B站这些年做了什么




  • 做了哪些基础组件


    讲解了一下之前写的 SwipeLoadingLayout




  • 介绍一下Android的事件传递机制




  • 你写的这个分享模块是如何设计的


    对外采用流式调用的形式,内部通过策略模式区分不同的平台以及分享类型,给每个平台创建了一个中间Activity作为分享SDK请求的发起方(SDK.getApi().share())以及分享结果的接收方(onActivityResult),然后通过广播将分享的结果送入到分享模块内进行处理,最终调用用户设置的分享回调告知结果




  • 看你之前在扇贝的时候有开发过一些性能监测工具,那有做过性能优化吗




  • 你是如何收集这些性能数据的




  • 有没有对哪方面做过一些针对性的优化




  • Android系统为什么会触发ANR,它的机制是什么




  • 有解过ANR相关的问题吗?有哪几种类型?




  • 算法题:二叉树的层序遍历




  • Queue除了LinkedList还有哪些实现类




  • 现在还在面其他公司吗?你自己后面职业生涯的选择是怎么样的?




  • 给我介绍了一下团队,说我面试的这个部门应该说是小红书最核心的团队,包括主页、搜索、图文、视频等等都在部门业务范畴内,部门主要分三层,除了业务层之外还有基础架构层以及性能优化层




  • 反问阶段




    • 部门分三层的话,那新人进来的话是需要从业务层做起吗?


      不是这样的,我们首先会考虑这个同学能干什么,然后会考虑这个同学愿意去做什么,进来后,有经验的同学也会来带你的,不会一上来就让你抗输出,总之会把人放到适合他的团队里




    • 小红书会使用到一些跨端技术吗?


      会,之前在一些新的App上使用的Flutter,现在主要用的是RN,还会使用到一些DSL,这个不能算跨段。为什么在小红书社区App中跨端技术提及的比较少,是因为小红书App非常重视用户体验,对性能的要求比较高






三面




  • 自我介绍




  • 介绍一下目前负责的业务




  • 工作过程中有碰到过什么难题,最后是怎么解决的


    一开始脑抽了没想到该说什么,随便扯了一个没啥技术含量的东西,又扯了一个之前做的信号捕获的工具,后来回忆起来了,重新说了一个关于DEX编排的东西(主DEX中方法数超过65535导致打包失败,写了个脚本将一部分Class从主DEX中移除到其他DEX中)




  • 如何设计一个头像的自定义View,要求使头像展示出来是一个圆形




  • 介绍一下Android事件的分发流程




  • 如何处理View的防误触




  • 怎么处理滑动冲突




  • ActivityonCreate方法中调用了finish方法,那它的生命周期会是怎样的




  • 如果我想判断一个Activity中的一个View的尺寸,那我什么时候能够拿到




  • RecyclerView如何实现一个吸顶效果




  • JavaKoltin你哪个用的比较多




  • 有用过Kotlin的协程吗




  • Kotlin中的哪些Feature你用的多,觉得写的好呢




  • 你是怎么理解MVVM




  • 你有用过Jetpack Compose




  • 有用过kotlin中的by lazylateinit




  • kotlin中怎么实现单例,怎么定义一个类的静态变量




  • 算法题:增量元素之间的最大差值




  • 你这次看机会的原因是什么




  • 反问阶段我感觉之前问的差不多了,这次就没再问什么问题了




HR面




  • 现在是离职还是在职状态




  • 介绍一下之前负责的工作




  • 用户量怎么样




  • 这个项目是从0到1开发的吗




  • 这个业务有什么特点,对于客户端开发有什么挑战与困难




  • 团队分工是怎样的




  • 这个项目能做成现在这个样子,你自己的核心贡献有哪些




  • 这个事情对你来说有什么收获吗




  • 在B站的工作节奏是怎么样的




  • 离职的原因是什么呢




  • 你自己希望找一个什么样的环境或者什么阶段的业务




  • 你对小红书有什么了解吗




  • 未来两三年对于职业发展的想法




  • 你觉得现在有什么限制了你或者你觉得你需要提升哪些部分




  • 反问阶段



    • 问了一些作息、福利待遇之类的问题




总结


小红书面试总体而言给我的体验是很好的,每轮面试后基本上都是当天就能出结果,然后约下一轮的面试。最终从一面到HR面结束出结果,一共花了9天时间,还是挺快的。二面结束后,一面的面试官加我微信说小红书目前很缺人,感兴趣的同学也可以来试试。


作者:dreamgyf
来源:juejin.cn/post/7304267413637333029
收起阅读 »

关于鸿蒙开发,我暂时放弃了

起因 在最近鸿蒙各种新闻资讯说要鸿蒙不再兼容android之后,我看完了鸿蒙视频,并简单的撸了一个demo。 # 鸿蒙HarmonyOS从零实现类微信app效果第一篇,基础界面搭建 # 鸿蒙HarmonyOS从零实现类微信app效果第二篇,我的+发现...
继续阅读 »

image.png


image.png


起因


在最近鸿蒙各种新闻资讯说要鸿蒙不再兼容android之后,我看完了鸿蒙视频,并简单的撸了一个demo。


企业微信截图_6f8acb94-bd68-4f56-9460-4a59d2370a4a.png



鸿蒙的arkui,使用typescript作为基调,然后响应式开发,对于我这个old android来说,确实挺惊艳的。而且在模拟器中运行起来也很快,写demo的过程鸡血满满,着实很愉快。


后面自己写的文章,也在掘金站点上获得了不错的评价。


企业微信截图_fa34f233-af43-4567-8dac-57ef5666f1bd.png


image.png


打击


今天下午,刚好同事有一个遥遥领先(meta 40 pro),鸿蒙4.0版本


怀着秀操作的想法,在同事手机上运行了起来。very nice。 一切出奇的顺利。


but ...


尼玛,点击的时候,直接卡住不对,黑屏。让人瞬间崩溃。


本着优先怀疑自己的原则,我找了一个官方的demo。 运行起来。


额...


尼玛。还是点击之后卡住了,大概30s之后,才跳转到新的页面。


image.png


这一切,让我熬夜掉的头发瞬间崩溃。


放弃了...


放弃了...


后续


和其他学习鸿蒙的伙伴沟通,也遇到了同样的问题,真机不能运行,会卡线程。但是按下home键,再次回到界面,页面会刷新过来


我个人暂时决定搁置对于鸿蒙开发的学习了,后续如果慢慢变得比较成熟之后,再次接触学习吧。


作者:王先生技术栈
来源:juejin.cn/post/7304538094736343052
收起阅读 »

自由职业的好与坏:谈谈我的真实感受

回家自由职业八个多月了,有很多很多和上班不一样的感受。 最近心态算是平稳了,打算把这些写下来。 当然,感受这个东西很主观,每个人都不一样,所以没普适性,看看就好。 整体来说,自由职业有好有坏。 陪家人时间变多了 回家自由职业第一个好处就是陪家人的时间多了很多很...
继续阅读 »

回家自由职业八个多月了,有很多很多和上班不一样的感受。


最近心态算是平稳了,打算把这些写下来。


当然,感受这个东西很主观,每个人都不一样,所以没普适性,看看就好。


整体来说,自由职业有好有坏。


陪家人时间变多了


回家自由职业第一个好处就是陪家人的时间多了很多很多,之前在一线城市打工,我都是过年的时候回家待几天,甚至国庆我都不愿意回家。


那时候接收到家人的消息就是通过电话聊下最近遇到的事情,帮忙出出主意之类的,就像是以一个旁观者的身份在听故事。


而现在我们是在一起经历着这些事情,比如今天上午我妈骑车带我去银行办了一些业务,比如我们会讨论快餐店的哪个咸汤好喝。


这些鸡毛蒜皮的小事,就是生活本身。


在老家陪着家人一起生活,会让我有种踏实感,因为我的根就在这里。


收入焦虑带来的效率不增反降


说实话,自由职业注定会面临一些焦虑,因为之前打工的时候,只要每天按时上下班,每月就能收到工资。


比如之前 30 号中午会收到银行卡工资到账的消息。


而现在这种稳定的收入没有了。


虽然我写东西是能赚到钱的,但现在写东西和之前写东西心态就不一样了。


现在会感觉玩游戏、看小说、刷短视频,都会有负罪感。


一天过去如果啥也没写,就会焦虑。


而这种焦虑反而会让我产生逃避心理,会更多的去玩游戏、看小说、刷短视频。


所以我自由职业后的产出是不增反降了不少。


之前平均每周 3 篇左右的技术文章,现在可能就一篇,而且小册更的也不快。


我最近在有意识的戒掉这些,所以好多了。


有人说当你存款到一定的程度就没这种焦虑了,可能吧,但我还没解锁这个前提。


脱离工作场景后技术成长变慢


工作的时候,做一个个需求,你会遇到各种问题,而解决这些问题的过程中,你会发现一些新的知识,或者对一些技术会有一些新的研究。


而这些其实就是技术成长,也是我之前写文章的重要来源。


但自由职业后就不会自然的收获这些技术成长,需要你刻意的去研究一些东西,而且可能不会有太深入细节的场景应用。


这也导致了我经常不知道写什么。


这也是我文章更的少了的一个原因。


可以自由利用时间做你想做的事情


说了好几个自由职业的缺点,再来讲几个优点。


自由职业有大把时间去研究你感兴趣的一些东西。


比如 Nest 和后端技术栈,我在上次换工作后就很少有机会涉及了,也没时间去做各种实践。


而现在可以花大量时间在这些东西上,短时间内就可以把它研究到一定的程度。


可以自由利用自己的时间,这个还是挺过瘾的。


复利会让你后期起飞


说实话,其实我的收入在自由职业之后是下跌了不少的。


单看收入的话,为什么还要继续呢?


其实写文章、写小册这种事情是有积累效应的,会一直产生复利。


比如我很久之前写过的文章,每天还会有人会阅读点赞


比如我去年写的调试小册,前几天双十一还卖了好几百份


这些会一直源源不断增加我的影响力。


而且会慢慢就会有更多人期待我的下一篇文章、下一本小册。


我现在写了 300 篇文章,还算是有小小的影响力,那如果我再写 500 篇、1000 篇呢?


现在我每本小册平均销量在 5500,那如果我再写 5 本、再写 10 本呢?


只有对这个方向有热情,就是可以长期做下去的,这就是很多人说的相信长期价值。


而且国内程序员还有 35 岁问题,但当你能不靠公司独立获取一定程度的收入,这种问题就会消失。


所以我根本不迷茫 35 岁以后能干啥。


我之前说的想写一辈子技术文章也不是说说而已。


与收入有关的一些问题


很多人关心我收入的问题,那我就来细说下这方面。


我获取收入的方式主要是公众号广告和小册。


公众号广告一个 1000,我大概一个月接 3、4 个,这些够我生活费用了。


感谢印客学院的负责推广的小姐姐,人很 nice。


很多人会反感广告,但你让一个人靠爱发电去写 300 篇技术文章也不现实。


所以,这个就当作很自然的事情就好了。


小册这个,确实有点难受。


比如我的 nest 小册,单看毛收入,是 50 万。


但是扣税 20%、平台分成 30%,加上去掉各种 7 折、 5 折活动、再去掉分销的返现。


能到手一半就不错了。


我最近听朋友说小鹅通这种就没有分成,而且当天打钱,差不多你卖了 50 万能到手 49 万。


类似的,知识星球、小报童等等这些平台,都差不多收入多少到手多少。


所以说,在掘金或者其他平台上出课,有好有坏吧。


要不要交社保


打工的时候公司给交社保,自由职业后就没公司给交了,要不要自己交也是个问题。


这个问题也是自由职业肯定会面临的。


据说社保里有用的就是医保,可以报销医疗费,但我感觉用不大到。


再就是养老保险要交满 15 年社保,我也就交了 5 年多,要不要自己再交 10 年我还没想好。


目前我还没自己交过。


脱离社会的游离感


自由职业之后,不再有固定的上下班时间,不用定闹钟,感受不到周一的痛苦、周五和周末的快乐。


没有这种周的概念,会感觉每天都差不多,时间过的特别快。


而且自己一个人在家里写东西,没有同事、领导等交流,没有团建。


你会有种脱离社会的游离感。


可能喜欢交际的人受不了这种,但我还好,我还是挺喜欢一个人的状态的。


不过感觉不到周和周的界限这个,确实会让我感觉生活少了一些东西,过于平淡了。


小县城没有夜生活和小众圈子


小县城是没有夜生活的,我们这最大的超市 9 点就关门了。


10 点的时候,整个城市差不多就都睡了。


而在北京,12 点睡觉都感觉很早了。


而且,之前在北京上海的时候,不管你有啥特殊爱好,都能找到不少志同道合的人,可以和你交流这些小众的话题。


但回到小县城之后,就不要想了,找不到的。


总结


回到老家小县城很久了,有很多的感受,但直到今天才梳理了一下。


自由职业有好有坏,会有收入焦虑、时间多了但产出不一定会高很多、脱离工作场景遇到的可研究的技术问题会变少,会有脱离社会的游离感,但是陪家人的时间变多了,而且可以把大把时间花在想做的事情上,可以做一些有积累的事情。


当然,感受这个比较主观,只是我个人的一些总结。


当你感觉状态不对的时候,可以像我一样剖析下自己,看看自己最近遇到的问题、发生的变化,和状态好的时候的一些区别。


把这些问题摆出来,并且找到问题的原因,也就能更好的去调整自己。


我的自由职业之路大概率会一直持续下去,希望过段时间我能调节好自己,有超过打工时的状态和输出效率。未来可期,加油吧。


作者:zxg_神说要有光
来源:juejin.cn/post/7304561386888511514
收起阅读 »

糟糕!试用期被裁了

如果你觉得找工作已经够难了?那么抱歉,接下来我说的这件事情可能更令你更糟心。 什么事情呢? 如题所见,就是“试用期被裁这件事”。 每年都会有同学找到我,说自己被裁了,比如下面这些。 同程旅行被裁: 一家知名外企被裁: 其他类似的同学还有很多,比如 B 站试...
继续阅读 »

如果你觉得找工作已经够难了?那么抱歉,接下来我说的这件事情可能更令你更糟心。


什么事情呢?


如题所见,就是“试用期被裁这件事”。


每年都会有同学找到我,说自己被裁了,比如下面这些。


同程旅行被裁:
211efa6455114289e6f46309f7f72bf.jpg
一家知名外企被裁:
c99be7e145f29436f1fc5fd9ca1bb7a.jpg
其他类似的同学还有很多,比如 B 站试用期被裁、小红书被裁、得物被裁等等,因为换了手机,之前的聊天没有了,所以这里只能给大家看最近的截图了。


为什么被裁?


每个人被裁的原因可能都不一样,但大概可能被分为两类:



  1. 主观原因

    1. 技术能力不够

    2. 理解能力不够

    3. 表达、沟通能力不够

    4. 上下级关系没有处理好



  2. 客观原因

    1. 公司财务收紧

    2. 公司业务线调整

    3. 公司转型




其中,客观原因已经超出了我们的掌控范围,所以不是本文要讨论的重点。我们本文主要讨论的是第一类问题,以及如何规避这些问题。


如何避免被裁?


其实知道了被裁的原因,反向提升自己的能力,就可以避免被裁的厄运了(当然非主观原因除外)。


1.多提升技术能力


时刻提醒自己,技术岗的核心竞争力是“技术”,所以既然选择了“技术”这条路,那么只有不断的学习,才是提升自己竞争力的关键。逆水行舟,有时候不是自己退步了,而是身边的人都进步了,那你自然而然就成了垫底的人了,这个时候,离末位淘汰和被裁就不远了。所以,一定要注意。


2.多做有效沟通


沟通分为有效沟通和无效沟通两类,有效沟通是指信息能够准确、清晰地传达、理解和接收的过程。它是在交流中实现真正的相互理解和达成共识的能力。


有效沟通包括以下几个方面:



  1. 表述清晰明确:有效沟通需要明确表达自己的意思,使用简洁、清晰的语言,避免模糊或含糊不清的词语或说法。理解者也要确保准确理解对方的意思,必要时可以进行追问或澄清。

  2. 倾听和理解:有效沟通不仅需要表达自己的观点,也需要认真倾听对方的观点和意见。倾听包括积极关注对方所说的话,并努力理解对方的观点和感受。

  3. 适应对方:有效沟通需要考虑对方的背景、情况和特点。要注意使用对方能够理解和接受的语言和方式进行沟通,避免使用无意义的行话或专业术语。

  4. 反馈和确认:为了确保信息的准确传达和理解,有效沟通需要进行反馈和确认。在沟通过程中,可以提问、请求对方重述或总结所表达的内容,以确保信息传达的一致性和准确性。


3.带上解决方案和自己的思考


上面教你要“多沟通”,但不代表,遇到任何事情都要给领导汇报和沟通,这样只会适得其反。我之前就有一个同事,工作 8~9 年了,我当时是部门负责人,他在工作中的任何问题,无论大小,都要和我反复沟通和确认。刚开始我还能好脾气的和他聊几句,后面就发现,如果要一直这样,那么我什么事也做不了,什么任务也完成不了,自己就像他的私人顾问一样。


这让我很苦恼,后来不得不开除他,因为如果留他在公司,那么我的所有任务都会延期和完成不了。


所以,领导一定是比你忙的,他的事情是最多的。所以遇到事情之后,一定要三思,自己先琢磨,如果自己深入思考之后,还是解决不了,那么这个时候就需要及时和领导沟通汇报了,千万不要怕丢脸。


还有就是沟通之前,最好自己先有几种方案,然后再和领导沟通汇报,让他参与到这件事并做最终的决策。


这里要遵循的原则是:小事、自己能搞明白的事,认真思考之后,能不打扰领导就尽量不打扰领导,但如果真的遇到问题,千万不能藏着掖着,要及时上报,获得领导的支持和协助。


4.笨鸟先飞 + 谦虚谨慎


刚去公司的时候,无论你的技术再好,一定要谦虚谨慎,起码你的业务能力相比于老同事差的还很远,所以这个时候一定要下“奔功夫”。也就是早上早早去公司,提前熟悉公司的业务和开发流程,下班之后,如果同事都在加班,那么自己就多看会公司的文档,刚去切记不要给领导留下偷奸耍滑的印象,一定要能下得了“笨功夫”。


5.多听 + 充分准备


尤其在开需求会的时候,因为刚去公司可能对业务不熟悉,这个时候一定要多听,少发表意见。不要刚到一家公司就指手画脚的,先了解了实际情况之后再发表意见,不要给同事和领导留下“轻浮”的印象。


并且,最好在开需求大会之前,先深入了解业务和需求,做好充分的准备,以便自己能跟得上开会的节奏,也能发表真正有建设性的意见。


6.做事积极 + 可靠


公司给你分配的任务,尤其是试用期,不要表现悲观、不满等情绪,刚去公司试用期还是以低调、谦虚为主。领导给你分下来的活,无论大小,积极主动去做,因为事情你总是要做的,积极也是做,消极也是做,还不如积极一点,给领导留下一个好印象。


其次,做事一定要有结果和反馈,比如领导给你安排一件事要 3 天做完,那么 3 天之后,一定要把当前的任务的具体情况主动主动汇报给领导,这样领导才会觉得你办事靠谱,起码把他说的事认真落实了。


能不能完成任务是能力问题,有没有把领导安排的事积极落实是态度问题,所有的领导都喜欢听话的、办事靠谱的员工,这一点至关重要。


小结


找到工作后,能否平安度过试用期至关重要,所以在试用期期间,一定要下足“笨功夫”,用心做事、谨慎谦虚、积极主动、认真负责,多提升自己的技术能力、多和领导进行有效的沟通,以上这些都是平安度过试用期的关键。加油,少年~


作者:Java中文社群
来源:juejin.cn/post/7304538151455309875
收起阅读 »

努力学习和工作就等于成长吗?

努力学习和工作与成长的关系是一个值得去深思的问题。 有趣的是,参加实习的时候,我将手上的工作做完之后去学其他的技术了,因为那时候刚好比较忙,所以领导就直接提了一个箱子过来,然我去研究一下那个硬件怎么对接。 我看了下文档,只提供了两种语言,C++和JavaScr...
继续阅读 »

努力学习和工作与成长的关系是一个值得去深思的问题。


有趣的是,参加实习的时候,我将手上的工作做完之后去学其他的技术了,因为那时候刚好比较忙,所以领导就直接提了一个箱子过来,然我去研究一下那个硬件怎么对接。


我看了下文档,只提供了两种语言,C++和JavaScript,显然排除了C++,而是使用JavaScript,不过对于写Java的我来说,虽然也玩过两年的JS,但是明显还是不专业。


我将其快速对接完成后,过了几天,又搞了几台硬件过来叫我对接。


显然这次我不想去写好代码再发给前端了,于是直接拉前段代码来和他们一起开发了。


一个后端程序员硬生生去写了前端。


那么这时候,有些人就会说,“哎呀,能者多劳嘛,你看你多么nb,啥都能干,领导就喜欢这种人了”


屁话,这不是能力,这是陷阱。



之前看到一个大佬在他的文章中写道,“如果前端和后端都能干的人,那么大概率是前端能力不怎么滴,后端能力也不怎么滴”。


我们排除那种天生就学习能力特别强的人,这种人天生脑子就是好,学啥都很快,而且学得特别好,但是这样的人是很少数的,和我们大多数人是没关的。


就像有一个大佬,后端特别厉害,手写各种中间件都不在话下,起初我以为他是个全才。


知道有一天,他要出一门教程,然后自己连最基本的CSS和HTML都不会写,然后叫别人给他写。


那么,这能说明他不厉害吗?


各行各业,精英大多都是在自己的领域深耕的。


这个世界最不缺的就是各领域的高手。


在职场中,也并不是什么都会就代表领导赏识你,只能证明你这颗螺丝比较灵活,可以往左边扭,也可以往右边扭。



在自己擅长的领域去做,把一件事尽可能垂直。


之前和一朋友聊天,他说他干过python,干过java,干过测试,干过开发,干过实施......


反正差不多什么都干过了,但是为什么后面还是啥也没干成?


我们顶多能说他职业经历丰富,但是不能说他职业经验丰富,经历是故事,而经验才是成长。


可见垂直是很重要的,不过执着追求垂直也未必是一件好事,还要看风往那边吹,不然在时代发展的潮流中也会显得无力。


就像前10年左右,PHP可谓是一领Web开发的龙头!


那句“PHP是世界上最好的语言”可谓是一针强心剂。


可是现在看来,PHP已经谈出Web领域了,很多PHP框架早已转型,比如swoole,swoft等,只留下那句“PHP是世界上最好的语言”摇摇欲坠。


可笑的是,之前看到一个群友说,领导叫他去维护一套老系统,而老系统就是PHP写的,于是他去学了好久ThinkPHP框架,但是过了半年,这个项目直接被Java重构了。


真是造化弄人啊!



深度学习和浅尝辄止


在我们还没有工作的时候,在学校看着满入眼帘的技术,心中不免有一种冲动,“老子一定要把它全部学完”


于是从表面去看一遍,会一点了,然后马上在自己学习计划上打一个勾。


但是当遇到另外一个新技术的时候,完全又懵了,于是又重复之前的动作。


这看似学了很多,但是实际上啥也没学会。


个人的精力完全是跟不上时代的发展的,十年前左右,随便会一点编程知识,那找工作简直是别人来请的,但是现在不一样了,即使源码看透了,机会也不多。


而如果掌握了核心,那么无论技术再怎么变革,只需要短暂学习就能熟练了。


就像TCP/IP这么多年了,上层建筑依然是靠它。



看似努力,实则自我感动!


在我们读书的时候,总有个别同学看似很努力,但是考试就是考不好。


究其本质,他的努力只是一种伪装,可能去图书馆5个小时,刷抖音就用了四个小时,然后发个朋友圈,“又是对自己负责的一天”。


也有不少人天天加班,然后也会发个朋友圈,“今天的努力只是为了迎接明天更好的自己”。


事实如此吗?


看到希望,有目的性的努力才是人间清醒。


如果觉得自己学得很累,工作得很累,但是实际上啥也没学到,啥也没收获,那么这样得努力是毫无意义的。


这个世界欺骗别人很容易,但是欺骗自己很难!


作者:追梦人刘牌
来源:juejin.cn/post/7303804693192081448
收起阅读 »

日本排放的核污水就像软件项目迭代中的技术债

日本最终还是排放了核污水 2023年8月24日,日本最后还是排放了核污水。网上的讨论很多,有说不应该排放的,也有说污染其实不严重,不需要担心的。首先说明,我是反对排放的。不是因为核污水超标,即使没有超标,我也不认为应该排放到海里。 为什么呢,因为这些辐射元素排...
继续阅读 »

日本最终还是排放了核污水


2023年8月24日,日本最后还是排放了核污水。网上的讨论很多,有说不应该排放的,也有说污染其实不严重,不需要担心的。首先说明,我是反对排放的。不是因为核污水超标,即使没有超标,我也不认为应该排放到海里。


为什么呢,因为这些辐射元素排放到海水中后,会通过食物链富集效应,最终大量的集中到人类身上。即使排放时的指标是安全的,最终,辐射污染富集到人体身上后,迟早要超标的。


关于日本排放核污水的事,我觉得和软件开发过程中的技术债很像。所以今天就蹭个热点,聊一聊项目中的技术债。


软件项目的迭代过程


我记得大学的时候,课本上学的软件迭代还是瀑布流的迭代方式。这个就比较简单,提出一个大项目,然后细化各个功能。架构师拿到十分完善的需求文档后,开始架构设计。然后开发,测试,上线,验收,完成整个项目。在这个过程中,需求是明确的,按照需求去实现就行。


可是后来出现了敏捷开发。老板们一看,这个好呀,敏捷开发,不就是快速开发吗,大大提高开发速度。尤其是互联网公司,讲究的就是一个唯快不破。然后,,,很明显就感觉到,项目中的技术债越来越多了。


原先项目需求明确后,即使开发过程中有需求变化,也都是在代码上线前修改,可以把看到的不合理代码设计给改掉,影响可控。敏捷后,项目需求是逐步迭代上线的,有时候需求间是相互影响的,再加上互联网的人员换的也勤快,需求时间也短,这前后的代码越来越不融洽。


技术债的积累


技术债是怎么积累的呢,其实是项目过程中,为了短期的收益(上线时间)而做出的技术上的妥协(怎么快怎么来),这些妥协可能会在未来导致更多的工作,或者可能的线上问题。可以看出,技术债务并不总是坏事。有时,为了满足紧迫的市场需求或截止日期,团队可能需要做出一些妥协。关键是要意识到这些妥协,并在适当的时候偿还这些债务。如果需求上线了,就不管对应的技术债了,这个技术债可不就是越来越多吗。


就像文章最开始的日本排放核污水。从当年日本核电站地震出事,日本采用了注水冷却并储存核污水的方案,我们就能意识到,这个核污水一定要处理掉的,不然越堆越多,最后就只能排放出来的。10几年了,日本就没想过要解决核污水,现在说要排放,我觉得吧,这个估计是10几年前就确定的方案了,只是没有往外说,最后就看什么时间排放。最后,我们就这样又一次见证了历史。


重构


技术债不断积累后,会导致新需求越做越慢,bug还多。这个时候,老板就会觉得做这个需求的人不行,做的又慢,bug又多。但是只有做需求的人才知道,真的改不动呀。


所以,当你做一个需求,感觉改不动,或者明明没有改多少代码,但是bug特别多。 那么一定要想一想这个项目是不是存在了很长时间了。如果存在了很长时间了,那么大概率需要解决技术债的时候到了。怎么解决,重构!!


怎么重构,这个话题很大,一两句说不清楚。不过,一旦你成功重构了一个老项目,那么大概率你的技术水平能有一大步的提升。


就像有人说的,一个文明的衰退等于一个新的文明的诞生。






作者:写代码的浩
来源:juejin.cn/post/7271140848850272267
收起阅读 »

Java代码是如何被CPU狂飙起来的?

无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。但是对于一个Java程序员来说写了那么久的代码,我们总要搞清楚自己写的Java代码到底是怎么运行起来的。另外在求职面试的时候这个问题也常...
继续阅读 »

无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。但是对于一个Java程序员来说写了那么久的代码,我们总要搞清楚自己写的Java代码到底是怎么运行起来的。另外在求职面试的时候这个问题也常常会聊到,面试官主要想通过它考察求职同学对于Java以及计算机基础技术体系的理解程度,看似简单的问题实际上囊括了JVM运行原理、操作系统以及CPU运行原理等多方面的技术知识点。我们一起来看看Java代码到底是怎么被运行起来的。


Java如何实现跨平台


在介绍Java如何一步步被执行起来之前,我们需要先弄明白为什么Java可以实现跨平台运行,因为搞清楚了这个问题之后,对于我们理解Java程序如何被CPU执行起来非常有帮助。


为什么需要JVM


write once run anywhere曾经是Java响彻编程语言圈的slogan,也就是所谓的程序员开发完java应用程序后,可以在不需要做任何调整的情况下,无差别的在任何支持Java的平台上运行,并获得相同的运行结果从而实现跨平台运行,那么Java到底是如何做到这一点的呢?


其实对于大多数的编程语言来说,都需要将程序转换为机器语言才能最终被CPU执行起来。因为无论是如Java这种高级语言还是像汇编这种低级语言实际上都是给人看的,但是计算机无法直接进行识别运行。因此想要CPU执行程序就必须要进行语言转换,将程序语言转化为CPU可以识别的机器语言。


image.png


学过计算机组成原理的同学肯定都知道,CPU内部都是用大规模晶体管组合而成的,而晶体管只有高电位以及低电位两种状态,正好对应二进制的0和1,因此机器码实际就是由0和1组成的二进制编码集合,它可以被CPU直接识别和执行。


image.png


但是像X86架构或者ARM架构,不同类型的平台对应的机器语言是不一样的,这里的机器语言指的是用二进制表示的计算机可以直接识别和执行的指令集集合。不同平台使用的CPU不同,那么对应的指令集也就有所差异,比如说X86使用的是CISC复杂指令集而ARM使用的是RISC精简指令集。所以Java要想实现跨平台运行就必须要屏蔽不同架构下的计算机底层细节差异。因此,如何解决不同平台下机器语言的适配问题是Java实现一次编写,到处运行的关键所在。


那么Java到底是如何解决这个问题的呢?怎么才能让CPU可以看懂程序员写的Java代码呢?其实这就像在我们的日常生活中,如果双方语言不通,要想进行交流的话就必须中间得有一个翻译,这样通过翻译的语言转换就可以实现双方畅通无阻的交流了。打个比方,一个中国厨师要教法国厨师和阿拉伯厨师做菜,中国厨师不懂法语和阿拉伯语,法国厨师和阿拉伯厨师不懂中文,要想顺利把菜做好就需要有翻译来帮忙。中国厨师把做菜的菜谱告诉翻译者,翻译者将中文菜谱转换为法文菜谱以及阿拉伯语菜谱,这样法国厨师和阿拉伯厨师就知道怎么做菜了。


image.png


因此Java的设计者借助了这样的思想,通过JVM(Java Virtual Machine,Java虚拟机)这个中间翻译来实现语言转换。程序员编写以.java为结尾的程序之后通过javac编译器把.java为结尾的程序文件编译成.class结尾的字节码文件,这个字节码文件需要JVM这个中间翻译进行识别解析,它由一组如下图这样的16进制数组成。JVM将字节码文件转化为汇编语言后再由硬件解析为机器语言最终最终交给CPU执行。


640.png


所以说通过JVM实现了计算机底层细节的屏蔽,因此windows平台有windows平台的JVM,Linux平台有Linux平台的JVM,这样在不同平台上存在对应的JVM充当中间翻译的作用。因此只要编译一次,不同平台的JVM都可以将对应的字节码文件进行解析后运行,从而实现在不同平台下运行的效果。


image.png


那么问题又来了,JVM是怎么解析运行.class文件的呢?要想搞清楚这个问题,我们得先看看JVM的内存结构到底是怎样的,了解JVM结构之后这个问题就迎刃而解了。


JVM结构


JVM(Java Virtual Machine)即Java虚拟机,它的核心作用主要有两个,一个是运行Java应用程序,另一个是管理Java应用程序的内存。它主要由三部分组成,类加载器、运行时数据区以及字节码执行引擎。


image.png


类加载器


类加载器负责将字节码文件加载到内存中,主要经历加载-》连接-》实例化三个阶段完成类加载操作。


image.png


另外需要注意的是.class并不是一次性全部加载到内存中,而是在Java应用程序需要的时候才会加载。也就是说当JVM请求一个类进行加载的时候,类加载器就会尝试查找定位这个类,当查找对应的类之后将他的完全限定类定义加载到运行时数据区中。


运行时数据区


JVM定义了在Java程序运行期间需要使用到的内存区域,简单来说这块内存区域存放了字节码信息以及程序执行过程数据。运行时数据区主要划分了堆、程序计数器虚拟机栈、本地方法栈以及元空间数据区。其中堆数据区域在JVM启动后便会进行分配,而虚拟机栈、程序计数器本地方法栈都是在常见线程后进行分配。


image.png


不过需要说明的是在JDK 1.8及以后的版本中,方法区被移除了,取而代之的是元空间(Metaspace)。元空间与方法区的作用相似,都是存储类的结构信息,包括类的定义、方法的定义、字段的定义以及字节码指令。不同的是,元空间不再是JVM内存的一部分,而是通过本地内存(Native Memory)来实现的。在JVM启动时,元空间的大小由MaxMetaspaceSize参数指定,JVM在运行时会自动调整元空间的大小,以适应不同的程序需求。


字节码执行引擎


字节码执行引擎最核心的作用就是将字节码文件解释为可执行程序,主要包含了解释器、即使编译以及垃圾回收器。字节码执行引擎从元空间获取字节码指令进行执行。当Java程序调用一个方法时,JVM会根据方法的描述符和方法所在的类在元空间中查找对应的字节码指令。字节码执行引擎从元空间获取字节码指令,然后执行这些指令。


JVM如何运行Java程序


在搞清楚了JVM的结构之后,接下来我们一起来看看天天写的Java代码是如何被CPU飙起来的。一般公司的研发流程都是产品经理提需求然后程序员来实现。所以当产品经理把需求提过来之后,程序员就需要分析需求进行设计然后编码实现,比如我们通过Idea来完成编码工作,这个时候工程中就会有一堆的以.java结尾的Java代码文件,实际上就是程序员将产品需求转化为对应的Java程序。但是这个.java结尾的Java代码文件是给程序员看的,计算机无法识别,所以需要进行转换,转换为计算机可以识别的机器语言。


image.png


通过上文我们知道,Java为了实现write once,run anywhere的宏伟目标设计了JVM来充当转换翻译的工作。因此我们编写好的.java文件需要通过javac编译成.class文件,这个class文件就是传说中的字节码文件,而字节码文件就是JVM的输入。


image.png


当我们有了.class文件也就是字节码文件之后,就需要启动一个JVM实例来进一步加载解析.class字节码。实际上JVM本质其实就是操作系统中的一个进程,因此要想通过JVM加载解析.class文件,必须先启动一个JVM进程。JVM进程启动之后通过类加载器加载.class文件,将字节码加载到JVM对应的内存空间。


image.png


当.class文件对应的字节码信息被加载到中之后,操作系统会调度CPU资源来按照对应的指令执行java程序。


image.png


以上是CPU执行Java代码的大致步骤,看到这里我相信很多同学都有疑问这个执行步骤也太大致了吧。哈哈,别着急,有了基本的解析流程之后我们再对其中的细节进行分析,首先我们就需要弄清楚JVM是如何加载编译后的.class文件的。


字节码文件结构


要想搞清楚JVM如何加载解析字节码文件,我们就先得弄明白字节码文件的格式,因为任何文件的解析都是根据该文件的格式来进行。就像CPU有自己的指令集一样,JVM也有自己一套指令集也就是Java字节码,从根上来说Java字节码是机器语言的.class文件表现形式。字节码文件结构是一组以 8 位为最小单元的十六进制数据流,具体的结构如下图所示,主要包含了魔数、class文件版本、常量池、访问标志、索引、字段表集合、方法表集合以及属性表集合描述数据信息。


image.png


这里简单说明下各个部分的作用,后面会有专门的文章再详细进行阐述。


魔数与文件版本


魔数的作用就是告诉JVM自己是一个字节码文件,你JVM快来加载我吧,对于Java字节码文件来说,其魔数为0xCAFEBABE,现在知道为什么Java的标志是咖啡了吧。而紧随魔数之后的两个字节是文件版本号,Java的版本号通常是以52.0的形式表示,其中高16位表示主版本号,低16位表示次版本号。。


常量池


在常量池中说明常量个数以及具体的常量信息,常量池中主要存放了字面量以及符号引用这两类常量数据,所谓字面量就是代码中声明为final的常量值,而符号引用主要为类和接口的完全限定名、字段的名称和描述符以及方法的名称以及描述符。这些信息在加载到JVM之后在运行期间将符号引用转化为直接引用才能被真正使用。常量池的第一个元素是常量池大小,占据两个字节。常量池表的索引从1开始,而不是从0开始,这是因为常量池的第0个位置是用于特殊用途的。


访问标志


类或者接口的访问标记,说明类是public还是abstract,用于描述该类的访问级别和属性。访问标志的取值范围是一个16位的二进制数。


索引


包含了类索引、父类索引、接口索引数据,主要说明类的继承关系。


字段表集合


主要是类级变量而不是方法内部的局部变量。


方法表集合


主要用来描述类中有几个方法,每个方法的具体信息,包含了方法访问标识、方法名称索引、方法描述符索引、属性计数器、属性表等信息,总之就是描述方法的基础信息。


属性表集合


方法表集合之后是属性表集合,用于描述该类的所有属性。属性表集合包含了所有该类的属性的描述信息,包括属性名称、属性类型、属性值等等。


解析字节码文件


知道了字节码文件的结构之后,JVM就需要对字节码文件进行解析,将字节码结构解析为JVM内部流转的数据结构。大致的过程如下:


1、读取字节码文件


JVM首先需要读取字节码文件的二进制数据,这通常是通过文件输入流来完成的。


2、解析字节码


JVM解析字节码的过程是将字节码文件中的二进制数据解析为Java虚拟机中的数据结构。首先JVM首先会读取字节码文件的前四个字节,判断魔数是否为0xCAFEBABE,以此来确认该文件是否是一个有效的Java字节码文件。JVM接着会解析常量池表,将其中的常量转换为Java虚拟机中的数据结构,例如将字符串常量转换为Java字符串对象。解析类、接口、字段、方法等信息:JVM会依次解析类索引、父类索引、接口索引集合、字段表集合、方法表集合等信息,将这些信息转换为Java虚拟机中的数据结构。最后,JVM将解析得到的数据结构组装成一个Java类的结构,并将其放入元空间中。


在完成字节码文件解析之后,接下来就需要类加载器闪亮登场了,类加载器会将类文件加载到JVM内存中,并为该类生成一个Class对象。


类加载


加载器启动


我们都知道,Java应用的类都是通过类加载器加载到运行时数据区的,这里很多同学可能会有疑问,那么类加载器本身又是被谁加载的呢?这有点像先有鸡还是先有蛋的灵魂拷问。实际上类加载器启动大致会经历如下几个阶段:


image.png


1、以linux系统为例,当我们通过"java"启动一个Java应用的时候,其实就是启动了一个JVM进程实例,此时操作系统会为这个JVM进程实例分配CPU、内存等系统资源;


2、"java"可执行文件此时就会解析相关的启动参数,主要包括了查找jre路径、各种包的路径以及虚拟机参数等,进而获取定位libjvm.so位置,通过libjvm.so来启动JVM进程实例;


3、当JVM启动后会创建引导类加载器Bootsrap ClassLoader,这个ClassLoader是C++语言实现的,它是最基础的类加载器,没有父类加载器。通过它加载Java应用运行时所需要的基础类,主要包括JAVA_HOME/jre/lib下的rt.jar等基础jar包;


4、而在rt.jar中包含了Launcher类,当Launcher类被加载之后,就会触发创建Launcher静态实例对象,而Launcher类的构造函数中,完成了对于ExtClassLoader及AppClassLoader的创建。Launcher类的部分代码如下所示:


public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
//类静态实例
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;

public static Launcher getLauncher() {
return launcher;
}
//Launcher构造器
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}

try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}

Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}

if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}

System.setSecurityManager(var3);
}

}
...
}

双亲委派模型


为了保证Java程序的安全性和稳定性,JVM设计了双亲委派模型类加载机制。在双亲委派模型中,启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)以及应用程序类加载器(Application ClassLoader)按照一个父子关系形成了一个层次结构,其中启动类加载器位于最顶层,应用程序类加载器位于最底层。当一个类加载器需要加载一个类时,它首先会委派给它的父类加载器去尝试加载这个类。如果父类加载器能够成功加载这个类,那么就直接返回这个类的Class对象,如果父类加载器无法加载这个类,那么就会交给子类加载器去尝试加载这个类。这个过程会一直持续到顶层的启动类加载器。


image.png


通过这种双亲委派模型,可以保证同一个类在不同的类加载器中只会被加载一次,从而避免了类的重复加载,也保证了类的唯一性。同时,由于每个类加载器只会加载自己所负责的类,因此可以防止恶意代码的注入和类的篡改,提高了Java程序的安全性。


数据流转过程


当类加载器完成字节码数据加载任务之后,JVM划分了专门的内存区域内承载这些字节码数据以及运行时中间数据。其中程序计数器、虚拟机栈以及本地方法栈属于线程私有的,堆以及元数据区属于共享数据区,不同的线程共享这两部分内存数据。我们还是以下面这段代码来说明程序运行的时候,各部分数据在Runtime data area中是如何流转的。


public class Test {
public static void main(String[] args) {
User user = new User();
Integer result = calculate(user.getAge());
System.out.println(result);
}

private static Integer calculate(Integer age) {
Integer data = age + 3;
return data;
}

}

以上代码对应的字节码指令如下所示:


image.png


如上代码所示,JVM创建线程来承载代码的执行过程,我们可以将线程理解为一个按照一定顺序执行的控制流。当线程创建之后,同时创建该线程独享的程序计数器(Program Counter Register)以及Java虚拟机栈(Java Virtual Machine Stack)。如果当前虚拟机中的线程执行的是Java方法,那么此时程序计数器中起初存储的是方法的第一条指令,当方法开始执行之后,PC寄存器存储的是下一个字节码指令的地址。但是如果当前虚拟机中的线程执行的是naive方法,那么程序计数器中的值为undefined。


那么程序计数器中的值又是怎么被改变的呢?如果是正常进行代码执行,那么当线程执行字节码指令时,程序计数器会进行自动加1指向下一条字节码指令地址。但是如果遇到判断分支、循环以及异常等不同的控制转移语句,程序计数器会被置为目标字节码指令的地址。另外在多线程切换的时候,虚拟机会记录当前线程的程序计数器,当线程切换回来的时候会根据此前记录的值恢复到程序计数器中,来继续执行线程的后续的字节码指令。


除了程序计数器之外,字节码指令的执行流转还需要虚拟机栈的参与。我们先来看下虚拟机栈的大致结构,如下图所示,栈大家肯定都知道,它是一个先入后出的数据结构,非常适合配合方法的执行过程。虚拟机栈操作的基本元素就是栈帧,栈帧的结构主要包含了局部变量、操作数栈、动态连接以及方法返回地址这几个部分。


image.png


局部变量


主要存放了栈帧对应方法的参数以及方法中定义的局部变量,实际上它是一个以0为起始索引的数组结构,可以通过索引来访问局部变量表中的元素,还包括了基本类型以及对象引用等。非静态方法中,第0个槽位默认是用于存储this指针,而其他参数和变量则会从第1个槽位开始存储。在静态方法中,第0个槽位可以用来存放方法的参数或者其他的数据。


操作数栈


和虚拟机栈一样操作数栈也是一个栈数据结构,只不过两者存储的对象不一样。操作数栈主要存储了方法内部操作数的值以及计算结果,操作数栈会将运算的参与方以及计算结果都压入操作数栈中,后续的指令操作就可以从操作数栈中使用这些值来进行计算。当方法有返回值的时候,返回值也会被压入操作数栈中,这样方法调用者可以获取到返回值。


动态链接


一个类中的方法可能会被程序中的其他多个类所共享使用,因此在编译期间实际无法确定方法的实际位置到底在哪里,因此需要在运行时动态链接来确定方法对应的地址。动态链接是通过在栈帧中维护一张方法调用的符号表来实现的。这张符号表中保存了当前方法中所有调用的方法的符号引用,包括方法名、参数类型和返回值类型等信息。当方法需要调用另一个方法时,它会在符号表中查找所需方法的符号引用,然后进行动态链接,确定方法的具体内存地址。这样,就能够正确地调用所需的方法。


方法返回地址:


当一个方法执行完毕后,JVM会将记录的方法返回地址数据置入程序计数器中,这样字节码执行引擎可以根据程序计数器中的地址继续向后执行字节码指令。同时JVM会将方法返回值压入调用方的操作栈中以便于后续的指令计算,操作完成之后从虚拟机栈中奖栈帧进行弹出。


知道了虚拟机栈的结构之后,我们来看下方法执行的流转过程是怎样的。


1、JVM启动完成.class文件加载之后,它会创建一个名为"main"的线程,并且该线程会自动调用定义在该类中的名为"main"的静态方法,这也是Java程序的入口点;


2、当JVM在主线程中调用当方法的时候就会创建当前线程独享的程序计数器以及虚拟机栈,在Test.class类中,开始执行mian方法 ,因此JVM会虚拟机栈中压入main方法对应的栈帧;


image.png


3、在栈帧的操作数栈中存储了操作的数据,JVM执行字节码指令的时候从操作数栈中获取数据,执行计算操作之后再将结果压入操作数栈;


4、当进行calculate方法调用的时候,虚拟机栈继续压入calculate方法对应的栈帧,被调用方法的参数、局部变量和操作数栈等信息会存储在新创建的栈帧中。其中该栈帧中的方法返回地址中存放了main方法执行的地址信息,方便在调用方法执行完成后继续恢复调用前的代码执行;


image.png


5、对于age + 3一条加法指令,在执行该指令之前,JVM会将操作数栈顶部的两个元素弹出,并将它们相加,然后将结果推入操作数栈中。在这个例子中,指令的操作码是“add”,它表示执行加法操作;操作数是0,它表示从操作数栈的顶部获取第一个操作数;操作数是1,它表示从操作数栈的次顶部获取第二个操作数;


6、程序计数器中存储了下一条需要执行操作的字节码指令的地址,因此Java线程执行业务逻辑的时候必须借助于程序计数器才能获得下一步命令的地址;


7、当calculate方法执行完成之后,对应的栈帧将从虚拟机栈中弹出,其中方法执行的结果会被压入main方法对应的栈帧中的操作数栈中,而方法返回地址被重置到main现场对应的程序计数器中,以便于后续字节码执行引擎从程序计数器中获取下一条命令的地址。如果方法没有返回值,JVM仍然会将一个null值推送到调用该方法的栈帧的操作数栈中,作为占位符,以便恢复调用方的操作数栈状态。


8、字节码执行引擎中的解释器会从程序计数器中获取下一个字节码指令的地址,也就是从元空间中获取对应的字节码指令,在获取到指令之后,通过翻译器翻译为对应的汇编语言而再交给硬件解析为机器指令,最终由CPU进行执行,而后再将执行结果进行写回。


CPU执行程序
通过上文我们知道无论什么编程语言最终都需要转化为机器语言才能被CPU执行,但是CPU、内存这些硬件资源并不是直接可以和应用程序打交道,而是通过操作系统来进行统一管理的。对于CPU来说,操作系统通过调度器(Scheduler)来决定哪些进程可以被CPU执行,并为它们分配时间片。它会从就绪队列中选择一个进程并将其分配给CPU执行。当一个进程的时间片用完或者发生了I/O等事件时,CPU会被释放,操作系统的调度器会重新选择一个进程并将其分配给CPU执行。也就是说操作系统通过进程调度算法来管理CPU的分配以及调度,进程调度算法的目的就是为了最大化CPU使用率,避免出现任务分配不均空闲等待的情况。主要的进程调度算法包括了FCFS、SJF、RR、MLFQ等。


CPU如何执行指令?
前文中我们大致搞清楚了类是如何被加载的,各部分类字节码数据在运行时数据区怎么流转以及字节码执行引擎翻译字节码。实际上在运行时数据区数据流转的过程中,CPU已经参与其中了。程序的本质是为了根据输入获得相应的输出,而CPU本质就是根据程序的指令一步步执行获得结果的工具。对于CPU来说,它核心工作主要分为如下三个步骤;


1、获取指令


CPU从PC寄存器中获取对应的指令地址,此处的指令地址是将要执行指令的地址,根据指令地址获取对应的操作指令到指令寄存中,此时如果是顺存执行则PC寄存器地址会自动加1,但是如果程序涉及到条件、循环等分支执行逻辑,那么PC寄存器的地址就会被修改为下一条指令执行的地址。


2、指令译码


将获取到的指令进行翻译,搞清楚哪些是操作码哪些是操作数。CPU首先读取指令中的操作码然后根据操作码来确定该指令的类型以及需要进行的操作,CPU接着根据操作码来确定指令所需的寄存器和内存地址,并将它们提取出来。


3、执行指令


经过指令译码之后,CPU根据获取到的指令进行具体的执行操作,并将指令运算的结果存储回内存或者寄存器中。


image.png


因此一旦CPU上电之后,它就像一个勤劳的小蜜蜂一样,一直不断重复着获取指令-》指令译码-》执行指令的循环操作。


CPU如何响应中断?


当操作系统需要执行某些操作时,它会发送一个中断请求给CPU。CPU在接收到中断请求后,会停止当前的任务,并转而执行中断处理程序,这个处理程序是由操作系统提供的。中断处理程序会根据中断类型,执行相应的操作,并返回到原来的任务继续执行。


在执行完中断处理程序后,CPU会将之前保存的程序现场信息恢复,然后继续执行被中断的程序。这个过程叫做中断返回(Interrupt Return,IRET)。在中断返回过程中,CPU会将处理完的结果保存在寄存器中,然后从栈中弹出被中断的程序的现场信息,恢复之前的现场状态,最后再次执行被中断的程序,继续执行之前被中断的指令。
那么CPU又是如何响应中断的呢?主要经历了以下几个步骤:


image.png


1、保存当前程序状态


CPU会将当前程序的状态(如程序计数器、寄存器、标志位等)保存到内存或栈中,以便在中断处理程序执行完毕后恢复现场。


2、确定中断类型


CPU会检查中断信号的类型,以确定需要执行哪个中断处理程序。


3、转移控制权


CPU会将程序的控制权转移到中断处理程序的入口地址,开始执行中断处理程序。


4、执行中断处理程序


中断处理程序会根据中断类型执行相应的操作,这些操作可能包括保存现场信息、读取中断事件的相关数据、执行特定的操作,以及返回到原来的程序继续执行等。


5、恢复现场


中断处理程序执行完毕后,CPU会从保存的现场信息中恢复原来程序的状态,然后将控制权返回到原来的程序中,继续执行被中断的指令。


后记


很多时候看似理所当然的问题,当我们深究下去就会发现原来别有一番天地。正如阿里王坚博士说的那样,要想看一个人对某个领域的知识掌握的情况,那就看他能就这个领域的知识能讲多长时间。想想的确如此,如果我们能够对某个知识点高度提炼同时又可以细节满满的进行展开阐述,那我们对于这个领域的理解程度就会鞭辟入里。这种检验自己知识学习深度的方式也推荐给大家。


作者:慕枫技术笔记
来源:juejin.cn/post/7207769757570482234
收起阅读 »

前端半自动化部署

web
在前端项目部署时,通常会经历以下步骤: 构建项目:在部署之前,需要使用相应的构建工具(如Webpack、Vite等)对项目进行构建,生成生产环境所需的静态文件(如HTML、CSS、JavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化...
继续阅读 »

在前端项目部署时,通常会经历以下步骤:


image.png



  1. 构建项目:在部署之前,需要使用相应的构建工具(如WebpackVite等)对项目进行构建,生成生产环境所需的静态文件(如HTMLCSSJavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化等操作。

  2. 选择部署方式:根据项目的实际需求,选择适合的部署方式。常见的部署方式包括将静态文件部署到静态文件托管服务(如NetlifyVercelGitHub Pages等)、与后端API服务一起部署到云服务器(如AWS、阿里云、腾讯云等)等。

  3. 配置域名和SSL证书:如果你有自定义域名,需要在域名服务商处将域名解析到部署好的静态文件托管服务或云服务器上。同时,为了保障网站的安全性,建议配置SSL证书,使网站能够通过HTTPS协议进行访问。

  4. 持续集成/持续部署(CI/CD :可以考虑使用CI/CD工具(如GitHub ActionsGitLab CITravis CI等)来实现自动化的构建和部署流程,以提高开发效率并确保部署过程的稳定性。

  5. 性能优化:在部署完成后,可以对网站进行性能优化,包括使用CDN加速、资源压缩、缓存配置等,以提高网站的加载速度和用户体验。

  6. 监控和日志:部署完成后,建议设置监控系统以及日志记录系统,及时发现和解决线上问题。


具体的部署流程会因项目和需求的不同而有所差异。


本文从部署方案一步步做实践,最终实现半自动化部署,当然也可以直接使用Docker或其他方案实现自动化部署。


手动化的部署流程是利用xshell连接服务器,利用xftp进行文件传输。操作流程相对比较原始化。


image.png


假如要实现协同开发人员可以实现共同部署,并且可以减去每次部署都要打开xshell。可以写一段脚本实现连接服务器进行文件传输过程,保证打包后能够运行脚本自动化上传文件。


首先要解决连接服务器问题,可以通过ssh实现。(ftpssh是两种常用的远程文件传输协议,可以高效地将代码上传到服务器。)


await ssh.connect({
host: '主机名',
username: '用户名',
password: '密码'
})

服务器连接成功后,可以进行文件传输


await ssh.putDirectory('本地目录路径', '远程目录路径', {
recursive: true, // 上传整个目录
concurrency: 10, // 同时上传的文件数量
tick(localPath, remotePath, error) { // 通过tick回调函数来监听上传过程中的状态
if(error) {
console.log(`无法上传${localPath}${remotePath}${error}`)
} else {
console.log(`${localPath}上传至${remotePath}`)
}
}
})

此时即可实现脚本的基本功能,只需要在每次npm run build结束后,自动执行这段脚本即可。


因此,只需要在package.json文件中scripts命令下添加一行代码即可。


"build:deploy": "vue-cli-service build && node deploy.js"

这段代码因不同的框架版本可能有所不同,只需要在普通npm run build执行内容后面拼接node deploy.jsdeploy.js就是我们所写的脚本文件。


打包完之后,上传文件过程如图所示,非常丝滑:


企业微信截图_17006194685935.png


此时会有一个问题,打包的dist文件每次上传至服务器时,dist文件一直被覆盖,无法实现按版本回滚。


只需要在上传之前,修改服务器旧的dist文件名,这样旧版本就得以保存。


// 判断服务器dist文件是否存在
let newDistExist = await ssh.execCommand(`ls 旧版本`)
while(newDistExist.code === 0) {
i ++
newDistExits = await ssh.execCommand(`ls 旧版本i`)
}
// 重命名旧版本dist文件
await ssh.execCommand(`mv 旧版本 新版本`)

此时即可实现旧版本保存,以便可以按版本实现回滚操作。


上述过程仅仅是针对个人打包上传服务器,如果想要实现协同开发,则要实现代码共享(例如上传git仓库),为了安全性,账号密码不能以明文暴露。


可以采取以下三种方案,当然没有绝对意义上的安全。



  1. terminal实现账号密码输入


可以利用password-prompt插件,它可以帮助你在命令行中以安全的方式提示用户输入密码。


const getUserInfo = async() => {
const username = await passwordPrompt('输入用户名:')
const password = await passwordPrompt('输入密码:', { method: 'hide' })
return { username, password }
}

将输入的usernamepassword传到远程服务器进行校验即可。



  1. 账号密码加密


由于代码要上传git,所以可以在git上传前进行加密处理,调用gitpre-commit钩子,执行加密,每次pull时候进行解密处理,调用post-merge钩子,调用post-merge钩子时候仅仅需要输入解密口令即可。解密口令只需要做到组员共享即可,此时的解密口令同样可以借助password-prompt插件进行输入。相对第一种方案,输入的内容更少了😂。


const crypto = require('crypto') // 密钥和加密算法 
const secretKey = 'your-secret-key' // 这里替换为你自己的密钥
const algorithm = 'aes-256-cbc' // 使用的加密算法
function decryptData(encryptedData) {
const decipher = crypto.createDecipher(algorithm, secretKey)
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8')
decryptedData += decipher.final('utf8')
return decryptedData
}
// 从环境变量或其他安全方式获取加密的敏感信息
const encryptedInfo = process.env.ENCRYPTED_INFO // 这里假设加密的信息存储在环境变量中
// 解密敏感信息
const decryptedInfo = decryptData(encryptedInfo)
// 在这里使用解密后的敏感信息进行后续操作
console.log('Decrypted info:', decryptedInfo)

git的钩子函数可以采用git hooks工具哈士奇(husky)进行配置,具体配置方式不再赘述。


以上就是本文在前端半自动化部署方面的探索,大家可以贡献自己的想法/做法呀!


作者:一颗多愁善感的派大星j
来源:juejin.cn/post/7303862023618805795
收起阅读 »

Android 自定义理化表达式View

一、前言 在 Android 中实现上下标我们一般使用 SpannableString 去完成,需要计算开始位置和结束位置,也要设置各种 Span,而且动态性不是很好,因为无法做到规则统一约束,因此有必要进行专有规则设定,提高代码使用的灵活程度。 当然,也有很...
继续阅读 »

一、前言


在 Android 中实现上下标我们一般使用 SpannableString 去完成,需要计算开始位置和结束位置,也要设置各种 Span,而且动态性不是很好,因为无法做到规则统一约束,因此有必要进行专有规则设定,提高代码使用的灵活程度。


当然,也有很多开源的项目,但是对于简单的数学和化学表达式,大多都缺少通用性,仅限于项目本身使用,这也是本篇实现的主要目的之一。对于其他类型如求和公式、平方根公式、分子分母其实也可以通过本篇的思想,进行一些列改造即可,当然也可以借助语法树,实现自己的公式编辑器。



二、效果预览



三、实现


实现其实很简单,本身就是借助Canvas#drawTextXXX实现,但是我们这里仍然需要回顾的问题是字体测量和基线计算问题。


3.1 字体测量


常用的宽高测量如下


        //获取文本最小宽度(真实宽度)
private static int getTextRealWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.width();
}

//获取文本最小高度(真实高度)
private static int getTextRealHeight(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.height();
}

//真实宽度 + 笔画左右两侧间隙(一般绘制的的时候建议使用这种,左右两侧的间隙和字形有关)
private static int getTextWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
return (int) paint.measureText(text);
}

//真实宽度 + 笔画上下两侧间隙(符合文本绘制基线)
private static int getTextHeight(Paint paint) {
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int textHeight = ~fm.top - (~fm.top - ~fm.ascent) - (fm.bottom - fm.descent);
return textHeight;
}

3.2基线计算


在Canvas 绘制,实际上Html中的Canvas一样都需要计算意义,因为文字的受到不同文化的影响,表现形式不同,另外音标等问题存在,所以使用基线来绘制更合理。



推导算法如下


       /**
* 基线到中线的距离=(Descent+Ascent)/2-Descent
* 注意,实际获取到的Ascent是负数。公式推导过程如下:
* 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
*/

public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}

3.3 全部代码


public class MathExpressTextView extends View {

private final List<TextInfo> TEXT_INFOS = new ArrayList<>();
private int textSpace = 15;
private String TAG = "MathExpressTextView";
protected Paint mTextPaint;
protected Paint mSubTextPaint;
protected Paint mMarkTextPaint;
protected float mContentWidth = 0f;
protected float mContentHeight = 0f;
protected float mMaxSize = 0;


public void setMaxTextSize(float sizePx) {
mMaxSize = sizePx;

mTextPaint.setTextSize(mMaxSize);
mSubTextPaint.setTextSize(mMaxSize / 3f);
mMarkTextPaint.setTextSize(mMaxSize / 2f);

invalidate();
}

public void setTextSpace(int textSpace) {
this.textSpace = textSpace;
}

public void setContentHeight(float height) {
mContentHeight = height;
invalidate();
}

public MathExpressTextView(Context context){
this(context,null);
}
public MathExpressTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
setEditDesignTextInfos();
}

public MathExpressTextView setText(String text, String subText, String supText, float space) {
this.TEXT_INFOS.clear();
TextInfo.Builder tb = new TextInfo.Builder(text, mTextPaint, mSubTextPaint)
.subText(subText)
.supText(supText)
.textSpace(space);

this.TEXT_INFOS.add(tb.build());
return this;
}

public MathExpressTextView appendMarkText(String text) {
TextInfo.Builder tb = new TextInfo.Builder(text, mMarkTextPaint, mMarkTextPaint);
this.TEXT_INFOS.add(tb.build());
return this;
}

public MathExpressTextView appendText(String text, String subText, String supText, float space) {
TextInfo.Builder tb = new TextInfo.Builder(text, mTextPaint, mSubTextPaint)
.subText(subText)
.supText(supText)
.textSpace(space);

this.TEXT_INFOS.add(tb.build());
return this;
}

private void setEditDesignTextInfos() {

if (!isInEditMode()) return;
// setText("2H", "2", "", 10)
// .appendMarkText("+");
// appendText("O", "2", "", 10);
// appendMarkText("=");
// appendText("2H", "2", "", 10);
// appendText("O", "", "", 10);

// setText("sin(Θ+α)", "", "", 10)
// .appendMarkText("=");
// appendText("sinΘcosα", "", "", 10);
// appendMarkText("+");
// appendText("cosΘsinα", "", "", 10);

setText("cos2Θ", "1", "", 10)
.appendMarkText("=");
appendText("cos", "", "2", 10);
appendText("Θ", "1", "", 10);
appendMarkText("-");
appendText("sin", "", "2", 10);
appendText("Θ", "1", "", 10);

}
public Paint getTextPaint() {
return mTextPaint;
}

public Paint getSubTextPaint() {
return mSubTextPaint;
}

public Paint getMarkTextPaint() {
return mMarkTextPaint;
}

private float dpTopx(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}


private void init() {

mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setAntiAlias(true);
mTextPaint.setStyle(Paint.Style.STROKE);

mMarkTextPaint = new Paint();
mMarkTextPaint.setColor(Color.WHITE);
mMarkTextPaint.setAntiAlias(true);
mMarkTextPaint.setStyle(Paint.Style.STROKE);

mSubTextPaint = new Paint();
mSubTextPaint.setColor(Color.WHITE);
mSubTextPaint.setAntiAlias(true);
mSubTextPaint.setStyle(Paint.Style.STROKE);

setMaxTextSize(dpTopx(30));

}


private void setSubTextShader() {
if (this.colors != null) {
float textHeight = mSubTextPaint.descent() - mSubTextPaint.ascent();
float textOffset = (textHeight / 2) - mSubTextPaint.descent();
Rect bounds = new Rect();
mSubTextPaint.getTextBounds("%", 0, 1, bounds);
mSubTextPaint.setShader(new LinearGradient(0, mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f, 0,
mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f - bounds.height(), colors, positions, Shader.TileMode.CLAMP));
} else {
mSubTextPaint.setShader(null);
}

}

private void setMarTextShader() {
if (this.colors != null) {
float textHeight = mMarkTextPaint.descent() - mMarkTextPaint.ascent();
float textOffset = (textHeight / 2) - mMarkTextPaint.descent();
Rect bounds = new Rect();
mMarkTextPaint.getTextBounds("%", 0, 1, bounds);
mMarkTextPaint.setShader(new LinearGradient(0, mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f, 0,
mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f - bounds.height(), colors, positions, Shader.TileMode.CLAMP));
} else {
mMarkTextPaint.setShader(null);
}

}

private void setTextShader() {
if (this.colors != null) {
float textHeight = mTextPaint.descent() - mTextPaint.ascent();
float textOffset = (textHeight / 2) - mTextPaint.descent();
Rect bounds = new Rect();
mTextPaint.getTextBounds("A", 0, 1, bounds);
mTextPaint.setShader(new LinearGradient(0, mContentHeight / 2 + textOffset, 0, mContentHeight / 2 + textOffset - bounds.height(), colors, positions, Shader.TileMode.CLAMP));
} else {
mTextPaint.setShader(null);
}
}


public void setColor(int unitColor, int numColor) {
mSubTextPaint.setColor(unitColor);
mTextPaint.setColor(numColor);
}

RectF contentRect = new RectF();

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

if (mContentWidth <= 0) {
mContentWidth = getWidth();
}
if (mContentHeight <= 0) {
mContentHeight = getHeight();
}

if (mContentWidth == 0 || mContentHeight == 0) return;

setTextShader();
setSubTextShader();
setMarTextShader();

if (TEXT_INFOS.size() == 0) return;

int width = getWidth();
int height = getHeight();


contentRect.left = (width - mContentWidth) / 2f;
contentRect.top = (height - mContentHeight) / 2f;
contentRect.right = contentRect.left + mContentWidth;
contentRect.bottom = contentRect.top + mContentHeight;

int id = canvas.save();
float centerX = contentRect.centerX();
float centerY = contentRect.centerY();
canvas.translate(centerX, centerY);

contentRect.left = -centerX;
contentRect.right = centerX;
contentRect.top = -centerY;
contentRect.bottom = centerY;


float totalTextWidth = 0l;
int textCount = TEXT_INFOS.size();

for (int i = 0; i < textCount; i++) {
totalTextWidth += TEXT_INFOS.get(i).getTextWidth();
if (i < textCount - 1) {
totalTextWidth += textSpace;
}
}

drawGuideBaseline(canvas, contentRect, totalTextWidth);

float startOffsetX = -(totalTextWidth) / 2f;
for (int i = 0; i < textCount; i++) {
TEXT_INFOS.get(i).draw(canvas, startOffsetX, contentRect.centerY());
startOffsetX += TEXT_INFOS.get(i).getTextWidth() + textSpace;
}

canvas.restoreToCount(id);

}

private void drawGuideBaseline(Canvas canvas, RectF contentRect, float totalTextWidth) {

if (!isInEditMode()) return;

Paint guidelinePaint = new Paint();
guidelinePaint.setAntiAlias(true);
guidelinePaint.setStrokeWidth(0);
guidelinePaint.setStyle(Paint.Style.FILL);

RectF hline = new RectF();
hline.top = -1;
hline.bottom = 1;
hline.left = -totalTextWidth / 2;
hline.right = totalTextWidth / 2;
canvas.drawRect(hline, guidelinePaint);

RectF vline = new RectF();
hline.left = -1;
vline.top = contentRect.top;
vline.bottom = contentRect.bottom;
vline.right = 1;

canvas.drawRect(vline, guidelinePaint);
}


private static class TextInfo {
Paint subOrSupTextPaint = null;
String subText = null;
String supText = null;
Paint textPaint = null;
String text;
float space;

private TextInfo(String text, String subText, String supText, Paint textPaint, Paint subOrSupTextPaint, float space) {
this.text = text;
if (this.text == null) {
this.text = "";
}
this.subText = subText;
this.supText = supText;
this.space = space;
this.textPaint = textPaint;
this.subOrSupTextPaint = subOrSupTextPaint;
}

public void draw(Canvas canvas, float startX, float startY) {

if (this.textPaint == null) {
return;
}

canvas.drawText(this.text, startX, startY + getTextPaintBaseline(this.textPaint), this.textPaint);

if (this.subOrSupTextPaint == null) {
return;
}
if (this.supText != null) {
RectF rect = new RectF();
rect.left = startX + space + getTextWidth(this.text, this.textPaint);
rect.top = -getTextHeight(this.textPaint) / 2;
rect.bottom = 0;
rect.right = rect.left + getTextWidth(supText, this.subOrSupTextPaint);
canvas.drawText(supText, rect.left, rect.centerY() + getTextPaintBaseline(this.subOrSupTextPaint), this.subOrSupTextPaint);
}


if (this.subText != null) {
RectF rect = new RectF();
rect.left = startX + space + getTextWidth(this.text, this.textPaint);
rect.top = 0;
rect.bottom = getTextHeight(this.textPaint) / 2;
rect.right = rect.left + getTextWidth(subText, this.subOrSupTextPaint);
canvas.drawText(subText, rect.left, rect.centerY() + getTextPaintBaseline(this.subOrSupTextPaint), this.subOrSupTextPaint);
}

}

/**
* 基线到中线的距离=(Descent+Ascent)/2-Descent
* 注意,实际获取到的Ascent是负数。公式推导过程如下:
* 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
*/

public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}


public float getTextWidth() {

if (textPaint == null) {
return 0;
}

float width = 0;

width = getTextWidth(this.text, textPaint);

float subTextWidth = 0;
if (this.subText != null && subOrSupTextPaint != null) {
subTextWidth = getTextWidth(this.subText, subOrSupTextPaint) + space;
}

float supTextWidth = 0;
if (this.supText != null && subOrSupTextPaint != null) {
supTextWidth = getTextWidth(this.supText, subOrSupTextPaint) + space;
}
return width + Math.max(subTextWidth, supTextWidth);
}


//获取文本最小宽度(真实宽度)
private static int getTextRealWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.width();
}

//获取文本最小高度(真实高度)
private static int getTextRealHeight(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.height();
}

//真实宽度 + 笔画左右两侧间隙(一般绘制的的时候建议使用这种,左右两侧的间隙和字形有关)
private static int getTextWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
return (int) paint.measureText(text);
}

//真实宽度 + 笔画上下两侧间隙(符合文本绘制基线)
private static int getTextHeight(Paint paint) {
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int textHeight = ~fm.top - (~fm.top - ~fm.ascent) - (fm.bottom - fm.descent);
return textHeight;
}


private static class Builder {

Paint subOrSupTextPaint = null;
Paint textPaint = null;
String subText = null;
String supText = null;
String text;
float space;

public Builder(String text, Paint textPaint, Paint subOrSupTextPaint) {
this.text = text;
this.textPaint = textPaint;
this.subOrSupTextPaint = subOrSupTextPaint;
}

public Builder subText(String subText) {
this.subText = subText;
return this;
}

public Builder supText(String supText) {
this.supText = supText;
return this;
}

public Builder textSpace(float space) {
this.space = space;
return this;
}

public TextInfo build() {
return new TextInfo(text, this.subText, this.supText, this.textPaint, this.subOrSupTextPaint, this.space);
}
}

}
private int[] colors = new int[]{
0xC0FFFFFF, 0x9fFFFFFF,
0x98FFFFFF, 0xA5FFFFFF,
0xB3FFFFFF, 0xBEFFFFFF,
0xCCFFFFFF, 0xD8FFFFFF,
0xE5FFFFFF, 0xFFFFFFFF};
private float[] positions = new float[]{
0f, 0.05f,
0.3f, 0.4f,
0.5f, 0.6f,
0.7f, 0.8f,
0.9f, 1f};

public void setShaderColors(int[] colors) {
this.colors = colors;
}

public void setShaderColors(int c) {
this.colors = new int[]{c, c, c, c,
c, c, c, c, c, c};
}

}

3.4 使用


        MathExpressTextView m1 = findViewById(R.id.math_exp_1);
MathExpressTextView m2 = findViewById(R.id.math_exp_2);
MathExpressTextView m3 = findViewById(R.id.math_exp_3);
MathExpressTextView m4 = findViewById(R.id.math_exp_4);


m1.setShaderColors(0xffFF4081);
m1.setText("2H","2","",10)
.appendMarkText("+")
.appendText("O","2","",10)
.appendMarkText("
=")
.appendText("
2H","2","",10)
.appendText("
O","","",10);

m2.setShaderColors(0xffff9922);
m2.setText("
2","","2",10)
.appendMarkText("
+")
.appendText("
5","","-1",10)
.appendMarkText("
=")
.appendText("
4.2","","",10);

m3.setShaderColors(0xffFFEAC4);
m3.setText("
H","2","0",10)
.appendMarkText("
+")
.appendText("
Cu","","+2",10)
.appendText("
O","","-2",10)
.appendMarkText("
==")
.appendText("
Cu","","0",10)
.appendText("
H","2","+1",10)
.appendText("
O","","-2",10);


m4.setText("
985","","GB",10)
.appendMarkText("
+")
.appendText("
211","","MB",10);

四、总结


相对来说本篇相对简单,没有过多复杂的计算。但是对于打算实现公式编辑器的项目,可参考本方案的设计思想:



  • 组合化:通过大公式,组合小公式,这样也方便使用语法树,提高通用性。

  • 对象化:单独描述单独片段

  • 规则化:对不同的片段进行规则化绘制,如appendMarkText方法


作者:时光少年
来源:juejin.cn/post/7303792111719792666
收起阅读 »

极简原生js图形验证码

web
       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。 示例: 思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑...
继续阅读 »

       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。


示例:


1700643433840.png



思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑登录出错刷新,此处只做样式不写进去)
实现过程为1.先写一个canvas标签做绘图容器。    →     2.将拿到的值绘制到容器中并写好样式。    →     3.点击刷新重新绘制。



写一个canvas标签当容器


<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>

并设置容器宽高背景颜色或图片等样式


写一个数值绘制到canvas的方法


//text为传递的数值
function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

拿到数值调用绘制方法



此处为样式示例,因此数值我用4位随机数表示,实际情况为你从后端取得的值,并依靠这个值在后端判断验证码是否一致。



// 调用函数生成验证码并显示在页面上  
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });

监听标签点击实现点击刷新



此处要注意一定要先清空canvas中已绘制图像再渲染新数值,因此直接将清除范围设置较大。



 // 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

最后实现效果:


1700645148990.png


完整代码演示


<!DOCTYPE html>
<html>

<head>
<title>String to Captcha</title>
</head>

<body>
<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>


<script>
// 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
</script>
</body>

</html>

作者:方苕爱吃瓜
来源:juejin.cn/post/7304182005285830693
收起阅读 »

数据库优化之:like %xxx%该如何优化?

今天给大家分享一个小知识,实际项目中,like %xxx%的情况其实挺多的,比如某个表单如果支持根据公司名进行搜索,用户一般都是输入湖南xxx有限公司中的xxx进行搜索,所以对于接口而言,就必须使用like %xxx%来支持,从而不符合最左前缀原则导致索引失效...
继续阅读 »

今天给大家分享一个小知识,实际项目中,like %xxx%的情况其实挺多的,比如某个表单如果支持根据公司名进行搜索,用户一般都是输入湖南xxx有限公司中的xxx进行搜索,所以对于接口而言,就必须使用like %xxx%来支持,从而不符合最左前缀原则导致索引失效,那么该如何优化这种情况呢?


第一种可以尝试的方案就是利用索引条件下推,我先演示再讲原理,比如我有下面一张订单表:


就算给company_name创建一个索引,执行where company_name like '%腾讯%'也不会走索引。


但是如果给created_at, company_name创建一个联合索引,那么执行where created_at=CURDATE() and company_name like '%腾讯%'就会走联合索引,并且company_name like '%腾讯%'就会利用到索引条件下推机制,比如下图中Extra里的Using index condition就表示利用了索引条件下推。


所以,并不是like %xxx%就一定会导致索引失效,原理也可以配合其他字段一起来建联合索引,从而使用到索引条件下推机制。


再来简单分析一下索引条件下推的原理,在执行查询时先利用SQL中所提供的created_at条件在联合索引B+树中进行快速查找,匹配到所有符合created_at条件的B+树叶子节点后,再根据company_name条件进行过滤,然后再根据过滤之后的结果中的主键ID进行回表找到其他字段(回表),最终才返回结果,这样处理的好处是能够减少回表的次数,从而提高查询效率。


当然,如果实在不能建立或不方便建立联合索引,导致不能利用索引条件下推机制,那么其实可以先试试Mysql中的全文索引,最后才考虑引入ES等中间件,当然Mysql其他一些常规优化机制也是可以先考虑的,比如分页、索引覆盖(不select *)等。


作者:爱读源码的大都督
来源:juejin.cn/post/7301955975337738279
收起阅读 »

让Android开发Demo页面变得简单起来

Github: github.com/eekidu/devl… DevLayout DevLayout支持使用代码的方式,快速添加常用调试控件,无需XML,简化调试页面开发过程 背景 我们在开发组件库的时候,通常会开发一个Demo页面,用于展示或者调试该组件库...
继续阅读 »

Screenshot_20231122_163444.png


Github: github.com/eekidu/devl…


DevLayout


DevLayout支持使用代码的方式,快速添加常用调试控件,无需XML,简化调试页面开发过程


背景


我们在开发组件库的时候,通常会开发一个Demo页面,用于展示或者调试该组件库。
这种页面对UI的美观度要求很低,注重的是快速实现

使用XML布局方式开发会比较繁琐,该库会简化这一页面UI的开发流程:



  • 对常用的控件进行了封装,可以通过调用DevLayout的方法进行创建;

  • 并按流式布局或者线性布局的方式摆放到DevLayout中。


image.png

引入依赖


在Project的build.gradle在添加以下代码


allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

在Module的build.gradle在添加以下代码


最新版本:


implementation 'com.github.eekidu:devlayout:Tag'

使用


DevLayout是一个ViewGr0up,你可以把它摆放到页面上合适的位置,然后通过调用它的方法来添加需要子控件。


//1、创建或者获取一个DevLaout实例
var mDevLayout = findViewById<DevLayout>(R.id.devLayout)


//2、调用方法添加调试控件

/**
* 添加功能按钮
*/

mDevLayout.addButton("功能1") {
//点击回调
}

/**
* 添加开关
*/

mDevLayout.addSwitch("开关1") { buttonView, isChecked ->
//状态切换回调
}

/**
* 添加SeekBar
*/

mDevLayout.addSeekBar("参数设置1") { progress ->
//进度回调
}.setMax(1000).setProgress(50).setEnableStep(true)//启用步进


/**
* 添加输入框
*/

mDevLayout.addEditor("参数设置") { inputText ->
textView.text = inputText
}

/**
* 单选,切换布局样式
*/

mDevLayout.addRadioGr0up("布局方式")
.addItem("流式布局") {
mDevLayout.setIsLineStyle(false)
}.addItem("线性布局") {
mDevLayout.setIsLineStyle(true)
}.setChecked(0)

/**
* 添加日志框
*/

mDevLayout.addLogMonitor()

/**
* 输出日志
*/

mDevLayout.log(msg)
mDevLayout.logI(msg)
mDevLayout.logD(msg)
mDevLayout.logW(msg)
mDevLayout.logE(msg)


/**
* 添加换行
*/

mDevLayout.br()
/**
* 添加分割线
*/

mDevLayout.hr()

//其他类型控件见Demo MainActivity.kt


耗时监控


我们调试代码一个重要的目的就是:发现耗时方法从而进行优化,DevLayout提供一个简易的耗时打印功能,实现如下:
大部分需要调试的代码,会在控件的回调中触发,那么对回调进行代理,在代理中监控原始回调的执行情况,就可以得到调试代码的执行耗时。


伪代码如下:


class ClickProxyListener(val realListener: OnClickListener) : OnClickListener {

override fun onClick(v: View) {
val startTime = Now()// 1、记录起始时间

realListener.onClick(v)//原始回调执行

val eTime = Now() - startTime//2、计算执行耗时
log("执行耗时:${eTime}")
}
}

//创建代理对象
val listenerProxy = ClickProxyListener(realListener)

由于控件种类很多,回调类的类型也都不一样,如何对形形色色的回调统一进行监控?


动态代理:封装了ProxyListener代理类,对原始回调进行代理


open class ProxyListener<T>(val realListener: T) : InvocationHandler {

override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any {
val startTime = Now()// 1、记录起始时间

val result = method.invoke(realListener, *(args ?: emptyArray()))//原始回调执行

val eTime = Now() - startTime//2、计算执行耗时
log("执行耗时:${eTime}")
return result
}
}

//动态创建代理对象
val listener = Proxy.newProxyInstance(_, listenerInterface , ProxyListener(realListener))

结合该例子感受动态代理的优点:




  • 灵活性:动态代理允许在运行时创建代理对象,而不需要在编译时指定具体的代理类。这使得代理对象可以根据需要动态地适应不同的接口和实现类。




  • 可扩展性:动态代理可以用于实现各种不同的功能,例如日志记录、性能监控、事务管理等。通过在代理对象的方法调用前后插入额外的逻辑,可以轻松地扩展现有的代码功能。




  • 解耦合:动态代理可以将代理逻辑与真实对象的实现逻辑分离。这样,代理对象可以独立于真实对象进行修改和维护,而不会影响到真实对象的代码。




  • 减少重复代码:通过使用动态代理,可以将一些通用的代码逻辑抽取到代理类中,从而减少代码的重复性。这样可以提高代码的可维护性和可读性。




  • 动态性:动态代理可以在运行时动态地创建代理对象,这意味着可以根据需要动态地修改代理对象的行为。这种灵活性使得动态代理在一些特定的场景下非常有用,例如AOP(面向切面编程)。




日志


日志是调试代码的重要方式,在某些场景下需要将日志输出到UI上,方便在设备没有连接Logcat,无法通过控制台监测日志时,也能对程序执行的中间过程或执行结果有一定的展示。


我们可以添加一个日志框到UI界面上,以此来展示Log信息,方式如下:


//添加日志框,默认尺寸,添加后也可以通过UI调整
mDevLayout.addLogMonitor()
mDevLayout.addLogMonitorSmall()
mDevLayout.addLogMonitorLarge()

//输出日志
mDevLayout.log(msg)
mDevLayout.logI(msg)
mDevLayout.logD(msg)
mDevLayout.logW(msg)
mDevLayout.logE(msg)

支持过滤:



  • 按等级过滤

  • 按关键词过滤,多关键字格式:key1,key2


同时,日志信息会在Logcat控制台输出,通过 tag:DevLayout 进行过滤查看。


image.png


最后


Github: github.com/eekidu/devl…


欢迎Star,如果有更好的优化方案,欢迎在github上提出,我们一起互相学习!


作者:Caohaikuan
来源:juejin.cn/post/7304182005285584933
收起阅读 »

软件著作权证书申请

大家好,我是小悟 对我们行业来说,软著有什么作用不言而喻。对于在读学生来说,可能对加学分、评奖学金、保研、简历装饰有帮助。对于企业来说,可能对高企申请、应用市场上架有帮助。对于职场人士来说,可能对职称评定、升职加薪有帮助。但最重要的一点是,能保护你的软件成果...
继续阅读 »

大家好,我是小悟


image.png


对我们行业来说,软著有什么作用不言而喻。对于在读学生来说,可能对加学分、评奖学金、保研、简历装饰有帮助。对于企业来说,可能对高企申请、应用市场上架有帮助。对于职场人士来说,可能对职称评定、升职加薪有帮助。但最重要的一点是,能保护你的软件成果。


关于这个软著模板,已经有很多小伙伴领取到了,能不能用,全在于小伙伴们自己,只要用心去做,我觉得总会成功。

有按照模板申请成功的


图片


也有觉得模板没用的


图片


甚至还有觉得没用爆粗口的,难听,这就不放图了,这种人就是想着不要发挥自己的一点点脑筋,就是想找个不用自己修改一丁点的,最好是直接就能用的。很无奈啊。


但大部分小伙伴是有礼貌的


图片


经过实践,自己已经申请过成功很多张软著证书了,积累了一定经验。


image.png


image.png


image.png


image.png


image.png


image.png


您的一键三连,是我更新的最大动力,谢谢


山水有相逢,来日皆可期,谢谢阅读,我们再会


我手中的金箍棒,上能通天,下能探海


作者:悟空码字
来源:juejin.cn/post/7303827467037442048
收起阅读 »

你的团队是“活”的吗?

最近有同学离职,让我突然思考一个话题。 之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水。 为什么团队要保持一定的人员流动性呢? “优”...
继续阅读 »

最近有同学离职,让我突然思考一个话题。


之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水


为什么团队要保持一定的人员流动性呢?



  • “优”胜“劣”汰。这里不是指恶意竞争和卷。而是通过一定的人员流动性,有进有出,从而找到更加适合团队的人。找到跟团队价值观一致的,志同道合的成员。而跟团队匹配度不是很高的人,可以去寻找更加适合自己的团队和岗位,这对于双方都是有好处的。

  • 激活团队。当一个团队保持稳定太久,就会有点思想固化,甚至落后了。这时候,需要通过一些新鲜血液,带来不同的思想和经验,来激活团队,这就像鲶鱼一样。


那想要形成一个“活”的团队,需要什么条件呢?



  • 薪资待遇要好。首先是基本福利待遇要高于业界平均水平。其次,绩效激励是有想象空间的。如果没有这个条件,那人员流动肯定是入不敷出的,优秀的人都被挖跑了。

  • 团队专业。团队在业界有一定的影响力,在某一方面的专业技术和产出保持业界领先。这个条件隐含了一个信息,就是团队所在业务是有挑战的,因为技术产出一般都是依赖于业务的,没有业务实践和验证,是做不出优秀的技术产出的。因此,待遇好、有技术成长、有职业发展空间,这三者是留住人才的主要手段。

  • 梯队完整。在有了前面 2 个条件之后,就有了吸引人才的核心资源了。那接下来就需要有一个完整的梯队。因为资源是有限的,团队资源只能分配到有限人手里,根据最经典的 361,待遇和职业发展空间最多只能覆盖 3 成,技术成长再多覆盖 3 成人已经不错了。那剩下的 4 成人怎么办?所以,团队需要有一些相对稳定的人,他们能完成安排的事情,不出错,也不需要他们卷起来。


这是我当前的想法,我想我还需要更多的经验和讨论的。


那我目前的团队是“活”的吗?答案是否定的。


首先,过去一年,公司的招聘被锁了,内部转岗也基本转不动。薪资待遇就更不用说了。整个环境到处都充斥着“躺”的氛围。


其次,团队专业度一般,在金融业务,前端的发挥空间极其有限。我也只能尽自己所能,帮大家寻求一些技术成长的空间,但还是很有限。


最后,梯队还没有完整,还在建设中,不过也是步履维艰。因为前两个条件限制,别说吸引优秀人才了,能不能保住都是个问题。


最近公司开始放开招聘了,但还不是大面积的,不过还是有希望可以给有人员流失的团队补充 hc 的。但比较难受的是,这个 hc 不是过我的手的,哈哈,又有种听天由命的感觉。


这就是我最近的一个随想,那么,你的团队是“活”的吗?


作者:潜龙在渊灬
来源:juejin.cn/post/7298347391164383259
收起阅读 »

像mysql一样查询ES,一看就会,爽歪歪

ElasticSearch是现在最流行的搜索引擎了,查询快,性能好。可能唯一的缺点就是查询的语法Query DSL(Domain Specific Language)比较难记,今天分享一个直接用sql查询ES的方法。 ::: 1.简介 先简单介绍一下这个s...
继续阅读 »

ElasticSearch是现在最流行的搜索引擎了,查询快,性能好。可能唯一的缺点就是查询的语法Query DSL(Domain Specific Language)比较难记,今天分享一个直接用sql查询ES的方法。 :::




1.简介


先简单介绍一下这个sql查询,因为社区一直反馈这个Query DSL 实在是太难用了。大家可以感受一下下面这个es的查询。


GET /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title""search" } },
        {
          "bool": {
            "should": [
              { "term": { "category""books" } },
              { "term": { "category""music" } }
            ]
          }
        }
      ],
      "filter": {
        "range": {
          "price": { "gte": 20, "lte": 100 }
        }
      }
    }
  },
  "aggs": {
    "avg_price_per_category": {
      "terms": {
        "field""category",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field""price"
          }
        }
      }
    }
  }
}

这个查询使用了bool查询来组合多个条件,包括must、should和filter。同时也包含了聚合(aggs)来计算不同类别的平均价格。对于业务查询来讲,这个查询很普通。但是还是很难理解,特别是对于新手来讲,更难记了,很容易出错。


如果是mysql的查询,就是这么写


SELECT title, category, price 
FROM my_index 
WHERE (title = 'search' AND (category = 'books' OR category = 'music')) 
AND price >= 20 AND price <= 100 
GR0UP BY category 
ORDER BY AVG(price) DESC 
LIMIT 10

mysql 的查询就很简洁明了,看起来更舒服,后续维护也更方便。


既然都是查询,为啥不兼容一下mysql的语法呢,像很多工具现在都是兼容mysql的语法,比如说hive,starrocks,flink等等,原因就是因为mysql的用户多,社区活跃。还有一个原因就是因为mysql的语法比较简单,容易理解。所以ElasticSearch 官方ElasticSearch 从 6.3.0 版本也开始支持 SQL 查询了,这就是一个喜大奔普的事情了,哈哈。



下面是官方的文档和介绍,大家可以看看 http://www.elastic.co/guide/en/el…


2.准备环境


大家在ES官网下载一下ES 启动就可以了,注意的是ES 需要JDK环境,然后就是需要在6.3.0以上的版本。 http://www.elastic.co/cn/download…



建议也下载一下kibana



我这边下载的是7.15.2版本


3.搞起


创建一个索引 my_index


PUT /my_index
{
  "mappings": {
    "properties": {
      "title": { "type""text" },
      "category": { "type""keyword" },
      "price": { "type""float" }
    }
  }
}

插入一些数据


POST /my_index/_doc/1
{
  "title""ES学习手册",
  "category""books",
  "price": 29.99
}

POST /my_index/_doc/2
{
  "title""on my way",
  "category""music",
  "price": 13.57
}

POST /my_index/_doc/3
{
  "title""Kibana中文笔记",
  "category""books",
  "price": 21.54
}

传统的查询所有


GET /my_index/_search
{
  
}

返回的是文档的格式


如果用sql 查询


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index"
}

返回的是类似数据库的表格形式,是不是写起来更舒服呢。



  1. 分页limit


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index limit 1"
}


和mysql 一样没啥,很简单。



  1. order by 排序


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index order by price desc"
}



  1. gr0up by 分组


POST /_sql?format=txt
{
  "query""SELECT category,count(1) FROM my_index group by category"
}



  1. SUM 求和


POST /_sql?format=txt
{
  "query""SELECT sum(price) FROM my_index"
}



  1. where


POST /_sql?format=txt
{
  "query": "SELECT * FROM my_index where price = '13.57'"
}


看看是不是支持时间的转换的处理,插入一些数据


POST /my_index/_doc/4
{
  "title""JAVA编程思想",
  "category""books",
  "price": 21.54,
  "create_date":"2023-11-18T12:00:00.123"
}

POST /my_index/_doc/5
{
  "title""Mysql操作手册",
  "category""books",
  "price": 21.54,
  "create_date":"2023-11-17T07:00:00.123"
}

时间转换为 yyyy-mm-dd 格式


POST /_sql?format=txt
{"query": "SELECT title, DATETIME_FORMAT(create_date, 'YYYY-MM-dd') date from my_index where category'books'" }


时间加减


POST /_sql?format=txt
{"query": "SELECT date_add('hour', 8,create_date) date from my_index where category'books'" }


字符串拆分


POST /_sql?format=txt
{
  "query""SELECT SUBSTRING(category, 1, 3) AS SubstringValue FROM my_index"
}


基本上mysql 能查的 es sql 也能查,以后查询ES 数据就很方便的,特别是对于做各种报表的查询。像这样。



一般对于这种报表,返回的数据都是差不多json数组的格式。而对于es sql,查询起来很方便


[
        {
            "data": "5",
            "axis": "总数"
        },
        {
            "data": "0",
            "axis": "待出库"
        },
        {
            "data": "0",
            "axis": "配送中"
        },
        {
            "data": "5",
            "axis": "已签收"
        },
        {
            "data": "0",
            "axis": "交易完成"
        },
        {
            "data": "0",
            "axis": "已取消"
        },
        {
            "data": "5",
            "axis": "销售"
        }

4.总结


ES SQL查询的优点还是很多的,值得学习。使用场景也很多



  1. 简单易学:ES SQL查询使用SQL语法,对于那些熟悉SQL语法的开发人员来说,学习ES SQL查询非常容易。

  2. 易于使用:ES SQL查询的语法简单,易于使用,尤其是对于那些不熟悉Query DSL语法的开发人员来说。

  3. 可读性强:ES SQL查询的语法结构清晰,易于阅读和理解。


5.最后附上相关链接


ES 官方下载

http://www.elastic.co/cn/download…


ES sql文档 http://www.elastic.co/guide/en/el…


作者:Yanyf765
来源:juejin.cn/post/7302308448581812258
收起阅读 »

独立开发月入1K的实践之路与可持续思考

梦想不止,CV不息。 在从事后端开发的这几年里,尝试过很多所谓的副业,企图在某一时刻实现财富自由,我相信这也是很多程序员的梦想。但程序员做出一款产品很容易,如果想要很多人用起来就可以说是痴人说梦了。第一就是不知道怎么推广,第二是抓不住客户的痛点,以程序员思维做...
继续阅读 »

梦想不止,CV不息。


在从事后端开发的这几年里,尝试过很多所谓的副业,企图在某一时刻实现财富自由,我相信这也是很多程序员的梦想。但程序员做出一款产品很容易,如果想要很多人用起来就可以说是痴人说梦了。第一就是不知道怎么推广,第二是抓不住客户的痛点,以程序员思维做出的产品,与市场格格不入。 中间试过公众号运营,做了个粉丝2W的号,但变现成了问题,就放弃了。也试过一些google chrome插件,抖音去水印小程序、小区信息发布,这些开发周期都在3天到1周,但苦于开发好之后没有人用,都一 一的放弃了。


很多次的试水,虽然做的东西不尽人意,但从另一方面来说,的确丰富了自己的技术栈。于是我开始思考,我可不可以用心的做一款产品,然后让人用起来,即使不盈利,如果自己的产品有人用,也算是很有成就感了。 


做什么? 在开发之前,我翻遍了论坛和博客,想要从中找到普通人的需求。中间发现很多帖子:独立开发者的三件套:todo、记账、笔记。很多人对此嗤之以鼻,但我想试一试todo。


先别喷,看看我的想法。


当你不知道做什么的时候,一定不要让自己闲着。todo的需求简单,开发周期极短,1周内可以完成上线,甚至还能更新几个迭代。 就这样,我翻了市面上的所有待办清单,对已有产品的待办逻辑进行了梳理,分析了差异点。结合我自己的办公需求,我决定做一个极简的todo,毕竟审美有限,功能多了肯定不好看。 独立开发不只要考虑开发,还要考虑开发后的运营。我开发出来之后,怎么能够快速投入到市场,让人用起来。


这时我发现了uTools,分析了一下uTools的用户量,用户特征,新插件下载使用率,以及审核发布机制,这些能够满足自己的期望值,就决定以uTools为依托,开发一款以element ui+springboot的待办应用,起名大气一点:超级待办!不管产品超不超级,先吸引人下载就好了。 


开发周期零零散散大概一周,就上线到uTools了。 



主要功能有任务的增删改查,可以快速拖拽,快速编辑、关联笔记、分类、待办的微信推送提醒。还注册了一个微信服务号,实现uTools超级待办与微信互通,随时查看自己的待办,目前通过该应用 公众号粉丝到400左右。




第一天下载量达到了50多,没有任何用户反馈。刚巧第二天,uTools发了新应用的推文,我的应用可能因为提交审核时间,排在推文的第一,下载量达到了300多,这时,各种用户反馈开始来了。用户量超出了自己的预期,于是我开始在反馈中做出功能的取舍,然后进行一版一版的迭代上线。 


从6.25上线到现在,一共迭代了55个版本,每个迭代用时大概30分钟-1个小时。 用户量满1000后,开始进行差异化付费模式,主打基础功能免费+个性化功能付费。从9月开始付费到现在,每月平均付费1K左右,并且在11月,我只迭代了3次,总用时20分钟。目前总下载量10596个,实际注册用户数4099个,日活114左右。 



后续规划: 


1、多思考,多动手,多充实技术栈。


2、尝试围绕几百条反馈,总结需求点,继续完善应用,开发收费点。 


3、尝试多做些其他类型的应用。 


一些思考: 


1、不要鄙视独立开发三件套,他是你入门独立开发的第一步,能够在一定程度上锻炼你的产品思维能力。 


2、开发之前需要考虑产品的全生命周期,包含时间成本、需求取舍、盈利预期,推广模式。


3、重视客户需求反馈,做出自己的判断,把反馈完美的融入到现有的产品中去。 


4、不要低估产品的可用性,即使你做个记事本,也是有人用的。 


5、苍蝇再小也是肉,苍蝇多了,肉也就多了。 


最后希望各位独立开发者盆满钵满,早日财富自由,躺赚斗金。 


有同学可能会关心这么小的服务,提供在线服务会不会连服务器成本都收不回来。其实有另一款盈利产品在支撑,目前每月4K左右,能够兼顾成本,该项目后续我再进行分享。


作者:探路
来源:juejin.cn/post/7303847314896175116
收起阅读 »

如何判断一个对象是否可以被回收

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。 首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以...
继续阅读 »

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。


首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以被回收呢?一般都是通过引用计数法,可达性算法。


引用计数法


对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效(改为引用其他对象,赋值为null,或者生命周期结束)则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象,一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。


public void f(){
Object a = new Object(); // 对象a引用计数为1
g(a);
// 退出g(a),对象b的生命周期结束,对象a引用计数为1
}// 退出f(), 对象a的生命周期结束,引用计数为0

public void g(Object a){
Object b = a; // 对象a引用计数为2
Object c = a; // 对象a引用计数为3
Object d = a; // 对象a引用计数为4
d = new Object(); // 对象a引用计数为3
c = null; // 对象a引用计数为2
}

引用计数法实现起来比较容易,但是存在一个严重的问题,那就是无法检测循环依赖。如下所示:


public class A{
public B b;
public A(){

}
}

public class A{
public A a;
public B(){

}
}

A a = new A(); // a的计数为1
B b = new B(); // b的计数为1
a.b = b; // b的计数为2
b.a = a; // a的计数为2
a = null; // a的计数为1
b = null; // b的计数为1

最终a,b的计数都为1,无法被识别为垃圾,所以无法被回收。


Python使用的就是引用计数算法,Python的垃圾回收机制,很大一部分是为了处理可能产生的循环引用,是对引用计数的补充。


虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。


可达性算法


介绍


Java最终并没有采用引用计数算法,JVM的主流垃圾回收器采取的是可达性分析算法。


我们把对象之间的引用关系用数据结构中的有向图来表示。图中的顶点表示对象。如果对象A中的变量引用了对象B,那么,我们便在对象A对应的顶点和对象B对应的顶点之间画一条有向边。


在有向图中,有一组特殊的顶点,叫做GC Roots。哪些对象可以作为GC Roots呢?



  1. 系统加载的类:rt.jar。

  2. JNI handles。

  3. 线程运行栈上所有引用,包括方法参数,创建的局部变量等。

  4. 已启动未停止的java线程。

  5. 已加载类的静态变量。

  6. 用于同步的监控,调用了对象的wait()/notify()/notifyAll()。


JVM以GC Roots为起点,遍历(深度优先遍历或广度优先遍历)整个图,可以遍历到的对象为可达对象,也叫做存活对象,遍历不到的对象为不可达对象,也叫做死亡对象。死亡对象会被虚拟机当做垃圾回收。


JVM实际上采用的是三色算法来遍历整个图的,遍历走过的路径被称为reference chain。



  • Black: 对象可达,且对象的所有引用都已经扫描了(“扫描”在可以理解成遍历过了或加入了待遍历的队列)

  • Gray: 对象可达,但对象的引用还没有扫描过(因此 Gray 对象可理解成在搜索队列里的元素)

  • White: 不可达对象或还没有扫描过的对象



引用级别


遍历到的对象一定会存活吗?事实上,JVM会根据对象A对对象B的引用强不强烈作出相应的回收措施。


基于此JVM根据引用关系的强烈,将引用关系分为四个等级:强引用,软引用,弱引用,虚幻引用。


强引用


类似Object obj = new Object() 这类的引用都属于强引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,只有在和GC Roots断绝关系时,才会被回收。


如果要对强引用进行垃圾回收,需要设置强引用对象为 null,或者让其超出对象的生命周期范围,则认为改对象不存在引用。类似obj = null;


参考代码:


public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

软引用


用于描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。可以使用SoftReference 类来实现软引用。


Object obj = new Object();
SoftReference<Object> softRef = new SoftReference(obj);

弱引用


也是用于描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。可以使用WeakReference 类来实现弱引用。


Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
obj = null;
System.gc();
TimeUnit.SECONDS.sleep(200);
System.out.println(weakReference.get());
System.out.println(weakReference.isEnqueued());

虚引用


它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置一个虚引用关联的唯一目的是能在这个对象被垃圾回收时收到一个系统通知。可以通过PhantomReference 来实现虚引用。


Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, refQueue);
System.out.println(phantomReference.get());
System.out.println(phantomReference.isEnqueued());

基于虚引用,有一个更加优雅的实现方式,那就是Java 9以后新加入的Cleaner,用来替代Object类的finalizer方法。


STW


虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。我们把运行应用程序的线程叫做用户线程,把执行垃圾回收的线程叫做垃圾回收线程,如果在执行垃圾回收线程的同时还在执行用户线程,那么对象的引用关系可能会在垃圾回收途中被用户线程修改,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)


误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存,导致程序出错。


为了解决漏报的问题,保证垃圾回收线程不会被用户线程打扰,最简单粗暴的方式就是在垃圾回收的过程中,暂停用户线程,直到垃圾回收结束,再恢复用户线程,这就是STW(STOP THE WORLD)。


但是如果STW的时间过程,就会严重影响程序的性能,因此优化垃圾回收过程,尽量减少STW的时间,是垃圾回收器努力优化的方向,


安全点


上述除了STW的响应时间的问题,还有另外一个问题,就是如何从一个正确的状态停止,再从这个状态正确恢复。Java虚拟机中的STW是通过安全点(safepoint)机制来实现的。当Java虚拟机收到STW请求,它便会等待所有的线程都到达安全点,才允许请求Stop-the-world的线程进行独占的工作。


当然,安全点的初始目的并不是让用户线程立刻停下,而是找到一个稳定的执行状态。在这个执行状态下,JVM的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析,才能找到完整GC Roots。


是不是所有的用户线程在垃圾回收的时候都要停止呢?实际上,JVM也做了优化,如果某个线程处于安全区(不会改变对象引用关系的一段连续的代码区间),那么这个线程不需要停止,可以和垃圾回收线程并行执行。一旦离开安全区,JVM会检查是否处于STW阶段,如果是,则需要阻塞该线程,等垃圾回收完再恢复。


作者:Shawn_Shawn
来源:juejin.cn/post/7304181581303447589
收起阅读 »

被约谈,两天走人,一些思考

五档尼卡引爆全场 前言 个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流 来龙去脉 上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎...
继续阅读 »

五档尼卡引爆全场



前言


个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流


来龙去脉


上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎有什么事发生


微信上问了一句:什么情况?


前端同事:裁员,做好准备



公司现状


从我去年入职公司后,就在喊降本增效了,周一晨会时不时也会提一下降本增效,毕竟大环境不好,公司的业务这两年也受到不小的影响


今年好几个项目组人手不够,两三月前还在疯狂面试前后端测试产品,我们这边的业务线前端都面试都超过五十个人了,最后招了一个前端一个后端一个测试


想着这种情况,公司薪资给的也不高,新员工不大量招就算降本了吧,再优化优化各方面流程等提提效率,没想到降本的大刀直接落下来首先砍掉的是技术开发人员


裁员情况


公司北京总部这边,目前我们部门是裁了两个前端一个后端,其他部门也有有裁员,人数岗位就不清楚了


从被裁掉的同事那边了解到的消息,上周三下班后下午找他们谈的,周四交接,周五下班后就走了,按照法律规定赔偿


上周只是一个开始,应该是边裁边看,什么时候结束最终裁员比例目前还不清楚,由其他来源得到的消息来源说是这次裁员力度很大


现在如果不是核心项目员工,如果不是和领导关系比较好的员工,每个人头上都悬着一把达摩克利斯之剑


个人思考


看待裁员


我认为首先是放平心态吧


国际经济形去全球化,贸易战,疫情,到现在的各种制裁,俄乌战争等,极端气候频发,真是多灾多难的年代


裁员这几年大家也见多了,该来的总会来


我认为裁员好说,正常赔偿就行,好聚好散,江湖再见


企业层面


裁员也是企业激发组织活力的一种方式,正常看待就行,关于企业组织活力介绍的,这里推荐一本前段时间刚读完的一本书 《熵减:华为活力之源》



熵是来源于物理科学热力学第二定律的概念,热力学第二定律又称熵增定律。熵增表现为功能减弱直到逐渐丧失,而熵减表现为功能增强...



个人层面


1.如果公司正常走法律流程,拿赔偿走人,继续找工作,找工作的过程也能发现自己的不错,更加了解市场,甚至倒逼自己成长


2.如果公司只想着降低成本,不做人事,有那种玩下三滥手段的公司,一定要留好证据,拍照,录音,截图,保存到自己的手机或者云盘里,不想给赔偿或恶意玩弄手段的,果断仲裁,我们员工相对企业来讲是弱势群体,这时候要学会用法律武器保护自己(可能也是唯一的武器)



这年头行情不好,老板损失的可能只是近期收益,有的员工失去的可能是全家活下去的希望



日常准备


做好记录


日常自己工作上的重大成果,最好定期梳理一下,或者定期更新简历,也可以不更新简历,找地方记录下来,例如项目上的某个重大模块的开发升级,或者做的技术上的性能优化等,我是有写笔记博客的习惯,技术相关的有时间一般会写成文章发到社区里


保持学习


日常保持学习的基本状态,这个可能我们每个人都会有这个想法,但是能定期沉下心来去学习提升,系统地去提升自己的时候,很少能坚持下来,万事开头难,开头了以后剩下的是坚持,我自己也是,有些事情经常三天打鱼,两天晒网,一起加油


关注公司


如果公司有查考勤,或者重点强调考勤了,一般都是有动作了,我们公司这次就是,年中会后的第二周吧,大部门通报考勤情况,里面迟到的还有排名,没多久就裁员了


保护自己


有的公司可能流程操作不规范,也有的可能不想赔偿或者少赔偿,可能会在考勤上做文章,例如迟到啥的,如果公司有效益不好的苗头,一定要注意自己这方面的考勤,以及自己的绩效等,做好加班考勤截图,领导HR与自己的谈话做好录音,录屏等,后面可能用的上,也可能会让自己多一点点谈判筹码


经营关系


虽然裁员明面上都是根据工作表现来的,好多时候大家表现都差不多,这个时候就看人缘了,和领导关系好的,一般都不是优先裁员对象,和领导团队成员打成一片真的很重要



以前我还有过那种想法:


我一个做技术的,我认真做好我自己的工作不就行了?专心研究技术,经过多年的工作发现,很多时候真的不行,我们不是做的那种科研类的,只有自己能搞,国内的大部分软件开发岗可能都是用的开源的技术做业务相关的,这种没什么技术难度,技术上来看基本没有什么替代性的难度


可能可替代性比较难的就是某个技术人长期负责的某个大模块,然后写了一堆屎山吧,毕竟拉屎容易,吃屎难


越是优秀的代码,可读性越强,简洁优雅,像诗一样



关于简历


如果是刚毕业的,可能简历上还好,大部分都优化都是已经是有一定的工作经验了,简历的更新就比较重要了,尤其工作了两三年了,如果简历看起来内容很少,不是那么丰富或者看起来很简陋,在简历筛选这一关会降低自己的面试几率,这时候一定要丰富一下,也有一些可能不知道自己简历是否丰富的,网上有那种简历模板可以搜搜看看,也可以找大佬帮忙看看,也有技术圈提供简历优化的有偿服务


再找工作


我个人的感觉是如果还是继续老本行继续打工,这年头行情不好,最好第一时间找工作,不能因为拿了赔偿就想着休一个月再说之类的,我周围有那种本来准备休半个月或一个月的,结果一下子休了一年以上的,我面试的时候如果遇到那种空窗期很长的,如果第一轮技术面能力都差不多的情况,到第二轮的领导面或者HR面,他们有优先考虑让空窗期短的人加入


关于空窗期


基本所有的公司都会关注离职空窗期,如果这个空窗期时间长了,那么求职的竞争力会越来越小,我在面试的时候我也会比较关注空窗期,因为我会有如下思考(举个例子,纯属乐子哈)


1.为什么这个人求职者三个月多了不找工作,家里有矿?家里有矿还上班,工作不会是找个地方打发时间的吧



我朋友的朋友就是这样,北京土著,家中独子,前几年拆迁了,家里好几套房,自己开俩车,人家上班就是找地方交个社保,顺便打发一下时间




2.能力不行吗?找工作这么久都没找到,是太菜了吗?还是太挑剔了?长时间不敲代码,手也生疏了,来我们团队行不行呀,我们这里赶项目压力这么大,招进来万一上手一段时间干不了怎么办,自己还被牵连了



几年前在某家公司做团队leader的时候,我们做的又是AI类项目,用的技术也比较前沿,当时AI的生态还不完善,国内做AI的大部分还处于摸索阶段,项目中用的相关技术栈也没个中文文档,由于公司创业公司,价格给的很低,高手招不进来,没办法只能画饼招综合感觉不错的那种,结果好几个人来了以后又是培训,又是有把手把手教的,结果干了没多久干不动走了,留下的烂摊子还得自己处理



关于社保


如果自己家里没矿,最好还是别让社保断了,拿北京举例,社保断了影响医疗报销,影响买车摇号等等


如果实在没找到工作,又马上要断缴社保了,可以找个第三方机构帮忙代缴,几千块钱,这时候的社保补缴相对来讲代价就比较高了



我遇到的情况是,社保断了一个月,后来找到工作了,第三方机构补缴都补不了,后来一通折腾总算弄补缴上了



关于入职


先拿offer,每一家公司的面试都认真对待,抱着一颗交流开放互相尊重的心


如果自己跳槽频繁,再找公司,可能需要考虑一下自己是否能够长待了,跳槽越频繁,后面找工作越困难,没有哪个公司希望招的人干一年就走了


所以面试结束后,最好根据需要面试情况,以及网上找到的资料,分析一下公司的业务模式了,分析这家公司的行业地位,加入的业务线或者部门是否赚钱,所在的团队在公司属于什么情况,分析团队是否是边缘部门,招聘的业务线是否核心业务线,如果不是核心业务线,可能过段时间效益不好还会被砍掉,有时候虽然看拿了对应的赔偿,但是再找工作,与其他同级选手对比的话,竞争力会越来越低


不论是技术面试官,还是负责面试的HR,大部分也都是公司的普通员工,他们可能不会为公司考虑,基本都会为自己考虑的,万一招了个瘟神到公司或者团队里,没多久自己团队也解散了怎么整



这里也许迷信了,基于我的一些经历来看有些人确实会有一些人是看风水,看人分析运势的


之前在创业公司的时候,有幸和一些投资人,上市公司的总裁,央企董事长等所谓的社会高层接触过,越是那些顶级圈里的人,有些人似乎很看中这个,他们有人研究周易,有人信仰佛教,有人招聘必须看人面相,有人师从南怀瑾等等



再次强调


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步


作者:草帽lufei
来源:juejin.cn/post/7264236820725366840
收起阅读 »

气死了😤,过五关,斩六将,结果被 HR 捅了一刀!!

大家有没有遇到过这样的事情:“过五关,斩六将。通过了两轮、甚至是三轮的技术面,最后 HR 面被“捅死”了” 这样的事情,最近在一位同学身上连续出现了两次,弄得人都麻了。 所以,我们昨天直接腾讯会议沟通了一个小时,分析这到底是因为什么问题导致的。 经过沟通之...
继续阅读 »

大家有没有遇到过这样的事情:“过五关,斩六将。通过了两轮、甚至是三轮的技术面,最后 HR 面被“捅死”了”


这样的事情,最近在一位同学身上连续出现了两次,弄得人都麻了。



所以,我们昨天直接腾讯会议沟通了一个小时,分析这到底是因为什么问题导致的。



经过沟通之后发现,这位同学在 HR 面中所存在的问题,是很多其他的同学也都存在的。所以这一篇文章,咱们就主要来说一说【HR 面的常见问题,以及应对方式】


01、HR 面出现的时机


HR 面根据公司不同,在整个面试流程中出现的位置也不相同。一般情况下会有两个不同的时机:



  1. 技术面试开始之前

  2. 技术面试结束之后


通常,一个完整的面试流程中,HR 只会出现一次。所以,以上两个时机是【二选一】的。


但是,在不同的时机之下,HR 面所代表的意义也会完全不同。


1:技术面试前进行 HR 面


把 HR 面试放到整个技术面试开始之前,通常的作用是:判断你的技术是否可以匹配团队业务、询问你的基本个人信息是否符合公司“价值观”。


这样的情况下,HR 通常会拿着技术部门预先给的八股文让你进行回答,只要你的回答不会偏离八股文太多,通常都会得到面试的机会。(同理:内容不要也不要偏离八股文太多,秀技术不要在这里秀,因为TA听不懂😂)


好处是,这样的公司 HR 权限一般有限,技术团队用人权限会更大。只要技术面试可以顺利通过,那么入职就问题不大。



如果你遇到的面试 HR 面是在初面,那么恭喜你!你会少遇到很多幺蛾子事。



2:技术面试后进行 HR 面


这样的 HR 面一般是作为终面进行的。这也就意味着:HR 有可能拥有一票否决权。


此时你就要小心了,一个不慎,可能就会前功尽弃。


所以,接下来咱们就来看看 HR 面的应对方式。


02、HR 面基础注意事项


无论你有多么迫切的期望得到这份工作,你都没有必要把自己的位置放的过低(过于高傲也是不行的)。


记住:面试本质上是一个双向的选择。


在 保持礼貌 的同时,以 平常心 看待当前的面试。反而会比过于 “谄媚” 或过于 “高傲” 要有效的多。


03、自我介绍


自我介绍是一个必答题。


那么我们在进行自我介绍时,应该 重点描述,匹配当前岗位的业务能力和技术能力。如果有 数据可以支撑的,尽量多说 数据不要 有太多的 主观 词汇。


同时,自我介绍的时长,尽量控制在 40 秒 - 60 秒 之内。


能力强的同学,可以根据面试公司的业务不同,把自己的履历在自我介绍中尽量的往面试公司的业务上去靠。


需要注意的是:HR 面和技术面的自我介绍是不一样的。因为 HR 听不懂太多的技术词汇,他们关心的点都是在“非技术”的问题上



面试时容易紧张的同学,可以事先把自我介绍写在纸上(200-270字即可),背下来去说,总比临阵说不出来要好。



04、你为什么从上家公司离职


一般 HR 都会问这样的问题,同时这个问题也并不好回答(因为真实的离职原因一般很难说)。



内心的真实离职原因:还能因为啥?老板太SB、给的钱太少、加班太多、饼太硬......



心里可以这么想,但是你嘴上不能这么说吧。


所以嘴上说的时候就要注意了,不要说在很多公司都会出现的问题。


可以从以下几点进行思考:



  1. 首先:千万不要暴露自己的问题。(记住:没有公司想要一个问题员工)

  2. 其次:不要贬低上家公司,即时它确实很差劲

  3. 最后:不要贬低同事或领导,因为这在别人听来会非常主观


可以从 公司搬家、自己搬家、公司裁员(现在裁员的多)、更高挑战 等, 现公司不太可能会出现的问题 上回答。


05、你对加班怎么看


我相信很多同学都是 不喜欢加班的,包括我在内。


但是,你 绝对不能 直接说 我不加班。除非你不想要这个 offer


网上有很多比较 “含蓄” 的说法,如:



任何公司,加班都是不可避免的。


如果工作实在有需要,我会愿意加班。


同时,我也会尽量提升工作效率,努力保证在规定的时间内,完成工作。



但是这种说法在实际面试中,可能并没有那么可靠。毕竟大家都不是傻子,话里话外什么意思,TA还能听不懂吗?


所以尽量不要耍这种小聪明(特别是急需这份工作的同学),可以直接说:“加班这种事情不可避免,之前的公司加班也挺多的,所以正常的加班还是没有问题的” 。


以这样的说辞,先拿到 offer 再说。


06、你凭什么认为你值这个钱?


这个话虽然不中听,但是在很多垃圾 HR 嘴里确实蹦出来过。


你也没必要生气,怼他一顿走了虽然很爽,但是这个 offer 同样也会飞掉了。先咽下这口气,等以后入职了再慢慢拾到TA。


这样的问题可以直接从 前面的技术面试 以及 过往履历的匹配度 上面去说。


例如:



之前和技术面试官的沟通整体还是非常愉快地。面试官那边对我的技术也非常认可,同时贵公司目前在做的业务和我过去的工作经验匹配度也比较高,所以说我和该岗位的整体匹配度还是很高的。


同时,我要的薪资也是符合目前市场行情价的,我之前的薪资是xxx,所以我并没有漫天要价



07、你的学校也太差了,还不是本专业。比你履历好的多的是,我们为什么要选择你?


和上一个问题类似,这种问题我是真的在 HR 面试中遇到过的。


这样的问题会很让人生气,同时也很难进行回答。


所以,当你遇到类似这种让人生气的问题时,先想清楚 你是想要一时爽还是想要这个 offer 。


如果想要这个 offer 的话,那么回答可以参考 【06、你凭什么认为你值这个钱?】回答的前半部分。


08、你认为你的缺点都有什么


一般 HR 问这个问题,都是从一些《HR的自我修养》这类书中看到的。但是不得不说,这样的问题并不好回答。


我要是直说我这人:好吃懒做,喜欢插科打屁 好像也不太合适。


所以说最好的方式是:列举出一些不太严重的问题。


这里列举出来几点,为大家提供参考:



  1. 不要说自己的大缺点:比如,“我很懒、我喜欢摸鱼打滑”。这种千万不要说。

  2. 不要把优点当成缺点说:比如,“我总是追求工作的完美,所以在一项工作上会花费更多事件”。毕竟大家都不是傻子。

  3. 不要说影响应聘当前岗位的缺点:比如,“应聘开发,说我总是很粗心,项目上线总是丢三落四”。


大家可以从一些,不影响当前应聘岗位的缺点说起,同时 强调自己已经认识到了这个问题,并且在改正


比如:



我之前特别喜欢熬夜,特别是休息日的时候。


这样并不好,所以我现在在有意识的,强迫自己早早休息。



09、还有拿到其他的 offer 吗?


HR 这么问其实是想要判断 给你发了 offer 之后,你能入职的概率有多少。 因为 HR 自己很清楚自己公司的成色。


但是,这样的问题如果直接说【没有其他 offer】也不好,因为这也有可能会被认为 “你没人要” 。


所以,最好的一个回答方式是:表现出来有 offer,但是对当前贵公司非常认可。


例如:



我当前有几个面试已经再走 offer 的流程了。但是在贵公司的整个面试过程,是我感觉比较专业的,也是体验比较好的。同时贵公司的业务也是我比较喜欢的,如果贵公司给我发 offer 的话,那么我会优先选择贵公司入职。



10、 你还有什么想要问我的吗?


这个问题一般是面试的结尾才会出现的。


这里建议大家,尽量抓住这个机会,多了解了解总是没错的。


但是要注意,尽量不要 问一些 “敏感” 的问题。


比如:



  • 公司忙吗?加班多不多?

  • 公司福利咋样,有下午茶吗?

  • 上班打卡吗?迟到扣钱吗?

  • 这只会 充分暴露,我们想养老的本心 😄 ,所以不要这么说。


如果你特别想要聊这些事情,那么可以在 收到 offer 之后,再去聊。


在收到 offer 之前,尽量聊一些工作之内的事情。


比如:



  • 当前岗位的明确工作内容

  • 公司对该岗位的要求

  • 是否会有新人培训

  • 团队人数与在公司的工作年限(如果团队有很多人在公司的工作年限比较长,那么可以侧面证明公司还不赖。)


总结


目前国内的很多 HR 面试,确实让整个面试的过程变得更加的复杂。


同时不得不说,很多中小企业的 HR 并不专业,很问出很多非常业余并且容易激发矛盾的问题(比如:第六题和第七题)。但是同时部分公司 HR 的权限又非常高,这就导致很多同学不得不陪TA们去玩这个愚蠢的游戏。


最后,老规矩:祝大家都可以拿到满意的 offer,高薪入职心仪的公司~~


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

工信部又出新规!爬坑指南

一、背景 工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。 二、整改 2.1 个...
继续阅读 »

一、背景


工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。


二、整改


2.1 个人信息保护


2.1.1 基本模式(无权限、无个人信息获取模式)


这次整改涉及到最大的一个点就是基本模式,基本模式指的是在用户选择隐私协议弹窗时,不能点击“不同意”即退出应用,而是需要给用户提供一个除联网功能外,无任何权限,无任何个人信息获取的模式且用户能正常使用。


这个说法有点抽象,我们来看下友商已经做好的案例。


腾讯视频



从腾讯视频的策略来看,用户第一次使用app,依旧会弹出一个“用户隐私协议”弹窗供用户选择,但是和以往不同的是,“不同意”按钮替换为了“不同意并进入基本功能模式”,用户点击“不同意并进入基本功能模式”则进入到一个简洁版的页面,只提供一些基本功能,当用户点击“进入全功能模式”,则再次弹出隐私协议弹窗。当杀死进程后,再次进入则直接进入基本模式。


网易云音乐



网易云音乐和腾讯视频的产品策略略有不同,在用户在一级授权弹窗点击“不同意”,会跳转至二级授权弹窗,当用户在二级弹窗上点击“不同意,进入基本功能模式”,才会进入基本功能页面,在此页面上点击“进入完整功能模式”后就又回到了二级授权页。当用户杀死进程,重新进入app时,还是会回到一级授权页。


网易云音乐比腾讯视频多了一个弹窗,也只是为了提升用户进入完整模式的概率,并不涉及到新规。


另外,B站、酷狗音乐等都已经接入了基本模式,有兴趣的伙伴可以自行下载体验。


2.1.2 隐私政策内容


如果app存在读取并传送用户个人信息的行为,需要检查其是否具备用户个人信息收集、使用规则,并明确告知读取和传送个人信息的目的、方式和范围。


判断权限是否有读取、修改、传送行为,如果有,需要在隐私协议中明文告知。


举个例子,app有获取手机号码并且保存在服务器,需要在协议中明确声明:读取并传送用户手机号码。


2.2 app权限调用


2.2.1 应用内权限调用



  1. 获取定位信息和生物特征识别信息


在获取定位信息以及生物特征识别信息时需要在调用权限前,单独向用户明示调用权限的目的,不能用系统权限弹窗替代。



如上图,申请位置权限,需要在申请之前,弹出弹窗供用户选择,用户同意调用后才可以申请位置权限。



  1. 其他权限


其他权限如上面隐私政策一样,需要在调用时,声明是读取、修改、还是传送行为,如下图



2.3 应用软件升级


2.3.1 更新


应用软件或插件更新应在用户授权的情况下进行,不能直接更新,另外要明确告知用户此行为包含下载和安装。


简单来说,就是在app进行更新操作时,需要用弹窗告知用户,是否更新应用,更新的确认权交给用户,并且弹窗上需要声明此次更新有下载和安装两个操作。如下图



2.4 应用签名


需要保证签名的真实有效性。


作者:付十一
来源:juejin.cn/post/7253610755126476857
收起阅读 »

运气大于努力???

标题前言   我想看见这个标题,可能要是近两年,换工作或者刚毕业的小伙伴们最身有体感吧。我先从我自身出发,来讲一下这个我理解,也可能是大家遇见的最多的。 须知:   我是7月25号晚上被辞退,什么原因我上篇文文章已经写过了,就不细细描述了。本人男,25岁,专科...
继续阅读 »

标题前言


  我想看见这个标题,可能要是近两年,换工作或者刚毕业的小伙伴们最身有体感吧。我先从我自身出发,来讲一下这个我理解,也可能是大家遇见的最多的。


须知:


  我是7月25号晚上被辞退,什么原因我上篇文文章已经写过了,就不细细描述了。本人男,25岁,专科,工作经验三年。个人能力不出众,没做过什么可以拿出来吹嘘的东西。就这样平平无奇的开始了,在找工作的路上征战,征战地(上海~杭州),目前是已经入职2个多月了,今天也是有时间上来谈下感受,当然也谢谢上篇文章下各位小伙伴以及各位前辈的鼓励以及帮助


进入正题


  刚刚被辞的时候,我是调整了半个月,准备了一个星期*(一定要自己准备好,别浪费机会,现在的机会很可贵,花点时间没关系的),当时我想的是要走两条路,一个是产品助理,一个就是接着前端了。我其实已经在思考变局这件事。所以也算是一次尝试。虽然尝试的结果并不好,没有什么机会




  回归正题,我当时还是首选的上海,在上海找工作,也面试了几家。大概我总结一下啊,8月份的行情可以说是,一塌糊涂。首先我个人比较跨,特别大的公司,因为我学历的问题,基本是约不到面试的。我就讲我面的几家公司,总体是体量100多人的公司双休很少,大部分是大小周,100以下可能就是双休,但是你基本上要独立负责了。(当然我是没面过外包的,外包我个人觉得是可以的,但是我个人不怎么喜欢外包的氛围。如果给的多的话,无脑冲,哈哈)。后来因为个人感情问题也是决定去杭州,当时也是网上也是各种传递说,专科真别去杭州,一点机会没有,去了也是白去。但是我也是对感情一腔热血啊,直接就冲了。发现没有想象的那么差,机会也是有的。也大大小小的面了不少的公司。就突然觉得运气大于努力了!为什么这么说,因为无论是我在上海或者是杭州,面试的时候都会有觉得不错,而且回答的很好的时候。但是往往这些就是说,回去等通知,因为我这边还有约的。到时候通知你,当然也可能遇见过。这时候你自己回家的时候就会有幻想,觉得嗯,可以,应该没啥问题。可是这些往往没了后续。也遇见过说,技术面过以后,说你要的薪资我们肯定给不到的,你能接受多少多少吗?这时候你看下招聘信息,你就挠头,心里想着:“啊,不是,招聘信息上不是有吗”。为啥又给不到,那你就要说的低一点,试试能不能行。然后照常等通知。再遇见,去面试,花了很久过去,然后人事说 技术开会,人事说,技术出差,人事说,技术这会在忙。然后让你等一会,等一会人事就会告诉你,今天不行了,等晚上技术给你电话面试。然后你就点头说,好好好,那晚上几点呢?8点钟吧。ok这时候你已经得到了有用的信息,准备回家了。开始回家准备,7点半就已经严阵以待,8点钟没有电话,8点半没有,9点了你开始想用招聘信息联系一下,然后得到的信息是,技术忙忘了,明天吧,,我只想说别等了,等不到的。再有的,在面试过程中,面试官上来用他自己很明白很清楚的话术问你问题的,你知道,但是你听不懂他先问啥的。这时候你就怀疑自己,我是不是不会,没遇见过。不妨你再问一边,你说,你能把这个问题举个例子吗?可能这时候你就会豁然开朗,原来他问的是这个。我个人遇见get 跟git 一个发音的,导致他觉得我没用过。我为什么会知道,因为他问的问题我基本没答上来。最后他让我问他有什么问题的时候,我才给他说,我说你想问什么呢?你问的我咋都没听过,然后他就开始给我解答,他问的问题是什么,我知道以后我都裂开了。你这问的跟你刚刚问的有关系吗?害,当时觉得自己小丑。当然结果肯定不过。那时候就陷入了一个很奇怪的误区,我每天那么努力,去背,去准备。结果完全用不上。




  看了看那些找工作的群,很多人不如你,却都拿着比你高的工资,嘴里说着不知道为啥就过了,也没问啥问题,就过了,听见后。心理一阵恍惚。觉得自己运气真差啊。真不争气啊。不知所措,迷茫冲上心头,就是不知道差哪了,虽然不入那些真正的大佬牛皮,可是也比刚出来的培训生好吧(这里解释一下,没有其他意思,个人帮很多培训的看简历,以及回答问题,如有冒犯,本人现在就很对不起,可私聊喷我,当场对不起,评论区还是正向一点),为啥事事不如意。然后就那种失望的情绪会笼罩你,会让你觉得,努力不过如此。




  努力,百度百科里是这么解释的:努力指用尽力气去做事情,后来指一种做事情的积极态度,当我们失去积极的态度的时候我们其实压根就没努力。那运气,百度百科里是怎么写的呢,某种事件发生的概率微小、随机性强、无法计算且不可控制的情况下,事件结果产生后恰好与某人的猜想或个人情况决定一致,并且在现实中发生一般为不可思议或完全不可能存在的背景下发生的事件。这句话不做任何解答,自己多读两遍,然后再去认知你的努力。


结束啦


  其实我写的东西很简单,又很生活,讲不出来大道理,聊不出人生观。只有生活,也只是我自己的生活。你们的生活嘛,当然是要 努力 .那运气呢?你们肯定都是有的,如果自己最近运气不好也没有关系。去走一走,转一转,可能你的运气,就会来了。各位也可以评论一下,各位对运气还有努力的看法,评论我会认真回。这篇文章如果有冒犯到的地方,也请见谅。


作者:想努力的菜菜
来源:juejin.cn/post/7295328074601218060
收起阅读 »

聊聊Android中的DRM工具-Widevine

曾几何时,我一直好奇,像爱奇艺、腾讯视频、优酷这些视频平台是如何控制版权的,就比如,如何防止用户下载后发布到其他渠道,最近接触了DRM技术,瞬间就懂了。 DRM介绍 DRM(Digital Rights Management),即数字版权管理,是在数字内容交易...
继续阅读 »

曾几何时,我一直好奇,像爱奇艺、腾讯视频、优酷这些视频平台是如何控制版权的,就比如,如何防止用户下载后发布到其他渠道,最近接触了DRM技术,瞬间就懂了。


DRM介绍


DRM(Digital Rights Management),即数字版权管理,是在数字内容交易过程中,对知识产权进行保护的技术、工具和处理过程。它的目的是防止数字内容被未经授权的用户复制、修改和分发,以保护知识产权所有者的权益。在日常生活中,我们经常与 DRM 技术打交道。比如,电影上映前,我们不能在视频网站上观看电影,只能去电影院。这是内容提供(发行)商对自己的数字内容进行管理的一种结果。


DRM工作原理


先贴一张图,然后我们再做简单的说明drm工作原理


上图中,RAM想要给SHYAM传递小纸条,但因为距离较远,中间需要三个人进行传达,为了防止这三个人偷看小纸条内容,他们找来了HARI,HARI手上有一本密码本,每次RAM传递小纸条之前先找HARI拿到密码本,然后根据密码本的规则对小纸条内容进行加密,然后再将加密后的小纸条传递给SHYAM,这样,即使中间三个人偷看了小纸条,因为没有密码本,所以也看不懂纸条的内容。SHYAM收到小纸条后,再向HARI获取密码本,然后对小纸条内容进行解密,这样SHYAM就能看到原始内容了。


现在,我们把RAM看成是视频发行商,SHYAM看成是观众,HARI看成是版权管理商,就有了以下这种关系图


drm工作原理2


从上图中可以看出,我们想要向认证用户安全地发送一部电影。需要:



  • 向DRM厂商的服务器请求密码本

  • 然后使用密码本加密视频

  • 将电影视频发送给用户

  • 用户向DRM厂商的服务器请求密码本解密视频

  • 现在用户就可以观看电影了


这下视频版权管理是不是就一目了然了。但以上只是最初DRM的设计思想,现实中却无法正常运行,因为还没有解决多种分辨率的问题,这就需要对视频进行切片(ABR)和打包。


视频切片和打包


ABR: 通过使用ABR技术,电影可以被编码成不同的码率-分辨率组合(也称为码率阶梯)并被分割成小的视频块或者切片。每个视频切片包含几秒钟视频,可以被单独解码。


打包是指将电影分割成小的视频切片,并使用清单(manifest)或者播放列表对其进行描述。当用户想要播放电影的时候,他需要按照播放列表的信息播放。


根据可用带宽,播放器请求特定码率版本的视频切片,CDN响应后返回被请求切片。


drm工作原理3


这就结束了吗?不,这里面还存在很大的一个问题需要解决,视频的加密问题。


视频加密


前面说,视频发行商在发布视频时,需要向DRM服务商获取密码本,这里的密码本实际上是一种授权,就是说经过DRM服务商的授权,他才会对你的视频进行版权保护,并不是对视频内容进行加密,真正的视频加密还得涉及到密码学相关的技术,最常用的加密方式是AES,AES属于对称加密,这就涉及到密钥的保存。在DRM中,密钥也保存在DRM服务商手上,随着视频清单一起发送给视频播放器


drm工作原理4


好了,DRM的核心原理大概就是这些,如果想了解更详细的内容,可阅读下面的参考文献。


DRM厂商


上述DRM工作原理图中,有一个很重要的角色就是DRM服务商,目前主要有三大服务商,分别对应自己的DRM技术方案,分别是:




  • Apple FairPlay




  • Google Widevine




  • Microsoft PlayReady




国内爱奇艺最近也自主研发了自己的DRM解决方案:iQIYI DRM-S。而国内的视频平台几乎都是打包了所有的的DRM方案,以针对不同的平台和系统。以下是爱奇艺的整体DRM解决方案


爱奇艺drm方案


Widevine介绍


Widevine仅适用于基于Chromium的操作系统、Android设备以及其他Google相关设备和浏览器。


Widevine的安全级别



  • L1


在L1级别,提供了最高的安全性。内容在设备内进行解密,并使用硬件保护,以防止原始数据泄露。通常用于高质量视频和高分辨率的流媒体。获得L1认证的设备可以播放高质量的内容。像Amazon Prime Video和Netflix等流媒体服务需要L1安全性。如果在未获得认证的设备上观看,无法播放高清或超高清的高质量内容。



  • L2


L2具有较高的安全性,但不像L1那么严格。即使设备未获得L1认证,仍然可以播放内容。一些设备使用软件来保护数据。对于较低分辨率的视频和音乐内容,可能会使用L2。如果想要享受更高质量的内容,建议使用获得L1认证的设备,而不是L2。虽然L2可能不够满足要求,但某些内容仍然可能提供高质量的视频。因此,不能一概而论地认为必须使用L1。



  • L3


L3的安全级别最低。主要用于模拟器和一些旧设备等情况,内容保护相对较弱,分析和复制相对容易。此外,一些服务如Amazon Prime Video和Netflix也可能使用L3。虽然可以使用L3,但风险较高,不应期望高质量的内容。使用L3时需要谨慎考虑这些因素。


查看Widevine级别


可以使用DRM Info App查看设备的widevine安全级别,该App可以在Google Play上找到,文末贴了App的下载链接。大多数主流制造商的智能手机通常都支持L1至L3的某一个级别。如果发现您的设备不支持Widevine,那可能是制造商为了简化流程或者您的智能手机不符合标准。


image-20231121164459796


如果app打开闪退,说明设备并不支持Widevine。


测试Widevine功能


许多流媒体app都使用了Widevine,比如Youku、腾讯视频、IQIYI、YouTube、Netflix等,这里推荐使用Google的官方播放器ExoPlayer进行测试,文末提供下载链接


image-20231121165120972


(重点)在Android中集成Widevine


step1:获取Widevine源码


官网下载Widevine源码,注意,AOSP默认是没有Widevine源码的,需要手动集成,因为需要跟Google签订法律协议,然后由Google授权访问Widevine代码库,具体见Google官网流程。


step2:将源码放置到vendor目录下vendor/widevine/


image-20231121170544865


step3:添加编译配置


device/qcom/{product combo name}/BoardConfig.mk中添加


#这里设置的L3级别,L1级别需要跟Google签订协议,获取Keybox
BOARD_WIDEVINE_OEMCRYPTO_LEVEL := 3

device/qcom/{product combo name}/{product combo name}.mk中添加


PRODUCT_PROPERTY_OVERRIDES += drm.service.enabled=true
PRODUCT_PACKAGES += com.google.widevine.software.drm.xml \
com.google.widevine.software.drm
PRODUCT_PACKAGES += libwvdrmengine

vendor/qcom/proprietary/common/config/device-vendor.mk中修改


SECUREMSM += InstallKeybox
#L3级别需要删除oemcrypto库
#SECUREMSM += liboemcrypto
#SECUREMSM += liboemcrypto.a
SECUREMSM += libhdcpsrm

最后编译刷机,使用app工具验证即可,如果能显示Widevine级别,说明集成成功。


总结


好了,现在你应该彻底知道Widevine是怎么回事了


参考链接


中学生也能看懂的DRM


构建DRM系统的重要基石——EME、CDM、AES、CENC和密钥


爱奇艺DRM修炼之路


什么是Widevine?Widevine DRM详解


Google Widevine


Widevine安全级别查看app:


链接:pan.baidu.com/s/1lIJq-_eg…
提取码:fnk6


ExoPlayer:


链接:pan.baidu.com/s/1dUseWHIi…
提取码:nszh


作者:小迪vs同学
来源:juejin.cn/post/7303723984180101139
收起阅读 »

因为山就在那里

第一次爬北京的山,看到了不一样的风景,也收获到了意外的体验。虽说是3A级景区,但这次给我的体验远不止3A,恰巧赶上了日落,路上遇到了小猫,2000多米的爬山体验,给我的感觉是,棒极了。 千里之行,始于足下 也许是最近在学校待久了吧,课堂上一些公式化的学习生活...
继续阅读 »


第一次爬北京的山,看到了不一样的风景,也收获到了意外的体验。虽说是3A级景区,但这次给我的体验远不止3A,恰巧赶上了日落,路上遇到了小猫,2000多米的爬山体验,给我的感觉是,棒极了。




千里之行,始于足下


也许是最近在学校待久了吧,课堂上一些公式化的学习生活有时让人觉得些许枯燥,刚好最近特别想去爬山,想去那就去呗!于是乎,我咨询了一下经常在北京各大山区骑行游览的同学,他随手推荐了离学校不远的蟒山。我上地图搜了一下,在网上简略地看了一下攻略,就决定出发了。没想到的是,另外有两位同学也打算出去走走,我说,那就一起呗,反正也不远。就这样,我收获了两个小伙伴。出发前,我东西收拾得差不多了,但我依然还在想是不要再准备些什么?思索了一下,我觉得不用再考虑太多了,千里之行,始于足下,想做就直接去做先,直接行动起来!




山路崎岖,拥抱自然


在到达正式爬山之前,其实还是经历了许多的小插曲。当天我们三人是坐地铁到达关东站,然后骑着共享单车去到蟒山国家森林公园的入口。但是骑到一半突然觉出似乎山区附近没有停车点,于是本来已经快到山脚下的我们决定原路返回一段距离去最近的停车点。好在最近停车点不远,也没有迂回多少。之后我们开始等公交,可等了一会儿也没见车来,我索性提出直接打车,然后刚打车就有师傅接单了,发现路费也不贵,其实也就10来分钟的路程,不过上山有点坡度,所以骑车可能会久些。就这样我们来到了入口。检完票进去后就面临路线的选择,我想着先从山脚走起吧,就先往文化广场的方向走去。蟒山森林公园其实好多小景点都别有一番风味。


一走进森林公园,给我的感觉就是:“清爽”!在这里,你可以深吸一口气,不用担心烟尘啥的,这里的空气让人很舒服。旁边这些小树林也别有也一番风味,阳关透过枝叶映射在地面上,一条一条的,很有感觉。最重要的是,这里人流很少,是真正的清净!路上有很多的休息椅子,累了也能随时坐会儿,开始给我一种真正世外桃源的感觉。





在体验一番过后,突然来到了观蟒台,说实话, 我好像没咋观出哪里是蟒,可能是还在蟒山脚的缘故,看得不是很开阔,但是风景还是不错的。之后我们开始往山上的方向走去,途中经过了蟒山叠翠、碑林、然后就到了文化广场,广场上面有个水池(忘记叫什么了)。这些景点我个人感觉不是很惊艳,但也算是有该有的吧,水池是个环形的,能看到山的倒影,也算还行吧。




山不向我,我向山去


经过这些小景点的兜兜转转后,我们才算最终踏上了通向山顶的道路,也能明显地感到坡道有些坡度了。我们往五观台的方向爬去,五观台,听说是观五样东西,一个是京城什么的,一个是水库什么的,其他几个也记不太清了,反正就是看五样不同的景观。开始上去的时候有路牌提示,说据顶点还有2000来米,大约需要2个小时,当时看了还是有些吃惊的,竟然这么高吗?没事,反正我也没咋去过,就直接上呗。于是,一个接着一个的台阶就这样在我的脚下流过。



开始的山景似乎没特别惊艳的,因为也许还没到半山腰,景色不是很丰富,但也有不同的植被,也能看到一些小山的景色。一开始觉得没什么难爬的,可能会稍微耗点体力,但是走了一小阵后发现确实有些小累,于是我们开始稍微歇会儿。歇会儿过后又继续前进,路上也碰到了一些来爬山的人,不过也不多。我们一路走着,路上也有不一样的景色,当我们走着走着时,发现太阳似乎快要落山了,有一个角度望去特别的好看,突然的这样的景色也让我给惊艳到了。





不知为什么,从此刻起,突然觉得现在山上的景色好好看,让原本些许累的我们也放松了一下,脚上的累也消了不少。就这样,我们一边欣赏着夕阳,一边继续前行。路上还遇上了一只“学长”,“学长”挺胖挺可爱的。



太阳马上就落山了,我们也目睹了这场绝美的日落







其实太阳落得很快,估计就几秒的时间,可能美好的东西总是很短暂吧!这也让我想起了“夕阳无限好,只是近黄昏”,但是呢,我又觉着“莫道桑榆晚,为霞尚满天”,虽然从岁数来说,放在我们身上不太合适吧,但是我觉得从做事角度,其实还是要怀抱希望,做自己热爱的,现在出发去做,不会太迟。我没有用手机录下它日落那一瞬,而更多是用眼睛去感受了。


半山腰有些拥挤,到山顶看看




太阳已经落山了,但距离山顶还有一段距离,考虑到待会还要下山,心里有些犹豫。但是呢,山顶就在眼前了,总得去看看吧,反正现在也不算太晚,路上还有些人,于是我们就一鼓作气,冲向了山顶。



可能是冬天+北方吧,天黑得比较快,这会儿月亮也出来了,人们也点起了灯火,从山顶上望去,夜景还是挺好看的。




天黑了,也不敢待太久,稍微看看后就下山去了,幸好我们有三个人,不然我还真不敢上来。


总结


本次爬山给我的收获还是挺大的,在山里面,花花草草,虫虫鸟鸟,一山一木,一水一露,让人感到十分清净。我觉得的是,得顺其自然,做你想做的事,有想法就直接行动;做你热爱的事情,喜欢才会有动力;做能让你感到开心和快乐的事情,不必背着自己的内心,做一些不开心不快乐的事。很喜欢的的一句话:“为什么要登山?因为山就在那里”。是的,山就在那里,它们不会走动,你去它那,它就会给你反馈,给你舒心。也许它们不会说话,但是它又无时无刻不在与你互动呢?偶遇的绝美日落,半山腰出现的“可爱学长”,问鼎山峰后的夜景,这才是真正的交流,所以,喜欢就去做呗,不用太在意结果,路途中的美景本身就是一种收获,在过程中,也许就能找到答案了。


作者:椰佬小KK
来源:juejin.cn/post/7302371147978113078
收起阅读 »

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

你好呀,是歪歪。 前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,解决思路忙中有序,处理手段雷厉风行,把它给扼杀在萌芽阶段了,那这玩意肯定得引发一个比较严重的生产问题。 从问题出现到...
继续阅读 »

你好呀,是歪歪。


前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,解决思路忙中有序,处理手段雷厉风行,把它给扼杀在萌芽阶段了,那这玩意肯定得引发一个比较严重的生产问题。


从问题出现到定位到这个问题的根本原因,我大概是花了两天半的时间。


所以写篇文章给大家复盘一下啊,这个案例就是一个纯技术的问题导致的,和业务的相关度其实并不大,所以你拿过去直接添油加醋,稍微改改,往自己的服务上套一下,那就是你的了。


我再说一次:虽然现在不是你的,但是你看完之后就是你的了,你明白我意思吧?



表象


事情是这样的,我这边有一个服务,你可以把这个服务粗暴的理解为是一个商城一样的服务。有商城肯定就有下单嘛。


然后接到上游服务反馈,说调用下单接口偶尔有调用超时的情况出现,断断续续的出现好几次了,给了几笔流水号,让我看一下啥情况。当时我的第一反应是不可能是我这边服务的问题,因为这个服务上次上线都至少是一个多月前的事情了,所以不可能是由于近期服务投产导致的。


但是下单接口,你听名字就知道了,核心链接上的核心功能,不能有一点麻痹大意。


每一个请求都很重要,客户下单体验不好,可能就不买了,造成交易损失。


交易上不去营业额就上不去,营业额上不去利润就上不去,利润上不去年终就上不去。


想到这一层关系之后,我立马就登陆到服务器上,开始定位问题。


一看日志,确实是我这边接口请求处理慢了,导致的调用方超时。


为什么会慢呢?



于是按照常规思路先根据日志判断了一下下单接口中调用其他服务的接口相应是否正常,从数据库获取数据的时间是否正常。


这些判断没问题之后,我转而把目光放到了 gc 上,通过监控发现那个时间点触发了一次耗时接近 1s 的 full gc,导致响应慢了。


由于我们监控只采集服务近一周的 gc 数据,所以我把时间拉长后发现 full gc 在这一周的时间内出现的频率还有点高,虽然我还没定位到问题的根本原因,但是我定位到了问题的表面原因,就是触发了 full gc。


因为是核心链路,核心流程,所以此时不应该急着去定位根本原因,而是先缓解问题。


好在我们提前准备了各种原因的应急预案,其中就包含这个场景。预案的内容就是扩大应用堆内存,延缓 full gc 的出现。


所以我当即进行操作报备并联系运维,按照紧急预案执行,把服务的堆内存由 8G 扩大一倍,提升到 16G。


虽然这个方法简单粗暴,但是既解决了当前的调用超时的问题,也给了我足够的排查问题的时间。



定位原因


当时我其实一点都不慌的,因为问题在萌芽阶段的时候我就把它给干掉了。



不就是 full gc 吗,哦,我的老朋友。


先大胆假设一波:程序里面某个逻辑不小心搞出了大对象,触发了 full gc。


所以我先是双手插兜,带着监控图和日志请求,闲庭信步的走进项目代码里面,想要凭借肉眼找出一点蛛丝马迹......



没有任何收获,因为下单服务涉及到的逻辑真的是太多了,服务里面 List 和 Map 随处可见,我很难找到到底哪里是大对象。


但是我还是一点都不慌,因为这半天都没有再次发生 Full GC,说明此时留给我的时间还是比较充足的,


所以我请求了场外援助,让 DBA 帮我导出一下服务的慢查询 SQL,因为我想可能是从数据库里面一次性取的数据太多了,而程序里面也没有做控制导致的。


我之前就踩过类似的坑。


一个根据客户号查询客户有多少订单的内部使用接口,接口的返回是 List<订单>,看起来没啥毛病,对不对?


一般来说一个个人客户就几十上百,多一点的上千,顶天了的上万个订单,一次性拿出来也不是不可以。


但是有一个客户不知道咋回事,特别钟爱我们的平台,也是我们平台的老客户了,一个人居然有接近 10w 的订单。


然后这么多订单对象搞到到项目里面,本来响应就有点慢,上游再发起几次重试,直接触发 Full gc,降低了服务响应时间。


所以,经过这个事件,我们定了一个规矩:用 List、Map 来作为返回对象的时候,必须要考虑一下极端情况下会返回多少数据回去。即使是内部使用,也最好是进行分页查询。


好了,话说回来,我拿到慢查询 SQL 之后,根据几个 Full gc 时间点,对比之后提取出了几条看起来有点问题的 SQL。


然后拿到数据库执行了一下,发现返回的数据量其实也都不大。


此刻我还是一点都不慌,反正内存够用,而且针对这类问题,我还有一个场外援助没有使用呢。


第二天我开始找运维同事帮我每隔 8 小时 Dump 一次内存文件,然后第三天我开始拿着内存文件慢慢分析。


但是第二天我也没闲着,根据现有的线索反复分析、推理可能的原因。


然后在观看 GC 回收内存大小监控的时候,发现了一点点端倪。因为触发 Full GC 之后,发现被回收的堆内存也不是特别多。


当时就想到了除了大对象之外,还有一个现象有可能会导致这个现象:内存泄露。


巧的是在第二天又发生了一次 Full gc,这样我拿到的 Dump 文件就更有分析的价值了。基于前面的猜想,我分析的时候直接就冲着内存泄漏的方向去查了。


我拿着 5 个 Dump 文件,分析了在 5 个 Dump 文件中对象数量一直在增加的对象,这样的对象也不少,但是最终定位到了 FutureTask 对象,就是它:



找到这玩意了再回去定位对应部分的代码就比较容易。


但是你以为定位了代码就完事了吗?


不是的,到这里才刚刚开始,朋友。


因为我发现这个代码对应的 Bug 隐藏的还是比较深的,而且也不是我最开始假象的内存泄露,就是一个纯粹的内存溢出。


所以值得拿出来仔细嗦一嗦。


示例代码


为了让你沉浸式体验找 BUG 的过程,我高低得给你整一个可复现的 Demo 出来,你拿过去就可以跑的那种。


首先,我们得搞一个线程池:



需要说明一下的是,上面这个线程池的核心线程数、最大线程数和队列长度我都取的 1,只是为了方便演示问题,在实际项目中是一个比较合理的值。


然后重点看一下线程池里面有一个自定义的叫做 MyThreadFactory 的线程工厂类和一个自定义的叫做 MyRejectedPolicy 的拒绝策略。


在我的服务里面就是有这样一个叫做 product 的线程池,用的也是这个自定义拒绝策略。


其中 MyThreadFactory 的代码是这样的:



它和默认的线程工厂之间唯一的区别就是我加了一个 threadFactoryName 字段,方便给线程池里面的线程取一个合适的名字。


更直观的表示一下区别就是下面这个玩意:



原生:pool-1-thread-1

自定义:product-pool-1-thread-1



接下来看自定义的拒绝策略:



这里的逻辑很简单,就是当 product 线程池满了,触发了拒绝策略的时候打印一行日志,方便后续定位。


然后接着看其他部分的代码:



标号为 ① 的地方是线程池里面运行的任务,我这里只是一个示意,所以逻辑非常简单,就是把 i 扩大 10 倍。实际项目中运行的任务业务逻辑,会复杂一点,但是也是有一个 Future 返回。


标号为 ② 的地方就是把返回的 Future 放到 list 集合中,在标号为 ③ 的地方循环处理这个 list 对象里面的 Future。


需要注意的是因为实例中的线程池最多容纳两个任务,但是这里却有五个任务。我这样写的目的就是为了方便触发拒绝策略。


然后在实际的项目里面刚刚提到的这一坨逻辑是通过定时任务触发的,所以我这里用一个死循环加手动开启线程来示意:



整个完整的代码就是这样的,你直接粘过去就可以跑,这个案例就可以完全复现我在生产上遇到的问题:


public class MainTest {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor productThreadPoolExecutor = new ThreadPoolExecutor(1,
                1,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1),
                new MyThreadFactory("product"),
                new MyRejectedPolicy());

        while (true){
            TimeUnit.SECONDS.sleep(1);
            new Thread(()->{
                ArrayList<Future<Integer>> futureList = new ArrayList<>();
                //从数据库获取产品信息
                int productNum = 5;
                for (int i = 0; i < productNum; i++) {
                    try {
                        int finalI = i;
                        Future<Integer> future = productThreadPoolExecutor.submit(() -> {
                            System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
                            return finalI * 10;
                        });
                        futureList.add(future);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                for (Future<Integer> integerFuture : futureList) {
                    try {
                        Integer integer = integerFuture.get();
                        System.out.println(integer);
                        System.out.println("future.get() = " + integer);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }

    static class MyThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGr0up group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
        private final String threadFactoryName;

        public String getThreadFactoryName() {
            return threadFactoryName;
        }

        MyThreadFactory(String threadStartName) {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGr0up() :
                    Thread.currentThread().getThreadGr0up();
            namePrefix = threadStartName + "-" +
                    poolNumber.getAndIncrement() +
                    "-";
            threadFactoryName = threadStartName;
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

    public static class MyRejectedPolicy implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (e.getThreadFactory() instanceof MyThreadFactory) {
                MyThreadFactory myThreadFactory = (MyThreadFactory) e.getThreadFactory();
                if ("product".equals(myThreadFactory.getThreadFactoryName())) {
                    System.out.println(THREAD_FACTORY_NAME_PRODUCT + "线程池有任务被拒绝了,请关注");
                }
            }
        }
    }
}

你跑的时候可以把堆内存设置的小一点,比如我设置为 10m:



-Xmx10m -Xms10m



然后用 jconsole 监控,你会发现内存走势图是这样的:



哦,我的老天爷啊,这个该死的图,也是我的老伙计了,一个缓慢的持续上升的内存趋势图, 最后疯狂的触发 gc,但是并没有内存被回收,最后程序直接崩掉:



这绝大概率就是内存泄漏了啊。


但是在生产上的内存走势图完全看不出来这个趋势,我前面说了,主要因为 GC 情况的数据只会保留一周时间,所以就算把整个图放出来也不是那么直观。


其次不是因为我牛逼嘛,萌芽阶段就干掉了这个问题,所以没有遇到最后频繁触发 gc,但是没啥回收的,导致 OOM 的情况。


所以我再带着你看看另外一个视角,这是我真正定位到问题的视角。就是分析内存 Dump 文件。


分析内存 Dump 文件的工具以及相关的文章非常的多,我就不赘述了,你随便找个工具玩一玩就行。我这里主要是分享一个思路,所以就直接使用 idea 里面的 Profiler 插件了,方便。


我用上面的代码,启动起来之后在四个时间点分别 Dump 之后,观察内存文件。内存泄露的思路就是找文件里面哪个对象的个数和占用空间是在持续上升嘛,特别是中间还发生过 full gc,这个过程其实是一个比较枯燥且复杂的过程,在生产项目中可能会分析出很多个这样的对象,然后都要到代码里面去定位相关逻辑。


但是我这里极大的简化了程序,所以很容易就会发现这个 FutureTask 对象特别的抢眼,数量在持续增加,而且还是名列前茅的:



然后这个工具还可以看对象占用大小,大概是这个意思:



所以我还可以看看在这几个文件中 FutureTask 对象大小的变化,也是持续增加:



就它了,准没错。


好,问题已经能复现了,GC 图和内存 Dump 的图也都给你看了。


到这里,如果有人已经看出来问题的原因了,可以直接拉到文末点个赞,感谢大佬阅读我的文章。


如果你还没看出端倪来,那么我先给你说问题的根本原因:



问题的根本原因就出在 MyRejectedPolicy 这个自定义拒绝策略上。



在带你细嗦这个问题之前,我先问一个问题:



JDK 自带的线程池拒绝策略有哪些?



这玩意,老八股文了,存在的时间比我从业的时间都长,得张口就来:



  • AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常,这是默认的策略。

  • DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务

  • CallerRunsPolicy:由调用线程处理该任务

  • DiscardPolicy:也是丢弃任务,但是不抛出异常,相当于静默处理。


然后你再回头看看我的自定义拒绝策略,是不是和 DiscardPolicy 非常像,也没有抛出异常。只是比它更高级一点,打印了一点日志。


当我们使用默认的策略的时候:



或者我们把框起来这行代码粘到我们的 MyRejectedPolicy 策略里面:



再次运行,不管是观察 gc 情况,还是 Dump 内存,你会发现程序正常了,没毛病了。


下面这个走势图就是在拒绝策略中是否抛出异常对应的内存走势对比图:



在拒绝策略中抛出异常就没毛病了,为啥?



探索


首先,我们来看一下没有抛出异常的时候,发生了什么事情。


没有抛出异常时,我们前面分析了,出现了非常多的 FutureTask 对象,所以我们就找程序里面这个对象是哪里出来的,定位到这个地方:



future 没有被回收,说明 futureList 对象没有被回收,而这两个对象对应的 GC Root 都是new 出来的这个线程,因为一个活跃线程是 GC Root。


进一步说明对应 new 出来的线程没有被回收。


所以我给你看一下前面两个案例对应的线程数对比图:



没有在拒绝策略中抛出异常的线程非常的多,看起来每一个都没有被回收,这个地方肯定就是有问题的。


然后随机选一个查看详情,可以看到线程在第 39 行卡着的:



也就是这样一行代码:



这个方法大家应该熟悉,因为也没有给等待时间嘛,所以如果等不到 Future 的结果,线程就会在这里死等。


也就导致线程不会运行结束,所以不会被回收。


对应着源码说就是有 Future 的 state 字段,即状态不正确,导致线程阻塞在这个 if 里面:



if 里面的 awaitDone 逻辑稍微有一点点复杂,这个地方其实还有一个 BUG,在 JDK 9 进行了修复,这一点我在之前的文章中写过,所以就不赘述了,你有兴趣可以去看看:《Doug Lea在J.U.C包里面写的BUG又被网友发现了。》


总之,在我们的案例下,最终会走到我框起来的代码:



也就是当前线程会在这里阻塞住,等到唤醒。


那么问题就来了,谁来唤醒它呢?


巧了,这个问题我之前也写过,在这篇文章中,有这样一句话:《关于多线程中抛异常的这个面试题我再说最后一次!》



如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException 和 set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。



带你看一眼 FutureTask 的 run 方法:



也就是说 FutureTask 状态变化的逻辑是被封装到它的 run 方法里面的。


知道了它在哪里等待,在哪里唤醒,揭晓答案之前,还得带你去看一下它在哪里诞生。


它的出生地,就是线程池的 submit 方法:



java.util.concurrent.AbstractExecutorService#submit




但是,朋友,注意,我要说但是了。


首先,我们看一下当线程池的 execute 方法,当线程池满了之后,再次提交任务会触发 reject 方法,而当前的任务并不会被放到队列里面去:



也就是说当 submit 方法不抛出异常就会把正常返回的这个状态为 NEW 的 future 放到 futureList 里面去,即下面编号为 ① 的地方。然后被标号为 ② 的循环方法处理:



那么问题就来了:被拒绝了的任务,还会被线程池触发 run 方法吗?


肯定是不会的,都被拒绝了,还触发个毛线啊。


不会被触发 run 方法,那么这个 future 的状态就不会从 NEW 变化到 EXCEPTION 或者 NORMAL。


所以调用 Future.get() 方法就一定一直阻塞。又因为是定时任务触发的逻辑,所以导致 Future 对象越来越多,形成一种内存泄露。


submit 方法如果抛出异常则会被标号为 ② 的地方捕获到异常。


不会执行标号为 ① 的地方,也就不会导致内存泄露:



道理就是这么一个道理。


解决方案


知道问题的根本原因了,解决方案也很简单。


定位到这个问题之后,我发现项目中的线程池参数配置的并不合理,每次定时任务触发之后,因为数据库里面的数据较多,所以都会触发拒绝策略。


所以首先是调整了线程池的参数,让它更加的合理。当时如果你要用这个案例,这个地方你也可以包装一下,动态线程池,高大上,对吧,以前讲过。


然后是调用 Future.get() 方法的时候,给一个超时时间,这样至少能帮我们兜个底。资源能及时释放,比死等好。


最后就是一个教训:自定义线程池拒绝策略的时候,一定一定记得要考虑到这个场景。


比如我前面抛出异常的自定义拒绝策略其实还是有问题的,我故意留下了一个坑:



抛出异常的前提是要满足最开始的 if 条件:



e.getThreadFactory() instanceof MyThreadFactory



如果别人误用了这个拒绝策略,导致这个 if 条件不成立的话,那么这个拒绝策略还是有问题。


所以,应该把抛出异常的逻辑移到 if 之外。


同时在排查问题的过程中,在项目里面看到了类似这样的写法:



不要这样写,好吗?


一个是因为 submit 是有返回值的,你要是不用返回值,直接用 execute 方法不香吗?


另外一个是因为你这样写,如果线程池里面的任务执行的时候出异常了,会把异常封装到 Future 里面去,而你又不关心 Future,相当于把异常给吞了,排查问题的时候你就哭去吧。


这些都是编码过程中的一些小坑和小注意点。


反转


这一小节的题目为什么要叫反转?


因为以上的内容,除了技术原理是真的,我铺垫的所有和背景相关的东西,全部都是假的。



整篇文章从第二句开始就是假的,我根本就没有遇到过这样的一个生产问题,也谈不上扼杀在摇篮里,更谈不上是我去解决的了。


但是我在开始的时候说了这样一句话,也是全文唯一一句加粗的话:



虽然现在不是你的,但是你看完之后就是你的了,你明白我意思吧?



所以这个背景其实我前几天看到了“严选技术”发布的这篇文章《严选库存稳定性治理系列:一个线程池拒绝策略引发的血案》


看完他们的这篇文章之后,我想起了我之前写过的这篇文章:《看起来是线程池的BUG,但是我认为是源码设计不合理。》


我写的这篇就是单纯从技术角度去解析的这个问题,而“严选技术”则是从真实场景出发,层层剥茧,抵达了问题的核心。


但是这两篇文章遇到的问题的核心原因其实是一模一样的。


我在我的文章中的最后就有这样一段话:



巧了,这不是和“严选技术”里面这句话遥相呼应起来了吗:



在我反复阅读了他们的文章,了解到了背景和原因之后,我润色了一下,写了这篇文章来“骗”你。


如果你有那么几个瞬间被我“骗”到了,那么我问你一个问题:假设你是面试官,你问我工作中有没有遇到过比较棘手的问题?


而我是一个只有三年工作经验的求职者。


我用这篇文章中我假想出来的生产问题处理过程,并辅以技术细节,你能看出来这是我“包装”的吗?


然后在描述完事件之后,再体现一下对于事件的复盘,可以说一下基于这个事情,后面自己对监控层面进行了丰富,比如接口超时率监控、GC 导致的 STW 时间监控啥的。然后也在公司内形成了“经验教训”文档,主动同步给了其他的同事,以防反复踩坑,巴拉巴拉巴拉...


反正吧,以后看到自己觉得好的案例,不要看完之后就完了,多想想怎么学一学,包装成自己的东西。


这波包装,属于手摸手教学了吧?


求个赞,不过分吧?


作者:why技术
来源:juejin.cn/post/7186512174779465765
收起阅读 »

学习能力必然是职场的核心能力

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。 从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,...
继续阅读 »

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。


从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,互联网的风停下来之后,市场的需求变了,从单一的编程语言、单一业务的能力变成更加综合的能力,需要的人逐渐变为T型人才甚至π型人才。此时,学习能力就变得更加重要。否则,面临的只能是市场的淘汰。


下面分享一下自己最近三周学习Golang的一些经验和方法,大家可以拿来借鉴的其他学习方面上:


第一、实践。任何的学习都离不开实践。不能够运用到实践中的学习大概率是无效学习,而实践也是学习最有效的手段。在刚开学学习Golang时,找了一份基础语法的文档,花一两个小时看了一遍,知道常见的语法结构怎么用的,便开始搭建项目,写业务功能。其实这样的效果最快,以具体的功能实践来驱动学习,同时把对这方面的手感和思路锻炼出来。


第二、系统学习。单纯动手实践的过程中会掺杂着业务逻辑的实现,学习效率和范围上会有一些局限,属于用到什么学什么,缺点是不够系统。这时还需要一两本书,通读全书,帮助系统的了解这门语言(或某个行业)是怎么运作的,整个生态是什么样的,底层逻辑是怎样的,以便查漏补缺。在系统学习这块,建议以书籍为主,书籍的优势就是方便、快捷、系统、准确。


第三、交流。之前找一个懂的大佬请教和交流不是那么容易。但随着AI的发展,交流形式不仅仅限于大佬了,也可以是GPT。GPT最强大的能力是无所不知,知无不言。当然,对于它提供的结果也需要辩证的去看,某些地方可能会有错误,但大方向基本上是没错的,再辅以佐证,基本上能够解决80%的问题。


如果有机会参与面试,无论是作为面试官或者被面试者,都是一个交流的过程。在相互沟通的过程中了解市场需要什么,市场流行什么。


最后,针对某些问题,还是得去跟大佬交流才行,交流的过程中会碰撞出很多火花来。比如,不断的迭代某个算法,学到更好的实现方式,了解到你不知道的知识点等。曾经,一个字符串截取的功能,与大佬交流了三次,升级了三版,也学到了不同的API的使用方法和特性。


第四,输出。检验是否学会的一个标准就是你能否清晰的给别人描述出来,让别人听得懂。这一条是否很耳熟?对,它就是费曼学法,世界公认的最快的学习法。如果没办法很好的表达,说明这块掌握的还不是很清楚。当然,这个过程中也属于交流,也会拿到别人的反馈,根据别人的反馈来认识到自己的掌握程度和薄弱点。


第五,利用别人的时间。个人的时间总是有限的,不可能什么事情都自己做,也不可能都亲手验证。而作为管理者,最大的技能之一就是靠别人、靠团队来实现目标。那么,一个技术方案是否可行,是否有问题,也可以交给别人来调研、实践、验证。这样,可以让学习的效率并行起来。


另外,我们可能都听说过“一万小时定律”,这个概念是极具迷惑性的,会让你觉得学习任何东西都需要花费大量的时间的。其实不然,一万小时定律指的是学习一个复杂的领域并且成为这个领域的专家。


而我们在生活和实践的过程中,往往不需要什么方面都成为专家,只需要知道、掌握或会用某一领域的知识即可。对于入门一个新领域,一般来说,可能只需要20小时、100小时不等,没有想象中那么难。对于一个懂编程语言的人来说,从零学习另外一门语言,一般也就一两周时间就可以上手了。因此,我们不要对此产生畏惧心理。


上面讲的是学习方法,但最根本的是学习的意愿。你是选择花一年时间学习一门技术,然后重复十年,还是愿意每年都不断的学习迭代自己?两者的结果差距超乎你的想象。


作者:程序新视界
来源:juejin.cn/post/7257285697382449189
收起阅读 »

使用单例模式管理全局音频

引言 在现代Web应用中,音频播放是一项常见的功能需求。为了更好地管理全局音频,确保在页面切换、隐藏等情况下能够得到良好的用户体验,我们需要一种可靠的音频管理方案。本文将详细介绍一种基于单例模式的全局音频管理器,使用TypeScript语言和Howler库实现...
继续阅读 »

引言


在现代Web应用中,音频播放是一项常见的功能需求。为了更好地管理全局音频,确保在页面切换、隐藏等情况下能够得到良好的用户体验,我们需要一种可靠的音频管理方案。本文将详细介绍一种基于单例模式的全局音频管理器,使用TypeScript语言和Howler库实现。


背景


在开发Web应用时,往往需要在全局范围内管理音频播放。这可能涉及到多个组件或页面,需要一种机制来确保音频播放的一致性和稳定性。单例模式是一种设计模式,通过保证类只有一个实例,并提供一个全局访问点,来解决这类问题。


单例模式的优势


避免多次实例化


单例模式确保一个类只有一个实例存在,避免了不同部分对同一个资源进行多次实例化的情况。在音频管理器的场景下,如果允许多个实例存在,可能导致不同部分播放不同的音频,或者相互之间干扰。


全局访问点


通过单例模式,我们可以在整个应用中通过一个全局访问点获取音频管理器的实例。这使得在不同组件或模块中都能方便地调用音频管理器的方法,实现全局统一的音频控制。


统一状态管理


单例模式有助于统一状态管理。在音频管理器中,通过单例模式,我们可以确保整个应用中只有一个状态(例如是否正在播放、页面是否可见等)被正确地管理和维护。


技术实现


类结构与构造函数


首先,让我们看一下AudioManager的类结构。它包含一个私有静态实例,一个私有音频对象,以及一些控制音频播放状态的属性。构造函数是私有的,确保只能通过静态方法getInstance来获取实例。


class AudioManager {
private static instance: AudioManager;
private sound: Howl | undefined;
private isPlaying: boolean;
private isPageVisible: boolean;

private constructor() {
// 构造函数逻辑
}

// 其他方法和事件处理逻辑
}


构造函数中,我们初始化了一些基本属性,如isPlaying(是否正在播放)和isPageVisible(页面是否可见)。同时,通过visibilitychange事件监听页面可见性的变化,调用handleVisibilityChange方法处理相应逻辑。


单例模式实现


接下来,我们看一下如何通过单例模式确保只有一个AudioManager实例存在。


public static getInstance(): AudioManager {
if (!AudioManager.instance) {
AudioManager.instance = new AudioManager();
}
return AudioManager.instance;
}


通过getInstance方法,我们能够获取到AudioManager的唯一实例。在这个方法内部,我们检查instance是否已经存在,如果不存在,则创建一个新的实例。这确保了在应用中任何地方获取到的都是同一个实例。


页面可见性处理


在构造函数中,我们通过visibilitychange事件监听页面可见性的变化,并在handleVisibilityChange方法中处理相应逻辑。


private handleVisibilityChange(): void {
this.isPageVisible = !document.hidden;

if (this.isPageVisible) {
this.resume();
} else {
this.pause();
}
}


这部分逻辑确保了当页面不可见时暂停音频播放,页面重新可见时恢复播放状态,从而提升用户体验。


音频播放控制


play、stop、pause、resume等方法用于控制音频的播放状态。


public play(url: string): void {
// 音频播放逻辑
}

public stop(): void {
// 音频停止逻辑
}

public pause(): void {
// 音频暂停逻辑
}

public resume(): void {
// 音频恢复播放逻辑
}


在play方法中,我们通过Howler库创建一个新的音频对象,设置其来源和播放结束的回调函数。其他方法则用于停止、暂停和恢复音频的播放。


使用示例


全部代码:


import { Howl } from 'howler';

class AudioManager {
private static instance: AudioManager;
private sound: Howl | undefined;
private isPlaying: boolean;
private isPageVisible: boolean;

private constructor() {
this.isPlaying = false;
this.isPageVisible = !document.hidden;

document.addEventListener('visibilitychange', () => {
this.handleVisibilityChange();
});
}

public static getInstance(): AudioManager {
if (!AudioManager.instance) {
AudioManager.instance = new AudioManager();
}
return AudioManager.instance;
}

private handleVisibilityChange(): void {
this.isPageVisible = !document.hidden;

if (this.isPageVisible) {
this.resume();
} else {
this.pause();
}
}

public play(url: string): void {
if (this.isPlaying) {
this.stop();
}

this.sound = new Howl({
src: [url],
onend: () => {
// 音频播放结束时的回调
this.isPlaying = false;
// 在这里可以添加其他处理逻辑,例如停止或切换到下一个音频
}
});

this.sound.play();
this.isPlaying = true;
}

public stop(): void {
if (this.sound) {
this.sound.stop();
this.isPlaying = false;
}
}

public pause(): void {
if (this.sound && this.sound.playing()) {
this.sound.pause();
}
}

public resume(): void {
if (this.sound && this.isPlaying && this.isPageVisible) {
this.sound.play();
}
}

public getSound(): Howl | undefined {
return this.sound;
}
}

export default AudioManager.getInstance();


最后,让我们看一下如何在应用中使用这个全局音频管理器。


import AudioManager from './AudioManager';

// 播放音频
AudioManager.play('https://example.com/audio.mp3');

// 暂停音频
AudioManager.pause();

// 恢复音频
AudioManager.resume();

// 停止音频
AudioManager.stop();


通过引入AudioManager并调用其方法,我们可以方便地在应用中管理全局音频,而无需关心实例化和状态管理的细节。


应用场景


多页面应用


在多页面应用中,全局音频管理器的单例模式特性尤为重要。不同页面可能需要协同工作,确保用户在浏览不同页面时音频状态的一致性。


// 在页面1中播放音频
AudioManager.play('https://example.com/audio1.mp3');

// 切换到页面2,音频状态保持一致
AudioManager.resume();


组件化开发


在组件化开发中,不同组件可能需要协同工作以实现统一的音频控制。单例模式确保了所有组件共享同一个音频管理器实例,避免了冲突和不一致的问题。


// 在组件A中播放音频
AudioManager.play('https://example.com/audioA.mp3');

// 在组件B中暂停音频,整体状态保持一致
AudioManager.pause();


页面可见性


通过监听页面可见性的变化,我们确保在用户切换到其他标签页或最小化应用时,音频能够自动暂停,节省系统资源。


// 页面不可见时,自动暂停音频
// 页面重新可见时,自动恢复播放

结语


通过单例模式,我们实现了一个可靠的全局音频管理器,有效解决了在Web应用中音频播放可能遇到的问题。通过对代码逻辑的详细解释,我们希望读者能够更深入地理解这一设计模式的应用,从而在实际项目中更好地运用和扩展。同时,使用Howler库简化了音频操作的复杂性,使得开发者能够更专注于业务逻辑的实现。希望本文对您理解和使用单例模式管理全局音频有所帮助。


作者:一码平川哟
来源:juejin.cn/post/7303797715392479284
收起阅读 »

[compose] 仿刮刮乐效果

web
需求 下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。 分析 传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并...
继续阅读 »

需求


下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。


分析


传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并。


大致思路:

1、使用onDraw()方法中的Canvas绘制底层中奖层

2、在上面绘制一个蒙版层Bitmap, 在蒙版层Bitmap里面,放置一个新的Canvas

3、绘制一个灰色的矩阵,绘制一个path,将paint的Xfermode设置为 PorterDuff.Mode.DST_IN
4、手指移动时,更新Path路径


Compose实现

1、通过实现DrawModifier,重写draw() 方法

2、绘制原始内容层,drawContent()
3、绘制蒙版和手势层,


//配置画笔 blendMode = Xfermode
private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

drawIntoCanvas {
//设置画布大小尺寸
val rect = Rect(0f, 0f, size.width, size.height)
//从原始画布层,转换一个新的画布层
it.saveLayer(rect, layerPaint)
//设置新画布大小尺寸
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
//绘制手指移动path
it.drawPath(startPath, pathPaint)
it.restore()
}

完整代码


fun ScrapeLayePage(){
var linePath by remember { mutableStateOf(Offset.Zero) }
val path by remember { mutableStateOf(Path()) }
Column(modifier = Modifier
.fillMaxWidth()
.pointerInput("dragging") {
awaitEachGesture {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
//按住时,更新起始点
Press -> {
path.moveTo(
event.changes.first().position.x,
event.changes.first().position.y
)
}
//移动时,更新起始点 移动时,记录路径path
Move -> {
linePath = event.changes.first().position
}
}
}
}
}
.scrapeLayer(path, linePath)
) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.mipmap.cat),
contentDescription = ""
)
Text(text = "这是一只可爱的猫咪~~")
}
}

fun Modifier.scrapeLayer(startPath: Path, moveOffset: Offset) =
this.then(ScrapeLayer(startPath, moveOffset))

class ScrapeLayer(private val startPath: Path, private val moveOffset: Offset) : DrawModifier {

private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

private val layerPaint = Paint().apply {
color = Color.Gray
}

override fun ContentDrawScope.draw() {
drawContent()
drawIntoCanvas {
val rect = Rect(0f, 0f, size.width, size.height)
//从当前画布,裁切一个新的图层
it.saveLayer(rect, layerPaint)
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
it.drawPath(startPath, pathPaint)
it.restore()
}
}
}

图片.png

参考资料



作者:Gx
来源:juejin.cn/post/7303075105390133259
收起阅读 »

技术管理经验

做了几年的技术管理,简单几点经验可以分享交流一下,管理比起程序还复杂很多,因为一个面向人,一个面向机器,不能以相同的方式来思考,重点是找到平衡,不能用一种方式解决所有问题;好的管理者日常要看的面相非常多,尤其是一、二线带团队,不仅要关注好每个人,同时还要能做好...
继续阅读 »

做了几年的技术管理,简单几点经验可以分享交流一下,管理比起程序还复杂很多,因为一个面向人,一个面向机器,不能以相同的方式来思考,重点是找到平衡,不能用一种方式解决所有问题;好的管理者日常要看的面相非常多,尤其是一、二线带团队,不仅要关注好每个人,同时还要能做好承上启下的作用,要做到面面俱到难度十分高,以下就根据几个维度来分享我自己的经验。


做一个自己都喜欢的团队


每个人带的团队都会有自己的风格,我对自己的要求就是创造自己都愿意待的团队,至于什么是我自己喜欢的团队,简单归类了几个要点,有目标、有空间、有坚持,有明确且有挑战的目标,有足够的空间和舞台来发挥所长,有耐心能坚持做正确的事,能有机会和一群伙伴来达成目标真的是再幸福不过的事,这过程肯定会遇到无数的问题,只要确认不断保持向前,坚持就是胜利✌️


找到共同的目标,坚持做对的事


很多时候团队会存在支持多个目标/业务,这时候就必须类似技术抽象一样的来找到团队内的最大公约数,不一定只有一个但必须要有,核心点还是找到共同的目标,有了共同目标、愿景后可以更好的凝聚方向。


如果一个管理者只是在做分配资源的事,那么还不如不要这个管理者,毕竟传声筒的工作完全可以交给系统解决,例如团队 OKR 只是把每个小方向聚合,那么团队大概率是没方向,等待着别人来推进。


换位思考,保持沟通,面对的是人不是机器


任何沟通最重要的就是换位思考,如果能做到这件事大概率沟通上可以减少很多问题;沟通的过程重点应该是通,关键点不是自己说了多少或做了多少,而是对方获得了多少,多理解对方背后考量的因素,去解决问题而不是解决出问题的人。


过去常听到对于技术人的评价是 “无法沟通”,这种情况大概率是双方没找到频率,每个人都有自己习惯的沟通方式,作为管理者应该要在团队内帮助大家达成一种沟通默契,不应该要求所有人都能使用同一种方式来交流,以及该有的底线划清楚;非常多优秀的人是很有个性的,懂得如何把这样的人摆好位置,会比起把这样的人驯服更有意义,只要在影响范围可控的情况下,应该多花点时间思考在整体排兵布阵。


团队成长 > 个人成长


带团队后最大的不同点就在于需要思考的是整个团队的产出,而不是自己的产出;尤其是原先自己是同团队最优秀的成员因此被提拨出来作管理,自己原先是其他人的 150% 以上的贡献度,但开始带团队后必须要能接受自己的产出会大幅下降,可能自己剩下 50%,团队其他人提升 20%,达到团队整体产出提升。


放权、给予试错空间很重要,想要让团队成员成长势必要让他逐步能承担起更重要的责任与任务;以战养兵是在工作中很好的方案,过程给予适度的辅助与容错,很多错没经历过是不会懂,读万卷书不如行万里路。


建团队,招聘 培养 汰换


建团队是每个管理者都会面对的问题,团队人才密度、人才水平都会决定采用不同的管理方式,其中招聘我认为是最重要的,这关卡的越严格后面就会越轻松,强烈建议这关必须把握好底线,水桶永远会从最低的那一边开始漏水。


招聘是一门大学问,坊间也有很多相关的书籍介绍,并且有专门的服务和职位,在这就以用人的部分来谈谈,一般我自己决定用人就两个原则,“我是否愿意汇报给他/她?”或 “我是否愿意把背后交给他/她?” 这两个问题概括大量的信息和判断,包含当前/未来是否需要这样的能力、团队匹配度、潜力和领导力等,团队整体布局上最好能保持更多元,并且别怕团队成员能力超越自己,就像曾子说的 “用師者王,用友者霸,用徒者亡。”


培养简单可以分为两块专业和软素质,专业部分培养一般相对容易,刚好是相同专业可以直接指导,如果是不同的领域可以找外部进来帮忙,软素质部分大多还是依靠团队日常培养,这块重点主要还是言传身教,身为主管日常的一举一动都会成为团队同学的标杆,因此必须对于自己的言行有一定的要求,尤其是公开场合。


汰换对管理者来说是个很重要的责任,尤其在面对这个环节一定要慎重的考量,要对于每个汰换最好都能有记录留底,帮助自己复盘;一般来说必须找到当前的瓶颈,究竟是卡在事还是人,如果是卡在人,也必须分析清楚原因,因为造成团队不稳定因素?潜力?阻碍团队发展?包含考虑这个瓶颈是不是自己。


1 to 10,10 to N,定义规则


团队 <10 的时候可与所有团队成员都非常的亲近,能明确的知道每个人在做些什么,甚至是团队成员家里的猫受伤了都能知道,在这个时期管理的负担并不是特别大,至少能有一半的时间能在执行日常工作,这时期可能会出现蜡烛两头烧的情况,必须要能尽快的找到平衡点,不需要扣每个细节,但要抓准方向。


团团在 >10 后在 <=15 可能整体变化不大,不过可以很明显的感受到管理成本提升,这个时期会慢慢的开始发现到靠着过去手把手了解细节方式精力会严重不足,需要靠着各种规则、规范来达到找寻最大公约数。


随着管理的范围越大会发现想要知道每个同学的情况不现实,靠着规则来减少管理精力,引导团队走向正确的方向,这其中会有很多的权衡,出现很多错误解读或过度解读信息的问题,通过不断的调整来达到平衡,然而这个平衡是来自于成熟度及规模。


Netflix 的团队有出了一本《零规则》,描述的是大多更像是以原则来取代规则减少理解成本,相信每个人都能按照规则走,再定期抽查的机制保障,从书中来看是可行的,为了适配不同地区文化不同也做了适度的调整,是一种感觉可行的模式,但还是要根据当前团队情况调整。


绩效管理


绩效只是管理的工具和手段,通过这个工具来帮助团队发展和个人提升,让每个同学知道自己阶段的优缺点以和校正方向,让好的更好,阶段性不好的同学能找到方向,达到个人和团队的成长。


绩效管理一定要记得 “功在平时”,绝对不能把绩效管理作为周期性的工作处理,日常付出的越多在绩效评分的时候问题就越少,同时也能帮助绩效沟通上更顺畅,把事情放到最后一刻很容易造成意外,一个不小心就成了事故。


不能忘本


大多技术管理者都是从 IC 开始,保持手感很重要,随着管理的幅度增加会大量降低自己的时间,最好还是能保持着写代码的手感,过往的经验能帮助增加判断力,但技术是不断在翻新,缺少了手感短期可能问题不大,要是习惯了会逐步脱节,严重点就会出现 “技术官僚” 问题,毕竟迭代这么快,哪怕知道原理也容易出现多个类似产品出现后该如何更好的判断问题。


多体验自己做出来的东西,感受技术带来的价值,相信自己每一行代码都在提升人的生活水平,而不是完成一个个任务;技术是个很不同的工种,除了使用工具还能够创造新的工具,为别人提升效率同时也能为自己提升效率。


小结


修身 齐家 治国 平天下,先能以身作则才能更好的要求其他人,让更多人能一起朝着相同方向前进,就像优秀的技术管理者大多也会是个优秀的 IC;保持同理心,多换位思考,帮助更多人激发潜力,各自发挥所长并找到其中的平衡点,这门学问就像是艺术一样的有趣。


到今天仍然觉得自己不是个合格的管理者,还有很多事要学,还有很多要改,没办法做到完美,只能尽可能在前往更好的路上加速前进着,欢迎各种交流讨论,互相学习🤝


作者:iskenhuang
来源:juejin.cn/post/7268301435066368035
收起阅读 »

程序员接外包的三个原则以及有意思的讨论

原则一:乙方来做决策 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接 因为在这种甲方眼里,你只是“施工方”,他...
继续阅读 »

原则一:乙方来做决策



  • 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了

  • 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接

  • 因为在这种甲方眼里,你只是“施工方”,他们即不需要你的经验价值,更不会为你的经验买单。所以这种甲方你会做得很累,当他们觉得“你的工作强度不足以匹配付给你的费用时(他们总这样觉得)”,他们就会不停地向你提出新的开发需求

  • 所以,你要尽量找那种尊重你经验价值,总是向你请教,请你帮他做决策的甲方


原则二:为甲方护航



  • 甲方未来会遇到什么问题,你们双方其实都不知道,所以你需要一边开发一边解决甲方实际遇到的问题。

  • 因此,不要为了完成合同上的工作内容而工作,要为甲方遇到的实际问题,为了甲方的利益而开发,要提前做好变通的准备。

  • 永远不要觉得你把合同上的功能列表做完,你就能休息了。你要解决的真正问题是,为甲方护航,直至甲方可以自己航行。


原则三:不做没有用户的项目



  • 如果甲方的项目没有太多用户使用,这种项目就不要接。

  • 除了代码的累计经验,还有一种经验也很重要,那就是“了解用户的市场经验”

  • 只有真正面对有实际用户的项目,你才能有“解决市场提出的问题”的经验,而不是停留在“解决甲方提出的问题”

  • 拥有市场经验,你就会有更高的附加价值,再配上尊重你经验价值的甲方,你就会有更高的收入

  • 永远记住:真正愿意在你身上花钱的甲方,他的目的一定是为了让你帮他赚钱!


以上只是我根据自己经验的一家之言,可能对我有用,不一定对别人也有用。肯定还有很多有价值的原则,希望大家根据自己的经验一起来分享。


下面是一些有意思的讨论


原则 2 、3 都是虚的,就不讨论了。

只说原则一:


一般而言,甲方跟你对接的,一定不是老板。

所以他的核心目的一定是项目实施成功。

但项目是不是真的能给企业带来效益,其实是优先级特别低的一个选项。


拿日常生活举个例子。夫妻、情侣之间,你媳妇儿托你办个事,比如让你买个西瓜。

你明知道冬天的西瓜又贵又不好吃,你会怎么办?


A ,买西瓜,回去一边吃西瓜一起骂水果摊老板没良心。

B ,给你媳妇儿上农业课。然后媳妇儿让你跪搓衣板。

C ,水果摊老板训你一顿,以冬天吃白菜豆腐好为由,非卖你一颗大白菜。你家都不敢回。


这里面,你就是那个甲方对接人。你怎么选?


所以乙方一定不能做决策。乙方做决策的结果,就是甲方对接人被利益集团踹开或者得罪甲方对接人,最终导致项目失败




我也来说三个原则

1.要签合同,合同越细越好;

2.要给订金,订金越多越好;

3.尾款不结不给全部源码。




原则一:外包大部分就是苦力活,核心有价值的部分有自己公司的人干轮不到外包,你不干有的是人干,不会有人尊重你经验价值,甲方说怎么干就怎么干,写到合同里,按合同来,没甲方懂自家业务,别替甲方做决策,万一瞎建议导致项目出现大问题,黄了,外包钱都拿不回来


原则二:给多少钱办多少事,如果甲方给钱痛快,事少,可以看自己良心对甲方多上点心,否则别给自己加戏,不然很可能把自己感动了,甲方却想着好不容易碰上这么个人,白嫖


原则三:不做没有用户的项目,太片面,不是所有外包项目都是面对海量用户,但是做所有外包项目都是为了赚钱,假如有个富二代两三万找你做个毕设,简单钱多不用后续维护,这种接不接?假如某工厂几十万定制内部系统,可能只有几个人用,这种接不接


总之外包就是赚个辛苦钱,别指望这个来提升自己技术和自我价值,外包行业水太深,你这几个原则都太理想化




某富豪要盖一栋私人别墅,招建筑工人,现在缺一名搅拌水泥的工人,

找到了张三,张三说我去过很多工地,啥活儿都干过,经验极其丰富,我可以指导一切事物,我再给你兼职当个总设计师吧,一切事物听我的决策没错。

我每天做完我的本职工作搅拌水泥砂浆,我还能熬夜给建筑设计布局,风水,房间规划,材料采购等等,我啥都会,直接干到建筑完工

富豪很感兴趣,说那你来吧,我盖的是自己住的别墅,张三一听连连摆手:你是盖私人别墅啊?不行不行,我不去了,我以前盖的都是高楼大厦,住户多对我技术水平有严峻的考验,做成了对我有很大提高,私人别墅才几个人用,对我职业生涯一点帮助都没




永远不要接外包

这才是正确的答案

做私活的时间

不如自己休息休息,陪陪家人




屁事真多,有钱就行了,管他项目有没有人,人家产品低能你还得兜底,接外包考虑的是能不能满足需求。啥条件啊还能挑三拣四,给多少钱干多少活。 招投标接的 30 万以上的项目才有可能考虑你说的这些东西。





呵呵 我的意见是:



  1. 给钱就做(前提是合规,不是合理),先给定金,拿到定金开工。

  2. 遇到扯皮,就停止开发。

  3. 要有空闲的时间,偶尔做做(上面说的对:永远不要做外包)。


展开来说,做外包的长期收益很低。就当临时玩一下,所以给钱就做,不管你的需求合理不合理,比如甲方想给智障人士开发一款数独小游戏,好,给钱,签合同,支付定金,开工。


开工了3天,甲方突然说,那个我想加个魔方游戏。不好意思,不行,立即停止开发,开始和甲方掰扯,如果掰扯不明白,就终止合同,如果掰扯明白就继续。


不说了,我要和甲甲甲甲甲方掰扯去了。


作者:Data_Adventure
来源:juejin.cn/post/7256590619412676663
收起阅读 »

再高级的打工人也只是打工人!

再高级的打工人也只是打工人! OpenAI CEO 奥特曼被罢免的事情人尽皆知「虽然,今天又复职了。。」,我们能从中学到什么呢? CEO 也能被裁,这应该是最近几年被裁名单里面,职级最高的一个人了吧。你再也不用担心给别人说自己被裁有多丢人了,因为 CEO 都...
继续阅读 »

再高级的打工人也只是打工人!


OpenAI CEO 奥特曼被罢免的事情人尽皆知「虽然,今天又复职了。。」,我们能从中学到什么呢?



CEO 也能被裁,这应该是最近几年被裁名单里面,职级最高的一个人了吧。你再也不用担心给别人说自己被裁有多丢人了,因为 CEO 都会被裁。


另外,我想说,再高级的打工人也只是打工人!


作为打工人,被裁,尤其是这几年,是在是再正常不过的事情。尽管 CEO 的罢免和总裁的辞职对于外界来说可能是一个令人震惊的消息,但在公司内部,这些变动可能只是公司内部的调整和重组的一部分。


因为,再高级的打工人对于整个公司来说,都不是不可替代的。甚至,连公司的老板也不是不可替代的。国内很多大公司都开始这么培养接班人,离开了老板,公司照常运转的公司,才是一个体制健全的公司,这样的公司才能实现基业长青!当然,老板的不同在于,自己能决定自己什么时候离开。


不管是谁,当公司想裁你的时候,你就不再重要,就跟我上次被裁一样,上午还在写需求、改 bug,下午 6 点多通知被裁,第二天就不用来了!虽然这个对比优点不恰当,毕竟我对于公司来说只是一个普通打工人。


一声被裁,马上就得搬着东西走人。



当然,对于奥特曼(Sam Altman)这样的人来说,被裁不像我们大多数人一样需要考虑下一份工作、生活有没有保障、房贷能不能还上等一系列生存问题。


但是这件事也提醒了我们,无论我们的职位有多高级,我们仍然处于职场中不可忽视的变化之中。因此,我们应该时刻保持敏感和警觉,不断更新自己的知识和技能,以应对潜在的职业变动和挑战。


尤其是对于很多大厂中低层管理者而言,放弃了技术,管理能力也不见得有竞争力,毕竟国内大部分管理者都是野路子,没有经过系统的学习与培训。


如果你放弃了技术,之后因为各种原因被裁,那你可能还不如底层员工,他们还能更好的获取下一份工作,毕竟金字塔的底层岗位最多。


那我们能做什么呢?


做一家「一人企业」。什么是「一人企业」,就是把自己当做一个企业、一个产品来运营,来推销,把自己的能力最大化。


什么人适合做「一人企业」呢?借用饭大的话来说:主业稳定、有时间&有乐趣、不躺平&能利他。



所以,我不是在劝你无脑去干,而是满足了这三个条件,你再去干,你也应该是去干。


当自己的老板!


作者:伍六七AI编程
来源:juejin.cn/post/7302644330883727360
收起阅读 »

一个30岁专科java猿自白,对苍白的现实不妥协!!!

🙈 序言 时光弹指一挥,三十而立的年纪,生活在自己眼里,可以说一地鸡毛… 我通过这篇文章来分享我的经历,在软件行业栉风沐雨的六年… 🚂 茫 16年专科毕业,确切的说是在英雄联盟游戏里毕业,夙兴夜寐的三年,在游戏、吃喝上忙碌,一周7天,五个通宵。就这样,从学校出...
继续阅读 »


🙈 序言
时光弹指一挥,三十而立的年纪,生活在自己眼里,可以说一地鸡毛…
我通过这篇文章来分享我的经历,在软件行业栉风沐雨的六年…


🚂 茫
16年专科毕业,确切的说是在英雄联盟游戏里毕业,夙兴夜寐的三年,在游戏、吃喝上忙碌,一周7天,五个通宵。就这样,从学校出来,开始做销售,第一份工作,卖鞋油,给别人擦鞋,三月后,去了企业管理咨询公司,卖课,做了半年,兜里比脸还干净,又去做保险,更惨淡,就这样,17年,踏上北漂之路!


image.png


🪂 17 入行


17年三月,清楚记得自己报道的日子。七天的基础班,倒数第一名的成绩,差点把我劝退,硬着头皮坚持,扛下来了,我是最笨的人,但也是最努力的人,培训的那半年,每天早八点,晚12的敲代码。只因为,这是自己唯一的退路!半年后,我是班里第二个找到工作的人,入职某外包公司,现在想想自己那时候的选择,还是有一些遗憾。


image.png


☠ 18 踩坑最多的一年
拿到第一个月工资的时候,简直不敢相信,激动的和家里人分享。如果不做这个行业,我觉得月薪1万1很可能就是做梦,现在我也能理解到认知那句话了。这算是第一次找到了自信,在工作上有了结果。工作没想象的难,但也非常难受,学习能力方面确实和本科是有差距,纯野路子,没有什么专业基础,为此,18年,加了很多班,通宵达旦的修复很多自己写的Bug,后来,能力得到认可,当然,这是一点一滴的积累,血和泪只有自己知道。


🔥 19 裸辞
18年上半年买了房子,开始背负房贷,又贷款买了车位,这个时候每月贷款就需要还6K,去掉房租,生活费,身无分文,很多个夜晚挣扎着无法入睡,在压力驱使下,不得不找一个薪水更高一些的工作,找工作的过程对我的打击很大,找了将近一个月,才得到一份工作,比原来高2K,14薪,不过是内部员工,这一点是值得欣慰的。在找工作的过程中能明显感觉到社会对专科的歧视很大,因为有很多聊的很好的场面,但都是因为学历失之交臂!
这一年,我成了开发组长,管理四五个人开发,说组长感觉又不太合适,因为自己的开发任务也比较大,就算这样,也还好,因为房贷压力小了,能腾出手忙一点发展的事儿了,这个时候,没有时间谈个人的私事,这一年,工作中忙碌,兼顾生活,贷款,家庭,遇到了很多研发方面的困难,不过都一一克服了,还获得了客户的好评,这儿就不细说了,后续博客会写一部分。


🤡 20 裸辞
这一年,房子交房了,要装修,除了贷款的压力,还得装修房子,在工作上,已顺风顺水,有啥困难都能解决,个人能力团队也都很认可,但薪水成了一个生活发展的阻碍,为此向公司提了一下涨薪,被驳回了。然后我又提出裸辞,公司开始打算涨薪挽留,我拒绝了!然后我又去了外包公司,只为多拿点钱,让压力小一些,把家里的事情处理完。


下图来纪念以下公司的前台!!!


image.png


🦧 21 沉淀
裸辞也算非常顺利,半月的时间就接上了,薪资的话18K。工作很轻松,很多任务都能快速高效完成,在这个空闲的时间里,也没有闲着,会读读书,思考一下人生的意义!以下是我读完的书!


image.png


读书给我带来了一些动力和精神的慰藉,我也没有想到,一个大学不务正业的人能够静下来看书看到一千多个小时!我读的书没有太多技术方面的,在那个时候最大的作用应该是抵抗那些来自生活得压力和孤独吧。这一年的收获和前几年相比来说,应该是人更平和了,不在那么功利和浮躁,开始看淡一些物质相关的东西!


🧸 22 被解雇
作为一个外包公司,服务的项目做完了,又赶上疫情,被解雇也算是顺理成章的事情,这个时候也没有埋怨,也赶上春节,在家里呆了半个月,就开始投递简历,心里想的是在奋斗一年,找个离家近一点的工作。这个时候的就业环境已经不太乐观了,就学历门槛就很卡了,复习了几天,然后面了三四家吧,就又踏上北漂征程,这次也是外包公司,薪资22K。
三月入职,9月晋升,薪水涨到24.3K,工作上没怎么加过班,工作时间内也都能很出色的完成任务,这一年的总结就是遇到了很多优秀的人,学到了很多方法论,自己更能独立解决问题,在研发层面能够结合业务提出更多的解决方案,的确,这一年做方案的能力很强了。


🎨 23 前篇裸辞
23年五月,通过了PMP项目管理考试,顺利获得证书。作为一个山东人,深受孔孟思想的影响,不孝有三,无后为大!如今也快30岁了,别说结婚,对象的影子也没见!这个压力也慢慢加到了生活了。当我感受到痛苦和不想漂泊的时候,今年五月底,我递交了辞职申请!
离职需要在呆一个月,处理一下自己手头的尾部工作和交接事宜,7月1号,踏上了回家的征程。我很感谢这个公司,让我经历压力没那么大了,房子装修完了,就剩贷款了!
用此图来纪念一下!!!


image.png


🎨 23 中篇挣扎
回到家,作为一个驾-照拿了八年没摸过车的人,这个时候买了车,找了个教练,练了练车,差不多了,就开始闭关一月,学习,看视频,属于恶补吧。
8月开始投递简历,本是泰安人,想在家乡谋一份差事,为家乡发展做点贡献,自己钱包也多一点,这个双赢的机会在我面了两三家公司后打消了念头。
开始投递济南的公司,面了一家,项目经理职位,16k,就去工作了,也是一家外包公司。局方公司背景还是不错的!
工作了三个月,我又裸辞了,原因是太累,在提升自己层面上,得到的比较少。因为有贷款的压力,我辞职是做了很大心理建设的!


image.png


🔅 23 后篇 展望
辞职后,无缝衔接了一家公司,就是专门做研发,薪资12K,大小周。没有嫌弃,没有抱怨。目前自己的想法很简单,就是打造自己的产品,能够拿得出手一些东西,比如博客,比如项目,比如情感的一些价值!以前的自己好高骛远,现在的自己更脚踏实地,一步一个脚印,把自己的价值分享出来,成就别人,也成就自己!


⚜ 未来我会怎么做



  • 做好自己的本职工作-核心

  • 少花一点时间放在娱乐上

  • 多做一些有价值有产出的事儿 比如博客 比如读书分享 比如研究新项目

  • 跟随趋势走 拥抱变化

  • 更好的利用技术而不是沉迷技术

  • 更平和的心态处事

  • 简单的态度生活处世 己所不欲 勿施于人


作者:码农佩奇
来源:juejin.cn/post/7303720074842652709
收起阅读 »

微信小程序记住密码,让登录解放双手

web
密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明: 记住密码 在请求登录接口成功后,我们需要判...
继续阅读 »

密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明:


记住密码


在请求登录接口成功后,我们需要判断用户是否勾选记住密码,如果是,则将记住密码状态账号信息存入本地。
下次登录时,获取本地的记住密码状态,如果为true则获取本地存储的账号信息,将信息回填登录表单。

在这里插入图片描述

在这里插入图片描述


密码加密


我在这里例举两种加密方式MD5Base64
MD5:
1、不可逆
2、任意长度的明文字符串,加密后得到的固定长度的加密字符串
3、实质是一种散列表的计算方式


Base64:
1、可逆性
2、可以将图片等二进制文件转换为文本文件
3、可以把非ASCII字符的数据转换成ASCII字符,避免不可见字符
4、实质是 一种编码格式,如同UTF-8


我们这里使用Base64来为密码做加密处理。


npm install --save js-base64

引入Base64


// js中任意位置都可引入
let Base64 = require('js-base64').Base64;

可以通过encodedecode对字符串进行加密和解密


let Base64 = require('js-base64').Base64;

let pwd = Base64.encode('a123456');
console.log(pwd); // YTEyMzQ1Ng==

let pws2 = Base64.decode('YTEyMzQ1Ng==');
console.log(pwd2); // a123456

到这里我们对密码的简单加密和解密就完成了。
需要注意的是,Base64是可以解密的,所以单纯使用Base64进行加密是不安全的,所以我们要对Base64进行二次加密操作,生成一个随机字符串 + Base64的加密字符。


/***
*
@param {number} num 需要生成多少位随机字符
*
@return {string} 生成的随机字符
*/

const randomString = (num) => {
let str = "",
arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
let index = null;
for (let i = 0; i < num; i++) {
index = Math.round(Math.random() * (arr.length - 1));
str += arr[index];
}
return str;
}

调用randomString函数,根据你传入的数字来生成指定长度的随机字符串,然后将随机字符串与Base64生成的随机字符凭借,完成对密码的二次加密。


let pwd = randomWord(11) + Base64.encode(password); // J8ndUzNIPTtYTEyMzQ1Ng==

到这里就完成了密码加密操作。
在用户登录时,将账号密码存入本地,存入本地方式有很多,例如:CookieslocalStoragesessionStorage等,关于使用方法网上有很多,这里我们使用微信小程序的存储方式wx.setStorageSyn


// 我们这里使用微信小程序的存储方式wx.setStorageSync
let account = {
username: 'test‘,
password: pwd
}
wx.setStorageSync('
account', account)

在这里插入图片描述


二次登录


用户勾选记住密码后,第二次进入系统,直接从本地获取账号密码,对密码进行解密后回填到表单。
先判断用户是否勾选记住密码,然后对密码进行解密。


init() {
let state = wx.getStorageSync('rememberMe')
if (state) {
let account = wx.getStorageSync('account')
let Base64 = require('js-base64').Base64;
let pwd = Base64.decode(account.password.slice(11))
this.setData({
username: account.username,
password: pwd
})
}
this.setData({ rememberMe: state })
}

将解密后的数据回显到表单上,用户就可以直接登录了。


最后


关于记住密码业务,需要保证用户的密码是加密存储,这里用的是微信小程序示例,在web上的流程也是如此,你可以在vue项目中使用本文提到的方法。


作者:DCodes
来源:juejin.cn/post/7303739766106472488
收起阅读 »

登录是前端做全栈的必修课

web
如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。 1. 注册和登录 首先,用户需要通过注册和登录来获取访问令牌...
继续阅读 »

如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。


1. 注册和登录


首先,用户需要通过注册和登录来获取访问令牌。


1.1 注册接口


在注册接口中,用户提供必要的注册信息(如用户名和密码),服务器对用户进行验证并创建用户账户。


示例代码(Node.js + Express):


// 注册接口
app.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;

// 检查用户名和邮箱是否已被注册
if (users.some(user => user.username === username)) {
return res.status(400).json({ error: '用户名已被注册' });
}

if (users.some(user => user.email === email)) {
return res.status(400).json({ error: '邮箱已被注册' });
}

// 使用bcrypt对密码进行哈希处理
const hashedPassword = await bcrypt.hash(password, 10);

// 创建新用户对象
const user = {
id: Date.now().toString(),
username,
email,
password: hashedPassword
};

// 将用户信息存储到数据库
users.push(user);

// 创建访问令牌
const token = jwt.sign({ userId: user.id }, 'secretKey');

res.status(201).json({ message: '注册成功', token });
} catch (error) {
res.status(500).json({ error: '注册失败' });
}
});

1.2 登录接口


在登录接口中,用户提供登录凭据(如用户名和密码),服务器验证凭据的正确性并颁发访问令牌。


示例代码(Node.js + Express):


app.post('/login', (req, res) => {
// 获取登录凭据
const { username, password } = req.body;

// 在此处进行用户名和密码的验证,如检查用户名是否存在、密码是否匹配等

// 验证成功,颁发访问令牌
const token = createAccessToken(username);

// 将访问令牌写入 Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true, // 仅在 HTTPS 连接时发送 Cookie
sameSite: 'Strict' // 限制跨站点访问,提高安全性
});

// 返回登录成功的响应
res.status(200).json({ message: '登录成功' });
});

2. 接口校验登录态


在需要校验登录态的受保护接口中,服务器将校验请求中的登录凭据(Cookie 或访问令牌)的有效性。


示例代码(Node.js + Express):


app.get('/protected', (req, res) => {
// 从请求的 Cookie 中提取访问令牌
const token = req.cookies.token;

// 或从请求头部中提取访问令牌,如果采用前端存储和发送访问令牌方式
// const token = req.headers.authorization.split(' ')[1]; // 示例代码,需根据实际情况进行解析

// 检查访问令牌的有效性
if (!token) {
return res.status(401).json({ error: '未提供访问令牌' });
}

try {
// 验证访问令牌
const decoded = verifyAccessToken(token);

// 在此处进行更详细的用户权限校验等操作

// 返回受保护资源
res.status(200).json({ message: '访问受保护资源成功' });
} catch (error) {
res.status(401).json({ error: '无效的访问令牌' });
}
});

3. 自动化登录态管理


要实现自动或无感化的登录态管理,前端需要在每个请求中自动携带访问令牌(Cookie 或请求头部)。


3.1 使用 HTTP Cookie


当使用 HTTP Cookie 时,浏览器会自动将 Cookie 包含在每个请求的头部中,无需手动设置。


示例代码(前端使用 JavaScript):


// 发送请求时,浏览器自动携带 Cookie
fetch('/protected');

3.2 前端存储和发送访问令牌


当使用前端存储和发送访问令牌时,前端需要在每个请求的头部中手动设置访问令牌。


示例代码(前端使用 JavaScript):


// 从存储中获取访问令牌
const token = localStorage.getItem('token');

// 设置请求头部
const headers = {
'Authorization': `Bearer ${token}`
};

// 发送请求时,手动设置请求头部
fetch('/protected', { headers });

在上述示例代码中,我们使用了前端的 localStorage 来存储访问令牌,并在发送请求时手动设置了请求头部的 Authorization 字段。


请注意,无论使用哪种方式,都需要在服务器端进行访问令牌的验证和安全性检查,以确保请求的合法性和保护用户数据的安全。


补充说明:



  • createUser:自定义函数,用于创建用户账户并将其保存到数据库或其他持久化存储中。

  • createAccessToken:自定义函数,用于创建访问令牌。

  • verifyAccessToken:自定义函数,用于验证访问令牌的有效性。


写在最后


文章旨在答疑扫盲,内容简明扼要方便学习了解,请确保在实际应用中采取适当的安全措施来保护用户的登录凭据和敏感数据,保持学习,共勉~


作者:vin_zheng
来源:juejin.cn/post/7303463043249635362
收起阅读 »

从小米14安装不上应用说起【适配64位】

一、原因 某天早上,同事突然对我说我换了小米14pro手机但是安装不了公司的打卡软件,怎么办呀。一时间,我也不知道原因,看到给我发的安装不上的截图陷入了沉思。随即打开在git仓库里找到这个项目,到本地编译打开,开始思考解决办法。 二、解决思路 从网上查询了一番...
继续阅读 »

一、原因


某天早上,同事突然对我说我换了小米14pro手机但是安装不了公司的打卡软件,怎么办呀。一时间,我也不知道原因,看到给我发的安装不上的截图陷入了沉思。随即打开在git仓库里找到这个项目,到本地编译打开,开始思考解决办法。


二、解决思路


从网上查询了一番,小米14pro 只支持安装64位的应用,可能是老项目没有做64位的适配。等到项目编译好,打开模块下的build.gradle文件,果然如此,没有做64位的适配。


ndk {
abiFilters 'armeabi', "x86", "x86_64"
}

针对64位做适配,一般都是适配so库,一般来说,如果你的项目中没有使用到so库或者C,C++代码,都是支持64位的。
这里再做下说明,ABI是Application Binary Interface(应用程序二进制接口)的缩写,在Android中,它指的是Android操作系统与设备硬件供应商提供的库之间的接口。ABI定义了程序各个组件在二进制级别上的交互方式,其中一个重要的方面是处理器使用的指令集架构。Android支持多种ABI以适应具有不同硬件架构的设备。



  • ARM(Advanced RISC Machine):

    • ARM是移动设备中常见的架构。

    • 变体:armv5、armv7-A、armv8-A(64位)等。



  • x86:

    • x86是台式机和笔记本电脑中常见的架构。

    • 变体:x86、x86_64(64位)。



  • MIPS(Microprocessor without Interlocked Pipeline Stages):

    • MIPS架构在过去在一些Android设备中被广泛使用,但近年来变得不那么常见。




好了,回归到正题,就要针对项目中这种情况处理so库了,因为这个老项目是从其他项目演变过来的,用不到这些so库,所以我的解决办法就是全部删除掉(当然要对项目中的源代码进行处理),再进行打包处理。
如果你们处理项目中的没有兼容的so库,推荐一个检测插件EasyPrivacy,接入这个就可以方便查看那些so库没有做适配了。
找到没有适配的so库之后,需要找到提供者获取最新兼容的so库或者找到相关官网看是否提供64位的so库。当然代码中还需要进行处理。


ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', "x86", "x86_64"
}

这样子打包的时候,在apk中的libs文件夹下就会出现四个对应的文件夹,里面就是对应的so库了。但是这样会增大包的体积。在android{}中配置如下代码,这样子打包之后就会出现四种包对应不同架构的包,这样子包的体积也会减小。


splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
universalApk true //generate an additional APK that contains all the ABIs
}
}

回归正题,当我把打好的包发给同事的时候,同事告诉我还是不行。思来想去,排除64位的问题,那么剩下的只有Android版本的问题了。新出的手机肯定是搭载最新的Android版本,目前Android 14 有什么新变动还没有了解到。
image.png
从官网看到,映入眼前的第一条就是这个,检查项目中的代码,发现targetSdkVersion 还是16,怪不得安装不上,至此所有问题解决。


作者:风罗伊曼
来源:juejin.cn/post/7303741345323221044
收起阅读 »

陪伴了 14 年的 API 下线了

hi 大家好,我是 DHL。就职于美团、快手、小米。 Android 开发者应该深有感触,近几年 Android 每次的更新,对开发者的影响都非常的大,而这次 Android 14 的更新,直接让陪伴我们多年的老朋友 overridePendingTransi...
继续阅读 »

hi 大家好,我是 DHL。就职于美团、快手、小米。


Android 开发者应该深有感触,近几年 Android 每次的更新,对开发者的影响都非常的大,而这次 Android 14 的更新,直接让陪伴我们多年的老朋友 overridePendingTransition 下线。


这篇文章主要想介绍一下我们的老朋友 overridePendingTransition,它陪伴了我们度过了 14 年,如今也完成了它的使命,现已功成身退,这个方法在 Android 14 中被废弃了。



在 2009 年的时候,正式将 overridePendingTransition 添加到 Android Eclair(2.0) 源码中,Android 开发者对它应该有一种熟悉有陌生的感觉吧,我们刚开始学 Android 写 Activity 跳转动画的时候,都接触过这个。


Intent intent = new Intent(B.this, C.class);
startActivity(intent);
overridePendingTransition(R.anim.fade_in, R.anim.fade_out);

这段代码对每个 Android 同学都非常熟悉,而且至今在项目里面,到处都有它的身影。如果我们要为 Antivity 添加进入或者退出动画,那么只需要在 startActivity() 或者 finish() 方法之后立即调用 overridePendingTransition 即可。


14 年后的今天,Android 14 的横空出世 overridePendingTransition 也完成了它的使命,在 Android 14 的源码中正式被废弃了,感兴趣的小伙伴,可以打开 Android 14 的源码看一下。



当得知它都被废弃了,确实感到有些意外,源码中推荐我们使用新方法 overrideActivityTransition 代替 overridePendingTransition


我还以为是什么更好的方法,结果推荐的方法更加的难用,为了一个虚有其表的功能,废弃了这个 API,还给开发者增加了很大的负担。


按照 Android 官方的解释和源码中的说明,废弃掉这个方法是因为在 Android 14 中引入了新的返回动画,而 overrideActivityTransition 方法不能和它很好的做兼容,所以需要用新的方法去替换。


什么是新的返回动画


比如使用返回手势可以在应用后面显示主屏幕的动画预览。



小伙伴们一起来评评这个功能实用性怎么样,为了这个功能废弃掉我们的老朋友,如果是你,你会这么做吗?另外我们在看看新的 API 的设计。



新的 API 相比于旧 API 多了一个参数 overrideType,一起来看看源码中是如何描述这个参数 overrideType


For example, if we want to customize the opening transition when launching 
Activity B which gets started from Activity A, we should call this method inside
onCreate with overrideType = OVERRIDE_TRANSITION_OPEN because the Activity B
will on top of the task. And if we want to customize the closing transition when
finishing Activity B and back to Activity A, since B is still is above A, we
should call this method in Activity B with overrideType = OVERRIDE_TRANSITION_CLOSE.

If an Activity has called this method, and it also set another activity animation
by Window#setWindowAnimations(int), the system will choose the animation set from
this method.

翻译一下就是,每次想使用过渡动画,都必须告诉系统 overrideType 使用什么参数,比如当我们从 Activity A 打开 Activity B 时,需要使用参数 overrideType = OVERRIDE_TRANSITION ,当我们从 Activity B 返回到 Activity A 时,需要使用参数 overrideType = OVERRIDE_TRANSITION_CLOSE


这个参数不是应该由系统自动来处理吗,开发者只需要关心参数 enterAnimexitAnim 即可,这明显没有带来任何好处,还给开发者增加了很多负担。


这只是其中一个改变,Android 开发者应该都深有感触,每次 Android 的更新,都有一堆无用的改变,还给开发者增加了很多负担,每次的适配都是一堆体力活,这样就导致了 App 对 SDK 的版本产生了强烈的依赖。


不过好在有经验的开发者,经历过一次有一次的适配之后,积累了经验,在新的项目中,会对大部分 Android API 进行封装,如果 API 有大的变化,不需要对整个项目进行修改。




全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。



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

你知道为什么template中不用加.value吗?

web
Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。 询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢? proxyRefs Vue3 中有有个方法prox...
继续阅读 »

Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。


询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢?


proxyRefs


Vue3 中有有个方法proxyRefs,这属于底层 API 方法,在官方文档中并没有阐述,但是 Vue 里是可以导出这个方法。


例如:


<script setup>
import { onMounted, proxyRefs, ref } from "vue";

const user = {
name: "wendZzoo",
age: ref(18),
};
const _user = proxyRefs(user);

onMounted(() => {
console.log(_user.name);
console.log(_user.age);
console.log(user.age);
});
</script>

上面代码定义了一个普通对象user,其中age属性的值是ref类型。当访问age值的时候,需要通过user.age.value,而使用了proxyRefs,可以直接通过user.age来访问。



这也就是为何template中不用加.value的原因,Vue3 源码中使用proxyRefs方法将setup返回的对象进行处理。


实现proxyRefs


单测


it("proxyRefs", () => {
const user = {
name: "jack",
age: ref(10),
};
const proxyUser = proxyRefs(user);

expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);

proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);

proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});

定义一个age属性值为ref类型的普通对象userproxyRefs方法需要满足:



  1. proxyUser直接访问age是可以直接获取到 10 。

  2. 当修改proxyUserage值切这个值不是ref类型时,proxyUser和原数据user都会被修改。

  3. age值被修改为ref类型时,proxyUseruser也会都更新。


实现


既然是访问和修改对象内部的属性值,就可以使用Proxy来处理getset。先来实现get


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {}
});
}

需要实现的是proxyUser.age能直接获取到数据,那原数据target[key]ref类型,只需要将ref.value转成value


使用unref即可实现,unref的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


get(target, key) {
return unref(Reflect.get(target, key));
}

实现set


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unref(Reflect.get(target, key));
},
set(target, key, value) {},
});
}

从单侧中可以看出,我们是测试了两种情况,一种是修改proxyUserageref类型, 一种是修改成不是ref类型的,但是结果都是同步更新proxyUseruser。那实现上也需要考虑这两种情况,需要判断原数据值是不是ref类型,新赋的值是不是ref类型。


使用isRef可以判断是否为ref类型,isRef的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
}

当原数据值是ref类型且新赋的值不是ref类型,也就是单测中第 1 个情况赋值为 10,将ref类型的原值赋值为valueref类型值需要.value访问;否则,也就是单测中第 2 个情况,赋值为ref(30),就不需要额外处理,直接赋值即可。


验证


执行单测yarn test ref



作者:wendZzoo
来源:juejin.cn/post/7303435124527333416
收起阅读 »

【Java集合】单列集合Set:HashSet与LinkedHashSet详解,为什么它比List接口更严格?

前言 上篇我们介绍了单列集合中常用的list接口,本篇我们来聊聊单列集合中的另外一个重要接口Set集合。1、Set 介绍java.util.Set接口和java.util.List接口一样,同样实现了Collection接口,它与Collection...
继续阅读 »

前言 上篇我们介绍了单列集合中常用的list接口,本篇我们来聊聊单列集合中的另外一个重要接口Set集合。

1、Set 介绍

java.util.Set接口和java.util.List接口一样,同样实现了Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。


与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复,这里的某种规则,我们在后面中给大家揭秘,大家不要着急。
1 无序
2 不可重复

它没有索引,所以不能使用普通for 循环进行遍历。

Set 集合 遍历元素的方式  迭代器,增强for

来,我们通过案例练习来看看


//创建集合对象

HashSet hs = new HashSet();

//使用Collection的方法添加元素
hs.add("hello");
hs.add("world");
hs.add("java");
//无法添加,在执行这行代码时,会报错误
hs.add("world");
//遍历
for(String s : hs) {
System.out.println(s);
}

Set接口类型,定义变量,Collection的常用方法 add()没有报错,说明Set 完全实现了Collection中的方法;
在添加代码 hs.add("world");无法加入,验证了 Set的不可重复;
多次运行遍历发现,输入的顺序是改变的,说明Set是无序的。

Set集合有多个实现子类,这里我们介绍其中的java.util.HashSet、java.util.LinkedHashSet这两个集合。

2、HashSet 集合介绍

通过java文档,我们知道java.util.HashSet是Set接口的一个实现类

  • 它所存储的元素是不可重复的

  • 元素都是无序的(即存取顺序不一致)

  • 没有索引,没有带索引的方法,也不能使用普通for循环遍历

  • java.util.HashSet  是由哈希表(实际上是一个 HashMap 实例)支持,换句话说它的底层的实现数据结构是 哈希表结构,而哈希表结构的特点是查询速度非常快。

我们先来使用一下HashSet集合,体验一下,在进行讲解:

    public class Demo1Set {

public static void main(String[] args) {

//创建集合对象
HashSet hs = new HashSet();

//添加元素
hs.add("hello");
hs.add("world");
hs.add("java");

hs.add("world");

//使用增强for遍历
for(String s : hs) {
System.out.println(s);
}

}
}

输出结果如下
world
java
hello

***发现world 单词只存储了一个,集合中没有重复元素***

3、HashSet集合存储数据的结构

3.1 哈希表数据结构

我们在前面的文章中,已经介绍过基本的数据结构,大家可以回顾以下。

哈希表是什么呢?简单的理解,在Java中,哈希表有两个版本,在jdk1.8 之前,哈希表 = 数组+链表 ,而在 jdk1.8 之后,哈希表 = 数组+链表+红黑树(一种特殊的二叉树) 我们在这里对哈希表的讲述,为了便于我们学习,只需要记住它在计算机中的结构图即可。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询…无论你是初学者还是有经验的开发者,这里都有你需要的一切。最重要的是,所有资源完全免费!点击这里,立即开始你的学习之旅!


再一个,现在我们大多使用的jdk版本是1.8之后的,所以我们讲解的哈希表结构是 第二个版本。 废话不多说,来看看图吧:

  1. 我们在之前已经介绍过数组和链表的结构图,所以我们在这里就来简单介绍一下红黑树结构。

我们在生活中树的结构,树根-树枝-叶子。计算机世界的树,刚好与我们现实中的树成镜像相反,树根在上,树枝在下。那每个树枝上只有不超过两个叶子的就叫做  二叉树,而红黑树就是我们一颗特殊的二叉树,结构就是这样: image

  1. 说完红黑二叉树呢,我们来看看我们的哈希表结构图:

    1. 有数组

    2. 有链表

    3. 有红黑树

    image

    3.2 哈希值

    我们刚刚通过哈希表的介绍,知道 元素在结构中的存放位置,是根据hash值来决定的,而 链表的长度达到某些条件就可能触发把链表演化为一个红黑树结构。那么hash值是什么呢?说白了就是个数字,只不过是通过一定算法计算出来的。
    接下来我们介绍一下:

    • 哈希值:是JDK 根据对象地址或者字符串或者数字算出来的 int 类型的数值

    • 如何获取哈希值?

    在java基础中,我们学习过 Object类是所有类的基类,每个类都默认继承Object类。通过API 文档查找,有个方法 public int hashCode():返回对象的哈希码值

    我们看下这个方法的使用

    首先定义一个person类

    具有两个属性,设置getset方法,设置构造函数

       public class Demo2HashCode {

    public static void main(String[] args) {

    String str1 = "hello";
    String str2 = new String("hello");

    System.out.println("str1 hashcode =" + str1.hashCode());
    System.out.println("str2 hashcode =" + str2.hashCode());

    //通过上下两段代码的对比,我们可以知道 String 类重写了HashCode()方法。

    Student student = new Student("玛丽", 20);
    Student student2 = new Student("沈腾", 30);
    System.out.println("student hashcode =" + student.hashCode());
    System.out.println("student2 hashcode =" + student2.hashCode());

    }
    }

    好,我们了解了hash值概念和获取方式,那我们就来看看元素事怎么加入到 hashset中的

    3.3  添加元素过程

    那么向 HashSet集合中,添加一个元素时,到底执行了哪些流程呢? 首先我们在实例化HashSet 对象,同过api文档,在调用空参构造器后,创建了一个长度为16的数组。 其次在调用add方法之后,它的执行流程如下:

    1. 根据对象的哈希值计算存储位置

          如果当前位置没有元素则直接存入
      如果当前位置有元素存在,则进入第二步
    2. 当前元素的元素和已经存在的元素比较哈希值如果哈希值不同,则将当前元素进行存储如果哈希值相同,则进入第三步

    3. 通过equals()方法比较两个元素的内容如果内容不相同,则将当前元素进行存储如果内容相同,则不存储当前元素流程图来表示:

    4. image

    了解了元素添加的原理后,在添加元素的过程中,如果说某一条链表达到一定数量,就会触发条件,去演化为一条红黑树,防止我们学起来太吃力,在这里不做深入探究。 总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。

    4、HashSet 存储自定义类型元素

        //优化后的student
    public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @Override
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Student)) return false;

    Student student = (Student) o;

    if (age != student.age) return false;
    return name.equals(student.name);
    }

    @Override
    public int hashCode() {
    int result = name.hashCode();
    result = 31 * result + age;
    return result;
    }
    }


    public class Demo3HashSet {

    public static void main(String[] args) {

    HashSet students = new HashSet<>();

    Student student = new Student("玛丽", 20);
    Student student2 = new Student("沈腾", 30);
    Student student3 = new Student("沈腾", 30);

    students.add(student);
    students.add(student2);
    students.add(student3);

    for (Student studentObj : students) {
    System.out.println("Hashset 元素=" + studentObj);
    }
    }
    }

    执行结果
    Hashset 元素=Student{name='玛丽', age=20}
    Hashset 元素=Student{name='沈腾', age=30}

    5、LinkedHashSet

    我们知道HashSet 保证元素的唯一,可以元素存放进去是没有顺序的,那么我们有没有办法保证有序呢? 打开API文档,我们查看 HashSet下面有一个子类 java.util.LinkedHashSet,这个名字听起来和我们之前学过的LinedList  有点像呢。通过文档,LinkedHashSet 具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。 简单的理解为:在进行集合添加元素的同时,不仅经过程序的执行形成一个哈希表结构,还维护了一个记录插入前后顺序的双向链表。 我们通过案例来感受一下:

        public class Demo4LinkedHashSet {

    public static void main(String[] args) {

    // Set demo = new HashSet<>();

    Set demo = new LinkedHashSet<>();
    demo.add("hello");
    demo.add("world");
    demo.add("ni");
    demo.add("hao");
    demo.add("hello");

    for (String content : demo) {
    System.out.println("---" + content);
    }
    }
    }

    执行结果:
    第一次:
    ---world
    ---hao
    ---hello
    ---ni

    第二次:
    ---hello
    ---world
    ---ni
    ---hao

    小节

    到这里我们已经讲完了单列集合的两种基本接口,分别是List 和 Set,然后通过一些简单的案例,我们也初步了解了它们的使用,其实大家可能还是有些懵逼的;但是在这里,还是要多练习,多看看别人的代码,看看别的优秀的代码,是在怎样的场景下使用的,是怎么互相转换使用的;希望在大家阅读这个系列文章的时候,一方面使一些刚学习的新人,有所帮助;一方面为一些已经工作了一段时间的朋友,温故而知新;更重要的一点,让大家知道,技术的出现,是为了解决问题而创造出来的,他本来就是为了解决生活的难题,只不过使用了一些好的,快捷的方式而已。
    可能文章中有些地方讲的不恰当,大家可以私信,探讨探讨,互相提高。本篇就到这里,happy ending!

      收起阅读 »

      软件设计中你考虑过重试了吗?

      你好,我是刘牌! 人生做事情失败了,拍拍裤子,站起来再试试,那么为啥软件中请求失败了为何就放弃了,而不是不再试试呢! 前言 今天分享一下重试操作,我们知道网络是不可靠的,那么在进行网络请求时,难免会出现请求失败,连接失败等情况,为了保证软件的稳定性和良好的...
      继续阅读 »

      你好,我是刘牌!



      人生做事情失败了,拍拍裤子,站起来再试试,那么为啥软件中请求失败了为何就放弃了,而不是不再试试呢!



      前言


      今天分享一下重试操作,我们知道网络是不可靠的,那么在进行网络请求时,难免会出现请求失败,连接失败等情况,为了保证软件的稳定性和良好的体验,很多时候我们不应该将程序内部出现的问题都抛出给用户,而是应该尽最大可能将软件内部不可抗拒的问题在程序内部处理掉,那么很多时候我们会采取重试操作。


      背景和问题


      程序产生网络故障或者其他一些故障是无法避免的,可能因为一些原因导致某些服务在短时间或者一段时间断连,可能是服务器负载过高而导致的,也可能是数据库导致故障从而影响服务,也可能是GC过于频繁而导致服务很不稳定等等,总之,导致服务不可用的因素很多很多。


      对于程序的出错,如果不属于业务上的异常,不应该抛给用户,比如抛出“无法连接远程服务”,“服务器负载过高”,“数据库异常”这类异常给用户实际上没有任何意义,反而会影响用户用户体验,因为用户并不关心这些,他们也读不懂这些领域词汇,所以应该去避免这些问题。


      解决方案


      程序发生异常是无法避免的,我们只有采取一些补救措施,在最大程度上提高程序的稳定性和用户体验,对于程序产生的问题,有一些可能只是瞬时的,系统能够很快恢复,有一些需要一定的时间,而有一些需要介入人工,所以需要花费的时间更多,那么就需要根据不同的情况来处理,下面对其进行分类。


      取消


      当系统中的异常是暂时无法处理的,这时候就应该直接取消任务,因为如果不取消,而是让用户一直等待,那么就会导致用户的操作无法进行下一步,而是一直等待,用户体验就会变得很差,这时候应该给用户友好的提示,提醒他们稍后再进行办理,浪费别人的时间等于谋财害命。


      重试


      如果请求因为网络原因或者服务短暂的不可用,这种故障时间很短,很快就能恢复,比如一些服务是多实例部署,刚好请求到的那个服务出现网络故障而没能请求成功,如果我们直接返回异常,那么肯定不合适,因为其他服务实例还能提供服务,所以应该对请求进行重试,重试后可能请求到了其他正常的服务,即使请求到了之前的服务,那么可能它已经恢复好了,能够正常提供服务了,这里重试是没有时间间隔的,是不间断地请求,直到请求成功,这种适用于服务很够很快恢复的场景。


      间隔重试


      间隔重试就是不会一下进行重试,而是隔一个时间段再进行重试,比如一些服务因为过于繁忙导致负载过高而暂时对外提供服务,那么这时候如果不断发起重试,只会导致服务负载更高,我们应该隔个时间段再进行重试,让服务处理堆积的任务,等服务负载降下来再重试,这个时间间隔需要我们进行考量,按照合适的值去设置,比如1s,这完全根据实际场景去衡量。


      上面对三种方案进行描述,我们只描述了重试,但是重试次数也是我们要去考量的一个值,如果一个服务20s才恢复,那么我们重试20秒肯定不太合适,不过也要看具体业务,面向客户的话肯定大多客户接受不了,这时候我们应该设置重试次数,比如重试了三次还不能成功,那么久取消任务,而不是一直重试下去。



      重试次数也要根据实际情况来设置,如果一直重试,而服务一直无法恢复,那么也会消耗资源,并且用户导致用户请求一直在等待,用户体验不好,设置设置次数过少,那么可能会导致没有足够重试,从而导致浪费了一些重试次数,最后还没有成功,如下,第三次就重试成功,如果设置为两次,那么前两次没有成功就返回,用户还需重新再发起请求。



      从上面可以看出,这些设置都没有黄金值,而是需要我们根据业务和不断地测试才能找出合适的值。


      怎么重试,参数怎么管理


      上面对重试进行一些理论的讲解,那么在实际场景中我们应该如果去做呢,首先要考虑我们的业务中是否真的有必要重试,如果没必要,那么就完全没必要去增加复杂度,如果需要,那么就需要进行良好的设计,保证其优雅和扩展性。


      不同的业务有不同的重试逻辑,所以我们需要在不同的地方实现不同的逻辑,但是重试次数和重试时间间隔这些参数应该是需要可动态配置的,比如今天服务负载过高,那么时间间隔可以设置稍微长一点,次数可以设置多一点,然后负载较低的时候,参数可以设置小一点,这些配置信息可以写入配置中心中。


      也有一些重试框架供我们使用,比如spring-retry,我们可以借助一些框架来管理我们的重试任务,更方便管理。


      总结


      以上对重试的一些介绍就完了,我们介绍了重试的场景,重试产生的背景,还有一些解决方案,还对重试的一些管理进行介绍,重试的方案很多,实现方式也有很多,它不是固定的技术,而是一种思想,也是我们在软件设计中应该考虑的一个点,它能提高软件的稳定性和用户体验,但是也需要我们进行考量。



      今天的分享就到这里,感谢你的观看,我们下期见!



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

      简历中的项目经历可以怎么写?

      概述 工作这10多年来,也经常做招聘的工作,面试过的人超过50人次了,而看过的候选人的简历则有几百份了,但是清晰且能突出重点的简历,确实很少遇到。这里基本可以说明一个问题,很多候选人是不太清楚如何写出一份好的简历的。 下面基于简历中的项目经历,重点铺开说一下。...
      继续阅读 »

      概述


      工作这10多年来,也经常做招聘的工作,面试过的人超过50人次了,而看过的候选人的简历则有几百份了,但是清晰且能突出重点的简历,确实很少遇到。这里基本可以说明一个问题,很多候选人是不太清楚如何写出一份好的简历的。


      下面基于简历中的项目经历,重点铺开说一下。在社招中,项目经历面试官重点考察的地方。


      写项目经历需要注意的地方


      项目经历是介绍你实战经历的地方,同时也能反映你对已掌握的技能的使用情况。对于应聘偏技术类的岗位来说,这块非常的重要。


      下面会以支付中心作为例子进行阐述。




      • 项目背景,也即是你一定要非常清楚启动这个项目的缘由是啥。如果这个都说不清楚的话,那说明,你真的就是埋头干活,偏执行的角色。对项目并没有一个整体的认识。就算你只是这个项目的普通参与者,也需要主动的去了解和理解该项目立项的原因。有个注意的地方是,项目背景的文字描述不要太长,一两句就可以了。比如说:当前支付中心耦合在订单系统中,为了提升支付模块的稳定性、维护性、性能和扩展性,需要将支付模块独立出来,统一为其他内部系统提供支付能力;




      • 项目功能介绍,介绍一下这个项目能做什么,有什么核心模块,需要应付什么量级的流量。以支付中心为例子:为内部的订单系统提供支付能力,对内提供了微信、支付宝、抖音、海外、信用卡、钱包、礼品卡以及组合支付的支付、回调、退款、查询、业务对账等能力。平时需要应付每秒1万的支付请求。




      • 技术架构设计,这里考察的是技术选型的严谨性和模块设计的合理性。如果项目用到了RabbitMQ、Redis、Kafka等一些技术,你自己心里一定有个底,就是当时为什么选用这些技术,是经过深思熟虑的吗?是经过了很多轮的技术栈对比后决定使用的吗。也即是技术选型是一个严谨的论证的一个过程。而设计这块,则要说清楚模块划分的缘由以及解决方案。还是以支付中心为例子:通过支付网关,对外提供统一的接口,而内部则通过支付路由模块,进行具体的支付方式路由,并把单独的支付方式,以物理单元进行隔离,避免各种支付方式在出故障时,相互影响。为了应付高频的支付动作,采用数据库分库的方式缓解写的压力。




      • 我负责的模块,如果你参与的项目是部门核心项目,但是自己参与的模块确是边缘模块或者只是参与了很小的一部分,虽然你也能在这个项目里,得到成长。但是那是称不上个人亮点的。因为面试官会更倾向于:你为这个项目做了什么贡献,因为你,项目有了什么好的改变和突破性进展。因此,做项目的时候,不妨跟自己的领导多反馈一下,希望能独立主导一些重要的模块。如果领导觉得当前的你还无法独立hold住重要的模块,你也不要气馁,平时多多提升自己,争取后续能主导一些重要模块。这个真的很重要,为了将来的自己,你必须得这么做。在做项目的时候,如果你长期一直起着螺丝钉的作用的话,对你极其不利,甚至可以说,你是在浪费时间。




      • 难点和踩过的坑,难点也即是亮点。在你负责的模块里,具体的难点是什么,你是通过什么方案解决的。而解决的过程中,又遇到什么大坑?怎么优化的。这个其实是一种引导,把面试官引入到你自己比较熟悉又印象深刻的领域,如果你准备充分的话,是能给面试官一个好的印象的,是能加分的。同时能解决掉难点,对自身成长也是有利的,且还能说明的你韧性不错,有追求。




      • 取得的成效,不能只是重视过程,而不重视结果,这是不可取的。你需要用结果和数据体现你的价值。比如说,支付中心上线后,你负责的业务模块,慢调用和慢SQL消失了,接口响应速度提升了10倍,上线半年,无任何大故障。等等。




      项目经历写几个合适?


      如果按照上面的的方式来书写项目的话,那每个项目的文字描述是不短的,一个项目的描述就大概要占用半页了。因此,简历里的项目不能太多,2到3个就可以了。项目主要在精不在多,把自己负责比较多的且能作为自己的一个亮点的核心项目,说清楚道明白,更为重要。


      现在的你应该做什么?


      赶紧好好总结一些当前和之前做过的项目,按照上面列的方式,好好梳理和思考一下,提炼一些重要的内容出来。争取能作为自己履历的亮点。如果你发现到目前为止,还没有能为自己带来竞争力的项目,那赶紧好好反思一下,赶紧争取去做。


      小结


      如果你不是什么名人或者知名大佬,学历和履历也一般般,那么你只能通过曾经做过好的项目来增强自己的竞争力了。HR也会通过你的项目经历来了解你的能力。项目经历一定要真实,要突出亮点和难点,并说清楚自己在项目起到什么作用。


      作者:SamDeepThinking
      来源:juejin.cn/post/7200953096893136955
      收起阅读 »

      一体多面:哪有什么DO、BO、DTO,只不过是司空见惯的日常

      1 分层疑问 无论DDD还是MVC模式构建项目,势必涉及到工程结构的分层,每一层由于定位不同,所以访问的对象也不同,那么对象在每一层传递时就会涉及到对象的转换,这时有人会产生以下疑问: 对象种类多,增加理解成本 对象之间转换,增加代码成本 编写代码时有时不同...
      继续阅读 »

      1 分层疑问


      无论DDD还是MVC模式构建项目,势必涉及到工程结构的分层,每一层由于定位不同,所以访问的对象也不同,那么对象在每一层传递时就会涉及到对象的转换,这时有人会产生以下疑问:



      • 对象种类多,增加理解成本

      • 对象之间转换,增加代码成本

      • 编写代码时有时不同层对象几乎一样


      即使有这样的疑问,我也认为分层是必要的,所以本文我们尝试回答上述疑问。




      2 通用分层模型


      两种通用模型是MVC和DDD,我之前在文章《DDD理论建模与实现全流程》也详细讨论DDD建模和落地全流程,本文只涉及对象的讨论,所以会对模型有所简化。




      2.1 模型分类



      • 数据对象:DO(data object)

      • 业务对象:BO(business object)

      • 视图对象:VO(view object)

      • 数据传输对象:DTO(data transfer object)

      • 领域对象:DMO(domain object)

      • 聚合对象:AGG(aggregation)




      2.2 MVC


      MVC模型总体分为三层:



      • 持久层(persistence)

      • 业务层(business)

      • 表现层(presentation/client)


      每一层使用对象:



      • 持久层

        • 输入对象:DO

        • 输出对象:DO



      • 业务层

        • 输入对象:BO

        • 输出对象:BO



      • 表现层

        • 输入对象:VO/DTO

        • 输出对象:VO/DTO






      2.3 DDD


      DDD模型总体分为四层:



      • 基础设施层(infrastructure)

      • 领域层(domain)

      • 应用层(application)

      • 外部访问层(presentation/client)


      每一层使用对象:



      • 基础设施层

        • 输入对象:DO

        • 输出对象:DO



      • 领域层

        • 输入对象:DMO

        • 输出对象:DMO



      • 应用层

        • 输入对象:AGG

        • 输出对象:DTO



      • 外部访问层

        • 输入对象:VO/DTO

        • 输出对象:VO/DTO






      3 生活实例


      这些对象看起来比较复杂,理解成本很高,好像是为了分层硬造出来的概念。其实不然,这些对象在生活中司空见惯,只不过大家并没有觉察。我们设想有一家三口,小明、小明爸爸和小明妈妈,看看这些对象是怎么应用在生活中的。




      3.1 MVC


      3.1.1 数据对象(DO)


      数据对象作用是直接持久化至数据库,是最本质的一种对象,这就像小明在卧室中穿着背心睡觉,这接近于人最本质的一种状态,小明此时是一种数据对象。




      3.1.2 业务对象(BO)


      小明起床走出卧室,这时小明就不仅仅是他自己了,他还多了很多身份,例如儿子、学生、足球队队员,不同身份输入和输出信息是不一样的。作为儿子要回应家长的要求,作为学生要回应老师的要求,作为足球队员要回应教练的要求。作为小明从数据对象,在不同的身份场景中变成了不同的业务对象。




      3.1.3 视图对象/数据传输对象(VO/DTO)


      小明吃完早饭准备去上学,但是嘴角粘上了饭粒,出门前要把饭粒擦掉。数据传输对象需要注意简洁性和安全新,最重要的是只携带必要信息,而不应该携带不必须要信息,所以此时小明变成了视图对象。




      3.2 DDD


      3.2.1 领域对象(DMO)


      领域对象要做到领域完备,从本质上来说与业务对象相同,但是通常使用充血模型,业务对象通常使用贫血模型。




      3.2.2 聚合对象(AGG)


      学校要开家长会要求小明、小明妈妈和小明爸爸全部参加,其中小明负责大扫除,小明妈妈负责出黑板报,小明爸爸负责教小朋友们踢足球。此时学校和家长联系时是以家庭为单位的,家庭就是聚合对象。




      4 一体多面


      通过上述实例我们看到,即使是同一个自然人,在不同的场景下也有不同的身份,不同的身份对他的要求是不同的,输入和输出也是不同的。这就是一体多面。


      同理对于同一个对象,即使其在数据库只有一条数据,但是由于场景的不同,输入和输出也是不同的,所以有了各种看似复杂的对象。我们再回看上面三个问题,可以尝试给出本文的答案:


      对象种类多,增加理解成本:这是必须要付出的成本,小明不能嘴角挂着饭粒去上学


      对象之间转换,增加代码成本:这是必须要付出的成本,不同角色切换时必须要付出切换成本,小明不能用回应足球队教练的输出回应老师或者老师,这是截然不同的角色


      编写代码时有时不同层对象属性几乎一样:小明作为一个自然人,他自身固有特性也是相同的,所以表现在对象上是很多属性是一致的。但是不同的角色是有不同要求的,所以为了一些细微的差别,也是要新增对象的,这是必要的代价


      作者:JAVA前线
      来源:juejin.cn/post/7302740437529395211
      收起阅读 »

      Mysql升级后字符编码引起的血泪教训

      描述 现在大部分企业所使用的MySQL数据库相信都已经从5.7升级到了8,性能也得到了大幅度的提升 MySQL 8.0对于数据管理带来了很多改变,使得MySQL成为一个更强大、更灵活和更易于使用的数据库管理系统。 MySQL 8.0提供了更好的JSON支持...
      继续阅读 »

      描述


      现在大部分企业所使用的MySQL数据库相信都已经从5.7升级到了8,性能也得到了大幅度的提升


      MySQL 8.0对于数据管理带来了很多改变,使得MySQL成为一个更强大、更灵活和更易于使用的数据库管理系统。




      1. MySQL 8.0提供了更好的JSON支持,包括更快的JSON函数和表达式,以及新的JSON数据类型和索引。




      2. MySQL 8.0引入了窗口函数,这些函数可以用来计算分析函数的结果并根据指定的排序规则进行分组。




      3. MySQL 8.0提供了更好的空间数据支持,包括新的空间数据类型和函数,例如ST_Distance_Sphere函数,它可以计算两个点之间的球面距离。




      4. MySQL 8.0提供了更好的安全性,包括更安全的默认配置、更严格的密码策略、更多的SSL/TLS选项等。




      5. MySQL 8.0提供了更好的性能,包括新的索引算法、更好的查询优化器、更好的并发控制等。




      MySQL5.7


      查看版本号

      image.png


      查看编码格式

      image.png


      从结果可以看出,MySQL8默认字符编码为utf8mb4


      查看排序规则

      image.png


      从结果可以看出,MySQL8默认排序规则为utf8mb4_general_ci


      总结

      MySQL5.7 默认字符编码是utf8mb4,默认排序规则是utf8mb4_general_ci


      MySQL8


      查看版本号

      image.png


      查看编码格式

      image.png


      “character_set_client” 表示客户端字符集


      “character_set_connection” 表示连接字符集


      “character_set_server” 表示服务器字符集


      从结果可以看出,MySQL8默认字符编码为utf8mb4


      查看排序规则

      image.png


      从结果可以看出,MySQL8默认排序规则为utf8mb4_0900_ai_ci


      总结

      MySQL8 默认字符编码是utf8mb4,默认排序规则是utf8mb4_0900_ai_ci


      utf8 与 utf8mb4 区别




      1. 存储字符范围不同:



        • utf8 编码最多能存储 3 个字节的 Unicode 字符,支持的 Unicode 范围较窄,无法存储一些辅助平面字符(如 emoji 表情)。

        • utf8mb4 编码最多能存储 4 个字节的 Unicode 字符,支持更广泛的 Unicode 范围,包括了 utf8 所不支持的一些特殊字符和 emoji 表情等。




      2. 存储空间不同:



        • utf8 编码时,字符长度可以是最多 3 个字节。

        • utf8mb4 编码时,字符长度可以是最多 4 个字节。




      3. 对于存储 Emoji 和特殊字符的支持:



        • utf8mb4 能够存储和处理来自辅助平面的字符,包括emoji表情,这些字符需要使用 4 个字节来编码。而 utf8 不支持这些字符。




      utf8mb4_general_ci 与 utf8mb4_0900_ai_ci 区别




      1. utf8mb4_general_ci



        • 这是MySQL中较为通用的字符集和校对规则。

        • utf8mb4 是一种用于存储 Unicode 字符的编码方式,支持更广泛的字符范围,包括 emoji 等。

        • general_ci 是一种排序规则,对字符进行比较和排序时不区分大小写,对于大多数情况来说是足够通用的。




      2. utf8mb4_0900_ai_ci



        • 这是MySQL 8.0.0 版本后引入的校对规则。

        • 0900 表示MySQL 8.0.0 版本。

        • ai_ci 是指采用 accent-insensitive 方式,即对于一些有重音符号的字符,排序时会忽略重音的存在。




      主要区别在于排序规则的不同。utf8mb4_0900_ai_ci 在排序时会对重音符号进行忽略,所以某些含有重音符号的字符在排序时可能会与 utf8mb4_general_ci 有所不同。


      索引不生效问题


      表结构

      CREATE TABLE `user` (
      `id` bigint NOT NULL COMMENT '主键',
      `username` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
      `password` varchar(50) NOT NULL DEFAULT '' COMMENT '密码',
      `store_id` bigint NOT NULL DEFAULT 0 COMMENT '门店id',
      `is_delete` int NOT NULL DEFAULT '0' COMMENT '是否删除',
      PRIMARY KEY (`id`),
      KEY `idx_store_id` (`store_id`)
      ) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';


      CREATE TABLE `user_role` (
      `id` bigint NOT NULL COMMENT '主键',
      `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户id',
      `role_id` bigint NOT NULL DEFAULT 0 COMMENT '角色id',
      `is_delete` int NOT NULL DEFAULT '0' COMMENT '是否删除',
      PRIMARY KEY (`id`),
      KEY `idx_user_id` (`user_id`),
      KEY `idx_role_id` (`role_id`)
      ) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关系表';


      查询

      SELECT DISTINCT
      t1.id,
      t1.username
      FROM
      user t1
      JOIN user_role t2 ON t2.user_id = t1.id
      WHERE
      t1.is_delete = 0
      and t2.is_delete = 0
      and t1.store_id = 2
      AND t2.role_id NOT IN (9, 6)


      执行计划

      企业微信截图_c83704fd-f85a-4dc7-901f-00a9cf35857e.png


      通过执行计划发现明明字段上加了索引,为什么索引没有生效


      explain format = tree 命令

      企业微信截图_e26332e8-cad7-42fc-bfb7-7c06fbadf26b.png


      问题找到了


      (convert(t2.user_id using utf8mb4) = t1.id))



      在回头看看表结构

      image.png


      为什么会不一致呢?

      mysql5.7 升级之前 两个表都是 CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci


      mysql5.7 升级到 mysql8 后,user_role 更新过表结构


      修改表排序规则


      ALTER TABLE user CHARACTER COLLATE = utf8mb4_0900_ai_ci;



      image.png


      再次查看执行计划

      企业微信截图_5a4e736a-a9b1-413a-b517-17e552d1b783.png


      企业微信截图_a97f807a-8c3b-4a8e-ad2f-9ad47a6f398e.png


      总结

      开发一般都不太注意表结构的字符编码和排序规则,数据库升级一定要先统一字符编码和排序规则


      查询的问题


      由于先发布应用,后执行的脚步,没有通知测试所以没有生产验证,导致第二天一大早疯狂报警


      image.png


      一看就是两个表字段排序规则不一致导致的


      只能修改表结构排序规则 快速解决


      总结


      升级MySQL是一个常见的操作,但在升级过程中可能会遇到各种问题。本文主要介绍排序规则不一致导致的问题,希望能对大家在升级MySQL时有所帮助。在进行任何升级操作之前,务必备份数据库,以防数据丢失。同时,建议定期对数据库进行性能优化,以提高系统的高可用。


      作者:三火哥
      来源:juejin.cn/post/7303349226066444288
      收起阅读 »

      直播点赞喷射表情效果实现

      web
      最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。 找了一个别人的效果图 就来实现这个效果。 写一个点赞按钮 <style> .like-box { ...
      继续阅读 »

      最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。


      找了一个别人的效果图


      点赞.gif


      就来实现这个效果。


      写一个点赞按钮


        <style>
      .like-box {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background-color: #ddd;
      position: relative;
      top: 360px;
      display: flex;
      align-items: center;
      justify-content: center;
      left: 300px;
      cursor: pointer;
      }

      .like-box i {
      font-size: 25px;
      }
      </style>
      <div class="like-box" id="like-box">
      <i class="icon-dianzan iconfont"></i>
      </div>

      1700297207118.png


      其中中间的图标用的是阿里巴巴矢量图标库的图标字体。


      动态创建表情


      动态表情用一个div表示。背景是表情图片。有6个备选表情


      image.png


      div样式


       .like-box div {
      position: absolute;
      width: 48px;
      height: 48px;
      background-image: url("./public/images/bg1.png");
      background-size: cover;
      z-index: -1;
      }

      使用js创建表情图标,并插入到点赞div


      const likeBox = document.getElementById('like-box') // id为like-box
      const createFace = function () {
      const div = document.createElement('div')
      return div
      }

      likeBox.addEventListener('click', function () {
      const face = createFace()
      likeBox.appendChild(face)
      })

      实现表情动画效果


      从最终效果图中可以看出,最终效果是由多个表情组成,更准确的说是由多个不同运动轨迹表情实现。所以关键是表情的轨迹实现。


      而在运动轨迹过程中,有大小缩放效果、有淡出效果。所以至少有三个animation。


      实现缩放效果


      使用animation实现缩放效果,添加animation样式


          @keyframes scale {
      0% {
      transform: scale(0.3);
      }

      100% {
      transform: scale(1.2);
      }
      }

      当动态创建表情div时,将缩放效果添加到div上,添加后效果


      缩放效果.gif


      实现淡出效果


      使用animation实现淡出效果,添加animation样式


          @keyframes opacity {
      0% {
      top: 0;
      }

      10% {
      top: -10px;
      }

      75% {
      opacity: 1;
      top: -180px;

      }

      100% {
      top: -200px;
      opacity: 0;
      }
      }

      当动态创建表情div时,将淡出效果添加到div上,添加后具体效果


      淡入淡出.gif


      实现不同轨迹效果


      单一轨迹

      创建单一轨迹样式效果


       @keyframes swing_1 {
      0% {}

      25% {
      left: 0;
      }

      50% {
      left: 8px;
      }

      75% {
      left: -15px;
      }

      100% {
      left: 15px;
      }
      }

      当动态创建表情div时,将单一轨迹样式效果添加到div上,添加后具体效果


      单一轨迹.gif


      多轨迹

      多轨迹有点麻烦,但是也不是很麻烦。具体思路是创建多个轨迹样式,然后在动态创建表情时给表情div随机添加各种轨迹样式,添加后具体效果


      多轨迹.gif


      最终js代码


      const likeBox = document.getElementById('like-box')

      const createFace = function () {
      // 随机表情
      const face = Math.floor(Math.random() * 6) + 1;
      // 随机轨迹
      const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

      const div = document.createElement('div')

      div.className = `face${face} trajectory${trajectory}`;
      return div
      }


      likeBox.addEventListener('click', function () {
      const face = createFace()
      likeBox.appendChild(face)
      })

      移除产生的表情div


      为了避免一直添加div,乃至最后降低动画性能,需要等animation结束后移除动画div


      const likeBox = document.getElementById('like-box')

      const createFace = function () {
      const face = Math.floor(Math.random() * 6) + 1;
      const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

      const div = document.createElement('div')

      div.className = `face${face} trajectory${trajectory}`;
      // 移除div
      div.addEventListener("animationend", () => {
      if(likeBox.contains(div)){
      likeBox.removeChild(div)
      }
      });
      return div
      }

      likeBox.addEventListener('click', function () {
      const face = createFace()
      likeBox.appendChild(face)
      })

      总结


      所有的效果实现都通过css的animation实现。实际还可以使用canvas实现。关键是实现的思路。


      核心思路:使用css的animation实现,基于对动画效果的拆分;拆出单一效果,之后所有效果同时发挥作用。


      最近想到一件事,初高中学到的数学知识应该可以应用在开发中。有些前端效果实际就是数学知识的一种应用。更准确的说是高中数学函数图形以及几何图形那块,这两块都有曲线的函数或者方程表示。


      如果要实现的效果是曲线或者轨迹的话,完全可以考虑它的坐标关系是不是数学中学到的。进而知道关系,进而开发出效果。


      我一直想把中学和大学的知识应用,我想上面就是应用的一个点。


      代码地址:github.com/zhensg123/r…


      (本文完)


      参考文章


      H5 直播的疯狂点赞动画是如何实现的?(附完整源码)


      作者:通往自由之路
      来源:juejin.cn/post/7303463043248291874
      收起阅读 »

      我们工作快乐吗

      工作满意度是幸福感的重要预测指标。对于工作有多满意,很大程度上也决定了对人生有多满意。有研究表明,在幸福感的所有预测指标中,工作仅次于婚姻。盖洛普公司主导了一次针对全球十几万人的调查研究,包括了从原始部落到现代文明的不同工作形态,结果表明,不管在地球的哪个角落...
      继续阅读 »

      工作满意度是幸福感的重要预测指标。对于工作有多满意,很大程度上也决定了对人生有多满意。有研究表明,在幸福感的所有预测指标中,工作仅次于婚姻。盖洛普公司主导了一次针对全球十几万人的调查研究,包括了从原始部落到现代文明的不同工作形态,结果表明,不管在地球的哪个角落,「一份有意义的工作可以显著地预测幸福感。」


      在希腊神话里有这样一个故事,一个叫西西弗斯的国王,他自作聪明,戏弄了死神和冥王等众神。众神把他抓到阴间,给他一个恶劣的惩罚,他需要每天把一块大石头从山脚推到山顶,每当他正好要到达山顶的时候,大石就会滑落回山脚,西西弗斯只有顶着烈日,日复一日地将石头从推到山顶,如此循环往复。「西西弗斯这个故事代表了没有意义工作的极致,也是一项非常残酷的惩罚。」


      著名的富士康连续自杀事件,在工厂车间,几百个工人,日复一日地做着重复而简单的工作,不就是西西弗斯的神话在现实中发生?在无休止的加班和重复劳动中,不知道工作的意义是什么,如此麻木而紧张的工作状态,导致了最终的悲剧。


      上班如上坟:最糟糕的工作状态


      现在有一句话描述的特别贴切——“上班如上坟”,这句话非常形象地表达出了最糟糕的工作状态。


      马云曾经说过一个深受大家认同的观点,一个人离职,原因无外乎两点:



      • 钱没给够

      • 受委屈了


      其实真实发生的离职情况来看,第二点的因素占绝大多数,当我们工作中遇到了很多不快乐的事情时,就会触发大脑的情绪思维,从而产生了离职的念头。


      在互联网行业,大多数产生这种“上班如上坟”的状态是以下几种情况:



      • 「内卷」,一种是**「工作时长」上的卷,一天有做不完的工作,干不完的活,每天工作可能超过12个小时,甚至没有周末,类似于富士康的模式,简单低效且麻木,没有基本的休息时间保障;另一种是「内心」上的卷,可能是过高的要求,无法完成的目标,带来的无尽压力,产生了「绝望感和窒息感」**,长期在这个状态下非常容易出现心理问题。

      • 「得不到认可」,一直被领导PUA,这会导致一种无论如何努力都不被认可的无力感,产生非常大的精神内耗,会怀疑自己和否定自己,有很大的负面情绪产生。


      一旦达到“上班如上坟”这个状态,实际上已经是非常糟糕的情况了。往往这种状态下,大家会选择摆烂,消极对待工作,逃避,有离职冲动等等,工作和生活都会受到极大影响。


      金钱不过老六


      在正式工作之前,我们对于薪资有无比强烈的渴求,那时的offer选择真的很简单,哪一家给的钱多就去。在工作之后,很多公司挖人的手段也是以高薪涨幅为第一吸引力,在环境比较好的时候,往往跳槽才是获得更高薪酬的最有效手段。


      大多数打工人面临的是生存问题,大城市高额的生活成本,房贷车贷子女教育,是压在每个人心头的大山,尤其在深圳这样的地方,无数人在这个地方有着共同的目标——搞钱,金钱的重要性不言而喻。


      但是,我们是否过于重视金钱而忽视了其他?在赫兹伯格的研究中,「金钱排在成就、认可、工作本身、责任、晋升之后」,在第6位,「前5项因素才是真正驱动员工的动力所在」


      在赫兹伯格提出的著名的双因素理论中,金钱属于保健因素,它是必要的保障,只会让员工产生不满的情绪,而无法得到满意的情绪:


      image


      image


      实际上,我们在跳槽拿到高薪的最快乐的阶段,是在拿到Offer后的那段时间,以及拿到第一个月的薪水的时候,其他的时间的情绪感受都与金钱本身关系不大了。


      物质资源永远是有限的,在大幅度提高的情况下才能产生激励的效果。钱并不是万能的,从员工的角度来说,个人的身心健康应该是更重要的东西。


      快乐工作的价值


      谷歌在早些时候平均年薪仅在美国科技公司中排12位,但是却获得了最佳雇主排行榜的榜首。谷歌对员工开出了丰厚的条件:



      • 免费用餐

      • 班车接送

      • 股票期权

      • 发放IPad

      • 社交活动,比如瑜伽、葡萄酒、足球比赛、露天烧烤、吉他等

      • 健身房、免费按摩、免费理发、体能训练等等


      这些福利并非浪费,盖洛普指出,员工满意度提高5%,连带提升11.9%的外部客户满意度,同时企业效益也会提升2.5%


      后来这一类的福利策略也被其他公司纷纷效仿。快乐工作的第一步是需要有一个舒心的环境,让大家能够对公司满意,才能更加自发地了解探寻工作的意义。


      Denison Consulting公司调查显示,员工心情不愉快的企业年销售额仅增长了0.1%,而心情愉快的同期增长15.1%。企业若能将员工满意度提高20%,便可将财务绩效提升42%


      快乐工作,会让企业产率、质量、销售额、客户满意度、创新性、适应性都有一定的提升,降低员工缺勤率、跳槽率、紧张与疲劳感,这是雇主与雇员的双赢策略。


      「我们需要追求的终极目标是,让工作成为一种享受。」


      管理是控制还是服务


      相信绝大多数的管理者认为管理是服务,但是在实际行为中却不一定,比如:



      • 不应该对员工说“谢谢”,这种假客气只会产生距离感?

      • 不需要征询员工意见,我对结果要负责应该我说了算?

      • 成功来自于能力和实力,与其他因素无关?

      • 无法容忍下属的挑战,有悖于合作的伦理并产生糟糕的示范效应?

      • 每个人都得为自己的情绪负责,管理还有更重要的事情要做?

      • 保持开放的沟通固然重要,但是很容易让员工自以为是,需要保持距离?

      • 比起目标、绩效、团队的前途,友善和蔼的风格只会让权威旁落?

      • 每天压力这么大,谁能有时间去培养员工提升他们的能力呢?

      • 授权并不靠谱,现在这个环境,更适合各司其职,稳稳当当?

      • 说实话,所谓的愿景和将来就是忽悠,我如何与员工谈论自己都不相信的未来?


      这些问题能够帮助我们反思,自己究竟是在控制还是在服务,作为管理者,一个细微的举动可能在员工看来就会引起不小的情绪波动。


      我们冷漠吗


      工作氛围的糟糕,上班如上坟,十有八九跟管理者有很大的关系,更多的时候,冷漠与氛围直接相关,比如:



      • 同事见面都会热切的打招呼吗?

      • 同事们互相信任,困惑、焦虑和感受都能相互分享吗?

      • 大家在各种沟通场合中能够畅所欲言吗?是否藏着掖着?

      • 吃饭时会形成若干个自发团体吗?有人形单影只吗?

      • 下班之后同事还有可能有聚会吗?

      • 团队稳定性很高吗?团建大家都积极参与吗?

      • 新人容易融入吗?会有计划培养新人吗?

      • 奖励时常发生吗?无论是物质上的还是精神上的

      • 大家认可自己的工作是有价值的吗?

      • 领导的压力和情绪会透传给下属吗?

      • 领导愿意和员工单独交流吗?

      • 领导与员工相处舒服吗?

      • 领导善于夸奖人吗?


      这些问题有助于判别团队的氛围如何,一个好的工作氛围,才能享受工作,激发更大的动力,这样的团队才能创造更大的价值。


      内心驱动模式


      从个人内心的动力上,大概可以分为三类驱动模式:


      恐惧驱动



      • 害怕最亲近的人离开?

      • 害怕失去工作,失去工作能力?

      • 害怕失去健康?

      • 害怕不成功,窝窝囊囊过一辈子?

      • 害怕资产缩水,辛辛苦苦的钱贬值?

      • 害怕晚年凄凉,孤苦伶仃?

      • 害怕被领导同事瞧不起,不被认可?

      • 害怕家庭解体?

      • 害怕死亡?


      责任驱动



      • 要对工作负责,我做事是有品质的

      • 要对同事负责,不能给别人带来麻烦

      • 要对领导负责,不能辜负他的信任

      • 要对父母负责,懂得报恩

      • 要对那一位负责,已经是利益共同体

      • 要对孩子负责,作好的榜样

      • 要对银行负责,需要还贷

      • 要对朋友负责,有真心的朋友不容易

      • 要对国家负责,民族的脊梁

      • 要对自己负责,希望回首往事不留遗憾


      梦想驱动



      • 我想赚很多钱,可以衣食无忧想要什么就有什么

      • 我想成为大家都喜欢的人,因为我而快乐

      • 我想周游世界,看看外面的风景

      • 我想健康长寿,尽量活得久一点

      • 我想每天能够幸福满足,活出质感

      • 我想好日子延续下去,不要大起大落要平安喜乐

      • 我想大家都能够快快乐乐在一起

      • 我想做自己喜欢的事,可以不要为了钱而工作

      • 我想帮助更多需要帮助的人,人本来应该善良


      在工作上,「责任驱动、梦想驱动占据主导地位的人是非常稀缺的」,作为一个管理者,如果因为自己,把这类人变成了行尸走肉,只剩下恐惧驱动,真的是最大的过错,令人发指,天理难容!


      个人感悟


      上班如上坟这种状态,十有八九都是跟管理者有很大的关系。在目前互联网增速放缓的今天,物质金钱上已经无法带来激励的时候,这类问题尤为突出。若工作氛围很糟糕,更是雪上加霜,团队没有凝聚力,无法获得更高的产出,导致更多的裁员,形成恶性循环。


      在目前的大环境下,企业面对生存压力,沉重的指标压在管理者的身上,更需要管理者能够创造快乐的工作氛围,即使在互联网这个不得不加班的大环境下,也能够让工作成为一种享受,不要再让员工产生额外的内耗,驱动团队创造更大的价值,共克时艰。


      作者:孟健
      来源:juejin.cn/post/7292442438249791538
      收起阅读 »

      前段时间面试了一些人,有这些槽点跟大家说说​

      前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。简历书写和自我介绍今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备去年工作经历都是小公司的还有几个进了面试,今...
      继续阅读 »

      前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。

      简历书写和自我介绍

      1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备

      1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了

      2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求

      3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色

      4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多

      5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!

      6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)

      7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点

      8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说

      9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到

      10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点

      11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略

      12. 你可以这样审视自己的简历和自我介绍:

        a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点

        b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考

        c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点

      面试问题

      1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质

      2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任

      3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质

      项目经历

      项目经历就是我们过往做过的项目。

      项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。

      有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?

      大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。

      在项目经历上,面试者常见的问题有这些:

      1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

      2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

      3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)

      出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。

      在看面试者的项目经历时,面试官主要关注这三点:

      1. 之前做的项目有没有难度

      2. 项目经验和当前岗位需要的是否匹配

      3. 经过这些项目,这个人的能力有哪些成长

      因此,我们在日常工作和准备面试时,可以这样做:

      1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

      2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

      3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

      4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

      5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

      6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

      7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验

      技能知识点

      技能知识点就是我们掌握的编程语言、技术框架和工具。

      相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。

      在技能知识点方面,面试者常见的问题有这些:

      1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

      2. 技术不对口:没有岗位需要的领域技术

      3. 技术过剩:能力远远超出岗位要求


      第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。

      第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。

      第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。

      在我面试的人里,通过面试的都有这些特点:

      1. 技术扎实:不仅仅基础好,还有深度

      2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事

      有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?

      软素质

      这里的「软素质」指面试时考察的、技术以外的点。

      程序员的日常工作里,除了写代码还需要做这些事:

      1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

      2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

      3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


      因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:

      1. 理解能力和沟通表达能力

      2. 业务能力

      3. 稳定性


      第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。

      第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。

      业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。

      遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。

      第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。

      针对以上这三点,我们可以这样做:

      1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

      2. 回答问题时有逻辑条理,可以采用类似总分总的策略

      3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

      4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标

      总结

      好了,这就是我前段时间面试的感悟和吐槽。

      总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。


      作者:拭心又在思考了我的天
      来源:mp.weixin.qq.com/s/TvfKTXUHZAR37f9oi3W12w

      收起阅读 »

      为什么手机厂商都纷纷入局自研操作系统?

      时间线 2020 年 9 月 10 日,华为召开了开发者大会,正式推出了 HarmonyOS 2.0 系统,并宣布为开发者提供完整分布式设备与应用开发生态。 2023年10月17日,小米集团首席执行官雷军正式公布了这款操作系统,并称该系统将逐步接替之前小米开...
      继续阅读 »

      时间线


      2020 年 9 月 10 日,华为召开了开发者大会,正式推出了 HarmonyOS 2.0 系统,并宣布为开发者提供完整分布式设备与应用开发生态。


      image.png


      2023年10月17日,小米集团首席执行官雷军正式公布了这款操作系统,并称该系统将逐步接替之前小米开发的MIUI系统,成为小米智能硬件互联生态的统一计算平台。首款搭载HyperOS的智能手机为小米14系列手机。10月20日、23日,MIUI的微信、微博等公众平台账号陆续将MIUI改为其中文名“小米澎湃OS”。


      image.png


      2023年11月3日消息,在此前举行的 2023 vivo 开发者大会上,vivo 已经宣布首款搭载蓝河操作系统的设备将是 vivo WATCH 3 手表。vivo 副总裁周围在接受媒体采访时明确表示,vivo 自研蓝河操作系统不兼容安卓应用。


      image.png


      以上这几年国内著名手机厂商公布自研操作系统的时间线。笔者最近比较对国内操作系统的发展动向比较感兴趣,所以就开始了解收集这方面的信息,想通过这篇文章帮我看清操作系统发展脉络或者说补充一些相关认知盲区。


      先用一张图厘清操作系统发展脉络


      大家应该都知道华为、小米和Vivo分别是国内三个著名的Android手机厂商。国内移动互联网快速的发展,也是得益于Android的开放性,国内手机厂商基本都是基于ASOP(Android Open Source Project)魔改源码逐渐形成自己的定制操作系统,比如HarmonyOS、小米的MIUI和Vivo的OriginOS。如下图所示:


      Xnip2023-11-20_19-34-03.jpg


      iOS 是由苹果开发的移动操作系统。苹果最早于2007年1月9日的Macworld大会上公布这个系统,最初是设计给iPhone使用的,后来陆续套用到iPod touch、iPad上。iOS与苹果的macOS操作系统一样,属于类Unix的商业操作系统。苹果凭借它极具竞争力的软硬件创新产品,妥妥是一方巨头,跟Android开放生态不一样,苹果的操作系统并不开源。


      但从2023年国内手机厂商公布的操作系统来看,从曾经套壳Android逐渐演变成去安卓化趋势。那为啥会有这种趋势发生,或者说这真的是未来操作系统发展趋势吗,国内厂商可能会面临什么样的挑战?这个问题的答案我们需要时间去验证,我们只能先做些畅想。


      国内手机厂商是如何发展起来的?


      国内手机厂商的发展主要经历了以下几个阶段:



      1. 初步阶段(2000-2003年):在这个阶段,国内手机市场刚刚起步,消费者对手机的需求逐渐增加。一些国内厂商开始生产手机,主要以低端和中端产品为主,如联想、TCL等。

      2. 品牌竞争阶段(2004-2006年):随着消费者对手机品牌的关注度增加,一些知名品牌开始崛起,如华为、中兴、酷派等。这些品牌通过自主研发、产品创新和技术积累,逐渐在中高端市场占据一席之地。

      3. 智能手机时代(2007-2010年):智能手机的兴起,为国产手机厂商提供了新的发展机遇。在这个阶段,小米、OPPO、vivo等厂商迅速崛起,通过高性价比、良好的用户体验和市场营销策略,逐渐在国内市场占据重要地位。

      4. 全球化竞争(2011年至今):随着国内手机市场的逐渐饱和,国产手机厂商开始将目光投向海外市场,如印度、东南亚等新兴市场。此外,厂商也在积极布局5G、物联网等新兴领域,以寻求新的增长点。


      Android系统最早于2008年9月23日正式发布。进入中国市场的时间则是在2009年左右,随着第一款搭载Android系统的手机HTC Dream(在中国市场称为G1)的上市,Android系统开始在中国市场逐渐普及。


      华为、小米和vivo这些手机厂商开始使用Android系统的时间如下:



      1. 华为:华为早在2009年就推出了第一款搭载Android系统的智能手机——华为U8220。此后,华为逐步发展成为全球领先的手机厂商之一,并在Android系统上推出了自家的EMUI(Emotion UI)用户界面。

      2. 小米:小米成立于2010年4月,同年8月推出了基于Android系统的MIUI定制系统。2011年8月,小米发布了第一款搭载Android系统的手机——小米1。凭借高性价比和出色的用户体验,小米迅速崛起成为国内外知名的手机品牌。

      3. vivo:vivo成立于2009年,2011年推出了第一款搭载Android系统的智能手机——vivo X1。随后,vivo在Android系统上推出了自家的Funtouch OS用户界面,并逐步发展成为市场份额较高的手机厂商之一。


      从上面我们回顾了下国内手机厂商的发展历史,可以说没有Android系统进入国内市场,中国的智能手机时代历史进程就不可能这么快,国内手机厂商也正是抓住了这波机遇完成了国产化,并且发展至今。


      更具象化分析智能手机厂商发展


      从我个人的分析理解,国内手机厂商发展分为以下几个阶段:



      1. 第一阶段:本土化定制,实现自主可控
        这个阶段国内手机厂商主要关注于对Android系统进行本土化定制,通过开发自家的用户界面(如华为的EMUI、小米的MIUI、vivo的Funtouch OS等),满足国内用户的需求和使用习惯。同时,厂商也开始关注硬件研发和生产,逐步减少对外部供应商的依赖,提高自主可控能力。

      2. 第二阶段:自家产品生态化拓展建设
        在第二阶段,国内手机厂商开始关注产品生态的构建,将手机作为核心,围绕其开发各类智能硬件和应用服务,如智能家居、可穿戴设备、云服务等。这有助于提高用户粘性,形成闭环的生态系统,从而提升品牌竞争力和市场份额。

      3. 第三阶段:公布自研操作系统,引入AI大模型完善产品体验
        在第三阶段,国内手机厂商开始研发自家的操作系统,如华为的鸿蒙OS(HarmonyOS),以降低对Android系统的依赖,提高自主创新能力。同时,厂商也开始引入人工智能技术,通过AI大模型优化产品性能、提升用户体验,为未来5G、物联网等新技术应用做好准备。


      目前手机厂商已经来到第三阶段,自华为公布鸿蒙系统之后,小米和Vivo也相继公布自家的自研操作系统,自主创新似乎是国内厂商不约而同的战略共识。


      避免被“卡脖子”再发生


      2019年5月15日,华为被美国正式制裁,截止目前,已经过去了4年。那么这些年华为到底被制裁了什么?


      华为受到的制裁主要包括以下几个方面:



      1. 美国实体清单:2019年5月,美国政府将华为及其关联公司列入实体清单,禁止美国企业与华为进行商业往来,除非获得特别许可。这意味着华为无法从美国供应商处购买关键零部件和技术,如芯片、软件等。

      2. 芯片供应限制:2020年5月,美国政府进一步收紧对华为的制裁,要求全球芯片制造商在向华为供应美国技术含量超过25%的产品时,必须获得美国政府的许可。这使得华为的芯片供应受到严重影响,尤其是高端芯片。

      3. 软件限制:华为受到制裁后,谷歌停止为华为新款手机提供GMS(谷歌移动服务),包括谷歌应用商店、Gmail、YouTube等关键应用。尽管华为推出了自家的HMS(华为移动服务)替代GMS,但在全球范围内,GMS仍然具有很高的市场需求和用户粘性。

      4. 网络设备限制:美国政府将华为视为国家安全威胁,禁止美国电信运营商使用联邦补贴购买华为的网络设备。此外,美国还在全球范围内施压,敦促其他国家在5G网络建设中排除华为。

      5. 金融制裁:美国政府还对华为进行金融制裁,限制华为及其关联公司在美国的金融交易,使其在全球范围内融资和开展业务受到限制。


      这些制裁对华为的业务造成了很大影响,尤其是在芯片供应、软件服务和5G网络设备方面。然而,华为在此期间加大了自主研发投入,努力寻求突破和替代方案。


      华为是如何应对技术封锁的?



      • 推出麒麟9000S芯片突破5G芯片断供


      image.png
      今年爆火的Mate60系列手机搭载就是这款芯片,华为终于扬眉吐气了一番。



      • 推出自研鸿蒙全场景分布式操作系统


      image.png



      • 推出HMS 替代 GMS


      image.png


      当然还有很多自主创新的突破,从这些战略手段来看,华为确实非常值得尊敬的企业,至少给国内众多科技企业打了个样。


      从华为被制裁的案例来看,国内其他一众手机厂商谁都无法预测自己可能是下一个华为。那么实现自主可控就显得非常迫切了,除非国内一众厂商放弃海外市场这块蛋糕,但这看起来是不太可能的事情。


      万物互联生态蛋糕


      我们知道不管是华为还是小米,它们的产品可不仅仅只是手机和PC,还有全场景的IoT智能硬件(包括智能家居、可穿戴设备,平板等等),未来还有智能驾驶汽车业务,比如现在小米正在如火如荼正在进行的造车运动。


      小米IoT平台设备接入数据
      image.png
      小米IoT万物互联布局
      image.png


      小米智能汽车
      image.png


      面对如此庞大的设备和连接数,小米需要一套融合的系统框架统一支持全生态设备与应用,要成为未来百亿设备、百亿连接,万物互联的公有底座。这也能理解这为什么小米会推出澎湃OS来替代MIUI,未来构建万物互联的大格局所做出的战略性布局。


      然而国内手机厂商的产品布局大致相同,属于强竞争关系,虽说鸿蒙系统较早就发布,但从商业竞争来看,这是另外一个层面的自主可控,让小米、Vivo这些厂商投入到鸿蒙怀抱目前来看是不太现实的,毕竟谁都不想当谁的小弟,或许形成操作系统联盟生态更合适。


      写在最后


      从最开始时间线开始我们了解到国产厂商纷纷入局自研操作系统,然后通过一张图理解了操作系统的发展脉络,再了解到国产手机厂商是如何发展起来的,Android的进入让国产手机进入智能化时代,我们再具象化理解目前如华为、小米和Vivo这些手机厂商发展的几个阶段。从华为被美国制裁,国内厂商引发的担忧和战略布局万物互联生态,最终为了能够实现自主可控,摆脱外部依赖的风险,纷纷入局自研操作系统。从各家厂商分别分布自己家的自研操作系统,说明这件事相比于自研芯片的难度要小很多。从操作系统发展脉络来看至少是站在巨人的肩膀上,完全自主产权这样的说法听听就好。要撼动Android和iOS的地位,要看开发者买不买单,这么多年所沉淀下来的生态要是这么容易就能够替代,那么当年Windows Phone就不会落寞而去,相比于微软大家觉得我们的差距还有多大呢。


      作者:巫山老妖
      来源:juejin.cn/post/7303413519907586057
      收起阅读 »

      虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑

      今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。 2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然...
      继续阅读 »

      今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。


      2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。


      股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。


      接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!


      本文倒叙描述,先聊聊最后的疯狂和偏执!


      不甘失败,疯狂上杠杆


      股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。


      我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!


      终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。


      没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!


      image.png


      这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!


      当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。


      正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。


      后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。


      我刚毕业,哪来的那么多钱!


      我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。


      于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!


      于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。


      不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!


      跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。


      我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。


      我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。


      当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。


      所以股价最低点的时候,基本都没有钱加仓。


      侥幸反弹,但不忍心止盈


      股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。


      股价开始慢慢回暖,后来开始凶猛的反弹,当时的我一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!


      后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。


      我觉得还会继续涨,我还在畅想:公司达到,万亿市值。


      我觉得自己当时真的 失了智了。


      结婚买房,卖在最高点


      这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。


      因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。


      后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。


      后来我盘算着,不对劲。我还有贷款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。


      不行,我要把贷款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。


      我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!


      在这这样的焦虑之下,我把股票全部都卖光了!


      冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!


      image.png


      全是侥幸


      我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。


      我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!


      于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!


      不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!


      然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。


      我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!


      我觉得,自己就是一个赌狗!


      image.png


      image.png


      然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!


      极其谨慎的理财开局


      一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。


      谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。


      盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。


      后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。


      是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!


      如饥似渴的学习投资技巧


      经过一段时间的理财,我对于理财越来越熟悉。


      胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。


      尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。


      有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。


      于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!


      那一刻,我飘飘然……


      我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。


      我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。


      总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。


      换句话说:我上头了


      非常荒谬的炒股开局


      当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。


      由此可见,股市里难有常胜将军!


      当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。


      当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……


      自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭


      这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!


      换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。


      image.png


      刚入股市,就赶上牛市,顺风顺水


      2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。


      上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!


      image.png


      在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。


      开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。


      我自认为,我能禁得住 炒股这个毒品。


      但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!


      长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。


      其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……


      当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……


      换句话说:岂止是炒股上头,我已经中毒了!


      image.png


      之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……


      总结


      以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。


      炒股就是赌博


      我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。


      赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!


      什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?


      漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!



      ”把钱还我,我不玩了“




      ”我只要把钱赢回来,我就不玩了“



      这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。


      尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!


      能救一个是一个!


      作者:五阳神功
      来源:juejin.cn/post/7303348013934034983
      收起阅读 »

      这样解释shift,面试官直接起立!

      面试官提问 面试官:候选人你好,请解释下面的现象: 数组1000个元素,shift操作和对象delete操作的benchmark 差不多。 数组10w个元素时,shift操作和对象delete操作的benchmark 相差巨大。 场景如下: 数组1000个...
      继续阅读 »

      面试官提问


      面试官:候选人你好,请解释下面的现象:



      1. 数组1000个元素,shift操作和对象delete操作的benchmark 差不多。

      2. 数组10w个元素时,shift操作和对象delete操作的benchmark 相差巨大。


      场景如下:
      数组1000个元素时:shift和delete相差无几
      63368106fc02fd76c9cd6cc951dc064.png


      数组100000个元素时,shift比delete慢了100倍
      a120b0387b1118d83ffbc4ac0b3f052.png


      开始表演!


      候选人:总的来说,这是因为数组在1k长度时,v8引擎能申请到一段连续内存做shift运算,在利用L1缓存的优势下,速度能和object的delete有得一比。


      顺便一提,利用L1缓存思想做性能优化,也是最近游戏界中ECS架构为何能获得如此高的关注度的原因之一。


      我们回到咱们前端来讲,口说无凭,眼见为实,让我们直接打开V8引擎源码一睹为快。


      第一步、我们找到[].shift相关v8引擎源码


      我们可以看到ArrayPrototypeShift代码会尝试执行TryFastArrayShift函数,若该函数抛出Slow或者Runtime标志,则运行相应的逻辑。


      image.png


      第二步、我们进入 TryFastArrayShift 这个函数继续看,这个函数有两个逻辑:



      1. 若没有连续内存,则抛出Slow

      2. 若数组长度>100,则抛出Runtime



      回到我们案例:我们数组长度如果在1000,则抛出的是Runtime标志;如果在10W,则抛出的是Slow标志。



      image.png


      第三步、查看Slow和Runtime的逻辑。




      • slow对应的GenericArrayShift函数逻辑如下 :



        • 先把数组转换为对象

        • 再遍历对象的key,每一个key都往前移一位。(这也是ECMAScript-262规范定义的算法)
          image.png




      • runtime对应的ArrayShift函数逻辑如下:



        1. 申请连续内存

        2. 遍历并移位

          image.png




      最后总结,从上面三步我们确认了V8引擎的执行逻辑:



      1. 10W数据,很难申请到连续内存,通常就无法利用L1缓存,导致比较卡慢

      2. 1000数据,较容易申请到连续内存,通常能利用到L1缓存,速度较快。

        1. 100长度以下的数组,直接走C++逻辑。

        2. 100长度以上的数组,走汇编逻辑加速。




      这也就说明了为什么1000数据和10W数据执行上有一定差异的原因。



      注意:ArrayShift函数源码可能有误,也请大佬指点。


      PS:这个ArrayShift源码不在v8中,而在汇编中。


      PS:这是因为buitin的代码需要在vs上编译出来才能查到相关代码的引用地址,目前比较忙,先分享思路,我后续会进行更新订正。



      小结


      上诉面试经历是我刚编的,希望能来更多的朋友讨论,毕竟大家对v8源码或多或少有一点陌生的畏难心理,我也如此。本问题实际是来自我交流群的某一次讨论。


      不过道理是真的:


      平时遇到某些问题,咱们也不妨从V8源码入手分析。


      有时候源码比各类文章讲得更清晰。


      日积月累,总会有一些意想不到的收获。


      参考文章



      作者:尘码在划水
      来源:juejin.cn/post/7302330573382107148
      收起阅读 »

      Altman王者归来!强势要求解散董事会,OpenAI终极宫斗一触即发

      【新智元导读】 董事会打脸了!Altman众望所归上演王者归来,戴着访客证出现在OpenAI总部,并且强势要求董事会解散。CEO的复仇之路反转再反转,双方目前仍在对峙。 从被扫地出门到王者回归,乔布斯用了12年,而Sam Altman,仅仅用了两天。 现在,A...
      继续阅读 »
      【新智元导读】 董事会打脸了!Altman众望所归上演王者归来,戴着访客证出现在OpenAI总部,并且强势要求董事会解散。CEO的复仇之路反转再反转,双方目前仍在对峙。

      从被扫地出门到王者回归,乔布斯用了12年,而Sam Altman,仅仅用了两天。


      现在,Altman已经以胜利者的姿态重返OpenAI探讨自己的去留问题,并且对董事会提出了新的要求——


      「更换现有的董事会成员,并且得到证明自己并无过错的声明。」


      简单来说就是,我可以回来,但你们得走。

      图片临时CEO Mira Murati、首席战略官Jason Kwon、首席运营官Brad Lightcap,都站在了Altman这一边,希望董事会辞职。董事们让步了,原则上同意辞职,但还未正式执行,正在评估新董事的人选。截止发稿时,双方还在僵持中。但Altman,应该是已经掌握了主动权。


      「王者回归」之路



      当地时间周日,六小时之前,Sam Altman po出自己佩戴OpenAI访客证进入大楼的照片,皱着眉、眼神复杂地望向镜头,同时打下这样一句话——



      这是我第一次,也是最后一次,戴上OpenAI的访客证。



      图片而在Altman被离职的同时也一起辞职的OpenAI总裁Greg Brockman,也和Altman一起与OpenAI展开了谈判。上周六,四人董事会将Altman无情踢出之后,又在周日反悔了,跪求Altman重返OpenAI。原因一方面是金主爸爸们给董事会的压力,另一方面,则是大量员工的追随和支持。董事会是让步了,但Altman却未必会接受了。现在,他手头的选择很多,如果回OpenAI,他就要求重新设立新董事会;或者,他甚至可以带着大批愿意离职的前员工,直接另起炉灶创立新公司。从这里也能看出,真正让Altman不可替代的,是OpenAI顶级科学家对他的无限忠诚。他们,才是OpenAI的中流砥柱,也是ChatGPT的核心贡献者。


      金主之怒


      据彭博社报道,微软CEO纳德拉对于董事会的行为非常愤怒。据悉,他在事件爆发后一直和Altman保持着联系,并且保证会支持他。要知道,微软是OpenAI最大的投资者,投入了130亿美元,拥有OpenAI Global LLC 49%的股份。图片与此同时,OpenAI最主要的风投支持者们,包括其第二大股东Thrive Capital、Tiger Global、Khosla Ventures以及Sequoia Capital,都表示希望Altman回归。而且,无论Altman接下来要做什么,他们都会给予支持。图片这不由得让人想起硅谷的另一起著名事件——众所周知,史蒂夫·乔布斯在1985年的时候被自己亲手创立的苹果解雇。随后,他创立了NeXT,一家生产高端计算机的公司。而彼时的苹果,已经风雨飘摇。1997年,乔布斯正式回归。很快,他就把苹果从一个苦苦挣扎的科技公司转变为一个全球巨头。图片


      员工纷纷表态


      另一创始人、OpenAI总裁及董事会主席Greg Brockman,在第一时间辞职,坚决表示自己和Altman同进退。图片随着事情的发酵,Altman发推表示:我太爱OpenAI团队了。图片同时,大量OpenAI的核心员工和高管,都转发了Altman的推特,纷纷po出爱心,表示支持。图片这些OpenAI核心员工对于Altman的支持,似乎在告诉董事会,开了他,OpenAI很有可能面临大量的员工流失。图片而这些人,正是OpenAI能够走到今天,成为科技圈最受瞩目,甚至能够改变科技行业未来的公司的原因。图片为了安抚员工,OpenAI和Altman展开复职谈判之后,OpenAI高管在一份发给员工的备忘录中称,他们对Altman和Brockman的回归「非常乐观」。


      董事会被架到火上烤


      现在,Altman复职谈判最大的障碍是,他希望能够解散炒掉他的董事会,并引入新的董事会成员。对此,原董事会很有可能不得不重新发表一个声明,推翻原本炒掉Altman的声明,为Altman平反。这样的话,他们不但把自己架到了火上烤,还让所有人都有理由对董事会的「合法性」提出质疑。图片根据外媒报道,如果董事会真的重组,新加入董事会的成员可能会包括:Salesforce Inc.前联席首席执行官Bret Taylor。图片以及另一位来自微软的高管。而推动前董事会裁掉Altman的OpenAI首席科学家Ilya,能否继续留在董事会之中,就不得而知了。毕竟,矛盾的地方在于,不久前的开发者大会已经充分昭示了Altman的商业野心。而董事会成员,尤其以Ilya为主,则对AI的安全性产生了担忧。对此,马斯克也不忘趁此时机倒油,表达对Ilya的支持,同时也就间接表达了对Altman的质疑。



      我十分担心。Ilya有良好的道德观,他并不是一个追求权力的人。除非他认为绝对必要,否则他绝不会采取如此激进的行动。



      图片


      虽然,董事会还在犹豫,但如果Altman真要决定创办新公司,必定有一大批员工会忠心追随。


      太长不看版


      总结一下就是,在过去短短几天内,OpenAI就发生了一系列惊天大动荡——



      • OpenAI发布公告,宣布解除Sam Altman CEO和Greg Brockman董事会主席的职务。



      • CTO Mira Murati被任命为临时CEO。



      • 很快,Brockman也发帖表示已经辞职。



      • Altman和Brockman发表联合声明,对董事会的做法表示「震惊和悲痛」。


      图片



      • 三位高级研究员Jakob Pachocki、Aleksander Madry和Szymon Sidor,纷纷辞职表示抗议。



      • 据了解,首席科学家Ilya Sutskever在解雇Altman的过程中发挥了关键作用。



      • 第二天,OpenAI似乎迫于压力,又想让Altman回归CEO职位。



      • 对此,Altman提出了自己的条件,包括要求解雇他的董事会成员辞职。而董事会则犹豫不决。



      • 有媒体报道称,如果未能达成协议,将会有大批员工辞职。


      Altman有意成立新的AI公司



      据知情人士透露,Altman正计划成立一家新的人工智能企业。而OpenAI前总裁Greg Brockman有望加入该公司。不过,目前我们还不清楚这家企业的具体情况。图片


      与此同时,关于Altman在开发人工智能方面的雄心壮志和更多细节也已浮出水面。不久前,他与包括芯片设计公司Arm在内的半导体高管进行了讨论,商讨如何尽早设计出新的芯片,为OpenAI这样的大语言模型公司降低成本。


      图片


      据彭博社报道,Altman计划创办的这家芯片公司,将会打造类似于谷歌TPU(张量处理单元)的人工智能芯片。为此,Altman一直在中东为这个代号为Tigris的项目筹集资金。


      TPU等定制设计的芯片被认为有朝一日有可能超越英伟达制造的人工智能加速器。人工智能公司都对其梦寐以求,但开发一款AI芯片需要巨量的时间和资源。


      图片


      不过公司尚未成立,与投资者的谈判也还处于早期阶段。但无论新公司采取何种形式,Altman和Brockman都可以避免重复目前在OpenAI遇到的问题。除了这家芯片公司,Altman还一直在为他与苹果公司前设计总监Jony Ive合作开发的一款人工智能硬件设备筹集资金。


      图片


      据一位知情人士透露,最近几个月,Altman定期参加孙正义在加州Woodside豪宅举行的晚宴,与微软首席执行官Satya Nadella等其他科技高管讨论人工智能、芯片和其他科技话题的未来。或许,正是Altman的野心和副业,使他与董事会本已紧张的关系变得更加复杂。


      竞争对手急于挖角


      这边Altman前脚刚走,Cohere和Adept等竞争对手已经开始在OpenAI挖人了,而谷歌DeepMind也收到了来自OpenAI员工的新简历。


      这些举动表明了Altman下台后OpenAI面临的风险:四位高管的离职有可能引发一连串的辞职潮,使其难以维持去年的高速发展。随着Altman考虑回归,许多高管也在考虑回归。


      图片


      Adept是一家估值10亿美元的初创公司,它正在创建一个人工智能模型,可以在用户的电脑上为他们完成任务。


      该公司的代表在Altman被解雇后24小时内,联系了OpenAI的多名现任工程师和研究人员。


      一位知情人士表示,OpenAI的一些员工在董事会发表声明解雇Altman后的几个小时内,向谷歌的人工智能实验室DeepMind提交了简历。OpenAI的主要创业公司竞争对手在LinkedIn上发布了一则招聘信息,称其正在招聘多名技术项目经理。


      图片


      而Cohere公司的联合创始人兼首席执行官Aidan Gomez则在公司的招聘页面上发布了一个链接,指出该公司正在招聘「机器学习技术人员」。


      图片


      代码生成初创公司Replit的创始人兼首席执行官Amjad Masad也同样在OpenAI宣布领导层变动三小时后发布了公司招聘页面的链接。


      图片


      ——看起来像是落井下石?不过这种相互挖角的事情对于OpenAI等公司可能也习以为常。OpenAI成立于2015年,目前有700多名员工,其中一些人就是从谷歌、Meta和Stripe等大型科技公司挖来的。去年他们高调聘用的一些员工包括特斯拉自动驾驶汽车前主管Andrej Karpathy和Stripe隐私与数据保护前主管EmmaRedmond。据知情人士透露,OpenAI最近通过提供数百万美元的股票套餐来吸引谷歌的员工。毫不夸张地说,Altman在从其他公司招募人才方面发挥了关键作用。


      网友吃瓜整活乐开了花


      微软在幕后发大力了,现在看看OpenAI谁说了算。图片Altman亮出了自己的最后王牌,「权力转换卡」!图片相同的姿势,相同的结局,只是Altman效率高太多了。图片和一年前马老板收购推特后相似的场景再次上演!图片


      参考资料:


      time.com/6337449/ope…


      http://www.theinformation.com/articles/al…


      http://www.theinformation.com/articles/op…


      http://www.theverge.com/2023/11/19/…


      http://www.theverge.com/2023/11/19/…


      作者:新智元
      来源:juejin.cn/post/7303423871708987427
      收起阅读 »