注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

如何实现比 setTimeout 快 80 倍的定时器?

起因 很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4m...
继续阅读 »

起因


很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard 规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:

let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。



更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?


探索


假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的 window.postMessage()



这篇文章里的作者给出了这样一段代码,用 postMessage 来实现真正 0 延迟的定时器:

(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessageaddEventListener('message') 的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:



全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。


作者设计了一个实验方法,就是分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试


实验代码:

function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过 setZeroTimeout 也就是 postMessage 版本来递归计数到 100,然后切换成 setTimeout 计数到 100。


直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。



Performance 面板


只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage 版的定时器和 setTimeout 版的定时器是如何分布的。



这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:

const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


通过本文,你大概可以了解如下几个知识点:



  1. setTimeout 的 4ms 延迟历史原因,具体表现。

  2. 如何通过 postMessage 实现一个真正 0 延迟的定时器。

  3. postMessage 定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。

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

搞懂Kotlin委托

1、委托是什么 委托是一种设计模式,它的基本理念是操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。也就是说在委托模式中,会有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。 委托模式中,有三个角色,约束、委托...
继续阅读 »

1、委托是什么


委托是一种设计模式,它的基本理念是操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。也就是说在委托模式中,会有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。


委托模式中,有三个角色,约束委托对象被委托对象


委托模式其实不难理解,生活中有很多类似的地方。假如你手里有一套房子想要租出去,想要把房子租出去,联系房客、带人看房是必不可少的,如果让你自己来进行前面所说的工作,可能会占用你自己的业余生活时间,所以这种时候就可以把这些事委托给中介处理,接下来你不需要自己联系房客,也不需要亲自带人看房子,这些工作都由中介完成了,这其实就是一种委托模式。在这里,约束就是联系房客、看房子等一系列操作逻辑,委托对象是你,也就是房主,被委托对象是中介。


在Kotlin中将委托功能分为两种:类委托属性委托


1.1、类委托


类委托的核心思想是将一个类的具体实现委托给另一个类去完成


以上面租房子的例子,我们使用Kotlin的by关键字亲自实现一下委托模式。


首先,我们来定义一下约束类,定义租房子需要的业务:联系房客、看房子。

// 约束类
interface IRentHouse {
// 联系房客
fun contact()
// 带人看房
fun showHouse()
}

接着,我们来定义被委托类,也就是中介。

// 被委托类,中介
class HouseAgent(private val name: String): IRentHouse {
override fun contact() {
println("$name中介 联系房客")
}
override fun showHouse() {
println("$name中介 带人看房")
}
}

这里我们定义了一个被委托对象,它实现了约束类的接口。


最后,定义委托类,也就是房主。

// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse by agent {
// 签合同
fun sign() {
println("房主签合同")
}
// 带人看房
override fun showHouse() {
println("房主带人看房")
}
}

这里定义了一个委托类HouseOwner,同时把被委托对象作为委托对象的属性,通过构造方法传入。


在Kotlin中,委托用关键字by,by后面就是委托的对象,可以是一个表达式。


测试:

fun main() {
val agent = HouseAgent("张三") // 初始化一个名叫张三的中介
val owner = HouseOwner(agent) // 初始化房主,并把“中介”介绍给他
owner.contact()
owner.showHouse()
owner.sign()
}

运行结果如下

张三中介 联系房客
房东带人看房
房主签合同

可以看到,在整个租房过程中,房主的一些工作由中介帮着完成,例如联系房客。如果房东心血来潮,觉得自己更理解自己的房子,想把原来委托给中介的工作自己处理,也可以自己来进行,例如带人看房。最后签合同只有房主能处理,所以这是房主独有的操作。以上就是一个委托的简单应用。


而这也是委托模式的意义所在,就是让大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法,那么房东租房的整个逻辑就能顺利进行了。


有的人可能会说,这样的话不用by关键字我也可以实现委托。如下:

// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse {
// 签合同
fun sign() {
println("房主签合同")
}
// 联系房客
override fun contact() {
agent.contact()
}
// 带人看房
override fun showHouse() {
println("房主带人看房")
}
}

运行结果如

张三中介 联系房客
房东带人看房
房主签合同

可以看到,与前面的输出结果一样。


但是这种写法是有一定弊端的,如果约束接口中的待实现方法比较少还好,如果有几十甚至上百个方法的话就会出现问题。


前面也说过委托模式的最大意义在于,大部分的委托类方法实现可以调用被委托对象中的方法。而既然使用委托模式,就说明委托类中的大部分的方法实现是可以通过调用被委托对象中的方法实现的,这样的话每个都像“联系房客”那样去调用被委托对象中的相应方法实现,还不知道要写到猴年马月,而且会产生大量样板代码,很不优雅。


所以Kotlin提供了关键字by,在接口声明的后面使用by关键字,再接上被委托的辅助对象,这样可以免去仅调用被委托对象方法的模版代码。


而如果要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利。


如果想加入独有的方法逻辑,直接写一个方法即可。


这几种情况在前面的“租房”场景中都有体现。


1.2、属性委托


属性委托的核心思想是将一个属性的具体实现委托给另一个类去完成


我们来看一下委托属性的语法结构

class Test {
// 属性委托
var prop: Any by Delegate()
}

可以看到,这里使用by关键字连接了左边的prop属性和右边的Delegate实例,这种写法就代表着将prop属性的具体实现委托给了Delegate类去完成


1.2.1、什么是属性委托

前面也说了属性委托是将一个属性的具体实现委托给另一个类去完成。那么属性把什么委托了出去,被委托类又有哪些实现呢?


其实,属性委托出去的是其set/get方法,委托给了被委托类的setValue/getValue方法。

// 属性的get/set方法
var prop: Any
get() {}
set(value) {}

// 委托后
var prop: Any by Delegate()

注意这里prop声明的是var,即可变变量,如果委托给Delegate类的话,则必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。

class Delegate {
private var propValue: String? = null

operator fun getValue(thisRef: Any, property: KProperty<*>): String? {
return propValue
}

operator fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
propValue = value
}
}

到这里,属性委托已经完成了,这时候,当你点开by关键字的时候会出现如下提示。



这就表明prop已经把具体实现委托给Delegate类完成。


当调用prop属性的时候会自动调用Delegate类的getValue()方法,当给prop属性赋值的时候会自动调用Delegate类的setValue()方法。


如果prop声明的是val,即不可变变量,则Delegate类只需要实现getValue()方法即可。


有些人第一次看到方法中的参数可能有点懵,但其实这是一种标准的代码实现样板,并且官方也提供了接口类帮助我们实现,具体的接口类下面会说到。


虽然是一套固定的样板,但我们也要理解其中参数的含义。



  • thisRef:用于声明该Delegate类的委托功能可以在什么类中使用。必须与 属性所在类 类型相同或者是它的父类,如果是扩展函数,则指的是扩展的类型。

  • property:KProperty是Kotlin中的一个属性操作类,可用于获取各种属性相关的值。多数情况下都不需要修改。

  • value:具体要赋值给委托属性的值,必须与getValue的返回值相同。


那么为什么要使用属性委托呢?


假如想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段field中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作委托给一个辅助对象(类),这个辅助对象就是被委托类。说白了,就是避免样板代码,防止出现大量重复逻辑。


1.2.2、ReadOnlyProperty/ReadWriteProperty接口

前面说到,如果要实现属性委托,就必须要实现getValue/setValue方法,可以看到getValue/setValue方法结构比较复杂,很容易遗忘,为了解决这个问题,Kotlin 标准库中声明了2个含所需operator方法的 ReadOnlyProperty/ReadWriteProperty 接口。

interface ReadOnlyProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

val属性实现ReadOnlyProperty接口,var属性实现ReadWriteProperty接口。这样就可以避免自己写复杂的实现方法了。

// val 属性委托实现
class Delegate1: ReadOnlyProperty{
private var propValue: String = "zkl"

override fun getValue(thisRef: Any, property: KProperty<*>): String {
return propValue
}
}
// var 属性委托实现
class Delegate2: ReadWriteProperty{
private var propValue: String? = null

override fun getValue(thisRef: Any, property: KProperty<*>): String? {
return propValue
}

override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
propValue = value
}
}

2、Kotlin标准库的几种委托


2.1、延迟属性 lazy


2.1.1、使用by lazy进行延迟初始化

使用by lazy()进行延迟初始化相信大家都不陌生,在日常使用中也能信手拈来。如下,是DataStoreManager对象延迟初始化的例子。

//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager(store) }

by lazy()代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当变量(instance)首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋值给变量。 调用如下:

fun main() {
println(instance::class.java.simpleName)
println(instance::class.java.simpleName)
println(instance::class.java.simpleName)
}

打印结果如下:

第一次调用时执行
DataStoreManager
DataStoreManager
DataStoreManager

可以看到,只有第一次调用才会执行代码块中的逻辑,后续调用只会返回代码块的最终值


那么什么时候适合使用by lazy进行延迟初始化呢?当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,就非常适合了。


当然,如果变量第一次初始化时抛出异常,那么lazy将尝试在下次访问时重新初始化该变量。


2.1.2、拆解by lazy

可能大家刚接触的时候会觉得by lazy本是一体使用的,其实不是,实际上,by lazy并不是连在一起的关键词,只有by才是Kotlin中的关键字,lazy只是一个标准库函数而已


那么就把二者拆开看,先点开by关键字

@kotlin.internal.InlineOnly
public inline operator fun Lazy.getValue(thisRef: Any?, property: KProperty<*>): T = value

会发现它是Lazy 类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy类中,这个value便是返回的值。

//惰性初始化类
public interface Lazy {

//懒加载的值,一旦被赋值,将不会被改变
public val value: T

//表示是否已经初始化
public fun isInitialized(): Boolean
}

接下来看一下lazy,这个就是一个高阶函数,用来创建lazy实例的。

public actual fun  lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer)

可以看到,该方法中会把initializer,也就是代码块中的内容,传递给SynchronizedLazyImpl类进行初始化并返回。大部分情况我们使用的都是这个方法。


当然我们也可以设置mode,这样会调用下面的lazy方法,该方法中会根据mode类型来判断初始化那个类。如下

public actual fun  lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

// 使用如下
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
println("第一次调用时执行")
DataStoreManager(store)
}

三个mode解释如下:



  • LazyThreadSafetyMode.SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全

  • LazyThreadSafetyMode. PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。

  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程


而第一个lazy不设置mode时默认的就是SYNCHRONIZED,也是最常用的mode,这里我们直接看一下对应类的代码:

//线程安全模式下的单例
private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable {
private var initializer: (() -> T)? = initializer
//用来保存值,当已经被初始化时则不是默认值
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
//锁
private val lock = lock ?: this

override val value: T
//见分析1
get() {
//第一次判空,当实例存在则直接返回
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
//使用锁进行同步
return synchronized(lock) {
//第二次判空
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
//真正初始化
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

//是否已经完成
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

这个单例就是双重校验锁实现的。


2.2、可观察属性


Kotlin除了提供了lazy函数实现属性延迟加载外,还提供了Delegates.observableDelegates.vetoable标准库函数来观察属性变化。先来看observable


2.2.1、observable函数
    public inline fun  observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty =
object : ObservableProperty(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}

可以看到,该标准库函数接收了两个参数initialValueonChange



  • initialValue:初始值

  • onChange:属性值变化时的回调逻辑。回调有三个参数:propertyoldValuenewValue,分别表示:属性、旧值、新值。


使用如下:

var observableProp: String by Delegates.observable("初始值") { property, oldValue, newValue ->
println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
}
// 测试
fun main() {
observableProp = "第一次修改值"
observableProp = "第二次修改值"
}

打印如下:

属性:observableProp 旧值:初始值 新值:第一次修改值 
属性:observableProp 旧值:第一次修改值 新值:第二次修改值

可以看到,当把属性委托给Delegates.observable后,每一次赋值,都能观察到属性的变化。


2.2.2、vetoable函数

vetoable函数与observable一样,都可以观察属性值变化,不同的是,vetoable可以通过代码块逻辑决定属性值是否生效。

    public inline fun  vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty =
object : ObservableProperty(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}

接收的两个参数与observable函数几乎相同,不同的是onChange回调有一个Boolean的返回值。


使用如下:

var vetoableProp: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
newValue > 0
}
// 测试
fun main() {
println("vetoableProp:$vetoableProp")
vetoableProp = 2
println("vetoableProp:$vetoableProp")
vetoableProp = -1
println("vetoableProp:$vetoableProp")
vetoableProp = 3
println("vetoableProp:$vetoableProp")
}

打印如下:

vetoableProp:0
属性:vetoableProp 旧值:0 新值:2
vetoableProp:2
属性:vetoableProp 旧值:2 新值:-1
vetoableProp:2
属性:vetoableProp 旧值:2 新值:3
vetoableProp:3

可以看到-1的赋值并没有生效。


那么具体的逻辑是什么呢?


回看observable和vetoable的源码可以发现,二者继承了ObservableProperty抽象类,不同的是observable重写了该类afterChange方法,vetoable重写了该类beforeChange方法,并且beforeChange会有一个Boolean的返回,返回的是我们自己写的回调逻辑的返回值。


那么接着看setValue逻辑

protected open fun beforeChange(property: KProperty<*>, oldValue: V, newValue: V): Boolean = true

protected open fun afterChange(property: KProperty<*>, oldValue: V, newValue: V): Unit {}

public override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}

可以看到会先执行beforeChange方法,如果beforeChange为false则直接返回,并且不会更新值,为true时才会更新值,接着执行afterChange方法。这里beforeChange方法默认返回true。


其实只要查看这些函数源码可以发现,其内部调用的都是代理类,所以说白了这些都是属性委托。


3、总结


委托在Kotlin中有着至关重要的作用,但是也不能滥用,中间毕竟多了一个中间类,不合理的使用不但不会有帮助,反而会占用内存。前面也说过类委托最大的意义在于,大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法。属性委托的意义在于,对于比较复杂的一些属性,它们处理起来比把值保存在支持字段field中更复杂,且它们的逻辑相同,为了防止出现大量模版代码,可以使用属性委托。所以在使用委托前,我们也可以按照上面的标准考虑一下,合理的使用委托可以减少大量样板代码,提高代码的可扩展性和可读性。


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

Gson与Kotlin"摩擦"的那件小事

大家好,本篇文章分享一下之前使用gson和kotlin碰撞出的一些火花,脑瓜子被整的懵懵的那种。 准备知识 总所周知,当Gson没有无参构造函数时,会使用UnSafe以一种非安全的方式去创建类的对象,这样会产生两个问题: 属性的默认初始值会丢失,比如某个类中...
继续阅读 »

大家好,本篇文章分享一下之前使用gson和kotlin碰撞出的一些火花,脑瓜子被整的懵懵的那种。


准备知识


总所周知,当Gson没有无参构造函数时,会使用UnSafe以一种非安全的方式去创建类的对象,这样会产生两个问题:



  1. 属性的默认初始值会丢失,比如某个类中有这么一个属性public int age = 100,经过unsafe创建该类对象,会导致age的默认值100丢失,变为0;




  1. 会绕过Kotlin的空安全检验,因为经过unsafe创建的对象不会在属性赋值时进行可null校验。


所以一般比较在使用Gson反序列化时,比较推荐的做法就是反序列化的类要有无参构造函数。


PS:其实提供了无参构造函数,还是有可能会绕过Kotlin空安全校验,毕竟在Gson中属性是通过反射赋值的,所以一些人会推荐使用Moshi,这个笔者还没怎么使用过,后续会了解下。


看一个脑瓜子懵的例子


先上代码:

class OutClass {
val age: Int = 555

override fun toString(): String {
return "OutClass[age = $age]"
}

inner class InnerClass {
val age1: Int = 897

override fun toString(): String {
return "InnerClass[age = ${this.age1}]"
}

}
}

以上两个类OutClassInnerClass看起来都有无参构造函数,现在我们来对其进行一一反序列化。


1. 反序列化OutClass

fun main(args: Array<String>) {
val content = "{"content": 10}"
val out = OutClass::class.java
val obj = Gson().fromJson(content, out)
println(obj)
}

反序列化使用的字符串是一个OutClass类不存在的属性content,咱们看下输出结果:



看起来没毛病,由于存在无参构造函数,且反序列化所使用的字符串也不包括age字段,age的默认值555得以保留。


2. 反序列化InnerClass


先上测试代码:

fun main(args: Array<String>) {
val content = "{"content": 10, "location": null}"
val out = OutClass.InnerClass::class.java
val obj = Gson().fromJson(content, out)
println(obj)
}

运行结果如下:



不是InnerClass也是有无参构造函数的吗,为啥age字段的默认值897没有被保留,当时给整蒙了。


于是进行了下debug断点调试,发现最终是通过Unsafe创建了InnerClass



当时是百思不得其解,后续想了想,非静态内部类本身会持有外部类的引用,而这个外部类的引用是通过内部类的构造方法传入进来的,咱们看一眼字节码:



所以非静态内部类根本就没有无参构造方法,所以最终通过Gson反序列化时自然就是通过Unsafe创建InnerClass对象了。


如果想要解决上面这个问题,将非静态内部类改成静态内部类就行了,或者尽量避免使用非静态内部类作为Gson反序列化的类。


另外大家如果感兴趣想要了解下Gson是如何判断的反射无参构造方法还是走Unsafe创建对象的,可以看下源码:


ReflectiveTypeAdapterFactory#create ——>ConstructorConstructor#get


介绍下typeOf()方法


回忆下我们之前是怎么反序列化集合的,看下下面代码:

fun main(args: Array<String>) {
val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
val obj = Gson().fromJson<List<OutClass>>(content, object : TypeToken<List<OutClass>>(){}.type)
println(obj)
}

要创建一个很麻烦的TypeToken对象,获取其type然后再进行反序列化,输出如下正确结果:



为了避免麻烦的创建TypeToken,我之前写了一篇文章来优化这点,大家感兴趣的可以看下这篇文章:Gson序列化的TypeToken写起来太麻烦?优化它


然后之前有个掘友评论了另一个官方提供的解决方法:



于是我赶紧试了下:

@OptIn(ExperimentalStdlibApi::class)
fun main(args: Array<String>) {
val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
val obj = Gson().fromJson<List<OutClass>>(content, typeOf<List<OutClass>>().javaType)
println(obj)
}

运行输出:



没毛病,这个写法要比创建一个TypeToken简单多了,这个api是很早就有了,不过到了kotlin1.6.0插件版本才稳定的,请大家注意下这点:



十分推荐大家使用这种方式,官方支持,就突出一个字:稳。


总结


本篇文章主要是给大家介绍了Gson反序列化非静态内部类时的坑,以及介绍了一个官方支持的api:typeOf(),帮助大家简化反序列化集合的操作,希望本篇文章能对比有所帮助。


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

Android-我对代理模式的理解

以下业务场景不大现实,我这里只是提供一种思路 想象一种场景:有一天,产品经理让你记录某些地方的行为日志并且存储到本地方便查阅,你可能会写下如下代码:interface ILogger { fun logInfo(action: String) ...
继续阅读 »

以下业务场景不大现实,我这里只是提供一种思路

想象一种场景:有一天,产品经理让你记录某些地方的行为日志并且存储到本地方便查阅,你可能会写下如下代码:

interface ILogger {

fun logInfo(action: String)

fun logError(action: String)
}


class Logger : ILogger{

override fun logInfo(action: String) {
//存储到本地
saveToLocalFile(action)
}

override fun logError(action: String) {
//存储到本地
saveToLocalFile(action)
}

private fun saveToLocalFile(action: String) {}
}

当需要调用的时候:

val logger: ILogger = Logger()
logger.logError("出现问题")

当然了,你更大概率是考虑用一个单例类直接调用,而不是每次都这样写。

假如某天换了个产品经理,要求你在这些存储日志之前,先将日志上传到服务器,存储日志后,做一个埋点记录

class Logger : ILogger{

override fun logInfo(action: String) {
//上传到服务器
upLoadToCloud(action)
//存储到本地
saveToLocalFile(action)
//埋点
eventTracking(action)
}

override fun logError(action: String) {
//上传到服务器
upLoadToCloud(action)
//存储到本地
saveToLocalFile(action)
//埋点
eventTracking()
}

private fun saveToLocalFile(action: String) {}

private fun upLoadToCloud(action: String) {}

private fun eventTracking() {}
}

设计模式讲究一个职责单一,那么以上代码最直观的就是不同的功能耦合在一起。


什么是代理模式


一句话解释就是:在不改变原有功能的基础上,通过代理类扩展新的功能,使得功能之间解耦,或者框架和业务之间解耦,有点装饰器模式的味道。


静态代理

interface ILogger {

fun logInfo(action: String)

fun logError(action: String)
}

class Logger : ILogger{

override fun logInfo(action: String) {
//存储到本地
saveToLocalFile(action)
}

override fun logError(action: String) {

//存储到本地
saveToLocalFile(action)
}

private fun saveToLocalFile(action: String) {}

}

class LoggerProxy(val logger: Logger) : ILogger {

override fun logInfo(action: String) {
//上传到服务器
upLoadToCloud(action)
//通过传进来的logger对象来调用原来的实现方法
logger.logInfo(action)
//埋点
eventTracking(action)
}

override fun logError(action: String) {
//上传到服务器
upLoadToCloud(action)
//通过委托logger对象来调用原来的实现方法
logger.logError(action)
//埋点
eventTracking(action)
}

private fun upLoadToCloud(action: String) {}

private fun eventTracking(action: String) {}
}

//使用方式
val logger: ILogger = LoggerProxy(Logger())
logger.logError("出错了")

在第25行,我们新添加了一个新的LoggerProxy代理类同样的实现了ILogger接口,在两个方法中,我们按顺序完成了功能的调用,将上传到服务器和埋点的逻辑和存储到本地的逻辑进行了分离,代理类LoggerProxy在业务的执行前后附加了其他的逻辑。

看到这你可能会觉得,有点脱裤子放屁了。确实,当前代码量特别小,对于当前代码体现的可能不太明显,如果你正在一个设计相对大型的框架,业务和框架代码的分离显得就相对重要了。

作为一种设计思想,他提供的是一种思路,让你写出来的不是面向过程的代码,有好有坏,当然在实际项目中不要为了设计模式而设计模式,不然就适得其反了,写出来的代码可读性差。


动态代理


对于静态代理,上面的代码中我们在代理类中的前后加了两个不同的功能,这两个相对职责不同的功能耦合在了一起,我由于偷懒没将其中的一个功能拆走,正常情况是应该再写一个代理类去做相同的一部分操作,如果功能更多的话就要写更多的代理类,繁琐度可想而知。

再一个,静态代理是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。而动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。

class Logger : ILogger{

override fun logInfo(action: String) {
println("存储到本地: $action")
saveToLocalFile(action)
}

override fun logError(action: String) {
println("存储到本地: $action")
saveToLocalFile(action)
}

private fun saveToLocalFile(action: String) {}

}


class LoggerProxy(private val target: ILogger): InvocationHandler {

fun createProxy() = Proxy.newProxyInstance(
ILogger::class.java.classLoader,
arrayOf<Class<*>>(ILogger::class.java),
LoggerProxy(target)
) as ILogger

override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
val action = args!![0].toString()
if (method?.name == "logInfo") {
uploadToCloud(action)
target.logInfo(action)
eventTracking(action)
} else if (method?.name == "logError") {
uploadToCloud(action)
target.logError(action)
eventTracking(action)
}
return null
}

private fun uploadToCloud(action: String) {
println("上传数据到服务器")
}

private fun eventTracking(action: String) {
println("埋点")
}
}

interface ILogger {

fun logInfo(action: String)

fun logError(action: String)
}

调用方式
val proxy = LoggerProxy(Logger())
proxy.createProxy().logError("出错了")

打印顺序
1. 上传数据到服务器
2. 存储到本地: 出错了
3. 埋点


  1. 在动态代理中,当我们通过createProxy()创建代理对象后,调用logError或logInfo方法的时候

  2. 代理对象的invoke()方法会被调用

  3. 由于我们传入的只有action这个参数,在invoke方法中,可通过args[0]来获取传入的数据;通过method.name获取待执行的方法名,以此来判断逻辑的走向


代理的创建方式createProxy()方法中的代码大部分都是固定的。


总结


静态代理:静态代理在编译时期就已经确定代理类的代码,代理类和被代理类在编译时就已经确定;如果需要扩展的功能越来越多,静态代理的缺点很明显就是要写大量的代理类,管理和维护都不太方便。

动态代理:动态代理在运行时动态生成代理对象,关系灵活,由于是在运行时动态的生成代理类,动态代理解决了静态代理大量代理类的问题,但是有个新的问题就是反射相对耗时一点。


我们常用的Retrofit就有用到动态代理,感兴趣的同学可以去深入了解下,这边就不过多讲解,包括AOP(面向切面编程),动态权限申请等等。


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

来个面试题,看看你对 kotlin coroutine掌握得如何?

给出下面代码:lifecycleScope.launch(Dispatchers.IO) { val task1 = async { throw RuntimeException("task1 failed") } v...
继续阅读 »

给出下面代码:

lifecycleScope.launch(Dispatchers.IO) {
val task1 = async {
throw RuntimeException("task1 failed")
}

val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}

问:app 会发生什么?输出的日志是怎样子的?为什么?


......


......


......


......


......


......


......


......


......


答:app 会 crash,输出日志为


I/test: catch task1: java.lang.RuntimeException: task1 failed
I/test: is coroutine active: false
I/test: catch task2: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=DeferredCoroutine{Cancelling}
I/test: scope end.

魔幻吗?


那我们就来分析下为啥结果是这个样子的。


协程有一个很基础的设定:默认情况下,异常会往外层 scope 抛,用以立刻取消外层 scope 内的其它的子 job。


在上面的例子中,假设:lifecycleScope.launch 创建的子 scope 为 A。task1 用 async 创建 scope A 的子 scope 为 B。task2 用 async 创建 scope A 的子 scope 为 C。


当 scope B 发生异常,scope B 会将异常抛给 scope A,scope A 会 cancel 掉 task2 和自己,再把异常抛给 lifecycleScope,因为 lifecycleScope 没有 CoroutineExceptionHandler 并且 scope A 是通过 launch 启动的,所以 crash 就发生了。


那如何打断异常的这个传播链呢?


答案就是使用 SupervisorJob,或者用基于它的 supervisorScope。它不会把异常往上抛,也不会取消掉其它的子 job。但是,SupervisorJob 对 launch 和 async 启动的协程的态度是不一样的,它的源码注释里写明了的,简单的认为它会吃掉异常是会踩坑的。



翻译出来就是,如果是 launch 启动的子协程,是需要 CoroutineExceptionHandler 配合处理的,如果是 async 启动的协程,就是真的不抛,等到 Deferred.await 时再抛。


所以,在上面的代码中,虽然 lifecycleScope 有用到 SupervisorJob,但异常从 scopeA 往上传时,因为没有 CoroutineExceptionHandler,所以跪了。


那么为什么 async 要往上抛异常,导致 await 的 try catch 还需要 supervisorScope 的配合?感觉有点反人类?


想象一下下面的场合:

lifecycleScope.launch {
val task1 = async { "非常耗时的操作,但没有异常" }
val task2 = async { throw RuntimeException("") }
val result1 = task1.await()
val result2 = task2.await()
}

因为 task2 有异常,所以整个协程必定会失败。如果等 await 时才跑错误, 那么就需要等耗时的 task1 执行完成,轮到 task 的 await 调用时,异常才能跑出来,虽然也没啥问题,就是白白耗费了 task1 的执行。


而依据当前的设计,task2 抛出异常,那么外层 scope 就会把 task1 也给取消了,整个 scope 也就执行结束了。async 源码里提到的原因是为了 structured concurrency,也是期望使用者更多的关注 scope 以及 scope 内各个任务的关联关系吧。不过这坑确实有点让人有时摸不着头脑,可能以后就变了也说不定。


剩下一个问题是,task1 失败后就往上抛吗?为啥 catch task1 后还有日志打印出来?


其实上面已经提到了,异常抛给 scope A 后,它会 cancel 掉自己,再往上抛,而 cancel 掉自己并不是强制终止掉协程的执行,而是先变更状态为 cancelling,所以日志中 isActive 已经变成 false 了,第二个异常也不是 task2 的异常,而是 await 本身抛出的 CancellationException。这里告诉我们要注意两点:



  1. try catch 时如果是 CancellationException,要记得 rethrow。

  2. 一些循环、耗时的点,要记得用 isActive 或者 ensureActive 检查,不要写出不能正常 cancel 的协程。像 delay 等 api,官方已经做好了这方面的检查,极大地方便了开发者。这个线程 interrupted 相关知识点是同一个道理。


了解了各种坑点以及背后的原因,我们就可以把协程用得飞起了。最后,修复文章开头提到的问题,就是简单包个 supervisorScope 就行啦。

lifecycleScope.launch(Dispatchers.IO) {
supervisorScope {
val task1 = async {
throw RuntimeException("task1 failed")
}

val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}
}

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

一点Andorid开发小建议:妥善使用和管理Handler

在Android开发中,我们经常使用Android SDK提供的Handler类来完成线程间通信的任务,但是项目代码中,经常看到Handler过于随意地使用,这些使用可能有一些隐患,本文记录下这些问题,并给出相关建议。 问题一:使用默认无参构造来创建Handl...
继续阅读 »

在Android开发中,我们经常使用Android SDK提供的Handler类来完成线程间通信的任务,但是项目代码中,经常看到Handler过于随意地使用,这些使用可能有一些隐患,本文记录下这些问题,并给出相关建议。


问题一:使用默认无参构造来创建Handler


Handler有个无参构造方法,有的时候偷懒在Activity中直接通过无参构造方法来创建Handler对象,例如:

private final Handler mHandler = new Handler();

那么这个Handler对象会使用当前线程的Looper,这在Activity或自定义View中可能没问题,因为主线程是存在Looper的,但是在子线程中就会出现异常,因为子线程很可能没有进行过Looper.prepare()。另外一个隐患是,new Handler()使用“当前线程”的Looper,可能预期是在子线程,但是一开始的外部调用是在主线程,那么这个使用可能影响主线程的交互体验。



  • 建议:创建Handler对象时必须传递Looper参数来确保Looper的存在并显式控制其线程。


问题二:Looper.getMainLooper()的滥用


在Activity中我们可以使用runOnUiThread方法把一个过程放在主线程执行,但是在其他地方,通过Looper.getMainLooper()也是一个简单的方法,例如:

new Handler(Looper.getMainLooper()).post(() -> {
//do something ...
});

因为太方便了,所以到处都可以用,那么就存在了一个隐患:任何线程都可以通过这种方式来影响主线程。有可能在配置较差的手机出现意料之外的卡顿,而且这种卡顿可能有一定随机性,不容易复现,给排查问题的时候造成一定难度。



  • 建议:避免在线程外部创建该线程的Handler,例如,尽量避免在ActivityFragment和自定义View以外的地方创建主线程的Handler


问题三:在业务功能层面直接创建Hanlder


在开发中,有时候需要利用Handler来切换线程,或者利用Handler的消息功能,然后直接使用Handler,例如下面这段代码:

new Thread(() -> {
presenter.doTaskInBackground();
new Handler(Looper.getMainLooper()).post(() -> {
updateViewInMainThread();
});
}).start()

这个代码槽点太多,这里主要讲下关于Handler的。其实程序的结果并没有什么问题,不足之处在于把Handler这种平台相关的概念混到了业务功能代码里,就好像一个人在读诗朗诵,念完“两个黄鹂鸣翠柳”,然后下面冒出一句“我先喝口水”,喝完水然后继续念。



  • 建议:将Handler的创建和销毁放到框架层面,甚至可以封装一套使用的接口,而不是直接使用postsend等方法。


问题四:没有及时移除Hanlder的消息和回调


HandlerpostDelayedpostAtTime是两个便利的方法,但是这个方法并没有和组件的生命周期绑定,很容易造成Activity或其他大型对象无法及时释放。



  • 建议:不需要的时候,及时调用removeCallbacksAndMessages方法

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

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

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

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


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


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


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


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


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


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


我的预期目标


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


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


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


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


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


找工作流水账


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



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



第一家


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


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



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



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


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


第二家


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



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



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



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



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


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


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



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





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


第三家


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



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



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


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



  • k8s 相关的一些组件、Operator

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

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


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


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


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


第四家


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


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




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


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


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


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


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


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


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


选择



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


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


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


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


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


面对裁员能做的事情


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


避免裁员


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



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

  • 未婚女性,这点确实有点政治不正确,但确实就是现在的事实,这个需要整个社会,政府来一起解决。

  • 做事本本分分,没有贡献也没出啥事故。

  • 边缘业务,也容易被优化缩减成本。


那如何避免裁员呢,当然首先尽量别和以上特征重合,一些客观情况避免不了,但我们可以在第三点上主动“卷”一下,当然这个的前提是你还想在这家公司干。


还有一个方法是提前向公司告知降薪,这点可能很多人不理解,因为我们大部分人的收入都是随着跳槽越来越高的;但这些好处是否是受到前些年互联网过于热门的影响呢?


当然个人待遇是由市场决定的,现在互联网不可否认的降温了,如果你觉得各方面呆在这家公司都比出去再找一个更好,那这也不失为一个方法;除非你有信心能找到一个更好的,那就另说了。


未来计划


我觉得只要一家公司只要有裁员的风声传出来后,即便是没被裁,你也会处于焦虑之中;要想避免这种焦虑确实也很简单,只要有稳定的被动收入那就无所谓了。


这个确实也是说起来轻松做起来难,我最近也一直在思考能不能在工作之余做一些小的 side project,这话题就大了,只是我觉得我们程序员先天就有自己做一个产品的机会和能力,与其把生杀大权给别人,不如握在自己手里。


当然这里得提醒下,在国内的企业,大部分老板都认为签了合同你的 24 小时都是他的,所以这些业务项目最好是保持低调,同时不能影响到本职工作。


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

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

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

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


十年,谁来成就你?


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


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


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


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


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


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


核心竞争力,祭出三板斧


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



  1. 多面试,验本事。

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

  3. 拥有自己的 Github 项目。 



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


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


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


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


记忆与思考,经验与思维


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




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




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






  •  




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


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



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



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

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

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


多面试,验本事


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


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


风平浪静,居安思危


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



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

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

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

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



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


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


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


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


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


乘风破浪,未雨绸缪


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


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


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


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



放平心态,查缺补漏


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



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

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


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


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


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


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


持续进步


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


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


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


狠下心来,坚持到底


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


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


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


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


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



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


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


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


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


幸运,需要自己争取


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


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



意外地成为了讲师


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


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


跌跌撞撞,逆水行舟


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


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


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


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


伺机而动,用心准备


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


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


建立个人 IP


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


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



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

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

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


链接资源,提影响力


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


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


主动出击,勿失良机


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


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


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


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


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


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



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


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


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


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


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


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


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


零碎时间,稳中求进


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



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


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


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


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


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


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


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


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


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


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


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



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

我是埋点SDK,看我如何让甲方爸爸的页面卡顿10s+

背景音: Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK... 昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就...
继续阅读 »

背景音:



Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK...



昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就开始有用户反馈说网页卡卡地,走得比蜗牛还慢。


六点二十分,第一个用户提交了投诉工单,但这只是个开始。


今天早上九点十分,公司的运维团队已经接到了一大堆反馈工单,许多用户都遭受到了同样的问题。这是一个巨大的问题,一旦得不到解决,可能导致数万的用户受到影响。运维人员立即开始了排查工作,想要找出问题所在。


经过一个小时的紧急排查,他们终于想到了昨日的这名前端程序员,一经沟通发现是SDK版本更新引起的问题。在新的版本中,有一些不稳定的代码导致了性能问题。


然而,这不仅仅是个技术问题,因为接下来,他们要开始着手写事故报告,准备给上层领导交代。


接下来,进入正题:


一、问题排查定位


根据更新的版本体量,可以缩小和快速定位问题源于新引入埋点SDK



  1. 打开 开发者工具-性能分析,开始记录

  2. 刷新页面,重现问题

  3. 停止记录,排查分析性能问题


性能分析


如上图,按照耗时排序,可以快速定位找到对应的代码问题。


首先把编译压缩后的代码整理一下,接下来,深入代码一探究竟。


代码耗时.png


⏸️暂停一下,不妨猜猜看这里是为了干嘛?


🍵喝口茶,让我们沿着事件路径,反向继续摸清它的意图吧。


image.png


这里列举了231个字体名称,调用上文的 detect() 来分析。


⏸️暂停一下,那么这个操作为什么会耗时且阻塞页面渲染呢?


...


休息一下,让我们去看看这段代码的来龙去脉。


上面我们大概猜到代码是用于获取用户浏览器字体,那就简单检索一下 js get browser font


搜索结果.png


代码示例.png


证据确凿,错在对岸。


二、解决问题


相信大家也看出来了,我不是埋点SDK,我也不是甲方爸爸,我只能是一位前端开发。


联系反馈至SDK方,需要走工单,流程,而这一切要多少时间?


我唔知啊!领导也不接受啊!


👐没办法,只能自己缝补缝补了。


那么如何解决呢?



  1. 尝试修复 getFonts detect 字体检测逻辑,避免多次回流导致性能问题




image.png




  1. 缩短待检测字体目录。


人生苦短,我选方案3,直接修改返回值,跳过检测

getFonts () { return 'custom_font' }

那么让我们继续搬砖吧。



  1. 寻根


image.png


首先找到 SDK 加载对应 JS 的加载方式,看看能不能动点手脚。
这里可以看到,是采用很常见的 通过 appendScript loadJs 的方案,那么就可以复写拦截一下 appendChild 函数。



  1. 正源


通过拦截 appendChild,将SDK加载的JS改为加载修复后的JS文件。


核心代码如下:

var tempCAppend = document.head.appendChild
document.head.appendChild = function (t) {
if (t.tagName === 'SCRIPT' && t.src.includes('xxx.js')) {
t.src = 'custom_fix_xxx.js'
}
return tempCAppend.bind(this)(t)
}

三、后续


这件事情发生在21年底,今天为什么拿出来分享一下呢?


近期排查 qiankun 部分子应用路由加载异常的时候,定位到与 document.head.appendChild 被复写有关,于是去看SDK方是否修复,结果纹丝未动....


结合近期境遇,不得不感慨,业务能不能活下去,真的和代码、技术什么的毫无关系。


其他


❄️下雪了,简单看了几眼文心一言的发布会,更凉了。


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

外包仔的自我救赎

本人96年后端Javaer一枚,现在在某知名大厂做外包仔(一入外包深似海,从此自研是路人)。 为什么做外包仔? 开始是没得选 毕业的第三年,通过培训班转行Java,包装了两年经验。非科班出身又是半路出家,当时也不懂外包的概念,于是就糊里糊涂进了外包公司。...
继续阅读 »
本人96年后端Javaer一枚,现在在某知名大厂做外包仔(一入外包深似海,从此自研是路人)。

为什么做外包仔?



开始是没得选



毕业的第三年,通过培训班转行Java,包装了两年经验。非科班出身又是半路出家,当时也不懂外包的概念,于是就糊里糊涂进了外包公司。第一家公司只干了三个多月就跑路了,一方面是工资太低(8K),另一方面是技术比较老旧(SSH)。第二家公司也是外包,但是项目还不错(spring cloud),薪资也可以接受(12K)。



后来是给的多



做开发工作的第二年,跳槽时本来想着找一家自研公司,但是没忍住外包公司开的价格,一时脑热又进了外包,也就是现在这家大厂外包。薪资比较满意(18K),项目也很不错(toC业务,各种技术都有涉及)。


下定决心跳出外包



为什么要离开



干过外包的小伙伴们多多少少会有一些低人一等的感觉,说实话笔者自己也有。就感觉你虽然在大厂,但你是这里身份最低的存在,很多东西是需要权限才能接触到的。再者就是没有归属感,没有年会、没有团建、甚至不知道自己公司领导叫什么(只跟甲方主管和外包公司交付经理有接触)。



潜心修炼技术



在最近这个项目里确实学到了很多生产经验,自己写的接口也确实发生过线上故障,不再是单单的CRUD,也会参与一些接口性能的优化。对业务有了一定的的理解,技术上也有了一定的提升,大厂的开发流程、开发规范确实比较健全。



背诵八股文



三月份开始就在为跳槽做准备,先后学习了并发编程、spring源码、Mysql优化、JVM优化、RocketMQ以及分布式相关的内容(分布式缓存、分布式事务、分布式锁、分布式ID等)。学到后面居然又把前面的忘了


大环境行情和现状



大范围裁员



今年从金三银四开始,各大互联网公司就都在裁员,直到现在还有公司在裁员,说是互联网的寒冬也不为过。笔者所在的厂也是裁员的重灾区,包括笔者自己(做外包都会被优化,说是压缩预算)也遭重了,但是外包公司给换了另外一个项目组(从北京换到了杭州)。



招聘网站行情



笔者八月份先在北京投了一波简历(自研公司,外包不考虑了),三十多家公司只有一家公司给了回应(做了一道算法笔试题,然后说笔者占用内存太多就没有后续了),九月中旬又在杭州投了一波简历(也是只投自研),六十多家公司回复也是寥寥无几,甚至没约到面试(有大把的外包私聊在下,是被打上外包仔的标签了吗)。


如何度过这个寒冬



继续努力



工作之余(摸鱼的时候),笔者仍然坚持学习,今天不学习,明天变垃圾。虽然身在外包,但是笔者仍有一颗向往自研的心,仍然想把自己学到的技术运用到实际生产中(现在项目用到的技术都是甲方说了算,当然我也会思考哪些场景适合哪些技术)。



千万不要辞职



现在的项目组做的是内部业务,并发几乎没有,但是业务相对复杂。笔者只能继续狗着(简历还是接着投,期望降低一些),希望互联网的寒冬早日结束,希望笔者和正在找工作的小伙伴们早日找到心仪的公司(respect)。


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

环信十周年趴——我的游戏人生

   我觉得热爱电子游戏的八零后和九零后,真是太幸福了。     在那个宣传电子游戏是电子迷幻剂的时代,恰巧也是电电子游戏最飞速迅猛发展的时代,日新月异的计算机硬件发展,带来的是游戏画面、游戏玩法以及游戏综合素质的大幅提...
继续阅读 »

   我觉得热爱电子游戏的八零后和九零后,真是太幸福了。


    在那个宣传电子游戏是电子迷幻剂的时代,恰巧也是电电子游戏最飞速迅猛发展的时代,日新月异的计算机硬件发展,带来的是游戏画面、游戏玩法以及游戏综合素质的大幅提升,作为热爱游戏的我,有幸经历过这个时代,并且伴随着那些时间烙印的游戏,连同点点滴滴的回忆,一同化作成为了现在对我来说十分珍贵的青春纪念册。


    我的童年生活在一个部队院校的家属院中,那所院校位于中部省份一个县的边陲地区,我小时候经常会和几个同在家属院中生活的小伙伴,一起去在学院的各个地方玩闹。广阔的部队操场,四百米障碍场,荒芜的大片作训场,一座一座具有苏联特色的小楼都是我们玩耍的“宝地”。有一次我们跟着其中一个小伙伴进到一个学员宿舍楼,看到学员的娱乐室有几台电脑正在开着,上面正在运行着“反恐精英”,以及“红色警戒”等游戏,学员都很热情,也让我们玩了玩,我却连鼠标怎么操作都不知道,只好笑哈哈的站起来将电脑还给他们。在这里,我第一次接触到了电子游戏。后面我们又去过几次,有学员教我们如何玩名为“警察抓小偷”的游戏,很简单的操作,只需要控制方向键以及一个键就可以,这带给了我一种独特的欢乐,也给我留下了深刻的印象。


    再后来,到了上小学的时候,家属院内的房子要分配给新的教工们,我们将部队房子腾退了,随家里大人搬到省会的一个部队院校承建的小区,院校的很多职工干部也都在这里安了家,于是童年的几个发小又聚集在一起了。小孩子的精力总是旺盛的,毕竟那是一个用一根树枝挖土都可以玩一下午的年纪。不满足于在小区里乱窜,我们也会去各个发小的家里互相串门。游戏王的卡牌、数码宝贝的“拍片儿”,还有现在列做违禁品的仿真玩具枪,各式各样的小玩意,每过一段时间大家手里的小玩意都会同一时间更换掉,然后又有新的东西出现。我们一直在探索发现新的东西,直到在一个发小家里看到了电脑游戏。


    我仍然记得第一次真正的接触到游戏的那天,正值盛夏,午后太阳便被乌云层层遮住,下午两三点钟的天气阴沉的如同水墨画一般,随意泼洒在天空,伴随着远处时不时传来的轰隆隆的雷声。我们只好聚在一个发小的家里,一起坐在电脑前,看着他玩起了游戏。游戏的开场是一辆跑车疾驰而过无人的街道,然后是枪声传来,几个游戏角色狂奔出来,接着主角被放倒了,然后游戏转场到另一个雨夜,主角坐在囚车上,被一伙帮派人员顺带着劫出,然后便正式开始了。恰巧,游戏中也是雷雨天,而当时窗外也应景的下起了雨,那副场景深深地烙印在了我的脑海中。


    那款游戏叫《侠盗猎车3》,又名GTA 3。


    我们一起玩的很开心,在拙劣的操控和因画面拖影而造成不流畅的画面下,大家谁也没有通过第一个任务到达终点,我们都嘻嘻哈哈的看着其中一个小伙伴开着汽车翻车,爬出车外,然后又重新回到开场的地点,直到傍晚,雨停下来了,天空中的乌云也消散了,露出蓝灰色的天空,夕阳为剩下的云镶嵌了一圈金边儿。我们也互相道别,各自回去自己的家了。


    这款游戏是我第一次接触的游戏,从此就好像留下了记忆烙印一般。我深深地迷上了这个游戏。又过了一年,家里终于换了电脑,那是一台奔腾处理器,只有几十兆内存和八十千兆硬盘的方正电脑,配上的是传统的17寸大脑袋显示器,这台电脑伴随我度过了小学时期,我从外面买来了当时几元一张的盗版游戏光盘,安装上了侠盗猎车3,靠着从各处搜集来的资料和盗版盘上附带的游戏攻略,我也慢慢的接触到了剧情,完成一个又一个游戏中的任务。到后面又接触了罪恶都市、过山车大亨2、包青天七侠五义、合金弹头之类的游戏,由于当年能够接触的游戏少,每一款我都玩得很细致,这些游戏到现在我还会偶尔打开,游玩一会。


    快上初中时,老电脑也来到要更换的时候了,我已经忘记了哪个部件出现了损坏,长辈便又去买了一台电脑主机回来,第二款主机的配置增强了一些,显卡是GeForce 7300LE,靠着显卡和处理器的升级,我玩上了诸如圣安地列斯、马克思佩恩、荣誉勋章血战太平洋、极品飞车、战地等游戏,这些游戏在我看来画面更为精致,场景更加宏大,内容也更加丰富,它们又陪伴我度过了一段美好的时光。


    到了上初中以后,那台17寸显示器也不堪重负坏掉了,在我向家长的恳求下,家里的电脑便被更换为19寸液晶显示器和一套全新的主机,显卡更换为了Nvidia 9600GT,处理器也更换为了AMD的羿龙x2系列处理器。我便彻底接触到了第七世代(即微软Xbox 360和PS3游戏机热销的时候)的那些经典大作。诸如侠盗猎车4、使命召唤4、5、6、无主之地、刺客信条、孤岛危机、战地叛逆连队和战地3等游戏,每个游戏中的场景都宏大真实,各有特色,它们带我开启了“高成本、高质量、高体量游戏,即3A大作”的大门。同期还有些腾讯的网游,诸如穿越火线、QQ飞车、QQ三国,我也和同学一起玩过,但是我对那些游戏感到兴趣缺缺,也对网络游戏中需要签到、完成日常任务的模式感到疲劳,因此也只是基本有过接触。


    高中毕业的那个暑假,家长又一次同意了我更换配置的请求,因此电脑升级成AMD的FX4100处理器、8G内存、HD6770显卡,以前的中低特效运行的游戏变为了高特效运行,我因此玩到了更多同期的新游戏、诸如质量效应3、虐杀原形2、使命召唤8,上古卷轴5等作品。


    大学时期,我用的是一台戴尔的笔记本,当时的配置是i7 3632QM处理器、 GT 640M显卡、1TB的机械硬盘以及8G的内存,但是彼时也正是游戏机平台从PS3向PS4更新换代的时期,一些新出的游戏作品为了适配最新的PS4游戏机,对配置要求都大幅增加。这台电脑的显卡和家里的台式机电脑显卡性能类似,因此我没有再接触那些新出的游戏作品,主要玩的游戏也转变为当时大热的英雄联盟、反恐精英Online,还有之前接触的一些网络游戏之类。


    毕业以后,我从事iOS开发的工作,在新的城市中,奔波于工作、租房以及各式各样其他的事情里,也没空接触游戏了,直到2017年,工作几年的我,因收到绝地求生的大火影响,也想跃跃欲试体验一把吃鸡的快感,便在那年的618下单了一台神舟的笔记本,这次完全是我自己挑选的型号以及配置。处理器是Intel的i5 6300HQ,显卡是GTX 1060,再加上我自己更换的屏幕,增加的128gb固态硬盘,以及16gb的内存。绝地求生、使命召唤、生化危机、巫师3,侠盗猎车5,这些游戏又带我走进了一个个更加宏大、真实的游戏世界中。购买了这台笔记本后,我如同打开了一个全新游戏世界的盒子,再过了半年,我又买到了PS4,在此之前我接触的游戏机只有一台老旧的索尼PS1,还有一台亲戚送的Wii体感游戏机,尽管冒着极低的分辨率,我仍然通关了天诛、最终幻想、恐龙危机,还有Wii平台的生化危机0,生化危机4这些游戏。买到PS4以后,神秘海域,最后生还者,战神4,血源诅咒这些游戏让我领略到了动作游戏的独特魅力。便尝试购买各类游戏主机,再之后两年多的时间里,我购买过Xbox one和Xbox one S,任天堂Switch,PS3,Xbox 360,还有Playstation vita以及好几台3ds、我购买这些游戏机后,会先安装那些在我青少年时期玩不到,或一直期待的游戏,然后再把它们卖出。到疫情前,我的手里还有一台PS4的升级版PS4 Pro,一台Xbox one S以及一台任天堂switch。


   疫情来势汹汹,我的收入也因经济原因下跌,我不得不卖掉这些游戏机,好能够度过那些困难的日子。就这样,时间来到2022年的夏天。这个时候的我,仅仅有一台微软最新的Xbox Series X,在前几年购买的笔记本也扔到了老家里。就在那年夏天的某一天,我坐在家里,正听着窗外的蝉鸣,感受空调吹来的风时,突然决定我想要买一台主机来玩。说干就干,没多久我便从二手购物平台买到一台在当时已经过时的电脑主机,买家标价只有千余元,配置是一台搭载AMD 锐龙六核处理器的2600,华硕的A320M主板,不带显卡,内存16G,加上120g的固态硬盘,400瓦的电源,我又额外购买了一片蓝宝石RX 580显卡。这台电脑也算是组成了,在拼凑完成并打开它的一瞬间,我心中的喜悦之情简直无以言表,我意识到组装电脑带来的快乐和童年时期接触到游戏的那些惊喜一样。尽管过去的那些年我没有过多的研究电脑硬件,但是我仍然知道我购买的是一台在疫情前就十分热销的主流游戏配置。有了这次令我满足的经历没多久,我又开始觉得显卡游戏性能似乎有些不够,便又开始在二手平台搜罗,没多久买到了一块GTX 1070显卡,这是六年前的高端型号,我很满足这片显卡的性能。不出意外的,没几天回去,我又开始的置换的道路,我不断的买入二手显卡,并卖出之前收购到的旧显卡,在踩了两次坑,交了两次学费后,我终于收到一片品相非常好的七彩虹RTX 2070 Super显卡。同时对于处理器,我也没有停下更新的脚步,从R5 2600,再到R5 3600X,再到R5 3800X,电源也更换成了600瓦的新电源。更换完这些配件后,我看着眼前的电脑,突然觉得索然无味,没多久,我便将整机一同挂出,卖给了一位同城的用户。


    在这次经历后,我发现我对电脑硬件的折腾几乎可以比肩队游戏的热情,卖出电脑后没两个月,我又开始撺掇着自己再配一台电脑了,不出两周,一台搭载着AMD R5 5500处理器,AMD RX 5700的显卡,16g内存和1tb固态硬盘的侧透玻璃机箱便组成了,我安装好了系统,玩的却是和之前一样的游戏,令我感觉意兴阑珊,年后,这台主机也一并卖出给了一位同城的买家。


    2023年的春天,万物都显示出欣欣向荣的样子,我心中那股对电脑硬件的热情也一并复苏了。我开始挑选硬件,思考要一台什么样的配置。起初,我的预想是用几千元预算,配置一台低配置处理器和入门级显卡的电脑,然而看到各类购物平台在淡季时低价销售各类配件后,我逐渐将配置一层一层加高,经过不断的购买和置换,从AMD的R5,到Intel的i5,再到i7,主板也从适配AMD处理器的B450主板,到Intel处理器的B660主板,再到B760主板。显卡更是层层加码,从RTX 3060Ti,到RTX 3070Ti、RTX 3080………,散热器也从风冷更换为了水冷,还加了灯效风扇。在这篇文章写成时,这台电脑已经变成了一台Intel十二代i7处理器、RTX 3080Ti的前代旗舰配置了,我问过自己,折腾的这一切是为了什么?然后我给出了自己的回答,大概是源自热爱吧。


    敬游戏玩家。



本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026


收起阅读 »

老菜鸟为什么喜欢代码重构

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗? “今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼? “君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另...
继续阅读 »

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗?


“今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼?


“君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另外一番滋味呢...


好了!扯多了,还是谈一谈正题:老菜鸟为什么喜欢代码重构!


屎山上挖呀挖


最近几个月流行的"花园种花"跟大家分享一下:


222213.jpg


小朋友们准备开始
在小小的花园里面 ~ 挖呀挖呀挖 ~ 种小小的种子 ~ 开小小的花
在大大的花园里面 ~ 挖呀挖呀挖 ~ 种大大的种子 ~ 开大大的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花
在什么样的花园里面 ~ 挖呀挖呀挖 ~ 种什么样的种子 ~ 开什么样的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花


每天对着自己项目中的代码,真的想改编一下:


在小小的屎山里面   ~ 挖呀挖呀挖 ~ 
在大大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~

没被铲走的屎山就是好屎山



诸位,想不想一睹屎山真容?----去你自己项目上找吧,哈哈; 重构并不一定是因为屎山,屎山可能不一定需要重构





  1. 于公司、项目而言,能正常运行、满足客户需要的代码就是OK的,甭管它屎山还是花园,能帮公司赚到钱的就是王道也!




  2. 为什么会有屎山的存在,源于项目早期的设计、架构不合理、中后期编码不规范、或者说压根就没有架构、没有规范;




  3. 很多大厂或一些比较讲究的公司,有自己的架构师、质量团队,那他们产出的项目质量就会非常高,就算是重构也可能是跨代、技术层面的升级,绝非屎山引起的重构!




  4. 爷爷都是从孙子过来的,谁还没个小菜成长记呢,谁小时候(初级阶段)还没(写过一些垃圾代码)在裤子上拉过屎呢;能接受别人的喷说明你心智成熟了,能发现项目中的糟糕代码说明你技术成长了;




  5. 最重要的一点,用发展的眼光看问题,以当下的技术、潮流去审视多年前的项目,还是要充满敬意,而不只是吐槽!




优化


你需要可能是优化


重构并不一定是因为屎山,可能是技术自身的转换升级; 屎山可能不一定要重构,或许只需要做代码的优化; 倘若项目的技术栈还比较年轻,那我们面对的可能是优化,而不是重构;



那糟糕的代码源自何处呢?


  • 曾经的你? 曾经梦想仗剑走天涯,如今大肚秃顶撸代码;

  • 已经离职的同事?人走茶凉,雁过拔毛;


话说谁还不是从小菜成长起来的呢,当你发现项目上诸多不合理时,说明你的技术已经成长、提高了不少;



  • 可能你接手了一个旧的项目,看到的代码是公司几年前的产品设计

  • 可能你自己今年前写的代码,因为项目赚钱了,又要升级

  • 可能你接手了(不太讲究的)同事的代码

  • 可能就是因为赶进度,只要功能实现


如果仅仅是代码不够友好,我们需要的或许只是长期优化了...


网友谈重构



当你看到眼前的屎山会作何感想? TMD,怎么会会会有如此的代码呢,某某某真**,ε=(´ο`*)))唉? 正常,如果你没有这样的感慨,下文就不用看了,直接吐槽就行了...



看看网友回复



  • 看心情




  • 别自己找trouble




  • 又不是不能用,你领导怕成本太高吧,新项目可以用新架构




  • 除非你自己愿意花时间去重构,不然哪个老板舍得花这个钱和时间




  • 应该选择成为领导,让底下人996用新技术重构
    .... 下面的更绝




  • 小伙子还年轻吧,动不动就重构




  • 代码和你 有一个能跑就行
    哈哈,太多了,诸位,你会怎么想,面对糟糕的代码你会重构吗?




cg3.jpg


问题



当你发现问题时说明你用心了;当你吐槽槽糕代码时,说明你技术提升了;当你想爆粗口时说明你对美好生活是充满向往的;



那么,你的项目上可能有哪些问题呢(以前端代码为例)?



  • 技术栈过于古老

  • 架构设计不合理

  • 技术选型、规范不合理

  • 不合理的三目运算符

  • 过多的if嵌套

  • 回调地狱

  • 冗余的方法、函数

  • 全局变量混乱

  • 项目结构混乱

  • 路由管理糟糕

  • 状态数据管理混乱

  • CSS样式样式混乱

  • .....
    .... 哪些该重构,哪些该优化?


sikao6.jpg


机会



“祸兮福之所倚,福兮祸之所伏”, 问题的背后往往就是机会;



路人甲: 明明就是一座屎山,又何来机会一说?有扒拉屎山的功夫,搞点新技能,搞点原创不开心吗?答案是肯定的


路人乙: 解决屎 OR 新项目搭建,会选择哪个呢? 我想脑子正常点的人应该会选择重新搭建吧!


个人觉得对于经验比较丰富的开发当然选容易的,对于经验不丰富的开发而言当然也选容易的!对于有一定基础 && 想要快速提升综合能力者,解决屎山或许别有一番滋味,未尝不是一件闻起来臭吃起来香的(臭豆腐)幸事;



  • 理解老旧项目的初始架构、设计有助于了解、理解技术发展的脉络

  • 有很多老旧项目的设计、架构是非常优秀的,值得去深入学习背后的思想

  • 重构的整个过程也是不断审视自己不足的过程,查漏补缺,提升最短的那块板

  • 一切技术服务于业务,重构的过程也是深入理解业务的过程

  • 有机会重构是一种幸福,面对诸多压力本身就是是一种心智的磨练,不经历风云怎能见彩虹; 大成功者必是大磨难者!


挑战



问题的背后是机会, 机会的身边往往伴随着诸多挑战;面对重构不去争取,那老油条和小菜鸟有有什么区别(什么价值),不要等到退出行业了,再说: “曾经有一次重构的机会摆在我面前,我没有珍惜,等到了失去的时候才后悔莫及,尘世间最痛苦的事莫过于此。如果老板可以再给我一个再来一次的机会的话,我会说:重构!重构! 重构!。如果一定要加一个期限的话,我希望是公司设定的时间之前!”



QQ截图20230628143530.jpg



  • 时间:领导、公司会不会给予足够的时间深入重构,万一没有如期完成呢?

  • 回报:重构是否能达到预期,是否能带来质变级的体验,万一重构后没成功呢?

  • 能力:自身能力是否能承担得起重构的重任,是否具备一定的抗压能力

  • 博弈:重构也是一种资源的博弈,考虑如何让自己的利益最大化的


重构




  • 重构的机会应该是去争取的,而不是被赋予的




  • 生活中处处有压力,又总是压得人喘不过气,如果连个代码重构都不敢想、不敢搞,那生活中的种种不如意又当如何?正如那句话:“做人如果没有梦想,和咸鱼有什么区别?”




QQ截图20230628143731.jpg


收获


其实,个人感觉收获最大的还是心智的磨练; 其次是技术的提升;


结语


生活已经够累了,跟大家闲扯一下,放松!放松!放松! 最重要的天热了要多喝水,多吃蔬菜和水果,适度的体育活动!欢迎大家说说自己的看法


heshui15.jpg

收起阅读 »

记两次优化导致的Bug

web
人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。 废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到...
继续阅读 »

人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。


废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到的问题。Bug本天成,菜鸡偶遇之。


和requestFrameAnimation有关。


和requestFrameAnimation就是在下一帧的时候调用, 调用这个方法后会返回一个id,凭借此id可以取消下一帧的调用cancelAnimationFrame(id)


在图形渲染中,经常会使用连续渲染的方式来不断更新视图,使用此api可以保证每一帧只绘制一次,一帧绘制一次就够了,两次没有意义。 当然,这里说的一次,是指一整个绘制,包含了后处理(比如three的postprocess)的一系列操作。


大致情况是这样的, 现在已经封装好了一个3d渲染器,然后在更新场景数据的,会有比较多的循环,这个时候,3d渲染就可以停掉了,这里就是做了这个优化。


来看主要代码,渲染器部分。只要调用animate方法就会循环渲染, pause停止,resume恢复渲染。 看上去好像没啥问题。


// 渲染 
animate = () => {
if (this.paused ) return ;
this.animateId = requestAnimationFrame(this.animate);
};
pause() {
this.paused = true;
}
resume() {
if (this.paused) {
this.paused = false;
this.animate()
}
}

再看,更新数据的部分。 问题就出在这里,更新数据这个操作,是没有任何异步的,也就是说在当前帧的宏任务里,先暂停后恢复, 结果就是,下一帧执行animate的时候, paused仍为true, 这个优化毫无意义。


view.current.pause() ;

//更新数据完成
view.current.resume()


无意义倒也没啥,但是 resume方法执行了之后,会新开一个requestAnimationFrame, 上一个requestAnimationFrame的循环又没有取消,


所以现在, 一帧里会执行两次render方法。 这个卡顿啊,就十分明显了,也就是当时的项目模型还不是特别大,只有在某些场景模型较大的时候才会卡,所以没急着处理。


过了几个月,临近更新了,不得不解决。排查的时候,是靠git记录二分才找出来的。


其次,要说明的是,使用了requestAnimationFrame连续渲染,这种在一帧里先暂停再继续的操作肯定是无意义的,因为下一帧才执行,只看执行前的结果。


当然,前面的暂停和继续的逻辑,也是一个隐患。 于是,就改成了我惯用的那种方式。 那就是暂停的时候,只是不渲染,循环继续空跑,如此而已。


  // 渲染
animate = () => {
!this.paused && this.renderer.render(this.scene, this.camera);
this.animateId = requestAnimationFrame(this.animate);
};

pause() {
this.paused = true;
}

resume() {
this.paused = false;
}

和 URL.create 有关


上面的那个代码,还可以说是写的人不熟悉requestAnimationFrame的特性造成的。 这一次的这个,真的是因为,引用关系比较多。


这次是一个纯2d项目,这里有一个将(后端)处理后的图片在canvas2d编辑器上显示出来的操作。这个图,叫产品图, 产品图是用户上传的, 也可以不经过后端处理,就直接进入到2d编辑器里。 后端处理之后,返回的是一个url。 所以有了下面的函数。


  function afterImgMatter(img:File|string) {
setShowMatter(false);
if (img instanceof File) {
tempImg.src = URL.createObjectURL(img);
} else {
tempImg.src = img
}

console.log(tempImg.src);
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
if (refCanvas.current) {
tempImg.onload = () => {

// 产品图层不可手动删除
productImg = refCanvas.current!.addImg(tempImg, false, null, 'product');
if (img instanceof File) {
productImg.id = globalData.productId;
} else {
productImg.id = globalData.matteredId;
}
setCanvasEditState(true);
!curScene && changeSene(scenes[0]);

}
}
}


如果是没经过后端处理的,那个图片就是文件转URL,就用到了这个方法URL.createObjectURL, 这个方法为二进制文件生成一个临时路径,mdn强调了一定要手动释放它。 浏览器在 document 卸载的时候,会自动释放它们,也就是说在这之前GC不会自动释放他们,即便找不到引用了。


那这个优化,我必然不能不做。 所以,我就判断了一下,如果之前的src是这个方法生成的,我就去释放它。 于是,在重新上传图片,也就是二次触发这个方法的时候出问题了, 画布直接白屏了,原来是报错了。


Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.


说我提供的图像源是破碎的,所以这个drawImage方法不能执行,其实上面还有一个报错,我之前一直没在意, 说得是一个url路径失效了,图片加载失败了,因为我释放了路径,所以我觉得出现这个,应该是正常的。


但是,现在结合drawImage的执行失败,这里还是有问题的。 我发现,确实就是因为我释放了要用做图像源的那个路径。 因为这里的productImg和 tempImg其实是通一个引用,只不过语义不同。


解决办法也很简单,那就是把释放的这一段代码,放到onload的回调里执行即可,图片加载完成之后,释放这个url也能正常工作


    tempImg.onload = () => {
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
}

这里之所以会有 productImg和 tempImg通一个引用,语义不同, 也是因为我想优化一下。之前是每次加载图片的时候,都new Image,实际上这个可以用同一个对象,所以就有了tempImg


结束


本文记录了两个bug,顺带说了一下requestAnimationFrame URL.createObjectURL的部分用法。


没有直接阐述他们的用法,有兴趣了解的可以直接看文档。


requestAnimationFrame


URL.createObj

ectURL

收起阅读 »

一个专科程序员的2023年中总结

前言 本篇是笔者对自己的这一年来的年中总结,之前也是陆陆续续看过一些总结,在6月17日的时候,爬了一下青城山,突然思绪从脑子当中炸出,写下了这篇文章 说在前面的话 在今年的二月份,我入职了我现在的这家公司,相比于去年的情况整体来说是好了一些的。去年笔者的情...
继续阅读 »

前言



本篇是笔者对自己的这一年来的年中总结,之前也是陆陆续续看过一些总结,在6月17日的时候,爬了一下青城山,突然思绪从脑子当中炸出,写下了这篇文章



说在前面的话


在今年的二月份,我入职了我现在的这家公司,相比于去年的情况整体来说是好了一些的。去年笔者的情况是蛮糟糕的,大概是半年是没有收入的,不是说我没有在工作辣,而是你们知道。


不过,这段经历让我心态有了一定的提升,在联盟中,有一句台词是这样的,天下万般兵刃,唯有过往最伤人。当人处于这种情况下,真的很难熬住。回到当时的爬山场景,我也在想,就是我爬的很累很累了,但是我还是要迈出下一步,究竟是为了什么才一路向上的。


我在爬山的时候,累了,我就休息一下,然后又继续前进,路虽不是那么平坦,心却平坦了一些,就像之前我也是这么过来的,让自己休息好了,再继续前进。


所以,我发现我后来对这些事情无关紧要了,就如莎士比亚说过的,凡是过往,皆为序章。一旦发生了的事情就已经成为了过去。不要沉溺于过去,把握当下,面对未来。


路途中,我看见很多人步伐各不相同,那是因为他们有着不一样的心之所向。


回顾


先说一下,这半年中,我自己做了什么




  • 面试,找工作,不过这一部分,我没有背八股文,几乎是知道的能说就说




  • 参与了华为 DevUI 组件库开源 顺利提了一个 PR ,后续也会持续关注继续做




  • 工作中,实现了一些个人觉得比较好的组件,签到日历,级联选择器,数字滚动,上下漂浮动画组件等等




  • 对外的话,做了我自己的一个小工具,JsonToTypescript ,可以把 JSON 格式转为对应的 Typescript 接口声明




  • 搭建了自己的 个人博客 使用了 hexo 框架




  • 准备了我自己的长期专栏 手摸手带你学习Javascript 、后期会写一些面试题相关




  • 更了几篇文章,后续也会一直更新




  • 持续学习自己定的方向,前端工程化




这些内容的话,我觉得在做组件那一块我的进步相对于之前来说是有提升的,写作的话,看了诗词相关的视频,也会用到我的内容中,整体还不错。


这里我们谈一谈组件吧,简单谈一谈我的一些浅薄理解。


Vue 组件设计


1. 如何设计一个组件?



  • 定义 Props (核心)

  • 确定组件的职责是做什么?展示、数据录入、布局等等

  • 根据主题设计组件样式


假如,我们现在设计一个抽奖组件


如下图所示:


抽奖转存失败,建议直接上传图片文件

有一个这样的抽奖实现,当我点击下这个开始按钮,那么需要,沿着按钮顺时针跑。对咯,我们更多的时候,其实会看见一个 UI 设计图之类的,你需要转变为这种更直观一些的


通常我第一步简单分析以后,我不会考虑很多其他场景,大部分还是业务中,够用就行


第二步,我们开始设计当前组件的 Props ,命名的话大家自己语义化一些,需要注意下


我们简单定义四个



  1. source 数据来源,为了让他有更好的复用性,数据是传递来的,在其他开发者使用的时候,需要遵循你的组件规则,这要从使用者的角度转变。你就想,怎么让别人好用

  2. speed 速率,用于控制顺时针跑的快慢,也就是抽奖速度

  3. change emit 事件,当点击的时候考虑是否对外发送什么通知之类的,非必要

  4. finish emit 事件,当组件完成之后触发的事件


第三步分析实现原理


如上图,我给大家标记出来了数据占位是 [0,1,2,3,4,5,6,7,8] 除了4以后,其他也就是奖品所在位置了


实现动画效果,顺时针抽奖,我们可以直观看见运动轨迹是 0-1-2-5-8-7-6-3 ,此为一个周期,要配合 speed 控制速率实现


展示高亮效果,就是拟定一个 active 变量,然后依次设置不同的值,也就是 排他思想


最后就是完成我们的代码逻辑


React 业务组件相关设计


1. 如何设计一个 React 组件?


我平时开发主要还是以 Vue 为主,所以,有些理念是学习来的,大家随意抽出对自己有用的为自己使用就好辣



  1. 单一职责:每个业务组件应该专注于解决特定的功能或问题,这会使得我们组件简洁,并且好维护

  2. 组件拆分:将复杂的业务需求拆分为小组件,每个组件负责特定的页面或者是功能

  3. PropsState 的设计:React 是单向数据流,合理的定义 PropsState ,确保值能够传递和保存到对应的组件。Props 的用于接受父组件传递的数据,State 用于管理数据状态


假如我需要设计点击一个新增\修改按钮设计出 Drawer 既可以新增又可以修改


我这里列出一个图,大家具体看这个图就好


image-20230625192757624.png
以上是对于组件的一些个人理解,后续学到实用的东西,也会进行相关内容的输出。


说一些面试相关的吧


我觉得在面试这件事,虽然现在投简历很多不会回复或者多看一眼。在这样做,我很担心,就是很多朋友,有些空余时间,没有去思考过,自己要走的一条路是什么。


面试的时候,你可能需要准备的知识,是要超过你目前的期望薪资的知识储备量的,这本身就是自身要具备的硬技能。


我个人在今年面试的时候,没准备,会有答不上,答不好的情况,但是基础的还行,这一块不需要背,自己知道的,没什么深度。不过写专栏就是在审视自己这个问题。我不建议大家学习我不去准备,我只是当时纯纯偷懒罢了...


笔者,是一个普通平凡的人,没有什么高质量的输出,还在不断学习中,后续会继续更新我的创作。


什么是平凡


说实话,写这个命题的时候,我是有点纠结的。曾看过这样一句话,一个人怎样看待自己,决定了此人的命运,指向了他的归宿。


我觉得自己会有一些不同的思想和见解,每个人亦是如此。也立过鸿鹄之志,我思考,我是想去拥有过一个不错的生活,为之而奋斗。不知道什么时候开始,我觉得这已然成为我的一种束缚,为什么呢?因为不快乐。


所以,我,是一个普通平凡的人,我自人间浪漫,平生事、南北西东。(我在人间放纵而活,不受世俗约束,平生事,无所谓南北西东)


虽然,并不会太洒脱,人生,总有十有八九不如人意。顺其自然就好啦,做自己喜欢的事,坚持下去就好了。总有一天可以在一寸冰封的土地,绽放出绚丽的春华,那是属于你个人。


这车水马龙的人间,读者们可以试着慢慢来,不问结果反而一身轻松,路的尽头是什么也许从来不重要。生活,有所为,有所爱,清醒、自律、知进退。


我认清自我,找到自我,从今以后,不在过我应该过的生活,而是去过我想过的人生,平凡挺好的,放下了一些,能变得更富有。


自己的方向


今年后半年大致是以下几点



  1. 更新进阶的 JavaScript 知识,这一部分大纲已经整理好了,在空余的时间,我会慢慢更新

  2. 学习 Nest ,做一下个人小应用

  3. 如果可以有时间更新自己的面试题专栏

  4. 持续更文


都是很简单的,没有太难的任务了,目前个人产出还是自我满足就够了。


结语


多花时间研究自己

作者:sakana
来源:juejin.cn/post/7248606482027675706

收起阅读 »

跨端技术总结

通过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解目前各家公司在跨平台方向上有哪些不同的项目,用了什么不同技术实现方式,然后在对比常用的react native ,flutter 和WebAssembly具体在技术上的区别在哪里。 客户端渲染...
继续阅读 »

通过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解目前各家公司在跨平台方向上有哪些不同的项目,用了什么不同技术实现方式,然后在对比常用的react native ,flutter 和WebAssembly具体在技术上的区别在哪里。


image.png


客户端渲染执行逻辑


android


层次的底部是 Linux,它提供基本的系统功能,如进程管理,内存管理,设备管理,如:相机,键盘,显示器等内核处理的事情。体系结构第三个部分叫做Java虚拟机,是一种专门设计和优化的 Android Dalvik 虚拟机。应用程序框架层使用Java类形式的应用程序提供了许多的更高级别的服务。允许应用程序开发人员在其应用程序中使用这些服务。应用在最上层,即所有的 Android 应用程序。一般我们编写的应用程序只被安装在这层。应用的例子如:浏览器,游戏等。


image.png


绘制流程




  1. 创建视图




ui生成就是把代码中产生的view和在xml文件配置的view,经过measure,layout,dewa 形成一个完整的view树,并调用系统方法进行绘制。Measure 用深度优先原则递归得到所有视图(View)的宽、高;Layout 用深度优先原则递归得到所有视图(View)的位置;到这里就得到view的在窗口中的布局。Draw 目前 Android 支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),会通过系统方法把要绘制的view 合成到不同的缓冲区上


最初的ui配置


image.png


构建成内存中的view tree


image.png


2.视图布局


image.png


3.图层合成


SurfaceFlinger 把缓存 区数据渲染到屏幕,由于是两个不同的进程,所以使用 Android 的匿名共享内存 SharedClient 缓存需要显示的数据来达到目的。 SurfaceFlinger 把缓存区数据渲染到屏幕(流程如下图所示),主要是驱动层的事情,这 里不做太多解释。


image.png


4. 系统绘制


image.png


绘制过程首先是 CPU 准备数据,通过 Driver 层把数据交给 CPU 渲 染,其中 CPU 主要负责 Measure、Layout、Record、Execute 的数据计算工作,GPU 负责 Rasterization(栅格化)、渲染。由于图形 API 不允许 CPU 直接与 GPU 通信,而是通过中间 的一个图形驱动层(Graphics Driver)来连接这两部分。图形驱动维护了一个队列,CPU 把 display list 添加到队列中,GPU 从这个队列取出数据进行绘制,最终才在显示屏上显示出来。


ios:


架构


image.png



  1. Core OS layer



  • 核心操作系统层包括内存管理、文件系统、电源管理以及一些其他的操作系统任务,直接和硬件设备进行交互



  1. Core Services layer



  • 核心服务层,我们可以通过它来访问iOS的一些服务。包含: 定位,网络,数据 sql



  1. Media layer



  • 顾名思义,媒体层可以在应用程序中使用各种媒体文件,进行音频与视频的录制,图形的绘制,以及制作基础的动画效果。



  1. Cocoa Touch layer



  • 本质上来说它负责用户在iOS设备上的触摸交互操作

  • 包括以下这些组件: Multi-Touch Events Core Motion Camera View Hierarchy Localization Alerts Web Views Image Picker Multi-Touch Controls.


ios 的视图树


image.png


ios的 绘制流程:


image.png


image.png


image.png


显示逻辑



  • CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;

  • RenderServer解析提交的子树状态,生成绘制指令;

  • GPU执行绘制指令;

  • 显示渲染后的数据;


提交流程


image.png


1、布局(Layout)


调用layoutSubviews方法; 调用addSubview:方法;


2、显示(Display)


通过drawRect绘制视图; 绘制string(字符串);



每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。 当一个视图第一次或者某部分需要更新的时候iOS系统总是会去请求drawRect:方法。


以下是触发视图更新的一些操作:



  • 移动或删除视图

  • 通过将视图的hidden属性设置为NO

  • 滚动消失的视图再次需要出现在屏幕上

  • 视图显式调用setNeedsDisplay或setNeedsDisplayInRect:方法


视图系统都会自动触发重新绘制。对于自定义视图,就必须重写drawRect:方法去执行所有绘制。视图第一次展示的时候,iOS系统会传递正方形区域来表示这个视图绘制的区域。


在调用drawRect:方法之后,视图就会把自己标记为已更新,然后等待下一次视图更新被触发。



3、准备提交(Prepare)


解码图片; 图片格式转换;


4、提交(Commit)


打包layers并发送到渲染server;


递归提交子树的layers;


web :


web内容准备阶段


web 通常需要将所需要的html,css,js都下载下来,并进行解析执行后才进行渲染,然后是绘制过程,先来看下前期工作


image.png


一个渲染流程会划分很多子阶段,整个处理过程叫渲染流水线,流水线可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。每个阶段都经过输入内容 -->处理过程-->输出内容三个部分。



  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构


image.png



  1. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets(生存CSSDOM树),计算出DOM节点的样式


image.png


styleSheets格式
image.png



  1. 创建布局树(LayoutTree),并计算元素的布局信息。


我们有DOM树和DOM树中元素的样式,那么接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。根据元素的可见信息构建出布局树。


image.png
4. 对布局树进行分层,并生成分层树(LayerTree)。


image.png



  1. 为每个图层生成绘制列表,并将其提交到合成线程。


image.png



  1. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图


image.png


image.png



  1. 合成线程发送绘制图块命令DrawQuad给浏览器进程。浏览器进程根据DrawQuad消息生成页面,并显示到显示器上


image.png


web 在android 中的绘制


WebView实际上是一个ViewGroup,并将后端的具体实现抽象为WebViewProvider,而WebViewChromium正是一个提供基于Chromium的具体实现类。


再回到WebView的情况。当WebView部件发生内容更新时,例如页面加载完毕,CSS动画,或者是滚动、缩放操作导致页面内容更新,同样会在WebView触发invalidate方法,随后在视图系统的统筹安排下,WebView.onDraw方法会被调用,最后实际上调用了AwContents.onDraw方法,它会请求对应的native端对象执行OnDraw方法,将页面的内容更新绘制到WebView对应的Canvas上去。


draw()先得到一块Buffer,这块Buffer是由SurfaceFlinger负责管理的。


然后调用view的draw(canvas)当view draw完后,调用surface.java的unlockAndPostCanvas().


将包含有当前view内容的Buffer传给SurfaceFlinger,SurfaceFlinger将所有的Buffer混合后传给FrameBuffer.至此和native原有的view 渲染就是一样的了。


image.png


成熟的框架的底层原理:


react :


RN 的 Android Bridge 和 IOS Bridge 是两端通信的桥梁, 是由一个转译的桥梁实现的不同语言的通信, 得以实现单靠 JS 就调用移动端原生 APi


架构


image.png



  • RN 的核心驱动力就来自 JS Engine, 我们所有的 JS 代码都会通过 JS Engine 来编译执行, 包括 React 的 JSX 也离不开 JS Engine, JavaScript Core 是其中一种 JS 引擎, 还有 Google 的 V8 引擎, Mozilla 的 SpiderMonkey 引擎。

  • RN 是用类 XML 语言来表示结构, 用 StyleSheet 来规划样式, 但是 UI 控件调用的是 RN 里自己的两端实现控件(android 和 IOS)



  • JavaScript 在 RN 的作用就是给原生组件发送指令来完成 UI 渲染, 所以 JavaScript Core 是 RN 中的核心部分



  • RN 是不用 JS 引擎的 UI 渲染控件的, 但是会用到 JS 引擎的 DOM 操作管理能力来管理所有 UI 节点, 每次在写完 UI 组件代码后会交给 yoga 去做布局排版, 然后调用原生组件绘制

  • bridge 负责js 和native的通讯,以android为例:Java层与Js层的bridge分别存有相同一份模块配置表,Java与Js互相通信时,通过bridge里的配置表将所调用模块方法转为{moduleID,methodID,args}的形式传递给处理层,处理层通过bridge的模块配置表找到对应的方法执行,如果有callback,则回传给调用层


image.png


通讯机制


Java -> Js: Java通过注册表调用到CatalystInstance实例,通过jni,调用到 javascriptCore,传递给调用BatchedBridge.js,根据参数{moduleID,methodID}require相应Js模块执行。


Js -> Java: JS不主动传递数据调用Java。在需要调用调Java模块方法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等待Java的事件触发,再把MessageQueue中的{moduleID,methodID}返回给Java,再根据模块注册表找到相应模块处理。


事件循环


JS 开发者只需要开发各个组件对象,监听组件事件, 然后利用framework接口调用render方法渲染组件。


而实际上,JS 也是单线程事件循环,不管是 API调用, virtural DOM同步, 还是系统事件监听, 都是异步事件,采用Observer(观察者)模式监听JAVA层事件, JAVA层会把JS 关心的事件通过bridge直接使用javascriptCore的接口执行固定的脚本, 比如"requrire (test_module).test_methode(test_args)"。此时,UI main thread相当于work thread, 把系统事件或者用户事件往JS层抛,同时,JS 层也不断调用模块API或者UI组件 , 驱动JAVA层完成实际的View渲染。JS开发者只需要监听JS层framework定义的事件即可


react 的渲染流程


image.png


首先回顾一下当前Bridge的运行过程。当我们写了类似下面的React源码。


<View style={{ backgroundColor: 'pink', width: 200, height: 200}}/>

JS thread会先对其序列化,形成下面一条消息


UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])

通过Bridge发到ShadowThread。Shadow Tread接收到这条信息后,先反序列化,形成Shadow tree,然后传给Yoga,形成原生布局信息。接着又通过Bridge传给UI thread。UI thread 拿到消息后,同样先反序列化,然后根据所给布局信息,进行绘制。


从上面过程可以看到三个线程的交互都是要通过Bridge,因此瓶颈也就在此。


image.png


首次渲染流程



  1. Native 打开 RN 页面

  2. JS 线程运行,Virtual DOM Tree 被创建

  3. JS 线程异步通知 Shadow Thread 有节点变更

  4. Shadow Thread 创建 Shadow Tree

  5. Shadow Thread 计算布局,异步通知 Main Thread 创建 Views

  6. Main Thread 处理 View 的创建,展示给用户


image.png


react native 新架构


image.png



  • JSI:JSI是Javascript Interface的缩写,一个用C++写成的轻量级框架,它作用就是通过JSI,JS对象可以直接获得C++对象(Host Objects)引用,并调用对应方法


另外JSI与React无关,可以用在任何JS 引擎(V8,Hermes)。有了JSI,JS和Native就可以直接通信了,调用过程如下:JS->JSI->C++->ObjectC/Java



  • Fabric 是 UI Manager 的新名称, 将负责 Native UI 渲染, 和当前 Bridge 不同的是, 可以通过 JSI 导出自己的 Native 函数, 在 JS 层可以直接使用这些函数引用, 反过来 Native 可以直接调用 JS 层, 从而实现同步调用, 这带来更好的数据传输和性能提升


image.png


对比


image.png


flutter:


生产环境中 Dart 通过 AOT 编译成对应平台的指令,同时 Flutter 基于跨平台的 Skia 图形库自建了渲染引擎,最大程度地保证了跨平台渲染的一致性


image.png



  • embedder: 可以称为嵌入器,这是和底层的操作系统进行交互的部分。因为flutter最终要将程序打包到对应的平台中,对于Android平台使用的是Java和C++,对于iOS和macOS平台,使用的是Objective-C/Objective-C++。



  • engine:Flutter engine基本上使用C++写的。engine的存在是为了支持Dart Framework的运行。它提供了Flutter的核心API,包括作图、文件操作、网络IO、dar运行时环境等核心功能。Flutter Engine线程的创建和管理是由embedder负责的。



  • Flutter framework: 这一层是用户编程的接口,我们的应用程序需要和Flutter framework进行交互,最终构建出一个应用程序。


Flutter framework主要是使用dart语言来编写的。framework从下到上,我们有最基础的foundational包,和构建在其上的 animation, painting和 gestures 。


再上面就是rendering层,rendering为我们提供了动态构建可渲染对象树的方法,通过这些方法,我们可以对布局进行处理。接着是widgets layer,它是rendering层中对象的组合,表示一个小挂件。


Widgets 理解


Widgets是Flutter中用户界面的基础。你在flutter界面中能够观察到的用户界面,都是Widgets。大的Widgets又是由一个个的小的Widgets组成,这样就构成了Widgets的层次依赖结构,在这种层次结构中,子Widgets可以共享父Widgets的上下文环境。在Flutter中一切皆可为Widget。


举例,这个Containerks 控件里的child,color,Text 都是Widget。


  color: Colors.blue,
child: Row(
children: [
Image.network('http://www.flydean.com/1.png'),
const Text('A'),
],
),
);

Widgets表示的是不可变的用户UI界面结构。虽然结构是不能够变化的,但是Widgets里面的状态是可以动态变化的。根据Widgets中是否包含状态,Widgets可以分为stateful和stateless widget,对应的类是StatefulWidget和StatelessWidget。


渲染和绘制


渲染就是将上面我们提到的widgets转换成用户肉眼可以感知的像素的过程。Flutter代码会直接被编译成使用 Skia 进行渲染的原生代码,从而提升渲染效率。


代码首先会形成widgets树如下,这些widget在build的过程中,会被转换为 element tree,其中ComponentElement是其他Element的容器,而RenderObjectElement是真正参与layout和渲染的element。。一个element和一个widget对应。然后根据elementrtree 中需要渲染的元素形成RenderTree ,flutter仅会重新渲染需要被重新绘制的element,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget。 最后还会一个layer tree,表示绘制的图层。



四棵树有各自的功能



image.png


Flutter绘制流程


image.png



  • Animate,触发动画更新下一帧的值

  • Build,触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree

  • Layout,触发布局操作,确定布局大小和位置信息

  • CompositeBits,更新需要合成的 Layer 层标记

  • Paint,触发 RenderObject Tree 的绘制操作,构建 Layer Tree

  • Composite,触发 Layer Tree 发送到 Engine,生成 Engine LayerTree


在 UIThread 构建出四棵树,并在 Engine 生成 Scene,最后提交给 RasterThread,对 LayerTree 做光栅化合成上屏。


Flutter 渲染流程


image.png




  • UIThread


    UIThread 是 Platform 创建的子线程,DartVM Root Isolate 所有的 dart 代码都运行在该线程。阻塞 UIThread 会直接导致 Flutter 应用卡顿掉帧。




  • RasterThread


    RasterThread 原本叫做 GPUThread,也是 Platform 创建的子线程,但其实它是运行在 CPU 用于处理数据提交给 GPU,所以 Flutter 团队将其名字改为 Raster,表明它的作用是光栅化。


    C++ Engine 中的光栅化和合成过程运行在该线程。




  • C++ Engine 触发 Platform 注册 VSync 垂直信号回调,通过 Platform -> C++ Engine -> Dart Framework 触发整个绘制流程




  • Dart Framework 构建出四棵树,Widget Tree、Element Tree、RenderObject Tree、Layer Tree,布局、记录绘制区域及绘制指令信息生成 flutter::LayerTree,并保存在 Scene 对象用以光栅化,这个过程运行在 UIThread




  • 通过 Flutter 自建引擎 Skia 进行光栅化和合成操作, 将 flutter::LayerTree 转换为 GPU 指令,并发送给 GPU 完成光栅化合成上屏显示操作,这个过程执行在 RasterThread




整个调度过程是生产者消费者模型,UIThread 负责生产 flutter::Layer Tree,RasterThread 负责消费 flutter::Layer Tree。


flutter 线程模型


image.png


Mobile平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。所有Engine实例共享同一个Platform Runner和线程。




  • Platform Task Runner


    Flutter Engine的主Task Runner,可以理解为是主线程,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。改线程不仅仅处理与Engine交互,它还处理来自平台的消息。




  • UI Task Runner Thread(Dart Runner)


    UI Task Runner被Flutter Engine用于执行Dart root isolate代码,Root isolate运行应用的main code。负责触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree,生成最终的Layer Tree。


    Root Isolate还是处理来自Native Plugins的消息响应,Timers,Microtasks和异步IO(isolate是有自己的内存和单线程控制的运行实体,isolate之间的内存在逻辑上是隔离的)。




  • GPU Task Runner


    GPU Task Runner中的模块负责将Layer Tree提供的信息转化为实际的GPU指令,执行设备GPU相关的skia调用,转换相应平台的绘制方式,比如OpenGL, vulkan, metal等。GPU Task Runner同时也负责配置管理每一帧绘制所需要的GPU资源




  • IO Task Runne




IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备


Dart 是单线程的,但是采用了Event Loop 机制,也就是不断循环等待消息到来并处理。在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。


image.png


isolate机制尽管 Dart 是基于单线程模型的,但为了进一步利用多核 CPU,将 CPU 密集型运算进行隔离,Dart 也提供了多线程机制,即 Isolate。每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。Isolate 通过发送管道(SendPort)实现消息通信机制。我们可以在启动并发 Isolate 时将主 Isolate 的发送管道作为参数传给它,这样并发 Isolate 就可以在任务执行完毕后利用这个发送管道给我们发消息。


如果需要在启动一个 Isolate 执行某项任务,Isolate 执行完毕后,发送消息告知我们。如果 Isolate 执行任务时,同时需要依赖主 Isolate 给它发送参数,执行完毕后再发送执行结果给主 Isolate这样的双向通信,让并发 Isolate 也回传一个发送管道即可。


weex:


架构:


image.png



  1. 将weex源码生成JS Bundle,由template、style 和 script等标签组织好的内容,通过转换器转换成JS Bundle

  2. 服务端部署JS Bundle ,将JS Bundle部署在服务器,当接收到终端(Web端、iOS端或Android端)的JS Bundle请求,将JS Bundle下发给终端

  3. WEEX SDK初始化,初始化 JS 引擎,准备好 JS 执行环境

  4. 构建渲染指令树,Weex 里都使用 DOM API 把 Virtual DOM 转换成真实的Element 树,而是使用 JS Framework 里定义的一套 Weex DOM API 将操作转化成渲染指令发给客户端,形成客户端的真实控件

  5. 页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通信用的是 callNative 和 callJS 这两个底层接口。callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用。callJS 是由 JS Framework 实现的,并且也注入到了执行环境中,提供给客户端调用。


渲染过程


Weex 里页面的渲染过程和浏览器的渲染过程类似,整体可以分为【创建前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送渲染指令】->【绘制原生 UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在 JS Framework 中处理,最后一步是由原生渲染引擎实现的。 页面渲染的大致流程如下


image.png


各家项目的实现方式:


淘宝新⼀代⾃绘渲染引擎 的架构与实践


Weex 技术发展历程


image.png


Weex 2.0 简版架构


最上层的前端生态还是没变的,应该还是以vue的响应式编程为主。


image.png


2.0多了js和c++的直接调用,减少js引擎和布局引擎的通讯开销。


image.png


image.png


Weex 2.0 重写了渲染层的实现,不再依赖系统 UI,改成依赖统一的图形库 Skia 自绘渲染,和 Flutter 原理很像,我们也直接复用了 Flutter Engine 的部分代码。底层把 Weex 对接过的各种原生能力、三方扩展模块都原样接入。对于上层链路,理论上讲业务 JS 代码、Vue/Rax、JS Framework 都是不需要修改的。在 JS 引擎层面也做了一些优化,安卓上把 JavaScriptCore 换成了 QuickJS,用 bytecode 加速二次打开性能,并且结合 Weex js bundle 的特点做针对性的优化。


字节码编译原理


image.png


渲染原理


渲染引擎通用的渲染管线可以简化为【执行脚本】-->【构建节点】-->【布局/绘制】--> 【合成/光栅化】--【上屏】这几个步骤。Weex 里的节点构建逻辑主要在 JS 线程里执行,提交一颗静态节点树到 UI 线程,UI 线程计算布局和绘制属性,生成 Layer Stack 提交到 GPU 线程。


image.png


天猫:WAFT:基于WebAssembly和Skia 的AIoT应用开发框架


整体方案


image.png


为什么选择WebAssemy?


支持 AOT 模式,拔高了性能上限;活跃的开源社区,降低项目推进的风险;支持多语言,拓宽开发者群体。


WebAssembly(又名wasm)是一种高效的,低级别的编程语言。 它让我们能够使用JavaScript以外的语言(例如C,C ++,Rust或其他)编写程序,然后将其编译成WebAssembly,进而生成一个加载和执行速度非常快的Web应用程序


WebAssembly是基于堆栈的虚拟机的二进制指令格式,它被设计为编程语言的可移植编译目标。目前很多语言都已经将 WebAssembly 作为它的编译目标了。


image.png


waft 开发方式


可以看到是采用类前端的开发方式,限定一部分css能力。最后编译为WebAssembly,打包成wasm bundle。 在进行aot 编译城不同架构下的机器码。


image.png


运行流程


可以看到bundle 加载过程中,会执行UI区域的不同的生命周期函数。然后在渲染过程则是从virtual dom tree 转化到widget tree,然后直接通过skia 渲染库直接进行渲染。


image.png


Waft 第二阶段成果-跨平台


image.png


美团KMM在餐饮SaaS中的探索与实践


KMP:Kotlin Multiplatform projects 使用一份kotlin 代码在不同平台上运行


KMM:Kotlin MultiplatformMobile 一个用于跨平台移动应用程序的 SDK。使用 KMM,可以构建多平台移动应用程序并在 Android 和 iOS 应用程序之间共享核心层和业务逻辑等方面。开发人员可以使用单一代码库并通过共享数据层和业务逻辑来实现功能。其实就是把一份逻辑代码编译为多个平台的产物编译中间产物,在不同平台的边缘后端下转为不同的变异后端产物,在不同平台下运行。


image.png


IR 全称是 intermediate representation,表示编译过程中的中间信息,由编译器前端对源码分析后得到,随后会输入到后端进一步编译为机器码


IR 可以有一系列的表现方式,由高层表示逐渐下降(lowering)到低层


我们所讨论的 Kotlin IR 是抽象语法树结构(AST),是比较高层的 IR 表示类型。


有了完备的 IR,就可以利用不同的 后端,编出不同的目标代码,比如 JVM 的字节码,或者运行在 iOS 的机器码,这样就达到了跨端的目的


image.png


对比


image.png


总结


当前存在4种多端方案:



  1. Web 容器方案

  2. 泛 Web 容器方案

  3. 自绘引擎方案

  4. 开放式跨端框架


image.png


引用文章:


zhuanlan.zhihu.com/p/20259704​​


zhuanlan.zhihu.com/p/281238593​​


zhuanlan.zhihu.com/p/388681402​​


juejin.cn/post/708412…​​


guoshuyu.cn/home/wx/Flu…​​


oldbird.run/flutter/u11…​​


w4lle.com/2020/11/09/…​​


blog.51cto.com/jdsjlzx/568…​​


http://www.devio.org/2021/01/10/…​​


gityuan.com/flutter/​​


gityuan.com/2019/06/15/…​​


zhuanlan.zhihu.com/p/78758247​​


http://www.finclip.com/news/f/5

作者:美好世界
来源:juejin.cn/post/7249624871721041975
035…​​

收起阅读 »

年终奖也许会迟到,年中(终)绩效可不会

它来了,它来了,它抱着恐怖的绩效跑来了!!! 公司惯例,年中绩效考核,年终奖也许会迟到,年中(终)绩效可不会!!! 公司考核标准基于·smart·原则: 按照公司的标准,作为领导者写下了通用类年中评价的套话, 千变万变套路不变。 年中评价 项目组: **...
继续阅读 »

它来了,它来了,它抱着恐怖的绩效跑来了!!!

公司惯例,年中绩效考核,年终奖也许会迟到,年中(终)绩效可不会!!!


公司考核标准基于·smart·原则:
51578860_36515712.719082.jpg


按照公司的标准,作为领导者写下了通用类年中评价的套话, 千变万变套路不变。


年中评价


项目组: ***平台--***监管项目组


产品列表



  • ****平台·PC端

  • ****平台·移动端

  • ****服务平台

  • **平台·医院端

  • **平台·患者端

  • **平台·管理端

  • **平台·药房端

  • ***数据资产·PC端

  • ***数据资产·移动端


测试组(1-6 月)


测试人员:测试A同学测试B同学测试C同学测试D同学


测试-推荐模板




  • 引言部分:开始评语时,可以表达对员工的认可和欣赏,例如强调其专业能力、工作态度和对团队的贡献(对测试人员半年内的工作认可)。




  • 工作表现:评价员工在测试工作方面的表现。可以包括以下内容:



    • 质量控制:评估员工在确保产品质量方面的能力,包括发现和报告问题、提供准确的测试结果等。

    • 测试方法和策略:评价员工在测试方法和策略上的创新能力和应用情况,以提高测试效率和覆盖范围。

    • 缺陷管理:考察员工在缺陷管理方面的能力,包括及时跟踪和解决缺陷、与开发团队合作等。





  • 团队合作:强调员工在团队合作中的表现。包括:



    • 与开发人员的合作:评价员工与开发人员的有效沟通和协作,以促进问题解决和改进产品质量(与开发对接,与产品对接)。

    • 跨部门合作:考察员工与其他团队成员、产品经理等部门的合作,以确保测试工作与整个开发流程的顺畅衔接(与运营沟通,与合作团队沟通)。





  • 发展潜力:讨论员工的发展潜力和个人成长方面的表现。包括:



    • 学习能力:评价员工学习新技能和掌握新技术的能力,以提高测试工作的效率和质量。

    • 自我提升:强调员工在自我提升方面的积极性,例如主动参加培训、学习新的测试工具和技术等(跨领域 多涉猎 了解前后端相关基础知识)。





  • 目标设定:与员工一起讨论并设定下一阶段的发展目标和改进计划。确保目标具体、可衡量和可达成,并提供必要的支持和资产(定目标,完善现有流程的缺陷)。




  • 总结和鼓励:总结评语时,强调员工在测试工作中的优点和成就,并鼓励其继续努力和发展。




测试-参考评语



  • 引言部分:我非常欣赏你在过去半年中在测试团队中的出色表现。

  • 工作表现:你的精确测试和准确的缺陷报告帮助我们提高了产品质量,使得我们的监管平台系统能够更高效地发现和解决问题;在最近的**流转平台·患者端中,你的测试工作帮助我们发现并解决了重要的问题,确保了产品的稳定性和可靠性;积极与开发人员合作,通过有效的沟通和合作解决了许多测试和开发之间的问题;

  • 发展潜力:希望你进一步拓展自己的技能,参与更多复杂项目的测试,并考虑接触新的测试方法和领域,以更好地发展自己的测试职业生涯;

  • 目标设定: 期待你在下半年中继续提高测试覆盖率和质量,尤其是在自动化测试方面的能力;

  • 总结和鼓励:你在过去的半年里展现出了坚韧的工作态度、出色的技术能力和团队精神;对你的未来发展充满信心,相信你将继续在测试领域中取得更大的成就。


测试-完整评语


  我非常欣赏你在过去半年中在测试团队中的出色表现;你的精确测试和准确的缺陷报告帮助我们提高了产品质量,使得我们的监管平台系统能够更高效地发现和解决问题;在最近的**流转平台·患者端中,你的测试工作帮助我们发现并解决了重要的问题,确保了产品的稳定性和可靠性;积极与开发人员合作,通过有效的沟通和合作解决了许多测试和开发之间的问题;

  希望你进一步拓展自己的技能,参与更多复杂项目的测试,并考虑接触新的测试方法和领域,以更好地发展自己的测试职业生涯;期待你在下半年中继续提高测试覆盖率和质量,尤其是在自动化测试方面的能力;

  你在过去的半年里展现出了坚韧的工作态度、出色的技术能力和团队精神;对你的未来发展充满信心,相信你将继续在测试领域中取得更大的成就。


测试-有待改进


无自动化测试


无接口测试


无性能测试


无测试用例


. . . 待补充


开发组(1-6 月)


后端人员: 后端A同学后端B同学后端C同学后端D同学后端E同学后端F同学

前端人员: 前端A同学前端B同学前端C同学前端D同学前端E同学, 前端F同学前端G同学前端H同学


开发-推荐模板




  • 引言部分:开门见山地表达对开发人员的肯定和欣赏




  • 技术能力和贡献



    • 强调开发人员在技术方面的能力和专业知识

    • 举例说明开发人员在某个具体项目或任务中的成就和贡献(具体工作模块 以及相关业务梳理

    • 强调开发人员在团队合作中的积极性和贡献(团队文档 主动分担任务





  • 创新和解决问题能力



    • 强调开发人员的创新意识和解决问题的能力(bug 跟进 解决效率

    • 举例说明开发人员在解决技术难题或改进现有系统方面的成果(实际项目****平台,****管理端,****患者端,****药房端,***数据资产)





  • 质量和效率



    • 强调开发人员在代码质量和开发效率方面的表现(暂时无bug统计,不好评估)。

    • 提及开发人员采用的工具和技术,以提高开发效率和质量(CI/CD Jenkins 脚手架)。





  • 发展潜力和建议



    • 强调开发人员的学习能力和自我提升意愿

    • 提供具体的建议和指导,帮助开发人员进一步发展和提升





  • 总结和展望



    • 总结开发人员在过去半年的工作表现,并再次表达对其的肯定和感谢(跨部门项目技术支持)

    • 展望未来,激励开发人员继续努力和成长(跨领域 多涉猎)




开发-参考评语



  • 引言部分:我非常欣赏你在过去半年中在开发团队中的出色表现。

  • 技术能力和贡献:你的编码技巧和架构设计使得我们能够按时交付了一个功能强大且稳定的产品;积极与测试人员和产品经理合作,确保需求理解准确,并及时响应和解决问题。

  • 创新和解决问题能力:你提出了一些创新的想法和解决方案,帮助我们改进了产品的用户体验和性能;

  • 质量和效率: 你的代码规范和代码审查质量有助于减少缺陷率,并提高了整体代码质量;你的使用和推广新的开发工具(CI/CD Jenkins)自动化测试框架(暂无)为团队节省了大量时间和精力。

  • 发展潜力和建议: 建议你继续扩展你的技术广度,并积极参与跨部门项目,以进一步发展你的领导能力。

  • 总结和展望:总体而言,你在过去半年的表现令人印象深刻,感谢你为团队做出的出色贡献;相信在你的持续努力下,你将继续在技术领域中取得更大的突破和成功。


开发-个人-完整评语


  你展现出了卓越的技术能力和深厚的专业知识。在应对各种技术挑战和难题时,展现了出色的解决问题的能力。你熟练掌握各种编程语言和技术工具,并能够灵活应用它们来实现高质量的代码和解决方案。

  你在开发过程中展现出了创新和创造力。你不断寻找新的解决方案和优化方法,通过引入新技术和工具,推动产品的创新和发展。你的创造力为我们带来了许多惊喜和突破。

  你的编码技巧和架构设计使得我们能够按时交付了一个功能强大且稳定的产品;积极与测试人员和产品经理合作,确保需求理解准确,并及时响应和解决问题;提出了一些创新的想法和解决方案,帮助我们的产品改进了产品的用户体验和性能;代码规范和代码审查质量有助于减少缺陷率,并提高了整体代码质量;

  你持续学习和追求个人成长。参加培训课程、研讨会和行业活动,不断更新自己的技术知识和技能。你的学习精神和求知欲将帮助我们保持在行业的前沿。

  建议你继续扩展你的技术广度,并积极参与跨部门项目,以进一步发展你的领导能力。
  感谢你们在过去的半年里付出的努力和奉献。你们的工作和成就为整个团队带来了无限的骄傲和信心。我期待着在未来的工作中继续与你们携手合作,共同创造更加辉煌的业绩。


开发-有待改进


无前端异常监控


无接口自动化


无Bug统计


. . . 待补充


产品组(1-6 月)


产品人员: 产品A同学产品B同学产品C同学产品D同学


产品-推荐模板




  • 引言部分:开门见山地表达对产品经理的赞赏和认可(后续可提出不满意地方)




  • 产品规划和战略



    • 强调产品经理在产品规划和战略方面的能力.

    • 举例说明产品经理在某个具体项目或产品中的规划和执行能力





  • 需求管理和沟通



    • 强调产品经理在需求管理和沟通方面的表现(产品文档 产品说明 需求管理

    • 提及产品经理与客户或用户之间的良好关系和沟通能力(与开发 与测试 与运营





  • 项目管理和协调能力



    • 强调产品经理在项目管理和协调方面的能力(需求池 量把控 人员协调 任务分配

    • 提及产品经理在团队合作中的积极性和协作精神





  • 用户体验和市场反馈(运营/医院/领导)



    • 强调产品经理对用户体验和市场反馈的重视和应对能力

    • 举例说明产品经理在改进产品体验方面的成果(产品反馈 响应速度 优化





  • 发展潜力和建议



    • 强调产品经理的发展潜力和提升空间(医疗业务能力 产品转换能力 业务能力)。

    • 提供具体的建议和指导,帮助产品经理进一步发展和提升。





产品-参考评语



  • 引言部分:回顾过去的半年,我想表达对你在项目中的杰出表现和卓越工作的赞赏。你展现出的才华、责任心和敬业精神让整个团队都感到鼓舞和激励。(套话

  • 产品规划和战略:你对市场趋势的敏锐洞察力和战略眼光使得我们能够制定出有前瞻性和竞争力的产品路线图

  • 需求管理和沟通:你的处方流转产品规划和推进使得我们成功地推出了一款满足客户需求并取得市场成功的产品

  • 项目管理和协调能力:作为产品经理,你成功地协调了跨部门的合作,与开发团队、设计团队和测试团队紧密合作,确保项目的顺利推进。你的沟通和协调能力为团队提供了强大的支持,并帮助我们克服了许多挑战.

  • 用户体验和市场反馈(运营/医院/领导):你不断关注用户体验和市场反馈,并能够及时调整产品策略和功能,以满足运营、医院的需求;你的用户调研和用户测试帮助我们发现并解决了产品中的痛点,提升了用户满意度.

  • 发展潜力和建议: 作为产品经理,技术理解能力对于与开发团队的有效沟通和协作至关重要。继续加强对技术的学习和理解,与开发团队密切合作,深入了解产品的技术实施细节,以便更好地与团队协作并推动产品的技术实现


产品-个人-完整评语


  回顾过去的半年,我想表达对你在项目中的杰出表现和卓越工作的赞赏。你展现出的才华、责任心和敬业精神让整个团队都感到鼓舞和激励。

  你展现了出色的洞察力和理解能力,能够深入了解用户的需求和期望。你与用户进行积极的沟通和交流,从而确保我们的产品能够准确地满足他们的需求;在产品规划和策划方面展现出卓越的能力。你能够将复杂的业务需求转化为清晰、可执行的产品规划,并制定了一系列的策略和目标,使整个团队能够朝着共同的方向努力。

  作为产品经理,你成功地协调了跨部门的合作,与开发团队、设计团队和测试团队紧密合作,确保项目的顺利推进。你的沟通和协调能力为团队提供了强大的支持,并帮助我们克服了许多挑战;你以身作则,展现出卓越的项目管理技巧和能力。你能够有效地制定项目计划、管理进度和资源,确保项目按时交付,并保持高质量的工作成果。你的组织能力和决策能力使整个团队能够高效地工作

  行业和技术的发展变化很快,作为产品经理,需要不断学习和保持敏锐的洞察力。参加行业会议、研讨会和培训课程,与同行交流经验,持续学习新的工具和技能,保持对行业趋势的关注

  我相信你拥有无限的潜力,只要持续努力和学习,你将能够在产品经理的职业道路上取得更大的成就。请继续保持对工作的热情和责任心,发挥你的创造力和领导能力,我期待在未来的工作中见证你的成长和成功。


产品-有待改进


需求调研能力不足


需求内审力度不足


医疗业务能力不足


产品整合能力不足


. . . 待补充






愿你我远离套路,投进,,的怀抱!!!


以上内容仅代表个人观点!!!


下班~~~


7-21062Q025150-L.gif


下图提供下载参考:
年中网络版.png

收起阅读 »

如何实现比 setTimeout 快 80 倍的定时器?

web
很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard 规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。



更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?


探索


假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的 window.postMessage()



这篇文章里的作者给出了这样一段代码,用 postMessage 来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessageaddEventListener('message') 的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:



全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。


作者设计了一个实验方法,就是分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过 setZeroTimeout 也就是 postMessage 版本来递归计数到 100,然后切换成 setTimeout 计数到 100。


直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。



Performance 面板


只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage 版的定时器和 setTimeout 版的定时器是如何分布的。



这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


通过本文,你大概可以了解如下几个知识点:



  1. setTimeout 的 4ms 延迟历史原因,具体表现。

  2. 如何通过 postMessage 实现一个真正 0 延迟的定时器。

  3. postMessage 定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7249633061440749628

收起阅读 »

项目提交按钮没防抖,差点影响了验收

web
前言 一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误...
继续阅读 »

前言


一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误个别单据流程给弄不正常了,一些报表的数据统计也不对了,客户相关人员很不满意,马上该交付了,出这问题可还了得,项目款不按时给了,这责任谁都担不起🤣


QQ图片20230627163527.jpg


领导紧急组织相关技术人员开会分析原因


初步分析原因


发生这个情况前端选手应该会很清楚这是怎么回事,明显是项目里的按钮没加防抖导致的,按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。


看下项目情况


用到的框架和技术


项目使用 angular8 ts devextreme 组合。对!这就是之前文章提到的那个屎山项目(试用期改祖传屎山是一种怎么样的体验


项目规模


业务单据页面大约几百个,项目里面的按钮几千个,项目里面的按钮由于场景复杂,分别用了如下几种写法:



  • dx-button

  • div

  • dx-icon

  • input type=button

  • svg


由于面临交付,领导希望越快越好,最好一两天之内解决问题


还好我们领导没有说这问题当天就要解决 😁


解决方案


1. 添加防抖函数


按钮点击添加防抖函数,设置合理的时间


function debounce(func, wait) {
let timeout;
return function () {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait)
}
}

优点


封装一个公共函数,往每个按钮的点击事件里加就行了


缺点


这种情况有个问题就是在业务复杂的场景下,时间设置会比较棘手,如果时间设置短了,接口请求慢,用户多次点击还会出现问题,如果时间设置长了,体验变差了


2. 设置按钮禁用


设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用


this.disabled = true
this.disabled = false

优点


原生按钮和使用的UI库的按钮设置简单


缺点


div, icon, svg 这种自定义的按钮的需要单独处理效果,比较麻烦


3. 请求拦截器中添加loading


在请求拦截器中根据请求类型显示 loading,请求结束后隐藏


优点


直接在一个地方设置就行了,不用去业务代码里一个个加


缺点


由于我们的技术栈使用的 angular8 内置的请求,无法实现类似 axios 拦截器那种效果,还有就是项目中的接口涉及多个部门的接口,不同部门的规范命名不一样,没有统一的标准,在实际的业务场景中,一个按钮的行为可能触发了多个请求,因此这个方案不适合当前的项目


4. 添加 loading 组件(项目中使用此方案)


新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。


loading 组件核心代码


import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private isLoading$ = new BehaviorSubject<boolean>(false);
  private message$ = new BehaviorSubject<string>('正在加载中...');
  constructor() {}
  show(): void {
    this.isLoading$.next(true);
  }
  hide(): void {
    this.isLoading$.next(false);
  }
}

主要是 show()hide() 函数,将 loading 组件绑定到 app.components.ts 中,绑定组件到window 对象上,


window['loading'] = this.loadingService

在按钮点击时触发 show() 函数,业务代码执行结束后触发 hide() 函数


window['loading'].show();
window['loading'].hide();

优点


这种方式很好的解决了问题,由于 loading 有遮罩层还避免了用户点击某提交按钮后,接口响应慢,这时候去点击了别的操作按钮的情况。


缺点


需要在业务单据的按钮提交的地方一个个加


问题来了,一两天解决所有问题了吗?


QQ图片20230627165837.png


这么大的项目一两天不管哪种方案,把所有按钮都处理好是不现实的,经过分析讨论,最终选择了折中处理,先把客户提出来的几个业务单据页面,以及相关的业务单据页面添加上提交 loading 处理,然后再一边改 bug 一边完善剩余的地方,优先保证客户正常使用



还有更好的解决思路吗?欢迎JYM讨论交流


作者:草帽lufei
来源:juejin.cn/post/7249288087820861499

收起阅读 »

面试官问:如何实现 H5 秒开?

web
我在简历上写了精通 H5,结果面试官上来就问: 同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开? 由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点: 网络优化:http2、dns 预解析、使用 CDN 图片优化:压缩、懒加...
继续阅读 »

我在简历上写了精通 H5,结果面试官上来就问:



同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开?



image.png


由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点:




  • 网络优化:http2、dns 预解析、使用 CDN

  • 图片优化:压缩、懒加载、雪碧图

  • 体积优化:分包、tree shaking、压缩、模块外置

  • 加载优化:延迟加载、骨架屏

  • ...



看得出来面试官不太满意,最后面试也挂了。于是我请教了我的好友 Gahing ,问问他的观点。



Gahing:


你列的这些优化手段本身没啥问题,如果是一个工作一两年的我会觉得还可以。但你已经五年以上工作经验了,需要有一些系统性思考了。



好像有点 PUA 的味道,于是我追问道:什么是系统性的思考?



Gahing:


我们先说回答方式,你有没有发现,你回答时容易遗漏和重复。


比如说「图片懒加载」,你归到了「图片优化」,但其实也可以归到「加载优化」。同时你还漏了很多重要的优化手段,比如资源缓存、服务端渲染等等。


究其原因应该是缺少抽象分类方法。



那针对这个问题,应该如何分类回答?



Gahing:


分类并非唯一,可以有不同角度,但都需遵从 MECE 原则(相互独立、完全穷尽) ,即做到不重不漏




  • 按页面加载链路分类:容器启动、资源加载、代码执行、数据获取、绘制渲染。




  • 按资源性能分类:CPU、内存、本地 I/O、网络。该分类方法又被叫做 USE 方法(Utilization Saturation and Errors Method)




  • 按协作方分类:前端、客户端、数据后台、图片服务、浏览器引擎等。




  • 按流程优化分类前置、简化、拆分



    • 前置即调整流程,效果上可能是高优模块前置或并行,低优模块后置;

    • 简化即缩减或取消流程,体积优化是简化,执行加速也是简化;

    • 拆分即细粒度拆解流程,本身没有优化效果,是为了更好的进行前置和简化。

    • 这个角度抽象层次较高,通常能回答出来的都是高手。




  • 多级分类:使用多个层级的分类方法。比如先按页面加载链路分类,再将链路中的每一项用协作方或者流程优化等角度再次分类。突出的是一个系统性思维。




选择好分类角度,也便于梳理优化方案的目标。



现在,尝试使用「页面加载链路+流程优化+协作方」的多级分类思维,对常见的首屏性能优化手段进行分类。


image.png


PS: 可以打开飞书文档原文查看思维导图


好像有点东西,但是我并没有做过性能优化,面试官会觉得我在背八股么?



Gahing:


可以没有实操经验,但是得深入理解。随便追问一下,比如「页面预渲染效果如何?有什么弊端?什么情况下适用?」,如果纯背不加理解的话很容易露馅。


另外,就我个人认为,候选人拥有抽象思维比实操经验更重要,更何况有些人的实操仅仅是知道怎么做,而不知道为什么做。



那我按上面的方式回答了,能顺利通过面试么 🌝 ?



Gahing:


如果能按上面的抽象思维回答,并顶住追问,在以前应该是能顺利通过面试的(就这个问题)。


但如今行业寒冬,大厂降本增效,对候选人提出了更高的要求,即系统性思考业务理解能力


从这个问题出发,如果想高分通过,不仅需要了解优化方案,还要关注研发流程、数据指标、项目协作等等,有沉淀自己的方法论和指导性原则,能实施可执行的 SOP。。




最后,我还是忍不住问了 Gahing :如果是你来回答这个问题,你会怎么回答?



Gahing:


H5 秒开是一个系统性问题,可以从深度和广度两个方向来回答。


深度关注的是技术解决方案,可以从页面加载链路进行方案拆解,得到容器启动、资源加载、代码执行、数据获取、绘制渲染各个环节。其中每个环节还可以从协作方和流程优化的角度进一步拆解。


广度关注的是整个需求流程,可以用 5W2H 进行拆解,包括:



  • 优化目标(What):了解优化目标,即前端首屏加载速度

  • 需求价值(Why):关注需求收益,从技术指标(FMP、TTI)和业务指标(跳失率、DAU、LT)进行分析

  • 研发周期(When):从开发前到上线后,各个环节都需要介入

  • 项目协作(Who):确定优化专项的主导方和协作方

  • 优化范围(Where):关注核心业务链路,确定性能卡点

  • 技术方案(How):制定具体的优化策略和行动计划

  • 成本评估(How much):评估优化方案的成本和效益。考虑时间、资源和预期收益,确保优化方案的可行性和可持续性。


通过 5W2H 分析法,可以建立系统性思维,全面了解如何实现 H5 秒开,并制定相应的行动计划来改进用户体验和页面性能。





限于篇幅,后面会单独整理两篇文章来聊聊关于前端首屏优化的系统性思考以及可实施的解决方案。


👋🏻 Respect!欢迎一键三连 ~


作者:francecil
来源:juejin.cn/post/7249665163242307640
收起阅读 »

日常宕机?聊聊内存存储的Redis如何持久化

Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。 Redis 中的两种持久化方式:...
继续阅读 »

Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。


Redis 中的两种持久化方式: RDB(Redis DataBase)和 AOF(Append Of File)


1. RDB(Redis DataBase)


在指定的 时间间隔内 将内存中的数据集 快照 写入磁盘,也就是行话讲的快照(Snapshot),它恢复时是将快照文件直接读到内存里


1.1 原理


不使用Fork存在的问题





  • Redis 是一个 单线程 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。




  • 在 持久化的同时内存数据结构 还可能在 变化,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束。





Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是 最后一次持久化后的数据可能丢失


1.2 fork 函数


根据操作系统多进程 COW(Copy On Write) 机制Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存里面的代码块和数据段。




  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术

  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。



1.3 RDB流程图


image.png


1.4 RDB相关配置


1.4.1 配置文件



  • RDB文件默认在redis主目录下的 dump.rdb


image.png



  • 快照默认的保持策略




  1. 先前的快照是在3600秒(1小时)前创建的,并且现在已经至少有 1 次新写入,则将创建一个新的快照;

  2. 先前的快照是在300秒(5分钟)前创建的,并且现在已经至少有 100 次新写入,则将创建一个新的快照;

  3. 先前的快照是在60秒(1分钟)前创建的,并且现在已经至少有 10000 次新写入,则将创建一个新的快照;



image.png


1.4.2 相关指令


save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。


bgsave: Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。


lastsave:获取最后一次成功执行快照的时间


image.png


1.5 RDB如何备份



  1. config get dir  查询rdb文件的目录

  2. 关闭Redis

  3. 将备份文件 dump.rdb 移动到 redis 安装目录并启动 redis 服务即可,备份数据会直接加载。


image.png


2. AOF(Append Of File)


RDB快照不是很持久。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 kill -9 的实例意外发生,则写入 Redis 的最新数据将丢失。


2.1 原理


AOF(Append Of File) 是以 日志 的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来( 读操作不记录 ) , 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作


2.2 AOF流程图



  1. 客户端的请求写命令会被append追加到AOF缓冲区内;

  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;


image.png


2.3 AOF相关配置


2.3.1 AOF启动配置


image.png


配置文件默认关闭AOF,将配置文件设置为 appendonly yes 启动AOF。


AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)


2.3.2 AOF 同步fsync频率设置


image.png



  • appendfsync always始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

  • appendfsync everysec每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

  • appendfsync noredis不主动进行同步,把同步时机交给操作系统。


借助 glibc 提供的 fsync(int fd) 函数来讲指定的文件内容 强制从内核缓存刷到磁盘。但  "强制开车"  仍然是一个很消耗资源的一个过程,需要  "节制" !通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 fsync 操作就可以了。


Redis 同样也提供了另外两种策略,一个是 永不 fsync,来让操作系统来决定合适同步磁盘,很不安全,另一个是 来一个指令就 fsync 一次,非常慢。但是在生产环境基本不会使用,了解一下即可。


2.3.3 查看appendonly.aof文件


image.png
image.png
最后一条del non_existing_key没有追加到appendonly.aof文件中,因为它没有对数据实际造成修改


2.4 AOF如何备份


修改默认的appendonly no,改为yes


2.4.1 正常恢复



  1. config get dir  查询rdb文件的目录

  2. 关闭Redis

  3. 将备份文件 appendonly.aof 移动到 redis 安装目录并启动 redis 服务即可,备份数据会直接加载。


2.4.2 异常恢复



  1. 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复备份被写坏的AOF文件。

  2. 恢复:重启redis,备份数据会直接加载。


2.5 Rewrite重写


Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志 "瘦身"
Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其 原理 就是 开辟 (fork) 一个子进程 对内存进行 遍历 转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件 中。序列化完毕后再将操作期间发生的 增量 AOF 日志 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。


2.5.1 配置文件


image.png



  • no-appendfsync-on-rewrite=yes:不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

  • no-appendfsync-on-rewrite=no:还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)


image.png



  • auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

  • auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。



例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB


系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,


如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。



2.5.2 Rewrit流程图




  1. bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

  2. 主进程fork出子进程执行重写操作,保证主进程不会阻塞。

  3. 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

  4. 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

  5. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。



image.png


3. 总结


3.1 Redis 4.0 混合持久化


重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。


Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小,于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。


3.2 官方建议



  • 官方推荐两个都启用。

  • 如果对数据不敏感,可以选单独用RDB。

  • 不建议单独用 AOF,因为可能会出现Bug。

  • 如果只是做纯内存缓存,可以都
    作者:芒猿君
    来源:juejin.cn/post/7249382407245037623
    不用。

收起阅读 »

Spring Cloud 框架优雅关机和重启

背景 我们编写的Web项目部署之后,经常会因为需要进行配置变更或功能迭代而重启服务,单纯的kill -9 pid的方式会强制关闭进程,这样就会导致服务端当前正在处理的请求失败,那有没有更优雅的方式来实现关机或重启呢? 优雅停机 在项目正常运行的过程中,如果直接...
继续阅读 »

背景


我们编写的Web项目部署之后,经常会因为需要进行配置变更或功能迭代而重启服务,单纯的kill -9 pid的方式会强制关闭进程,这样就会导致服务端当前正在处理的请求失败,那有没有更优雅的方式来实现关机或重启呢?


优雅停机


在项目正常运行的过程中,如果直接不加限制的重启可能会发生一下问题



  1. 项目重启(关闭)时,调用方可能会请求到已经停掉的项目,导致拒绝连接错误(503),调用方服务会缓存一些服务列表导致,服务列表依然还有已经关闭的项目实例信息

  2. 项目本身存在一部分任务需要处理,强行关闭导致这部分数据丢失,比如内存队列、线程队列、MQ 关闭导致重复消费


为了解决上面出现的问题,提供以下解决方案:



  1. 关于问题 1 采用将需要重启的项目实例,提前 40s 从 nacos 上剔除,然后再重启对应的项目,保证有 40s 的时间可以用来服务发现刷新实例信息,防止调用方将请求发送到该项目

  2. 使用 Spring Boot 提供的优雅停机选项,再次预留一部分时间

  3. 使用 shutdonwhook 完成自定的关闭操作


一、主动将服务剔除


该方案主要考虑因为服务下线的瞬间,如果 Nacos 服务剔除不及时,导致仍有部分请求转发到该服务的情况


在项目增加一个接口,同时在准备关停项目前执行 stop 方法,先主动剔除这个服务,shell 改动如下:


run.sh


function stop()  
{
echo "Stop service please waiting...."
echo "deregister."
curl -X POST "127.0.0.1:${SERVER_PORT}/discovery/deregister"
echo ""
echo "deregister [${PROJECT}] then sleep 40 seconds."
# 这里 sleep 40 秒,因为 Nacos 默认的拉取新实例的时间为 30s, 如果调用方不修改的化,这里应该最短为 30s
# 考虑到已经接收的请求还需要一定的时间进行处理,这里预留 10s, 如果 10s 还没处理完预留的请求,调用方肯定也超时了
# 所以这里是 30 + 10 = 40sleep 40
kill -s SIGTERM ${PID}
if [ $? -eq 0 ];then
echo "Stop service done."
else
echo "Stop service failed!"
fi
}

在项目中增加 /discovery/deregister 接口


Spring Boot MVC 版本


import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@RestController
@RequestMapping("discovery")
@Slf4j
public class DeregisterInstanceController {

@Autowired
@Lazy
private ServiceRegistry serviceRegistry;

@Autowired
@Lazy
private Registration registration;


@PostMapping("deregister")
public ResultVO<String> deregister() {
log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());
try {
serviceRegistry.deregister(registration);
} catch (Exception e) {
log.error("deregister from nacos error", e);
return ResultVO.failure(e.getMessage());
}
return ResultVO.success();
}
}

Spring Cloud Gateway


通过使用 GatewayFilter 方式来处理


package com.br.zeus.gateway.filter;  

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;

import com.alibaba.fastjson.JSON;
import com.br.zeus.gateway.entity.RulesResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Component
@Slf4j
public class DeregisterInstanceGatewayFilter implements GatewayFilter, Ordered {

@Autowired
@Lazy
private ServiceRegistry serviceRegistry;

@Autowired
@Lazy
private Registration registration;

public DeregisterInstanceGatewayFilter() {
log.info("DeregisterInstanceGatewayFilter 启用");
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isAlreadyRouted(exchange)) {
return chain.filter(exchange);
}

log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());

RulesResult result = new RulesResult();
try {
serviceRegistry.deregister(registration);
result.setSuccess(true);
} catch (Exception e) {
log.error("deregister from nacos error", e);
result.setSuccess(false);
result.setMessage(e.getMessage());
}

ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(result));
response.setStatusCode(HttpStatus.OK);
return response.writeWith(Mono.just(bodyDataBuffer));
}

@Override
public int getOrder() {
return 0;
}
}


在路由配置时,增加接口和过滤器的关系


.route("DeregisterInstance", r -> r.path("/discovery/deregister")  
.filters(f -> f.filter(deregisterInstanceGatewayFilter))
.uri("https://example.com"))

二、Spring Boot 自带的优雅停机方案


要求 Spring Boot 的版本大于等于 2.3


在配置文件中增加如下配置:


application.yaml


server:  
shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 10s

当使用 server.shutdown=graceful 启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。使用 timeout-per-shutdown-phase 配置最长等待时间,超过该时间后关闭


三、使用 ShutdownHook


public class MyShutdownHook {
public static void main(String[] args) {
// 创建一个新线程作为ShutdownHook
Thread shutdownHook = new Thread(() -> {
System.out.println("ShutdownHook is running...");
// 执行清理操作或其他必要的任务
// 1. 关闭 MQ
// 2. 关闭线程池
// 3. 保存一些数据
});

// 注册ShutdownHook
Runtime.getRuntime().addShutdownHook(shutdownHook);

// 其他程序逻辑
System.out.println("Main program is running...");

// 模拟程序执行
try {
Thread.sleep(5000); // 假设程序运行5秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}

// 当程序退出时,ShutdownHook将被触发执行
}
}
作者:双鬼带单
来源:juejin.cn/post/7249286832168566840

收起阅读 »

websocket 实时通信实现

web
轮询和websocket对比 开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式: 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果 这种方式比较古老,但是兼容性...
继续阅读 »

轮询和websocket对比


开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式:




  1. 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果


    这种方式比较古老,但是兼容性强。


    缺点就是不断请求,耗费了大量的带宽和 CPU 资源,而且存在一定的延迟性




  2. websocket 长连接:全双工通信,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,更加方便




websocket 实现


创建 websocket 连接


建立ws连接,有如下两种形式:


ws 代表明文,默认端口号为 80,例如ws://http://www.example.com:80, 类似http


wss 代表密文,默认端口号为 443,例如wss://http://www.example.com:443, 使用SSL/TLS加密,类似https


const useWebSocket = (params: wsItem) => {
// 定义传参 url地址 phone手机号
let { url = "", phone = "" } = params;
const ws = (useRef < WebSocket) | (null > null);
// ws数据
const [wsData, setMessage] = (useState < wsDataItem) | (null > null);
// ws状态
const [readyState, setReadyState] =
useState < any > { key: 0, value: "正在连接中" };
// 是否在当前页
const [isLocalPage, setIsLocalPage] = useState(true);

// 创建Websocket
const createWebSocket = () => {
try {
window.slWs = ws.current = new WebSocket(
`wss://${url}/ws/message/${phone}`
);
// todo 全局定义发送函数
window.slWs.sendMessage = sendMessage;
// todo 准备初始化
initWebSocket();
} catch (error) {
// 创建失败需要进行异常捕获
slLog.error("ws创建失败", error);
// todo 准备重连
reconnect();
}
};

return { isLocalPage, wsData, closeWebSocket, sendMessage };
};

初始化 websocket


当前的连接状态定义如下,使用常量数组控制:


const stateArr = [
{ key: 0, value: "正在连接中" },
{ key: 1, value: "已经连接并且可以通讯" },
{ key: 2, value: "连接正在关闭" },
{ key: 3, value: "连接已关闭或者没有连接成功" },
];

主要有四个事件,连接成功的回调函数(onopen)、连接关闭的回调函数(onclose)、连接失败的回调函数(onerror)、收到消息的回调函数(onmessage)


const initWebSocket = () => {
ws.current.onopen = (evt) => {
slLog.log("ws建立链接", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 心跳检查重置
keepHeartbeat();
};
ws.current.onclose = (evt) => {
slLog.log("ws链接已关闭", evt);
};
ws.current.onerror = (evt) => {
slLog.log("ws链接错误", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 重连
reconnect();
};
ws.current.onmessage = (evt) => {
slLog.log("ws接受消息", evt.data);
if (evt && evt.data) {
setMessage({ ...JSON.parse(evt.data) });
}
};
};

ws_1.png


websocket 心跳机制


在使用 ws 过程中,可能因为网络异常或者网络比较差,导致 ws 断开链接了,此时 onclose 事件未执行,无法知道 ws 连接情况。就需要有一个心跳机制,监控 ws 连接情况,断开后,可以进行重连操作。


目前的实现方案就是:前端每隔 5s 发送一次心跳消息,服务端连续 1 分钟没收到心跳消息,就可以进行后续异常处理了


const timeout = 5000; // 心跳时间间隔
let timer = null; // 心跳定时器

// 保持心跳
const keepHeartbeat = () => {
timer && clearInterval(timer);
timer = setInterval(() => {
if (ws.current?.readyState == 1) {
// 发送心跳 消息接口可以自己定义
sendMessage({
cmd: "SL602",
content: { type: "heartbeat", desc: "发送心跳维持" },
});
}
}, timeout);
};

如下图所示,为浏览器控制台中的截图,可以查看ws连接请求及消息详情。


注意:正常情况下,是需要对消息进行加密的,最好不要明文传输。


ws_2.png


websocket 重连处理


let lockFlag = false; // 避免重复连接
// 重连
const reconnect = () => {
try {
if (lockFlag) {
// 是否已经执行重连
return;
}
lockFlag = true;
// 没连接上会一直重连
// 设置延迟避免请求过多
lockTimer && clearTimeout(lockTimer);
var lockTimer = setTimeout(() => {
closeWebSocket();
ws.current = null;
createWebSocket();
lockFlag = false;
}, timer);
} catch (err) {
slLog.error("ws重连失败", err);
}
};

websocket 关闭事件


关闭事件需要暴露出去,给外界控制


// 关闭 WebSocket
const closeWebSocket = () => {
ws.current?.close();
};

websocket 发送数据


发送数据时,数据格式定义为对象形式,如{ cmd: '', content: '' }


// 发送数据
const sendMessage = (message) => {
if (ws.current?.readyState === 1) {
// 需要转一下处理
ws.current?.send(JSON.stringify(message));
}
};

页面可见性


监听页面切换到前台,还是后台,可以通过visibilitychange事件处理。


当页面长时间处于后台时,可以进行关闭或者异常的逻辑处理。


// 判断用户是否切换到后台
function visibleChange() {
// 页面变为不可见时触发
if (document.visibilityState === "hidden") {
setIsLocalPage(false);
}
// 页面变为可见时触发
if (document.visibilityState === "visible") {
setIsLocalPage(true);
}
}

useEffect(() => {
// 监听事件
document.addEventListener("visibilitychange", visibleChange);
return () => {
// 监听销毁事件
document.removeEventListener("visibilitychange", visibleChange);
};
}, []);

页面关闭


页面刷新或者是页面窗口关闭时,需要做一些销毁、清除的操作,可以通过如下事件执行:


beforeunload:当浏览器窗口关闭或者刷新时会触发该事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。


onunload:当文档或一个子资源正在被卸载时,触发该事件。beforeunload在其前面执行,如果点的浏览器取消按钮,不会执行到该处。


function beforeunload(ev) {
const e = ev || window.event;
// 阻止默认事件
e.preventDefault();
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
}
function onunload() {
// 执行关闭事件
ws.current?.close();
}

useEffect(() => {
// 初始化
window.addEventListener("beforeunload", beforeunload);
window.addEventListener("unload", onunload);
return () => {
// 销毁
window.removeEventListener("beforeunload", beforeunload);
window.removeEventListener("unload", onunload);
};
}, []);

执行 beforeunload 事件时,会有如下取消、确认弹框


ws_3.png


参考文档:



作者:时光足迹
来源:juejin.cn/post/7249204284180086842
收起阅读 »

IM 聊天组件

web
IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示 传入参数 自定义内容:标题(title)、内容(children)、底部(footer) 弹框组件显隐控制: 一般通过一个变量控制显示或隐藏(visible); 并且暴露出一个事件,控制该变量(...
继续阅读 »

IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示


im_3.png


传入参数


自定义内容:标题(title)、内容(children)、底部(footer)


弹框组件显隐控制:


一般通过一个变量控制显示或隐藏(visible);


并且暴露出一个事件,控制该变量(setVisible)


interface iProps {
title?: string // 标题
maskClose?: boolean // 点击 x 或 mask 回调
visible?: boolean // 是否显示
setVisible: (args) => void // 设置是否显示
children?: React.ReactNode | Array<React.ReactNode> // 自定义内容
footer?: React.ReactNode | Array<React.ReactNode> // 自定义底部
}

基础结构


IM 聊天组件基础结构包含:头部、内容区、尾部


function wsDialog(prop: iProps) {
const wsContentRef = useRef(null); // 消息区
const { title = "消息", maskClose, visible, setVisible } = prop; // 传入参数
const [message, setMessage] = useState(""); // 当前消息
const imMessage = useSelector(
(state: rootState) => state.mediaReducer.imMessage
); // 消息列表 全局管理

return (
<Modal
className={styles.ws_modal}
visible={visible}
transparent
onClose={handleMaskClose}
popup
animationType="slide-up"
>

<div className={styles.ws_modal_widget}>
{/* 头部 */}
<div className={styles.ws_header}></div>
{/* 内容区 */}
<div ref={wsContentRef} className={styles.ws_content}></div>
{/* 尾部区域 */}
<div className={styles.ws_footer}></div>
</div>
</Modal>

);
}

头部区


头部区域主要展示标题和关闭图标


标题内容可以自定义


不仅可以点击“右上角关闭图标”进行关闭


也可以通过点击“遮罩”进行关闭


// 头部关闭事件
function handleClose() {
slLog.log("[wsDialog]点击了关闭按钮");
setVisible(false);
}

// 弹框遮罩关闭事件
function handleMaskClose() {
if (maskClose) {
slLog.log("[wsDialog]点击了遮罩关闭");
setVisible(false);
}
}

// 头部区域
<div className={styles.ws_header}>
<div>{title}</div>
<div className={styles.ws_header_close} onClick={handleClose}>
<Icon type="cross" color="#999" size="lg" />
</div>

</div>;

内容区


消息内容分类展示:



  1. 文本:直接展示内容

  2. 图片:通过 a 标签包裹展示,可以在新标签页中打开,通过target="_blank"控制

  3. 文件:不同类型文件展示不同的图标,包括 zip、rar、doc、docx、xls、xlsx、pdf、txt 等;文件还可以进行下载


<div ref={wsContentRef} className={styles.ws_content}>
{imMessage &&
imMessage.length &&
imMessage.map((o, index) => {
return (
<div
key={index}
className={`${styles.item} ${
o.category === "send" ? styles.self_item : ""
}`}
>

<div className={styles.title}>{o.showName + " " + o.showNum}</div>
{/* 消息为图片 */}
{o.desc === "img" ? (
<a
className={`${styles.desc} ${styles.desc_image}`}
href={o.fileUrl}
title={o.fileName}
target="_blank"
>

<img src={o.fileUrl} />
</a>
) : o.desc === "file" ? (
// 消息为文件
<div className={`${styles.desc} ${styles.desc_file}`}>
<img
className={styles.file_icon}
src={handleSuffix(o.fileSuffix)}
/>

<div className={styles.file_content}>
<a title={o.fileName}>{o.fileName}</a>
<div>{o.fileSize}</div>
</div>
<img
className={styles.down_icon}
src={downIcon}
onClick={() =>
handleDownload(o)}
/>
</div>
) : (
// 消息为文本
<div className={`${styles.desc} ${styles.desc_message}`}>
{o.message}
</div>
)}
</div>

);
})}
</div>

文件下载通过 a 标签模拟实现


// 下载文件
function handleDownload(o) {
slLog.log("[SLIM]下载消息文件", o.fileUrl);
const a = document.createElement("a");
a.href = o.fileUrl;
a.download = o.fileName;
document.body.appendChild(a);
a.target = "_blank";
a.click();
a.remove();
}

监听消息内容,自动滚动到最底部处理


useEffect(() => {
if (visible && imMessage && imMessage.length) {
// 滚动到底部
wsContentRef.current.scrollTop = wsContentRef.current.scrollHeight;
}
}, [visible, imMessage]);

尾部区


主要是操作区,用于展示和发送文本、图片、文件等消息。


图片和文件通过原生input实现,通过accept属性控制文件类型


<div className={styles.ws_footer}>
<div className={styles.tools_panel}>
{/* 上传图片 */}
<div className={styles.tool}>
<img src={imageIcon} />
<input type="file" accept="image/*" onChange={handleChange("img")} />
</div>
{/* 上传文件 */}
<div className={styles.tool}>
<img src={fileIcon} />
<input
type="file"
accept=".doc,.docx,.pdf,.txt,.xls,.xlsx,.zip,.rar"
onChange={handleChange("file")}
/>

</div>
</div>

<div className={styles.input_panel}>
{/* 输入框,上传文本 */}
<input
placeholder="输入文本"
value={message}
onChange={handleInputChange}
className={`${styles.message} ${styles.mMessage}`}
onKeyUp={handleKeyUp}
/>

{/* 消息发送按钮 */}
<div onClick={handleMessage} className={styles.btn}>
发送
</div>
</div>

</div>

获取图片、文件信息:


// 消息处理
function handleChange(type) {
return (ev) => {
switch (type) {
case "img":
case "file":
msgObj.type = type === "img" ? 4 : 7;
const e = window.event || ev;
const files = e.target.files || e.dataTransfer.files;
const file = files[0];
msgObj.content = file;
break;
}
};
}

实现回车键发送消息:


通过输入框,发送文本消息时,一般需要监听回车事件(onKeyUp 事件中的 event.keyCode 为 13),也能发送消息


// 回车事件
function handleKeyUp(event) {
const value = event.target.value;
if (event.keyCode === 13) {
slLog.log("[wsDialog]onKeyUp", value, event.keyCode);
handleInputChange(event);
handleMessage();
}
}

组件封装


组件级别:公司级、系统级、业务级


组件封装优势:



  1. 提升开发效率,组件化、统一化管理

  2. 考虑发布成 npm 形式,远程发布通用


组件封装考虑点:



  1. 组件的分层和分治

  2. 设置扩展性(合理预留插槽)

  3. 兼容性考虑(向下兼容)

  4. 使用对象考虑

  5. 适用范围考虑


组件封装步骤:



  1. 建立组件的模板:基础架子,UI 样式,基本逻辑

  2. 定义数据输入:分析逻辑,定义 props 里面的数据、类型

  3. 定义数据输出:根据组件逻辑,定义要暴露出来的方法,$emit 实现等

  4. 完成组件内部的逻辑,考虑扩展性和维护性

  5. 编写详细的说明文档


作者:时光足迹
来源:juejin.cn/post/7249286405025022009
收起阅读 »

关于正则表达式,小黄人有话要说!!!

web
引言(关于正则表达式,小黄人有话要说!!!) 掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率! 本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是...
继续阅读 »

38dbb6fd5266d016a9ef9caf912bd40734fa3546.jpeg


引言(关于正则表达式,小黄人有话要说!!!)


掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率!


本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都能帮助你更好地理解和应用正则表达式。


如果您认为这篇文章对您有帮助或有价值,请不吝点个赞支持一下。如果您有任何疑问、建议或意见,欢迎在评论区留言。


image.png


如果你想快速入门 JavaScript 正则表达式,不妨点击这里阅读文章 "点燃你的前端技能!五分钟掌握JavaScript正则表达式"


字面量和构造函数


在 JavaScript 中,我们可以使用正则表达式字面量构造函数来创建正则表达式对象。


// 使用字面量
let regexLiteral = /pattern/;
// 使用构造函数
let regexConstructor = new RegExp('pattern');

正则表达式的方法


在 JavaScript 中,你可以使用正则表达式的方法进行模式匹配和替换。以下是一些常用的方法:




  • test():测试一个字符串是否匹配正则表达式。


    const regex = /pattern/;
    regex.test('string'); // 返回 true 或 false



  • exec():在字符串中执行正则表达式匹配,返回匹配结果的数组。


    const regex = /pattern/;
    regex.exec('string'); // 返回匹配结果的数组或 null



  • match():在字符串中查找匹配正则表达式的结果,并返回匹配结果的数组。


    const regex = /pattern/;
    'string'.match(regex); // 返回匹配结果的数组或 null



  • search():在字符串中搜索匹配正则表达式的结果,并返回匹配的起始位置。


    const regex = /pattern/;
    'string'.search(regex); // 返回匹配的起始位置或 -1



  • replace():在字符串中替换匹配正则表达式的内容。


    const regex = /pattern/;
    'string'.replace(regex, 'replacement'); // 返回替换后的新字符串



  • split():将字符串根据匹配正则表达式的位置分割成数组。


    const regex = /pattern/;
    'string'.split(regex); // 返回分割后的数组



u=1690536536,1627515251&fm=253&fmt=auto&app=138&f=JPEG.webp


基本元字符


正则表达式由字母、数字和特殊字符组成。其中,特殊字符被称为元字符,具有特殊的意义和功能。以下是一些常见的基本元字符及其作用:


元字符及其作用




  • 字符类 []



    • [abc]:匹配任意一个字符 a、b 或 c。

    • [^abc]:匹配除了 a、b 或 c 之外的任意字符。

    • [0-9]:匹配任意一个数字。

    • [a-zA-Z]:匹配任意一个字母(大小写不限)。




  • 转义字符 \



    • \d:匹配任意一个数字字符。

    • \w:匹配任意一个字母、数字或下划线字符。

    • \s:匹配任意一个空白字符。




  • 量词 {}



    • {n}:匹配前一个元素恰好出现 n 次。

    • {n,}:匹配前一个元素至少出现 n 次。

    • {n,m}:匹配前一个元素出现 n 到 m 次。




  • 边界字符 ^



    • ^pattern:匹配以 pattern 开头的字符串。

    • pattern$:匹配以 pattern 结尾的字符串。

    • \b:匹配一个单词边界。




  • 其他元字符



    • .:匹配任意一个字符,除了换行符。

    • |:用于模式的分组和逻辑 OR。

    • ():捕获分组,用于提取匹配的子字符串。

    • ?::非捕获分组,用于匹配但不捕获子字符串。




实例演示


现在,让我们通过一些实例来演示正则表达式中元字符的实际作用:


u=3528014621,1838675307&fm=253&fmt=auto&app=138&f=JPEG.webp



  • 字符类 []


let regex = /[abc]/;
console.log(regex.test("apple")); // true
console.log(regex.test("banana")); // false


  • 转义字符 \


let regex = /\d{3}-\d{4}/;
console.log(regex.test("123-4567")); // true
console.log(regex.test("abc-1234")); // false


  • 量词 {}


let regex = /\d{2,4}/;
console.log(regex.test("123")); // true
console.log(regex.test("12345")); // false
console.log(regex.test("12")); // true


  • 边界字符 ^


// 以什么开头
let regex = /^hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("world hello")); // false

// 单词边界
const pattern = /\bcat\b/;
console.log(pattern.test("The cat is black.")); // 输出:true
console.log(pattern.test("A cat is running.")); // 输出:true
console.log(pattern.test("The caterpillar is cute.")); // 输出:false


  • 其他元字符


// 捕获分组与模式分组
let regex = /(red|blue) car/;
console.log(regex.test("I have a red car.")); // true
console.log(regex.test("I have a blue car.")); // true
console.log(regex.test("I have a green car.")); // false

// 点号元字符
const pattern = /a.b/;
console.log(pattern.test("acb")); // 输出:true
console.log(pattern.test("a1b")); // 输出:true
console.log(pattern.test("a@b")); // 输出:true
console.log(pattern.test("ab")); // 输出:false

修饰符的使用


修饰符用于改变正则表达式的匹配行为,常见的修饰符包括 g(全局)、i(不区分大小写)和 m(多行)。


// 使用 `g` 修饰符全局匹配
const regex = /a/g;
const str = "abracadabra";
console.log(str.match(regex)); // 输出:['a', 'a', 'a', 'a']

// 使用 `i` 修饰符进行不区分大小写匹配
const pattern = /abc/i;
console.log(pattern.test("AbcDef")); // 输出:true
console.log(pattern.test("XYZ")); // 输出:false

十个高度实用的正则表达式示例


u=4075901265,1581553886&fm=253&fmt=auto&app=120&f=JPEG.webp



  1. 验证电子邮件地址:


const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
console.log(emailPattern.test("example@example.com")); // 输出:true
console.log(emailPattern.test("invalid.email@com")); // 输出:false


  1. 验证手机号码:


const phonePattern = /^\d{11}$/;
console.log(phonePattern.test("12345678901")); // 输出:true
console.log(phonePattern.test("98765432")); // 输出:false


  1. 提取 URL 中的域名:


const url = "https://www.example.com";
const domainPattern = /^https?://([^/?#]+)(?:[/?#]|$)/i;
const domain = url.match(domainPattern)[1];
console.log(domain); // 输出:"www.example.com"


  1. 验证日期格式(YYYY-MM-DD):


const datePattern = /^\d{4}-\d{2}-\d{2}$/;
console.log(datePattern.test("2023-05-12")); // 输出:true
console.log(datePattern.test("12/05/2023")); // 输出:false


  1. 验证密码强度(至少包含一个大写字母、一个小写字母和一个数字):


const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(passwordPattern.test("Password123")); // 输出:true
console.log(passwordPattern.test("weakpassword")); // 输出:false


  1. 提取文本中的所有链接:


const text = "Visit my website at https://www.example.com. For more information, check out http://www.example.com/docs.";
const linkPattern = /https?://\S+/g;
const links = text.match(linkPattern);
console.log(links); // 输出:["[https://www.example.com](https://www.example.com/)", "<http://www.example.com/docs>"]


  1. 替换字符串中的所有数字为特定字符:


const text = "I have 3 apples and 5 oranges.";
const digitPattern = /\d/g;
const modifiedText = text.replace(digitPattern, "*");
console.log(modifiedText); // 输出:"I have * apples and * oranges."


  1. 匹配 HTML 标签中的内容:


const html = "<p>Hello, <strong>world</strong>!</p>";
const tagPattern = /<[^>]+>/g;
const content = html.replace(tagPattern, "");
console.log(content); // 输出:"Hello, world!"


  1. 检查字符串是否以特定后缀结尾:


const filename = "example.txt";
const suffixPattern = /.txt$/;
console.log(suffixPattern.test(filename)); // 输出:true


  1. 验证邮政编码(5 位或 5+4 位数字):


const zipCodePattern = /^\d{5}(?:-\d{4})?$/;
console.log(zipCodePattern.test("12345")); // 输出:true
console.log(zipCodePattern.test("98765-4321")); // 输出:true
console.log(zipCodePattern.test("1234")); // 输出:false

u=3763318279,485967013&fm=253&fmt=auto&app=138&f=JPEG.webp


通过正则表达式的核心概念和用法,结合实例和讲解。在实际开发中,不难发现正则表达式是一个强大的工具,可用于字符串处理、模式匹配和验证输入等方面。掌握正则表达式的技巧,可以大大提升 JavaScript 编程的效率和灵活性。


结语


感谢您的阅读!希望本文带给您有价值的信息。


如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。


写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!


290be963d171f8b42f347d7e97b62252.jpg.source.jpg


作者:Sailing
来源:juejin.cn/post/7249231231967232037
收起阅读 »

Android 冷启动优化的3个小案例

背景 为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机...
继续阅读 »

背景


为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机上加起来也不超过50ms的收益,但为了冷启动场景的极致优化,给用户带来更好的体验,任何有收益的优化手段都是值得尝试的。


类预加载


一个类的完整加载流程至少包括 加载、链接、初始化,而类的加载在一个进程中只会触发一次,因此对于冷启动场景,我们可以异步加载原本在启动阶段会在主线程触发类加载过程的类,这样当原流程在主线程访问到该类时就不会触发类加载流程。


Hook ClassLoader 实现


在Android系统中,类的加载都是通过PathClassLoader 实现的,基于类加载的父类委托机制,我们可以通过Hook PathClassLoader 修改其默认的parent 来实现。


首先我们创建一个MonitorClassLoader 继承自PathClassLoader,并在其内部记录类加载耗时


class MonitorClassLoader(
dexPath: String,
parent: ClassLoader, private val onlyMainThread: Boolean = false,
) : PathClassLoader(dexPath, parent) {

val TAG = "MonitorClassLoader"

override fun loadClass(name: String?, resolve: Boolean): Class<*> {
val begin = SystemClock.elapsedRealtimeNanos()
if (onlyMainThread && Looper.getMainLooper().thread!=Thread.currentThread()){
return super.loadClass(name, resolve)
}
val clazz = super.loadClass(name, resolve)
val end = SystemClock.elapsedRealtimeNanos()
val cost = end - begin
if (cost > 1000_000){
Log.e(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
} else {
Log.d(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
}
return clazz;

}
}

之后,我们可以在Application attach阶段 反射替换 application实例的classLoader 对应的parent指向。


核心代码如下:


    companion object {
@JvmStatic
fun hook(application: Application, onlyMainThread: Boolean = false) {
val pathClassLoader = application.classLoader
try {
val monitorClassLoader = MonitorClassLoader("", pathClassLoader.parent, onlyMainThread)
val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList")
pathListField.isAccessible = true
val pathList = pathListField.get(pathClassLoader)
pathListField.set(monitorClassLoader, pathList)

val parentField = ClassLoader::class.java.getDeclaredField("parent")
parentField.isAccessible = true
parentField.set(pathClassLoader, monitorClassLoader)
} catch (throwable: Throwable) {
Log.e("hook", throwable.stackTraceToString())
}
}
}

主要逻辑为



  • 反射获取原始 pathClassLoader 的 pathList

  • 创建MonitorClassLoader,并反射设置 正确的 pathList

  • 反射替换 原始pathClassLoader的 parent指向 MonitorClassLoader实例


这样,我们就获取启动阶段的加载类了



基于JVMTI 实现


除了通过 Hook ClassLoader的方案实现,我们也可以通过JVMTI 来实现类加载监控。关于JVMTI 可参考之前的文章 juejin.cn/post/694278…


通过注册ClassPrepare Callback, 可以在每个类Prepare阶段触发回调。




当然这种方案,相比 Hook ClassLoader 还是要繁琐很多,不过基于JVMTI 还可以做很多其他更强大的事。


类预加载实现


目前应用通常都是多模块的,因此我们可以设计一个抽象接口,不同的业务模块可以继承该抽象接口,定义不同业务模块需要进行预加载的类。


/**
* 资源预加载接口
*/

public interface PreloadDemander {
/**
* 配置所有需要预加载的类
* @return
*/

Class[] getPreloadClasses();
}

之后在启动阶段收集所有的 Demander实例,并触发预加载


/**
* 类预加载执行器
*/

object ClassPreloadExecutor {


private val demanders = mutableListOf<PreloadDemander>()

fun addDemander(classPreloadDemander: PreloadDemander) {
demanders.add(classPreloadDemander)
}

/**
* this method shouldn't run on main thread
*/

@WorkerThread fun doPreload() {
for (demander in localDemanders) {
val classes = demander.preloadClasses
classes.forEach {
val classLoader = ClassPreloadExecutor::class.java.classLoader
Class.forName(it.name, true, classLoader)
}
}
}

}

收益


第一个版本配置了大概90个类,在终端机型测试数据显示 这些类的加载需要消耗30ms左右的cpu时间,不同类加载的消耗时间差异主要来自于类的复杂度 比如继承体系、字段属性数量等, 以及类初始化阶段的耗时,比如静态成员变量的立即初始化、静态代码块的执行等。


方案优化思考


我们目前的方案 配置的具体类列表来源于手动配置,这种方案的弊端在于,类的列表需要开发维护,在版本快速迭代变更的情况下 维护成本较大, 并且对于一些大型App,存在着非常多的AB实验条件,这也可能导致不同的用户在类加载上是会有区别的。


在前面的小节中,我们介绍了使用自定义的 ClassLoader可以手动收集 启动阶段主线程的类列表,那么 我们是否可以在端上 每次启动时 自动收集加载的类,如果发现这个类不在现有 的名单中 则加入到名单,在下次启动时进行预加载。 当然 具体的策略还需要做详细设计,比如 控制预加载名单的列表大小, 被加入预加载名单的类最低耗时阈值, 淘汰策略等等。


Retrofit ServiceMethod 预解析注入


背景


Retrofit 是目前最常用的网络库框架,其基于注解配置的网络请求方式及Adapter的设计模式大大简化了网络请求的调用方式。 不过其并没有采用类似APT的方式在编译时生成请求代码,而是采用运行时解析的方式。


当我们调用Retrofit.create(final Class service) 函数时,会生成一个该抽象接口的动态代理实例。



接口的所有函数调用都会被转发到该动态代理对象的invoke函数,最终调用loadServiceMethod(method).invoke 调用。



在loadServiceMethod函数中,需要解析原函数上的各种元信息,包括函数注解、参数注解、参数类型、返回值类型等信息,并最终生成ServiceMethod 实例,对原接口函数的调用其实最终触发的是 这个生成的ServiceMethod invoke函数的调用。


从源码实现上可以看出,对ServiceMethod的实例做了缓存处理,每个Method 对应一个ServiceMethod。


耗时测试


这里我模拟了一个简单的 Service Method, 并调用archiveStat 观察首次调用及其后续调用的耗时,注意这里的调用还未触发网络请求,其返回的是一个Call对象。




从测试结果上看,首次调用需要触发需要消耗1.7ms,而后续的调用 只需要消耗50微妙左右。



优化方案


由于首次调用接口函数需要触发ServiceMethod实例的生成,这个过程比较耗时,因此优化思路也比较简单,收集启动阶段会调用的 函数,提前生成ServiceMethod实例并写入到缓存中。


serviceMethodCache 的类型本身是ConcurrentHashMap,所以它是并发安全的。



但是源码中 进行ServiceMethod缓存判断的时候 还是以 serviceMethodCache为Lock Object 进行了加锁,这导致 多线程触发同时首次触发不同Method的调用时,存在锁等待问题



这里首先需要理解为什么这里需要加锁,其目的也是因为parseAnnotations 是一个好事操作,这里是为了实现类似 putIfAbsent的完全原子性操作。 但实际上这里加锁可以以 对应的Method类型为锁对象,因为本身不同Method 对应的ServiceMethod实例就是不同的。 我们可以修改其源码的实现来避免这种场景的锁竞争问题。




当然针对我们的优化场景,其实不修改源码也是可以实现的,因为 ServiceMethod.parseAnnotations 是无锁的,毕竟它是一个纯函数。 因此我们可以在异步线程调用parseAnnotations 生成ServiceMethod 实例,之后通过反射 写入 Retrofit实例的 serviceMethodCache 中。这样存在的问题是 不同线程可能同时触发了一个Method的解析注入,但 由于serviceMethodCache 本身就是线程安全的,所以 它只是多做了一次解析,对最终结果并无影响。


ServiceMethod.parseAnnotations是包级私有的,我们可以在当前工程创建一个一样的包,这样就可以直接调用该函数了。 核心实现代码如下


package retrofit2

import android.os.Build
import timber.log.Timber
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier

object RetrofitPreloadUtil {
private var loadServiceMethod: Method? = null
var initSuccess: Boolean = false
// private var serviceMethodCacheField:Map<Method,ServiceMethod<Any>>?=null
private var serviceMethodCacheField: Field? = null

init {
try {
serviceMethodCacheField = Retrofit::class.java.getDeclaredField("serviceMethodCache")
serviceMethodCacheField?.isAccessible = true
if (serviceMethodCacheField == null) {
for (declaredField in Retrofit::class.java.declaredFields) {
if (Map::class.java.isAssignableFrom(declaredField.type)) {
declaredField.isAccessible =true
serviceMethodCacheField = declaredField
break
}
}
}
loadServiceMethod = Retrofit::class.java.getDeclaredMethod("loadServiceMethod", Method::class.java)
loadServiceMethod?.isAccessible = true
} catch (e: Exception) {
initSuccess = false
}
}

/**
* 预加载 目标service 的 相关函数,并注入到对应retrofit实例中
*/

fun preloadClassMethods(retrofit: Retrofit, service: Class<*>, methodNames: Array<String>) {
val field = serviceMethodCacheField ?: return
val map = field.get(retrofit) as MutableMap<Method,ServiceMethod<Any>>

for (declaredMethod in service.declaredMethods) {
if (!isDefaultMethod(declaredMethod) && !Modifier.isStatic(declaredMethod.modifiers)
&& methodNames.contains(declaredMethod.name)) {
try {
val parsedMethod = ServiceMethod.parseAnnotations<Any>(retrofit, declaredMethod) as ServiceMethod<Any>
map[declaredMethod] =parsedMethod
} catch (e: Exception) {
Timber.e(e, "load method $declaredMethod for class $service failed")
}
}
}

}

private fun isDefaultMethod(method: Method): Boolean {
return Build.VERSION.SDK_INT >= 24 && method.isDefault;
}

}

预加载名单收集


有了优化方案后,还需要收集原本在启动阶段会在主线程进行Retrofit ServiceMethod调用的列表, 这里采取的是字节码插桩的方式,使用的LancetX 框架进行修改。



目前名单的配置是预先收集好,在配置中心进行配置,运行时根据配置中写的配置 进行预加载。 这里还可以提供其他的配置方案,比如 提供一个注解用于标注该Retrofit函数需要进行预解析,



之后,在编译期间收集所有需要预加载的Service及函数,生成对应的名单,不过这个方案需要一定开发成本,并且需要去修改业务模块的代码,目前的阶段还处于验证收益阶段,所以暂未实施。


收益


App收集了启动阶段20个左右的Method 进行预加载,预计提升10~20ms。


ARouter


背景


ARouter框架提供了路由注册跳转 及 SPI 能力。为了优化冷启动速度,对于某些服务实例可以在启动阶段进行预加载生成对应的实例对象。


ARouter的注册信息是在预编译阶段(基于APT) 生成的,在编译阶段又通过ASM 生成对应映射关系的注入代码。



而在运行时以获取Service实例为例,当调用navigation函数获取实例最终会调用到 completion函数。



当首次调用时,其对应的RouteMeta 实例尚未生成,会继续调用 addRouteGroupDynamic函数进行注册。



addRouteGroupDynamic 会创建对应预编译阶段生成的服务注册类并调用loadInto函数进行注册。而某些业务模块如何服务注册信息比较多,这里的loadInto就会比较耗时。



整体来看,对于获取Service实例的流程, completion的整个流程 涉及到 loadInto信息注册、Service实例反射生成、及init函数的调用。 而completion函数是synchronized的,因此无法利用多线程进行注册来缩短启动耗时。


优化方案


这里的优化其实和Retroift Service 的注册机制类似,不同的Service注册时,其对应的元信息类(IRouteGroup)其实是不同的,因此只需要对对应的IRouteGroup加锁即可。


在completion的后半部分流程中,针对Provider实例生产的流程也需要进行单独加锁,避免多次调用init函数。



收益


根据线下收集的数据 配置了20+预加载的Service Method, 预期收益 10~20ms (中端机) 。


其他


后续将继续结合自身业务现状以及其他一线大厂分享的样例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面继续尝试优化。


如果通过本文对你有所收获,可以来个点赞、收藏、关注三连,后续将分享更多性能监控与优化相关的文章。


也可以关注个人公众号:编程物语


image.png


本文相关测试代码已分享至github: github.com/Knight-ZXW/…


APM性能监控与优化专栏


性能优化专栏历史文章:


作者:卓修武K
来源:juejin.cn/post/7249228528573513789
tbody>
文章地址
Android平台下的cpu利用率优化实现juejin.cn/post/724324…
抖音消息调度优化启动速度方案实践juejin.cn/post/721766…
扒一扒抖音是如何做线程优化的juejin.cn/post/721244…
监控Android Looper Message调度的另一种姿势juejin.cn/post/713974…
Android 高版本采集系统CPU使用率的方式juejin.cn/post/713503…
Android 平台下的 Method Trace 实现及应用juejin.cn/post/710713…
Android 如何解决使用SharedPreferences 造成的卡顿、ANR问题juejin.cn/post/705476…
基于JVMTI 实现性能监控juejin.cn/post/694278…
收起阅读 »

Flutter卡片分享功能实现:将你的内容分享给世界

前言 在app中,在实现分享功能的时候,通常会有一种以卡片形式展示和分享内容的分享方式。这种功能可以将信息以整洁、易读的方式呈现给用户,使他们能够快速了解内容的关键信息,并将其分享给其他人。那么在这篇文章中,就一起来探索下,如何使用Flutter来实现这卡片...
继续阅读 »

前言



在app中,在实现分享功能的时候,通常会有一种以卡片形式展示和分享内容的分享方式。这种功能可以将信息以整洁、易读的方式呈现给用户,使他们能够快速了解内容的关键信息,并将其分享给其他人。那么在这篇文章中,就一起来探索下,如何使用Flutter来实现这卡片分享功能吧~


源代码:http://www.aliyundrive.com/s/FH7Xc2vyL…


效果图:



实现方案


为了卡片的样式的灵活性和可定制性,本文采用对组件进行截图的方式来实现卡片保存分享的功能,选择这个方案还有一点好处就是充分利用了flutter跨平台的优势。当然也会有一定的缺点,例如对于性能的考虑,当对复杂的嵌套卡片组件截图时,渲染和图像转换的计算量是需要考虑的,当然也可以选择忽略不计~


创建弹窗&卡片布局


在生成分享卡片的同时还会有其他的操作选项,例如保存图片、复制链接、浏览器打开等等,所以通常分享卡片的形式为弹窗形式,中间为分享卡片主体,剩余空间为操作项。



操作项组件封装:


class ImageDialog extends StatelessWidget {
const ImageDialog({
Key? key,
required this.items,
...
}) : super(key: key);
final List<ItemLittleView> items;
...

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
...
child: Row(
children: items
.map((e) => itemLittleView(
label: e.label,
icon: e.icon,
onTap: () {
Navigator.pop(context);
e.onTap?.call();
}))
.toList()),
),
],
);
}

Widget itemLittleView({
required String label,
required String icon,
Function()? onTap,
}) =>
InkWell(
onTap: onTap,
child: Container(
margin: EdgeInsets.only(right: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
//图标
),
Container(
//文字
),
],
),
),
);
}
}

class ItemLittleView {
final String label;
final String icon;
final Function()? onTap;

ItemLittleView({required this.label, required this.icon, this.onTap});
}

需要加入新的操作项时,只需要简单的添加一个ItemLittleView即可。


ImageDialog(
items: [
ItemLittleView(
label: "生成图片 ",
icon: "assets/images/icon/ic_down.png",
onTap: () => doSaveImage(),
),
...
],
),

卡片的布局则根据业务的需求自定义即可,本文也只是一个简单的例子。


渲染并截取组件截图


在flutter中可以使用RepaintBoundary将将组件渲染为图像。



  • 第一步:定义全局的GlobalKey,用于获取卡片布局组件的引用


var repaintKey = GlobalKey();

RepaintBoundary(
key: repaintKey,
//分享卡片
child: shareImage(),
),


  • 第二步:使用RenderRepaintBoundary的toImage方法将其转换为图像


Future<Uint8List> getImageData() async {
BuildContext buildContext = repaintKey.currentContext!;
//用于存储截取的图片数据
var imageBytes;
//通过 buildContext 获取到 RenderRepaintBoundary 对象,表示要截取的组件边界
RenderRepaintBoundary boundary =
buildContext.findRenderObject() as RenderRepaintBoundary;

//这行代码获取设备的像素密度,用于设置截取图片的像素密度
double dpr = ui.window.devicePixelRatio;
//将边界对象 boundary 转换为图像,使用指定的像素密度。
ui.Image image = await boundary.toImage(pixelRatio: dpr);
// image.width
//将图像转换为ByteData数据,指定了数据格式为 PNG 格式。
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
//将ByteData数据转换为Uint8List 类型的图片数据。
imageBytes = byteData!.buffer.asUint8List();
return imageBytes;
}


  • 第三步:获取权限&保存截图


//获取权限
_requestPermission() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
].request();

final info = statuses[Permission.storage].toString();
}

Future<String> saveImage(Uint8List imageByte) async {
//将回调拿到的Uint8List格式的图片转换为File格式
//获取临时目录
var tempDir = await getTemporaryDirectory();
//生成file文件格式
var file =
await File('${tempDir.path}/image_${DateTime.now().millisecond}.png')
.create();
//转成file文件
file.writeAsBytesSync(imageByte);
print("${file.path}");
String path = file.path;
return path;
}

//最后通过image_gallery_saver来保存图片
/// 执行存储图片到本地相册
void doSaveImage() async {
await _requestPermission();
Uint8List data = await getImageData();
String path = await saveImage(data);
final result = await ImageGallerySaver.saveFile(path);
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text("保存成功!"),
);
});
}

到这里,分享卡片的功能就实现啦~


总结


在本文中,我们探索了使用Flutter实现卡片分享功能的过程。在开发app时,卡片分享功能可以为用户提供更好的交互和共享体验,我猜大家在开发的过程中也会有很大的概率碰上这样的需求。通过设计精美的卡片样式,可以帮助更快速的推广APP。


关于我


Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 ,也可以通过掘金的新的私信功能联系到我。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万

作者:编程的平行世界
来源:juejin.cn/post/7249347871564300345
一哪天我进步了呢?😝

收起阅读 »

某外包面试官:你还不会uniapp?😲😲

uniapp主要文件夹 pages.json 配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等 main.js 入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router...
继续阅读 »

uniapp主要文件夹


pages.json


配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等


main.js


入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router,路由须在pages.json中进行配置。如果开发者坚持使用vue-router,可以在插件市场找到转换插件。


App.vue


是uni-app的主组件,所有页面都是在App.vue下进行切换的,是页面入口文件。但App.vue本身不是页面,这里不能编写视图元素。除此之外,应用生命周期仅可在App.vue中监听,在页面监听无效。


pages


页面管理部分用于存放页面或者组件


manifest.json


文件是应用的配置文件,用于指定应用的名称、图标、权限等。HBuilderX 创建的工程此文件在根目录,CLI 创建的工程此文件在 src 目录。


package.json


配置扩展,详情内容请见官网描述package.json概述


uni-app属性的绑定


vue和uni-app动态绑定一个变量的值为元素的某个属性的时候,会在属性前面加上冒号":";


uni-app中的本地数据存储和接收


// 存储:
uni.setStorage({key:“属性名”,data:“值”}) //异步
ni.setStorageSync(KEY,DATA) //同步
//接收:
ni.getStorage({key:“属性名”,success(res){res.data}}) //异步
uni.getStorageSync(KEY) //同步
//移除:
uni.removeStorage(OBJECT) //从本地缓存中异步移除指定 key。
uni.removeStorageSync(KEY) //从本地缓存中同步移除指定 key。
//清除:
uni.clearStorage() //清理本地数据缓存。
ni.clearStorageSync() //同步清理本地数据缓存。

页面调用接口



  • getApp() 函数 用于获取当前应用实例,一般用于获取globalData

  • getCurrentPages() 函数 用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。

  • uni.emit(eventName,OBJECT) uni.emit(eventName,OBJECT)uni.emit(eventName,OBJECT) uni.on(eventName,callback) :触发和监听全局的自定义事件

  • uni.once(eventName,callback):监听全局的自定义事件。uni.once(eventName,callback):监听全局的自定义事件。

  • 事件可以由 uni.once(eventName,callback):监听全局的自定义事件。

  • 事件可以由uni.emit 触发,但是只触发一次,在第一次触发之后移除监听器。

  • uni.$off([eventName, callback]):移除全局自定义事件监听器。


uni-app的生命周期


  beforeCreate(创建前)
created(创建后)
beforeMount(载入前,挂载)
mounted(载入后)
beforeUpdate(更新前)
updated(更新后)
beforeDestroy(销毁前)
destroyed(销毁后)

路由与页面跳转



  1. uni.navigateTo 不关闭当前页的情况下跳转其他页面

  2. uni.redirectTo 关闭当前页的情况下跳转其他页面

  3. uni.switchTab 跳转去tabBar,关闭其他非tabBar页面

  4. uni.reLaunch 关闭所有页面,跳转到其他页面

  5. uni.navigateBack 返回

  6. edxit 退出app


跨端适配—条件编译


1. #ifdef APP-PLUS
需条件编译的代码 //app
#endif
2. #ifndef H5
需条件编译的代码 //H5
endif
3. #ifdef H5 || MP-WEIXIN
需条件编译的代码 //小程序
#endif

uniapp上传文件时使用的api


uni.uploadFile({
url: '要上传的地址',
fileType:'image',
filePath:'图片路径',
name:'文件对应的key',
success: function(res){
console.log(res)
},})

uniapp选择文件、图片上传


选择文件


uni.chooseFile({
count: 6, //默认100
extension:['.zip','.doc'],
success: function (res) {
console.log(JSON.stringify(res.tempFilePaths));
}
});

选择图片文件


uni.chooseFile({
count: 10,
type: 'image',
success (res) {
// tempFilePath可以作为img标签的src属性显示图片
const tempFilePaths = res.tempFiles
}
})

uni-app的页面传参方式


第一种:
直接在跳转页面的URL路径后面拼接,如果是数组或者json格式记得转成字符串格式哦。然后再目的页面onload里面接受即可


//现页面
uni.navigateTo({
url:'/pages/notice/notice?id=1'
})
//目的页面接收
//这里用onshow()也可以
onLoad(options) {
var data = options.id;
console.log(data)
}

第二种:
直接在main.js注册全局变量



  • 例如我用的是vue框架,先在main.js文件注册变量myName

  • Vue.prototype.myName= '玛卡巴卡';

  • 在目标文件读取全局变量,注意全局变量不要与我们在当前页声明的变量名重复

  • let name = this.myName; // 玛卡巴卡


第三种:设置本地存储也比较方便



  • 这里建议使用uni.setStorageSync这个是同步,不会出现去了目标页面取值取不到的问题

  • uni.setStorage是异步存值,获取值也是一样建议使用uni.getStorageSync


uniapp实现下拉刷新


实现下拉刷新需要用到uni.onPullDownRefresh和uni.stopPullDownRefresh这个两个函数,函数与生命周期同等级可以监听页面下拉动作


uniapp实现上拉加载


uniapp中的上拉加载是通过onReachBottom()这个生命周期函数实现,当下拉触底时就会触发。我们可以在此函数内调用分页接口请求数据,用以获取更多的数据


scroll-view吸顶问题



  • 问题:
    scroll-view 是常会用到的一个标签,我们可以使用 position:sticky 加一个边界条件例如top:0
    属性实现一个粘性布局,在容器滚动的时候,如果我们的顶部标签栏触碰到了顶部就不会再滚动了,而是固定在顶部。但是在小程序中如果你在scroll-view元素中直接为子元素使用sticky属性,你给予sticky的元素在到达父元素的底部时会失效。

  • 解决:
    在scroll-view元素中,再增加一层view元素,然后在再将使用了sticky属性的子元素放入view中,就可以实现粘贴在某个位置的效果了


ios输入框字体移动bug



  • 问题:在IOS端有时,当输入框在输入后没有点击其他位置使输入框失焦的话,如果滚动窗口内部的字体也会跟着滚动

  • 解决:



  1. 尝试了下,发现textarea不会和input一样出现字体随着页面滚动的情况,这是一个兼容方案

  2. 还有个不优雅的方案是输入完成后使用其他事件让其失焦或者disable,例如弹窗或者弹出层出来的时候可以暂时让input禁止,然后弹窗交互完成后再放开


rpx、px、em、rem、%、vh、vw的区别是什么?



  • rpx 相当于把屏幕宽度分为750份,1份就是1rpx

  • px 绝对单位,页面按精确像素展示

  • em 相对单位,相对于它的父节点字体进行计算

  • rem 相对单位,相对根节点html的字体大小来计算

  • % 一般来说就是相对于父元素

  • vh 视窗高度,1vh等于视窗高度的1%

  • vw 视窗宽度,1vw等于视窗宽度的1%


uni-app的优缺点



  • 优点:



  1. 一套代码可以生成多端

  2. 学习成本低,语法是vue的,组件是小程序的

  3. 拓展能力强

  4. 使用HBuilderX开发,支持vue语法

  5. 突破了系统对H5条用原生能力的限制



  • 缺点:



  1. 问世时间短,很多地方不完善

  2. 社区不大

  3. 官方对问题的反馈不及时

  4. 在Android平台上比微信小程序和iOS差

  5. 文件命
    作者:margin_100px
    来源:juejin.cn/post/7245936314851622970
    名受限

收起阅读 »

日常开发中,提升技术的13个建议

前言 大家好,我是田螺。 最近有位读者问我:田螺哥,日常开发中,都是在做业务需求,如何提升自己的技术呢? 所以,本文田螺哥整理了提升技术的13个建议,小伙伴们,一起加油。 1. 打好基础,深入学习语言特性 比如,对于Java程序员来说,要了解Java语言的基...
继续阅读 »

前言


大家好,我是田螺


最近有位读者问我:田螺哥,日常开发中,都是在做业务需求,如何提升自己的技术呢? 所以,本文田螺哥整理了提升技术的13个建议,小伙伴们,一起加油。



1. 打好基础,深入学习语言特性


比如,对于Java程序员来说,要了解Java语言的基本概念和核心特性,包括面向对象编程、集合框架、异常处理、多线程等等。可以通过阅读Java的官方文档、教程、参考书籍或在线资源来学习。


如果最基本的基础都不扎实,就不要谈什么提升技术啦。 比如说:



  • 你知道HashMap和ConcurrentHashMap的区别嘛?

  • 在什么时候使用ConcurrentHashMap?操作文件的时候

  • 你知道在finally块中释放资源嘛?

  • 你知道在哪些场景适合用泛型嘛?


因此,要提升自身技术,首先就是要把基础打扎实。 有些小伙伴说,上班没时间学基础呀,其实不是这样的,基础这玩意,每天地铁上下班看看,下班后回到家在看看,周末在家看看,多点写写代码,一般一两个月,你的基础就很好啦。


又有些小伙伴说,如何提升Java基础呢? 可以:



  • 阅读Java相关书籍或教程,如Java编程思想、Java核心技术、Java虚拟机、菜鸟教程等

  • 阅读Java博客和社区参与讨论:关注Java领域的博客、论坛和社区,了解最新的技术动态和解决方案,与其他开发者交流。

  • 多实践,多敲代码:在B站找个Java基础视频看,平时多实践、多敲代码



2. 熟悉掌握常用的开发工具


工欲善其事,必先利其器. 所以一位好的程序员,往往编码效率就更高。而提升编码效率,一般要求熟悉并灵活应用工具.比如Eclipse、IntelliJ IDEA、Maven、Navicat等。熟悉运用这些工具,可以提高开发效率。


我举个例子,比如你熟悉掌握IntelliJ IDEA的快捷键,三两下就把实体类的setter和getter方法生成了,而有些的程序员,还在一行一行慢慢敲。。



3. 日常工作中,总结你踩过的坑


优秀的程序员,之所以优秀,是因为他会总结踩过的坑,避免重蹈覆辙。所以,田螺哥建议你,日常开发中,如果你踩了哪些坑,就需要总结下来.茶余饭后,再温习温习.


比如,你知道:



  • Redis分布式锁使用,可能会有哪些坑嘛?

  • 线程池使用有哪些坑?

  • Java日期处理又又哪些坑嘛?

  • Arrays.asList使用可能有哪些坑?


如果一时间忘记的话,可以看下我以前的这些文章:



这些都是我工作总结出来的,也希望你们日常开发中,遇到哪些坑,都总结下来哈。



4.工作中,阅读你项目优秀的代码和设计文档


孔子说,三人行,必有我师。大家平时在看代码的时候,不要总吐槽着项目的烂代码。其实,可以多点关注写得优秀的代码,然后看懂别人为什么这些写,仿造着来写。


当然,一些好的设计文档也是:人家为什么这么设计,好处在哪里,不足又在哪里,如果是你来设计,你如何思考等等。把好的设计,读懂后,记录下来,变成自己的知识.



5.日常工作中,总结一些通用的技术方案.


在日常工作中呢,注意整理一些通用的技术方案。


比如幂等设计、分布式锁如何设计、分布式事务设计、接口优化、限流设计、分库分表设计、深分页问题解决等等. 大家可以看下我之前的一些通用方案设计的文章哈:



当然,田螺哥也建议你,日常开发中,把自己遇到的一些通用设计方案总结下来,熟悉掌握这些通用技术方案。



6.参与技术讨论,积极技术分享


参与技术讨论和交流,可以有助于你与其他Java开发者分享经验、解决问题和学习新知识。进行技术分享,可以加深自己的理解、建立专业声誉、促进个人成长、为技术社区做贡献等等。


比如你做需求遇到的一些难题,都可以跟有经验的同事、或者技术leader讨论讨论。一些常见的难题,讨论完可以记录下来,然后做技术分享



7. 主人翁意识,积极攻克项目的难题


作为一名开发工程师,具备主人翁意识并积极攻克项目的难题,是非常重要的。遇到项目中的比较棘手问题时,先不管是谁的问题,我们都要持有主人翁意识,积极主动地找到解决方案并采取行动。


而在技术找解决方案的过程,我们也就成长了。当攻克问题后,你也获得领导的认可,好绩效不远了,一举多得



8. 思考项目中,哪些可以提升效率


日常开发中,几乎大多数程序员都是在进行增删改查。如何如何避免自己成为平凡的增删改查程序员呢。


我觉得可以这样做:平时工作中,思考项目中,有哪些可以提升的效率。包括熟悉开发工具、掌握适当的调试技巧、熟悉常用框架、持续学习和关注技术发展等等。


比如:



  • 好的的debug调试技巧,可以让你快速找到问题

  • 再比如一个插件easyyapi可以一键让你快速生成yapi接口文档,而不用一个一个字段手工敲接口文档。


当然,日常开发中,还有很多可以提升效率的技巧/工具,等待我们去发现



9. 熟悉你的业务,让自己不容易被替代


我们普通程序员,多数都是做业务的。一般工作个五年以上,水平差不了太多。如何避免自己被淘汰呢?我个人建议是,尽量做到熟悉你们做的业务,让你变得不容易被替代。



10. 多看看你的系统,可能存在哪些问题,如接口耗时、慢SQL等等


一般的系统,多多少少都有些问题。比如接口耗时过长、慢SQL、fullGC频繁等等。


首先需要掌握这些技能,比如如何优化接口,如何优化慢SQl、fullGC如何排查等等。大家可以看下这几篇文章哈:



11. 学以致用,将理论知识应用到实际项目中


很多小伙伴说,看过很多计算机相关的书,阅读过很多博客,背了很多八股文,依然做不好一个系统。


我觉得,大家可以多点思考,把平时积累的东西,应用到实际项目中。背八股文不是没用,你可以把它应用到实际开发中的。比如说,你看了田螺哥的文章,IO模型详解


这个表面看起来就是一个常见的八股文知识点,工作中似乎没用到。但是我在工作中,就用到这种类似的异步思想



比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。



再比如,你看完田螺哥的:MySQL索引15连问,抗住!,你是不是可以回头看看,你的系统中,那些sql的索引加的是否合理呢?是不是可以思考一下如何优化,对吧。因此,就是要学以致用



12. 阅读一些优秀框架的源码,如spring、rockectMq等等


如果你有空余的时间,就建议你看看一些优化框架的源码,比如spring、rockectMq等等。


对于spring源码的话,可以按模块来呀,比如aop,控制反转,spring事务等,你先写个demo,然后debug跟踪流程,通过调试器逐步跟踪源码执行过程,观察各个方法的调用关系和数据变化。最好是结合电子书一起,如(Spring源码深度解析这本书一起)


优秀框架的源码,我们可以学习到很多编码思想的,加油。



13. 多编码,少偷懒,养成编程的好习惯


作为程序员,一定要多打代码,不要偷懒,代码敲多了,你就会了。还有就是,少点偷懒,坚持!努力!养成热爱编程的好习惯


总之,提升技术需要不断学习、实践、总结和积累经验



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

来这公司一年碰到的问题比我过去10年都多

无意间发现我们 Kafka 管理平台的服务的 open files 和 CPU 监控异常,如下图,有一台机器 CPU 和 opfen files 指标持续在高位,尤其是 open files 达到了4w+。 原因分析 第一反应是这个服务请求很高?但是这个服...
继续阅读 »

无意间发现我们 Kafka 管理平台的服务的 open files 和 CPU 监控异常,如下图,有一台机器 CPU 和 opfen files 指标持续在高位,尤其是 open files 达到了4w+。




原因分析


第一反应是这个服务请求很高?但是这个服务是一个管理服务不应该有很高的请求量才对,打开监控一看,QPS少的可怜。



既然机器还在就找 devops 同学帮忙使用 Arthas 简单看下是什么线程导致的,竟然是 GC 线程,瞬时 CPU 几乎打满了。



查看了 GC 监控,每分钟 5~6 次相比其他的正常节点要多很多,并且耗时很长。


问题节点GC Count



正常节点GC Count



应该是代码出问题了,继续求助 devops 将线上有问题的机器拉了一份 dump,使用 MAT 工具分析了下,打开 dump 就提示了两个风险点,两个都像是指标相关的对象。



查看详情发现两个可疑对象,一个是 60+M 的 byte[], 一个是 60+M 的 map,都是指标相关的对象,问题应该出在指标上。



初步去排查了下代码,看是否有自定义指标之类的,发现一个 job 会对指标进行操作,就把 job 停了一段时间,GC 少了很多,但是 open files 只减少了一点点, 很明显不是根本原因。




继续深入,将 byte[] 保存成字符串查看(确实文本也有60+M),发现全是 JMX 的指标数据,我们的系统使用了两种指标一种是Micrometer,一种是 prometheus-jmx-exporter,这个 byte[] 数组就是第二种指标的数据。



并且这些指标中发现有非常多的 kafka_producer 开头的指标。



为了验证是否属于 JMX 的指标数据,再次求助 devops 拉取线上有问题机器的 JMX 指标接口, 看返回的是否是 60M+ 的指标数据,发现根本拉不下来。



到此基本确认问题出在 JMX 指标上, 那这些指标谁注册的呢?


通过指标名称在源代码里搜索,发现是来自org.apache.kafka.common.network.Selector.SelectorMetrics,是 kafka-client注册的指标。


具体的创建顺序如下,每创建一个KafkaProducer,就会以 client id 为唯一标识创建一个SelectorMetrics, 而创建 KafkaProducer 会创建一个守护线程,并开启一个长连接定时去 Broker 拉取/更新 Metadata 信息,这个就是open files飙高的根本原因。


KafkaProducer -> Sender -> Selector -> SelectorMetrics



难道创建了很多 KafkaProducer???查看构造方法调用的地方,找到了真凶。。。



这段代码是为了支持延迟消息,业务服务每发一个延迟消息,就会执行一次这段逻辑, 就会创建一个 KafkaProducer,并且会随着收到的消息越来越多导致创建的 KafkaProducer 越来越多,直至系统无法承受。。。


庆幸的是我们延迟消息并不是太多,没有直接把系统给打挂掉


那为什么只有一个节点会有问题,其他节点没有问题呢?这个比较简单直接说结果了,就是这段消费逻辑消费的 topic 只有一个分区....


解决方案:


由于 Kafka 管理平台会连接多个 Broker,所以此处将创建的 KafkaProducer 根据 Cluster 缓存起来进行复用。


问题总结:



  1. KafkaProducer 本身是一个很重的对象,并且线程安全,创建的时候注意考虑场景

  2. 此次问题完全是凭运气提前发现了,证明监控系统也存在不够完善的地方, 我们使用 Prometheus 的标准差函数 (stddev() by()) 配置了资源严重倾斜的监控告警,防止出现类似的问题。

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

三分钟,趁同事上厕所的时间,我覆盖了公司的正式环境数据

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!! 而且要执行update!!update it_xtgnyhcebg I set taskStatus = XXX 而且是没有加where条件的,相当于全表更新,这可马虎不得,我们...
继续阅读 »

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!!
而且要执行update!!

update it_xtgnyhcebg I set taskStatus = XXX

而且是没有加where条件的,相当于全表更新,这可马虎不得,我们在任何操作正式数据库之前一定一定要对数据库备份!!不要问我怎么知道的,因为我就因为有一次把测试环境的数据覆盖到正式环境去了。。。


在这里插入图片描述


别到时候就后悔莫及,那是没有用的!


在这里插入图片描述
在这里插入图片描述


由于这个需求是需要在跨库操作的,所以我们在查询数据的时候需要带上库的名称,例如这样

SELECT
*
FROM
BPM00001.ACT_HI_PROCINST P
LEFT JOIN BPM00001.ACT_HI_VARINST V ON V.PROC_INST_ID_ = P.ID_
AND V.NAME_ = '__RESULE'


这样如果我们在任何一个库里面,只要在一个mysql服务里面都可以访问到这个数据
查出这个表之后
在这里插入图片描述
我们需要根据这里的内容显示出不同的东西
就例如说是“APPROVAL”我就显示“已通过”
这就类似与java中的Switch,其实sql也能实现这样的效果
如下:
在这里插入图片描述
这就是sql的case语句的使用
有了这些数据之后我们就可以更新数据表了,回到我们之前讨论过的,这是及其危险的操作
我们先把要set的值给拿出来
在这里插入图片描述


在这里插入图片描述
但是我们怎么知道这个里面的主键呢?
你如果直接这么加,肯定是不行的
在这里插入图片描述
所以我们需要在sql后面加入这样的一条语句
在这里插入图片描述
注意,这个语句一定要写在set语句的里面,这样sql就能依据里面判断的条件进行一一赋值
最后,将这个sql语句执行到生产库中


拓展:


作为查询语句的key绝对不能重复,否则会失败(找bug找了半天的人的善意提醒)
例如上面的语句中P.BUSINESS_KEY_必须要保证是唯一的!!


在这里插入图片描述
成功执行!!!
怎么样,这些sql的小妙招你学会了吗?


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

安卓知识点-应届生扫盲安卓WebView

作者 大家好,我叫Jack冯; 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队; 目前主要负责海外游戏发行安卓相关开发。 背景 最近在接触活动相关需求,其中涉及到一个安卓的WebView; 刚毕业的我,对安卓知识积累比较少,所以在这里对...
继续阅读 »

作者


大家好,我叫Jack冯;


本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队;


目前主要负责海外游戏发行安卓相关开发。


背景


最近在接触活动相关需求,其中涉及到一个安卓的WebView;


刚毕业的我,对安卓知识积累比较少,所以在这里对Webview进行相关学习,希望自己可以在安卓方面逐步积累。


Webview介绍


1、关于MockView


( 1 ) 在targetSdkVersion 28/29的工程里面查看WebView继承关系

java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.FrameLayout
↳ android.layoutlib.bridge.MockView
↳ android.webkit.WebView


( 2 ) 使用26/27等低版本SDK,查看源码中的WebView 继承关系

java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.AbsoluteLayout
↳ android.webkit.WebView

( 3 )对比


两种方式对比,AbsoluteLayout和FrameLayout都是重写ViewGroup的方法,如与布局参数配置相关的 generateDefaultLayoutParams()、checkLayoutParams()等。两种方式明显不同的是多了一层MockView 。这里来看看MockView是什么:

public class MockView extends FrameLayout{
...
//创建方式
public MockView(Context context) {...}
public MockView(Context context,AttributeSet attrs) {...}
public MockView(Context context,AttributeSet attrs,int defStyleRes) {...}
//重写添加view方法
@Override
public void addView(View child){...}
@Override
public void addView(View child,int index){...}
@Override
public void addView(View child,int width,int height){...}
@Override
public void addView(View child,ViewGroup.LayoutParams params){...}
@Override
public void addView(View child,int index,ViewGroup.LayoutParams params){...}
public void setText(CharSequence text){...}
public void setGravity(int gravity){...}
}

MockView,译为"虚假的view"。


谷歌发布的Sdk其实只是为了提供App开发运行接口,实际运行时候替换为当前系统的Sdk。


具体说就是当谷歌在新的系统(Framework)版本上准备对WebView实现机制进行改动,同时又希望把新的sdk提前发出来,不影响用到WebView的App开发,于是谷歌提供给Android开发的sdk中让WebView继承自MockView,这个WebView只是暴露了接口,没有具体实现;这样当谷歌关于WebView新的实现做好,利用WebView,app也就做好了


2、基本使用


(1)创建


①一般方式:

WebView webView = findViewById(R.id.webview);

②建议方式:

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);

好处:构建不用依赖本地xml文件,自定义页面参数;手动销毁避免内存泄露;


③更多方式 : 继承Webview和主要API等进行拓展

public class BaseWebView extends WebView {...}
public class BaseWebClient extends WebClient {...}
public class BaseWebChromeClient extends WebChromeClient {...}

(2)加载


① 加载某个网页

webView.loadUrl("http://www.google.com/");

②新建assets目录,将html文件放到目录下,通过路径加载本地页面

 webView.loadUrl("file:///android_asset/loadFailed.html");

③使用evaluateJavascript(String script, ValueCallback resultCallback)方法加载,(Android4.4+)

mWebView.evaluateJavascript("file:///android_asset/javascript.html",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("测试", "onReceiveValue:"+value );
}
});

3、WebViewClient


当URL即将加载到当前窗口,如果没有提供WebViewClient,默认情况下WebView将使用Activity管理器为URL选择适当的处理器。


如果提供了WebViewClient,按自定义配置要求来继续加载URL。


(1)常用方法

//加载过程对url的处理(webview加载、系统浏览器加载、其他操作等)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
}
//加载失败页面
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
view.loadUrl("file:///android_asset/js_error.html");
}
//证书错误处理
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
}
//开始加载页面(可自定义页面加载计时等)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.e(TAG, "onPageStarted:" + url);
}
//结束加载页面
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e(TAG, "onPageFinished: " + url);
}

(2)关于shouldOverrideUrlLoading


如果在点击链接加载过程需要更多的控制,就可以在WebViewClient()中重写shouldOverrideUrlLoading()方法。


涉及shouldOverrideUrlLoading()的情形,大概分为三种:


(1)没有设定setWebViewClient(),点击链接使用默认浏览器打开;


(2)设定setWebViewClient(new WebViewClient()),默认shouldOverrideUrlLoading()返回false,点击链接在Webview加载;


(3)设定、重写shouldOverrideUrlLoading()


返回true:可由应用代码处理该 url,WebView 中止处理(若重写方法没加上view.loadUrl(url),不加载);


返回false:由 WebView 处理加载该 url。(即使没加上view.loadUrl(url),也会在当前Webview加载)


【一般应用】

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url != null) {
if (!(url.startsWith("http") || url.startsWith("https"))) {
return true;
}
//重定向到别的页面
//view.loadUrl("file:///android_asset/javascript.html");
//区别不同链接加载
view.loadUrl(url);
}
return true;
}

(3)常见误区


【误区1】 : 需要重写 shouldOverrideUrlLoading 方法才能阻止浏览器打开页面。


解释:WebViewClient 源码中 shouldOverrideUrlLoading 方法已经返回 false,不设定setWebViewClient(),默认使用系统浏览器加载。如果重写该方法并返回true, 就可以实现在app页面中加载新链接而不去打开浏览器。


【误区2】 : 每一个url加载过程都会经过 shouldOverrideUrlLoading 方法。


Q1:加载一定会触发shouldOverrideUrlLoading?


Q2:触发时机一定在onPageStarted调用之前?


解释:关于shouldOverrideUrlLoading的触发


1)如果在点击页面链接时通过标签跳转,触发方法如下:


shouldOverrideUrlLoading() —> onPageStarted()—> onPageFinished()


2)如果使用loadUrl加载时,触发方法如下:


onPageStarted()—>onPageFinished()


3)如果使用loadUrl加载重定向地址时,触发方法如下:


shouldOverrideUrlLoadings—>onPageStarted —> onPageFinished


ps:多次重定向的过程,


onPage1Started


—>shouldOverrideUrlLoadings


—>onPage2Started —> xxx...


—> onPageNFinished


结论:shouldOverrideUrlLoading()方法不是每次加载都会调用,WebView的前进、后退等不会调用shouldOverrideUrlLoading方法;非loadUrl方式加载 或者 是重定向的,才会调用shouldOverrideUrlLoading方法。


【误区3 】: 重写 shouldOverrideUrlLoading 方法返回true比false的区别,多调用一次onPageStarted()和onPageFinished()。


解释:返回True:应用代码处理url;返回False,则由 WebView 处理加载 url。


ps:低版本系统(华为6.0),测试 False比True会多调用一次onPageStarted()和onPageFinished(),这点还在求证中。


4、WebChromeClient


对比WebviewClient , 添加了处理JavaScript对话框,图标,标题和进度等。


处理对象 : 影响浏览器的事件


(1)常用方法:

//alert弹出框
public boolean onJsAlert(WebView view, String url, String message,JsResult result){
return true;//true表示拦截
}

//confirm弹出框
public boolean onJsConfirm(WebView view, String url, String message,JsResult result){
return false;//false则允许弹出
}

public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

//打印 console 信息。return true只显示log,不显示js控制台的输入;false则都显示出来
public boolean onConsoleMessage(ConsoleMessage consoleMessage){
Log.e("测试", "consoleMessage:"+consoleMessage.message());
}

//通知程序当前页面加载进度,结合ProgressBar显示
public void onProgressChanged(WebView view, int newProgress){
if (newProgress < 100) {
String progress = newProgress + "%";
Log.e("测试", "加载进度:"+progress);
webProgress.setProgress(newProgress);
}
}

(2)拦截示例:


JsResult.comfirm() --> 确定按钮的调用方法


JsResult.cancle() --> 取消按钮


示例:拦截H5的弹框,并显示自定义弹框,点击按钮后重定向页面到别的url

@Override
public boolean onJsConfirm(final WebView view, String url, String message, final JsResult result) {
Log.e("测试", "onJsConfirm:"+url+",message:"+message+",jsResult:"+result.toString());
new AlertDialog.Builder(chromeContext)
.setTitle("拦截JsConfirm显示!")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
//重定向页面
view.loadUrl("file:///android_asset/javascript.html");
result.confirm();
}
}).setCancelable(false).create().show();
return true;
}

5、WebSettings


用于页面状态设置\插件支持等配置.


(1)常用方法

WebSettings webSettings = webView.getSettings();
/**
* 设置缓存模式、支持Js调用、缩放按钮、访问文件等
*/
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(true);

//允许WebView使用File协议,访问本地私有目录的文件
webSettings.setAllowFileAccess(true);

//允许通过file url加载的JS页面读取本地文件
webSettings.setAllowFileAccessFromFileURLs(true);

//允许通过file url加载的JS页面可以访问其他来源内容,包括其他的文件和http,https等来源
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setLoadsImagesAutomatically(true);
webSettings.setDefaultTextEncodingName("utf-8")

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


作者:37手游移动客户端团队
链接:https://juejin.cn/post/7245084484756144186
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

鹅厂组长,北漂 10 年,有房有车,做了一个违背祖宗的决定

前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。 抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」...
继续阅读 »

前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。


抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」时腾讯做的「艰难的决定」来的轻松。如今距离这个决定过去了快 3 个月,我也还在适应着这个决定带来的变化。


按照工作汇报的习惯,先说结论:



在北漂整整 10 年后,我回老家合肥上班了



做出这个决定的唯一原因:



没有北京户口,积分落户陪跑了三年,目测 45 岁之前落不上



户口搞不定,意味着孩子将来在北京只能考高职,这断然是不能接受的;所以一开始是打算在北京读几年小学后再回老家,我也能多赚点钱,两全其美。


因为我是一个人在北京,如果在北京上小学,就得让我老婆或者让我父母过来。可是我老婆的职业在北京很难就业,我父母年龄大了,北京人生地不熟的,而且那 P 点大的房子,住的也憋屈。而将来一定是要回去读书的,这相当于他们陪着我在北京折腾了。


或者我继续在北京打工赚钱,老婆孩子仍然在老家?之前的 6 年基本都是我老婆在教育和陪伴孩子,我除了逢年过节,每个月回去一到两趟。孩子天生过敏体质,经常要往医院跑,生病时我也帮不上忙,所以时常被抱怨”丧偶式育儿“,我也只能跟渣男一样说些”多喝热水“之类的废话。今年由于那啥,有整整 4 个多月没回家了,孩子都差点”笑问客从何处来“了。。。


5月中旬,积分落户截止,看到贴吧上网友晒出的分数和排名,预计今年的分数线是 105.4,而实际分数线是 105.42,比去年的 100.88 多了 4.54 分。而一般人的年自然增长分数是 4 分,这意味着如果没有特殊加分,永远赶不上分数线的增长。我今年的分数是 90.8,排名 60000 左右,每年 6000 个名额,即使没有人弯道超车,落户也得 10 年后了,孩子都上高一了,不能在初二之前搞到户口,就表示和大学说拜拜了。


经过我的一番仔细的测算,甚至用了杠杆原理和人品守恒定理等复杂公式,最终得到了如下结论:



我这辈子与北京户口无缘了



所以,思前想后,在没有户口的前提下,无论是老婆孩子来北京,还是继续之前的异地,都不是好的解决方案。既然将来孩子一定是在合肥高考,为了减少不必要的折腾,那就只剩唯一的选择了,我回合肥上班,兼顾下家里。


看上去是个挺自然的选择,但是:



我在腾讯是组长,团队 20 余人;回去是普通工程师,工资比腾讯打骨折



不得不说,合肥真的是互联网洼地,就没几个公司招人,更别说薪资匹配和管理岗位了。因此,回合肥意味着我要放弃”高薪“和来之不易的”管理“职位,从头开始,加上合肥这互联网环境,基本是给我的职业生涯判了死刑。所以在 5 月底之前就没考虑过这个选项,甚至 3 月份时还买了个显示器和 1.6m * 0.8m 的大桌子,在北京继续大干一场,而在之前的 10 年里,我都是用笔记本干活的,从未用过外接显示器。


5 月初,脉脉开始频繁传出毕业的事,我所在的部门因为是盈利的,没有毕业的风险。但是营收压力巨大,作为底层的管理者,每天需要处理非常非常多的来自上级、下级以及甲方的繁杂事务,上半年几乎都是凌晨 1 点之后才能睡觉。所以,回去当个普通工程师,每天干完手里的活就跑路,貌似也不是那么不能接受。毕竟自己也当过几年 leader 了,leader 对自己而言也没那么神秘,况且我这还是主动激流勇退,又不是被撸下来的。好吧,也只能这样安慰自己了,中年人,要学会跟自己和解。后面有空时,我分享下作为 leader 和普通工程师所看到的不一样的东西。


在艰难地说服自己接受之后,剩下的就是走各种流程了:

1. 5月底,联系在合肥工作的同学帮忙内推;6月初,通过面试。我就找了一家,其他家估计性价比不行,也不想继续面了
2. 6月底告诉总监,7月中旬告诉团队,陆续约或被约吃散伙饭
3. 7月29日,下午办完离职手续,晚上坐卧铺离开北京
4. 8月1日,到新公司报道

7 月份时,我还干了一件大事,耗时两整天,历经 1200 公里,不惧烈日与暴雨,把我的本田 125 踏板摩托车从北京骑到了合肥,没有拍视频,只能用高德的导航记录作为证据了:


北京骑摩托回合肥


这是导航中断的地方,晚上能见度不行,在山东花了 70 大洋,随便找了个宾馆住下了,第二天早上出发时拍的,发现居然是水泊梁山附近,差点落草为寇:


水泊梁山


骑车这两天,路上发生了挺多有意思的事,以后有时间再分享。到家那天,是我的结婚 10 周年纪念日,我没有提前说我要回来,更没说骑着摩托车回来,当我告诉孩子他妈时,问她我是不是很牛逼,得到的答复是:



我觉得你是傻逼



言归正传,在离开北京前几天,我找团队里的同学都聊了聊,对我的选择,非常鲜明的形成了两个派系:

1. 未婚 || 工作 5 年以内的:不理解,为啥放弃管理岗位,未来本可以有更好的发展的,太可惜了,打骨折的降薪更不能接受

2. 已婚 || 工作 5 年以上的:理解,支持,甚至羡慕;既然迟早都要回去,那就早点回,多陪陪家人,年龄大了更不好回;降薪很正常,跟房价也同步,不能既要又要

确实,不同的人生阶段有着不同的想法,我现在是第 2 阶段,需要兼顾家庭和工作了,不能像之前那样把工作当成唯一爱好了。


在家上班的日子挺好的,现在加班不多,就是稍微有点远,单趟得 1 个小时左右。晚上和周末可以陪孩子玩玩,虽然他不喜欢跟我玩🐶。哦,对了,我还有个重要任务 - 做饭和洗碗。真的是悔不当初啊,我就不应该说会做饭的,更不应该把饭做的那么好吃,现在变成我工作以外的最重要的业务了。。。


比较难受的是,现在公司的机器配置一般,M1 的 MBP,16G 内存,512G 硬盘,2K 显示器。除了 CPU 还行,内存和硬盘,都是快 10 年前的配置了,就这还得用上 3 年,想想就头疼,省钱省在刀刃上了,属于是。作为对比,腾讯的机器配置是:



M1 Pro MBP,32G 内存 + 1T SSD + 4K 显示器


客户端开发,再额外配置一台 27寸的 iMac(i9 + 32G内存 + 1T SSD)



由奢入俭难,在习惯了高配置机器后,现在的机器总觉得速度不行,即使很多时候,它和高配机没有区别。作为开发,尤其是客户端开发,AndroidStudio/Xcode 都是内存大户,16G 实在是捉襟见肘,非常影响搬砖效率。公司不允许用自己的电脑,否则我就自己买台 64G 内存的 MBP 干活用了。不过,换个角度,编译时间变长,公司提供了带薪摸鱼的机会,也可以算是个福利🐶


另外,比较失落的就是每个月发工资的日子了,比之前少了太多了,说没感觉是不可能的,还在努力适应中。不过这都是小事,毕竟年底发年终奖时,会更加失落,hhhh😭😭😭😭


先写这么多吧,后面有时间的话,再分享一些有意思的事吧,工作上的或生活上的。


遥想去年码农节时,我还在考虑把房子从昌平换到海淀,好让孩子能有个“海淀学籍”,当时还做了点笔记:


买房笔记


没想到,一年后的我回合肥了,更想不到一年后的腾讯,股价竟然从 500 跌到 206 了(10月28日,200.8 了)。真的是世事难料,大家保重身体,好好活着,多陪陪家人,一起静待春暖花开💪🏻💪🏻


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

分享Android App的几个核心概念

Application启动 点击桌面图标启动App(如下流程图) 针对以上流程图示: ActivityManagerService#startProcessLocked() Process#start() ActivityThread#main(),入口分...
继续阅读 »

Application启动


点击桌面图标启动App(如下流程图)


1240.jpg


针对以上流程图示:



  • ActivityManagerService#startProcessLocked()

  • Process#start()

  • ActivityThread#main(),入口分析的地方

  • ActivityThread#attach(),这个里面的逻辑很核心 ActivityManagerService#attachApplication(),通过Binder机制调用了ActivityManagerService的attachApplication

  • ActivityManagerService#attachApplicationLocked(),整个应用进程已经启动起来

  • ActivityManagerService#thread.bindApplication,具体回到ActivityThread

  • ActivityThread.ApplicationThread#bindApplication(),最后看到sendMessage处理bind逻辑

  • ActivityThread#handleBindApplication(),设置进程的pid,初始化进程信息

  • ActivityThread#mInstrumentation.callApplicationOnCreate,看到Application进入onCreate()方法中,这就是从最开始main()方法开始到最后的Application的onCreate()的创建过程


Window创建


如何创建Window


在创建Activity实例的同时,会调用Activity的内部方法attach方法完成window的初始化。Activity类中相关源码如下所示:

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
//创建 PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}


  • Window是一个抽象类,具体实现是PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main

  • 创建Window需要通过WindowManager创建,通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互


Android组件设计


ActivityManagerService




  • 启动组件



    • 组件启动时,检查其所要运行在的进程是否已创建。如果已经创建,就直接通知它加载组件。否则,先将该进程创建起来,再通知它加载组件。




  • 关闭组件



    • 组件关闭时,其所运行在的进程无需关闭,这样就可以让组件重新打开时得到快速启动。




  • 维护组件状态



    • 维护组件在运行过程的状态,这样组件就可以在其所运行在的进程被回收的情况下仍然继续生存。




  • 进程管理




    • 在适当的时候主动回收空进程和后台进程,以及通知进程自己进行内存回收




    • 组件的UID和Process Name唯一决定了其所要运行在的进程。




    • 每次组件onStop时,都会将自己的状态传递给AMS维护。




    • AMS在以下四种情况下会调用trimApplications来主动回收进程:



      • A.activityStopped,停止Activity

      • B.setProcessLimit,设置进程数量限制

      • C.unregisterReceiver,注销Broadcast Receiver

      • D.finishReceiver,结束Broadcast Receiver






Binder




  • 为组件间通信提供支持



    • 进程间;进程内都可以




  • 高效的IPC机制



    • 进程间的组件通信时,通信数据只需一次拷贝

    • 进程内的组件通信时,跳过IPC进行直接的通信




说一说DecorView


DecorView是什么




  • DecorView是FrameLayout的子类,它是Android视图树的根节点视图



    • DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。
    <LinearLayout >
    <ViewStub
    android:id="@+id/action_mode_bar_stub"/>
    <FrameLayout>
    <TextView
    android:id="@android:id/title"/>
    </FrameLayout>

    <FrameLayout
    android:id="@android:id/content"/>
    </LinearLayout>



  • 上面的id为content的FrameLayout中,在代码中可以通过content来得到对应加载的布局

    ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
    ViewGroup rootView = (ViewGroup) content.getChildAt(0);



Activity 与 PhoneWindow 与 DecorView 关系


12401.jpg


一个 Activity 对应一个 PhoneWindow,一个 PhoneWindow 持有一个 DecorView 实例,DecorView 本身是一个 FrameLayout。


如何创建DecorView




  • 从Activity中的setContentView()开始



    • 在Activity中的attach()方法中,生成了PhoneWindow实例。既已有Window对象,那么就可以设置DecorView给Window对象了。

    • 从中获取mContentParent。获得到后,通过installDecor方法生成DecorView,源码中操作比较复杂,大概先从主题中获取样式,根据样式加载对应的布局到DecorView中,为mContentParent添加View,即Activity中的布局。

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

三本学渣大厂之路

故事的开始 故事从 16 年开始,那年高三。和许多高三的学生一样,每天家和学校两点一线。本来应该老老实实备战高考,争取考个好大学,走向美好的未来。但一次意外的受伤,生活的轨迹开始发生了偏移。 由于备考压力大,学校也管的严,平时的消遣只剩了跑跑步活动下筋骨。但在...
继续阅读 »

故事的开始


故事从 16 年开始,那年高三。和许多高三的学生一样,每天家和学校两点一线。本来应该老老实实备战高考,争取考个好大学,走向美好的未来。但一次意外的受伤,生活的轨迹开始发生了偏移。


由于备考压力大,学校也管的严,平时的消遣只剩了跑跑步活动下筋骨。但在一次跑步的时候拉伤了肌腱,当时便疼痛难忍请假去了医院。医生说需要静养一周再去上学。本来也没啥,但是学校每周有周考,父亲认为这点小伤有啥好养的,耽误了学习怎么办?于是矛盾产生了,本来压力就大,有些厌学情绪在。再被要求一定要去周考,自然就不乐意了。


家乡是个四川的十八线城市,父亲是个正常的厂里员工,在他看来没有什么比好好学习考上一个好的大学更重要。只要没到性命攸关的时候,没啥理由可以放弃。十多岁正是逆反心理严重的时候,让怎么样偏不想怎么样。


于是在跟父亲大吵一架后,离家出走了。出走这段时间,也没走多远。因为没钱,白天找个书店蹭书看,晚上找个黑网吧没人的角落对付一下也就过了。那几天想了很多,关于人生或是其它的什么,但唯独没有想过回去读书。想着看能不能打个工,买票离开家乡。但由于当时没满十八,没有老板收。就这样过了几天,在一天晚上打算在网吧睡觉时,被警察通过监控找到了。于是被送回了家里,回家后自然还是较着劲,不管怎么说就是不回学校。


半年后直接参加了 16 年的高考,成绩自然不理想。考了 400 + 分,也就过了三本线,离一本线差一百多。选来选去在一堆三本中选了 成都东软学院 这个民办大学,当年说是取消三本于是学校就变成了二本。选东软的原因很简单,这是当时的分数能上的有计算机专业的学校。而选择计算机的原因也很简单,这是相对来说比较感兴趣的专业。


这个结果父亲自然是不满意的,反复让回去复读。但始终没能说动,于是他说不复读可以,反正就养大学几年,到时候该干嘛干嘛去,别回去找他。如果能妥协当时也就不会离家出走了,于是欣然答应,心中想着就算没能上一个好的大学,也不代表这辈子就完了。


开学后发现事情没有那么简单,总的来说除了环境不错,东软没啥值得说道的。学习的氛围不能说没有,只能说很少。于是矮个子里选高个去了学校里为数不多的实验室,到了实验室才开始了正式接触编程的生活。


浑浑噩噩


刚进实验室就发现,在这个不怎么样的学校,有不少竟然在大学前接触过编程了。没办法只好从头开始学起。实验室主攻的是算法,说来也好笑,到现在我的算法水平都是吃的那个时候的老本。


每天有空就去实验室,上课的时候就刷题(ACM),经常刷题刷到深夜。刚开学半个学期就刷了 300+ 算法题,虽然日子充实却逐渐开始迷茫了。每天除了刷题不知道出路在哪里,也看不到未来。


迷茫的情况在第一次参加算法比赛时就更严重了,本来在学校里凭着多刷的题,在新生里面水平还不错。但和电子科大之类的 985、211比起来啥也不是。到现在还记得,那次别人队里就一个人,时间没到就A完了。而我到比赛时间结束也就 A 了 5 道题。



A题:提交代码通过这道算法题所有数据即 Accept



比赛后,虽然拿了个省级三等奖,但我开始认识到算法这条路不太适合继续走下去。有很多大佬在高中的时候就参加了信息竞赛,虐电子科大都跟玩儿似的,更不用说我一个三本学渣了。


没多久就退出了ACM实验室。退出过后天天琢磨怎么样才能毕业后顺利找到工作,有人说多参加比赛拿奖,评奖评优对找工作有帮助(这个时候根本没有想着能进大厂),于是我又去参加各种比赛。


忙活一年,零零散散混了几个国奖,也拿到了奖学金。但我始终觉得这条路不太合适,因为学校已经落后一截了,在好的学校别人也能评奖评优,含金量不知道比三本的年级前几高了多少,到了毕业同台竞争肯定没戏。


虽然意识到了问题,却没有明确的方向,只能先做好手头的事情。


日子一天天浑浑噩噩地过去,有很多次想过要不就这样算了,跟室友们在召唤师峡谷交流交流感情不香嘛?每天忙东忙西,却仍旧一无所获。


直到那件事的发生,状况才开始了改变~


找到方向



虽然大学的生活一言难尽,但幸运的是遇见了相守一生的人


高考最美好的地方不是得偿所愿,而是阴差阳错



17年12月 女朋友过生,前面也说过,父亲十分反对我上三本,所以生活费也可想而知。为了准备礼物,绞尽脑汁,终于在一天刷 B 站的时候有了个主意。当时刷到一个用代码实现一个 3D 爱心的视频,于是跟着视频自己做了一个,在生日那天送给了女友。


虽然到现在那个爱心不知道丢到了哪个角落,但是那颗爱心让我发现了不同于算法的另一条道路。难以否认的是,除了搞算法起点太低之外,放弃算法的另一个重要原因就是打代码天天对着命令行(C语言),实在提不起兴趣。


前端的所见即所得深深吸引了我,于是决定走上前端的路子。


有了方向,剩的就是一路前行。


当时拿出了之前拿的所有奖学金,给自己报了个前端培训班。在三本这样的学校老师教的与行业脱节严重,只好寻求外援。


于是生活又回到了之前从早到晚,脚不沾地的样子。白天上课,晚上上培训班。虽然日子很忙,但心里有了方向也就能咬牙坚持了。


顺带说一句,虽然那个时候上了培训班,但是没有放下正常的学业。我的方向是单片机,虽然学校前沿的技术不太行,但计算机专业应有的计算机基础还是在教的。


有了算法和计算机基础,才使我跟上培训班的有了区分。也是后面能走的更远的基础。


屡败屡战


忙碌的日子持续到了18年,期间每年只有过年回家,暑假就呆在学校里自学。


过了一年苦修的日子,虽然知道还有很多知识没掌握,但毕竟还是少年,想掂量掂量自己的斤两。于是开始投起了简历,意料之中的是,所有简历都没有回音,毕竟大学实在有点拉,好多成都人,都不知道成都附近有成都东软这么个学校。(直到22年疫情封控,东软给成都做的健康码挂了)


当时想了想,反正也没人要,不如去看看大厂究竟要什么样的人,认清差距也好。于是把当时的BAT投了个遍(B那个时候还是百度)


讽刺的是之前算法的那些奖,小公司的简历筛选没过,反而过了阿里和腾讯的筛选(后来去了腾讯才知道,算法竞赛的简历会被标注)没多久就收到了笔试的邀请,凭借之前的算法和基础积累,竟然都通过了笔试,进入了第一轮面试。


还没来得及高兴,两盆凉水就泼到了脸上。不出意外,阿里和腾讯的一面挂了。在面试时能明显的感觉到面试官已经没啥能问的了(啥都不会还问啥)


面试挂了过后,消沉了一小段时间。但生活还得继续,痛定思痛之下开始反思失败的原因。主要在两点:



  1. 缺少项目经验

  2. 对知识的深度理解有限


于是针对这两点,我开始有意识地补充这方面地能力。缺少经验就去给老师打黑工,就为了能多一些实战的项目经验。知识深度不够就写博客,坚持日更,写每天学的内容(感兴趣的朋友可以去我的主页瞅瞅之前的博客园)


此外继续面试积累经验(又被腾讯挂了一次)


就这样又一年过去了。


柳暗花明


时间来到了19年,我继续投几家大厂的春招。相比之前已经有了肉眼可见的进步,最多的一次面到了美团的四面,但还是没能上岸(后面才知道跟美团八字不合)


没办法,继续沉淀吧。


终于,经过五轮面试拿到了腾讯 CSIG(腾讯云) 19年暑期实习的 offer。


现在已经记不清当时到底有多开心了,但想来应该是挺开心的。


拿到 offer 过后,有一个小小的问题。那个时候我其实是不能去实习的,因为才大三。没办法只好去找学校沟通。期间经过一堆的纠缠,终于学校还是愿意开个口子,让我去深圳实习。


19年7月坐上了成都飞往深圳的飞机,这也是我第二次坐飞机。也是在这个时候跟家里的关系才略微缓和了一些。但关系还是不好,因为我逐渐意识到,人的成长伴随着离开,只有离开家庭,依靠自己的力量在社会立足,才能真正长大(如果是富二代,那当我没说)


苦逼实习


去到深圳后,第一件事情就是找住的地方。本来在成都上学的时候我觉得像成都这样的物价房价已经很高了,在深圳才知道什么是小巫见大巫。



好笑的是刚开始跟女朋友在一起时,最纠结一点的就是她是成都人,我怎么在成都买得起房子,娶得起她



为了省钱,只好租在离公司很远的地方(20公里) 好在腾讯有班车,交通倒是省下一笔。即使这样也过得穷困潦倒(压三付一是真的离谱)。


生活上的困苦倒还好,工作上也不太顺心,主要有三个原因:



  1. 学校太差,感觉和同事格格不入(就没有一个不是重本的)

  2. 虽然做过项目,但没有工作经验,很多事情都要学

  3. 上班太卷(连着上了20天班,早9晚10)


虽然困难比较多,但还是十分珍惜这个机会,想着多提升自己也就没太在意。


很快就到了 9 月份,决定实习留用的时间。如果能留用,相当于就拿到了来年的校招offer,20年毕业就不用再找了。


也是到这个时候才知道,部门并没有 HC 就是找廉价实习劳动力的。


没办法,求人不如求己,于是乎又开始了面试。


正式上岸


虽然实习的时候被压榨的比较狠,但还是学到了东西。


凭着实习的经验和之前的积累,拿到了两个 offer



  • 京东凹凸实验室的 SP Offer

  • 腾讯 IEG 的白菜 Offer


由于腾讯钱给的比较多,还是去了腾讯。


拿到 Offer 后为了避免 Offer 被撕,10月份就去新部门报道了。去了新部门才知道,不是所有的班都是朝9晚10。


上班期间抽空做了个毕设,回去答辩的时候正好遇上疫情爆发。本来打算来个毕业旅游啥的,也只能云毕业了。


答完辩在家里呆了两个月调整了下,20年9月正式回到深圳,成为当当当(深圳修路的声音)的受害者之一


职场沉浮


回到公司一来就被派去接手一个边缘业务,那个业务本来是其它团队的,但由于那个团队去做新业务了,老的业务就丢给了我们团队。


其它同事早就有了自己负责的业务,没得选我只好去捡起这个烂摊子。


这个业务之前的负责同学都转走了,因此剩下交接的人员是之前负责团队的外包同学,于是上手变得十分困难。



  1. 腾讯的光荣传统口口相传,没有文档

  2. 外包同学没有一个全面的系统的了解

  3. 在上手之余还得管理外包


对于一个刚毕业的菜鸡,一下子就被砸得晕头转向,每天忙到很晚却没啥用。一直帮外包擦屁股,有些时候觉得还不如自己一个人写。


但是由于我之前的经历,我其实对自己的技术能力没有什么信心。于是在带外包的时候CR也没怎么做,让外包同学觉得我不管他们,有了意见。此外,这个业务原本的团队除了这几个外包同学还有三个正职,这才维持了业务的正常运转。


但现在只有我一个菜鸡正职,当然不怎么玩得转。于是当时的业务负责人找我谈话,希望我在做需求之余,还得抓外包的管理。我以自己的能力还不足为理由拒绝了。



当时的我没意识到这是个机会,由于之前的经历让我有点技术至上论,一心只想提升技术,觉得管理没用,换家公司就不行了,现在想来小丑就是我自己



于是在腾讯的两年就搞搞技术,混个辛苦绩效(三星堆),后面招了个高T来接手我原来负责的部分和外包的管理,次次四五星。



腾讯绩效1-5星,3星合格



因为腾讯是百分比绩效,所以我以一个相当离谱的理由背了低星。说我产出低,因为我 Git 提交的代码行数最少,没想到段子成为了现实。


不过幸好当时早有预感(有意无意的不给我活干),提前找好了下家(字节),才没有被搞得措手不及。


几经辗转,又回到了成都。


在字节卷了一阵后,逐渐开始觉得这样的生活没啥意思。付出自己所有的时间、健康就为了卷某次的绩效?


在最近减肥的过程中逐渐找到了答案,其实工作学习和减肥一样,重要的是习惯,而不是靠毅力。之前靠毅力一天只吃两顿、天天骑动感单车瘦了十斤,但由于阳了,两礼拜不能运动,直接反弹打回原形。


后来渐渐佛系了,不要求每天要运动多少,节多少食,只是养成一个有空运动运动的习惯,不知不觉间体重慢慢降到了之前心心念念的目标,也没有反弹。


想来工作学习也是一样,不必刻意追求结果。现在每天准点下班,到点就走,多去思考而不是多做,反而取得了不错的绩效。


写在最后


从离家出走的叛逆到决定证明自己,从浑浑噩噩到找到方向,从屡败屡战到佛系和解,其实人就是在经历的一件件事情中,找到自己真正想追寻的。


如果想要达到一个目标,好的习惯比坚韧毅力重要,好的方向比闷头努力重要,好的方法比恶意竞争重要。


种一棵树最好的时间就是现在,祝好~


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

一天约了4个面试,复盘一下面试题和薪资福利

前言 昨天我的交流群里,有位宝藏群友分享了自己的面试经历:一天约了4个线上面试,收获满满。 为大家分享了面试题和每个公司给出的薪酬范围。 简单说下这位群友的情况:3年开发经验,最近2年做Go语言开发,还有1年Java/PHP工作经验。手撕CSAPP选手,每天打...
继续阅读 »

前言


昨天我的交流群里,有位宝藏群友分享了自己的面试经历:一天约了4个线上面试,收获满满。


为大家分享了面试题和每个公司给出的薪酬范围。


简单说下这位群友的情况:3年开发经验,最近2年做Go语言开发,还有1年Java/PHP工作经验。手撕CSAPP选手,每天打卡分享手写的学习笔记。


注意:是每天手写笔记学习!每天+手写!


image.png


也有群友反馈,有人海投200多份简历没人回复,boss直聘上都是已读不回。对比宝藏群友一天约4个面试可以说是云泥之别。


很重要的一个原因,就是简历不行。或者你海投的公司都不靠谱,如果你海投200家都是已读不回,大概率不是这200家公司的问题......


我的经验分享


这是我之前找工作和简历优化的经验总结,希望对大家有帮助:


【求职复盘】我是怎么做到面试一次就拿到offer的?


【简历优化】怎么引导面试官提问自己准备好的问题?


【简历优化】如何在简历中最大化体现出自己的学习能力?


也欢迎大家关注我在掘金的 # 简历优化专栏


群友面经分享


再次感谢宝藏群友的分享,给了大家刷题的方向、市场的薪酬行情、更重要的是给了大家信心


在求职市场哀鸿遍野的情况下,还能做到一天能约到4个面试,不说别人,起码给了我很大的信心,对市场还是看好的。(如果这篇文章能给10个人带来信心,我就心满意足了)


(相同的事情,不同的人看会有不同的反馈:比如对于这件事情我是看到了市场的信心,还是有不少公司在招聘的,并且待遇不差,要求确实不低,没有像网传中说只读不会连面试机会都没有那么悲观;有的群友看到面试题的反馈是太卷,会的不多;有的群友看到的反馈是找开发岗位,Docker CNI 的实现都要考吗.....)


20K-30K 深圳


Docker 底层、多阶段构建、原子指令你怎么理解、CSP和Actor分布式模型的区别、内存对齐、Channel 和select的基本用法、赋值你认为有多少条汇编指令、比较出名的开源项目pr、Redis持久化、GMP模型、一致性算法


13-20K * 13 深圳


3个算法题,暴力1道,2道有思路,一个贪心、一个动态规划、一个冒泡。聊异步、业务、持久化


13-20K * 13 厦门


TCP 粘包怎么解决、同步控制 waitgroup 、数据库索引优化、TCP 如何实现可靠性、队列,树,栈的应用场景和区别、TCP 在 linux 中一些参数的含义、一些十进制转换二进制、十六进制、如何定位死锁,链表简单题


16-20K 上海


战争迷雾怎么实现、共识算法、Channel、Make和New的区别、GMP、数组类型算法、UDP实现可靠协议、分布式模型、Panic没被Recover怎么处理、切片扩容、Docker CNI 的实现、数据落盘怎么做的、Lua


总结


看到这里大家心里应该有个数了,可以看看自己的期望薪资,再看看目前市场上考察的这些知识点,查漏补缺。


也欢迎大家私信我,或者关注我的公众号 程序员升职加薪之旅,后面会持续更新面试题、面试复盘相关的文章,希望对大家有帮助,更欢迎大家的投稿分享。


需要做简历指导的也可以关注公众号,加我微信。


大厂面经


受高启强的影响,我也在读《孙子兵法》,分享这段话给大家:求其上,得其中;求其中,得其下,求其下,必败。


映射一下目前互联网的就业市场,道理简单明了:如果你想进中厂,就要做进大厂的准备。如果你想找到月薪1W+的工作,就需要做月薪1W5+的准备。如果你的目标就是找到工作,起码要做冲洗中小厂的准备。如果你的目标就是找个小公司混日子,大概率找不到工作。


为了更好的帮助到大家,我还整理了网络上很有价值的大厂面经:字节、腾讯、滴滴、腾讯云、小米、小米游戏。


希望对大家有帮助,建议收藏,并且转发给好朋友。



下面先分享一下我 学习小圈子 里字节嘉宾关于求职面试的答疑,大厂更看重的是什么? 给大家指指方向,少走弯路。



有问必答


提问


大佬好,最近我要去面试试水了,想问一下 有没有关于java或者go遇到的生产案例分享 最好是关于jdk或者第三方包的bug,容易加分。感谢!


回答


你是面校招还是社招?一般面试官会根据你的简历中项目经历、实现细节来展开逐层递进,你说的生产案例最好还是自己实战经历过的,不然很容易就发现不是你的项目或者会被打上项目参与不深入的标签。


每一次面试都尽量准备充分,不要抱着水水的心态,大公司面试都会留痕和面评的,如果你是想丰富下面试经验,建议你先找一些小公司或者不太想去的公司面一面找找感觉,自己心仪的公司和岗位一定要准备充分再去发起面试流程!


群友


我是属于社招,一般面试官会问处理过的最亮的技术点,目前是游戏平台后端开发 但实话实说 所用技术和闪光点太普通。


不是项目造假的意思,就是准备几个生产上处理过的几个难度较高的技术问题


嘉宾


建议你可以仔细盘一盘负责项目的文档、代码等资源,即使很多东西不是从0到1自己做的,也可以借鉴和领悟下其中的技术实现细节;平时也可以多写写技术文章,输出些自己工作内容中有技术特色的地方。


群友


我负责的项目就是我从0到1弄好的,包括文档和代码,里面确实没有拿高薪的技术亮点。


嘉宾


技术亮点是客观的项目经历,除非面试官也做过类似东西,能和你产生互动否则是不太感兴趣和深入聊的,技术栈是共同语言也便于考察个人技术能力,面试的时候也要学会主导话题,扬长避短多聊自己的优势点。说到“拿高薪”,这里说一句大白话:有多大本事拿多少钱。能力和薪资是正相关匹配的,一家公司招聘人才的能力模型会参考专业知识、工作阅历、个人性格等多方面,而面试的招聘过程双向是有信息差的,最终影响你薪酬水平的是面试结果(带有信息差的能力评价)+ 你当前的薪资水平、职级(自身当前的社会客观能力反馈)+ HR可操作的涨幅空间。


群友


理解,谢谢大佬的诚恳的解答,我还是从技术栈下功夫,这样和面试官的共鸣会高一些,也不再执迷于某个技术亮点。


嘉宾


不客气,加油!技术栈扎实绝对没问题💪


重点干货已经加粗标记了,上面这个问答建议再看一遍,很经典的问题。



以下面经来自网络,感谢大佬们的分享,非本人,我只是做了面经的搬运工,希望对大家有帮助。



字节面经


一面


自我介绍+算法题:



  1. leetcode-cn.com/problems/fi…

  2. leetcode-cn.com/problems/be…

  3. leetcode-cn.com/problems/be…


问答



  1. 索引,倒排索引,切词,如何根据 doc id 有没有出现某个 token

  2. 服务高可用是怎么做的

  3. MySQL 可重复读、读提交区别、原理

  4. 爬虫 URL 去重,设计存储结构(FST,前缀树+后缀树) MySQL (a,b,c) 索引,几条 SQL 走索引的情况

  5. 思考题:概率 p 生成 0,1-p 生成 1,如何 1/2 概率生成 1


二面


算法题:



  1. leetcode-cn.com/problems/be…

  2. leetcode-cn.com/problems/be…

  3. leetcode-cn.com/problems/co…


技术问题



  1. 讲一下 es 索引的过程

  2. 切词怎么切,切词算法,降噪

  3. 让你带应届生,怎么带,

  4. 有什么工程经验可以分享

  5. Redis 缓存淘汰有哪些


三面


自我介绍


算法题:



  1. leetcode-cn.com/problems/fi…


技术面



  1. 文章下面的评论,按点赞数排序,SQL 怎么写

  2. 把所有评论放到内存里,怎么设计数据结构,存储并排序

  3. select * 会有什么问题

  4. 缓存热 key 怎么解决

  5. 职业发展

  6. 领导如何评价你

  7. 项目难点,亮点


滴滴面经


一面



  1. 介绍项目

  2. 问我为什么选择GO,看我有Java从业经历。

  3. 介绍一下java 和 go 区别,我猜是让我说一些他们的不同点,go 比java 哪里好。我说了一些 特性
    3.1 问我协程比进程好在哪里? 我自己顺便说了进程线程 协程三者关系 4. 问我想从事什么

  4. 项目中有bloom介绍了一下怎么使用的,精度,损失

  5. GPM模型

  6. redis使用模式 主从 哨兵 巴拉巴拉

  7. 接着聊项目,然后问了算法

  8. 渐进式的聊面试,很轻松

  9. 问我能不能接受看php? 反问时候,聊了一下GORM,应用情况。他们的go-spring,还有他们的夜莺系统。因为看过一点点所以想问问。有培养体系,教我如何写GO(这个我很欣慰),说有大佬内部课程。


二面



  1. 自我介绍(面试官也不看我,一脸严肃我特害怕。然后自我介绍磕磕绊绊的)

  2. 问我看源码吗?

  3. 问了问GC 发展史,都怎么玩的 每次优化了啥

  4. 问了问我go 内存 优化了那些东⻄(这题我忘了咋问的了)

  5. 问了一下我项目里nodejs 升级为 java 为啥会快了那么多。 6.问了红黑树特性,哪个数据结构用到了。我介绍了一下 红黑树 一些特性 比如 平均查找时间 低 插入删除需要 左旋右旋调平衡。 我想到 java里 hashmap 用到了这个结构 7.问了一下map的底层结构 顺便介绍了一下 sync map

  6. 找出两个大文件交集

  7. 算法 leetcode 两棵树 b 是 a子集那道题思路 怎么做 dfs 然后比较 值和 指针

  8. 聊了一下 我的项目 召回相关的 和 nodejs java 迁移 效率提升问题

  9. 聊了一下 go-spring 夜莺 还有 didi 有个 写sql的github 项目 想问一下应用情况。问我能不能 接受 看看php 之类的

  10. 问了问我为啥离职


腾讯面经


一面



  • 算法题二选一


  • MySQL 隔离级别

  • MySQL 锁

  • MySQL 存储结构(b+树)

  • 索引 回表 是什么

  • 消息队列,rabbitmq

  • rabbitmq 如何保证可靠性(生产者可靠性、消费者可靠性、存储可靠性) - rabbitmq 几种模式

  • es 索引的过程

  • 线上是如何分表分库的,用什么做分表分库的策略,跨表查询

  • MySQL 如何同步到 es

  • 线上 Redis 用的是什么模式

  • 缓存热 key 怎么办


二面



  • 介绍项目

  • defer 、go 继承,手写快排

  • 登录流程,JWT、session、cookie


三面



四面(面委)



  • 项目为主

  • tcp quick_ack 、 nodelay ,socket 编程

  • 职业规划

  • 为什么换工作


五面(GM)



  • 项目

  • go 协程机制


腾讯云


这个面经来源于网络,这位朋友主要技术方向是k8s、容器、云计算。


有服务上云的实践经历,了解cicd基本流程,求知意向是容 器研发、基础架构研发、运维研发之类的(主要还是研发方向)。


项目方向:


项目的话我不多说什么,就是自己的项目细节自己肯定清楚,如果项目中不是自己做的
部分,建议不要在简历上写太多,写清楚自己做了什么,容易被抠细节问,项目一般都会抠细节,特别细的那种!!!


语言栈:


因为主要语言栈是go,所以一般都比较少问python。


golang


1、gin框架路由怎么实现的,具体正则怎么匹配?限流中间件怎么实现? 2、go的slice 与数组的区别,slice的实现原理,源码? 3、golang的协程调度,gpm模型。协程调度 过程中的锁。 4、golang的channel实现,channel有缓存和无缓存,一般会直接撸码 (三个goroutine顺序打印)。 5、golang的关键字defer、recover、pannic之类的实现 原理。 6、sync包里面的锁、原子操作、waitgroup之类的。 7、make和new的区别, 引用类型和非引用类型,值传递之类的。


python


1、python多线程、多进程。 2、python的装饰器怎么实现的?


操作系统


1、进程、线程、协程间的区别以及他们间的切换之类的,有时候会问到语言级别的协 程。 2、io复用、用户态/内核态转换 3、awk命令 4、linux查看端口占用 5、top命 令,free命令中的各个参数表示什么,buff/cache都表示什么?


k8s & 容器:


1、简单聊一下什么是云原生、什么是k8s、容器,容器与虚机相比优势。 2、k8s组 件,pod创建的过程,operator是什么? 3、docker是怎么实现的,底层基石 namespace和cgroup。 4、k8s的workload类型,使用场景,statefulset你们是怎么用 的? 5、limit和request,探针,一般怎么排查pod问题,查看上次失败的pod日志。 6、sidecar是什么,怎么实现的? 7、pv,pvc,动态pv怎么实现 8、k8s的声明式api 怎么实现的,informar源码。 9、cicd,发布模式。 10、svc的负载均衡、服务发现, ipvs与iptables。 以上基本是会被问的点(虽然有一些问题我也不是很熟),另外很多 会被问k8s的网络之类的,因为我比较菜,这块被问的比较少。


计算机网络:


1、tcp三次握手四次挥手,为什么不能是两次握手,三次挥手?握手和挥手过程中的状 态。 2、time_wait作用,为什么是2msl,close_wait作用,time_wait过多怎么办? 3、http请求的过程,浏览器输入网址请求过程?dns解析的详细过程? 4、https与http 的区别,https第一次服务端回传是否加密? 5、tcp与udp区别,tcp怎么保证可靠性。 6、http请求头、分隔符、⻓连接怎么实现


数据库:


1、mysql的事务,事务使用场景。 2、mysql的索引,什么情况下索引失效,聚簇索引 与非聚簇索引,索引的存储b+树与b-树区别。 3、join的内外连接,最左匹配原则。 4、redis的数据结构,hmap怎么实现的,持久化怎么做,go操作redis的方式。 数据库 方向有被问到,我基本没答上来(一般都告诉他只会基础,开发直接使用gorm)。


数据结构与算法:


1、倒排索引和B+树 2、判断链表是否有环,时间复杂度要求0(1) 3、LeetCode上合并 区间的题 4、leetcode的股票买卖的题 5、二叉树的最近公共祖先 6、有序数组合并 7、什么是平衡二叉树、最小堆 8、大文件的top10问题 9、golang实现栈、队列


其他:


1、git 的相关操作,合并commit,合并之类的。 2、场景设计(比较多)


小米面经


一面



  1. innodb MVCC实现

  2. b+树是怎么组织数据的,数据的顺序一定是从左到右递增的么

  3. ⻚分裂伪代码,b+树的倒数底层层可以⻚分裂么

  4. 合并k个有序链表

  5. redis的hashtable是怎么扩容的

  6. select poll epoll,epoll具体是怎么实现的

  7. GMP是怎么调度,channel是怎么收发消息的,channel的recq和g是怎么建立关系

  8. innodb二次写是什么

  9. undo里面具体存的是什么

  10. b+树节点具体存的是什么

  11. mysql一⻚最大能存多少数据

  12. myisam和innodb索引上的区别

  13. innodb commit之前,redo 的prepare然后binlog commit,然后redo再commit有
    什么缺点?5.6之后是怎么优化的? 14. redo和binlog的区别

  14. 读锁和写锁区别


二面



  1. 蛇形打印二叉树

  2. myisam为什么不支持事务,如果要支持事务要怎么做

  3. 函数只能返回1-7的随机数,请用这个函数返回1-5,要求平均 4. 聊项目


三面



  1. go的协程调度和os的线程调度有什么区别

  2. 只有写锁实现读写锁

  3. go的调度是怎么实现的

  4. go的网络IO为什么快?还有优化空间么

  5. epoll为什么这么快,还有优化空间么?如果要你实现一个网络IO应该怎么实现

  6. 设计一个每秒80万qps的过滤器

  7. 过滤器用redis实现,宕机期间数据怎么恢复

  8. 设计一个下单 扣减库存的分布式应用,请求超时了怎么办,一直重试超时了怎么办

  9. 数组A1 2和数组B2 3是一个关系圈,A能通过2找到3,数组A1 2和数组B2 3和数组
    C 3 5也是一个关系圈,给一个二维数组求关系数


小米游戏面经


一、 介绍连接池项目



  1. 介绍连接池常用的参数,最大连接数,最小存活数这些意义,为什么要有这些

  2. 当链接超过最大连接数怎么处理,等待有空闲连接还是创建一个继续给出,比较两
    者的优劣

  3. 连接池清理链接的逻辑,如何优化的

  4. 当连接池中有一些链接不可用了怎么办,如何保证这些连接的可用

  5. 当出现下游某个实例挂掉了,连接池应该怎么处理

  6. 对比 mysql redis http 连接池的实现


二、 介绍负载均衡算法



  1. 介绍平滑负载均衡算法,实现

  2. 当出现下游出现不可用,负载均衡算法怎么处理


三、 介绍聊天室项目



  1. 介绍实现原理的,互相通信的逻辑

  2. 聊天室服务端如何把消息下发给用户

  3. 介绍websocket包的字段

  4. 当有用户掉线怎么处理


四、 redis相关



  1. redis的数据结构

  2. 各个数据结构的操作

  3. 各个数据结构的使用场景

  4. 如何保证 Redis 的高可用

  5. 当有一个key读取的频率非常高怎么办


五、 算法相关



  1. 介绍快速排序 优先队列的实现


总结+鸡汤


就业环境再好,也有人找不到工作。


就业环境再差,也有人能找到工作。


要么学历🐂🍺,要么技术🐂🍺,要么都🐂🍺。


如果学历无法改变,请让技术🐂🍺,其他的都是扯淡~


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

我有一刀,可斩全栈

引言 夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。 概念 什么是全栈 全栈(Full-...
继续阅读 »

引言


夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。


概念


什么是全栈



全栈(Full-Stack)是指一种解决问题域全局性技术的能力模型。


很多现代项目开发,需要掌握多种技术,以减少沟通成本、解决人手不够资源紧张、问题闭环的问题。全栈对业务的价值很大,如对于整个业务的统筹、技术方案的判断选型、问题的定位解决等,全栈技术能力有重要影响。另外对于各种人才配套不是很齐全的创业公司,全栈能解决各种问题,独挡多面,节省成本,能在早期促进业务快速发展。


技术有两个发展方向,一种是纵向一种是横向的,横向的是瑞士军刀,纵向的是削铁如泥的干将莫邪。这两个方向都没有对与错,发展到一定程度都会相互融合,就好比中国佛家禅修的南顿北渐,其实到了最后,渐悟与顿悟是一样的,顿由渐中来。可以说全栈什么都会,但又什么都不会。



全栈定义


狭义


全栈 = 前端 / 终端 + 后端


广义(问题全域)


全栈 = 呈现端(硬件 + 操作系统(linux/windows/android/ios/..) + 浏览器/宿主环境+端差异【机型、定制】) +H5+小程序(多端统一框架)+ 前端开发/终端开发 + 网络 + 后端开发(架构/算法) + 数据(SQL/NoSQL/半结构/时序/图特性) + 测试 + 运维


+软实力=文档能力+UI能力+业务能力+设计能力+技术视角(前瞻性)选型+不同语言掌握能力+项目管理能力+架构设计能力+客户沟通能力+技术撕逼能力+运营能力


价值


全局性思维


一个交付项目的全周期,除了传统的软件过程,需求调研、规划、商务、合同签订、立项、软件过程、交付、实施运维等,麻雀虽小,五脏俱全,如果对并发、相应、扩展性、并行开发等有硬性要求,软件过程会变得异常复杂,因此后来又拆前端架构、后端架构定向的解决某个领域内的技术规划岗位,因为人力反倒是小问题,要的是快和结果稳定,项目可以迅速肢解投入,每个岗位注重领域和边界问题,以做沟通的核心基础,对于一个团队特别是互联网企业来说,有一个全局性思维的人非常非常重要,这个角色常常会被赋予(产品/项目)或其他Tile,什么事业线、军团之类的,本质上也是对人员的细节化和边界的扩充。
回到本质问题,当人成为问题的时候,以3个人为例,一般开发层的东西,3个合理偏重的 【狭义全栈】,做事的效率和执行沟通结果和3个1+2的分端是完全不同的,一个是以业务块沟通的,一个是以功能块沟通的,一个是对业务块结果负责,一个是对功能块结果负责。


其实刚入职那会儿,就有人和我说,服务是看不到的,端是直面的,这其中有个度的问题,不过度设计、不过度随意,保持需求和设计在合理区间内,有适度的前瞻性即可。
我之前接触的单端普遍会犯在业务不可能的场景下,纯粹讨论逻辑性的问题,导致的无休止的无意义讨论,最终的反思是 我想把这个东西做好, 举个不太恰当的例子叫 "有一种冷,叫妈妈觉得你冷",我把这种归结起来就是不对结果负责,只对自己负责,这也多半是因为岗位边界的问题导致的。


沟通成本


项目越大,沟通成本越高,做过项目管理的都知道,项目中的人力是1+1<2的,人越多效率越低。因为沟通是需要成本的,不同技术的人各说各话,前端和后端是一定会掐架的。每个人都会为自己的利益而战,毫不为己的人是不存在的。


而全栈工程师的沟通成本会主要集中在业务上,因为各种技术都懂,胸有成竹,自己就全做了。即使是在团队协作中,与不同技术人员的沟通也会容易得多,让一个后端和一个前端去沟通,那完全是鸡同鸭讲,更不用说设计师与后端了。但如果有一个人懂产品懂设计懂前端懂后端,那沟通的结果显然不一样,因为他们讲的,彼此都能听得懂,相信经历过(纯业务/纯管理/纯产品)蹂躏过的开发应该有体会。


性价比与结果控制


创业公司不可能像大公司一样,各方面的人才都有。所以需要一个多面手,各种活都能一肩挑,独挡多面的万金油。对于创业公司,不可能说DBA前端后端客户端各种人才全都备齐了,很多工作请人又不饱和,不请人又没法做,外包又不放心质量,所以全栈工程师是省钱的一妙招,大公司不用担心人力,小公司绕不过的就是人力,当人力被卡住,事情被挡住了,独当一面可不只是说说而已,此时的价值就会被凸显,技术解决问题的途径很多样。


这里说个题外话,性价比是对企业的,那对个人来说,意味着个人的能量和价值会放大,如果你细心观察开源的趋势,会发现整体性的项目趋势变多了,而且基本在微小的时候可能只是单人支撑的,这个趋势从百度技术领跑再到阿里转换时有过方向和风格的转换。


困境


说得不好听一点,全栈工程师就是什么都会,什么都不会,但有需求,结果、时间、风险都会被很好的评估,因为思路和理念是完全不同的,全栈天然的就必然会重视执行结果,单端只注重过程,事情做了,坏的结果跟我一点儿关系都没有,其中甘苦,经历了才知道,所以也注定面试是不占优势的,而且全栈根本没有啥标准的划分,也注定游离在小公司才能如鱼得水,当然,如果你的目标是星辰大海,工作自由,这个事就另当别论了。


发展


天下大事分久必合,合久必分,最开始的没有前端,到分出前端,没有安卓/IOS到分出岗位,再到手机端合到前端,pc到前端,”大前端“的概念,不管技术怎么进步或者变化,总归是要为行业趋势负责的,就好比你为300人的企业用户考虑高并发,完全不计较实施和人力成本,很多的事情都是先试水再铺开的,没那么技术死板。


感觉整个软件生态发展至今,提供便利的同时,也用框架把每个人往工具这个方向上在培养,这本就是符合企业利益的事,但减量环境下,螺丝钉的支撑意义被无限的减弱和消磨,很多的单端从业一段时间后,想做事儿,发现另外领域的空白,也开始往横向考虑,这本就是危机思考和方向驱动的结果,一个大周期的循环又开始了,特别是在java国内的一家独大,再到个体开始挣扎的时候,多态的语言开始反噬,反噬的驱动力也从服务器这个层级开始了挣扎,亦如当年的java跨平台先机一样。


前端的框架随着框架的便捷性和易用性越来越完善,其竞争力变得隐形了,回归了工程化问题的解决能力,去年也提过,变化中思考,稳定中死亡,到了思考自己的核心竞争力是什么的时候了,这何尝不是自由工作者的春天。


端扩散


软件的路程发展已经有了很长一段路,概念和业务层级的提升服务有限,自动化、半自动化、AI的概念渐渐的可以走向技术成熟,端的发展又有了去处,只不过这个过程很慎重,需要打通很多封闭的东西,再加上工业信息化的政策加持,单纯的信息录入或者业务系统已经掀不起多大风浪,而纯互联网的金融、物联网也被玩的渣都不剩,突围和再上一层的变革,短时间内,公司级的突破已经很难找到出路,从收缩阵地,裁剪人员可见一斑。


复杂度提升


如果说有确切的变化,那基本就是我机器上的编译器环境和用的工具越来越多样,解决问题的途径和手段越来越多,不再是原来的一个整合ide解决所有问题,这就好比,我原先手上只有木棍,武器用它、做房子用它、生火也用它,挖掘的它所有的应用途径,那有一天,我有了刀、有了席梦思的床、有了大别墅,却因为害怕放着不用。当然,我之前听别人说过一个理论:”只要能解决好结果,哪怕你徒手,我也无所谓“,他站在老板的角度上,至于你是累死也好,花10倍的工作量也好,都无所谓。作为个体来说,既然只要结果,那就别怪我偷工作量了,个体的掌握技能的多样性,背后可是有语言生态支持的,因此复杂度的提升,也带来了生态支持,并非一边倒的情况。


人心异化


我依然怀念头几年的环境,都是集中在解决问题,目标一致,各自解决各自的问题,拼到一起,就是整体结果,各自的同事关系轻松和谐,上线前的交付大家一起搞的1点多,下班宵夜美滋滋,现在端分离和职责明确,天然存在利益冲突,摸鱼划水,撕逼的情况,虽说可能是部分老鼠屎引起的,但谁说这不是热情消退的结果呢,生活归生活,工作归工作,但生活真的归了生活,工作真的只归了工作吗?


思考


全栈的title就跟我参与了xxx开源项目一样,貌似也成为提升竞争力,标签化的一种,架构师、小组长、技术经理、总监,这些title,在离职那一刻其实都毫无意义,有意义的也只是待遇和自身的能力,如果你怀着高title在另外一家公司风生水起的想法,那很多3个月离职的经历,再一家还是3个月,难道不是面试能力和自身的能力出现不对等了嘛,可能是所有的公司都坑,那有没有可能是我们韧性太低,选择不慎呢。


好像刚工作那会儿,经常会被问到职业规划,之后很少被问到,却不停的在想,我能干嘛,今后想干嘛,之后就是无休止的躁动和不停的学习,不停的接项目,不停的用新技术,10年多的坚持,平均12点,找的工作基本也都是相对轻松的,那我能干啥,好像貌似什么也做不了,想法创意不停的被对比否认,找到合适的却不停的为盈利性的项目让路,貌似什么都会,貌似什么都没做成,原本以为是觉得自己修炼不够,没法实现自己的项目,后来发现,其实自己的第二职业,只需要一条路,一往无前的坚持,最终会有结果,尽管这个结果可能不好,但事情实践了,回想起刚工作那会儿”先理顺环节,再开发,还是先出东西再说“的争论,这会儿我完全认同了 ”先结果,再谈未来“


因此,别管什么 ”前端已死“”java已死“,大环境不好,行业低迷,去行动吧,亲手埋葬也许,焕发新生也好,回到内心,做好与行业诀别的决心,背水一战。即便是为了生活被迫转行,也可毫不顾忌的说,努力过,没戏,直面内心,回想起18年看到的新闻,”程序猿直播7天0观众“,我想我能够做的也只能是武装与坚持,至于大环境怎样,行业怎样,到那一天再说吧,套用领导的话”别想那些有的没的,做好自己的事“,至少,我人为,当软件公司不易时,恰恰是个体的机会,当个体的力量开始有竞争力,那全栈的优势会有很好的发挥,这个场景在我有意识的5人实践和2人优势互补中已经得到了长效的验证。


未来


也许从当前的公司离职那天,就是我职业生涯结束那天,我已经做好了心里预期,但我希望可以作为一个自由工作者,这是我后半段反复思考的结果,至于结果怎样,我只能说,预期的努力我已经做了,时机和后续有待生活的刀斩我不屈之心。


PS


认清内心、从容面对,不要有什么鸵鸟心态,事实不逃避,行动不耽误,这是斩龙之刀,破除未知的迷雾,我所能提的也只是从心和认知,没啥发展途径和规划,因为技术的发展,总是未知和充满惊喜的,这也正是它的魅力所在。


最后


我深怕自己本非美玉,故而不敢加以刻苦琢磨,却又半信自己是块美玉,故又不肯庸庸碌碌,与瓦砾为伍。于是我渐渐地脱离凡尘,疏远世人,结果便是一任愤懑与羞恨日益助长内心那恬弱的自尊心。


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

程序员为了少加班想了这几招

天天开会,我还有时间写代码吗? 接口自测过没,怎么联调还有这么多问题? 需求怎么又变了,PD到底有没有个准信? 又是版本发布倒排,需求这么多能做的完吗? 以上各种场景大家在日常的工作中是不是经常碰到,今天慕枫就和大家聊一个非常有意思的话题,程序员为什么总是要加...
继续阅读 »

天天开会,我还有时间写代码吗?
接口自测过没,怎么联调还有这么多问题?
需求怎么又变了,PD到底有没有个准信?
又是版本发布倒排,需求这么多能做的完吗?
以上各种场景大家在日常的工作中是不是经常碰到,今天慕枫就和大家聊一个非常有意思的话题,程序员为什么总是要加班,到底谁偷走了程序员的时间,我们应该怎么做才能避免不必要的加班。


谁偷走了程序员的时间


要想少加班就要搞清楚是什么因素导致了程序员加班才能完成工作,也就是说我们得先弄明白程序员的时间到底被谁偷走了。我们先分析下程序员每天的工作主要包含了哪些内容,因为只有搞清楚程序员时间都用在哪里了,我们才能对症下药制定相应的优化计划,避免时间黑洞浪费精力资源在一些不重要的事情上,把时间和精力更多投入到重要的事情上以及需要完成的任务上。


有的同学可能会说,程序员嘛,工作不就是码代码嘛。如果真的只是这样,我想大家真的要烧高香了,实际上程序员的日常工作远远不至于写代码一个事情,甚至有的时候一天代码没写几行都在开会。


慕枫将程序员的工作主要划分到以下三大块内容中,接下来我们来具体分析下如何在这些繁杂的事项中把本该属于程序员编码、思考设计的时间给抢回来。


源源不断的会议


业务需求KO会议


在阿里,一般当业务方需求过来之后,PD或者产品经理会组织会议对技术同学进行产品需求进行KO或者澄清。在会议中PD或者产品经理需要讲清楚为什么要做这些需求、需求的内容是什么以及想要达到的业务效果,同时接受来自技术同学汹涌而来的挑战,一般这种会议打底要1到两个小时,有的时候讨论激烈的话可能都不止,甚至可能需要多次会议来回讨论才能确定需求。


技术评审会议


需求确认过之后,技术同学就需要进行对应的方案设计,我们想清楚如何做这个需求,同时输出相应的设计文档,设计文档中主要包含实现逻辑、修改点、时间计划、灰度策略等等,设计方案完成之后组内会先过下方案,把关下质量。同时负责这次整体需求的技术PM需要组织业务全链路节点的技术同学来对焦各自的技术方案,看看有没有什么遗漏之处,需不需要上下游业务节点的支撑,这个会议基本上1到两个小时。


测试评审会议


技术方案确认之后,测试同学就需要根据PRD文档以及设计方案来编写测试用例,也会组织会议找技术同学来看看测试方案合不合理,测试用例还有没有遗漏的地方。这个会议基本上1到两个小时。


故障复盘会议


这个会议大概是程序员最不想参加的会议了,在互联网公司出现故障是需要进行复盘的,一旦要开故障复盘会议就意味着出现了线上故障。程序员最担心的就是出现线上故障,如果搞出来一个P0级别的,基本一年就白干了,提前预定本财年3.25。


其他会议


还有一些其他会议,比如项目KO会议、平台对接会议、方案讨论会议、交互评审会议等等。


开发


技术同学开发编写代码以及功能自测,完了之后再和前端同学或者上下游业务的同学进行联调,这个过程也是比较耗时间的。因为在联调的时候总是会遇到这样或者那样的问题,比如环境问题、数据问题、代码Bug等等。其中最主要的就是代码Bug问题,因为有的技术同学可能由于时间关系根本没来得及自测就和大家进行联调,直接把联调当作自己的单元测试和功能自测了,所以这种情况下基本都是一边联调一边修改的状态。另外在联调的时候可能也会发现一些设计上对不齐的地方,这个时候也需要进行修改,同样需要耗费时间。


碎片化的杂事


杂事就很多了,比如别的团队的技术同学来向你咨询业务问题,你得花费时间向别人解答。小组内每个同学都需要进行技术分享,那你就得准备PPT,还得认真准备,因为我们需要不断建立自己的技术影响力,如果讲的东西太水了,实际上浪费自己的时间也浪费别人的时间。功能发布上线之前,TL以及组内的同学需要review你的代码,这也会耗费一个小时左右的时间。线上如果出现报警,我们就需要排查定位问题修复bug。类似这种碎片化的杂事充斥在我们的工作中。


如何避免不必要的加班


我们分析完了每天的时间都花在哪里之后,就可以进行针对性的进行时间压缩。


不必要的会议能不参加就不参加


这里举个栗子,在需求澄清会议中,一般会对一批需求进行澄清,这些需求很多时候只有一小部分和我们相关,所以在会议上我们对和我们相关的需求进行重点讨论和理解,在确认完需求内容之后就可以撤了,其他和自己不相关的需求没有必要再耗时间在会议上,这样可以大大压缩参加会议的时间。


避免表面一致


在整个研发活动中我们需要避免表面一致情况的发生,什么意思呢?就是我们需要真正把需求弄明白想清楚再动手写代码,否则在没搞清楚需求情况下写代码,很容易出现做出来的功能和产品要求的出现偏差的情况,那么在验收的时候就容易不通过,然后就是互相扯皮,研发说产品当时没有讲清楚,产品说是研发没有理解透需求。最后就是返工重新修改,白白浪费了时间。所以我们不如在需求确认阶段,多花点时间和产品经理进行需求的深入沟通,把需求吃透了确保没有理解上的偏差,避免表面一致,这样可以帮助我们节约很多时间。


琐碎事情统一处理


我们所负责的业务不可避免的会有业务合作方来咨询你如何用平台以及对接的事情,比如平台如何使用,有哪些开放能力等等。来咨询的同学一般呈现散点状,也就是说不固定时间来咨询,有的时候你正在奋笔疾书写代码,但是别人过来咨询就会打断你的编码思路占用开发时间。所以这个时候你可以考虑专门下午四点到五点固定时间段拉个群来统一回复别人的问题,或者说如何对接的写一篇文档,有别人过来问的就先让文档,这就好比智能客服一样,这样可以帮助你挡掉70%左右的问题。不免这种业务答疑打乱你原本的开发工作。


加强代码质量管理


自己写的代码的时候要注重代码质量控制,这样在和别人联调的时候可以保证自己没问题节约一部分时间,另外上线后没有问题,更加节省了后续在向上定位排查解决问题以及复盘的时间,如果代码质量不好匆忙上线可能会浪费自己更多的时间。


总结


本文主要和大家分析了下程序员的时间都花费在哪些事项当中,我们怎么做才能避免不必要的加班。如果文中有适合大家使用的小技巧,可以在实际工作中试一试。我一直觉得工作是为了更好的生活,所以我们努力减少不必要的加班,多陪陪家人,多分给生活多一点时间,相信可以减少我们的精神内耗。


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

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的而且消息已读后需要多次刷新会话列表才会清空未读消息

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的
而且消息已读后需要多次刷新会话列表才会清空未读消息

如果你的同事还不会配置commit提交规范,请把这篇文章甩给他

Git
前言 首先问问大家在日常工作中喜欢哪种commit提交?git commit -m "代码更新" git commit -m "解决公共样式问题" git commit -m "feat: 新增微信自定义分享" 如果你是第三种,那我觉得你肯定了解过com...
继续阅读 »

前言


首先问问大家在日常工作中喜欢哪种commit提交?

git commit -m "代码更新"

git commit -m "解决公共样式问题"

git commit -m "feat: 新增微信自定义分享"

如果你是第三种,那我觉得你肯定了解过commit提交规范,可能是刷到过同类文章也可能是在工作中受到的要求


我自己是在刚出来实习的一家公司了解到的,依稀记得“冒号要用英文的,冒号后面要接空格...”


虽然我一直保持这种习惯去提交代码,但是后面遇到的同事大部分都是放飞自我的提交,看的我很难受


因此这篇文章就教还不会配置的小伙伴如何配置被业界广泛认可的 Angular commit message 规范以及呼吁大家去使用。


先来了解下commit message的构成

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

对应的commit记录如下图


微信截图_20230608114515.png




  • type: 必填 commit 类型,有业内常用的字段,也可以根据需要自己定义



    • feat 增加新功能

    • fix 修复问题/BUG

    • style 代码风格相关无影响运行结果的

    • perf 优化/性能提升

    • refactor 重构

    • revert 撤销修改

    • test 测试相关

    • docs 文档/注释

    • chore 依赖更新/脚手架配置修改等

    • workflow 工作流改进

    • ci 持续集成

    • types 类型定义文件更改

    • wip 开发中

    • undef 不确定的分类




  • scope: commit 影响的范围, 比如某某组件、某某页面




  • subject: 必填 简短的概述提交的代码,建议符合 50/72 formatting




  • body: commit 具体修改内容, 可以分为多行, 建议符合 50/72 formatting




  • footer: 其他备注, 包括 breaking changes 和 issues 两部分




git cz使用


只需要输入 git cz ,就能为我们生成规范代码的提交信息。


一、安装工具

npm install -g commitizen // 系统将弹出上述type、scope等来填写
npm install -g cz-conventional-changelog // 用来规范提交信息

ps:如果你是拉取别人已经配置好git cz的项目,记得也要在自己环境安装


然后将cz-conventional-changelog添加到package.json中

commitizen init cz-conventional-changelog --save --save-exact

微信截图_20230608155514.png


二、使用git cz提交


安装完第一步的工具后,就可以使用git cz命令提交代码了


微信图片_20230608092741.png


微信图片_20230608092732.png


如图,输入完git cz命令后,系统将会弹出提交所需信息,只需要依次填写就可以


commitlint使用


如果你不想使用git cz命令去提交代码,还是习惯git commit的方式去提交


那么接下来就教大家怎么在git commit命令或者vscode工具中同样规范的提交代码


一、安装工具

npm install --save-dev husky
npm install --save-dev @commitlint/cli
npm install --save-dev @commitlint/config-conventional

二、配置



  • 初始化husky
npx husky install


  • 添加hooks
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit \$1'


  • 在项目根目录下创建commitlint.config.js,并配置如下
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-case': [2, 'always', ['lower-case', 'upper-case']],
'type-enum': [2, 'always',[
'feat', // 增加新功能
'fix', // 修复问题/BUG
'style', // 代码风格相关无影响运行结果的
'perf', // 优化/性能提升
'refactor', // 重构
'revert', // 撤销修改
'test', // 测试相关
'docs', // 文档/注释
'chore', // 依赖更新/脚手架配置修改等
'workflow', // 工作流改进
'ci', // 持续集成
'types', // 类型定义文件更改
'wip', // 开发中
'undef' // 不确定的分类
]
]
}
}

三、验证


没配置前能直接提交


微信图片_20230608092753.png


配置之后就会规范提交


微信图片_20230608092757.png


总结


以上两种方式都很简单,几个步骤下来就可以配置好,希望大家都能养成一个开发好习惯~


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

Handler真的难?看完这篇文章你就懂了!

在Android开发中,Handler是一个非常重要的组件,它可以用来实现线程之间的通信和任务调度。本篇文章将介绍Handler的使用方式和原理,帮助读者更好地理解Android开发中的线程处理。 什么是Handler? Handler是Android中的一个...
继续阅读 »

在Android开发中,Handler是一个非常重要的组件,它可以用来实现线程之间的通信和任务调度。本篇文章将介绍Handler的使用方式和原理,帮助读者更好地理解Android开发中的线程处理。


什么是Handler?


Handler是Android中的一个消息处理器,它可以接收并处理其他线程发来的消息。简单来说,Handler就是一个用来处理消息的工具类,它可以将消息发送给其他线程,也可以接收其他线程发送的消息进行处理。


Handler的使用方式


使用Handler的基本流程为:创建Handler对象 -> 发送消息 -> 处理消息。


在使用 Handler 之前,需要了解一些相关概念:



  • 线程:是独立运行的程序段,执行的代码是一个单独的任务。

  • 消息队列:是一种存储消息的数据结构,支持先进先出的队列操作。

  • Looper:可以让线程不停地从消息队列中取出消息并处理,是线程与消息队列交互的桥梁。

  • Message:是 Android 中处理消息的基本类,可以携带一些数据,用于在 Handler 中进行处理。


创建Handler对象


在使用Handler之前,需要先创建一个Handler对象。创建Handler对象的方式有两种:




  • 在主线程中创建Handler对象:


    在主线程中创建Handler对象非常简单,只需要在主线程中创建一个Handler对象即可:

    Handler handler = new Handler();



  • 在子线程中创建Handler对象:


    在子线程中创建Handler对象需要先获取到主线程的Looper对象,然后使用Looper对象来创建Handler对象:

    Handler handler = new Handler(Looper.getMainLooper());



发送消息


创建Handler对象之后,就可以使用它来发送消息了。发送消息的方式有两种:




  • 使用Handler的post()方法:


    使用Handler的post()方法可以将一个Runnable对象发送到Handler所在的消息队列中。Runnable对象中的代码会在Handler所在的线程中执行。

    handler.post(new Runnable() {
    @Override
    public void run() {
    // 在Handler所在的线程中执行的代码
    }
    });



  • 使用Handler的sendMessage()方法:


    使用Handler的sendMessage()方法可以将一个Message对象发送到Handler所在的消息队列中。Message对象中可以携带一些数据,用于在Handler中进行处理。

    Message message = new Message();
    message.what = 1;
    message.obj = "Hello World!";
    handler.sendMessage(message);



除了基本用法,Handler还有一些高级用法,下面列举了几个常用的:



  • 使用HandlerThread创建带有消息队列的线程,避免频繁地创建线程;

  • 使用Message.obtain()来获取Message对象,避免频繁地创建对象;

  • 使用Handler的sendEmptyMessage()方法来发送空消息。


处理消息


当其他线程发送消息到Handler所在的消息队列中时,Handler就会接收到这些消息并进行处理。处理消息的方式有两种:




  • 重写Handler的handleMessage()方法:


    重写Handler的handleMessage()方法可以处理其他线程发送的消息。handleMessage()方法中的代码会在Handler所在的线程中执行。

    Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case 1:
    String message = (String) msg.obj;
    // 处理消息的代码
    break;
    default:
    break;
    }
    }
    };



  • 实现Handler.Callback接口:


    实现Handler.Callback接口可以处理其他线程发送的消息。Callback接口中的方法会在Handler所在的线程中执行。

    Handler.Callback callback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
    switch (msg.what) {
    case 1:
    String message = (String) msg.obj;
    // 处理消息的代码
    break;
    default:
    break;
    }
    return true;
    }
    };
    Handler handler = new Handler(callback);



Handler的原理


在Handler的背后,实际上是使用了消息队列和线程通信的机制。当其他线程发送消息时,消息会被加入到Handler所在的消息队列中。然后,Handler会从消息队列中取出消息进行处理。


消息队列和Looper


消息队列和Looper是 Handler 实现的基础。每个线程都有一个消息队列(Message Queue),消息队列中存储着队列的所有消息(Message)。线程通过一个 Looper 来管理它的消息队列,通过不断地从消息队列中读取消息,实现了线程的消息循环 (Message Loop) 的功能。

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
// 一旦有消息,就会返回Message对象
msg.target.dispatchMessage(msg);
}
}

如上所示,一个线程中的消息无限循环直到队列里没有消息为止(MessageQueue.next())。消息通过 Message.target 属性来找到它想要执行的 Handler,从而被分配到正确的线程中并且得到执行。一旦有消息,就会调用 dispatchMessage(Message) 方法进行分发。


消息分发


消息分发是Handler 的核心部分,在它的内部逻辑中,也是最为关键的部分。


在 Handler 中,消息分发的流程如下:


1.1. 发送消息


由其他线程调用 Handler 的方法向消息队列中发送消息。

Handler handler = new Handler() ;
handler.post(new Runnable(){
@Override
public void run() {
// 在其他线程发送消息
}
});

1.2. 创建 Message 对象


将需要传输的数据封装成 Message 类型的对象,然后将该对象塞入消息队列中。

Message msg = new Message();
msg.obj = "消息内容";
handler.sendMessage(msg);

1.3. 将消息加入消息队列


Handler 将消息放入消息队列中。


在 Handler 内部,新构建的消息通过 enqueueMessage() 方法被加入到 MessageQueue 相应的内存块中,并且会在该内存块的标记 next 表示下一个内存块的索引号。

public void sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return;
}
msg.target = this;
queue.enqueueMessage(msg, uptimeMillis);
}

enqueueMessage() 方法的核心逻辑,就是紧接着找到消息队列中最近的一个时间戳比当前时间小的消息,将新消息插入到这个消息之后。

boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}

if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

boolean needWake;
if (mBlocked) {
// If the queue is blocked, then we don't need to wake
// any waiters since there can be no waiters.
msg.markInUse();
needWake = false;
} else {
msg.markInUse();
needWake = mMessagesForQueue.enqueueMessage(msg, when);
}

if (needWake) {
nativeWake(mPtr);
}
}

return true;
}

1.4. Looper 开启消息循环


Looper 不断轮询内部 MessageQueue 中的消息,获取消息后在 Handler 中进行分发处理。


在 Looper 类中,强制让当前线程创建一个 Looper 对象,并通过调用 QualityLooper 构造函数 create 方法捕获该对象(一般用于构建线程的消息循环)。接下来,通过调用 run 方法被延迟1秒钟来启动上下文中的消息循环。

public static void prepare() {
prepare(true);
}

public static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
continue;
}
msg.target.dispatchMessage(msg);
}
}

这个方法是一个无限循环方法,在每个循环中,Looper 都会从自己的消息队列中获取一个消息,如果队列为空,则一直循环等待新的消息到来,直到被调用 quit() 方法,才终止循环。在获取到消息之后,调用 msg.target.dispatchMessage(msg) 进行消息的分发处理。


1.5. 查找目标 Handler


Looper 不断轮询消息队列,获取消息后,注意到 MessageQueue.next() 方法中有这样一行代码:

msg.target.dispatchMessage(msg);

1.6. 传递 Message 对象


从消息中获取到 target 属性,它就是当前这个Message对象所属的 Handler,并执行该Handler的 handleMessage(Message) 方法。


dispatchMessage(Message) 的核心代码是判断 Message.target 是否为 null,不为 null 则将消息传递给目标 Handler,如果为 null,则直接抛出异常。

void dispatchMessage(Message msg) {
if (msg.callback != null) {
// 消息带有回调方法,如果 callback 不为空,那么就直接执行
handleCallback(msg);
} else {
if (mCallback != null) {
// 尝试将消息抛给 mCallback
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg); // 如果消息中没有 callback,那就执行 handleMessage(msg)
}
}

// 处理具体的 Message
public void handleMessage(Message msg) {
switch (msg.what) {
// 根据消息类型分发处理
default:
break;
}
}

当 Handler 接收到消息时,它会回调自己的 handleMessage(Message) 方法处理消息。


handleMessage(Message) 方法中,我们可以编写各种不同的逻辑,并对当前情况下的消息进行处理。这通常包括对消息类型的检查以及消息携带的数据的解析和操作。


当我们在 handleMessage(Message) 方法中完成了所有处理后,我们就可以将数据发送回发送消息的线程,或将数据传递给其他线程进行进一步处理。


总结


本篇文章深入探讨了 Handler 的原理,主要包括了消息队列和 Looper 的相关概念,以及消息的发送和处理。除此之外,还讲了当不同线程的消息需要在 Handler 中处理时,需要用到 Looper、MessageQueue 和 Handler 这三个关键组件的协同工作。


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

如何深入掌握 Android 系统开发的拦路虎 Binder

0. 为什么要深入学习 Binder Binder 是整个 Android 的基石 所有的系统服务都是基于 Binder,比如 AMS WMS PMS SurfaceFlinger Audiofilinger 以及硬件操作服务等等 Android 四大组件...
继续阅读 »

0. 为什么要深入学习 Binder



  • Binder 是整个 Android 的基石

    • 所有的系统服务都是基于 Binder,比如 AMS WMS PMS SurfaceFlinger Audiofilinger 以及硬件操作服务等等

    • Android 四大组件的底层实现离不开 Binder



  • 做系统开发需要自定义一些系统服务,这些工作需要我们了解 Binder

  • Android O 以后的 Treble 计划,基于 Binder 魔改出了 HwBinder VndBinder。

  • ANR 冻屏 卡顿 卡死等偶现 BUG 可能与 Binder 相关


1. 学习 Binder 的四个阶段



  • 会用,能添加 Java Native 系统服务

  • 熟读应用层各种情景下的源码

  • 熟读内核里面的数据结构和流程

  • 能解决各种奇奇怪怪的 bug


2. 准备工作


下载编译好 AOSP + Kernel,能通过自定义内核的方式启动虚拟机。


这部分内容比较简单,可以参考:



3. 预备基础知识


预备基础知识快速过一遍,忘了再回头再看



4. Binder 基本原理


首先要明确一点 Binder 是一个 RPC(Remote Procedure Call) 框架,也就是说借助于 Binder,我们可以在 A 进程中访问 B 进程中的函数。


4.1 IPC 原理


RPC 一般基于 IPC 来实现的,IPC 就是跨进程数据传输,大白话就是在 A 进程可以访问到 B 进程中的数据,或者说 B 进程中的数据可以传递给 A 进程,都是一个意思。


在 Linux 中,每个进程都有自己的虚拟内存地址空间。虚拟内存地址空间又分为了用户地址空间和内核地址空间。



不同进程之间用户地址空间的变量和函数是不能相互访问的。


使得 A 进程能访问到 B 进程中数据的手段我们就称之为 IPC。


虽然用户地址空间是不能互相访问的,但是不同进程的内核地址空间是相同和共享的,我们可以借助内核地址空间作为中转站来实现进程间数据的传输。


具体的我们在 B 进程使用 copy_from_user 将用户态数据 int a 拷贝到内核态,这样就可以在 A 进程的内核态中访问到 int a



更进一步,可以在 A 进程中调用 copy_to_user 可以将 int a 从内核地址空间拷贝到用户地址空间。至此,我们的进程 A 用户态程序就可以访问到进程 B 中的用户地址空间数据 int a



为了访问 int a ,需要拷贝两次数据。能不能优化一下?我们可以通过 mmap 将进程 A 的用户地址空间与内核地址空间进行映射,让他们指向相同的物理地址空间:



完成映射后,B 进程只需调用一次 copy_from_user,A 进程的用户空间中就可以访问到 int a了。这里就优化到了一次拷贝。


4.2 RPC 原理


接着我们来看以下,Binder 的 RPC 是如何实现的:


一般来说,A 进程访问 B 进程函数,我们需要:



  • 在 A 进程中按照固定的规则打包数据,这些数据包含了:

    • 数据发给那个进程,Binder 中是一个整型变量 Handle

    • 要调用目标进程中的那个函数,Binder 中用一个整型变量 Code 表示

    • 目标函数的参数

    • 要执行具体什么操作,也就是 Binder 协议



  • 进程 B 收到数据,按照固定的格式解析出数据,调用函数,并使用相同的格式将函数的返回值传递给进程 A。



Binder 要实现的效果就是,整体上看过去,进程 A 执行进程 B 中的函数就和执行当前进程中的函数是一样的。


5. Binder 应用层工作流程


Binder 是一个 RPC(Remote Procedure Call) 框架,翻译成中文就是远程过程调用。也就是说通过 Binder:



  • 可以在 A 进程中访问 B 进程中定义的函数

  • 进程 B 中的这些等待着被远程调用的函数的集合,我们称其为 Binder 服务(Binder Service)

  • 进程 A 称之为 Binder 客户端(Binder Client),进程 B 称之为 Binder 服务端(Binder Server)

  • 通常,系统中的服务很多,我们需要一个管家来管理它们,服务管家(ServiceManager) 是 Android 系统启动时,启动的一个用于管理 Binder 服务(Binder Service) 的进程。通常,服务(Service) 需要事先注册到服务管家(ServiceManager),其他进程向服务管家(ServiceManager) 查询服务后才能使用服务。

  • Binder 的 RPC 能力通过 Binder 驱动实现


通常一个完整的 Binder 程序涉及 4 个流程:



  1. 在 Binder Server 端定义好服务

  2. 然后向 ServiceManager 注册服务

  3. 在 Binder Client 中向 ServiceManager 获取到服务

  4. 发起远程调用,调用 Binder Server 中定义好的服务


整个流程都是建立在 Binder 驱动提供的跨进程调用能力之上:



6. Android Binder 整体架构


从源码实现角度来说,Binder 整体架构实现如下:



有点复杂,我们一点点说:




  • VFS 是内核中的一个中间层,向上对应用层提供统一的系统调用函数,这些系统调用函数主要是 open mmap ioctl write read ioctl 等,向下封装不同的外设(字符设备,块设备),系统文件,文件系统的操作。Binder 是一个字符驱动,当应用层调用到 binder 的 open mmap ioctl release 系统调用时,经过 vfs 的一层包装后,就会调用到 Binder 驱动中的 binder_open bider_mmap binder_ioctl binder_release 函数。




  • 不同于一般的驱动,Binder 应用层的使用要复杂不少,如果直接使用 open mmap ioctl release 系统调用会使得应用程序非常复杂且难以复用相同功能的代码,刚开始 google 的工程师做了一套简单的封装,把常用的操作封装为一系列的函数,这些函数都在 binder.c 中,ServiceManger 的就是通过 binder.c 中封装的函数实现的(Android10及以前)。源码中还存在一个 bctest.c 的程序,这个是 binder.c 的一个测试程序。C 语言级别的封装虽然简单,但使用起来还是稍显麻烦,很多细节也没有考虑进去,所以 google 的工程师又封装了一个叫 libbinder 的库,我们 native 层的 binder 服务端与客户端都是基于这个库来实现的,Java 层的 binder 服务端与客户端都是通过 JNI 间接使用 libbinder 库实现的,从使用上来说 libbinder 更为简单,但是 libbinder 本身比 binder.c 复杂了不少。




7. C 层实现分析


很多博客教程会忽略这一层的分析,相比 libbinder 库的封装,binder.c 会简单不少 ,方便初学者理解 binder 应用层工作流程。


我们可以模仿 bctest.c 写一个完整的 Binder 应用层 demo。


这个工作已经有大佬完成了:


github.com/weidongshan…


但是也有一些问题,这个代码是基于 Android5 的,稍微有点老了,我在以上实现的基础上做了一些修改和适配工作,使得代码可以在 Android10 上跑起来:


github.com/yuandaimaah…


关于这个示例程序的分析,可以参考以下几篇文章:



8. 驱动分析


驱动分析这部分结合 C 层应用的实现来分析驱动的实现,主要搞清楚:



  • 三个情景的流程:注册,获取,使用

  • 三个情景下内核中各种数据结构的变化


这部分内容可以参考之前分享的:



9. C++ 层分析


首先我们要写一个基于 libbinder 库的 Demo,能跑起来就行:



接着分析三个情景下的执行过程与各个函数的功能:



当然还有两个特殊的场景也需要进行分析:



  • 死亡通知

  • 多线程


这部分内容会陆续在公众号和掘金平台推送。


10. Java 层分析


学习这部分的前提是了解 JNI 编程。这个可以参考系列文章:



我们先写一个 Demo,能跑起来就行:



接着我们分析三个情景下的执行过程与各个函数的功能:



当然还有一些其他高级特性也需要我们分析,这部分内容会在后续推送:



  • AIDL 中 in out inout oneway 的分析

  • Parcel 数据结构分析

  • Java 层死亡通知

  • Java 层多线程分析


11. 疑难问题


不论是应用开发还是系统开发我们都会遇到一些棘手的 bug,很多时候这些 bug 都和 binder 有关,总结起来,大概可以分为几类:



  • 死锁

  • 线程池满了

  • 代理对象内存泄露

  • 传输数据过大

  • 关键方法内发起 Binder 同步调用导致卡顿

  • Android O 异步远程调用无限阻塞冻屏 bug


这类 bug 很多都难以复现,很多时候都不了了之了,导致拥有这部分经验的同学很少。


这部分内容工作量巨大,我会在接下来的时间陆续在公众号和掘金推送相关的文章。


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

我的前端开发学习之路,从懵懂到自信

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。 刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接...
继续阅读 »

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。


刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接触一些流行的框架和库时,我才发现自己真正的水平有多菜。当时,我就像一只踩在滑板上的小猪,不断地摔倒,但是我并没有放弃,我一直在努力地学习和实践。


在学习的过程中,我遇到了许多困难和挑战,但是也有很多有趣的体验和经历。有一次,我在编写一个简单的网页时,花了一整天的时间,结果发现自己的代码有一个很小的错误,导致整个网页无法正常显示。当时我感觉自己就像一个猴子在敲打键盘,非常无助和懊恼。但是,通过不断地调试和修改,我最终找到了错误,并且成功地将网页显示出来。当时,我感觉自己就像一只成功攀爬上树的小猴子,非常自豪和兴奋。


除了遇到困难和挑战,我在学习前端开发过程中也经历了许多有趣的体验。有一次,我在编写一个小型的应用程序时,发现我的代码出现了一个非常有趣的小 bug。当用户在页面上进行操作时,页面上的一些元素会突然出现在屏幕的右侧,然后又突然消失不见。当时我还担心这个 bug 会影响用户的正常使用,但是后来发现这个 bug 其实很有趣,而且还能给用户带来一些意外的乐趣。于是我就把这个 bug 留了下来,并且在用户操作时添加了一些特效,让这个小 bug 变成了一个有趣的亮点。


12.jpg
总结一波:
第一点,学习前端开发需要有耐心。前端开发不是一个短时间内可以学会的技能,它需要大量的时间和精力。尤其是在学习的早期,你可能会觉得有些技术和概念非常难以理解。但是,只要你有耐心,坚持不懈地学习,最终你一定会掌握这些技能。


第二点,建立一个良好的学习计划非常重要。前端开发有很多不同的技术和概念,你需要有一个清晰的学习计划来帮助你系统地学习和掌握这些知识。首先,你需要了解 HTML、CSS 和 JavaScript 这三大基本技术。其次,你可以学习一些流行的框架和库,如 React、Vue、jQuery 等,这些技术可以帮助你更快捷地构建网站和应用程序。


第三点,实践是学习前端开发的关键。你可以通过练习编写代码来更好地理解前端开发的技术和概念。在学习的过程中,你可以尝试编写一些小项目,比如一个简单的网页或者一个小型的应用程序。通过实践,你可以更深入地了解前端开发的各个方面,并且提高你的编程技能。


第四点,不要害怕向他人寻求帮助。前端开发是一个非常开放和社交的领域,你可以通过参加社区活动、参与在线讨论、向他人寻求帮助等方式来更好地学习和成长。有时候,你可能会遇到一些困难,或者对某些概念不是很理解,这时候向他人寻求帮助是非常重要的。你可以参加一些线上或线下的前端开发社区,与其他开发者交流经验和技巧,也可以在 GitHub 等平台上查看其他人的代码,从中学习和借鉴。


第五点,不断更新自己的知识和技能。前端开发是一个不断发展和变化的领域,新技术和新概念层出不穷。因此,你需要不断地更新自己的知识和技能,跟上前端开发的最新动态。你可以通过阅读博客、参加培训课程、观看技术视频等方式来学习新的技术和概念。


总之,学习前端开发需要有耐心、建立一个良好的学习计划、实践、寻求帮助和不断更新自己的知识和技能。这些都是非常重要的,也是我在学习前端开发过程中得到的宝贵经验。通过不断地学习和实践,相信你我可以成为一名优秀的前端开发工程师。


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

闲聊:从我那二百多人的技术群讲起

随着我不断输出原创作品,我的公众号粉丝数量也迎来了漂亮的增长曲线。 我4个月前创建的技术群,也很快的突破了微信“扫码入群”的人数上限(200人),来到了250+。 于是,事情便开始有意思了。 虽然我从建群之初就早早给群定了一个基调「学习交流群」,可是随着五...
继续阅读 »

随着我不断输出原创作品,我的公众号粉丝数量也迎来了漂亮的增长曲线。


图片


我4个月前创建的技术群,也很快的突破了微信“扫码入群”的人数上限(200人),来到了250+。


图片


于是,事情便开始有意思了。


虽然我从建群之初就早早给群定了一个基调「学习交流群」,可是随着五湖四海的人涌入群之后,群里的聊天内容渐渐有了失控的迹象,毕竟人多嘴杂。


眼看着我辛辛苦苦拉起来的小群就快废了,于是我一次次站出来对全员喊话,控制局面,一次,两次,三次……无数次。


图片


经常是,我好不容易把话题拉回正轨,没过多久就会有人带头水群,这可把我头疼坏了。


图片


我每天下班路上就在想,要怎么把这二百多人构成的群体有序组织起来,不产生「坏味道」呢?于是我开始潜水到别人的粉丝群去观摩,结果非常令人失望。


几乎所有人数超过100的粉丝群,不管是金融领域,还是技术领域,都无一例外的出现了两个现象:


要么没人说话,集体沉默。要么全员飙车,集体水群


前者全员沉默的群,群主多半是来「养鱼」的,他们长期玩消失,很少在群里说话,也自然不会组织大家进行一些探讨和交流。


后者全员飙车的群,群主也是玩消失的,但是群成员活跃,要么一起骂街骂社会骂公司,要么一起聊骚聊八卦,话题早就背离了入群的初衷。


其中有一个群,群主还算负责,但是没什么管理能力,群友天天刷屏,严重影响其它人的正常交流。于是群主不知从哪想了一个招:新建一个VIP群,这是个收费群,每个月收群员10块钱,其中受邀优质群员可以免收(我是其中之一)。


这个操作一出来,他的大群和公众号就炸锅了……一堆人骂他,什么难听的都有。


我把这一切默默的看在眼里,记在心里,每当我想要重新拉一个「核心技术交流群」的时候,我就摇摇头,不行,不能这么干,你这是在把所谓的「精英阶层」抽离出来区别对待,看似获得了一小群人的簇拥,但是你失去的是「普通的大多数」


于是,我换了个思路:堵不如疏,我新建了一个「聊骚摸鱼群」,把群二维码直接丢大群里,来吧,兄弟姐妹们,要开车要八卦的,上车!


图片


同时,所有人禁止在大群里聊骚摸鱼水群,忍不住的,到隔壁群去,主群只做技术交流。


这个操作一出,效果极好!


主群里每天稳定高频的交流着技术和职场信息。


图片


而隔壁群里也默默的开着车。


图片


各取所需,相得益彰!


一个两百多人的组织尚且如此,我们的社会何尝不是呢?


根据熵增定律能量是不会凭空消失的,只会进行转移


人的本性是动物性,而非人性。人的本能是欲望,而非克制。人天生就有物欲,喜欢吃喝,美色,钱财,权力……是因为人类群居生活中需要安全和稳定,所以产生了道德,法律,伦理等约束力,对人的动物性进行管控。


可是,一味的克制约束不发泄,人是会出事的!因为能量释放不出去是会原地「爆炸」的。

所以你以为所谓的「奶头乐」文化是怎么来的?



奶头乐理论


由于生产力的不断上升,世界上的一大部分人口将不必也无法积极参与产品和服务的生产。为了安慰这些“被遗弃”的人,避免阶级冲突,方法之一就是制造“奶头”、喂之以“奶头”——使令人陶醉的消遣娱乐和充满感官刺激的产品(比如网络、电视和游戏)填满人们的生活、转移其注意力和不满情绪,令其沉浸在“快乐”中不知不觉丧失思考能力、无心挑战现有的统治阶级。



如果不给人类群体一些「释放能量」的渠道和方式,那么这股巨大的能量迟早会产生毁灭性的打击,造成组织倾覆。


这便是堵不如疏


不过很可惜,我们身边充斥着大量无能的管理者,对待团队只会一招:忍,狠,滚


他们较少关注团队的情绪和诉求,自然也就无法获得团队成员的充分信任,毕竟这年头,人们逐渐意识到「情绪价值」对自己非常重要!


老话说得好:“吃得亏,吃不得气”,便是这个道理。


好啦,今天就闲聊到这里吧,本来我是准备再发一篇技术文的,但是下班路上突然想到这件事,越想越有意思,就分享出来给大家。


后面,我的「沐洒布吉岛」还会努力的维持着技术文化氛围,给众多支持我的粉丝一个交代。


而隔壁群嘛……


图片


还是赶紧转让群主保命吧,哈哈哈哈。


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

35岁愿你我皆向阳而生

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。 当我在20多岁研究生刚刚毕业的时候,恰逢互...
继续阅读 »

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。


当我在20多岁研究生刚刚毕业的时候,恰逢互联网蓬勃发展,到处都是机会,那时候年轻气盛,我充满了能量和热情。我渴望学习新的技术和承担具有挑战性的项目。我花费了无数的时间编码、测试和调试,常常为了追求事业目标而牺牲了个人生活。


慢慢的当我接近30岁的时候,我开始意识到我的优先事项正在转变。我在职业生涯中取得了很多成就,但我也想专注于我的个人生活和关系。我想旅行、和亲人朋友共度时光,并追求曾经被工作忽视的爱好。


现在,35岁的我发现自己处于一个独特的位置。我在自己的领域中获得了丰富的经验,受到同事和同行的尊重。然而,我也感到一种不安和渴望尝试新事物的愿望。这时候我很少在代码上花费时间,而是更多的时间花到了项目管理上,一切似乎很好,但是疫情这几年,行业了很大的影响,公司的运营也变得步履维艰,在安静的会常常想到未来的的规划。


一、焦虑情绪的来源


35岁程序员的焦虑情绪源于其所处的行业环境。技术不断发展,新的编程语言、框架、工具层出不穷,要跟上这些变化需要付出大量的时间和精力。此外,随着年龄的增长,身体和心理健康也会面临各种问题。这些因素加在一起,让35岁程序员感到无从下手,不知道该如何面对未来。


二、面对焦虑情绪的方法


1学习新技能


学习新技能是应对技术革新的必经之路。与其等待公司提供培训或者等待机会,35岁程序员应该主动寻找新技术,并投入时间和精力去学习。通过参加课程、阅读文献,甚至是找到一位 mentor,35岁程序员可以更快地适应新技术,保持竞争力。


2关注行业动态


35岁程序员要时刻关注技术行业的最新动态。阅读技术博客、参加社区活动,以及了解公司的发展方向和战略规划,这些都是成为行业领跑者所必须的。通过增强对行业趋势的了解,35岁程序员可以更好地做出决策,同时也可以通过分享经验获得他人的认可和支持。


3 与年轻人合作


与年轻的程序员合作可以带来许多好处。他们可能拥有更新的知识和技能,并且乐于探索新事物。35岁的程序员应该通过与年轻人合作,学习他们的思考方式和方法论。这样不仅可以加速学习新技能,还可以提高自己的领导能力。


每周我都会组织公司内部的技术交流活动,并积极号召大家发表文章,通过这些技术分享,我发现每个人擅长的东西不同,交流下来大家的收获都很大。


4重新审视个人价值观


在35岁之后,程序员可能会重新审视自己的职业生涯和个人发展方向。当面临焦虑情绪时,建议去回顾一下自己的愿景和目标。这有助于确定下一步的工作方向和计划。此外,35岁程序员也应该考虑个人的非技术技能,例如领导力、沟通能力和团队合作精神,这些技能对长期职业成功至关重要。


5 敞开心扉学会沟通


 程序员给大家的一个刻板印象就是不爱沟通,刻板木讷,大家都是干活的好手,但是一道人际关系处理上就显得有些不够灵活,保持竞争力的一个很关键的点也在于多沟通,多社交,让自己显得更有价值,有一句老话说的好:多一个朋友,多一条路。沟通需要技巧,交朋友更是,这也是我们需要学习的。


三、总结


35岁是程序员生涯中的一个重要节点,同时也是一个充满挑战和机会的时期。如何应对焦虑情绪,保持竞争力并保持个人发展的连续性,这需要程序员深入思考自己的职业规划和发展方向。


通过学习新技能、关注行业动态、与年轻人合作以及审视个人价值观,35岁程序员可以在未来的职业生涯中不断成长和发展。


归根到底,无论如何生活的好与坏都在于我们对待生活的态度,幸福是一种感受,相由心生,无论你处于何种生活状态,都希望大家向阳而生。


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

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思



  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。

  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!

  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏脾气!

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

北京十年,来深圳了

离开北京是计划 2013年去的北京,至今整十年,来去匆匆。 几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。 给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。 我们...
继续阅读 »

离开北京是计划


2013年去的北京,至今整十年,来去匆匆。


几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。


给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。


我们希望孩子从他有正式的社交关系开始-幼儿园阶段,尽早适应一个省市的教育理念和节奏,不要等到中小学、甚至高中阶段突然的打断孩子的节奏,插班到一个陌生的班级。他同时要面临环境和学业的压力,不是每个孩子都能很快调整过来的。


我自己小学阶段换了好几次学校,成绩的波动很明显,不希望孩子再面临同样的风险。


另一方面,基于我们年龄的考虑,也要尽快离开,岁数太大了,换城市跳槽不一定能找到合适的岗位。


19年,基于对移动端市场的悲观,我开始考虑换一个技术方向。2020年公司内转岗,开始从事图形相关技术开发,计划2023年离开北京,是考虑要留给自己3年的时间从零开始积累一个领域的技术。


来深圳市是意外


这几年一直在关注其他城市的"落户政策"、"互联网市场"、"房价"、"政府公共服务"。有几个城市,按优先级:杭州、广州、武汉、深圳。这些都是容易落户的城市,我们想尽快解决户口的困扰。


看几组数据:




2023年5月份数据


可以看到,杭州的房价排在第6位,但是收入和工作机会排进前4,所以首选杭州,性价比之王。


广州的房价和工作收入都排第5,中策。


武汉的工作机会排进前10,但是房价在10名开外,而且老家在那边,占尽地利,下策。


深圳的房价高的吓人,和这个城市提供的医疗、教育太不匹配,下下策。


最后选择深圳是形势所逼,今年行情史上最差,外面的机会很少。我和老婆都有机会内部转岗到深圳,所以很快就决定了。


初识深圳


来之前做了基本的调研,深圳本科45岁以内 + 1个月社保可以落户。我公司在南山,老婆的在福田,落户只能先落到对应的区。


我提前来深圳,一个星期租好了房子,确定了幼儿园。


老婆步行15分钟到公司,孩子步行500米到幼儿园,我步行 + 地铁1小时到公司。


福田和南山的教育资源相对充足,有些中小学名校今年都招不满,租房也能上,比龙华、宝安、龙岗等区要好很多。


听朋友说,在龙华一个很差的公立小学1000个小孩报名,只有500个学位。


有不少房东愿意把学位给租户使用,办理起来也不麻烦,到社区录入租房信息即可。和北京一样,采取学区划分政策,按积分排名录取,非常好的学校也要摇号碰运气。


租房


中介小哥陪我看了三四天房子,把这一片小区都看了个遍。考虑近地铁、近幼儿园、有电梯、装修良好等因素。


我本来想砍200房租,中介说先砍400,不行再加。结果我说少400,房东直接说好。我原地愣住了,之前排练的戏份都用不上了,或许今年行情不好,租房市场也很冷淡吧。


小区后面是小山,比较安静。


小区附近-0


小区附近-1


小区附近-2


小区附近-3


外出溜达,路过一所小学


深圳的很多小区里都有泳池
小区-泳池


夜晚的深圳,高楼林立,给人一种压迫感,和天空格格不入。明亮的霓虹灯,和北京一样忙碌。


晚上8点的深圳


晚上10点的深圳


对教育的看法



幸运的人一生都被童年治愈,不幸的人一生都在治愈童年--阿德勒



身边的朋友,有不少对孩子上什么学校有点焦虑,因为教育和高考的压力,有好友极力劝阻我来深圳。我认为在能力的范围内尽力就好,坦然面对一切。


焦虑是对自己无能为力的事情心存妄念。 如果一个人能坦然面对结果,重视当下,不虚度每一分每一秒,人生就不应该有遗憾。人生是来看风景的,结局都是一把灰,躺在盒子里,所以不要太纠结一定要结果怎么样。


学校是培养能力的地方,学历决定一个人的下限,性格和价值观决定上限,你终究成要为你想成为的人,不应该在自我介绍时除了学历能拿出手,一无是处。


不少人不能接受孩子比自己差。可是并没有什么科学依据能证明下一代的基因一定优于上一代吧,或许他们只是不能接受孩子比他们差,他们没有面子,老无所依。我天资一般,我也非常能接受孩子天资平庸,这是上天的旨意。


有些父母根本没有做好家庭教育,试图通过卷学校、一次性的努力把培养的责任寄托于学校。挣钱是成就自己,陪伴是成就孩子,成功的父母居中取舍。


陪伴是最好的家庭教育,如果因为工作而疏忽了孩子,我认为这个家庭是失败的,失败的家庭教育会导致家庭后半生矛盾重重,断送了全家人的幸福。


一个人缺少父爱,就缺少勇敢和力量,缺少母爱就缺少细腻与温和,孩子的性格很容易不健全。除非他自己很有天赋,能自己走出童年的阴影。


因为他长大面对的社会关系是复杂的,他需要在性格层面能融入不同的群体。性格不健全的孩子更容易走向偏激、自私、虚伪、或者懦弱,很多心理学家都是自我治疗的过程中,成为心理学大师。


一个人的一生中,学历不好带来的困扰是非常局部的,但是性格带来的问题将困扰其一生,包括工作、交朋结友、娶妻生子,并且还会传染给下一代。


榜样是最好的教育方法,没有人会喜欢听别人讲大道理,言传不如身教。有些人自己过的很可怜,拼命去鸡娃,那不是培养孩子,那是转移压力,过度投资,有赌棍的嫌疑。你自己过的很苦逼,你如何能说服孩子人生的意义是幸福?鸡娃的尽头是下一代鸡娃。


你只有自己充满能量,积极面对人生,你的孩子才会乐观向上;你只有自己持续的阅读、成长,你的孩子才会心悦诚服的学习;你只有自己做到追求卓越,你的孩子才会把优秀当成习惯。


不要给孩子传递一种信号,人生是苦的,要示范幸福的能力,培养孩子积极地入世观。


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

Gson与Kotlin的老生常谈的空安全问题

问题出现 偶然在一次debug中发现了一个按常理不该出现的NPE,用以下简化示例为例:Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.g...
继续阅读 »

问题出现


偶然在一次debug中发现了一个按常理不该出现的NPE,用以下简化示例为例:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.getValue()" because "<local1>" is null

对应的数据模型如下:

class Book(  
val id: Int,
val name: String?
) {
val summary by lazy { id.toString() + name }
}

发生在调用book.summary中。第一眼我是很疑惑了,怎么by lazy也能是null,因为summary本身就是一个委托属性,所以看看summary是怎么初始化的吧,反编译为java可知,在构造函数初始化,这完全没啥问题。

public final class Book {
@NotNull
private final Lazy summary$delegate;
private final int id;
@Nullable
private final String name;

@NotNull
public final String getSummary() {
Lazy var1 = this.summary$delegate;
Object var3 = null;
return (String)var1.getValue();
}

...略去其他

public Book(int id, @Nullable String name) {
this.id = id;
this.name = name;
this.summary$delegate = LazyKt.lazy((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

@NotNull
public final String invoke() {
return Book.this.getId() + Book.this.getName();
}
}));
}
}


所以唯一的可能性就是构造函数并未执行。而这块逻辑是存在json的解析的,而Gson与kotlin的空安全问题老生常谈了,便立马往这个方向排查。


追根溯源


直接找到Gson里的ReflectiveTypeAdapterFactory类,它是用于处理普通 Java 类的序列化和反序列化。作用是根据对象的类型和字段的反射信息,生成相应的 TypeAdapter 对象,以执行序列化和反序列化的操作。
然后再看到create方法,这也是TypeAdapterFactory的抽象方法

  @Override
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();

if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}

FilterResult filterResult =
ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException(
"ReflectionAccessFilter does not permit using reflection for " + raw
+ ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;

// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
// on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
return adapter;
}

ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}

最后到了ObjectConstructor<T> constructor = constructorConstructor.get(type);这一句,这很明显是一个类的构造器,继续走到里面的get方法

  public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();

// ...省略其他部分逻辑

// First consider special constructors before checking for no-args constructors
// below to avoid matching internal no-args constructors which might be added in
// future JDK versions
ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
if (specialConstructor != null) {
return specialConstructor;
}

FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
if (defaultConstructor != null) {
return defaultConstructor;
}

ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}

...

// Consider usage of Unsafe as reflection,
return newUnsafeAllocator(rawType);
}

先来看看前三个Constructor,



  • newSpecialCollectionConstructor

    • 注释说是提供给特殊的无参的集合类构造函数创建的构造器,里面的也只是判断了是否为EnumSet和EnumMap,未匹配上,跳过



  • newDefaultConstructor

    • 里面直接调用的Class.getDeclaredConstructor(),使用默认构造函数创建,很明显看最上面的结构是无法创建的,抛出NoSuchMethodException



  • newDefaultImplementationConstructor

    • 里面都是集合类的创建,如Collect和Map,也不是




最后,只能走到了newUnsafeAllocator()

  private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
if (useJdkUnsafe) {
return new ObjectConstructor<T>() {
@Override public T construct() {
try {
@SuppressWarnings("unchecked")
T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
return newInstance;
} catch (Exception e) {
throw new RuntimeException(("Unable to create instance of " + rawType + ". "
+ "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args "
+ "constructor may fix this problem."), e);
}
}
};
} else {
final String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe "
+ "is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args "
+ "constructor, or enabling usage of JDK Unsafe may fix this problem.";
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(exceptionMessage);
}
};
}
}

缘由揭晓


方法内部调用了UnsafeAllocator.INSTANCE.newInstance(rawType);
我手动尝试了一下可以创建出对应的实例,而且和通常的构造函数创建出来的实例有所区别


image.png
很明显,summary的委托属性是null的,说明该方法是不走构造函数来创建的,里面的实现是通过Unsafe类的allocateInstance来直接创建对应ClassName的实例。


解决方案


看到这便已经知道缘由了,那如何解决这个问题?


方案一


回到上面的Book反编译后的java代码,可以看到只要调用了构造函数即可,所以添加一个默认的无参构造函数便是一个可行的方案。改动如下:

class Book(
val id: Int = 0,
val name: String? = null
) {
val summary by lazy { id.toString() + name }
}

或者手动加一个无参构造函数

class Book(
val id: Int,
val name: String?
) {
constructor() : this(0, null)

val summary by lazy { id.toString() + name }
}

而且要特别注意一定要提供默认的无参构造函数,不然通过newUnsafeAllocator创建的实例就导致kotlin的空安全机制就完全失效了


方案二


用moshi吧,用一个对kotlin支持比较好的json解析库即可。


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

两个Kotlin优化小技巧,你绝对用的上

数据对象data object的支持 @Repeatable注解的优化 接下来就带大家介绍下上面三个特性。 一. 数据对象data object的支持 该特性由kotlin1.7.20插件版本提供,并处于实验阶段。 这个特性主要是和原来的object声明的...
继续阅读 »
  1. 数据对象data object的支持

  2. @Repeatable注解的优化


接下来就带大家介绍下上面三个特性。


一. 数据对象data object的支持


该特性由kotlin1.7.20插件版本提供,并处于实验阶段。



这个特性主要是和原来的object声明的单例类的toString()方法输出有关,在了解这个特性之前,我们先看下下面一个例子:

object Single1

fun main() {
println(Single1)
}

输出:



这个输出本质上就是一个类名、@、地址的拼接,有时候你想要打印输出的仅仅是类名,就得需要重写下toString()方法:

object Single1 {

override fun toString(): String {
return "Single1"
}
}

然后再看一个密封类的例子:

sealed interface Response {

data class Success(val response: String): Response

data class Fail(val error: String): Response

object Loading : Response

}

fun main() {
println(Response.Success("{code: 200}"))
println(Response.Fail("no net"))
println(Response.Loading)
}

输出:



可以看到,大家都是密封子类,但就这个Loading类的输出比较"丑陋",没有上面两个兄弟类的输出简洁清爽。


接下来我们就要介绍下主人公数据对象data object了,这个东西其实使用起来和object一模一样,核心的区别就是前者的toString() 更加简洁。


接下来从一个例子一探究竟:

data object Single2

fun main() {
println(Single2)
}

看下输出:



输出是不是比上面的object Single1更加简单明了。最重要的是在密封类中使用效果更加,我们把上面密封类Loading声明为data object

    data object Loading : Response

看下最终的输出结果:



这下子输出结果是不是清爽更多!!


讲完了应用,我们再java的角度看下其背后的实现机制,相比较于objectdata object会多了下面这三个重写方法:

public final class Single2 {

@NotNull
public String toString() {
return "Single2";
}

public int hashCode() {
return -535782198;
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (!(var1 instanceof Single2)) {
return false;
}

Single2 var2 = (Single2)var1;
}

return true;
}
}

我们需要关心的toString()方法就是直接重写返回了当前的类名。


如果想要使用这个特性,我们只需要增加如下配置即可:

compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

二. @Repeatable注解优化


该特性由kotlin1.6.0插件版本提供优化。



在了解这个特性之前,我们先回忆下@Repeatable这个注解在java中的使用:


如果一个注解在某个方法、类等等上面需要重复使用,那就需要@Repeatable帮助。



  • 首先定义需要重复使用的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Fruits.class)
public @interface Fruit {
String name();
String color();
}


  • 然后定义注解容器,用来指定可重复使用的注解类型
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Fruits {
Fruit[] value();
}

然后就可以在代码中这样使用:

@Fruits({
@Fruit(name = "apple", color = "red"),
@Fruit(name = "origin", color = "yellow"),
})
public class Detail {
}

大家有没有发现,可重复注解定义起来还是由一丢丢的麻烦,接下来轮到我们kotlin重磅出击了。先看下面一个例子:

@Repeatable 
annotation class Animal(val name: String)

在kotlin中我们只要声明一个需要重复使用的注解即可,kotlin编译器会自动帮助我们生成注解容器@Animal.Container,然后我们就能在代码中这样使用:

@Animal(name = "dog")
@Animal(name = "horse")
public class Detail {
}

是不是非常简单便捷了。


如果你偏要显示指明一个包含注解,也可以,通过以下方式即可实现:

@JvmRepeatable(Animals::class)
annotation class Animal(val name: String)

annotation class Animals(val value: Array<Animal>)

然后除了上面的使用方式,你在kotlin中还可以这样使用:

@Animals([Animal(name = "dog"), Animal(name = "dog")])
class Detail {
}

请注意:



  1. 如果非要显示声明一个注解容器,其属性的名称一定要为value

  2. 其次,注解容器和可重复性直接不能同时声明在同一个元素上;


另外,其实这个特性kotlin早就支持了,只不过kotlin1.6.0插件版本之前,kotlin这个特性只只支持RetentionPolicy.SOURCE生命周期的注解,并且还和java的可重复注解不兼容。


总结


这两个小技巧相信在大家日常开发中还是比较实用的,希望本篇能对你有所帮助。


参考文章:


Improved string representations for singletons and sealed class hierarchies with data objects


Repeatable annotations with runtime retention for 1.8 JVM target


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

你不常用的 FileReader 能干什么?

web
前言 欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群! 本文灵感源于上周小伙伴遇到一个问题: "一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!" 这会有什么问题呢? 按原逻辑就是调用该接口后,就会一股脑...
继续阅读 »

前言



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



本文灵感源于上周小伙伴遇到一个问题:


"一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!"


1C306E8E.jpg


这会有什么问题呢?


按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。


但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。


如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!


1C3802FC.gif


FileReader 实现 Blob 从 String 到 JSON


复现问题


为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!


前端


由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:


    // 发起请求
const request = () => {
axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then((res) => {

// 转换为 bloc 对象
const blob = new Blob([res.data])

// 获取导出文件名,decodeURIComponent为中文解码方法
const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

// 通过a标签进行下载
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = fileName;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
});
}

后端


这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:


    const xlsx = require("node-xlsx");

const Koa = require("koa");
const app = new Koa();

const cors = require("koa2-cors");

// 处理跨域
app.use(
cors({
origin: "*", // 允许来自指定域名请求
maxAge: 5, // 本次预检请求的有效期,单位为秒
methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
credentials: true, // 是否允许发送 Cookie
})
);

// 响应
app.use(async (ctx) => {
// 文件名字
const filename = "人员信息";

// 数据
const data = [
{ name: "赵", age: 16 },
{ name: "钱", age: 20 },
{ name: "孙", age: 17 },
{ name: "李", age: 19 },
{ name: "吴", age: 18 },
];

// 表格样式
const oprions = {
"!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
};

// JSON -> Buffer
const buffer = JSONToBuffer(data, oprions);

// 设置 content-type
ctx.set("Content-Type", "application/vnd.openxmlformats");

// 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
ctx.set(
"Content-Disposition",
"attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
);

// 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

// 将 buffer 返回给前端
ctx.body = buffer;
});

// 将数据转成 Buffer
const JSONToBuffer = (data, options = {}) => {
let xlsxObj = [
{
name: "sheet",
data: [],
},
];

data.forEach((item, idx) => {
// 处理 excel 表头
if (idx === 0) {
xlsxObj[0].data.push(Object.keys(item));
}

// 处理其他 excel 数据
xlsxObj[0].data.push(Object.values(item));
});

// 返回 buffer 对象
return xlsx.build(xlsxObj, options);
};

// 启动服务
app.listen(3000);

正常效果展示


1.gif


异常效果展示


可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。


1.gif


并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer


image.png


返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?


responseType 惹的祸


还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?


没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer



这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。



不设置 responseType 行不行?


那么既然是这个配置的问题,那么我们不设置不就好了!


确实可行,如下是未设置 responseType 获取到的结果:


image.png


但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:


image.png


FileReader 来救场


实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?


1CB04D6B.jpg


没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:


// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob, 'utf-8')

是不是很简单啊!


值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。


怎么判断当前是该下载?还是该转成 JSON?


这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。


根据 filename 判断


正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。


不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式


208EA3E8.gif


根据 Content-Type 判断


这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。


更改后的代码,如下:


axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then(({headers, data}) => {
console.log("FileReader 处理前:", data)

const IsJson = headers['content-type'].indexOf('application/json') > -1;

if(IsJson){
const reader = new FileReader()

// readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
// 若后端直接返回的就是 blob 类型,则直接使用即可
reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')

reader.onload = () => {
// 将字符内容转为 JSON 格式
console.log("FileReader 处理后:", JSON.parse(reader.result))
}
return
}

// 下载逻辑
download(data)
});

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。


image.png


FileReader 还能干什么?


以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?


不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!


FileReader 是什么?


FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。


不过还要注意如下两条规则:



  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件

  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取


总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。


四种读取方式


FileReader 可以如下四种方式读取目标文件:




  • FileReader.readAsArrayBuffer()



    • 开始读取指定的 Blob中的内容,读取完成后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象




  • FileReader.readAsBinaryString() (非标准



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含所读取文件的 原始二进制数据




  • FileReader.readAsDataURL()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容




  • FileReader.readAsText()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 字符串 以表示所读取的文件内容




如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。


FileReader 的其他应用场景


预览本地文件


通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。


如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。


这一部分比较简单,就不贴代码占篇幅了,效果如下:


1.gif


传输二进制格式数据


通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?


此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据


具体代码如下:


// 文件变化
const fileChange = (e: any) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsArrayBuffer(file)

reader.onload = () => {
upload(reader.result, 'http://xxx')
}
}

// 上传
const upload = (binary, url) => {
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.overrideMimeType("application/octet-stream");

//直接发送二进制数据
xhr.send(binary);

// 监听变化
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 响应成功
}
}
}
}

最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。


以上就是本文的全部内容了,希望本文对你有所帮助!!!


21E0754A.jpg

收起阅读 »

数组去重你想到几种办法呢?

web
前言 你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对...
继续阅读 »

前言


你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对于数组去重这道简单的面试题时,我们可以回答的方法有什么吧。


数组去重


1. 不使用数组API方法


首先我来介绍一种不是用数组身上的API的去重解法,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for(let i = 0; i < array.length; i++){
for( var j = 0; j < res.length; j++){
if(array[i] === res[j]){
break;
}
}
if(j === res.length){
res.push(array[i])
}
}
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]


既然不使用数组自带的API方法,那我们首先考虑的就是用双重for循环了,如上述代码:



  1. 我们准备了一个空的结果数组

  2. 我们对需要去重的数组进行循环

  3. 在第一层数据中再套一层循环,根据下标判断结果数组内是否有重复项。


我们调用该方法,打印结构如上述代码的注解处,成功的实现了对数组的去重。


2. 使用 indexOf


既然有不使用数组API的,那就肯定有使用数组API的,下面看我使用indexOf完成数组的去重,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for (let i = 0; i < array.length; i++) {
if (res.indexOf(array[i]) === -1) { // 返回找到的第一个值得下标
res.push(array[i])
}
}
return res
}
console.log(unique(array))// [ '1', 1, '2', 2 ]


如上述代码, 我们巧妙了使用了indexOf查找结果数组中是否已经存在,如果不存在才向结果数组中添加,实现了数组去重。


在上述代码的基础上,我们还可以转变一下,将for循环内的语句改为


if (array.indexOf((array[i])) == array.lastIndexOf(array[i])) {
i++
} else {
array.splice(array.lastIndexOf(array[i]), 1)
}

不新增其他变量,直接通过indexOf和lastIndexOf判断该值是否在原数组内为唯一值,从而直接修改原数组,实现数组的去重。


3. 使用 sort


对于数组去重,我们除了通过下标找出是否有重复项之外,我们还可以先排序,然后在判断前后项是否相同来实现去重,代码如下:


var  array = [1, 3, 5, 4, 2, 1, 2, 4, 4, 4]
function unique(array) {
let res = []
let sortedArray = array.concat().sort() //concat() 返回新的数组
let seen;
for (let i = 0; i < sortedArray.length; i++) {
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i]
}
return res
}
console.log(unique(array)); // [ 1, 2, 3, 4, 5 ]

如上述代码, 我们先获取一个排好序的新数组,再对新数组进行循环,判断保存前一个值的seen与当前值是否相同来实现数组去重。


温馨小提示: 由于数组的排序方法不能区分数组和字符串,所以想要使用此方法必须要保证数组的值的类型相同,不然会出bug


4. 使用 filter


既然都用到了sort排序了,那我直接抬出ES6数组新增的filter过滤器API也不过分吧,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = array.filter((item, index, array) => {
return array.indexOf(item) === index
})
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

如上述代码,filter直接使用array.indexOf(item) === index作为过滤条件返回出一个新的数组,实现数组去重。


如上述代码,我们结合了 indexOf方法作为过滤条件,那我们也可以结合一下sort方法吧,直接使用一行代码就解决了数组的去重。代码如下:


function unique(array) {
return array.concat().sort().filter((item, index, array) => !index || item !== array[item - 1])
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

5. 使用Set、Map、或者对象


除了上述的通过数组API和不使用数组API的方法外,我们还能想到的就是借助对象来实现数组的去重。使用Set数据结构是我们最容易想到的办法,使用Map与对象方法的相似,都是以数组的值作为key,再将所有的可以取出来组成一个数组。 我就不给小伙伴们演示代码了,感兴趣的小伙伴可以自己动手试试。


(对于对象的key只能为字符串这个问题,我们可以换个思路,将下标存为key,值存为value,判断不同key的值相不相同来实现数组去重。我们还可以在存key时加上其类型,然后进行一次转换。)


自己封装一个去重API


在介绍上述数组去重的方法后,我们再来总结一下,将其融合成一个有复用性,而且还可以适用不同情况的API方法。


我来介绍一下如下我封装的一个数组去重的API方法,



  1. 该方法可接受三个参数,第一个参数为需要去重的数组,第二个参数为该数组是否为排好序的数组,第三个参数为一个回调函数

  2. 该回调函数也有三个参数,分别为值,下标,需要去重数组。该回调函数的作用是方便用户对数组进行一些额外的处理(例如将大写转为小写)

  3. 第二,三参数可不传递。


var array = [1, 2, '1', 'a', 'A', 2, 1]
var array2 = [1, 1, '1', 2, 2]
function uniq(array, isSorted, iteratee) {
let seen = []
let res = []
for(let i = 0; i < array.length; i++){
let computed = iteratee ? iteratee(array[i], i,array) : array[i]
if(isSorted) {
if(!i || seen !== array[i]){
res.push(array[i])
}
seen = array[i]
}else if(iteratee) {
if(seen.indexOf(computed) === -1){
seen.push(computed)
res.push(computed)
}
}
else {
if(res.indexOf(array[i]) === -1) {
res.push(array[i])
}
}
}
return res
}
let result = uniq(array, false, function(item, index, arr){
return typeof item == 'string' ? item.toLowerCase() : item
})
console.log(result); // [ 1, 2, '1', 'a' ]
console.log(uniq(array2, true)); // [ 1, 2 ]

总结


对于数组的去重,当我们能在面试中说到这个多方法的话,这道面试题也就过了,虽然这道面试不难,但如果我们想要想到这个多方法的话,还是

作者:潘小七
来源:juejin.cn/post/7248835844659970105
需要许多知识储备的。

收起阅读 »