注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

浅谈 Android 线上帧率统计方案演进

帧率是我们衡量应用流畅度的一个重要基准指标。本文将简单介绍 Android 线上帧率计算方案的演进和业界基于帧率来衡量卡顿的相关指标设计。 帧率计算方案的演进 Choreographer.postFrameCallback 自 Android 4.1 引入 C...
继续阅读 »

帧率是我们衡量应用流畅度的一个重要基准指标。本文将简单介绍 Android 线上帧率计算方案的演进和业界基于帧率来衡量卡顿的相关指标设计。


帧率计算方案的演进


Choreographer.postFrameCallback


自 Android 4.1 引入 Choreographer 来调度 UI 线程的绘制相关任务之后,我们便有了一个简单衡量 UI 线程绘制效率的方案:通过持续调用 Choreographer 的 postFrameCallback 方法来得到基于 VSync 周期的回调,基于回调的间隔或者方法参数中的当前帧起始时间 frameTimeNanos 来计算帧率( 注意到基于回调间隔计算帧率的情况,由于 postFrameCallback 注册的回调类型是 Animation,早于 Traversal 但晚于 Input,实际回调的起点并不是当前帧的起点 )。


这一方案简单可靠,但关键问题在于 UI 线程通常都不会不间断地执行绘制任务,在不需要执行绘制任务( scheduleTraversals )时,UI 线程原本是不需要请求 VSync 信号的,而持续调用 postFrameCallback 方法的情况,则会连续请求 VSync 信号,使得 UI 线程始终处于比较活跃的状态,同时计算得到的帧率数据实际也会包含不需要绘制时的情况。准确来说,这一方案实际衡量的是 UI 线程的繁忙程度。


Matrix 早期方案


怎么能得到真正的帧率数据呢?腾讯 Matrix 的早期实现上,结合 Looper Printer 和 Choreographer 实现了一个比较巧妙的方案,做到了统计真正的 UI 线程绘制任务的细分耗时。


具体来说,基于 Looper Printer 做到对 UI 线程每个消息执行的监控,同时反射给 Choreographer 的 Input 回调队列头部插入一个任务,用来监听下一帧的起点。队头的 Input 任务被调用时,说明当前所在的消息是处理绘制任务的,则消息执行的终点也就是当前帧的终点。同时,在队头的 Input 任务被调用时,给 Animation 和 Traversal 的回调队列头部也插入任务,如此总共得到了四个时间点,可以将当前帧的耗时细分为 Input,Animation 和 Traversal 三个阶段。在当前处理绘制任务的消息执行完后,重新注册一个 Input 回调队列头部的任务,便可以继续监听下一帧的耗时情况。


这一方案没有使用 postFrameCallback( 不主动请求 VSync 信号 ),避免了前一个方案的问题,但整体方案上偏 hack,可能存在兼容性问题( 实际 Android 高版本上对 Choreographer 的内部回调队列确实有所调整 )。此外,当前方案也会受到上一方案的干扰,如果存在其他业务在持续调用 postFrameCallback,也会使得这里统计到的数据包含非绘制的情况。


JankStats 方案


实际在 Android 7.0 之后,官方已经引入了 FrameMetrics API 来提供帧耗时的详细数据。androidx 的 JankStats 库主要就是基于 FrameMetrics API 来实现的帧耗时数据统计。


在 Android 4.1 - 7.0 之间,JankStats 仍然是基于 Choreographer 来做帧率计算的,但方案和前两者均不同。具体来说,JankStats 通过监听 OnPreDrawListener 来感知绘制任务的发生,此时,通过反射 Choreographer 的 mLastFrameTimeNanos 来获取当前帧的起始时间,再通过往 UI 线程的消息队列头部抛任务的方式来获取当前帧的 UI 线程绘制任务结束时间( 在支持异步消息情况下,将该消息设置为异步消息,尽量保证获取结束时间的任务紧跟在当前任务之后 )。


这一方案简单可靠,而且得到的确实是真正的帧率数据。


在 Android 7.0 及以上版本,JankStats 则直接通过 Window 的新方法 addOnFrameMetricsAvailableListener,注册回调得到每一帧的详细数据 FrameMetrics。
FrameMetrics 的数据统计具体是怎么实现的?简单来说,Android 官方在整个绘制渲染流程上都做了打点来支持 FrameMetrics 的数据统计逻辑,具体包括了



  • 基于 Choreographer 记录了 VSync,Input,Animation,Traversal 的起始时间点

  • 基于 ViewRootImpl 记录了 Draw 的起始时间点( 结合 Traversal 区分开了 MeasureLayout 和 Draw 两段耗时 )

  • 基于 CanvasContext( hwui 中 RenderThread 的渲染流程 )记录了 SyncDisplayList,IssueDrawCommand,SwapBuffer 等时间点,Android 12 上更是进一步支持了 GPU 上的耗时统计


可以看到,FrameMetrics 提供了以往方案难以给到的详细分阶段耗时( 特别注意 FrameMetrics 提供的数据存在系统版本间的差异,具体的数据处理可以参考 JankStats 的内部实现 ),而且在内部实现上,相关数据在绘制渲染流程上默认便会被统计( 即使我们不注册监听 ),基于 FrameMetrics 来做帧率计算在数据采集阶段带来的额外性能开销微乎其微。


帧率相关指标设计


简单的 FPS( 平均帧率 )数据并不能很好的衡量卡顿。在能够准确采集到帧数据之后,怎么基于采集到的数据做进一步处理得到更有实际价值的指标才是更为关键的。


Android Vitals 方案


Android Vitals 本身只定义了两个指标,基于单帧耗时简单区分了卡顿问题的严重程度。将耗时大于 16ms 的帧定义为慢帧,将耗时大于 700ms 的帧定义为冻帧。


JankStats 方案


JankStats 的实现上,则默认将单帧耗时在期望耗时 2 倍以上的情况视为卡顿( 即满帧 60FPS 的情况,将单帧耗时超过 33.3ms 的情况定义为卡顿 )。


Matrix 方案


Matrix 的指标设计则在 Android Vitals 的基础上做了进一步细分,以掉帧数为量化指标( 即满帧 60FPS 的情况,将单帧耗时在 16.6ms 到 33.3ms 间的情况定义为掉一帧 ),将帧耗时细化为 Best / Normal / Middle / High / Frozen 多类情况,其中 Frozen 实际对应的就是 Android Vitals 中的冻帧。


可以看到以上三者都是基于单帧耗时本身而非平均帧率来衡量卡顿。


手淘方案


手淘的方案则给出了更多的指标。


基于帧率数据本身的,细分场景定义了滑动帧率和卡顿帧率。
滑动帧率比较好理解;
卡顿帧率指的是,在出现卡顿帧( 定义为单帧耗时 33.3ms 以上 )之后,持续统计一段时间的帧耗时( 直到达到 99.6ms ( 6 帧 )并且下一帧不是卡顿帧 )来计算帧率,通过单独统计以卡顿帧为起点的细粒度帧率来避免卡顿被平均帧率掩盖的问题。和前面几个方案相比,卡顿帧率的特点在于一定程度上保留了帧数据的时间属性,一定程度上可以区分出离散的卡顿帧和连续的卡顿。


基于单帧耗时的,将冻帧占比( 即大于 700 ms 的帧占总帧数的比例 )作为独立指标;此外还有参考 iOS 定义的 scrollHitchRate,即滑动场景下,预期外耗时( 即每一帧超过期望耗时部分的累加 )的占比。


此外,得益于 FrameMetrics 的详细数据,手淘的方案还实现了简单的自动化归因,即基于 FrameMetrics 的分阶段耗时数据简单判断是哪个阶段导致了当前这一帧的卡顿。


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

超好用的官方core-ktx库,了解一下(终)~

ktx
Handler.postDelayed()简化lambda传入 不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码:public final boolean postDelayed(@NonNull Run...
继续阅读 »

Handler.postDelayed()简化lambda传入


不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码:

public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}

可以看到Runnable类型的参数r放在第一位,在Kotlin中我们就无法利用其提供的简洁的语法糖,只能这样使用:

private fun test11(handler: Handler) {
handler.postDelayed({
//编写代码逻辑
}, 100)
}

有没有感觉很别扭,估计官方也发现了这个问题,就提供了这样一个扩展方法:

public inline fun Handler.postDelayed(
delayInMillis: Long,
token: Any? = null,
crossinline action: () -> Unit
): Runnable {
val runnable = Runnable { action() }
if (token == null) {
postDelayed(runnable, delayInMillis)
} else {
HandlerCompat.postDelayed(this, runnable, token, delayInMillis)
}
return runnable
}

可以看到将函数类型(相当于上面的Runnable中的代码执行逻辑)放到了方法参数的最后一位,这样利用kotlin的语法糖就可以这样使用:

private fun test11(handler: Handler) {
handler.postDelayed(200) {

}
}

可以看到这个函数类型使用了crossinline修饰,这个是用来加强内联的,因为其另一个Runnable的函数类型中进行了调用,这样我们就无法在这个函数类型action中使用return关键字了(return@标签除外),避免使用return关键字带来返回上的歧义不稳定性


除此之外,官方core-ktx还提供了类似的扩展方法postAtTime()方法,使用和上面一样!!


Context.getSystemService()泛型实化获取系统服务


看下以往我们怎么获取ClipboardManager:

private fun test11() {
val cm: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}

看下官方提供的方法:

public inline fun <reified T : Any> Context.getSystemService(): T? =
ContextCompat.getSystemService(this, T::class.java)

借助于内联泛型实化简化了获取系统服务的代码逻辑:

private fun test11() {
val cm: ClipboardManager? = getSystemService()
}

泛型实化的用处有很多应用场景,大家感兴趣可以参考我另一篇文章:Gson序列化的TypeToken写起来太麻烦?优化它


Context.withStyledAttributes简化操作自定义属性


这个扩展一般只有在自定义View中较常使用,比如读取xml中设置的属性值,先看下我们平常是如何使用的:

private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
val ta = context.obtainStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
)
//获取属性执行对应的操作逻辑
val tmp = ta.getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)

ta.recycle()
}

在获取完属性值后,还需要调用recycle()方法回收TypeArray,这个一旦忘记写就不好了,能让程序保证的写法那就尽量避免人为处理,所以官方提供了下面的扩展方法:

public inline fun Context.withStyledAttributes(
@StyleRes resourceId: Int,
attrs: IntArray,
block: TypedArray.() -> Unit
) {
obtainStyledAttributes(resourceId, attrs).apply(block).recycle()
}

使用如下:

private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
context.withStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
) {
val tmp = getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)
}
}

上面的写法就保证了recycle()不会漏写,并且带接收者的函数类型block: TypedArray.() -> Unit也能让我们省略this直接调用TypeArray中的公共方法。


SQLiteDatabase.transaction()自动开启事务读写数据库


平常对SQLite进行写操作时为了效率及安全保证需要开启事务,一般我们都会手动进行开启和关闭,还是那句老话,能程序自动保证的事情就尽量避免手动实现,所以一般我们都会封装一个事务开启和关闭的方法,如下:

private fun writeSQL(sql: String) {
SQLiteDatabase.beginTransaction()
//执行sql写入语句
SQLiteDatabase.endTransaction()
}

官方core-ktx也提供了相似的扩展方法:

public inline fun <T> SQLiteDatabase.transaction(
exclusive: Boolean = true,
body: SQLiteDatabase.() -> T
): T {
if (exclusive) {
beginTransaction()
} else {
beginTransactionNonExclusive()
}
try {
val result = body()
setTransactionSuccessful()
return result
} finally {
endTransaction()
}
}

大家可以自行选择使用!


<K : Any, V : Any> lruCache()简化创建LruCache


LruCache一般用作数据缓存,里面使用了LRU算法来优先淘汰那些近期最少使用的数据。在Android开发中,我们可以使用其设计一个Bitmap缓存池,感兴趣的可以参考Glide内存缓存这块的源码,就利用了LruCache实现。


相比较于原有创建LruCache的方式,官方库提供了下面的扩展方法简化其创建流程:

inline fun <K : Any, V : Any> lruCache(
maxSize: Int,
crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
@Suppress("USELESS_CAST")
crossinline create: (key: K) -> V? = { null as V? },
crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
{ _, _, _, _ -> }
): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun sizeOf(key: K, value: V) = sizeOf(key, value)
override fun create(key: K) = create(key)
override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
onEntryRemoved(evicted, key, oldValue, newValue)
}
}
}

看下使用:

private fun createLRU() {
lruCache<String, Bitmap>(3072, sizeOf = { _, value ->
value.byteCount
}, onEntryRemoved = { evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap? ->
//缓存对象被移除的回调方法
})
}

可以看到,比之手动创建LruCache要稍微简单些,能稍微节省下使用成本。


bundleOf()快捷写入并创建Bundle对象


image.png


bundleOf()方法的参数被vararg声明,代表一个可变的参数类型,参数具体的类型为Pair,这个对象我们之前的文章有讲过,可以借助中缀表达式函数to完成Pair的创建:

private fun test12() {
val bundle = bundleOf("a" to "a", "b" to 10)
}

这种通过传入可变参数实现的Bundle如果大家不太喜欢,还可以考虑自行封装通用扩展函数,在函数类型即lambda中实现更加灵活的Bundle创建及写入:


1.自定义运算符重载方法set实现Bundle写入:

operator fun Bundle.set(key: String, value: Any?) {
when (value) {
null -> putString(key, null)

is Boolean -> putBoolean(key, value)
is Byte -> putByte(key, value)
is Char -> putChar(key, value)
is Double -> putDouble(key, value)
is Float -> putFloat(key, value)
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Short -> putShort(key, value)

is Serializable -> putSerializable(key, value)
//其实数据类型自定参考bundleOf源码实现
}
}

2.自定义BundleBuild支持向Bundle写入多个值

class BundleBuild(private val bundle: Bundle) {

infix fun String.to(that: Any?) {
bundle[this] = that
}
}

其中to()方法使用了中缀表达式的写法


3.暴漏扩展方法实现在lambda中完成Bundle的写入和创建

private fun bundleOf(block: BundleBuild.() -> Unit): Bundle {
return Bundle().apply {
BundleBuild(this).apply(block)
}
}

然后就可以这样使用:

private fun test12() {
val bundle = bundleOf {
"a" to "haha"
//经过一些逻辑操作获取结果后在写入Bundle
val t1 = 10 * 5
val t2 = ""
t2 to t1
}
}

相比较于官方库提供的bundleOf()提供的创建方式,通过函数类型也就是lambda创建并写入Bundle的方式更加灵活,并内部支持执行操作逻辑获取结果后再进行写入。


总结


关于官方core-ktx的研究基本上已经七七八八了,总共输出了五篇相关文章,对该库了解能够节省我们编写模板代码的时间,提高开发效率,大家如果感觉写的不错,可以点个赞支持下哈,感谢!!


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

咱不吃亏,也不能过度自卫

我之前写了一篇《吃亏不是福》,主要奉劝大家不要吃亏。这属于保护弱者的一面。 这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。 我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。 小刘一听,感...
继续阅读 »

我之前写了一篇《吃亏不是福》,主要奉劝大家不要吃亏。这属于保护弱者的一面。


这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。


我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。


小刘一听,感觉自己有被指控的风险。


他立刻严厉起来:“每天都来公司,不一定就算全勤。没打卡我是不统计的”。


最后小刘一查,发现是自己统计错了。


小刘反而更加强势了:“这种事情,你应该早点跟我反馈,而且多催着我确认。你自己的事情都不上心,扣个钱啥的只能自己兜着”


这就是明显的不愿意吃亏,即使自己错了,也不愿意让自己置于弱势。


你的反应,决定别人怎么对你。这种连言语的亏都不吃的人,并不会让别人敬畏,反而会让人厌恶,进而影响沟通


我还有一个同事老王。他是一个职场老人,性格嘻嘻哈哈,业务能力也很强。


以前同事小赵和老王合作的时候,小赵宁愿经两层人传话给老王,也不愿意和他直接沟通。


我当时感觉小赵不善于沟通。


后来,当我和老王合作的时候,才体会到小赵的痛苦。


因为,老王是一个什么亏都不吃的人,谁来找他理论,他就怼谁。


你告诉他有疏漏,他会极力掩盖问题,并且怒怼你愚昧无知。


就算你告诉他,说他家着火了。他首先说没有。你一指那不是烧着的吗?他回复,你懂个屁,你知道我几套房吗?我说的是我另一个家没着火。


有不少人,从不吃亏,无论什么情况,都不会让自己处于弱势。


这类人喜欢大呼小叫,你不小心踩他脚了,他会大喊:践踏我的尊严,和你拼了!


心理学讲,愤怒源于恐惧,因为他想逃避当前不利的局面


人总会遇到各种不公的待遇,或误会,或委屈。


遇到争议时,最好需要确认一下,排除自己的问题。


如果自己没错,那么比较好的做法就是:“我认为你说得不合理,首先……其次……最后……”。


不盲目服软,也不得理不饶人,全程平心静气,有理有据。这种人绝对人格魅力爆棚,让人敬佩。


最后,有时候过度强硬也是一种策略,可以很好地过滤和震慑一些不重要的事物。


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

抽象类与抽象方法

类到对象是实例化。对象到类是抽象。 抽象类: 1.什么是抽象类? 类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。 类本身是不存在的,所以抽象类无法创建对象(无法实例化) 2.抽象类属于什么类型?抽象类也属于引用数据类型。 3.抽象类怎么定义...
继续阅读 »

类到对象是实例化。对象到类是抽象。


抽象类:
1.什么是抽象类?
类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。
类本身是不存在的,所以抽象类无法创建对象(无法实例化)


2.抽象类属于什么类型?抽象类也属于引用数据类型。


3.抽象类怎么定义?@#^%&^^&^%
语法:
(修饰符列表)abstract class类名{类体}


4.抽象类是无法实例化的,无法创建对象的,所以抽象类是用来被子类继承的。


5.final和abstract不能联合使用,这两个关键字是对立的。


6.抽象类的子类可以是抽象类。


7.抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法提供子类使用的。


8.抽象类关联到一个概念,抽象方法,什么是抽象方法呢?
抽象方法标识没有实现的方法,没有方法体的方法。例如:
public abstract void dosome();


9.抽象方法的类必须是抽象类,抽象类的方法不一定要是抽象方法。
抽象方法的特点是:特点1:没有方法体,以分号结尾。特点2:前面修饰符列表中有abstract关键字


10.抽象类中不一定有抽象方法,但抽象方法必须出现在抽象类中


11.非抽象方法集成抽象类,必须重写抽象类里的方法,如果是抽象类继承抽象类,那么就不一定要重写父类的方法


1663425406511.jpg

public abstract class AbstractTest extends AbstractChildTest{
/**
* 类到对象是实例化。对象到类是抽象
*
*/
public static void main(String[] args) {

}

@Override
void Bird() {

}
}
abstract class AbstractChildTest{
//子类继承抽象类
abstract void Bird();
}

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

检测zip文件完整(进阶:APK文件渠道号)

zip
朋友聊天讨论到一个问题,怎么检测zip的完整性。zip是我们常用的压缩格式,不管是Win/Mac/Linux下都很常用,我们做文件的下载也会经常用到,网络充满不确定性,对于多个小文件(比如配置文件)的下载,我们希望只发起一次连接,因为建立连接是很耗费资源的,即...
继续阅读 »

朋友聊天讨论到一个问题,怎么检测zip的完整性。zip是我们常用的压缩格式,不管是Win/Mac/Linux下都很常用,我们做文件的下载也会经常用到,网络充满不确定性,对于多个小文件(比如配置文件)的下载,我们希望只发起一次连接,因为建立连接是很耗费资源的,即使现在http2.0可以对一条TCP连接进行复用,我们还是希望网络请求的次数越少越好,不管是对于稳定性还是成功失败的逻辑判断,都会有益处。


这个时候我们常用的其实就是把他们压缩成一个zip文件,下载下来之后解压就好了。


但很多时候zip会解压失败,如果我们的zip已经下载下来了,其实不存在没有访问权限的问题了,那原因除了空间不够之外,就是zip格式有问题了,zip文件为空或者只下载了一半。

这个时候就需要检测一下我们下载下来的zip是不是合法有效的zip了。

有这么几种思路:



  1. 直接解压,抛异常表明zip有问题

  2. 下载前得到zip文件的length,下载后检测文件大小

  3. 使用md5或sha1等摘要算法,下载下来后做md5,然后比对合法性

  4. 检测zip文件结尾的特殊编码格式,检测是否zip合法


这几种做法有利有弊,这里我们只看第4种。

我们讨论之前,可以大致了解一下zip的格式ZIP文件格式分析,我们关注的是End of central directory record,核心目录结束标记,每个zip只会出现一次。



































































OffsetBytesDescription
04End of central directory signature = 0x06054b50核心目录结束标记(0x06054b50)
42Number of this disk当前磁盘编号
62number of the disk with the start of the central directory核心目录开始位置的磁盘编号
82total number of entries in the central directory on this disk该磁盘上所记录的核心目录数量
102total number of entries in the central directory核心目录结构总数
124Size of central directory (bytes)核心目录的大小
164offset of start of central directory with respect to the starting disk number核心目录开始位置相对于archive开始的位移
202.ZIP file comment length(n)注释长度
22n.ZIP Comment注释内容

我们可以看到,0x06054b50所在的位置其实是在zip.length减去22个字节,所以我们只需要seek到需要的位置,然后读4个字节看是否是0x06054b50,就可以确定zip是否完整。

下面是一个判断的代码

        //没有zip文件注释时候的目录结束符的偏移量
private static final int RawEndOffset = 22;
//0x06054b50占4个字节
private static final int endOfDirLength = 4;
//目录结束标识0x06054b50 的小端读取方式。
private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};

private boolean isZipFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
if (file.length() <= RawEndOffset + endOfDirLength) {
return false;
}
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//seek到结束标记所在的位置
randomAccessFile.seek(fileLength - RawEndOffset);
byte[] end = new byte[endOfDirLength];
//读取4个字节
randomAccessFile.read(end);
//关掉文件
randomAccessFile.close();
return isEndOfDir(end);
} else {
return false;
}
}

/**
* 是否符合文件夹结束标记
*/
private boolean isEndOfDir(byte[] src) {
if (src.length != endOfDirLength) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != endOfDir[i]) {
return false;
}
}
return true;
}

有人可能注意到了,你上面写的结束标识明明是0x06054b50,为什么检测的时候是反着写的。这里就涉及到一个大端小端的问题,录音的时候也能会遇到大小端顺序的问题,反过来读就好了。


涉及到二进制的查看和编辑,我们可以使用010editor这个软件来查看文件的十六进制或者二进制,并且可以手动修改某个位置的二进制。





他的界面大致长这样子,小端显示的,我们可以看到我们要得到的06 05 4b 50


我们看上面的表格里面最后一个表格里的 .ZIP file comment length(n).ZIP Comment ,意思是描述长度是两个字节,描述长度是n,表示这个长度是可变的。这个有啥作用呢?

其实就是给了一个可以写额外的描述数据的地方(.ZIP Comment),他的长度由前面的.ZIP file comment length(n)来控制。也就是zip允许你在它的文件结尾后面额外的追加内容,而不会影响前面的数据。描述文件的长度是两个字节,也就是一个short的长度,所以理论上可以寻址2^16^个位置。

举个例子:

修改之前:

修改之前


修改之后

修改之后

看上面两个文件,修改之前长度为0,我们把它改成2(注意大小端),我们改成2,然后随便在后面追加两个byte,保存,打开修改之后的zip,发现是可以正常运行的,甚至我们可以在长度是2的基础上追加多个byte,其实还是可以打开的。

所以回到标题内容,其实apk就是zip,我们同样可以在apk的Comment后面追加内容,比如可以当做渠道来源,或者完成这样的需求:h5网页A上下载的需要打开某个ActivityA,h5网页B上下载的需要打开某个ActivityB。


原理还是上面的原理,写入渠道或者配置,读取apk渠道或者配置,做相应统计或者操作。

        //magic -> yocn
private static final byte[] MAGIC = new byte[]{0x79, 0x6F, 0x63, 0x6E};
//没有zip文件注释时候的目录结束符的偏移量
private static final int RawEndOffset = 22;
//0x06054b50占4个字节
private static final int endOfDirLength = 4;
//目录结束标识0x06054b50 的小端读取方式。
private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};
//注释长度占两个字节,所以理论上可以支持 2^16 个字节。
private static final int commentLengthBytes = 2;
//注释长度
private static final int commentLength = 8;

private boolean isZipFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
if (file.length() <= RawEndOffset + endOfDirLength) {
return false;
}
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
//seek到结束标记所在的位置
randomAccessFile.seek(fileLength - RawEndOffset);
byte[] end = new byte[endOfDirLength];
//读取4个字节
randomAccessFile.read(end);
//关掉文件
randomAccessFile.close();
return isEndOfDir(end);
} else {
return false;
}
}

/**
* 是否符合文件夹结束标记
*/
private boolean isEndOfDir(byte[] src) {
if (src.length != endOfDirLength) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != endOfDir[i]) {
return false;
}
}
return true;
}

/**
* zip(apk)尾追加渠道信息
*/
private void write2Zip(File file, String channelInfo) throws IOException {
if (isZipFile(file)) {
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//seek到结束标记所在的位置
randomAccessFile.seek(fileLength - commentLengthBytes);
byte[] lengthBytes = new byte[2];
lengthBytes[0] = commentLength;
lengthBytes[1] = 0;
randomAccessFile.write(lengthBytes);
randomAccessFile.write(getChannel(channelInfo));
randomAccessFile.close();
}
}

/**
* 获取zip(apk)文件结尾
*
* @param file 目标哦文件
*/
private String getZipTail(File file) throws IOException {
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
//seek到magic的位置
randomAccessFile.seek(fileLength - MAGIC.length);
byte[] magicBytes = new byte[MAGIC.length];
//读取magic
randomAccessFile.read(magicBytes);
//如果不是magic结尾,返回空
if (!isMagicEnd(magicBytes)) return "";
//seek到读到信息的offest
randomAccessFile.seek(fileLength - commentLength);
byte[] lengthBytes = new byte[commentLength];
//读取渠道
randomAccessFile.read(lengthBytes);
randomAccessFile.close();
char[] lengthChars = new char[commentLength];
for (int i = 0; i < commentLength; i++) {
lengthChars[i] = (char) lengthBytes[i];
}
return String.valueOf(lengthChars);
}

/**
* 是否以魔数结尾
*
* @param end 检测的byte数组
* @return 是否结尾
*/
private boolean isMagicEnd(byte[] end) {
for (int i = 0; i < end.length; i++) {
if (MAGIC[i] != end[i]) {
return false;
}
}
return true;
}

/**
* 生成渠道byte数组
*/
private byte[] getChannel(String s) {
byte[] src = s.getBytes();
byte[] channelBytes = new byte[commentLength];
System.arraycopy(src, 0, channelBytes, 0, commentLength);
return channelBytes;
}

//读取源apk的路径
public static String getSourceApkPath(Context context, String packageName) {
if (TextUtils.isEmpty(packageName))
return null;
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
return appInfo.sourceDir;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}

这里使用了一个魔数的概念,表明是否是写入了我们特定的渠道,只有写了我们特定渠道的基础上才会去读取,防止读到了没有写过的文件。

读取渠道的时候首先获取安装包的绝对路径。Android系统在用户安装app时,会把用户安装的apk拷贝一份到/data/apk/路径下,通过getSourceApkPath 可以获取该apk的绝对路径。如果使用rw可能会有权限问题,所以读取的时候只使用r就可以了。


参考:
ZIP文件格式分析
全民K歌增量升级方案


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

由浅入深,详解 Lifecycle 生命周期组件的那些事

Hi , 你好 :) 引言 在2022的今天,AndroidX 普遍的情况下,JetPack Lifecycle 也早已经成为了开发中的基础设施,小到 View(扩展库) ,大到 Activity,都隐藏着它的身影,而了解 Lifecycle 也正是理解 Je...
继续阅读 »

Hi , 你好 :)


引言


在2022的今天,AndroidX 普遍的情况下,JetPack Lifecycle 也早已经成为了开发中的基础设施,小到 View(扩展库) ,大到 Activity,都隐藏着它的身影,而了解 Lifecycle 也正是理解 JetPack 组件系列库生命感知设计的基础。



本篇定位中级,将从背景到源码实现,从而理解其背后的设计思想。



导航


看完本篇,你将会搞清以下问题:



  • Lifecycle 的前世今生;

  • Lifecycle 可以干什么;

  • Lifecycle 源码解析;


背景


在开始前,我们先聊聊关于 Lifecycle 的前世今生,从而便于我们更好的理解 Lifecycle 存在的意义。


洪荒之时


Lifecycle 之前(不排除现在😂),如果我们要在某个生命周期去执行一些操作时,经常会在Act或者Fragment写很多模版代码,如下两个示例:



  1. 比如,有一个定时器,我们需要在 Activity 关闭时将其关闭,从而避免因此导致的内存问题,所以我们自然会在 onDestory() 中去stop一下。这些事看起来似乎不麻烦,但如果是一个重复多处使用的代码,细心的开发者会将其单独抽离成为一个 case ,从而通过组合的方式降低我们主类中的逻辑,但不可避免我们依然还要存在好多模版代码,因为每次都需要 onStop() 清理或者其他操作(除非写到base,但不可接受☹️)。


📌 如果能不需要开发者自己手动,该有多好?



  1. 在老版本的友盟中,我们经常甚至需要在基类的 Activity 中复写 onResume()onPause() 等方法,这些事情说麻烦也不麻烦,但总是感觉很不舒服。不过,有经验的开发者肯定会想喷,为什么一个三方库,你就自己不会通过application.registerActivityLifecycleCallbacks 监听吗🤌🏼。


📌 注意,Application有监听全局Act生命周期的方法,Act也有这个方法。🤖


盘古开天


JetPack 之前,Android 一直秉承着传统的 MVC 架构,即 xml 作为 View, Activity 作为 ControlModel 层由数据模型或者仓库而定。虽然说官方曾经有一个MVP的示例,但真正用的人并不多。再加上官方一直也没推荐过 Android 的架构指南,这就导致传统的Android开发方式和系统的碎片化一样☹️,五花八门。随着时间的推移,眼看前端的MVVM已愈加成熟,后有Flutter,再加上开发者的需求等背景下,Google于2017年发布了新的开发架构: AAC,全名 Architecture,并且伴随着一系列相关组件,从而帮助开发者提高开发体验,降低错误概率,减少模版代码。


而本篇的主题 Lifecycle 正是其中作为基础设施的存在,在 sdk26 之后,更是被写入了基础库中。


那Lifecycle到底是干什么的呢?



Lifecycle 做的事情很简单,其就是用于检测组件(FragmentAct) 的生命周期,从而不必强依赖于 ActivityFragment ,帮助开发者降低模版代码。



常见用法


在官网中,对于 Lifecycle 的生命周期感知的整个流程如下所示:


image.png




Api介绍


相关字段




  • Event


    生命周期事件,对应具体的生命周期:


    ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY, ON_ANY




  • State


    生命周期状态节点,与 Event 紧密关联,Event 是这些结点的具体边界,换句话说,State 代表了这一时刻的具体状态,其相当于一个范围集,在这个范围里,都是这个状态。而Event代表这一状态集里面触发的具体事件是什么。


    INITIALIZED

    构造函数初始化时,且未收到 `onCreate()` 事件时的状态;

    CREATED

    在 `onCreate()` 调用之后,`onStop()` 调用之前的状态;

    STARTED

    在 `onStart()` 调用之后,`onPause()` 调用之前的状态;



RESUMED

    在 `onResume()` 调用时的状态;

`DESTROYED`

`onDestory()` 调用时的状态;

相关接口




  • LifecycleObserver


    基础 Lifecycler 实现接口,一般调用不到,可用于自定义的组件,从而避免在 Act 或者 Fragment 中的模版代码,例如ViewTreeLifecyleOwner




  • LifecycleEventObserver


    可以接受所有生命周期变化的接口;




  • FullLifecycleObserver


    用于监听生命周期变化的接口,提供了 onCreate()onStart()onPause()onStop()onDestory()onAny();




  • DefaultLifecycleObserver


    FullLifecycleObserver 的默认实现版本,相比前者,增加了默认 null 实现;






举个栗子


如下所示,通过实现 DefaultLifecycleObserver 接口,我们可以在中重写我们想要监听的生命周期方法,从而将业务代码从主类中拆离出来,且更便于复用。最后在相关类中直接使用 lifecycle.addObserver() 方法添加实例即可,这也是google推荐的用法。


image.png



上述示例中,我们使用了 viewLifecycle ,而不是 lifecycle ,这里顺便提一下。


见名之意,前者是视图(view)生命周期,后者则是非视图的生命周期,具体区别如下:


viewLifecycle 只会在 onCreateView-onDestroyView 之间有效。


lifecycle 则是代表 Fragment 的生命周期,在视图未创建时,onCreate(),onDestory() 时也会被调用到。





或者你有某个自定义View,想感知Fragment或者Act的生命周期,从而做一些事情,比如Banner组件等,与上面示例类似:


image.png



当然你也可以选择依赖:androidx.lifecycle:lifecycle-runtime 扩展库。


从而使用 view.findViewTreeLifecycleOwner() 的扩展函数获得一个 LifecycleOwner,从而在View内部自行监听生命周期,免除在Activity手动添加观察者的模版代码。


lifecycle.addObserver(view)



源码解析


Lifecycle



在官方的解释里,Lifecycle 是一个类,用于存储有关组件(Act或Fragment)声明周期状态新的类,并允许其他对象观察此类。



直接去看 Lifecycle 的源码,其实现方式如下:


image.png
总体设计如上所示,比较简单,就是观察者模式的接口模版:



使用者实现 LifecycleObserver 接口(),然后调用 addObserver() 添加到观察者列表,取消观察者时调用 rmeoveObserver() 移除掉即可。在相应的生命周期变动时,遍历观察者列表,然后通知实现了 LifecycleObserver 的实例,从而调用相应的方法。



因为其是一个抽象类,所以我们调用的一般都是它的具体实现类,也就是 LifecycleRegistry ,目前也是其的唯一实现类。




LifecycleRegistry


Lifecycle 的具体实现者,正如其名所示,主要用于管理当前订阅的 观察者对象 ,所以也承担了 Lifecycle 具体的实现逻辑。因为源码较长,所以我们做了一些删减,只需关注主流程即可,伪代码如下:

public class LifecycleRegistry extends Lifecycle {

// 生命周期观察者map,LifecycleObserver是观察者接口,ObserverWithState具体的状态分发的包装类
   private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap;
// 当前生命周期状态
   private State mState = INITIALIZED;
// 持有生命周期的提供商,Activity或者Fragment的弱引用
   private final WeakReference<LifecycleOwner> mLifecycleOwner;
// 当前正在添加的观察者数量,默认为0,超过0则认为多线程调用
   private int mAddingObserverCounter = 0;
// 是否正在分发事件
   private boolean mHandlingEvent = false;
// 是否有新的事件产生
   private boolean mNewEventOccurred = false;
// 存储主类的事件state
   private ArrayList<State> mParentStates = new ArrayList<>();

   @Override
   public void addObserver(@NonNull LifecycleObserver observer) {
       // 初始化状态,destory or init
       State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
    // 📌 初始化实际分发状态的包装类
       ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
    // 将观察者添加到具体的map中,如果已经存在了则返回之前的,否则创建新的添加到map中
       ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
// 如果上一步添加成功了,putIfAbsent会返回null
       if (previous != null) {
           return;
      }
    // 如果act或者ff被回收了,直接return
       LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
       if (lifecycleOwner == null) {
           return;
      }
// 当前添加的观察者数量!=0||正在处理事件
       boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
    // 📌 取得观察者当前的状态
       State targetState = calculateTargetState(observer);
       mAddingObserverCounter++;
    // 📌 如果当前观察者状态小于当前生命周期所在状态&&这个观察者已经被存到了观察者列表中
       while ((statefulObserver.mState.compareTo(targetState) < 0
               && mObserverMap.contains(observer))) {
        // 保存当前的生命周期状态
           pushParentState(statefulObserver.mState);
        // 返回当前生命周期状态对应的接下来的事件序列
           final Event event = Event.upFrom(statefulObserver.mState);
          ...
           // 分发事件
           statefulObserver.dispatchEvent(lifecycleOwner, event);
        // 移除当前的生命周期状态
           popParentState();
           // 再次获得当前的状态,以便继续执行
           targetState = calculateTargetState(observer);
      }

    // 处理一遍事件,保证事件同步
       if (!isReentrance) {
           sync();
      }
    // 回归默认值
       mAddingObserverCounter--;
  }
...
   static class ObserverWithState {
       State mState;
       LifecycleEventObserver mLifecycleObserver;

       ObserverWithState(LifecycleObserver observer, State initialState) {
        // 初始化事件观察者
           mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
           mState = initialState;
      }

       void dispatchEvent(LifecycleOwner owner, Event event) {
           State newState = event.getTargetState();
           mState = min(mState, newState);
           mLifecycleObserver.onStateChanged(owner, event);
           mState = newState;
      }
  }
...
}

我们重点关注的是 addObserver() 即订阅生命周期变更时的逻辑,具体如下:



  1. 先初始化当前观察者的状态,默认两种,即 DESTROYED(销毁) 或者 INITIALIZED(无效),分别对应 onDestory()onCreate() 之前;

  2. 初始化 状态观察者(ObserverWithState) ,内部使用 Lifecycling.lifecycleEventObserver() 将我们传递进来的 生命周期观察者(LifecycleObser) 包装为一个 生命周期[事件]观察者LifecycleEventObserver,从而在状态变更时触发事件通知;

  3. 将第二步生成的状态观察者添加到缓存map中,如果之前已经存在,则停止接下来的操作,否则继续初始化;

  4. 调用 calculateTargetState() 获得当前真正的状态。

  5. 开始事件轮训,如果 当前观察者的状态小于此时真正的状态 && 观察者已经被添加到了缓存列表 中,则获得当前观察者下一个状态,并触发相应的事件通知 dispatchEvent(),然后继续轮训。直到不满足判断条件;



需要注意的是, 关于状态的判断,这里使用了compareTo() ,从而判断当前状态枚举是否小于指定状态。





Activity中的实现


Tips:


写过权限检测库的小伙伴,应该很熟悉,为了避免在 Activity 中手动实现 onActivityRequest() ,从而实现以回调的方式获得权限结果,我们往往会使用一个透明的 Fragment ,从而将模版方法拆离到单独类中,而这种实现方式正是组合的思想。


LifecycleActivity 中的实现正是上述的方式。




如下所示,当我们在 Activity 中调用 lifecycle 对象时,内部实际上是调用了 ComponentActivity.mLifecycleRegistry,具体逻辑如下:


image.png
不难发现,在我们的 Activity 初始化时,相应的 LifecycleRegistry 已经被初始化。


在上面我们说过,为了避免对基类的入侵,我们一般会用组合的方式,所以这里的 ReportFragment 正是 LifecycleActivity 中具体的逻辑承载方,具体逻辑如下:


ReportFragment.injectIfNeededIn


image.png


内部会对sdk进行判断,对应着两套流程,对于 sdk>=29 的,通过注册 Activity.registerActivityLifecycleCallbacks() 事件实现监听,对于 sdk<29 的,重写 Fragment 相应的生命周期方法完成。


ReportFragment 具体逻辑如下:


image.png




Fragment中的实现


直接去 Fragment.lifecycle 中看一下即可,伪代码如下:


image.png


总结如下:lifecycle 实例会在 Fragment 构造函数 中进行初始化,而 mViewLifecycleOwner 会在 performCreateView() 执行时初始化,然后在相应的 performXxx 生命周期执行时,调用相应的 lifecycle.handleLifecycleEvent() 从而完成事件的通知。


总结


Lifecycle 作为 JetPack 的基石,而理解其是我们贯穿相应生命周期的关键所在。


关于生命周期的通知,Lifecycle 并没有采用直接通知的方式,而是采用了 Event(事件) + State(状态) 的设计方式。




  • 对于外部生命周期订阅者而言,只需要关注事件 Event 的调用;

  • 而对于Lifecycle而言,其内部只关注 State ,并将生命周期划分为了多个阶段,每个状态又代表了一个事件集,从而对于外部调用者而言,无需拘泥于当前具体的状态是什么。



在具体的实现底层上面:




  • Activity 中,采用组合的方式而非继承,在 Activity.onCreate() 触发时,初始化了一个透明Fragment,从而将逻辑存于其中。对于sdk>=29的版本,因为 Activity 本身有监听生命周期的方法,从而直接注册监听器,并在相应的会调用触发生命周期事件更新;对于sdk<29的版本,因为需要兼容旧版本,所以重写了 Fragment 相应的生命周期方法,从而实现手动触发更新我们的生命周期观察者。

  • Fragment 中,会在 Fragment 构造函数中初始化相应的 Lifecycle ,并重写相应的生命周期方法,从而触发事件通知,实现生命周期观察者的更新。



每当我们调用 addObserver() 添加新的观察者时:



内部都会对我们的观察者进行包装,并将其包装为一个具体的事件观察者 LifecycleEventObserver,以及生成当前的观察者对应的状态实例(内部持有LifecycleEventObserver),并将其保存到 缓存map 中。接着会去对比当前的 观察者的状态lifecycle此时实际状态 ,如果 当前观察者状态<lifecycle对应的状态 ,则触发相应 Event 的通知,并 更新此观察者对应的状态 ,不断轮训,直到当前观察者状态 >= lifecycle 对应状态。



参阅



关于我


我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮助,欢迎点赞支持,你的支持是我持续创作的最大鼓励!


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

雨下不下,花都结果;风吹不吹,我都是我。

最近不怎么忙了,想来写写文章,但突然也不知道从哪里开始聊,感情?工作?还是这稀里糊涂的生活。 2023年大年初六,我结束了和女朋友长达四年零七个月的爱情长跑,但很遗憾不是进入婚姻殿堂而是分手。 从18岁到23岁几乎已经占了我青春为数不多最美好的几年。但好在我熬...
继续阅读 »

最近不怎么忙了,想来写写文章,但突然也不知道从哪里开始聊,感情?工作?还是这稀里糊涂的生活。


2023年大年初六,我结束了和女朋友长达四年零七个月的爱情长跑,但很遗憾不是进入婚姻殿堂而是分手。
从18岁到23岁几乎已经占了我青春为数不多最美好的几年。但好在我熬过来了,最痛苦的那段已经过去了,我独自一人趴在地板上哭的泪水和口水混在一滩也没人看见的那段日子已经过去了,接下来是漫长而偶尔刺痛的恢复不知道要几年。


微信图片_20230330154857.jpg

“男人不经历分手永远不会成长”这句话忘了是谁说的了。以前觉得纯扯蛋的成不成长跟分手有毛关系,现在我认了,心里性格的变化自己都肉眼可见。越来越多的人生道理从几年前的不屑一顾到现在发生在自己身上才认同。可能成长就是这个样子?毕竟谁都是第一次做人,也不知道成长了该因为变成熟了而高兴还是因为离懵懂幼稚的青春越来越远而难过。


年后回来项目就开始忙,压力骤增。入职到现在八个月,平均一个月加班一次(周末)但过完年回来开始忙基本天天晚上九点十点走,其实作为程序员来说这种程度也还好,但跟年前相比确实忙了很多。年前基本是到点下班就走,但也好,忙一点,让自己不那么闲就不会胡思乱想那些乱七八糟的事情。不会那么伤感也不会那么焦虑。而且拿这个工资心里也没那么愧疚,要不天天摸鱼发工资我都想给老板退回去一点。
不过分手后也真的能攒下钱来了,就很神奇真的很神奇,两个月攒了一万二了。还借了朋友2000。以前每个月都剩不下钱来。今年回家终于不用听爸妈再说那句“攒攒钱吧”了


微信图片_20230330152333.jpg
微信图片_20230330150759.jpg

好在还有街舞、吉他、健身、养猫。这些事让我觉得不那么无聊,劳累的工作之余,周末还是要找点事情做。
不能让自己闲下来,闲下来就忍不住开始emo。


其实来南京快一年了,很去那些景点多看看,鸡鸣寺、玄武湖、夫子庙、还有逼哥的1701live house(偷偷告诉你我是因为听了很多年逼哥才选择来南京生活) 拍拍照什么的。但碍于一个人,没有小伙伴一起。觉得怪无聊的,就也放弃了,但我会改变自己的,接下来的日子我也会一个人吃火锅,一个人看电影,一个人去旅游,一个人逛小吃街,一个人拍照打卡。


其实也不是没有朋友,南京也有几个小伙伴小兄弟,认识好些年,关系也都很好,但我们的热衷不在一个频道,他们除了敲代码就是吃鸡、CSGO、联盟、元神。而我除了联盟偶尔还和他们打一打以外,基本也不怎么玩游戏了,可能是岁数大了,游戏瘾没有前几年那么狠了。


虽然现在也有很多爱好但好像也都只是单纯的打发时间,并没有哪件事不做就难受的不行的感觉,换句话说现在好像多少有点无欲无求?


微信图片_20230330152914.jpg

上周去报了江宁一个摩托车驾校,准备考个D证买个摩托车玩一玩(目前计划先买个二手GSX或者春风骑个一两年,没把自己撞死的话再换个najia或者450),但工作的地方在玄武区好像不让骑。那就只能出去玩骑啦,但也祈祷我能顺利考过,可能是我太笨了去年在北京练摩托车,油离配合老弄不好,动不动就熄火,让人笑话,去练了两次就不练了,希望这次能认真一点。


微信图片_20230330154408.jpg

其实也不是只顾着玩了,工作这方面的也在时不时的计划以及改变计划,因为毕竟现在一个岗位放出去一万个简历等着的行情确实不友好。所以现在的行情基本就是已经不再缺少初级、初中级前端。中级少了很多。那么想要在这行继续混下去就只有一条路那就是使劲的打怪刷经验,进化为高级前端。


由于前三年都是vuer,最近半年才转的reacter。所以react写起来还是没有vue那么得心应手,但是我总感觉react才是前端的未来(狗头保命)如果接下来的计划中准备好好恶补一下技术的话,应该就是以react为主vue为辅了。


去年立的flag依旧没变:在南京待1-2年狠补技术去上海冲刺,目标还是带10+人数的前端leader,天天熬夜加班的那种。


作者:我看你像个promise
来源:juejin.cn/post/7216223889487478840
收起阅读 »

从前后端的角度分析options预检请求

本文分享自华为云社区《从前后端的角度分析options预检请求——打破前后端联调的理解障碍》,作者: 砖业洋__ 。 options预检请求是干嘛的?options请求一定会在post请求之前发送吗?前端或者后端开发需要手动干预这个预检请求吗?不用文档定义堆砌...
继续阅读 »

本文分享自华为云社区《从前后端的角度分析options预检请求——打破前后端联调的理解障碍》,作者: 砖业洋__ 。


options预检请求是干嘛的?options请求一定会在post请求之前发送吗?前端或者后端开发需要手动干预这个预检请求吗?不用文档定义堆砌名词,从前后端角度单独分析,大白话带你了解!


从前端的角度看options——post请求之前一定会有options请求?信口雌黄!


你是否经常看到这种跨域请求错误?


image.png


这是因为服务器不允许跨域请求,这里会深入讲一讲OPTIONS请求。


只有在满足一定条件的跨域请求中,浏览器才会发送OPTIONS请求(预检请求)。这些请求被称为“非简单请求”。反之,如果一个跨域请求被认为是“简单请求”,那么浏览器将不会发送OPTIONS请求。


简单请求需要满足以下条件:



  1. 只使用以下HTTP方法之一:GETHEADPOST

  2. 只使用以下HTTP头部:AcceptAccept-LanguageContent-LanguageContent-Type

  3. Content-Type的值仅限于:application/x-www-form-urlencodedmultipart/form-datatext/plain


如果一个跨域请求不满足以上所有条件,那么它被认为是非简单请求。对于非简单请求,浏览器会在实际请求(例如PUTDELETEPATCH或具有自定义头部和其他Content-TypePOST请求)之前发送OPTIONS请求(预检请求)。


举个例子吧,口嗨半天是看不懂的,让我们看看 POST请求在什么情况下不发送OPTIONS请求


提示:当一个跨域POST请求满足简单请求条件时,浏览器不会发送OPTIONS请求(预检请求)。以下是一个满足简单请求条件的POST请求示例:


// 使用Fetch API发送跨域POST请求
fetch("https://example.com/api/data", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "key1=value1&key2=value2"
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));

在这个示例中,我们使用Fetch API发送了一个跨域POST请求。请求满足以下简单请求条件:



  1. 使用POST方法。

  2. 使用的HTTP头部仅包括Content-Type

  3. Content-Type的值为"application/x-www-form-urlencoded",属于允许的三种类型之一(application/x-www-form-urlencoded、multipart/form-data或text/plain)。


因为这个请求满足了简单请求条件,所以浏览器不会发送OPTIONS请求(预检请求)。


我们再看看什么情况下POST请求之前会发送OPTIONS请求,同样用代码说明,进行对比


提示:在跨域请求中,如果POST请求不满足简单请求条件,浏览器会在实际POST请求之前发送OPTIONS请求(预检请求)。


// 使用Fetch API发送跨域POST请求
fetch("https://example.com/api/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "custom-value"
},
body: JSON.stringify({
key1: "value1",
key2: "value2"
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));

在这个示例中,我们使用Fetch API发送了一个跨域POST请求。请求不满足简单请求条件,因为:



  1. 使用了非允许范围内的Content-Type值("application/json" 不属于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。

  2. 使用了一个自定义HTTP头部 “X-Custom-Header”,这不在允许的头部列表中。


因为这个请求不满足简单请求条件,所以在实际POST请求之前,浏览器会发送OPTIONS请求(预检请求)。


你可以按F12直接在Console输入查看Network,尽管这个网址不存在,但是不影响观察OPTIONS请求,对比一下我这两个例子。


总结:当进行非简单跨域POST请求时,浏览器会在实际POST请求之前发送OPTIONS预检请求,询问服务器是否允许跨域POST请求。如果服务器不允许跨域请求,浏览器控制台会显示跨域错误提示。如果服务器允许跨域请求,那么浏览器会继续发送实际的POST请求。而对于满足简单请求条件的跨域POST请求,浏览器不会发送OPTIONS预检请求。


后端可以通过设置Access-Control-Max-Age来控制OPTIONS请求的发送频率。OPTIONS请求没有响应数据(response data),这是因为OPTIONS请求的目的是为了获取服务器对于跨域请求的配置信息(如允许的请求方法、允许的请求头部等),而不是为了获取实际的业务数据,OPTIONS请求不会命中后端某个接口。因此,当服务器返回OPTIONS响应时,响应中主要包含跨域配置信息,而不会包含实际的业务数据


本地调试一下,前端发送POST请求,后端在POST方法里面打断点调试时,也不会阻碍OPTIONS请求的返回


image.png


2.从后端的角度看options——post请求之前一定会有options请求?胡说八道!


在配置跨域时,服务器需要处理OPTIONS请求,以便在响应头中返回跨域配置信息。这个过程通常是由服务器的跨域中间件(Node.jsExpress框架的cors中间件、PythonFlask框架的flask_cors扩展)或过滤器(JavaSpringBoot框架的跨域过滤器)自动完成的,而无需开发人员手动处理。


以下是使用Spring Boot的一个跨域过滤器,供参考


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

public CorsConfig() {
}

@Bean
public CorsFilter corsFilter()
{
// 1. 添加cors配置信息
CorsConfiguration config = new CorsConfiguration();
// Response Headers里面的Access-Control-Allow-Origin: http://localhost:8080
config.addAllowedOrigin("http://localhost:8080");
// 其实不建议使用*,允许所有跨域
config.addAllowedOrigin("*");

// 设置是否发送cookie信息,在前端也可以设置axios.defaults.withCredentials = true;表示发送Cookie,
// 跨域请求要想带上cookie,必须要请求属性withCredentials=true,这是浏览器的同源策略导致的问题:不允许JS访问跨域的Cookie
/**
* withCredentials前后端都要设置,后端是setAllowCredentials来设置
* 如果后端设置为false而前端设置为true,前端带cookie就会报错
* 如果后端为true,前端为false,那么后端拿不到前端的cookie,cookie数组为null
* 前后端都设置withCredentials为true,表示允许前端传递cookie到后端。
* 前后端都为false,前端不会传递cookie到服务端,后端也不接受cookie
*/

// Response Headers里面的Access-Control-Allow-Credentials: true
config.setAllowCredentials(true);

// 设置允许请求的方式,比如get、post、put、delete,*表示全部
// Response Headers里面的Access-Control-Allow-Methods属性
config.addAllowedMethod("*");

// 设置允许的header
// Response Headers里面的Access-Control-Allow-Headers属性,这里是Access-Control-Allow-Headers: content-type, headeruserid, headerusertoken
config.addAllowedHeader("*");
// Response Headers里面的Access-Control-Max-Age:3600
// 表示下回同一个接口post请求,在3600s之内不会发送options请求,不管post请求成功还是失败,3600s之内不会再发送options请求
// 如果不设置这个,那么每次post请求之前必定有options请求
config.setMaxAge(3600L);
// 2. 为url添加映射路径
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
// /**表示该config适用于所有路由
corsSource.registerCorsConfiguration("/**", config);

// 3. 返回重新定义好的corsSource
return new CorsFilter(corsSource);
}
}


这里setMaxAge方法来设置预检请求(OPTIONS请求)的有效期,当浏览器第一次发送非简单的跨域POST请求时,它会先发送一个OPTIONS请求。如果服务器允许跨域,并且设置了Access-Control-Max-Age头(设置了setMaxAge方法),那么浏览器会缓存这个预检请求的结果。在Access-Control-Max-Age头指定的时间范围内,浏览器不会再次发送OPTIONS请求,而是直接发送实际的POST请求,不管POST请求成功还是失败,在设置的时间范围内,同一个接口请求是绝对不会再次发送OPTIONS请求的。


后端需要注意的是,我这里设置允许请求的方法是config.addAllowedMethod("*")*表示允许所有HTTP请求方法。如果未设置,则默认只允许“GET”和“HEAD”。你可以设置的HTTPMethodGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE


经过我的测试,OPTIONS无需手动设置,因为单纯只设置OPTIONS也无效。如果你设置了允许POST,代码为config.addAllowedMethod(HttpMethod.POST); 那么其实已经默认允许了OPTIONS,如果你只允许了GET,尝试发送POST请求就会报错。


举个例子,这里只允许了GET请求,当我们尝试发送一个POST非简单请求,预检请求返回403,服务器拒绝了OPTIONS类型的请求,因为你只允许了GET,未配置允许OPTIONS请求,那么浏览器将收到一个403 Forbidden响应,表示服务器拒绝了该OPTIONS请求,POST请求的状态显示CORS error



Spring Boot中,配置允许某个请求方法(如POSTPUTDELETE)时,OPTIONS请求通常会被自动允许。这意味着在大多数情况下,后端开发人员不需要特意考虑OPTIONS请求。这种自动允许OPTIONS请求的行为取决于使用的跨域处理库或配置,最好还是显式地允许OPTIONS请求。


点击关注,第一时间了解华为云新鲜技术~


作者:华为云开发者联盟
来源:juejin.cn/post/7233587643724234811
收起阅读 »

快速生成定制化的Word文档:Python实践指南

1.1. 前言 众所周知,安服工程师又叫做Word工程师,在打工或者批量SRC的时候,如果产出很多,又需要一个一个的写报告的情况下会非常的折磨人,因此查了一些相关的资料,发现使用python的docxtpl库批量写报告效果很不错,记录一下。 1.2. 介绍 d...
继续阅读 »

1.1. 前言


众所周知,安服工程师又叫做Word工程师,在打工或者批量SRC的时候,如果产出很多,又需要一个一个的写报告的情况下会非常的折磨人,因此查了一些相关的资料,发现使用python的docxtpl库批量写报告效果很不错,记录一下。


1.2. 介绍


docxtpl 是一个用于生成 Microsoft Word 文档的模板引擎库,它结合了 docx 模块和 Jinja2 模板引擎,使用户能够使用 Microsoft Word 模板文件并在其中填充动态数据。它提供了一种方便的方式来生成个性化的 Word 文档,并支持条件语句、循环语句和变量等控制结构,以满足不同的文档生成需求。


官方GitHub地址:github.com/elapouya/py…


官方文档地址:docxtpl.readthedocs.io/en/latest/



简单来说:就是创建一个类似Jinja2语法的模板文档,然后往里面动态填充内容就可以了



安装:


pip3 install docxtpl

1.3. 基础使用


from docxtpl import DocxTemplate

doc = DocxTemplate("test.docx")
context = {'whoami': "d4m1ts"}
doc.render(context)
doc.save("generated_doc.docx")

其中,test.docx内容如下:


test.docx


生成后的结果如下:


gen


1.4. 案例介绍


1.4.1. 需求假设


写一份不考虑美观的漏扫报告,需要有统计结果图和漏洞详情,每个漏洞包括漏洞名、漏洞地址、漏洞等级、漏洞危害、复现过程、修复建议六个部分。


1.4.2. 模板文档准备


编写的模板文档如下,使用到了常见的iffor赋值等,保存为template.docx,后续只需要向里面填充数据即可。


template


1.4.3. 数据结构分析


传入数据需要一串json字符串,因此我们根据模板文档梳理好json结构,然后传入即可。


梳理好的数据结构如下:


{
"饼图": "111",
"柱状图": "222",
"漏洞简报": [
{
"漏洞名": "测试漏洞名1",
"漏洞等级": "高危"
}
],
"漏洞详情": [
{
"漏洞名": "测试漏洞名1",
"漏洞地址": "http://blog.gm7.org/",
"漏洞等级": "高危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
}
]
}

编写代码测试一下可行性:


from docxtpl import DocxTemplate

doc = DocxTemplate("template.docx")
context = {
"饼图": "111",
"柱状图": "222",
"漏洞简报": [
{
"漏洞名": "测试漏洞名1",
"漏洞等级": "高危"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "严重"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "中危"
}
],
"漏洞详情": [
{
"漏洞名": "测试漏洞名1",
"漏洞地址": "http://blog.gm7.org/",
"漏洞等级": "高危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名2",
"漏洞地址": "http://bblog.gm7.org/",
"漏洞等级": "严重",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名3",
"漏洞地址": "http://cblog.gm7.org/",
"漏洞等级": "中危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
}
]
}

doc.render(context)
doc.save("generated_doc.docx")

很好,达到了预期的效果。


res


1.4.4. 加入图表


在上面的过程中,内容几乎是没问题了,但是图表还是没有展示出来。生成图表我们使用plotly这个库,并将生成内容写入ByteIO


相关代码如下:


import plotly.graph_objects as go
from io import BytesIO

def generatePieChart(title: str, labels: list, values: list, colors: list):
"""
生成饼图
https://juejin.cn/post/6911701157647745031#heading-3
https://juejin.cn/post/6950460207860449317#heading-5

:param title: 饼图标题
:param labels: 饼图标签
:param values: 饼图数据
:param colors: 饼图每块的颜色
:return:
"""

# 基础饼图
fig = go.Figure(data=[go.Pie(
labels=labels,
values=values,
hole=.4, # 中心环大小
insidetextorientation="horizontal"
)])
# 更新颜色
fig.update_traces(
textposition='inside', # 文本显示位置
hoverinfo='label+percent', # 悬停信息
textinfo='label+percent', # 饼图中显示的信息
textfont_size=15,
marker=dict(colors=colors)
)
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
}
)
image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io


def generateBarChart(title: str, x: list, y: list):
"""
生成柱状图
https://cloud.tencent.com/developer/article/1817208
https://blog.csdn.net/qq_25443541/article/details/115999537
https://blog.csdn.net/weixin_45826022/article/details/122912484

:param title: 标题
:param x: 柱状图标签
:param y: 柱状图数据
:return:
"""

# x轴长度最为18
b = x
x = []
for i in b:
if len(i) >= 18:
x.append(f"{i[:15]}...")
else:
x.append(i)

# 基础柱状图
fig = go.Figure(data=[go.Bar(
x=x,
y=y,
text=y,
textposition="outside",
marker=dict(color=["#3498DB"] * len(y)),
width=0.3
)])
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
},
xaxis_tickangle=-45, # 倾斜45度
plot_bgcolor='rgba(0,0,0,0)' # 背景透明
)
fig.update_xaxes(
showgrid=False
)
fig.update_yaxes(
zeroline=True,
zerolinecolor="#17202A",
zerolinewidth=1,
showgrid=True,
gridcolor="#17202A",
showline=True
)

image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io

1.4.5. 最终结果


要插入图片内容,代码语法如下:


myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))

完整代码如下:


from docxtpl import DocxTemplate, InlineImage
from docx.shared import Mm
import plotly.graph_objects as go
from io import BytesIO


def generatePieChart(title: str, labels: list, values: list, colors: list):
"""
生成饼图
https://juejin.cn/post/6911701157647745031#heading-3
https://juejin.cn/post/6950460207860449317#heading-5

:param title: 饼图标题
:param labels: 饼图标签
:param values: 饼图数据
:param colors: 饼图每块的颜色
:return:
"""

# 基础饼图
fig = go.Figure(data=[go.Pie(
labels=labels,
values=values,
hole=.4, # 中心环大小
insidetextorientation="horizontal"
)])
# 更新颜色
fig.update_traces(
textposition='inside', # 文本显示位置
hoverinfo='label+percent', # 悬停信息
textinfo='label+percent', # 饼图中显示的信息
textfont_size=15,
marker=dict(colors=colors)
)
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
}
)
image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io


def generateBarChart(title: str, x: list, y: list):
"""
生成柱状图
https://cloud.tencent.com/developer/article/1817208
https://blog.csdn.net/qq_25443541/article/details/115999537
https://blog.csdn.net/weixin_45826022/article/details/122912484

:param title: 标题
:param x: 柱状图标签
:param y: 柱状图数据
:return:
"""

# x轴长度最为18
b = x
x = []
for i in b:
if len(i) >= 18:
x.append(f"{i[:15]}...")
else:
x.append(i)

# 基础柱状图
fig = go.Figure(data=[go.Bar(
x=x,
y=y,
text=y,
textposition="outside",
marker=dict(color=["#3498DB"] * len(y)),
width=0.3
)])
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
},
xaxis_tickangle=-45, # 倾斜45度
plot_bgcolor='rgba(0,0,0,0)' # 背景透明
)
fig.update_xaxes(
showgrid=False
)
fig.update_yaxes(
zeroline=True,
zerolinecolor="#17202A",
zerolinewidth=1,
showgrid=True,
gridcolor="#17202A",
showline=True
)

image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io


doc = DocxTemplate("template.docx")
context = {
"饼图": InlineImage(doc, image_descriptor=generatePieChart(
title="漏洞数量",
labels=["严重", "高危", "中危", "低危"],
values=[1, 1, 1, 0],
colors=["#8B0000", "red", "orange", "aqua"]
), width=Mm(130)),
"柱状图": InlineImage(doc, image_descriptor=generateBarChart(
title="漏洞类型",
x=["测试漏洞名1", "测试漏洞名2", "测试漏洞名3"],
y=[1, 1, 1]
), width=Mm(130)),
"漏洞简报": [
{
"漏洞名": "测试漏洞名1",
"漏洞等级": "高危"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "严重"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "中危"
}
],
"漏洞详情": [
{
"漏洞名": "测试漏洞名1",
"漏洞地址": "http://blog.gm7.org/",
"漏洞等级": "高危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名2",
"漏洞地址": "http://bblog.gm7.org/",
"漏洞等级": "严重",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名3",
"漏洞地址": "http://cblog.gm7.org/",
"漏洞等级": "中危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
}
]
}

doc.render(context)
doc.save("generated_doc.docx")

结果如下:


result


作者:初始安全
来源:juejin.cn/post/7233597845919662139
收起阅读 »

看了十几篇MVX架构的文章后,我悟了...

当经过学一遍又一遍,改一遍又一遍,觉得学不懂想哭的时候,我选择了放弃(洗澡),然后我悟了。 适当开摆有益身心健康 当你纠结于文件该放在哪个包下,纠结于什么功能的代码应该放在哪个类中,于是在学习过程中不断修改,不断重构...开始了这样一个循环。 这有很大一部分原...
继续阅读 »

当经过学一遍又一遍,改一遍又一遍,觉得学不懂想哭的时候,我选择了放弃(洗澡),然后我悟了。


适当开摆有益身心健康


当你纠结于文件该放在哪个包下,纠结于什么功能的代码应该放在哪个类中,于是在学习过程中不断修改,不断重构...开始了这样一个循环。


这有很大一部分原因是因为不统一,架构是一种设计思想,而且大部分是由国外公司、大牛提出,首先在语言理解上就会有一定的差异和误解,如果我们能正确理解设计原则,就可以事半功倍。


就比如并发和并行,看过很多对于这两个的解释就是:并发是多个任务交替使用CPU,同一时刻只有一个任务在跑;并行是多个任务同时跑。


其实就是一种误解,【并发】和【并行】描述的是两个频道的事情。并发是一种处理方法,通过拆分代码,各部分代码互不影响,这样可以充分利用多核心。所以如果想让一个事情变得容易【并行】,先得让制定一个【并发】的方法。倘若一个事情压根就没有【并发】的方法,那么无论有多少个可以干活的人,也不能【并行】。


回到MVX架构,对于怎么样分包,怎么样拆分代码,我觉得应该从思想原理入手,因为文章的写法是各个作者理解,他们的理解不一定就是正确的,包括我。


就以谷歌推荐的架构原则来说,它有以下4点:分离关注点、通过数据模型驱动界面、单一数据源、单向数据流;推荐的架构图如下:


image.png


按照上面这张图,我们在Activity中写界面和界面的展示的数据,现在回看架构原则第一点”关注分离点”,于是我们把界面和界面的数据拆分开,这个过程是自然而然的,所以我更倾向于发挥自己的想象力去把架构实现好,而不是去进行拙略的模仿,现在回想起来20年时我在写项目的时候会自己思考如何去改进,于是自然而然添加了事件和状态(单向数据流),而在之前我并没有去看关于这方面的文章。


当你学习累了,那就大喊一句“开摆”,什么屁架构文章一边去,不学了。(优秀的文章还是值得我们学习的,这里只是我的情绪宣泄)


也许回过头你就学会了,这并不是什么魔法,而是把你从一个深坑中拉了出来,让你的大脑能换个方向思考问题。


我们需要重点学习的是设计原则,剩下的就是发挥我们的想象力。


相关资料


如何理解:程序、进程、线程、并发、并行、高并发? - 大宽宽 知乎 (zhihu.com)


应用架构指南  |  Android 开发者  |  Android Developers (google.cn)


作者:Fanketly
来源:juejin.cn/post/7234057845620408375
收起阅读 »

我给我的博客加了个在线运行代码功能

web
获取更多信息,可以康康我的博客,所有文章会在博客上先发布随记 - 记录指间流逝的美好 (xiaoyustudent.github.io) 前言 新的一年还没过去,我又开始搞事情了,偶尔一次用到了在线编辑网页代码的网站,顿时想到,能不能自己实现一个呢?(PS:反...
继续阅读 »

获取更多信息,可以康康我的博客,所有文章会在博客上先发布随记 - 记录指间流逝的美好 (xiaoyustudent.github.io)


前言


新的一年还没过去,我又开始搞事情了,偶尔一次用到了在线编辑网页代码的网站,顿时想到,能不能自己实现一个呢?(PS:反正也没事干),然后又想着,能不能用在博客上呢,这样有些代码可以直接展现出来,多好,说干就干,让我们康康怎么去实现一个在线编辑代码的功能吧。(PS:有瑕疵,还在学习!勿喷!orz)


大致介绍


大概的想法就是通过iframe标签,让我们自己输入的内容能够在iframe中显示出来,知识点如下,如果有其他问题,欢迎在下方评论区进行补充!



  1. 获取输入的内容

  2. 插入到iframe中

  3. 怎么在博客中显示



当然也有未解决的问题:目前输入的js代码不能操作输入的html代码,查了许多文档,我会继续研究的,各位大佬如果有想法欢迎讨论



页面搭建


页面搭建很简单,就是三个textarea块,加4个按钮,就直接上代码了


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>在线编辑器</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script>
$(document).ready(function () {
$('.HTMLBtn').click(function () {
$("#cssTextarea").fadeOut(function () {
$("#htmlTextarea").fadeIn();
});
})

$('.CSSBtn').click(function () {
$("#htmlTextarea").fadeOut(function () {
$("#cssTextarea").fadeIn();
});
})
});
</script>
<style>
* {
padding: 0;
margin: 0;
}

body,
html {
width: 100%;
height: 100%;
}

.main {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
}

.textarea-box {
display: flex;
flex-direction: column;
width: calc(50% - 20px);
padding: 10px;
background: rgba(34, 85, 85, 0.067);
}

.textarea-function-box {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.textarea-function-left,.textarea-function-right {
display: flex;
flex-direction: row;
}

.textarea-function-left div,
.textarea-function-right div {
padding: 5px 10px;
border: 1px solid rgb(9, 54, 99);
border-radius: 3px;
cursor: pointer;
}

.textarea-function-left div:not(:first-child) {
margin-left: 10px;
}

#htmlTextarea,
#cssTextarea {
height: calc(100% - 30px);
width: calc(100% - 20px);
margin-top: 10px;
padding: 10px;
overflow-y: scroll;
background: #fff;
}

.html-div {
background-color: cadetblue;
margin-top: 10px;
flex: 1;
}

.iframe-box {
width: 50%;
flex: 1;
overflow: hidden;
}
</style>
</head>

<body>
<div class="main">
<div class="textarea-box">
<div class="textarea-function-box">
<div class="textarea-function-left">
<div class="HTMLBtn">HTML</div>
<div class="CSSBtn">CSS</div>
</div>
<div class="textarea-function-right">
<input type="text" id="input_name">
<div class="run">运行</div>
<div class="download">保存</div>
</div>
</div>
<textarea id="htmlTextarea" placeholder="请输入html代码"></textarea>
<textarea id="cssTextarea" placeholder="请输入css代码" style="display: none;"></textarea>
</div>
<div class="iframe-box">
<iframe style="height: 100%;width: 100%;" src="" frameborder="0"></iframe>
</div>
</div>
</body>
</html>

忽略我的样式,能用就行!!


运行代码


这里是核心功能,应该怎么把代码运行出来呢,我这里用的是iframe,通过获取iframe元素,然后把对应的代码插入进去


$('.run').click(function () {
var htmlTextarea = document.querySelector('#htmlTextarea').value;
var cssTextarea = document.querySelector('#cssTextarea').value;
htmlTextarea += '<style>' + cssTextarea + '</style>'
// 获取html和css代码
let frameWin, frameDoc, frameBody;
frameWin = document.querySelector('iframe').contentWindow;
frameDoc = frameWin.document;
frameBody = frameDoc.body;
// 获取iframe元素

$(frameBody).html(htmlTextarea);
// 使用jqury的html方法把代码插入进去,这样能够直接执行
})

这样一个基本的在线代码编辑网页就完成了,接下来,我们看下怎么把这玩意给用在博客当中!


hexo设置


首先我们需要创建一个文件夹,用来放置我们写好的在线的html文件。在source文件夹下新建文件online,并且设置禁止渲染此文件夹,打开_config.yml文件,并设置以下


skip_render: online/*

页面设置


我目前想到的办法就是保存文件,然后在hexo里使用,添加以下代码


<div class="textarea-function-right">
<input type="text" id="input_name">
<div class="download">保存</div>
<!-- .... -->
</div>

<script>
function fake_click(obj) {
var ev = document.createEvent("MouseEvents");
ev.initMouseEvent(
"click", true, false, window, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
obj.dispatchEvent(ev);
}

function export_raw(name, data) {
var urlObject = window.URL || window.webkitURL || window;
var export_blob = new Blob([data]);
var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
save_link.href = urlObject.createObjectURL(export_blob);
save_link.download = name;
fake_click(save_link);
}

$(document).ready(function () {
$(".download").click(function () {
let scriptStr = $('html').first().context.getElementsByTagName("script")[2].innerHTML
var htmlTextarea = document.querySelector('#htmlTextarea').value != "" ? document.querySelector('#htmlTextarea').value : '""';
var cssTextarea = document.querySelector('#cssTextarea').value != "" ? document.querySelector('#cssTextarea').value : '""';
let htmlStr = $('html').first().context.getElementsByTagName("html")[0].innerHTML.replace(scriptStr, "").replace('<div class="download">保存</div>', "").replace('<input type="text" id="input_name">',"").replace("<script><\/script>", "<script>$(document).ready(function(){document.querySelector('#htmlTextarea').value = `" + htmlTextarea + "`;document.querySelector('#cssTextarea').value = `" + cssTextarea + "`;})<\/script>")
let n = $('#input_name').val()!=""?$('#input_name').val():"text";
export_raw(n+'.html', htmlStr);
})
})
</script>

可能很多同学会好奇为啥我这里用的script标签框起来,我们看下这个图片和这个代码


script.png


et scriptStr = $('html').first().context.getElementsByTagName("script")[2].innerHTML

很简单,我们保存后的代码,是没有这一段js代码的,所以需要替换掉,而这里一共有3个script块,最后一个,也就是下标为2的script块会被替换掉。同理,后面替换掉保存按钮,input输入框(输入框是输入文件名称的,默认名称是text)。


同时这里把我们输入的数据,通过js代码的方式加入进保存后的文件里,实现打开文件就能看到我们写的代码。之后我们把保存后的文件放在刚才我们创建的online文件夹下


text.png


hexo里面使用


使用就很简单了,我们通过iframe里面的src属性即可


<iframe src="/online/text.html" style="display:block;height:400px;width:100%;border:0.5px solid rgba(128,128,128,0.4);border-radius:8px;box-sizing:border-box;"></iframe>

展示图


show.png


作者:新人打工仔
来源:juejin.cn/post/7191520909709017144
收起阅读 »

判断数组成员的几种方法

web
在开发中经常需要我们在数组中查找元素又或者是判断元素是否存在,所以我列举了几种常用的方法供掘友参考学习。 indexOf() 首先想到的就是indexOf()方法,查找元素,并返回第一个找到的位置索引 [1,2,3,2].indexOf(2)  // 1 ...
继续阅读 »



在开发中经常需要我们在数组中查找元素又或者是判断元素是否存在,所以我列举了几种常用的方法供掘友参考学习。


indexOf()


首先想到的就是indexOf()方法,查找元素,并返回第一个找到的位置索引


 [1,2,3,2].indexOf(2)  // 1

他还支持第二个可选参数,指定开始查找的位置


 [1,2,3,2].indexOf(2,2)  // 3

但是indexOf()有个问题,他的实现是由===作为判断的,所以这容易造成一些问题,比如他对于NaN会造成误判


[NaN].indexOf(NaN) // -1
console.log(NaN === NaN) // false

如上,由于误判,没有找到匹配元素,所以返回-1,而在ES6对数组的原型上新增了incudes()方法,他可以代替indexOf(),下面来看看这个方法。


includes()


在ES6之前只有字符串的原型上含有include()方法来判断是否包含字串,而数组在ES6中也新增了include()方法来判断是否包含某个元素,下面来看看如何使用。


[1,2,3].includes(2) // true

数组实例直接调用,参数为要查找的目标元素,返回值为布尔值。而且他能很好地解决indexOf()的问题:


[NaN].includes(NaN) // true

如上includes()可以正确地判断NaN的查找问题,而includes()是用来判断是否包含,查找条件也比较单一,那么如果想要自定义查找条件,比如查找的范围,可以使用这么一对方法:find()与findIndex()接下来看一看他们是如何使用的。


find()与findIndex()


find()findIndex()可以匹配数组符合条件的元素


find()


find()支持三个参数,分别为valueindexarr,分别为当前值,当前位置,与原数组,,返回值为符号条件的值


let arr = [1,2,10,6,19,20]
arr.find((value,index,arr) => {
   return value > 10
}) // 19

如上,我以元素大于10为范围条件,返回了第一个符合范围条件的值:19。而find()可以返回符合条件的第一个元素,那么我们要是想拿到符合条件的第一个元素索引就可以使用findIndex()


findIndex()


findIndex()find相似也支持三个参数,但是返回值不同,其返回的是符合条件的索引


let arr = [1,2,10,6,19]
arr.findIndex((value,index,arr) => {
   return value > 10
}) // 4

例子与find()相同,返回的是19对应的索引


对于NaN值


find()findIndex()NaN值也不会误判,可以使用Object.is()来作为范围条件来判断NaN值,如下


[NaN].find((value)=> {
   return Object.is(NaN,value)
}) // NaN

如上例子,findIndex()也同理


最后


判断元素在某数组中是否存在的四种方法就说到这里,对掘友有所帮助的话就点个小心心吧,也欢迎关

作者:猪痞恶霸
来源:juejin.cn/post/7125632393821552677
注我的JS进阶专栏。

收起阅读 »

JS实现继承的几种方式

web
继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。 下面我们就来看看 JavaScript 中都有哪些实现继承的方法。 原...
继续阅读 »

继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。



下面我们就来看看 JavaScript 中都有哪些实现继承的方法。


原型链继承


原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。



原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例



下面我们结合代码来了解一下。



function Animal (name) {

  // 属性

  this.name = name

  this.type = 'Animal'

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}




// 子类

function Cat (name) {

  this.name = name

}

// 原型继承

Cat.prototype = new Animal()

// 将Cat的构造函数指向自身

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

console.log(cat.type) // Animal

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


在子类Cat中,我们没有增加type属性,因此会直接继承父类Animaltype属性。


在子类Cat中,我们增加了name属性,在生成子类实例时,name属性会覆盖父类Animal属性值。


同样因为Catprototype属性指向了Animal类型的实例,因此在生成实例Cat时,会继承实例函数和原型函数。



需要注意:
Cat.prototype.constructor = Cat


如果不将Cat原型对象的constructor属性指向自身的构造函数,那将指向父类Animal的构造函数。



原型链继承的优点


简单,易于实现


只需要设置子类的prototype属性指向父类的实例即可。


可通过子类直接访问父类原型链属性和函数


原型链继承的缺点


子类的所有实例将共享父类的属性


子类的所有实例将共享父类的属性会带来一个很严重的问题,父类包含引用值时,子类的实例改变该引用值会在所有实例中共享。



function Animal () {

  this.skill = ['eat', 'jump', 'sleep']

}

function Cat () {}

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat1 = new Cat()

let cat2 = new Cat()

cat1.skill.push('walk')

console.log(cat1.skill) // ["eat", "jump", "sleep", "walk"]

console.log(cat2.skill) // ["eat", "jump", "sleep", "walk"]


在子类实例化时,无法向父类的构造函数传参


在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类关联,从而导致无法向父类的构造函数传递参数。


无法实现多继承


子类的prototype只能设置一个值,设置多个值时,后面的值会覆盖前面的值。


构造函数继承(借助 call)



构造函数继承的主要思想:在子类的构造函数中通过call()函数改变thi的指向,调用父类的构造函数,从而将父类的实例的属性和函数绑定到子类的this上。




  // 父类

function Animal (age) {

  // 属性

  this.name = 'Animal'

  this.age = age

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}

function Cat (name) {

  // 核心,通过call()函数实现Animal的实例的属性和函数的继承

  Animal.call(this)

  this.name = name

}




let cat = new Cat('Tom')

cat.sleep() // Tom正在睡觉

cat.eat() // Uncaught TypeError: cat.eat is not a function


通过代码可以发现,子类可以正常调用父类的实例函数,而无法调用父类原型上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数


构造继承的优点


解决了子类实例共享父类属性的问题


call()函数实际时改变父类Animal构造函数中this的指向,然后调用this指向了子类Cat,相当于将父类的属性和函数直接绑定到了子类的this中,成了子类实例的熟属性和函数,因此生成的子类实例中是各自拥有自己的属性和函数,不会相互影响。


创建子类的实例时,可以向父类传参



   // 父类

function Animal (age) {

  this.name = 'Animal'

  this.age = age

}

function Cat (name, parentAge) {

  // 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承

  Animal.call(this, parentAge)

  this.name = name

}




let cat = new Cat('Tom', 10)

console.log(cat.age)


可以实现多继承


在子类的构造函数中,可以多次调用call()函数来继承多个父对象。


构造函数的缺点


实例只是子类的实例,并不是父类的实例


因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系。


只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数


与上面原因相同。


无法复用父类的构造函数


因为父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例的引用,这会造成不必要的内存消耗,影响性能。


组合继承



组合继承的主要思想:结合构造继承和原型继承的两种方式,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。




// 父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}




// 子类

function Cat (name) {

  // 通过构造函数继承实例的属性和函数

  Animal.call(this)

  this.name = name

}

// 通过原型继承原型对象上的属性和函数

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


组合继承的优点


既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数


既是子类的实例,又是父类的实例


不存在引用属性共享的问题


构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。


可以向父类的构造函数中传参


组合继承的缺点


父类的实例属性会被绑定两次


在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性,生成的实例时又调用了一次父类的构造函数。


寄生组合继承


组合继承方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。


在进行子类的prototype属性的设置时,可以去掉父类实例的属性的函数



  //父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}

// 子类

function Cat (name) {

  // 继承父类的实例和属性

  Animal.call(this)

  this.name = name

}

// 继承父类原型上的实例和属性

Cat.prototype = Object.create(Animal.prototype)

Cat.prototype.constructor = Cat

let cat = new Cat('Tom')




console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头



其中最关键的语句:






Cat.prototype = Object.create(Animal.prototype)






只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。



这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。


整体看下来,这六种继承方式中,寄生组合式继承是这里面最优的继承方式。


总结


image.png


作者:蜡笔小群
来源:juejin.cn/post/7168856064581091364
收起阅读 »

我们在搜索一个问题的时候浏览器究竟做了什么

web
1+1=?,这个问题一直困扰着我,这天摸鱼的时间,我打开浏览器,在地址栏中输入http://www.baidu.com,按下回车,从这时起,我的疑虑从1+1=?变成了打开百度时浏览器到底做了什么工作? 这算是一个面试常见题,反正我被提问了无数次 TuT 为什...
继续阅读 »

1+1=?,这个问题一直困扰着我,这天摸鱼的时间,我打开浏览器,在地址栏中输入http://www.baidu.com,按下回车,从这时起,我的疑虑从1+1=?变成了打开百度时浏览器到底做了什么工作?



这算是一个面试常见题,反正我被提问了无数次 TuT

为什么面试官总喜欢提问这个问题?一般面试官问这个问题,是为了考察前端的广度和深度。



在浏览器地址栏键入URL地址


当我们在浏览器地址栏输入一个URL地址后,浏览器会开一个线程来对我们输入的URL进行解析处理。


1. DNS域名解析


我们在浏览器中输入的URL通常是一个域名,浏览器并不认识域名,它只知道IP,而我们平时记住的一般是域名,所以需要一个解析过程,让浏览器认得我们输入的内容。



  • IP地址:IP地址是某一台主机在互联网上的地址

  • 域名:和IP差不多意思,也是主机在互联网上的地址,人们一般记住的是域名,更有实质性意义。如果把IP比作经纬度,那么域名就是街道地址。

  • DNS:Domain Name System,负责提供域名和IP对应的查询服务,即域名解析系统;

  • URL:Uniform Resource Locator,统一资源定位符,其实就是我们说的‘网址’。


由此可以推断出,当我们敲下一行url地址时,浏览器首先做的是,先到DNS问问这个url的实际位置在哪里?


2. 发起请求



  • 协议:把数据打包成一种传输双方都看得懂的形式。

  • http:Hyper Text Transfer Protocol,超文本传输协议,通常运行在TCP之上。超文本就是用文本的方式来传输超越文本的内容,例如图片、音频等等。

  • TCP:Transmission Control Protocol,传输控制协议,是传输层的协议,用于保持通信的完整性。

  • 服务器:其实就是一台主机(电脑),目标资源可以存放在它的存储空间中。


拿到ip地址后,浏览器向目标地址发起http或者https协议的网络请求。

HTTP请求的本质是TCP/IP的请求构建。建立连接时需要进行3次握手进行验证,断开连接时需要4次挥手进行验证,保证传输的可靠性。


握手挥手过程有点复杂,具体的另出一篇文章说明!


3次握手


三次握手1.gif



  • 第一次握手:客户端发送网络包,服务端收到。

    服务端得出结论:客户端的发送能力、服务端的接收能力正常

  • 第二次握手:服务端发包,客户端收到。

    客户端得出结论:服务端的接收、发送能力正常,客户端的接收、发送能力正常。但是服务器并不能确认客户端接收能力是否正常。

  • 第三次握手:客户端发包,服务端收到。

    服务端得出结论:客户端的接收、发送能力正常,服务器的发送、接收能力正常。


三次握手.gif


4次挥手



  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号,此时客户端处于FIN_WAIT1状态。

    即发出连接释放报文段,并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1状态,等待服务端的确认。

  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,并把客户端的序列号值 +1 作为 ACK 的报文序号列值,表明已经收到客户端的报文,此时服务端处于 CLOSE_WAIT 状态。

  • 第三次挥手:服务器端关闭与客户端的连接,发送一个 FIN Seq=N 给客户端。

  • 第四次挥手:客户端返回 ACK 报文确认,确认序号 ACK 未收到的序号 N+1


四次挥手.gif


3. 服务器响应


服务器上运行的WEB服务软件根据http协议的规则,返回URL中指定的资源。

HTTP请求到达服务器,服务器进行对应的处理,最后要把数据传给浏览器,即返回网络响应。

和请求部分类似,网络响应具有三个部分:响应行、响应头和响应体,发起请求后,服务器会返回一个状态码,以示下一步的操作。


响应完成后TCP连接会断开吗


首先需要判断一下Connection字段,如果请求头或响应头中包含Connection: Keep-Alive,表示建立了持久连接,这样TCP会一直保持,之后请求统一站点的资源会复用这个连接;否则会断开TCP连接,响应流程结束。


状态码


状态码由3位数组成,第一个数字定义了响应的类别:



  1. 1xx:表示请求已接收,继续处理;

  2. 2xx:表示请求已被成功接收;

  3. 3xx:要想完成请求必须进行更进一步的操作;

  4. 4xx:客户端有误,请求有语法错误或请求无法实现;

  5. 5xx:服务器端错误,服务器未能实现合法的请求。


服务器返回相应文件


请求成功后,服务器会返回相应的网页,浏览器接收到响应成功的报文后会开始下载网页,至此,通信结束。


浏览器解析渲染页面


浏览器在接收到HTML/css/js文件之后是怎么将页面渲染在浏览器上的?

浏览器在拿到服务器返回的网页之后,首先会根据顶部定义的 DTD 类型进行对应的解析,解析过程将被交给内部的 GUI 渲染线程来处理。




  • 浏览器:浏览器是一个本地应用程序,它根据一定的标准引导页面加载和渲染页面

  • css:Cascading Style Sheets,层叠样式表,用于html中各元素样式显示

  • JS:JavaScript,在web应用中控制用户和网页应用的交互逻辑



参考文章



  1. 超详细讲解页面加载过程

  2. 网页加载过程简述


作者:在因斯坦
来源:juejin.cn/post/7174744415221579832
收起阅读 »

疫情过后的这个春招,真的会回暖吗?

哈喽大家好啊,我是Hydra。 今天是正月初七,不知道大家有没有复工,反正我今天已经坐在办公室里开始码字了。 这个春节假期相信大家过的都不错,可以看到今年无论是回家探亲、还是外出旅游的人数,都比疫情放开前两年有了爆发式的增长。假期我躺在被窝里刷抖音,每当刷到哪...
继续阅读 »

哈喽大家好啊,我是Hydra。


今天是正月初七,不知道大家有没有复工,反正我今天已经坐在办公室里开始码字了。


这个春节假期相信大家过的都不错,可以看到今年无论是回家探亲、还是外出旅游的人数,都比疫情放开前两年有了爆发式的增长。假期我躺在被窝里刷抖音,每当刷到哪个景点人满为患到走不动路的时候,都觉得自己宅在家里哪也不去真的是太对了。


好了回归正题,很多小伙伴们非常关注的一个问题,在经历了疫情放开、大规模感染的相对平稳后,这一届春招真的会回暖吗?


在聊春招之前,我觉得还是有必要再来科普一下春招的时间线。



  • 12月,一般只有少量的企业开始进行春招提前批预热,或是进行秋招的补录

  • 1月,部分公司开启春招正式批

  • 3-4月,才是春招的高峰期,大部分公司在这个时间段陆续开启春招

  • 5月,大部分的企业会结束招聘


为了了解今年的形势,我也逛了不少论坛,了解到有一些大厂在去年12月底的时候,就已经开始了秋招的补录,不少人收到了补录的通知。



通过整体氛围来看,今年春招大概率会比去年进行一波升温,在岗位的可选择性上,大伙可能也有更多的大厂岗位可以进行一波冲击。尽管如此我还是劝大家要尽早准备,因为虽然说是春招,但并不是真正到了春天才真正开始,并且春招的难度比秋招可能还要高上不少。


首先,相对于秋招来说,春招的岗位会少很多,因为春招更多是对于秋招的补充,是一个查漏补缺的过程,对秋招中没有招满、或者有新岗位出现的情况下,才会在春招中放出该岗位。少量的岗位,需要你能更好的把握信息资源,迅速出击。


其次,你可能拥有更多的竞争对手,考研、考公失利的同学如果不选择二战,将会大量涌入春招,而对于秋招找到的工作不满意想要跳槽的同学,有笔试面试经验、工作经历,将会成为你春招路上麻烦的对手。


所以说到底,大家还是不要过于盲目乐观,扎扎实实的准备肯定是不能少的,毕竟春招的难度摆在这里。在看到大规模补录的同时,我们也不能否认背后的裁员依旧存在。有可能你现在看到的hc,就是在不久前刚刚通过裁员所释放的。


另外,我还是得说点泼冷水的话,虽然看上去形势一片大好,岗位放开了很多,但不代表薪资待遇还是和以前一样的,从一个帖子中可以看到,即便是在杭州和成都的中厂里,降薪也是存在的。



因为说到底,疫情并不是经济下行的根本原因,想要寄希望于疫情放开后经济能够快速复苏基本是不可能的。


国内的互联网公司,已经过了那个爆发式发展的黄金时期,甚至说一句互联网公司规模已经能隐隐约约窥到顶峰也不过分。美联储加息、中概股暴跌、企业融资困难…面对这些困难的环境,即使疫情放开也于事无补。


尽管环境如此困难,我仍然认为互联网行业是小镇做题家们快速实现社会价值、积累财富的黄金职业。看看大厂里十几万、几十万的年终奖,并不是每个行业都能做到的。


最后还是建议大家,积极准备,不管这个春招是否回暖,还是要做到尽量不留遗憾,不要给自己找借口,再寄希望于下一个秋招。


2023年,我们一起加油!


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

浅谈理论:为什么图论是当今各行各业必备的知识?

引子 “若无必要,勿增实体”--奥卡姆剃刀原理 图论(Graph Theory)是被大众严重低估的数学基础理论。它不是研究图像、图片、图表之类的理论,而是一个抽象而简单的数学理论。图论中的图(Graph)是一个抽象概念,非常类似于关系网络(Relation...
继续阅读 »

引子



“若无必要,勿增实体”--奥卡姆剃刀原理



图论(Graph Theory)是被大众严重低估的数学基础理论。它不是研究图像、图片、图表之类的理论,而是一个抽象而简单的数学理论。图论中的(Graph)是一个抽象概念,非常类似于关系网络(Relationship Network),有对应的节点(Node)或顶点(Vertext),节点之间又有关联关系或(Edge)。图论的概念非常简单,就是图、节点、边。本篇文章将简单的介绍一下图论的基础概念,以及图论在真实世界中的应用。(注意!本文不是科学论文,所以不会有枯燥的数学公式,请放心食用)


graph


图论简述


在图论中,有三个重要的概念:



  1. 节点(Node):可以理解为某个实体,例如关系网络中的张三、李四、王五;

  2. (Edge):可以理解为实体间的关系,例如,张三和李四是夫妻,王五是他们儿子;

  3. (Graph):可以理解为所有节点、边的集合,例如张三、李四、王五组成的幸福一家。


从上面的三个基本概念,我们可以推断出节点之间的关系,例如李四的大哥李一,就是王五的舅舅,王五也是他的侄子。


graph-basic-concepts


当然,图论的应用远不止于此,在我们真实世界中,图论的应用非常之广,以至于普通大众都难以察觉它的存在。


下面,我们将介绍一下图论在技术领域的常见应用。


搜索引擎


谷歌、百度、必应,这些都是我们日常使用的搜索引擎(Search Engine)。如果我们需要了解什么知识,一般会打开搜索引擎,输入关键词(Keyword),然后搜索引擎就会返回相关的结果,而且还通常极为精确。其实,搜索引擎技术背后的核心理论就是图论。


咱们可以将某个网站的每个网页(Web Page)想象成一个节点,页面上超链接(Hyperlink)就是对应的,而网站(Site)就是,其包含所有的网页(节点)以及网页关系(边),例如下图。


webpage-graph


而搜索引擎为什么能魔法般的将搜索结果返回给用户,就是巧妙应用了 PageRank 的算法,将节点关系用线性代数表示出来,然后计算与关键词最相似的节点或网页,最后实现搜索引擎的基础搜索功能。对 PageRank 算法感兴趣的朋友,可以查看相应的资料,网上很多,就不赘述了。


自然语言


自然语言(Natural Language)其实就是中文、英文、日文等自然人对话所使用的语言。例如,张三给李四说 “我爱你”,或者你问朋友 “上周我在市区发现了个超棒的咖啡馆,这周末有空一起去么?”,甚至本篇文章的所有文字,都是自然语言。


那么,图论跟自然语言有什么关系?其实,如果我们仔细思考下,会发现中文有主语、谓语、宾语三种词性,例如 “我爱你” 这样简单的主谓宾句子。而实际上,这个句子可以由图来表示,其中主语(“我”)和宾语(“你”)是节点谓语(“爱”)就是,即表示主语对宾语的关系。当然,对于复杂的句子,关系会更复杂一些,对应的图也就更复杂,但总归可以用图、节点、边来表示各个词语之间的关系。


下图表示的是 "Susan might not believe you" 这个句子的图,它是一个树状结构(Tree Structure)。而(Tree)也是一种图,它是一种特殊结构。


natural-language-graph


现在很火的翻译软件、语音识别、聊天机器等,背后的自然语言处理(Natural Language Processing)技术都来自于图论。


思考题:程序代码(例如 Python)可以用图论处理么?


人工智能


最近一段时间,人工智能(Artificial Intelligence)技术大红大紫,其中核心技术来自于深度神经网络(Deep Neural Network),即神经网络(Neural Network)的一种特殊形式。而神经网络的概念,简单可以理解为由神经元组成的大脑神经网络类似的结构。大脑中些神经元的电信号会通过神经网络传递到其他神经元,从而产生意识、想法、认知等抽象概念,而这背后的逻辑非常简单,就是电信号触发,只不过数量极为庞大。而如今深度神经网络的成功,也受益于计算机上搭建的大规模神经网络运算系统。全球著名人工智能公司 OpenAI 的 GPT-3 语言模型,由 1750 亿个参数组成,这已经接近成人大脑神经元数量级了。


万物皆图论


图论的概念非常简单,就像物理世界中的原子、分子、化学键这样的抽象概念,很容易在数学上进行处理。因此,大到全球互联网,小到蛋白质分子结构,图论在真实世界中的绝大部分领域都可以广泛得到应用。因此,学习好图论,对我们在工作生活学习中解决真实问题有非常大的帮助。例如,最优时间安排问题,就可以用图论来建模;而白领们常用的透视表,同样可以用图论中的二部图来处理。最近,笔者也在试着将图论应用到智能网页信息提取技术。学习图论有助于帮助我们更好的理解这个世界,从而可以更加合理的处理好我们自己的生活和工作。


社区


如果您对笔者的文章感兴趣,可以加笔者微信 tikazyq1 并注明 "码之道",笔者会将你拉入 "码之道" 交流群。


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

裸辞回家遇见了她

22年,连续跳了二三家公司,辗转七八个城市。 可能还是太年轻,工作上特别急躁,加班太多会觉得太累,没事情做又觉得无聊烦躁。去年年末回老家过年因为一些巧合遇见了她。年初就润,回到了老家。当时因为苏州疫情就没回去,便开始在老家这边的坎坷之旅。 年初千里见网友 说起...
继续阅读 »

22年,连续跳了二三家公司,辗转七八个城市。

可能还是太年轻,工作上特别急躁,加班太多会觉得太累,没事情做又觉得无聊烦躁。去年年末回老家过年因为一些巧合遇见了她。年初就润,回到了老家。当时因为苏州疫情就没回去,便开始在老家这边的坎坷之旅。


年初千里见网友


说起来也是缘分,去年年末的时候,有个人加了我微信,当时也是一头雾水,还以为是传销或者什么。一看名字微信名:“xxx”,也不像是啊,当时没放在心上就随便聊了聊,也没咋放心上。后来我朋友告诉我她推的(因为觉得我挺清秀人品也还行),就把她推给了我。但是我这人自卑又社恐,加上她在我老家那边,就想反正自己好多年也不想回老家那个地方,现在即使网恋也是耽误人家,后面就没咋搭理她。

到过年的时候,我和我妈匆匆忙忙回到了老家,当时家里宅基地刚好重建装修完,背了一屁股的债务,当时很多人劝我不要建房子在老家,有钱直接在省会那边付个首付也比老家强,可我一直觉得这个房子是我奶奶心心念念了一辈子的事情。转念一想一辈人有一辈人的使命,最多就是自己再多奋斗几年就没多去计较。


后面过年期间,我和她某明奇妙的聊起来了,可能是我觉得离她近了,然后就有一丝丝念想吧,当时因为一些特殊原因,过年的时她也在上班。那几天基本每天从早聊到晚,稍微有点暧昧,之后还一起玩游戏,玩了几局,我也很菜没能赢,就这样算是更深一步了解她吧,当时也不好断定她是怎样的人。就觉得她很温柔、活泼、可爱、直爽,后面想了想好像很久好久没用遇到这样的女孩子了吧,前几年也遇到不少女孩子都没有这种感觉。是不是自己单身太久产生的幻觉。经过一段时间的发酵,我向我朋友打听了下她。


我朋友说人品没问题,就是有点矮,我想着女孩子没啥影响,反正我也矮。就决定去见见她,她也没拒绝我。缘分到了如果不抓住的话也不知道下一次是什么时候。其实那时候我们还只是看过照片,彼此感觉都是那种一般人,到了这个年纪(毕业二三年)其实都不是太在乎颜值,只要不是丑得不能见人(颜值好的话肯定是加分项)。虽然我们都在老家,但她兼职那边还是有点远,去那边需要转很多车。但也没什么,我义无反顾去见了他,也许这就是大多数人奔现的样子吧(但我心里是比较排斥这个词的)。


那天早上一大早我就急冲冲起来了,洗个了头,吹了个自认为很帅的发型,戴上小围巾就出发了(那晚上其实下了很大的雪)。因为老家比较远我都比较害怕那边没有班车,因为当时才大年初三,我们那边的习俗是过年几天不跑车,跑车一年的财运都会受影响。到路上果然没让我失望,路上一辆车都没有,也是运气好,我前几天刚好听到我表姐说要去城里,我就问了问,果真就今天去(就觉得很巧合,跟剧本一样),他们把我送到高铁站,道了个谢,就跑去赶了最早一班的高铁。


怀着忐忑的心情出发了,那时差不多路上就是这个样子吧(手机里视频传不上去)。


image.png
在路上的时候她一直强调说自己这样不行,那样不可以怕我嫌弃,我当时倒是不自卑,直接对人家就是一顿安慰。到了省会那边,又辗转几个地方去买花,那时过年基本没什么花店开门。转了几个大的花店市场才发现一家花店,订了一束不大不小的花, 又去超市买了个玩偶和巧克力,放了几颗德芙在衣服包里面(小心机)。前前后后忙完这些已经下午一点了,对比下行程,可能有点赶不上车了。匆忙坐了班车到了她上班那个市区 ,本以为一切都会很顺利,结果到了那边转车的班车停运了,当时其实是迷茫的。不知道要不要住宿等到第二天。


那时我想起本来就是一腔热情才跑过来的,也许过了那个劲就不会有那个动力去面对了,心里默想:“所爱隔山海,山海皆可平”。心疼的打了个车花了差不多五百块(大冤种过年被宰)。就这样踏上最后一段路程。路上见到不一样的山峰,矮而尖而且很密集,那个司机说天眼好像就是建筑在这边吧,路上我就一直想:即使人家见了我嫌弃我这段旅行也算很划算的吧。最终晚上七点到达了目的地,下车了还是有点紧张,我害怕她不喜欢我这样的,毕竟了解不多,也许就是你一厢情愿的认为这就是缘分和命运的安排。


终将相遇


最后一刻,我都还在想,她会不会看到我就跑了,然后不来见我。但应该不至于此,毕竟我相信我的老朋友(七年死党),也相信她的人品。我看见一个人从前面走来我还以为是她,都准备迎上去了,走近一看咋是个阿姨(吓我一跳还以为被骗了),等我反应过来那个阿姨已经走远了。然后一个声音从我对面传来:“我在这,我在这边”,我转头过去惊艳到我了,这这这是本人吗?深邃的眼眸,樱桃小嘴,不是很尖的脸蛋,短发到肩,微风吹起刘海飘啊飘,像飘进了我的心里,头后发带将一些头发束起,然后发带结成蝴蝶结,一身长白棉袄配白皮鞋,显得俏皮又惊艳。我还来不及细想,我就迎了过去,提前想好的台词都没有说出来,倒是显得有一些尴尬。


当时自卑感油然而生,自己觉得配不上她。寒暄了几句我将花递给她,没有惊喜的表情,只有一句:我都没给你准备什么礼物,你这样我会很不好意思的,她这样说我该是开心还是难过呢?我心里觉得大概要凉了。就怕一句:你是个好人,我们就这样吧。其实当时我们也没说啥喜欢啥的就是有点暧昧。所幸没有发生她嫌弃我的事情,我们延着路边一路闲聊下去,一开始我还有点拘谨,毕竟常年当程序员社交能力不是很行。


慢慢的,我们说了很多很多,她请我吃了个饭(之前说过请她没倔过她),一路走着走着,说着大学的事,小时候的事,工作的事,一时间显得我们不是陌生人,而是多年未见的好友,一下子就觉得很轻松很幸福,反正我已经深深的迷上她的人美心善。她也说了离家老远跑来这边上班的原因(不方便透露)。走着走着我发现她的手有点红,就说道:我还给你准备了个惊喜,把手伸进我衣服包里吧,我在里面放了几颗糖,上班那么辛苦有点糖就不苦了。后面我有点唐突抓住她的手,我说给她暖一下太冰了。她说放我包里就暖和了,我看她脸都红了,也觉得有点唐突了。后面发现还是太冰了,没多想就用牵住了她,嘿嘿!她直接害羞的低下了头。一下子幸福感就涌上来了。


后面很晚的时候要分别了,送他回了宿舍,并把包里的玩偶以及剩下的零食一并给了她。她说第二天来送我,我便回了酒店。


第二天我们俩随便吃了点东西(依旧很害羞没敢坐我对面),她就送我上车了,临走时她塞了一个东西在我手里,打开一看昨天的发带,抬头她已走远她小声说了一句:我们有缘再见。也许是想着我在苏州她在遵义太远了吧,可能就是最后一面了,有点伤心也没多问。


微信图片_20220831164746.jpg


感情生活波折


回去的第二天我便回到苏州那边,但是很久之前就谋划着辞职,一方面是觉得在这边技术得不到提升,一方面是觉得想换个环境吧,毕竟这边太闲了让我找不到价值。可能年轻急躁当时没多想就直接裸辞了,期间我对她说:我辞职后来看她,她有点不愿意(说感觉我们的感情有点空中楼阁),可能觉得见一面不足以确定什么吧,我可能觉得给不了他幸福也舍不得割舍吧。


后面裸辞后,蹭着苏州没有因为疫情封禁,直接带了二件衣服就回了老家。(具体细节不说了)


第二次见她,可能觉得有点陌生吧,不过慢慢的就过了那个尴尬期,我们一起去逛公园、去逛街、彼此送小礼物、一起吃饭,即使现在回来依旧觉得很美好。但是我依旧没有表白,可能我觉得这些事顺理成章的不需要。一次巧合我去了她家帮她做家务、洗头、做饭。哈哈哈,像一个家庭主男一样。可能就是那次她才真的喜欢上我的吧。


有一次见面之后因为一些很严重的事我们吵架了,本来以为就要在此结束了。后来我又去见她了,我觉得女孩子有什么顾虑很正常的,也许是不够喜欢啥的,准备最后见一面吧,但见面之后准备好的说辞一句没说,还是像原来那样相处,一下子心里就有点矛盾,后面敞开心扉说开了,心里纠结的问题也就解决了。慢慢的我们也彼此接受了,从一见钟情到建立关系,真的经历很多东西。不管是少了那一段经历我和她都不会有以后。我的果决她的温柔都是缺一不可的。


后续


她考研上岸,我离开苏州在贵阳上班。我们依旧还有很长一段路要走。后续把工作篇发出来(干web前端的)


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

职场上有什么谎言?

努力干活就能赚多点钱 职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的...
继续阅读 »

努力干活就能赚多点钱


职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的报酬,而是更倾向于平衡员工的工作与生活,提高员工的幸福感和满意度。因此,新进职场人士应该认识到,在职场中坚持适度的工作量、良好的工作习惯和优秀的职业素养才是取得成功的重要因素。


我都是为你好


“我都是为你好”可能是一种常见的谎言,在不同情境下被使用。在某些情况下,这可能真诚地表达出对他人的关心和照顾,但在其他情况下,这也可能成为掩盖自己私人动机或者行为错误的借口。因此,在职场和日常生活中,我们需要学会审视这句话所蕴含的背后意图,并判断其是否真实可信。同时,我们也应该秉持着开放、坦诚、尊重和理解的态度,与他人进行良好的沟通和相处,以建立健康、和谐的人际关系。


他做得比你好,向他好好学习


“他做得比你好,向他好好学习”是一句非常有益的建议,可以让人们从成功的经验中汲取营养,不断提高自己的能力和水平。在职场中,人们面对不同的工作任务和挑战,而且每个人的工作方式、思维模式和经验都不同,因此,我们应该善于借鉴他人的优点和长处,吸取别人的经验和教训,不断完善自己的职业素养和技能。然而,这并不意味着要完全依赖和模仿别人,而是应该在合适的时机,根据自身实际情况和需要,加以改进和创新,开拓自己的专业视野和发展空间。


在职场中,有些人可能会通过拍马屁、拉关系等不正当手段来获取自己的利益或者提高自己的地位。然而,这种做法可能会导致负面后果和损失,例如破坏工作团队的合作氛围、损害自己的职业形象和信誉等。因此,我们应该始终保持清醒和冷静的头脑,不受拍马屁等诱惑,专注于自己的工作和职责,努力提高自己的专业水平和职业素养。同时,我们也应该与他人建立良好的人际关系,以合理、公正、透明的方式展示自己的才华和成果,赢得别人的尊重和信任,并在适当的时刻借助他人的力量来实现共同的目标。


公司不怎么赚钱,理解一下,行情好了加工资


如果公司在过去设定了一些目标和承诺,但无法兑现或者没有达到预期的结果,那么这就是一种失信行为。画饼充当推销手段,可能会对员工、客户和利益相关方造成误导和不良影响,并破坏公司的商誉和形象。因此,公司应该根据市场实际情况和自身能力水平,制定合理、可行的计划和策略,避免过于浮夸和虚幻的承诺,注重落实和执行,加强与员工、客户和社会各方的沟通和互动,建立坦诚、透明的企业文化和价值观念。同时,员工也应该保持客观、谨慎、理性的态度,不盲目追求高回报或者虚假宣传,始终以个人职业道德和职责为先,为公司和自己的未来发展负责任。


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

关于 Android App 架构,你可能会被问到的 20 个问题

LiveData 是否已经被弃用?没有被弃用。在可以预见的未来也没有废弃的计划。LiveData 可以使用简单的方式获取一个易于观察、状态安全的对象。虽然其缺少一些丰富的操作符,但是对于一些简单的 UI 业务场景已经足够。Flow 有&nb...
继续阅读 »

LiveData 是否已经被弃用?

没有被弃用。在可以预见的未来也没有废弃的计划。

LiveData 可以使用简单的方式获取一个易于观察、状态安全的对象。虽然其缺少一些丰富的操作符,但是对于一些简单的 UI 业务场景已经足够。

Flow 有 LiveData 相同的功能,其包含大量丰富的操作符,可以简单的完成复杂的业务逻辑处理。相应的会增加一定的使用门槛,如果不需要 Flow 的 全部功能,继续使用 LiveData 即可。

更多内容:Migrating from LiveData to Kotlin’s Flow

什么是业务逻辑?为什么将业务逻辑移动到 Data Layer ?

业务逻辑是给 App 带来价值的东西,是一系列决定 App 如何运行的规则集。比如,如果你有一个显示新闻文章的 App,你点击书签按钮,从书签文章到持久的交互,这就是业务逻辑。

将业务逻辑移动到 Data Layer 的主要原因是提供单一信源,App 的不同页面和不同逻辑使用到这部分数据将从同一个地方进行获取,方便统一管理;其次是分离职责,将不同的处理逻辑(控制UI的与操作数据)分散在不同的层级中,可以减少各自交互的复杂度。

如何在 ViewModel 中获取系统资源(字符串、颜色等)?

访问资源只应该放在 UI 层,如 View 或者是 Compose。 可以在 ViewModel 中提供资源 ID,但是不应该直接在 ViewModel 中直接访问资源。除了单一职责之外,就是可以响应手机配置的变化:

  • 切换语言的时候,会变成对应的语种字符串;
  • 切换亮暗模式的时候,对应的颜色会随之变化;

另外就是 Context 的上下文与 ViewModel 的生命周期不一致,可能会导致内存泄漏。

如何让 Service 与 Compose/ViewModel 进行交互?

Modern Android App Arch.001.png

他们不应该直接进行交互或者说是互相引用。

应该采用单一信源的方式定义一个 Repository,在 Service/ViewModel 中调用 Repository 中的方法来更改状态,UI(Views/Compose) 层通过数据流的方式监听数据变化。

可以确定的是,在 ViewModel 中应尽可能少的依赖 Android 系统组件,如 Service、Activity 等。

未来平稳过度到 KMM/KMP 的最佳实践有哪些?

通常最佳实践会随着时间的推移而建立起来的。对于未来如果 KMM/KMP ,并不需要做过多的事情,遵循架构指南即可。比如:将数据的操作放在 Data Layer,在此层尽量少的调用系统 API。但是目前并需要定义跨端的接口定义,因为 KMM/KMP 还是比较新的技术,在他被大规模使用之前还有不少变数。

ViewModel 被建议当做 State Holders 之后,其在 MVVM 中的职责?

ViewModel 在 MVVM 中就是一个状态持有者。在不同的语义场景下可能有不同的含义。如果是整个页面中的 View(不论是 Activity 还是 Compose 中的目的地),其对应的是 AAC 中的 ViewModel 类。如果是普通的自定义 View 或是 Compose 中的一个可组合函数,那么 View 的状态持有者定义成一个普通的类就可以。这里可以类比下 RecycleView 中的 ViewHolder 设计。

所以这是根据 UI 的范围所决定的,整个页面对应的就是 ViewModel,局部页面对应的就是普通的类。

ViewModel 可以当做是一种特殊的状态管理器,他可以持有普通的 State Holders。

架构指南中的层级对模块化开发有什么建议么?

官方正在研究模块化的架构指南,希望今年能够完成。

应该在 DataSource/Repostory/UseCase 中使用 Flow 么?

可以,但是应该在合适的地方使用 Flow。如果是提供一个一次性的数据(比如从云端接口返回的一个数据),使用 suspend 函数是比较合适的。如果是从 Room 或者是其他类似的数据源中提供可以变化的数据流时,应该使用 Flow。

在 Repository 中会合并多个数据源的数据,这些数据可能会会随时间的变化而变化,因此需要使用 Flow。

什么时候应该使用 UseCase ?

UseCase 主要是为了解决以下两个问题:

  • 封装复杂的业务逻辑,避免出现大型的 ViewModel 类;
  • 封装多 ViewModel 通用的业务逻辑,避免代码重复;

满足上述两个条件的任何一个都可以使用 UseCase。除此之外,使用 UseCase 之后可以 ViewModel 就不必依赖 Repository,而是在构造函数中直接使用 UseCase,这样在构造函数中就可以知道 ViewModel 中做了哪些事情。

在 Repository 中传递多个 Suspend 函数是反模式的么?

并不是,如果只是进行一次性的操作应该使用 suspend 函数。否则的话应该使用 Flow

ViewModel 中如何处理页面跳转?

这部分内容在 UI Layer 中有提到,我们把他称之为 UI Event 之类的东西。不管是 UI 事件还是构建 UI 的数据都应该放在 UiState 中,一旦 UI 层监听到对应的事件,进行响应的跳转即可。

具体取决于采用何种方式建模,比如你可以把他定义为一个 Boolean 值,在 ViewModel 中更新对应的 UiState,UI 层就可以根据对应的事件跳转到对应的页面中了。

以用户登录为例,用户登录成功之后需要进入到主页面。这其实是一种状态的变化,从未登录变成了已登录,所以只要改变 UiState UI 层就可以处理他。你可能会说这是不是太复杂了?

对一些人来说这可能是一个顿悟时刻,不会把命令视为事件。ViewModel 中的事件整体上来看就是 App 的状态数据,这样 ViewModel 并不是告诉 UI 层应该做什么,而是 UI 层根据 App 的状态去做一些事情,这是思维方式上的转变。

如果根据用户配置定义导航视图?

和上一个问题基本类似,通过设置不同的 UiState 来控制导航到不同的视图。

WorkManager/Service 在架构中的什么位置?

WorkManager/Service 应该作为入口类来调用 Repository 中的 API。这样可以使我们的业务逻辑与 Android 系统的 API 中解放出来,因为 Android 系统 API 的行为逻辑一般是我们不可控的。当然还有另外的好处,比如更容易测试、以及后续有可能迁移到 KMM 。

当然,WorkManager 有其 API 自身的优势,其内部逻辑与节省电量 、WIFI 链接等 Android 平台的优势,可以根据设备自身的状态进行一次性或者周期性的任务。当然,如果是正常的耗时操作(如网络请求)使用简单的协程即可。

为什么有时在 Repository 中使用 IoDispatcher 访问数据库,有时不用?

Room 数据库目前已支持 suspend 函数,其默认会将任务放到后台线程中执行,当然也可以对其进行自定义的配置。当你调用 Room 的一个 suspend 函数的时候,你并不需要关心线程的问题,把这个问题交给 Room 处理即可。这样 Room 的所有操作都会放到 Room 控制的线程中执行,以尽可能的减少因访问同一个文件而导致的互相争夺资源的问题。

不管由于什么原因你无法使用/提供 suspend 函数,Room 不支持将其移动到另一个线程中。这种情况下可以使用 IO 线程或者 IoDispatcher 。通常情况下还是建议使用 suspend 函数或是 Flow。

另外,每个 DataSource 中都建议处理自己的线程问题,所以在 Repository 中并不需要关心 IO 线程的问题。

当我们处理内存敏感数据时,将复杂数据从一个屏幕传输到另一个屏幕的建议方法是什么?

最原始的处理方式可能是使用 Activity 当做两个 Fragment 的中介,在 Activity 中实现 Fragment 中定义接口,当 Fragment 被关联到 Activity 之后就可以调用 Fragment 中相关的逻辑了。当然这种方式已经一去不复返了。

现在可以使用 ViewModel 调用 UseCase 或者是 Repository 来更新数据,另外的 ViewModel 会使用 Flow 的方式接收到数据的变化,然后根据变化做出相应的处理即可。

在 Compose 中应该使用 MVVM 还是 MVI ?

两者没有本质的区别,都是采用单向数据流的方式。MVI 的特点是将 UI 事件封装成枚举方式,统一在一个函数中处理事件。

随着项目的扩展,我应该将 Domain Layer 或者 Data Layer移动到单独的模块吗?

这取决于你是否想进行模块化,但是两者的差异并不大,这里并没有一个明确的答案。 随着项目的扩展建议把 Data Layer 放到单独的 module 中,如果有 Domain Layer 的话,也是建议把他放在单独的模块中。

[!info] 注: 如果是在多仓库的模块架构以及同一数据层可能有不同 UI 实现的场景下建议采用上述方式,如果没有类似需求的话,个人建议通过 Feature (特性)划分模块,这样可以减少不必要的编译耗时,同样的功能也想多内聚。 当然,如果有对外提供 SDK 需求的,也应将 Data Layer 定义在单独的模块中。

将错误从其他层返回到表示层的最佳方法是什么?

指导文档 中的建议的方式是使用协程的异常处理机制将错误传递到展示层(UI Layer)。

对于使用协程或者是 Flow 的方式,建议使用协程的异常处理机制,当然 Folw 中也可以使用 catch 操作符;对于使用 suspend 函数的地方可以使用 try/catch 块。

另外一种 Data Layer 和上层交互的方式就是使用 Kotlin 自带的 Result<T> 类,此类中除了包含正常的数据 T 之外,还包含错误信息。除此之外提供很多好用的 API ,建议使用。

更多内容:Exceptions in coroutines

新的架构方式是否适用与非移动端的领域?如平板、Auto 等

在上述的领域上,仍然建议使用 UI LayerDomain LayerData Layer 这种架构方式。这种架构方式主要是用到了关注点分离以及数据驱动的方式,而这两种设计原则对非移动端的应用也是适用的。

多 Activity 还是单 Activity + 多 Fragment ?

Activity 有其自身的一些特定及职责,在以下场景中应该使用 Activity:

  • 当前页面需要支持 PIP(画中画)功能时;
  • 当 App 有多个入口可以启动时,如在其他 App 中拉起当前 App 中的一个二级页面,这个页面就需要使用 Activity;

简而言之,就是需要在 manifest 文件中 exported 需要被设置为 true 的逻辑都需要使用 Activity 来实现。

除此之外的一些逻辑,通常是 App 内部的一些页面导航,这个时候应该使用 Fragment 来实现。当然,现在我们还有另外一个新的选择,那就是使用 Compose 。

总结

这篇文章中的内容根据 Arch - MAD Skills 中的问答部分,结合自己的理解整理而得,会包含个人观点,更多详细内容可以观看原文。


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

封装Kotlin协程请求,这一篇就够了

协程(coroutines)的封装 在默认的Kotlin协程环境中,我们需要自定义协程的作用域CoroutineScope,还有负责维护协程的调度等等,有没有方法可以让协程的使用者屏蔽对底层协程的认识,简单就能使用呢?这里带来了一个封装思路。(这里没有考虑生命...
继续阅读 »

协程(coroutines)的封装


在默认的Kotlin协程环境中,我们需要自定义协程的作用域CoroutineScope,还有负责维护协程的调度等等,有没有方法可以让协程的使用者屏蔽对底层协程的认识,简单就能使用呢?这里带来了一个封装思路。(这里没有考虑生命周期相关的封装,如果需要实际落地可以在deferred里面封装),这里有个背景,比如在java kotlin混合开发的时候,如果java同学不会kotlin,就需要更上一层的抽象了


封装前例子


假如我们有个一个suspend函数

suspend fun getTest():String{
delay(5000)
return "11"
}

我们要实现的封装是:


1.执行suspend函数,并且使用者对底层无感知,无需了解协程就可以使用,这就要求我们屏蔽CoroutineScope细节


2.自动类型转换,比如返回String我们就应该可以在成功的回调中自动转型为String


3.成功的回调,失败的回调等


4.要不,来个DSL风格

//
async{
// 请求
getTest()

}.await(onSuccess = {
//成功时回调,并且具有返回的类型
Log.i("print",it)
},onError = {

},onComplete = {

})

image.png
可以看到,编译时就已经将it变为我们想要的类型了!
我们最终想要实现上面这种方式


封装开始


思路:我们自动对请求进行线程切换,使用Dispatchers即可,还有就是我们需要有监听的回调和DSL写法,所以就可以考虑用协程的async方式发起一个请求,返回值是Deferred类型,我们就可以使用扩展函数实现.await的形式!如果熟悉flutter的同学看的话,是不是很像我们的dio请求方式呢!下面是代码,可以根据更细节的需求进行补充噢:

fun <T> async(loader:suspend () ->T): Deferred<T> {
val deferred = CoroutineScope(Dispatchers.IO).async {
loader.invoke()
}
return deferred
}

fun <T> Deferred<T>.await(onSuccess:(T)->Unit,onError:(e:Exception)->Unit,onComplete:(()->Unit)?=null){
CoroutineScope(Dispatchers.Main).launch {

try{
val result = this@await.await()
onSuccess(result)

}catch (e:Exception){
onError(e)
}
finally {
onComplete?.invoke()
}
}
}

总结


是不是非常好玩呢!我们实现了一个dio风格的请求,对于开发者来说,只需定义suspend修饰的函数,就可以无缝使用我们的请求框架!


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

我尝试以最简单的方式帮你梳理 Lifecycle

前言 我们都知道 Activity 与 Fragment 都是有生命周期的,例如:onCreate()、onStop() 这些回调方法就代表着其生命周期状态。我们开发者所做的一些操作都应该合理的控制在生命周期内...
继续阅读 »

前言


我们都知道 Activity 与 Fragment 都是有生命周期的,例如:onCreate()onStop() 这些回调方法就代表着其生命周期状态。我们开发者所做的一些操作都应该合理的控制在生命周期内,比如:当我们在某个 Activity 中注册了广播接收器,那么在其 onDestory() 前要记得注销掉,避免出现内存泄漏。


生命周期的存在,帮助我们更加方便地管理这些任务。但是,在日常开发中光凭 Activity 与 Fragment 可不够,我们通常还会使用一些组件来帮助我们实现需求,而这些组件就不像 Activity 与 Fragment 一样可以很方便地感知到生命周期了。


假设当前有这么一个需求:



开发一个简易的视频播放器组件以供项目使用,要求在进入页面后注册播放器并加载资源,一旦播放器所处的页面不可见或者不位于前台时就暂停播放,等到页面可见或者又恢复到前台时再继续播放,最后在页面销毁时则注销掉播放器。



试想一下:如果现在让你来实现该需求?你会怎么去实现呢?


实现这样的需求,我们的播放器组件就需要获取到所处页面的生命周期状态,在 onCreate() 中进行注册,onResume() 开始播放,onStop() 暂停播放,onDestroy() 注销播放器。


最简单的方法:提供方法,暴露给使用方,供其自己调用控制。

class VideoPlayerComponent(private val context: Context) {

/**
* 注册,加载资源
*/
fun register() {
loadResource(context)
}

/**
* 注销,释放资源
*/
fun unRegister() {
releaseResource()
}

/**
* 开始播放当前视频资源
*/
fun startPlay() {
startPlayVideo()
}

/**
* 暂停播放
*/
fun stopPlay() {
stopPlayVideo()
}
}

然后,我们的使用方MainActivity自己,主动在其相对应的生命周期状态进行控制调用相对应的方法。

class MainActivity : AppCompatActivity() {
private lateinit var videoPlayerComponent: VideoPlayerComponent

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
videoPlayerComponent = VideoPlayerComponent(this)
videoPlayerComponent.register(this)
}

override fun onResume() {
super.onResume()
videoPlayerComponent.startPlay()
}

override fun onPause() {
super.onPause()
videoPlayerComponent.stopPlay()
}

override fun onDestroy() {
videoPlayerComponent.unRegister()
super.onDestroy()
}

}

虽然实现了需求,但显然这不是最优雅的实现方式。一旦使用方忘记在 onDestroy() 进行注销播放器,就容易造成内存泄漏,而忘记注销显然是一件很容易发生的事情😂 。


回想初衷,之所以将方法暴露给使用方来调用,就是因为我们的组件自身无法感知到使用者的生命周期。所以,一旦我们的组件自身可以感知到使用者的生命周期状态的话,我们就不需要将这些方法暴露出去了。


那么问题来了,组件如何才能感知到生命周期呢?


答:Lifecycle !


直接上案例,借助 Lifecycle 我们改进一下我们的播放器组件👇

class VideoPlayerComponent(private val context: Context) : DefaultLifecycleObserver {

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
register(context)
}

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
startPlay()
}

override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
stopPlay()
}

override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
unRegister()
}

/**
* 注册,加载资源
*/
private fun register(context: Context) {
loadResource(context)
}

/**
* 注销,释放资源
*/
private fun unRegister() {
releaseResource()
}

/**
* 开始播放当前视频资源
*/
private fun startPlay() {
startPlayVideo()
}

/**
* 暂停播放
*/
private fun stopPlay() {
stopPlayVideo()
}
}

改进完成后,我们的调用方MainActivity只需要一行代码即可。

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

lifecycle.addObserver(VideoPlayerComponent(this))
}
}

这样是不是就优雅多了。


那这 Lifecycle 又是怎么感知到生命周期的呢?让我们这就带着问题,出发探一探它的实现方式与源码!


如果让你来做,你会怎么做


在查看源码前,让我们试着思考一下,如果让你来实现 Jetpack Lifecycle 这样的功能,你会怎么做呢?该从何入手呢?


我们的目的是不通过回调方法即可获取到生命周期,这其实就是解耦,实现解耦的一种很好方法就是利用观察者模式。


利用观察者模式,我们就可以这么设计👇


截屏2022-12-13 下午3.59.21.png


被观察者对象就是生命周期,而观察者对象则是需要知晓生命周期的对象,例如:我们的三方组件。


接着我们就具体探探源码,看一看Google是如何实现的吧。


Google 实现方式


Lifecycle



一个代表着Android生命周期的抽象类,也就是我们的抽象被观察者对象。

public abstract class Lifecycle {

public abstract void addObserver(@NonNull LifecycleObserver observer);

public abstract void removeObserver(@NonNull LifecycleObserver observer);

public enum Event {
ON_CREATE,
ON_START,
ON_RESUME,
ON_PAUSE,
ON_STOP,
ON_DESTROY,
ON_ANY;
}

public enum State {
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
}

}

内包含 State 与 Event 分别者代表生命周期的状态与事件,同时定义了抽象方法 addObserver(LifecycleObserver) 与removeObserver(LifecycleObserver) 方法用于添加与删除生命周期观察者。


Event 很好理解,就像是 Activity | Fragment 的 onCreate()onDestroy()等回调方法,它代表着生命周期的事件。


那这 State 又是什么呢?何为状态?他们之间又是什么关系呢?


Event 与 State 之间的关系


关于 Event 与 State 之间的关系,Google官方给出了这么一张两者关系图👇


theRelationOfEventAndState.png


乍一看,可能第一感觉不是那么直观,我整理了一下👇


event与state关系图.png



  • INITIALIZED:在 ON_CREATE 事件触发前。

  • CREATED:在 ON_CREATE 事件触发后以及 ON_START 事件触发前;或者在 ON_STOP 事件触发后以及 ON_DESTROY 事件触发前。

  • STARTED:在 ON_START 事件触发后以及 ON_RESUME 事件触发前;或者在 ON_PAUSE 事件触发后以及 ON_STOP 事件触发前。

  • RESUMED:在 ON_RESUME 事件触发后以及 ON_PAUSE 事件触发前。

  • DESTROYED:在 ON_DESTROY 事件触发之后。


Event 代表生命周期发生变化那个瞬间点,而 State 则表示生命周期的一个阶段。这两者结合的好处就是让我们可以更加直观的感受生命周期,从而可以根据当前所处的生命周期状态来做出更加合理操作行为。


例如,在LiveData的生命周期绑定观察者源码中,就会判断当前观察者对象的生命周期状态,如果当前是DESTROYED状态,则直接移除当前观察者对象。同时,根据观察者对象当前的生命周期状态是否 >= STARTED来判断当前观察者对象是否是活跃的。

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
......

@Override
boolean shouldBeActive() {
//根据观察者对象当前的生命周期状态是否 >= STARTED 来判断当前观察者对象是否是活跃的。
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
//根据当前观察者对象的生命周期状态,如果是DESTROYED,直接移除当前观察者
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
removeObserver(mObserver);
return;
}
......
}
......

}

其实 Event 与 State 这两者之间的联系,在我们生活中也是处处可见,例如:自动洗车。


自动洗车.png


想必现在你对 Event 与 State 之间的关系有了更好的理解了吧。


LifecycleObserver



生命周期观察者,也就是我们的抽象观察者对象。

public interface LifecycleObserver {

}

所以,我们想成为观察生命周期的观察者的话,就需要具体实现该接口,也就是成为具体观察者对象。


换句话说,就是如果你想成为观察者对象来观察生命周期的话,那就必须实现 LifecycleObserver 接口。


例如Google官方提供的 DefaultLifecycleObserver、 LifecycleEventObserver 。


截屏2022-12-14 下午2.33.11.png


LifecycleOwner


正如其名字一样,生命周期的持有者,所以像我们的 Activity | Fragment 都是生命周期的持有者。


大白话很好理解,但代码应该如何实现呢?



抽象概念 + 具体实现



抽象概念:定义 LifecycleOwner 接口。

public interface LifecycleOwner {
@NonNull
Lifecycle getLifecycle();
}

具体实现:Fragment 实现 LifecycleOwner 接口。

public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner,
ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner,
ActivityResultCaller {

public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
......

}

具体实现:Activity 实现 LifecycleOwner 接口。

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware,
LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner,
ActivityResultRegistryOwner,
ActivityResultCaller {

@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}

......

}

这样,Activity | Fragment 就都是生命周期持有者了。


疑问?在上方 Activity | Fragment 的类中,getLifecycle() 方法中都是返回 mLifecycleRegistry,那这个 mLifecycleRegistry 又是什么玩意呢?


LifecycleRegistry



Lifecycle 的一个具体实现类。



LifecycleRegistry 负责管理生命周期观察者对象,并将最新的生命周期事件与状态及时通知给对应的生命周期观察者对象。


添加与删除观察者对象的具体实现方法。

//用户保存生命周期观察者对象
private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap = new FastSafeIterableMap<>();

@Override
public void addObserver(@NonNull LifecycleObserver observer) {
enforceMainThreadIfNeeded("addObserver");
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
//将生命周期观察者对象包装成带生命周期状态的观察者对象
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
... 省略代码 ...
}

@Override
public void removeObserver(@NonNull LifecycleObserver observer) {
mObserverMap.remove(observer);
}

可以从上述代码中发现,LifecycleRegistry 还对生命周期观察者对象进行了包装,使其带有生命周期状态。

static class ObserverWithState {
//生命周期状态
State mState;
//生命周期观察者对象
LifecycleEventObserver mLifecycleObserver;

ObserverWithState(LifecycleObserver observer, State initialState) {
//这里确保observer为LifecycleEventObserver类型
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
//并初始化了状态
mState = initialState;
}

//分发事件
void dispatchEvent(LifecycleOwner owner, Event event) {
//根据 Event 得出当前最新的 State 状态
State newState = event.getTargetState();
mState = min(mState, newState);
//触发观察者对象的 onStateChanged() 方法
mLifecycleObserver.onStateChanged(owner, event);
//更新状态
mState = newState;
}
}

将最新的生命周期事件通知给对应的观察者对象。

public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
... 省略代码 ...
ObserverWithState observer = mObserverMap.entrySet().getValue();
observer.dispatchEvent(lifecycleOwner, event);

... 省略代码 ...
mLifecycleObserver.onStateChanged(owner, event);
}

那 handleLifecycleEvent() 方法在什么时候被调用呢?


相信看到下方这个代码,你就明白了。

public class FragmentActivity extends ComponentActivity {
......

final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}

@Override
protected void onDestroy() {
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}

@Override
protected void onPause() {
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
}

@Override
protected void onStop() {
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
}

......

}

在 Activity | Fragment 的 onCreate()onStart()onPause()等生命周期方法中,调用LifecycleRegistry 的 handleLifecycleEvent() 方法,从而将生命周期事件通知给观察者对象。


总结


Lifecycle 通过观察者设计模式,将生命周期感知对象生命周期提供者充分解耦,不再需要通过回调方法来感知生命周期的状态,使代码变得更加的精简。


虽然不通过 Lifecycle,我们的组件也是可以获取到生命周期的,但是 Lifecycle 的意义就是提供了统一的调用接口,让我们的组件可以更加方便的感知到生命周期,方便广达开发者。而且,Google以此推出了更多的生命周期感知型组件,例如:ViewModelLiveData。正是这些组件,让我们的开发变得越来越简单。


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

我就问Zygote进程,你到底都干了啥

前言 OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote。 提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了什么呢。 还是...
继续阅读 »

前言


OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote
提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了什么呢。
还是老规矩,让我们抱着几个问题来看文章。最后在结尾,再对问题进行思考回复。



  1. 你能大概描述一遍Zygote的启动流程吗

  2. 我们为什么可以执行java代码,和zygote有什么关系

  3. Zygote到底都做了哪些事情


另外,我最近也看了一些写底层的文章。要么就是言简意赅到只知道Zygote的作用,但是完全不知道如何实现的,就像是背作文一样。要么是全篇都是代码,又臭又长,让人完全没有看下去的动力。


不过作为一个读者,太长的我可能完全不想看,太短的又真的完全就是被课文一点都不明白原理。


因此,本篇文章,仍旧会引入一些代码,方便大家通过代码方便记忆。但是又会将代码进行精简,以免给大家造成过度疲劳。我们的目的都是希望用最少的时间,能掌握更多的知识。


读源码时:不要过于纠结细节,不要过于纠结细节,不要过于纠结细节!重要的事情说三遍!!!


OK,让我们进入正题瞅瞅Zygote到底是个什么东西。




1.C++还是Java


选这个当标题当然是有原因的。我们知道的是Android系统启动的时候,运行的是Linux内核,执行的是C++代码。这是一个很有趣的事情。


因为我们在写App的时候,AndroidStudio默认给我们创建的都是Activity.java,而选择的语言,要么是Java要么就是Kotlin


启动时候运行的是C++代码,应用层却可以使用Java代码,这到底是为什么呢?其实这就是Zygote的功劳之一


2.Native层


Init进程创建Zygote时,会调用app_main.cpp的main() 方法。此时它依旧运行的是C++代码。我们通过下面的代码,来看下它到底做了什么。


这里它做的最关键的一件事就是启动AndroidRuntime(Android运行时)。另外这里需要注意的是 start方法 中的 "com.android.internal.os.ZygoteInit" 这个类似于全类名。他到底是做什么的?别着急,大概2分钟后,你可能就会得到答案。


Zygote的新手村:app_main.cpp

int main(int argc, char* const argv[])
{
// 创建Android运行时对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 代码省略...

// 调用AppRuntime.start方法,
// 而AppRuntime是AndroidRuntime的子类,并且没有重写start方法
// 因此调用的是AndroidRuntime的start方法
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}

精简后的代码告诉我们,这里一共就做了两件事,第一件创建AppRuntime,第二件调用start方法


不过,AppRuntimeAndroidRuntime的子类,他没有重写start方法,因此这里调用的是 AndroidRuntime的start() 方法。


奇迹的诞生地:AndroidRuntime:


这里我依旧只保留关键代码,源码关键注释也进行了保留。由下方代码看出这里做了三件改变命运的事情。



  1. startVM -- 启动Java虚拟机

  2. startReg -- 注册JNI

  3. 通过JNI调用Java方法,执行com.android.internal.os.ZygoteInit 的 main 方法
/*
* Start the Android runtime.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
}

/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}

有了JVM,注册了JNI,我们就可以执行Java代码了。


这里我个人建议不要过于纠结细节,比如JVM是如何创建的,JNI是如何注册的。如果感兴趣的童鞋,可以下载源码,到app_main.cpp里进行查看。这里就不进行赘述了。否则代码量太大,适得其反。




3.Java层


命运的十字路口:ZygoteInit.java:


之所以说是命运的十字路口,因为Zygote会在这里创建SystemServer,但是二者却走向了截然不同的道路。


从这里就开始执行Java代码了,当然这些Java代码是运行在JVM中的。


让我们通过代码来看一下,到底都做了些什么。

class ZygoteInit{

/**
* This is the entry point for a Zygote process. It creates the Zygote server, loads resources,
* and handles other tasks related to preparing the process for forking into applications.
* This process is started with a nice value of -20 (highest priority).
*/
// 上面是源码中的注释,小伙伴们可以自行翻译一下。
// 创建了ZygoteServer,加载资源,并且介绍了这个进程的优先级是最高的-20.
public static void main(String argv[]) {
ZygoteServer zygoteServer = null;

// 1. 预加载资源,常用的:resource,class,library等在此处进行加载
preload(bootTimingsTraceLog);

// 2. 创建ZygoteServer,实际是一个Socket用来进行跨进程间通信用的。
zygoteServer = new ZygoteServer(isPrimaryZygote);

// 3. fork出SystemServer进程,这个进程会创建AMS,ATMS,WMS,电池服务等一切只有你想不到没有它做不到的服务
forkSystemServer(abiList, zygoteSocketName, zygoteServer);

// 4.里面是一个while(true)循环,等待接收AMS创建进程的消息,类似于handler中的Looper.loop()
zygoteServer.runSelectLoop(abiList);
}
}

上面代码里的注释基本说明了ZygoteInit都干了啥。下面会再稍微总结下:



  1. 创建了ZygoteServer:这是一个Socket相关的服务,目的是进行跨进程通信。

  2. 预加载preload:预加载相关的资源。

  3. 创建SystemServer进程:通过forkSystemServer分裂出了两个进程,一个Zygote进程,一个SystemServer进程。而且由于是分裂的,所以新分裂出来的进程也拥有虚拟机,也能调用JNI,也拥有预加载的资源,也会执行后续的代码。

  4. 执行runSelectLoop():内部是一个while(true)循环,等待AMS创建新的进程的消息。(想想Looper.loop())




4. 戛然而止


没错就是这么突然,Zygote的故事到这就结束了。至于它是如何创建SystemServer,如何去创建App那就是后面的故事了。所以你还记得我的问题嘛?我替你总结一下Zygote到底做了什么:



  1. 创建虚拟机

  2. 注册JNI

  3. 回调Java方法ZygoteInit.java 的main方法,从这儿开始运行Java代码

  4. 创建ZygoteServer,内部包含Socket用于跨进程通信

  5. 预加载class,resource,library等相关资源

  6. fork 出了 SystemServer进程。他俩除了返回的pid不同,剩下一模一样。通过返回值不同来决定剩下的代码如何运行。这个留待后续进行讲解。

  7. 进入while(true)循环等待,等待AMS创建进程的消息(类似于Looper.loop()




5. 多说一句


这个系列才刚刚进行到第二章,我尽量让文章不是特别长的情况下,既有代码整理思路,又有总结方便记忆。代码是源码中摘抄的省略了很大一部分,只能保证执行顺序。有兴趣的小伙伴可以下载源码进行查看。

其实最近我看了很多文章和视频,最后却选择写文章来对知识进行总结。归根结底就是因为一句话,好记性不如烂笔头。要是真的想快速记住,真的需要亲自查看一下源码。翻源码的同时,也是在不知不觉中锻炼你阅读源码的能力。 万一以后让你学习一个新的库,你也知道大概该怎么看。


都已经到这儿了,就稍微厚个脸皮,希望各位看官,能够点个赞加个关注。毕竟一篇文章可能就要耗费几个小时的时间,上一篇文章就几个赞,感谢这几个小伙伴,让我有继续写下去的动力。 真心感谢你们。


另外,文章中有哪里出错,请及时指出。毕竟各位才是真正的大佬。希望各位Androider,可以一起取暖,度过这个互联网寒冬!加油各位!!!




6. Zygote功能图


zygote.png


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

谈公平:你十年寒窗,比人家三代家业?

有一个想法,憋了好久了,迟迟没敢说,怕三观正不正,被喷。 网上经常看到这样的一幕:一个老太太在路灯下啃着馒头卖菜,旁边广场上是穿着华丽跳舞的老太太。 网友说,这个世界太不公平了,都是同样的年龄,为什么差别这么大。取消那些跳舞老太太的退休金,让她们也去卖菜。 ...
继续阅读 »

有一个想法,憋了好久了,迟迟没敢说,怕三观正不正,被喷。


网上经常看到这样的一幕:一个老太太在路灯下啃着馒头卖菜,旁边广场上是穿着华丽跳舞的老太太。


WechatIMG5.jpeg


网友说,这个世界太不公平了,都是同样的年龄,为什么差别这么大。取消那些跳舞老太太的退休金,让她们也去卖菜。


我看完评论就在想:都去卖菜,就是公平吗?或者,都去跳广场舞才是公平?


到底什么是公平?


其实大众很难说清楚什么是公平,但是经常会提“这不公平!”。


一个出身贫困的农村孩子,经过千辛万苦,终于考上了大学。他毕业后,起早贪黑努力工作。终于,他在城里买下了两套房子,自住一套,出租一套。


看到上面的例子,大家都会说,哎呀,这叫天道酬勤!老天不会辜负努力的孩子。正是他通过自己十几年的努力,才有了今天的成绩。相比于他早就辍学的同村伙伴,相比于那些逃课打游戏的大学同学,这一切对于他来说,是公平的!


哎,再来一个例子。有一个富二代,他从小就逃课、打架,高考也没考好,花钱读了个海外大学,大学也没学啥,就学会了一周换一个女朋友。他刚毕业,他父亲就给他一栋大楼,让他练习如何赚钱。


这个例子,公平吗?


想好了再回答,考验感性和理性的时候到了。


不着急回答。对于上面的例子,我想把时间线再拉长一些。


这位富二代的爷爷,也是出身农村。他老人家从小就父母双亡。一开始他跟着伯父生活,但是伯母经常虐待他。因此,他离家出走,到城里乞讨。后来,他被好心的包子铺老板收留。他在包子铺打工,因为能吃苦,干活勤快,又懂得感恩,所以深受老板和顾客的喜爱。凭借着一膀子力气,他在城里做到了成家立业娶媳妇,后来还有了下一代。这下一代出生便在城里,教育条件也好,从小又跟着父母做生意,见得多,识得也广。后来,下一代凭借着父母积攒的老客户,在事业上越做越大,最终形成了自己的商业帝国。再后来,就有了上面的那个富二代。


大家说,将时间线拉长之后,这三代人也是一个“天道酬勤”的故事吧?开头富二代爷爷的经历,可以对标大学生的一生。


我发现人类的社会和动物的族群,有一个很明显的区别,那就是物质和精神的继承。


Snip20230517_1.png


在一个狮群里,狮王是靠本领拼出来的。下一任狮王,是不是由它的儿子当,能否继续拥有这一方领土和交配权,得看小狮子能不能打败其他狮子,成为最强者。不止是狮子,猴子也是一样。猴群里猴王是最强壮、最聪明的猴子。要是老猴王不行了,新猴王会通过战斗取代它。之所以需要最强的猴子当猴王,那是因为它能够保护整个猴群。虽然,这个猴王不是无所不能。但在这群猴子里面,找不出来比它更合适的了。


动物界的这些名利、地位、经验,是没法传给下一代的。它们的群落会不定期重新洗牌。


但是我们人类社会却不一样。一个富豪,就算他儿子不聪明,甚至身体有残疾,一样有很多人像对待猴王一样仰视他,他一样能获得最优质的繁殖资源。


你说,人类的这种方式高级不高级。


从短期看,不高级。上面说的那种人,要是在动物界,早就被淘汰了。一头斑马,即便是脚部受了点伤,也基本就宣告死亡了。大自然就是要优胜劣汰。谁让这头斑马不注意,凭什么狮子单单就咬伤了你,而不是别人。就算运气不好,这也是一种劣势。动物界,就是以此来保证强大的遗传特征,流通在群体的基因库中。


但是从长期看,人类的这种方式却很高级。因为正是有了资源继承这种特权,才让类人一想到能为子孙后代留点东西、做点事情,就不怕苦不怕累,成为永动机。


我们人类为什么这么想?我们是韭菜成精了吗?


你可能不知道,你是被遗传基因控制的。


Snip20230517_2.png


你的身高、体重,哪里长手,哪里长耳朵,都是写在基因里的。你只是照着图纸在搭建而已。


你不要觉得你有自我。哥们,咱们不配!你知道吗?基因才有自我。


基因的想法不是让你活下去,而是让它自己活下去。而且,还要一代更比一代强。它活下去的方式,就是依靠你来进行繁殖和生育。你之所以怕死,其实是基因怕自己遗传不下去。有些动物,比如鲑鱼、蝴蝶等,它们繁殖完就死掉了。


人类的基因很高级,有了后代后不但不死,而且还让你把孩子养大,甚至还给孩子看孩子。幸好DNA里没法存货物,不然有人可能抱着金条出生。这不是因为有爱,这是因为基因的控制!


为什么动物就做不到这些?因为在自然界,资源是有限的,有时候自己和后代只能保留一个。基因选择了留它自己。


人类通过物质和财富的“遗传”,解决了这个问题。那你说,这种方式是不是非常高级。


也正是这种物质和精神可以继承,才让人类从动物界中脱颖而出,成为了地球的主人。


说这么多,还扯到了生物和伦理,好像有点跑题了。但这也从某一个方面佐证了一些道理。有些事,从局部来看很不公平。但是,当把时间线拉长再看,这又是合理的。


大自然怎么会过一天算一天呢?她是想永生的。


这时,当我们再面对一些局部不公平时,你不必太过于消沉。把时间拉长,你可以把自己作为一个起点。想想那些让你感到气愤的“持有特权者”,他们从几十年前,就已经开始像你现在一样努力了。


难道你想要用你的几年奋斗,去超越人家几十年甚至上百年的沉淀吗?从长远来看,恐怕这也不公平吧?


WechatIMG4.jpeg


一个指导大家考研的老师,他说她女儿可以不用考研。短期看,这好像这是个笑话。但是,他说,考研是为了更好地谋生。她的女儿可以不用为生计发愁,把精力投入到她喜欢做的事情。


“人人平等”最早源于宗教。他们说,不管你如何威风,最后到上帝那里都一样。而法律上的平等,是为了避免社会失去秩序。


世上没有绝对的公平。因为单就“公平”这个词的定义,就很难说清楚。


好了,就说这么多吧。


我是一个理工男,思考问题的方式可能有些偏激。文中提到了“奋斗”和“努力”(不知何时这两个词变味了),我不是想给大家灌鸡汤。理工科最讨厌鸡汤,一点逻辑都没有。我只是从理性的角度,给大家分享一种思路。


总之,到与不到的,还请大家多多包涵。



是的,没错,我就是掘金@TF男孩。文章写的比代码还好的程序员。



作者:TF男孩
来源:juejin.cn/post/7234078632653013048
收起阅读 »

程序员的坏习惯

前言 每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。 不遵循项目规范 每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、...
继续阅读 »

前言


每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。


图片.png


不遵循项目规范


每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、魔鬼数字、提交代码覆盖他人代码等问题经常发生,如果大家能够遵循相关规范,这些问题都可以避免。


用复杂SQL语句来解决问题


程序员在开发功能时,总想着是否能用一条SQL语句来完成这个功能,于是实现的SQL语句写的非常复杂,包含各种子查询嵌套,函数转换等。这样的SQL语句一旦出现了性能问题,很难进行相关优化。


缺少全局把控思维,只关注某一块业务


新增新功能只关注某一小块业务,不考虑系统整体的扩展性,其他模块已经有相关的实现了,却又重复实现,导致重复代码严重。修改功能不考虑对其他模块的影响。


函数复杂冗长,逻辑混乱


一个函数几百行,复杂函数不做拆分,导致代码变得越来月臃肿,最后谁也不敢动。函数还是要遵循设计模式的单一职责,一个函数只做一件事情。如果函数逻辑确实复杂,需要进行拆分,保证逻辑清晰。


缺乏主动思考,拿来主义


实现相关功能,先网上百度一下,拷贝相关的代码,能够运行成功认为万事大吉。到了生产却出现了各种各样的问题,因为网上的demo程序和实际项目的在场景使用上有区别,尤其是相关的参数配置,一定要弄清楚具体的含义,不同场景下,设置参数的值不同。


核心业务逻辑,缺少相关日志和注释


很多核心的业务逻辑实现,整个方法几乎没看到相关注释和日志打印,除了自己能看懂代码逻辑,其他人根本看不懂。一旦生产出了问题,找不到有效的日志输出,问题根本无法定位。


修改代码,缺少必要测试


很多人都会存在侥幸心里,认为只是改了一个变量或者只修改一行代码,不用自测了应该没有问题,殊不知就是因为改一行代码导致了严重的bug。所以修改代码一定要进行自测。


需求没理清,直接写代码


很多程序员在接到需求后,不怎么思考就开始写代码,写着写着发现自己的理解与实际的需求有偏差,造成无意义返工。所以需要多花些时间梳理需求,整理相关思路,能规避很多不合理的问题。


讨论问题,表达没有逻辑、没有重点


讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明。需要学会沟通和表达,才能进行有效的沟通和合作。


不能从错误中吸取教训


作为一位开发人员,你会犯很多错误,这不可避免也没什么大不了的。但如果你总是犯同样的错误,不能从中吸取教训,那态度就出现问题了。


总结


关于这些坏习惯,你是否中招了,大家应该尽早规避这些坏习惯,成为一名优秀的程序员。


作者:剑圣无痕
来源:juejin.cn/post/7136455796979662862
收起阅读 »

十分钟,让你学会Vue的这些巧妙冷技巧

web
前言 写了两年的Vue,期间学习到好几个提高开发效率和性能的技巧,现在把这些技巧用文章的形式总结下来。 1. 巧用$attrs和$listeners $attrs用于记录从父组件传入子组件的所有不被props捕获以及不是class与style的参数,而$lis...
继续阅读 »

前言


写了两年的Vue,期间学习到好几个提高开发效率和性能的技巧,现在把这些技巧用文章的形式总结下来。


1. 巧用$attrs$listeners


$attrs用于记录从父组件传入子组件的所有不被props捕获以及不是classstyle的参数,而$listeners用于记录从父组件传入的所有不含.native修饰器的事件。那下面的代码作例子:


Vue.component('child', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})

new Vue({
data:{a:1,title:'title'},
methods:{
handleClick(){
// ...
},
handleChange(){
// ...
}
},
template:'
<child class="child-width" :a="a" b="1" :title="title" @click.native="handleClick" @change="handleChange">'
,

})

则在<child/>在中



  • attrs的值为{a:1,b:"1"}

  • listeners的值为{change: handleChange}


通常我们可以用$attrs$listeners作组件通信,在二次封装组件中使用时比较高效,如:


Vue.component("custom-dialog", {
// 通过v-bind="$attrs"和v-on="$listeners"把父组件传入的参数和事件都注入到el-dialog实例上
template: '<el-dialog v-bind="$attrs" v-on="$listeners"></el-dialog>',
});

new Vue({
data: { visible: false },
// 这样子就可以像在el-dialog一样用visible控制custom-dialog的显示和消失
template: '<custom-dialog :visible.sync="visible">',
});

再举个例子:


Vue.component("custom-select", {
template: `<el-select v-bind="$attrs" v-on="$listeners">
<el-option value="选项1" label="黄金糕"/>
<el-option value="选项2" label="双皮奶"/>
</el-select>`
,
});

new Vue({
data: { value: "" },
// v-model在这里其实是v-bind:value和v-on:change的组合,
// 在custom-select里,通过v-bind="$attrs" v-on="$listeners"的注入,
// 把父组件上的value值双向绑定到custom-select组件里的el-select上,相当于<el-select v-model="value">
// 与此同时,在custom-select注入的size变量也会通过v-bind="$attrs"注入到el-select上,从而控制el-select的大小
template: '<custom-select v-model="value" size="small">',
});

2. 巧用$props


$porps用于记录从父组件传入子组件的所有被props捕获以及不是classstyle的参数。如


Vue.component('child', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})

new Vue({
data:{a:1,title:'title'},
methods:{
handleClick(){
// ...
},
handleChange(){
// ...
}
},
template:'
<child class="child-width" :a="a" b="1" :title="title">'
,

})

则在<child/>在中,$props的值为{title:'title'}$props可以用于自组件和孙组件定义的props都相同的情况,如:


Vue.component('grand-child', {
props: ['a','b'],
template: '<h3>{{ a + b}}</h3>'
})

// child和grand-child都需要用到来自父组件的a与b的值时,
// 在child中可以通过v-bind="$props"迅速把a与b注入到grand-child里
Vue.component('child', {
props: ['a','b'],
template: `
<div>
{{a}}加{{b}}的和是:
<grand-child v-bind="$props"/>
</div>
`

})

new Vue({
template:'
<child class="child-width" :a="1" :b="2">'
,
})

举一个我在开发中常用到$props的例子:


element-uiel-tabstab-click事件中,其回调参数是被选中的标签 tab 实例(也就是el-tab-pane实例),此时我想获取el-tab-pane实例上的属性如name值,可以有下面这种写法:


<template>
<div>
<el-tabs v-model="activeTab" @tab-click="getTabName">
<el-tab-pane name="123" label="123">123</el-tab-pane>
<el-tab-pane name="456" label="456">456</el-tab-pane>
</el-tabs>
</div>
</template>

<script>
export default {
name: "App",
data() {
return {
activeTab: "123",
};
},
methods: {
getTabName(tab) {
console.log(tab.$props.name);
},
},
};
</script>

此时效果如下所示,可看到控制台打印出我们点击的tab上的name属性值:


tab-prop.gif


示例代码放在sandbox里。


3. 妙用函数式组件


函数式组件相比于一般的vue组件而言,最大的区别是非响应式的。它不会监听任何数据,也没有实例(因此没有状态,意味着不存在诸如createdmounted的生命周期)。好处是因只是函数,故渲染开销也低很多。


把开头的例子改成函数式组件,代码如下:


<script>
export default {
name: "anchor-header",
functional: true, // 以functional:true声明该组件为函数式组件
props: {
level: Number,
name: String,
content: String,
},
// 对于函数式组件,render函数会额外传入一个context参数用来表示上下文,即替代this。函数式组件没有实例,故不存在this
render: function (createElement, context) {
const anchor = {
props: {
content: String,
name: String,
},
template: '<a :id="name" :href="`#${name}`"> {{content}}</a>',
};
const anchorEl = createElement(anchor, {
props: {
content: context.props.content, //通过context.props调用props传入的变量
name: context.props.name,
},
});
const el = createElement(`h${context.props.level}`, [anchorEl]);
return el;
},
};
</script>

4. 妙用 Vue.config.devtools


其实我们在生产环境下也可以调用vue-devtools去进行调试,只要更改Vue.config.devtools配置既可,如下所示:


// 务必在加载 Vue 之后,立即同步设置以下内容
// 该配置项在开发版本默认为 `true`,生产版本默认为 `false`
Vue.config.devtools = true;

我们可以通过检测cookie里的用户角色信息去决定是否开启该配置项,从而提高线上 bug 查找的便利性。


5. 妙用 methods


Vue中的method可以赋值为高阶函数返回的结果,例如:


<script>
import { debounce } from "lodash";

export default {
methods: {
search: debounce(async function (keyword) {
// ... 请求逻辑
}, 500),
},
};
</script>

上面的search函数赋值为debounce返回的结果, 也就是具有防抖功能的请求函数。这种方式可以避免我们在组件里要自己写一遍防抖逻辑。


这里有个例子sandbox,大家可以点进去看看经过高阶函数处理的method与原始method的效果区别,如下所示:


high-class-method.gif


除此之外,method还可以定义为生成器,如果我们有个函数需要执行时很强调顺序,而且需要在data里定义变量来记录上一次的状态,则可以考虑用生成器。


例如有个很常见的场景:微信的视频通话在接通的时候会显示计时器来记录通话时间,这个通话时间需要每秒更新一次,即获取通话时间的函数需要每秒执行一次,如果写成普通函数则需要在data里存放记录时间的变量。但如果用生成器则能很巧妙地解决,如下所示:


<template>
<div id="app">
<h3>{{ timeFormat }}</h3>
</div>
</template>

<script>
export default {
name: "App",
data() {
return {
// 用于显示时间的变量,是一个HH:MM:SS时间格式的字符串
timeFormat: "",
};
},
methods: {
genTime: function* () {
// 声明存储时、分、秒的变量
let hour = 0;
let minute = 0;
let second = 0;
while (true) {
// 递增秒
second += 1;
// 如果秒到60了,则分加1,秒清零
if (second === 60) {
second = 0;
minute += 1;
}
// 如果分到60了,则时加1,分清零
if (minute === 60) {
minute = 0;
hour += 1;
}
// 最后返回最新的时间字符串
yield `${hour}:${minute}:${second}`;
}
},
},
created() {
// 通过生成器生成迭代器
let gen = this.genTime();
// 设置计时器定时从迭代器获取最新的时间字符串
const timer = setInterval(() => {
this.timeFormat = gen.next().value;
}, 1000);
// 在组件销毁的时候清空定时器和迭代器以免发生内存泄漏
this.$once("hook:beforeDestroy", () => {
clearInterval(timer);
gen = null;
});
},
};
</script>

页面效果如下所示:


gen-method.gif


代码地址:code sandbox


但需要注意的是:method 不能是箭头函数



注意,不应该使用箭头函数来定义 method 函数 (例如 plus: () => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined



6. 妙用 watch 的数组格式


很多开发者会在watch中某一个变量的handler里调用多个操作,如下所示:


<script>
export default {
data() {
return {
value: "",
};
},
methods: {
fn1() {},
fn2() {},
},
watch: {
value: {
handler() {
fn1();
fn2();
},
immediate: true,
deep: true,
},
},
};
</script>

虽然fn1fn2都需要在value变动的时候调用,但两者的调用时机可能不同。fn1可能仅需要在deepfalse的配置下调用既可。因此,Vuewatch的值添加了Array类型来针对上面所说的情况,如果用watchArray的写法处理可以写成下面这种形式:


<script>
watch:{
'value':[
{
handler:function(){
fn1()
},
immediate:true
},
{
handler:function(){
fn2()
},
immediate:true,
deep:true
}
]
}
</script>

7. 妙用$options


$options是一个记录当前Vue组件的初始化属性选项。通常开发中,我们想把data里的某个值重置为初始化时候的值,可以像下面这么写:


this.value = this.$options.data().value;

这样子就可以在初始值由于需求需要更改时,只在data中更改即可。


这里再举一个场景:一个el-dialog中有一个el-form,我们要求每次打开el-dialog时都要重置el-form里的数据,则可以这么写:


<template>
<div>
<el-button @click="visible=!visible">打开弹窗</el-button>
<el-dialog @open="initForm" title="个人资料" :visible.sync="visible">
<el-form>
<el-form-item label="名称">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.gender">
<el-radio label="male"></el-radio>
<el-radio label="female"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>

<script>
export default {
name: "App",
data() {
return {
visible: false,
form: {
gender: "male",
name: "wayne",
},
};
},
methods: {
initForm() {
this.form = this.$options.data().form;
},
},
};
</script>

每次el-dialog打开之前都会调用其@open中的方法initForm,从而重置form值到初始值。如下效果所示:


option_data.gif


以上代码放在sanbox


如果要重置data里的所有值,可以像下面这么写:


Object.assign(this.$data, this.$options.data());
// 注意千万不要写成下面的样子,这样子就更改this.$data的指向。使得其指向另外的与组件脱离的状态
this.$data = this.$options.data();

8. 妙用 v-pre,v-once


v-pre


v-pre用于跳过被标记的元素以及其子元素的编译过程,如果一个元素自身及其自元素非常打,而又不带任何与Vue相关的响应式逻辑,那么可以用v-pre标记。标记后效果如下:


image.png


v-once



只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。



对于部分在首次渲染后就不会再有响应式变化的元素,可以用v-once属性去标记,如下:


<el-select>
<el-option
v-for="item in options"
v-once
:key="item.value"
:label="item.label"
:value="item.value"
>
{{i}}</el-option
>
</el-select>

如果上述例子中的变量options很大且不会再有响应式变化,那么如例子中用上v-once对性能有提升。


9. 妙用 hook 事件


如果想监听子组件的生命周期时,可以像下面例子中这么做:


<template>
<child @hook:mounted="removeLoading" />
</template>

这样的写法可以用于处理加载第三方的初始化过程稍漫长的子组件时,我们可以加loading动画,等到子组件加载完毕,到了mounted生命周期时,把loading动画移除。


初次之外hook还有一个常用的写法,在一个需要轮询更新数据的组件上,我们通常在created里开启定时器,然后在beforeDestroy上清除定时器。而通过hook,开启和销毁定时器的逻辑我们都可以在created里实现:


<script>
export default {
created() {
const timer = setInterval(() => {
// 更新逻辑
}, 1000);
// 通过$once和hook监听实例自身的beforeDestroy,触发该生命周期时清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(timer);
});
},
};
</script>

像上面这种写法就保证了逻辑的统一,遵循了单一职责原则。


作者:村上小树
来源:juejin.cn/post/7103066172530098206
收起阅读 »

学了设计模式,我重构了原来写的垃圾代码

前言 最近笔者学习了一些设计模式,都记录在我的专栏 前端要掌握的设计模式 中,感兴趣的掘友可以移步看看。本着 学东西不能停留在用眼睛看,要动手实践 的理念,笔者今天带来的是一篇关于代码逻辑重构的文章,将学到的东西充分运用到实际的项目中。 重构代码的背景 要重构...
继续阅读 »

前言


最近笔者学习了一些设计模式,都记录在我的专栏 前端要掌握的设计模式 中,感兴趣的掘友可以移步看看。本着 学东西不能停留在用眼睛看,要动手实践 的理念,笔者今天带来的是一篇关于代码逻辑重构的文章,将学到的东西充分运用到实际的项目中。


重构代码的背景


要重构的代码是之前笔者的一篇文章——我是怎么开发一个Babel插件来实现项目需求的?,大概的逻辑就是实现 JS 代码的一些转换需求:



  1. 去掉箭头函数的第一个参数(如果是ctx(ctx, argu1) => {}转换为(argu1) => {}

  2. 函数调用加上this.: sss(ctx) 转换为 this.sss()

  3. ctx.get('one').$xxx() 转换为 this.$xxxOne()

  4. const crud = ctx.get('two'); crud.$xxx();转换为this.$xxxTwo()


  5. /**
    * 处理批量的按钮显示隐藏
    * ctx.setVisible('code1,code2,code3', true)
    * 转化为
    * this.$refs.code1.setVisible(true)
    * this.$refs.code2.setVisible(true)
    * this.$refs.code3.setVisible(true)
    */


  6. 函数调用把部分 API 第一参数为ctx的变为arguments

  7. 函数调用去掉第一个参数(如果是ctx

  8. 函数声明去掉第一个参数(如果是ctx

  9. 普通函数 转为 () => {}

  10. 标识符ctx 转为 this

  11. ctx.data 转为 this

  12. xp.message(options) 转换为 this.$message(options)

  13. const obj = { get() {} } 转化为 const obj = { get: () => {} }


具体的实现可参考之前的文章,本文主要分享一下重构的实现。


重构前


所有的逻辑全写在一个 JS 文件中:
image.png
还有一段逻辑很长:
image.png


为啥要重构?


虽然主体部分被我折叠起来了,依然可以看到上面截图的代码存在很多问题,而且主体内容只会比这更糟:



  1. 难以维护,虽然写了注释,但是让别人来改根本看不明白,改不动,甚至不想看

  2. 如果新增了转换需求,不得不来改上面的代码,违反开放封闭原则。因为你无法保证你改动的代码不会造成原有逻辑的 bug。

  3. 代码没有章法,乱的一批,里面还有一堆 if/elsetry/catch

  4. 如果某个转换逻辑,我不想启用,按照现有的只能把对应的代码注释,依然是具有破坏性的


基于以上的这些问题,我决定重构一下。


重构后


先来看下重构后的样子:
image.png
统一将代码放到一个新的文件夹code-transform下:



  • transfuncs文件夹用来放具体的转换逻辑

  • util.js中有几个工具函数

  • trans_config.js用于配置transfuncs中转换逻辑是否生效

  • index.js 导出访问者对象 visitor(可以理解为我们根据配置动态组装一个 visitor 出来)


transfuncs下面的文件格式


如下图所示,该文件夹下的每个 JS 文件都默认导出一个函数,是真正的转换逻辑。
image.png
文件名命名规则:js ast树中节点的type_执行转换的大概内容


其余三个文件内容概览


image.png
其中笔者主要说明一下index.js


import config from './trans_config'

const visitor = {}
/**
* 导出获取访问者对象的函数
*/

export function generateVisitorByConfig() {
if (Object.keys(visitor).length !== 0) {
return visitor
}
// 过滤掉 trans_config.js 中不启用的转换规则
const transKeys = Object.keys(config).filter(key => config[key])
// 导入 ./transfuncs 下的转换规则
const context = require.context('./transfuncs', false, /\.js$/)
const types = new Set()
// 统计我们定义的转换函数,是哪些 ast 节点执行转换逻辑
// 别忘了文件名命名规则:js ast树中节点的type_执行转换的大概内容
// 注意去重,因为我们可能在同一种节点类型,会执行多种转换规则。
// 比如 transfuncs 下有多个 CallExpression_ 开头的文件。
context.keys().forEach(path => {
const fileName = path.substring(path.lastIndexOf('/') + 1).replace('.js', '')
const type = fileName.split('_')[0]
types.add(type)
})

const arrTypes = [...types]
// 到此 arrTypes 可能是这样的:
// ['CallExpression', 'FunctionDeclaration', 'MemberExpression', ...]
// 接着遍历每种节点 type

arrTypes.forEach(type => {
const typeFuncs = context.keys()
// 在 transfuncs 文件夹下找出以 对应 type 开头
// 并且 trans_config 中启用了的的文件
.filter(path => path.includes(type) && transKeys.find(key => path.includes(key)))
// 得到文件导出的 function
.map(path => context(path).default)
// 如果 typeFuncs.length > 0,就给 visitor 设置该节点执行的转换逻辑
typeFuncs.length > 0 && (visitor[type] = path => {
typeFuncs.forEach(func => func(path, attribute))
})
})
// 导出 visitor
return visitor
}

最后调用:


import { generateVisitorByConfig } from '../code-transform'

const transed = babel.transform(code, {
presets: ['es2016'],
sourceType: 'module',
plugins: [
{
visitor: generateVisitorByConfig()
}
]
}).code

有些掘友可能对babel的代码转换能力、babel插件不是很了解, 看完可能还处于懵的状态,对此建议各位先去我的上一篇我是怎么开发一个Babel插件来实现项目需求的? 大致看下逻辑,或者阅读一下Babel插件手册,看完之后自然就通了。


总结


到此呢,该部分的代码重构就完成了,能够明显看出:



  1. 文件变多了,但是每个文件做的事情更专一了

  2. 可以很轻松启用、禁用转换规则了,trans_config中配置一下即可,再也不用注释代码了

  3. 可以很轻松的新增转换逻辑,你只需要关注你在哪个节点处理你的逻辑,注意下文件名即可,你甚至不需要关心引入文件,因为会自动引入。

  4. 更容易维护了,就算离职了你的同事也能改的动你的代码,不会骂人了

  5. 逻辑更清晰了

  6. 对个人来说,代码组织能力提升了😃


👊🏼感谢观看!如果对你有帮助,别忘了 点赞 ➕ 评论 + 收藏 哦!


作者:Lvzl
来源:juejin.cn/post/7224205585125556284
收起阅读 »

4年前端的迷茫与挣扎

我是一名前端开发工程师。 2022年这是我工作的第4个年头,2023年将是第5个年头,和朋友聊到这里总会难免地透露出些许错愕,不知不觉已经工作这么久了,久到已经成为了刚工作时我们导师的工龄了。 以前的我会认为,工作4年后的我一定在前端开发领域稍有建树,而此时的...
继续阅读 »

我是一名前端开发工程师。


2022年这是我工作的第4个年头,2023年将是第5个年头,和朋友聊到这里总会难免地透露出些许错愕,不知不觉已经工作这么久了,久到已经成为了刚工作时我们导师的工龄了。


以前的我会认为,工作4年后的我一定在前端开发领域稍有建树,而此时的我猛地发觉了自己的普通。看着同龄人中,有的持续进行写作,公众号积累了不少粉丝,有的有明确的技术方向,不断深耕。而我望向远方的路,却仍是迷茫。有人们总说要向前看,但我真的不知道哪里是前。


人生的意义是什么?


2022年知乎给我推送最多的消息,都与『人生的意义是什么』这个问题相关。我本以为我是有了答案的,我认为人生的意义是去追逐永恒。


因为苏格拉底说人会追求自身所缺乏的东西,而人最缺乏的东西是生命,一个人的生命在宇宙的尺度是如此的短暂。我用这个来解释人为什么会结婚,生育,抚养下一代,因为孩子延续了父母两个个体生命。而人类整个群体因此突破了个体的局限,我们的文明从诞生跨越至今。


image.png


但我逐渐意识到,结婚,生育,抚养下一代并不是我想度过的人生,我甚至内心深处可能自己都并不是很想活,但确实可以肯定的是我也是不敢死。所以我努力说服自己去寻找其他追求永恒的方式,比如像那些哲学家流传下珍贵的思想,那些作家书写下传世名作,寻找一些属于我的东西能够超越我生命的尺度。


我想有人读到这里可能会拍桌而起,并愤怒地跟我说,去他妈的,人生什么意义都没有。我也认可,但我更喜欢为人生认为地去赋予一些意义,免得像个无头的苍蝇。所以我更喜欢这样理解,人生的实在并无意义,所以你拥有无限的自由去决定该如何着手你的人生。


因为我只会写代码,所以开始将我的代码视为自己生命的延伸,希望就算从这个世界离去,也能在将自己存在过的痕迹延续的久一点。


在开源上的尝试



2022年 Commit 肝了1.1k次,PR了74次,创建了98个 Issue,多数 Issue 写的技术文章,随笔或翻译。今年有一阵子是真的比较肝,早上上班前,晚上下班后,甚至2022清明的3天假期也都用来写项目。


image.png


image.png


思考


最开始在 Github 上写的项目都是基于工作中发现的一些问题,例如 Taro2 自动化升级 Taro3 项目的工具,在百度小程序开发者工具中注入 Chrome 浏览器插件。


值得一提的是,我工作中负责的一个产品,它使用 Taro 开发小程序,使用 Next.js 开发 SSR,同一个项目需要维护两个代码库。就开始思考是否能够让 Taro H5 直接支持 SSR,以此将这两个代码库合并,不用每次需求迭代我需要前后开发两个项目。有了大概的设想,就在清明放假期间着手去做了这件事,搞出了一个最小可用项目。最终业余时间不断完善它,最后在 leader 的帮助下,协调出时间将两个代码库合并为一个!


经历了这些之后,现在我对我个人参与开源的定位如下所示,这同样也写在我 Github 个人主页的 README 中。其中我觉得最重要的是能够在平时的工作中发现问题,并能够将其分类或定义为一个别人也有可能会遇到的一个一般性问题,这甚至比解决方案更加重要。



  • 👋 不断学习并挑战自己。

  • 🌱 正在学习 Golang & Rust。

  • 🤔 如何让前端开发更加愉悦?

    • 让重复的工作自动化。

    • 定义一般问题并提供解决方案。

    • 减少需做决定的数量。

    • 工具应当稳定、高效且灵活。



  • 😕 10月不适合写代码。

  • 🤪 间歇性开发工具,持续性制造垃圾。


从创造产品角度讲,Tim Qian 写的一篇文章令我收益颇多,让我的思维产生从参与开源社区是为了玩技术,到玩技术是为了让自己的点子快速实现的转变。


另外在我看了很多顶级开源项目之后,发觉这些伟大的项目的诞生不仅是为了解决某个问题,其中还蕴含着不同的人对于问题的不同划分和定义,最终表现出不同的解决方案,通过这些痕迹你甚至能看出一些作者对于这个世界的认识和他独特的价值观。


成果


让我觉得开源做的算是有些成果时,是发觉我有免费使用 Github Copilot 资格的时候,应该是因为被邀请进了 Taro 仓库成员的缘故,被认作流行开源项目的维护者。在这之前的情绪一直比较低沉,因为确实没得到多少 Star,但最终与自己和解了,虽然没有做出令人印象深刻的项目,但至少肝过。


image.png


接下的目标


现在得到最多 Star 的是 Taro SSR 插件,现在是 189 个 Star,也迁移到了 Taro 官方库中维护,期望明年能够有一个超过 512 个 Star 的项目。


至于为什么是 512 个,这于 Github 的成就系统有关。


image.png


工作上变动


换了公司


2022年真的是互联网寒冬,之前的公司开始裁员,其中一位应届生被裁了,整个楼都是他的哀嚎声,对我来说还挺震撼的。后来我就跳槽了,并不是被裁了,而是在整体裁员的压抑的氛围下,私下面试的几家公司以防止被裁。其中的大多数都发了 offer,但确实其中有一家给的有点超过我的预期,就在各种纠结之下直接跳槽了。


这次跳槽经历感觉对面试最有帮助的是《labuladong 的算法小抄》的这本书,我写算法题的能力很差,但这本书就用着一种应试教育的方法,让你短时间能速成面试算法做题家。因为种种原因,它的豆瓣评分很低,但他对我来说确实是对症下药了。


image.png


痛苦的 Landing


在新公司最终顺利转正了,但对我个人而言是真的不顺利。


一方面是新的工作是做内部机审业务的B端前端,业务面向于技术研发逻辑极为复杂,花费几个月的时间仍然有非常多不了解的东西,且你不知道该从业务的何处下手,才能让你对业务能够更加深入。


另一方面,由于对整体业务的不熟悉,很多事情是驱动不了的,比如团队中要搞 Web IDE 这件事,但我对于现有业务有哪些,哪些有 Web IDE 的诉求,具体的诉求是什么这些事情,种种问题导致难以下手。明显的感觉就是,与在老东家相比,给你的事情你没法做的很好。


回老家?


因为新工作的压抑,导致一度想回老家找个轻松点的活做。但令打工人悲伤的事情来了,回老家的结果很有可能是,薪资缩水一半,可能还得加班。拿着二线城市的薪资,加着一线城市的班,何必呢,遂打消了这个想法。


选择真的很重要,做着差不多的工作,选择在不同的城市薪资就有如此大的差别,就更别说更大事情上面的选择了。


继续挣扎


由于转正了,所以就沉下心来在新的环境中继续挣扎,不断调整自己。新的一年里,会不断去反思哪些方面是自己能够做的,然后做的足够更好,希望能在这段新的工作中成就我们的业务,也能成就我自己。


最后


再见2022,你好2023。


image.png


作者:SyMind
来源:juejin.cn/post/7190433155399516215
收起阅读 »

一点点Vue性能优化方案分享

web
我们在开发Vue项目时候都知道,在vue开发中某些问题如果前期忽略掉,当时不会出现明显的效果,但是越向后开发越难做,而且项目做久了就会出现问题,这就是所说的蝴蝶效应,这样后期的维护成本会非常高,并且项目上线后还会影响用户体验,也会出现加载慢等一系列的性能问题,...
继续阅读 »

我们在开发Vue项目时候都知道,在vue开发中某些问题如果前期忽略掉,当时不会出现明显的效果,但是越向后开发越难做,而且项目做久了就会出现问题,这就是所说的蝴蝶效应,这样后期的维护成本会非常高,并且项目上线后还会影响用户体验,也会出现加载慢等一系列的性能问题,下面举一个简单的例子。



举个简单的例子



如果加载项目的时候加载一张图片需要0.1s,其实算不了什么可以忽略不计,但是如果我有20张图片,这就是2s的时间, 2s的时间不算长一下就过去了,但是这仅仅的只是加载了图片,还有我们的js,css都需要加载,那就需要更长的时间,可能是5s,6s...,比如加载时间是5s,用户可能等都不会等,直接关闭我们的网站,最后导致我们网站流量很少,流量少就没人用,没人用就没有钱,没有钱就涨不了工资,涨不了工资最后就是跑路了😂。通过上面的例子可以看出性能问题是多么的重要甚至关系到了我们薪资😂,那如何避免这些问题呢?废话不多说,下面分享一下自己在写项目的时用到的一些优化方案以及注意事项。



1.不要将所有的数据都放在data中



可以将一些不被视图渲染的数据声明到实例外部然后在内部引用引用,因为Vue2初始化数据的时候会将data中的所有属性遍历通过Object.definePrototype重新定义所有属性;Vue3是通过Proxy去对数据包装,内部也会涉及到递归遍历,在属性比较多的情况下很耗费性能



<template>
<button @click="updateValue">{{msg}}</button>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
msg:'true'
}
},
created(){
this.text = 'text'
},
methods:{
updateValue(){
keys = !keys
this.msg = keys?'true':'false'
}
}

}
</script>

2.watch 尽量不要使用deep:true深层遍历



因为watch不存在缓存,是指定监听对象,如果deep:true,并且监听对象类型情况下,会递归处理收集依赖,最后触发更新回调



3. vue 在 v-for 时给每项元素绑定事件需要用事件代理



vue源码中是通过addEventLisener去给dom绑定事件的,比如我们使用v-for需要渲染100条数据并且并为每个节点添加点击事件,如果每个都绑定事件那就存在很多的addEventLisener,这里不用说性能上肯定不好,那我们就需要使用事件代理处理这个问题



<template>
<ul @click="EventAgent">
<li v-for="(item) in mArr" :key="item.id" :data-set="item">{{item.day}}</li>
</ul>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{
day:1,
id:'xx1'
},{
day:2,
id:'xx2'
},{
day:2,
id:'xx2'
},
...
]
}
},
methods:{
EventAgent(e){
// 注意这里 在项目中千万不要写的这么简单,我只是为了方便理解才这么写的
console.log(e.target.getAttribute('data-set'))
}
}

}
</script>

4. v-for尽量不要与v-if一同使用



vue的编译过程是template->vnode,看下面的例子



// 假设data中存在一个arr数组
<div id="container">
<div v-for="(item,index) in arr" v-if="arr.length" key="item.id">{{item}}</div>
</div>


上面的例子有可能大家经常这么做,其实这么做也能达到效果但是在性能上面不是很好,因为Ast在转化为render函数的时候会将每个遍历生成的对象都会加入if判断,最后在渲染的时候每次都每个遍历对象都会判断一次需要不需要渲染,这样就很浪费性能,为了避免这个问题我们把代码稍微改一下



<div id="container" v-if="arr.length"> 
<div v-for="(item,index) in arr" >{{item}}</div>
</div>


这样就只判断一次就能达到渲染效果了,是不是更好一些那



5. v-for的key进行不要以遍历索引作为key


<template>
<ul>
<li v-for="(item,index) in mArr" :key="item.id
<input type="checkbox" :value="item.is" />
</li>
<button @click="remove">
移除
</button>
</ul>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{is:false,id:1},{is:false,id:2}
]
}
},
methods:{
remove(e){
console.log('asd')
this.mArr.shift()
}
}

}
</script>


  • 默认是都没有选中,当我们在页面中选中第一个


image.png



  • 点击下面的移除按钮,再看效果


image.png



  • 调整一下代码


<template>
<ul>
<li v-for="(item,index) in mArr" :key="index">
<input type="checkbox" :value="item.is" />
</li>
<button @click="remove">
移除
</button>
</ul>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{is:false,id:1},{is:false,id:2}
]
}
},
methods:{
remove(e){
console.log('asd')
this.mArr.shift()
}
}

}
</script>


还是选中状态,就很神奇,解释一下为什么这么神奇,因为我们选中的是0索引,然后点击移除后索引为1的就变为0,Vue的更新策略是复用dom,也就是说我索引为1的dom是用的之前索引为0的dom并没有更改,当然没有key的情况也是如此,所以key值必须为唯一标识才会做更改



6. SPA 页面采用keep-alive缓存组件


<template>
<div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>


使用了keep-alive之后我们页面不会卸载而是会缓存起来,keep-alive底层使用的LRU算法(淘汰缓存策略),当我们从其他页面回到初始页面的时候不会重新加载而是从缓存里获取,这样既减少Http请求也不会消耗过多的加载时间



7. 避免使用v-html




  1. 可能会导致xss攻击

  2. v-html更新的是元素的 innerHTML 。内容按普通 HTML 插入, 不会作为 Vue 模板进行编译 。



8. 提取公共代码,提取组件的 CSS



将组件中公共的方法和css样式分别提取到各自的公共模块下,当我们需要使用的时候在组件中使用就可以,大大减少了代码量



9. 首页白屏-loading



当我们第一次进入Vue项目的时候,会出现白屏的情况,为了避免这种尴尬的情况,我们在Vue编译之前使用加载动画避免



 
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Vue</title>
<style>

</style>
</head>
<body>
<noscript>
<strong>We're sorry but production-line doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div id="loading">
loading
</div>
</div>
<!-- built files will be auto injected -->
</body>
</html>



加loading只是解决白屏问题的一种,也可以缩短首屏加载时间,就需要在其他方面做优化,这个可以参考后面的案例



10. 拆分组件



主要目的就是提高复用性、增加代码的可维护性,减少不必要的渲染,对于如何写出高性能的组件这里就不展示了,自己可以多看看那些比较火的UI库(Element,Antd)的源码



11. 合理使用 v-if 当值为false时内部指令不会执行,具有阻断功能



如果操作不是很频繁可以使用v-if替代v-show,如果很频繁我们可以使用v-show来处理



key 保证唯一性 ( 默认 vue 会采用就地复用策略 )



上面的第五条已经讲过了,如果key不是唯一的情况下,视图可能不会更新。



12. 获取dom使用ref代替document.getElementsByClassName


mounted(){
console.log(document.getElementsByClassName(“app”))
console.log(this.$refs['app'])
}


document.getElementsByClassName获取dom节点的作用是一样的,但使用ref会减少获取dom节点的消耗



13. Object.freeze 冻结数据



首先说一下Object.freeze的作用




  • 不能添加新属性

  • 不能删除已有属性

  • 不能修改已有属性的值

  • 不能修改原型

  • 不能修改已有属性的可枚举性、可配置性、可写性


data(){
return:{
objs:{
name:'aaa'
}
}
},
mounted(){
this.objs = Object.freeze({name:'bbb'})
}


使用Object.freeze处理的data属性,不会被getter,setter,减少一部分消耗,但是Object.freeze也不能滥用,当我们需要一个非常长的字符串的时候推荐使用



14. 合理使用路由懒加载、异步组件



当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。然后当路由被访问的时候才加载对应组件,这样就更加高效了



// 未使用懒加载的路由
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";

Vue.use(VueRouter);

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About
}
];
// 使用懒加载
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const Home = () => import('../views/Home')
const About = () => import('../views/About')

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About
}
];

15. 数据持久化的问题



数据持久化比较常见的就是token了,作为用户的标识也作为登录的状态,我们需要将其储存到localStoragesessionStorage起来每次刷新页面Vuex从localStoragesessionStorage获取状态,不然每次刷新页面用户都需要重新登录,重新获取数据




  • localStorage 需要用户手动移除才能移除,不然永久存在。

  • sessionStorage 关闭浏览器窗口就失效。

  • cookie 关闭浏览器窗口就失效,每次请求Cookie都会被一同提交给服务器。


16. 防抖、节流



这两个算是老生常谈了,就不演示代码了,下面介绍一个场景,比如我们注册新用户的时候用户输入昵称需要校验昵称的合法性,考虑到用户输入的比较快或者修改频繁,这时候我们需要使用节流,间隔性的去校验,这样就减少了判断的次数达到优化的效果。后面我们还需要需要用户手动点击保存才能注册成功,为了避免用户频繁点击保存并发送请求,我们只监听用户最后一次的点击,这时候就用到了节流操作,这样就能达到优化效果



17. 重绘,回流



  • 触发重绘浏览器重新渲染部分或者全部文档的过程叫回流

    • 频繁操作元素的样式,对于静态页面,修改类名,样式

    • 使用能够触发重绘的属性(background,visibility,width,height,display等)



  • 触发回流浏览器回将新样式赋予给元素这个过程叫做重绘

    • 添加或者删除节点

    • 页面首页渲染

    • 浏览器的窗口发生变化

    • 内容变换





回流的性能消耗比重绘大,回流一定会触发重绘,重绘不一定会回流;回流会导致渲染树需要重新计算,开销比重绘大,所以我们要尽量避免回流的产生.



18. vue中的destroyed



组件销毁时候需要做的事情,比如当页面卸载的时候需要将页面中定时器清除,销毁绑定的监听事件



19. vue3中的异步组件



异步组件与下面的组件懒加载原理是类似,都是需要使用了再去加载



<template>
<logo-img />
<hello-world msg="Welcome to Your Vue.js App" />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'

// 简单用法
const HelloWorld = defineAsyncComponent(() =>
import('./components/HelloWorld.vue'),
)
</script>


20. 组件懒加载


<template>
<div id="content">
<div>
<component v-bind:is='page'></component>
</div>

</div>
</template>

<script>

// ---* 1 使用标签属性is和import *---
const FirstComFirst = ()=>import("./FirstComFirst")
const FirstComSecond = ()=>import("./FirstComSecond")
const FirstComThird = ()=>import("./FirstComThird")

export default {
name: 'home',
components: {

},
data: function(){
return{
page: FirstComFirst
}
}

}
</script>


原理与路由懒加载一样的,只有需要的时候才会加载组件



21. 动态图片使用懒加载,静态图片使用精灵图



  • 动态图片参考图片懒加载插件 (github.com/hilongjw/vu…)

  • 静态图片,将多张图片放到一起,加载的时候节省时间


22. 第三方插件的按需引入



element-ui采用babel-plugin-component插件来实现按需导入



//安装插件
npm install babel-plugin-component -D
// 修改babel文件

module.exports = {
presets: [['@babel/preset-env', { modules: false }], '@vue/cli-plugin-babel/preset'],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'lodash',
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
},
'element-ui'
],
[
'component',
{
libraryName: '@xxxx',
camel2Dash: false
},
]
]
};

23. 第三方库CDN加速


//vue.config.js

let cdn = { css: [], js: [] };
//区分环境
const isDev = process.env.NODE_ENV === 'development';
let externals = {};
if (!isDev) {
externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'ant-design-vue': 'antd',
}
cdn = {
css: [
'https://cdn.jsdelivr.net/npm/ant-design-vue@1.7.2/dist/antd.min.css', // 提前引入ant design vue样式
], // 放置css文件目录
js: [
'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js', // vuejs
'https://cdn.jsdelivr.net/npm/vue-router@3.2.0/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/ant-design-vue@1.7.2/dist/antd.min.js'
]
}
}



module.exports = {
configureWebpack: {
// 排除打包的某些选项
externals: externals
},
chainWebpack: config => {
// 注入cdn的变量到index.html中
config.plugin('html').tap((arg) => {
arg[0].cdn = cdn
return arg
})
},

}
//index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- 引入css-cdn的文件 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>">
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 放置js-cdn文件 -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>" ></script>
<% } %>
<div id="app"></div>
</body>
</html>

最后



以上的优化方案不紧在代码层面起到优化而且在性能上也起到了优化作用,文章内容主要是从Vue开发的角度和部分通过源码的角度去总结的,文章中如果存在错误的地方,或者你认为还有其他更好的方案,请大佬在评论区中指出,作者会及时更正,感谢!



作者:摸鱼的汤姆
来源:juejin.cn/post/7116163839644663822
收起阅读 »

10 个超棒的 JavaScript 简写技巧

web
今天我要分享的是10个超棒的JavaScript简写方法,可以加快开发速度,让你的开发工作事半功倍哦。 开始吧! 1.合并数组 普通写法: 我们通常使用Array中的concat()方法合并两个数组。用concat()方法来合并两个或多个数组,不会更改现有的数...
继续阅读 »

今天我要分享的是10个超棒的JavaScript简写方法,可以加快开发速度,让你的开发工作事半功倍哦。


开始吧!


1.合并数组


普通写法:


我们通常使用Array中的concat()方法合并两个数组。用concat()方法来合并两个或多个数组,不会更改现有的数组,而是返回一个新的数组。请看一个简单的例子:


let apples = ['🍎', '🍏'];
let fruits = ['🍉', '🍊', '🍇'].concat(apples);

console.log( fruits );
//=> ["🍉", "🍊", "🍇", "🍎", "🍏"]


简写写法:


我们可以通过使用ES6扩展运算符(...)来减少代码,如下所示:


let apples = ['🍎', '🍏'];
let fruits = ['🍉', '🍊', '🍇', ...apples]; // <-- here

console.log( fruits );
//=> ["🍉", "🍊", "🍇", "🍎", "🍏"]


2.合并数组(在开头位置)


普通写法:
假设我们想将apples数组中的所有项添加到Fruits数组的开头,而不是像上一个示例中那样放在末尾。我们可以使用Array.prototype.unshift()来做到这一点:


let apples = ['🍎', '🍏'];
let fruits = ['🥭', '🍌', '🍒'];

// Add all items from apples onto fruits at start
Array.prototype.unshift.apply(fruits, apples)

console.log( fruits );
//=> ["🍎", "🍏", "🥭", "🍌", "🍒"]


简写写法:


我们依然可以使用ES6扩展运算符(...)缩短这段长代码,如下所示:


let apples = ['🍎', '🍏'];
let fruits = [...apples, '🥭', '🍌', '🍒']; // <-- here

console.log( fruits );
//=> ["🍎", "🍏", "🥭", "🍌", "🍒"]


3.克隆数组


普通写法:


我们可以使用Array中的slice()方法轻松克隆数组,如下所示:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
let cloneFruits = fruits.slice();

console.log( cloneFruits );
//=> ["🍉", "🍊", "🍇", "🍎"]


简写写法:


我们可以使用ES6扩展运算符(...)像这样克隆一个数组:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
let cloneFruits = [...fruits]; // <-- here

console.log( cloneFruits );
//=> ["🍉", "🍊", "🍇", "🍎"]


4.解构赋值


普通写法:


在处理数组时,我们有时需要将数组“解包”成一堆变量,如下所示:


let apples = ['🍎', '🍏'];
let redApple = apples[0];
let greenApple = apples[1];

console.log( redApple ); //=> 🍎
console.log( greenApple ); //=> 🍏


简写写法:


我们可以通过解构赋值用一行代码实现相同的结果:


let apples = ['🍎', '🍏'];
let [redApple, greenApple] = apples; // <-- here

console.log( redApple ); //=> 🍎
console.log( greenApple ); //=> 🍏


5.模板字面量


普通写法:


通常,当我们必须向字符串添加表达式时,我们会这样做:


// Display name in between two strings
let name = 'Palash';
console.log('Hello, ' + name + '!');
//=> Hello, Palash!

// Add & Subtract two numbers
let num1 = 20;
let num2 = 10;
console.log('Sum = ' + (num1 + num2) + ' and Subtract = ' + (num1 - num2));
//=> Sum = 30 and Subtract = 10


简写写法:


通过模板字面量,我们可以使用反引号(``),这样我们就可以将表达式包装在${…}`中,然后嵌入到字符串,如下所示:


// Display name in between two strings
let name = 'Palash';
console.log(`Hello, ${name}!`); // <-- No need to use + var + anymore
//=> Hello, Palash!

// Add two numbers
let num1 = 20;
let num2 = 10;
console.log(`Sum = ${num1 + num2} and Subtract = ${num1 - num2}`);
//=> Sum = 30 and Subtract = 10


6.For循环


普通写法:


我们可以使用for循环像这样循环遍历一个数组:


let fruits = ['🍉', '🍊', '🍇', '🍎'];

// Loop through each fruit
for (let index = 0; index < fruits.length; index++) {
console.log( fruits[index] ); // <-- get the fruit at current index
}

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


简写写法:


我们可以使用for...of语句实现相同的结果,而代码要少得多,如下所示:


let fruits = ['🍉', '🍊', '🍇', '🍎'];

// Using for...of statement
for (let fruit of fruits) {
console.log( fruit );
}

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


7.箭头函数


普通写法:


要遍历数组,我们还可以使用Array中的forEach()方法。但是需要写很多代码,虽然比最常见的for循环要少,但仍然比for...of语句多一点:


let fruits = ['🍉', '🍊', '🍇', '🍎'];

// Using forEach method
fruits.forEach(function(fruit){
console.log( fruit );
});

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


简写写法:


但是使用箭头函数表达式,允许我们用一行编写完整的循环代码,如下所示:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
fruits.forEach(fruit => console.log( fruit )); // <-- Magic ✨

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


8.在数组中查找对象


普通写法:


要通过其中一个属性从对象数组中查找对象的话,我们通常使用for循环:


let inventory = [  {name: 'Bananas', quantity: 5},  {name: 'Apples', quantity: 10},  {name: 'Grapes', quantity: 2}];

// Get the object with the name `Apples` inside the array
function getApples(arr, value) {
for (let index = 0; index < arr.length; index++) {

// Check the value of this object property `name` is same as 'Apples'
if (arr[index].name === 'Apples') { //=> 🍎

// A match was found, return this object
return arr[index];
}
}
}

let result = getApples(inventory);
console.log( result )
//=> { name: "Apples", quantity: 10 }


简写写法:


上面我们写了这么多代码来实现这个逻辑。但是使用Array中的find()方法和箭头函数=>,允许我们像这样一行搞定:


// Get the object with the name `Apples` inside the array
function getApples(arr, value) {
return arr.find(obj => obj.name === 'Apples'); // <-- here
}

let result = getApples(inventory);
console.log( result )
//=> { name: "Apples", quantity: 10 }


9.将字符串转换为整数


普通写法:


parseInt()函数用于解析字符串并返回整数:


let num = parseInt("10")

console.log( num ) //=> 10
console.log( typeof num ) //=> "number"


简写写法:


我们可以通过在字符串前添加+前缀来实现相同的结果,如下所示:


let num = +"10";

console.log( num ) //=> 10
console.log( typeof num ) //=> "number"
console.log( +"10" === 10 ) //=> true


10.短路求值


普通写法:


如果我们必须根据另一个值来设置一个值不是falsy值,一般会使用if-else语句,就像这样:


function getUserRole(role) {
let userRole;

// If role is not falsy value
// set `userRole` as passed `role` value
if (role) {
userRole = role;
} else {

// else set the `userRole` as USER
userRole = 'USER';
}

return userRole;
}

console.log( getUserRole() ) //=> "USER"
console.log( getUserRole('ADMIN') ) //=> "ADMIN"


简写写法:


但是使用短路求值(||),我们可以用一行代码执行此操作,如下所示:


function getUserRole(role) {
return role || 'USER'; // <-- here
}

console.log( getUserRole() ) //=> "USER"
console.log( getUserRole('ADMIN') ) //=> "ADMIN"


补充几点


箭头函数:


如果你不需要this上下文,则在使用箭头函数时代码还可以更短:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
fruits.forEach(console.log);


在数组中查找对象:


你可以使用对象解构和箭头函数使代码更精简:


// Get the object with the name `Apples` inside the array
const getApples = array => array.find(({ name }) => name === "Apples");

let result = getApples(inventory);
console.log(result);
//=> { name: "Apples", quantity: 10 }


短路求值替代方案:


const getUserRole1 = (role = "USER") => role;
const getUserRole2 = role => role ?? "USER";
const getUserRole3 = role => role ? role : "USER";


编码习惯


最后我想说下编码习惯。代码规范比比皆是,但是很少有人严格遵守。究其原因,多是在代码规范制定之前,已经有自己的一套代码习惯,很难短时间改变自己的习惯。良好的编码习惯可以为后续的成长打好基础。下面,列举一下开发规范的几点好处,让大家明白代码规范的重要性:



  • 规范的代码可以促进团队合作。

  • 规范的代码可以减少 Bug 处理。

  • 规范的代码可以降低维护成本。

  • 规范的代码有助于代码审查。

  • 养成代码规范的习惯,有助于程序员自身的
    作者:AK_哒哒哒
    来源:juejin.cn/post/7105967944613494792
    成长。

收起阅读 »

吃亏就是吃亏,福在何处?

老话说得好:吃亏是福。 老话(我也不知道是谁)还举个了例子: 小亏和小赚都是公司的员工。有一个升职的名额,老板把名额给了小亏,原因是小亏喜欢吃亏。老板说:领导岗位,得有胸怀,能吃亏,老想赚别人便宜,这种人当不了领导。 作为论据,黑白对调也不会有违和感: ...
继续阅读 »

老话说得好:吃亏是福


老话(我也不知道是谁)还举个了例子:



小亏和小赚都是公司的员工。有一个升职的名额,老板把名额给了小亏,原因是小亏喜欢吃亏。老板说:领导岗位,得有胸怀,能吃亏,老想赚别人便宜,这种人当不了领导。



作为论据,黑白对调也不会有违和感:



老板选择了给小赚升职,他说:小亏吃亏的性格,会让公司的经营处于劣势,激烈的市场竞争需要野性,利润哪有嫌多的,还吃亏?因此,选择小赚更合适。



这种故事可以随意编,讲得通就可以。编故事的人,无非就是想让因果符合自己的观点


实际上,这类故事的逻辑漏洞很多。这都是辩论赛的把戏。


那么,我们扔掉故事,从现实进行分析。


生物具有学习和记忆的能力,他会根据当前反馈,决定下一次的行为


一个幼儿,触摸到尖锐的物体,他会感觉到疼痛,以后就会躲避;如果触摸到柔软的物体,他会感觉到舒适,以后就会戏耍。


今天你对这件事的反应,会决定明天别人怎么对待你


如果你网购买到次品,不退货,选择吃亏。那么之后,大数据会瞄准你。因为,你可以接受这类商品。你能接受就给你,他不能接受就不给他。这个没毛病。


如果公司里分奖品不够,你说我不要了,留给别人吧,选择吃亏。那么下次,还不够的时候,多半还是你没有。因为,大家都知道你不在乎奖品。其他人没有,闹着上吊呢?


我们平时从教科书里读到的结局和现实不一样,故事里都是:上次给的是假货,我没有计较,下次商家给了两份以示歉意;上次分东西我没要,我高风亮节,领导不但给补上了奖品,还送给了我一面锦旗。


然而事实,多数并非如此。因为,吃亏其实就是秀下限,是在告诉别人,你不抵触什么


管理者一直教我们吃亏是福,是品质,但是他们却不愿意吃亏。


就像我们一直很喜欢老实的人,但是自己不愿意当个老实的人。另外,老实人和好人(《做好人的意义》)也是有区别的。


因为老实意味着具备可靠的利用性,当然我自己不能被别人利用


大家都想要一群老黄牛,但是谁都不愿意当这头牛。


人的差异,在于资源的多少。吃亏让资源丢失,还秀下限,真不知福从哪里来。


因此,吃亏不是福。


上面所说的吃亏,不管是买油条给个断茬的,还是买包子给了个昨儿剩下的,这都是日常生活的小亏。


但是,还有一类智慧深远的人,他们很愿意吃“大亏”。比如赚到钱,他全分给弟兄们,自己一分不留。历史上很多这样的人,比如《后汉书·董卓列传》记载,董卓宰了自己家的耕牛招待陌生客人。耕牛在农耕社会对于农户来说,那可是极其重要啊。客人感动地稀里哗啦,吃完了又给他赶来上千头牛。


他可不是傻到家了。因为他也知道,今天这事,会决定明天别人怎么对你。今天让兄弟们感觉跟着自己有肉吃,兄弟们纷纷臣服,最后你成了大哥,坐拥天下。其实,这个小技巧老子(我说的是李耳)早就发现了,因此他《道德经》里说:因其不为大,故能成其大。翻译一下就是:因为他不拿大头,反而大头都成了他的。


但是,像端茶倒水、打印复印、搬家具啥的,你就别去吃亏,时间长了,别人可能把你当成保姆了。


但是,“吃亏是福”这句话,也能继续用。因为,多数场景下,我们还是可以将故事圆起来的。比如,你虽然丢了一百块钱,但是你同事崴着脚了。你会说,这叫:破财免灾,吃亏是福。

作者:TF男孩
来源:juejin.cn/post/7176405836787351613

收起阅读 »

菜鸡也会有春天吗III

转眼马上要毕业三年了,也是在小砖厂搬砖头的第三个年头,时间过得是真快,租的房子马上到期了,今天刚帮舍友搬完家,每次搬家都很emo。去年这个时候换房子正好遇到大学实验室同学东哥,我俩都是20年毕业的,东哥在毕业之后就一直在实验室老师的公司发展,去年老师有北京的项...
继续阅读 »

转眼马上要毕业三年了,也是在小砖厂搬砖头的第三个年头,时间过得是真快,租的房子马上到期了,今天刚帮舍友搬完家,每次搬家都很emo。去年这个时候换房子正好遇到大学实验室同学东哥,我俩都是20年毕业的,东哥在毕业之后就一直在实验室老师的公司发展,去年老师有北京的项目,东哥就机缘巧合下来到了北京。


有句话叫曾经沧海难为水,来了北京之后由于薪资待遇各方面东哥都不是很满意,于是就从老师那离职了,举目无亲之下就跟我凑合着租了一间主卧。仿佛又回到了在大学实验室的日子,当年我俩的位置也挨着。。。。


故事的开始总是那么和谐,后面因为一些工作节奏上的不同有些不兼容,好在时间还是过的很快,一年时间就这么匆匆过去,前篇有两篇记录了大学生活的文章,这次借着搬家机会回忆下自己短暂的北漂生涯。


学业末尾


image.png


这张照片应该是大四上学期结束拍的,那时候感觉自己身陷囹圄,对未来充满了恐惧和焦虑,实验室王老师可谓精通各种pua手段,全方位打击实验室同门出去找工作的念头。我们那个实验室本质上是给王老师写外包项目的组织,好听点叫实验室,其实就是个黑作坊。


那时候的心态是偷偷面试,怕老师知道,其实根本原因还是不自信,怕离开了实验室又没拿到offer,老师自己也有公司,虽然是个很小很小的外包公司,但是起码是个保底的底牌,实在不行,还能跟着老师干。但就是因为这个想法,导致两头都没搞好。


这里奉劝一下有跟我类似经历的同学们,当断则断,有些时候想要左右逢源结果可能是两头都拉裤兜子,虽然破釜沉舟这四个字听起来很冠冕堂皇,但关键时候还是要有魄力。


印象大四上学期去面试了蘑菇街,当时八股文一点没背,以为自己在实验室写了两句增删改查,打过ACM拿过铜牌,过个面试岂不轻松拿捏。结果问一个不会问一个不会,给面试官问笑了都。


从那之后从淘宝店打印了一本Java核心知识的pdf,每天复习,天天就是背八股,那会的内心是非常挣扎的,偷摸复习效率不高,而且身边的同门们都沉浸在老师画的大饼里无法自拔,包括东哥也是,那会感受到一种从未有过的无力感。


这时候想起了腿哥,我跟腿哥一起进入的实验室,我选的Java后端方向,他选的爬虫方向,由于对老师实验室没有啥实质性的价值,因为老师接不到一些爬虫的外包项目嘛,于是就把腿哥发配到边疆了,王老师有个师弟,在北京开了家小互联网公司,直接把腿哥保送到那里去了,毕业后一月8k。


说实话,给哥们羡慕坏了,在北京,还能学到本事,我们在实验室老师每月给600的补助,时不时还拖欠。


因为我学校是个末尾普本学校,身边人大部分选择了三个方向,考研、考公、考编或者考教师,我们班像我一样找研发工作的一个没有,弄得我想跟别人沟通的机会都没有。


很快秋招来了,金九银十,那段时间的节奏就是,1、找机会 2、做笔试 3、笔试挂或者没后续,四处碰壁的我在牛客发了一篇求offer的帖子。


image.png


看着这么多小伙伴也挣扎在水深火热中,让我稍微释怀了下。


至暗时刻


很快情况迎来了转机,大四上学期结束,疫情肆虐全国,学校通知暂缓返校,具体返校时间听通知。那机会这不就来了。不用在老师眼皮底下了,不用偷偷摸摸的了。


每天的生活节奏变成了吃饭、复习、面试,春招的发挥明显比秋招好了很多,很多公司的笔试都顺利进入面试了,但是面试却又过不去,要么倒在一面上,要么倒在二面上,虽然每次面试都有积累,但架不住一直面试失败,曾经一度怀疑自己是个傻逼,以至于有些公司发来面试邀请第一反应是肯定面不过去,那段时间绝对算是人生的至暗时刻。


虽然在拉钩上面过了几家小公司,但就觉得很不服气,感觉自己明明值得好公司,为什么总是事与愿违,拿到offer也不愿意签,都是一些小的不能再小的小作坊。


实验室的另一个同门史总,也想出来找找机会,那会我俩没事就qq语音聊面试的事,但是那年因为疫情的原因吧,机会的确不多,挣扎了差不多一个多月,史总选择了妥协,选择了跟着老师干。


这时候老师还给我打电话逼我就范,大体意思是今年行情太差了,你要自己出去找肯定完蛋,赶紧跟我干吧,我这名额也不多,你要答应晚了我就不要了!


当时恨不得骂他两句,我就不信靠我自己找不到工作了还!


曙光初现


某天下午,我在老家下地帮忙干农活呢(说起来很喜欢在田间农作,干活的时候面朝黄土背朝天,心无杂念,烦心事都抛在脑后内心很平静)。接到了现在这家厂子的面试,要说面试,三分实力七分运气,真的是这样,有时候跟面试官聊的很投机就过了,甚至都没有聊很多技术相关的东西,或者说还没到军火展示的环节就决定了给你过还是不过。


一面我印象做了一道简单的二分查找的算法,当时看过一段时间kotlin,IDEA上装了一些插件被面试官看到了,瞬间来了兴致,聊了很多语言方面的东西,随后就问了问一些常见八股就过了。


二面直接就聊规划聊发展,甚至聊最近看了什么书,当时脸不红心不跳的吹了波牛逼,什么数学之美、浪潮之巅、人月神话啥高端说什么,心里想着千万别问我有什么心得。


image.png
终于拿到了人生中第一个offer,一家做大数据的公司,薪资各方面也很满意,当时签完offer后很冷静,没有想象中的热泪盈眶之类的,有的只是感慨吧,感慨自己明明也不差,为什么求职之路如此多艰呢。


跟HR聊完具体入职时间,等待入职的日子算是继高考后最放松的日子了,终于不用背枯燥乏味的八股文了,终于可以安心打游戏了,终于不用被老师阴阳怪气了。。。。终于可以去梦寐以求的大城市了!


开始北漂


这三年有很多人问我当时为啥选择北京,其实与其说我选择了北京,不如说北京选择了我。


2020年5月25号,曲阜东站到北京南站,当时我那一节车厢总共没有几个人,大家因为疫情都不敢出门,颇有点明知山有虎,偏向虎山行的意思


到了北京南站,琳琅满目的地铁线标识,我记得特别清楚,当时拎着两个大行李箱,一时找不到去做地铁的地方,然后有个负责指挥交通的大妈,大家应该很常见,带着小红帽穿着黄马甲,配个小音箱,搁那嗷嗷的喊"快点快点,快关门了"。


我想走扶梯,但是大妈非不让我走,说带行李箱的坐直梯,当时人又多根本听不到她说啥,反正意思就是不让做扶梯,我提溜着俩大箱子有点不知所措,我寻思这大妈不让坐扶梯,我绕开她,去另个扶梯口坐不完了嘛,结果去了另一个扶梯口也不让进,但是我听明白啥意思了。


妈的当时我胳膊累的一点劲没有,还得去找直梯,你倒是领我去啊。灰头土脸的去沙河找腿哥凑合了一晚。第二天去租房,租房大家一定要提前做好攻略,中介都不是啥好东西,净找软柿子捏,租之前房间各个东西拍好照,很多本来就是坏的,到时候他会讹你。


第一次租房的体验特别差,合租舍友是个跑滴滴的,经常凌晨两三点、三四点回来,我那房间挨着厨房,他每次回来还要自己做饭,又吵又脏,呼呼的蟑螂。。。。


好在跟腿哥是一个小区,周末没事我就去找他,能给疲乏的生活一点慰藉吧,一度不想回屋子。偶尔找腿哥转一转北京散散心。


87377d32ly1gngkkpingbj21400u0n8e.jpg


工作上刚开始压力也是拉的满满的,好几次晚上1点、12点回家,虽然当时很累,但是没有过要放弃的念头。


image.png


那时候总是希望可以通过自己的努力实现阶级跨越,从底层阶级跨越到中产阶级,现在俨然是被磨平了棱角。


心猿意马


来公司的这三年,每年金三银四都是我心猿意马的时候,因为拿完年终了嘛,总想找一些机会,去年的3月21号,公司裁员了,猝不及防的裁员,而且杀伐决断,当天签补偿当天走,当时得知裁员名单里没有自己的时候心情特别复杂,又开心又不开心,也好也不好。


裁员的第二天,我请假去望京面了一家做投资app的小公司,结果很顺利,offer也发了,薪资也很满意,但是因为公司体量的问题,比现在的公司还小,最终还是没去,只能说遗憾至今吧。那时候没想明白自己来北京是干嘛来了,还有一些大厂情节,想去大厂。


我现在想明白了,我就是来赚钱来了,谁给我钱多我跟谁干,反正我又不留在北京。


解甲归田


不知不觉在公司搬了三年砖,从沙河搬到了回龙观,有时候经常跟自己开玩笑,是不是就在一家公司从一而终了,又到了每年心猿意马的金三银四了,今年无论如何都要再找找机会,就把今年当做是最后一年。还是坚决贯彻落实,给的高就润。


今年是北京的第三年,算上今天刚租的房子,已经算是换了四个地方了。每次换房子心情都很复杂,这次尤为严重,感觉自己有点抗不住了,漂泊的滋味属实不好受,准备最后换这一次就回山东了。


立帖为证,等以后躺平了也能回忆回忆自己有段激情燃烧的岁月。


最后希望大家都有更好的发展~


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

他扔出一张对话截图:王总说的

其实,我还在纠结,到底是写,还是不写。 也罢,给大家留作一个茶余饭后的谈资吧。如有雷同,纯属胡扯。 我在一家公司从事AI开发。我的身份是一个无法再底层的一线小开发。 产品总监又在群里贴出了那张对话截图。那是产研中心的王总跟他的对话,大体意思是王总要他组织一个同...
继续阅读 »

其实,我还在纠结,到底是写,还是不写。


也罢,给大家留作一个茶余饭后的谈资吧。如有雷同,纯属胡扯。


我在一家公司从事AI开发。我的身份是一个无法再底层的一线小开发。


产品总监又在群里贴出了那张对话截图。那是产研中心的王总跟他的对话,大体意思是王总要他组织一个同合作方的会议。


这个产品总监在这个群里,已经是第三次贴出这张截图了


第一次,他说要开一个公司层面的会。总监们纷纷发问:意义何在?


产品总监扔出这张截图:王总说的。


第二次,产品部的兄弟们有疑惑:开这个会的目的是什么?


产品总监第二次扔出这张截图:王总说的。


第三次,产品总监让我主讲这个会议。


我愣了:开会的目的是什么?


产品总监第三次扔出这张截图,表示:王总说的。


我说:不管哪个总说的,我都感觉意义不大。我可预料到过程,我说你们真好,对方说感谢夸奖,然后结束会议。


这个事情,还得从我的工作说起。我不仅从事AI项目的自研工作,同时也参与一些同第三方厂商的对接工作。因为公司的AI开发就我一个人。有些基础的AI能力,我们就打算买入第三方的服务。


因此,我的领导就安排我做调研工作。我先是调研国内AI行业的巨头。再调研垂直领域头部企业的AI开放能力。调研结束后,我将结果汇总,形成分析报告。然后,通过会议的形式,我把结果给直接领导讲了一遍。后来,直接领导拉来组内的产品,我又原样讲了第2遍。再后来,我的直接领导预约了他的直接领导,也就是我的二级领导,我又原样讲了第3遍。再后来,二级领导拉来公司所有的产品,我又原样讲了第4遍


再后来,已经不知是哪级领导,预约了给王总汇报,又安排我再原样讲第5遍。王总打断了我,他说,我不想知道你调研了多少家,以及哪家好哪家差,你辛苦那是你的工作,我不care这个。我想问,你的规划是什么?成本是多少?你将应用到哪些场景?能解决什么问题?创造多少收益?


同志们,记着我开头的声明,我是一个底层小员工。我最终还是没能回答上来。


会议结束之后,我和直接领导建议,是不是先让产品梳理一下我们到底有哪些AI需求。直接领导觉得,应该分两方面。第一,我们技术先自己想想,先按自己的想法走,这条路快。第二,慢慢地渗透给产品,让他们梳理一下,到底哪些场景会用到哪些AI能力,这条路可能要慢一些。


后来,王总主动安排下来一项任务。王总找到一家垂直行业的AI能力平台,想知道我们能不能用,好不好用。


最终这事,还是落到我的头上。我就把清单上的每一个接口,都做了调用和解析,并且采用可视化的形式来呈现结果。


我将结果给直接领导汇报了一次。结果就是,这次王总找的厂商,确实不错,带有行业加持,效果比之前我们找的都要好。直接领导找来产品总监和二级领导,我又原样讲了一遍


我的直接领导这次很机智,他想让产品梳理一下我们的产品,到底哪些地方会用到哪些AI能力。语音的能力要不要用?图像的能力具体怎么用?以便于我们技术可以进一步分析这些能力,到底能不能为我们所用。


再后来,就出现开头那一幕,产品总监安排我,同合作厂商再讲第3遍我的分析报告。并且他再次声明,那是王总安排的。


于是我就回复道:



不管哪个总安排的,这个会议意义不大。我只能说,你们的接口确实不错,他们也只能回复感谢支持。然后,尴尬结束。



因为,我们的产品规划,到底哪里用AI,用哪些AI,现在还是个空。


产品总监听到这里,很生气。他连发3条消息:



第一:到底他们的接口符不符合我们的业务场景,不符合要让他们整改,让它们攻克


第二:这绝不是一个你好我好的过场会


第三:请知悉。



群里,安静了一会儿。


我说。好吧,那我就以我自己梳理的往上靠吧。


会上,依然是我主讲。我又把已经讲了2遍的内容,讲了第3遍。我把他们每一个接口都做了分析,我表示这比之前调研的接口,效果都要好。这确实也是事实。


但是,具体我们能用吗?确实得先有产品规划,我才能确定是否能用到。为了避免成为“你好我好的过场会”,对于他们无法实现的,我提出质疑,他们说下一个版本会改好。


我马上记录下来,并确认:咱们下一个版本会改好的,对吧?


说完这句话,我收到一条钉钉消息。


产品总监发来的:来自王总的提醒,并不是我们给他们钱了,还没合作呢,不要有质问的语气!


我一看,好家伙,王总也参会了。一般提前预约都约不到的王总,居然悄悄参会了


我讲完了。王总发言说:我是中途赶来的。我想说,咱们这批接口是真的很好。你们接口的开放,是行业之大幸,对推动行业振兴很有帮助。对方说,王总太客气了,通过和咱们的交流,我们也有很大收获,也学到了很多知识。


我心想:这不还是一个你好我好的过场会。


会议结束了。


过了一会儿,我领导的领导打来电话,询问了会议的情况。最主要还是我群里发的那条:不管哪个总说的,对我来说,这个会意义不大


领导安慰我说,我估计你也是话赶话,赶到那里了。我断定你没有什么坏心眼,也不会故意使坏


我当时懵了一下。不知道他们领导层之间,到底是谁把什么消息,传播成了什么,上升到了什么层面。无职业素养地蔑视领导?以道德败坏形式破坏战略合作?王总的紧急出现,到底是巧合还是听到了什么风声?


不过,这些都无所谓,我只是一个底层小职员。我的职场生命力是最强的。我去80%的公司都可以再次成为一个小职员。


纵观整个过程。我们发现,一个企业的中层管理对企业起着至关重要的作用


第一次会议,王总不关注调研细节,这是没问题的。一个老总如果关心员工是如何进行调研的,反而是不称职。第二次会议,王总为公司找到好的资源,希望加强沟通,安排开会促进交流,这也是值得肯定的。


但是,对于每一个中层管理者,却不能让高层和基层进行100%的信息交换。尤其是向领导转发员工的截图,或者向员工转发领导的截图。


我经常看很多中层做类似的转发:给老板发某某员工抱怨公司的话,给员工发老板嫌弃员工不加班的截屏。这种行为很像是友商派来的内鬼。


大多数情况下,一个职场人了解自己的直接领导需要什么,但是不会很了解领导的领导的领导需要什么。一个领导多数理解直接下属怎么想,但是无法理解下属的下属怎么想。


每一位中层管理者,不管是上传和下达,都要做一次信息的过滤和加工。比如领导抱怨员工不加班,中层需要做的不是转发,而是加紧工作计划,说要提前上线,让员工忙起来。你要一说就是老板要看加班,还排好张三加二四六,李四加一三五,那两头都得气疯了。


我的两个例子就是个反面教材。


其实,我不需要直接给王总汇报。我至多向上汇报两级(如果他们真的非要分那么多级)。某级领导结合王总的近期规划,甚至最近的心情,去做一次简要汇报。而对于同一件事情来说,如果一个基层员工参会的次数,远超过领导参会的次数,这可能是一个预警。它表示,中层管理者根本没有加工信息,完全走转发路线。


王总安排给产品总监的会议,后来我也发现其实是高层之间的会议。安排我去参加确实意义不大。让我主讲更不可取。因为我了解的信息太少了,哪个叫张总,哪个是孙总,他们之间是什么样的商业关系,他们相互间的地位如何。如果非让我参加,应当是提前打好招呼,并且把我安排到殿外侯旨,问我时我再回答。


这些,基本上都得需要中层管理者来考虑。


对于上传下达,能做好过滤和加工,这样的中层是伟大的。啥也不做,这样的中层很难成长。添油加醋,煽风点火,这样的中层不予评价。


我碰到的产品总监是个聪明人,不在以上之列。他从一开始往外放对话截图,其实就表明了态度:其实我也不想开这会,但是领导非要开,还安排给我,大家配合一下,就混过去了。


但是,走到我这儿,我却发了一个牢骚。我感觉,第一,你不愿开你就跟领导直说,愿意开就用心安排,那是你的直接领导。第二,你给我一个会议号就完了,你这不是让我配合,是完全转交给我了呀。


再反过来讲,这会议真的没有意义吗?来了好的业务资源,我们不该去把握住吗?怎么一件好事,最后落得人人都不爽的地步。我抢了你的钱,局面是我赢你输。但是,一个事情搞得大家都输的情况,也是很难的。


活,还是我干了,事儿我也惹了。我始终还是没能当成一个,传统意义上,让你开会你就开会,哪儿那么多废话的俗人。可能这世界很需要俗人。


最后,奉劝大家在公司少发表意见。尤其和领导沾边的言论。


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

怎么去选择一个公司?

一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。 那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。 企业文化和价值观 行业势头 工资待遇 公司规模 人才水平 企业文化和价值观 无...
继续阅读 »

一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。


那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。



  • 企业文化和价值观

  • 行业势头

  • 工资待遇

  • 公司规模

  • 人才水平


企业文化和价值观


无法适应企业文化和价值观的员工,注定会被边缘化,获取不到资源,直到被淘汰。而适应企业文化和价值观的员工,在公司做事情则更能够得心应手。


如何选择适合自己的企业文化和价值观


如果你打算在一个公司长期发展,可以试着找找里面的熟人,聊聊公司内部的做事风格,比如晋升、奖金、淘汰、组内合作、跨部门合作以及如何处理各种意外情况等,这样就能实际感受到企业的文化和价值观了,然后再根据自己的标准,判断是否适合自己。


行业势头


行业一般会有风口期、黄金发展期和下降期三个阶段。



  • 处于下降趋势的行业要慎重考虑。

  • 处于风口期的行业发展趋势还不是很明显,如果你之前从事的行业和新的风口相关,那么不妨试试;如果你对这些风口背后的行业不是很熟悉,那不妨等风口的势头明朗了,再做打算。

  • 处于黄金发展期的行业发展已经稳定,有成熟的盈利模式,在这样的行业中积累经验,会在行业的发展上升期变得越来越值钱。如果你对这些行业感兴趣,不妨考虑相关的公司。


工资待遇


工资待遇不仅仅包括固定工资,还有一次性收入、奖金、股票以及各种福利等。


很多新入职的员工会有一些的奖金,例如签字费、安家费等,这些是一次性的,有时还会附加”规定时间内不能离职”等约束条件。这部分钱的性价比比较低,但一般金额还不错。


奖金主要看公司,操作空间很大,它和公司的经营状况关联紧密,谈Offer时约定的数额到后面不一定能够兑现,尤其是这两年整个互联网行业都不景气,很多公司的奖金都“打骨折”甚至直接取消了。


其他福利一般包括商业医疗保险、年假、体检、补贴等,它和公司所在行业有关联,具有公司特色。


股票也是待遇中很重要的一部分,很多公司在签Offer时会约定一定数量的股票,但是会分四年左右结清,这需要考虑你能坚持四年吗?四年之后没有股票要怎么办?


公司规模


如果待遇和岗位差不多,建议优先选择头部大公司,这样你可以学到更多的经验,接触更有挑战的业务场景,让自己成长的更快。


如果你看好一个行业,那么需要努力进入这个行业的头部公司。


人才水平


一个公司的人才水平,决定了公司对人才的态度和公司内部合作与管理的风格。


举个例子,如果一个公司里程序员的水平都很一般,那么这个公司就更倾向于不相信员工的技术能力,并制定非常细致和严格的管理规范和流程,避免员工犯错。如果你的水平高,就会被各种管理规范和流程束缚住。同时,如果你发现与你合作的人的水平都很“感人”,你也需要调整自己的风格,让自己的工作成果能够适应公司普遍的水平。


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

模块化时,如何进行模块拆分?

本文内容根据 Android 开发者大会内容整理而得,详见:By Layer or Feature? Why Not Both?! Guide to Android App Modularization 问题 随着项目的逐步增多,项目中的类文件如何存放、组织...
继续阅读 »

本文内容根据 Android 开发者大会内容整理而得,详见:By Layer or Feature? Why Not Both?! Guide to Android App Modularization


问题



  1. 随着项目的逐步增多,项目中的类文件如何存放、组织逐步变成一个问题;

  2. 在模块化的过程中,如何拆分代码或者拆分代码仓库需要一个指导原则;


image.png


衡量模块化好坏的标准


衡量模块化好坏主要有三个层面:耦合程度、内聚程度以及模块的颗粒度。耦合是衡量模块相互依赖的程度,内聚衡量单个模块的元素在功能上的相关性,颗粒度是指代码模块化的程度。好的模块应做到以下几点:



  1. 低耦合:模块不应该了解其他模块的内部工作原理。 这意味着模块应该尽可能地相互独立,以便对一个模块的更改对其他模块的影响为零或最小。




  1. 高内聚:意味着模块应该仅包含相关性的代码。在一个示例电子书应用程序,将书籍和支付的代码混合在同一个模块中可能是不合适的,因为它们是两个不同的功能领域。





  1. 适中颗粒度:代码量与模块数量有恰当的比例,模块太多会产生一定的开销,模块太少达不到复用的效果。




按照层级(Layer)划分


层级是指 Android 官方架构指南中的三层架构,分别为 UI LayerDomain LayerData Layer



在小型项目中按照 Layer 进行分层是可以满足需求的,随着项目的增大,需要将更多功能拆分到更多的模块中,否则就会出现高耦合和低内聚的情况。


按照特性(Feature)划分


若是按照特性划分,每个模块中会包含相似/相同逻辑(如 Reviews),从而导致代码冗余。



模块间的依赖逻辑耦合严重,不利于独立开发。



同时使用分层和特性划分


可以同时结合分层与特性划分的优势,扬长避短。首先按照特性拆分,在拆分后的这个小的逻辑单元中再按照层级来分,这也是壳工程的一个基本思路。



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

97程序猿的自述

为什么突然想写文章了? 不要问,问就是因为无聊。(开玩笑) 1.确实是闲下来了,比刚开始工作的时候的埋头苦干,满身充实到现在闲下来之后开始迷茫 2.回头看,在一些职业生涯的选择上幸运过也不幸过。 3.希望把自己的一些经历写出来,提供给一些同在这条路上的同学们...
继续阅读 »

为什么突然想写文章了?


不要问,问就是因为无聊。(开玩笑)



1.确实是闲下来了,比刚开始工作的时候的埋头苦干,满身充实到现在闲下来之后开始迷茫


2.回头看,在一些职业生涯的选择上幸运过也不幸过。


3.希望把自己的一些经历写出来,提供给一些同在这条路上的同学们一些参考



个人标签



97、天蝎男、本四、科班、深漂、培训、后端、外包、大厂背书



本四、科班



院校:末流本科;




专业:计算机科学与技术




和常见的大学生一样,玩不断的游戏,端游:DNF、CF、大逃杀、英雄联盟等,手游:王者、吃鸡、狼人杀。通宵,逃课(可能少但是是相对而言)




在学校除了必修课程中有和计算机相关的C语言,R语言,汇编语言,算法,Java(最后成为一名JAVA开发工程师)等,仿佛程序员和我并没有挂钩的地方。




比较幸运的是我是19年毕业,当时江西师范类专业是最后一届教师资格证校内颁发的证书的,所以有信息技术的教师资格证,可能以后还有机会回老家教书。




个人认为在学校还是比较合格的,四六级、普通话、二级、连续的校级奖学金(一、二、三等都有)、优秀毕业生,优秀毕业设计等,虽然最后除了英语证书在互联网职场生涯仿佛没有什么帮助。



其实到现在这个年纪了,经常会有学校的老师推荐我给一些要出校园的学弟学妹指导,或者一些亲戚朋友考上大学的学校和专业做指导。如果有需要的话可以私信我沟通,还是比较热于与人沟通,和不同的人会有不同的收获。


实习or培训



校招路线对于普通院校来讲是不太现实的,没有好的公司会来学校开招聘会(还是会有一些本土企业,国企等)。


当然我也听有一些同事说过可以去别的学校参加招聘会,但实际上我并不认可,可以简单来讲,在大学并没有学到什么,没有核心竞争力。




当然起初大四出来实习的时候,准确的来说是大三暑假,学校要求去学校实习(师范类专业),两个选择,自己选学校去实习,或者学校随机安排去一些学校实习。


我选了自己去学校实习,但实际上我并没有去院校实习,而是来了深圳。我觉得大部分像我一样家里没矿,没厂也不吃国家粮的同学大部分都会这样选择。
18年7月,那是我第一次来深圳,当然比起许多独自深漂的大学生来讲,我是比较幸运的,爸妈从我上大学开始就已经从老家来到深圳做生意了,从刚开始的租房到二手房生意,有亏损有盈利,到现在疫情现状的基本上只能维稳。


至少来到深圳开始我就没有为租房而烦恼,也没有为吃不到家里的菜而觉得可怜。
刚开始从江西来到深圳的时候,因为家里有个叔叔是在阿里做技术专家(其实现在也不知道具体级别,只知道应该还是蛮高的职位的)虽然不是亲叔叔,还是有血缘关系的,小时候也是一起在老家,但自从去了上海之后就基本上两三年过年见一次了。


爸妈从刚从学校要安排工作实习我说来深圳的时候就一直和我说让我多问问他,看看有没有什么好的建议找个好工作。和叔叔打了很多个电话,也获得很多建议,一直都放在我的微信收藏夹里面,不知道什么时候删掉了qaq。当时给我的建议是先自学,因为我也比较实诚,在学校没学到就是没学到,也直说了。


兜兜转转,我试过了直接找工作,自学,最后工作无门才走上了培训,其实关于培训可能大部分人或者职场人都比较歧视。我也不想讲太多关于培训的事情,讲个大概就好了,一般都是半年左右,当然如果是转行之类的年龄比较大的可能会存在学习或者就业困难可能耗时会多上一到两个月,我是从18年8月到12月。培训后的第一个礼拜就找到工作,第一次入职的时间是12月20日。是一个外包公司,深圳某本,还算是比较大外包公司吧。



外包



关于人力外包,网上有很多不好的消息,很多人都会问,软件外包好不好之类的,说实话这个没有答案。如果你有好的工作机会你也不会去问软件外包好不好,工作了这么多年倒是明白了一个道理没有什么工作好不好的,只有钱多和钱少
大厂驻场开发,两年半,也是我目前最长的一份工作经历。


有一些我以前不太敢说的事情,现在也可以坦然面对了。我是幸运的,外包包装简历(写假的工作简历)成功入职,我清楚的记得那是第三次面试,一共二面,我以为一面我已经挂了,到后面被通知二面再到入职。


入职后我要装成四年工作经验的开发,不敢多问,问之前一定要搞清楚,确定什么该问什么不该问。我可以很自信的说,我工作上手后并不弱于工作多年的一些目前我认识只会写CURD的同事。


甚至于同事也不敢说真实年龄,明明是97,需要说成94。也碰到过一些很尴尬的事情。



某厂



其实在外包两年左右的时间开始我就已经提出了辞呈,面试了几家小公司(规模500人,开发团队10人左右),并且已经谈好了薪资准备入职,当时20年的时候,薪资也比较满意16K左右。于是就和本司(某本)说了之后在和leader沟通。其实一直都挺感谢我的leader,从入职对互联网什么都不懂,除了培训的一些基础操作外,到后面做骨干开发,小组长,转内部,虽然也有我努力,遇事愿意去做的原因。但如果换一个领导,可能并不会像现在这样有大厂背书,可以有更多的岗位考虑。




由于转内通道等一系列内部原因,大概花了半年时间才把整个流程走下来(这个事前leader也有和我沟通,意思就是现在有这么一个机会,你愿不愿意等),当时我其实还是蛮犹豫的,但为了背书我留了下来。甚至到后面leader们都离职了,我的流程也走下来了。




关于前公司的很多事情,其实还是蛮有意思的,不论是企业文化,还是和一些前同事,开发,测试,产品,项目经理等吃饭,打麻将。



我是幸运的,至少到目前来讲我认为。我时常和朋友讲,像我这个学历,这个工龄是不会被大厂给筛选过。


职场


tip:先讲一下大家比较感兴趣的吧


薪资
18年入职:10.5K*12


19年普调:11.6*12


20年普调+特批:14.7*12


21年转内 17.2 + 10(年终)


22年跳槽 23*14(ps:行情不好没有14了)



到目前工作第四年,不多不少刚好是第名义上的四家公司。没有换城市,一直在深圳。


也没有中间休息过,除了离职前把年假休掉外,基本上全勤。有时候真的很想直接辞职去旅旅游,说实话疫情的三年导致本来就没有旅游过的我更没有机会去玩,一直都觉得很可惜。


到现在年龄越来越大,家庭长辈的各方面的压力也越来越大,以后就更没有机会去玩了,从开始的不屑于去谈论更好的工资,离职不考虑没工资,到现在需要这么一份工作,不敢辞职,年龄越来越大真的越来越不像自己。



未完待续


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

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

你好呀,我是歪歪。 去年的最后一天,我在 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »