注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

不过是享受了互联网的十年红利期而已。

你好呀,我是歪歪。 去年的最后一天,我在 B 站发布了这个视频: 我真没想到这个长达一个小时的视频的播放量能到这么多,而且居然是一个投币比点赞和收藏还多的视频。 评论区加上弹幕,有上千条观众的留言。每一条留言都代表一个观众的感受,里面极大部分的感受总结起来大...
继续阅读 »

你好呀,我是歪歪。


去年的最后一天,我在 B 站发布了这个视频:



我真没想到这个长达一个小时的视频的播放量能到这么多,而且居然是一个投币比点赞和收藏还多的视频。


评论区加上弹幕,有上千条观众的留言。每一条留言都代表一个观众的感受,里面极大部分的感受总结起来大多是表示对于我个人这十年经历感叹和羡慕,但是我是觉得十年的时间这么长,谁还不能提炼出几两故事和几段感悟呢?


觉得感叹的,只不过是在我的视频里面看到了几分自己的影子。觉得羡慕的,只不过是以另外一种我羡慕的方式生活着。


还是有人说是逆袭,我并不觉得这是逆袭。逆袭一般是说绝地反击的故事,但是我觉得这十年,我还没有真正的走到过“绝地”,更多的只是随着时代随波逐流,我个人的努力,在时代的浪潮前,微不足道,只不过在一系列的机缘巧合之下,我使劲的方向恰好和浪潮的方向一致而已。


我当时没有想到一个好的词语来形容这个“和浪潮的方向一致”,所以过年期间我也一直在仔细的思考这个问题。


直到过年期间,我坐在火炉前听家里的长辈聊天,一个长辈问另外一个晚辈:你什么时候把女朋友带回来给我们看看呢?


晚辈戏谑的回答说:我现在自己都过的不好呢,怕是没有女孩子愿意跟我哦。


长辈说:我以前嫁过来的时候,你爷爷以前还不是啥都没有,就一间土巴屋,一个烂瓦房。结婚嘛,两个人一起努力不就把日子过好了。


我当时好想说一句:那个时代过去了,现在不一样了。


然而终究还是没说出口,但是就在把这句话咽下去的瞬间,我想明白了前面关于“浪潮”的问题,其实就一句话:


我只不过是有幸享受到了时代的红利罢了。有时候的看起来让人羡慕的人、成功的人,只不过是享受到了时代的红利罢了,与个人的努力程度关系真的不大。


我说的时代的红利,就是互联网技术、计算机专业野蛮发展的这十年光景。


在视频里面,我说了一句话:我是被调剂到计算机专业的。


然后有一些弹幕表示非常的震惊:



是的,在 2012 年,计算机专业并不是一个被看好的热门专业,甚至有点被“淘汰”的感觉。


我记得那一年录取之后,给一个亲戚说是就读计算机专业,他说:怎么学了个这专业啊,以后每个家庭里面都会有一台计算机,到时候人人都会使用计算机,还学它干啥?


这句话虽然现在看起来很搞笑,但是在那个时候,我还没有接触到它的时候,我觉得很有道理。


虽然我是调剂到“计算机”的,但是前提也得是我填报志愿的时候填写了“计算机专业”,对吧。


所以问题就来了:我当年是怎么高瞻远瞩,怎么深思熟虑,怎么推演计算,怎么预测未来,想着要填报一个计算机专业呢?


为了回答这个问题,我今年回到老家,专门把这个东西翻了出来:



这是我高考结束那年,学校发的 4 本和填报志愿相关的书,书里面主要的内容就是过去三年各个批次,各个学校,各个专业的报考人数、录取人数、录取最低分数、录取平均分数、录取最高分数的信息统计:



我当年填报志愿,就是通过翻阅这四本书来找到自己可以填报的大学。但是我的高考志愿特别好填,因为我高考成绩只超过二本线 13 分,所以我直接看二本院校里面每年卡着分数线招收学生的学校就可以了。在这个条件下,没有多少学校可以选择。


最后录取我的大学,是 2012 年那一年刚刚由专科学校升级为二本院校的一所大学。所以那一年是它第一次招本科生,没有过往的数据可以参考,报它的原因是因为我感觉它刚刚从专科升级为本科,录取分数应该不会太高。


填报志愿的时候一个学校可以填写六个专业,刚好它也只有六个本科专业,所以我就按照报纸上的顺序,挨个填写,而且还勾选了“服从调剂”。


而这六个专业,我也通过前面的书翻到了:



当时对于这六个专业,我完全没有任何了解。根本不知道要学习什么内容,更加不知道毕业之后会从事什么工作。


后来入校之后我才知道,报材料成型及控制工程和机械电子工程专业的人最多,计算机科学与技术由于报的人没有报满,所以我被调剂过去了。


可以肯定的说,如果当年这个学校没有计算机的本科专业,我就不会走上计算机的道路。


其实我填报志愿的状态,和当年绝大部分高考学生的状态一样,非常的茫然。在高中,学校也只教了高考考场上要考的东西,为了这点东西,我们准备了整整三年。但是现在回头去看,如何填报志愿,其实也是一件非常值得学习了解的事情,而不是高考结束之后,学校发几本资料就完事的。


我当时填报志愿时最核心的想法是,只要有大学录取就行了,至于专业什么的,不重要。


在志愿填报指南的书里面,我发现有大量的篇幅站在 2012 年视角去分析未来的就业形势。



这部分,我仔细的读了一遍,发现关于计算机部分提到的并不多,只有寥寥数语,整体是持看好态度,但是大多都是一些正确的“废话”,对于当年的我来说,很难提炼出有价值的信息,来帮助我填写志愿。


后来得知被计算机录取了之后的第一反应是,没关系,入校之后可以找机会转专业,比如转到机械。


为什么会想着机械呢?


因为那一年,或者说那几年,最火的专业是土木工程,紧随其后的大概就是机械相关的专业:



而这个学校没有土木专业,那就是想当然的想往人多的,也是学校的王牌专业“机械”转了。


计算机专业,虽然也榜上有名,但是那几年的风评真的是非常一般,更多的是无知,就像是亲戚说的那句:以后人人都有一台计算机,你还去学它干啥?


我也找到了一份叫做《2011年中国大学生就业报告》的报告,里面有这样一句话:



真的如同弹幕里面一个小伙伴说的:土木最火,计算机下水道。


所以我在十年前被调剂到计算机专业,也就不是一个什么特别奇怪的事情了。


你说这是什么玩意?


这里面没有任何的高瞻远瞩、深思熟虑、推演计算、预测未来,就是纯粹的运气。


就是恰好站在时代的大潮前,撅着屁股,等着时代用力的拍上那么一小下,然后随着浪花飘就完事了吗?


我也曾经想过,如果我能把它包装成一个“春江水暖鸭先知”的故事,来体现我对于未来精准的预判就好了,但是现实情况就是这么的骨感和魔幻,没有那么多的预判。


所以有很多人,特别是一些在校的或者刚刚毕业的大学生,通过视频找到我,来请教我关于职业发展,关于未来方向,关于人生规划的问题。



说真的,我有个屁的资格和能力来帮你分析这些问题啊。我自己这一摊子事情都没有搞清楚,我的职业前路也是迷雾重重,我何德何能给别人指出人生的方向?


当然,我也能给出一些建议,但是我能给出的所有的回复,纯粹是基于个人有限的人生阅历和职业生涯,加上自己的一些所见所闻,给出的自己角度的回答。


同样的问题,你去问另外一个人,由于看问题的角度不同,可能最终得出的答案千差万别。


甚至同样的职场相关的问题,我可以给你分析的头头是道,列出一二三四点,然后说出每一点的利益得失,但是当我在职场上遇到一模一样的问题时,我也会一时慌张,乱了阵脚,自然而然的想要去寻求帮助。


在自媒体的这三年,我写过很多观点输出类的文章,也回答过无数人的“迷茫”。对于这一类求助,有时是答疑,常常是倾听,总是去鼓励。


我并不是一个“人生导师”,或者说我目前浅薄的经验,还不足以成为一个“人生导师”,我只不过是一个有幸踩到了时代红利的幸运儿而已。


在这十年间,我踩到了计算机的红利,所以才有了后面看起来还算不错的故事。


踩到了 Java 的红利,所以才能把这个故事继续写下去。


踩到了自媒体的红利,所以才有机会把这些故事写出来让更多的人看到。


现在还有很多很多人摩肩擦踵的往计算机行业里面涌进来,我一个直观的感受就是各种要求都变高了,远的就不说了,如果是三年前我回到成都的时候,市场情况和现在一样的话,我是绝对不可能有机会进入到现在这家公司,我只不过是恰好抓住了一个窗口期而已。


还有很多很多的人,义无反顾的去学 Java,往这个卷得没边的细分领域中冲的不亦乐乎,导致就业岗位供不应求,从而企业提升了面试难度。我记得 2016 年我毕业的时候,在北京面试,还没有“面试造火箭”的说法,当年我连 JVM 是啥玩意都不知道,更别提分布式相关的技术了,听都没听过。然而现在,这些都变成了“基础题”。


还有很多人,看到了自媒体这一波流量,感觉一些爆款文章,似乎自己也能写出来,甚至写的更好。或者感觉一些非常火的视频,似乎自己也能拍出来,甚至拍的跟好。


然而真正去做的话,你会发现这是一条“百死一生”的道路,想要在看起来巨大的流量池中挖一勺走,其实很难很难。


但是如果把时间线拉回到 2014 年,那是公众号的黄金时代,注册一个公众号,每天甚至不需要自己写文章,去各处搬运转载,只需要把排版弄好看一点,多宣传宣传,然后坚持下去,就能积累非常可观的关注数量,有关注,就有流量。有流量,就有钱来找你。从一个公众号,慢慢发展为一个工作室,然后成长为一个公司的故事,在那几年,太多太多了。


诸如此类,很多很多的现象都在表明则一个观点:时代不一样了。


我在刚刚步入社会的时候,看过一本叫做《浪潮之巅》的书,书里面的内容记得不多了,但是知道这是一本把计算机领域中的一些值得记录的故事写出来的好书。


虽然书的内容记得不多了,但是书的封面上写的一段话我就很喜欢。


就用它来作为文章的结尾吧:



近一百多年来,总有一些公司很幸运地、有意识或者无意识地站在技术革命的浪尖之上。一旦处在了那个位置,即使不做任何事,也可以随着波浪顺顺当当地向前漂个十年甚至更长的时间。在这十几年间,它们代表着科技的浪潮,直到下一波浪潮的来临。这些公司里的人,无论职位高低,在外人看来,都是时代的幸运儿。因为,虽然对一个公司来说,赶上一次浪潮不能保证其长盛不衰;但是,对一个人来说,一生赶上一次这样的浪潮就足够了。一个弄潮的年轻人,最幸运的,莫过于赶上一波大潮。



以上。








如果我这篇文章结束在这个地方,那么你先简单的想一想,你看完之后那一瞬间之后的感受是什么?


会不会有一丝丝的失落感,或者说是一丢丢的焦虑感?


是的,如果我的文章就结束在这个地方,那么这就是一篇试图“贩卖焦虑”的文章。


我在不停的暗示你,“时代不一样了”,“还是以前好啊”,“以前做同样的事情容易的多”。


这样的暗示,对于 00 后、90 后的人来说,极小部分感受是在缅怀过去,更多的还是让你产生一种对当下的失落感和对未来的焦虑感。


比如我以前看到一些关于 90 年代下海经商的普通人的故事。就感觉那个时代,遍地是黄金,处处是机会,只要稍稍努力就能谱写一个逆天改命的故事,继而感慨自己的“生不逢时”。


只是去往回看过去的时代,而没有认真审视自己的时代,当我想要去形容我所处的时代的时候,负面的形容词总是先入为主的钻进我的脑海中。


我之前一直以为是运气一直站在我这边,但是我真的是发布了前面提的到视频,然后基于视频引发了一点讨论之后,我才开始更加深层次的去思考这个问题,所以我是非常后知后觉的才感受到,我运气好的大背景是因为遇到了时代的红利。


要注意前面这一段话,我想强调的是“后知后觉”这个词。这个词代表的时间,是十年有余的时间。


也就是说在这十年有余的时间中,我没有去刻意的追求时代的红利、也没有感知到时代的红利。


这十年间,概括起来,我大部分时间只是做了一件事:努力成长,提升自我。


所以在我的视频的评论区里面还有一句话出现的频率特别高:越努力,越幸运。


我不是一个能预判未来的人,但是我并不否认,我是一个努力的人,然而和我一样努力,比我更加努力的人也大有人在。


你要坚信,你为了自己在社会上立足所付出的任何努力是不可能会白费的,它一定会以某种形式来回报你。


当回报到来的时候,也许你认为是运气,其实是你也正踩在时代的红利之上,只不过还没到你“后知后觉”的时候,十年后,二十年后再看看吧。


在这期间,不要囿于过去,不要预测未来,你只管努力在当下就好了。迷茫的时候,搞一搞学习,总是没错的。



(特么的,这味道怎么像是鸡汤了?不写了,收。)



最后,用我在网上看的一句话作为结尾吧:



我未曾见过一个早起、勤奋,谨慎,诚实的人抱怨命运不公;我也未曾见过一个认真负责、努力好学、心胸开阔的年轻人,会一直没有机会的。



以上就是我对于处于“迷茫期”的一些大学生朋友的一点点个人的拙见,也是我个人的一些自省。


共勉。


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

Kotlin协程:协程上下文与上下文元素

一.EmptyCoroutineContext     EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的...
继续阅读 »

一.EmptyCoroutineContext


    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的context,minusKey方法返回自身,代码如下:

public object EmptyCoroutineContext : CoroutineContext, Serializable {
private const val serialVersionUID: Long = 0
private fun readResolve(): Any = EmptyCoroutineContext

public override fun <E : Element> get(key: Key<E>): E? = null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
public override fun plus(context: CoroutineContext): CoroutineContext = context
public override fun minusKey(key: Key<*>): CoroutineContext = this
public override fun hashCode(): Int = 0
public override fun toString(): String = "EmptyCoroutineContext"
}

二.CombinedContext


    CombinedContext是组合上下文,是存储Element的重要的数据结构。内部存储的组织结构如下图所示:

image.png


    可以看出CombinedContext是一种左偏(从左向右计算)的列表,这么设计的目的是为了让CoroutineContext中的plus方法工作起来更加自然。


    由于采用这种数据结构,CombinedContext类中的很多方法都是通过循环实现的,代码如下:

internal class CombinedContext(
// 数据结构左边可能为一个Element对象或者还是一个CombinedContext对象
private val left: CoroutineContext,
// 数据结构右边只能为一个Element对象
private val element: Element
) : CoroutineContext, Serializable {

override fun <E : Element> get(key: Key<E>): E? {
var cur = this
while (true) {
// 进行get操作,如果当前CombinedContext对象中存在,则返回
cur.element[key]?.let { return it }
// 获取左边的上下文对象
val next = cur.left
// 如果是CombinedContext对象
if (next is CombinedContext) {
// 赋值,继续循环
cur = next
} else { // 如果不是CombinedContext对象
// 进行get操作,返回
return next[key]
}
}
}
// 数据结构左右分开操作,从左到右进行fold运算
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)

public override fun minusKey(key: Key<*>): CoroutineContext {
// 如果右边是指定的Element对象,则返回左边
element[key]?.let { return left }
// 调用左边的minusKey方法
val newLeft = left.minusKey(key)
return when {
// 这种情况,说明左边部分已经是去掉指定的Element对象的,右边也是如此,因此返回当前对象,不需要在进行包裹
newLeft === left -> this
// 这种情况,说明左边部分包含指定的Element对象,因此返回只右边
newLeft === EmptyCoroutineContext -> element
// 这种情况,返回的左边部分是新的,因此需要和右边部分一起包裹后,再返回
else -> CombinedContext(newLeft, element)
}
}

private fun size(): Int {
var cur = this
//左右各一个
var size = 2
while (true) {
cur = cur.left as? CombinedContext ?: return size
size++
}
}

// 通过get方法实现
private fun contains(element: Element): Boolean =
get(element.key) == element

private fun containsAll(context: CombinedContext): Boolean {
var cur = context
// 循环展开每一个CombinedContext对象,每个CombinedContext对象中的Element对象都要包含
while (true) {
if (!contains(cur.element)) return false
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return contains(next as Element)
}
}
}
...
}

三.Key与Element


    Key接口与Element接口定义在CoroutineContext接口中,代码如下:

public interface Key<E : Element>

public interface Element : CoroutineContext {
// 一个Key对应着一个Element对象
public val key: Key<*>
// 相等则强制转换并返回,否则则返回空
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
// 自身与初始值进行fold操作
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
// 如果要去除的是当前的Element对象,则返回空的上下文,否则返回自身
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}

四.CoroutineContext


    CoroutineContext接口定义了协程上下文的基本行为以及Key和Element接口。同时,重载了"+"操作,相关代码如下:

public interface CoroutineContext {

public operator fun <E : Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
// 如果要与空上下文相加,则直接但会当前对象,
if (context === EmptyCoroutineContext) this else
// 当前Element作为初始值
context.fold(this) { acc, element ->
// acc:已经加完的CoroutineContext对象
// element:当前要加的CoroutineContext对象

// 获取从acc中去掉element后的上下文removed,这步是为了确保添加重复的Element时,移动到最右侧
val removed = acc.minusKey(element.key)
// 去除掉element后为空上下文(说明acc中只有一个Element对象),则返回element
if (removed === EmptyCoroutineContext) element else {
// ContinuationInterceptor代表拦截器,也是一个Element对象
// 下面的操作是为了把拦截器移动到上下文的最右端,为了方便快速获取
// 从removed中获取拦截器
val interceptor = removed[ContinuationInterceptor]
// 若上下文中没有拦截器,则进行累加(包裹成CombinedContext对象),返回
if (interceptor == null) CombinedContext(removed, element) else {
// 若上下文中有拦截器
// 获取上下文中移除到掉拦截器后的上下文left
val left = removed.minusKey(ContinuationInterceptor)
// 若移除到掉拦截器后的上下文为空上下文,说明上下文left中只有一个拦截器,
// 则进行累加(包裹成CombinedContext对象),返回
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
// 否则,现对当前要加的element和left进行累加,然后在和拦截器进行累加
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}

public fun minusKey(key: Key<*>): CoroutineContext

... // (Key和Element接口)
}

1.plus方法图解


    假设我们有一个上下文顺序为A、B、C,现在要按顺序加上D、C、A。


1)初始值A、B、C

27ee3db5-ba83-4f8b-b155-de7974e76e4a.png

2)加上D

335ec6b6-b12f-4367-a274-5f65b4330517.png

3)加上C

6c36e62f-f050-47ca-b769-c29a91ef6f07.png

4)加上A

de380c56-5377-4fcc-a8c3-e6a579bf6609.png


2.为什么要将ContinuationInterceptor放到协程上下文的最右端?


    在协程中有大量的场景需要获取ContinuationInterceptor。根据之前分析的CombinedContext的minusKey方法,ContinuationInterceptor放在上下文的最右端,可以直接获取,不需要经过多次的循环。


五.AbstractCoroutineContextKey与AbstractCoroutineContextElement


    AbstractCoroutineContextElement实现了Element接口,将Key对象作为构造方法必要的参数。

public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

    AbstractCoroutineContextKey用于实现Element的多态。什么是Element的多态呢?假设类A实现了Element接口,Key为A。类B继承自类A,Key为B。这时将类B的对象添加到上下文中,通过指定不同的Key(A或B),可以得到不同类型对象。具体代码如下:

// baseKey为衍生类的基类的Key
// safeCast用于对基类进行转换
// B为基类,E为衍生类
public abstract class AbstractCoroutineContextKey<B : Element, E : B>(
baseKey: Key<B>,
private val safeCast: (element: Element) -> E?
) : Key<E> {
// 顶置Key,如果baseKey是AbstractCoroutineContextKey,则获取baseKey的顶置Key
private val topmostKey: Key<*> = if (baseKey is AbstractCoroutineContextKey<*, *>) baseKey.topmostKey else baseKey

// 用于类型转换
internal fun tryCast(element: Element): E? = safeCast(element)
// 用于判断当前key是否是指定key的子key
// 逻辑为与当前key相同,或者与当前key的顶置key相同
internal fun isSubKey(key: Key<*>): Boolean = key === this || topmostKey === key
}

1.getPolymorphicElement方法与minusPolymorphicKey方法


    如果衍生类使用了AbstractCoroutineContextKey,那么基类在实现Element接口中的get方法时,就需要通过getPolymorphicElement方法,实现minusKey方法时,就需要通过minusPolymorphicKey方法,代码如下:

public fun <E : Element> Element.getPolymorphicElement(key: Key<E>): E? {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,则基类强制转换成衍生类,并返回
@Suppress("UNCHECKED_CAST")
return if (key.isSubKey(this.key)) key.tryCast(this) as? E else null
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,则强制转换,并返回
@Suppress("UNCHECKED_CAST")
return if (this.key === key) this as E else null
}
public fun Element.minusPolymorphicKey(key: Key<*>): CoroutineContext {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,基类强制转换后不为空,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (key.isSubKey(this.key) && key.tryCast(this) != null) EmptyCoroutineContext else this
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (this.key === key) EmptyCoroutineContext else this
}

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

Compose 中嵌套原生 View 原理

Compose 是用于构建原生 Android UI 的现代工具包,他只需要在 xml 布局中添加 ComposeView,或是通过 setContent 扩展函数,即可将 Compose 组件绘制界面中。 Compose 天然就支持被原生 View 嵌套,但...
继续阅读 »

Compose 是用于构建原生 Android UI 的现代工具包,他只需要在 xml 布局中添加 ComposeView,或是通过 setContent 扩展函数,即可将 Compose 组件绘制界面中。


Compose 天然就支持被原生 View 嵌套,但也支持嵌套原生 View,Compose 是通过自己的一套重组算法来构建界面,测量和布局已经脱离了原生 View 体系。既然脱离了这套体系,那 Compose 是如何完美支持嵌套原生 View 的呢?脱离了原生 View 布局体系的 Compose,是如何对原生 View 进行测量和布局的呢?


带着疑问我们从示例 demo 开始,然后再翻阅源码.


一、示例


Compose 通过 AndroidView 组件来嵌套原生 View,示例如下:

TimeAssistantTheme {
Surface {
Column {
// Text 为 Compose 组件
Text(text = "hello world")
// AndroidView 为 Compose 组件
AndroidView(factory = {context->
// 原生 ImageView
ImageView(context).apply {
setImageResource(R.mipmap.ic_launcher)
}
})
}
}
}

Preview 效果


Compose 完美展现原生 View 效果,接下来,我们需要对 AndroidView 一探究竟。


二、源码分析


1、分析 AndroidView


AndroidView 通过 factory 闭包来拿到我们的 ImageView,我们在探索 AndroidView 源码的时候,只需要观察这个 factory 究竟被谁使用了:

@Composable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
) {
...
ComposeNode<LayoutNode, UiApplier>(
factory = {
//1、创建 ViewFactoryHolder
...
val viewFactoryHolder = ViewFactoryHolder<T>(context, parentReference)
// 2、factory 被赋值给了 ViewFactoryHolder
viewFactoryHolder.factory = factory
...
// 3、从 ViewFactoryHolder 拿到 LayoutNode
viewFactoryHolder.layoutNode
},
...
)


  1. 创建了个 ViewFactoryHolder

  2. 将包裹原生 View 的 factory 函数赋值给 ViewFactoryHolder

  3. 从 ViewFactoryHolder 中拿到 LayoutNode 给 ComposeNode,后面会讲解该操作


大家可能对 ComposeNode 有点陌生,如果你阅读过 Compose 中组件源码的话,例如 Text,在你一直跟踪下去的时候会发现,他们都有一个共同点,那就是都会走到 ComposeNode,并且,ComposeNode 函数中会拿到 factory 的返回值 LayoutNode 来创建一个 Node 节点来参与 Compose 的绘制。也即Compose 在排版和布局的时候,操控的就是 LayoutNode,并且这个 LayoutNode 能拿到 Compose 执行中的一些回调,例如 measure 和 layout 来改变自身的位置和状态。


小结:在 AndroidView 这个函数中我们发现,原生 View 是通过外部包裹一层 Compose 组件参与到 Compose 布局中的


2、分析 ViewFactoryHolder


我们来看下,原生 View 的 factory 函数,在赋值给 ViewFactoryHolder 做了些什么:

@OptIn(ExperimentalComposeUiApi::class)
internal class ViewFactoryHolder<T : View>(
context: Context,
parentContext: CompositionContext? = null
) : AndroidViewHolder(context, parentContext), ViewRootForInspector {
internal var typedView: T? = null
override val viewRoot: View? get() = parent as? View

var factory: ((Context) -> T)? = null
...
set(value) {
// 1、将 factory 复制给幕后字段
field = value
// 2、factory 不为空
->if (value != null) {
// 3、invoke factory 函数,拿到原生 View 本身
typedView = value(context)
// 4、将原生 View 复制给 view
view = typedView
}
}
...
}

在赋值发生时,会触发 ViewFactoryHolder 中 factory 的 set(value),value 就是嵌套原生 view 的 factory 函数



  1. 将 factory 函数赋值给幕后字段,也即 ViewFactoryHolder.factory = factory

  2. 判断 factory 是否为空,我们提供了原生 ImageView 组件,这里为 true

  3. 执行 factory 函数,也即拿到我们的 ImageView 组件,赋值给全局变量的 typedView

  4. 并且也赋值给了 view


我们需要找到原生 ImageView 被谁持有,目前来看的话,typedView 被复制到了全局,没有被其他变量持有,被复赋值的 view 并不在 ViewFactoryHolder 中,那么,我们需要去 ViewFactoryHolder 的父类 AndroidViewHolder 看看了


3、分析 AndroidViewHolder


跟进 view 字段:

@OptIn(ExperimentalComposeUiApi::class)
\internal abstract class AndroidViewHolder(
context: Context,
parentContext: CompositionContext?
// 1、AndroidViewHolder 是一个继承自 ViewGroup 的原生组件
) : ViewGroup(context) {
...
/**
* The view hosted by this holder.
*/
-> var view: View? = null
internal set(value) {
if (value !== field) {
// 2、将 view 赋值给幕后字段
field = value
// 3、移除所有子 View
removeAllViews()
// 4、原生 view 不为空
-> if (value != null) {
// 5、将原生 view 添加到当前的 ViewGroup
addView(value)
// 6、触发更新
runUpdate()
}
}
}
...
}


  1. 需要注意的是,AndroidViewHolder 是一个继承自 ViewGroup 的原生组件

  2. 将原生 view 赋值给幕后字段,也即 view 的实体是 ImageView

  3. 移除所有的子 View,看来,AndroidViewHolder 只支持添加一个原生 View

  4. 判断原生 view 是否为空,我们提供了 ImageView ,所以该判断为 true

  5. 将原生 view 添加到当前的 ViewGroup,也即我们的 ImageView 被添加到了 AndroidViewHolder 中

  6. runUpdate 会触发 Compose 的一系列更新,我们先暂时不管他


小结:我们提供的原生 View,最终会被 addView 到 ViewFactoryHolder 中,只是 addView 这个操作是发生在他的父类 AndroidViewHolder 中的,然后将原生 ImageView 赋值到全局变量 view 中


现在,我们还有一些疑问,原生 view 虽然被 addView 到 ViewFactoryHolder 中了,那 ViewFactoryHolder 这个 ViewGroup 是如何被添加到界面上的呢?ViewFactoryHolder 是如何测量和布局的呢?我们需要回到 AndroidView 的函数中,找到 AndroidView 中的 viewFactoryHolder.layoutNode 进行源码跟进


4、分析 ViewFactoryHolder.layoutNode


layoutNode 字段也在 ViewFactoryHolder 的父类 AndroidViewHolder 中:

val layoutNode: LayoutNode = run {
// 1、一句注释直接讲透
// Prepare layout node that proxies measure and layout passes to the View.
-> val layoutNode = LayoutNode()

...
// 2、注册 attach 回调
layoutNode.onAttach = { owner ->
// 2.1 重点: 将当前 ViewGroup 添加到 AndroidComposeView 中
(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
if (viewRemovedOnDetach != null) view = viewRemovedOnDetach
}
// 3、注册 detach 回调
layoutNode.onDetach = { owner ->
// 3.1 重点: 将当前 ViewGroup 从 AndroidComposeView 中移除
(owner as? AndroidComposeView)?.removeAndroidView(this)
viewRemovedOnDetach = view
view = null
}
// 4、注册 measurePolicy 绘制策略回调
layoutNode.measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
...
// 4.1、layoutNode 的测量,触发 AndroidViewHolder 的测量
measure(
obtainMeasureSpec(constraints.minWidth, constraints.maxWidth,layoutParams!!.width),
obtainMeasureSpec(constraints.minHeight,constraints.maxHeight,layoutParams!!.height)
)
// 4.1、layoutNode 的布局,触发 AndroidViewHolder 的布局
-> return layout(measuredWidth, measuredHeight) {
layoutAccordingTo(layoutNode)
}
}
...
}
// 5、返回 layoutNode
layoutNode
}

这段代码有点多,但却是最精华的核心部分:



  1. 注释直接道破,这个 LayoutNode 会代理原生 View 的 measure、layout,将测量和布局结果反应到 AndroidViewHolder 这个 ViewGroup 中

  2. 注册 LayoutNode 的 attach 回调,这个 attach 可以理解成 LayoutNode 被贴到了 Compose 布局中触发的回调,和原生 View 被添加到布局中,触发 onViewAttachedToWindow 类似

    1. 将当前 AndroidViewHolder 添加到 AndroidComposeView 中



  3. 注册 LayoutNode 的 detach 回调,这个 detach 可以理解成 LayoutNode 从 Compose 布局中被移除触发的回调,和原生 View 从布局中移除,触发 onViewDetachedFromWindow 类似

    1. 将当前 ViewGroup 从 AndroidComposeView 中移除



  4. 注册 LayoutNode 的绘制策略回调,在 LayoutNode 被贴到 Compose 中,Compose 在重组控件的时候,会触发 LayoutNode 的绘制策略

    1. 触发 ViewGroup 的 measure 测量

    2. 触发 ViewGroup 的 layout 布局



  5. 返回 LayoutNode


在 2.1 的 attach 步骤中发现,我们的 ImageView 经过 AndroidViewHolder 的包裹,被 addAndroidView 到了 AndroidComposeView 中,这里我们又有个疑问,owner 转换成的 AndroidComposeView 是从哪来的?addAndroidView 做了哪些事情?


这里先小结下:
AndroidViewHolder 中的 layoutNode 是一个不可见的 Compose 代理节点,他将 Compose 中触发的回调结果应用到 ViewGroup 中,以此来控制 ViewGroup 的绘制与布局


5、分析 AndroidComposeView.addAndroidView

internal class AndroidComposeView(context: Context) :
ViewGroup(context), Owner, ViewRootForTest, PositionCalculator, DefaultLifecycleObserver {
...
internal .val androidViewsHandler: AndroidViewsHandler
get() {
if (_androidViewsHandler == null) {
_androidViewsHandler = AndroidViewsHandler(context)
// 1、将 AndroidViewsHandler addView 到 AndroidComposeView 中
addView(_androidViewsHandler)
}
return _androidViewsHandler!!
}

// Called to inform the owner that a new Android View was attached to the hierarchy.
-> fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
androidViewsHandler.holderToLayoutNode[view] = layoutNode
// 2、AndroidViewHolder 被添加到 AndroidViewsHandler 中
androidViewsHandler.addView(view)
androidViewsHandler.layoutNodeToHolder[layoutNode] = view
...
}
}


  1. 将 AndroidViewsHandler 添加到 AndroidComposeView 中

  2. 将 AndroidViewHolder 添加到 AndroidViewsHandler 中


现在 addView 的逻辑已经走到了 AndroidComposeView,我们现在还需要知晓 AndroidComposeView 从何而来


这次,我们需要先从 ComposeView 开始分析:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidWidget()
}
}

在 Activity 的 onCreate 方法中,我们通过 setContent 将 ComposeView 应用到界面上,我们需要跟踪这个 setContent 拓展函数一探究竟:

public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
...

if (existingComposeView != null) with(existingComposeView) {
...
// 1、设置 compose 布局
setContent(content)
} else ComposeView(this).apply {
...
// 1、设置 compose 布局
setContent(content)
...
// 2、调用 Activity 的 setContentView 方法,布局为 ComposeView
setContentView(this, DefaultActivityContentLayoutParams)
}
}


  1. 调用 ComposeView 内部的 setContent 方法,将 compose 布局设置进去

  2. 调用 Activity 的 setContentView 方法,布局为 ComposeView,这也是 Activity 中没有找到设置 setContentView 的原因,因为拓展函数已经做了这个操作


我们需要跟踪下 ComposeView 的 setContent 方法:

-> fun setContent(content: @Composable () -> Unit)
-> fun createComposition()
-> fun ensureCompositionCreated()

-> internal fun ViewGroup.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
// 1、获取 ComposeView 的子 View 是否为 AndroidComposeView
-> if (childCount > 0) {
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
// 2、如果为空,则创建个 AndroidComposeView,并调用 addView 将 AndroidComposeView 添加进 ComposeView
} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
return doSetContent(composeView, parent, content)
}

-> private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
...
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
// 3、将 AndroidComposeView 设置到 WrappedComposition 中,并返回 Composition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
return wrapped
}



  1. 获取 ComposeView 的子 View 是否为 AndroidComposeView

  2. 如果获取为空,则创建个 AndroidComposeView,并调用 addView 将 AndroidComposeView 添加进 ComposeView

  3. 将 AndroidComposeView 设置到 WrappedComposition 中,并返回 Composition,这也就是为什么在 LayoutNode 中,能拿到 owner ,并且为 AndroidComposeView 的原因


三、总结


至此,我们分析完了原生 View 是如何添加进 Compose 中的,我们可以画个图来简单总结下:




  • 橙色:在 Compose 中嵌套 AndroidView 才会有,如果没有使用,则没有橙色层级

  • 黄色: 嵌套的原生 View,此处演示的为示例的 ImageView

  • 绿色:Compose 的控件,也即 LayoutNode


然后我们遍历打印一下 view 树,以此来确认我们的跟踪的是否正确

System.out: viewGroup --> android.widget.FrameLayout{47cc49 V.E...... ........ 0,95-1080,2400 #1020002 android:id/content}
System.out: viewGroup --> androidx.compose.ui.platform.ComposeView{134250 V.E...... ........ 0,0-232,257}
System.out: viewGroup --> androidx.compose.ui.platform.AndroidComposeView{8e162e1 VFED..... ........ 0,0-232,257}
System.out: viewGroup --> androidx.compose.ui.platform.AndroidViewsHandler{fbb7614 V.E...... ......ID 0,0-232,257}
System.out: viewGroup --> androidx.compose.ui.viewinterop.ViewFactoryHolder{4b0e4aa V.E...... ......I. 0,59-198,257}
System.out: view --> android.widget.ImageView{8438ebd V.ED..... ........ 0,0-198,198}

现在,我们可以来回答开头说的问题了:



  • Compose 是通过 addView 的方式,将原生 View 添加到 AndroidComposeView 中的,他依然使用的是原生布局体系

  • 嵌套原生 View 的测量与布局,是通过创建个代理 LayoutNode ,然后添加到 Compose 中参与组合,并将每次重组返回的测量信息设置到原生 View 上,以此来改变原生 View 的位置与大小

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

每个前端都应该掌握的7个代码优化的小技巧

web
本文将介绍7种JavaScript的优化技巧,这些技巧可以帮助你更好的写出简洁优雅的代码。 1. 字符串的自动匹配(Array.includes) 在写代码时我们经常会遇到这样的需求,我们需要检查某个字符串是否是符合我们的规定的字符串之一。最常见的方法就是使用...
继续阅读 »

本文将介绍7种JavaScript的优化技巧,这些技巧可以帮助你更好的写出简洁优雅的代码。


1. 字符串的自动匹配(Array.includes


在写代码时我们经常会遇到这样的需求,我们需要检查某个字符串是否是符合我们的规定的字符串之一。最常见的方法就是使用||===去进行判断匹配。但是如果大量的使用这种判断方式,定然会使得我们的代码变得十分臃肿,写起来也是十分累。其实我们可以使用Array.includes来帮我们自动去匹配。


代码示例:


// 未优化前的写法
const isConform = (letter) => {
if (
letter === "a" ||
letter === "b" ||
letter === "c" ||
letter === "d" ||
letter === "e"
) {
return true;
}
return false;
};

// 优化后的写法
const isConform = (letter) =>
["a", "b", "c", "d", "e"].includes(letter);

2.for-offor-in自动遍历


for-offor-in,可以帮助我们自动遍历Arrayobject中的每一个元素,不需要我们手动跟更改索引来遍历元素。


注:我们更加推荐对象(object)使用for-in遍历,而数组(Array)使用for-of遍历


for-of


const arr = ['a',' b', 'c'];
// 未优化前的写法
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
console.log(element);
}

// 优化后的写法
for (const element of arr) {
console.log(element);
}
// expected output: "a"
// expected output: "b"
// expected output: "c"

for-in


const obj = {
a: 1,
b: 2,
c: 3,
};
// 未优化前的写法
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = obj[key];
// ...
}

// 优化后的写法
for (const key in obj) {
const value = obj[key];
// ...
}

3.false判断


如果你想要判断一个变量是否为null、undefined、0、false、NaN、'',你就可以使用逻辑非(!)取反,来帮助我们来判断,而不用每一个值都用===来判断


// 未优化前的写法
const isFalsey = (value) => {
if (
value === null ||
value === undefined ||
value === 0 ||
value === false ||
value === NaN ||
value === ""
) {
return true;
}
return false;
};

// 优化后的写法
const isFalsey = (value) => !value;

4.三元运算符代替(if/else


在我们编写代码的时候肯定遇见过if/else选择结构,而三元运算符可以算是if/else的一种语法糖,能够更加简洁的表示if/else


// 未优化前的写法
let info;
if (value < minValue) {
info = "Value is最小值";
} else if (value > maxValue) {
info = "Value is最大值";
} else {
info = "Value 在最大与最小之间";
}

//优化后的写法
const info =
value < minValue
? "Value is最小值"
: value > maxValue ? "Value is最大值" : "在最大与最小之间";

5.函数调用的选择


三元运算符还可以帮我们判断当前情况下该应该调用哪一个函数,


function f1() {
// ...
}
function f2() {
// ...
}
// 未优化前的写法
if (condition) {
f1();
} else {
f2();
}

// 优化后的写法
(condition ? f1 : f2)();

6.用对象代替switch/case选择结构


switch case通常是有一个case值对应一个返回值,这样的结构就类似于我们的对象,也是一个键对应一个值。我们就可以用我们的对象代替我们的switch/case选择结构,使代码更加简洁


const dayNumber = new Date().getDay();

// 未优化前的写法
let day;
switch (dayNumber) {
case 0:
day = "Sunday";
break;
case 1:
day = "Monday";
break;
case 2:
day = "Tuesday";
break;
case 3:
day = "Wednesday";
break;
case 4:
day = "Thursday";
break;
case 5:
day = "Friday";
break;
case 6:
day = "Saturday";
}

// 优化后的写法
const days = {
0: "Sunday",
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday",
};
const day = days[dayNumber];

7. 逻辑或(||)的运用


如果我们要获取一个不确定是否存在的值时,我们经常会运用if判断先去判断值是否存在,再进行获取。如果不存在我们就会返回另一个值。我们可以运用逻辑或(||)的特性,去优化我们的代码


// 未优化前的写法
let name;
if (user?.name) {
name = user.name;
} else {
name = "Anonymous";
}

// 优化后的写法
const name = user?.name || "Anonymous";

写在最后


伙伴们,如果你觉得我写的文章对你有帮助就给zayyo点一个赞👍或者关注➕都是对我最大的支持。当然你也可以加我微信:IsZhangjianhao,邀你进我的前端学习交流群,一起学习前端,

作者:zayyo
来源:juejin.cn/post/7169420903888584711
成为更优秀的工程师~

收起阅读 »

python干饭神器---今天吃什么?python告诉你

一、前言 hello,大家好,我是干饭大王,打工人每天最幸福的时刻莫过于干饭了 😎,然而最纠结的时刻莫过于今天吃什么呢? 😂,于是乎我用python写了一个干饭神器,今天分享给大家,别忘了给我点赞 收藏 评论哟~ 话不多说,先看效果图: 还有隐藏 “福利”...
继续阅读 »

一、前言


hello,大家好,我是干饭大王,打工人每天最幸福的时刻莫过于干饭了 😎,然而最纠结的时刻莫过于今天吃什么呢? 😂,于是乎我用python写了一个干饭神器,今天分享给大家,别忘了给我点赞 收藏 评论哟~


话不多说,先看效果图:
在这里插入图片描述
在这里插入图片描述


还有隐藏 “福利” 帮你的完成减肥大业:


在这里插入图片描述


在本篇博客中,我们将会介绍如何使用Python编写一个带有界面的可切换的轮播图程序。轮播图程序可以从指定的文件夹中读取图片,并可以自动切换到下一张图片。如果图片比较大,则可以进行缩放以适应窗口大小。


二、准备工作


在开始编写程序之前,我们需要安装以下依赖项:



  • tkinter:用于创建GUI界面。

  • Pillow:用于处理图片。


tkinter是python一个内置集成库,Pillow可以使用pip命令来安装:


pip install tkinter Pillow

当安装完成后,我们就可以开始编写程序了。


三、编写代码


说了折磨多,你们是不是非常期待我的代码呀,看代码之前,别忘了先点个关注哟~,接下来,进入代码编写环节


2.1 导入所需的库


首先,我们需要导入所需的库:


import os
import tkinter as tk
from PIL import ImageTk, Image

其中,os库用于读取指定目录下的文件列表;tkinter库用于创建GUI界面;Pillow库用于处理图片。


2.2 创建GUI界面


初始化窗口,并设置窗口的标题,以及窗口的初始大小。


mywindow=tk.Tk()
mywindow.title('今天吃什么')
mywindow.minsize(500,500)

2.3 设置窗口居中显示


我们首先 winfo_screenwidthwinfo_screenheight 函数获取屏幕的尺寸(screenwidth和screenheight),并根据屏幕尺寸计算了窗口的大小和位置。最后使用 geometry 函数 将应用程序窗口设置到屏幕中央。


def getWindowsSize(width, height, screen_width, screen_height):
x = int(screen_width / 2 - width / 2)
y = int(screen_height / 2 - height / 2)
return '{}x{}+{}+{}'.format(width, height, x, y)


screenwidth = mywindow.winfo_screenwidth()
# 得到屏幕宽度
screenheight = mywindow.winfo_screenheight()

print("screenheight:%s,screenheight:%S ",screenheight,screenwidth)

# x tkinter窗口距离屏幕左边距离
mywindow_x = 650
# y tkinter窗口距离屏幕上边距离
mywindow_y = 575
mywindow.geometry(getWindowsSize(mywindow_x,mywindow_y,screenwidth,screenheight))

2.4 设置切换按钮和当前食物的label 以及图片展示 切换方法


photoDefault 用来读取默认首图的图片,imageNameList用来存放 imgs文件下的所有文件路径,nextChoice() 用于当你不满意第一次随机的食物,点击 不满意?换一个 按钮时切换下一个食物。mylab 用来展示图片,eatlab 用来展示食物的中文名,mybut 用于切换。


photoDefault = ImageTk.PhotoImage(Image.open("imgs/今天吃点啥.gif"))

path = 'imgs/'
imagNamelist = os.listdir(path)

def nextChoice():
# 重新排序文件
random.shuffle(imagNamelist)
imagePath=imagNamelist[0]
print('今天吃',imagePath)
finalImg = Image.open("imgs/"+imagePath)
photo = ImageTk.PhotoImage(finalImg.resize((400, 400)))
mylab.config(image = photo)
mylab.image = photo
mybut.config(text='不满意?换一个')
eatlab.config(text=imagePath.split(".")[0])




mylab=tk.Label(master=mywindow,
image= photoDefault
)
mylab.pack()

eatlab=tk.Label(master=mywindow,
text='今天吃点啥?',
fg='white',
bg='green',
font=('微软雅黑',14),
width=20,
height=3
)
eatlab.pack()

mybut=tk.Button(mywindow,
text='点我决定',
font=('微软雅黑',12),
width=15,
height=3,
command=nextChoice
)
mybut.pack()

mywindow.mainloop()

2.5 完整代码


import os
import tkinter as tk
import random
from PIL import Image, ImageTk

mywindow=tk.Tk()
mywindow.title('今天吃什么')
mywindow.minsize(500,500)

def getWindowsSize(width, height, screen_width, screen_height):
x = int(screen_width / 2 - width / 2)
y = int(screen_height / 2 - height / 2)
return '{}x{}+{}+{}'.format(width, height, x, y)


screenwidth = mywindow.winfo_screenwidth()
# 得到屏幕宽度
screenheight = mywindow.winfo_screenheight()

print("screenheight:%s,screenheight:%S ",screenheight,screenwidth)

# x tkinter窗口距离屏幕左边距离
# mywindow_x = mywindow.winfo_x()
# # y tkinter窗口距离屏幕上边距离
# mywindow_y = mywindow.winfo_y()

# x tkinter窗口距离屏幕左边距离
mywindow_x = 650
# y tkinter窗口距离屏幕上边距离
mywindow_y = 575
mywindow.geometry(getWindowsSize(mywindow_x,mywindow_y,screenwidth,screenheight))


photoDefault = ImageTk.PhotoImage(Image.open("imgs/今天吃点啥.gif"))

path = 'imgs/'
imagNamelist = os.listdir(path)

def nextChoice():
# 重新排序文件
random.shuffle(imagNamelist)
imagePath=imagNamelist[0]
print('今天吃',imagePath)
finalImg = Image.open("imgs/"+imagePath)
photo = ImageTk.PhotoImage(finalImg.resize((400, 400)))
mylab.config(image = photo)
mylab.image = photo
mybut.config(text='不满意?换一个')
eatlab.config(text=imagePath.split(".")[0])




mylab=tk.Label(master=mywindow,
image= photoDefault
)
mylab.pack()

eatlab=tk.Label(master=mywindow,
text='今天吃点啥?',
fg='white',
bg='green',
font=('微软雅黑',14),
width=20,
height=3
)
eatlab.pack()

mybut=tk.Button(mywindow,
text='点我决定',
font=('微软雅黑',12),
width=15,
height=3,
command=nextChoice
)
mybut.pack()

mywindow.mainloop()

四、打包exe运行程序,方便在其他windows电脑上使用


4.1 安装pyinstaller打包程序


要想打包成windows下可以运行的 exe 可以使用pip安装如下依赖包,


pip3 install pyinstaller

4.2 执行打包


进入我们的源代码目录,比如我的就是:D:\project\opensource\t-open\python-labs\src\eat , 然后执行如下命令即可, eat2.py 是我们写的python源代码,eatSomething.exe 是我们最后打完包的程序名称。


pyinstaller -F -w eat2.py -n eatSomething.exe

在这里插入图片描述


4.3 移动img文件,运行程序


打包完成后 会在 dist目录生成 eatSomething.exe 应用程序,由于打包的时候我们并没有把 imgs 文件夹的图片拷贝进去,所以我们可以手动把 imgs 全部拷贝到 dist 文件夹下,当然你也可以 把你喜欢的食物照片放到 imgs 文件夹下面。
在这里插入图片描述
在这里插入图片描述
然后我们就可以双击 eatSomething.exe
在这里插入图片描述


祝大家不在纠结今天吃什么,做一个果断 坚毅 的人✌️✌️✌️
如果你喜欢的话,就不要吝惜你的一键三连了~ 🤝🤝🤝
谢谢大家!


这里是文章的表白神器所有代码+图片,对文章不是太懂得小伙伴们可以自取一下哟:今天吃点啥的源代码地址:



  1. 码云地址gitee.com/T-OPEN/pyth…

  2. 源代码和程序下载地址download.csdn.net/download/we…


作者:TOPEN
来源:juejin.cn/post/7231526222634582071
收起阅读 »

211 冷门文科毕业生的前端之路

白驹过隙。 毕业之景还历历在目,一晃如今却已经是两年过去。犹记得毕业时的焦虑及惆怅,在前端知识的苦海中迷茫摸索,而如今却已对前端知识得心应手,信手拈来,多了些从容和自信。从一个搞不清 JSON.parse(JSON.stringify()) 的毛头小子,到如今...
继续阅读 »




白驹过隙。


毕业之景还历历在目,一晃如今却已经是两年过去。犹记得毕业时的焦虑及惆怅,在前端知识的苦海中迷茫摸索,而如今却已对前端知识得心应手,信手拈来,多了些从容和自信。从一个搞不清 JSON.parse(JSON.stringify()) 的毛头小子,到如今覆盖主流前端框架、工程化、大前端等等领域的中阶前端打工仔,短短一年多的时间,我应该是完成了一个比较大的飞跃。虽然还是留在浅浅的应用层,原理层并未深入,但是对于这种知识面的覆盖率我已经相当满意。这已经足够支撑我独自完成开源项目,足够支撑我应对工作中的各类问题,甚至还富有余地,效率提升了非常多,也拥有更多的时间去做自己喜欢做的事。


但从来没有谁的路是一帆风顺,平坦宽阔的,回想两年前的这个时候,我还在迷茫和未知中挣扎。


毕业季:在迷茫中疯狂碰壁


21年寒假结束,我便马不停蹄地赶往学校,因为毕业临近,而我却仍然没找到工作。


在春节前,和大多数人一样,我也是焦虑的。身边的朋友陆续签约实习,薪资都还不错,而我却屡次面试受挫,一路陪跑。还没有搞清楚问题出在哪里的我,不挑行业,不挑企业,只看薪资是否在 5000 元以上,也只看岗位名称是否“高大上”,比如管培生之类的。毕竟是 211 毕业,农村出身的我,只有薪资过 5000 一个要求,算不得过分。于是病急乱投医,例如什么养猪、养牛的都瞎投,结果人家还看不上我。毕竟我是文科的,笔杆子水平一般,简历上乏善可陈,成绩倒数,也没有任何奖项。这样空乏的履历如同我的钱包,缺乏竞争力使得我在诸多面试中败下阵来。


这些面试里,我印象最深的是成都知名连锁超市沃某玛。接到面试邀请后,要求着正装,我特地去租了西装皮鞋,赶了 6 小时高铁去到成都。与我一起参加面试的还有很多从北京黑龙江过来的同学。面试采取无领导开放式面试,几位面试官要求 5 人一队做游戏,两个团队一起竞争,并要求大家在游戏过程中讨论出制胜方案。我当然知道这场面试要考察的点:一是注重目标达成,二是积极参与讨论。我一开始也确实是这么做的,但是活动进行到中间,我发现这临时组建的团队,几个陌生人,几个性格各异性别不一、学校不同的人,出现了混乱。也许是出于竞争关系,其他人不在乎目标,不在乎队友,不在乎任何东西,大家不尊重别人的讲话,像一群鸭子“呱呱”乱叫。想到如此混乱敷衍的一场面试就决定了我此次远行的结果,我忽然就觉得荒唐可笑。加之早晨因为不熟悉地方并没有吃早餐,饿瘪瘪的肚子一直在抗议,我就没有再积极地参与到他们毫无结果的自说自话中了。虽然我已经找到制胜之策,但是直到游戏结束,也没人听取我的意见,但是两个队伍神奇般的都没有达成目标。毫无疑问,这场面试我并没有进入下一轮,最后也是灰溜溜地返回了学校。


这次面试,让我更加坚定了缘分这种说法,那些不合时宜出现的人或者事,那些没有选择你的,或者你没有选择的,一定是因为你们缘分不够,而不在于它不够好或者你不够优秀。


机缘巧合:接触编程


就这样盲目地找了半年,无果。在此以后,我决定利用春节假期做些什么。


学院里曾经组织过一次课外学习,是联合腾讯课堂学习微信小程序开发的课外实践课程,由学校就业中心牵头做的。学院当时的书记一下子就争取到了很多的课程兑换码,我也是第一时间就报名学习,最后顺利结业。虽然没有学到真正的技术,但因为有了这层基础,我决定春节假期好好地做一个微信小程序,学以致用,不管以后如何,全当一个兴趣爱好,总比浪费时间有意义。


寒假的每一天我都在敲代码,尽管我只会最简单的,但还是憋出了一个简单的阅读小程序。打个不恰当的比喻,如果两年后的我现在拥有高中大学的编程水平,那时候的我不过是刚学会了加减乘除。


回到开头,寒假一结束我就赶往学校。因为假期做出了小程序,我觉得我可以往程序员的方向试试。我之前在各种行业之间反复横跳,尽管我对互联网很感兴趣,但是之前一直了解、学习的是运营和广告之类的知识,也是一窍不通,加上屡次碰壁,简历上乏善可陈的优势,我意识到必须找准行业,必须把精力集中在一个点上,才能解决问题。于是结合我自己喜欢互联网,懂一点点编程这两点,我觉得往程序员发展也未尝不可一试。


开启艰辛的自学之路


开始针对性的学习。


在一个文科学院,临近毕业的时间点,开始自学编程。没有人教,没有人指导,只有我自己。虽然从小到大就是如此,除了父亲给予的部分经济支持,我一直一个人,也是习惯了。我依稀记得大创课上的老师是学校里的信息老师,于是我找他问了一个非常简单的问题。问题就是,我在写 CSS 时,把 : 误写成了 =


.title {
color = '#fff';
}

他给我说明了问题所在,给我分享了菜鸟教程的官网,让我跟着官网学习。就这么和他简单的两句对话,便是我唯一一次得到的指导,菜鸟也确实在前期的学习中给我给予了很大的帮助。而我也知道老师的意思,他是让我自己去看,毕竟学习编程这种事情,隔着屏幕很难知道你的问题,也很难做出帮助。编程的学习之路上,恐怕会遇到几千个问题,一个陌生老师怎么能回答得过来。即使是培训班的老师也未必能手把手教你,出于礼貌和自知之明,毅然决定依靠自己。


当然了,有人带没人带是天壤之别,有妈的孩子和没妈的孩子能一样吗?


编程需要编辑器,编辑器也有很多,我连用什么编辑器来学习写代码最合适都不知道,只是在学习小程序时使用的是 vscode,也只知道 vscode。那时连插件的使用都不会,下了一堆下来会出现冲突,不下的话有些功能又用不了,于是捣鼓编辑器就用了很多天。最后虽然没有彻底弄明白,但是勉强还是可以写代码了。


其实对于我来说,最痛苦的事是在前期自学找视频教程时,课程中会提到 github、less、sass 诸如此类的技术点,有时极为困惑,都有 css 了,为什么还有什么 less、sass?而这些东西都需要我自己一点一点去探索,没有人解答,哪怕只是简单的一句解释。而去询问别人呢,并不会得到什么礼貌的回应,周围的朋友都是文科生,想要寻求一点帮助确实太难。结识到一些前端朋友,也只是互相鼓励,对于很多知识点,别人也不是很懂。当然了,现在说这些似乎很矫情,似乎每个前端都是这样过来的。


面对学不完的知识点,有种一眼望不到头的感觉,更不清楚企业需要什么样的人,学到哪种程度可以进企业开发。这种感觉好像整个人漂浮在半空中,又像是溺在水里,找不到着力点,令人窒息。


对于一个初学者来说,培养编程思维、编程能力实在是太困难。毕竟不是每个人都天赋异禀,对于电脑,农村出身的我也是到了中学才开始接触,为了避免被网络游戏毒害,我在中学时期强忍对电脑的好奇,导致接触的机会更加少了。


网络教程中,很多老师会说,看文档就好了,跟着文档就可以学习了,不用费劲。如今来看这话不假,但是不适合 1 年经验以内的新人。一个人的知识储备不够,看文档只会是一件更加痛苦的事。还有源码的学习,如今的招聘市场可谓害人不浅,一个应届生、初学者居然需要知道源码和源码级别的原理,实在不可理解,工作经验 3 年的人看框架源码尚且困难,何况是应届生呢?


扯远了。


就这样,毕竟是个 211 学生,不至于蠢到不会自学。在网上搜罗学习路线,学习视频,一点一点看,一点一点敲,我终于是摸到了编程的一点点门槛。但这时的我仍然菜得不行,什么闭包、原型链、vue 在我眼里都还是模糊的概念。但是毕业临近,顾不得那么多,工作才是最要紧的事情,于是开始面试,投递简历。但老实说,这时候我还没有真正地建立编程素养,妥妥的一个门外汉,连初学者都还差点意思,没有每日编码的习惯,没有每日阅读博客的精神,还处在前期的痛苦之中,如果有个人带的话,我想结果会好很多。


但是时间不等人,由不得我了。


山重水复疑无路


时间来到四月,距离毕业之期还有两个月。我在百般失利的情况下,抱着试一试的态度向云南某机(云南最大的手机销售公司)投递了简历。幸运的是面试通过,还进入了他们的人才培养计划,本以为一切就这样顺利发展下去,可我的编程基础,却为我离开某机埋下了地雷。


我好像会,却又好像不会,我好像在努力处理好每一件事,但是我并没有做好。我最大的错误就是入职某机太早,没有趁此机会加强学习。那时候我还没有找到编程学习的窍门,没有养成每日编码的习惯,看视频缓慢的进度让我看不到希望,对什么知识点都是一知半解,有问题找不到人解答使我心生愤懑,一腔不快找不到发泄的地方,最后演变成必败的结果。


在这里给大家讲讲过程,也不怕大家笑话。


我是人才计划里第一个入职的人,带我的是整个前端的技术大佬(没有管理权,但是技术最厉害的),是腾讯离职回来养老的,应该是阿里 P8 级别。也许大佬就是大佬,眼睛里容不得一点沙子。他先教我 git,给了个网站让我自己看,也不说具体要求和细节,就你自己看,看了半天找不着方向,下班前被他考察时一脸懵逼。被他问了以后才知道他的要求,又重新学一遍。


我是真的觉得这种不善言谈的技术大牛,多少都带点病,就是自己不能给别人讲解清楚一个知识点,却总是怪罪别人能力太差。我在几家公司里遇到过几个,运气不好遇上这种导师,恐怕在这个公司的职业生涯已经宣告结束一半。遇到这种导师,希望大家勇敢的说出来换一个,就说不好磨合,不然吃亏的就是自己。


当然了,也不全是别人的问题,自己菜就好好反思。


给时间学习,做 todoList,给时间做博客,甚至 axios 都会帮你封装好,接口使用开源接口。总之就是他们的培养计划是非常好的,也非常宽容的给学习时间。但是导师自从知道我是文科生,给我的工作安排以及指导完全模糊。而我在实现目标的过程中呢,耍了点小聪明,更让人觉得我菜得不行了,坚定了让我滚蛋的决心。


哈哈~


我自己在这个导师的带领下呢,由于前期没处理好和他的关系,导致有些害怕和焦虑,以至于中间犯了很多低级的错误。中间也被 HR 约谈了,这期间我还完全不知道导师对我的偏见,以及迫切地希望我这个门外汉、文科生赶紧滚蛋的想法,还和他们有说有笑。HR 两人的一唱一和加上阴阳怪气的嘴脸一直盘旋在我脑海里,我在这后面动摇过几次,以至于在最终考核中大脑一片空白,没能正常发挥,也下定了离开的决心。


在我入职三周后,进来了两个同学,他们两人因为我之前踩的坑都给他们排过雷,所以很多错误他们没有犯,讲道理,在知识的广度还有基础上,我是比他们更好的,这期间的开发中,我不止一次的帮助过他们,大家关系到现在也仍然不错,虽然最后留下来的人中没有我。


但是就像我在开篇中说的那样,并不是某机不够好,也不是我真的不优秀,而是我们之间之间没有缘分。


柳暗花明又一村


离开某机,我心态变得愈加焦虑,时常惊醒,时常颤抖。对于未来的模糊感,使我寝食难安。我开始放平心态来学习,来弥补之前的不足,开始刷题,背面经,开始有规律的投递简历,并且面试。离开某机的第一家面试,面试官问了我一些对应届生有点难,但是还不算过分的题目,但是我并不能回答得出来。我装作很自信的样子,以为能够吼得住面试官,但是面试官问了我一句:你好像很自信?我不以为然,为什么不自信?直到很久以后,我才明白这句话的意思是:小伙子,你很菜,要谦虚。


最近几周的面试都是类似的,使我丧失了一部分信心。对于应届生,小公司不愿意花时间试错培养,也不愿意开超过 3000 的工资,而超过 3000 的公司,要求都过高,例如拿过竞赛的国奖,可我这个半路出家的文科生哪里参加过什么竞赛。


昆明这样的小地方,想找个好点的科技公司,或者说愿意要应届生的公司,很难,而公司想要招一个优秀的人才,更难。但是遇到真正的人才,这些公司也未必会开出合适的价格,除了画饼还是画饼。


面试三周,没有收到一家 offer,我已经萌生了离开昆明的想法,准备回家观望,或许备考公务员,或许另谋出路。我准备降低简历投递频率,摆烂到毕业。之前投递的两家还没有面试,我也心生退意,但是大学好室友让我再试一试,没准就成了。也是这一试,改变了我后续的命运。


最后这家面试,我没有抱着任何希望,照常写个人信息的表格,照常准备面试。结果与面试官相谈甚欢,聊了一个多小时。走到出租屋楼下的时候,HR 给我打了入职邀请的电话。我只记得那个傍晚,天气晴朗,小区门口坐着几个爷爷奶奶在吹晚风。


入职。


我仍然没摆脱从某机离开的焦虑,我从没经历真正的企业开发,我害怕犯错,我害怕完成不了任务。


我的确菜得像个小朋友。


在新公司认识了几个好朋友,也是因为他们的帮助我才走到今天,很难想象,如果没有遇到他们,我的路还要曲折多少。新公司有着新人培训和一系列的考核,纯靠背诵记忆是很难通过的,尽管后面看来考试不通过也没有太大关系,但是当时刚开始严格起来,对于应届生来说确实是一个考核指标。幸运的是,公司的领导还算包容,当时开发是基于另一个人写的简易框架,问题很多,所以没有人会认为是我的能力存在问题(也确实不是我的问题)。接触公司项目后,我的知识点像爆炸般都串起来了,我开始有意识的去深入学习,去看技术博客,去研究如何写好代码。这时候我已经具备了非常不错的编程素质了,加上遇到一个很好的小组领导,我得以最大程度地成长。


千里马常有,而伯乐不常有。


这时我已经学着写一些 Vue2 组件库了,模仿 Element UI 写过 10 来个基础组件,组件开发的学习使我对 Vue 掌握更上一层楼,也奠定了后来开发 React 组件库的基础,当然,这些都是后话。在这个公司,我在所谓的翔山代码里自顾自地写着自己认为优秀的代码,注释、变量、函数都尽量做到最好,最主要的是在公司变态的开发进度下还坚持保持清晰的代码风格。前公司的进度可能是一周写好几个页面,页面有树、查询、表格、弹窗表单、Echarts 图表等,我在离职前一周写的代码超过 10000 行,我是有认真数过的。总之,我之前零散的知识,突然爆炸般串起来了,因为公司前端基建非常差,得以接触 base64request headerBlob 等较为困难的前端知识点及 Web API。同时,还接触了 momentswiperEcharts 等库。更重要的是见识到了 Axios 都不会封装的“资深前端”,见识到了“资深前端”是如何写出 3000 多行代码而不做拆分的。


短短半年,我得到了非常大的进步。


远走他乡


但是,和所有的小公司一样,这个公司,加班严重,朝令夕改,问题响应后迟迟得不到解决,还有一个最令人讨厌的 CEO。年前两个月使用各种手段逼迫员工加班、离职,暗自修改公司奖惩制度,强迫 996。出于以上种种原因,在这里坚持半年后,我便和几个好同事一起选择了离职。而在此之前的半年的里,公司的员工已经换了至少 3 茬,很多人待不了几天,就因为加班严重而离职。


22 年春节,没有寒假。


两周后,我又回到了昆明的出租屋,距离房租到期还有一个多接近两个月,我放慢了步伐,低频率投递简历,一边玩一边学习,慢慢调整心态。几周后收到面试邀请,这回我的简历是被捞的,经过简单的笔试面试,我收到了现在这家公司的 offer。当时面试另一家也在 offer 中了,也是知名企业的技术研发部门,但是出于各种原因没有选择,现在看来无疑是正确的。我前面说过,人也好,工作也罢,我们与这些事物的相遇,是缘分,冥冥之中,那个该出现的事物一定会出现,你们将一起走过一段旅程。


出差杭州,新公司,新环境,新技术。


从农业社会进入工业社会


如果上一家公司的技术是自行车,那么现在公司的技术就是火箭。技术的复杂度与前沿程度要领先 4 ~ 6 年(尽管这个项目也是三四年前的了),业务体量、代码体量也是几百多倍。


焦虑,痛苦,害怕。


就像农业社会的人突然来到现代文明社会,看到满街的汽车和摩天大楼,有种碾压感。


全新的技术栈摆在我的面前,很多要学习的东西,而留给我的时间并不算多。


22 年的春天,杭州为了迎接亚运会正在赶工修建地铁和路面,工地上的机器 24 小时连轴转,我并不能好好的休息,加上焦虑使我睡眠很浅,我便每天早晨 7 点钟起来看视频,学到 9 点再洗漱去公司上班,晚上下班后,又经常学到深夜。就这样持续两周,我大概已经能够熟练的编写新技术栈的代码了,但是公司项目里用到的一些配套技术,还有公司自研的组件,都需要我去持续研究。


其实进入公司第二天就开始接需求了,接到了一个看似很简单,却又很难的需求。公司的基础建设现在仍然令我瞠目结舌,很多东西我以前都没有接触过,以至于我理解起来有些困难。当时带我的师兄,是个不善言谈的“真程序员”,我问他的问题被他误解后给我讲解了很深奥的理论知识,而我面对这样的东西的时候一头雾水,并不能帮助我解决实际问题。幸运的是,由于他很忙,换了另一个人带我,这个同事有着丰富的经验,性格很好,也很会给新人讲解问题,以及了解新人到底真正想要得到的帮助。在各方努力下,我开始渐渐有了感觉,领导也很重视我,给我布置了很多很有难度的任务,在这些任务中,我开始熟悉公司的代码,系统架构,业务流程,开始有了掌控感,能够 hold 住所有需求。同时,我也开始带新来的同事,这令我开始以更高的姿态去和别人对话,开始学会选择,学会拒绝,学会刚柔并用,学会更好地处理问题,推动事务的前进。


这一年,领导很拼,我跟着领导也很拼,做了很多建设,做了很多优化,尽管结果上可能并没有太好,但是我工作的量,以及负责的事务却在团队里是很拔尖的。


找到自我


工作之余,写完了一个 React 入门项目,使用 React + React-router + Redux + React-redux + json-server 实现的一个后台管理系统。紧接着又将我之前的静态页面小程序改造,技术栈使用 Taro,也就是 React + TS + Dva + 微信云开发。但这时其实我对 TS 还有 Taro 都是不熟悉的,基本是找的开源项目搭建了项目基础结构,TS 用得也不多。但是这次最大的收获是学会了一点 UI 设计,给自己的小程序加了很多有意思的小功能,但是不得不说代码写得很烂,对 Taro 一窍不通,文档都没怎么看过,代码结构存在很大的问题。不过马上一年了,今年夏天准备把小程序使用 Uniapp + Koa + TS + Vue3 再次重构一遍。


在此期间,我还投入到 Webpack 的学习中,虽然翻来覆去学了好几次,但是都不是很认真,不过看看文档写写配置没什么问题了。除了每天刷几篇博客外,下半年主要精力还是投入在公司业务里,直到 10 月份回成都后,才有了更多的时间。这时我已经计划自己单独写一个 React 组件库了,看了很多文章和源码,学习到了不少东西。在 12 月份左右写了 12 个组件,但是属于是初体验,很多东西没考虑,比如主题定制、国际化等。在 12 月也就终止了项目,觉得应该能做得更好,所以重新设计了项目架构,甚至图标库也是完全自己搭建。不过因为新年到来,一直搁置到现在。


这几个月,主要还是因为受亲戚所托,给他们开发了一个 app,也算不得外包,就是亲戚的原因,没法拒绝。这次开发直接写吐了,功能多不说,连带 UI、需求、合同、前端都让我一个人干了,所以很累。因此 3 月到 5 月这段时间不是很想写代码,简单看了下 Nest.js 入门,顺带学了下 Koa,下一步准备把 Koa 巩固下,组件库的事得放一放了。


写在最后


与朋友见过几次,都说我开始卷起来了。也确实是的,几乎每天都在看博客,编码的习惯也开始养成了,会的很多,不会的更多。但是焦虑已经没有了,更多的是沉稳,对于可能会出现的一些变化已经做好了心理准备。现在走进了一个难得的舒适区,我准备多躺一会儿,这样的机会,不是人人都有,这样的时光,在人生的长途旅行里,可不多见。


也祝大家都能达成所愿。

收起阅读 »

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


这些经验对正在阅读文章的你有用吗,欢迎一起交流,让我们一起交流您遇到的问题。 


这篇文章是去年写的,今天增加了点内容,掘金上同步更新了一下,希望可以被更多的人看到。


如果这篇文章说道你心里了,可以点赞、分享、评论、收藏、转发哦。


作者:爱海贼的无处不在
来源:juejin.cn/post/7232625387296096312
收起阅读 »

使用Vue3 + AR撸猫,才叫好玩

web
先来个预告效果图开场: 前言:浏览苹果官网时,你会看到发现每个设备在介绍页底部有这么一行文字:“用增强现实看看***”。使用苹果设备点击之后就能将该设备投放于用户所在场景视界,在手机摄像头转动的时候,也能看到物体对象不同的角度,感觉就像真的有一台手机放在你...
继续阅读 »

先来个预告效果图开场


cat (1).gif



前言:浏览苹果官网时,你会看到发现每个设备在介绍页底部有这么一行文字:“用增强现实看看***”。使用苹果设备点击之后就能将该设备投放于用户所在场景视界,在手机摄像头转动的时候,也能看到物体对象不同的角度,感觉就像真的有一台手机放在你的面前。(效果如下图。注意:由于该技术采用苹果自有的arkit技术,安卓手机无法查看)



微信图片_20211105154040.png


聪明的你可能已经想到了,为什么只能用苹果手机才能查看,那有没有一种纯前端实现的通用的web AR技术呢?


纯前端解决方案


纯前端技术的实现可以用下图总结:


image.png


以JSARToolKit为例:



  • 使用WebRTC获取摄像头信息,然后在canvas画布上绘制原图;

  • JSARToolKit计算姿态矩阵,进而渲染虚拟信息


image.png


实现核心步骤


image.png



  • (识别)WebRTC获取摄像头视频流;

  • (跟踪)Tracking.js 、JSFeat 、ConvNetJS 、deeplearn.js 、keras.js ;

  • (渲染)A-Frame、 Three.js、 Pixi.js 、Babylon.js


比较成熟的框架:AR.js


好比每个领域都有对应的主流开发框架,Web AR领域比较成熟框架的就是AR.js,它在增强现实方面主要提供了如下三大功能:



  1. 图像追踪。当相机发现一幅2D图像时,可以在其上方或附近显示某些内容。内容可以是2D图像、gif、3D模型(也可以是动画)和2D视频。案例:艺术品、学习资料(书籍)、传单、广告等等。

  2. 基于位置的AR。这种“增强现实”技术利用了真实世界的位置,在用户的设备上显示增强现实的内容。开发者可以利用该库使用户获得基于现实世界位置的体验。用户可以随意走动(最好是在户外)并通过智能手机看到现实世界中任何地点的 AR 内容。若用户移动和旋转手机,AR内容也会同步做出反应(这样一些AR内容就被“固定”到真实位置了,且会根据它们与用户的距离做出适当的变化)。这样的解决方案让我们做出交互式旅游向导成为可能,比如游客来到一个新的城市,游览名胜古迹、博物馆、餐馆、酒店等等都会更方便。我们也可以改善学习体验,如寻宝游戏、生物或历史学习游戏等,还可以将该技术用于情景艺术(视觉艺术体验与特定的现实世界坐标相结合)。

  3. 标记跟踪。当相机发现一个标记时,可以显示一些内容(这与图像跟踪相同)。标记的稳定性不成问题,受限的是形状、颜色和尺寸。可以应用于需要大量不同标记和不同内容的体验,如:(增强书籍)、传单、广告等。


开始上手体验AR.js


开发调试开启https


由于使用到摄像头敏感权限,调试时必须基于https环境打开才能正常运行。如果是以往,自己手动搭建个https环境调试对于很多新手来说还是比较麻烦耗费时间,好在最新的基于vite+vue3的脚手架搭建的项目,可以轻松用一行命令开启https访问。用脚手架初始化好代码之后,先修改package.json文件,在dev命令中加上--host暴露网络请求地址(默认不开启),如下图:


image.png


接着用下面命令运行即可开启https:


npm run dev -- --https

image.png


先跑跑官方demo,看看效果


学习一门新框架或语言,最好的方式就是先将官方demo跑起来体验看看。


下面是官方代码展示的案例效果(注:该录制动图体积较大,请耐心等待加载)


案例一


案例二


wow~ 是不是感觉还蛮有意思的?接下来正式进入文章的主题,开始撸猫吧🐱


开始



前面有提到,AR.js基于三种方式展示内容,下面将使用基于图像追踪(Image Tracking) 方式实现。顾名思义,图像追踪就是基于一张图片,根据图片的特性点识别图片并跟踪展示AR内容,例如当摄像头捕捉到该图片时,就可以将要展示的内容悬浮于该图片上方展示。



引入依赖库


Ar.js从版本3开始采用了新的架构,使用jsartoolkit5进行跟踪定位,而渲染库有两种方式可选:A-Frame 或 Three.js。A-Frame方式就是通过html标签的方式简化创建场景素材,比如说展示一张图片,可以直接使用 <a-image></a-image>方式展示。


修改index.html文件:


先将vue代码注入注释掉


image.png
然后引入依赖:


<script src='./src/sdk/ar-frame/aframe-master.min.js'></script>
<script src='./src/sdk/ar-frame/aframe-ar-nft.js'></script>

撸猫姿势一:展示猫图片


<body>
<a-scene embedded arjs>
<a-assets>
<img id="my-image" src="./src/assets/cat.jpg">
</a-assets>
<a-marker preset="hiro">
<a-image rotation="90 0 0" src="#my-image"></a-image>
<!-- <a-box position="0 0.5 0" material="color: green;"></a-box> -->
</a-marker>
<a-entity camera></a-entity>
</a-scene>
</body>

简单解释下上面的代码:



  1. <a-scene>声明一个场景,你可以理解相当于一个body元素,里面嵌入其他标签元素;

  2. <a-marker>标签声明的是标识图片,也就是相机识别到标识图片时,做相应的处理;这里采用插件预设的hiro图片,下面效果动图可以看到

  3. 使用<a-assets>包裹使用到的素材,相当于声明引入素材,接着在<a-marker>中使用


看下效果:


喵


撸猫姿势二:播放视频


除了展示图片,还可以展示视频,先看效果:


cat (1).gif


代码如下:


  <a-scene vr-mode-ui="enabled: false;" renderer='antialias: true; alpha: true; precision: mediump;' embedded
arjs='trackingMethod: best; sourceType: webcam; debugUIEnabled: false;'>


<a-assets>
<video
src="https://ugcydzd.qq.com/uwMROfz2r57CIaQXGdGnC2ddPkb5Wzumid6GMsG9ELr7HeBy/szg_52471341_50001_d4615c1021084c03ad0df73ce2e898c8.f622.mp4?sdtfrom=v1010&guid=951847595ac28f04306b08161bb6d1f7&vkey=3A19FB37CFE7450C64A889F86411FC6CE939A42CCDAA6B177573BBCB3791A64C441EFF5B3298E3ED4E99FFA22231772796F5E8A1FCC33FE4CAC487680A326980FFCC5C56EB926E9B4D20E8740C913D1F7EBF59387012BEC78D2816B17079152BC19FCEF09976A248C4B24D3A5975B243614000CAA333F06D850034DA861B01DCA1D53B546120B74F%22"
preload="auto" id="vid" response-type="arraybuffer" loop crossorigin webkit-playsinline muted playsinline>

</video>
</a-assets>

<a-nft videohandler type='nft' url='./src/assets/dataNFT/pinball' smooth="true" smoothCount="10"
smoothTolerance="0.01" smoothThreshold="5">

<a-video src="#vid" position='50 150 -100' rotation='90 0 180' width='300' height='175'>
</a-video>
</a-nft>
<a-entity camera></a-entity>
</a-scene>


<script>
window.onload = function () {
AFRAME.registerComponent('videohandler', {
init: function () {
var marker = this.el;

this.vid = document.querySelector("#vid");

marker.addEventListener('markerFound', function () {
// 识别到标识图,开始播放视频
this.vid.play();
}.bind(this));

marker.addEventListener('markerLost', function () {
// 丢失标识图,停止播放视频
this.vid.pause();
this.vid.currentTime = 0;
}.bind(this));
}
});
};
</script>

🐱:喵~是不是感觉更酷更好玩了?


撸猫姿势三: 配合声网技术,与你家的猫隔空喊话



如果你是一位前端开发者,相信你一定知道阮一峰这个大佬。曾经在他的每周科技周刊看到这么一个有趣的事情:在亚马逊某片雨林里,安装了录音设备,实时将拾取到的鸟叫声传到一个网站,你可以打开该网站听到该片雨林里的实时鸟叫声,简单的说就是该网站可以听到该片雨林的”鸟叫直播 "。(可惜现在一时找不到该网站网址)
而作为工作党,爱猫人士的我们,可能有着上述同样的情感需求:要出差几天,家里的猫一时没法好好照顾,想要实时看到家里的爱猫咋办?



买台监控摄像头呗


当然是打开声网找到解决方案:视频通话
(这里为声网文档点个赞,整个产品的文档分类规划的特别清晰,不像某些云服务产品文档像是垃圾桶里翻东西)


image.png


使用vue3写法改造文档demo


先安装依赖包:


"agora-rtc-sdk-ng": "latest"

app.vue中代码:


<script setup>
import AgoraRTC from "agora-rtc-sdk-ng";
import { ref } from "vue";

const joinBtn = ref(null);
const leaveBtn = ref(null);

let rtc = {
localAudioTrack: null,
client: null,
};

let options = {
appId: "2e76ff53e8c349528b5d05783d53f53c",
channel: "test",
token:
"0062e76ff53e8c349528b5d05783d53f53cIADkwbufdA1BIXWsCZ1oFKLEfyPRrCbL3ECbUg71dsv8HQx+f9gAAAAAEAACwxdSy/6RYQEAAQDK/pFh",
uid: 123456,
};

rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

rtc.client.on("user-published", async (user, mediaType) => {
await rtc.client.subscribe(user, mediaType);
console.log("subscribe success");

if (mediaType === "video") {
const remoteVideoTrack = user.videoTrack;
const remotePlayerContainer = document.createElement("div");
remotePlayerContainer.id = user.uid.toString();
remotePlayerContainer.textContent = "Remote user " + user.uid.toString();
remotePlayerContainer.style.width = "640px";
remotePlayerContainer.style.height = "480px";
document.body.append(remotePlayerContainer);
remoteVideoTrack.play(remotePlayerContainer);
}

if (mediaType === "audio") {
const remoteAudioTrack = user.audioTrack;
remoteAudioTrack.play();
}

rtc.client.on("user-unpublished", (user) => {
const remotePlayerContainer = document.getElementById(user.uid);
remotePlayerContainer.remove();
});
});

// 加入通话
const handleJoin = async () => {
await rtc.client.join(
options.appId,
options.channel,
options.token,
options.uid
);
rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
rtc.localVideoTrack = await AgoraRTC.createCameraVideoTrack();
await rtc.client.publish([rtc.localAudioTrack, rtc.localVideoTrack]);
const localPlayerContainer = document.createElement("div");
localPlayerContainer.id = options.uid;
localPlayerContainer.textContent = "Local user " + options.uid;
localPlayerContainer.style.width = "640px";
localPlayerContainer.style.height = "480px";
document.body.append(localPlayerContainer);
rtc.localVideoTrack.play(localPlayerContainer);
console.log("publish success!");
};
// 离开通话
const handleLeave = async () => {
rtc.localAudioTrack.close();
rtc.localVideoTrack.close();
rtc.client.remoteUsers.forEach((user) => {
const playerContainer = document.getElementById(user.uid);
playerContainer && playerContainer.remove();
});
await rtc.client.leave();
};
</script>

<template>
<div>
<button ref="joinBtn" @click="handleJoin" type="button" id="join">
加入
</button>
<button ref="leaveBtn" @click="handleLeave" type="button" id="leave">
离开
</button>
</div>
</template>

<style></style>


跑起来效果:
image.png


这时就相当于在家安装了一个摄像头,如果我们需要远程查看,就可以通过声网官方提供的一个测试地址加入通话


手机打开上述网址,输入你的项目appId跟token,可以看到成功加入通话:


IMG_8973.PNG


111636875423_.pic_hd.jpg


下方图片是手机摄像头捕捉到的画面,原谅我用猫照片代替😂


让视频画面跑在AR.js画面中


这个由于个人时间关系,暂时就不研究实现。这里提供一个想法就是:
单纯的视频画面看起来有点单调,毕竟有可能猫并不在视频画面中出现,结合撸猫姿势一提到的展示图片,其实我们可以在ar场景中视频区域周围,布置照片墙或其他酷炫一点的subject,这样的话我们打开视频即使看不到猫星人,也可以看看它的照片之类的交互。


结束语


本文借征文活动,简单入手了解了下web AR相关知识,在这几天学习的过程中觉得还是蛮好玩的,此文也当抛砖引玉,希望更多开发者了解AR相关的知识。


AR在体验上真的很酷,未来值得期待。


最近几年苹果一直致力于推进AR技术体验并带来相关落地产品,例如为了配合提升AR体验,带来雷达扫描,空间音频功能。值得一提的是,今年的苹果秋季发布会,苹果的邀请函也是利用到了AR + 空间音频技术,即使你不是果粉,当你实际上手体验的时候,你依然会真正发自内心的感觉:wow~cool。可以点此视频观看了解。


而目前的Web AR技术相比于苹果自有的ARkit技术,在体验上还存在一些差距(如性能问题,识别不稳定),同时缺乏生态圈,希望Web AR技术在未来得到快速发展,毕竟web端跨平台通用特性,让人人的终端都可以跑起来才是实现AR场景大规模应用的前提。


Facebook押注的元宇宙概念中,其实也包含了AR技术,所以在元宇宙世界到来之前,AR技术值得我们每一个前端开发者关注学习。


彩蛋


如果你问我最喜欢什么猫,我会说--“房东的猫”,~哈哈哈🐱~


1511636712878_.pic_hd.jpg


参考资料


AR.js官网


AR.js中文翻译文档


跨平台移动Web AR的关键技术 介绍及应用


声网文档


作者:码克吐温
来源:juejin.cn/post/7030342557825499166
收起阅读 »

如何避免旧代码成包袱?5步教你接手别人的系统

👉腾小云导读 老系统的代码,是每一个程序员都不想去触碰的领域,秉着能跑就行的原则,任由其自生自灭。本期就给大家讲讲,接手一套故障频发的复杂老系统需要从哪些地方着手。内容包括:代码串讲、监控建设和告警治理、代码缺陷修复、研发流程建设。在细节上,结合腾讯研发生态,...
继续阅读 »

👉腾小云导读


老系统的代码,是每一个程序员都不想去触碰的领域,秉着能跑就行的原则,任由其自生自灭。本期就给大家讲讲,接手一套故障频发的复杂老系统需要从哪些地方着手。内容包括:代码串讲、监控建设和告警治理、代码缺陷修复、研发流程建设。在细节上,结合腾讯研发生态,介绍有哪些工具可以使用,也介绍一些告警治理、代码 bug 修复的经验、研发流程建设等。欢迎阅读。


👉看目录,点收藏


1 项目背景


2 服务监控


2.1 平台自带监控


2.2 业务定制监控


3 串讲文档


3.1 串讲文档是什么


3.2 为什么需要串讲文档


3.3 怎么输出串讲文档


4 代码质量


4.1 业务逻辑 bug


4.2 防御编程


4.3 Go-python 内存泄露问题


4.4 正确使用外部库


4.5 避免无限重试导致雪崩


4.6 真实初始化


4.7 资源隔离


4.8 数据库压力优化


4.9 互斥资源管理


5 警告治理


5.1 全链路超时时间合理设置


5.2 基于业务分 set 进行资源隔离


5.3 高耗时计算使用线程池


6 研发流程


7 优化效果总结


7.1 健全的 CICD


7.2 更完备的可观测性


7.3 代码 bug 修复


7.4 服务被调成功率优化


7.5 外部存储使用优化


7.6 CPU 用量下降


7.7 代码质量提升


7.8 其他优化效果


01、项目背景


内容架构为 QB 搜索提供内容接入、计算、分发的全套服务。经过多年的快速迭代,内容架构包括 93 个服务,光接入主链路就涉及 7 个服务,支持多种接口类型。其分支定制策略多且散,数据流向混杂,且有众多 bug。


项目组接手这套架构的早期,每天收到大量的业务故障反馈以及服务自身告警。即便投入小组一半的人力做运维支持,依旧忙得焦头烂额。无法根治系统稳定性缺陷的同时,项目组还需要继续承接新业务,而新业务又继续暴露系统缺陷,陷入不断恶化的负循环,漏洞百出。没有人有信心去承诺系统稳定性,团队口碑、开发者的信心都处于崩溃的边缘。


在此严峻的形势下,项目组全员投入启动稳定性治理专项,让团队进入系统稳定的正循环


02、服务监控


监控可以帮助我们掌握服务运行时状态,发现、感知服务异常情况。通过看到问题 - 定位问题 - 修复问题来更快的熟悉模块架构和代码实现细节。下面分两部分介绍,如何利用监控达成稳定性优化。


2.1 平台自带监控


若服务的部署、发布、运行托管于公共平台,则这些平台可能会提供容器资源使用情况的监控,包括:CPU 利用率监控、内存使用率监控、磁盘使用率监控等服务通常应该关注的核心指标。我们的服务部署在123 平台(司内平台),有如下常用的监控。


平台自带监控:


监控类型解析
服务运行监控123 平台的 tRPC 自定义监控:event_alarm 可监控服务异常退出。
节点服务资源使用监控内存使用率监控:检查是否存在内存泄露,会不会不定期 OOM。
磁盘使用率监控:检查日志是否打印的过多,日志滚动配置是否合理。
CPU 使用率监控
如存在定期/不定期的 CPU 毛刺,则检查对应时段请求/定期任务是否实现不合理。如CPU 配额高而使用率低,则检查是配置不合理导致资源浪费,还是服务实现不佳导致 CPU 打不上去。

外部资源平台监控:


数据库连接数监控:检查服务使用 DB 是否全是长连接,使用完没有及时 disconnect 。
数据库慢查询监控:SQL 命令是否不合理,DB 表是否索引设置不合理。数据库 CPU 监控:检查服务是否全部连的 DB 主机,对于只读的场景可选择用只读账号优先读备机,降低 DB 压力。其他诸如腾讯云 Redis 等外部资源也有相关的慢查询监控,亦是对应检查。

2.2 业务定制监控


平台自带的监控让我们掌控服务基本运行状态。我们还需在业务代码中增加业务自定义监控,以掌控业务层面的运转情况。


下面介绍常见的监控完善方法,以让各位对于业务运行状态尽在掌握之中。


2.2.1 在主/被调监控中增加业务错误码


一般来说,后台服务如果无法正常完成业务逻辑,会将错误码和错误详情写入到业务层的回包结构体中,然后在框架层返回成功。


这种实现导致我们无法从平台自带的主/被调监控中直观看出有多少请求是没有正常结果的。一个服务可能看似运行平稳,基于框架层判断的被调成功率 100%,但其中却有大量没有正常返回结果的请求,在我们的监控之外。


此时如果我们想要对这些业务错误做监控,需要上报这些维度:请求来源(主调服务、主调容器、主调 IP、主调 SET)、被调容器、错误码、错误数,这和被调监控有极大重合,存在资源浪费,并且主、被调服务都有开发成本。


若服务框架支持,我们应该尽可能使用框架层状态包来返回业务自定义的错误码。以tRPC框架为例服务的<被调监控 - 返回码>中,会上报框架级错误码和业务错误码:框架错误码是纯数字,业务错误码是<业务协议_业务码>。如此一来,就可以在主/被调服务的平台自带监控中看到业务维度的返回码监控。


2.2.2 在主/被调监控中注入业务标识


有时候一个接口会承担多个功能,用请求参数区分执行逻辑A / 逻辑B。在监控中,我们看到这个接口失败率高,需要进一步下钻是 A 失败高还是 B 失败高、A/B 分别占了多少请求量等等。


对这种诉求,我们可以在业务层上报一个带有各种主调属性的多维监控以及接口参数维度用于下钻分析。但这种方式会造成自定义监控和被调监控字段的重叠浪费。


更好的做法是:将业务维度注入到框架的被调监控中,复用被调监控中的主调服务、节点、SET、被调接口、IP、容器等信息。


2.2.3 单维属性上报升级成多维属性上报


单维属性监控指的是上报单个字符串(维度)、一个浮点数值以及这个浮点数值对应的统计策略。多维监控指的是上报多个字符串(维度)、多个浮点数值以及每个浮点数值对应的统计策略。


下面以错误信息上报场景为例,说明单维监控的缺点以及如何切换为多维上报。


作为后台服务,处理逻辑环节中我们需要监控上报各类关键错误,以便及时感知错误、介入处理。内容架构负责多个不同业务的接入处理,每个业务具有全局唯一的资源标识,下面称为 ResID。当错误出现时,做异常上报时,我们需要上报 “哪个业务”,出现了“什么错误”。


使用单维上报时,我们只能将二者拼接在一起,上报 string (ResID + "." + ErrorMsg)。对不同的 ResID 和不同的 ErrorMsg,监控图是铺平展开的,只能逐个查看,无法对 ResID 下钻查看每类错误分别出现了多少次或者对 ErrorMsg 下钻,查看此错误分别在哪些 ID 上出现。


除了查看上的不便,多维属性监控配置告警也比单维监控便利。对于单维监控,我们需要对每一个可能出现的组合配置条件:维度值 = XXX,错误数 > N 则告警。一旦出现了新的组合,比如新接入了一个业务,我们就需要再去加告警配置,十分麻烦且难以维护。


而对于多维监控表,我们只需设置一条告警配置:对 ResID && 错误维度 下钻,错误数 > N 则告警。新增 ResID 或新增错误类型时,均会自动覆盖在此条告警配置内,维护成本低,配置简单。


03、串讲文档


监控可以帮助我们了解服务运行的表现,想要“深度清理”服务潜在问题,我们还需要对项目做代码级接手。在稳定性治理专项中,我们要求每个核心模块都产出一份串讲文档,而后交叉学习,使得开发者不单熟悉自己负责的模块,也对完整系统链路各模块功能有大概的理解,避免窥豹一斑。


3.1 串讲文档是什么


代码串讲指的是接手同学在阅读并理解模块代码后,系统的向他人介绍对该模块的掌握情况。代码串讲文档则是贯穿串讲过程中的分享材料,需要对架构设计、代码实现、部署、监控、理想化思考等方面进行详细介绍,既帮助其他同学更好的理解整个模块,也便于评估接手同学对项目的理解程度。


代码串讲文档通常包括以下内容:模块主要功能,上下游关系、整体架构、子模块的详细介绍、模块研发和上线流程、模块的关键指标等等。在写串讲文档的时候也通常需要思考很多问题:这个功能为什么需要有?设计思路是这样的?技术上如何实现?最后是怎么应用的?


3.2 为什么需要串讲文档


原因
解析
确保代码走读的质量串讲文档涵盖了模块最重要的几个部分,要求开发人员编写串讲文档的这些章节,可保障他在走读此模块时高度关注到这些部分,并总结输出成文档,确保了代码学习的深度和质量。
强化理解编写串讲文档的过程,也是对模块各方面的提炼总结。在编写的过程中,开发人员可能会发现走读中未想到的问题。通过编写代码串讲文档,开发人员可以更好地理解整个系统的结构和实现,加强对代码和系统的理解,为后续接手、维护该系统提供了帮助。
团队知识沉淀和积累串讲文档是团队内部进行知识分享和沟通的载体,也是团队知识不断沉淀积累的过程,可以作为后续新人加入团队时了解系统的第一手材料,也可以作为其他同学后续多次翻阅、了解该系统的材料。

3.3 怎么输出串讲文档


增代码串讲文档的时候,需要从2个方面进行考虑——读者角度和作者角度。


作者角度: 需要阐述作者对系统和代码的理解和把握,同时也需要思考各项细节:这个功能为什么需要有、设计思路是怎样的、技术上如何实现、最后是怎么应用的等等。


读者角度: 需要考虑目标受众是哪些,尽可能地把读者当成技术小白,思考读者需要了解什么信息,如何才能更好地理解代码的实现和作用。


通常,代码串讲文档可以包含以下几个部分:


文档构成
信息
模块主要功能首先需要明确模块的主要功能,并在后续的串讲中更加准确地介绍代码的实现。
上下游关系在介绍每个模块时,需要明确它与其他模块之间的上下游关系,即模块之间的调用关系,这有助于了解模块之间的依赖关系,从而更好地理解整个系统的结构和实现。
名词解释对一些关键词、专业术语和缩写词等专业术语进行解释,不同团队使用的术语和缩写可能不同,名词解释可以减少听众的阅读难度,提高沟通效率。
整体架构介绍整个系统的设计思路、实现方案和架构选择的原因以及优缺点。
子模块的详细解读在介绍每个子模块时,需要对其进行详细的解读,包括该模块的具体功能、实现方式、代码实现细节等方面。
模块研发和上线流程介绍模块的研发和上线流程,包括需求分析、设计、开发、测试、上线等环节。这有助于了解模块的开发过程和上线流程,以及研发团队的工作流程。
模块的关键指标介绍模块的关键指标,包括不限于业务指标、服务监控、成本指标等。
模块当前存在问题及可能的解决思路主要用于分析该模块目前存在的问题,并提出可能的解决思路和优化方案。
理想化的思考对该模块或整个系统的未来发展和优化方向的思考和展望,可以对模块的长期目标、技术选型、创新探索、稳定性建设等方面进行思考。
中长线工作安排结合系统现状和理想化思考,做出可行的中长线工作计划。
串讲中的问题这部分用于记录串讲中的问题及解答,通常是Q&A的形式,串讲中的问题也可能是系统设计相关的问题,后续将作为todo项加入到工作安排中。

04、代码质量


代码质量很大程度上决定服务的稳定性。在对代码中业务逻辑 bug 进行修复的同时,我们也对服务的启动、数据库压力及互斥资源管理做优化。


4.1 业务逻辑bug


4.1.1 内存泄漏


如下图代码所示,使用 malloc 分配内存,但没有 free,导致内存泄露。该代码为 C 语言风格,现代 C++ 使用智能指针可以避免该问题。


图片


4.1.2 空指针访问成员变量


如下图所示的代码,如果一个非虚成员函数没有使用成员变量,因编译期的静态绑定,空指针也可以成功调用该成员函数。但如果该成员函数使用了成员变量,那么空指针调用该函数时则会 core。该类问题在接入系统仓库中比较普遍,建议所有指针都要进行合理的初始化。


图片


4.2 防御编程


4.2.1 输入防御


如下图所示,如果发生了错误且没有提前返回,request 将引发 panic。针对输入,在没有约定的情况下,建议加上常见的空指针判断及异常判断。


图片


4.2.2 数组长度防御-1


如下图所示,当 url 长度超过 512 时,将会被截断,导致产出错误的url。建议针对字符串数组的长度进行合理的初始化,亦或者使用string来替代字符数组。


图片


4.2.3 数组长度防御-2


如下图所示,老代码不判断数组长度,直接取值。当出现异常数据时,该段代码则会core。建议在每次取值时,基于上下文做防御判断。


图片


4.2.4 野指针问题


下图中的ts指针指向内容和 create_time 一致。当 create_time 被 free 之后,ts 指针就变成了野指针。


该代码为 C 语言风格代码,很容易出现内存方面的问题。建议修改为现代 C++风格。


图片


下图中,临时变量存储的是 queue 中的值的引用。当 queue pop 后,此值会被析构;而变量引用的存储空间也随之释放,访问此临时变量可能出现未定义的行为。


图片


4.2.5 全局资源写防护


同时读写全局共有资源,尤其生成唯一 id,要考虑并发的安全性。


这里直接通过查询 DB 获取最大的 res_id,转成 int 后加一,作为新增资源的唯一 id。如果并发度超过 1,很可能会出现 id 重复,影响后续操作逻辑。


图片


4.2.6 lua 添加 json 解析防御


如下图所示的 lua 脚本中,使用 cjson 将字符串转换 json_object。当 data_obj 不是合法的 json 格式字符串时,decode 接口会返回 nil。修复前的脚本未防御返回值为空的情况,一旦传入非法字符串,lua 脚本就会引发 coredump。


图片


4.3 Go-python 内存泄露问题


如下图 85-86 行代码所示,使用 Go-python 调用 python 脚本,将 Go 的 string 转为PyString,此时 kv 为 PyObject。部分 PyObject 需要在函数结束时调用 DecRef,减少引用计数,以确保资源释放,否则会造成内存泄露。


判定依据是直接看 python sdk 的源码注释,如果有 New Reference , 那么必须在使用完毕时释放内存,Borrowed Reference 不需要手动释放。


图片


4.4 正确使用外部库


4.4.1 Kafka Message 结束字符


当生产者为 Go 服务时,写入 kafka 的消息字符串不会带有结束字符 '\0'。当生产者为 C++ 服务时,写入 kafka 的消息字符串会带有结束字符 '\0'。


如下图 481 行代码所示,C++中使用 librdkafka 获取消费数据时,需传入消息长度,而不是依赖程序自行寻找 '\0' 结束符。


图片


4.5 避免无限重试导致雪崩


如下图所示代码所示,失败之后立马重试。当出现问题时,不断立即重试,会导致雪崩。给重试加上 sleep,减缓下游雪崩的速度,留下缓冲的时间。


图片


4.6 真实初始化


如果每次服务启动都存在一定的成功率抖动,需要检查服务注册前的初始化流程,看看是不是存在异步初始化,导致未完成初始化即提供对外服务。如下图 48 行所示注释,原代码中初始化代码为异步函数,服务对外提供服务时可能还没有完成初始化。


图片


4.7 资源隔离


时延/成功率要求不同的服务接口,建议使用不同的处理线程。如下图中几个 service,之前均共用同一个处理线程池。其中 secure_review_service 处理耗时长,流量不稳定,其他接口流量大且时延敏感,线上出现过 secure_review_service 瞬时流量波峰,将处理线程全部占住且队列积压,导致其他 service 超时失败。


图片


4.8 数据库压力优化


4.8.1 分批拉取


当某个表数据很多或单次传输量很大,拉取节点也很多的时候,数据库的压力就会很大。这个时候,可以使用分批读取。下图 308-343 行代码,修改了 sql 语句,从一批拉取改为分批拉取。


图片


4.8.2 读备机


如果业务场景为只读不写数据,且对一致性要求不高,可以将读配置修改为从备机读取。mysql 集群一般只会有单个主机、多个备机,备机压力较小。如下图 44 行代码所示,使用readuser,主机压力得到改善。


图片


4.8.3 控制长链接个数


需要使用 mysql 长链接的业务,需要合理配置长链接个数,尤其是服务节点数很多的情况下。连接数过多会导致 mysql 实例内存使用量大,甚至 OOM;此外 mysql 的连接数是刚性限制,超过阈值后,客户端无法正常建立 mysql 连接,程序逻辑可能无法正常运转。


4.8.4 建好索引


使用 mysql 做大表检索时,应该建立与查询条件对应的索引。本次优化中,我们根据 DB 慢查询统计,找到有大表未建查询适用的索引,导致 db 负载高,查询速度慢。


4.8.5 实例拆分


非分布式数据库 (如 mariaDB) 存储空间是有物理上限的,需要预估好数据量,数据量过多及时进行合理的拆库。


4.8.6 分布式数据库负载均衡


分布式数据库一般应用在海量数据场景,使用中需要留意节点间负载均衡,否则可能出现单机瓶颈,拖垮整个集群性能。如下图是内容架构使用到的 hbase,图中倒数两列分别为请求数和region 数,从截图可看出集群的 region 分布较均衡,但部分节点请求量是其他节点几倍。造成图中请求不均衡的原因是集群中有一张表,有废弃数据占用大量 region,导致使用中的 region 在节点间分布不均,由此导致请求不均。解决方法是清理废弃数据,合并空数据 region。


图片


4.9 互斥资源管理


4.9.1 避免连接占用


接入系统服务的 mysql 连接全部使用了连接池管理。每一个连接在使用完之后,需要及时释放,否则该连接就会被占住,最终连接池无资源可用。下图所示的 117 行连接释放为无效代码,因为提前 return。有趣的是,这个 bug 和另外一个 bug 组合起来,解决了没有连接可用的问题:当没有连接可用时,获取的连接则会为空。该服务不会对连接判空,导致服务 core 重启,连接池重新初始化,又有可用的连接了。针对互斥的资源,要进行及时释放。


图片


4.9.2 使用 RAII 释放资源


下图所示的 225 行代码,该任务为互斥资源,只能由一个节点获得该任务并执行该任务。GetAllValueText 执行完该任务之后,应该释放该任务。然而在 240 行提前 return 时,没有及时释放该资源。


优化后,我们使用 ScopedDeferred 确保函数执行完成,退出之前一定会执行资源释放。


图片


05、告警治理


告警轰炸是接手服务初期常见的问题。除了前述的代码质量优化,我们还解决下述几类告警:


全链路超时配置不合理。下游服务的超时时间,大于上游调用它的超时时间,导致多个服务超时告警轰炸、中间链路服务无效等待等。
业务未隔离。某个业务流量突增引起全链路队列阻塞,影响到其他业务。请求阻塞。请求线程中的处理耗时过长,导致请求队列拥堵,请求消息得不到及时处理。

5.1 全链路超时时间合理设置


未经治理的长链路服务,因为超时设置不合理导致的异常现象:




  • 超时告警轰炸: A 调用 B,B 调用 C,当 C 异常时,C 有超时告警,B 也有超时告警。在稳定性治理分析过程中,C 是错误根源,因而 B 的超时告警没有价值,当链路较长时,会因某一个底层服务的错误,导致海量的告警轰炸。




  • 服务无效等待: A 调用 B,B 调用 C,当 A->B 超时的时候,B 还在等 C 的回包,此时 B 的等待是无价值的。




这两种现象是因为超时时间配置不合理导致的,对此我们制定了“超时不扩散原则”,某个服务的超时不应该通过框架扩散传递到它的间接调用者,此原则要求某个服务调用下游的超时必须小于上游调用它的超时时间。


5.2 基于业务分 set 进行资源隔离


针对某个业务的流量突增影响其他业务的问题,我们可将重点业务基于 set 做隔离部署。确保特殊业务只运行于独立节点组上,当他流量暴涨时,不干扰其他业务平稳运行,降低损失范围。


5.3 高耗时计算使用线程池


如下图红色部分 372 行所示,在请求响应线程中进行长耗时的处理,占住请求响应线程,导致请求队列阻塞,后续请求得不到及时处理。如下图绿色部分 368 行所示,我们将耗时处理逻辑转到线程池中异步处理,从而不占住请求响应线程。


图片


06、研发流程


在研发流程上,我们沿用司内其他技术产品积累的 CICD 建设经验,包括以下措施:


研发方式具体流程
建设统一镜像统一开发镜像,任意服务都可以在统一镜像下开发编译
配置工蜂仓库保护 master 分支,代码经过评审才可合入
规范分支和 tag 命名,便于后续信息追溯
规范化 commit 信息,确保信息可读,有条理,同样便于后续信息追溯
建设蓝盾流水线MR 流水线提交 MR 时执行的流水线,涵盖代码静态检查、单元测试、接口测试,确保合入 master 的代码质量达到基本水平
提交构建流水线:MR 合入后执行的流水线,同样涵盖代码检查,单元测试,接口测试,确保 master 代码随时可发布
XAC 发布流水线:发布上线的流水线,执行固化的灰度->全量流程,避免人工误操作
落实代码评审机制确保代码合入时,经过了至少一位同事的检查和评审,保障代码质量、安全

07、优化效果总结


7.1 健全的CICD


7.1.1 代码合入


在稳定性专项优化前,内容架构的服务没有合理的代码合入机制,导致主干代码出现违背编码规范、安全漏洞、圈复杂度高、bug等代码问题。


优化后,我们使用统一的蓝盾流水线模板给所有服务加上流水线,以保证上述问题能被自动化工具、人工代码评审拦截。


7.1.2 服务发布


在稳定性优化前,内容架构服务发布均为人工操作,没有 checklist 机制、审批机制、自动回滚机制,有很大安全隐患。


优化后,我们使用统一的蓝盾流水线模板给所有服务加上 XAC 流水线,实现了提示发布人在发布前自检查、double_check 后审批通过、线上出问题时一键回滚。


7.2 更完备的可观测性


7.2.1 多维度监控


在稳定性优化前,内容架构服务的监控覆盖不全,自定义监控均使用一维的属性监控,这导致多维拼接的监控项泛滥、无法自由组合多个维度,给监控查看带来不便。


优化后,我们用更少的监控项覆盖更多的监控场景。


7.2.2 业务监控和负责人制度


在稳定性优化前,内容架构服务几乎没有业务维度监控。优化后,我们加上了重要模块的多个业务维度监控,譬如:数据断流、消息组件消息挤压等,同时还建立值班 owner、服务 owner 制度,确保有告警、有跟进。


7.2.3 trace 完善与断流排查文档建设


在稳定性优化前,虽然已有上报鹰眼 trace 日志,但上报不完整、上报有误、缺乏排查手册等问题,导致对数据处理全流程的跟踪调查非常困难。


优化后,修正并补全了 trace 日志,建设配套排查文档,case 处理从不可调查变成可高效率调查。


7.3代码bug修复


7.3.1 内存泄露修复


在稳定性优化前,我们观察到有3个服务存在内存泄露,例如代码质量章节中描述内存泄露问题。


图片


7.3.2 coredump 修复 & 功能 bug 修复


在稳定性优化前,历史代码中存在诸多 bug 与可能导致 coredump 的隐患代码。


我们在稳定性优化时解决了如下 coredump 与 bug:


  • JSON 解析前未严格检查,导致 coredump 。
  • 服务还未初始化完成即接流,导致服务重启时被调成功率猛跌。
  • 服务初始化时没有同步加载配置,导致服务启动后缺失配置而调用失败。
  • Kafka 消费完立刻 Commit,导致服务重启时,消息未实际处理完,消息可能丢失/
  • 日志参数类型错误,导致启动日志疯狂报错写满磁盘。

7.4 服务被调成功率优化


在稳定性优化前,部分内容架构服务的被调成功率不及 99.5% ,且个别服务存在严重的毛刺问题。优化后,我们确保了服务运行稳定,调用成功率保持在 99.9%以上。


7.5 外部存储使用优化


7.5.1 MDB 性能优化


在稳定性优化前,内容架构各服务对MDB的使用存在以下问题:低效/全表SQL查询、所有服务都读主库、数据库连接未释放等问题。造成MDB主库的CPU负载过高、慢查询过多等问题。优化后,主库CPU使用率、慢查询数都有大幅下降。


图片


7.5.2 HBase 性能优化


在稳定性优化前,内容架构服务使用的 HBase 存在单节点拖垮集群性能的问题。


优化后,对废弃数据进行了清理,合并了空数据 region,使得 HBase 调用的 P99 耗时有所下降。


图片


7.6 CPU 用量下降


在稳定性优化前,内容架构服务使用的线程模型是老旧的 spp 协程。spp 协程在在高吞吐低延迟场景下性能有所不足,且未来 trpc 会废弃 spp。


我们将重要服务的线程模型升级成了 Fiber 协程,带来更高性能和稳定性的同时,服务CPU利用率下降了 15%。


7.7 代码质量提升


在稳定性优化前,内容架构服务存在很多不规范代码。例如不规范命名、魔术数字和常量、超长函数等。每个服务都存在几十个代码扫描问题,最大圈复杂度逼近 100。


优化后,我们对不规范代码进行了清扫,修复规范问题,优化代码命名,拆分超长函数。并在统一清理后,通过 MR 流水线拦截,保护后续合入不会引入新的规范类问题。


7.8 其他优化效果


经过本轮治理,我们的服务告警量大幅度下降。以系统中的核心处理服务为例,告警数量从159条/天降低到了0条/天。业务 case 数从 22 年 12 月接手以来的 18 个/月下降到 4 个/月。值班投入从最初的 4+ 人力,降低到 0.8 人力。


我们项目组在完成稳定性接手之后,下一步将对全系统做理想化重构,进一步提升迭代效率、运维效率。希望这些经验也对你接管/优化旧系统有帮助。如果觉得内容有用,欢迎分享。

作者:腾讯云开发者
来源:juejin.cn/post/7233564835044524092
收起阅读 »

Kindle 可旋转桌面时钟

web
前言 自己的 Kindle 吃灰很久了,想做个时钟用,但是网上可选的时钟网站比较少,这些时钟网站里面,要么太简单 界面也比较丑陋,要么内容太多 有些本末倒置了,要么网址特别长 输入网址的时候太麻烦。 干脆自己写一个,没多少代码。 (我的 Kindle 差不多十...
继续阅读 »

前言


自己的 Kindle 吃灰很久了,想做个时钟用,但是网上可选的时钟网站比较少,这些时钟网站里面,要么太简单 界面也比较丑陋,要么内容太多 有些本末倒置了,要么网址特别长 输入网址的时候太麻烦。


干脆自己写一个,没多少代码。
(我的 Kindle 差不多十年前的了,系统比较旧,导致需要处理 Kindle 的浏览器兼容性,这里花了一些时间)


使用


image.png


可以通过以下两个网址中的任意一个访问:



Github 项目开源地址:https://github.com/lecepin/kindle-time


配置项


可以将以下参数添加到网址的 URL 中进行生效。



  • fs: 设置字体大小. 默认 7。

  • r: 设置屏幕渲染。默认不旋转。主要用在屏幕横向显示,可以通过设置 90 来实现旋转成横屏。

  • l: 设置语言。默认中文显示。可以设置为 en 实现英文显示。


这些配置项,可以单独使用,也可以一起使用。


例如:http://ktime.leping.fun/?fs=10&r=90


image.png


保持屏幕常亮


在 Kindle 主页面的顶部搜索中,输入 ~ds 回车一下,即可开启屏幕常亮功能。关闭的话,只能通过重启 Kindle 实现。


作者:lecepin
来源:juejin.cn/post/7188831785097101349
收起阅读 »

一个97年的前端卷不动了,跑去学瑜伽?

大家好, 我是刘子弃。现在是23年5月, 裸辞已经快两个月了。 前两天看到行业前辈左耳朵耗子的不幸消息。 突然有一个想把自己这两个月来的心路历程记录一下的想法。 23年3月, 一个倒霉的周五 那是3月的一天周五,周五本来是打工人比较快乐的一天,但是还没上班就...
继续阅读 »

大家好, 我是刘子弃。现在是23年5月, 裸辞已经快两个月了。 前两天看到行业前辈左耳朵耗子的不幸消息。 突然有一个想把自己这两个月来的心路历程记录一下的想法。



23年3月, 一个倒霉的周五


那是3月的一天周五,周五本来是打工人比较快乐的一天,但是还没上班就预示那天的不平凡。 刚起床准备上班。匆忙的洗漱吃片面包就冲向地铁。刚到地铁口发现手机坏了, 读不出SIM卡。重启也无济于事。


先回家连上wifi到公司群说明情况。 就奔向家附近唯一一家手机维修点。 好不容易到了之后发现今天居然不营业。 只好去旁边花几百块买了一个红米备用机(感谢红米)。


一路坎坷到了公司,通知今天周会要宣布一个大事情。 我们项目组做的是web3相关的业务(我非常热爱这个项目组和每天做的工作内容!)。 之前就有风传出来要去香港落地。 当时还激动了一下, 结果周会宣布:“我们解散啦!”


开始毕业


由于项目突然黄了。 就要考虑转岗或者拿大礼包的事情。 显而易见我选择了拿了大礼包。 为什么不去转岗到其他组呢?其中有对前端这个方向未来发展的考虑, 最重要的考虑还是健康吧。 因为生活不规律, 陆陆续续出现过如下几种身体情况:



  1. 胸口痛

  2. 神经衰弱

  3. 颈椎痛

  4. 失眠

  5. 突然来一下全身无力

  6. 注意力难集中

  7. 脑鸣

  8. 鬼压床看到各种幻觉,甚至有几次都感觉魂都快飘出来了。


就这样, 毫无规划的我就毕业了。


计划恢复健康


本以为不上班了会好一点, 结果每天的状态还是比较差。 去体检万幸没有什么大问题。 不幸的是症状还是一如既往的存在。 最后实在不不行就去了精神科果然有了一点轻度的抑郁症。 所以在决定之后做什么之前, 我决定先养好身体, 恢复健康。 毕竟身体是最重要的。


开始卷起了瑜伽教培


由于之前接触过一些身心灵行业的人。 也有过一些冥想的经验和经历。 在健身和身心灵两个方向中我选择了一个最兼顾和均衡的方向就是练习瑜伽。 索性直接报了一个教培班。 一是恢复身体。二是系统的学习一下防止受伤、更快的练习、避免不正当的危险操作。 并且最后可以拿到认证证书打算之后作为一个长久的职业方向发展一下。 就这样报名了瑜伽教培。 现在已经完成了200小时认证, 6月完成500小时认证。目前身体经过训练确实基本健康了, 症状都没有了。身体舒适了不少。 (或许是不上班都会健康哈哈哈)。 精神也放松了许多, 每天早上起来舒爽+ 没有压力的感觉终于又回来了。


11ca0851fc75aad27b66c4510731059.jpg


之后做什么


当然还是需要考虑收入的问题。 裸辞之后, 如果不干程序员了去干什么。我想不止我一个人想过这个问题。 跑滴滴? 外卖员? 对我来说有点不现实。 但是可能也由不得我选择。现在的就业情况能有一个offer就乐上天了。 所以我想做一个实验。 借各位大佬的光。如果您也想过类似的问题。可以把建议告诉我。 我去实际操作一下。 然后再反馈给大家。


作者:刘子弃
来源:juejin.cn/post/7233589699215147069
收起阅读 »

Android推送实践总结

前提条件 这篇文章主要讲集成阿里的EMAS推送过程中的一些坑和接入流程,主要是对接入过程的一个总结(毕竟浪费了我3天时间)。 开通了EMAS服务并创建了应用(创建方式) 创建了各厂商的应用(国内的厂商必须有,要不然离线状态收不到通知,三星除外(貌似也不算国内...
继续阅读 »

前提条件


这篇文章主要讲集成阿里的EMAS推送过程中的一些坑和接入流程,主要是对接入过程的一个总结(毕竟浪费了我3天时间)。



  • 开通了EMAS服务并创建了应用(创建方式

  • 创建了各厂商的应用(国内的厂商必须有,要不然离线状态收不到通知,三星除外(貌似也不算国内厂商-O-))


接入SDK


接入方式官方文档上已写好步骤,这里就不重复赘述了,这里只做个总结。



  1. 工程的setting.gradle中添加阿里云仓库,代码如下:

pluginManagement {
repositories {
...
maven { url "https://maven.aliyun.com/nexus/content/repositories/releases/" }
}
}
dependencyResolutionManagement {
// repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
...
maven { url "https://maven.aliyun.com/nexus/content/repositories/releases/" }
maven {url 'https://developer.huawei.com/repo/'}//接入华为推送时需要
}
}



  1. 工程的build.gradle中添加gradle依赖
buildscript {
dependencies {
...
classpath 'com.aliyun.ams:emas-services:1.0.4'//可以在阿里云仓库查找最新的版本:https://developer.aliyun.com/mvn/search
}
}



  1. 在项目app的build.gradle下面添加插件和依赖,并在同级目录下添加配置文件aliyun-emas-services.json


    所有依赖版本均可在阿里云仓库找到最新版本:developer.aliyun.com/mvn/search

plugins {
...
id 'com.aliyun.ams.emas-services'
}
android {
defaultConfig {
...
ndk {
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86_64'//你支持的cpu架构
}
}
dependencies {
implementation 'com.alipay.sdk:alipaysdk-android:15.8.12'//阿里云基础依赖
implementation 'com.aliyun.ams:alicloud-android-push:3.8.4.1'//推送依赖
implementation 'com.aliyun.ams:alicloud-android-third-push:3.8.4'//辅助推送依赖使用厂商推送时需要,提高到达率
implementation 'com.aliyun.ams:alicloud-android-third-push-xiaomi:3.8.4'
implementation 'com.aliyun.ams:alicloud-android-third-push-huawei:3.8.4'
implementation 'com.aliyun.ams:alicloud-android-third-push-vivo:3.8.4'
implementation 'com.aliyun.ams:alicloud-android-third-push-oppo:3.8.4'
}



  1. aliyun-emas-services.json中配置信息修改


    aliyun-emas-services.json中包含多种服务,对应有status来控制是否启用,1为启用0为不启用,推送服务对应为cps_service,如果只用推送可以只开启这个服务具体可看文档

{
"config": {
"emas.appKey":"*******",
"emas.appSecret":"*******",
"emas.packageName":"*******",
"hotfix.idSecret":"*******",
"hotfix.rsaSecret":"*******",
"httpdns.accountId":"*******",
"httpdns.secretKey":"*******",
"appmonitor.tlog.rsaSecret":"*******",
"appmonitor.rsaSecret":"*******"
},
"services": {
"hotfix_service":{//移动热修复
"status":0,
"version":"3.3.8"
},
"ha-adapter_service":{//性能分析/远程日志基础库
"status":0,
"version":"1.1.5.4-open"
},
"feedback_service":{//移动用户反馈
"status":0,
"version":"3.4.2"
},
"tlog_service":{//远程日志
"status":0,
"version":"1.1.4.5-open"
},
"httpdns_service":{//HTTPDNS
"status":0,
"version":"2.3.0-intl"
},
"apm_service":{//性能分析
"status":0,
"version":"1.1.1.0-open"
},
"man_service":{
"status":1,
"version":"1.2.7"
},
"cps_service":{//移动推送
"status":1,
"version":"3.8.4.1"
}
},
"use_maven":true,//true表示使用远程依赖,false表示使用本地libs依赖
"proguard_keeplist":"********"
}


  1. 项目app的AndroidManifest.xml文件中配置appkey
...
<uses-permission android:name="android.permission.INTERNET" /> <!-- 网络访问 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <!-- 检查Wi-Fi网络状态 -->
<uses-permission android:name="android.permission.GET_TASKS" /> <!-- 推送使用,运行状态 -->
<uses-permission android:name="android.permission.REORDER_TASKS" /> <!-- 推送使用,运行状态 -->
<application
android:allowBackup="false"
tools:replace="android:allowBackup">
<meta-data
android:name="com.alibaba.app.appkey"
android:value="阿里云的APPKEY" />
<meta-data
android:name="com.alibaba.app.appsecret"
android:value="阿里云的appsecret" />
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="appid=华为的appid" />
<meta-data
android:name="com.vivo.push.api_key"
android:value="vivo的apikey" />
<meta-data
android:name="com.vivo.push.app_id"
android:value="vivo的appid" />

<receiver
android:name=".push.PushReceiver"//接收通知的广播继成自MessageReceiver,当app为在线状态或走阿里云通道时调用
android:exported="false">
<intent-filter>
<action android:name="com.alibaba.push2.action.NOTIFICATION_OPENED" />
</intent-filter>
<intent-filter>
<action android:name="com.alibaba.push2.action.NOTIFICATION_REMOVED" />
</intent-filter>
<intent-filter>
<action android:name="com.alibaba.sdk.android.push.RECEIVE" />
</intent-filter>
</receiver>

<activity
android:name=".push.PushPopupActivity"//辅助推送Activity继成自AndroidPopupActivity,当走厂商渠道,通知被点击时调用到onSysNoticeOpened()方法中
android:exported="true"
android:label="@string/title_activity_third_push_notice"
android:launchMode="singleInstance"
android:screenOrientation="portrait" >
<!-- scheme 配置开始 vivo通道需要添加 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="${applicationId}"
android:path="/thirdpush"
android:scheme="agoo" />
</intent-filter>
<!-- scheme 配置结束 -->
</activity>





  1. 初始化SDK


    在Application的onCreate()方法中初始化

open class BaseApp : Application() {
override fun onCreate() {
super.onCreate()
PushServiceFactory.init(this)
initNotificationChannel(this)
MiPushRegister.register(this, "小米的appid", "小米的appkey")
HuaWeiRegister.register(this)
VivoRegister.register(this)
OppoRegister.register(this,"oppo的appkey","oppo的appSecret")
val pushService = PushServiceFactory.getCloudPushService()
if (ProjectSwitch.isDebug) {
pushService.setLogLevel(CloudPushService.LOG_DEBUG) //仅适用于Debug包,正式包不需要此行
}
pushService.setNotificationShowInGroup(true)
pushService.register(this, object : CommonCallback {
override fun onSuccess(response: String?) {
}
override fun onFailed(errorCode: String, errorMessage: String) {
}
})
}

private fun initNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val mNotificationManager: NotificationManager? =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
// 通知渠道的id,后台推送选择渠道时对应的id。
val id = "1"
// 用户可以看到的通知渠道的名字。
val name: CharSequence = "通知渠道的名字"
// 用户可以看到的通知渠道的描述。
val description = "渠道的描述"
val importance: Int = NotificationManager.IMPORTANCE_HIGH
val mChannel = NotificationChannel(id, name, importance)
// 配置通知渠道的属性。
mChannel.setDescription(description)
// 设置通知出现时的闪灯(如果Android设备支持的话)。
mChannel.enableLights(true)
mChannel.setLightColor(Color.RED)
// 设置通知出现时的震动(如果Android设备支持的话)。
mChannel.enableVibration(true)
mChannel.setVibrationPattern(longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400))
// 最后在notificationmanager中创建该通知渠道。
mNotificationManager?.createNotificationChannel(mChannel)
}
}
}

各厂商集成步骤


查看官方辅助通道集成文档help.aliyun.com/document_de…


问题处理


完成上面一堆后本以为可以大功告成了吗,no~no~no~,怎么可能这么简单,接下来就是踩坑之旅了。



  1. Android13对通知做了权限控制,需要显示请求通知权限:developer.android.google.cn/about/versi…

  2. 各厂商对通知做了限制处理,导致必须申请对应渠道才能正常使用通知:



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

如何成为一名优秀的工程师?给每个人的5点建议

一位工程师,如何才能称得上优秀?除了写得一手好 Code,什么样的工作态度和方法才是一个优秀工程师的必备?今天给大家分享百度前COO、计算机科学博士及优秀的管理者陆奇,他提到的这5点要求,相信对每个工程师都适用。 1. 永远相信技术 首先要相信技术,整个工业界...
继续阅读 »

一位工程师,如何才能称得上优秀?除了写得一手好 Code,什么样的工作态度和方法才是一个优秀工程师的必备?今天给大家分享百度前COO、计算机科学博士及优秀的管理者陆奇,他提到的这5点要求,相信对每个工程师都适用。


1. 永远相信技术


首先要相信技术,整个工业界,特别是各种高科技大公司,对技术坚定的、不动摇的信念特别重要。盖茨提到微软公司的宗旨就是:写软件代表的是世界的将来。为什么?未来任何一个工业都会变成软件工业,因为任何工业任何行业自动化的程度会越来越高,最后你所处理的就是信息和知识。


但现在软件的做法又往前提了一次,因为在人工智能时代,不光是写代码,你必须懂算法,懂硬件,懂数据,整个人工智能的开发过程有一个很大程度的提高,但是,技术,特别是我们这个工业所代表的技术一定是将来任何工业的前沿。


所以,我们一定要有一个坚定不移的深刻的理念,相信整个世界终究是为技术所驱动的


2. 站在巨人的肩膀上做创新


在美国硅谷、在中国,互联网创业公司也好,大型公司也好,大家的起点是越来越高的。为什么现在创新速度那么快?主要是起点高了。我们可以使用的代码模块,使用的服务的能力,都是大大的提升。


所以每一次你写一行新的代码,第一要做的,先想一想你这行代码值得不值得写,是不是有人已经做了同样的工作,可能做得比你还好一点。有没有其他人已经解决这个问题,然后你可以把你的时间放在更好的创新上。特别是大公司里面重复或者是几乎重复的 Code 实在太多,浪费太多的资源,对每个人的职业生涯都不是好事情。


在大公司内部,你写代码之前想一想,你这行代码要不要写,是不是别人已经有了,站在别人的肩膀上去做这件事情


3. 追求Engineering Excellence


Engineering Excellence,也就是工程的技术的卓越性和能力。


任何市场上竞争就像打仗一样,就看你的部队体能、质量,每一个士兵他的训练的程度,和你给他使机关枪、坦克,还是什么样的武器。


Engineering Excellence 是一个永无止境的、个人的、团队的,能力的追求和工具平台的创新,综合在一起可以给我们带来的长期的、核心的竞争力,为社会创造价值,最终的目的是给每个用户、每个企业、整个社会创造价值。


我们要么不做,要做的事情一定做最好,这是我对大家的要求。数据库也好,做大平台也好,大数据也好,我们要做什么事情,我们一定要下决心,这是我对你们每个人的要求,做什么事情一定要做最好,一定要是做业界最强的。


4. 每天学习


每天学习,可能是对每个人都是最最重要的。


每个人可以把自己想象是一个软件、一个代码,今天的版本一定要比昨天版本好,明天的版本肯定会比今天好,因为即使犯了错误,里面有 If statement,说如果见到这个错误,绝对不要再犯。


英文有句话是 Life is too short, don’t live the same day twice. 同样一天不要重活两次。每天都是不一样,每天为什么不一样,因为每天都变成最好,每天都变得更好。今天的版本一定要比昨天好,每个优秀的工程师,杰出的技术领袖,一定要保持自己学习的能力,特别是学习的范围。


做 Computer science 的,如果只学 Computer science,不去学一些其他的行业,肯定不够。举个例子,经济学必须要学。为什么这样讲?Computer science 它有个很大的限制,他是假定你有输入以后有输出,这种解决问题的方式有它的好处,但有它的限制性。


比如做地图导航,如果你纯粹用这个方式去做,你只是把一个拥挤的地方移到另外一个拥挤的地方。经济学,它对问题的建模是不一样的。它起点是假定是一个整体的一个生态,每个人的输入都是另外一个人的输出,你要用经济学的方式来描述地图导航的问题,你就会去算一个 Equilibrium,市场也是这样。


另外,学产品,如果不懂产品,你不可能成为一个最好的工程师。真正要做世界一流的工程师不光要懂产品,还要懂整个商业,懂生态。因为你的工作的责任,是能够看到将来,把技术展望到将来的需求,把平台、把开发流程、把你的团队为将来做准备。所以学习是非常非常重要的。


5. Ownership


最后是一种职场里面的心态,Ownership,就是看到机会不需要问别人,有机会就去做,看到问题也不要去问别人,就把它解决掉。把公司当成我们自己每个人的事业来做,Own everything,你在职业生涯一定是走得最快,从我做起,从身边的每一件事情做起。


总结来说,就是Believe in 技术、站在巨人的肩膀上做创新、追求 Engineering Excellence、每天学习、Take Ownership,陆奇送给每一位工程师的建议,你 get 到了吗?


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

敏捷开发

相对于瀑布模式,敏捷开发会提高产品的交付效率。市场变化太快,我们可以先做MVP,交付出来,看看市场反应。或许才是我们技术人员对于业务的赋能。其实,敏捷开发不一定会加班 996, 以前我对它是有误解的。 下面,我自己写一下这几年工作实践,对于 敏捷开发的新的理解...
继续阅读 »

相对于瀑布模式,敏捷开发会提高产品的交付效率。市场变化太快,我们可以先做MVP,交付出来,看看市场反应。或许才是我们技术人员对于业务的赋能。其实,敏捷开发不一定会加班 996, 以前我对它是有误解的。


下面,我自己写一下这几年工作实践,对于 敏捷开发的新的理解


敏捷理论体系解读


敏捷软件开发宣言:


agilemanifesto.org/iso/zhchs/m…


敏捷软件开发宣言


个体和互动 高于 流程和工具


工作的软件 高于 详尽的文档


客户合作 高于 合同谈判


响应变化 高于 遵循计划


左右都很重要


敏捷软件开发--计划



  • 2-4周 详细计划

  • 1-3个月 粗略计划

  • 1年 大致想法


每年3-4 茶话会,写纸条,写出自己的未来的一年的计划,投票,内部开会,产品线。


敏捷软件开发宣言的原则



  1. 及早交付有价值的软件

  2. 传递信息效果的最好,效率最高的方式: 面对面的交谈


敏捷开发框架 Scrum


三个角色


第一个角色 1. 产品负责人


负责建立和维护产品特性



  1. 和客户不断沟通和协作确定产品应该做什么,定义用户需求,确定需求的优先级

  2. 必须拥有决策权

  3. 要对团队内部的工作流程 和 技术水平有所了解


第二个角色 2. scrum教练


确保团队能够实现更快的交付



  1. 要能激励团队士气,促进团队合作,确保团队有效率

  2. 要能帮助团队排除干扰,确定冲刺目标的顺利进行

  3. 教练不是事无巨细的管理者,而更像是服务团队的仆人


第三个角色 scrum团队


5-9人 团队



  1. 自YOU组织,平等

  2. 在一个 Sprint 里面,成员应该尽量保持稳定


4个活动


4个活动-冲刺规划会议


标识着冲刺的开始(时长一般不超过4个小时)




  1. scrum团队选择并理解要完成的工作




  2. 议程1:由产品负责人介绍产品,确定sprint 将要完成什么任务;明确产品待办列表中优先级最高的任务;明确冲刺目标;确定本轮冲刺中要完成的任务数量;




  3. 议程2: scrum 团队研究本轮冲刺 如何完成要交付的任务; 团队对冲刺要完成的 任务数量 和复杂度达成共识; 对需求充分理解并 进行估算; 将 产品代办列表 中的内容 转化成 软件开发过程中 的具体任务




4个活动-每日站立会议


团队成员在会上轮流发言,回答:



  1. 昨天我做了什么

  2. 今天我准备做什么

  3. 我在工作中遇到了什么苦难,是否阻碍了工作进展

  4. 不建议超过15分钟

  5. 汇报预计今天能完成的进度


4个活动-冲刺复审会议


用来演示在这轮冲刺中开发的产品功能,并由产品负责人验收


4个活动-冲刺回顾会议



  1. 用来给团队定期自我审视

  2. 建议控制在30分钟以内


三个工件-产品待办列表


一份有序的待办事项列表


涵盖产品开发中已知的每一项需求,并且应该是产品需求变动的唯一来源


产品负责人是唯一的产品待办列表的负责人


其他人可以提出意见,但只有产品负责人有权做决定


产品待办列表



  1. 功能性需求 或者 非功能性需求

  2. 缺陷

  3. 技术债务


产品待办列表--评判标准- DEEP原则



  1. 粗细得当: 产品待办列表 应当 远粗近细, 越远的需求应该越粗;越近期要开发的需求应该越细致

  2. 估算过的:对每一个事项有一个工作量估算 和 技术风险 估算

  3. 涌现的:产品待办列表不是一成不变的,而是涌现的

  4. 排好优先级别的


冲刺待办列表


开发团队估算工作量,识别冲刺待办列表优先级等等


敏捷 vs DevOps


DevOps教程:DevOps vs 敏捷


http://www.cnblogs.com/icodewalker…


DevOps 与敏捷


azure.microsoft.com/zh-cn/overv…


DevOps知识图谱


DevOps知识图谱


roadmap.sh/devops


案例分享:阿里是如何DevOps的


阿里云 云效


http://www.aliyun.com/product/yun…


Jenkins BlueOcean插件


zhuanlan.zhihu.com/p/70355846


案例分享:Amazon是如何DevOps的


亚马逊 基于AWS的DevOps实践指南



AWS视频中心


aws.amazon.bokecc.com/searchresul…


一站式DevOps平台-Hygieia


Hygieia搭建


手动搭建:



基于Docker搭建:



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

Bundle源码解析

Bundle源码解析 做一个调用系统分享json的时候遇到一个问题,在用Bundle传递String太大的时候会报错,所以可以计算String的大小,size小的时候传String,size大的时候可以把String存文件然后分享文件。但是问题来了,这个大小的...
继续阅读 »

Bundle源码解析


做一个调用系统分享json的时候遇到一个问题,在用Bundle传递String太大的时候会报错,所以可以计算String的大小,size小的时候传String,size大的时候可以把String存文件然后分享文件。但是问题来了,这个大小的边界在哪儿呢?到底传多大的数据才会报错呢?

我们先看一个报错的错误栈:

Caused by: android.os.TransactionTooLargeException: data parcel size 1049076 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:1129)
at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:3754)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1671)
at android.app.Activity.startActivityForResult(Activity.java:4586) 
at android.app.Activity.startActivityForResult(Activity.java:4544) 
at android.app.Activity.startActivity(Activity.java:4905) 
at android.app.Activity.startActivity(Activity.java:4873)

跟着各种startActivity追溯上去,BinderProxy#transact方法里面调用transactNative方法,这是个native方法,我们去往androidxref网站查看。追查到/frameworks/base/core/jni/android_util_Binder.cpp里面android_os_BinderProxy_transact方法,最后调用signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());方法771行,看到:


TransTooBig.png


发现把parcelSize限制在了200K的大小,当大于200K的时候就会报错。


那到底Bundle扮演了什么角色呢?我们从一个最简单的场景看起:


使用方法
//putExtra
public static void startActivity(Context context) {
Intent intent = new Intent(context, TestActivity.class);
intent.putExtra("KEY", 1);
context.startActivity(intent);
}

//使用getStringExtra获取
String msg = getIntent().getStringExtra(KEY);

几乎是最简单的StartActivity场景,我们分步来看这几句代码都做了什么:


数据的写入 - Intent#putExtra

Intent#putExtra实际上是调用了Bundle#putExtra:

//Intent.java
public @NonNull Intent putExtra(String name, int value) {
if (mExtras == null) {
mExtras = new Bundle();
}
mExtras.putInt(name, value);
return this;
}

//BaseBundle.java
BaseBundle() {
this((ClassLoader) null, 0);
}
BaseBundle(@Nullable ClassLoader loader, int capacity) {
mMap = capacity > 0 ? new ArrayMap<String, Object>(capacity) : new ArrayMap<String, Object>();
//初始化了一个叫mMap的空ArrayMap,同时初始化了一个mClassLoader,用来实例化Bundle里面的对象。
mClassLoader = loader == null ? getClass().getClassLoader() : loader;
}

public void putInt(@Nullable String key, int value) {
unparcel();
mMap.put(key, value);
}

mMap是用来存储我们需要传递的Kay-Value的,在putInt的方法里面调了一个unparcel()方法,然后往mMap里面put了一个value,看起来很简单,其实我们可以看到在所有的putXXX方法里面都是先调用了一个unparcel()再执行了put方法,其实我们可以从名字和逻辑猜出来这个方法是做什么的,其实就是在put之前如果已经有了序列化的数据,需要先反序列化填到eMap里面,再尝试去添加新的数据。

带着这个猜测我们来看unparcel()方法

	//BaseBundle.java
void unparcel() {
synchronized (this) {
final Parcel source = mParcelledData;
if (source != null) {//parcelledData不为空的时候会走initializeFromParcelLocked。mParcelledData什么时候赋值呢?在后面初始化的时候能看到。
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
} else {
...
}
}
}
//把data从NativeData中读取出来,save到mMap中。
private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel, boolean parcelledByNative) {
...
final int count = parcelledData.readInt();//拿到count,也就是size,是write的时候第一个写入的。
if (count < 0) {
return;
}
ArrayMap<String, Object> map = mMap;
if (map == null) {
map = new ArrayMap<>(count);//根据拿到的count初始化map
} else {
map.erase();
map.ensureCapacity(count);
}
try {
if (parcelledByNative) {
parcelledData.readArrayMapSafelyInternal(map, count, mClassLoader);
} else {
parcelledData.readArrayMapInternal(map, count, mClassLoader);
}
} catch (BadParcelableException e) {
...
} finally {
mMap = map;
if (recycleParcel) {
recycleParcel(parcelledData);
}
mParcelledData = null;
mParcelledByNative = false;
}
}
//Parcel.java
void readArrayMapSafelyInternal(@NonNull ArrayMap outVal, int N, @Nullable ClassLoader loader) {
while (N > 0) {
String key = readString();
Object value = readValue(loader);
outVal.put(key, value);
N--;
}
}

public final Object rea(@Nullable ClassLoader loader) {
int type = readInt();
switch (type) {
...
case VAL_INTEGER:
return readInt();
...
}

public final int readInt() {
return nativeReadInt(mNativePtr);
}
private static native int nativeReadInt(long nativePtr);

调用到native方法里面,我们可以到androidxref网站查看相关源码,下面的代码基于Android 9.0

// /frameworks/base/core/jni/android_os_Parcel.cpp
788 {"nativeReadInt", "(J)I", (void*)android_os_Parcel_readInt},

// /frameworks/base/core/jni/android_os_Parcel.cpp
static jint android_os_Parcel_readInt(jlong nativePtr)
402{
403 Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
404 if (parcel != NULL) {
405 return parcel->readInt32();
406 }
407 return 0;
408}

// /frameworks/native/libs/binder/Parcel.cpp
int32_t Parcel::readInt32() const
1822{
1823 return readAligned<int32_t>();
1824}

// /frameworks/native/libs/binder/Parcel.cpp
template<class T>
T Parcel::readAligned() const {
1606 T result;
1607 if (readAligned(&result) != NO_ERROR) {
1608 result = 0;
1609 }
1610
1611 return result;
1612}
/frameworks/base/core/jni/android_os_Parcel#android_os_Parcel_readInt(jlong nativePtr)
- frameworks/native/libs/binder/Parcel#Parcel::readInt32()

// /frameworks/native/libs/binder/Parcel.cpp
template<class T>
status_t Parcel::readAligned(T *pArg) const {
1583 COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
1584
1585 if ((mDataPos+sizeof(T)) <= mDataSize) {//越界检查
1586 if (mObjectsSize > 0) {
1587 status_t err = validateReadData(mDataPos + sizeof(T));//检测是否可以读取到这么多数据
1588 if(err != NO_ERROR) {
1590 mDataPos += sizeof(T);
1591 return err;
1592 }
1593 }
1594
1595 const void* data = mData+mDataPos;//指针偏移,指向期待的数据地址。
1596 mDataPos += sizeof(T);//数据偏移量增加。
1597 *pArg = *reinterpret_cast<const T*>(data);//指针强转赋值,读取到对应的数据
1598 return NO_ERROR;
1599 } else {
1600 return NOT_ENOUGH_DATA;
1601 }
1602}

最终都是调用到Parcel的方法,根据count循环的读取KEY/VALUE,先读key,再根据value的类型读value,我们可以看到最终都是调用了nativeReadXxx方法去读取对应的值。


Parcel的读取大致是首先我们init的时候会拿到一个native给我们的地址值nativePtr,java拿到这个地址值用一个long存储起来,long是64位的,所以用来存储32/64位的机器的地址就都够用了,相当于java调用native去申请了一块内存,并且java可以随时随地的访问到这块内存地址,再根据偏移量就可以拿到这块内存上存储的内容。
我们知道为了方便读取之类的原因,Native里面都是要做内存对齐的,所以Parcel的存储都是最少为4个字节,So,存储一个byte和一个int都会占用4个字节。


所有的存储和读取都是在native层面进行的,只需要得到偏移量就能够访问,毫无疑问这种处理是高效的。

但是问题就是我们无法得知下一个是Int还是Long,所以需要严格保证顺序,序列化和反序列化要保证顺序。
关于Parcel推荐看(关于Parcel),深入浅出。


所以,Intent#putExtra其实就是调用了bundle#putExtra,先检查是否有unparcel的数据,有就先读取出来,一个流程图如下:


readInt.png


上面是写一个数据的代码,我们知道Intent实现了Parcelable接口,所以Parcelable的写入接口方法就是writeToParcel方法,最终调用了BaseBundle的writeToParcelInner.

//Intent.java
public void writeToParcel(Parcel out, int flags) {
...
out.writeBundle(mExtras);
}

//Parcel.java
public final void writeBundle(@Nullable Bundle val) {
if (val == null) {
writeInt(-1);
return;
}

val.writeToParcel(this, 0);
}

public void writeToParcel(Parcel parcel, int flags) {
...
super.writeToParcelInner(parcel, flags);
...
}

//BaseBundle.java
void writeToParcelInner(Parcel parcel, int flags) {
// 如果parcal自己set了一个ReadWriteHelper,先调用unparcel,mMap赋值,mParcelledData为null
if (parcel.hasReadWriteHelper()) {
unparcel();
}
final ArrayMap<String, Object> map;
synchronized (this) {
if (mParcelledData != null) {
if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) {
parcel.writeInt(0);
} else {
int length = mParcelledData.dataSize();
parcel.writeInt(length);
parcel.writeInt(mParcelledByNative ? BUNDLE_MAGIC_NATIVE : BUNDLE_MAGIC);
parcel.appendFrom(mParcelledData, 0, length);
}
return;
}
map = mMap;
}

// 如果map为空,直接写入length = 0
if (map == null || map.size() <= 0) {
parcel.writeInt(0);
return;
}
int lengthPos = parcel.dataPosition();//先记录开始的位置
parcel.writeInt(-1); // dummy, will hold length 写入-1占length的位置
parcel.writeInt(BUNDLE_MAGIC); //写入MAGIC CODE

int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(map);
int endPos = parcel.dataPosition();

// 表示游标回到之前记录的开始位置
parcel.setDataPosition(lengthPos);
int length = endPos - startPos;
parcel.writeInt(length); //写入length
parcel.setDataPosition(endPos); //aprcel回到结束位置。
}

Parcel的写入是按照顺序先写入data的length,再写入一个MAGIC,然后写入真正的data。当然,读取的时候也是按照这个顺序来读取的。mParcelledData是存储的Parcel格式的Bundle的,如果mParcelledData不为空,那么mMap一定是空的。如果data被unparcel出来了,那么mMap有数据mParcelledData为空。
所以,写入的时候发现mParcelledData不为空,直接把mParcelledData写入parcel就好了,否则调用writeXXX把mMap写入parcel。



parcel的dataPosition()表示当前在写的位置,类似写文件吧,seek到不同的位置开始写对应位置的数据。默认获取到的dataPosition()是在数据的最后的位置。



写入的时候有个小Trick,这里写入的时候先记录一个开始的位置lengthPos,先写一个-1代表length,再写MAGIC CODE,然后开始写数据并且记录开始结束位置startPosendPos,得到数据的length之后再seek到-1的位置用length把-1覆盖掉再seek到结束位置。


写入逻辑结束。


数据读取 - getIntent().getStringExtra

数据读取,直接调用到了Bundle的getString方法,除了调用了unparcel,上面我们看了unparcel的代码,作用就是给mMap赋值,反序列化之后就是直接从mMap中取数据了。

//Intent.java
public @Nullable String getStringExtra(String name) {
return mExtras == null ? null : mExtras.getString(name);
}

//BaseBundle.java
public String getString(@Nullable String key) {
unparcel();
final Object o = mMap.get(key);
try {
return (String) o;
} catch (ClassCastException e) {
typeWarning(key, o, "String", e);
return null;
}
}

void unparcel() {
synchronized (this) {
final Parcel source = mParcelledData;//这里我们看下上面没看到的条件,mParcelledData什么时候赋值呢?
if (source != null) {
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
} else {
...
}
}
}

那mMap是什么时候赋值的呢?我们知道Bundle是实现了Parcelable接口的,需要实现序列化反序列化方法,我们在Intent里能找到CREATOR方法:

//Intent.java
public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
= new Parcelable.Creator<Intent>() {
public Intent createFromParcel(Parcel in) {
return new Intent(in);
}
public Intent[] newArray(int size) {
return new Intent[size];
}
};

反序列化的调用顺序,代码逻辑很简单,都略过,只看调用逻辑:

Intent#Intent(Parcel in)
- Intent#readFromParcel(Parcel in)
- Parcel#readBundle()
- Parcel#readBundle(ClassLoader loader)
- Bundle#Bundle(Parcel parcelledData, int length)
- BaseBundle#BaseBundle(Parcel parcelledData, int length)
- BaseBundle#readFromParcelInner(Parcel parcel, int length)

//调用到readFromParcelInner方法,看一下这个方法做了什么。

//BaseBundle.java
void readFromParcelInner(Parcel parcel) {
int length = parcel.readInt();//先读了一个length
readFromParcelInner(parcel, length);
}
//BaseBundle.java
private void readFromParcelInner(Parcel parcel, int length) {
...
final int magic = parcel.readInt();
final boolean isJavaBundle = magic == BUNDLE_MAGIC;
final boolean isNativeBundle = magic == BUNDLE_MAGIC_NATIVE;
if (!isJavaBundle && !isNativeBundle) {
throw new IllegalStateException("Bad magic number for Bundle: 0x" + Integer.toHexString(magic));
}

if (parcel.hasReadWriteHelper()) {
synchronized (this) {
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);
}
return;
}

// Advance within this Parcel
int offset = parcel.dataPosition();
parcel.setDataPosition(MathUtils.addOrThrow(offset, length));

Parcel p = Parcel.obtain();
p.setDataPosition(0);
p.appendFrom(parcel, offset, length);
p.adoptClassCookies(parcel);
p.setDataPosition(0);

mParcelledData = p;
mParcelledByNative = isNativeBundle;
}

So,结合我们上面看过的源码,Bundle实际数据的存储有两种形式,一种是作为Parcel存储在mParcelledData里面,一种是作为K-V存储在mMap里面。区别是parcel.hasReadWriteHelper(),有没有给Parcel设置ReadWriteHelper



  1. 如果设置了,实际逻辑是调用了initializeFromParcelLocked,跟上面我们看过的unparcel方法调用的一致,把数据unparcel出来放到mMap中。

  2. 如果没设置,obtain一个可用的Parcel,把数据从传过来的parcel中放进去,赋值给mParcelledData


后面如果有putXXX操作的时候再通过unparcel方法反序列化到mMap中使用。


总结:



  • Bundle里面最多能传递200K的数据。

  • 调用putExtra的时候K-V数据需要放到mMap中

    • 如果Bundle是新new出来的,初始化一个mMap,K-V直接放到mMap中。

    • 如果Bundle之前就存在,readFromParcelInner方法中反序列化,结束后数据有两种方式存储,一种是K-V结构存储在mMap中,一种是以parcel结构存储在mParcelledData中。如果还需要putExtra,如果mMap中有值,直接put到mMap中,否则调用unparcelmParcelledData反序列化到mMap中再put到mMap中。



  • Bundle的存和读都是调用的Parcel的WriteXXX/ReadXXX方法,都会调用到nativeWriteXXX/nativeReadXXX,native中也有一个Parcel与java中的对应。类似于DexClassLoaderDexFile的处理,也是java持有一个native申请的地址指针,读写的时候都是用这个指针去操作。

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

热榜排行爬虫详解

作为一个爬虫必须摸不一样的鱼,平时大家怎么看热榜,今天爬一个热榜数据,咱就在ied中读热榜。还是一个乌龙事件听我细细道来网站地址 1,话不多说,今天图也不看了直接进入主题,打开网站首页抓个包 2,可见接口中有两个参数,第二个参数盲猜是请求时候的时间戳,第一个...
继续阅读 »

作为一个爬虫必须摸不一样的鱼,平时大家怎么看热榜,今天爬一个热榜数据,咱就在ied中读热榜。还是一个乌龙事件听我细细道来网站地址


1,话不多说,今天图也不看了直接进入主题,打开网站首页抓个包


image.png
2,可见接口中有两个参数,第二个参数盲猜是请求时候的时间戳,第一个参数有点长不像是正常的时间戳,多翻几页发现第一个参数也是一个时间戳只是后面加了三个000,咱们就去掉三个零(这里多请求了几页没有发现翻页的变化规律)


image.png
2.1,第二个参数转换发现就是请求时间没错


image.png
2.2,第一个参数转换瞬间我这充满智慧的大脑里出现了无数想法(这是随机的)(这是文章发布时间)(文章发布时间放到翻页怎么获取呢)(这是网站反爬生成一堆时间戳映射到page上做翻页)(这是通过算法和请求时间做比较生成对应的page进行翻页)(。。。)然后我就去了后台看看查一下这个参数名


image.png


image.png
只一眼,并带着对这种小网站的看不起,直接到response中一检索果然。小网站哪有什么高端反爬


image.png
3,直接上代码,翻页就不再多谢,拿到参数之后可以自己向下补充了

headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Cookie': 'deviceId=web.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqaWQiOiJhNjNkZjYxZS00ODZhLTQzNTgtODNmMy1hNDlkMjdkMmI4ZmUiLCJleHBpcmUiOiIxNjY1MjIyMzY3MDAwIn0.eQF9za4cSq8huEESJPn0nDP3PUsDiVNZ4CM_fTAeWMg; Hm_lvt_03b2668f8e8699e91d479d62bc7630f1=1662630378',
'Pragma': 'no-cache',
'Referer': 'https://dig.chouti.com/',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
}
import time
params = {
'afterTime': '1681236005077000',
'_': f'{int(time.time()*1000)}',
}

response = requests.get('https://dig.chouti.com/link/hot', params=params, headers=headers).json()['data']
next_afterTime = response[-1]['operateTime']
print(next_afterTime)
for res in response:
title = res['title']
url = res['url']
print(title)
yes = input()
if yes == '1':
print(url)

4,ok这样的话就只需要看到想看的题目就输入1返回url,就可以自行观赏了,跑一下


VeryCapture_20230412162714.gif


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

31岁,还可以拼一拼大厂么?

大家好呀,我是楼仔。 上周有一个粉丝问我:楼哥,我刚学习 Java,可以在半年内达到入门初级所有的要求么? 年前还和一位百度的前同事聊天,他目前在阿里,职级对标阿里 P7,年薪 120W,这个算是相当高的,因为 P7 一般在 80W 左右,就算是 P6 的级别...
继续阅读 »

大家好呀,我是楼仔。


上周有一个粉丝问我:楼哥,我刚学习 Java,可以在半年内达到入门初级所有的要求么?


年前还和一位百度的前同事聊天,他目前在阿里,职级对标阿里 P7,年薪 120W,这个算是相当高的,因为 P7 一般在 80W 左右,就算是 P6 的级别,年薪也有 50W。



大厂,薪资高,技术强,这也是很多同学想去大厂的原因,那 30 岁之后,还有必要去拼大厂么?


粉丝提问


半路转行的,水了五六年经验了,小公司,没大佬级别人物,瞎忙,基本就是做一些公司业务之类的,公司也不注重技术,项目短快,能挣钱就行,发现到现在是啥都不会。


本来在现在这家公司就是打算往管理岗转的,现在有个问题就是领导的一些作风和思想不是很认可,所以才想着跳了。


最近准备跳了,在看你整理的面试题,想找一个不太忙的公司,好好研究研究技术,来年拼一拼大厂,大佬给点建议呗。


楼仔回答


我觉得这个需要根据家庭情况来考虑。


如果家庭情况还好,目前的薪资能承受家庭的经济压力,就不建议再去拼大厂,因为去大厂,工资确实会高和很多,但是到了 35-40 岁之后,危机会很严重。


如果家庭情况不太好,或者正缺钱,那可以去拼一下,如果不是公司需要裁员,干到 35 岁肯定是没问题的,如果技术也打磨得还不错,即使没有混到管理层,也可以干到 40 岁。


这个回答就非常现实了,程序员本来就是吃青春饭的,如果不转管理层,或者混到专家、架构师级别,很容易就到年龄天花板。


总结一下:



  • 30岁之前,强烈建议去大厂,多挣些钱,发展更好;

  • 30岁之后,相对要计划职业的长久发展,如果专家和管理层拼不上去,建议去国企相对稳定的公司。


听了我的一番话,这位球友又说了他的其它想法,考个在职博士,完了找个能长久点的工作。


我就直接问他“你确定考了在职博士,能找到长久点的工作么?”



我给的建议是,先确定你要去哪家单位,如果这家单位需要博士学历,你再去考,而不是先去考个博士学历,然后再去看能找哪些单位,方向反了。





看得出来,我的建议是真的帮助到他了。


写在最后


其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。


比如我星球中有的球友,连一些基本的后端技术栈都没有掌握好,就去卷源码,卷各种高大尚的技术,就是典型的方向不对。


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

10 个有用的 Kotlin flow 操作符

Kotlin 拥有函数式编程的能力,运用得当,可以简化代码,层次清晰,利于阅读,用过都说好。 然而操作符数量众多,根据使用场景选择合适的操作符是一个很大的难题,网上搜索了许久只是学习了一个操作符,还要再去验证它,实在浪费时间,开发效率低下。 一种方式是转至命令...
继续阅读 »

Kotlin 拥有函数式编程的能力,运用得当,可以简化代码,层次清晰,利于阅读,用过都说好。


然而操作符数量众多,根据使用场景选择合适的操作符是一个很大的难题,网上搜索了许久只是学习了一个操作符,还要再去验证它,实在浪费时间,开发效率低下。


一种方式是转至命令式开发,将过程按步骤实现,即可完成需求;另一种方式便是一次学习多个操作符,在下次选择操作符时,增加更多选择。


本篇文章当然关注的是后者,列举一些比较常用 Kotlin 操作符,为提高效率助力,文中有图有代码有讲解,文末有参考资料和一个交互式学习的网站。


让我们开始吧。


1. reduce


reduce 操作符可以将所有数据累加(加减乘除)得到一个结果


Kotlin_操作符.002.jpeg

listOf(1, 2, 3).reduce { a, b ->
a + b
}

输出:6

如果 flow 中没有数据,将抛出异常。如不希望抛异常,可使用 reduceOrNull 方法。


reduce 操作符不能变换数据类型。比如,Int 集合的结果不能转换成 String 结果。


2. fold


fold 和 reduce 很类似,但是 fold 可以变换数据类型


Kotlin_操作符.003.jpeg


有时候,我们不需要一个结果值,而是需要继续操 flow,可使用 runningFold 。

flowOf(1, 2, 3).runningFold("a") { a, b ->
a + b
}.collect {
println(it)
}

输出:
a
a1
a12
a123

同样的,reduce 也有类似的方法 runningReduce

flowOf(1, 2, 3).runningReduce { a, b ->
a + b
}.collect {
println(it)
}

输出:
1
3
6

3. debounce


debounce 需要传递一个毫秒值参数,功能是:只有达到指定时间后才发出数据,最后一个数据一定会发出


Kotlin_操作符.004.jpeg


例如,定义 1000 毫秒,也就是 1 秒,被观察者发出数据,1秒后,观察者收到数据,如果 1 秒内多次发出数据,则重置计算时间。

flow {
emit(1)
delay(590)
emit(2)
delay(590)
emit(3)
delay(1010)
emit(4)
delay(1010)
}.debounce(
1000
).collect {
println(it)
}

输出结果:
3
4

rebounce 的应用场景是限流功能


4. sample


sample 和 debounce 很像,功能是:在规定时间内,只发送一个数据


Kotlin_操作符.005.jpeg

flow {
repeat(4) {
emit(it)
delay(50)
}
}.sample(100).collect {
println(it)
}

输出结果:
1
3

sample 的应用场景是截流功能


debounce 和 sample 的限流和截流功能已有网友实现,点击这里


5. flatmapMerge


简单的说就是获得两个 flow 的乘积或全排列,合并并且平铺,发出一个 flow。


描述的仍然不好理解,一看代码便能理解了


Kotlin_操作符.006.jpeg

flowOf(1, 3).flatMapMerge {
flowOf("$it a", "$it b")
}.collect {
println(it)
}

输出结果:
1 a
1 b
3 a
3 b

flatmapMerge 还有一个特性,在下一个操作符里提及。


6. flatmapConcat


先看代码。


Kotlin_操作符.007.jpeg

flowOf(1, 3).flatMapConcat {
flowOf("a", "b", "c")
}.collect {
println(it)
}

功能和 flatmapMerge 一致,不同的是 flatmapMerge 可以设置并发量,可以理解为 flatmapMerge 是线程安全的,而 flatmapConcat 不是线程安全的。


本质上,在 flatmapMerge 的并发参数设置为 1 时,和 flatmapConcat 基本一致,而并发参数大于 1 时,采用 channel 的方式发出数据,具体内容请参阅源码。


7. buffer


介绍 buffer 的时候,先要看这样一段代码。

flowOf("A", "B", "C", "D")
.onEach {
println("1 $it")
}
.collect { println("2 $it") }

输出结果:
1 A
2 A
1 B
2 B
1 C
2 C
1 D
2 D

注意输出的内容。


加上 buffer 的代码。

flowOf("A", "B", "C", "D")
.onEach {
println("1 $it")
}
.buffer()
.collect { println("2 $it") }

输出结果:
1 A
1 B
1 C
1 D
2 A
2 B
2 C
2 D

输出内容有所不同,buffer 操作符可以改变收发顺序,像有一个容器作为缓冲似的,在容器满了或结束时,下游开始接到数据,onEach 添加延迟,效果更明显。


8. combine


合并两个 flow,长的一方会持续接受到短的一方的最后一个数据,直到结束


Kotlin_操作符.008.jpeg

flowOf(1, 3).combine(
flowOf("a", "b", "c")
) { a, b -> b + a }
.collect {
println(it)
}

输出结果:
a1
b3
c3

9. zip


也是合并两个 flow,结果长度与短的 flow 一致,很像木桶原理。
Kotlin_操作符.009.jpeg

flowOf(1, 3).zip(
flowOf("a", "b", "c")
) { a, b -> b + a }
.collect {
println(it)
}

输出结果:
a1
b3

10. distinctUntilChanged


就像方法名写的那样,和前一个数据不同,才能收到,和前一个数据想通,会被过滤掉。


Kotlin_操作符.010.jpeg

flowOf(1, 1, 2, 2, 3, 1).distinctUntilChanged().collect {
println(it)
}

输出结果:
1
2
3
1

最后


以上就是今天要介绍的操作符,希望对大家有所帮助。


参考文章:Android — 9 Useful Kotlin Flow Operators You Need to Know


Kotlin 交互式操作符网站:交互式操作符网站


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

使用 Kotlin Compose Desktop 实现了一个简易的"手机助手"

一. adbd-connector adbd-connector 是一个实现 adb server 和 adb daemon 之间的通信协议的库,使用 Kotlin 编写。支持 PC 端直接连接 Android 设备操作 adb 相关的指令。 github 地...
继续阅读 »

一. adbd-connector


adbd-connector 是一个实现 adb server 和 adb daemon 之间的通信协议的库,使用 Kotlin 编写。支持 PC 端直接连接 Android 设备操作 adb 相关的指令。


github 地址:github.com/fengzhizi71…


二. 背景


在下图中的 adb client 和 adb server 都共存在 PC 中,PC 上安装过 adb 程序就会有。adb dameon (简称adbd)是存在于 Android 手机中的一个进程/服务。


当我们启动命令行输入 adb 命令时,实际上使用的是 adb client,它会跟 PC 本地的 adb server 进行通信(当然,有一个前提先要使用 adb-start 启动 adb server 才能调用 adb 命令)。


然后 adb server 会跟 Android 手机进行通信(手机通过 usb 口连上电脑)。最终,我们会看到 adb client 返回命令执行的结果。


一次命令的执行包含了 1-6 个步骤。其实,还要算上 adb server 内部的通信和 adb dameon 内部的通信。一次命令的执行,路径会很长。


所以,一次完整的 adb 通信会涉及如下的概念:



  • adb client:运行在 PC 上,通过在命令行执行 adb,就启动了 adb client 程序

  • adb server:运行于 PC 的后台进程,用于管理 adb client 和 daemon 之间的通信

  • adb daemon(adbd):运行在模拟器或 Android 设备上的后台服务。当 Android 系统启动时,由 init 程序启动 adbd。如果 adbd 挂了,则 adbd 会由 init 重新启动。

  • DDMS:DDMS 将 IDE 和手机设备之间建立起了一座桥梁,可以很方面的查看到目标机器上的信息。

  • JDWP:即 java debug wire protocol,Java 调试线协议,是一个为 Java 调试而设计的通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。


adb-connector.png


在 adb server 和 adbd 之间有一个 TCP 的传输协议,它定义在 Android 源码的 system/core/adb/protocol.txt 文件中。只要是能通过 adb 命令连接的手机,都会遵循这个协议,无论是 Android 或是鸿蒙系统。


因此,基于这个协议实现了一个 TCP 的客户端(adbd-connector)就可以跟手机的 adbd 服务/进程进行通信,从而实现 adb 的所有指令。


另外,我还使用 Kotlin Compose Desktop 在这个协议上做了一层 UI,实现了一个可以在 PC 上使用的简易"手机助手",且支持 Mac、Linux、Windows 等系统。



在手机连接前,先要打开手机的开发者模式。在连接过程中,手机会弹出信任框,提示是否允许 usb 调试。需要点击信任,才能完成后续的连接。
还要打开手机的 5555 端口(使用 adb 命令:adb tcpip 5555),以及获取手机连接当前 wifi 的局域网 ip 地址。有了局域网的 ip 地址和端口,才可以通过 adbd-connector 跟 adbd 进行连接。



三. adbd-connector 使用


3.1 手机的连接效果:


连上手机,获取手机的基础信息


1.png


3.2 执行 adb shell 命令的效果:


执行 adb shell 相关的命令(输入时省略 adb shell,直接输入后面的命令)


2.png


3.png


3.3 install app


目前还处在疫情期间,所以就在应用宝上找了一个跟生活相关的 App,最主要是这个 App 体积小


安装 App 时分成2步:


1.使用 push 命令将 apk 推送到手机的目录 /data/local/tmp/ 下


4.png



  1. 使用 adb shell pm install 命令安装 apk


5.png


6.png


3.4 uninstall app


adb shell pm uninstall 包名


7.png


四. 总结


这款工具 github.com/fengzhizi71… 是一个 PoC(Proof of Concept)的产物,参考了很多开源项目,特别是 github.com/sunshinex/a…


它能够实现绝大多数的 adb 命令。后续这个项目的部分代码可能会用于公司的项目。所以,这个仓库不一定会持续更新了。而且,这款工具使用起来也很繁琐,需要打开手机的 5555 端口以及输入手机局域网 ip 的地址。因此在实际业务中,还有很多东西需要改造以适合自身的业务。


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

骨灰级程序员那些年曾经告诉我们的高效学习的态度

一、背景 近日听闻某骨灰级程序员突然与世长辞,记得再2019年的时候有幸读到他的专栏,专栏中关于如何高效学习的总结让我收货颇丰,老师说: 学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。 大部分人都认为自己爱学习...
继续阅读 »

一、背景


近日听闻某骨灰级程序员突然与世长辞,记得再2019年的时候有幸读到他的专栏,专栏中关于如何高效学习的总结让我收货颇丰,老师说:


学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。


大部分人都认为自己爱学习,但是:



  • 他们都是只有意识没有行动,他们是动力不足的人。

  • 他们都不知道自己该学什么,他们缺乏方向和目标。

  • 他们都不具备自主学习的能力,没有正确的方法和技能。

  • 更要命的是,他们缺乏实践和坚持。


对于学习首先需要做的是端正态度,如果不能自律,不能坚持,不能举一反三,不能不断追问等,那么,无论有多好的方法,你都不可能学好。所以,有正确的态度很重要。



二、 主动学习和被动学习


老师关于主动学习和被动学习的见解也让我们技术人员收货颇丰。


1946 年,美国学者埃德加·戴尔(Edgar Dale)提出了「学习金字塔」(Cone of Learning)的理论。之后,美国缅因州国家训练实验室也做了相同的实验,并发布了「学习金字塔」报告。



人的学习分为「被动学习」和「主动学习」两个层次。



  • 被动学习:如听讲、阅读、视听、演示,学习内容的平均留存率为 5%、10%、20% 和 30%。

  • 主动学习:如通过讨论、实践、教授给他人,会将原来被动学习的内容留存率从 5% 提升到 50%、75% 和 90%。


这个模型很好地展示了不同学习深度和层次之间的对比。


我们可以看到,你听别人讲,或是自己看书,或是让别人演示给你,这些都不能让你真正获得学习能力,因为你是在被别人灌输,在听别人说。


你开始自己思考,开始自己总结和归纳,开始找人交流讨论,开始践行,并开始对外输出,你才会掌握到真正的学习能力。


所以,学习不是努力读更多的书,盲目追求阅读的速度和数量,这会让人产生低层次的勤奋和成长的感觉,这只是在使蛮力。要思辨,要践行,要总结和归纳,否则,你只是在机械地重复某件事,而不会有质的成长的。



好多书中也这么说:



这里推荐阅读下这本书:



三、深度学习和浅度学习


对于当前这个社会:



  • 大多数人的信息渠道都被微信朋友圈、微博、知乎、今日头条、抖音占据着。这些信息渠道中有营养的信息少之又少。

  • 大多数公司都是实行类似于 996 这样的加班文化,在透支和消耗着下一代年轻人,让他们成长不起来。

  • 因为国内互联网访问不通畅,加上英文水平受限,所以,大多数人根本没法获取到国外的第一手信息。

  • 快餐文化盛行,绝大多数人都急于速成,心态比较浮燥,对事物不求甚解。


所以,在这种环境下,你根本不需要努力的。你只需要踏实一点,像以前那样看书,看英文资料,你只需要正常学习,根本不用努力,就可以超过你身边的绝大多数人。


在这样一个时代下,种种迹象表明,快速、简单、轻松的方式给人带来的快感更强烈,而高层次的思考、思辨和逻辑则被这些频度高的快餐信息感所弱化。于是,商家们看到了其中的商机,看到了如何在这样的时代里怎么治愈这些人在学习上的焦虑,他们在想方设法地用一些手段推出各种代读、领读和听读类产品,让人们可以在短时间内体会到轻松获取知识的快感,并产生勤奋好学和成长的幻觉。


这些所谓的“快餐文化”可以让你有短暂的满足感,但是无法让你有更深层次的思考和把知识转换成自己的技能的有效路径,因为那些都是需要大量时间和精力的付出,不符合现代人的生活节奏。人们开始在朋友圈、公众号、得到等这样的地方进行学习,导致他们越学越焦虑,越学越浮燥,越学越不会思考。于是,他们成了“什么都懂,但依然过不好这一生”的状态。


只要你注意观察,就会发现,少数的精英人士,他们在训练自己获取知识的能力,他们到源头查看第一手的资料,然后,深度钻研,并通过自己的思考后,生产更好的内容。而绝大部分受众享受轻度学习,消费内容。你有没有发现,在知识的领域也有阶层之分,那些长期在底层知识阶层的人,需要等着高层的人来喂养,他们长期陷于各种谣言和不准确的信息环境中,于是就导致错误或幼稚的认知,并习惯于那些不费劲儿的轻度学习方式,从而一点点地丧失了深度学习的独立思考能力,从而再也没有能力打破知识阶层的限制,被困在认知底层翻不了身。


四、如何深度学习


有4点最关键:



  • 高质量的信息源和第一手的知识。

  • 把知识连成地图,将自己的理解反述出来。

  • 不断地反思和思辨,与不同年龄段的人讨论。

  • 举一反三,并践行之,把知识转换成技能。


从以上4点来说,学习会有三个步骤。


知识采集。信息源是非常重要的,获取信息源头、破解表面信息的内在本质、多方数据印证,是这个步骤的关键。


知识缝合。所谓缝合就是把信息组织起来,成为结构体的知识。这里,连接记忆,逻辑推理,知识梳理是很重要的三部分。


技能转换。通过举一反三、实践和练习,以及传授教导,把知识转化成自己的技能。这种技能可以让你进入更高的阶层。


我觉得这是任何人都是可以做到的,就是看你想不想做了。


五、学习的目的



5.1、学习是为了找到方法


学习不仅仅是为了找到答案,而更是为了找到方法。很多时候,尤其是中国的学生,他们在整个学生时代都喜欢死记硬背,因为他们只有一个 KPI,那就是在考试中取得好成绩,所以,死记硬背或题海战术成了他们的学习习惯。然而,在知识的海洋中,答案太多了,你是记不住那么多答案的。


只有掌握解题的思路和方法,你才算得上拥有解决问题的能力。所有的练习,所有的答案,其实都是在引导你去寻找一种“以不变应万变”的方法或能力。在这种能力下,你不需要知道答案,因为你可以用这种方法很快找到答案,找到解,甚至可以通过这样的方式找到最优解或最优雅的答案。


这就好像,你要去登一座山,一种方法是通过别人修好的路爬上去,一种是通过自己的技能找到路(或是自己修一条路)爬上去。也就是说,需要有路才爬得上山的人,和没有路能造路的人相比,后者的能力就会比前者大得多得多。所以,学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。


5.2、学习是为了找到原理


学习不仅仅是为了知道,而更是为了思考和理解。在学习的过程中,我们不是为了知道某个事的表面是什么,而是要通过表象去探索其内在的本质和原理。真正的学习,从来都不是很轻松的,而是那种你知道得越多,你的问题就会越多,你的问题越多,你就会思考得越多,你思考得越多,你就会越觉得自己知道得越少,于是你就会想要了解更多。如此循环,是这么一种螺旋上升上下求索的状态。


但是,这种循环,会在你理解了某个关键知识点后一下子把所有的知识全部融会贯通,让你赫然开朗,此时的那种感觉是非常美妙而难以言语的。在学习的过程中,我们要不断地问自己,这个技术出现的初衷是什么?是要解决什么样的问题?为什么那个问题要用这种方法解?为什么不能用别的方法解?为什么不能简单一些?


这些问题都会驱使你像一个侦探一样去探索背后的事实和真相,并在不断的思考中一点一点地理解整个事情的内在本质、逻辑和原理。一旦理解和掌握了这些本质的东西,你就会发现,整个复杂多变的世界在变得越来越简单。你就好像找到了所有问题的最终答案似的,一通百通了。


5.3、学习是为了了解自己


学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己。英文中有句话叫:You do not know what you do not know,可以翻译为:你不知道你不知道的东西。也就是说,你永远不会去学习你不知道其存在的东西。就好像你永远 Google 不出来你不知道的事,因为对于你不知道的事,你不知道用什么样的关键词,你不知道关键词,你就找不到你想要的知识。


这个世界上有很多东西是你不知道的,所以,学习可以让你知道自己不知道的东西。只有当我们知道有自己不知道的东西,我们才会知道我们要学什么。所以,我们要多走出去,与不同的人交流,与比自己聪明的人共事,你才会知道自己的短板和缺失,才会反过来审视和分析自己,从而明白如何提升自己。


山外有山,楼外有楼,人活着最怕的就是坐井观天,自以为是。因为这样一来,你的大脑会封闭起来,你会开始不接受新的东西,你的发展也就到了天花板。开拓眼界的目的就是发现自己的不足和上升空间,从而才能让自己成长。


5.4、学习是为了改变自己


学习不仅仅是为了成长,而更是为了改变自己。很多时候,我们觉得学习是为了自己的成长,但是其实,学习是为了改变自己,然后才能获得成长。为什么这么说呢?我们知道,人都是有直觉的,但如果人的直觉真的靠谱,那么我们就不需要学习了。而学习就是为了告诉我们,我们的很多直觉或是思维方式是不对的,不好的,不科学的。


只有做出了改变后,我们才能够获得更好的成长。你可以回顾一下自己的成长经历,哪一次你有质的成长时,不是因为你突然间开窍了,开始用一种更有效率、更科学、更系统的方式做事,然后让你达到了更高的地方。不是吗?当你学习了乘法以后,在很多场景下,就不需要用加法来统计了,你可以使用乘法来数数,效率提升百倍。


当你有一天知道了逻辑中的充要条件或是因果关系后,你会发现使用这样的方式来思考问题时,你比以往更接近问题的真相。学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长。


六、总结


**1、首先,学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,但会让人痛苦,并随时可能找理由放弃。如果你不能克服自己 DNA 中的弱点,不能端正自己的态度,不能自律,不能坚持,不能举一反三,不能不断追问等,那么,无论有多好的方法,你都不可能学好。因此,有正确的态度很重要。

**


2、要拥有正确的学习观念:学习不仅仅是为了找到答案,而更是为了找到方法;学习不仅仅是为了知道,而更是为了思考和理解;学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己;学习不仅仅是为了成长,而更是为了改变自己,改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。


喜欢的分享、转发、收藏、评论、关注哦


作者:爱海贼的无处不在
来源:juejin.cn/post/7233240426312007737
收起阅读 »

世界那么大,我并不想去看看

回家自由职业有一个半月了,这段时间确实过的挺开心的。 虽然收入不稳定,但小册和公众号广告的收入也足够生活。 我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。 一...
继续阅读 »

回家自由职业有一个半月了,这段时间确实过的挺开心的。


虽然收入不稳定,但小册和公众号广告的收入也足够生活。


我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。


一年就是 36500 元。


加上其他的支出,一年生活费差不多 5w 元。


10 年是 50w,40 年就是 200 万元。


而且还有利息,也就是说,我这辈子有 200 万就能正常活到去世了。


我不会结婚,因为我喜欢一个人的状态,看小说、打游戏、自己想一些事情,不用迁就另一个人的感受,可以完全按照自己的想法来生活。


并不是说我现在有 200 万了,我的意思是只要在二三十年内赚到这些就够了。


我在大城市打工,可能一年能赚 50 万,在家里自由职业估计一年也就 20 万,但这也足够了。


而且自由职业能赚到最宝贵的东西:时间。


一方面是我自己的时间,我早上可以晚点起、晚上可以晚点睡、下午困了可以睡个午觉,可以写会东西打一会游戏,不想干的时候可以休息几天。


而且我有大把的时间可以来做自己想做的事情,创造自己的东西。


一方面是陪家人的时间,自从长大之后,明显感觉回家时间很少了,每年和爸妈也就见几天。


前段时间我爸去世,我才发觉我和他最多的回忆还是在小时候在家里住的时候。


我回家这段时间,每天都陪着我妈,一起做饭、吃饭,一起看电视,一起散步,一起经历各种事情。


我买了个投影仪,很好用:



这段时间我们看完了《皓镧传》、《锦绣未央》、《我的前半生》等电视剧,不得不说,和家人一起看电视剧确实很快乐、很上瘾。


再就是我还养了只猫,猫的寿命就十几年,彼此陪伴的时间多一点挺好的:



这些时间值多少钱?没法比较。


回家这段时间我可能接广告多了一点,因为接一个广告能够我好多天的生活费呢。


大家不要太排斥这个,可以忽略。


其实我每次发广告总感觉对不起关注我的人,因为那些广告标题都要求起那种博人眼球的,不让改,就很难受。



小册的话最近在写 Nest.js 的,但不只是 nest。


就像学 java,我们除了学 spring 之外,还要学 mysql、redis、mongodb、rabbitmq、kafka、elasticsearch 等中间件,还有 docker、docker compose、k8s 等容器技术。


学任何别的后端语言或框架,也是这一套,Nest.js 当然也是。


所以我会在 Nest.js 小册里把各种后端中间件都讲一遍,然后会做 5 个全栈项目。


写完估计得 200 节,大概会占据我半年的时间。


这段时间也经历过不爽的事情:





这套房子是我爸还在的时候,他看邻居在青岛买的房子一周涨几十多万,而且我也提到过可能回青岛工作,然后他就非让我妈去买一套。


当时 18 年青岛限购,而即墨刚撤市划区并入青岛,不限购,于是正好赶上房价最高峰买的。


然而后来并没有去住。


这套房子亏了其实不止 100 万。


因为银行定存利息差不多 4%,200 万就是每年 8万利息,5年就是 40万。


但我也看开了,少一百万多一百万对我影响大么?


并不大,我还是每天花那些钱。


相比之下,我爸的去世对我的打击更大,这对我的影响远远大于 100 万。


我对钱没有太大的追求,对很多别人看重的东西也没啥追求。


可能有的人有了钱,有了时间会选择环游中国,环游世界,我想我不会。


我就喜欢宅在家里,写写东西、看看小说、打打游戏,这样活到去世我也不会有遗憾。


我所追求的事情,在我小时候可能是想学医,一直觉得像火影里的纲手那样很酷,或者像大蛇丸那样研究一些东西也很酷。


但近些年了解了学医其实也是按照固定的方法来治病,可能也是同样的东西重复好多年,并不是我想的那样。


人这一辈子能把一件事做好就行。


也就是要找到自己一生的使命,我觉得我找到了:我想写一辈子的技术文章。


据说最高级的快乐有三种来源:自律、爱、创造。


写文章对我来说就很快乐,我想他就是属于创造的那种快乐。


此外,我还想把我的故事写下来,我成长的故事,我和东东的故事,那些或快乐或灰暗的日子,今生我一定会把它们写下来,只是现在时机还不成熟。


世界那么大,我并不想去看看。


我只想安居一隅,照顾好家人,写一辈子的技术文章,也写下自己的故事。


这就是我的平凡之路。


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

快到 35 的"大龄"程序员

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。 我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部...
继续阅读 »

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。


我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部,做前端开发,也是 IMWeb 团队的一员,先后做过腾讯课堂和企鹅辅导业务,2020年正式任命 leader,管理差不多10人左右的前端开发团队;2022年3月,因(cheng)为(xu)某(yuan)些(dou)原(zhi)因(dao),加入虾皮金融产品部,现负责消费贷业务+催收业务的前端开发和管理工作。


我的自我介绍完了,如果大家不想浪费更多时间深入了解我的话,知道以上信息已经足够了,为了大家的脑细胞着想,提供给大家 3 个不用思考的快捷选项:



  1. 对我不感兴趣,可以左上角关闭页面(我可以对天发誓,这绝对不是相亲贴);

  2. 觉得可以交个朋友,给自己保留一个未来有惊喜的可能,可以关注我的公众号或者加我微信;

  3. 还想听我唠唠嗑的,欢迎继续看下去呀,一定满足大家的好奇心。




感谢你能够继续看下去。我想了很久,怎么样才能不至于让我的自我介绍写成流水账,但是,当我想了更久的时间之后,我发现,我想把这份流水账写出来更难,因为,很多的经历我都不记得了,我只能把我的记忆片段写下来,拼凑出我的职业生涯。好记性不如烂笔头,我觉得本文我可以永远留存并持续迭代,直到我的职业生涯结束的时候,可以用来回顾我的人生,也不失一桩美事。我也推荐大家这样做。


我的前端之路的伊始


我的第一份进入企业的工作是在2011年,大三实习,在杭州阿里,阿里妈妈广告部门(部门全称已经不记得了),后台开发,你没有看错,是的,我是后台开发,那会儿我还不知道前端,大学课程也没有一门是教前端的。


我对于阿里的印象,绝不是现在的"味儿"。我对阿里最大的印象还停留在当初那个时代,有三:



  1. 江湖气派,店小二文化,随性,直来直往,互相接受度非常高,我是非常喜欢这个文化的,当时阿里实习是不能拥有花名的,这是我职业生涯最大的遗憾之一,我还很清楚记得,当时我曾经查过,好像还没有人取名曹操,不过也是我的异想天开,因为即使我转正,我也没有那个资格取这个花名。

  2. 开放,真得非常开放,当我在新人入职欢迎聚餐中,脱到只剩裤衩的时候,我相信我那会一定是完全理解了开放这个词了。虽然直到现在回忆起来,还会有点不适,但是,当经历了那次聚餐之后,隐隐中,我会潜意识地得觉得,好像自己没什么是不可以“坦诚相待”的。

  3. 倒立文化,换个角度思考,我自认为我完全做到了,当我换个角度思考我的职业的时候,我走上了前端之路。。。


虽然在我拿到转正 offer 的时候,还是毅然决然选择保研(其实是被父母逼的)并转前端,但是我还是觉得,我在阿里的大半年实习期间,是我整个开发生涯中成长最快的时期,在那里,我学到了太多太多,以至于到现在我的开发习惯还会保留当时的一些痕迹:




  • 当我碰到需要服务运维的场景,我一定是首选 bash 脚本,然后是 python,最后才是 js,基本不会是 js 的,因为没什么是前两者做不到的。定时任务,文件锁,离线计算,文本处理等等,到现在我还记忆犹新。




  • 记不清写了多少 Map Reduce 了,但是当时,我真得被 Hadoop 的设计原理给深深的吸引到了,原来,大数据处理&分析,分布式计算和分布式文件系统有这么多的挑战,但它的解决方案又是这么的精简,直到现在,我仍然坚信,任何架构设计,一定是精简的,当我跟别人讨论架构的时候,如果他讲不清楚,或者表达非常复杂的时候,我就知道,不是设计有问题,就是表达有问题,我们还有更优的方案。天地良心,当时实习的时候,我真的是非常认真的做后台开发的,当时我还啃下了那本大象书呢,现在想想也觉得不容易,当年我是真喜欢看书呀。




  • 架构设计非常“好玩” ,在当时,阿里内部有非常多的技术分享,我常常去听我自己喜欢的分享,让我的技术视野得到了非常大的增长,比如:



    • 中文的分词要比英文的分词要难很多,最终发现,自然语言处理不是我的菜;

    • 推荐系统的结果是竞速的,当时真的有想入职百度,去学习搜索引擎的冲动;

    • 秒杀的多重降级、动态降级,各种“砍砍砍”,非常有意思。


    在当时,我学到的一个最重要的知识是,任何架构设计都是因地制宜的,不能墨守成规




在实习转正答辩的时候,最后问我的未来规划的时候,我的回答更多是偏架构设计和 UI 相关,现在回想起来都会觉得搞笑,当时我一度以为是转正失败了,但是没想到阿里开放到这都给我发了 offer,真得很感激我的老领导,但也觉得很对不起他们,因为我真的不想淹没在数据的海洋里,我更喜欢开发一些"看得着,摸得到"的东西,我会觉得做这个更有意思,所以,我选择了前端。


一波三折的腾讯梦


先说说为什么想去腾讯吧,因为我是广东人,父母都在深圳,都希望我回深圳,当时深圳不用多说,大公司就腾讯了,所以,我在实习和毕业的选择上一直都非常明确,就是深圳腾讯,但是我自己都没想到我回深圳是这么的坎坷。


研一找实习的时候,我第一次面试腾讯挂了,当时是电话面试,我记得是早上,很突然接到了面试电话,然后突然开始面试,我完全没有准备,很自然地就挂了,跟我同一个项目的做 web 服务的同学拿到腾讯的实习 offer 了,当时心理还有点不平衡,但是后面我也很快拿到新的 offer 了。


插一段题外话,当时我跟另外两个同学一起跟着导师外包项目,项目也挺有意思的,因为我们是嵌入式方向的实验室,所以我们做的是一个实时监控系统,有个同学主要负责传感器和网络编程,另外一个同学主要负责 web 后台服务,我负责前端页面(extjs),我们的项目是给一家医院做冰柜的温度实时监控系统,在冰柜中放入温度传感器,然后不断把冰柜的温度数据通过各个局域网网络节点传输器一路传到中心服务器中,然后中心服务负责存储并分析数据,落库并返回数据到前端,展示实时监控页面并添加告警功能。整个系统非常有意思,通过这个项目,我深深地感受到物联网的魅力,软硬件结合的威力。这还只是单向的,如果可以做到双向,再加上智能化,那基本就可以取代人的工作了,实际上,现在很多的无人XXX系统,他们的本质都是这个,现在互联网环境这么差,哪天干不下去了,换个行业,做物联网+虚拟+AI,做现实虚拟,实业升级事业,也是大有可为的。


回归正题,在腾讯突然面挂之后,我就开始认真复习,专门找前端的实习工作,然后很快就找到了网易的教育部门的前端开发 offer,这段经历我印象最深刻的是当时那批前端的笔试当中,我是最高分的,面试也没怎么问就拿到 offer 了,果然有笔试就是好呀,妥妥我的强项。或者是因为我有这段经历,所以后面我才会被分配到腾讯做教育吧。。。


在网易,我做的是网易云课堂和网易公开课相关的前端工作,在网易的实习过程中,我的前端基础和实践不断加强,三剑客,前端组件库,前端基础库,模块化,构建,浏览器兼容处理等等,基础技术收获很多,但是大的方面上,没什么特别的收获,就像网易的公司文化一样,没什么特别的感受,至今都没留下什么。在网易,印象最深的两个点就是:



  • 除了游戏,万般皆下品,主要靠情怀。其实这点跟在腾讯做教育也差不多;

  • 网易的伙食真的是互联网第一,不存在之一。


研二找工作的时候,我研究了腾讯的校招路演,发现有以下问题:



  • 杭州算是最后一站那种,时间很晚,到我们这边黄花菜都凉了;

  • 杭州离上海很近,过来招聘的团队应该基本都是上海的;

  • 像我这样的杭州毕业生不去阿里想去腾讯的奇葩真得不多了。


因此,我决定跑去上海参加校园招聘。当年校招我只面了百度跟腾讯,当时校园招聘都是统一笔试,面试,我记得百度是去他们上海分公司内部面试的,面了 2 轮就到 hr 了,还能留下记忆的是当时 2 面面试官对我的阿里经历很感兴趣,问了非常多,我当时就懵了,你们不是招前端的么。


然后是腾讯的面试,在一家 5 星级酒店的房间面的,当时进去就问我,能不能接受 web 服务研发岗位,我当时第一反应就是,你有无搞错呀!?但是机敏如我,肯定是立刻回答可以接受的,虽然这是一个随时都可以被废弃的万金油 api 岗位,但是它胜在可上可下,呸,是可前可后,啊呸,是可前端可后台,必须难不倒我呀,然后就是很无聊的面试,问了一些简单的前端题,了解了一下实习项目,最后做了一道智力题就结束了,相比百度的面试,有点看不过去了。最后问了我填的志愿是深圳的岗位,问我服不服从调剂,我说只想看深圳岗位,让我一度以为我又挂了,不过最后还是顺利进到 hr 的房间。。。面试,随便瞎聊,最后确认我只想回深圳,并表示可以给我争取调剂。


在回杭州的火车上,我知道百度的 offer 基本稳了,不过是上海的,腾讯的 offer 还是内心忐忑,实在是腾讯的面试有点“敷衍”了,那会儿我都在思考怎么忽悠我爸妈先在上海工作2年再回深圳了。不过没过2天,就收到了腾讯的 offer,是深圳易迅的前端开发岗位,当时在上海招聘的 90% 都是易迅(腾讯收购)的招聘,也很感谢当时帮我调剂的面试官跟 hr 了。兴奋的我在跟百度 hr 电话的时候就直接拒掉了百度 offer,现在回想起来,还真有点轻率了。


很快,我就决定提前到腾讯实习,当我坐在回深圳的火车时,看到了一则新闻:腾讯决定出售整个 ECC 给京东置换京东股份,并和京东开启战略合作。我不太记得我回家那天是什么心情,我只记得我办理入职手续的时候,窗外的天空是没有太阳的。我甚至都没认识全我的团队,因为当时所有工作都暂停了,那会儿,不是开大会,就是漫长的等待,现在想想,还挺像现在经历这场寒冬的我们一样,迷茫,忐忑,甚至有点慌张。


我加入了应届生群,在联名信上“签名”,在论坛上堆楼,终于,高层听到了我们的声音,跟京东友好协商之后,给予了我们这届应届生自主选择权 —— 是去京东还是留在腾讯,待遇不变。毫不犹豫地,我选择了腾讯。


写到这里,我还是很感慨,我的腾讯梦还真是一波三折,除了幸运还是幸运,或许因为在这件事情上花光了我前半生积攒的运气,以至于直到到现在所有的年会我都是阳光普照,深圳车牌摇号还遥遥无期,但是,我的腾讯之路还是开启了。。。


我职业生涯中最大的幸运 —— IMWeb 团队


多动动脑子


刚转来 IMWeb 团队,我接到的第一个任务是做一个爬虫,要爬淘宝教育的课程和购课数据。这不是很简单吗,之前做过呀,殊不知噩梦即将开始...


不到半天我就写好了,包括邮件模板,也自测好了,正式启动,美滋滋去喝杯茶,回来就能交差了。当我摸鱼回来一看,咦,脚本停了,接口报错,被限频了。于是我进入了疯狂调试模式,添加重试逻辑,不断调整请求频率策略,最终祭出终极策略,3分钟请求1次,这下不会被限频了吧,在稳定跑了1个小时没问题之后,我安心的下班回家了。


第二天到公司,数据跑完了,完美。于是,我做了最后的数据校对和计算调整,然后调通自动发送邮件的逻辑,再次执行。当我美滋滋地再次摸鱼回来,发现脚本又停了,这次是新的错误,没有错误信息,就是 5xx,黑人问号啊,于是各种调试各种排查,最终得出一个结论,ip 被拉进黑名单了。


好家伙,算你狠。于是我上网各种研究代理,不管免费付费,能用就是好代理,再次调整策略,申请十多个账号轮流爬,光荣牺牲了一批又一批的 ip 之后,我还是败下阵来。那个时候,我觉得我的人生都是黑暗的,我的面前立着一座大山,我怎么样都翻不过去。


当老大咨询进度的时候,我并没有得到任何安慰和建议,而是一句“多动动脑子”。


我已经忘记当时的我是什么心情,被打击成什么样了。也已经忘记了一周后是怎样完成任务的。我只记得,之后我只花了半天时间就爬了网易云课堂和慕课网的数据,他们就是毫不设防的裸......奔。


任性如我


对于我们程序员来说,碰到的最棘手的问题中,无法复现的问题肯定名列前茅。


有一次需求发布,现网验证的时候发现了一个问题,在本地和 test 环境都复现不了,live 打断点也复现不了,真是绝了,打断点没问题,不打断点有问题,我大概能猜到问题,但是需要打印一些日志来定位最终问题,可是只能在 live 才有效,先不说 live 构建会自动删掉 console.log 语句,执行一次 live 部署非常慢,如果要折腾几次来调试,那半天都解决不了问题了。


急性子的我肯定受不了这种折磨,所以我选择了直接登录现网服务器改代码调试。先把压缩文件 down 下来,本地格式化,找到对应位置添加 console.log,然后传回服务器覆盖文件,禁用 cdn 资源,直接在现网复现排查问题。几分钟不到就确定问题,然后修改代码重新部署一次过完成最终需求发布。整个过程行云流水,但是我内心慌得一比,这要是出问题被发现,那后果不敢想象。


还有好几次的 Node 服务问题,我也是直接现网调试,其实 Node 服务才是最适合这么做的场景,但是,我并不是推荐大家这样做。再到后面,我行我素的我越来越能够理解流程机制的用意和作用,现在踏上管理岗位,我更希望小伙伴们是严格遵照流程规范来工作,但我的内心深处,还是住着一个不羁的我


“万恶的” owner


“清龙,这个需求就由你来当 owner 吧。”


“owner?要做什么?”


“就是这个需求的负责人,看看需求进度有没有问题,发布别延期就行”


“好”


【需求开发中...】


“清龙,现在需求进度怎样?有没有风险?”


“我这边没问题,我问一下后台同学看看”


“你可以每天下班前收集一下大家的进展,然后在群里同步哈”


“好”


【需求测试中...】


“清龙,需求测得怎么样啦?”


“......(这不应该问测试吗)应该问题不大,我这边的 bug 都处理完了,我找测试跟进一下测试进度哈”


“可以每天下班前找测试对齐一下测试的整体进度,让测试在群里同步哈”


“好”


【需求发布中...】


“清龙,需求发得怎么样啦?”


“后台发完了,前端正在发,问题不大”


“牛呀,一定要做好现网验证,发布完成记得要在群里周知哈”


“好~”


自从团队推行 owner 机制,工作量是噌噌噌地往上涨,但是工作能力也有很大的提升。


怎么说呢,这是毁誉参半的机制,重点在于每个人怎么看待这个事情,它可以是卷、分担压力的借口;它也可以是培养新人,锻炼项目管理能力,提升沟通协调能力的最佳实践机会。


我眼中的 IMWeb 团队


它是综合的。我们团队涉猎的领域非常广,移动端,pc 端,后台均有涉猎,正因如此,我们有非常好的土壤茁壮成长,尝试各种新技术。在很早的时候,我就在数据接口低代码平台落地 GraphQL,实现了基于mysql 的 GraphQL 的 node 版本,不说业界,在公司内肯定是领先的。在公司成长的过程中,我们团队也在成长,在前端工程化上也有很多的实践和成果。后面腾讯搞 Oteam,我们团队也多有贡献。


它是着眼于业务的。 我们团队推崇做产品的主人翁,坚持不懈地以技术手段助力业务发展。我们做的所有项目都是为了业务服务,为了整个团队服务。我们团队是专业的,没有钻技术的牛角尖,更多地是扎根于业务,一切以实际出发,更多以落地与实践为主。但我们团队的业务并不是很出彩,属于半公益的教育,至今我仍然唏嘘不已,只能感叹时运不济,现在回过头来细品,再厉害的技术,没有好的业务相辅相成,也是无法一直走下去,业务是王道啊。


它是被信任与敢于信任的。作为前端团队,能够有那么大的空间来施展身手,这足以说明我们团队是受到领导的充分信任的,我们团队也非常努力来对得起这份信任。而团队也非常信任团队里的每一个人,会给予很多的试错机会和时间,就看我们有没有耐心,主动与坚持了。


在一个已经建立了一定文化的团队是幸福的,它是需要细品的,但很多人都不愿意去感受。这两年,我过得很难受,不知变通地我一直守着这份坚持,与已经被潜移默化的团队文化对抗,最终只是落得个遍体鳞伤。但是我并不后悔,反而很庆幸,因为最后我找到了自己内心的真相,一直以来,我觉得是 IMWeb 团队造就了我,其实,我所依恋的一直都是它的价值观与文化,而我愿意一直为之践行。


我的管理之路


我正式任命是在 2020年上半年,但实际上,我在 2018 年下半年就从腾讯课堂调到了企鹅辅导,从一组调到三组,并开始做一些团队管理的工作。整体而言我的管理经验成长的非常缓慢,这是我自己的结论。


首先,我的角色转变比较缓慢。经常看到小伙伴们做事情太“慢”,我都忍不住要自己上,或者直接告诉他们答案,我知道这很不好,但是初期的我就是忍不住,我感觉我的管理之路就是憋气之路,最后总结就是,在大方向上,我要站出来,但是具体实施层面,我要当个隐身人,这对我来说,非常难受。


其次,我是主猫头鹰次考拉的重事风格,不太擅长管理小伙伴的情绪还有激励,沟通和语言艺术真是我需要投入一生去学习锻炼的课程。另外,我有一个最大的问题就是不喜欢冲突,直接导致我不太擅长争取资源,这会让我觉得很对不起小伙伴们,这点也是我离开腾讯最大的原因吧。感觉我比较适合增量市场,在存量市场这点真的是致命的,不过专心搞好业务不挺好吗,何苦浪费时间在这些地方。

作者:潜龙在渊灬
来源:juejin.cn/post/7189612924116140092

收起阅读 »

Vue项目打包优化

web
最近做完了一个项目,但是打包之后发现太大了,记录一下优化方案 Element、Vant 等组件库按需加载 静态资源使用cdn进行引入 开启gzip压缩 路由懒加载 #首先看看啥也没做时打包的大小 可以使用 webpack-bundle-analyzer 插...
继续阅读 »

最近做完了一个项目,但是打包之后发现太大了,记录一下优化方案



  • Element、Vant 等组件库按需加载

  • 静态资源使用cdn进行引入

  • 开启gzip压缩

  • 路由懒加载


#首先看看啥也没做时打包的大小


可以使用 webpack-bundle-analyzer 插件在打包时进行分析



 


可以看到有2.5M的大小,下面就进行优化


Element、Vant 等组件库按需加载


可以看到,在打包的文件中,占据最大比例的是这两个组件库,我们可以使用按需加载来减小 按需加载按照官方文档来就行,需要注意配置bebel.config.js


  // 在bebel.config.js的plugins选项中配置    
["component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ],
["import", { "libraryName": "vant", "libraryDirectory": "es", "style": true }, 'vant']

配置后的大小,可以明显的看到有减小



静态资源使用cdn进行引入


接下来占比最大的就是一些可以通过cdn进行引入的静态资源了


设置例外


vue.config.js文件中进行设置例外,不进行打包


设置例外的时候 key:value 中key的值为你引入的库的名字,value值为这个库引入的时候挂在window上的属性


    config.externals({
"vue":'Vue',
"vue-wordcloud":'WordCloud',
"@wangeditor/editor-for-vue":"WangEditorForVue",
})

然后在项目的 public/index.html 文件中进行cdn引入


cdn的话有挺多种的,比如bootcdnjsdelivr等,推荐使用jsdelivrnpm官方就是使用的这个


    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.runtime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-wordcloud@1.1.1/dist/word-cloud.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@wangeditor/editor-for-vue@1/dist/index.js"></script>

完成后



gzip压缩


开启gzip压缩需要使用插件,可以用compression-webpack-plugin


vue.config.js文件中进行配置


chainWebpack: config => {
config.optimization.minimize(true)
},
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
return {
plugins: [
new CompressionPlugin({
test: /\.js$|\.css$|\.html$/,
algorithm: 'gzip',
threshold: 10240,
deleteOriginalAssets: false,
})
]
}
}
}

完成后



在配置完成后,在本地并不能开启使用,需要配置Nginx进行支持才行


路由懒加载


路由懒加载的基础是webpackMagic Comments 官方文档


    // 在路由配置中,通过下面这种方式导入对应组件
component: () => import(/* webpackChunkName: "mColumn" */ '../mviews/ColumnView.vue')

完成后


路由懒加载并不会减小文件打包后的大小,但是可以让文件分为不同的模块,当路由跳转时,才加载当前路由对应的组件


这样就可以大大减少首屏白屏的时间,不用在第一次进入的时候就加载全部的文件



如图,当第一次进入的时候只会加载到home.xx.js,当进入另一个路由的时候就会去加载对应组件,如图中的socket.xx.js


作者:羞男
来源:juejin.cn/post/7224036517139939386
收起阅读 »

Vue 必备的这些操作技巧

web
🎈 键盘事件 在 js 中我们通常通过绑定一个事件,去获取按键的编码,再通过 event 中的 keyCode 属性去获得编码 如果我们需要实现固定的键才能触发事件时就需要不断的判断,其实很麻烦 let button = document.querySel...
继续阅读 »

🎈 键盘事件



  • js 中我们通常通过绑定一个事件,去获取按键的编码,再通过 event 中的 keyCode 属性去获得编码

  • 如果我们需要实现固定的键才能触发事件时就需要不断的判断,其实很麻烦


let button = document.querySelector('button')

button.onkeyup = function (e) {
console.log(e.key)
if (e.keyCode == 13) {
console.log('我是回车键')
}
}


  • vue 中给一些常用的按键提供了别名,我们只要在事件后加上响应的别名即可

  • vue 中常见别名有:up/向上箭头down/向下箭头left/左箭头right/右箭头space/空格tab/换行esc/退出enter/回车delete/删除


// 只有按下回车键时才会执行 send 方法
<input v-on:keyup.enter="send" type="text">


  • 对于 Vue 中未提供别名的键,可以使用原始的 key 值去绑定,所谓 key 值就是 event.key 所获得的值

  • 如果 key 值是单个字母的话直接使用即可,如果是由多个单词组成的驼峰命名,就需要将其拆开,用 - 连接


// 只有按下q键时才会执行send方法
<input v-on:keyup.Q="send" type="text">

// 只有按下capslock键时才会执行send方法
<input v-on:keyup.caps-lock="send" type="text">


  • 对于系统修饰符 ctrlaltshift 这些比较复杂的键使用而言,分两种情况

  • 因为这些键可以在按住的同时,去按其他键,形成组合快捷键

  • 当触发事件为 keydown 时,我们可以直接按下修饰符即可触发

  • 当触发事件为 keyup 时,按下修饰键的同时要按下其他键,再释放其他键,事件才能被触发。


// keydown事件时按下alt键时就会执行send方法
<input v-on:keydown.Alt="send" type="text">

// keyup事件时需要同时按下组合键才会执行send方法
<input v-on:keyup.Alt.y="send" type="text">


  • 当然我们也可以自定义按键别名

  • 通过 Vue.config.keyCodes.自定义键名=键码 的方式去进行定义


// 只有按下回车键时才会执行send方法
<input v-on:keydown.autofelix="send" type="text">

// 13是回车键的键码,将他的别名定义为autofelix
Vue.config.keyCodes.autofelix=13

🎈 图片预览



  • 在项目中我们经常需要使用到图片预览,viewerjs 是一款非常炫酷的图片预览插件

  • 功能支持包括图片放大、缩小、旋转、拖拽、切换、拉伸等

  • 安装 viewerjs 扩展


npm install viewerjs --save


  • 引入并配置功能


//引入
import Vue from 'vue';
import 'viewerjs/dist/viewer.css';
import Viewer from 'v-viewer';

//按需引入
Vue.use(Viewer);

Viewer.setDefaults({
'inline': true,
'button': true, //右上角按钮
"navbar": true, //底部缩略图
"title": true, //当前图片标题
"toolbar": true, //底部工具栏
"tooltip": true, //显示缩放百分比
"movable": true, //是否可以移动
"zoomable": true, //是否可以缩放
"rotatable": true, //是否可旋转
"scalable": true, //是否可翻转
"transition": true, //使用 CSS3 过度
"fullscreen": true, //播放时是否全屏
"keyboard": true, //是否支持键盘
"url": "data-source",
ready: function (e) {
console.log(e.type, '组件以初始化');
},
show: function (e) {
console.log(e.type, '图片显示开始');
},
shown: function (e) {
console.log(e.type, '图片显示结束');
},
hide: function (e) {
console.log(e.type, '图片隐藏完成');
},
hidden: function (e) {
console.log(e.type, '图片隐藏结束');
},
view: function (e) {
console.log(e.type, '视图开始');
},
viewed: function (e) {
console.log(e.type, '视图结束');
// 索引为 1 的图片旋转20度
if (e.detail.index === 1) {
this.viewer.rotate(20);
}
},
zoom: function (e) {
console.log(e.type, '图片缩放开始');
},
zoomed: function (e) {
console.log(e.type, '图片缩放结束');
}
})


  • 使用图片预览插件

  • 单个图片使用


<template>
<div>
<viewer>
<img :src="cover" style="cursor: pointer;" height="80px">
</viewer>
</div>
</template>


<script>
export default {
data() {
return {
cover: "//www.autofelix.com/images/cover.png"
}
}
}
</script>



  • 多个图片使用


<template>
<div>
<viewer :images="imgList">
<img v-for="(imgSrc, index) in imgList" :key="index" :src="imgSrc" />
</viewer>
</div>

</template>

<script>
export default {
data() {
return {
imgList: [
"//www.autofelix.com/images/pic_1.png",
"//www.autofelix.com/images/pic_2.png",
"//www.autofelix.com/images/pic_3.png",
"//www.autofelix.com/images/pic_4.png",
"//www.autofelix.com/images/pic_5.png"
]
}
}
}
</script>


🎈 跑马灯



  • 这是一款好玩的特效技巧

  • 比如你在机场接人时,可以使用手机跑马灯特效,成为人群中最靓的仔

  • 跑马灯特效其实就是将最前面的文字删除,添加到最后一个,这样就形成了文字移动的效果


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>跑马灯</title>
<style type="text/css">
#app {
padding: 20px;
}
</style>
</head>

<body>
<div id="app">
<button @click="run">应援</button>
<button @click="stop">暂停</button>
<h3>{{ msg }}</h3>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.0/dist/vue.min.js"></script>
<script>
new Vue({
el: "#app",
data: {
msg: "飞兔小哥,飞兔小哥,我爱飞兔小哥~~~",
timer: null // 定时器
},
methods: {
run() {
// 如果timer已经赋值就返回
if (this.timer) return;

this.timer = setInterval(() => {
// msg分割为数组
var arr = this.msg.split('');
// shift删除并返回删除的那个,push添加到最后
// 把数组第一个元素放入到最后面
arr.push(arr.shift());
// arr.join('')吧数组连接为字符串复制给msg
this.msg = arr.join('');
}, 100)
},
stop() {
//清除定时器
clearInterval(this.timer);
//清除定时器之后,需要重新将定时器置为null
this.timer = null;
}
}
})
</script>

</html>

🎈 倒计时



  • 对于倒计时技巧,应用的地方很多

  • 比如很多抢购商品的时候,我们需要有一个倒计时提醒用户开抢时间

  • 其实就是每隔一秒钟,去重新计算一下时间,并赋值到 DOM


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>倒计时</title>
</head>

<body>
<div id="app">
<div>抢购开始时间:{{count}}</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.0/dist/vue.min.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: '', //倒计时
seconds: 864000 // 10天的秒数
}
},
mounted() {
this.Time() //调用定时器
},
methods: {
// 天 时 分 秒 格式化函数
countDown() {
let d = parseInt(this.seconds / (24 * 60 * 60))
d = d < 10 ? "0" + d : d
let h = parseInt(this.seconds / (60 * 60) % 24);
h = h < 10 ? "0" + h : h
let m = parseInt(this.seconds / 60 % 60);
m = m < 10 ? "0" + m : m
let s = parseInt(this.seconds % 60);
s = s < 10 ? "0" + s : s
this.count = d + '天' + h + '时' + m + '分' + s + '秒'
},
//定时器没过1秒参数减1
Time() {
setInterval(() => {
this.seconds -= 1
this.countDown()
}, 1000)
},
}
})
</script>

</html>

🎈 自定义右键菜单



  • 在项目中,我们有时候需要自定义鼠标右键出现的选项,而不是浏览器默认的右键选项

  • 对于如何实现右键菜单,在 Vue 中其实很简单,只要使用 vue-contextmenujs 插件即可

  • 安装 vue-contextmenujs 插件


npm install vue-contextmenujs


  • 引入


//引入
import Vue from 'vue';
import Contextmenu from "vue-contextmenujs"

Vue.use(Contextmenu);


  • 使用方法

  • 可以使用 <i class="icon"></i> 可以给选项添加图标

  • 可以使用 style 标签自定义选项的样式

  • 可以使用 disabled 属性禁止选项可以点击

  • 可以使用 divided:true 设置选项的下划线

  • 可以使用 children 设置子选项


<style>
.custom-class .menu_item__available:hover,
.custom-class .menu_item_expand {
background: lightblue !important;
color: #e65a65 !important;
}
</style>


<template>
<div style="width:100vw;height:100vh" @contextmenu.prevent="onContextmenu"></div>
</template>


<script>
import Vue from 'vue'
import Contextmenu from "vue-contextmenujs"
Vue.use(Contextmenu);

export default {
methods: {
onContextmenu(event) {
this.$contextmenu({
items: [
{
label: "返回",
onClick: () => {
// 添加点击事件后的自定义逻辑
}
},
{ label: "前进", disabled: true },
{ label: "重载", divided: true, icon: "el-icon-refresh" },
{ label: "打印", icon: "el-icon-printer" },
{
label: "翻译",
divided: true,
minWidth: 0,
children: [{ label: "翻译成中文" }, { label: "翻译成英文" }]
},
{
label: "截图",
minWidth: 0,
children: [
{
label: "截取部分",
onClick: () => {
// 添加点击事件后的自定义逻辑
}
},
{ label: "截取全屏" }
]
}
],
event, // 鼠标事件信息
customClass: "custom-class", // 自定义菜单 class
zIndex: 3, // 菜单样式 z-index
minWidth: 230 // 主菜单最小宽度
});
return false;
}
}
};
</script>


🎈 打印功能



  • 对于网页支持打印功能,在很多项目中也比较常见

  • 而 Vue 中使用打印功能,可以使用 vue-print-nb 插件

  • 安装 vue-print-nb 插件


npm install vue-print-nb --save


  • 引入打印服务


import Vue from 'vue'
import Print from 'vue-print-nb'
Vue.use(Print);


  • 使用

  • 使用 v-print 指令即可启动打印功能


<div id="printStart">
<p>红酥手,黄縢酒,满城春色宫墙柳。</p>
<p>东风恶,欢情薄。</p>
<p>一怀愁绪,几年离索。</p>
<p>错、错、错。</p>
<p>春如旧,人空瘦,泪痕红浥鲛绡透。</p>
<p>桃花落,闲池阁。</p>
<p>山盟虽在,锦书难托。</p>
<p>莫、莫、莫!</p>
</div>
<button v-print="'#printStart'">打印</button>

🎈 JSONP请求



  • jsonp解决跨域 的主要方式之一

  • 所以学会在 vue 中使用 jsonp 其实还是很重要的

  • 安装 jsonp 扩展


npm install vue-jsonp --save-dev


  • 注册服务


// 在vue2中注册服务
import Vue from 'vue'
import VueJsonp from 'vue-jsonp'
Vue.use(VueJsonp)

// 在vue3中注册服务
import { createApp } from 'vue'
import App from './App.vue'
import VueJsonp from 'vue-jsonp'
createApp(App).use(VueJsonp).mount('#app')


  • 使用方法

  • 需要注意的是,在使用 jsonp 请求数据后,回调并不是在 then 中执行

  • 而是在自定义的 callbackName 中执行,并且需要挂载到 window 对象上


<script>
export default {
data() {...},
created() {
this.getUserInfo()
},
mounted() {
window.jsonpCallback = (data) => {
// 返回后回调
console.log(data)
}
},
methods: {
getUserInfo() {
this.$jsonp(this.url, {
callbackQuery: "callbackParam",
callbackName: "jsonpCallback"
})
.then((json) => {
// 返回的jsonp数据不会放这里,而是在 window.jsonpCallback
console.log(json)
})
}
}
}
</script>


正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿


作者:极客飞兔
来源:juejin.cn/post/7121564385151025165

收起阅读 »

24 岁技术人不太平凡的一年

一年前,我整夜混迹于魔都淮海中路的酒吧迪厅,而白天,在外滩海景餐厅吃着四位数一顿的西餐。租住的洋房里陈列着各种奢侈品…… 一年后,我结婚了,并且买了套房子。 一、魔都 2021 年,是我爸妈非常兴奋的一年。因为那年我去了上海,找了一份年薪 60 几万的工作。他...
继续阅读 »

一年前,我整夜混迹于魔都淮海中路的酒吧迪厅,而白天,在外滩海景餐厅吃着四位数一顿的西餐。租住的洋房里陈列着各种奢侈品……


一年后,我结婚了,并且买了套房子。


一、魔都


2021 年,是我爸妈非常兴奋的一年。因为那年我去了上海,找了一份年薪 60 几万的工作。他们可以和邻居亲戚们炫耀他们的儿子了。



但我没什么感觉,对我而言,60 万只是手机上的一串数字。而且我知道,60 万并不多。因为那个整日听我汇报的、大腹便便的、看上去不怎么聪明的中年人拿着远比我高的薪水,我和他都一样,只是一条资本家的狗而已。


我一向不打算结婚,也不打算要孩子,也就意味着我不需要存钱。


我不会像我的领导那样为了在上海买一套 1200 万的房子而过得那样抠搜。我可以花一万多租一个小洋房,我可以在家里塞满那些昂贵的电器,我可以每周去国金、恒隆、新天地消费。我会买最新款的苹果全家桶,甚至连手机壳都要买 Lv 的。


但即使这样,钱还是花不完。


别人都因为没钱而烦恼,而我却因为钱花不完而烦恼。


后来,在机缘巧合之下,我开始了绚丽多彩的夜生活。在夜店,只需要花 1 万块钱,就可以当一晚上的王。这种快感是难以描述的。我成了 Myst、LolaClub、Ak、wm、pops 们的常客。开始了一段纸醉金迷的日子。


二、求职


找工作是一件再容易不过的事,因为几乎所有人都有工作。人只要降低要求,就一定能够找到工作。找工作难,是因为你渴望的工作岗位所需要的能力与你当前的能力并不匹配。


刚来到上海的时候,我非常自信,整天到处闲逛。朋友问我为什么不准备面试题?我说没有这种必要,他说不是很理解,我说那算了,不用理解。


因为技术上的知识,就那么多。你有自己的理解,有自己的经验,完全不需要胆怯,也没必要胆怯。合适就去,不合适就再看看。


能否找到好工作,更多的是你和面试官是否聊得来;你所做的事情,是否和这家公司的业务所匹配;你的经历、价值观和这家公司的愿景使命价值观是否契合。


我清楚我的能力究竟如何,我知道我应该去哪种公司。


但我需要了解这些公司的不同。于是我投了很多简历,大小公司都有。像携程、美团、得物、拼多多、小红书、微盟这些公司基本上都有去面试。跑了很多地方,徐家汇、北外滩、五角场、陆家嘴、奉贤岛、静安寺……。


全职面试,一天四五场,很忙。


但不到一周就确定好了 offer。


朋友说我是一个果断的人,我只是觉得没必要消耗时间。


明白自己的能力上限在哪,懂得在合适的环境最大化释放自己的价值。就一定可以收获一份合适的工作,或者说是一份事业。


找到自己的使命,就可战无不胜。


三、爱情


人缺什么,就会去寻找什么。


在 24 岁的年纪里,我缺少的不是梦想、金钱或者使命感,而是爱情。


我遇到了 Emma,她带给我的滋味,与我所有的想象都不一样。


我在上海,她在三亚。我们在相隔千里的城市,我坐着飞机去找她。结果因为晚了几分钟,于是我买了张全价票,硬是要在当天飞过去,因为我等这一天已经等了半个月了。


我们住在三亚的希尔顿。在三亚有两个希尔顿,结果她找错了,去了另一个希尔顿,后来我们很庆幸幸亏没有人开门。


虽然 Emma 笨笨的,但我喜欢她这样。


我们去了亚龙湾、天涯海角和南海观音。


三亚虽然是中国最南方的岛最南侧的城市,但有个外号是东北第四城。前三个是黑龙江、吉林和辽宁。一个城市的名字可能是错的,但外号绝对不会错。海南到处都是东北人。


爱情刚开始都会轰轰烈烈,我会给 Emma 精心挑选手链,去国金帮她挑选面膜,亲手给她写情书,那些情书至今还在我们的衣柜里。后来去全上海只有两个店的 Graff 买了一对情侣对戒。


我不介意给心爱的女人花钱。我觉得,一个男人把钱都花在哪儿了,就知道这个男人的心在哪儿。


当然,认识久了,特别是同居之后,这种轰轰烈烈式的浪漫就会褪去,更多的是生活中的点点滴滴。


有人说过一句话:


和喜欢的人在一起的快乐有两种,一种是一起看电影旅行一起看星星月亮分享所有的浪漫。还有一种则是分吃一盒冰淇淋一起逛超市逛宜家把浪漫落地在所有和生活有关的细节里。


但无论是前者还是后者,我都想和你一起,因为重要的是和你,而不是浪漫。


往事历历在目,虽然现在很难再有当时的那种感觉,可我更享受当下的温馨。


四、旅游


认识 Emma 之后,我们开始养成了旅游的习惯,每个月都会留出两三天时间一起出去旅游。


2 月,去了丽江。爬了玉龙雪山、逛了拉市海和木府。



玉龙雪山真的很神奇,在丽江市任何一个角落,都可以抬头看见白雪皑皑的雪山。


2 月 14 号是情人节,我在丽江的街头给 Emma 买了一束玫瑰。



3 月,我在上海,本来打算去北京,结果发生航变。然后再打算做高铁去杭州西湖,结果杭州顺丰出事了。哪都去不了,只能在上海本地旅游。去了陆家嘴的上海中心大厦。其实很早之前我就和朋友去过一次了,这次去的主要目的是和 Emma 锁同心锁。


我记得我们去的时候,已经是晚上九点五十,工作人员说十点关门,因为疫情原因,明天开始将无限期关停,具体开放时间等通知。如果现在上去,我们只能玩十分钟。没有犹豫,花了四百多买了两张票上去上了把锁。



那时我还不知道,这种繁华将在不久后彻底归于死寂。


后面几天,我们在上海很多地方溜达,外滩、豫园、迪士尼乐园。


迪士尼乐园是上海人流量最大的游乐场,几乎全年都是爆满。但这次疫情期间的迪士尼乐园的游客很少,很多项目都不需要排队。


但那天真的很冷,我们为了看烟花冻得瑟瑟发抖。夜晚的迪士尼城堡真的很美。



4-7 月上海封城,哪儿也没去。


8 月去了博鳌亚洲论坛。如果不是去开会的话,真没什么好玩的。



9 月打算坐轮船去广州徐闻、再坐绿皮火车去拉萨,因为我们认为这样会很浪漫。结果因疫情原因,没办法离开海南,只去了骑驴老街和世纪大桥旁边的云洞书店。




这座书店建在海边,进入看书要提前预约。


实际上没几个看书的,全是美女在拍照。


10 月去了济南大明湖和北京的天安门广场、八大胡同等地方。


很没意思,图我都不想放。


预约了故宫门票,并且打算去八达岭长城。但是在北京土生土长四十年的同事告诉我,他从来没去过故宫,并且不建议去八达岭长城。于是我把故宫的门票退了,并且把八达岭长城的计划也取消了。


11 月一整个月都在忙着买房子的事情,天天跟着中介跑,哪儿也没去。


五、封控、方舱、辞职与离沪


上海疫情暴发,改变了很多事。


这一部分我删掉了,涉及敏感话题。


总结来说就是:封控,挨饿,感染奥密克戎,进方舱,出舱,继续封控。


6 月 1 日,上海终于开放了。


虽然可以离开小区,但离开上海还是很难。


首先我需要收拾家里的东西,然后需要寄回我爸妈家,但当时所有的物流都处于瘫痪状态。当时我也想过直接丢掉,但我的东西真的很多,有二十多个箱子,差不多是我的全部家当了。


再之后需要购买飞机票,但所有的航班都在无限期航变。我给飞机场的工作人员打了很多次电话,没事就问。后来她们有些不耐烦了,告诉我,现在没有一架飞机能从上海飞出去,让我不要再打了。


可我不死心。


最后我选择了乘坐火车。


上海没有直达海口的火车,所以需要在南昌中转。由于我是上海来的,所以当地的政策是要隔离 7 天,除非你不出火车站。上海到南昌的火车是第一天下午到,而南昌到海口的火车是第二天的中午才出发。这也就意味着我要在南昌站睡一晚。而从上海到海口的整个路程,需要花费 3 天。这对以前的我来说是不可思议的。


但爱情赋予我的力量是难以想象的。


我在火车站旁边的超市买了一套被子就上了去南昌的火车,然后躺在按摩椅上睡了一晚。


第四天一大早,我终于到了海南。


六、求职


2022 年,是非常难找工作的。


特别是我准备搬到海南。海南是没有大型互联网公司的,连个月薪上万的工作都找不到。这样估计连自己都养不活。


所以我准备找一份远程工作。相比较于国内,国外远程工作的机会明显更多,可惜我英语不好,连一场基本的面试所需要的词汇都不够。所以我只能找一些国内的远程机会。


机缘巧合之下,终于找到了一家刚在美国融到资的创业公司。


他们需要一位带有产品属性和创造力的前端架构师。前前后后面了大概一周,我成功拿到 offer。这是我第一次加入远程团队。


印象中面试过程中技术相关的点聊了以下几点:



  • WebSocket 的弊端。

  • Nodejs 的 Stream。

  • TLV 相比 JSON 的优劣势。

  • HTTP 1.1/2 和 HTTP3(QUIC)的优劣势。

  • 对云的看法。

  • 真正影响前端性能的点。

  • 团队管理方面的一些问题。


因为公司主要业务是做云,所以大多数时间都是在聊云相关的问题。老板是一个拥有 20 年技术背景的老技术人,还是腾讯云 TVP。在一些技术上的观点非常犀利,有自己的独到之处。这一点上我的感受是非常明显的。


全球化边缘云怎么做?


现代应用的性能瓶颈往往不再是渲染性能。但各大前端框架所关注的点却一直是渲染。以至于现代主流前端框架都采用 Island 架构。


但另一个很影响性能的原因是时延。这不仅仅是前端性能的瓶颈,同时也是全球化分布式系统的性能瓶颈。


设想一下,你是做全球化的业务,你的开发团队在芝加哥,所以你在芝加哥买了个机房部署了你们的系统,但你的用户在法兰克福。无论你的系统如何优化,你的用户总是感觉卡顿。因为你们相隔 4000 多英里的物理距离,就一定要遭受 4000 多英里的网络延迟。


那该怎么办呢?很简单,在法兰克福也部署一套一模一样的系统就好了,用户连接距离自己最近的服务器就好了,这样就能避免高网络时延,这就是全球分布式。但如果你的用户也有来自印度的、来自新加坡、来自芬兰的。你该怎么办呢?难道全球 200 多个地区都逐一部署?那样明显不现实,因为成本太高了,99% 的公司都支付不起这笔费用。


所以很多云服务商都提供了全球化的云服务托管,这样就可以以低成本实现上述目标。


但大多数云服务商的做法也并不是在全球那么多地区部署那么多物理服务器。而是在全球主要的十几个地区买十几台 AWS 的裸机就够了。可以以大洲维度划分。有些做得比较好的云服务商可能已经部署了几十个节点,这样性能会更好。


这类云服务商中的一些小而美的团队有 vercel、deno deploy。这也是我们竞争目标之一。


招聘者到底在招什么人?


当时由于各种裁员,大环境不好,面试者非常多,但合适的人又非常少。后来据老板透露,他面试了 48 个人,才找到我。起初我还不信,以为这是常规 PUA,后来我开始负责三面的时候,在面试登记表中发现确实有那么多面试记录。其中竟然有熟人,也有一个前端网红。


后来我总结,很多人抱怨人难招,那他们到底在招什么人?


抛开技术不谈,他们需要有创造力的人。


爱因斯坦说:想象力比知识更重要。想象力是创造力的要素之一,也是创造力的前提。


那些活得不是很实际的人,会比那些活得踏踏实实的人有更大的机会去完成创造。


为什么要抛开技术不谈?因为职业生涯到了一定高度后,已经非唯技术论了。大家的技术都已经到了 80 分以上,单纯谈论技术,已经很难比较出明显差异,这时就要看你能用已有的技术,来做点什么,也就是创造力。这也是为什么我在文中没怎么谈技术面试的原因。


如果将马斯洛需求理论转化为工程师需求理论,那么同样可以有五级:




  1. 独立完成系统设计与实现。




  2. 具有做产品的悟性,关注易用、性能与稳定。




  3. 做极致的产品。对技术、组织能力、市场、用户心理都有很全面深入的理解。代表人物张小龙。




  4. 能够给世界带来惊喜的人。代表人物沃兹尼亚克。




  5. 推动人类文明进步,开创一个全新行业的人。代表人物马斯克。




绝大多数人都停留在前三个维度上,我也不例外。但我信奉尼采的超人主义,人是可以不断完善自我、超越自我、蜕变与进化的。既然沃兹尼亚克和马斯克能够做到的事情,我为什么不能去做呢?所以我会一直向着他们的方向前进。或许最终我并不能达到那种高度,但我相信我仍然可以做出一些不错的产品。


七、创业


搬到海南之后,时间明显多了起来。不需要上下班通勤,不去夜店酒吧,甚至连下楼都懒得去。三五天出去一次,去小区打一桶纯净水,再去京东超市买一大堆菜和食物,囤在冰箱。如果不好买到的东西,我都会选择网购。我一般会从山姆、网易严选、京东上面网购。


九月份十月份,海南暴发疫情,一共举行了十几轮全员核酸检测,我一轮都没参加。主要是用不到,出不去小区没关系,外卖可以送到小区门口,小区内的京东超市也正常营业,丝毫不影响我的日常起居。


所以我盘算着要做些事情。刚好之前手里经营着几个副业,虽然都没有赚到什么钱。但是局已经布好了。其中一个是互联网 IT 教育机构。是我和我的合伙人在 20 年就开始运营的,现在已经可以在知乎、B 站等渠道花一些营销费用进行招生。我们定价是 4200 一个月(后来涨价到 4800),一期教三个月左右。通过前几期的经验,我们预估,如果一期有十几个人到二十个人报名,就有大概将近 10 万多的收入,还是蛮可观的。所以我准备多花些精力做一下这个教育机构。


培训的内容是前端,目标是零基础就业。因为我在前端这方面有很大的优势,而且当时前端招聘异常火热。初级前端工程师需要掌握的技能也不多,HTML、CSS、JavaScript、Git、Vue,就这么几块内容而已。相比较成型慢的后端,前端是可以速成的,所以我认为这条路非常可行。


我负责的这一期,前期招了将近 20 人。



而下一期还没开课,已经招到了 5 个人。一切都向着好的方向发展。


但很快,十月、十一月左右,招生突然变难了。连知乎上咨询的人都明显变少了。我们复盘了下原因,原来是网上疯传现在是前所未有的互联网寒冬,加上各种大厂裁员的消息频发,搞得人心惶惶。甚至有人扬言,22 年进 IT 行业还不如去养猪。


因为我的合伙人是全职,他在北京生活花费很高,加上前期没有节制地投入营销费用,他已经开始负债了。但是营销费用是不能停的,一旦知+ 停掉,就很难继续招生。


后来经过再三讨论,最终还是决定先停掉。虽然我们笃定 IT 培训是红海,但感觉短期的大环境不行,不适合继续硬撑下去。



目前我带着这期学员即将毕业,而我们也会在 23 年停止招生。


培训的这个过程很累,一边要产出营销内容,一边要完善和设计教材、备课、一边还要批改作业。同时每天完成两个小时的不间断直播授课,不仅仅是对精力的考验,也是对嗓子的考验。


虽然创业未成,但失败是我们成长路上的垫脚石,而且这也谈不上什么失败。


我把经验教训总结如下,希望对你有所启发。


1. 市场和运营太重要了


中国有句谚语叫“酒香不怕巷子深”。意思是如果酒真香,不怕藏于深巷,人们会闻香而至。除了酒,这句话也隐喻好产品自己会说话,真才子自有人赏识。


但实际上,这是大错特错。


随着我阅历、知识、认知的提升,我越发觉得,往往越是这种一说就明白、人人都信的大道理,越是错的。


傅军老师讲过一句话:感受不等于经验、经验不等于知识、知识不等于定律、定律不等于真理。我深受启发。在这个快速发展的信息时代,必须保持独立思考的能力。


我思考的结果是:酒香也怕巷子深。


连可口可乐、路易威登、香奈儿这种世界顶级品牌每年都需要持续不断地花费大量的费用用于营销。由此可见营销的重要性,营销是一定有回报的。


我们的酒要香,同时也不能藏在巷子里。而且,我们的酒不需要特别香,只要不至于苦得无法下咽,就能卖出去。畅销品不一定就是质量好,某些国产品牌的手机就是一个简单的例子。


所以,只需要保证产品是 60 分以上就足够了,剩下的大部分精力都应该放在营销上面。绝大多数只懂得专注于打造 100 分产品的创业公司,基本上都死了。


正确的路径应该是:Poc、Alpha、Beta、MVP。就像下图所示,造车的过程中,产品在任何一个时间点都是可用的。



另外,决定一个公司成功的,一定是运营部或者市场部,绝对不是技术部。但是技术部有可能会让一个公司走向失败。如果一个公司要裁员,技术部门很可能首当其冲。当然这也要看公司文化,但大部分的 Boss 普遍更看重运营与市场。


2. 不要等万事俱备再开始


在创业早期,我们的主要招生渠道是 B 站,但我没有短视频经验。于是我买了暴漫 B 站 UP 主的课程。



本来以为买了课程就意味着学会了做视频,但实际上不是这样的。


做好一个 UP 主需要掌握非常多的知识,选题、文案、表达、拍摄、剪辑、互动,各个环节都有大学问。我很快发现不可能把整个课程全部研究透再去做视频。我需要保持频率地更新视频才能有更多人的关注。


随着业务的发展,后面主战场转到了知乎,工作内容也变成了软文和硬广。UP 主的很多技能还没来得及实践就被搁置了。


知乎也有一个写作训练营,学费大概也要三四千的样子。



我听了试听的三节课,感觉要学习的东西一样很多,所以我没有买这个课程。因为我知道我没有那么多精力。


最重要的是,我发现即使我没有什么技巧和套路,我写的软文一样有很多浏览量和转化率。知+平台的数据可以直接告诉我这篇文章或者回答写得好不好。


同时,很多相关的知识不是一成不变的,它们不是传统知识,需要从实际事物中脚踏实地地学习,我们没办法系统学习。


我们不可能做好所有的准备,但需要先去做。人生也该如此,保持渐进式。


3. 接受糟糕,不要完美主义


初创公司很多东西都是混乱的,缺乏完善的系统和结构,一切似乎都是拍脑袋决定。


报销很混乱、营销费用很混乱、内容管理很混乱、合同很混乱;甚至连招生清单都和实际上课的人对不上……


但我发现这是一个必须接受的现状,如果一切都井井有条,那就不是初创公司了。所以必须要适应这种乱糟糟、混乱的环境。


八、结婚与买房


朋友说我是一个果断的人。


因为从认识,到结婚、买房,一共用了不到一年时间。


其实起初我是强烈不建议买房的,我不看好中国的楼市,而且我们一个月花几千块租房住得非常舒服。而且我的父母在北方的城市也已经有两套房子了。但 Emma 认为没有房子不像个家,没安全感。安全感对一个女人实在太重要了,我想以她对我的了解,她怕我哪天突然悄无声息地就离开她。想了想,确实是这样,所以我就买了。


我们看了海口很多套房子,本来计划买个小两居,但看了几次,都觉得太紧凑了。我是要在家工作的,我有一张很大的双人桌,要单独留一个房间来放它。最后挑了一个三居室的房子。这种感觉和买电脑差不多,本来一台普通 Thinkpad 的预算,最终买了个 MacBookPro 顶配。



房子很漂亮,我们也很喜欢。



不过呢,同时也背负了两百万的贷款。不能再像以前那样随便花钱了。


买房只需要挑房屋布局、装修和地段,其他不需要关心。


买房贷款的注意事项:选择分期,选择还款周期最久的,不要提前还。今年由于疫情原因,房贷大概是 4.5 个点,比前几年要低。买房前保持一年流水,收入大于月供 3 倍。


还需要注意,买完房,还要考虑装修费用和家电的费用。


非刚需不建议买房,刚需的话不会考虑买房的弊端。


至于为什么结婚?


我和 Emma 都是不婚主义者。但是买房子如果不结婚,就只能写一个人的名字。但我们都不愿意写自己的名字。最后没办法,决定去办理结婚证。


但我们没有举办传统的婚礼,我们都不喜欢熙熙攘攘、吵吵闹闹。我们只想过好我们自己的生活,不希望任何人来打扰我们。


这辈子我搬了至少十次家,只有这次是搬进自己的家。


九、学习


今年在学习上主要有四个方面:



  • Web3。

  • 英语。

  • 游戏开发。

  • 自媒体。


目前对我来说,Web3 是最重要的事情。虽然我还没有 All in web3,但也差不多了。有人说 Web3 是骗局,但我认为不是骗局。


我承认 Web3 有非常多的漏洞,比如 NFT 的图片压根没上链,链上存储的只是一堆 URL。同时几乎所有用户都没有直接与区块链交互,而是与一堆中心化的平台交互。比如我们使用 MetaMask,其实数据流过了 MetaMask 和 Infura。目前的 Web3 并没有完全去中心化。


没有一个新鲜事物在一诞生就是完美的。正是由于这些不完美的地方,所以才需要我们去完善它们。


目前我已经加入了一个 Web3 团队。虽然团队不大,但是他们做的事情非常吸引我。Web3 是一个高速发展的火箭,对现在的人来说,你先上去再说,何必在乎坐在哪个位置上?


我很期待 2030 年的 Web3。


如果要进入 Web3,需要学习技术有很多,我把一些重要的技术栈列举如下:



  • Web3.js or Ethers.js or Wgami:与区块链交互的前端 SDK。

  • Solidity:智能合约编程语言。

  • Hardhat:以太坊开发环境。

  • IPFS:Web3 文件存储系统。


除了上述的技术之外,更多的是概念上的理解,比如 NFT、GameFi 相关的诸多概念等。


学习英语是因为我打算出国,同时我现在的工作环境也有很多英语,我不想让语言成为我继续提升的障碍。


至于游戏开发和自媒体,纯粹是想赚钱。


游戏开发主要是微信小游戏,靠广告费就能赚钱。微信小游戏的制作成本很低,两个人的小型团队就可以开发数款微信小游戏。而且赚钱和游戏质量并不成正比。跳一跳、羊了个羊都是成功案例。当然我的目标不是做那么大,大概只需要做到它们几百分之一的体量就非常不错了,要知道微信有将近 10 亿用户,总有人会玩你的游戏。


另外我学习自媒体和微信小游戏的原因差不多。自媒体的盘子越来越大。在 B 站、抖音这些短视频平台上,有着上千亿甚至更多的市场。这给更多人创造了创造收入的空间。


只要去做,总会有所收获。


在学习的过程中,我会记录大量笔记以及自我思考。但很少会更文。


大概在 9 月份,我参加了掘金的两次更文活动,输出了一些内容。


这个过程很累,但也有所收获。






后来考虑到时间和精力问题,所以很少更文了。


很遗憾,掘金目前只能依靠平台自身给创作者奖励,收入非常有限。如果掘金能够将变现程度发展成像 B 站、公众号或抖音那样,估计也会有更多的创作者加入。连 CSDN、博客园这些老牌产品都做不到。究其原因,还是这种性质的产品受众没有那么广泛,盘子太小了,体量自然无法涨上去。希望未来掘金能够找到属于自己的商业模式。


最后还是很感谢掘金这个平台,给了技术人们一个分享交流的空间。


十、未来


未来三年的计划是出国读硕,并全面转入 Web3 领域。


我不看好中国的经济,并且感觉在中国做生意会越来越难做。而且我更喜欢西方文化、氛围和环境。


在经历了国内疫情反复封控之后,我更加坚定了出国的打算。我不想继续生活在大陆。


我想看看外面的世界,同时系统化地提升一下自己的 CS 知识。


目前出国的思路是申请混合制研究生,F1、OPT、H1B、Green Card。目标是一所在 Chicago QS TOP500 垫底的高校。


另一条出国的路线是直接出国找一份美国的工作,但对目前的我来说是相当难,主要还是因为语言。


之所以是未来三年的计划,也是因为我的英语实在太差,目前按照 CEFR 标准,只达到了 A2。


按照 FSI 英语母语者学习外语的难度排名,中文这类语言是最难学习的。反过来,中文母语者学习英语,也是最难的。真正掌握大概需要 2200 小时,如果每天学习 3-4 小时的话,需要 88 周,将近两年。


FSI 的图片我删掉了,因为那张图上的中国版图有争议。


我的语言天赋很一般,甚至有些差。所以学习效果并不理想。我也不打算短时间内冲刺英语,因为那不太靠谱。我选择花更久的时间去磨它。


之前也想过去 Helsinki,但那边收入不高,税收太高。纠结了很久,还是觉得现在还年轻,多少还是应该有些压力的,去那种地方实在太适合养老了。


以上并不是故事,是我的亲身经历,分享出来的初衷是为大家提供更多职业和人生的思路与思考。希望对你有所帮助!



作者:代码与野兽
来源:juejin.cn/post/7178816031459115066
收起阅读 »

10个超级实用的Set、Map使用技巧

web
Set是一种类似于数组的数据结构,但是它的值是唯一的,即Set中的每个值只会出现一次。Set对象的实例可以用于存储任何类型的唯一值,从而使它们非常适用于去重。 Map是一种键值对集合,其中每个键都是唯一的,可以是任何类型,而值则可以是任何类型。Map对象的实例...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg


Set是一种类似于数组的数据结构,但是它的值是唯一的,即Set中的每个值只会出现一次。Set对象的实例可以用于存储任何类型的唯一值,从而使它们非常适用于去重。


Map是一种键值对集合,其中每个键都是唯一的,可以是任何类型,而值则可以是任何类型。Map对象的实例可以用于存储复杂的对象,并且可以根据键进行快速的查找和访问。


以下是Set和Map的一些常用方法:


Set:



  • new Set(): 创建一个新的Set对象

  • add(value): 向Set对象中添加一个新的值

  • delete(value): 从Set对象中删除一个值

  • has(value): 检查Set对象中是否存在指定的值

  • size: 获取Set对象中的值的数量

  • clear(): 从Set对象中删除所有值


Map:



  • new Map(): 创建一个新的Map对象

  • set(key, value): 向Map对象中添加一个键值对

  • get(key): 根据键获取Map对象中的值

  • delete(key): 从Map对象中删除一个键值对

  • has(key): 检查Map对象中是否存在指定的键

  • size: 获取Map对象中的键值对数量

  • clear(): 从Map对象中删除所有键值对


Set和Map是非常有用的数据结构,它们可以提高程序的性能和可读性,并且可以简化代码的编写。


Set


去重


使用 Set 可以轻松地进行数组去重操作,因为 Set 只能存储唯一的值。


const arr = [1, 2, 3, 1, 2, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]

数组转换


可以使用 Set 将数组转换为不包含重复元素的 Set 对象,再使用 Array.from() 将其转换回数组。


const arr = [1, 2, 3, 1, 2, 4, 5];
const set = new Set(arr);
const uniqueArr = Array.from(set);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

优化数据查找


使用 Set 存储数据时,查找操作的时间复杂度为 O(1),比数组的 O(n) 要快得多,因此可以使用 Set 来优化数据查找的效率。


const dataSet = new Set([1, 2, 3, 4, 5]);

if (dataSet.has(3)) {
console.log('数据已经存在');
} else {
console.log('数据不存在');
}

并集、交集、差集


Set数据结构可以用于计算两个集合的并集、交集和差集。以下是一些使用Set进行集合运算的示例代码:


const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);

// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set {1, 2, 3, 4}

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {2, 3}

// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1}

模糊搜索


Set 还可以通过正则表达式实现模糊搜索。可以将匹配结果保存到 Set 中,然后使用 Array.from() 方法将 Set 转换成数组。


const data = ['apple', 'banana', 'pear', 'orange'];

// 搜索以 "a" 开头的水果
const result = Array.from(new Set(data.filter(item => /^a/i.test(item))));
console.log(result); // ["apple"]

使用 Set 替代数组实现队列和栈


可以使用 Set 来模拟队列和栈的数据结构。


// 使用 Set 实现队列
const queue = new Set();
queue.add(1);
queue.add(2);
queue.add(3);
queue.delete(queue.values().next().value); // 删除第一个元素
console.log(queue); // Set(2) { 2, 3 }

// 使用 Set 实现栈
const stack = new Set();
stack.add(1);
stack.add(2);
stack.add(3);
stack.delete([...stack][stack.size - 1]); // 删除最后一个元素
console.log(stack); // Set(2) { 1, 2 }

Map


将 Map 转换为对象


const map = new Map().set('key1', 'value1').set('key2', 'value2');
const obj = Object.fromEntries(map);

将 Map 转换为数组


const map = new Map().set('key1', 'value1').set('key2', 'value2');
const array = Array.from(map);

记录数据的顺序


如果你需要记录添加元素的顺序,那么可以使用Map来解决这个问题。当你需要按照添加顺序迭代元素时,可以使用Map来保持元素的顺序。


const map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
map.set('d', 4);

for (const [key, value] of map) {
console.log(key, value);
}
// Output: a 1, b 2, c 3, d 4

统计数组中元素出现次数


可以使用 Map 统计数组中每个元素出现的次数。


const arr = [1, 2, 3, 1, 2, 4, 5];

const countMap = new Map();
arr.forEach(item => {
countMap.set(item, (countMap.get(item) || 0) + 1);
});

console.log(countMap.get(1)); // 2
console.log(countMap.get(2)); // 2
console.log(countMap.get(3)); // 1

统计字符出现次数


使用Map数据结构可以方便地统计字符串中每个字符出现的次数。


const str = 'hello world';
const charCountMap = new Map();
for (let char of str) {
charCountMap.set(char, (charCountMap.get(char) || 0) + 1);
}
console.log(charCountMap); // Map { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }

缓存计算结果


在处理复杂的计算时,可能需要对中间结果进行缓存以提高性能。可以使用Map数据结构缓存计算结果,以避免重复计算。


const cache = new Map();
function fibonacci(n) {
if (n === 0 || n === 1) {
return n;
}
if (cache.has(n)) {
return cache.get(n);
}
const result = fibonacci(n - 1) + fibonacci(n - 2);
cache.set(n, result);
return result;
}
console.log(fibonacci(10)); // 55

使用 Map 进行数据的分组


const students = [
{ name: "Tom", grade: "A" },
{ name: "Jerry", grade: "B" },
{ name: "Kate", grade: "A" },
{ name: "Mike", grade: "C" },
];

const gradeMap = new Map();
students.forEach((student) => {
const grade = student.grade;
if (!gradeMap.has(grade)) {
gradeMap.set(grade, [student]);
} else {
gradeMap.get(grade).push(student);
}
});

console.log(gradeMap.get("A")); // [{ name: "Tom", grade: "A" }, { name: "Kate", grade: "A" }]


使用 Map 过滤符合条件的对象


在实际开发中,我们常常需要在一个对象数组中查找符合某些条件的对象。此时,我们可以结合使用 Map 和 filter 方法来实现。比如:


const users = [
{ name: 'Alice', age: 22 },
{ name: 'Bob', age: 18 },
{ name: 'Charlie', age: 25 }
];
const userMap = new Map(users.map(user => [user.name, user]));
const result = users.filter(user => userMap.has(user.name) && user.age > 20);
console.log(result); // [{ name: 'Alice', age: 22 }, { name: 'Charlie', age: 25 }]

首先,我们将对象数组转换为 Map,以便快速查找。然后,我们使用 filter 方法来过滤符合条件的对象。


这里我们列举了一些使用SetMap的实用技巧,它们可以大大简化你的代码,并使你更有效地处理数据。SetMap是JavaScript中非常有用的数据结构,值得我们在编写代码时好好利用。


系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7225425984312328252
收起阅读 »

《如何超过大多数人》——陈皓(左耳朵耗子)

文章原文链接为:如何超过大多数人 ps:读这篇文章前先看看下面这段话,避免误导大家。 切记,这篇文章不要过度深思(任何东西都无法经得起审视,因为这世上没有同样的成长环境,也没有同样的认知水平同时也没有适用于所有人的解决方案;也不要去急着评判里面列出的观点,只...
继续阅读 »

文章原文链接为:如何超过大多数人



ps:读这篇文章前先看看下面这段话,避免误导大家。


切记,这篇文章不要过度深思(任何东西都无法经得起审视,因为这世上没有同样的成长环境,也没有同样的认知水平同时也没有适用于所有人的解决方案;也不要去急着评判里面列出的观点,只需代入到其中适度审视一番自己即可,能跳脱出来从外人的角度看看现在的自己处在什么样的阶段就行。具体怎么想怎么做全在你自己去不断实践中寻找那个适合自己的方案



正文开始:


当你看到这篇文章的标题,你一定对这篇文章产生了巨大的兴趣,因为你的潜意识在告诉你,这是一本人生的“武林秘籍”,而且还是左耳朵写的,一定有干货满满,只要读完,一定可以练就神功并找到超过大多数人的快车道和捷径……


然而…… 当你看到我这样开篇时,你一定会觉得我马上就要有个转折,告诉你这是不可能的,一切都需要付出和努力……然而,你错了,这篇文章还真就是一篇“秘籍”,只要你把这些“秘籍”用起来,你就一定可以超过大多数人。而且,这篇文章只有我这个“人生导师”可以写得好。毕竟,我的生命过到了十六进制2B的年纪,踏入这个社会已超过20年,舍我其谁呢?!


P.S. 这篇文章借鉴于《如何写出无法维护的代码》一文的风格……嘿嘿


相关技巧和最佳实践


要超过别人其实还是比较简单的,尤其在今天的中国,更是简单。因为,你只看看中国的互联网,你就会发现,他们基本上全部都是在消费大众,让大众变得更为地愚蠢和傻瓜。所以,在今天的中国,你基本上不用做什么,只需要不使用中国互联网,你就很自然地超过大多数人了。当然,如果你还想跟他们彻底拉开,甩他们几个身位,把别人打到底层,下面的这些“技巧”你要多多了解一下。


在信息获取上,你要不断地向大众鼓吹下面的这些事:



ps:是像大众哈。不要看错用在自己身上【狗头】


自己怎么做呢?反着做不就行了吗》》》




  • 让大家都用百度搜索引擎查找信息,订阅微信公众号或是到知乎上学习知识……要做到这一步,你就需要把“百度一下”挂在嘴边,然后要经常在群或朋友圈中转发微信公众号的文章,并且转发知乎里的各种“如何看待……”这样的文章,让他们爱上八卦,爱上转发,爱上碎片

  • 让大家到微博或是知识星球上粉一些大咖,密切关注他们的言论和动向…… 是的,告诉大家,大咖的任何想法一言一行都可以在微博、朋友圈或是知识星球上获得,让大家相信,你的成长和大咖的见闻和闲扯非常有关系,你跟牛人在一个圈子里你也会变牛。

  • 把今日头条和抖音这样的APP推荐给大家……你只需要让你有朋友成功地安装这两个APP,他们就会花大量的时间在上面,而不能自拔,要让他们安装其实还是很容易的,你要不信你就装一个试玩一会看看(嘿嘿嘿)。

  • 让大家热爱八卦,八卦并不一定是明星的八卦,还可以是你身边的人,比如,公司的同事,自己的同学,职场见闻,社会热点,争议话题,……这些东西总有一些东西会让人心态有很多微妙的变化,甚至花大量的时间去搜索和阅读大量的观点,以及花大量时间与人辩论争论,这个过程会让人上瘾,让人欲罢不能,然而这些事却和自己没有半毛钱关系。你要做的事就是转发其中一些SB或是很极端的观点,造成大家的一睦讨论后,就早早离场……

  • 利用爱国主义,让大家觉得不用学英文,不要出国,不要翻墙,咱们已经是强国了……这点其实还是很容易做到的,因为学习是比较逆人性的,所以,只要你鼓吹那些英文无用论,出国活得更惨,国家和民族都变得很强大,就算自己过得很底层,也有大国人民的感觉。


然后,在知识学习和技能训练上,让他们不得要领并产生幻觉



  • 让他们混淆认识和知识,以为开阔认知就是学习,让他们有学习和成长的幻觉……

  • 培养他们要学会使用碎片时间学习。等他们习惯利用碎片时间吃快餐后,他们就会失去精读一本书的耐性……

  • 不断地给他们各种各样“有价值的学习资料”,让他们抓不住重点,成为一个微信公众号或电子书“收藏家”……

  • 让他们看一些枯燥无味的基础知识和硬核知识,这样让他们只会用“死记硬背”的方式来学习,甚至直接让他们失去信心,直接放弃……

  • 玩具手枪是易用的,重武器是难以操控的,多给他们一些玩具,这样他们就会对玩具玩地得心应手,觉得玩玩具就是自己的专业……

  • 让他们喜欢直接得到答案的工作和学习方式,成为一个伸手党,从此学习再也不思考……

  • 告诉他们东西做出来就好了,不要追求做漂亮,做优雅,这样他们就会慢慢地变成劳动密集型……

  • 让他们觉得自己已经很努力了,剩下的就是运气,并说服他们去‘及时行乐’,然后再也找不到高阶和高效率学习的感觉……

  • 让他们觉得读完书”、“读过书”就行了,不需要对书中的东西进行思考,进行总结,或是实践,只要囫囵吞枣尽快读完就等同于学好了……


最后,在认知和格局上,彻底打垮他们,让他们变成韭菜。



  • 让他们尽可能地用拼命和加班,尽可能的996,并告诉他们这就是通往成功的唯一路径。这样一来,他们必然会被永远困在低端成为最低的劳动力

  • 让他们不要看到大的形势,只看到眼前的一亩三分地,做好一个井底之蛙。其实这很简单,就是不要告诉他还有另外一种活法,不要扩大他的认识……

  • 宣扬一夜暴富以及快速挣钱的案例,最好让他们进入“赌博类”或是“传销类”的地方,比如:股市、数字货币……要让他们相信各种财富神话,相信他们就是那个幸运儿,他们也可以成为巴菲特,可以成为马云……

  • 告诉他们,一些看上去很难的事都是有捷径的,比如:21天就能学会机器学习,用区块链就能颠覆以及重构整个世界等等……

  • 多跟他们讲一些小人物的励志的故事,这样让他们相信,不需要学习高级知识,不需要掌握高级技能,只需要用低等的知识和低级的技能,再加上持续不断拼命重复现有的工作,终有一天就会成功……

  • 多让他们跟别人比较,人比人不会气死人,但是会让人变得浮躁,变得心急,变得焦虑,当一个人没有办法控制自己的情绪,没有办法让自己静下心来,人会失去耐性和坚持,开始好大喜欢功,开始装逼,开始歪门邪道剑走偏锋……

  • 让他们到体制内的一些非常稳定的地方工作,这样他们拥有不思进取、怕承担责任、害怕犯错、喜欢偷懒、得过且过的素质……

  • 让他们到体制外的那些喜欢拼命喜欢加班的地方工作,告诉他们爱拼才会赢,努力加班是一种福报,青春就是用来拼的,让他们喜欢上使蛮力的感觉……

  • 告诉他们你的行业太累太辛苦,干不到30岁。让他们早点转行,不要耽误人生和青春……

  • 当他们要做决定的时候,一定要让他们更多的关注自己会失去的东西,而不是会得到的东西。培养他们患得患失心态,让他们认识不到事物真正的价值,失去判断能力……(比如:让他们觉得跟对人拍领导的马屁忠于公司比自我的成长更有价值)

  • 告诉他们,你现有的技能和知识不用更新,就能过好一辈子,新出来的东西没有生命力的……这样他们就会像我们再也不学习的父辈一样很快就会被时代所抛弃……

  • 每个人都喜欢在一些自己做不到的事上找理由,这种能力不教就会,比如,事情太多没有时间,因为工作上没有用到,等等,你要做的就是帮他们为他们做不到的事找各种非常合理的理由,比如:没事的,一切都是最好的安排;你得不到的那个事没什么意思;你没有面好主要原因是那个面试官问的问题都是可以上网查得到的知识,而不没有问到你真正的能力上;这些东西学了不用很快会忘了,等有了环境再学也不迟……


最后友情提示一下,上述的这些“最佳实践”你要小心,是所谓,贩毒的人从来不吸毒,开赌场的人从来不赌博!所以,你要小心别自己也掉进去了!这就是“欲练神功,必先自宫”的道理。


相关原理和思维模型


对于上面的这些技巧还有很多很多,你自己也可以发明或是找到很多。所以,我来讲讲这其中的一些原理。


一般来说,超过别人一般来说就是两个维度:



  1. 在认知、知识和技能上。这是一个人赖以立足社会的能力(参看《程序员的荒谬之言还是至理名言?》和《21天教你学会C++》)

  2. 在领导力上。所谓领导力就是你跑在别人前面,你得要有比别人更好的能力更高的标准(参看《技术人员发展之路》)


首先,我们要明白,人的技能是从认识开始,然后通过学校、培训或是书本把“零碎的认知”转换成“系统的知识”,而有要把知识转换成技能,就需要训练和实践,这样才能完成从:认识 -> 知识 -> 技能 的转换。


这个转换过程是需要耗费很多时间和精力的,而且其中还需要有强大的学习能力和动手能力,这条路径上有很多的“关卡”,每道关卡都会过滤掉一大部分人。比如:对于一些比较枯燥的硬核知识来说,90%的人基本上就倒下来,不是因为他们没有智商,而是他们没有耐心。


认知


要在认知上超过别人,就要在下面几个方面上做足功夫:


1)信息渠道。试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真,甚至会被传递信息的中间人hack其中的信息(也就是“中间人攻击”),而这些找不出信息源的人,只能“被人喂养”,于是,他们最终会被困在信息的底层,永世不得翻身。(比如:学习C语言,放着原作者K&R的不用,硬要用错误百出谭浩强的书,能有什么好呢?)


2)信息质量。信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级,我们都知道,在大数据处理中有一句名言,叫 garbage in garbage out,你天天看的都是垃圾,你的思想和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。


3)信息密度。优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事,


a)搜索并学习其关联的知识


b)沉思和反省


c)亲手去推理、验证和实践……


一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的书,而像Netflix的官方blogAWS CTO的blog等等地方也会经常有一些这样的文章。


知识


要在知识上超过别人,你就需要在下面几个方面上做足功夫:


1)知识树(图)任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)。而我们都知道,对于一棵树来说,“根基”是非常重要的,所以,学好基础知识也是非常重要的,对于一个陌生的地方,有一份地图是非常重要的,没有地图的你只会乱窜,只会迷路、练路、走冤枉路!


2)知识缘由。任何知识都是有缘由的,了解一个知识的来龙去脉和前世今生,会让你对这个知识有非常强的掌握,而不再只是靠记忆去学习。靠记忆去学习是一件非常糟糕的事。而对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。


3)方法套路学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路


技能

要在技能上超过别人,你就需要在下面几个方面做足功夫:


1)精益求精。如果你想拥有专业的技能,你要做不仅仅是拼命地重复一遍又一遍的训练,而是在每一次重复训练时你都要找到更好的方法,总结经验,让新的一遍能够更好,更漂亮,更有效率,否则,用相同的方法重复,那你只不过在搬砖罢了。


2)让自己犯错。犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。尤其是当你在出错后,被人鄙视,被人嘲笑后,你会有更大的动力提升自己,这样的动力才是进步的源动力。当然,千万不要同一个错误重复地犯!


3)找高手切磋。下过棋,打个球的人都知道,你要想提升自己的技艺,你必需找高手切磋,在和高手切磋的过程中你会感受到高手的技能和方法,有时候你会情不自禁地哇地一下,我靠,还可以这么玩!


领导力


最后一个是领导力,要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系,因为一个人的领导力跟他的标准很有关系,因为有领导力的人的标准比绝大多数人都要高。


1)识别自己的特长和天赋。首先,每个人DNA都可能或多或少都会有一些比大多数人NB的东西(当然,也可能没有),如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。那种,别人要非常努力,而且毫不费劲的事。一旦你有了这样的特长或天赋,那你就要大力地扩大你的领先优势,千万不要进到那些会限制你优势的地方。你是一条鱼,你就一定要把别人拉到水里来玩,绝对不要去陆地上跟别人拼,不断地在自己的特长和天赋上扩大自己的领先优势,彻底一骑绝尘。


2)识别自己的兴趣和事业。没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师,当年,Linus就是在学校里对minx着迷了,于是整出个Linux来,这就是兴趣驱动出的东西,一般来说,兴趣驱动的事总是会比那些被动驱动的更好。但是,这里我想说明一下什么叫“真∙兴趣”,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真∙兴趣”,这也就是你的“野心”和“好胜心”所在,其实上升到了你的事业。相信我,绝大多数人只有职业而没有事业的。


3)建立高级的习惯和方法。没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。对此,在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性,比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。除此之外,你还需要在方法上超过别人,你需要满世界的找各种高级的方法,其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。


4)勤奋努力执着坚持。如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了(参看《21天教你学会C++》中的十年学编程一节),我见过很多不聪明的人,悟性也不够(比如我就是一个),别人学一个东西,一个月就好了,而我需要1年甚至更长,但是很多东西都是死的,只要肯花时间就有一天你会搞懂的,耐不住我坚持十年二十年,聪明的人发明个飞机飞过去了,笨一点的人愚公移山也过得去,因为更多的人是懒人,我不用拼过聪明人,我只用拼过那些懒人就好了。


好了,就这么多,如果哪天你变得消极和不自信,你要来读读我的这篇文章

作者:𝓑𝓮𝓲𝓨𝓪𝓷𝓰
来源:juejin.cn/post/7207648496978870333
,子曰:温故而知新。

收起阅读 »

Dart 3.0 语法新特性 | Records 记录类型 (元组)

1. 记录类型的声明与访问 通过 () 将若干个对象组合在一块,作为一个新的聚合类型。定义时可以直接当放入对象,也可以进行命名传入:var record = ('first', a: 2, b: true, 'last'); print(record.runt...
继续阅读 »

1. 记录类型的声明与访问


通过 () 将若干个对象组合在一块,作为一个新的聚合类型。定义时可以直接当放入对象,也可以进行命名传入:

var record = ('first', a: 2, b: true, 'last');
print(record.runtimeType);

--->[打印输出]---
(String, String, {int a, bool b})

上面的 record 对象由四个数据构成,通过 runtimeType 可以查看到其运行时类型,类型为各个非命名数据类型 + 各命名类型。



非命名类型数据可以通过 $index 进行访问:

print(record.$1);
print(record.$2);

--->[打印输出]---
first
last


命名类型数据可以通过 名称 进行访问:

print(record.a);
print(record.b);

--->[打印输出]---
2
true

注意: 一个记录对象的数据不允许被修改:


image.png




2. 记录类型声明对象


一个 Records 本质上也是一种类型,可以用该类型来声明对象,比如现在通过 (double,double,double) 的记录类型表示三个坐标,如下定义 p0 和 p1 对象:

void main() {
(double x, double y, double z) p0 = (1, 2, 3);
(double x, double y, double z) p1 = (1, 2, 6);
}

既然可以实例化为对象,那么自然也可以将其作为参数类型传入函数中,如下 distance 方法传入两个三维点,计算距离:

double distance(
(double x, double y, double z) p0,
(double x, double y, double z) p1,
) {
num result = pow(p0.$1 - p1.$1, 2) + pow(p0.$2 - p1.$2, 2) + pow(p0.$3 - p1.$3, 2);
return sqrt(result);
}

但记录类型一旦显示声明,写起来比较繁琐;和函数类型类似,也可以通过 typedef 来定义类型的别名。如下所示,定义 Point3D 作为别名,功能是等价的,但书写和可读性会更好一些:

typedef Point3D = (double, double, double);

void main() {
Point3D p0 = (1, 2, 3);
Point3D p1 = (1, 2, 6);

print(distance(p0, p1));
}

double distance(Point3D p0, Point3D p1) {
num result = pow(p0.$1 - p1.$1, 2) + pow(p0.$2 - p1.$2, 2) + pow(p0.$3 - p1.$3, 2);
return sqrt(result);
}

同理,记录类型也可以作为返回值,这样可以解决一个函数返回多值的问题。如下 addTask 方法可以计算 1 ~ count 的累加值,返回计算结果和耗时毫秒数:

({int result, int cost}) addTask2(int count) {
int start = DateTime.now().millisecondsSinceEpoch;
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}
int end = DateTime.now().millisecondsSinceEpoch;
return (
result: sum,
cost: end - start,
);
}



3. 记录类型对象的等值


记录类型会根据字段的结构自动定义 hashCode 和 == 方法。 所以两个记录对象相等,就是其中的各个数值相等。但是通过 identical 可以看出 p0 和 p1 仍是两个对象,内存地址不同:

(double, double, double) p0 = (1, 2, 3);
(double, double, double) p1 = (1, 2, 3);
print(p0 == p1);
print(identical(p0, p1));

--->[打印输出]---
true
false

如下所示,第二个数据是 List<double> 类型,两个 [2] 是两个不同的对象,所以 p2,p3 不相等:

(double, List<double>, double) p2 = (1, [2], 3);
(double, List<double>, double) p3 = (1, [2], 3);
print(p2==p3);

--->[打印输出]---
false

下面测试中, 列表使用同一对象,则 p2,p3 相等:

List<double> li = [2];
(double, List<double>, double) p2 = (1, li, 3);
(double, List<double>, double) p3 = (1, li, 3);
print(p2==p3);

--->[打印输出]---
true



4. 记录类型的价值


对于编程语言来说,Dart 的记录类型也不是什么新的东西,就是其他语言中的元组。如下所示,可以创建一个 TaskResult 类来维护数据作为返回值。但如果只是返回一些临时的数据,为此新建一个类来维护数据就会显得比较繁琐,还要定义构造函数。

class TaskResult{
final int result;
final int cost;

TaskResult(this.result, this.cost);
}

TaskResult addTask(int count) {
int start = DateTime.now().millisecondsSinceEpoch;
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}
int end = DateTime.now().millisecondsSinceEpoch;
return TaskResult(sum, end - start);
}



除此之外,一个函数返回多个数据也可以使用 Map 对象:

Map<String,dynamic> addTask(int count) {
int start = DateTime.now().millisecondsSinceEpoch;
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}

int end = DateTime.now().millisecondsSinceEpoch;
return {
'result' : sum,
'cost': end - start
};
}

但这种方式的弊端也很明显,返回和使用时都需要固定的字符串作为 key。如果 key 写错了,代码在运行前也不会有任何错误,这样很容易出现风险。多人协作时,而且如果函数的书写者和调用者不是一个人,那该使用什么键得到什么值就很难分辨。

Map<String,dynamic> task = addTask2(100000000);
print(task['result']);
print(task['cost']);

所以,相比于新建 class 或通过 Map 来维护多个数据,使用记录类型更加方便快捷和精确。但话说回来,如果属性数据量过多,使用记录类型看起来会非常麻烦,也不能定义成员方法来操作、修改内部数据。所以它有自己的特点使用场景,比如临时聚合多个数据来方便使用。


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

程序员的十级孤独,你体会过几级

都说天才程序员是和疯子就差一步之遥。有极致孤独与追求,有自己的精神世界, 我就是有代码洁癖的,追求极致代码要求。如何看到别人写代码不按照自己的,来我就会很抓马 就会不通过他的代码质量检查,以致于我现在有点极致的病态要求了 尤其作为项目中的leader 我感受...
继续阅读 »

都说天才程序员是和疯子就差一步之遥。有极致孤独与追求,有自己的精神世界,


我就是有代码洁癖的,追求极致代码要求。如何看到别人写代码不按照自己的,来我就会很抓马 就会不通过他的代码质量检查,以致于我现在有点极致的病态要求了


尤其作为项目中的leader 我感受更加深刻


独立思考的孤独


作为项目负责人,上面是PE时间项目成本的压力,下面是产品经理的压力,你经常会被夹在中
非常的难受,左右为难,于是你在开发项目之前,你需要纠结很久,需要在产品质量,和时间成本上左右为难。


作为一个有代码洁癖,项目契约精神的人,我经常为这种事件很崩溃。


到底是为了,赚钱而赚钱,还是为了❓。


记得我入行的热爱,是对编程感兴趣。说好要 写出改变世界的产品呢,


一个28岁程序员入行自述和感受


团队合作中的孤独



记一次线上生产事故



作为项目负责人,线上生成事故是需要负责任的,按照事故级别和影响范围时间,会扣除相应工资,绩效等。严重还可能被开除。


每次项目发版上线,你必须要在现场,不然你根本不放心,所以你变成不管是不是你的任务,你变成一定是最晚走哪个。


每次线上事故,bug报警一定最先找到你。应为留的报警错误电话,邮箱全部都是你,我记得有一次线上生产活动促销出问题了。半夜,电话被打爆了。


产品,测试,老板,各个电话第一个找到的就是你。所以那以后你几乎,电脑离不开,手机什么24小时待命。 精神会高度集中。(毕竟这是老板要求


我们项目发版,都是等到晚上10点发布版本。到了时间。测试通知发布堡垒环境,在测试走一遍,没问题了。会发布到生产环境,在看一遍,确认没问题就可以下班。


遇到问题,就炸锅,你需要协调各个部位,问题方。出现问题要抓紧修复解决,那一般没有到凌晨 是下不了班,回不了家的。



你加班看到过,凌晨红日早霞吗



失败的孤独


测试无法通过、代码错误等失败体验日常存在,尤其在项目版本环境升级时候,解决日益增长的用户,和系统负载问题,成了你最大的技术难点


这些情况增加了恐惧心理重负,会形成工作时的孤独体验


社交困境下的孤独


你们公司有每周,项目分享交流会吗,需要你不断,更新学习了解最前沿的项目技术。


作为leader 你不得不分享。


这个时候各个部门的开发都会到场,你需要分享你对一些技术看法,和了解,他怎么样的应用场景。你得作为新技术的 带头人。


可能会在一些新的项目中去使用新的技术,因为这样会有一下 绩效奖励


比如 创新奖励,专利奖励(哈哈哈哈哈)


值班的孤独


有时,孤独是一种磨练 。有时,孤独是一种思念
孤独,我们每个人都经历过


孤独让我们与众不同


孤独将我们成就,孤独令我们优秀


有人说是,过年不回家


我觉得 可能是没有时间找女朋友吧!


孤独是一种病 久了 也就习惯了!


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


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


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

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


这些经验对正在阅读文章的你有用吗,欢迎一起交流,让我们一起交流您遇到的问题。 


这篇文章是去年写的,今天增加了点内容,掘金上同步更新了一下,希望可以被更多的人看到。


如果这篇文章说道你心里了,可以点赞、分享、评论、收藏、转发哦。


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

真正的成长没有速成剂,都是风吹雨打过来的

一个人真正的成长一定是极其不容易的,如果想通过一两本书,一两个鸡汤文案,一两场培训就能够获得成长,那简直是痴人说梦,真正的成长一定不会是轻松的,一定是经过一次又一次的跌倒,然后爬起,对所做过的事,所经历的事进行一次又一次的复盘,总结,思考,最终才能慢慢认识到事...
继续阅读 »

一个人真正的成长一定是极其不容易的,如果想通过一两本书,一两个鸡汤文案,一两场培训就能够获得成长,那简直是痴人说梦,真正的成长一定不会是轻松的,一定是经过一次又一次的跌倒,然后爬起,对所做过的事,所经历的事进行一次又一次的复盘,总结,思考,最终才能慢慢认识到事物的本质,成长不是时间的堆积,也不会因为年龄递增而获得。


思考的难关


毫不夸张的说,思考是这个世界上最难的事,因为思考是要动脑的,而在现在这个信息爆炸的时代,我们想要任何资讯,任何知识,都可以找到现成的答案,所以懒惰就此滋生出来,“都有现成的答案了,我干嘛还要去动脑,我动脑得到的东西也未必有现成的好,而且动脑肚子还消耗能量,这种消耗不亚于体力劳动”,所以思考是最难的,而思考也是获取知识最快的途径。


我们在读书的时候,有些同学看似十分努力,一天感觉都是泡在书本里面的,但是成绩往往都不理想,初高中时,班上有些同学十分努力,对于我这种混子来说,我一定是扛不住的,就比如背英语单词,我发现有一些同学采用“原始人”的方式去背诵,因为我们整个初中换了三四个英语老师,而每个老师的教学方式不一样,其中一个老师就是教死记硬背,英语单词就比如“good”,她的方式是,“g o o d , g o o d”,也就是一个字母一个字母的背诵,后来因为一些原因,又换了老师,老师又教了音标,但是最后我还是发现,很多同学依旧还是“g o o d”,后来我才发现,因为学音标还需要花时间,还要动点脑子,对于一个单词,还有不同的情况,所以还是司机硬背好,这种方式就是“蛮力”,其实只要稍微花点时间去研究一下音标,然后再好好学学,背单词就会轻松很多,所以初高中英语成绩一直比较好,当然,现在很差,词汇量很少,完全是后面吃了懒惰的大亏。


所以,思考虽然是痛苦的,但是熬过痛苦期,就能够飞速成长,如果沉浸在自我感动的蛮力式努力中,那么只会离成长越来越远。


懒惰的魔咒


说到懒惰,我们可能会想到睡懒觉,不努力学习,不努力工作,但这其实并不是懒惰,每天起得很早去搬砖,日复一日地干着重复的事,却没有半点成长,这才是真正的懒惰。


没有思考的勤快是一文不值的,在现在这个社会,各种工具十分普遍,如果我们依旧保持原始人的工作方式,那么最终只会把自己累死,就像很多统计工作,如果认为自己加班到十二点,人工统计出数据来,老板就会很欣赏你,觉得你很吃苦耐劳,那么这是愚蠢的,因为有很多工具可能五分钟就能够搞出来,可偏偏固执去搞一些没用的东西,这有用吗,还有现在是人工智能时代,各种GPT工具那么爽,直接让效率翻倍,但是我就是不用,我就喜欢自己从头搞,那也没办法。


很多时候,所谓的勤快不过是为了掩饰自己的懒惰而已,懒惰得不愿意去接受新的事物,不愿意去学习新东西,总觉得“老一套“万能。


成长过程中,要不断打破自己的认知,冲破自己的心灵上的懒惰,拥抱新鲜事物,这样才不至于和主流脱节。


环境的影响



在南瓜里度日,就成圆形;在竹子里生活,就成长形。



一个人的环境同样是塑造成长的重要因素。环境不仅指物理环境,也包括人际环境和心理环境。在环境中,我们需要学会适应和改变环境,让环境成为我们成长的动力。


人以类聚,物以群分,如果我们身边的人都是不思上进,终日惶惶,那么长时间下来,我们也会受影响,读书时,如果身边的同学都好学,那么自己也绝对不会变得很烂,相反,如果同学都整天无所事事,那么自己自然也不会好到哪里去,当身边的人都是勤于思考,有想法,那么大家就会有一个良好的氛围,这样成长得就比较快,工作中,如果大家都很有热情,分享很多,学习很多,那么自己也不好意思,自然也会去学习。


但是我们每个人的能力都不一样,所以遇到的环境也不一样,所以很多时候,这并不是我们能选择的,所以说在自己没能力选择的时候,那么就要克制自己,别人混,自己不能混,要时刻提醒自己不能松懈,也不要因为别人的闲言碎语而去”同流合污“,始终记住,一切都是为了自己变得更好,不要太在意别人的看法。


保持良好的心态


在这个浮躁的社会,我们的思想和意志总是被这个社会所影响,特别现在短视频如此火爆,”脉脉上面低于百万年薪不好意思发言,抖音上面人均劳斯莱斯,自己同学朋友又买了几套房“,我们的心态时不时会受到打击,原本平稳的步伐一下变得不稳了,想一步升天了,但是当步子迈大了,可能就受伤了。


我们要时刻提醒自己自己是为自己而活,无论别人是真还是假,和自己关系不大,不要被外界过于影响,这个世界上没有一个人的成功是轻易的,都是在黑暗中努力发光的,如果相信了速成,相信快速致富,那么镰刀造已经嫁到脖子上了,而且还割坏了很多把,一茬接着一茬!


即使此刻多么的不堪,也不要放弃,积累自己,也许有一天,我们再相逢,睁开眼睛看,我才是英雄,他日若遂凌云志,敢笑黄巢不丈夫!


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

Android gradle迁移至kts

背景 在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持...
继续阅读 »

背景


在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持kotlin好久了,但是由于编译速度或者转换成本的原因,真正实现kts转换的项目很少。在笔者的mac m1 中使用最新版的AS去编译build.gradle.kts,速度已经是和用groovy写的gradle脚本不相上下了,所以就准备写了这篇文章,希望做一个记录与分享。



















groovykotlin
好处:构建速度较快,运用广泛,动态灵活好处:编译时完成所有,语法简洁,android项目中可用一套语言开发构建脚本与app编写
坏处:语法糖的作用下,很难理解gradle运行的全貌,作用单一,维护成本较高坏处:编译略慢于groovy,学习资料较少

虽然主流的gradle脚本编写依旧是groovy,但是android开发者官网也在推荐迁移到kotlin


编译前准备


这里推荐看看这篇文章,里面也涵盖了很多干货,


全局替换‘’为“”


在kotlin中,表示一个字符串用“”,不同于groovy的‘ ’,所以我们需要全局替换。可以通过快捷方式command/control+shift+R 全局替换,选中匹配正则表达式并设定file mask 为 *.gradle:

正则表达式
'(.*?[^\\])'
作用范围为
"$1"

image.png


全局替换方法调用


在groovy中,方法是可以隐藏(),举个例子

apply plugin: "com.android.application"

这里实际上是调用apply方法,然后命名参数是plugin,内容围为"com.android.application",然而在kotlin语法中,我们需要以()或者invoke的方式才能调用一个方法,所以我们要给所有的groovy函数调用添加()

正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)

image.png
很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为

task(sourcesJar(type: Jar) {
from(android.sourceSets.main.java.srcDirs)
classifier = "sources"
})

gradle kt化


接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。


开始前准备


以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如


image.png
对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用


ext


我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!

val myNewProperty by extra("initial value")

但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!


task



  • 新建task
groovy版本

task clean(type: Delete) {
delete rootProject.buildDir
}

比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称

task<Delete>("clean",{
delete(rootProject.buildDir)
})

当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过

 ./gradlew help --task task名

去查看相应的类型



  • 已有task修改
    对于有些是已经在gradle编译时存在的函数任务,比如
groovy版本

wrapper{
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}

这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到

tasks {
named<Wrapper>("wrapper") {
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN

}
}

这种方式,去找到一个我们想要的task,并配置其内容



  • 生命周期函数
    我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast
tasks.create("greeting") {
doLast { println("Hello, World!") }
}

再比如dependOn

task<Jar>("javadocJar", {
dependsOn(tasks.findByName("javadoc"))
})

动态函数


sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员

sourceSets{
val main by getting{
jniLibs.srcDirs("src/main/libs")
jni.srcDirs()
}

}

也可以通过getByName方式去获取

sourceSets.getByName("main")

plugins


在比较旧的版本中,我们AS默认创建引入一个plugin的方式是

apply plugin: 'com.android.application'

其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成

plugins {
id("com.android.application")
}

就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。


说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!


Plugins解析


我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看


DefaultScriptPluginFactory

            final ScriptTarget initialPassScriptTarget = initialPassTarget(target);

ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);

// 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
CompileOperation<?> initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
Class<? extends BasicScript> scriptType = initialPassScriptTarget.getScriptClass();
ScriptRunner<? extends BasicScript, ?> initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
initialRunner.run(target, services);

PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

// 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
final ScriptTarget scriptTarget = secondPassTarget(target);
scriptType = scriptTarget.getScriptClass();

CompileOperation<BuildScriptData> operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);

final ScriptRunner<? extends BasicScript, BuildScriptData> runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
scriptTarget.attachScript(runner.getScript());
}
if (!runner.getRunDoesSomething()) {
return;
}

Runnable buildScriptRunner = () -> runner.run(target, services);

boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
}




可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。


image.png


那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码


重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段

public class DefaultCompileOperationFactory implements CompileOperationFactory {
private static final StringInterner INTERNER = new StringInterner();
private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
private static final String BODY_COMPILE_STAGE = "BODY";

private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
private final DocumentationRegistry documentationRegistry;

public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
this.documentationRegistry = documentationRegistry;
}

public CompileOperation<?> getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
}

public CompileOperation<BuildScriptData> getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
String operationId = scriptTarget.getId();
return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
}
}

getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行


@Override
public Statement transform(SourceUnit sourceUnit, Statement statement) {
...

if (scriptBlock.getName().equals(PLUGINS)) {
return transformPluginsBlock(scriptBlock, sourceUnit, statement);
}
...


总结


文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!


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

Android的线程和线程池

从用途上来说Android的线程主要分为主线程和子线程两类,主线程主要处理和界面相关的工作,子线程主要处理耗时操作。除Thread之外,Android中还有其他扮演线程的角色如AsyncTask、IntentService、HandleThread,其中Asy...
继续阅读 »

从用途上来说Android的线程主要分为主线程和子线程两类,主线程主要处理和界面相关的工作,子线程主要处理耗时操作。除Thread之外,Android中还有其他扮演线程的角色如AsyncTask、IntentService、HandleThread,其中AsyncTask的底层用到了线程池,IntentService和HandleThread的底层直接使用了线程。


AsyncTask内部封装了线程池和Handler主要是为了方便开发者在在线程中更新UI;HandlerThread是一个具有消息循环的线程,它的内部可以使用Handler;IntentService是一个服务,系统对其进行了封装使其可以更方便的执行后台任务,IntentService内部采用HandleThread来执行任务,当任务执行完毕后IntentService会自动退出。IntentService是一个服务但是它不容易被系统杀死因此它可以尽量的保证任务的执行。


1.主线程和子线程


主线程是指进程所拥有的的线程,在Java中默认情况下一个进程只能有一个线程,这个线程就是主线程。主线程主要处理界面交互的相关逻辑,因为界面随时都有可能更新因此在主线程不能做耗时操作,否则界面就会出现卡顿的现象。主线程之外的线程都是子线程,也叫做工作线程。


Android沿用了Java的线程模型,也有主线程和子线程之分,主线程主要工作是运行四大组件及处理他们和用户的交互,子线程的主要工作就是处理耗时任务,例如网络请求,I/O操作等。Android3.0开始系统要求网络访问必须在子线程中进行否则就会报错,NetWorkOnMainThreadException


2.Android中的线程形态


2.1 AsyncTask


AsyncTask是一个轻量级的异步任务类,它可以在线程池中执行异步任务然后把执行进度和执行结果传递给主线程并在主线程更新UI。从实现上来说AsyncTask封装了Thread和Handler,通过AsyncTask可以很方便的执行后台任务以及主线程中访问UI,但是AsyncTask不适合处理耗时任务,耗时任务还是要交给线程池执行。


AsyncTask的四个核心类如下:





    • onPreExecute():主要用于做一些准备工作,在主线程中执行异步任务执行之前

    • doInBackground(Params ... params):在线程池执行,此方法用于执行异步任务,params表示输入的参数,在此方法中可以通过publishProgress方法来更新任务进度,publishProgress会调用onProgressUpdate

    • onProgressUpdate(Progress .. value):在主线程执行,当任务执行进度发生改变时会调用这个方法

    • onPostExecute(Result result):在主线程执行,异步任务之后执行这个方法,result参数是返回值,即doInBackground的返回值。




2.2 AsyncTask的工作原理


2.3 HandleThread


HandleThread继承自Thread,它是一种可以使用Handler的Thread,它的实现在run方法中调用Looper.prepare()来创建消息队列然后通过Looper.loop()来开启消息循环,这样在实际使用中就可以在HandleThread中创建Handler了。

@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

HandleThread和Thread的区别是什么?





    • Thread的run方法中主要是用来执行一个耗时任务;

    • HandleThread在内部创建了一个消息队列需要通过Handler的消息方式来通知HandleThread执行一个具体的任务,HandlerThread的run方法是一个无限循环因此在不使用是调用quit或者quitSafely方法终止线程的执行。HandleTread的具体使用场景是IntentService。




2.4 IntentService


IntentService继承自Service并且是一个抽象的类因此使用它时就必须创建它的子类,IntentService可用于执行后台耗时的任务,当任务执行完毕后就会自动停止。IntentService是一个服务因此它的优先级要比线程高并且不容易被系统杀死,因此可以利用这个特点执行一些高优先级的后台任务,它的实现主要是HandlerThread和Handler,这点可以从onCreate方法中了解。

//IntentService#onCreate
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

当IntentService第一次被启动时回调用onCreate方法,在onCreate方法中会创建HandlerThread,然后使用它的Looper创建一个Handler对象ServiceHandler,这样通过mServiceHandler把消息发送到HandlerThread中执行。每次启动IntentService都会调用onStartCommand,IntentService在onStartCommand中会处理每个后台任务的Intent。

//IntentService#onStartCommand
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

//IntentService#onStart
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

onStartCommand是如何处理外界的Intent的?


在onStartCommand方法中进入了onStart方法,在这个方法中IntentService通过mserviceHandler发送了一条消息,然后这个消息会在HandlerThread中被处理。mServiceHandler接收到消息后会把intent传递给onHandlerIntent(),这个intent跟启动IntentService时的startService中的intent是一样的,因此可以通过这个intent解析出启动IntentService传递的参数是什么然后通过这些参数就可以区分具体的后台任务,这样onHandleIntent就可以对不同的后台任务做处理了。当onHandleIntent方法执行结束后IntentService就会通过stopSelf(int startId)方法来尝试停止服务,这里不用stopSelf()的原因是因为这个方法被调用之后会立即停止服务但是这个时候可能还有其他消息未处理完毕,而采用stopSelf(int startId)方法则会等待所有消息都处理完毕后才会终止服务。调用stopSelf(int startId)终止服务时会根据startId判断最近启动的服务的startId是否相等,相等则立即终止服务否则不终止服务。


每执行一个后台任务就会启动一次intentService,而IntentService内部则通过消息的方式向HandlerThread请求执行任务,Handler中的Looper是顺序处理消息的,这就意味着IntentService也是顺序执行后台任务的,当有多个后台任务同时存在时这些后台任务会按照外界发起的顺序排队执行。


3.Android中的线程池


线程池的优点:





    • 线程池中的线程可重复使用,避免因为线程的创建和销毁带来的性能开销;

    • 能有效控制线程池中的最大并发数避免大量的线程之间因互相抢占系统资源导致的阻塞现象;

    • 能够对线程进行简单的管理并提供定时执行以及指定间隔循环执行等功能。




Android的线程池的概念来自于Java中的Executor,Executor是一个接口,真正的线程的实现是ThreadPoolExecutor,它提供了一些列参数来配置线程池,通过不同的参数可以创建不同的线程池。


3.1 ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

ThreadPoolExecutor是线程池的真正实现,它的构造函数中提供了一系列参数,先看一下每个参数的含义:





    • corePoolSize:线程池的核心线程数,默认情况下核心线程会在线程池中一直存活即使他们处于闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeOut置为true那么闲置的核心线程在等待新的任务到来时会有超时策略,超时时间由keepAliveTime指定,当等待时间超过keepAliveTime设置的时间后核心线程就会被终止。

    • maxinumPoolSize:线程池中所能容纳的最大线程数,当活动线程达到做大数量时后续的新任务就会被阻塞。

    • keepAliveTime:非核心线程闲置时的超时时长,超过这个时长非核心线程就会被回收。

    • unit:用于指定超时时间的单位,常用单位有毫秒、秒、分钟等。

    • workQueue:线程池中的任务队列,通过线程池中的execute方法提交的Runnable对象会存储在这个参数中。

    • threadFactory:线程工厂,为线程池提供创建新的线程的功能。

    • handler:这个参数不常用,当线程池无法执行新的任务时,这可能是由于任务队列已满或者无法成功执行任务,这个时候ThreadPoolExecutor会调用handler的rejectExecution方法来通知调用者。




ThreadPoolExecutor执行任务时大致遵循如下规则:





    1. 如果线程池中的线程数量没有达到核心线程的数量那么会直接启动一个核心线程来执行任务;

    2. 如果线程池中线程数量已经达到或者超过核心线程的数量那么会把后续的任务插入到队列中等待执行;

    3. 如果任务队列也无法插入那么在基本可以确定是队列已满这时如果线程池中的线程数量没有达到最大值就会立刻创建非核心线程来执行任务;

    4. 如果非核心线程的创建已经达到或者超过线程池的最大数量那么就拒绝执行此任务,同时ThreadPoolExecutor会通过RejectedExecutionHandler抛出异常rejectedExecution。




3.2线程池的分类



  • FixedThreadPool:它是一种数量固定的线程池,当线程处于空闲状态时也不会被回收,除非线程池被关闭。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有空闲线程出来。FixedThreadPool只有核心线程并且不会被回收因此它可以更加快速的响应外界的请求。

  • CacheThreadPool:它是一种线程数量不定的线程池且只有非核心线程,线程的最大数量是Integer.MAX_VALUE,当线程池中的线程都处于活动状态时如果有新的任务进来就会创建一个新的线程去执行任务,同时它还有超时机制,当一个线程闲置超过60秒时就会被回收。

  • ScheduleThreadPool:它是一种拥有固定数量的核心线程和不固定数量的非核心线程的线程池,当非核心线程闲置时会立即被回收。

  • SignleThreadExecutor:它是一种只有一个核心线程的线程池,所有任务都在同一个线程中按顺序执行。

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

我从 Android 官方 App 中学到了什么?

最近 Android 官方开源了一个新的 App: Now in Android ,这个 App 主要展示了其他 App 可能没有的一些最佳实践、架构设计、以及完整的线上 App (后面会发布到 Google Play 商店中)解决方案,其次是帮助开发者及时了...
继续阅读 »

最近 Android 官方开源了一个新的 App: Now in Android ,这个 App 主要展示了其他 App 可能没有的一些最佳实践、架构设计、以及完整的线上 App (后面会发布到 Google Play 商店中)解决方案,其次是帮助开发者及时了解到自己感兴趣的 Android 开发领域。现在已经在 GitHub 中开源。


通过这篇文章你可以了解到 Now in Android 的应用架构:分层、关键类以及他们之间的交互。


目标&要求


App 的架构目标有以下几点:



  • 尽可能遵循 官方架构指南

  • 易于开发人员理解,没有什么太实验性的特性。

  • 支持多个开发人员在同一个代码库上工作。

  • 在开发人员的机器上和使用持续集成 (CI) 促进本地和仪器测试。

  • 最小化构建时间。


架构概述


App 目前包括 Data layerUI layerDomain layer 正在开发中。


Diagram showing overall app architecture


该架构遵循单向数据流的响应式编程方式。Data Layer 位于底层,主要包括:



  • UI Layer 需对 Data Layer 的变化做出反应。

  • 事件应向下流动。

  • 数据/状态应向上流动。


数据流是采用 Kotlin Flows 来实现的。


示例:在 For you 页面展示新闻信息


App 首次运行的时候,会尝试从云端加载新闻列表(选择 stagingrelease 构建变体时,debug 构建将使用本地数据)。加载后,这些内容会根据用户选择的兴趣显示给用户。


下图详细展示了事件以及数据是流转的。


Diagram showing how news resources are displayed on the For You screen


下面是每一步的详细过程。 Code 列中的内容是对应的代码,可以下载项目后在 Android Studio 查看。






























































步骤描述Code
1App 启动的时候,WorkManager 的同步任务会把所有的 Repository 添加到任务队列中。SyncInitializer.create
2初始状态会设置为 Loading,这样会在 UI 页面上展示一个旋转的动画。ForYouFeedState.Loading
3WorkManager 开始执行 OfflineFirstNewsRepository 中的同步任务,开始同步远程的数据源。SyncWorker.doWork
4OfflineFirstNewsRepository 开始调用 RetrofitNiaNetwork 开始使用 Retrofit 进行真正的网络请求。OfflineFirstNewsRepository.syncWith
5RetrofitNiaNetwork 调用云端接口。RetrofitNiaNetwork.getNewsResources
6RetrofitNiaNetwork 接收到远程服务器返回的数据。RetrofitNiaNetwork.getNewsResources
7OfflineFirstNewsRepository 通过 NewsResourceDao 将远程数据更新(增删改查)到本地的 Room 数据库中。OfflineFirstNewsRepository.syncWith
8NewsResourceDao 中的数据发生变化的时候,其会被更新到新闻的数据流(Flow)中。NewsResourceDao.getNewsResourcesStream
9OfflineFirstNewsRepository 扮演数据流中的 中间操作符, 将 PopulatedNewsResource (数据层内部数据库的一个实体类) 转换成公开的 NewsResource 实体类供其他层使用。OfflineFirstNewsRepository.getNewsResourcesStream
10ForYouViewModel 接收到 Success 成功, ForYouScreen 会使用新的 State 来渲染页面。页面将会展示最新的新闻内容。ForYouFeedState.Success

Data Layer


数据层包含 App 数据以及业务逻辑,会优先提供本地离线数据,它是 App 中所有数据的唯一信源。


Diagram showing the data layer architecture


每个 Repository 中都有自己的实体类(model/entity)。如,TopicsRepository 包含 Topic 实体类, NewsRepository 包含 NewsResource 实体类。


Repository 是其他层的公共的 API,提供了访问 App 数据的唯一途径。Repository 通常提供一种或多种数据读取和写入的方法。


读取数据


数据通过数据流提供。这意味着 Repository 的调用者都必须准备好对数据的变化做出响应。数据不会作为快照(例如 getModel )提供,因为无法确保它在使用时仍然有效。


Repository 以本地存储数据作为单一信源,因此从实例读取时不会出现错误。但是,当尝试将本地存储中的数据与云端数据进行合并时,可能会发生错误。有关错误的更多信息,请查看下面的数据同步部分。


示例:读取作者信息


可以用过订阅 AuthorsRepository::getAuthorsStream 发出的流来获得 List<Authors> 信息。每当作者列表更改时(例如,添加新作者时),更新后的 List<Author> 的内容都会发送到数据流中。如下:

class OfflineFirstTopicsRepository @Inject constructor(  
private val topicDao: TopicDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences,
) : TopicsRepository {

// 监听 Room 数据的变化,当数据发生变化的时候,调用者就会收到对应的数据
override fun getTopicsStream(): Flow<List<Topic>> = topicDao.getTopicEntitiesStream().map {
it.map(TopicEntity::asExternalModel)
}

// ...
}

写入数据


为了写入数据,Repository 库提供了 suspend 函数。由调用者来确保它们在合适的 scope 中被执行。


示例: 关注 Topic


调用 TopicsRepository.setFollowedTopicId 将用户想要关注的 topic id 传入即可。


OfflineFirstTopicsRepository 中定义:

interface TopicsRepository : Syncable {

suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)

}

ForYouViewModel 中定义:

class ForYouViewModel @Inject constructor(
private val topicsRepository: TopicsRepository,
// ...
) : ViewModel() {
// ...

fun saveFollowedInterests() {
// ...
viewModelScope.launch {
topicsRepository.setFollowedTopicIds(inProgressTopicSelection)
// ...
}
}
}

数据源(Data Sources)


Repository 可能依赖于一个或多个 DataSource。例如,OfflineFirstTopicsRepository 依赖以下数据源:



























名称使用目的
TopicsDaoRoom/SQLite持久化和 Topics 相关的关系型数据。
NiaPreferencesProto DataStore持久化和用户相关的非结构化偏好数据,主要是用户感兴趣的 Topics 内容。这里使用的是 .proto 文件。
NiANetworkRetrofit云端以 JSON 形式提供对应的 Topics 数据。

数据同步


Repository 的职责之一就是整合本地数据与云端数据。一旦从云端返回数据就会立即将其写入本地数据中。更新后的数据将会从本地数据(Room)中发送到相关的数据流中,调用者便可以监听到对应的变化。


这种方法可确保应用程序的读取和写入关注点是分开的,不会相互干扰。


在数据同步过程中出现错误的情况下,应采用对应的回退策略。App 中是经由 SyncWorker 代理给 WorkManager 的。 SyncWorkerSynchronizer 的实现类。


可以通过 OfflineFirstNewsRepository.syncWith 来查看数据同步的示例,如下:

class OfflineFirstNewsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao,
private val episodeDao: EpisodeDao,
private val authorDao: AuthorDao,
private val topicDao: TopicDao,
private val network: NiANetwork,
) : NewsRepository {

override suspend fun syncWith(synchronizer: Synchronizer) =
synchronizer.changeListSync(
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
// ...
}
)
}

UI Layer


UI Layer 包含:



ViewModelRepository 接收数据流并将其转换为 UI State。UI 元素根据 UI State 进行渲染,并为用户提供了与 App 交互的方式。这些交互作为事件(UI Event)传递到对应的 ViewModel 中。


Diagram showing the UI layer architecture


构建 UI State


UI State 一般是通过接口和 data class 来组装的密封类。State 对象只能通过数据流的转换发出。这种方法可确保:



  • UI State 始终代表底层应用程序数据 - App 中的单一信源。

  • UI 元素处理所有可能的 UI State


示例:For You 页面的新闻列表


For You 页面的新闻列表数据源是 ForYouFeedState ,他是一个 sealed interface 类,包含 LoadingSuccess 两种状态:



  • Loading 表示数据正在加载。

  • Success 表示数据加载成功。Success 状态包含新闻资源列表。
sealed interface ForYouFeedState {
object Loading : ForYouFeedState
data class Success(val feed: List<SaveableNewsResource>) : ForYouFeedState
}

ForYouScreen 中会处理 feedState 的这两种状态,如下:

private fun LazyListScope.Feed(
feedState: ForYouFeedState,
//...
) {
when (feedState) {
ForYouFeedState.Loading -> {
// show loading
}
is ForYouFeedState.Success -> {
// show feed
}
}
}

将数据流转换为 UI State


ViewModel 从一个或者多个 Repository 中接收数据流当做冷 。将他们一起 组合 成单一的 UI State。然后使用 stateIn 将冷流转换成热流。转换的状态流使 UI 元素可以读取到数据流中最后的状态。


示例: 展示已关注的话题及作者


InterestsViewModel 暴露 StateFlow<FollowingUiState> 类型的 uiState 。通过组合 4 个数据流来创建热流:



  • 作者列表

  • 已关注的作者 ID 列表

  • Topics 列表

  • 已关注 Topics 列表的 IDs


Author 转换为 FollowableAuthorFollowableAuthor 是对 Author 的包装类, 添加了当前用户是否已经关注了作者。对 Topic 也做了相同转换。 如下:

    val uiState: StateFlow<InterestsUiState> = combine(
authorsRepository.getAuthorsStream(),
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getTopicsStream(),
topicsRepository.getFollowedTopicIdsStream(),
) { availableAuthors, followedAuthorIdsState, availableTopics, followedTopicIdsState ->

InterestsUiState.Interests(
// 将 Author 转换为 FollowableAuthor,FollowableAuthor 是对 Author 的包装类,
// 添加了当前用户是否已经关注了作者
authors = availableAuthors
.map { author ->
FollowableAuthor(
author = author,
isFollowed = author.id in followedAuthorIdsState
)
}
.sortedBy { it.author.name },
// 将 Topic 转换为 FollowableTopic,同 Author
topics = availableTopics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in followedTopicIdsState
)
}
.sortedBy { it.topic.name }
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)

两个新的列表创建了新的 FollowingUiState.Interests UiState 暴露给 UI 层。


处理用户交互


用户对 UI 元素的操作通过常规的函数调用传递给 ViewModel ,这些方法作为 lambda 表达式传递给 UI 元素。


示例:关注话题


InterestsScreen 通过 followTopic lambda 表达式传递事件,然后会调用到 InterestsViewModel.followTopic 函数。当用户点击关注话题的时候,函数将会被调用。然后 ViewModel 就会通过通知 TopicsRepository 处理对应的用户操作。


如下在 InterestsRoute 中关联 InterestsScreenInterestsViewModel

@Composable  
fun InterestsRoute(
modifier: Modifier = Modifier,
navigateToAuthor: (String) -> Unit,
navigateToTopic: (String) -> Unit,
viewModel: InterestsViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val tabState by viewModel.tabState.collectAsState()

InterestsScreen(
uiState = uiState,
tabState = tabState,
followTopic = viewModel::followTopic,
// ...
)
}

@Composable
fun InterestsScreen(
uiState: InterestsUiState,
tabState: InterestsTabState,
followTopic: (String, Boolean) -> Unit,
// ...
) {
//...
}


扩展阅读


本文主要是根据 Now in Android 中的 Architecture Learning Journey 整理而得,感兴趣的可以进一步阅读原文。除此之外,还可以进一步学习 Android 官方相关的资料:



关于架构指南部分,我之前也整理了部分对应的解读部分,大家可以移步查看



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

程序员能有什么好出路?

关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。 如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的...
继续阅读 »

关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。


如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的描述出来了:

我30岁了,没啥竞争力,未来何去何从?

出路耶


我从事这个行业,其实是个人挺喜欢编程的,觉得编程是一件挺舒心的事情,所以没有考虑过换行。周围其实有一些同事,离开了这个行当,有一些赚了更多的钱,也有一些日子过的更不舒心,这里不予置评。


我简单的叙述一些可能的出路,这些出路没什么对错的区别,只是在我们人生抉择中,希望你能看到更多的选项。


技术深造


如果你在技术上有优势,这是一条可以走通的路子,未来的方向大致是“架构师”、“技术顾问”等等。这需要你有一些大型项目的经验,所以一些在大型公司就业的程序员,天然的拥有更多的机会。


通常技术深造主要是两部分:



  1. 技术视野,你需要一定的知识广度,对常用技术有深刻的理解,对部分不常用技术也要熟悉。

  2. 技术能力,有的时候,亲自动手能力、解决问题能力会很重要。


项目管理


很多程序员转行做了项目管理,其实在我们的日常工作中,项目管理一直伴随着我们,时长日久,我们对项目管理会变的更熟悉一些。这也造成了一些错觉,让我们觉得项目管理没那么难,“我去我也行”。


但是,项目管理从来不是一项普通的工作,相对于程序员,项目管理人员面临的环境会更加复杂。



  1. 面对客户。有时候,会遇见一些喜欢刁难我们的客户的。

  2. 面对团队。团队也可能不和谐。

  3. 计划乱了、工期排期、风险控制、质量管理、干系人管理等等专业知识。


自由职业


依赖于自己过硬的技术,可以承接一些外包的项目,成为一名自由的外包人员。



  1. 你的人际关系会很重要。周围有一些能打单的朋友,会让你工作的很舒服。

  2. 把事情做好,赢得信赖。

  3. 来自第三方平台的外包项目还是比较坑的,尽量做熟人生意。


跑单


当然,你在行业内可能会认识不少的朋友,他们的手里可能有些业务需要外包人员进行开发,那么拿下这些合同,找到自己朋友里面有时间做私活的人,然后我完成它。



  1. 你的人际关系更为重要。通常,这会给你带来财富。

  2. 做好自己的品牌,赢得认可,那么就有赢得钞票的机会。


插件独立开发者


一个人开发一个应用,然后上架,成功率是很低的。所以依托于平台,做一些平台内的插件,然后依托于平台推广,那么成功的几率会大一些。



  1. 你的技术能力很重要,毕竟没有专门的测试人员进行测试。

  2. 你选择的平台很重要,比如跨境电商、钉钉、微信、谷歌浏览器等等。

  3. 更加重要的是,你要对这个方向感兴趣。


独立开发者


如果你财富自由了,又喜欢编程,可以成为一名伟大的独立开发者,你脑海中的任何想法,都可以通过双手变为现实。



  1. 因为热爱,所以你会有更多的可能。

  2. 能力足够,可以参与开源的基金会,参与一些开源项目。

  3. 如果财富没自由,那也不影响我们在闲暇时间里追逐我们的梦想。


团购


IT行业是一个挺特殊的团体,他们的某些消费习惯趋于雷同,针对这些消费习惯和爱好,做一些团购,相信会赚到不少钱。



  1. 还是人际关系。

  2. 你喜欢做这些事情,从免费到收费循序渐进。

  3. 记住,双赢才能长久,IT行的聪明人是比较多的。


大公司养老团


找个大的,稳定的公司养老,但是也要留好退路,居安思危。


其他


比如炒股、搞理财的、做导游的、创业的……


每个人都会有自己的选择,有的人做好了准备,有的人还懵懵懂懂,2023年的行情如何还未可知,希望能长风破浪吧


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

JS中的高阶函数

web
JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。 具体来说,高阶函数可以...
继续阅读 »

JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。



具体来说,高阶函数可以有以下几种形式:



  1. 接受函数作为参数的高阶函数


function map(array, fn) {
let result = [];
for (let i = 0; i < array.length; i++) {
result.push(fn(array[i]));
}
return result;
}

let numbers = [1, 2, 3, 4, 5];
let squaredNumbers = map(numbers, function(x) {
return x * x;
});
console.log(squaredNumbers); // [1, 4, 9, 16, 25]

在上面的例子中,map函数接受一个数组和一个函数作为参数,然后使用该函数对数组中的每个元素进行转换,并返回转换后的结果。




  1. 返回函数的高阶函数


function multiplyBy(n) {
return function(x) {
return x * n;
};
}

let double = multiplyBy(2);
let triple = multiplyBy(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30

在上面的例子中,multiplyBy函数返回一个函数,该函数可以将传入的参数乘以n。我们可以使用multiplyBy函数创建一个新的函数,然后使用该函数对不同的值进行乘法运算。




  1. 同时接受和返回函数的高阶函数


function compose(f, g) {
return function(x) {
return f(g(x));
};
}

function square(x) {
return x * x;
}

function addOne(x) {
return x + 1;
}

let addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(3)); // 16

在上面的例子中,compose函数接受两个函数作为参数,然后返回一个新的函数,该函数首先对输入值应用g函数,然后将结果传递给f函数,并返回f(g(x))的结果。我们可以使用compose函数创建一个新的函数,该函数可以将其他两个函数的功能组合在一起,以实现更复杂的操作。



其实,即使是业务代码中也会有很多用到高阶函数的地方,比如数组的迭代方法(map、filter、reduce等)、定时器(setTimeout和setInterval),还有比较典型的函数柯理化、函数组合(compose)、偏函数等,通过使用高阶函数,我们可以将常见的操作抽象出来,并将它们作为可重用的函数进行封装,从而使代码更加简洁、灵活和易于维护。





在使用高阶函数时,有时候需要注意回调函数中的上下文问题。如果回调函数中的this关键字不是指向我们期望的对象,就会导致程序出现错误。为了解决这个问题,可以使用bindapplycall等方法来明确指定回调函数的上下文。


let obj = {
value: 0,
increment: function() {
this.value++;
}
};

let arr = [1, 2, 3, 4, 5];

arr.forEach(obj.increment.bind(obj));
console.log(obj.value); // 5

在上面的例子中,obj.increment.bind(obj)会返回一个新函数,该函数会将this关键字绑定到obj对象上。我们可以使用这个新函数来作为forEach方法的回调函数,以确保increment方法的上下文指向obj对象。



其余还有诸如函数副作用问题、内存占用问题和性能问题等。为了解决这些问题,可以使用一些优化技巧,比如明确指定回调函数的上下文、使用纯函数、使用函数柯里化或函数组合等。这些技巧可以帮助我们更加灵活地使用高阶函数,并提高代

作者:施主来了
来源:juejin.cn/post/7232838211030302777
码的性能和可维护性。

收起阅读 »

函数实现单例模式

web
单例模式 一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。 class Foo { static instance; static init() { if (!this.instance) t...
继续阅读 »

wallhaven-gpqye7.jpg


单例模式


一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。


class Foo {
static instance;
static init() {
if (!this.instance) this.instance = new Foo();
return this.instance;
}
constructor() {}
}

// 将单例实例化 并暴露出去
export default Foo.init()


如此,我们就实现了简单的单例模式,并且在其他文件引入的时候已经是实例化过一次的了,或者交由用户者自行调用 init 也是可以的



函数实现


而在函数的实现上,其实本身类就是函数的某种抽象,如果去掉这个 new 的话,单纯用函数又是怎么做的呢?


let ipcMainInstance;
export default () => {
const init = () => {
return {
name: "phy",
hobby: "play games"
};
};

return () => {
if (!ipcMainInstance) {
ipcMainInstance = init();
}
return ipcMainInstance;
};
};

使用


const ipcInit = createIpc();
ipcInit();


因为我们使用的是二阶函数进行 init,所以写法上是二次调用才是 init,每个人的设计写法不一样。



然而这种写法上,每次都要写一个 init 方法进行单例实例化的包裹,这明显是一个重复工作,我们是否可以将 init 方法独立成一个函子,让他帮我们自动将我们传进去的函数进行处理,返回来的就是一个单例模式的函数呢?


抽象单例模式函子


// 非void返回值
type NonVoidReturn<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R extends void
? never
: T
: any;

/**
* 创建单例模式的函子
* @param {function} fn
* @returns {any} fn调用的返回值 必须得有return 可推断
*/

const createSgp = <T extends (...args: any) => any>(fn: NonVoidReturn<T>) => {
let _instance: undefined | ReturnType<T>;

return () => {
if (!_instance) {
_instance = fn();
}
return _instance;
};
};

export default createSgp;


使用上



import createSgp from "./createSgp";

const useAuto = () => {
let count = 0;

const setCount = (num: number) => {
count = num;
};

const getCount = () => count

return {
getCount,
setCount
};
};

// 将其处理成单例模式 并且暴露出去
export default createSgp(useAuto);


如此我们就完成了单例模式的包裹处理,并且是一个单例模式的函数。



对于hooks使用单例模式函数的问题


其实上面的操作看起来很酷,实际上很少会用到,因为你得考虑到,我用单例模式的意义是什么,如果这个函数只需要调用一次,那么就有必要用单例模式,但是hooks一般用到的时候,都属于操作性逻辑,尽量不应在hooks里面去做hooks初始化时有函数自执行调用,这个调用应该交由用户去做,我是这么理解hooks的,而这也就导致,hooks不应该用单例了,而且hooks用单例会有bug,请看下面的代码:


  let count = 0;
const useCount = {
count,
add(num){
count += num
}
}

这里我就一次简化useCount的return出来的东西,那么我们思考下,如果说,这个add在外部调用了,那么这个count会变吗?答案是不会,为什么呢?



因为当前add操作的count,是外部的count,并不是return对象的count,这句话可能很绕,但是仔细思考,一开始useCount(),他return的count是长什么样,此时,他其实就是数字0,那么,add改的count真的是这个return对象的count吗?相信说到这里,你就懂为什么了。



那我如果真的要联动到这个count,怎么做呢?


  const useCount = {
count: 0,
add(num){
this.count += num
}
}


答案是,用到this,此时这个add操作的count就是此时return 对象的count了,而这也跟类一个原理了,因为类更改的成员属性,都是实例对象本身的,而不是外部的,所以,他能更新上。这个问题,也是后面我发现的,所以以此记录一下。



作者:phy_lei
来源:juejin.cn/post/7232499216529834039
收起阅读 »

小程序轮播图的高度如何与图片高度保持一致

web
一、存在现象 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例: 这是在iPhone Xr上的显示效果...
继续阅读 »

一、存在现象




  • 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例:




  • 这是在iPhone Xr上的显示效果:轮播图的指示点显示正常
    image.png




  • 这是在iPhone 5上的显示效果:轮播图的指示点就到图片下方去了
    image.png




二、解决方法


思路



  • 在图片加载完成后,获取到图片的高度,获取到之后进行赋值。这样的话,我们需要使用image标签的bindload属性,当图片加载完成时触发


image.png



  • 获取图片高度,可以当做获取这个轮播图组件的高度,这组件是小程序界面上的一个节点,可以使用获取界面上的节点信息APIwx.createSelectorQuery()来获取


const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // #the-id节点的上边界坐标
res[1].scrollTop // 显示区域的竖直滚动位置
})



  • 节点信息查询 API 可以用于获取节点属性、样式、在界面上的位置等信息。最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。如下图所示,里面有我们所需要的height,我们将这个height赋值给swiper组件,再令image标签mode="widthFix",即可自动适应轮播图高度和图片的高度保持一致



    • widthFix:缩放模式,宽度不变,高度自动变化,保持原图宽高比不变

    • HeightFix:缩放模式,高度不变,宽度自动变化,保持原图宽高比不变




  • 这是iPhone Xr上的数据,height:152.4375
    image.png




  • 这是iPhone 5上的数据,height:118.21875
    image.png




实现



  • wxml:轮播图


<swiper class="swiper" autoplay indicator-dots circular interval="{{4000}}" style="height: {{swiperHeight}}px;">
<block wx:for="{{banners}}" wx:key="bannerId">
<swiper-item class="swiper-item">
<image class="swiper-image" src="{{item.pic}}" mode="widthFix" bindload="getSwiperImageLoaded"></image>
</swiper-item>
</block>
</swiper>


  • js:只展示获取图片高度的代码,像获取轮播图数据代码已省略


Page({
data: {
swiperHeight: 0, // 轮播图组件初始高度
},

// 图片加载完成
getSwiperImageLoaded() {
// 获取图片高度
const query = wx.createSelectorQuery();
query.select(".swiper-image").boundingClientRect();
query.exec((res) => {
this.setData({ swiperHeight: rect.height });
});
},
})


  • 在上述代码中getSwiperImageLoaded方法也可以进行抽离到utils中成为一个工具函数,并用Promise进行返回,方便其他地方需要使用到


export default function (selector) {
return new Promise((resolve) => {
const query = wx.createSelectorQuery();
query.select(selector).boundingClientRect();
query.exec(resolve)
});
}


  • 所以在上述的实现代码中getSwiperImageLoaded方法可以进行如下的优化:


getSwiperImageLoaded() {
// 优化
queryRect(".swiper-image").then((res) => {
const rect = res[0];
this.setData({ swiperHeight: rect.height });
});
},


  • 如此一来,在iPhone 5上的轮播图组件展示也正常


image.png



  • 最后,因为获取的是轮播图,那么获取的数据就不止一条,按以上代码逻辑,获取到多少条数据就会执行多少遍setData赋值操作,所以可以考虑使用防抖或者节流进行进一步优化。


作者:晚风予星
来源:juejin.cn/post/7232625387296129080
收起阅读 »

CSS小技巧之圆形虚线边框

web
虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码: border-style: dotted dashed solid double; 这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的...
继续阅读 »

虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码:


border-style: dotted dashed solid double;

这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的边框样式为实线,左边的边框样式为双线。如下图所示:



border-style 除了上面所支持的样式还有 groove ridge inset outset 3D相关的样式设置,关于 border-style 的相关使用本文并不过多介绍,有兴趣的可以看官方文档。本文主要介绍使用CSS渐变实现更自定义化的虚线边框,以满足需求中的特殊场景使用。如封面图所示的6种情况足以体现足够自定义的边框样式,接下来看实现方式。


功能分析


基于封面图分析实现这类虚线边框应该满足一下几个功能配置:



  • 虚线的点数量

  • 虚线的颜色,可以纯色,多个颜色,渐变色

  • 虚线的粗细程度

  • 虚线点之间的间隔宽度


由于我们是自定义的虚线边框,所以尽可能不增加额外的元素,所以虚线的内容使用伪元素实现,然后使用定位覆盖在元素内容的上方,那么你肯定有疑问了,既然是覆盖在元素的上方,那不上遮挡了元素本身吗?



来到本文自定义圆形虚线边框的关键部分,这里我们使用CSS mask 实现,并配合使用 -webkit-mask-composite: source-in 显示元素本身的内容。



-webkit-mask-composite: 属性指定了将应用于一个元素的多个蒙版图像合成显示。当一个元素存在多重 mask 时,我们就可以运用 -webkit-mask-composite 进行效果叠加。



代码实现


首先基于上面分析的几个功能配置进行变量定义,方便后续更改变量值即可调整边框样式。


--n:20;   /* 控制虚线数量 */
--d:8deg; /* 控制虚线之间的距离 */
--t:5px; /* 控制虚线的粗细 */
--c:red; /* 控制虚线的颜色 */

对应不同的元素传入不同的值:


<div class="box" style="--n:3;--t:8px;--d:10deg;--c:linear-gradient(45deg,red,blue)">3</div>
<div class="
box" style="--n:6;--t:12px;--d:20deg;--c:green">6</div>

然后给伪元素设置基础的样式,定位,背景色,圆角等。


.box::after {
content: "";
position: absolute;
border-radius: 50%;
background: var(--c);
}

按不同的元素传入不同的背景色,最终的效果是这样的。



继续设置在mask中设置一个重复的锥形渐变 repeating-conic-gradient,代码如下:


repeating-conic-gradient(
from calc(var(--d)/2),
#000 0 calc(360deg/var(--n) - var(--d)),
#0000 0 calc(360deg/var(--n))
)



  • from calc(var(--d)/2) 定义了渐变的起点,以虚线之间的距离除以2可以让最终有对称的效果




  • #000 0 calc(360deg/var(--n) - var(--d)):定义了第一个颜色为黑色(#000),起点位置为0,终止位置为360deg/var(--n) - var(--d)度,基于虚线之间的距离和虚线的个数计算出每段虚线的渐变终止位置




  • #0000 0 calc(360deg/var(--n)):定义了第二个颜色为透明色,起点位置为0,终止位置为基于虚线的个数计算,这样与上一个颜色的差即是 --d 的距离,也就是我们控制虚线之间的距离。




基于上述代码现在的界面是如下效果:



上面有提到 -webkit-mask-composite 是应用于一个元素的多个蒙版图像合成显示,所以我们这里需要在mask中再增加一个蒙板进行合成最终的效果。


增加以下代码到mask中:


linear-gradient(#0000 0 0) content-box

注意这里使用了content-box作为背景盒模型,这意味着背景颜色只会应用到元素的内容区域,这段代码将创建一个只在元素内容区域的水平线性渐变背景,且是完全透明的背景色。


为什么是内容区域,因为这里和padding有关联,我们将定义的控制虚线的粗细 --t:5px; 应用到了伪元素的 padding 中。


padding: var(--t);

这样刚刚新增的透明背景就只会应用到下图的蓝色内容区域,再结合 -webkit-mask-composite,即``只剩下 padding 部分的内容,也就是我们的自定义边框部分。



增加以下代码:


-webkit-mask-composite: source-in;

即是最终的效果,因为这里增加的mask背景是透明色,这里 -webkit-mask-composite 的属性不限制使用 source-in, 其他的好几个都是一样的效果,有兴趣的可以了解了解。



都已经到这一步了,是不是应该再增加一些效果呢,给这个圆形的边框增加动起来的效果看看,增加一个简单的旋转动画 animation: rotate 5s linear infinite;,这样看着是不是更有感觉,适用的场景就多了。



码上掘金在线预览:



最后


到此整体代码实现就结束了,看完是不是感觉挺简单的,基于伪元素设置锥形渐变 repeating-conic-gradient并配合-webkit-mask-composite实现自定义圆形虚线边框的效果。这里是设置了 border-radius:50%; 圆角最终呈现的是圆形,有兴趣的可以更改CSS代码试试其他的形状颜色间距等。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


参考



codepen.io/t_afif/pen/…



作者:南城FE
来源:juejin.cn/post/7233052510553522213
收起阅读 »

我竟然完美地用js实现默认的文本框粘贴事件

web
前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状 废话连篇 默认情况对一个文本框粘贴,应该会有这样的功能: 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后 将选中的文字替换成粘贴的文本 但是由于需求,我们需要拦截粘...
继续阅读 »

前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状



废话连篇


默认情况对一个文本框粘贴,应该会有这样的功能:



  1. 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后

  2. 将选中的文字替换成粘贴的文本


但是由于需求,我们需要拦截粘贴的事件,对剪贴板的文字进行过滤,这时候粘贴的功能都得自己实现了,而一旦自己实现,上面2个功能就不见了,我们就需要还原它。


面对这样的需求,我们肯定要控制移动光标,可是现在的网上环境真的是惨,千篇一律的没用代码...于是我就发表了这篇文章。


先上代码


    <textarea id="text" style="width: 996px; height: 423px;"></textarea>
<script>
// 监听输入框粘贴事件
document.getElementById('text').addEventListener('paste', function (e) {
e.preventDefault();
let clipboardData = e.clipboardData.getData('text');
// 这里写你对剪贴板的私货
let tc = document.querySelector("#text");
tc.focus();
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
if(tc.selectionStart != tc.selectionEnd){
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionEnd)
}else{
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
}

// 重新设置光标位置
tc.selectionEnd =tc.selectionStart = start
});
</script>


怎么理解上述两个功能?
第一个解释:
比如说现在文本框有:



染念真的很生气



如果我们现在在真的后面粘贴不要,变成



染念真的不要很生气|



拦截后的光标是在生气后面,但是我们经常使用发现,光标应该出现在不要的后面吧!
就像这样:



染念真的不要|很生气



第2个解释:



染念真的不要很生气



我们全选真的的同时粘贴求你,拦截后会变成



染念真的求你不要很生气|



但默认应该是:



染念求你|不要很生气



代码分析


针对第2个问题,我们应该先要获取默认的光标位置在何处,tc.selectionStart是获取光标开始位置,tc.selectionEnd是获取光标结束位置。
为什么这里我写了一个判断呢?因为默认时候,我们没有选中一块区域,就是把光标人为移动到某个位置(读到这里,光标在位置后面,现在人为移动到就是前面,这个例子可以理解不?),这个时候两个值是相等的。



233|333


^--- ^


1-- - 4


tc.selectionEnd=4,tc.selectionStart = 4



如果相等,说明就是简单的定位,tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart); ,tc.value.substring(0,tc.selectionStart)获取光标前的内容,tc.value.substring(tc.selectionStart)是光标后的内容。
如果不相等,说明我们选中了一个区域(光标选中一块区域说明我们选中了一个区域),代码只需要在最后获取光标后的内容这的索引改成tc.selectionEnd



|233333|


^----- ^


1----- 7


tc.selectionEnd=7,tc.selectionStart = 1



在获取光标位置之前,我们应该先使用tc.focus();聚焦,使得光标回到文本框的默认位置(最后),这样才能获得位置。
针对第1个问题,我们就要把光标移动到粘贴的文本之后,我们需要计算位置。
获得这个位置,一定要在tc.value重新赋值之前,因为这样的索引都没有改动。
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;这个代码和上面解释重复,很简单,我就不解释了。
最后处理完了,重新设置光标位置,tc.selectionEnd =tc.selectionStart = start,一定让selectionEnd和selectionStart相同,不然选中一个区域了。


如果我们在value重新赋值之后获取(tc.value.substr(0,tc.selectionStart)+clipboardData).length,大家注意到没,我们操作的是tc.value,value已经变了,这里的重新定位光标开始已经没有任何意义了!



载于我的博客

收起阅读 »

CSS新特性:让你的网页变得更加酷炫

随着互联网的发展,网页设计越来越重要。在过去,人们主要关注网站内容,而不太关注样式和布局。但是现代网页设计已经超出了这些基础层面,它们需要吸引用户、提高用户体验并增强用户对产品或服务的信心。为了实现这些目标,CSS(层叠样式表)的新特性已经变得越来越重要。在本...
继续阅读 »

随着互联网的发展,网页设计越来越重要。在过去,人们主要关注网站内容,而不太关注样式和布局。但是现代网页设计已经超出了这些基础层面,它们需要吸引用户、提高用户体验并增强用户对产品或服务的信心。为了实现这些目标,CSS(层叠样式表)的新特性已经变得越来越重要。在本文中,我们将介绍一些新的CSS特性,它们可以让你的网页变得更加酷炫。


1. 自定义属性


自定义属性是CSS最新的功能之一。使用这个功能,你可以定义你自己的CSS属性,并在整个代码库中重复使用。你只需定义一次属性,然后通过var()函数在整个代码库中使用。例如:


:root {
--primary-color: #FF0000;
}

.button {
background-color: var(--primary-color);
}

在上面的例子中,我们定义了一个名为--primary-color的CSS变量,并将其设置为红色。然后,我们将这个变量用于按钮的背景颜色。当你需要改变主色调时,只需改变--primary-color变量的值即可。


2. 网格布局


CSS网格布局是CSS3的新特性之一。使用网格布局,你可以轻松地创建复杂的网页布局。它基于行和列,而不是像传统布局那样基于盒子模型。例如:


.wrapper {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
grid-template-rows: repeat(3, 100px);
gap: 20px;
}

.box {
background-color: #000;
color: #FFF;
padding: 20px;
}

在上面的例子中,我们定义了一个名为wrapper的容器,并将其设置为网格布局。我们制定了三个列,第一列占整个空间的1/4,第二列占整个空间的一半,第三列占整个空间的1/4。我们还定义了三个行,每行高度为100像素。我们还给每个box加入了一些样式,以展示我们的布局。


3. 媒体查询


媒体查询是CSS用于响应式设计的技术。这意味着当用户改变浏览器窗口大小时,网页的布局会自动适应。例如:


@media screen and (max-width: 768px) {
.menu {
display: none;
}
.menu-toggle {
display: block;
}
}

在上面的例子中,我们定义了一个媒体查询,当浏览器窗口小于768像素时,将隐藏菜单并显示菜单切换按钮。这样,在移动设备上,网页的导航栏就会变得更加友好。


4. 动画


CSS动画是一种让你的网页更加生动和酷炫的技术。使用CSS动画,你可以使各种元素在网页上动起来,例如按钮、菜单等等。例如:


.button {
background-color: #FF0000;
color: #FFF;
padding: 20px;
border-radius: 10px;
animation: pulse 2s infinite;
}

@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}

在上面的例子中,我们定义了一个名为pulse的CSS动画,并将其应用于按钮。这个动画可以让按钮缓慢地放大和缩小,使它看起来更加醒目和有趣。


5. 变换


变换是CSS中的一个很棒的特性,可以让你改变元素的位置、大小、旋转角度等属性。使用变换,你可以创建一些非常惊人的效果,例如立体旋转、翻转等等。例如:


.box {
background-color: #FF0000;
width: 100px;
height: 100px;
transform: rotate(45deg);
}

在上面的例子中,我们定义了一个名为box的盒子,并将其旋转了45度。这可以让盒子看起来更加有趣和吸引人。


总结:


本文介绍了一些最新的CSS特性,包括自定义属性、网格布局、媒体查询、动画和变换。使用这些特性,你可以轻松地创建一个现代化、酷炫的网页设计,提高用户体验并增强用户对产品或服务的信心。当然,在实际开发中,我们还需要根据实际情况选择合适的技术和工具,以达到最佳的效果。

作者:饺子不放糖
来源:juejin.cn/post/7233057834287366203

收起阅读 »

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

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


可以关注 程序员三时公众

作者:程序员三时
来源:juejin.cn/post/7232120266805526584
号 进行技术交流讨论

收起阅读 »

你管这破玩意叫缓存穿透?还是缓存击穿?

大家好,我是哪吒。 一、缓存预热 Redis缓存预热是指在服务器启动或应用程序启动之前,将一些数据先存储到Redis中,以提高Redis的性能和数据一致性。这可以减少服务器在启动或应用程序启动时的数据传输量和延迟,从而提高应用程序的性能和可靠性。 1、缓存预热...
继续阅读 »

大家好,我是哪吒。


一、缓存预热


Redis缓存预热是指在服务器启动或应用程序启动之前,将一些数据先存储到Redis中,以提高Redis的性能和数据一致性。这可以减少服务器在启动或应用程序启动时的数据传输量和延迟,从而提高应用程序的性能和可靠性。


1、缓存预热常见步骤


(1)数据准备


在应用程序启动或服务器启动之前,准备一些数据,这些数据可以是静态数据、缓存数据或其他需要预热的数据。


(2)数据存储


将数据存储到Redis中,可以使用Redis的列表(List)数据类型或集合(Set)数据类型。


(3)数据预热


在服务器启动或应用程序启动之前,将数据存储到Redis中。可以使用Redis的客户端工具或命令行工具来执行此操作。


(4)数据清洗


在服务器启动或应用程序启动之后,可能会对存储在Redis中的数据进行清洗和处理。例如,可以删除过期的数据、修改错误的数据等。


需要注意的是,Redis缓存预热可能会增加服务器的开销,因此应该在必要时进行。同时,为了减少预热的次数,可以考虑使用Redis的其他数据类型,如哈希表(Hash)或有序集合(Sorted Set)。此外,为了提高数据一致性和性能,可以使用Redis的持久化功能,将数据存储到Redis中,并在服务器重启后自动恢复数据。


2、代码实现


@Component
@Slf4j
public class BloomFilterInit
{
@Resource
private RedisTemplate redisTemplate;

//初始化白名单数据
@PostConstruct
public void init() {
//1 白名单客户加载到布隆过滤器
String key = "customer:1";
//2 计算hashValue,由于存在计算出来负数的可能,我们取绝对值
int hashValue = Math.abs(key.hashCode());
//3 通过hashValue和2的32次方后取余,获得对应的下标坑位
long index = (long)(hashValue % Math.pow(2,32));
log.info(key+" 对应的坑位index:{}",index);
//4 设置redis里面的bitmap对应类型白名单:whitelistCustomer的坑位,将该值设置为1
redisTemplate.opsForValue().setBit("whitelistCustomer",index,true);

}
}

二、缓存雪崩


Redis缓存雪崩是指在缓存系统中,由于某些原因,缓存的数据突然大量地被删除或修改,导致缓存系统的性能下降,甚至无法正常工作。


1、什么情况会发生缓存雪崩?


(1)误删除


由于误操作或故障,缓存系统可能会误删除一些正常的数据。这种情况通常会在数据库中发生。


(2)误修改


由于误操作或故障,缓存系统可能会误修改一些正常的数据。这种情况通常会在数据库中发生。


(3)负载波动


缓存系统通常会承受一定的负载波动,例如,在高峰期间,数据量可能会大幅增加,从而导致缓存系统的性能下降。


(4)数据变化频繁


如果缓存系统中的数据变化频繁,例如,每秒钟都会有大量的数据插入或删除,那么缓存系统可能会因为响应过慢而导致雪崩。


2、Redis缓存集群实现高可用


(1)主从 + 哨兵


(2)Redis集群


(3)开启Redis持久化机制aof/rdb,尽快恢复缓存集群。


3、如何避免Redis缓存雪崩?


(1)数据备份


定期备份数据,以防止误删除或误修改。


(2)数据同步


定期同步数据,以防止数据不一致。


(3)负载均衡


使用负载均衡器将请求分配到多个Redis实例上,以减轻单个实例的负载。


(4)数据优化


优化数据库结构,减少数据变化频繁的情况。


(5)监控与告警


监控Redis实例的性能指标,及时发现缓存系统的异常,并发出告警。


三、缓存穿透


Redis缓存穿透是指在Redis缓存系统中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。


1、什么情况会发生缓存穿透?


(1)数据量过大


当缓存中存储的数据量过大时,缓存的数据量可能会超过Redis的数据存储限制,从而导致缓存失去了它的作用。


(2)数据更新频繁


当缓存中存储的数据更新频繁时,缓存的数据可能会出现异步的变化,导致缓存无法被正常访问。


(3)数据过期


当缓存中存储的数据过期时,缓存的数据可能会失去它的作用,因为Redis会在一定时间后自动将过期的数据删除。


(4)数据权限限制


当缓存中存储的数据受到权限限制时,只有拥有足够权限的用户才能访问和处理这些数据,从而导致缓存失去了它的作用。


(5)Redis性能瓶颈


当Redis服务器的性能达到极限时,Redis缓存可能会因为响应过慢而导致穿透。


2、如何避免Redis缓存穿透?


(1)设置合理的缓存大小


根据实际需求设置合理的缓存大小,以避免缓存穿透。


(2)优化数据结构


根据实际需求优化数据结构,以减少数据的大小和更新频率。


(3)设置合理的过期时间


设置合理的过期时间,以避免缓存失去它的作用。


(4)增加Redis的并发处理能力


通过增加Redis的并发处理能力,以提高缓存的处理能力和响应速度。


(5)优化Redis服务器的硬件和软件配置


通过优化Redis服务器的硬件和软件配置,以提高Redis的性能和处理能力。


Redis缓存穿透


四、通过空对象缓存解决缓存穿透


如果发生了缓存穿透,可以针对要查询的数据,在Redis中插入一条数据,添加一个约定好的默认值,比如defaultNull。


比如你想通过某个id查询某某订单,Redis中没有,MySQL中也没有,此时,就可以在Redis中插入一条,存为defaultNull,下次再查询就有了,因为是提前约定好的,前端也明白是啥意思,一切OK,岁月静好。


这种方式只能解决key相同的情况,如果key都不同,则完蛋。


五、Google布隆过滤器Guava解决缓存穿透



1、引入pom


<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

2、创建布隆过滤器


BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);

(3)布隆过滤器中添加元素


bloomFilter.mightContain(1)

(4)判断布隆过滤器中是否存在


bloomFilter.mightContain(1)

3、fpp误判率


@Service
@Slf4j
public class GuavaBloomFilterService {
public static final int SIZE = 1000000;

//误判率
public static double fpp = 0.01;

//创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);

public void guavaBloomFilter() {
for (int i = 1; i <= SIZE; i++) {
bloomFilter.put(i);
}
ArrayList<Integer> list = new ArrayList<>(10000);

for (int i = SIZE + 1; i <= SIZE + (10000); i++) {
if (bloomFilter.mightContain(i)) {
log.info("被误判了:{}", i);
list.add(i);
}
}
log.info("误判总数量:{}", list.size());
}
}

六、Redis缓存击穿


Redis缓存击穿是指在Redis缓存系统中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。


1、什么情况会发生缓存击穿?


根本原因:热点Key失效


(1)数据量过大


当缓存中存储的数据量过大时,缓存的数据量可能会超过Redis的数据存储限制,从而导致缓存失去了它的作用。


(2)数据更新频繁


当缓存中存储的数据更新频繁时,缓存的数据可能会出现异步的变化,导致缓存无法被正常访问。


(3)数据过期


当缓存中存储的数据过期时,缓存的数据可能会失去它的作用,因为Redis会在一定时间后自动将过期的数据删除。


(4)数据权限限制


当缓存中存储的数据受到权限限制时,只有拥有足够权限的用户才能访问和处理这些数据,从而导致缓存失去了它的作用。


(5)Redis性能瓶颈


当Redis服务器的性能达到极限时,Redis缓存可能会因为响应过慢而导致击穿。


2、如何避免Redis缓存击穿?


(1)设置合理的缓存大小


根据实际需求设置合理的缓存大小,以避免缓存穿透。


(2)优化数据结构


根据实际需求优化数据结构,以减少数据的大小和更新频率。


(3)设置合理的过期时间


设置合理的过期时间,以避免缓存失去它的作用。


(4)增加Redis的并发处理能力


通过增加Redis的并发处理能力,以提高缓存的处理能力和响应速度。


(5)优化Redis服务器的硬件和软件配置


通过优化Redis服务器的硬件和软件配置,以提高Redis的性能和处理能力。


七、Redis缓存击穿解决方案


1、互斥更新


通过双检加锁机制。


2、差异失效时间



先更新从缓存B,再更新主缓存A,而且让从缓存B的缓存失效时间长于A,保证A失效时,B还在。


作者:哪吒编程
来源:juejin.cn/post/7233052510553636901
收起阅读 »

聊一聊Kotlin协程"低级"api

聊一聊kotlin协程“低级”api Kotlin协程已经出来很久了,相信大家都有不同程度的用上了,由于最近处理的需求有遇到协程相关,因此今天来聊一Kotlin协程的“低级”api,首先低级api并不是它真的很“低级”,而是kotlin协程库中的基础api,我...
继续阅读 »

聊一聊kotlin协程“低级”api


Kotlin协程已经出来很久了,相信大家都有不同程度的用上了,由于最近处理的需求有遇到协程相关,因此今天来聊一Kotlin协程的“低级”api,首先低级api并不是它真的很“低级”,而是kotlin协程库中的基础api,我们一般开发用的,其实都是通过低级api进行封装的高级函数,本章会通过低级api的组合,实现一个自定义的async await 函数(下文也会介绍kotlin 高级api的async await),涉及的低级api有startCoroutineContinuationInterceptor


startCoroutine


我们知道,一个suspend关键字修饰的函数,只能在协程体中执行,伴随着suspend 关键字,kotlin coroutine common库(平台无关)也提供出来一个api,用于直接通过suspend 修饰的函数直接启动一个协程,它就是startCoroutine@SinceKotlin("1.3")

@Suppress("UNCHECKED_CAST")
public fun <R, T> (suspend R.() -> T).startCoroutine(
作为Receiver
receiver: R,
当前协程结束时的回调
completion: Continuation<T>
) {
createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)
}

可以看到,它的Receiver是(suspend R.() -> T),即是一个suspend修饰的函数,那么这个有什么作用呢?我们知道,在普通函数中无法调起suspend函数(因为普通函数没有隐含的Continuation对象,这里我们不在这章讲,可以参考kotlin协程的资料)


image.png
但是普通函数是可以调起一个以suspend函数作为Receiver的函数(本质也是一个普通函数)


image.png
其中startCoroutine就是其中一个,本质就是我们直接从外部提供了一个Continuation,同时调用了resume方法,去进入到了协程的世界

startCoroutine实现

createCoroutineUnintercepted(completion).intercepted().resume(Unit)

这个原理我们就不细讲下去原理,之前也有写过相关的文章。通过这种调用,我们其实就可以实现在普通的函数环境,开启一个协程环境(即带有了Continuation),进而调用其他的suspend函数。


ContinuationInterceptor


我们都知道拦截器的概念,那么kotlin协程也有,就是ContinuationInterceptor,它提供以AOP的方式,让外部在resume(协程恢复)前后进行自定义的拦截操作,比如高级api中的Diapatcher就是。当然什么是resume协程恢复呢,可能读者有点懵,我们还是以上图中出现的mySuspendFunc举例子mySuspendFunc是一个suspned函数

::mySuspendFunc.startCoroutine(object : Continuation<Unit> {
override val context: CoroutineContext
get() = EmptyCoroutineContext

override fun resumeWith(result: Result<Unit>) {

}

})

它其实等价于val continuation = ::mySuspendFunc.createCoroutine(object :Continuation<Unit>{

    override val context: CoroutineContext
get() = EmptyCoroutineContext

override fun resumeWith(result: Result<Unit>) {
Log.e("hello","当前协程执行完成的回调")
}

})
continuation.resume(Unit)

startCoroutine方法就相当于创建了一个Continuation对象,并调用了resume。创建Continuation可通过createCoroutine方法,返回一个Continuation,如果我们不调用resume方法,那么它其实什么也不会执行,只有调用了resume等执行方法之后,才会执行到后续的协程体(这个也是协程内部实现,感兴趣可以看看之前文章)


而我们的拦截器,就相当于在continuation.resume前后,可以添加自己的逻辑。我们可以通过继承ContinuationInterceptor,实现自己的拦截器逻辑,其中需要复写的方法是interceptContinuation方法,用于返回一个自己定义的Continuation对象,而我们可以在这个Continuation的resumeWith方法里面(当调用了resume之后,会执行到resumeWith方法),进行前后打印/其他自定义操作(比如切换线程)



class ClassInterceptor() :ContinuationInterceptor { override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =MyContinuation(continuation)

}
class MyContinuation<T>(private val continuation: Continuation<T>):Continuation<T> by continuation{
override fun resumeWith(result: Result<T>) {
Log.e("hello","MyContinuation start ${result.getOrThrow()}")
continuation.resumeWith(result)

Log.e("hello","MyContinuation end ")
}
}

其中的key是ContinuationInterceptor,协程内部会在每次协程恢复的时候,通过coroutineContext取出key为ContinuationInterceptor的拦截器,进行拦截调用,当然这也是kotlin协程内部实现,这里简单提一下。


实战


kotlin协程api中的 async await


我们来看一下kotlin Coroutine 的高级api async await用法

CoroutineScope(Dispatchers.Main).launch {
val block = async(Dispatchers.IO) {
// 阻塞的事项

}
// 处理其他主线程的事务

// 此时必须需要async的结果时,则可通过await()进行获取
val result = block.await()
}

我们可以通过async方法,在其他线程中处理其他阻塞事务,当主线程必须要用async的结果的时候,就可以通过await等待,这里如果结果返回了,则直接获取值,否则就等待async执行完成。这是Coroutine提供给我们的高级api,能够将任务简单分层而不需要过多的回调处理。


通过startCoroutine与ContinuationInterceptor实现自定义的 async await


我们可以参考其他语言的async,或者Dart的异步方法调用,都有类似这种方式进行线程调用

async {
val result = await {
suspend 函数
}
消费result
}

await在async作用域里面,同时获取到result后再进行消费,async可以直接在普通函数调用,而不需要在协程体内,下面我们来实现一下这个做法。


首先我们想要限定await函数只能在async的作用域才能使用,那么首先我们就要定义出来一个Receiver,我们可以在Receiver里面定义出自己想要暴露的方法

interface AsyncScope {
fun myFunc(){

}

}
fun async(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

注意这个类,resumeWith 只会跟startCoroutine的这个协程绑定关系,跟await的协程没有关系
class AsyncStub(override val context: CoroutineContext = EmptyCoroutineContext) :
Continuation<Unit>, AsyncScope {
override fun resumeWith(result: Result<Unit>) {

// 这个是干嘛的 == > 完成的回调
Log.e("hello","AsyncStub resumeWith ${Thread.currentThread().id} ${result.getOrThrow()}")
}
}

上面我们定义出来一个async函数,同时定义出来了一个AsyncStub的类,它有两个用处,第一个是为了充当Receiver,用于规范后续的await函数只能在这个Receiver作用域中调用,第二个作用是startCoroutine函数必须要传入一个参数completion,是为了收到当前协程结束的回调resumeWith中可以得到当前协程体结束回调的信息

await方法里面

suspend fun<T> AsyncScope.await(block:() -> T) = suspendCoroutine<T> {
// 自定义的Receiver函数
myFunc()

Thread{
切换线程执行await中的方法
it.resumeWith(Result.success(block()))
}.start()
}

在await中,其实是一个扩展函数,我们可以调用任何在AsyncScope中定义的方法,同时这里我们模拟了一下线程切换的操作(Dispatcher的实现,这里不采用Dispatcher就是想让大家知道其实Dispatcher.IO也是这样实现的),在子线程中调用it.resumeWith(Result.success(block())),用于返回所需要的信息


通过上面定的方法,我们可以实现

async {
val result = await {
suspend 函数
}
消费result
}

这种调用方式,但是这里引来了一个问题,因为我们在await函数中实际将操作切换到了子线程,我们想要将消费result的动作切换至主线程怎么办呢?又或者是加入我们希望获取结果前做一些调整怎么办呢?别急,我们这里预留了一个CoroutineContext函数,我们可以在外部传入一个CoroutineContext

public interface ContinuationInterceptor : CoroutineContext.Element
而CoroutineContext.Element又是继承于CoroutineContext
CoroutineContext.Element:CoroutineContext

而我们的拦截器,正是CoroutineContext的子类,我们把上文的ClassInterceptor修改一下


class ClassInterceptor() : ContinuationInterceptor {
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MyContinuation(continuation)

}

class MyContinuation<T>(private val continuation: Continuation<T>) :
Continuation<T> by continuation {
private val handler = Handler(Looper.getMainLooper())
override fun resumeWith(result: Result<T>) {
Log.e("hello", "MyContinuation start ${result.getOrThrow()}")

handler.post {
continuation.resumeWith(Result.success(自定义内容))
}
Log.e("hello", "MyContinuation end ")
}
}

同时把async默认参数CoroutineContext实现一下即可

fun async(
context: CoroutineContext = ClassInterceptor(),
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

此后我们就可以直接通过,完美实现了一个类js协程的调用,同时具备了自动切换线程的能力

async {
val result = await {
test()
}
Log.e("hello", "result is $result ${Looper.myLooper() == Looper.getMainLooper()}")
}

结果

  E  start 
E MyContinuation start kotlin.Unit
E MyContinuation end
E end
E 执行阻塞函数 test 1923
E MyContinuation start 自定义内容数值
E MyContinuation end
E result is 自定义内容的数值 true
E AsyncStub resumeWith 2 kotlin.Unit

最后,这里需要注意的是,为什么拦截器回调了两次,因为我们async的时候开启了一个协程,同时await的时候也开启了一个,因此是两个。AsyncStub只回调了一次,是因为AsyncStub被当作complete参数传入了async开启的协程block.startCoroutine,因此只是async中的协程结束才会被回调。


image.png


本章代码


class ClassInterceptor() : ContinuationInterceptor {
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MyContinuation(continuation)

}

class MyContinuation<T>(private val continuation: Continuation<T>) :
Continuation<T> by continuation {
private val handler = Handler(Looper.getMainLooper())
override fun resumeWith(result: Result<T>) {
Log.e("hello", "MyContinuation start ${result.getOrThrow()}")

handler.post {
continuation.resumeWith(Result.success(6 as T))
}
Log.e("hello", "MyContinuation end ")
}
}
interface AsyncScope {
fun myFunc(){

}

}
fun async(
context: CoroutineContext = ClassInterceptor(),
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

class AsyncStub(override val context: CoroutineContext = EmptyCoroutineContext) :
Continuation<Unit>, AsyncScope {
override fun resumeWith(result: Result<Unit>) {

// 这个是干嘛的 == > 完成的回调
Log.e("hello","AsyncStub resumeWith ${Thread.currentThread().id} ${result.getOrThrow()}")
}
}


suspend fun<T> AsyncScope.await(block:() -> T) = suspendCoroutine<T> {
myFunc()

Thread{
it.resumeWith(Result.success(block()))
}.start()
}
模拟阻塞
fun test(): Int {
Thread.sleep(5000)
Log.e("hello", "执行阻塞函数 test ${Thread.currentThread().id}")
return 5
}
async {
val result = await {
test()
}
Log.e("hello", "result is $result ${Looper.myLooper() == Looper.getMainLooper()}")
}

最后


我们通过协程的低级api,实现了一个与官方库不同版本的async await,同时也希望通过对低级api的设计,也能对Coroutine官方库的高级api的实现有一定的了解。



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

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

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

作者:程序员三时
来源:juejin.cn/post/7232120266805526584

收起阅读 »

Kotlin | 理解泛型使用

泛型类 & 泛型方法 泛型,指的是具体的类型泛化,多用在集合中(如List、Map),编码时使用符号代替,在使用时再确定具体类型。 泛型通常用于类和方法中,称为泛型类、泛型方法,使用示例:/** * 泛型类 */ abstract class Ba...
继续阅读 »

泛型类 & 泛型方法


泛型,指的是具体的类型泛化,多用在集合中(如ListMap),编码时使用符号代替,在使用时再确定具体类型。


泛型通常用于类和方法中,称为泛型类、泛型方法,使用示例:

/**
* 泛型类
*/
abstract class BaseBook<T> {
private var books: ArrayList<T> = ArrayList()

/**
* 泛型方法
*/
fun <E : T> add(item: E) {
books.add(item)
println("list:$books, size:${books.size}")
}
}

/**
* 子类继承BaseBook并传入泛型参数MathBook
*/
class BookImpl : BaseBook<MathBook>()

fun main() {
BookImpl().apply {
add(MathBook("数学"))
}
}

执行main()方法,输出:

list:[MathBook(math=数学)], size: 1

Java泛型通配符


? extends E 定义上界


Java中的泛型是不型变的,举个例子:IntegerObject的子类,但是List<Integer>并不是List<Object>的子类,因为List是不型变的。如:
错误
如果想让List<Integer>成为List<Object>的子类,可以通过上界操作符 ? extends E 来操作。


? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身extends操作符可以限定上界通配符类型,使得通配符类型是协变的。注意,经过协变之后,数据是可读不可写的。示例:

//继承关系Child -> Parent 
class Parent{
protected String name = "Parent";
}

class Child extends Parent {
protected String name = "Child";
}

定义实体类,继承关系:Child -> Parent

class CList<E> {
//通过<? extends E>来定义上界
public void addAll(List<? extends E> list) {
//...
}
}

/**
* <? extends E>来定义上界,可以保证协变性
*/
public void GExtends() {
//1、Child是Parent的子类
Parent parent = new Child();

//2、协变,泛型参数是Parent
CList<Parent> objs = new CList<>();
List<Child> strs = new ArrayList<>(); //声明字符串List
strs.add(new Child());
objs.addAll(strs); //addAll()方法中的入参必须为List<? extends E>,从而保证了List<Child>是List<Parent>的子类。
}

addAll()方法中的入参必须为List<? extends E>,从而保证了List<Child>List<Parent>的子类。如果addAll()中的入参改为List<E>,则编译器会直接报错,因为List<Child>并不是List<Parent>的子类,如下:


错误


? super E 定义下界


? super E 可以看作一个E或者E的父类的“未知类型”,这里的父类包括直接和间接父类。super定义泛型的下界,使得通配符类型是逆变的。经过逆变之后,数据是可写不可读的,如: List<? super Child>List<Parent> 的一个超类。示例:

class CList<E> {
//通过<? super E>来定义下界
public void popAll(List<? super E> dest) {
//...
}

/**
* 逆变性
*/
public void GSuper(){
CList<Child> objs = new CList<>();
List<Parent> parents = new ArrayList<>(); //声明字符串List
parents.add(new Parent());
objs.popAll(parents); //逆变
}

可以看到popAll()的入参必须声明为List<? super E>,如果改为List<E>,编译器会直接报错:


错误


Kotlin泛型


Java 一样,Kolin 泛型本身也是不能型变的。



  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends T

  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super T


声明处型变


协变< out T>
interface GenericsP<T> {
fun get(): T //读取并返回T,可以认为只能读取T的对象是生产者
}

如上声明了GenericsP< T>接口,如果其内部只能读取并返回T,可以认为GenericsP实例对象为生产者(返回T)。

open class Book(val name: String)
data class EnglishBook(val english: String) : Book(english)
data class MathBook(val math: String) : Book(math)

已知EnglishBook、MathBookBook的子类,但是如果将Book、EnglishBook当成泛型放入GenericsP,他们之间的关系还成立吗?即:


error信息
可以看到编译器直接报错,因为虽然EnglishBookBook的子类,但是GenericsP<EnglishBook>并不是GenericsP<Book>的子类,如果想让这个关系也成立,Kotlin提供了out修饰符,out修饰符能够确保:



1、T只能用于函数返回中,不能用于参数输入中;
2、GenericsP< EnglishBook>可以安全的作为GenericsP< Book>的子类



示例如下:

interface GenericsP<out T> {
fun get(): T //读取并返回T,可以认为只能读取T的对象是生产者
// fun put(item: T) //错误,不允许在输入参数中使用
}

经过如上的改动后,可以看到GenericsP<EnglishBook>可以正确赋值给GenericsP<Book>了:
正确赋值


逆变< in T>
interface GenericsC<T> {
fun put(item: T) //写入T,可以认为只能写入T的对象是消费者
}

如上声明了GenericsC<T>接口,如果其内部只能写入T,可以认为GenericsC实例对象为消费者(消费T)。为了保证T只能出现在参数输入位置,而不能出现在函数返回位置上,Kotlin可以使用in进行控制:

interface GenericsC<in T> {
fun put(item: T) //写入T,可以认为只能写入T的对象是消费者
//fun get(): T //错误,不允许在返回中使用
}

继续编写如下函数:

    /**
* 称为GenericsC在Book上是逆变的。
* 跟系统源码中的Comparable类似
*/
private fun consume(to: GenericsC<Book>) {
//GenericsC<Book>实例赋值给了GenericsC<EnglishBook>
val target: GenericsC<EnglishBook> = to
target.put(EnglishBook("英语"))
}

可以看到GenericsC中的泛型参数声明为in后,GenericsC<Book>实例可以直接赋值给了GenericsC<EnglishBook>,称为GenericsCBook上是逆变的。在系统源码中我们经常使用的一个例子就是Comparable:

//Comparable.kt
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}

使用处型变(类型投影)


上一节中in、out都是写在类的声明处,从而控制泛型参数的使用场景,但是如果泛型参数既可能出现在函数入参中,又可能出现在函数返回中,典型的类就是Array:

class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}

这时候就不能在声明处做任何协变/逆变的操作了,如下函数中使用Array

 fun copy(from: Array<Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}

调用方:

val strs: Array<String> = arrayOf("1", "2")
val any = Array<Any>(2) {}
copy(strs, any) //编译器报错 strs其类型为 Array<String> 但此处期望 Array<Any>

错误原因就是因为Array<String>并不是Array<Any>的子类,即不是协变的,这里是为了保证数据的安全性。如果可以保证Array< String>传入copy()函数之后不能被写入,那么就保证了安全性,既然我们在声明Array时不能限制泛型参数,那么完全可以在使用处进行限制,如下:

 fun copy(from: Array<out Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}

可以看到对from添加了out限制,这种被称为使用处型变。即不允许from进行写入操作,那么就可以保证了from的安全性,再进行上面的调用时,copy(strs, any)就可以正确的执行了。


星投影< *>


当不使用协变、逆变时,某些场景下可以使用<*>来实现泛型,如:



  • 对于GenericsP<out T: Book>GenericsP< *>相当于GenericsP<out Book>,当T未知时,可以安全的从GenericsP<*>中读取Book值;

  • 对于GenericsC<in T>GenericsC<*>相当于 GenericsC<in Nothing>,当T未知时,没有安全方式写入GenericsC<*>

  • 对于Generics<T: Book>T为有上界Book的不型变参数,当Generics<*>读取时等价于Generics<out Book>;写入时等价于Generics<in Nothing>


泛型擦除


泛型参数会在编译期间存在,在运行期间会被擦除,例如:Generics<EnglishBook>Generics<MathBook> 的实例都会被擦除为 Generics<*>。运行时期检测一个泛型类型的实例无法通过is关键字进行判断,另外运行期间具体的泛型类型判断也无法判断,如: books as List<Book>,只会对非泛型部分进行检测,形如:books as List<*>


如果想具体化泛型参数,可以通过inline + reified的方式:

 /**
* inline + reified 使得类型参数被实化 reified:实体化的
* 注:带reified类型参数的内联函数,Java是无法直接调用的
*/
inline fun <reified T> isAny(value: Any): Boolean {
return value is T
}

参考


【1】https://www.kotlincn.net/docs/reference/generics.html
【2】https://mp.weixin.qq.com/s/vSwx7fgROJcrQwEOW7Ws8A
【3】https://juejin.cn/post/7042606952311947278


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

怎么做登录(单点登录)功能?

简单上个图(有水印。因为穷所以没开会员) 先分析下登陆要做啥 首先,搞清楚要做什么。 登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。 怎么落实? 怎么实现它?用什么实现? ...
继续阅读 »

简单上个图(有水印。因为穷所以没开会员)


怎么做登陆(单点登陆)?.png


先分析下登陆要做啥



首先,搞清楚要做什么。


登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。



怎么落实?



怎么实现它?用什么实现?




我们的项目是Springboot + Vue前后端分离类型的。


选择用token + redis 实现,权限的话用SpringSecurity来做。




前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。


单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。



上代码



概念这个东西越说越玄。咱们直接上代码吧。



接口:
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   //用户名、密码、验证码、uuid
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                     loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
}


用户信息验证交给SpringSecurity

/**
* 登录验证
*/
public String login(String username, String password, String code, String uuid)
{
   // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
   boolean captchaEnabled = configService.selectCaptchaEnabled();
   if (captchaEnabled)
  {
       //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
       validateCaptcha(username, code, uuid);
  }
   // 用户验证 -- SpringSecurity
   Authentication authentication = null;
   try
  {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
       //
       authentication = authenticationManager.authenticate(authenticationToken);
  }
   catch (Exception e)
  {
       if (e instanceof BadCredentialsException)
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
           throw new UserPasswordNotMatchException();
      }
       else
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
           throw new ServiceException(e.getMessage());
      }
  }
   finally
  {
       AuthenticationContextHolder.clearContext();
  }
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}

把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)
/**
* 校验验证码
*/
public void validateCaptcha(String username, String code, String uuid)
{
   //uuid是验证码的redis key
   String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
   String captcha = redisCache.getCacheObject(verifyKey);
   redisCache.deleteObject(verifyKey);
   if (captcha == null)
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
       throw new CaptchaExpireException();
  }
   if (!code.equalsIgnoreCase(captcha))
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
       throw new CaptchaException();
  }
}

token生成部分


这里,token

/**
* 创建令牌
*/
public String createToken(LoginUser loginUser)
{
   String token = IdUtils.fastUUID();
   loginUser.setToken(token);
   setUserAgent(loginUser);
   refreshToken(loginUser);

   Map<String, Object> claims = new HashMap<>();
   claims.put(Constants.LOGIN_USER_KEY, token);
   return createToken(claims);
}

刷新token
/**
* 刷新令牌
*/
public void refreshToken(LoginUser loginUser)
{
   loginUser.setLoginTime(System.currentTimeMillis());
   loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
   // 根据uuid将loginUser缓存
   String userKey = getTokenKey(loginUser.getToken());
   redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

验证token
/**
* 验证令牌
*/
public void verifyToken(LoginUser loginUser)
{
   long expireTime = loginUser.getExpireTime();
   long currentTime = System.currentTimeMillis();
   if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
  {
       refreshToken(loginUser);
  }
}


注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。


另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
   //...无关的代码删了
   httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
   @Autowired
   private TokenService tokenService;

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
       throws ServletException, IOException
  {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
      {
           //刷新token有效期
           tokenService.verifyToken(loginUser);
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
           authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
           SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
       chain.doFilter(request, response);
  }
}


这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。


这里JWT只是起到个加密的作用,无它。


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

装了我这 10 个 IDEA 神级插件后,同事也开始情不自禁的嘚瑟了

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。 Vuesion Theme 颜值是生产...
继续阅读 »

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。


Vuesion Theme


颜值是生产力的第一要素,IDE 整好看了,每天对着它也是神清气爽,有木有?就 Intellij IDEA 提供的暗黑和亮白主色,虽然说已经非常清爽了,但时间久了总觉得需要再来点新鲜感?


Vuesion Theme 这个主题装上后,你会感觉整个 Intellij IDEA 更高级了。



安装完插件就立马生效了,瞧这该死的漂亮,整个代码着色,以及文件的图标,都更炫酷了:



当然了,主题这事,萝卜白菜各有所爱,就像玩 dota,我就喜欢露娜。


lombok


可能提到 lombok,多多少少有些争议,但不得不说,这玩意的确是很能省代码,并且很多开源的第三方 jar 包,以及 Intellij IDEA 2020.3 以后的版本也都默认加了 lombok。



这么多注解可以选择,在写 VO、DO、DTO 的时候是真的省心省力。



如果没有 lombok 的帮助,那整个代码就要炸了呀。对比一下,是不是感受还挺明显的?



当然了,要使用 lombok,你得在 pom.xml 文件中引入 lombok 的依赖包。


xml
复制代码
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

File Expander


这个插件不仅可以反编译,还可以打开 tar.gz,zip 等压缩文件,



如果有小伙伴反驳说自己不装插件也可以打开 jar 包里的代码,那是因为你的 jar 在 classpath。如果单独打开一个 jar 包,不装插件是看不了的。



GitToolBox


如果你经常使用 Git 提交代码的话,这款插件就非常的爽。



它能直接提示你远程版本库里有多少文件更新,你有多少文件没有提交到版本库,甚至可以显示上一次提交的时间和版本更新者。



Maven Helper


这插件几乎人手一个了吧,Java 后端开发必备啊。



依赖可视化的神器,可以很清楚地知道依赖的关系图谱,假如有冲突的话,也是一目了然。



Translation


对于英文能力差的同学来说,这个翻译插件简直神了,它支持 Google 翻译、有道翻译、百度翻译、Alibaba 翻译。



刚好写这篇内容的时候,发现最新的版本是 3.3.5,趁机升级一波。有了这款翻译插件,看源码绝对是爽歪歪。以前遇到不认识的单词,真的是好烦,还要切到翻译软件那里查,现在可好,单词翻译、文档翻译、注释翻译,都有了。



arthas idea


Arthas 应该大家都很熟悉了,阿里开源的一款强大的 java 在线诊断工具。


但如果每次都要你输入一长串命令的话,相信你也会很崩溃,尤其是很多时候我还记忆模糊,很多记不住。这款插件刚好解决了我这个烦恼,极大地提高了生产力



使用起来也非常方便,直接进入你要诊断的方法和类,右键选择对应的命令,就会自动帮你生成了。



Free Mybatis plugin


Mybatis 基本上是目前最主流的 ORM 框架了,相比于 hibernate 更加灵活,性能也更好。所以我们一般在 Spring Boot 项目中都会写对应的 mapper.java 和 mapper.xml。


那有了这款插件之后,两者就可以轻松关联起来。



比如,我这里要查看 ArticleMapper 的 xml,那么编辑器的行号右侧就会有一个向右的→,直接点击就跳转过去了。



想跳转回来的话,也是同样的道理,所以有了这款产检,mapper 和 xml 之间就可以自由切换了,丝滑。


VisualGC


这里给大家推荐一个 JVM 堆栈可视化工具,可以和 Intellij IDEA 深度集成——VisualGC。



当我们需要监控一个进程的时候,直接打开 VisualGC面板,就可以查看到堆栈和垃圾收集情况,可以说是一目了然。



CheckStyle-IDEA


如果你比较追求代码规范的话,可以安装这个插件,它会提醒你注意无用导入、注释、语法错误❎、代码冗余等等。



在 CheckStyle 面板中,你可以选择 Google 代码规范或者 sun 的代码规范,跑一遍检查,就可以看到所有的修改建议了。



最后


以上这 10 款 Intellij IDEA 插件也是我平常开发中经常用到的,如果大家有更好更效率的插件,也可以评论里留言。


参考链接:juejin.cn/post/702802…


没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。


本文已收录到 GitHub 上星标 4k+ 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路😄。


Github 仓库:github.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



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

为什么选择FastAPI?

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。 一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数...
继续阅读 »

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。



一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数据库 ORM 支持,功能完善、要素齐全:自带大量常用工具和框架(比如分页,auth,权限管理), 适合快速开发企业级网站。完善的文档:经过多年的发展和完善,Django 有广泛的实践案例和完善的在线文档,可以让开发人员有好的借鉴途径。并且使用众多,能够借鉴的例子也很多。目前 NetDevOps 的平台后端,大多数都是基于 Python 的 Django 来开发的,原因是:python 的确上手快,django 支持完善,能够迅速的建立和设备的联系并生成自带的管理页面。


但是Django太全了太重了,随着开发周期越往后,开发模块越来越多,整个程序就显得异常臃肿,代码逻辑相互耦合十分严重,整个系统需要保持极高的稳定性。所以,我们希望能够有个小而精的框架,可以逐步的将Django项目中的模块慢慢做拆分,做解耦,做程序的可插拔。FastAPI这时候进入到我们的视野。



FastAPI 顾名思义,一个字,就是快。基于(并完全兼容)OPenAPI 的相关开放标准。



  • 高性能:FastAPI 采用异步编程模式,基于 Starlette 框架和 pydantic 库进行开发。其性能相比于 Flask 和 Django 均有很大提升。

  • 简单易用:FastAPI 提供了自动生成 API 文档的功能,丰富的文档可以让开发人员更快速地了解 API 的使用方法。

  • 规范化:FastAPI 默认支持 OpenAPI(前身为 Swagger)和 JSON Schema,从而规范化 API 的设计和发布。

  • 类型检查:FastAPI 强制使用类型注解,使得代码更加严谨,同时可以使用 mypy 等类型检查工具来保证代码的质量。

  • 整合多种数据库支持:FastAPI 可以无缝进行整合多种数据库的使用,比如 SQLAlchemy、Tortoise ORM 等。


使用 FastAPI 可以提升性能、简化开发、规范 API 设计、增加代码可读性和可维护性,从而促进开发效率的提升。 FastAPI 站在以下巨人的肩膀之上:


Starlette 负责 web 部分。 Pydantic 负责数据部分。


Starlette


Starlette 是一个轻量级的 ASGI 框架和工具包,特别适合用来构建高性能的 asyncio 服务.


Starlette 的主要特性:



  1. 性能表现优异

  2. WebSocket 支持.

  3. GraphQL 支持.

  4. 进程内的后台任务执行

  5. 启动和关闭服务的事件触发

  6. 测试客户端构建于 requests.

  7. 支持 CORS, GZip, Static Files, Streaming 响应.

  8. 支持会话和 Cookie

  9. 100% 测试覆盖率

  10. 100% 类型注解

  11. 无依赖


Pydantic


pydantic 库是 python 中用于数据接口定义检查与设置管理的库。


pydantic 在运行时强制执行类型提示,并在数据无效时提供友好的错误。


它具有如下优点:



  1. 与 IDE/linter 完美搭配,不需要学习新的模式,只是使用类型注解定义类的实例

  2. 多用途,BaseSettings 既可以验证请求数据,也可以从环境变量中读取系统设置

  3. 快速

  4. 可以验证复杂结构

  5. 可扩展,可以使用 validator 装饰器装饰的模型上的方法来扩展验证

  6. 数据类集成,除了 BaseModel,pydantic 还提供了一个 dataclass 装饰器,它创建带有输入数据解析和验证的普通 Python 数据类。


FastAPI 推荐使用 uvicorn 启动服务



Uvicorn 是基于 uvloop 和 httptools 构建的非常快速的 ASGI 服务器。


uvicorn 是一个基于 asyncio 开发的一个轻量级高效的 web 服务器框架


uvicorn 设计的初衷是想要实现两个目标:


使用 uvloop 和 httptools 实现一个极速的 asyncio 服务器


实现一个基于 ASGI(异步服务器网关接口)的最小应用程序接口。


它目前支持 http, websockets, Pub/Sub 广播,并且可以扩展到其他协议和消息类型。


uvloop 用于替换标准库 asyncio 中的事件循环,使用 Cython 实现,它非常快,可以使 asyncio 的速度提高 2-4 倍。asyncio 不用我介绍吧,写异步代码离不开它。


httptools 是 nodejs HTTP 解析器的 Python 实现。


综述


综上所述,我们推荐使用 FastAPI 来进行快速开发和程序构建,尤其是功能较为简单的应用,它也支持数据结构校验和异步。提高代码执行效率和开发效率。同时具备丰富的数据支持,基于 OpenAPI 的自助交互式文档,让开发过程中的调试也十分方便。快速敏捷开发是我们的追求。



  • 快速开发:提供基础框架和默认设置,使开发人员可以快速构建 API,并自动生成文档和测试样例。

  • 高性能:基于 Python 3.6+、ASGI 和 asyncio 等技术,提供高性能特性,因此在高流量和低延迟的应用场景下比其他框架有更好的响应速度和性能。

  • 文档自动生成:内置文档生成器,自动解析函数参数和返回值来生成规范化的文档,并支持 OpenAPI 和 JSON Schema 的标准格式。

  • 强类型支持:采用类型注解,提高代码的质量和可读性,减少出现运行时错误的可能性。

  • 多种数据库支持:支持多种数据库,包括 SQLAlchemy、Tortoise ORM 等,可以方便地处理数据库操作。


FastAPI 是一款快速、高性能、安全、易用、规范化的 Web 应用框架,能够极大地提高开发效率,保证应用的高性能和可靠性。


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

Socket学习网络基础

1.OSI七层网络模型浅析 当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了! OSI七层网络模型(从下往上) : 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传...
继续阅读 »

1.OSI七层网络模型浅析


当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了!


OSI七层网络模型(从下往上)




  • 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等!在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,这一层的单位是:bit比特

  • 数据链路层(Datalink) :可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等!另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两过程!这种建立起来的数据收发关系~该层的设备有:网卡,网桥,网路交换机,另外该层的单位为:

  • 网络层(Network) :主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了把任意两台终端设备数据链接起来的问题!简单点说就是:建立网络连接和为上层提供服务!该层的设备有:路由!该层的单位为:数据包,另外IP协议就在这一层!

  • 传输层(Transport) :向上面的应用层提供通信服务,面向通信部分的最高层,同时也是用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络层,并且保证这些数据段有效的到达对端!所以这层的单位是:数据段;而这层有两个很重要的协议就是:TCP传输控制协议UDP用户数据报协议,这也是本章节核心讲解的部分!

  • 会话层(Session) :负责在网络中的两节点之间建立、维持和终止通信。建立通信链接,保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!

  • 表示层(Presentation) :对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、数据格式转换和加密解密,压缩解压缩"等

  • 应用层(Application) :OSI参考模型的最高层,为用户的应用程序提供网络服务。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。



好的上面我们浅述了OSI七层网络模型,下面总结下:



OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊的网络功能,从网络功能角度观察:



  • 下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主

  • 第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;

  • 上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。


简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。




TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层(对应OSI的第四层)。TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这4层分别为:



  • 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。

  • 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。

  • 网络互连层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。

  • 主机到网络层:对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。


3.TCP/UDP区别讲解


好吧,前两点侃侃而谈,只是给大家普及下OSI七层模型和TCP/IP四层模型的概念,接下来要讲的是和我们Socket开发相关的一些概念名词了!


1)IP地址



2)端口



1. 用于区分不同的应用程序


2. 端口号的范围为0-65535,其中0-1023未系统的保留端口,我们的程序尽可能别使用这些端口!


3. IP地址和端口号组成了我们的Socket,Socket是网络运行程序间双向通信链路的终结点,是TCP和UDP的基础!


4. 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23




3)TCP协议与UDP协议的比较:


TCP协议流程详解:


首先TCP/IP是一个协议簇,里面包括很多协议的。UDP只是其中的一个。之所以命名为TCP/IP协议,因为TCP,IP协议是两个很重要的协议,就用他两命名了。


下面我们来讲解TCP协议和UDP协议的区别:


TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,即在收发数据钱,都需要与对面建立可靠的链接,这也是面试经常会问到的TCP的三次握手以及TCP的四次挥手三次握手:建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立,在Socket编程中,这一过程由客户端执行connect来触发,具体流程图如下:




  • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。


四次挥手:终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在Socket编程中,这一过程由客户端或服务端任一方执行close来触发,具体流程图如下:




  • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态

  • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。另外也可能是同时发起主动关闭的情况:



另外还可能有一个常见的问题就是:为什么建立连接是三次握手,而关闭连接却是四次挥手呢?答:因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。


UDP协议详解


UDP(User Datagram Protocol)用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。相比TCP就是无需建立链接,结构简单,无法保证正确性,容易丢包。


4.Java中对于网络提供的几个关键类


针对不同的网络通信层次,Java给我们提供的网络功能有四大类:



  • InetAddress:用于标识网络上的硬件资源

  • URL:统一资源定位符,通过URL可以直接读取或者写入网络上的数据

  • Socket和ServerSocket:使用TCP协议实现网络通信的Socket相关的类

  • Datagram:使用UDP协议,将数据保存在数据报中,通过网络进行通信


本节我们只介绍前两个类,Socket与Datagram到TCP和UDP的章节再讲解!


~InetAddress的使用例子


示例代码

    public static void main(String[] args) throws Exception{
//获取本机InetAddress的实例:
InetAddress address = InetAddress.getLocalHost();
System.out.println("本机名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
byte[] bytes = address.getAddress();
System.out.println("字节数组形式的IP地址:" + Arrays.toString(bytes));
System.out.println("直接输出InetAddress对象:" + address);
}

运行结果图



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